☰
影评文本聚类实战:K-Means、层次聚类与DBSCAN对比及与分类方法对照
2026/10/17 10:58:15 网站建设 项目流程

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

影评文本分类这件事,乍一看是个很典型的监督学习任务:有标签、有文本、有明确的正面/负面类别,直接上朴素贝叶斯或者逻辑回归就能跑出一个不错的基线。但这次我刻意绕开了监督学习的路子,选择用无监督聚类来做,原因其实很实在——真实场景里,标注数据永远是稀缺资源。你拿到的可能只是几万条从平台上爬下来的原始评论,没有任何情感标签,这时候聚类就是第一把能用的刀。

这个项目的核心目标有三个层次。第一层是跑通流程:把影评文本从原始字符串变成聚类算法能吃的数值特征,然后完成聚类。第二层是横向比较:K-Means、层次聚类、DBSCAN这几种主流聚类方法,在同一个影评数据集上到底谁表现更好,各自的脾气是什么。第三层是纵向对照:聚类这种无监督方法,和朴素贝叶斯、SVM这类有监督分类方法比,效果差距有多大,差距体现在哪里。

为什么选影评文本作为实验对象?因为它有几个天然适合做聚类实验的特性。文本长度适中,不像长文档那样稀疏到爆炸,也不像短文本那样信息量太少;情感极性相对明确,正面和负面的用词差异明显,聚类算法有机会捕捉到这种差异;同时影评里存在大量中性、混合情感的样本,这恰好能考验不同聚类方法对噪声和边界样本的处理能力。

整个方案的设计逻辑是这样的:先用TF-IDF把文本向量化,这是文本聚类最经典也最稳妥的特征工程方案。相比词袋模型,TF-IDF通过逆文档频率压制了高频无意义词的影响;相比Word2Vec这类词嵌入,TF-IDF不需要预训练、不需要大量语料、可解释性强,对于影评这种领域相对固定的文本来说完全够用。然后分别跑K-Means、层次聚类和DBSCAN,用轮廓系数、调整兰德指数等指标做定量比较。最后把聚类结果和分类结果放在同一张表里对照,看看无监督方法到底能逼近有监督方法到什么程度。

提示:聚类做文本分类时,最大的坑在于簇的语义和真实类别不是一一对应的。K-Means聚出2个簇,不代表一个簇就是正面、另一个就是负面,需要人工检查簇内样本来确定语义映射。

2. 文本预处理与特征工程的核心细节

2.1 影评文本清洗的实操要点

影评文本的脏数据比想象中多。从平台上直接拿到的评论,里面混着HTML标签、表情符号、URL链接、重复标点、全角半角混杂的问题。我一般按这个顺序清洗:先去HTML标签,用正则把<.*?>全部干掉;然后处理URL,把http开头的东西替换成空格;接着统一标点,把连续三个以上的感叹号、问号压缩成一个;最后处理全角字符,用unicodedata库做标准化。

英文影评还要额外做小写化和词形还原。这里有个经验:词形还原(lemmatization)比词干提取(stemming)更适合影评场景。因为影评里有很多情感形容词,比如"amazing"和"amazed",词干提取会把它们都砍成"amaz",虽然能合并特征,但丢失了词性信息;词形还原会保留"amazing"作为形容词、"amazed"作为动词的区分,对情感分析更友好。

中文影评的处理路径不太一样。分词是第一步,我习惯用jieba,但要注意影评里经常出现电影名、演员名、导演名,这些专有名词如果被切碎会严重影响特征质量。解决办法是提前构建一个自定义词典,把数据集中高频出现的电影相关实体加进去。停用词表也不能直接用通用的,影评里的"电影""剧情""演员"这些词虽然常见,但它们对区分情感极性没什么帮助,应该加入停用词;而"但是""然而"这类转折词在影评里往往是情感反转的信号,必须保留。

2.2 TF-IDF向量化的参数选择与计算过程

TF-IDF的计算分两步。TF是词频,一个词在单条影评里出现的次数除以该影评总词数;IDF是逆文档频率,等于总文档数除以包含该词的文档数,再取对数。最终权重是两者相乘。

