☰
情感分类三方法对比:词典法、机器学习与深度学习实战拆解
2026/10/29 22:06:15 网站建设 项目流程

简介:这是一份面向数据挖掘、机器学习与深度学习课程作业的情感分类实践项目,特别适合零基础学习者完成课设或毕业设计。项目用Python实现了情感词典法、传统机器学习(SVM、朴素贝叶斯等)和深度学习(CNN、BiLSTM、TextCNN等)三套可运行方案,并通过对比实验展示不同方法在情感判别任务中的准确率与适用场景。压缩包共16个文件,以py源码、csv数据集、txt词典与停用词、png结构图、md说明文档为主要构成,整体11.84MB,内容贯穿数据预处理、特征提取、模型构建、训练与评估等完整流程。目前已有58人学习。配套还附有详细的课设报告和数据集说明,情感词典法部分可帮助理解如何构建与使用情感词典,机器学习部分涵盖TF-IDF等特征工程,深度学习部分则演示神经网络模型的搭建与调参。三套方案的性能对比结果以图表形式保存在png文件中,便于直观理解模型差异,适合复现实验、撰写课程总结,是兼顾学习与提交作业的实用资源。

1. 课程大作业里的情感分类三件套:词典法、ML、DL到底怎么对比性能

这份资源我拆完之后的第一反应是:它把情感分类这条路上最典型的三个技术代际一次性打包了——情感词典法、传统机器学习、深度学习,而且不是各写各的 demo,是同一份微博数据集、同一个分类目标、同样完整的预处理和评估流程,最后用准确率、F1 这些指标横向对比。对于正在做课程设计或者毕业设计的同学来说,最值钱的地方在于你不需要自己从零去凑代码,三套方案的完整 pipeline 都是能直接跑的,README 和课设报告把复现路径写得比较清楚。

它适合两类人:一是机器学习或深度学习课程作业不知道从哪里下手、想拿一份可以复现的完整工程做参考的学生;二是已经跑通单一模型、但想看看三种方法在同一份数据上到底差多少的研究型学习者。数据用的是微博情感标注语料,10k 和 100k 两个规模都有,分别适合快速调试和完整实验。接下来我会按代码文件逐个拆,重点讲每个脚本的核心逻辑、参数含义和运行顺序,最后把我在复现时遇到的几个坑一并列出来。

2. 数据与词表先行:先搞懂 weibo_senti 数据集和四份词典文件

2.1 weibo_senti_10k 和 100k 的格式与分布

进入任何分类项目之前,我习惯先把数据集摊开看一遍。这个包里的数据是 CSV 格式,核心字段就两列:一列是评论文本,一列是情感标签。标签是二分类,正面和负面各占一半,没有中性类别,这意味着后面所有模型面对的都是二分类问题,评估指标直接用准确率、精确率、召回率、F1 就够用,不需要做多分类的混淆矩阵扩展。

10k 版本大概一万条样本,适合在词典法和传统机器学习上做快速验证,训练时间控制在分钟级;100k 版本则是十万条,主要给深度学习模型用——CNN、BiLSTM 这类模型在数据量小的时候很难体现出优势,但到了十万条规模,深度模型的文本表征能力就能明显和词典法拉开差距。我在实际使用时是先拿 10k 跑通全流程,确认每个脚本都没有问题,再切到 100k 做最终的性能对比实验。

2.2 词典文件各管什么:degree、inverse、stopwords、positive、negative

拆开压缩包后,词典部分一共有五份文件,它们在情感词典法里各自承担不同的职责。我建议先单独读这几份文件,理解每个文件的行格式,后面看 dict.py 的逻辑会顺畅很多。

positive_simplified.txt 和 negative_simplified.txt 是基础情感词表,分别存放积极和消极词汇,每行一个词。这是情感计算的第一层,文档中文本匹配到这些词就对应加分或减分。degree.txt 存放程度副词,比如“非常”“极其”“有点”,这类词本身不表达情感,但会放大或缩小后面情感词的权重,通常取值在 0.5 到 2 之间。inverse_words.txt 是反转词表,也叫否定词表,存放“不”“没”“无”这类词,它们会翻转情感极性——比如“不错”是正向,但“不太好”就变成了负向。stopwords.txt 是停用词表,在特征提取阶段过滤掉“的”“了”“是”这类无信息量的词。

