简介一份基于Python深度学习的新闻推荐系统毕业设计源码面向计算机相关专业学生与从业者适合用于毕业设计、期末课程设计或推荐系统实战入门帮助你快速掌握新闻推荐场景下的深度学习应用与Web系统集成方法。压缩包共444个文件、约8.44MB内容涵盖Python核心源码、HTML/CSS/JS前端页面、PHP服务端接口、模型持久化文件joblib/hdf5以及配置说明文档等从数据准备、模型训练、推荐逻辑到前端交互形成了完整可运行的项目闭环便于直接部署演示或二次开发。目前已有1235人学习下载该项目为个人原创毕业设计且评审分达95分以上经作者严格调试确保稳定运行读者不仅能获得一套可复用的推荐系统模板还能学习到新闻内容特征处理、用户行为建模、深度学习排序策略等关键细节同时通过清晰的目录结构和注释快速定位各模块为独立完成毕设或课程大作业提供有力支撑。1. 拿到深度学习新闻推荐系统源码之后先别急着解压跑通把一个命名成“基于python的深度学习的新闻推荐系统源码毕业设计”的 zip 包交到你手上最常见的翻车方式不是模型训不出来而是解压后对着十几个 Python 文件不知道从哪一步开始。这类毕业设计源码的典型构成是数据处理脚本、召回模型、排序模型、离线评估和一个小型展示界面。它要解决的问题很直接——给用户从大量新闻里挑出他可能感兴趣的 TopN 条核心是用户兴趣建模和新闻冷启动。很多同学拿到包后的第一反应是“先装依赖、跑通 demo”但我的建议相反先别急着跑。先花半小时把目录结构走一遍搞清楚每份脚本在数据流里处于哪个位置。这篇文章会按一线落地的方式把这个系统从选型、数据、特征、模型训练到坑位排查拆开讲清楚。如果你正在做毕业设计或者刚入行推荐想找个练手方向照着这个思路走比直接改别人的代码要省力得多。2. 系统架构与数据流双塔召回加深度排序为什么是毕业设计的安全牌新闻推荐和电商推荐有一个本质差异新闻的时效性极强一条热点新闻的生命周期可能只有几个小时用户兴趣也随时在漂移。你不可能像卖商品那样把 item 特征固化下来。所以新闻推荐里最常见的工程方案是“召回 排序”两段式结构召回阶段从几十万篇新闻里粗筛出几百个候选排序阶段用深度模型对候选做精排。这个结构既能在线上保证延迟又能让每个模块单独迭代对毕业设计来说也是最容易讲清楚、最容易答辩的结构。2.1 选型理由从协同过滤到双塔召回新闻场景的必然选择传统的协同过滤在新闻场景里有两个硬伤一是 user-item 共现矩阵极度稀疏大多数用户只点击过几十条新闻算不出可靠的相似度二是冷启动问题新新闻没有行为记录协同过滤永远推不出去。所以新闻推荐基本不会把协同过滤当主力而是用内容向量加行为序列来做用户兴趣表示。常见做法是双塔召回结构也叫 DSSM 风格的用户塔和新闻塔。用户塔输入用户行为序列、统计特征输出一个用户向量新闻塔输入新闻标题、正文、类目输出一个新闻向量。训练时让点击过的 pair 在向量空间里靠近没点击过的远离。线上使用时新闻向量可以提前算好灌入向量索引用户向量实时计算一次内积检索就能拿到候选集。这个方案对毕设来说是个安全牌模块边界清楚、训练稳定、可视化也好解释。排序阶段则用深度 CTR 模型输入用户向量、新闻向量和交叉特征输出点击概率。整体上召回保证“把对的新闻找回来”排序保证“把最对的新闻排在最前面”。这两部分合起来才是一个完整的新闻推荐系统而 zip 包里的所有代码基本都围绕这条链路展开。2.2 数据流与样本构造用户行为日志如何变成模型输入拿到源码后第一步是确认数据流。一套完整的训练数据包括三张表用户行为日志、新闻内容表、用户画像表。日志表里最关键的四列是 user_id、news_id、behavior 和 timestamp。很多毕设源码不会直接给你原始数据而是给一个处理好的 CSV但你要明白这些 CSV 是怎么来的因为答辩时老师大概率会问。我一般会先写一个样本构造脚本把点击日志转成模型可用的正负样本。新闻推荐里负样本的构造方式直接决定模型能不能收敛。随机采样是底线方案更好的做法是从“曝光未点击”里采负样本。但很多公开日志里没有曝光数据那就只能全局随机采样这时候要把正负样本比例控制在 1:4 左右否则模型会严重偏向负类。import pandas as pd import numpy as np def build_train_samples(log_path, item_path, neg_ratio4, min_click3): # 读取用户行为日志user_id, news_id, behavior, timestamp logs pd.read_csv(log_path) logs logs[logs[behavior] click].drop_duplicates( [user_id, news_id, timestamp] ) # 过滤行为数过少的用户避免训练样本太稀疏 cnt logs.groupby(user_id)[news_id].nunique() valid_users cnt[cnt min_click].index logs logs[logs[user_id].isin(valid_users)].copy() logs.sort_values([user_id, timestamp], inplaceTrue) # 点击过的当作正样本 logs[label] 1 pos logs[[user_id, news_id, label]] # 负样本随机采样用户没点过的新闻比例由 neg_ratio 控制 item_pool pd.read_csv(item_path)[news_id].unique() neg_list [] for uid, group in pos.groupby(user_id): clicked set(group[news_id]) candidates [n for n in item_pool if n not in clicked] if len(candidates) neg_ratio: neg_list.append(pd.DataFrame({ user_id: uid, news_id: np.random.choice(candidates, neg_ratio, replaceFalse), label: 0 })) neg pd.concat(neg_list, ignore_indexTrue) return pd.concat([pos, neg], ignore_indexTrue)这段代码的核心逻辑是先按用户去重并过滤行为过少的用户再做负采样。neg_ratio是正负样本比例毕设项目里建议调成 4也就是一份正样本配四份负样本。min_click控制每个用户的最少点击次数过滤掉只点过一次的用户这类用户的行为序列太短学不出稳定的兴趣表示。注意这里负采样用的是全量新闻池随机采样实际效果和曝光负样本有差距但如果日志里没有曝光字段这已经是最稳妥的选择。数据做好后下一步就是把新闻文本转成向量。这个环节处理得好不好直接决定双塔召回的上限。3. 新闻文本与用户特征工程从 jieba 分词到行为序列向量新闻推荐里文本是核心信号。一条新闻的标题往往就决定了用户点不点它所以特征工程的第一个重点是把标题和正文变成稠密向量。另一个重点是用户行为序列——用户最近点过的新闻列表代表他的短期兴趣。这两块特征会分别进入新闻塔和用户塔特征质量不够模型结构再花哨也救不回来。3.1 标题和正文的向量化word2vec 训练与序列 padding处理新闻文本的第一步是分词。中文场景下 jieba 是默认选择但有个细节容易被忽略jieba 的默认词典对新闻里的网络新词、人名、缩写覆盖很差。常见做法是加载一个额外的自定义词典把新闻类目里的高频实体词加进去。分词之后必须去停用词否则“的”“了”“在”这类词会把向量平均池化的结果冲淡。分词完成后词向量可以选择现成的预训练词向量也可以用自己的新闻语料训练 word2vec。我建议毕设项目里自己训练一个轻量版 word2vec原因有二一是公开新闻语料和你手头的新闻数据集领域不完全匹配二是自己训练的词表可以直接覆盖你数据里的专有名词。向量维度设 128 就够用窗口设 5min_count 设 3低频词直接丢弃。import jieba import numpy as np from gensim.models import Word2Vec STOP_WORDS set(open(data/stopwords.txt, encodingutf-8).read().split()) def tokenize(text): # 分词并过滤停用词与空白字符 return [w for w in jieba.cut(text) if w.strip() and w not in STOP_WORDS] def train_w2v(sentences_path, vec_dim128, window5, min_count3): # 按行读取新闻文本逐行分词后训练 word2vec sentences [] with open(sentences_path, encodingutf-8) as f: for line in f: sentences.append(tokenize(line.strip())) model Word2Vec(sentences, vector_sizevec_dim, windowwindow, min_countmin_count, workers4, sg1) return model def title_embedding(title, model, dim128): # 标题向量 所有词向量的平均池化 words tokenize(title) vecs [model.wv[w] for w in words if w in model.wv] if not vecs: return np.zeros(dim) return np.mean(vecs, axis0)这里sg1表示用 skip-gram 训练对低频词更友好适合新闻这种关键词稀疏的场景。title_embedding里做的是平均池化这也是最容易踩坑的点如果一条标题的所有词都不在词表里函数会返回全零向量。一个全零的新闻向量进入双塔后模型会把它当成“无信息”样本导致这条新闻永远得不到推荐机会。所以词表的 min_count 不要设太高宁留噪声不留空向量。3.2 用户行为序列的构造多兴趣与时间衰减用户兴趣不是静止的。用户昨天看体育新闻今天可能因为突发事件转向时政。所以构造用户向量时必须对行为序列做时间衰减——越近的点击权重越大。常见做法是取用户最近 50 条点击新闻按时间衰减系数加权平均衰减系数通常取 0.95 到 0.99 之间。另一个细节是行为序列的去重。用户可能在十分钟内反复点击同一条新闻如果不做去重这条新闻的向量会在用户兴趣里被放大好几倍。我的做法是同一个 session 内只保留一次点击session 切分按 30 分钟无操作为边界。处理完这些之后用户侧输入就是一个 128 维的稠密向量。def build_user_vec(user_seqs, news_vec_map, max_len50, decay0.98): # user_seqs: {user_id: [news_id, ...]}, news_vec_map: {news_id: 128维向量} user_vecs {} for uid, seq in user_seqs.items(): seq seq[-max_len:] # 只保留最近 max_len 条行为 if not seq: user_vecs[uid] np.zeros(128) continue # 时间衰减权重越靠近当前时间的点击权重越大 w np.array([decay ** (len(seq) - i) for i in range(len(seq))]) w w / w.sum() vecs np.array([news_vec_map[nid] for nid in seq if nid in news_vec_map]) if len(vecs) 0: user_vecs[uid] np.zeros(128) else: user_vecs[uid] (vecs * w[:, None]).sum(axis0) return user_vecs这段代码的核心在权重计算decay ** (len(seq) - i)让最近的点击拿到最大权重最早的点击权重指数级衰减。max_len设 50 是因为超过 50 条历史行为后更早的信息对当前兴趣几乎没有贡献。实际调参时你会发现decay从 0.98 调到 0.95对短期热点新闻的推荐效果有明显改善但长期兴趣类新闻的覆盖率会下降。这个参数是用户兴趣“短期 vs 长期”的调节旋钮答辩时可以拿它当分析点。4. 模型训练与召回排序双塔加 CTR 排序的落地代码特征准备好之后就进入整个毕业设计最核心的部分模型训练。我强烈建议把模型拆成两个独立的文件——召回模型和排序模型。召回模型用双塔结构产出候选集排序模型在候选集上做精排。两个模型分开训练、分开评估出了问题也容易定位。很多 zip 源码喜欢把两个模型揉在一个脚本里看起来省事但调试时你会后悔。4.1 双塔召回训练与候选集生成双塔召回的结构本身不复杂用户塔吃用户行为向量和统计特征新闻塔吃标题向量和类目 embedding两个塔各自过几层全连接最终都输出 64 维向量用向量内积算相似度。训练时用 binary crossentropy 作为损失函数正样本的相似度往 1 拉负样本的相似度往 0 压。这里有个容易被忽视的细节双塔输出的向量必须做 L2 归一化。如果不做归一化内积的大小会被向量模长主导热门新闻的向量模长普遍偏大模型会倾向于推荐热门内容而不是用户真正感兴趣的内容。归一化之后内积等价于余弦相似度模长信息被去掉个性化效果会明显提升。import tensorflow as tf from tensorflow.keras import layers, Model USER_DIM 128 # 用户侧输入维度即用户向量的维度 NEWS_DIM 128 # 新闻侧输入维度即标题向量的维度 EMB_DIM 64 # 双塔最终输出的向量维度 def build_tower(input_dim, tower_name): inp layers.Input(shape(input_dim,), nametower_name _input) x layers.Dense(256, activationrelu)(inp) x layers.Dropout(0.2)(x) x layers.Dense(128, activationrelu)(x) x layers.Dense(EMB_DIM, activationNone)(x) x layers.Lambda(lambda v: tf.math.l2_normalize(v, axis1))(x) return Model(inp, x, nametower_name) user_tower build_tower(USER_DIM, user_tower) news_tower build_tower(NEWS_DIM, news_tower) user_in layers.Input(shape(USER_DIM,)) news_in layers.Input(shape(NEWS_DIM,)) u_vec user_tower(user_in) n_vec news_tower(news_in) score layers.Dot(axes1, normalizeTrue)([u_vec, n_vec]) model Model([user_in, news_in], score) model.compile(optimizertf.keras.optimizers.Adam(1e-3), lossbinary_crossentropy, metrics[AUC])训练完成后新闻塔要对全量新闻跑一遍拿到所有新闻的向量然后用 faiss 建索引。faiss 是向量检索的事实标准用 CPU 跑几十万条新闻的检索也只需要几十毫秒。线上推荐时先用用户塔算用户向量再去 faiss 索引里搜 TopK 个候选。这里的 TopK 一般设 300 到 500太小会漏掉长尾兴趣太大会给排序模型增加负担。import faiss # 全量新闻向量建索引IndexFlatIP 表示内积检索 news_emb news_tower.predict(news_feature_all) # shape: [N, EMB_DIM] index faiss.IndexFlatIP(EMB_DIM) index.add(news_emb.astype(float32)) # 用户向量检索 Top500 候选 user_emb user_tower.predict(user_feature_batch) # shape: [B, EMB_DIM] scores, idx index.search(user_emb.astype(float32), 500)注意IndexFlatIP是暴力检索数据量超过百万时建议换成IndexIVFFlat或IndexHNSWFlat。毕设场景下几十万条新闻用IndexFlatIP完全够用而且实现最简单不容易出 bug。4.2 深度排序模型与核心参数调节召回拿到 500 条候选后排序模型的任务是从中挑出最可能被点击的 TopN 条。排序模型的输入特征是用户向量、新闻向量、余弦相似度、类目匹配度、新闻热度等。模型结构不需要太复杂一个三层 MLP 就能达到不错的效果。排序模型最容易遇到的问题不是结构而是特征泄漏。举例来说如果把“这条新闻是否被用户点击过”的直接统计特征放进排序特征里离线 AUC 会高得离谱但上线后特征拿不到模型直接崩。我见过某开发者的毕设源码里排序特征包含“用户过去点击该类目的次数”这类特征不叫泄漏它本身就是用户兴趣特征可以保留。真正的泄漏是“用户对该新闻的点击次数”这种训练时有、线上没有的特征。def build_sort_model(feat_dim): inp layers.Input(shape(feat_dim,)) x layers.Dense(128, activationrelu)(inp) x layers.BatchNormalization()(x) x layers.Dropout(0.2)(x) x layers.Dense(64, activationrelu)(x) x layers.Dense(1, activationsigmoid)(x) model Model(inp, x) model.compile(optimizertf.keras.optimizers.Adam(5e-4), lossbinary_crossentropy, metrics[AUC]) return model排序模型训练时负样本要从召回的候选集里采而不是从全量新闻里随机采。原因很简单排序模型只需要学会在候选集内分好坏不需要学会区分“用户感兴趣的新闻”和“用户完全不感兴趣的新闻”。如果你用随机新闻当排序负样本模型会走捷径AUC 虚高但线上效果惨不忍睹。这个坑在毕设答辩时经常被问到你能答上来就是加分项。双塔召回和排序模型的几个关键参数我按踩坑后的常用值列在下面你可以根据自己的数据集微调。参数召回模型常用值排序模型常用值调节方向embedding 维度6464维度越大表达力越强但训练数据少时容易过拟合学习率1e-35e-4排序模型特征更复杂学习率要更低batch size256128显存不足时优先降 batch sizeDropout0.20.2过拟合明显时调到 0.3训练轮数5~85~8用验证集 AUC 判断早停5. 毕业设计源码落地的 5 个常见坑从导入报错到模型不收敛这一章是血泪经验汇总。我帮人排查过不少新闻推荐源码发现很多问题不是模型设计不行而是卡在一些很基础的坑上。每一条我都按“现象、原因、解决”来写你遇到类似报错时可以直接对照。5.1 分词报错缺少自定义词典与停用词表现象运行预处理脚本时jieba 报错找不到dict.txt或者停用词表路径指向一个不存在的文件程序直接崩溃。原因zip 包里的代码把词典和停用词表写成了绝对路径比如/home/user/data/stopwords.txt换到你的机器上路径就失效了。还有的情况是源码打包时只打了.py文件资源文件漏掉了。解决强烈建议把所有资源路径改成相对路径用os.path.join拼接以项目根目录为基准。资源文件缺失时不要硬编码一个路径而是在代码里处理异常提示用户放置文件的位置。我一般会在项目里建一个data/目录里面放停用词表、自定义词典、原始日志路径统一用项目根目录动态拼接。5.2 排序模型预测全为“0”或“1”正负样本比例失衡现象排序模型训练完对测试集的预测概率几乎全部小于 0.1 或全部大于 0.9AUC 却很高但推荐结果明显不合理。原因典型的正负样本比例失衡。如果负样本是随机从全量新闻里采的模型很容易找到一个“廉价特征”把正负样本分开导致预测概率集中在两端。另外如果负样本数远大于正样本数模型会默认所有样本都是负类。解决先确认正负样本比例建议控制在 1:4 以内。然后看负样本来源排序模型的负样本必须从召回候选集里采不能从全量新闻里随机采。你可以打印一批预测概率的分布如果呈 U 型说明特征里存在泄漏或样本构造有问题。5.3 CPU 训练卡死embedding 维度与 batch size 设置不当现象训练开始后内存持续上涨几个小时后还在第一个 epochCPU 占用 100%看起来像死机了。原因很多源码把新闻 ID 和用户 ID 直接塞进 Embedding 层词表几万embedding 维度设成 256 甚至 512参数量爆炸。再加上 batch size 设成 1024CPU 训练时每个 batch 的计算量巨大。解决第一种做法是放弃原始 ID embedding改用我们已经算好的新闻向量和用户向量也就是前文提到的双塔结构输入维度只有 128参数量小了几个数量级。如果坚持用 ID embedding词表截断到 5 万以内embedding 维度 64batch size 降到 128。训练时密切观察内存变化内存只涨不降说明有数据加载泄漏。5.4 新发布的新闻永远推荐不出去冷启动被忽略现象模型上线后新发布的新闻没有任何曝光所有推荐结果都是老新闻。原因召回阶段的新新闻向量是算出来了但用户行为序列里从来没有它向量相似度普遍偏低根本进不了 TopK。排序阶段更严重——新新闻没有点击历史热度特征是 0排序模型会给它打很低的分数。解决两步走。召回阶段加一个“类目热门兜底”通道每个用户的 TopK 里有 10% 到 20% 的候选来自他感兴趣类目的热门新新闻。排序阶段给新新闻一个“新鲜度先验分”比如发布时间越近基础分越高。毕设里不需要做得太复杂但必须让新新闻有机会曝光否则用户刷到的永远是旧闻体验很差。5.5 离线 AUC 高但线上效果差数据集划分方式不对现象离线评估 AUC 达到 0.9 以上看起来模型很优秀但上线后点击率却不如一个简单的热门榜。原因数据集划分时用了随机打散而不是按时间划分。随机划分会导致同一条新闻的样本同时出现在训练集和测试集里模型在测试集上“见过”这批新闻AUC 虚高。这不是模型好是数据泄漏。解决必须按时间划分数据集。比如取前 80% 时间段的日志当训练集后 20% 当测试集。如果日志里有用户 id还要保证训练集和测试集的用户不重叠否则模型记住了用户 id 的向量测试时遇到同一批用户效果虚高。按时间划分后AUC 可能会掉到 0.75 左右这个数字才是接近线上真实水平的值。6. 上线前的最后一公里离线评估、增量更新与最小 API 封装模型训练完不等于项目完事你还需要一个可验证、可展示的闭环。毕设答辩时老师不只看模型结构更看你怎么评估、怎么上线。我习惯把最后一步做成三件事评估脚本、增量训练、一个最小可用的推荐接口三件事都不复杂但能让整个项目完整度上一个台阶。先写一个评估脚本。排序模型用 AUC召回模型用 RecallK这两个指标是推荐系统评估的地基。注意 RecallK 在召回阶段算K 取 10 或 50看你的 TopK 设置。from sklearn.metrics import roc_auc_score def evaluate(gold_list, pred_list): # gold_list: 真实标签pred_list: 模型预测的点击概率 auc roc_auc_score(gold_list, pred_list) # Recall10: 每个用户的推荐列表里是否包含真正点击的新闻 hits 0 for user_recs, pos_item in zip(cand_recs, true_items): if pos_item in user_recs[:10]: hits 1 recall hits / len(true_items) return auc, recall增量训练这块新闻推荐和商品推荐不同热点更替太快不可能一套模型跑一个月。我的做法是保留训练好的 word2vec 词表每天或每周用新增日志做增量训练。word2vec 的min_alpha设小一点比如 0.0001避免增量训练把原有词向量冲散。双塔模型同样用增量日志继续训练几个 epoch保存检查点。注意负采样池也要跟着更新否则新新闻仍然没有曝光机会。最后封装一个最小 API用 Flask 暴露一个/recommend接口接收 user_id返回推荐新闻列表。这个接口不用做得很复杂能打通链路就好。from flask import Flask, request, jsonify app Flask(__name__) app.route(/recommend, methods[GET]) def recommend(): uid request.args.get(user_id) u_vec load_user_vec(uid) # 读取或实时计算用户向量 rec_ids search(u_vec, top_k10) # faiss 召回 排序模型打分 return jsonify({user_id: uid, news_list: rec_ids})这个最小 API 的价值在于让整个系统从“脚本集合”变成“可演示的推荐系统”。你可以在答辩现场输入一个用户 ID看到实时返回的推荐结果比贴几张训练曲线有说服力得多。我自己的习惯是每次训练前先打印一个 batch 的输入 shape 和前向输出确认数据流没问题再开完整训练这个习惯帮我省下了无数个小时的等待。遇到 AUC 虚高时先怀疑数据集划分再怀疑特征泄漏最后才怀疑模型结构。希望帮到你。本文还有配套的精品资源点击获取