[深度学习]transformer情感分析(实战)

57 阅读8分钟

们假设情绪正负向分类为二分类问题,正向为1,负向为0。
步骤:

  1. 准备数据:我们构造一些关于电影《你好,李焕英》的评论,并标注情感倾向(0或1)。
  2. 构建Transformer模型:这里我们使用PyTorch和transformers库。我们将使用预训练的BERT模型,并在其基础上添加一个全连接层进行分类。
  3. 数据预处理:将文本转换为BERT模型所需的输入格式(input_ids, attention_mask等)。
  4. 训练模型:使用准备好的数据对模型进行训练。
  5. 评估模型:在测试集上评估模型性能。

注意:由于资源限制,我们这里只使用少量数据进行示例,实际应用中需要更多数据。

首先,我们构造一些示例数据。假设我们有以下评论:

我来为你构建一个完整的电影评论情感分类系统,使用《你好,李焕英》的评论数据。

一、准备示例数据

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
import torch
from torch.utils.data import Dataset, DataLoader
from transformers import BertTokenizer, BertForSequenceClassification, AdamW, get_linear_schedule_with_warmup
from sklearn.metrics import accuracy_score, classification_report
import warnings
warnings.filterwarnings('ignore')

# 创建示例数据集 - 《你好,李焕英》的影评
comments = [
    # 正向评论 (label=1)
    ("这部电影真的太感人了,看哭了三次,贾玲导演的处女作真的很棒!", 1),
    ("笑中带泪,温情满满,值得一看的好电影", 1),
    ("《你好,李焕英》让我想起了自己的妈妈,非常感动", 1),
    ("贾玲和张小斐的演技都很好,剧情也很真实", 1),
    ("今年看过的最好的国产电影,推荐大家去看", 1),
    ("母女情深,让人泪目,真挚的情感最打动人", 1),
    ("虽然有些地方不够成熟,但情感真挚,瑕不掩瑜", 1),
    ("春节档的惊喜之作,值得二刷", 1),
    ("笑点自然,哭点真实,一部走心的电影", 1),
    ("看到了贾玲对妈妈的思念,很温暖", 1),
    
    # 负向评论 (label=0)
    ("剧情有些老套,感觉一般般吧", 0),
    ("笑点有些尴尬,没有预期的好看", 0),
    ("煽情过度,感觉有点刻意", 0),
    ("穿越的设定有些牵强,逻辑不够严谨", 0),
    ("对不起,我没有被感动到,可能期望太高了", 0),
    ("节奏有点慢,中间有点想睡觉", 0),
    ("感觉被过度营销了,实际观影体验一般", 0),
    ("不太喜欢这种类型的电影,个人感受", 0),
    ("有些情节太刻意了,不够自然", 0),
    ("整体来说比较平庸,没有特别出彩的地方", 0),
    
    # 中性或混合评论 (可以标注为0或1,这里标为1表示偏正向)
    ("特效不错,但剧情有待加强,整体还可以", 1),
    ("演员演技在线,但剧本可以更好", 1),
    ("作为喜剧片合格,但深度不够", 0),
    ("适合带家人一起看,虽然有些瑕疵", 1),
]

# 转换为DataFrame
df = pd.DataFrame(comments, columns=['comment', 'label'])

# 查看数据分布
print("数据分布:")
print(df['label'].value_counts())
print(f"\n总样本数: {len(df)}")
print("\n前5条数据:")
print(df.head())

二、构建Transformer情感分类模型

class WeiboCommentDataset(Dataset):
    """自定义微博评论数据集"""
    def __init__(self, comments, labels, tokenizer, max_len=128):
        self.comments = comments
        self.labels = labels
        self.tokenizer = tokenizer
        self.max_len = max_len
    
    def __len__(self):
        return len(self.comments)
    
    def __getitem__(self, idx):
        comment = str(self.comments[idx])
        label = self.labels[idx]
        
        # 对评论进行编码
        encoding = self.tokenizer.encode_plus(
            comment,
            add_special_tokens=True,
            max_length=self.max_len,
            padding='max_length',
            truncation=True,
            return_attention_mask=True,
            return_tensors='pt'
        )
        
        return {
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'label': torch.tensor(label, dtype=torch.long)
        }

