Sentiment Analysis: Анализ тональности текста с LLM

nlpsentiment-analysisllmopensourcetext-classification
← Back to Blog

Введение

Sentiment Analysis (анализ тональности) — это задача определения эмоциональной окраски текста. LLM значительно упростили эту задачу, позволив перейти от простой классификации к детектированию сложных эмоций.


Типы анализа тональности

1. Полярность (Polarity)

Positive (Позитивный):
  "Этот телефон просто отличный! Батарея держится весь день."
  
Negative (Негативный):
  "Самый плохий сервис, который я видел. Никому не рекомендую."
  
Neutral (Нейтральный):
  "Телефон был доставлен в понедельник."

2. Эмоции (Emotion Detection)

Joy (Радость):
  "Я так счастлив! Мы выиграли матч!"
  
Anger (Гнев):
  "Это просто возмущение! Где мой заказ?"
  
Sadness (Грусть):
  "Мне так грустно. Мой питомец заболел."
  
Fear (Страх):
  "Я беспокоюсь о будущем. Кризис усиливается."
  
Surprise (Удивление):
  "Не могу поверить! Это лучший подарок в моей жизни!"
  
Disgust (Отвращение):
  "Меня тошнит от этого безобразия."

3. Детекция субъективности

Objective (Объективный):
  "Компания продала 1 млн устройств в Q3."
  
Subjective (Субъективный):
  "Я думаю, это лучший смартфон на рынке."

Классические подходы

TF-IDF + Naive Bayes

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline

# Обучение
model = Pipeline([
    ('tfidf', TfidfVectorizer(max_features=10000)),
    ('clf', MultinomialNB())
])

model.fit(train_texts, train_labels)

# Предсказание
prediction = model.predict(["Отличный продукт, очень доволен!"])
# → "positive"

LSTM для классификации

import torch
import torch.nn as nn

class SentimentLSTM(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTM(
            embed_dim, hidden_dim,
            num_layers=2,
            bidirectional=True,
            batch_first=True
        )
        self.fc = nn.Linear(hidden_dim * 2, num_classes)
        self.dropout = nn.Dropout(0.5)
    
    def forward(self, x):
        embedded = self.dropout(self.embedding(x))
        lstm_out, (hidden, cell) = self.lstm(embedded)
        
        # Объединяем forward и backward hidden states
        hidden = torch.cat((hidden[-2], hidden[-1]), dim=1)
        return self.fc(self.dropout(hidden))

# Использование
model = SentimentLSTM(vocab_size=30000, embed_dim=128, 
                      hidden_dim=256, num_classes=3)

BERT для sentiment

from transformers import AutoModelForSequenceClassification, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-multilingual")
model = AutoModelForSequenceClassification.from_pretrained(
    "sentiment-model",
    num_labels=3  # positive, negative, neutral
)

def analyze_sentiment(text):
    inputs = tokenizer(
        text, return_tensors="pt", 
        truncation=True, max_length=512
    )
    outputs = model(**inputs)
    probabilities = torch.softmax(outputs.logits, dim=-1)
    
    labels = ["negative", "neutral", "positive"]
    sentiment = labels[torch.argmax(probabilities)]
    confidence = torch.max(probabilities).item()
    
    return {"sentiment": sentiment, "confidence": confidence}

Sentiment Analysis с LLM

Zero-shot sentiment

from openai import OpenAI

client = OpenAI()

def zero_shot_sentiment(text):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """Определи тональность текста.
Ответь одним словом: positive, negative или neutral."""
            },
            {"role": "user", "content": text}
        ]
    )
    return response.choices[0].message.content

# Использование
print(zero_shot_sentiment("Отличный телефон, батарея держит весь день!"))
# → "positive"

print(zero_shot_sentiment("Плохое качество, разбился при падении."))
# → "negative"

Детекция эмоций

def detect_emotions(text):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """Определи все эмоции в тексте.
Типы: joy, anger, sadness, fear, surprise, disgust

Ответь в формате JSON:
{
  "emotions": [
    {"emotion": "joy", "confidence": 0.9},
    {"emotion": "surprise", "confidence": 0.6}
  ],
  "dominant": "joy"
}"""
            },
            {"role": "user", "content": text}
        ],
        response_format={"type": "json_object"}
    )
    return response.choices[0].message.content

# Результат:
# {
#   "emotions": [
#     {"emotion": "joy", "confidence": 0.95},
#     {"emotion": "surprise", "confidence": 0.7}
#   ],
#   "dominant": "joy"
# }

Structured output для sentiment

def structured_sentiment(text):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": f"Анализируй: {text}"}],
        response_format={
            "type": "json_object",
            "schema": {
                "sentiment": "positive | negative | neutral",
                "confidence": 0.0,
                "emotions": {"joy": 0.1, "anger": 0.0, ...},
                "key_phrases": ["отличный", "доволен"],
                "reasoning": "Текст содержит позитивные слова..."
            }
        }
    )

Sentiment Analysis с локальными моделями

llama.cpp для sentiment

import requests

def local_sentiment(text):
    prompt = f"""Определи тональность текста.

Текст: "{text}"

Ответь одним словом: positive, negative или neutral.

Текст: "Отличный продукт!"
Ответ:"""

    response = requests.post(
        "http://localhost:8080/completion",
        json={
            "prompt": prompt,
            "max_tokens": 20,
            "temperature": 0.1
        }
    )
    return response.json()["response"].strip()

Fine-tuned модели для sentiment

from transformers import AutoModelForSequenceClassification, AutoTokenizer

# Multilingual BERT для sentiment
model_name = "cardiffnlp/twitter-xlm-roberta-base-sentiment"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