这里有个细节值得注意:在情感词典法中,否定词和程度副词的处理顺序是决定效果的关键。通常的做法是从左到右扫描句子,遇到程度副词就把权重累乘,遇到否定词就翻转极性。但中文里“不太好看”和“太不好看”的语义差别很大,这种连续的否定+程度组合就要靠代码里的窗口逻辑去处理,不是简单翻转一次就能解决。

2.3 数据清洗与分词:jieba 在情感分类里的标准用法

在进入三套方法之前,还有一段公共的前置流程——数据清洗和分词。这个包的做法是先把文本里的 URL、@用户、多余空格去掉,然后对每条文本做 jieba 分词,再用停用词表过滤。虽然代码分成三个脚本,但预处理逻辑是一致的,这样三套方法在输入数据上才是公平对比的。

分词这一步对词典法的影响比对深度学习模型的影响大得多。词典法靠词匹配来累加情感分,如果分词粒度不对,比如“不高兴”被切成了“不高兴”整体而不是“不”+“高兴”,否定词反转逻辑就失效了。jieba 默认词典在这个场景下表现一般,我一般会在初始化时加载用户自定义词典,把情感词典里的词汇加进去,强制 jieba 优先按情感词边界切分。代码里如果没做这一步,你在复现时可以在 jieba 初始化后加一行jieba.load_userdict('positive_simplified.txt'),效果会明显改善。

3. 情感词典法实现拆解:dict.py 的评分规则与阈值设定

3.1 核心评分逻辑:正向分加负向分,如何叠加程度词和反转词

dict.py 实现的是最经典的情感词典打分法,整体思路不复杂:对一条文本完成分词后,逐个匹配情感词表,累加情感分数,最后用总分正负号判断情感极性。真正让代码有区分度的地方在于它如何处理程度副词和否定词。

我拆代码时看到的典型处理方式是维护一个情感分变量,初始为 0。遍历分词结果,如果当前词是情感词,就查看它前面是否有否定词和程度副词。如果前面有否定词,情感词的极性反转,加分变减分;如果同时还有程度副词,还要乘上程度权重。举例来说,“电影非常不好看”这句话,分词后“非常”是程度词(权重约 1.8),“不好看”由否定词“不”加上情感词“好看”组成,那么计算时,“好看”本身如果是 +2,遇到否定词变成 -2,再乘上“非常”的 1.8,最终约 -3.6。核心代码逻辑类似这样:

def score_sentence(words, sentiment_dict, degree_dict, inverse_words): score = 0.0 i = 0 while i < len(words): word = words[i] if word in sentiment_dict: base_score = sentiment_dict[word] degree_weight = 1.0 inverse_flag = 1 # 向前看两个词,判断否定和程度副词 if i >= 1: if words[i-1] in inverse_words: inverse_flag = -1 if words[i-1] in degree_dict: degree_weight = degree_dict[words[i-1]] if i >= 2: if words[i-2] in inverse_words and words[i-1] in degree_dict: inverse_flag = -1 score += base_score * inverse_flag * degree_weight i += 1 return score

窗口大小的设置很关键。这里向前看两个词是常见的取值,因为中文里“不 + 程度词 + 情感词”是高频组合,比如“不是很喜欢”。如果窗口开到 3 甚至更大,会引入跨分句的误判;如果只向前看 1 个词,又处理不了“不是很喜欢”这类连续修饰。我在自己的项目里一般把窗口设为 2,同时要求否定词和程度词必须紧邻情感词的左侧才生效。

3.2 阈值从哪来:没有标注数据时,能不能用 0 作为判断边界

词典法的最后一步是把情感总分映射到分类标签。很多初学者以为阈值设为 0 就万事大吉——总分大于 0 判正向,小于 0 判负向。但实际跑下来你会发现,大量中性文本或正负混杂的文本总分接近 0,直接用 0 做边界会把很多微弱情感样本强行分成两类,精度很难看。

