news 2026/9/23 14:08:22

中文情感分析系统实战:CNN-Bi-LSTM源码复现与毕业设计指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中文情感分析系统实战:CNN-Bi-LSTM源码复现与毕业设计指南

简介:这份资源是面向计算机相关专业学生与项目实战学习者的中文情感分析系统源码,采用CNN-Bi-LSTM混合神经网络实现,可作为毕业设计、课程设计或期末大作业的完整参考方案。项目经导师指导并通过评审,代码结构完整、可运行,适合希望快速上手深度学习文本分类的初学者对照学习。压缩包共35个文件,约73.2MB,包含13个Python源码文件、10个文本数据与说明文件、2个模型pb文件及配套权重索引文件,另有png与jpeg图片、md说明文档等,覆盖数据预处理、模型构建、训练与评估全流程。资源中提供了酒店评论数据集与评分报告脚本,便于读者理解中文情感分析从语料加载到结果输出的完整链路。目前已有72人学习下载,适合需要完整项目骨架、排错思路与目录结构参考的学习者使用。

1. 中文情感分析系统:从 CNN-Bi-LSTM 源码到可复现的毕业设计

电商评论、舆情短文本、课程作业里的影评数据集,只要涉及“这句话是正面还是负面”,就绕不开中文情感分析。但真正动手时你会发现,公开的 CNN-Bi-LSTM 源码要么只给一个模型文件,要么预处理和训练脚本对不上,跑起来 loss 不降、准确率卡在 50% 上下。这篇笔记围绕一套可复现的中文情感分析系统源码展开,把 CNN 提取局部 n-gram 特征、Bi-LSTM 建模长距离依赖这条经典路线拆成能照着敲的步骤。适合正在做毕业设计、需要一份能讲清原理又能跑出结果的学生,也适合想快速验证中文短文本分类方案的工程师。读完你能自己搭出数据清洗、词表构建、模型训练到推理可视化的完整链路,并且知道每个参数为什么这么设。

2. CNN-Bi-LSTM 做中文情感分析,为什么比单模型稳

2.1 卷积层和双向 LSTM 各自解决什么问题

中文情感分析的核心难点在于情感信号既藏在局部短语里,也藏在跨句的语义转折里。比如“虽然包装一般,但是用起来真香”,前半句负面词“一般”和后半句正面词“真香”之间隔了转折,单靠词袋模型会直接判错。

CNN 在这里的作用是滑动卷积核,一次性捕捉相邻几个词的组合特征。卷积核大小为 3 时,它能同时看到“用起来真香”这种三元组,相当于自动学习 n-gram,而且比手工构造 n-gram 更省事。池化层再做一次最大池化,把整句里最强的情感信号保留下来,位置不敏感,这对评论这种语序灵活的中文文本很关键。

Bi-LSTM 补的是另一块。它按时间步读入词向量,前向 LSTM 记住“虽然”开头的让步语气,后向 LSTM 从“真香”往回看,两个方向的隐状态拼接后,转折关系就被编码进去了。常见做法是把 CNN 的输出序列再喂给 Bi-LSTM,让局部特征在时序上再走一遍,最后取 Bi-LSTM 最后一个时间步的隐状态接全连接层做二分类。

选型理由很直接:TextCNN 快但抓不住长依赖,Bi-LSTM 能抓长依赖但局部特征靠词向量本身,两者串起来在中文短文本上通常比单用其中一个高 2 到 4 个百分点。代价是训练慢一些,显存占用高一些,但对毕业设计这种数据量(几万条)完全扛得住。

2.2 数据预处理:分词、去噪和标签对齐

中文和英文最大的区别是词与词之间没有空格,必须先分词。源码里一般用 jieba,但要注意情感分析场景下不能无脑分词,得保留否定词和程度副词。比如“不好”如果被切成“不”和“好”,情感极性就反了。常见做法是加载自定义词典,把“不好”“不错”“一般般”这类词加进去,或者直接用 jieba 的cut配合停用词表,但停用词表里千万别删“不”“没”“很”这些。