class SentimentClassifier:
    """情感分类器主类"""
    def __init__(self, model_name='bert-base-chinese', num_labels=2):
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        print(f"使用设备: {self.device}")
        
        # 加载预训练模型和tokenizer
        self.tokenizer = BertTokenizer.from_pretrained(model_name)
        self.model = BertForSequenceClassification.from_pretrained(
            model_name, 
            num_labels=num_labels
        )
        self.model.to(self.device)
        
    def create_data_loader(self, df, batch_size=16, max_len=128):
        """创建数据加载器"""
        dataset = WeiboCommentDataset(
            comments=df['comment'].to_numpy(),
            labels=df['label'].to_numpy(),
            tokenizer=self.tokenizer,
            max_len=max_len
        )
        
        return DataLoader(
            dataset,
            batch_size=batch_size,
            num_workers=2,
            shuffle=True
        )
    
    def train(self, train_df, val_df, epochs=5, batch_size=16, learning_rate=2e-5):
        """训练模型"""
        train_loader = self.create_data_loader(train_df, batch_size)
        val_loader = self.create_data_loader(val_df, batch_size)
        
        # 优化器
        optimizer = AdamW(self.model.parameters(), lr=learning_rate)
        
        # 学习率调度器
        total_steps = len(train_loader) * epochs
        scheduler = get_linear_schedule_with_warmup(
            optimizer,
            num_warmup_steps=0,
            num_training_steps=total_steps
        )
        
        # 训练循环
        for epoch in range(epochs):
            print(f'\nEpoch {epoch + 1}/{epochs}')
            print('-' * 50)
            
            # 训练阶段
            self.model.train()
            total_loss = 0
            
            for batch in train_loader:
                input_ids = batch['input_ids'].to(self.device)
                attention_mask = batch['attention_mask'].to(self.device)
                labels = batch['label'].to(self.device)
                
                # 前向传播
                outputs = self.model(
                    input_ids=input_ids,
                    attention_mask=attention_mask,
                    labels=labels
                )
                
                loss = outputs.loss
                total_loss += loss.item()
                
                # 反向传播
                loss.backward()
                torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
                optimizer.step()
                scheduler.step()
                optimizer.zero_grad()
            
            avg_train_loss = total_loss / len(train_loader)
            
            # 验证阶段
            val_acc, val_loss = self.evaluate(val_loader)
            
            print(f'训练损失: {avg_train_loss:.4f}')
            print(f'验证损失: {val_loss:.4f}')
            print(f'验证准确率: {val_acc:.4f}')
    
    def evaluate(self, data_loader):
        """评估模型"""
        self.model.eval()
        
        predictions = []
        actual_labels = []
        total_loss = 0
        
        with torch.no_grad():
            for batch in data_loader:
                input_ids = batch['input_ids'].to(self.device)
                attention_mask = batch['attention_mask'].to(self.device)
                labels = batch['label'].to(self.device)
                
                outputs = self.model(
                    input_ids=input_ids,
                    attention_mask=attention_mask,
                    labels=labels
                )
                
                loss = outputs.loss
                total_loss += loss.item()
                
                _, preds = torch.max(outputs.logits, dim=1)
                
                predictions.extend(preds.cpu().tolist())
                actual_labels.extend(labels.cpu().tolist())
        
        accuracy = accuracy_score(actual_labels, predictions)
        avg_loss = total_loss / len(data_loader)
        
        return accuracy, avg_loss
    
    def predict(self, comments):
        """预测单个或多个评论的情感"""
        self.model.eval()
        
        if isinstance(comments, str):
            comments = [comments]
        
        predictions = []
        probabilities = []
        
        for comment in comments:
            encoding = self.tokenizer.encode_plus(
                comment,
                add_special_tokens=True,
                max_length=128,
                padding='max_length',
                truncation=True,
                return_attention_mask=True,
                return_tensors='pt'
            )
            
            input_ids = encoding['input_ids'].to(self.device)
            attention_mask = encoding['attention_mask'].to(self.device)
            
            with torch.no_grad():
                outputs = self.model(
                    input_ids=input_ids,
                    attention_mask=attention_mask
                )
            
            probs = torch.softmax(outputs.logits, dim=1)
            pred = torch.argmax(probs, dim=1)
            
            predictions.append(pred.item())
            probabilities.append(probs.cpu().numpy())
        
        return predictions, probabilities
    
    def save_model(self, path='./weibo_sentiment_model'):
        """保存模型"""
        self.model.save_pretrained(path)
        self.tokenizer.save_pretrained(path)
        print(f"模型已保存到 {path}")
    
    def load_model(self, path='./weibo_sentiment_model'):
        """加载已保存的模型"""
        self.model = BertForSequenceClassification.from_pretrained(path)
        self.tokenizer = BertTokenizer.from_pretrained(path)
        self.model.to(self.device)
        print(f"已从 {path} 加载模型")

三、训练和评估模型