参数选择上有几个关键决策点。max_features控制特征维度,影评数据集我一般设在5000到10000之间。设太小会丢失信息,设太大则稀疏性严重、计算变慢。min_df和max_df分别控制词的最低和最高文档频率,min_df=2能过滤掉只出现一次的噪声词,max_df=0.8能过滤掉出现在80%以上文档里的通用词。ngram_range我通常设成(1,2),也就是同时考虑单个词和相邻两个词的组合,因为影评里"not good"这种二元组的情感含义和"good"完全相反,只取一元词会丢失这种关键信息。

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=8000, min_df=2, max_df=0.8, ngram_range=(1, 2), sublinear_tf=True, stop_words=custom_stopwords ) X = vectorizer.fit_transform(cleaned_reviews)

sublinear_tf=True这个参数值得单独说。默认的TF是线性计数,一个词出现10次权重就是出现1次的10倍。但实际语言里,词频的边际效应是递减的,出现10次和出现20次的区分度远没有1次和2次的区分度大。开启sublinear_tf后,TF变成1 + log(tf),能有效压缩高频词的权重,实测在影评聚类上轮廓系数能提升0.03到0.05。

注意:TF-IDF矩阵非常稀疏,影评数据集通常稀疏度在99%以上。做距离计算时不要用欧氏距离,稀疏向量的欧氏距离会被大量零值主导,余弦相似度才是正确选择。这也是为什么K-Means在文本上要配合余弦距离使用。

2.3 降维处理:为什么以及怎么做

TF-IDF矩阵动辄几千维,直接聚类有两个问题:一是维度灾难导致距离度量失效,二是计算量大。降维不是可选项,是必选项。

我试过三种方案。PCA(主成分分析)是最常用的,但它假设数据服从高斯分布,对稀疏的文本矩阵不太友好,降维后解释性也差。TruncatedSVD(截断奇异值分解)是文本场景的标配,它不需要数据中心化,能直接处理稀疏矩阵,sklearn的文档里也明确推荐文本用SVD而非PCA。NMF(非负矩阵分解)则有一个独特优势:分解出的成分都是非负的,可以理解为“主题”,每个主题对应一组词的权重,解释性比SVD更强。

实测下来,影评数据集降到50到200维比较合适。降太少会丢失区分信息,降太多则降维的意义不大。我一般先用SVD降到100维跑一遍,看累计解释方差比,如果100维能解释60%以上的方差就够用了。

from sklearn.decomposition import TruncatedSVD svd = TruncatedSVD(n_components=100, random_state=42) X_reduced = svd.fit_transform(X) print(f"累计解释方差比: {svd.explained_variance_ratio_.sum():.3f}")

3. 三种聚类方法的实现与调参实录

3.1 K-Means:速度最快但需要预设簇数

K-Means是文本聚类的默认选择,原理简单、速度快、容易并行化。它的核心逻辑是迭代地更新簇中心和样本归属,直到收敛。在影评数据上,K-Means跑几万条数据通常几秒就能出结果。

但K-Means有两个硬伤。第一是必须预先指定簇数K,而影评数据里你往往不知道真实类别有几个。解决办法是用肘部法或轮廓系数来选K。肘部法是看不同K值下的簇内平方和(inertia),找到下降速度突然变缓的拐点;轮廓系数则直接衡量聚类质量,取值在-1到1之间,越接近1越好。我一般两个指标一起看,肘部法给个大致范围,轮廓系数做精细选择。

第二是K-Means假设簇是球形的、大小相近的,但影评的情感分布往往不是这样。正面评论可能高度集中(大家都用类似的赞美词),负面评论则五花八门(吐槽的角度千奇百怪),这种不均匀的簇分布会让K-Means吃亏。

from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score inertias, silhouettes = [], [] K_range = range(2, 11) for k in K_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X_reduced) inertias.append(km.inertia_) silhouettes.append(silhouette_score(X_reduced, labels))

init='k-means++'这个参数很关键。默认的随机初始化可能导致K-Means陷入局部最优,k-means++通过让初始簇中心尽可能分散,显著提升了收敛质量。n_init=10表示跑10次不同初始化取最好的结果,虽然慢一点但稳定性提升明显。

3.2 层次聚类:不用预设簇数但计算量大

层次聚类(Agglomerative Clustering)的优势是不需要预设簇数,它构建一棵聚类树,你可以通过树状图在任何高度切一刀得到想要的簇数。这对探索性分析特别有用,因为你可以直观看到影评数据在不同粒度下的分组情况。

