☰
机器学习文本分类实战:影评多分类情感分析全流程解析
2026/10/22 6:15:49 网站建设 项目流程

做机器学习项目,最怕一上来就调参跑模型。这个“机器学习-文本处理之电影评论多分类情感分析”项目,名字看着像入门练手,实际做下来才会发现,真正让人卡壳的从来不是算法,而是从原始文本到可用特征之间那段琐碎的活。电影评论情感分析是文本分类里非常典型的任务:数据量够、标注来源现成、业务判断直观,但它又不是单纯把句子分成“好评”和“差评”那么简单。多分类这个限定词一出现,评估方式、模型选择、预处理策略全部要跟着调整。

这篇文章不是把代码贴一遍就完事,而是把我在这个项目里踩过的坑、试过的方案、最后留用的配置都整理出来。无论是刚接触NLP的学生,还是工作中需要快速落地文本分类的开发者,都可以照着这份流程复现,同时理解每一步为什么要这么做。

1. 任务拆解与方案选型

1.1 从二分类到多分类,难在哪

市面上大部分情感分析教程都是二分类:正面或负面。看起来好像只要把模型换成能输出多个类别就能解决问题,但实际操作后会发现,二分类跑得很好的流程,搬到多分类上经常全线崩盘。

多分类情感分析首先面对的是类别的边界问题。二分类可以把任何一句中性表达硬塞进“正面”或“负面”,反正没有第三种答案,模型犯错的余地反而小。多分类要求模型在一段“还行”“一般般”“不算太差但也没什么亮点”这种含糊评论里,判断出它属于哪个等级,这时文本里很多看似中性的词就会变成干扰信号。

其次是数据分布的挑战。用户的打分习惯决定了各类别天然不均衡。某部现象级大片上映后,评论区可能70%都是高分好评,真正中立或批评的声音只占一小部分。模型如果不做任何处理,直接追求准确率,很容易把所有样本都猜成多数类,得到一堆虚高的指标,真正上线做预测时对差评和中性评论完全失效。

还有一个容易忽略的问题:情感类别的表达密度不一样。正面评价在中文里常常集中出现“好看”“惊艳”“演技炸裂”“特效满分”这类高频词,很容易被捕捉;而负面评价经常带有反讽,比如“这部电影真是太‘精彩’了,精彩到我中途睡了三觉”,这种表达如果预处理时不保留引号,或者分词把“精彩”单独拎出来,模型几乎必错。

1.2 多分类的标签体系怎么定

做多分类,第一件事不是选模型,而是定义标签。不同的标签划分方式,直接决定了整个任务的学习难度。

这个项目我采用的是四分类方案,把原始影评星级映射成四个情感等级:

原始星级标签定义类别含义
4~5星A类强烈推荐,明显正面
3星B类中规中矩,略偏正面
2星C类小有失望,略偏负面
1星D类强烈不推荐,明显负面

这样划分的理由是尽量保留原文的区分度。如果按三分类把3星并入中性,2星以下并入负面,虽然任务简单了,但模型中性和负面之间的区分会变得很弱,后面做误差分析时很难解释。四分类更接近真实业务里“评分预测”的粗粒度版本,也更能体现多分类文本处理的复杂度。

标签确定后,要把多分类任务的目标函数也定下来。我选的是“宏平均F1”,不是准确率。原因后面会展开,简单说就是在类别不均衡的场景下,宏平均F1能把每个类别都当一回事,不会让多数类掩盖少数类的表现。

1.3 为什么第一版别碰深度学习模型

很多读者看到“电影评论情感分析”会直接想到BERT、LSTM,觉得效果肯定秒杀传统模型。这个项目我特意限定在“机器学习”语境下,原因是传统模型能让你把文本处理的每一环看得清清楚楚。

先不说BERT的推理成本,单是数据量就撑不住。手头几万条短评论,训练一个微调的预训练模型,效果往往只比调好的传统模型高一两个点,但训练时间和资源消耗是几十倍。传统方法里的TF-IDF、朴素贝叶斯、线性模型,每一步都有明确的数学含义,哪一类文本分错了,可以从特征权重倒推原因,这种可解释性在做项目复盘时极其宝贵。

等到整个传统流程跑通,把准确率、F1这些指标提到一个稳定的水平,再去考虑深度学习也不迟。这也是我建议所有初学NLP的人走的路线:先用小模型把问题吃透,再用大模型去提上限。

