ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

影评文本聚类实战:K-Means、层次聚类与DBSCAN对比及分类方法对照

影评文本聚类实战:K-Means、层次聚类与DBSCAN对比及分类方法对照

1. 影评文本聚类项目的整体设计思路

影评文本分类这件事,乍一看是个很典型的监督学习任务——有标签、有文本、有明确的正面负面之分。但实际做起来,你会发现一个很尴尬的问题:标注数据太贵了。尤其是当你想覆盖多个平台、多个语种、多个时间段的影评时,人工标注的成本几乎是个无底洞。所以我在这个项目里选择了一条更“野生”的路线:先用聚类把文本结构摸清楚,再拿分类方法做对照,看看无监督到底能做到什么程度。

这个项目的核心目标有三个。第一,验证聚类方法在影评情感分析场景下的可行性,也就是不依赖任何标签,能不能把好评和差评大致分开。第二,横向比较几种主流聚类算法——K-Means、层次聚类、DBSCAN——在这个特定任务上的表现差异。第三,把聚类结果和分类方法(比如朴素贝叶斯、逻辑回归、SVM)做对比,量化无监督与有监督之间的效果差距。说白了,就是想知道:如果我一分钱标注费都不花,能做到什么水平;如果花点钱做标注,又能提升多少。

适合谁来参考这份总结?如果你正在做文本挖掘的课程项目、想了解无监督方法在真实语料上的表现、或者正在纠结“要不要标注数据”这个问题,那这篇内容应该能帮你省下不少试错时间。我会把参数选择、特征工程、评估方式、踩过的坑都摊开讲,代码片段也会给到可以直接跑的程度。

注意:聚类本身不产生“标签”,它只产生“簇”。把簇映射到正面/负面,需要额外一步人工判断或启发式规则。这一点在项目设计阶段就要想清楚,否则后面评估会非常别扭。

2. 数据准备与文本预处理的关键细节

2.1 影评语料的获取与清洗

影评文本的来源可以很多样,公开数据集、爬取的评论、或者自己整理的语料库都行。我这次用的是混合来源,一部分是公开的中文影评数据集,一部分是模拟生成的短评。原始数据大概两万条左右,每条包含评论文本和可选的评分(评分只用于最后验证,不参与聚类)。

清洗这一步,很多人容易做得太粗暴。我见过有人直接去掉所有标点、数字、英文,结果把“8分”“10/10”“not bad”这种关键信息也干掉了。我的做法是分层次处理:

  • 第一层,去掉HTML标签、URL、@提及这些明显的噪声。
  • 第二层,保留中文、英文、数字,但把连续重复的标点压缩成一个,比如“好看!!!”变成“好看!”。
  • 第三层,不做停用词去除,而是把停用词表作为可选项,在特征工程阶段再决定要不要用。

为什么不在清洗阶段就去停用词?因为影评里“不”“没”“别”这些否定词一旦去掉,情感极性直接翻转。我试过在清洗阶段去掉停用词,结果“不好看”和“好看”在向量空间里几乎重合,聚类效果惨不忍睹。所以停用词处理一定要放在分词之后、向量化之前,并且要使用针对情感分析优化的停用词表,而不是通用的那一套。

2.2 中文分词与词性过滤的取舍

中文影评的分词工具,我对比过几款常用的。最终选的是jieba,原因很简单:它对网络新词和口语化表达的支持比较好,而且可以自定义词典。影评里经常出现“绝绝子”“yyds”“烂尾”这类词,通用分词器很容易切错,加自定义词典能明显改善。

分词之后要不要做词性过滤?我的经验是:不要一刀切。名词、动词、形容词当然要保留,但副词和部分语气词在情感分析里也有价值。比如“非常好看”和“好看”在情感强度上是有差异的。所以我最终保留了名词、动词、形容词、副词,去掉了纯虚词和标点符号。

这里有个细节:影评里经常出现“这部电影”“这部片子”这种高频但无区分度的短语。我的处理方式是在分词后,用TF-IDF的IDF值自动把这些词压下去,而不是手动维护一个黑名单。这样更通用,换一个语料库也不用重新调。

2.3 特征表示:TF-IDF还是词向量

特征表示这一步,我做了两组实验。一组用TF-IDF,一组用预训练词向量取平均。结论可能有点反直觉:在这个任务上,TF-IDF的表现并不比词向量差,甚至在某些聚类算法下更好。