更稳的做法是在验证集上搜索最优阈值。把训练集的分数的 10 分位和 90 分位之间的区间等分 50 份,逐个阈值计算 F1 分数,选 F1 最大的那个点作为最终判决边界。这个思路在代码里不一定实现了,但如果你要优化词典法性能,这是一条值得自己动手改的路径。另外,如果某些情感词在词表里不存在,比如网络新词“绝绝子”“yyds”,词典法的召回率会明显下降,这是方法本身的局限,不是代码 bug。

3.3 词典法在微博语料上的效果预期

使用微博 10k 数据集跑通后,词典法的准确率通常在 60% 上下浮动,好的情况下能到 65%。直观感受是:对表达直接、词汇明显的句子判断准确,对反讽和隐含情感几乎全部判错。这不是实现的问题,而是词典法天然缺少上下文建模能力。所以看这份代码时不要纠结于准确率数字本身,重点放在理解打分规则和阈值调优上。

4. 传统机器学习进阶:ml.py 的特征工程与三类模型对比

4.1 文本向量化:词袋模型和 TF-IDF 各自的适用边界

ml.py 走的是标准传统机器学习流程:分词 → 向量化 → 分类器 → 评估。向量化环节一般同时提供两种特征:词袋模型(CountVectorizer)和 TF-IDF。词袋模型统计词频,适合文本长度相对均匀、情感词出现频率能代表倾向的场景;TF-IDF 则给高频但无区分度的词降权,给低频但情感明确的词提权,在情感分类任务里通常略优于纯词袋。

在调参上,我认为有一个参数值得优先关注——min_df。它控制词在所有文档中出现的最少次数,默认值 1 意味着词只要出现过一次就进特征矩阵,这会让特征维度膨胀到几万维,训练变慢且容易过拟合。我一般会把它设为 5,即至少在 5 条文档中出现过的词才保留,同时配合max_df设置为 0.8,过滤出现在超过 80% 文档里的词,这些通常是停用词或高频无意义词。另外,ngram_range可以试着从(1, 1)调到(1, 2),把相邻词组纳入特征,对“不太好”这类否定+形容词组合会有正向帮助。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.pipeline import make_pipeline def build_pipeline(model, use_tfidf=True): if use_tfidf: vectorizer = TfidfVectorizer(min_df=5, max_df=0.8, ngram_range=(1, 2)) else: vectorizer = CountVectorizer(min_df=5, max_df=0.8, ngram_range=(1, 2)) pipeline = make_pipeline(vectorizer, model) return pipeline

4.2 三个经典分类器:朴素贝叶斯、SVM、逻辑回归在情感分类中的表现差异

传统机器学习的分类器选择上,这份代码主要会涉及朴素贝叶斯、SVM 和逻辑回归三个模型。它们在不同特征空间下的表现有规律可循。

朴素贝叶斯是情感分类的经典基线。它假设特征之间独立,这个假设在文本上严格说不成立,但因为实现简单、训练极快,在小样本上表现稳定,且对 TF-IDF 特征有很好的适应能力。我一般把它当作所有实验的第一个跑通模型,验证数据链路没有问题。

SVM 特别是在 RBF 核下表达能力更强,对特征维度和样本非线性能拟合得更好,但两个问题很现实:一是训练复杂度随样本量上升明显,10 万条样本配几万维特征在普通笔记本上可能要跑很久;二是 RBF 核的两个参数 C 和 gamma 需要调,网格搜索的耗时也是在传统模型里最高的。我的建议是:10k 数据上用 RBF 核没问题,100k 数据上建议换成线性核。

逻辑回归是三者里最容易被低估的。线性分类器在文本这类高维稀疏特征上,性能通常不输 SVM,而且训练速度快得多,还自带概率输出,方便后面画 ROC 曲线或做阈值调整。实际对比中,逻辑回归和线性 SVM 在微博情感数据上的准确率非常接近,但训练时间是 SVM 的几分之一。