数据清洗步骤我一般按这个顺序走:先去 HTML 标签和 URL,再统一全角半角,然后去掉纯数字和表情符号(表情可以单独做特征,但入门版先去掉),最后按标点切句,只保留长度在 5 到 200 字之间的样本。标签对齐指的是确保每条文本对应的 label 是 0 或 1,不能有缺失或字符串类型的标签混进去。

import re import jieba # 加载自定义词典,把情感短语加进去,避免否定词被切散 jieba.load_userdict("sentiment_dict.txt") def clean_text(text): # 去掉 HTML 标签和 URL text = re.sub(r"<[^>]+>", "", text) text = re.sub(r"http[s]?://\S+", "", text) # 全角转半角 text = "".join([chr(ord(ch) - 65248) if 65281 <= ord(ch) <= 65374 else ch for ch in text]) # 只保留中文、英文和基本标点 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z,。!?、]", "", text) return text.strip() def tokenize(text): # 精确模式分词,过滤空白 return [w for w in jieba.lcut(text) if w.strip()] # 示例 raw = "虽然包装一般,但是用起来真香!http://example.com" cleaned = clean_text(raw) tokens = tokenize(cleaned) print(tokens) # ['虽然', '包装', '一般', '但是', '用起来', '真香']

这段代码里clean_text负责去噪和归一化,tokenize负责切词。参数上,sentiment_dict.txt每行一个词,可以手动加“真香”“一般般”“不推荐”这类。注意re.sub里的字符集范围,\u4e00-\u9fa5覆盖常用汉字,如果数据里有生僻字或繁体,需要额外处理。分词后建议统计一下词频,把出现次数少于 2 的词替换成<UNK>,否则词表太大,嵌入层参数爆炸。

2.3 词表构建和序列截断的工程细节

词表构建不是简单地把所有词塞进去。我一般按词频从高到低排序,取前 20000 个词,保留<PAD><UNK>两个特殊 token。<PAD>用于补齐短句,<UNK>用于替换低频词。词表大小直接影响嵌入层参数量,20000 乘 128 维大约是 256 万参数,对毕业设计的显卡(比如 6G 显存)刚好。

序列截断长度取 128 或 256。中文评论大部分在 50 字以内,128 能覆盖 95% 以上的样本。超过的直接截断,不足的用<PAD>补到 128。这里有个坑:如果 batch 里所有样本都补到 128,但实际有效长度只有 20,Bi-LSTM 会在大量 padding 上浪费计算,还会引入噪声。解决办法是用pack_padded_sequence,但入门版可以先不做,等模型跑通再优化。

from collections import Counter import torch def build_vocab(token_lists, max_size=20000, min_freq=2): counter = Counter() for tokens in token_lists: counter.update(tokens) # 按频率排序,保留特殊 token vocab = {"<PAD>": 0, "<UNK>": 1} for word, freq in counter.most_common(max_size - 2): if freq >= min_freq: vocab[word] = len(vocab) return vocab def encode(tokens, vocab, max_len=128): ids = [vocab.get(w, vocab["<UNK>"]) for w in tokens] if len(ids) < max_len: ids += [vocab["<PAD>"]] * (max_len - len(ids)) else: ids = ids[:max_len] return ids # 假设 token_lists 是分词后的列表 vocab = build_vocab(token_lists) input_ids = encode(tokens, vocab) input_tensor = torch.tensor(input_ids).unsqueeze(0) # 增加 batch 维度

build_vocabmax_sizemin_freq是两个必调参数。max_size太大会导致嵌入层过拟合,太小会丢信息,20000 是中文情感分析的常用值。min_freq=2能过滤掉拼写错误和噪声词。encode函数里先截断再补齐,顺序不能反,否则短句会被截掉有效部分。unsqueeze(0)是为了模拟 batch 维度,实际训练时用DataLoader自动拼 batch。