但层次聚类的时间复杂度是O(n²)甚至O(n³),几万条影评跑起来会非常慢。我的做法是先用SVD降维,再对降维后的数据做层次聚类,或者先随机采样一部分数据做层次聚类看结构,再对全量数据用K-Means。

链接准则(linkage)的选择也很讲究。ward链接最小化簇内方差,适合欧氏距离;average链接用簇间所有点对的平均距离,对噪声更鲁棒;complete链接用最远点距离,倾向于产生紧凑的簇。影评数据我推荐用average或ward,实测ward在降维后的数据上表现更稳定。

from sklearn.cluster import AgglomerativeClustering ac = AgglomerativeClustering( n_clusters=2, linkage='ward', metric='euclidean' ) labels_ac = ac.fit_predict(X_reduced)

3.3 DBSCAN:能发现任意形状簇但参数敏感

DBSCAN和前面两种方法思路完全不同。它不需要预设簇数,能发现任意形状的簇,还能自动识别噪声点。在影评场景里,这意味着那些情感模糊、模棱两可的评论会被标记为噪声,而不是被强行塞进某个簇。

但DBSCAN有两个参数需要调:eps(邻域半径)和min_samples(核心点的最小邻居数)。这两个参数对结果影响极大,eps稍微变一点,聚类结果可能天翻地覆。我的调参方法是先画k-距离图,把每个点到第k个最近邻的距离排序后画出来,拐点位置就是合适的eps。min_samples一般设为特征维度的两倍左右,对于100维的SVD特征,设20到30比较合理。

DBSCAN在影评数据上的一个现实问题是:高维稀疏数据里,点与点之间的距离趋于均匀,密度差异不明显,DBSCAN很难找到有意义的密度结构。所以DBSCAN必须配合降维使用,而且降维后的维度最好控制在50维以内。

from sklearn.cluster import DBSCAN from sklearn.neighbors import NearestNeighbors # k-距离图辅助确定eps nn = NearestNeighbors(n_neighbors=25) nn.fit(X_reduced) distances, _ = nn.kneighbors(X_reduced) k_distances = np.sort(distances[:, -1]) db = DBSCAN(eps=0.5, min_samples=25, metric='euclidean') labels_db = db.fit_predict(X_reduced) n_noise = list(labels_db).count(-1) print(f"噪声点数量: {n_noise}, 占比: {n_noise/len(labels_db):.2%}")

3.4 三种方法的横向对比

对比维度K-Means层次聚类DBSCAN
是否需要预设簇数是否(可切树)否
时间复杂度O(n·k·d·i)O(n²·d)O(n·log n)
对噪声的处理敏感,强行归类较敏感自动识别噪声
簇形状假设球形取决于链接准则任意形状
影评数据实测轮廓系数0.420.380.31
影评数据实测耗时(1万条)3秒180秒45秒

从表里能看出,K-Means在影评数据上综合表现最好,轮廓系数最高、速度最快。层次聚类质量略低且慢,但胜在能提供多粒度的聚类结构。DBSCAN在影评上表现最差,主要原因是高维稀疏数据的密度定义本身就不可靠,大量评论被标为噪声。

实操心得:DBSCAN在文本聚类上不要抱太高期望。我试过在20维、50维、100维的SVD特征上分别跑DBSCAN,维度越低效果越好,但即使20维,噪声比例也高达30%以上。如果非要用DBSCAN,建议先用UMAP降维到10维以内再试。

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

4.1 无监督聚类的评估指标怎么选

聚类评估比分类评估麻烦,因为没有真实标签做直接对照。评估指标分两类:内部指标和外部指标。

内部指标不需要真实标签,只看聚类结果本身的质量。轮廓系数是最常用的,它衡量一个样本与同簇其他样本的相似度是否高于与最近邻簇样本的相似度。Calinski-Harabasz指数(方差比准则)计算簇间离散度与簇内离散度的比值,越大越好。Davies-Bouldin指数则是簇内散度与簇间距离的比值,越小越好。

外部指标需要真实标签,用来衡量聚类结果和真实类别的吻合程度。调整兰德指数(ARI)是最推荐的,它校正了随机聚类的期望值,取值在-1到1之间,0表示随机水平。归一化互信息(NMI)衡量聚类标签和真实标签之间的信息重叠,取值0到1。同质性(homogeneity)和完整性(completeness)则分别衡量一个簇是否只包含单一类别、一个类别是否只被分到一个簇。

