☰
情感分类三路方案:词典法、TF-IDF+SVM与深度学习实战对比
2026/10/30 1:09:24 网站建设 项目流程

简介:这份资源是面向高校数据挖掘、机器学习课程学习者与期末大作业需求者的完整情感分类项目源码,围绕情感词典法、传统机器学习与深度学习三条技术路线展开,适合作为课程设计参考或二次开发基础。压缩包共16个文件,约11.84MB,包含3个Python源码文件、2个CSV微博情感数据集、6个情感词典与停用词文本、4张模型结构示意图及1份README说明文档,覆盖从词典构建、数据预处理到模型训练的全流程。项目已积累324人学习下载,代码注释较为完整,便于初学者理解。读者可据此掌握基于情感词典的打分规则、传统机器学习分类流程,以及CNN、TextCNN、BiLSTM等深度学习模型的实现方式,并借助附带的词典文件与微博语料快速复现实验,也可在现有结构上替换数据集或调整模型进行扩展研究。

1. 从课程大作业到真实项目:情感分类三路方案怎么选

做数据挖掘课程大作业时,情感分类几乎是出现频率最高的题目。原因很直接:数据好找、标签明确、评价指标清晰,还能把数据挖掘课里学的预处理、特征工程、模型评估串成一条完整链路。但真正动手时,多数人会卡在同一个地方——情感词典法、传统机器学习、深度学习这三条路,到底选哪条,还是全做?

我见过太多大作业翻车的案例:有人直接上 BERT,结果环境配了三天,最后跑出来准确率还不如词典法;也有人只用词典法,被老师一句“工作量不够”打回。这篇笔记就按我实际带过几届学生做这个题目的经验,把三条路线的原理、代码、参数和踩坑点讲清楚。适合正在做数据挖掘课程大作业的本科生,也适合想快速跑通一个中文情感分类 baseline 的工程师。读完你至少能拿到一套可复现的 Python 源码结构,知道每条路线的边界在哪。

2. 情感词典法:不训练模型也能跑出 70% 准确率的基线

2.1 词典法的核心逻辑与适用边界

情感词典法的思路非常朴素:一句话的情感极性,等于句中所有情感词的情感得分之和,再考虑否定词和程度副词的修饰。比如“这个产品非常好用”里,“好”得 +1,“非常”作为程度副词把权重放大到 1.5,最终得分 +1.5,判为正面。

它的优势在于零训练成本、可解释性强、冷启动快。你不需要标注数据,不需要 GPU,甚至不需要 sklearn。但边界也很明显:对反讽、隐喻、网络新词几乎无能为力。“这波操作真是绝了”到底算褒还是贬,词典法给不出稳定答案。所以它适合做 baseline,或者作为集成模型的一个特征来源,不适合作为最终方案单独交差。

常见的中文情感词典有知网 HowNet 情感词典、大连理工情感词汇本体库、BosonNLP 情感词典。课程大作业里我一般推荐用大连理工的,覆盖度高且格式规整。

2.2 用 Python 实现一个可用的词典法分类器

下面这段代码是我常用的最小实现,依赖 jieba 分词和一份情感词典文件。词典格式假设为每行“词语 极性强度”,正面为正数,负面为负数。

import jieba import re # 加载情感词典,格式:词语 强度值 def load_sentiment_dict(path): sentiment_dict = {} with open(path, 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split() if len(parts) == 2: word, score = parts[0], float(parts[1]) sentiment_dict[word] = score return sentiment_dict # 否定词和程度副词表 negation_words = {'不', '没', '无', '非', '莫', '弗', '毋', '未', '否', '别', '休'} degree_words = { '非常': 1.8, '极其': 2.0, '十分': 1.6, '特别': 1.7, '很': 1.5, '太': 1.6, '最': 2.0, '更': 1.3, '较': 1.2, '稍微': 0.8, '有点': 0.7, '有些': 0.7 } def score_sentence(text, sentiment_dict): words = list(jieba.cut(text)) total = 0.0 i = 0 while i < len(words): word = words[i] if word in sentiment_dict: score = sentiment_dict[word] # 向前看两个词,处理否定和程度修饰 for j in range(max(0, i-2), i): if words[j] in negation_words: score *= -1 if words[j] in degree_words: score *= degree_words[words[j]] total += score i += 1 return total def predict(text, sentiment_dict): score = score_sentence(text, sentiment_dict) if score > 0: return 1 # 正面 elif score < 0: return 0 # 负面 else: return 2 # 中性 if __name__ == '__main__': sd = load_sentiment_dict('sentiment_dict.txt') test_cases = [ '这个产品质量非常好,物流也很快', '完全不推荐,用了两天就坏了', '还行吧,没什么特别的感觉' ] for t in test_cases: print(t, '->', predict(t, sd))