2. 数据准备与预处理实战

2.1 数据从哪里来,怎么最省事

这个项目的数据来源通常有两种:一是公开的开源标注语料,二是从影评社区抓取的结构化评论。我这次用的是公开英文影评语料的增量中文版,也就是把原始评论翻译成中文后,仍然使用原评分作为标签。这样做的好处是标签噪声可控,不需要自己找人标注。

如果完全没有现成数据,也可以写爬虫从某中文影评社区抓短评,但要注意抓下来的是评分、评论内容、点赞数等字段,需要自己清理出文本和标签两列。个人经验是:短评比长评更适合这个任务。长评动辄上千字,里面情感复杂、铺垫很长,预处理后噪声多;短评通常两三句话,观点集中,更适合用来学习分类。数据量在2万条以上就比较舒服了,太少的话四分类的少数类可能只有两三千条,模型很难学。

拿到数据后先做分层抽样。用train_test_split时一定要加stratify参数,按照标签分布原比例切分训练集和测试集。这个细节很多人忽略,导致训练集和测试集的类别比例不一致,最终评估指标失真。

2.2 文本清洗的正确打开方式

影评文本不是干净的学术论文,里面堆满了各种噪音:HTML标签、URL、表情符号、多余的空白、其他语言的字符。预处理的原则很简单:保留对情感判断有意义的信号,去掉纯噪音。

我的清洗顺序是:

  1. 去除HTML标签,用正则或解析库把<br>这类标签直接抹掉
  2. 统一把URL替换成空字符串,影评里的链接对情感判断没有贡献
  3. 去除零散的英文字母和数字,比如“第5排看IMAX”里的具体数字往往不是情感信号
  4. 把全角标点转成半角,方便后续分词
  5. 去掉过短的评论,比如字数少于5的样本,这种数据基本没有判断价值

有一个需要特别小心的点:不要粗暴删除所有标点。中文文本里“这部电影不怎么样!”的感叹号、“这个片???”里的问号,本身就在传递情绪。我建议保留中文标点,只去掉连续多个标点中的多余部分,让标点符号作为独立特征进入后面的向量化。

清洗结束后,做一个最基础的数据核验:统计每条评论去重后的长度分布,太短的要过滤,太长的要截断。截断策略我放在预处理函数的末尾,统一把评论限制在200个汉字以内,超过的直接切掉。理由很简单,四分类的情感判断依赖的是整体倾向,而不是一头一尾的细节,保留前200字足够抓住核心情绪。

2.3 分词、停用词与自定义词典

中文文本处理绕不开分词。电影评论里大量出现人名、片名、专业术语,比如“诺兰”“漫威”“蒙太奇”“叙事结构”,这些词用通用分词器很可能被切得七零八落。

这个项目我直接用的jieba分词,但加了两个关键步骤:加载自定义词典和调整停用词表。自定义词典里把常见的导演名、演员名、片名、电影类型词都加了进去,保证“科幻片”不会散成“科幻”和“片”。分词后的效果直接影响后面TF-IDF的维度,词切错了,几个重要特征就都废了。

停用词表我没有直接用网上下载的通用版本,而是基于原始数据自己统计了一遍。方法是把所有文本切词后排序,手动翻一遍最高频的150个词,把“我们”“这个”“怎么”“一个”这类无意义词挑出来加入停用词表。但这里有个坑:有些词看着像停用词,在特定语境下其实有情感信号,比如“什么鬼”“不是吧”里的“什么”和“不是”,如果简单放进停用词表,负面情感特征就没了。所以我的规则是:低频虚词可以放心删,高频实词尤其是动词和形容词,一律保留。

最终预处理函数可以封装成这样一个流程:

import jieba import re def clean_text(text): text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'http\S+', '', text) text = re.sub(r'[a-zA-Z0-9]', '', text) text = re.sub(r'[\s\u3000]+', '', text) return text.strip() def cut_text(text, user_words=None, stopwords=None): if user_words: jieba.load_userdict(user_words) words = jieba.cut(clean_text(text)) return [w for w in words if w not in stopwords and len(w) > 1]

分词函数单独列出来,后面做特征工程时直接调用。

3. 特征工程:让文本变成向量

3.1 词袋模型和TF-IDF,到底在算什么