原因在于影评的情感信号往往集中在少数关键词上,比如“烂”“惊艳”“拖沓”“催泪”。TF-IDF能把这些词的权重拉得很高,而词向量平均会把所有词的信息混在一起,反而稀释了关键信号。当然,如果你的语料里反讽、隐喻特别多,词向量可能会更有优势,因为它能捕捉一些语义组合。但就常规影评而言,TF-IDF加Bi-gram已经够用了。

我最终的特征配置是:TF-IDF,max_features=5000,ngram_range=(1,2),min_df=3,max_df=0.8。min_df=3是为了过滤掉只出现一两次的极低频词,max_df=0.8是为了压掉“电影”“好看”这种几乎每篇都有的词。这个配置不是拍脑袋定的,后面我会讲怎么用网格搜索来调。

3. 聚类算法的选型与参数调优实录

3.1 K-Means:简单但需要预处理

K-Means是我第一个试的算法,因为它快、好解释、调参直观。但K-Means有两个硬伤:一是需要预先指定簇数量K,二是对初始点敏感。影评情感分析里,你其实不知道到底该分几类——可能只有正面负面,也可能有“正面但批评演技”“负面但夸画面”这种细粒度类别。

我的做法是先用肘部法则和轮廓系数一起看。肘部法则看SSE的下降拐点,轮廓系数看簇内紧密度和簇间分离度。实际跑下来,K=2到K=5之间轮廓系数最高,但K=2的SSE下降已经很明显了。结合业务理解,我最终把K定在2到4之间做对比实验。

K-Means还有一个坑:它对特征的尺度很敏感。TF-IDF向量本身已经是归一化的,所以问题不大。但如果你用的是词频或者未归一化的向量,一定要先做标准化,否则高频词会主导距离计算。

from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score k_range = range(2, 8) sse = [] sil = [] for k in k_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(tfidf_matrix) sse.append(km.inertia_) sil.append(silhouette_score(tfidf_matrix, labels, sample_size=5000))

这段代码里,init='k-means++'是必须的,它比随机初始化稳定得多。n_init=10表示跑10次不同初始化取最好的,虽然慢一点但值得。sample_size=5000是因为轮廓系数在完整数据上计算太慢,抽样对趋势判断影响不大。

3.2 层次聚类:不用指定K但计算量大

层次聚类的优势是不用预先指定簇数量,你可以通过树状图来切。但它的计算复杂度是O(n^2)甚至更高,两万条数据跑起来已经有点吃力了。我试过在完整数据上跑,内存直接爆了,后来改成先随机采样5000条做层次聚类,观察簇的结构,再用K-Means在全量数据上复现。

层次聚类还有一个问题:一旦合并或分裂,就不能撤销。所以它对噪声和异常值很敏感。影评里那种“此评论已被删除”或者只有一两个字的短评,很容易把整个簇带偏。我的处理方式是先过滤掉长度小于5个词的评论,再做层次聚类。

从结果看,层次聚类在K=2时和K-Means的簇划分高度重合,但在K=3以上时,层次聚类倾向于产生一个超大簇加几个小簇,而K-Means的簇大小更均衡。这说明层次聚类对影评这种密度不均匀的数据,可能不是最优选择。

3.3 DBSCAN:能发现噪声但参数难调

DBSCAN最大的卖点是能自动发现簇数量,并且能识别噪声点。影评里确实有大量“水军”评论、复制粘贴的模板评论,这些在DBSCAN里会被标为噪声,理论上很美好。

但实际调参非常痛苦。DBSCAN有两个核心参数:eps(邻域半径)和min_samples(核心点的最小邻居数)。在TF-IDF的高维稀疏空间里,距离度量本身就有点失真,eps稍微变一点,结果就天差地别。我试过用k-距离图来辅助选eps,但影评数据的k-距离曲线没有明显的拐点,最后只能靠网格搜索。

更麻烦的是,DBSCAN在TF-IDF空间里经常把大部分点都标为噪声,或者把所有点都归到一个簇里。我试了十几组参数,最好的情况下也只有60%左右的点被分配到簇中,剩下40%都是噪声。这对于情感分析来说太浪费了,因为那些被标为噪声的评论里其实有很多明确的情感表达。