逻辑说明:score_sentence遍历分词结果,遇到情感词时向前回溯两个位置,检查是否有否定词或程度副词。否定词翻转符号,程度副词按倍数放大。这个窗口设为 2 是经验值,设太大容易误判,设太小会漏掉“不是特别好”这类结构。

参数说明:degree_words里的倍数是可调的,不同词典和语料下最优值不同。我一般先用默认值跑一遍,看混淆矩阵里哪类错得多,再针对性调整。如果负面样本被大量误判为正面,说明否定词处理不够,可以把回溯窗口扩到 3。

2.3 词典法的评估与常见误判分析

跑完上面代码,在一个 5000 条的中文评论数据集上,词典法通常能拿到 65% 到 75% 的准确率。别小看这个数字,很多学生第一次跑深度学习模型也就 80% 出头,但花的时间是词典法的几十倍。

误判主要集中在三类:一是网络新词,比如“绝绝子”“yyds”,词典里没有;二是反讽句,“这服务真是好得让我想投诉”;三是长句中的情感转移,“虽然外观不错,但性能太差”,词典法会把“不错”和“差”简单相加,可能得出中性。针对第一类,可以在预处理阶段加一个新词发现模块;第二类目前没有低成本解法,只能靠模型;第三类可以通过分句后加权来缓解。

3. 传统机器学习:TF-IDF 加 SVM 的稳定组合

3.1 为什么 TF-IDF 加 SVM 仍然值得做

深度学习火了好几年,但在情感分类这个任务上,TF-IDF 加线性 SVM 依然是一个很难被大幅超越的强基线。尤其在数据量小于一万条时,SVM 的表现往往和 BERT 微调差不多,训练时间却只要几秒。课程大作业里,老师看的是你完整的实验对比,不是单纯刷高准确率。把 SVM 作为传统机器学习代表,和词典法、深度学习形成三档对比,逻辑上非常完整。

TF-IDF 的核心是给每个词算一个权重:这个词在当前文档里出现越多、在整个语料里出现越少,权重越高。SVM 则是在高维空间里找一个超平面,把正面和负面样本分开。两者结合,对中文情感分类这种中等维度、线性可分性较好的任务非常合适。

3.2 从原始文本到 TF-IDF 特征矩阵的完整流程

下面这段代码覆盖了从读取数据、分词、去停用词到生成 TF-IDF 矩阵的全过程。假设数据是 CSV 格式,两列:text和label,label 为 0 或 1。

import pandas as pd import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.svm import LinearSVC from sklearn.metrics import classification_report, accuracy_score # 读取数据 df = pd.read_csv('data.csv', encoding='utf-8') print('样本数:', len(df), '正负比例:', df['label'].value_counts().to_dict()) # 加载停用词 def load_stopwords(path): with open(path, 'r', encoding='utf-8') as f: return set([line.strip() for line in f if line.strip()]) stopwords = load_stopwords('stopwords.txt') # 分词并去停用词 def preprocess(text): words = jieba.cut(str(text)) return ' '.join([w for w in words if w not in stopwords and len(w) > 1]) df['seg'] = df['text'].apply(preprocess) # 划分训练集和测试集,stratify 保证标签比例一致 X_train, X_test, y_train, y_test = train_test_split( df['seg'], df['label'], test_size=0.2, random_state=42, stratify=df['label'] ) # TF-IDF 向量化 vectorizer = TfidfVectorizer( max_features=5000, # 保留权重最高的 5000 个词 ngram_range=(1, 2), # 同时考虑单字和双字组合 min_df=3, # 至少在 3 个文档中出现才保留 max_df=0.9 # 出现在超过 90% 文档中的词丢弃 ) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) # 训练 LinearSVC clf = LinearSVC(C=1.0, max_iter=2000, class_weight='balanced') clf.fit(X_train_vec, y_train) # 评估 y_pred = clf.predict(X_test_vec) print('准确率:', accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=['负面', '正面']))

逻辑说明:preprocess里过滤掉长度小于等于 1 的词,是因为中文单字很多是噪声,比如“的”“了”“是”。TfidfVectorizer的ngram_range=(1,2)让模型能捕捉“不 好”这种双词组合,对否定句有帮助。class_weight='balanced'在正负样本不均衡时自动调整权重,避免模型偏向多数类。

参数说明:max_features=5000是经验值,数据量小可以降到 3000,数据量大可以升到 10000。C是 SVM 的正则化参数,C 越大越容易过拟合,C 越小越容易欠拟合。我一般从 1.0 开始,在验证集上按 0.1、0.5、1.0、2.0 网格搜一下。min_df=3能过滤掉只出现一两次的稀有词,减少噪声。

3.3 参数调优与交叉验证的实操细节

