ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的深度学习新闻推荐系统:数据、模型与部署全解析

基于Python的深度学习新闻推荐系统:数据、模型与部署全解析 简介这是一份基于 Python 深度学习的新闻推荐系统毕业设计源码主要面向计算机相关专业的学生、毕业设计团队以及有课程设计需要的开发者覆盖新闻推荐系统的完整开发链路。项目经过严格调试评审分达 95 分以上可直接运行能帮助理解新闻推荐系统的整体架构、深度学习模型与前后端交互流程也有利于毕业设计答辩和系统演示。压缩包共 444 个文件大小约 8.44MB包含 py、pyc 等 Python 程序文件HTML、CSS、JS、PHP 等前后端资源以及 JSON、XML 配置、joblib、hdf5 模型文件和字体图片素材其中 CSS/HTML 负责页面展示PHP/JS 承载交互与接口逻辑py 文件对应模型训练与推荐算法目录清晰便于按模块查阅。目前已有 1231 人学习下载在同类毕业设计资源中具有较高参考价值。读者既可以参照其中的模型训练与数据处理方法也能复用前端展示和接口设计快速完成相关课题减少从零搭建系统的时间成本。1. 基于 Python 的深度学习新闻推荐系统毕业设计选题里的数据、模型与展示三角每年的毕业设计选题清单里基于 Python 的深度学习新闻推荐系统都算常青款。它热度高不是偶然新闻场景数据可爬可下深度学习模型层塞得下 TextCNN、双塔召回、注意力机制这些热门结构交付时又可以用 Flask 或 FastAPI 快速包装成网页 demo。真正的风险在信息差。拿到一套“完整源码”的同学最容易卡在三个地方数据格式对不上、训练循环拼不起来、指标不知道看哪个数。这篇文章把完整链路逐层拆开从数据预处理、双塔模型、训练评估到服务化展示让复现和答辩都能接得住。2. 数据与特征工程新闻推荐系统从原始语料到模型输入的必经环节推荐系统不能直接吃 HTML 页面文本、新闻属性、用户点击行为都要先转换成数值 id 和定长序列。很多毕业设计跑不出模型问题不在网络结构而在数据和标签没对齐。这一章先把数据口径定死后面所有代码都基于同一个 vocab 和 id 映射。2.1 数据集选型公开新闻数据集与自采数据的取舍毕业设计最常见的是两条路。第一条直接用公开新闻推荐数据集MIND 是微软发布的新闻推荐基准每条新闻都带 news_id、category、subcategory、title、abstract行为日志里保留完整的曝光与点击记录适合直接做监督训练Adressa 是挪威新闻平台的数据带更细的会话级浏览日志。第二条自己写爬虫抓取门户网站的新闻好处是特征自由发布时间、栏目、热度统计都能自己控制坏处是要额外处理正文去噪、去重和时间对齐。数据通道字段重点适合的实验方向主要代价MIND标题、摘要、类别、impression有监督召回与精排官方划分格式需要单独解析Adressa会话ID、阅读时长、事件序列序列建模与时间衰减原始日志清洗量大自建爬虫发布时间、栏目、点击量自定义特征与冷启动需要自己维护抓取和去重我的建议是毕设优先选公开数据集把省下来的时间留给模型和页面展示如果想在论文里突出数据处理能力再额外补一个自采小规模语料做对比实验。无论选哪种都要在代码里保留data/raw和data/processed两个目录原始数据只读预处理结果单独落盘方便后面回溯。2.2 新闻文本预处理切词、停用词与词表截断对中文新闻TextCNN 的输入通常是“标题 摘要”。标题信息密度高摘要能补充实体和背景正文太长反而会把卷积核淹没在无意义内容里。我一般把标题和摘要拼接后截断到 32 个 token这个长度对二元组、三元组卷积都够用。# preprocess.py import jieba STOPWORDS set(line.strip() for line in open(data/stopwords.txt, encodingutf-8)) def tokens_of(text: str, max_len: int 32) - list[str]: text text.lower().strip() words [w for w in jieba.cut(text) if w.strip() and w not in STOPWORDS] return words[:max_len] def build_vocab(news_list: list[dict], min_freq: int 5, max_vocab: int 50000): freq: dict[str, int] {} for item in news_list: for w in item[tokens]: freq[w] freq.get(w, 0) 1 vocab {pad: 0, unk: 1} for w, c in sorted(freq.items(), keylambda x: -x[1]): if c min_freq or len(vocab) max_vocab: break vocab[w] len(vocab) return vocab代码里有三个参数值得写进论文min_freq5负责过滤只出现一两次的噪声词max_vocab50000防止 embedding 层占满显存max_len32决定卷积窗口扫描长度。pad必须固定为 0因为后面 embedding 层的padding_idx0和序列掩码都要依赖这个约定。如果换了新语料先跑一遍词频统计再看要不要调min_freq别照抄别人的值。注意停用词表单独放在data/stopwords.txt里别在代码里硬编码一长串字符串答辩时会被问“来源是什么”。2.3 用户行为序列与负样本构造训练集不是一堆新闻 id 那么简单新闻推荐和电商推荐最大的差异是新闻时效性很强。一个用户在两天前看的新闻今天不一定还想看同类型所以行为序列要以“最近点击”为主。训练样本的结构是用户最近点击过的新闻序列作为输入特征当前曝光中被点击的新闻作为正样本同一曝光里没有点击的新闻作为负样本。# samples.py import random def make_train_samples(user_history: dict, behavior: list[dict], recent_pool: list[str], max_hist: int 30, neg_ratio: int 4) - list[dict]: samples [] for record in behavior: history [n for n in user_history[record[uid]] if n in recent_pool][-max_hist:] if len(history) 2 or record[click] 0: continue # 同一批曝光里没被点击的新闻 negatives [n for n in record[impression] if n ! record[news_id]] random.shuffle(negatives) negatives negatives[:neg_ratio] samples.append({ uid: record[uid], history: history, neg_news: negatives, pos_news: record[news_id], }) return samples正样本一次只取一条负样本取四条1:4 是推荐系统里比较稳妥的比例。负样本太少了模型学不到区分度太多了正样本被淹没训练会明显变慢。max_hist30既覆盖大部分用户的短期兴趣又不会让序列过长拖慢训练。如果日志量很大可以在生成样本时按 uid 做一次去重把同一个用户的重复行为合并掉减少无效计算。3. 模型搭建用 TextCNN 和双塔结构实现新闻推荐系统的召回与排序模型设计的目标是先用一个轻量召回模型做粗筛再用排序模型精排。新闻推荐尤其适合这个思路因为新闻库动辄几万条每次请求都让所有新闻和用户向量做交叉计算不现实。3.1 为什么是双塔结构把计算压力拆分到离线侧双塔就是两个独立的向量编码器user tower 把用户历史行为压缩成一个向量news tower 把单条新闻文本压缩成一个向量训练时用内积接近程度作为推荐分数。新闻向量可以在启动服务时预计算好用户向量等请求来了再算一次两者做向量内积就是全量召回。这套逻辑最关键的收益是在线只有一次加法级别的计算瓶颈被转移到离线预计算上毕设也能在单机跑起来。双塔的局限也很明显两个塔各自编码用户和新闻之间的交叉特征表达不了。所以业界常见做法是双塔做召回精排换成 MLP 结构把用户向量、新闻向量、用户和新闻的逐元素积拼在一起输入全连接层这就是论文里常写的“召回 精排”两阶段漏斗。阶段模型输入输出召回双塔内积用户历史、候选新闻Top-N 新闻 id精排两层 MLP双塔向量拼接点击概率打分3.2 TextCNN 新闻编码器卷积核尺寸与池化方式TextCNN 在短文本分类里非常成熟新闻标题 32 个 token 的长度正好适合它。实现时三个不同尺寸的卷积核并行扫过文本序列再对每个通道做 max pooling提取二元、三元、四元词组级别的局部特征。# model.py import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size: int, embed_dim: int 128, num_filters: int 128, kernels: tuple (2, 3, 4)): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k, paddingk // 2, biasFalse) for k in kernels ]) self.dropout nn.Dropout(0.2) self.out_dim len(kernels) * num_filters def forward(self, token_ids): # token_ids: [B, L]L 是截断后的文本长度 x self.embedding(token_ids) # [B, L, embed_dim] x x.transpose(1, 2) # [B, embed_dim, L] pooled [torch.relu(conv(x)).max(dim2).values for conv in self.convs] return self.dropout(torch.cat(pooled, dim1))kernels(2, 3, 4)对应词组窗口长度paddingk // 2让卷积输出长度不变再用max(dim2)把最显著的特征挑出来。biasFalse很关键padding 位置嵌入值为 0如果卷积层带偏置padding 位置也会产生非零输出max pooling 可能选到这些噪声值。词表小、数据量少时可以把embed_dim从 128 降到 64训练会更快效果不一定差。3.3 用户历史行为编码从平均池化到注意力加权用户历史里有 30 条新闻向量最简单的做法是取平均。平均池化实现简单但会把早期偶然点开的新闻和近期强意图新闻视为同等重要。注意力机制可以解决这个问题每条历史新闻计算一个权重模型自己学习“哪次点击更重要”。class UserEncoder(nn.Module): def __init__(self, news_encoder: nn.Module, hidden_dim: int 128): super().__init__() self.news_encoder news_encoder self.attention nn.Sequential( nn.Linear(news_encoder.out_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) def forward(self, history_ids): # history_ids: [B, H, L]H 是历史条数上限 B, H, L history_ids.shape hist_vec self.news_encoder(history_ids.view(B * H, L)).view(B, H, -1) mask (history_ids.sum(dim2) ! 0) # 全 0 表示填充位置 score self.attention(hist_vec).squeeze(-1) # [B, H] score score.masked_fill(~mask, float(-inf)) weight torch.softmax(score, dim1) # [B, H] return (hist_vec * weight.unsqueeze(-1)).sum(dim1)mask 计算依赖前面把 padding 设为全 0 的约定~mask会选中填充位置并把注意力分数设为负无穷softmax 之后权重就是 0填充不会污染用户向量。这里的hidden_dim128只控制注意力打分网络的宽度和 embedding 维度保持一致即可。3.4 召回与精排的衔接MLP 排序器怎么吃双塔特征召回阶段用户向量和新闻向量做完内积得到 Top-50 候选。精排阶段对每个候选把用户向量、候选新闻向量、逐元素乘积拼接起来输入一个两层 MLP输出点击概率。逐元素乘积能直接表达“用户兴趣向量与新闻向量在每一维上是否同向”比单纯拼接多一层显式交互。class RankModel(nn.Module): def __init__(self, feature_dim: int, hidden_dim: int 128): super().__init__() self.mlp nn.Sequential( nn.Linear(feature_dim * 3, hidden_dim), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden_dim, 1), ) def forward(self, user_vec, news_vec): cross user_vec * news_vec return self.mlp(torch.cat([user_vec, news_vec, cross], dim-1))特征维度是feature_dim * 3因为拼接了三个等长向量。训练精排时标签是点击或未点击损失函数用 binary cross-entropy。4. 训练、评估与深度学习环境配置新闻推荐系统的指标能写进毕业论文吗训练部分的代码不复杂但目标函数、评估切分和环境配置三个地方踩坑最多。这一章给的方案适合在单卡 GPU 上跑也适合 CPU 小规模验证。4.1 训练目标函数用 batch 内负采样做 softmax 多分类推荐召回模型常用的训练方式是“in-batch negative sampling”一个 batch 里有 N 个用户和 N 条正样本新闻把当前用户和其他用户的正样本新闻当作负样本。这样不用额外构造负样本训练速度也快。# train.py import torch import torch.nn.functional as F optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size2, gamma0.5) for epoch in range(3): for batch in train_loader: user_vec user_encoder(batch[history]) # [B, D] news_vec news_encoder(batch[pos_news]) # [B, D] logits user_vec news_vec.T / 0.07 # [B, B] labels torch.arange(B, devicelogits.device) loss F.cross_entropy(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()这里的 news_vec.T计算用户向量与 batch 内所有正样本新闻向量的内积对角线位置是正样本其他位置是负样本所以标签正好是[0, 1, ..., B-1]。温度0.07把所有内积分数放大让 softmax 输出更尖锐模型更容易区分正负样本。温度调大预测概率分布会变平调小容易让训练初期梯度爆炸0.05 到 0.1 属于常用区间。4.2 评估指标AUC、Recall10、NDCG10 怎么算毕设论文里只用 loss 曲线是站不住的还需要三个指标AUC 衡量任意正样本得分高于负样本的概率Recall10 看召回结果里有没有正确新闻NDCG10 进一步看正确新闻排的位置。评估时一定要单独留出时间上靠后的数据不能随机切分否则新闻的时间效应会造成信息泄漏指标虚高到不真实。# evaluate.py import numpy as np from sklearn.metrics import roc_auc_score def compute_metrics(scores, labels, k10): auc roc_auc_score(labels, scores) order np.argsort(scores)[::-1][:k] hit sum(1 for i in order if labels[i] 1) recall hit / max(1, sum(labels)) dcg sum((1 / np.log2(i 2)) for i, idx in enumerate(order) if labels[idx] 1) idcg sum((1 / np.log2(i 2)) for i in range(min(k, sum(labels)))) ndcg dcg / max(1e-9, idcg) return auc, recall, ndcg指标问题毕业设计里怎么看AUC整体排序是否靠谱明显高于 0.5且不掉点即可Recall10正确新闻是否进 Top-10召回率随候选集增大应提升NDCG10排序位置是否合理与 Recall 一起看避免“召回了但排后面”注意不同数据集、不同负样本比例下这些指标不可横向比较提交论文时把实验条件写在同一张表里。4.3 深度学习环境配置与训练资源控制如果你还在深度学习环境配置阶段建议先装 CPU 版 PyTorch用 1 万条新闻的小样本跑通全流程再决定上不上 GPU。Python 安装完成后pip install torch是起步操作。词表超过 10 万、训练轮次设 10 轮以上才是 GPU 真正发挥作用的场景。毕设规模下合理的配置是vocab_size50000、embed_dim64、num_filters64、max_len20、batch_size64、epoch 数 2 到 3 轮。训练时打开模型train()模式评估切到eval()并关掉 dropout这个切换漏掉会导致评估指标时高时低。5. 服务化展示把新闻推荐系统源码跑成一个可演示项目拿到题目里的源码后先别急着改模型。推荐先确认目录结构里有没有独立的预处理、训练、接口三层没有的话按这一章补出来。服务化的目标不是高并发而是给答辩老师一个可以点开看的网页 demo。5.1 离线预计算新闻塔向量一次性入库训练结束后新闻塔参数已经固定在线阶段不需要再重复跑全文编码。写一个独立脚本把所有新闻的向量算出来存成 npy 文件启动接口服务时直接加载。# build_news_index.py import numpy as np import torch news_encoder.load_state_dict(torch.load(model/news_encoder.pt)) news_encoder.eval() news_ids [] news_vectors [] with torch.no_grad(): for news_id, tokens in news_token_ids.items(): vec news_encoder(torch.tensor([tokens])).numpy()[0] news_ids.append(news_id) news_vectors.append(vec) np.save(model/news_ids.npy, np.array(news_ids)) np.save(model/news_vectors.npy, np.vstack(news_vectors))no_grad()必须加否则每一批向量都会计算梯度图显存占用翻倍。news_ids和news_vectors的索引顺序要严格对应后面召回结果靠下标反查 id。如果新闻数量达到百万级再把 npy 换成 FAISS 索引。5.2 在线推荐接口从用户历史到 Top-N 的完整链路在线接口用 FastAPI 写比 Flask 少一些模板代码且原生支持 Pydantic 请求模型。核心接口只做四件事查用户历史、算用户向量、全量内积、返回 Top-N。# api.py from fastapi import FastAPI from pydantic import BaseModel import numpy as np app FastAPI(titleNews Recsys Demo) class RecommendRequest(BaseModel): user_id: int topk: int 10 app.post(/api/recommend) def recommend(req: RecommendRequest): if req.user_id not in user_history: return {items: hot_news[: req.topk]} # 冷启动用户兜底 user_vec user_encoder(history_ids_of(req.user_id)) scores news_vectors user_vec # 简单时间衰减发布超过 3 天的新闻削弱分数 scores scores * np.array([0.5 if news_age[i] 3 else 1.0 for i in range(len(news_ids))]) top_idx np.argsort(scores)[::-1][: req.topk] return {items: [news_ids[i] for i in top_idx]}news_vectors user_vec一行完成全量与用户向量的内积计算返回所有新闻的相似度分数。时间衰减可以手工实现超过 3 天的新闻分数乘以 0.5。冷启动处理在这一步直接返回热门榜保证新用户进来页面不是空的。5.3 前端页面与数据存储的简化边界毕设不需要上 MySQL 和 Redis。用户行为存内存字典或者 CSV新闻数据存 JSON 序列化文件完全够用。前端用静态 HTML 加 fetch 请求调接口把返回的新闻列表渲染成表格。!-- templates/index.html -- script fetch(/api/recommend, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ user_id: 0, topk: 10 }) }) .then(res res.json()) .then(data { const list document.getElementById(rec-list); data.items.forEach(id { const row document.createElement(li); row.textContent id : news_title[id]; list.appendChild(row); }); }); /script启动时先运行uvicorn api:app --port 8000再打开浏览器访问静态页面。整个过程不依赖外部数据库答辩现场换一台机器也能复现。6. 冷启动、长尾与答辩准备新闻推荐系统源码收尾的三个动作模型能跑通只是合格想拿高分还得处理两个推荐系统真正难缠的问题新用户没有历史、推荐结果同质化。这两个问题也是答辩老师最爱追问的地方。6.1 新用户冷启动的兜底实现用户没有任何点击记录时推荐热门榜是最稳妥的回退策略。但热门榜不能简单按总点击量排否则一篇老新闻会一直霸榜。用“短时间窗口内曝光量足够大的新闻点击率”排序公式可以写成score (clicks alpha) / (impressions beta)alpha和beta取 1.0 或 2.0 做平滑防止曝光量很小时点击率虚高。再叠加一个时间衰减系数超过 48 小时的新闻自动降权。6.2 用 MMR 思路压制同质化推荐新闻推荐经常出现连续几条都是同一个分类的情况读起来像垂直频道而不是新闻首页。MMR最大边际相关能在相关性和多样性之间取平衡每次选择当前候选里评分最高、同时又和已选集合重合度最低的新闻。毕设里不必算向量相似度把“分类相同”当作重合度惩罚即可。候选新闻去重后按score - 0.6 * same_category_penalty重新排序选出前 10 条。6.3 答辩高频追问对应的三个参数答复追问回答思路embedding 维度为什么选 128新闻标题信息密度低64 到 128 足够表达语义维度再大容易在小语料上过拟合负样本为什么取 4 个减少负样本会让模型区分度不足5 到 8 个收益递减4 是训练速度和效果的平衡点TextCNN 和 BERT 怎么取舍BERT 表达能力强但训练推理成本高TextCNN 在小数据和短文本上能拿到接近的效果且可解释性好提交源码前把预处理、训练、预计算、启动接口四个命令按顺序跑一遍确认从原始 zip 解压到浏览器看到推荐列表全程不超过五分钟。压缩包里保留requirements.txt和README.md写明 Python 版本、依赖安装方式和每个脚本的入口参数这一步能直接避免答辩现场“跑不起来”的尴尬。本文还有配套的精品资源点击获取
返回列表