3. 模型搭建与训练:从嵌入层到全连接层的参数怎么定

3.1 CNN 卷积核数量和 Bi-LSTM 隐藏层维度的搭配

模型结构按“嵌入层 → CNN → Bi-LSTM → 全连接”串。嵌入层维度一般取 128 或 256,和词表大小配合。CNN 部分我习惯用三个不同尺寸的卷积核,比如 2、3、4,每个尺寸 64 个核,这样能同时捕捉二元、三元、四元短语。卷积后接 ReLU 和最大池化,池化窗口覆盖整个序列长度,输出三个 64 维向量,拼接成 192 维。

Bi-LSTM 的隐藏层维度取 128,双向拼接后是 256 维。层数一般 1 层就够,2 层容易过拟合,除非数据量超过 10 万条。Dropout 加在嵌入层后和 Bi-LSTM 输出后,概率 0.3 到 0.5。全连接层从 256 维降到 2 维(二分类),中间可以加一个 64 维的隐藏层,但毕业设计为了简单可以直接 256 到 2。

import torch.nn as nn class CNN_BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim=128, cnn_filters=64, lstm_hidden=128, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) # 三个不同尺寸的卷积核 self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, cnn_filters, kernel_size=k) for k in [2, 3, 4] ]) self.lstm = nn.LSTM(cnn_filters * 3, lstm_hidden, batch_first=True, bidirectional=True) self.dropout = nn.Dropout(0.4) self.fc = nn.Linear(lstm_hidden * 2, num_classes) def forward(self, x): # x: [batch, seq_len] emb = self.embedding(x) # [batch, seq_len, embed_dim] emb = emb.permute(0, 2, 1) # [batch, embed_dim, seq_len] conv_outs = [] for conv in self.convs: c = torch.relu(conv(emb)) # [batch, cnn_filters, seq_len-k+1] c = torch.max(c, dim=2)[0] # [batch, cnn_filters] conv_outs.append(c) cnn_out = torch.cat(conv_outs, dim=1) # [batch, cnn_filters*3] # 扩展成序列喂给 LSTM,这里简单重复 seq_len 次 cnn_out = cnn_out.unsqueeze(1).repeat(1, x.size(1), 1) # [batch, seq_len, cnn_filters*3] lstm_out, _ = self.lstm(cnn_out) # [batch, seq_len, lstm_hidden*2] # 取最后一个时间步 last = lstm_out[:, -1, :] last = self.dropout(last) return self.fc(last)

这段代码里cnn_filters=64lstm_hidden=128是核心参数。卷积核尺寸[2,3,4]覆盖常见中文短语长度。padding_idx=0让嵌入层对<PAD>不更新梯度。注意permute是因为Conv1d要求通道在第二维。LSTM 输入这里做了简化,把 CNN 输出重复成序列,实际也可以把 CNN 输出作为每个时间步的额外特征拼到词向量上,但那样改动较大,入门版先跑通这个结构。

3.2 训练循环、学习率与早停策略

训练用 Adam 优化器,学习率 1e-3,batch size 64。损失函数用交叉熵,二分类也可以用 BCEWithLogitsLoss,但交叉熵更通用。每个 epoch 后在验证集上算准确率和 F1,如果验证 loss 连续 3 个 epoch 不下降就早停,保存验证集上最好的模型。

import torch.optim as optim from sklearn.metrics import f1_score device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = CNN_BiLSTM(vocab_size=len(vocab)).to(device) optimizer = optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() best_f1 = 0 patience = 3 wait = 0 for epoch in range(20): model.train() for batch_x, batch_y in train_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() optimizer.step() # 验证 model.eval() preds, labels = [], [] with torch.no_grad(): for batch_x, batch_y in val_loader: batch_x = batch_x.to(device) logits = model(batch_x) pred = torch.argmax(logits, dim=1).cpu().numpy() preds.extend(pred) labels.extend(batch_y.numpy()) f1 = f1_score(labels, preds, average="binary") print(f"Epoch {epoch}, F1: {f1:.4f}") if f1 > best_f1: best_f1 = f1 torch.save(model.state_dict(), "best_model.pt") wait = 0 else: wait += 1 if wait >= patience: print("Early stop") break