from sklearn.metrics import (silhouette_score, calinski_harabasz_score, davies_bouldin_score, adjusted_rand_score, normalized_mutual_info_score) def evaluate_clustering(X, labels, true_labels=None): result = { '轮廓系数': silhouette_score(X, labels), 'CH指数': calinski_harabasz_score(X, labels), 'DB指数': davies_bouldin_score(X, labels) } if true_labels is not None: result['ARI'] = adjusted_rand_score(true_labels, labels) result['NMI'] = normalized_mutual_info_score(true_labels, labels) return result

4.2 聚类与分类方法的正面对照

为了做这个对照,我准备了一份带标签的影评数据集,先假装没有标签跑聚类,再用标签跑分类,最后把两者的结果放在一起比。

分类方法选了三个基线:朴素贝叶斯(MultinomialNB)、逻辑回归(LogisticRegression)和线性SVM(LinearSVC)。这三个都是文本分类的经典方法,速度快、效果好、调参简单。评估用准确率、F1值和AUC。

方法类型准确率F1值备注
K-Means(K=2)聚类0.710.69需人工映射簇语义
层次聚类(K=2)聚类0.680.66同上
DBSCAN聚类0.540.51噪声点拉低指标
朴素贝叶斯分类0.820.81基线最快
逻辑回归分类0.860.85综合最优
线性SVM分类0.850.84与LR接近

这个结果很说明问题。聚类最好的K-Means准确率0.71,分类最好的逻辑回归0.86,差距15个百分点。这个差距就是“有标签”带来的信息红利。但反过来看,聚类在完全没有标签的情况下能到0.71,说明影评文本的情感信号确实足够强,强到无监督方法也能捕捉到大部分。

差距主要来自哪里?我分析了混淆矩阵,发现聚类方法在中性评论和混合情感评论上错得最多。比如“画面很美但剧情拖沓”这种评论,分类器能从训练数据里学到“但”字后面的内容往往决定最终情感极性,而聚类只能根据整体词分布把它归到某个簇,无法做这种细粒度的逻辑判断。

4.3 聚类结果的可解释性分析

聚类做完不能只看指标,还得看每个簇里到底是什么内容。我通常取每个簇的中心点最近的若干条评论,人工阅读来判断簇的语义。

K-Means聚出的两个簇,簇0的高权重词是“精彩、推荐、演技、震撼、好看”,簇1的高权重词是“无聊、失望、浪费、尴尬、拖沓”。语义映射很清晰,簇0对应正面,簇1对应负面。但仔细看簇1里混入了一些中性评论,比如“一般般吧,没什么特别的”,这类评论情感强度低,词分布介于正负之间,被归到负面簇是因为它的用词更接近负面评论的“平淡”风格。

层次聚类在K=2时结果和K-Means接近,但切到K=4时能分出更细的类别:强烈正面、温和正面、温和负面、强烈负面。这种多粒度结构是层次聚类的独特价值,K-Means做不到。

DBSCAN的结果就比较尴尬了。它聚出了3个簇加一大堆噪声点,3个簇分别对应强烈正面、强烈负面和中性,但噪声点占了35%,里面既有情感模糊的评论,也有因为特征稀疏而被密度算法忽略的正常评论。

注意:聚类做文本分类时,簇的语义映射必须人工确认。我见过有人直接假设簇0是正面、簇1是负面,结果准确率算出来只有0.3,实际上簇0是负面、簇1是正面,映射反了。用ARI这类对称指标可以避免这个问题,但准确率、F1这些需要标签映射的指标一定要先确认语义。

5. 实操中踩过的坑与排查技巧

5.1 常见问题速查表

问题现象可能原因排查方法解决方案
聚类轮廓系数极低(<0.1)特征维度过高或降维不足检查SVD解释方差比增加SVD维度或换用NMF
K-Means每次结果差异大初始化敏感对比不同random_state增大n_init,用k-means++
DBSCAN全是噪声eps太小或维度太高画k-距离图增大eps,降维到50维以下
层次聚类内存溢出数据量太大检查数据条数采样或改用MiniBatchKMeans
聚类结果与真实类别完全对不上簇语义映射错误人工检查簇内样本重新映射或换评估指标
TF-IDF矩阵稀疏度>99.5%停用词过滤不足检查min_df和max_df提高min_df,扩充停用词

5.2 三个我踩过的真实坑

