
简介这份资源是面向高校学生与课程设计者的中文短文本情感分析完整项目源码基于Python与LSTM实现可直接用于期末大作业或课程设计场景帮助解决从数据加载、文本预处理到模型训练与情感分类的完整链路问题。压缩包共14个文件约1.96MB包含3个py主程序脚本、5个txt样本与说明文本、1个csv数据集、1个pt模型权重文件及md说明文档等覆盖数据读取、正负样本、训练与测试各环节结构清晰便于二次修改。目前已有245人学习下载。项目提供可运行的训练与推理代码读者可据此理解中文分词、词向量映射、LSTM建模及情感极性判别流程并参考说明文档快速复现实验适合作为入门深度学习文本分类的实践范本。1. 中文短文本情感分析为什么 LSTM 在期末大作业里依然是性价比最高的选择电商评论、外卖评价、弹幕、工单标题这些文本有个共同点短。短到分词之后可能只剩七八个词短到 TF-IDF 加朴素贝叶斯就能跑出个能看的准确率也短到很多人以为「上深度学习是杀鸡用牛刀」。但真做过就知道短文本的坑恰恰在于信息密度低、上下文缺失、否定词和转折词一出现词袋模型立刻翻车。比如「不是不好用是根本没法用」词袋看到的是「不好用」和「没法用」两个负面片段可「不是」这个否定前缀被彻底忽略模型给出的极性判断就会飘。LSTM 在这里的价值不是「更高级」而是它按顺序读词门控结构能把「不是」这种前文状态带到后面的判断里。用 PyTorch 或 TensorFlow 搭一个基于 LSTM 的中文短文本情感分析模型代码量不大训练在普通笔记本上几十分钟能跑完准确率通常比词袋基线高 5 到 15 个百分点。这份「Python 实现基于 LSTM 的中文短文本情感分析源码」适合两类人一类是期末大作业需要完整可复现流程的学生另一类是想把情感分析接进自己业务、又不想一上来就上 BERT 的工程师。下面从数据准备一路讲到调参和避坑代码可以直接抄。2. 从原始中文到 LSTM 能吃的张量数据管道怎么搭2.1 中文短文本预处理的四个必做步骤中文和英文不一样没有天然空格分隔所以第一步永远是分词。常见做法是用 jieba加载自定义词典把业务词加进去比如「续航」「卡顿」「客服态度」这类在通用词典里可能被切碎的词。分词之后要做的是去噪去掉 URL、 用户、话题标签、纯数字和标点但注意保留否定词和程度副词它们是情感极性的关键信号。第二步是标签对齐。短文本情感分析通常是二分类正面/负面或三分类正面/中性/负面。如果数据源是星级评分常见映射是 1-2 星为负面、3 星为中性、4-5 星为正面。这里有个容易忽略的点中性样本往往最难标如果中性样本占比低于 10%建议直接做二分类否则模型会被中性类拖垮。第三步是构建词表。统计词频保留频率最高的 N 个词N 一般取 8000 到 20000。低频词统一映射到unk这样能控制嵌入矩阵大小减少过拟合。第四步是序列截断和填充。短文本长度分布通常集中在 10 到 50 个词之间取 95 分位数作为最大长度比如 64 或 128超过截断不足补零。import jieba import re from collections import Counter def clean_text(text): # 去掉 URL、、话题标签、多余空白 text re.sub(rhttp\S|www\.\S, , text) text re.sub(r\S, , text) text re.sub(r#\S#, , text) text re.sub(r\s, , text).strip() return text def tokenize(text): # 保留中文、英文、数字其余按标点切掉 text clean_text(text) words jieba.lcut(text) # 过滤纯标点和空白 words [w for w in words if re.match(r[\u4e00-\u9fa5a-zA-Z0-9], w)] return words def build_vocab(tokenized_corpus, max_size10000, min_freq2): counter Counter() for tokens in tokenized_corpus: counter.update(tokens) # 按频率排序保留高频词 vocab {pad: 0, unk: 1} for word, freq in counter.most_common(max_size): if freq min_freq: vocab[word] len(vocab) return vocab这段代码里max_size控制词表上限min_freq过滤只出现一次的词。实际调的时候如果发现unk占比超过 5%说明词表太小或者 min_freq 太高需要放宽。分词之后建议打印几条样本看看确认否定词没被切碎比如「不好用」应该切成「不好用」或「不」「好用」而不是「不」「好」「用」——后者会让 LSTM 更难学到否定组合。2.2 把词序列转成 Embedding 输入Dataset 与 DataLoader 的写法PyTorch 的标准流程是继承Dataset在__getitem__里把词转成 id 序列然后collate_fn里做 padding。这里有个细节padding 的 id 必须是 0而且计算 loss 时要忽略 padding 位置否则模型会学到「补零的位置都是中性」这种假规律。import torch from torch.utils.data import Dataset, DataLoader from torch.nn.utils.rnn import pad_sequence class SentimentDataset(Dataset): def __init__(self, texts, labels, vocab, max_len64): self.texts texts self.labels labels self.vocab vocab self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): tokens tokenize(self.texts[idx]) ids [self.vocab.get(w, self.vocab[unk]) for w in tokens] ids ids[:self.max_len] return torch.tensor(ids, dtypetorch.long), torch.tensor(self.labels[idx], dtypetorch.long) def collate_fn(batch): sequences, labels zip(*batch) # padding 到 batch 内最大长度pad id 为 0 padded pad_sequence(sequences, batch_firstTrue, padding_value0) labels torch.stack(labels) return padded, labels # 使用示例 # dataset SentimentDataset(train_texts, train_labels, vocab) # loader DataLoader(dataset, batch_size32, shuffleTrue, collate_fncollate_fn)max_len64是短文本的常用值如果数据里长文本多可以调到 128。batch_size32在 8GB 显存的机器上跑 LSTM 绰绰有余如果显存小可以降到 16。注意pad_sequence默认按 batch 内最长序列补齐这样比全局固定长度更省计算但要求模型能处理变长输入——LSTM 本身可以只要在 loss 里用ignore_index0忽略 padding。2.3 训练集、验证集、测试集的划分与类别不均衡处理短文本情感分析的数据集经常不均衡正面样本可能是负面的两三倍。直接训练会导致模型偏向多数类准确率看着高但负面样本召回率很低。常见做法有两种一是加权损失函数给少数类更高的权重二是过采样少数类但要注意过采样容易过拟合最好配合 dropout。from sklearn.model_selection import train_test_split from torch import nn # 先划分训练和临时集再从临时集里分验证和测试 train_texts, temp_texts, train_labels, temp_labels train_test_split( texts, labels, test_size0.3, random_state42, stratifylabels ) val_texts, test_texts, val_labels, test_labels train_test_split( temp_texts, temp_labels, test_size0.5, random_state42, stratifytemp_labels ) # 计算类别权重 from collections import Counter label_counts Counter(train_labels) total sum(label_counts.values()) weights [total / (len(label_counts) * label_counts[i]) for i in range(len(label_counts))] class_weights torch.tensor(weights, dtypetorch.float) criterion nn.CrossEntropyLoss(weightclass_weights, ignore_index0)stratifylabels保证划分后各类比例一致ignore_index0让 padding 位置不参与 loss。类别权重按总数 / (类别数 * 该类样本数)计算少数类权重会更大。如果数据极度不均衡比如 1:10建议先做二分类或者收集更多少数类样本不要硬调权重。3. LSTM 模型结构从 Embedding 到分类头的每一层怎么定3.1 Embedding 层随机初始化还是预训练词向量Embedding 层把词 id 映射成稠密向量。两种选择随机初始化让模型自己学或者加载预训练词向量比如用 Word2Vec 或 GloVe 在中文语料上训好的向量。短文本数据量小的时候预训练词向量通常能带来 2 到 5 个百分点的提升因为模型不用从零学词义。但预训练词向量文件往往几百 MB如果只是期末大作业随机初始化也够用训练几个 epoch 后嵌入会逐渐收敛。import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, num_layers1, num_classes2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM( input_sizeembed_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0 ) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_dim * 2, num_classes) # 双向所以乘 2 def forward(self, x): # x: (batch, seq_len) embedded self.embedding(x) # (batch, seq_len, embed_dim) lstm_out, (hidden, cell) self.lstm(embedded) # 取最后一个时间步的输出双向拼接 last_out lstm_out[:, -1, :] # (batch, hidden_dim * 2) out self.dropout(last_out) logits self.fc(out) return logitsembed_dim128是短文本的常用值词表一万左右时够用。hidden_dim128配合双向 LSTM输出维度是 256。num_layers1对短文本通常足够层数多了容易过拟合。dropout0.3是经验值如果训练集很小可以调到 0.5。padding_idx0让 embedding 层不更新 padding 对应的向量。3.2 双向 LSTM 与池化策略取最后一步还是做平均上面的代码取的是最后一个时间步的输出。但短文本里最后一个词未必是情感焦点比如「质量很好但是物流太慢了」最后一个词「了」没有情感信息。更稳的做法是对所有时间步做平均池化或最大池化把整句的信息聚合起来。def forward(self, x): embedded self.embedding(x) lstm_out, _ self.lstm(embedded) # (batch, seq_len, hidden_dim*2) # 平均池化忽略 padding 位置 mask (x ! 0).unsqueeze(-1).float() # (batch, seq_len, 1) summed (lstm_out * mask).sum(dim1) counts mask.sum(dim1).clamp(min1) avg_pooled summed / counts out self.dropout(avg_pooled) logits self.fc(out) return logits平均池化比取最后一步更稳尤其当关键情感词出现在句子中间时。mask的作用是把 padding 位置排除在平均之外否则补零会拉低均值。如果发现模型对否定句还是不敏感可以试试在 LSTM 后面加一个注意力层让模型自己学哪些词重要但那是进阶做法期末大作业用平均池化已经够。3.3 训练循环与早停几个必须监控的指标训练循环里要监控训练 loss、验证 loss、验证准确率和验证 F1。短文本情感分析里准确率会被多数类拉高所以 F1 更重要。早停的触发条件通常是验证 loss 连续 3 个 epoch 不下降。def train_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for batch_x, batch_y in loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() return total_loss / len(loader) def evaluate(model, loader, criterion, device): model.eval() total_loss 0 correct 0 total 0 with torch.no_grad(): for batch_x, batch_y in loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) logits model(batch_x) loss criterion(logits, batch_y) total_loss loss.item() preds logits.argmax(dim1) correct (preds batch_y).sum().item() total batch_y.size(0) return total_loss / len(loader), correct / totalclip_grad_norm_的max_norm5.0是 LSTM 的常用值梯度超过这个范数就按比例缩放防止梯度爆炸。优化器一般用 Adam学习率 1e-3如果 loss 震荡可以降到 5e-4。训练 epoch 数通常 10 到 20 就够配合早停。4. 避坑与排查短文本情感分析里最容易翻车的五个地方4.1 现象训练准确率 95%测试准确率 60%原因过拟合。短文本数据量小模型参数量相对大几个 epoch 就把训练集背下来了。解决加 dropout0.3 到 0.5、减小 hidden_dim比如从 256 降到 128、加 L2 正则优化器里设 weight_decay1e-4、早停。如果还不行说明数据量确实太少考虑数据增强比如同义词替换、随机删除非关键 token。4.2 现象模型把所有样本都预测成多数类原因类别不均衡且没有加权。解决在 CrossEntropyLoss 里加 weight 参数或者用 WeightedRandomSampler 让少数类被采样概率更高。检查一下验证集的 F1如果少数类 F1 接近 0基本就是这个原因。4.3 现象验证 loss 突然变成 NaN原因梯度爆炸或者学习率太大。解决加梯度裁剪clip_grad_norm_学习率降到 1e-3 或 5e-4。如果用了预训练词向量检查词向量里有没有 NaN 值。另外padding 位置如果参与了 loss 计算也可能导致 NaN确认 ignore_index0 设对了。4.4 现象分词结果里否定词被切碎原因jieba 默认词典对「不好用」「没法看」这类组合词切分不符合预期。解决加载自定义词典把业务里的否定组合加进去。或者用 jieba 的suggest_freq调整词频让「不好用」不被切开。这个坑很隐蔽因为切碎之后模型还是能训但准确率就是上不去。4.5 现象推理时单条预测和批量预测结果不一致原因推理时没有加model.eval()dropout 还在起作用或者单条预测时序列长度和训练时不一致padding 方式不同。解决推理前一定调model.eval()并且用torch.no_grad()。单条预测时按训练时的 max_len 补齐不要动态变长。5. 让模型真正能用推理封装、错误分析与一个提点技巧训练完模型只是第一步要让它能接进业务得把分词、词表映射、padding、推理封装成一个函数。下面这个predict函数可以直接拿去用输入原始中文句子输出情感标签和置信度。def predict(text, model, vocab, device, max_len64): model.eval() tokens tokenize(text) ids [vocab.get(w, vocab[unk]) for w in tokens][:max_len] # 补齐到 max_len if len(ids) max_len: ids ids [0] * (max_len - len(ids)) tensor torch.tensor(ids, dtypetorch.long).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) pred logits.argmax(dim1).item() confidence probs[0][pred].item() label_map {0: 负面, 1: 正面} return label_map.get(pred, 未知), confidence # 使用示例 # label, conf predict(物流很快但是包装破了, model, vocab, device) # print(label, conf)这个函数里max_len必须和训练时一致否则 padding 位置对不上。confidence低于 0.6 的样本建议人工复核尤其是业务里对误判敏感的场景。错误分析是提点最快的方法。把验证集里预测错的样本导出来按错误类型分组否定句、转折句、反讽句、中性被误判。反讽句是短文本情感分析的天花板LSTM 基本搞不定遇到就认了别硬调。否定句和转折句可以通过加注意力机制改善或者把「但是」「不过」「虽然」这些转折词作为额外特征拼到 LSTM 输出上。最后一个技巧如果数据里有星级评分可以把星级作为辅助任务让模型同时预测情感极性和星级多任务学习通常能提升 1 到 3 个百分点。具体做法是在 LSTM 输出后接两个全连接头一个输出二分类一个输出五分类loss 加权求和。这个改动不大但效果往往比调参明显。我自己做这类项目时习惯先把基线跑通再逐步加东西每加一个改动就记录验证集 F1 的变化。最怕的是一上来就堆双向 LSTM 加注意力加预训练词向量结果出了问题不知道是哪一层导致的。短文本情感分析没有银弹数据质量、分词、类别均衡这三件事做扎实比换模型结构管用得多。希望帮到你。本文还有配套的精品资源点击获取