所以我的结论是:DBSCAN适合做数据清洗的辅助工具,用来发现异常评论,但不适合作为影评情感聚类的主力算法。

3.4 三种聚类方法的横向对比

算法优点缺点影评场景适用性
K-Means快、可扩展、簇大小均衡需指定K、对初始点敏感高,适合快速探索
层次聚类不需指定K、树状图可解释计算量大、对噪声敏感中,适合小规模采样分析
DBSCAN自动发现簇、识别噪声参数难调、高维空间失效低,适合异常检测辅助

这个表格是我跑完所有实验后的总结。如果你时间有限,直接上K-Means加k-means++初始化,配合轮廓系数选K,是最稳妥的路线。

4. 聚类效果评估与分类方法对照

4.1 没有标签怎么评估聚类效果

聚类评估最尴尬的地方就是没有真实标签。我用了三种策略来交叉验证。

第一种,内部指标:轮廓系数、Calinski-Harabasz指数、Davies-Bouldin指数。这三个指标不需要标签,但它们的绝对值没有直观意义,只能用来比较不同参数下的相对好坏。

第二种,外部指标:我保留了一部分带有评分的数据作为验证集。把评分>=4的视为正面,<=2的视为负面,3分的去掉。然后用聚类结果和这个二分类标签算调整兰德指数(ARI)和归一化互信息(NMI)。注意,这个标签不参与聚类,只用于评估。

第三种,人工抽查:从每个簇里随机抽20条评论,人工判断这个簇的整体情感倾向。这一步虽然费时间,但能发现一些指标看不出来的问题。比如有一次轮廓系数很高,但抽查发现两个簇都是负面的,只是一个骂演技一个骂剧情,这种细分对情感二分类来说就是失败的。

4.2 聚类与分类的效果差距量化

分类方法我选了三个:朴素贝叶斯、逻辑回归、线性SVM。用同样的TF-IDF特征,80%数据训练,20%测试。分类的评估直接用准确率、F1值,因为标签是明确的。

对比结果如下:

方法准确率F1值备注
K-Means (K=2)0.720.70簇到情感的映射用多数投票
层次聚类 (K=2)0.690.67采样5000条
DBSCAN0.580.55仅对非噪声点评估
朴素贝叶斯0.810.80有监督
逻辑回归0.850.84有监督
线性SVM0.860.85有监督

这个差距很有意思。最好的聚类方法(K-Means)比最差的分类方法(朴素贝叶斯)低了将近10个百分点。但别忘了,聚类完全没用标签,而分类用了80%的标注数据。如果标注成本很高,聚类作为一个冷启动方案,72%的准确率其实是可以接受的。

还有一个发现:聚类错误主要集中在3分评论和反讽评论上。3分评论本身情感就模糊,人都不一定分得清,更别说算法了。反讽评论比如“真是太好看了,我看睡了三次”,TF-IDF很难捕捉这种语义反转,分类方法也经常错,但词向量方法可能会好一些。

4.3 簇到情感的映射策略

聚类出来的是簇编号,不是情感标签。怎么映射?我试了三种方法。

第一种,多数投票:如果验证集中某个簇的正面评论占多数,就把这个簇标为正面。简单但依赖验证集的质量。

第二种,关键词打分:对每个簇提取TF-IDF最高的20个词,人工判断这些词的情感倾向,然后给簇打分。比如一个簇的高频词是“精彩”“推荐”“感动”,那大概率是正面。

第三种,半监督标签传播:用少量标注数据作为种子,通过簇内的相似度把标签传播到整个簇。这个方法在簇内纯度较高时效果很好,但如果簇内混杂,错误会被放大。

我最终用的是第一种加第二种的混合策略:先用多数投票做初步映射,再用关键词打分做人工校验。这样既能自动化,又能发现明显的错误。

5. 实操中遇到的典型问题与排查技巧

5.1 簇大小极度不均衡怎么办

这是最常见的问题。K-Means跑出来,一个簇有18000条,另一个簇只有2000条。这种情况通常意味着特征空间里存在一个“超级密集区”,把所有模糊的评论都吸进去了。

我的排查思路是:先看这个超大簇的关键词。如果关键词都是“电影”“好看”“一般”这种通用词,说明特征区分度不够。解决办法有两个:一是增加TF-IDF的max_df阈值,把通用词压得更狠;二是引入词性加权,给形容词和副词更高的权重。

