TF-IDF+LDA+DBSCAN:构建观影用户画像的电影推荐聚类分析
发布时间:2026/9/15 17:10:51 作者:尧图编辑部 阅读量:1,286

简介基于词频-逆文档频率TF-IDF、隐含狄利克雷分配LDA与基于密度的空间聚类DBSCAN算法的观影用户电影推荐聚类分析完整项目定位于毕业设计、数据挖掘课程实践及推荐系统入门者。项目利用TF-IDF提取评论文本特征结合LDA发现潜在主题再通过DBSCAN完成用户聚类并产出推荐结果覆盖从数据预处理到可视化展示的完整流程。压缩包共20个文件以12个Python脚本为核心包含数据预处理、主题建模、聚类对比、图像绘制与GUI交互界面等模块另附PDF报告、Markdown设计文档、数据RAR包和运行说明文件整体大小7.21MB目录清晰便于检索。目前已有517人学习浏览结构和注释对二次开发较为友好。除可运行的完整代码外读者还能获得详细设计文档、实验数据以及K均值、DBSCAN和傅里叶变换等多角度对比实现既可支撑毕业设计论文撰写也能作为文本挖掘与推荐项目的基础参考。1. 为什么“TF-IDF LDA DBSCAN”组合适合观影用户的电影推荐聚类分析在真实的观影推荐场景里用户行为矩阵普遍稀疏新用户和长尾电影又让协同过滤很容易失效。如果只按评分做矩阵分解很多细节用户没法解释清而只做内容关键词匹配又无法区分“喜欢科幻烧脑”和“喜欢科幻爱情”。把 TF-IDF、LDA、DBSCAN 串起来做聚类分析正好是毕业设计里最稳的一条可解释路线TF-IDF 把用户看过的电影文本变成向量LDA 主题模型把文档向量压成可解释的主题分布DBSCAN 再在这个分布上按密度找群体边界。推荐动作不再是全局推荐而是先定位用户属于哪一簇再在簇内部召回候选电影。这篇文章会把每一步的矩阵形态、参数选择和代码细节拆开让你不用返工也能把整套流程跑通。2. TF-IDF特征与LDA主题模型构建用户观影画像拿到ratings.csv和movies.csv后我一般不会直接拿评分矩阵去做 DBSCAN而是先把每个用户的观影历史拼成一个“用户文档”。这里的文档不是电影标题列表而是把电影的类型、简介、主演这类文本字段拼接成一句话。这样 TF-IDF 和 LDA 才有真正意义上的“词”。用户文档的长度取决于观看量观看多的用户自然拥有更丰富的词分布这个差异也正是后续聚类要保留的信号。2.1 把观影行为转成TF-IDF矩阵TF-IDF 在这里要做两件事一是过滤掉几乎所有用户都看过的热门词二是保留对单个用户有辨识度的关键词。构造代码如下from sklearn.feature_extraction.text import TfidfVectorizer # user_texts: list[str]下标对应 user_id # 每项是把该用户看过的电影海报、类型、简介去重后合并的文本 tfidf_vec TfidfVectorizer( min_df2, max_df0.8, sublinear_tfTrue, max_features5000, token_patternr(?u)\b\w\b ) X_tfidf tfidf_vec.fit_transform(user_texts) print(X_tfidf.shape) # (用户数, 特征词数)min_df2表示只在 2 个及以上用户文档里出现过的词才保留避免一个人打错字产生孤立词。max_df0.8会把出现频率高于 80% 的常用词剔除因为这种词对区分用户没有贡献。sublinear_tfTrue很好用它把原始词频取对数压制一部长篇电影简介反复出现带来的虚高权重。max_features5000是经验值如果用户数不超过 3 万5000 维足够表现主题差异也能省下后续 DBSCAN 的算力。需要注意TfidfVectorizer 的输出是稀疏矩阵后面所有中间结果都要尽量维持csr_matrix否则高维向量会直接把内存吃满。TF-IDF 参数参考表参数建议值影响min_df2 或 0.5% 用户数去掉只在单个文档出现的噪声词max_df0.8 或 90% 用户数去掉不区分用户的公共词sublinear_tfTrue减弱长电影简介的重复词影响max_features3000-10000控制后续 LDA 和 DBSCAN 的计算规模norml2默认即可统一向量长度2.2 LDA主题模型的超参数设置与主题数选择LDA 主题模型要做的是从用户语料中抽取若干个隐性主题比如“科幻 / 悬疑 / 动画”这类观影趣味。一个常见错误是直接把上一节得到的 TF-IDF 矩阵喂给 LDA。严格来说LDA 假设词频来自多项分布TF-IDF 会破坏统计前提。更合理的做法是用 CountVectorizer 生成词频矩阵再用 LDA 拟合主题分布最后把 TF-IDF 和主题分布拼接成用户特征。from sklearn.feature_extraction.text import CountVectorizer from sklearn.decomposition import LatentDirichletAllocation count_vec CountVectorizer(min_df2, max_df0.8, max_features5000) X_count count_vec.fit_transform(user_texts) lda_model LatentDirichletAllocation( n_components8, doc_topic_prior0.1, topic_word_prior0.01, random_state42, max_iter50 ) user_topic lda_model.fit_transform(X_count) print(user_topic.shape) # (用户数, 主题数)n_components8表示主题数。主题数不建议直接拍脑袋可以同时计算lda_model.log_perplexity(X_count)perplexity 随主题数增加会持续下降但下降曲线出现“肘部”的位置就是候选主题数。之后再结合每个主题的前 10 个词看是否可解释。doc_topic_prior越小用户主题分布越稀疏效果上更倾向于“一人一个主主题”topic_word_prior越小每个主题的词越集中主题之间边界越清晰。这两个参数需要一起调不要只改其中一项。random_state42是为了毕业设计复现。一定不要省略否则每次跑出来的主题顺序都可能变。LDA 关键参数表参数作用起始值n_components主题数量6-10doc_topic_prior用户主题分布稀疏度0.1topic_word_prior主题词分布稀疏度0.01max_iter收敛轮数50learning_methodonline 适合大样本batch 样本少时用random_state复现结果422.3 把TF-IDF和LDA主题向量拼接成用户特征DBSCAN 聚类时既需要关注用户爱看的内容关键词也需要关注用户所在主题群体。直接把X_tfidf当输入特征维度高且噪声大只用user_topic又丢失了具体电影类型上的细粒度差异。常见做法是横向拼接from scipy import sparse from sklearn.preprocessing import normalize X_feature sparse.hstack([X_tfidf, user_topic], formatcsr) X_feature normalize(X_feature, norml2, axis1)横向拼接后每个用户由 TF-IDF 词向量加上 8 维主题分布共同描述。normalize很重要因为 LDA 主题分布每个用户之和为 1量纲与 TF-IDF 不同不归一化会导致 DBSCAN 的密度衡量完全被某个矩阵主导。到这里特征矩阵已经从稀疏的评分矩阵聚合为用户兴趣向量下一步就可以交给 DBSCAN 做密度聚类分析了。3. DBSCAN密度聚类用于观影用户分群的核心步骤3.1 为什么不用K-Means而用DBSCAN做用户聚类观影用户并不是均匀分布在特征空间里的。头部用户群可能是一个椭圆形团文艺片用户可能是细长条还有一部分用户只看热门大片在空间里散布得很散。K-Means 默认把所有点到质心距离误差最小化遇到非凸形状会把细长簇从中间劈开SPSS聚类分析里的系统聚类虽然能画树状图但样本超过几千就很吃力而且离群用户总是会被强行归入某一类。DBSCAN 不需要预设簇数它用“密度可达”判断样本是否属于同一簇并把低密度区域的样本直接标记为-1噪声点。在毕业设计里多出这样一个噪声用户集合不是缺点反而可以转成冷启动推荐策略。距离度量方面如果直接对稀疏 TF-IDF 矩阵用欧氏距离高维稀疏会让所有点之间的距离都差不多大聚类失去区分度。看电影偏好最自然的度量是余弦距离。所以我会用预计算余弦距离矩阵来跑 DBSCAN而不是让 DBSCAN 每次临时算距离。3.2 用K-距离图确定eps参数DBSCAN 最棘手的是eps和min_samples。min_samples可以先用默认值 5eps的常见做法是看 K-距离图。对每个用户找到第min_samples近的邻居记录距离后排序距离曲线从平缓到陡峭的拐点就是eps候选值。import numpy as np from sklearn.neighbors import NearestNeighbors from sklearn.cluster import DBSCAN from sklearn.metrics.pairwise import cosine_distances min_samples 5 # 先求每个点的第 min_samples 近邻距离 nn NearestNeighbors(n_neighborsmin_samples, metriccosine) nn.fit(X_feature) distances, _ nn.kneighbors(X_feature) k_dist np.sort(distances[:, -1]) # 画出 k_dist横轴是排序后的样本序号纵轴是距离值 # 找到“肘部”位置对应距离就是 eps 初值 eps 0.25 # 用预计算余弦距离矩阵做密度聚类分析 D cosine_distances(X_feature) cluster_model DBSCAN( epseps, min_samplesmin_samples, metricprecomputed, n_jobs-1 ) labels cluster_model.fit_predict(D) print(f簇数: {len(set(labels)) - (1 if -1 in labels else 0)}, 噪声用户: {(labels -1).sum()})这里的eps是余弦距离阈值不是相似度阈值。余弦距离范围是 0 到 2eps0.25表示两个用户靠得相当近。K-距离图的拐点如果不好判断可以同时跑几组eps记录噪声比和簇数曲线选择一个让簇数不骤降、噪声比低于 30% 的值。precomputed方式会生成一个 N×N 的稠密矩阵用户数超过 1 万时内存可能不够这时要么采样部分用户调参要么改用NearestNeighbors的半径查询手动实现密度连通。数据量在 5000 以内时预计算矩阵是最省心的方案。DBSCAN 参数说明表参数取值怎么调eps0.15-0.45K-距离图拐点min_samples3-10值越大噪声越多簇越少metriccosine比欧氏距离更适合文本向量输出 -1噪声点单独走冷启动推荐不进任何簇3.3 聚类结果怎么看拿到labels后不要只看簇个数。要画出每个簇的用户主题均值分布例如簇 0 在主题 3 上得分明显高就说明这个簇是一批对某个主题有共同偏好的用户。另一个必须做的检查是轮廓系数。DBSCAN 的轮廓系数可以直接用预计算矩阵计算from sklearn.metrics import silhouette_score mask labels ! -1 if mask.sum() 1 and len(set(labels[mask])) 1: score silhouette_score( X_feature[mask], labels[mask], metriccosine ) print(score)这段代码把噪声点剔除后再算轮廓因为噪声点会拉低整体分数。轮廓系数高于 0.1 就算有结构文本聚类往往比欧氏空间难提分不要期望所有数据都达到 0.3。更重要的是每个簇的实际业务解释。如果某个簇主题均值不明显说明eps太大把不同偏好的人揉到了一起。4. 根据DBSCAN聚类结果实现电影推荐聚类本身不产生推荐。真正按下住“电影推荐”逻辑的是把簇标签用起来同一簇的用户已经说明兴趣相近那么他们的观影行为可以作为互相推荐的数据来源。我会同时准备两套候选生成器一套是同簇协同过滤另一套是 TF-IDF 内容相似推荐最后按混合比例合并。4.1 同簇协同过滤的召回代码协同过滤部分不需要全量计算整个用户矩阵只需要在目标用户所在簇内找邻居。这样既快也天然完成了用户分群。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # score_matrix: 用户-电影评分矩阵缺失值填0shape(user_count, movie_count) # labels: DBSCAN 聚类结果 # user_id: 被推荐用户在 score_matrix 中的行号 def recommend_by_cluster(user_id, score_matrix, labels, ratio_matrix, top_n10): label labels[user_id] # 噪声用户不做簇内协同交给内容推荐兜底 if label -1: return content_based_recommend(user_id, ratio_matrix, top_n) cand_ids np.where(labels label)[0] cand_ids cand_ids[cand_ids ! user_id] user_vec score_matrix[user_id].reshape(1, -1) sims cosine_similarity(user_vec, score_matrix[cand_ids]).ravel() top_k cand_ids[np.argsort(-sims)[:20]] cluster_score score_matrix[top_k].mean(axis0) max_score cluster_score.max() if max_score 0: return content_based_recommend(user_id, ratio_matrix, top_n) watched score_matrix[user_id] 0 weighted_score cluster_score / max_score weighted_score[watched] -1 movie_ids np.argsort(-weighted_score)[:top_n] return movie_ids这里的关键是给未看过的电影打分并把已经看过的电影权重置为 -1强制排除。top_k20是经验值候选邻居太多会混入弱相关用户太少则覆盖率不够。在计算相似度前建议对每个用户的评分先做均值中心化也就是把当前用户自身平均分减掉能缓解“有人打分宽松有人打分严格”的偏差。4.2 内容候选与混合推荐策略内容推荐部分使用第 2 章的用户 TF-IDF 向量。当用户是-1噪声点或者同簇内评分记录太少时用 LDA 主题向量找内容相似电影。具体做法是把电影简介也做成 TF-IDF 向量再和该用户历史电影的平均向量做余弦相似度最后取 TopN 电影。混合推荐不是简单拼接。我一般会把两个候选列表按 6:4 比例加权同簇协同过滤占 60%内容相似占 40%。如果同簇协同过滤只覆盖了 3 部电影就从内容列表中补齐到 10 部避免最终列表空空荡荡。推荐策略选择表用户类型推荐策略数据来源所属簇的用户同簇协同过滤 内容补充簇内用户矩阵噪声点新用户内容相似推荐只看过的电影简介簇内邻居过少内容相似推荐为主用户平均向量热门电影偏好簇内过滤后按分排序评分矩阵均值4.3 跟SPSS聚类分析方法做对比在毕业设计答辩里评委如果问“为什么不是 K-Means 或 SPSS聚类分析”可以从离群点处理上解释。传统 SPSS聚类分析 输出的是树状图和层次聚类表适合小样本问卷但把它放大到上万用户后距离矩阵计算量会迅速膨胀而且层次聚类对边界样本必须给出归属。DBSCAN 则把这些边界用户直接命名为噪声配合上一节的内容推荐兜底反而让推荐系统对低活跃用户更友好。你可以用同一份数据跑一次层次聚类把两边的簇用户重叠度做成图表这个对比本身就是很好的论文素材。5. “附完整代码”的毕业设计代码包该怎么组织5.1 目录结构与依赖清单毕业设计压缩包打开后最先被看到的就是目录结构。项目代码不是把所有逻辑写进一个main.py而是要按数据处理、特征构建、聚类、推荐四条线拆开。project/ ├── data/ │ ├── raw/ │ │ ├── movies.csv │ │ └── ratings.csv │ └── processed/ │ └── user_docs.csv ├── src/ │ ├── __init__.py │ ├── preprocess.py │ ├── features.py │ ├── cluster_model.py │ ├── recommender.py │ └── main.py ├── outputs/ │ ├── k_distance.png │ ├── cluster_labels.csv │ └── recommendations.csv ├── requirements.txt └── README.md这里outputs/k_distance.png非常关键评委第一眼看到你在调eps时有可视化证据代码可信度会提高很多。requirements.txt 内容一般建议锁定到主版本比如scikit-learn1.3、pandas2.0、numpy1.24。不要全部锁死到具体小版本兼容性问题会让环境配置很痛苦。5.2 一个可复现的main.py流水线main.py 的作用是让整个流程一条命令跑完并且保证随机种子一致。推荐写成如下形式import argparse import random import numpy as np import pandas as pd from preprocess import build_user_docs from features import build_user_feature, train_lda from cluster_model import cluster_users from recommender import run_hybrid_recommend def main(): parser argparse.ArgumentParser() parser.add_argument(--data, requiredTrue, help原始数据目录) parser.add_argument(--method, choices[cluster, content, hybrid], defaulthybrid) parser.add_argument(--seed, typeint, default42) args parser.parse_args() random.seed(args.seed) np.random.seed(args.seed) ratings pd.read_csv(f{args.data}/ratings.csv) movies pd.read_csv(f{args.data}/movies.csv) user_docs build_user_docs(ratings, movies) X_feature, lda_model build_user_feature(user_docs, seedargs.seed) labels cluster_users(X_feature) result run_hybrid_recommend(ratings, labels, X_feature) result.to_csv(outputs/recommendations.csv, indexFalse) if __name__ __main__: main()这个骨架把数据流分得很清楚build_user_docs负责清洗合并build_user_feature返回 TF-IDF 拼接 LDA 主题后的矩阵cluster_users单独存有调参记录。当随机种子固定后LDA 主题顺序和 DBSCAN 的簇编号都能稳定复现这在毕业设计里比微调模型精度更重要。5.3 中间结果缓存与调试技巧第二次运行就要从头计算所有特征会让调试效率很低。我一般会在features.py里加一个文件缓存如果data/processed/user_matrix.npz存在就直接加载特征矩阵否则重新训练。这个看似简单的步骤能省掉大量调聚类参数时重复跑 TF-IDF 和 LDA 的时间。如果发现某个用户文档因为缺失简介导致空文本build_user_docs中要保留兜底字符串否则 TfidfVectorizer 会因空文档直接报错。6. 用留一法和主题一致性验证推荐聚类结果最后一个环节是验证模型不是靠肉眼满意。对 LDA 主题模型不要只盯log_perplexity更实用的指标是主题一致性。把训练好的 LDA 模型转成 Gensim 格式然后计算主题词之间的语义相似度。如果主题数从 4 变到 12一致性分数在前 8 个主题之后明显回落那 8 就是合适取值。调参记录里要有这条曲线而不是只写“最终选了 8”。对 DBSCAN最有效的验证是参数敏感性检查。固定min_samples5让eps从 0.15 到 0.4 每 0.02 取一次记录每次的簇数和噪声用户数。如果某一段eps变化下簇数剧烈跳变说明这个参数区域不稳真正可用的参数区间应该是变化平缓的位置。把这个表画成折线图比单纯报一个轮廓系数更能说明问题。最后是推荐效果验证。毕业设计没有线上点击率最合适的方法是留一法。对每个用户把最后一次观影行为摘出来用前面数据生成推荐列表如果被摘出的电影出现在 Top10 里就算命中。def leave_one_out_eval(rec_func, user_history, topk10): hits 0 total 0 for uid, items in user_history.items(): if len(items) 3: continue train_items items[:-1] heldout_item items[-1] recs rec_func(uid, train_items, topktopk) hits 1 if heldout_item in recs else 0 total 1 hit_rate hits / total if total else 0.0 return hit_ratelen(items) 3是保护条件只看过一两部的用户没有足够的训练上下文留一法会对他们失效。HitRate10 一般能做到 0.05 到 0.2 之间已经比纯随机的几十万分之一有区分度。建议把 HitRate5 和 HitRate10 都算一遍这样可以回答论文里常见的“推荐列表多长合适”。把主题一致性曲线、DBSCAN 参数敏感图、留一法命中率三张图放进毕业设计答辩整个结论会非常完整。本文还有配套的精品资源点击获取