上面代码跑完,在一个 10000 条左右的电商评论数据集上,准确率通常在 82% 到 88% 之间。想再往上提,重点调三个地方。

第一是分词粒度。jieba 默认模式会把“性价比高”切成“性价比”和“高”,但有些领域词需要自定义词典。比如“续航”在手机评论里是一个关键情感载体,如果被切碎,TF-IDF 就抓不到。可以在 jieba 加载时用jieba.load_userdict('userdict.txt')补充。

第二是 TF-IDF 的sublinear_tf参数。默认是 False,设为 True 后词频取对数,能抑制高频词的权重。在评论数据上我一般会打开,准确率通常有 0.5 到 1 个点的提升。

第三是交叉验证。不要只跑一次 train_test_split,用cross_val_score跑 5 折,看均值和方差。如果方差超过 2 个点,说明数据划分影响太大,需要检查是否有重复样本或标签泄漏。

4. 深度学习:用 BiLSTM 和 CNN 把准确率推到 90% 以上

4.1 深度学习方案选型:CNN、LSTM 还是预训练模型

到了深度学习这一档,选型就多了。课程大作业里常见的有三种:TextCNN、BiLSTM、BERT 微调。我的建议是,如果 GPU 资源有限或者时间紧,优先做 TextCNN 加 BiLSTM 的对比;如果追求最高准确率且能联网下载预训练权重,再上 BERT。

TextCNN 用不同尺寸的卷积核捕捉局部 n-gram 特征,训练快,参数少。BiLSTM 通过双向序列建模捕捉长距离依赖,对否定句和转折句更友好。BERT 微调效果最好,但需要 8G 以上显存,训练时间也长。课程大作业里,把 TextCNN 和 BiLSTM 都实现一遍,再和 SVM 对比,工作量已经足够。

4.2 用 PyTorch 搭建 TextCNN 和 BiLSTM 的代码模板

下面是一个统一的训练框架,通过model_type参数切换 TextCNN 和 BiLSTM。依赖 torch、numpy 和 sklearn。

import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader import numpy as np from collections import Counter # 构建词表 def build_vocab(texts, max_size=20000, min_freq=2): counter = Counter() for text in texts: counter.update(text.split()) vocab = {'<pad>': 0, '<unk>': 1} for word, freq in counter.most_common(max_size): if freq >= min_freq: vocab[word] = len(vocab) return vocab # 数据集类 class SentimentDataset(Dataset): def __init__(self, texts, labels, vocab, max_len=128): 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): words = self.texts[idx].split()[:self.max_len] ids = [self.vocab.get(w, 1) for w in words] ids = ids + [0] * (self.max_len - len(ids)) return torch.tensor(ids), torch.tensor(self.labels[idx]) # TextCNN 模型 class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=128, num_filters=100, filter_sizes=[2,3,4], num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embed_dim)) for fs in filter_sizes ]) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(num_filters * len(filter_sizes), num_classes) def forward(self, x): x = self.embedding(x) # [B, L, D] x = x.unsqueeze(1) # [B, 1, L, D] x = [torch.relu(conv(x)).squeeze(3) for conv in self.convs] x = [torch.max_pool1d(c, c.size(2)).squeeze(2) for c in x] x = torch.cat(x, dim=1) x = self.dropout(x) return self.fc(x) # BiLSTM 模型 class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=128, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=True) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): x = self.embedding(x) out, _ = self.lstm(x) out = torch.max_pool1d(out.transpose(1, 2), out.size(1)).squeeze(2) out = self.dropout(out) return self.fc(out) # 训练函数 def train_model(model, train_loader, val_loader, epochs=10, lr=1e-3): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) optimizer = optim.Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() best_acc = 0.0 for epoch in range(epochs): model.train() for ids, labels in train_loader: ids, labels = ids.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(ids), labels) loss.backward() optimizer.step() # 验证 model.eval() correct, total = 0, 0 with torch.no_grad(): for ids, labels in val_loader: ids, labels = ids.to(device), labels.to(device) preds = model(ids).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f'Epoch {epoch+1}, Val Acc: {acc:.4f}') if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'best_model.pt') return best_acc

逻辑说明:TextCNN 用三种尺寸的卷积核分别提取 2 元、3 元、4 元词组特征,再拼接后分类。BiLSTM 用双向 LSTM 编码序列,取每个时间步的最大值作为句子表示。两个模型都加了 dropout 防止过拟合。

参数说明:embed_dim=128是常见起点,数据量大可以加到 256。num_filters=100控制卷积核数量,太多容易过拟合。max_len=128覆盖大多数评论长度,超过的截断,不足的补零。学习率1e-3配合 Adam 是稳妥选择,如果 loss 震荡可以降到5e-4。

4.3 训练过程中的显存、过拟合与早停策略