学习率 1e-3 是 Adam 的默认值,如果 loss 震荡可以降到 5e-4。patience=3是早停耐心值,验证集小的时候可以设 5。保存模型用state_dict而不是整个模型,方便后续加载。注意验证时model.eval()torch.no_grad()要成对出现,否则 dropout 和 batch norm 会干扰结果。

3.3 类别不平衡时用加权损失和阈值调整

中文情感分析数据集经常正负样本比例悬殊,比如好评远多于差评。这时候准确率会虚高,模型全预测成多数类也能到 80%。解决办法有两个:一是损失函数加weight参数,给少数类更高权重;二是调整分类阈值,默认 0.5 改成 0.3 或 0.4,让少数类更容易被预测出来。

# 假设正类样本 8000,负类 2000 weight = torch.tensor([1.0, 4.0]).to(device) # 负类权重是正类的 4 倍 criterion = nn.CrossEntropyLoss(weight=weight) # 推理时调整阈值 def predict_with_threshold(logits, threshold=0.4): probs = torch.softmax(logits, dim=1) # 取正类概率和阈值比较 return (probs[:, 1] > threshold).long()

weight的计算方式是多数类样本数除以少数类样本数。阈值调整要在验证集上试,一般从 0.3 到 0.5 扫一遍,选 F1 最高的。注意阈值调整只影响推理,不影响训练。

4. 避坑与排查:源码跑不通时先看这 5 条

4.1 现象:loss 不降,准确率一直在 50% 附近

原因通常是标签没对齐或者嵌入层没更新。先检查batch_y是不是全是 0 或全是 1,再看padding_idx=0有没有设,如果没设,<PAD>也会参与梯度更新,把嵌入层带偏。解决方法是打印一个 batch 的标签分布,确认正负样本都有,然后检查嵌入层定义。

4.2 现象:分词后“不好”变成“不”“好”,情感极性反了

这是 jieba 默认词典的问题。解决方法是加载自定义词典,把“不好”“不错”“不推荐”“一般般”这类词加进去。如果不想维护词典,可以在分词后做后处理,把“不”和后面的形容词合并成一个 token。血泪经验是停用词表里千万别删否定词和程度副词。

4.3 现象:训练集准确率 99%,验证集只有 60%

典型过拟合。先看数据量,如果训练集不到 5000 条,模型参数又大,肯定过拟合。解决办法是加 Dropout、减小嵌入维度、早停,或者做数据增强,比如同义词替换、随机删除非关键词。注意验证集不能参与训练,也不能用来调超参数太多次,否则验证集也会过拟合。

4.4 现象:GPU 显存不够,batch size 降到 16 还是 OOM

检查序列长度是不是设成了 512 或更大。中文评论 128 足够,改成 128 能省一半显存。另外 CNN 的卷积核数量 64 乘 3 个尺寸,如果改成 32 也能省。还可以用梯度累积,batch size 设 16,累积 4 次再更新,等效 batch size 64。

4.5 现象:推理时单条预测结果和批量预测不一致

原因是model.eval()没加,dropout 还在起作用。另外单条预测时unsqueeze(0)加的 batch 维度要和训练时一致。如果训练时用了pack_padded_sequence,推理时也要用,否则 LSTM 在 padding 上的输出会不一样。解决方法是封装一个predict函数,内部统一做evalno_grad

5. 把模型用起来:推理封装、可视化与一个提点技巧

模型训练完只是半成品,毕业设计答辩时老师更想看你怎么用。我一般封装一个SentimentAnalyzer类,对外只暴露predict(text)方法,内部做清洗、分词、编码、推理、阈值判断,返回标签和置信度。这样演示时输入一句话就能出结果,比贴训练日志直观得多。