from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 切分数据 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 逻辑回归 + TF-IDF pipeline lr_pipeline = build_pipeline(LogisticRegression(C=1.0, max_iter=1000), use_tfidf=True) lr_pipeline.fit(X_train, y_train) y_pred = lr_pipeline.predict(X_test) print(classification_report(y_test, y_pred))

参数设置上重点说明两点:test_size=0.2是常规划分,但必须配合stratify=y,保证训练集和测试集里正负样本比例一致,否则遇到原始数据本身分布不均衡时,模型评估会失真;max_iter要调大到 1000,否则逻辑回归默认 100 次迭代用在 TF-IDF 高维特征上经常无法收敛,控制台会直接报ConvergenceWarning。

4.3 传统机器学习的性能预期与常见误区

在 10k 微博数据上,这三个模型的准确率普遍能到 70% 到 75% 区间,明显超过词典法的 60% 左右。逻辑回归和线性 SVM 的表现接近,朴素贝叶斯通常会略低两三个百分点,但差距不会太大。这一阶段能获得的结论是:标注数据带来的监督信号比人工规则更可靠,特征工程的收益在传统方法中是决定性的。

常犯的一个误区是认为 TF-IDF 一定比词袋好。实际上,在情感分类短文本场景中,很多情感词出现的绝对频率比它在全语料中的权重更有判别力,词袋模型表现并不差。我自己的习惯是两个都跑一遍,通过交叉验证决定选哪个特征,而不是凭经验拍板。

5. 深度学习模型落地:nn.py 里的 TextCNN、BiLSTM 与训练避坑指南

5.1 嵌入层选择与 embedding 矩阵初始化方式

深度学习部分的核心脚本是 nn.py,它包含三种网络结构:CNN、BiLSTM、TextCNN。所有模型的第一步都是把文本转成 ID 序列,再经过嵌入层映射成稠密向量。

在嵌入层这个环节,需要决定两件事:一是嵌入维度,常见取值是 100 或 200;二是预训练词向量,如果不使用预训练权重,就随机初始化让模型自己学。这个小规模课程项目里没有附带预训练词向量文件,所以用随机初始化是可以接受的。但有一个参数值得注意——嵌入层的padding_idx,一定要设置为 0,这样在训练时填充位置不会产生梯度更新,可以稳定训练过程。

import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=100, num_filters=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, num_filters, kernel_size=size, padding=size // 2) for size in [2, 3, 4] ]) self.fc = nn.Linear(num_filters * 3, num_classes) def forward(self, x): x = self.embedding(x) # (batch, seq_len, embed_dim) x = x.transpose(1, 2) # (batch, embed_dim, seq_len) conv_outputs = [] for conv in self.convs: conv_out = torch.relu(conv(x)) pooled = torch.max_pool1d(conv_out, conv_out.size(2)).squeeze(2) conv_outputs.append(pooled) x = torch.cat(conv_outputs, dim=1) return self.fc(x)

TextCNN 的关键机制是三个不同宽度的卷积核并行提取 n-gram 特征。核宽度 2、3、4 分别对应二元、三元、四元词组,这比在机器学习阶段手动设置ngram_range更灵活——卷积核能自动学习哪些词组组合最有情感判别力。num_filters=128表示每个宽度用 128 个卷积核,输出拼接后得到 384 维特征向量,再接全连接层。滤波器数量太小时特征表达不足,太大则训练变慢且容易过拟合,128 在这个数据规模下是稳妥的选择。

5.2 BiLSTM 的序列建模能力与训练细节

BiLSTM 结构就是双向长短时记忆网络,它从左到右和从右到左各扫一遍文本,然后把两个方向的隐藏状态拼接。对情感分类来说,右到左的信息流非常有用,因为“太差了”这种否定+情感的语义,反向信息流能更清晰地捕捉情感词后面的修饰关系。