深度学习部分最容易翻车的地方不是模型结构,而是训练过程。显存不够是最常见的,报错CUDA out of memory时,先把 batch_size 从 64 降到 32 或 16,再考虑缩短 max_len。如果还不行,检查是不是在验证阶段没有加torch.no_grad(),导致计算图一直累积。

过拟合的判断标准是训练集准确率持续上升但验证集准确率下降。这时候加 dropout 比例、加 L2 正则、或者直接早停。我一般设 patience=3,连续 3 个 epoch 验证集不提升就停。另外,BiLSTM 比 TextCNN 更容易过拟合,因为参数量更大,在小数据集上尤其明显。

还有一个玄学问题:同样的代码,不同随机种子跑出来的结果可能差 2 到 3 个点。解决办法是固定种子,并且用 3 个种子各跑一次取平均。虽然费时间,但报告里写出来的数字更可信。

5. 避坑与排查:三条路线各自最容易翻车的地方

5.1 词典法:否定词窗口和词典覆盖度

现象:句子“我不是很喜欢这个”被判为正面。原因:否定词“不”和情感词“喜欢”之间隔了“是”和“很”,如果回溯窗口只有 1,就抓不到否定关系。解决:把回溯窗口扩到 2 或 3,同时把“不是”作为一个整体否定词加入词表。

现象:大量网络评论被判为中性。原因:情感词典没有收录“绝绝子”“yyds”“踩雷”这类新词。解决:从训练集里统计情感词,人工补充进词典,或者用 TF-IDF 权重最高的词做候选,人工筛选后加入。

5.2 传统机器学习:标签泄漏和特征维度爆炸

现象:SVM 准确率异常高,达到 98% 以上。原因:预处理时把标签信息泄漏到了特征里,比如数据里混入了“正面”“负面”这样的词。解决:检查原始数据,把标签相关词加入停用词表,并且在划分训练测试集之后再 fit TF-IDF。

现象:训练时报错ValueError: X has N features, but LinearSVC is expecting M features。原因:对训练集和测试集分别 fit 了 TF-IDF,导致特征维度不一致。解决:只在训练集上 fit,测试集用 transform。

5.3 深度学习:分词器与预训练权重不匹配

现象:加载 BERT 时报错OSError: Can't load weights。原因:模型名称写错,或者本地缓存路径不对。解决:确认模型名称和 transformers 版本匹配,国内环境可以先下载到本地再指定路径。

现象:BiLSTM 训练 loss 不下降。原因:学习率太大导致梯度爆炸,或者词表太小导致大量词被映射为 unk。解决:把学习率降到 1e-4,检查词表大小,如果小于 5000 说明分词或过滤太激进。

5.4 数据层面:样本不均衡与重复样本

现象:模型把所有样本都预测为多数类,准确率看着不低但 F1 很差。原因:正负样本比例超过 5:1。解决:用class_weight='balanced',或者在深度学习里用 WeightedRandomSampler 重采样。

现象:验证集准确率波动极大。原因:数据里有重复样本,训练集和验证集出现了同样的文本。解决:在划分数据集之前先 drop_duplicates,并且用去重后的数据重新划分。

6. 把三路结果串成一张对比表:大作业报告怎么写才站得住

三条路线都跑通之后,最后一步是把结果整理成可对比的表格。这张表是你大作业报告的核心,也是老师判断你工作量和技术深度的直接依据。我一般会按下面这个结构来组织。

方案准确率F1 值训练时间依赖资源适用场景
情感词典法68%-75%0.65-0.72无需训练词典文件冷启动、baseline
TF-IDF + SVM82%-88%0.80-0.86秒级CPU中小数据集、快速迭代
TextCNN86%-90%0.85-0.89分钟级GPU 可选中等数据集、局部特征
BiLSTM88%-92%0.87-0.91分钟级GPU 推荐长文本、否定转折
BERT 微调92%-96%0.91-0.95小时级GPU 必需追求最高精度

表格里的数字是区间,不是固定值,因为不同数据集上差异很大。写报告时不要只写一个数字,要写“在 XX 数据集上,经过 5 折交叉验证,准确率为 XX±XX”。这样才显得专业。

还有一个技巧:把词典法的得分作为特征拼接到 TF-IDF 向量后面,再喂给 SVM。这个混合方案我试过多次,通常能在纯 SVM 基础上再提 1 到 2 个点。代码改动很小,在TfidfVectorizer输出后加一列词典得分即可。这个点写进报告里,能体现你对特征工程的理解,比单纯堆模型更有说服力。

最后说一个我自己的习惯:每次跑完实验,不管结果好坏,先把随机种子、参数配置、数据集版本记在一个 markdown 文件里。大作业周期通常两三周,中间隔几天再回来跑,没有记录根本复现不了当时的结果。这个习惯帮我省了无数次后悔药。希望帮到你。

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

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

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

立即咨询