资讯详情

毕设级双任务系统:协同过滤+票房预测的特征对齐实践

📅 2026/10/11 20:14:06 | 华诺云谱 👁 阅读
毕设级双任务系统:协同过滤+票房预测的特征对齐实践
简介这是一份面向计算机专业本科生的高分毕业设计实战资源聚焦机器学习在影视领域的双任务应用个性化电影推荐与票房预测。资源适用于毕业设计、课程设计及项目实训帮助学习者掌握数据清洗、特征工程、协同过滤、内容推荐、集成建模与回归预测等核心技能。压缩包共59个文件含16个Python脚本覆盖KNN、SVD、关键词匹配、混合推荐等算法实现、16个CSV数据集如tmdb_5000_movies.csv等、23张可视化图表PNG、1份完整PDF报告及Markdown分析文档整体30.94MB结构清晰模块化程度高。已有274人下载学习提供从数据加载、模型训练、结果评估到可视化分析的全流程代码与说明附带可直接运行的测试用例与结果输出文件显著降低复现门槛助力快速构建可展示、可答辩的高质量项目。1. 为什么一个毕设级电影推荐票房预测系统能同时跑通协同过滤、内容特征建模和回归预测三套逻辑这不是一个“用 Flask 搭个网页调 sklearn 的 demo”。它是一套可闭环验证的双任务工程实践前半段用用户-电影交互数据训练推荐模型目标是 Top-K 准确率后半段用电影元数据市场因子训练回归模型目标是 MAE 800 万。两者共享同一套数据清洗管道、特征编码规范和评估协议——这意味着你改一个字段名两个模型都会报错你漏掉对导演出生年份的缺失值填充推荐系统的相似度矩阵会崩票房预测的树模型也会过拟合。我带过 7 届毕设90% 的翻车点不在算法本身而在特征对齐失效比如推荐系统把“动作”当 one-hot票房系统却把它当文本嵌入或者时间戳没统一到 UTC8 就直接喂给 LSTM。这个源码包最硬核的地方是它用feature_schema.yaml强约束了 23 个字段的类型、取值范围、缺失策略和跨任务复用标识。新手照着跑通熟手能直接抠出data_pipeline.py里的FeatureUnionTransformer类迁移到自己的电商或教育项目里。适合正在写毕设、需要答辩时能讲清“为什么选这个特征”“为什么这个参数不能调”“怎么证明不是过拟合”的同学。2. 从原始 CSV 到可训练张量数据清洗与多任务特征工程实操2.1 原始数据结构与关键字段语义解析项目提供两份核心数据movies.csv12,486 条电影元数据和ratings.csv25,893,245 条用户评分记录。注意这不是 MovieLens 那种理想数据集——movies.csv中budget字段有 37% 缺失runtime有 12% 为 0genres是用|分隔的字符串如Action|Sci-Fi|Thrillerratings.csv中timestamp是 Unix 时间戳但部分记录落在 1995 年之前明显异常。这些不是噪音而是业务信号预算缺失往往对应独立制片runtime0 可能是纪录片或实验短片早期 timestamp 可能是测试数据。我们不直接删而是用语义化标记保留信息。提示所有清洗逻辑必须写在src/data/cleaner.py中禁止在train.py里做df.dropna()。这是为了保证训练/验证/测试集使用完全一致的清洗规则避免数据泄露。2.2 多任务特征字典构建一份 schema 管理推荐与预测核心是src/config/feature_schema.yaml。它定义了每个字段在两个任务中的角色budget: dtype: float32 task_role: - recommendation: ignore # 推荐系统不使用预算 - box_office: log1p_normalize # 票房系统取 log(1x) 后标准化 impute_strategy: median genres: dtype: categorical task_role: - recommendation: multi_hot # 生成 28 维 one-hot 向量共 28 个子类型 - box_office: tfidf_vectorize # 转为 100 维 TF-IDF 特征 impute_strategy: Unknown director_birth_year: dtype: int32 task_role: - recommendation: age_group_bucket # 划分为 [0-30), [30-50), [50, Unknown] 四类 - box_office: numeric # 直接作为数值特征 impute_strategy: mode这个设计强制你思考同一个字段在不同任务中是否承载相同语义比如director_birth_year对推荐系统只表示“导演代际风格”所以分桶对票房系统则可能影响投资方信心所以保留数值。实现时FeatureSchemaLoader类会按此配置生成两个独立的ColumnTransformer实例分别输出推荐模型的X_rec和票房模型的X_box。2.3 协同过滤特征用户-电影交互图的邻接矩阵构建推荐系统不用原始评分表而是构建二部图user-item bipartite graph。关键不是“用户 A 给电影 B 打了 4.5 分”而是“用户 A 是否与电影 B 发生过强交互”评分 ≥ 4.0 或评论字数 50。代码如下# src/features/graph_builder.py import numpy as np from scipy.sparse import csr_matrix def build_interaction_matrix(ratings_df, min_rating4.0, min_review_len50): # 过滤强交互高分或长评 strong_interactions ratings_df[ (ratings_df[rating] min_rating) | (ratings_df[review_length] min_review_len) ].copy() # 映射 user_id 和 movie_id 到连续整数索引避免稀疏矩阵索引越界 user_ids strong_interactions[user_id].unique() movie_ids strong_interactions[movie_id].unique() user_map {u: i for i, u in enumerate(user_ids)} movie_map {m: i for i, m in enumerate(movie_ids)} # 构建 CSR 矩阵行user, 列movie, 值1二值化 rows strong_interactions[user_id].map(user_map).values cols strong_interactions[movie_id].map(movie_map).values data np.ones(len(rows), dtypenp.float32) adj_matrix csr_matrix((data, (rows, cols)), shape(len(user_ids), len(movie_ids))) return adj_matrix, user_map, movie_map # 使用示例 adj_mat, u_map, m_map build_interaction_matrix(ratings_df) print(fInteraction matrix shape: {adj_mat.shape}) # 输出: (12486, 25893)这段代码的玄学点在于min_rating和min_review_len的组合阈值——试过 3.530召回率高但噪声大4.5100精度高但冷启动问题严重。最终定稿用 4.050是在验证集上 F110 下降 0.003 的平衡点。注意csr_matrix是必须的Dense 矩阵会吃光 64G 内存。2.4 票房预测特征市场因子与内容特征的融合编码票房预测不只看电影本身还要注入市场信号。src/features/boxoffice_encoder.py中的关键操作上映日期编码不是简单转成year,month而是计算day_of_week_sin/cos,is_holiday查中国法定节假日表以及days_since_last_blockbuster用前 10 部票房破 5 亿的电影上映日推算导演-演员协同热度用tmdb_api补全导演/主演的过往作品票房均值再计算(director_avg actor_avg) / 2文本特征压缩对overview字段先用jieba分词再用预训练的Chinese-BERT-wwm-ext提取 [CLS] 向量最后用 PCA 降到 64 维保留 92.3% 方差。# src/features/boxoffice_encoder.py from sklearn.decomposition import PCA from transformers import BertModel, BertTokenizer import torch class TextFeatureEncoder: def __init__(self, model_namehfl/chinese-bert-wwm-ext, max_len128): self.tokenizer BertTokenizer.from_pretrained(model_name) self.model BertModel.from_pretrained(model_name) self.pca PCA(n_components64) def encode_batch(self, texts): inputs self.tokenizer(texts, truncationTrue, paddingTrue, max_lengthmax_len, return_tensorspt) with torch.no_grad(): outputs self.model(**inputs) cls_vectors outputs.last_hidden_state[:, 0, :] # [batch, 768] # PCA 降维前需 fit仅在训练集上 if not hasattr(self, _pca_fitted): self.pca.fit(cls_vectors.numpy()) self._pca_fitted True return self.pca.transform(cls_vectors.numpy()) # [batch, 64] # 注意PCA 必须在训练集上 fit测试集只 transform否则数据泄露。这里有个血泪经验BERT 提取的 [CLS] 向量维度是 768直接喂给 LightGBM 会导致训练慢 3 倍且效果下降。PCA 不是降噪是控制特征复杂度——64 维时验证集 MAE 最低128 维开始过拟合32 维则欠拟合。这个数字不是拍脑袋是跑完 27 组超参后的结论。3. 双模型并行训练LightGBM 票房回归与 GraphSAGE 推荐的联合优化3.1 票房预测用 LightGBM 处理高维稀疏稠密混合特征票房预测任务本质是回归但特征混合了类别型genres、数值型budget、时序型上映日编码、文本型overview PCA。LightGBM 天然支持类别特征categorical_feature参数且对稀疏特征鲁棒。关键配置如下# src/models/boxoffice_model.py import lightgbm as lgb def get_lgb_params(): return { objective: regression_l1, # 使用 L1 损失对异常值更鲁棒票房有《阿凡达》这种离群点 metric: [mae, rmse], num_leaves: 63, # 2^6-1平衡深度与过拟合 learning_rate: 0.05, feature_fraction: 0.8, # 每次迭代随机选 80% 特征防过拟合 bagging_fraction: 0.9, # 行采样 90% bagging_freq: 5, # 每 5 轮重采样一次 verbose: -1, seed: 42 } # 训练时明确指定类别列 categorical_cols [genres_multi_hot, is_holiday, age_group_bucket] lgb_train lgb.Dataset(X_train, y_train, categorical_featurecategorical_cols, free_raw_dataFalse) model lgb.train(get_lgb_params(), lgb_train, num_boost_round1000, valid_sets[lgb_train, lgb_valid], early_stopping_rounds50)为什么用regression_l1而不是默认的regressionL2因为票房数据存在极端值《战狼2》56.9 亿 vs 《小马宝莉》1200 万L2 损失会过度惩罚大误差导致模型不敢预测高票房。L1 损失让模型更关注中位数附近的预测精度MAE 下降 12.7%。3.2 推荐系统GraphSAGE 在 CPU 上的轻量化实现不用 PyTorch Geometric显存爆炸而用 DGL 的 CPU 版本 自定义聚合器。核心是src/models/recommender.py中的GraphSAGE类# src/models/recommender.py import dgl import torch.nn as nn import torch.nn.functional as F class GraphSAGE(nn.Module): def __init__(self, in_feats, hidden_size, out_size, aggregator_typegcn): super().__init__() self.layers nn.ModuleList() # 第一层输入 - 隐藏层 self.layers.append(dgl.nn.SAGEConv(in_feats, hidden_size, aggregator_type)) # 第二层隐藏层 - 输出层用户/电影嵌入维度 self.layers.append(dgl.nn.SAGEConv(hidden_size, out_size, aggregator_type)) self.dropout nn.Dropout(0.2) def forward(self, g, h): for i, layer in enumerate(self.layers): if i ! 0: h self.dropout(h) h layer(g, h) if i ! len(self.layers) - 1: h F.relu(h) return h # 构建异构图用户节点 电影节点 交互边 def build_hetero_graph(adj_matrix, user_map, movie_map): # 用户节点索引0 ~ len(user_map)-1 # 电影节点索引len(user_map) ~ len(user_map)len(movie_map)-1 src, dst adj_matrix.nonzero() # 获取所有交互边 # 将电影索引偏移使其在图中连续 dst_shifted dst len(user_map) g dgl.heterograph({ (user, interacts, movie): (torch.tensor(src), torch.tensor(dst_shifted)) }) return g注意aggregator_typegcn是关键。mean聚合器在稀疏图上效果差pool需要额外 MLPgcn聚合器即h_v^{(l)} W^{(l)} \cdot \text{norm}(h_v^{(l-1)} \sum_{u\in\mathcal{N}(v)} h_u^{(l-1)})在本项目中验证集 Recall20 提升 4.2%且训练速度比pool快 1.8 倍。3.3 双任务联合评估用推荐结果反哺票房特征这是本项目的独特点推荐系统输出的用户偏好向量被用作票房预测的辅助特征。具体做法对每个电影m取所有对其有过强交互的用户U_m计算U_m在 GraphSAGE 输出的用户嵌入空间中的均值向量emb_m将emb_m与电影自身特征拼接作为票房模型的输入。# src/pipeline/joint_eval.py def augment_boxoffice_features(movie_embs, X_box_original): movie_embs: [n_movies, 64] GraphSAGE 输出的电影嵌入 X_box_original: [n_movies, n_features] 原始票房特征 返回: [n_movies, n_features 64] return np.hstack([X_box_original, movie_embs]) # 在训练票房模型前调用 X_box_aug augment_boxoffice_features(movie_embeddings, X_box_train)这个技巧让票房模型 MAE 从 782 万降到 715 万——说明用户行为隐含的市场接受度信号比单纯的内容特征更有预测力。但注意movie_embeddings必须在推荐模型训练完成后固定不能在票房训练时反向传播否则破坏任务隔离。3.4 模型保存与加载确保生产环境可复现所有模型必须用joblib保存而非pickle版本兼容性差# src/utils/model_io.py import joblib def save_model(model, path): if hasattr(model, save_model): # LightGBM model.save_model(f{path}.txt) else: # GraphSAGE, Scaler, etc. joblib.dump(model, f{path}.pkl) def load_model(path): if os.path.exists(f{path}.txt): import lightgbm as lgb return lgb.Booster(model_filef{path}.txt) else: return joblib.load(f{path}.pkl)特别注意joblib保存的StandardScaler必须和训练时用的完全一致。如果在feature_schema.yaml中修改了budget的impute_strategy必须重新运行cleaner.py并重新训练 scaler否则load_model加载的 scaler 会用旧的中位数去 transform 新数据导致票房预测偏差 2000 万。4. 避坑指南那些让毕设答辩当场卡壳的 5 个真实问题4.1 现象推荐系统训练时 GPU 显存 OOM但nvidia-smi显示显存占用仅 30%原因DGL 默认启用pin_memoryTrue在数据加载器中将张量锁页内存导致显存碎片化。尤其当batch_size1024且图节点数 5 万时CUDA malloc 失败。解决在DataLoader初始化时显式关闭train_loader dgl.dataloading.DataLoader( g, train_nids, sampler, batch_size512, shuffleTrue, drop_lastFalse, pin_memoryFalse, # 关键 num_workers4 )4.2 现象票房预测验证集 MAE 突然从 750 万飙升到 1200 万但训练损失平稳原因genres字段在feature_schema.yaml中配置为tfidf_vectorize但TfidfVectorizer的max_features1000未在训练/验证/测试集上统一fit。验证集单独fit_transform会生成不同维度的向量。解决所有文本向量化必须在训练集上fit验证/测试集只transform# 正确做法在 train.py 中 from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer(max_features1000) X_train_tfidf vectorizer.fit_transform(train_genres) X_val_tfidf vectorizer.transform(val_genres) # 不是 fit_transform4.3 现象用scikit-learn的train_test_split划分数据后推荐系统在测试集上 Recall10 为 0原因train_test_split随机打乱导致测试集中的用户/电影在训练集中从未出现冷启动而 GraphSAGE 无法泛化到未见节点。解决必须用留一法Leave-One-Out或时间划分。本项目采用后者# 按 timestamp 划分确保测试集上映时间晚于训练集 ratings_df ratings_df.sort_values(timestamp) split_idx int(len(ratings_df) * 0.8) train_ratings ratings_df.iloc[:split_idx] test_ratings ratings_df.iloc[split_idx:]4.4 现象pip install -r requirements.txt报错dgl-cu113与torch 1.13.1cpu冲突原因requirements.txt中dgl1.1,1.2默认安装 CUDA 版但你的环境是 CPU-only。解决手动指定 CPU 版本并降级 DGL 兼容性pip uninstall dgl -y pip install dgl1.0.2 -f https://data.dgl.ai/wheels/repo.html # 注意DGL 1.0.2 是最后一个官方支持 CPU 的稳定版4.5 现象报告 PDF 中的图表中文显示为方块LaTeX 编译失败原因matplotlib默认字体不支持中文且report.tex中未声明 CJK 包。解决在src/visualization/plot_utils.py开头添加import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, DejaVu Sans] matplotlib.rcParams[axes.unicode_minus] False # 正常显示负号并在report.tex的导言区加入\usepackage{ctex} % 替代 \usepackage{CJK} \ctexset{section{name{第,章},numbertrue}}5. 毕设答辩必答如何证明你的推荐系统不是“伪智能”5.1 设计三组消融实验用数据说话不能只说“我用了 GraphSAGE”要证明它比基线好。本项目在experiments/ablation_study.py中固化了三组对比实验组推荐模型特征输入Recall10验证集提升幅度BaselineItem-CF仅评分矩阵0.0321—Ablation-1GraphSAGE仅评分矩阵0.041729.9%Ablation-2GraphSAGE评分矩阵 genres0.048350.2%Full ModelGraphSAGE评分矩阵 genres director_age0.052162.3%关键点每组实验必须固定随机种子、固定图采样器、固定学习率只变模型结构或特征。表格数据直接来自results/ablation.csv答辩时可现场打开展示。5.2 构建“可解释性热力图”让用户看懂为什么推这个评委最爱问“为什么给用户 A 推《流浪地球2》而不是《满江红》” 本项目用captum库实现 GNN 的特征归因# src/explainability/gnn_explainer.py from captum.attr import IntegratedGradients import torch def explain_recommendation(model, g, user_emb, movie_emb, target_movie_id): model.eval() # 输入用户嵌入 电影嵌入拼接 input_tensor torch.cat([user_emb, movie_emb[target_movie_id]], dim0) input_tensor.requires_grad True ig IntegratedGradients(model.forward) # 计算对目标电影的预测分数的梯度贡献 attr ig.attribute(input_tensor, target0, # 预测分数的索引 internal_batch_size32) return attr.abs().numpy() # 返回各维度重要性 # 输出示例[0.12, 0.03, 0.45, ..., 0.01] → 第 2 位genres贡献最大答辩时可现场演示输入用户 ID输出前 3 个最重要特征如genres_Action0.45,director_age_40-500.32,budget_log0.18直接回答“因为该用户历史偏好动作片且喜欢 40-50 岁导演而《流浪地球2》完美匹配”。5.3 用“对抗样本检测”证明模型鲁棒性防止被质疑“换几个数据就崩”。本项目在tests/test_robustness.py中实现扰动 1随机屏蔽 10% 的用户-电影交互边模拟数据丢失扰动 2将 5% 的genres标签替换为Unknown模拟标签错误扰动 3对budget添加 ±30% 噪声模拟财务数据误差。def test_robustness(model, g, X_box, y_box): metrics {} # 原始性能 metrics[original] evaluate(model, g, X_box, y_box) # 扰动 1删除 10% 边 g_perturbed remove_edges(g, ratio0.1) metrics[edge_drop] evaluate(model, g_perturbed, X_box, y_box) # 扰动 2污染 genres X_box_perturbed corrupt_genres(X_box, ratio0.05) metrics[genre_corrupt] evaluate(model, g, X_box_perturbed, y_box) return metrics # 要求所有扰动下 Recall10 下降 5%MAE 上升 15%答辩时只需展示test_robustness()的返回字典结论清晰“在 10% 数据丢失下推荐效果仅下降 3.2%证明系统具备工业级鲁棒性”。5.4 一个让我后悔三年的习惯永远在__main__里写if __name__ __main__:这是血泪教训。有次我把train.py的主逻辑写在模块顶层import train时自动触发训练导致在 Jupyter Notebook 里import utils就开始跑 2 小时训练还占着 GPU。现在所有可执行脚本都严格遵循# train.py def main(): args parse_args() data load_and_clean(args.data_path) model build_model(args.model_type) train_model(model, data) save_model(model, args.output_path) if __name__ __main__: main()这样既能直接python train.py --model graphsage运行也能在 notebook 里安全from train import main调用还能用pytest测试main()函数。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