训练 BiLSTM 有两个值得注意的细节。第一是隐藏层维度,常见取hidden_dim=128,层数一般设 1 到 2 层,2 层能捕获更抽象的语义,但训练时间倍增且小数据下容易过拟合。第二是池化方式,BiLSTM 每个时间步都会输出一个隐藏状态,常用的做法是取最后一个时间步的输出,但更好的做法是做一个全局最大池化——把每个时间步的隐藏状态在时间维度做 max-pool,这样不会漏掉句子中间位置的关键信息。

class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim=100, hidden_dim=128, num_layers=2, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True, bidirectional=True) self.fc = nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): x = self.embedding(x) # (batch, seq_len, embed_dim) lstm_out, _ = self.lstm(x) # (batch, seq_len, hidden_dim * 2) pooled = torch.max_pool1d(lstm_out.transpose(1, 2), lstm_out.size(1)).squeeze(2) return self.fc(pooled)

注意全连接层的输入维度是hidden_dim * 2,因为双向 LSTM 把正反两个方向的隐藏状态拼在了一起。如果把bidirectional=False,就要把维度改成hidden_dim。这是改代码时最容易踩的维度不匹配错误。

5.3 训练参数边界:学习率、batch size、epoch 和早停机制怎么定

nn.py 的训练函数一般使用 Adam 优化器和交叉熵损失函数。我在复现中建议这样设参数:学习率 0.001,batch size 64,最大 epoch 数 20,并搭配早停机制——连续 3 个 epoch 验证集损失没有下降就提前终止。深度学习模型在小数据集上跑太多 epoch 很容易过拟合,微博情感数据本身噪声不小,训练集准确率刷到 99% 但验证集只有 75% 的情况经常出现。

def train_model(model, train_loader, val_loader, epochs=20, lr=0.001, patience=3): optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() best_f1 = 0 bad_counter = 0 for epoch in range(epochs): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() output = model(batch_x) loss = criterion(output, batch_y) loss.backward() optimizer.step() val_f1 = evaluate(model, val_loader) if val_f1 > best_f1: best_f1 = val_f1 torch.save(model.state_dict(), 'best_model.pt') bad_counter = 0 else: bad_counter += 1 if bad_counter >= patience: print(f'Early stop at epoch {epoch + 1}') break

这里把patience设为 3 是我个人常用的折中。太大模型要继续跑好几个没提升的 epoch 浪费时间,太小又容易在训练初期验证集波动时误停。保存验证集指标最优的权重而不是最后一个 epoch 的权重,这条经验很关键——深度学习训练后期验证指标波动很大,最后一个 epoch 的模型往往不是最优状态。

5.4 避坑章节:复现中最常踩的五类问题

问题一:torch 维度不匹配报错,模型前向传播直接挂掉。现象是mat1 and mat2 shapes cannot be multiplied。原因绝大多数是 BiLSTM 双向拼接后的维度在后续全连接层没对齐,或者 Embedding 层的padding_idx导致输出形状变化。解决方式是先在模型里print(x.shape)逐步打印传播过程中的形状,逐层核对期望维度,不要直接跑完整训练再排错。

问题二:训练 loss 下降缓慢,几个 epoch 后准确率纹丝不动。原因大概率是文本序列没有做截断或填充,同一个 batch 里的序列长度极度不一致,导致大量 pad token 参与计算。解决方式是设定max_len为 128,超过部分直接截断,不足部分填充到统一长度,同时确认padding_idx=0。

问题三:GPU 显存不足,日志报 CUDA out of memory。原因有两个:一是 batch size 设得太大,二是max_len太长导致序列矩阵过于庞大。解决方式是 batch size 从 64 降到 32,max_len从 256 降到 128。如果还在报错,就要检查数据加载时是否用了pin_memory=True,这个参数在某些机器上的内存拷贝开销也很大。

问题四:验证集 F1 比训练集低 20 个百分点以上,明显过拟合。原因通常是训练轮次过多,或模型容量过大而数据量只有 10k。解决方式是缩短max_len减小参数规模、增大dropout到 0.5、提前终止;如果还不行,就切到 100k 数据集训练,深度学习模型在 10k 规模下过拟合是正常现象。