def analyze_multilingual_sentiment(text):
    inputs = tokenizer(
        text, return_tensors="pt",
        truncation=True, max_length=512
    )
    
    with torch.no_grad():
        outputs = model(**inputs)
    
    probabilities = torch.softmax(outputs.logits, dim=-1)[0]
    
    labels = ["negative", "neutral", "positive"]
    results = {
        label: prob.item() for label, prob in zip(labels, probabilities)
    }
    
    return {
        "sentiment": max(results, key=results.get),
        "confidence": max(results.values()),
        "all_scores": results
    }

DistilBERT для быстрого sentiment

from transformers import pipeline

# Готовый пайплайн
sentiment_pipeline = pipeline(
    "sentiment-analysis",
    model="distilbert-base-uncased-finetuned-sst-2-english"
)

result = sentiment_pipeline("I love this product! It works great.")
# → [{"label": "POSITIVE", "score": 0.9998}]

# Multilingual
multilingual_pipeline = pipeline(
    "sentiment-analysis",
    model="papluca/xlm-roberta-base-multilingual-sentiment"
)

result = multilingual_pipeline("Отличный телефон!")
# → [{"label": "POSITIVE", "score": 0.98}]

Batch sentiment analysis

Обработка большого объёма

import pandas as pd
from tqdm import tqdm

def batch_sentiment_analysis(texts, batch_size=32):
    results = []
    
    for i in tqdm(range(0, len(texts), batch_size)):
        batch = texts[i:i+batch_size]
        
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "Определи тональность каждого текста."},
                {"role": "user", "content": "\n".join([f"{i}: {t}" for i, t in enumerate(batch)])}
            ],
            response_format={"type": "json_object"}
        )
        
        results.extend(parse_batch_response(response))
    
    return results

Анализ отзывов

def analyze_product_reviews(reviews):
    """Анализ отзывов о продукте"""
    results = []
    
    for review in reviews:
        sentiment = zero_shot_sentiment(review["text"])
        aspects = extract_aspects(review["text"])
        
        results.append({
            "review_id": review["id"],
            "sentiment": sentiment,
            "aspects": aspects,
            "date": review["date"]
        })
    
    # Агрегация
    positive_count = sum(1 for r in results if r["sentiment"] == "positive")
    
    return {
        "total": len(results),
        "positive": positive_count,
        "positive_ratio": positive_count / len(results),
        "aspect_sentiment": aspects_analysis(results)
    }

Aspect-based sentiment analysis

ABSA

Текст: "Батарея отличная, но экран тусклый."

Обычный sentiment: negative (из-за "но")

ABSA:
  Battery: positive
  Screen: negative
def aspect_based_sentiment(text):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """Извлеки аспекты и определи sentiment для каждого.

Ответь в формате JSON:
{
  "aspects": [
    {"aspect": "батарея", "sentiment": "positive", "text": "Батарея отличная"},
    {"aspect": "экран", "sentiment": "negative", "text": "экран тусклый"}
  ]
}"""
            },
            {"role": "user", "content": text}
        ],
        response_format={"type": "json_object"}
    )
    return response.choices[0].message.content

Sentiment tracking over time

Мониторинг тональности

import matplotlib.pyplot as plt
from datetime import datetime

def plot_sentiment_timeline(daily_sentiments):
    dates = [s["date"] for s in daily_sentiments]
    positive = [s["positive_ratio"] for s in daily_sentiments]
    negative = [s["negative_ratio"] for s in daily_sentiments]
    
    plt.figure(figsize=(12, 6))
    plt.plot(dates, positive, label="Positive")
    plt.plot(dates, negative, label="Negative")
    plt.fill_between(dates, positive, alpha=0.3)
    plt.fill_between(dates, negative, alpha=0.3)
    plt.legend()
    plt.title("Sentiment Timeline")
    plt.show()

Практические применения

1. Мониторинг соцсетей

# Отслеживание тональности упоминаний бренда
mentions = get_twitter_mentions("MyBrand")

sentiments = [analyze_sentiment(m["text"]) for m in mentions]

alert_if_negative_ratio(sentiments, threshold=0.3)

2. Анализ поддержки

# Анализ качества поддержки по тикетам
tickets = load_support_tickets()

for ticket in tickets:
    sentiment = analyze_sentiment(ticket["customer_message"])
    if sentiment["sentiment"] == "negative" and sentiment["confidence"] > 0.8:
        escalate(ticket)

3. Финанsentiment

# Анализ тональности новостей для трейдинга
news = load_financial_news()

sentiments = [analyze_sentiment(n["content"]) for n in news]

bullish_ratio = sum(1 for s in sentiments if s["sentiment"] == "positive")
bearish_ratio = sum(1 for s in sentiments if s["sentiment"] == "negative")

if bullish_ratio / len(sentiments) > 0.7:
    signal = "BUY"

Open Source инструменты

Модели:
  ✅ BERT (Google)
  ✅ RoBERTa (Facebook)
  ✅ XLM-RoBERTa (multilingual)
  ✅ DistilBERT (быстрый BERT)

Библиотеки:
  ✅ VADER (простой, без обучения)
  ✅ TextBlob (Python)
  ✅ NLTK Sentiment
  ✅ Transformers (HuggingFace)

Бенчмарки:
  ✅ SST (Stanford Sentiment Treebank)
  ✅ IMDB Reviews
  ✅ Twitter Sentiment140

Итоги

Sentiment Analysis с LLM стала значительно точнее и гибче. Zero-shot подход работает для большинства задач, а fine-tuning улучшает результаты для специфичных доменов.

Ключевые выводы:

  • Zero-shot sentiment с GPT-4o точен для большинства текстов
  • ABSA (aspect-based) даёт детальную картину
  • Локальные модели (BERT, DistilBERT) для приватных данных
  • VADER — быстрый baseline для английского
  • Multilingual модели для многоязычных данных