文本本身是离散符号序列,机器学习模型无法直接处理。特征工程的作用就是把一条评论转换成定长数值向量。最朴素的做法是词袋模型:统计每个词在评论中出现的次数,得到“这个词出现几次”的计数向量。

词袋模型有个直观的问题:所有词被平等对待。“电影”在一篇影评里出现10次,“演技”出现5次,模型会认为前者更重要,但做情感判断时明显是“演技”更关键。TF-IDF就是来解决这个问题的。

TF部分统计词在当前评论中的词频,IDF部分统计词在整个语料中的逆文档频率。一个词的IDF计算方法通常是:

IDF = log((N + 1) / (DF + 1)) + 1

其中N是总样本数,DF是包含该词的文档数。电影这个词几乎每条评论都有,DF很大,IDF趋近于0,权重被压得很低。相反,“惊艳”“失望”“拖沓”这些词只在部分评论里出现,IDF较大,权重得到保留。

我用一个具体的例子来说明。假设语料有2万条评论,其中“电影”出现在1.8万条里,“演技”出现在3000条里,那么“电影”的IDF大约是log(20001/18001)=0.105,“演技”的IDF大约是log(20001/3001)=1.897。再乘上各自的TF,“演技”的最终特征值会明显盖过“电影”。这一降一升,就是TF-IDF对高频虚词和情感特征词的重分配。

3.2 特征参数配置的实测心得

在TfidfVectorizer里,我用了这样一组参数:

from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer( max_features=10000, ngram_range=(1, 2), min_df=2, sublinear_tf=True ) X = tfidf.fit_transform(corpus)

每个参数背后都有原因。max_features=10000是把维度限制在1万维以内,避免出现几十万维的超稀疏矩阵,既降低训练耗时,也减少过拟合风险。选1万而不是2万,我用的是先跑一个全量特征版本,看特征重要性排序,发现前1万个词已经覆盖了绝大多数情感词,再多加维度收益很小。

ngram_range=(1, 2)是这里最值得说道的配置。只使用单字词时,“演技在线”和“演技不在线”都会被拆成“演技”“在线”“不在”,情感信息被打散。引入二元词对(bigram)后,“演技在线”“剧情拖沓”“特效爆炸”这样的短语会作为一个完整特征出现,对四分类的判别力提升非常明显。我跑过一个对比实验:只用一元词,测试集宏F1约0.72;加上二元词对,宏F1直接到0.79,提升接近7个百分点。

min_df=2要求一个词至少在2篇文档里出现过才保留,用来过滤只在某一条评论里出现的怪词。sublinear_tf=True则将对TF取对数,避免某个词在一篇长影评里被反复堆叠导致权重虚高。

3.3 为什么这个项目不推荐直接套词向量

现在提到文本特征,很多人第一反应是Word2Vec、GloVe之类的词向量。这类方法确实能捕捉词与词之间的语义相似性,但放在四分类情感分析里有一个实际问题:电影评论的情感判断高度依赖具体词汇,而不是抽象的语义距离。

“演技”和“表演”词向量距离很近,它们确实在语义上相关,但在这个任务里它们各自关联着不同的情感倾向,模型需要的是把这两个词拆开去学习各自的判别力。TF-IDF直接保留词的区分信息,词向量则把词映射到低维语义空间,信息被压缩了。

如果强行把所有词平均成一条句向量,更大的问题是丢失词序和否定关系。词向量的方法在学习“不”这个否定词时天然吃亏,而TF-IDF的bigram“不太好”“不推荐”能原样保留否定结构。因此在这个规模的数据集上,传统特征不仅效果不输词向量,而且训练速度更快,可解释性更强。

4. 模型训练与调参实测

4.1 先跑一个弱基线,再谈优化

我见过太多人直接上最强模型,跑出来不错的指标就以为成功,其实连“随机猜”的底都没摸清。做多分类项目,第一步永远是建立一个弱基线,通常是“预测所有样本为多数类”。

四分类任务里如果A类占40%,盲目全猜A类也能拿到40%准确率。这个数字可能比很多没调优的模型还高,但它毫无意义。我在项目里用的是DummyClassifier配合strategy='most_frequent'来跑一遍,得到基线准确率约40%,宏F1大概只有0.16。后续模型如果宏F1低于这个基线太多,肯定哪里出问题了。