class SentimentAnalyzer: def __init__(self, model_path, vocab, max_len=128, threshold=0.4): self.model = CNN_BiLSTM(vocab_size=len(vocab)).to(device) self.model.load_state_dict(torch.load(model_path, map_location=device)) self.model.eval() self.vocab = vocab self.max_len = max_len self.threshold = threshold def predict(self, text): cleaned = clean_text(text) tokens = tokenize(cleaned) ids = encode(tokens, self.vocab, self.max_len) tensor = torch.tensor(ids).unsqueeze(0).to(device) with torch.no_grad(): logits = self.model(tensor) probs = torch.softmax(logits, dim=1) score = probs[0, 1].item() label = "正面" if score > self.threshold else "负面" return label, score analyzer = SentimentAnalyzer("best_model.pt", vocab) print(analyzer.predict("虽然包装一般,但是用起来真香")) # ('正面', 0.87)

可视化部分,毕业设计常要求画混淆矩阵和 ROC 曲线。混淆矩阵用 sklearn 的confusion_matrix,ROC 用roc_curve,注意正类概率要传probs[:, 1]而不是标签。如果想让答辩更出彩,可以加一个注意力可视化,把 Bi-LSTM 每个时间步的隐状态权重画成热力图,标出哪些词对情感判断贡献大。这个不需要改模型结构,推理时返回lstm_outattention_weights即可。

最后一个提点技巧:中文情感分析里,否定词和转折词是两大信号源。可以在输入里额外拼接一个手工特征向量,比如“是否包含转折词”“否定词数量”,和 CNN-Bi-LSTM 的输出拼接后一起进全连接层。这个改动很小,但在小数据集上通常能提 1 到 2 个点。我自己的习惯是先把基线跑通,再逐个加特征,每加一个就在验证集上看 F1 变化,涨了才保留。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 14:05:22

REOF旋转经验正交函数实战:从EOF模态混合到SVD分解与MATLAB实现

简介&#xff1a;这份资源面向地球科学、气象与海洋领域的学习者和科研人员&#xff0c;聚焦EOF、REOF、SVD与CCA等常用统计分析方法在MATLAB中的实现&#xff0c;帮助解决多变量数据降维、空间模式识别与区域气候特征提取等问题&#xff0c;适合具备一定MATLAB基础、需要复现或…

作者头像 李华
网站建设 2026/9/23 14:04:08

智能卷宗柜按需生产厂家、口碑好的智能卷宗柜厂家实力公司推荐

在政务与司法办公数字化转型的浪潮中&#xff0c;智能卷宗物证柜已经成为各级法院、政务单位规范卷宗管理、保障材料安全的刚需设备。不少负责采购的工作人员&#xff0c;都在网上搜索靠谱的智能卷宗柜实力供应企业&#xff0c;想要找到口碑好、产能足的合作方&#xff0c;也会…

作者头像 李华
网站建设 2026/9/23 14:02:46

GEO优化实战:从SEO到生成式引擎的内容引用策略

1. GEO到底在优化什么&#xff1a;从SEO到生成式引擎的范式迁移1.1 一个被误读的概念&#xff1a;GEO不是SEO的换皮很多人第一次听到GEO&#xff08;生成式引擎优化&#xff0c;Generative Engine Optimization&#xff09;&#xff0c;第一反应是"这不就是SEO换了个马甲吗…

作者头像 李华
网站建设 2026/9/23 14:00:59

电源防反接电路设计:PMOS/NMOS方案对比与选型指南

做硬件的朋友&#xff0c;恐怕都有过把电源插反的经历。我第一块独立开发的板子&#xff0c;就是在5V/3A的DC输入口上忘了加电源防反接电路&#xff0c;结果一次误插直接让板载电源芯片冒了烟。后来我把市面上常见的电源防反接电路方案挨个试了一遍&#xff1a;二极管串联、整流…

作者头像 李华