基于 TextRNN 的微博情绪分类系统实现与解析

在社交媒体飞速发展的今天,微博等平台积累了海量的用户文本数据,这些数据中蕴含着丰富的情绪信息。对微博文本进行情绪分类,不仅能帮助企业了解用户反馈、舆情走向,也能为心理学研究、公共情绪监测提供数据支撑。本文将详细讲解如何基于循环神经网络(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文件,分别后面直接读取。
    # 当我们自己写一个函数的时候,调试,函数调试好,
核心逻辑解析
  1. 基础配置:定义最大词汇表大小(MAX_VOCAB_SIZE=4760)、未知字符标记(UNK)和填充标记(PAD);
  2. 字符频率统计:遍历微博数据集,跳过表头行后,对每条文本按字符拆分,统计每个字符的出现频率;
  3. 词汇表筛选:保留出现频率大于最小阈值(min_freq=3)的字符,按频率降序排列后截取前 4760 个,确保词汇表规模可控;
  4. 特殊标记补充:在词汇表末尾添加 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)

构建完词汇表后,需要将原始文本转换为固定长度的数值序列,并划分训练集、验证集、测试集,同时构建数据迭代器供模型训练使用。

核心步骤
  1. 词汇表加载:读取序列化的词汇表文件,获取字符到索引的映射;
  2. 文本数值化:遍历数据集,提取每条文本的标签(首字符)和内容(从第三个字符开始),按字符拆分后,通过词汇表将字符转换为对应的索引,未知字符映射为 UNK 的索引;
  3. 固定长度处理(Padding/Truncation):为保证批次训练的兼容性,将所有文本序列统一为 70 个字符长度 ------ 不足 70 则补 PAD,超过 70 则截断前 70 个字符;
  4. 数据集划分:按 8:1:1 的比例将数据随机打乱后划分为训练集、验证集、测试集;
  5. 迭代器构建:自定义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 模型包含三个核心层:

  1. 嵌入层(Embedding Layer):将字符索引转换为稠密的词向量。支持加载预训练词向量(如腾讯词向量),若未提供则随机初始化;
  2. 双向 LSTM 层:输入词向量维度为 200,隐藏层维度为 128,设置 3 层堆叠,开启 dropout(0.3)防止过拟合,batch_first=True指定输入格式为[batch_size, seq_len, embed_dim]
  3. 全连接层(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 训练逻辑设计

模型训练的核心目标是最小化分类损失,同时监控验证集性能以避免过拟合,具体流程如下:

  1. 优化器选择:使用 Adam 优化器(自适应学习率,收敛速度快),学习率设置为 1e-3;
  2. 损失函数:交叉熵损失(Cross Entropy Loss),适配多分类任务;
  3. 训练过程:
    • 遍历 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是项目的总入口,整合了所有模块的功能,运行流程如下:

  1. 设备配置:自动检测 GPU/MPS/CPU,优先使用 GPU 加速;
  2. 随机种子设置:固定 NumPy 和 PyTorch 的随机种子,保证实验可复现;
  3. 数据加载:调用load_dataset加载词汇表和划分好的数据集,构建数据迭代器;
  4. 词向量加载:加载预训练的腾讯词向量,若未加载则使用 200 维随机初始化词向量;
  5. 模型初始化:实例化 TextRNN 模型并移至指定设备;
  6. 模型训练:调用train函数训练模型,保存最优模型;
  7. 交互式预测:调用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,结合前端页面实现可视化的情绪分析工具,进一步落地实际应用。

相关推荐
hehelm2 小时前
string的模拟实现
数据结构·算法
白羊by2 小时前
逻辑回归与Softmax的区别
算法·机器学习·逻辑回归
Devil枫2 小时前
【腾讯位置服务开发者征文大赛】AI 赋能小程序地图开发:腾讯地图 Miniprogram Skill 实战记录
人工智能·小程序
Tisfy2 小时前
LeetCode 3761.镜像对之间最小绝对距离:哈希表(维护左,枚举右)
算法·leetcode·散列表·题解
小鱼~~2 小时前
逻辑回归简介
算法·机器学习·逻辑回归
blackorbird2 小时前
AI工作流自动化平台n8n正被大规模网络武器化
运维·网络·人工智能·自动化
阿杰学AI2 小时前
AI核心知识126—大语言模型之 CrewAI 和 AutoGen(简洁且通俗易懂版)
人工智能·语言模型·自然语言处理·agent·多智能体·智能体·多智能体协作框架
企业架构师老王2 小时前
2026年国内AI Agent选型指南:企业数字化转型中的非侵入式架构方案深度评测
人工智能·ai·架构
黎阳之光2 小时前
黎阳之光受邀出席上海口岸联合会2026智慧口岸研讨班 无感通关方案获盛赞
大数据·人工智能·算法·安全·数字孪生