问题五:分词和预处理不一致,导致深度学习结果比机器学习还差。现象是深度学习模型的准确率反而比逻辑回归低。原因很多时候不是模型不行,而是深度学习脚本里没有做同等的文本去噪,比如 URL、表情符、@符号直接保留进了模型输入。解决方式是把三份代码的预处理统一对齐——同样的去噪逻辑、同样的停用词表、同样的 max_len 截断。在做对比实验时,保证唯一的变量是模型本身,这比调参更重要。

6. 性能对比与复现验证:三套方法的精确评估技巧与词典法阈值优化

6.1 对比实验的公平性控制

这个课程大作业最有价值的部分是三套方法的性能对比结论。但对比实验的公平性如果不控制好,结论就是废的。我在复现时要求自己做到以下几点:三份脚本使用同一切分后的训练集和测试集,而不是各自随机切分一次;统一分词方式,全部用 jieba 加自定义词典;评估指标统一输出准确率、精确率、召回率、F1 四项,而不仅仅看准确率。

from sklearn.metrics import classification_report, f1_score results = {} results['dict'] = evaluate_dict_model(test_texts, test_labels) results['ml_lr'] = evaluate_ml_model(lr_pipeline, test_texts, test_labels) results['dl_textcnn'] = evaluate_dl_model(model, test_loader) for name, metrics in results.items(): print(f'{name}: {metrics}')

一个小技巧是用sklearn的classification_report输出完整指标矩阵,它会同时给每个类别的精确率、召回率和 F1,以及 macro 平均和 weighted 平均。对二分类情感任务,我习惯看 macro F1,因为它不偏向样本量多的那一类,比单一准确率更能反映模型在小类别上的真实表现。

6.2 阈值再优化:词典法唯一能低成本提升的空间

词典法的性能瓶颈不在代码实现而在阈值和词表覆盖度。如果你不想引入机器学习,还想把词典法往上推一推,可以在验证集上做阈值网格搜索。做法是把每条测试样本的总分算出来后,遍历从 0 到 1 的阈值,找出 F1 最大化的最优判定点。这个优化的收益通常能提升一到两个百分点的 F1,属于词典法里少有的低成本高回报手段。

在微博这种短文本领域,还可以做一个针对性补充:把语料中出现频率高的网络新词人工加入本来就有的情感词表,比如“冲”“yyds”“绝绝子”。加入几个高频网络词后,情感词典在微博数据上的覆盖率会立刻改观,这是一个数据领域特有的 trick。

6.3 结论如何写进课设报告:不止看准确率,还要关注训练成本和可解释性

如果你正在写课程设计报告,对比章节不要只放一张准确率表格,评委想看到的是对三个方法本质差异的分析。站在资源使用者的角度,我的建议是按下面几条逻辑组织:词典法准确率最低但完全不依赖标注数据,推理在毫秒级且每一步判断都有词表依据;传统机器学习需要数千条标注数据,但训练成本低,特征结果能反向分析哪些词对情感判断贡献最大;深度学习准确率最高,尤其在 100k 数据上能领先传统方法五个百分点以上,但需要 GPU、更多的调参工作,且模型内部是黑匣子,难以解释。

课设报告最有说服力的做法是三张折线图:第一张展示训练数据规模从 10k 到 100k 时三类方法的准确率变化曲线,你会看到深度学习在 10k 时甚至可能不如逻辑回归,但到 100k 会明显反超;第二张展示训练时间对比,词典法几乎瞬间完成,机器学习分钟级,深度学习小时级;第三张展示不同阈值下的 F1 变化曲线,用来佐证词典法对阈值敏感。这三张图一放,整个课设的深度立刻和网上随便找的代码拉开了距离。

对我来说,从这次拆包最大的收获是把三份脚本的数据流理清楚之后再跑对比实验,得到的结论才是真正可信的。从那以后,我每次拿到所谓的多模型对比代码,都会强制自己先做一次完整复现,把每份脚本的预处理环节对齐、确认评估指标口径一致,再开始谈性能差异。这个方法建议你也能用起来,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询