还有一个可能是K值选小了。有时候K=2确实只能分出“明显正面”和“其他”,把K加到3或4,那个超大簇就会裂开。所以不要死守肘部法则,多试几个K,结合业务理解来判断。

5.2 否定词和反讽评论的处理

否定词的处理我在前面提过,这里展开讲。中文里的否定结构很灵活,“不”“没”“别”“无”“非”都可以表示否定,而且还有“不得不”“不能不”这种双重否定。我的做法是在分词后,用规则匹配把“不+形容词”“没+动词”这种组合合并成一个token。比如“不好看”合并成“不好看_neg”,这样TF-IDF就能把它和“好看”区分开。

反讽更难。规则方法基本无效,只能靠模型。我试过在特征里加入标点符号的统计特征,比如感叹号数量、问号数量、省略号数量。反讽评论往往有更多的问号和省略号。这个特征加进去后,反讽的召回率提升了大概5个百分点,但准确率没怎么变,说明还是有大量反讽没被识别出来。

提示:如果你的影评数据里反讽比例很高,建议直接上预训练语言模型做微调,传统TF-IDF加聚类的路线天花板比较明显。

5.3 聚类结果不稳定的应对

K-Means每次跑出来的结果可能不一样,即使设置了random_state,换一个数据子集结果也会变。这种不稳定性在向别人汇报时很尴尬。

我的应对策略是:跑20次不同初始化,取ARI最高的那次作为最终结果,同时记录其他次的结果,看看簇的划分是否稳定。如果20次里有15次以上都产生了相似的簇结构,那说明这个结构是稳健的。如果每次都不一样,那说明数据本身没有明显的簇结构,这时候强行聚类就是自欺欺人。

还有一个技巧是用共识聚类:把多次聚类的共现矩阵作为新的特征,再跑一次层次聚类。这个方法能显著提升稳定性,但计算量会翻倍。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
簇大小极度不均衡特征区分度不够或K太小查看超大簇关键词调max_df、增加K、词性加权
轮廓系数高但人工抽查差簇内细分而非情感区分抽样人工判断调整K或换特征
DBSCAN大量噪声eps太小或高维失效k-距离图降维后聚类或换算法
结果每次不一样初始化敏感或数据无结构多次跑记录ARI共识聚类或固定初始化
反讽识别差TF-IDF无法捕捉语义反转检查错误样本加入标点特征或换模型

6. 特征工程与参数调优的进阶技巧

6.1 用网格搜索找TF-IDF的最优配置

TF-IDF的参数不少,手动调很容易顾此失彼。我写了一个简单的网格搜索,用轮廓系数作为评分标准。注意,这里不能用分类的准确率来调,因为聚类阶段没有标签。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import itertools param_grid = { 'max_features': [3000, 5000, 8000], 'ngram_range': [(1,1), (1,2)], 'min_df': [2, 3, 5], 'max_df': [0.7, 0.8, 0.9] } best_score = -1 best_params = {} for params in itertools.product(*param_grid.values()): config = dict(zip(param_grid.keys(), params)) vec = TfidfVectorizer(**config) X = vec.fit_transform(corpus) km = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X) score = silhouette_score(X, labels, sample_size=5000) if score > best_score: best_score = score best_params = config

这个搜索跑了大概半小时,最终最优配置是max_features=5000,ngram_range=(1,2),min_df=3,max_df=0.8。和我一开始拍脑袋定的差不多,但至少现在有数据支撑了。

6.2 降维对聚类的影响

TF-IDF矩阵通常是高维稀疏的,5000个特征对两万条数据来说,维度比样本还多。这种情况下距离度量会失真,聚类效果会下降。我试了两种降维方法:TruncatedSVD和UMAP。

TruncatedSVD(也就是LSA)把5000维降到100-300维,轮廓系数提升了大概0.05,而且跑得快了很多。UMAP降维效果更好,但计算慢,而且参数敏感。最终我选了TruncatedSVD,n_components=200,作为一个性价比很高的预处理步骤。

from sklearn.decomposition import TruncatedSVD svd = TruncatedSVD(n_components=200, random_state=42) X_reduced = svd.fit_transform(tfidf_matrix)

注意,降维之后的数据不再稀疏,K-Means跑起来会快很多,但DBSCAN在降维后的稠密空间里反而可能表现更好,因为距离度量更准确了。我试过在SVD降维后跑DBSCAN,噪声比例从40%降到了25%,但还是不如K-Means。