随后还不急着上复杂模型,先跑一个用默认参数的逻辑回归。不为别的,就为了验证特征工程链路有没有打通。如果默认逻辑回归的指标明显高于基线,说明特征里已经有足够信息,接下来调模型才有意义;如果默认参数表现也很差,先回头检查预处理和向量化参数,不要盲目增加模型复杂度。

4.2 三个模型横向对比

这个项目里我重点测了三个经典的分类器:朴素贝叶斯(MultinomialNB)、逻辑回归(LogisticRegression)、线性支持向量机(LinearSVC)。它们对高维稀疏文本特征都有天然适配性,但行为习惯差异很大。

模型优势劣势适配场景
MultinomialNB训练极快,小样本也能出效果特征独立性假设强,忽略词间关系快速验证、有朴素贝叶斯先验的场景
LogisticRegression概率输出可解释,正则参数C可调粒度好高维特征下需要调C,否则易欠拟合或过拟合需要分析类别概率、可解释性要求高的场景
LinearSVC大间隔决策,高维稀疏表现稳,通常准确率最高输出没有自然概率,需要额外封装追求最高准确率、不需要概率解释的场景

实际测试结果是LinearSVC的宏F1最高,达到0.81;逻辑回归紧随其后,约0.79;朴素贝叶斯表现也不错,0.75,但它在处理类别不平衡时略微吃亏。

有个使用细节需要说明:MultinomialNB要求特征值非负,TF-IDF的数值也满足这个条件,可以直接用。LinearSVC每次训练前先做一次特征归一化到[-1,1]区间,虽然TF-IDF稀疏矩阵本身数值范围不大,但归一化后收敛更快、波动更小。逻辑回归不需要归一化也能正常跑,因为它在稀疏矩阵上用的是对偶优化。

4.3 训练代码的完整骨架

训练部分,我采用的是训练集内部再划分出一部分作为验证集,用GridSearchCV做交叉验证调参。关键代码如下:

from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.svm import LinearSVC from sklearn.pipeline import Pipeline X_train, X_test, y_train, y_test = train_test_split( df['text'], df['label'], test_size=0.2, random_state=42, stratify=df['label'] ) pipe = Pipeline([ ('tfidf', TfidfVectorizer(max_features=10000, ngram_range=(1, 2), min_df=2, sublinear_tf=True)), ('svc', LinearSVC()) ]) param_grid = { 'svc__C': [0.1, 0.5, 1.0, 2.0, 5.0], 'svc__class_weight': [None, 'balanced'] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring='f1_macro', n_jobs=-1) grid.fit(X_train, y_train)

这里有两个细节值得反复强调。第一个是stratify参数,它保证训练集和测试集的类别比例和原始数据保持一致;第二个是交叉验证的评分标准,我特意用了scoring='f1_macro'而不是默认的准确率。如果评分标准选错,调参会把模型引向“多猜A类”的方向,宏F1反而不高。

交叉验证跑完,最优C值落在1.0,class_weight='balanced'更优。class_weight='balanced'会按照类别频率反比调整各类别惩罚权重,多数类每分错一次代价小,少数类每分错一次代价大,这直接从损失函数层面缓解了类别不均衡的问题。

4.4 调参过程中的三个直观发现

第一,C值过大时会崩。LinearSVC的C值从1.0调到10.0后,训练集宏F1高达0.95,测试集却降到0.76,这是典型的过拟合信号。C值控制的是误分类的惩罚力度,C越大模型越追求把所有训练样本分对,在高维稀疏特征下特别容易记住噪声。

第二,class_weight='balanced'对少数类提升明显。特别是D类(强烈差评),单独看D类的F1从0.58提到0.74,而A类只降了1个百分点。如果业务上更关心少数类,这个参数基本是必开的。

第三,逻辑回归和LinearSVC的调参思路完全不一样。逻辑回归主要调C,还顺带调整penalty是L1还是L2;LinearSVC则只有一个C值可以调,参数空间小得多,更容易寻优。

5. 评估分析与问题排查实录

5.1 多分类评估指标,别只用准确率

多分类的评估指标和二分类有本质区别。准确率只看整体分对的比例,但四分类场景下,如果A类占40%,模型把全部样本都猜成A类,准确率也能到40%,这个结果毫无价值。

