在社交媒体飞速发展的今天,微博等平台积累了海量的用户文本数据,这些数据中蕴含着丰富的情绪信息。对微博文本进行情绪分类,不仅能帮助企业了解用户反馈、舆情走向,也能为心理学研究、公共情绪监测提供数据支撑。本文将详细讲解如何基于循环神经网络(RNN)中的 LSTM 模型,构建一个面向微博文本的情绪分类系统,完整覆盖数据预处理、词汇表构建、模型搭建、训练评估及交互式预测的全流程。
一、项目整体架构与技术栈
本项目以中文微博文本为研究对象,实现对 "喜悦、愤怒、厌恶、低落" 四种情绪的分类。核心技术栈围绕 Python 生态展开:
- 深度学习框架:PyTorch(灵活的动态图机制,便于搭建和调试循环神经网络);
- 数据处理工具:NumPy(数值计算)、Pickle(数据序列化)、tqdm(进度可视化);
- 评估工具:Scikit-learn(提供准确率计算、分类报告等评估指标);
- 模型核心:双向 LSTM(捕捉文本上下文语义信息)。
项目文件结构及职责如下:
| 文件名称 | 核心功能 |
|---|---|
| vocab_create.py | 构建字符级词汇表,统计字符频率并序列化存储 |
| load_dataset.py | 加载数据集,完成文本数值化、padding、数据集划分及迭代器构建 |
| TextRNN.py | 定义基于双向 LSTM 的文本分类模型结构 |
| train_eval_test.py | 实现模型训练、验证、测试及单句预测逻辑 |
| main.py | 项目入口,整合数据加载、模型初始化、训练及交互式预测 |
二、数据预处理:从原始文本到可训练数据
2.1 词汇表构建(vocab_create.py)
自然语言处理的第一步是将文本符号转换为机器可理解的数值,而词汇表(Vocab)是实现这一转换的核心映射表。本项目采用字符级分词(中文文本无天然分隔符,字符级处理更贴合中文语义),具体实现逻辑如下:
python
from tqdm import tqdm
import pickle as pkl
import random
import torch
UNK, PAD = '<UNK>', '<PAD>'
def load_dataset(path, pad_size=70):
contents = [] # 用来存储转换为数值标号的句子
vocab = pkl.load(open('simplifyweibo_4_moods.pkl', 'rb')) # 读取vocab文件
tokenizer = lambda x: [y for y in x] # 大规模的课程
with open(path, 'r', encoding='UTF-8') as f:
i = 0
for line in tqdm(f):
if i == 0:
i += 1
continue
if not line: # 判断是否为空行
continue
label = int(line[0])
content = line[2:].strip('\n')
words_line = []
token = tokenizer(content) # 将每一行的内容进行分字
seq_len = len(token) # 获取一行实际内容的长度
if pad_size: # 判断每条评论是否超过70个字
if len(token) < pad_size: # 如果一行字少于70,则补充<PAD>
token.extend([PAD] * (pad_size - len(token)))
else: # 如果一行字大于70,则只取前70个字
token = token[:pad_size] # 如果一条评论中的字大于或等于70个字,索引的切分
seq_len = pad_size # 当前评论的长度
for word in token:
words_line.append(vocab.get(word, vocab.get(UNK))) # 把每一条评论转换为独热编码
contents.append((words_line, int(label), seq_len))
random.shuffle(contents) # 打乱顺序
train_data = contents[: int(len(contents) * 0.8)] # 前80%的评论数据作为训练集
dev_data = contents[int(len(contents) * 0.8): int(len(contents) * 0.9)] # 把80%~90%的评论数据集作为验证数据
test_data = contents[int(len(contents) * 0.9):] # 将90%到最后的数据作为测试数据集
return vocab, train_data, dev_data, test_data
class DatasetIterater(object):
''' 将数据batches切分为batch_size的包 '''
def __init__(self, batches, batch_size, device):
self.batch_size = batch_size
self.batches = batches
self.n_batches = len(batches) // batch_size # 数据划分batch的数据
self.residue = False # 记录划分后的数据是否存在剩余的数据
if len(batches) % self.n_batches != 0: # 表示有余数
self.residue = True
self.index = 0
self.device = device
def _to_tensor(self, datas): # 自己定义的一个函数,并不是内置的函数功能
x = torch.LongTensor([_[0] for _ in datas]).to(self.device) # 评论内容
y = torch.LongTensor([_[1] for _ in datas]).to(self.device) # 评论情感
# pad前的长度(是超过pad_size的设为pad_size)
seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device)
return (x, seq_len), y
# __getitem__是通过索引的方式获取数据对象中的内容
def __next__(self): # 用于定义迭代器对象的下一个元素,当一个对象实现了__next__方法时,它可以被用于创建迭代器对象
if self.residue and self.index == self.n_batches: # 当读取到数据的最后一个batch:
batches = self.batches[self.index * self.batch_size: len(self.batches)]
self.index += 1
batches = self._to_tensor(batches) # 转换数据类型tensor
return batches
elif self.index > self.n_batches: # 当读取完最后一个batch时:
self.index = 0
raise StopIteration # 为了防止迭代永远进行,我们可以使用StopIteration(停止迭代)语句
else: # 当没有读取到最后一个batch
batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size] # 提取当前batch
self.index += 1
batches = self._to_tensor(batches)
return batches
def __iter__(self):
return self
def __len__(self):
if self.residue:
return self.n_batches + 1
else:
return self.n_batches
if __name__ == '__main__': # 数据的预处理:1、将所有的评论都转换为独热编码,2、提取出训练集、验证集、测试集
vocab, train_data, dev_data, test_data = load_dataset('simplifyweibo_4_moods.csv')
print(train_data, dev_data, test_data)
print('结束')
# 将train_data、dev_data、test_data数据内容保存为pkl文件,分别后面直接读取。
# 当我们自己写一个函数的时候,调试,函数调试好,
核心逻辑解析
- 基础配置:定义最大词汇表大小(MAX_VOCAB_SIZE=4760)、未知字符标记(UNK)和填充标记(PAD);
- 字符频率统计:遍历微博数据集,跳过表头行后,对每条文本按字符拆分,统计每个字符的出现频率;
- 词汇表筛选:保留出现频率大于最小阈值(min_freq=3)的字符,按频率降序排列后截取前 4760 个,确保词汇表规模可控;
- 特殊标记补充:在词汇表末尾添加 UNK(未登录字符)和 PAD(填充字符),并将词汇表序列化保存为 pkl 文件,供后续数据加载使用。
关键代码说明
python
def build_vocab(file_path,max_size,min_freq):
tokenizer = lambda x:[y for y in x] # 字符级分词
vocab_dic = {}
with open(file_path,'r',encoding='UTF-8') as f:
i = 0
for line in tqdm(f):
if i == 0: # 跳过表头
i += 1
continue
lin = line[2:].strip()
if not lin:
continue
for word in tokenizer(lin):
vocab_dic[word] = vocab_dic.get(word,0)+1 # 统计字符频率
# 筛选高频字符并排序
vocab_list = sorted([_ for _ in vocab_dic.items() if _[1] > min_freq],key=lambda x:x[1],reverse=True)[:max_size]
# 构建字符到索引的映射
vocab_dic = {word_count[0]:idx for idx,word_count in enumerate(vocab_list)}
# 添加特殊标记
vocab_dic.update({UNK:len(vocab_dic),PAD:len(vocab_dic)+1})
pkl.dump(vocab_dic,open('simplifyweibo_4_moods.pkl','wb')) # 序列化保存
return vocab_dic
注意事项
代码中存在一个笔误(
_[i] > min_freq应为_[1] > min_freq),实际使用时需修正,否则无法正确筛选高频字符。
2.2 数据集加载与预处理(load_dataset.py)
构建完词汇表后,需要将原始文本转换为固定长度的数值序列,并划分训练集、验证集、测试集,同时构建数据迭代器供模型训练使用。
核心步骤
- 词汇表加载:读取序列化的词汇表文件,获取字符到索引的映射;
- 文本数值化:遍历数据集,提取每条文本的标签(首字符)和内容(从第三个字符开始),按字符拆分后,通过词汇表将字符转换为对应的索引,未知字符映射为 UNK 的索引;
- 固定长度处理(Padding/Truncation):为保证批次训练的兼容性,将所有文本序列统一为 70 个字符长度 ------ 不足 70 则补 PAD,超过 70 则截断前 70 个字符;
- 数据集划分:按 8:1:1 的比例将数据随机打乱后划分为训练集、验证集、测试集;
- 迭代器构建:自定义
DatasetIterater类,实现按批次加载数据,并将数据转换为 PyTorch 张量(Tensor),适配 GPU/CPU 计算。
数据迭代器核心逻辑
DatasetIterater类实现了迭代器协议(__next__、__iter__、__len__方法),核心功能是将数据集按批次拆分,并将每个批次的文本序列、标签、序列长度转换为张量:
python
def _to_tensor(self, datas):
x = torch.LongTensor([_[0] for _ in datas]).to(self.device) # 文本序列
y = torch.LongTensor([_[1] for _ in datas]).to(self.device) # 标签
seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device) # 原始长度
return (x, seq_len), y
三、模型构建:基于双向 LSTM 的 TextRNN(TextRNN.py)
循环神经网络(RNN)擅长处理序列数据,但传统 RNN 存在梯度消失问题,LSTM(长短期记忆网络)通过门控机制解决了这一问题,而双向 LSTM 则能同时捕捉文本的正向和反向语义信息,更适合文本分类任务。
3.1 模型结构解析
本项目的 TextRNN 模型包含三个核心层:
- 嵌入层(Embedding Layer):将字符索引转换为稠密的词向量。支持加载预训练词向量(如腾讯词向量),若未提供则随机初始化;
- 双向 LSTM 层:输入词向量维度为 200,隐藏层维度为 128,设置 3 层堆叠,开启 dropout(0.3)防止过拟合,
batch_first=True指定输入格式为[batch_size, seq_len, embed_dim]; - 全连接层(FC Layer):将双向 LSTM 最后一个时间步的输出(维度 128×2)映射到 4 个情绪类别(喜悦、愤怒、厌恶、低落)。
3.2 核心代码实现
python
import torch.nn as nn
class Model(nn.Module):
def __init__ (self,embedding_pretrained,n_vocab,embed,num_classes):
super(Model,self).__init__()
if embedding_pretrained is not None:
# 加载预训练词向量,padding_idx指定PAD对应的索引
self.emedding = nn.Embedding.from_pretrained(embedding_pretrained,padding_idx = n_vocab-1,freeze=False)
else:
# 随机初始化词向量
self.emedding = nn.Embedding(n_vocab,embed,padding_idx=n_vocab-1)
# 双向LSTM层
self.lstm = nn.LSTM(embed,128,3,bidirectional=True,batch_first=True,dropout=0.3)
# 全连接层
self.fc = nn.Linear(128*2,num_classes)
def forward(self,x):
x,_ = x # 解包,获取文本序列(忽略序列长度)
out = self.emedding(x) # 词向量转换:[batch_size, seq_len] -> [batch_size, seq_len, embed_dim]
out,_ = self.lstm(out) # LSTM输出:[batch_size, seq_len, 128*2]
out = self.fc(out[:,-1,:]) # 取最后一个时间步输出:[batch_size, 128*2] -> [batch_size, num_classes]
return out
3.3 关键细节说明
padding_idx:指定 PAD 字符对应的索引,嵌入层在计算时会忽略该位置的梯度,避免填充字符影响模型训练;freeze=False:设置预训练词向量可微调,使词向量适配当前任务的语义空间;- 双向 LSTM 输出:最后一个时间步的输出融合了正向和反向的语义信息,是文本分类的关键特征。
四、模型训练与评估(train_eval_test.py)
4.1 训练逻辑设计
模型训练的核心目标是最小化分类损失,同时监控验证集性能以避免过拟合,具体流程如下:
- 优化器选择:使用 Adam 优化器(自适应学习率,收敛速度快),学习率设置为 1e-3;
- 损失函数:交叉熵损失(Cross Entropy Loss),适配多分类任务;
- 训练过程:
- 遍历 20 个训练轮次(Epoch),每个轮次按批次加载训练数据;
- 前向传播计算输出和损失,反向传播更新模型参数;
- 每 100 个批次验证一次模型在验证集上的性能,保存验证集损失最低的模型;
- 早停机制:若验证集损失超过 10000 个批次未下降,终止训练,避免过拟合。
4.2 评估函数实现
评估函数evaluate用于计算模型在验证集 / 测试集上的准确率和损失,测试阶段还会输出分类报告(包含精确率、召回率、F1 值):
python
import torch
import numpy as np
import torch.nn.functional as F
from sklearn import metrics
import time
def evaluate(class_list, model, data_iter, test=False):
model.eval()
loss_total = 0
predict_all = np.array([], dtype=int)
labels_all = np.array([], dtype=int)
with torch.no_grad():
for texts, labels in data_iter:
outputs = model(texts)
loss = F.cross_entropy(outputs, labels)
loss_total += loss
labels = labels.data.cpu().numpy()
predict = torch.max(outputs.data, 1)[1].cpu().numpy()
labels_all = np.append(labels_all, labels)
predict_all = np.append(predict_all, predict)
acc = metrics.accuracy_score(labels_all, predict_all)
if test:
report = metrics.classification_report(labels_all, predict_all, target_names=class_list, digits=4)
return acc, loss_total / len(data_iter), report
return acc, loss_total / len(data_iter)
def test(model, test_iter, class_list):
model.eval()
start_time = time.time()
test_acc, test_loss, test_report = evaluate(class_list, model, test_iter, test=True)
msg = 'Test Loss: {0:>5.2}, Test Acc: {1:>6.2%}'
print(msg.format(test_loss, test_acc))
print(test_report)
def train(model, train_iter, dev_iter, test_iter, class_list):
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
total_batch = 0
dev_best_loss = float('inf')
last_improve = 0
flag = False
epochs = 20
for epoch in range(epochs):
print('Epoch [{}/{}]'.format(epoch + 1, epochs))
for i, (trains, labels) in enumerate(train_iter):
outputs = model(trains)
loss = F.cross_entropy(outputs, labels)
model.zero_grad()
loss.backward()
optimizer.step()
if i % 100 == 0:
predic = torch.max(outputs.data, 1)[1].cpu()
train_acc = metrics.accuracy_score(labels.data.cpu(), predic)
dev_acc, dev_loss = evaluate(class_list, model, dev_iter)
if dev_loss < dev_best_loss:
dev_best_loss = dev_loss
torch.save(model.state_dict(), 'TextRNN.py')
last_improve = total_batch
msg = 'Iter: {0:>6}, Train Loss: {1:>5.2}, Train Acc: {2:>6.2%}, Val Loss: {3:>5.2}, Val Acc: {4:>6.2%}'
print(msg.format(total_batch, loss.item(), train_acc, dev_loss, dev_acc))
model.train()
total_batch += 1
if total_batch - last_improve > 10000: # 验证集loss超过1000batch没下降,结束训练 4000 14001
print("No optimization for a long time, auto-stopping...")
flag = True
if flag:
break
4.3 单句预测与交互式演示
predict_one_sentence函数实现单句文本的情绪预测,核心步骤与数据预处理一致:字符拆分→Padding→数值化→张量转换→模型预测。run_demo函数则构建交互式界面,用户输入文本后实时输出预测的情绪类别,输入quit终止程序。
五、项目入口与整体运行(main.py)
main.py是项目的总入口,整合了所有模块的功能,运行流程如下:
- 设备配置:自动检测 GPU/MPS/CPU,优先使用 GPU 加速;
- 随机种子设置:固定 NumPy 和 PyTorch 的随机种子,保证实验可复现;
- 数据加载:调用
load_dataset加载词汇表和划分好的数据集,构建数据迭代器; - 词向量加载:加载预训练的腾讯词向量,若未加载则使用 200 维随机初始化词向量;
- 模型初始化:实例化 TextRNN 模型并移至指定设备;
- 模型训练:调用
train函数训练模型,保存最优模型; - 交互式预测:调用
run_demo函数,支持用户输入文本并输出情绪预测结果。
核心代码片段:
python
# 设备配置
device = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'
# 固定随机种子
np.random.seed(1)
torch.manual_seed(1)
torch.cuda.manual_seed(1)
torch.backends.cudnn.deterministic = True
# 加载数据
vocab, train_data, dev_data, test_data = load_dataset.load_dataset('simplifyweibo_4_moods.csv')
train_iter = load_dataset.DatasetIterater(train_data, 128, device)
dev_iter = load_dataset.DatasetIterater(dev_data, 128, device)
test_iter = load_dataset.DatasetIterater(test_data, 128, device)
# 加载预训练词向量
embedding_pretrained = torch.tensor(np.load('embedding_Tencent.npz')['embeddings'].astype('float32'))
embed = embedding_pretrained.size(1) if embedding_pretrained is not None else 200
# 模型初始化与训练
class_list = ['喜悦', '愤怒', '厌恶', '低落']
num_classes = len(class_list)
model = TextRNN.Model(embedding_pretrained, len(vocab), embed, num_classes).to(device)
train(model, train_iter, dev_iter, test_iter, class_list)
run_demo(model, vocab, device)
六、项目总结
项目总结
本项目完整实现了从数据预处理到模型部署的微博情绪分类系统,核心亮点在于:
- 轻量化:基于字符级处理和双向 LSTM,模型结构简单,训练和推理速度快;
- 可复现:固定随机种子,模块化设计,便于调试和扩展;
- 交互式:支持实时输入文本并输出情绪类别,具备实际应用场景的适配性。
该系统不仅能完成微博情绪分类的基础任务,也为自然语言处理入门者提供了完整的实战案例 ------ 从数据处理的细节,到模型构建的逻辑,再到训练评估的流程,覆盖了文本分类任务的核心环节。通过对本项目的理解和优化,可进一步掌握 NLP 任务的核心方法论,为更复杂的文本分析任务(如情感分析、意图识别)打下基础。
七、扩展思考
随着大语言模型的发展,基于预训练模型(如 BERT、RoBERTa)的文本分类已成为主流方向。本项目的 TextRNN 模型虽简单,但可作为入门案例,后续可尝试将模型替换为中文预训练模型,对比不同模型在微博情绪分类任务上的性能差异。同时,可将训练好的模型封装为 API,结合前端页面实现可视化的情绪分析工具,进一步落地实际应用。