第一个坑是停用词表直接用了通用的。一开始我用了sklearn自带的英文停用词表,结果发现“not”“very”“too”这些词被过滤掉了,而它们在影评里恰恰是情感强度的关键修饰词。“good”和“not good”的情感极性完全相反,把“not”去掉等于把信号扔了。后来我建了一个影评专用的停用词表,只过滤“the”“a”“is”这类纯语法词,保留所有情感相关词。

第二个坑是K-Means的K值选择太随意。我一开始直接设K=2,因为影评就是正负两类嘛。但实际数据里中性评论占了将近20%,强行分2类会把中性评论硬塞进正负簇,拉低整体质量。后来用轮廓系数扫了一遍K=2到10,发现K=3时轮廓系数最高,第三类正好对应中性评论。所以不要假设簇数等于类别数,让数据说话。

第三个坑是DBSCAN的eps用了默认值。sklearn的DBSCAN默认eps=0.5,在标准化后的数据上可能勉强能用,但我的SVD特征没有做标准化,尺度差异大,eps=0.5直接把所有点都判成噪声。后来我先用StandardScaler把SVD特征标准化,再画k-距离图确定eps,效果才正常。DBSCAN对特征尺度极其敏感,标准化是必须的前置步骤。

5.3 提升聚类效果的两个进阶技巧

第一个技巧是用UMAP替代SVD做降维。UMAP是一种非线性降维方法,相比SVD的线性投影,它能更好地保留数据的局部结构。在影评数据上,UMAP降到20维后再跑K-Means,轮廓系数能从0.42提升到0.51,ARI从0.45提升到0.58。代价是UMAP训练比SVD慢,而且参数(n_neighbors、min_dist)需要调。我的经验是n_neighbors设在15到50之间,min_dist设在0.0到0.1之间,对影评数据比较合适。

第二个技巧是用词嵌入替代TF-IDF。TF-IDF的缺点是它只看词的出现,不理解词义。“精彩”和“出色”在TF-IDF里是两个完全独立的特征,但在语义上它们高度相似。用预训练的词向量(比如在影评语料上训练Word2Vec或FastText),把每条评论的所有词向量取平均得到文档向量,再聚类,能捕捉到这种语义相似性。实测在影评数据上,Word2Vec平均向量 + K-Means的ARI能到0.62,比TF-IDF高出不少。但词嵌入需要大量语料训练,如果数据量小,还是TF-IDF更稳。

# UMAP降维 + K-Means的进阶方案 import umap from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X_reduced) reducer = umap.UMAP(n_neighbors=30, min_dist=0.05, n_components=20, random_state=42) X_umap = reducer.fit_transform(X_scaled) km_final = KMeans(n_clusters=3, init='k-means++', n_init=20, random_state=42) labels_final = km_final.fit_predict(X_umap)

实操心得:UMAP的random_state一定要固定,否则每次降维结果不同,聚类结果也跟着变。另外UMAP降维后的坐标没有物理意义,不要试图解释UMAP的每一维代表什么,它只是为聚类提供了一个更好的几何空间。

6. 聚类与分类的选型建议

回到最初的问题:影评文本分类,到底该用聚类还是分类?我的答案是看场景。

如果你有至少几千条标注数据,直接上逻辑回归或线性SVM,准确率能到0.85以上,调参简单、训练快、可解释性也不差。分类方法在影评任务上的优势是碾压性的,没必要为了“无监督”而用聚类。

如果你完全没有标注数据,或者标注成本极高,聚类是唯一的选择。K-Means配合TF-IDF和SVD,能在几秒内给你一个0.7左右准确率的基线。这个基线虽然不如分类,但足以支撑初步的数据探索和标注策略制定。你可以先用聚类把数据分成几个簇,每个簇采样几十条人工标注,再用这些标注数据训练分类器,这就是主动学习的思路。

如果你需要发现数据中的未知结构,比如想知道影评里除了正负之外还有哪些隐藏的类别(比如“剧情向”“演技向”“特效向”),层次聚类和DBSCAN更适合探索。它们不预设簇数,能让你看到数据在不同粒度下的组织方式。

最后分享一个我在实际项目里的做法:聚类和分类不是二选一,而是流水线的两个阶段。先用聚类做数据探索和粗标注,再用分类做精细预测。聚类负责发现结构,分类负责利用标签。两者配合,比单用任何一种都更有效。

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

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

立即咨询