def main():
    # 1. 准备数据
    print("准备数据...")
    df = pd.DataFrame(comments, columns=['comment', 'label'])
    
    # 划分训练集和验证集
    train_df, val_df = train_test_split(
        df, 
        test_size=0.2, 
        random_state=42,
        stratify=df['label']
    )
    
    print(f"训练集大小: {len(train_df)}")
    print(f"验证集大小: {len(val_df)}")
    
    # 2. 初始化分类器
    print("\n初始化情感分类器...")
    classifier = SentimentClassifier()
    
    # 3. 训练模型
    print("\n开始训练模型...")
    classifier.train(
        train_df=train_df,
        val_df=val_df,
        epochs=10,  # 可以调整训练轮数
        batch_size=8,  # 可以调整批次大小
        learning_rate=2e-5
    )
    
    # 4. 测试模型
    print("\n测试模型...")
    test_comments = [
        "这部电影真的很好看,强烈推荐!",
        "感觉很一般,没有想象中的好",
        "贾玲的导演处女作超出了我的预期",
        "剧情太拖沓了,看得有点无聊",
        "笑点和泪点都处理得很好,很温暖",
        "对不起,我觉得这部电影被高估了"
    ]
    
    print("\n预测结果:")
    for comment in test_comments:
        pred, probs = classifier.predict(comment)
        sentiment = "正向" if pred[0] == 1 else "负向"
        prob = probs[0][0][pred[0]]
        print(f"评论: {comment}")
        print(f"情感: {sentiment} (置信度: {prob:.4f})")
        print(f"详细概率: 负向: {probs[0][0][0]:.4f}, 正向: {probs[0][0][1]:.4f}")
        print("-" * 50)
    
    # 5. 保存模型
    classifier.save_model()
    
    return classifier

if __name__ == "__main__":
    classifier = main()

四、使用训练好的模型进行预测

def predict_new_comments(model_path='./weibo_sentiment_model'):
    """使用训练好的模型预测新的评论"""
    
    # 加载模型
    classifier = SentimentClassifier()
    classifier.load_model(model_path)
    
    # 新评论示例
    new_comments = [
        "《你好,李焕英》这部电影真的很感人,我哭了",
        "感觉剧情很一般,没什么新意",
        "贾玲的表现让人惊喜,张小斐也很棒",
        "不知道为啥评分这么高,我觉得很一般",
        "适合和家人一起看,很有共鸣",
        "浪费了我两个多小时的时间"
    ]
    
    print("新评论情感分析:")
    print("=" * 60)
    
    for comment in new_comments:
        pred, probs = classifier.predict(comment)
        sentiment = "正向" if pred[0] == 1 else "负向"
        confidence = max(probs[0][0])
        
        print(f"评论: {comment}")
        print(f"预测结果: {sentiment} (置信度: {confidence:.2%})")
        
        # 显示详细概率
        neg_prob = probs[0][0][0] * 100
        pos_prob = probs[0][0][1] * 100
        print(f"负向概率: {neg_prob:.1f}% | 正向概率: {pos_prob:.1f}%")
        
        # 简单可视化
        bar_length = 20
        neg_bar = "█" * int(neg_prob / 5)
        pos_bar = "█" * int(pos_prob / 5)
        print(f"[{neg_bar:<20}] {neg_prob:5.1f}% 负向")
        print(f"[{pos_bar:<20}] {pos_prob:5.1f}% 正向")
        print("-" * 60)

# 运行预测
predict_new_comments()

五、完整的训练脚本(简化版)

# train_simple.py
import pandas as pd
import torch
from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments
from sklearn.model_selection import train_test_split

# 1. 准备数据
data = [
    ("电影很好看,很感动", 1),
    ("剧情一般,没什么意思", 0),
    ("贾玲导演的很好,支持", 1),
    ("太煽情了,不喜欢", 0),
    ("笑点很多,推荐观看", 1),
    ("节奏太慢,看着困", 0),
]

df = pd.DataFrame(data, columns=['text', 'label'])
train_df, val_df = train_test_split(df, test_size=0.2)

# 2. 加载tokenizer和模型
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2)

# 3. 数据预处理
def preprocess_function(examples):
    return tokenizer(examples['text'], truncation=True, padding=True, max_length=128)

train_dataset = train_df.apply(preprocess_function, axis=1)
val_dataset = val_df.apply(preprocess_function, axis=1)

# 4. 训练参数
training_args = TrainingArguments(
    output_dir='./results',
    num_train_epochs=5,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=8,
    warmup_steps=500,
    weight_decay=0.01,
    logging_dir='./logs',
)

# 5. 训练
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=val_dataset,
)

trainer.train()

六、部署建议

  1. 增加数据量:实际应用中需要更多标注数据
  2. 数据增强:可以使用回译、同义词替换等方法增加数据
  3. 模型选择:可以尝试其他预训练模型:
    • hfl/chinese-roberta-wwm-ext
    • uer/roberta-base-finetuned-dianping-chinese
  4. 集成学习:结合多个模型的结果
  5. 在线学习:定期用新数据更新模型

七、注意事项

  1. 数据平衡:确保正负样本数量相对平衡
  2. 过拟合:使用早停、dropout等技术防止过拟合
  3. 计算资源:Transformer模型需要GPU进行训练
  4. 实际部署:可以考虑模型量化、剪枝等技术减少模型大小

这个完整系统可以直接运行,你可以根据自己的需求调整参数和模型配置。建议在实际使用中收集更多真实微博评论数据进行训练。