6.3 聚类数量的业务判断

纯靠指标选K是不够的。我最后选K=3而不是K=2,是因为业务上需要区分“正面”“负面”“中性/混合”。K=2时,中性评论会被强行分到正面或负面,导致两个簇的纯度都不高。K=3时,中性评论单独成簇,正面和负面的区分度明显提升。

这个判断需要你对业务有理解。如果你只是想做二分类,K=2就够了。但如果你想做更细粒度的分析,比如“正面但批评演技”“负面但夸画面”,那K可能需要5以上。但K太大也会导致簇内样本太少,统计意义下降。我的经验是,影评情感分析里K=3到K=5是比较合理的范围。

7. 项目复现的完整流程与代码框架

7.1 从原始文本到聚类结果的流水线

整个流程我整理成了一个可复现的流水线,按顺序跑就行:

  1. 加载原始影评数据,过滤掉长度小于5的评论。
  2. 用jieba分词,加载自定义词典,保留名词、动词、形容词、副词。
  3. 用TF-IDF向量化,参数用网格搜索的最优配置。
  4. 用TruncatedSVD降维到200维。
  5. 用K-Means聚类,K=3,init='k-means++',n_init=20。
  6. 用轮廓系数和Calinski-Harabasz指数评估。
  7. 用验证集算ARI和NMI。
  8. 对每个簇提取Top-20关键词,人工校验情感倾向。
  9. 和分类方法的结果做对比,输出对比表格。

这个流水线我跑一次大概15分钟,主要时间花在TF-IDF和K-Means上。如果数据量更大,可以考虑用MiniBatchKMeans替代KMeans,速度快很多,效果差距在1-2个百分点以内。

7.2 分类方法的对照实验设置

分类方法的实验设置要和聚类保持一致,否则对比不公平。我用同样的TF-IDF特征,同样的训练测试划分(80/20),同样的随机种子。分类器用scikit-learn的默认参数,不做额外调优,因为目的是对比“有无监督”的差距,而不是追求分类的最优性能。

如果你想让分类效果更好,可以试试点互信息做特征选择,或者用LightGBM。但那样对比就变成了“精心调优的分类”vs“默认参数的聚类”,差距会更大,参考意义反而下降。

7.3 结果可视化的几种方式

聚类结果可视化我用了两种方式。一种是t-SNE把高维数据降到二维,用不同颜色标出簇编号,直观看看簇的分离程度。另一种是词云,每个簇生成一个词云,快速了解这个簇的主题。

t-SNE的perplexity参数对可视化影响很大,我试了5、30、50,最终选了30,因为它在局部结构和全局结构之间比较平衡。词云的话,注意去掉那些在所有簇里都高频的词,否则每个词云看起来都差不多。

注意:可视化只是辅助理解,不要用可视化的分离程度来替代定量评估。t-SNE有时候会制造虚假的簇分离,看起来很美但实际指标很差。

8. 个人实操体会与后续扩展方向

这个项目做下来,我最大的体会是:聚类在文本分析里不是分类的替代品,而是探索工具。它能帮你快速了解语料的结构,发现一些你没想到的类别,比如“正面但吐槽结局”“负面但夸配乐”这种细分。但如果你要的是一个能直接用的情感分类器,聚类72%的准确率还是不够看,该标注还是得标注。

另一个体会是,特征工程比算法选择更重要。我试过换各种聚类算法,效果差异最多5个百分点。但把TF-IDF的max_df从0.9调到0.8,轮廓系数就提升了0.08。所以时间应该花在特征上,而不是反复换算法。

后续如果想继续深入,有几个方向可以试。一是用预训练语言模型提取句向量,再聚类,看看能不能捕捉反讽和隐喻。二是做半监督聚类,用少量标签引导聚类过程,比如COP-KMeans。三是把聚类结果作为特征输入到分类器里,做 stacking 集成。这些方向我还没时间跑,但理论上都有提升空间。

最后分享一个小技巧:如果你的影评数据里短文本特别多,TF-IDF的min_df不要设太高,否则很多词会被过滤掉,导致向量过于稀疏。我试过min_df=5,结果很多短评的向量几乎全零,聚类直接失效。min_df=2或3是比较安全的范围。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进