我最终用了三套指标组合:准确率、宏平均F1、加权平均F1。宏平均F1先分别计算每个类别的F1,再对所有类别取平均,少数类和多数类在最终数值里的权重一样。加权平均F1则是按各类别样本占比加权,数值会比宏平均高一些。两者结合看,如果宏平均F1明显低于加权平均F1,说明少数类被牺牲了。

这个项目最终结果大致如下:

准确率: 0.81 宏平均F1: 0.79 加权平均F1: 0.81

宏平均F1和加权平均F1的差只有0.02,说明模型并没有为了A类牺牲D类,类别间表现比较均衡。如果差值超过0.1,就要优先处理类别不平衡问题。

5.2 混淆矩阵里的细读

只看指标数字还不够,我习惯把测试集的混淆矩阵打印出来,一格一格看错在哪里。模拟测试结果如下:

实际\预测A类B类C类D类
A类17201304416
B类19054021030
C类6022039090
D类2540130445

从这张表能读出很多有意思的信息。A类被误判的样本主要集中在B类,说明“强烈推荐”和“比较推荐”之间的边界确实很难分;D类很少被误判成A类,模型对明显的差评抓得比较准;最混乱的是B类和C类之间,互相窜动非常频繁,这也符合直觉——大量评论本身表达的就是一种“还行但也没那么好”的模糊态度。

针对B/C类混淆严重的问题,我去翻了一批具体错例,发现不少是短评论,比如“还行”“一般吧”“没什么感觉”。这类文本信息量过少,再强的模型也没办法从两个词里准确判断它是偏正面还是偏负面。解决办法有两个方向:一是扩大ngram范围,让“还行吧”“一般得很”成为独立特征;二是对短评论做样本加权,让模型在遇到信息量不足的输入时不要强行判断。

5.3 常见问题速查表

这节整理一下项目过程中最常遇见的几个问题,基本覆盖了文本分类新手踩坑的80%场景:

现象主要原因解决方案
测试集准确率虚高,一切被预测为多数类切分测试集没做分层抽样,或类别严重不均衡切分时加stratify,分类器开class_weight='balanced'
模型训练集F1很高,测试集F1很低特征维度过大,正则系数C过大,或ngram引入过多噪声压缩max_features,降低C值,检查ngram_range
负面评论被大量预测成中性评论反讽、否定词没被模型捕捉保留标点符号,引入bigram特征,考虑额外抽取否定词特征
高频虚词干扰严重,混入主要特征停用词表过小,或直接把情感实词误删自己基于词频手工补充停用词表,且不删形容词和动词
中文专有名词被切成碎片未加载自定义词典分词前用jieba.load_userdict加载电影相关词表

还有一个容易被忽略的点:测试集的文本预处理和训练集必须完全一致。我见过有人训练时用的是jieba.cut后的词序列,预测时却直接对原始字符串做TF-IDF变换,结果特征空间对不上,预测结果完全失控。生产环境里要把清洗、分词、向量化整个流程包成同一个Pipeline,不要中途手写步骤。

5.4 调阈值这个隐藏大招

很多教程写到模型评估就收了,其实多分类还能再往前推一步。LinearSVC本身不输出概率,但可以用decision_function得到每个类别的决策分数。这个分数的绝对值能反映模型对当前样本判别的置信度。

我加了一个简单的阈值控制逻辑:当所有类别的最高分和次高分差异不大时,说明模型在几个类之间摇摆,这时宁可预测为B类(相对中性的类别),也不强行预测极端类别。通过这种方式,D类和A类的误判进一步减少,虽然整体准确率降了0.5%,但避免了极端情感判断翻车。在很多业务场景里,把一条强烈差评预测成中性,比把中性预测成强烈差评更危险,所以这种调整是有意义的。

多分类情感分析做到这一步,基本已经到了传统机器学习方法的上限。如果想继续提升,下一步可以考虑用LightGBM这类树模型把TF-IDF特征和大模型特征拼起来,或者换成预训练模型做迁移学习。但在走向那个方向之前,把预处理、特征、模型、评估这一整个链路跑通,才是这个项目真正的价值。至少我做完之后一个特别明确的体会是:文本情感边界本身是模糊的,四分类的难点不在于模型选得多高级,而在于能不能理解每一类样本的真实差异。把混淆矩阵一张张翻过去,你会比模型更了解这批评论。

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

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

立即咨询