简介:基于用户行为和内容的个性化新闻推荐系统毕业设计项目,面向计算机专业毕业生与推荐系统开发者,提供一套结构完整、可直接运行的工程化源码,涵盖用户行为日志采集、内容特征提取、推荐算法实现等模块,适合作为毕业设计答辩演示或后续研究的基础框架。压缩包共2000个文件,大小21.38MB,以HTML、JavaScript、PHP、CSS为主,分别承担前端页面、交互逻辑、后端接口与界面样式;同时包含JSON、XML、SQL等数据与配置文档,以及Python脚本、Shell脚本用于辅助数据处理与环境部署。部分CSS/JS引用了Bootstrap、Froala Editor等成熟前端库,并含php_xxtea相关加密文件,提示具备一定安全处理逻辑。目前已有212人学习/下载,资源内容较为完整,解压后可见清晰目录结构,包括前端展示、后端服务、数据库脚本、README等文档,以及可配置的部署脚本,帮助使用者快速理解系统运行流程并进行二次开发。
1. 个性化新闻推荐系统:行为驱动与内容理解的混合架构
移动端信息流点击率上不去,很多团队第一反应是换更大的模型,但真正的问题往往出在数据通道上——用户划走、停留时长、点击序列这些行为信号没有实时进画像,内容侧又只做了关键词匹配,没做主题和实体层面的理解。所谓“基于用户行为和内容的个性化新闻推荐系统”,本质是把这两条线拧成一股绳:行为侧负责捕捉兴趣漂移,内容侧负责解决冷启动和语义泛化。适合正在做推荐方向毕业设计、或者刚接手新闻类App推荐模块的工程师参考,这套方案不依赖深度模型,基于Python和常规数据库就能落地,核心是召回、排序、解释三个环节怎么串起来。
2. 用户行为画像与内容画像的构建方法
2.1 行为日志采集的结构化设计
推荐系统最怕的不是没数据,而是数据散落在不同服务里没法对齐。常见的做法是设计一份统一的行为日志协议,用JSON或Avro序列化,由客户端SDK上报到消息队列。重要的一点是区分“曝光”和“阅读”两类行为,曝光数据用于计算真实点击率,阅读行为用于刻画兴趣深度。
{ "user_id": "u_10293", "news_id": "n_88451", "action_type": "read", "stay_seconds": 45, "scroll_depth": 0.8, "device": "android", "timestamp": 1710000000, "referrer": "rec_list", "position": 3 }关键参数在于stay_seconds和scroll_depth。模板里把阅读行为定义为停留超过3秒,这能过滤掉误触;停留超过30秒且滚动深度大于0.5的样本,在计算兴趣权重时系数翻倍,因为这类行为说明用户真的读进去了。referrer字段用来区分推荐来源和自然浏览来源,防止把用户主动搜索的行为混进推荐反馈里影响排序。
2.2 新闻内容画像:从关键词到主题分布
内容侧不能只做分词后计数,得构建两层结构。第一层是命名实体,比如人名、机构名、地名,用正则加词典就能覆盖主要场景,不需要上模型;第二层是主题分布,把新闻按体育、财经、科技、娱乐等大类做多标签分类,每个类别带一个置信度分数。这两层信息最终合并成一篇新闻的画像向量。
def build_news_profile(news_id, title_tokens, body_tokens, entity_list, cat_scores): return { "news_id": news_id, "entity_vec": dict(entity_list), # {"OpenAI": 0.8, "Sam_Altman": 0.6} "topic_vec": cat_scores, # {"tech": 0.72, "biz": 0.31} "content_embed": l2_normalize(body_tokens) # 用TF-IDF向量做归一化 }参数设计上,entity_vec的权重需要比topic_vec高,因为实体能精确匹配用户历史读过的具体对象,比如“巴以冲突”是主题,但“停火谈判”才是用户真正关心的实体事件。TF-IDF向量在计算相似度前必须做L2归一化,否则长文章天然比短文章相似度高,排序结果会偏掉。
2.3 用户画像的衰减机制与实时更新
用户兴趣是会过期的,新闻推荐的用户画像必须带时间衰减。我一般会把用户最近7天的行为视为高权重区间,30天前的行为统一降权到20%以下。具体实现时,维护一张用户-新闻交互表,每次计算用户兴趣向量时,用exp(-lambda * age)做衰减系数,lambda取0.05时,7天前的行为权重约为0.7,30天前约为0.22。
| 时间窗口 | 衰减系数 | 权重含义 |
|---|---|---|
| 1天内 | 1.0 | 完整保留,近期兴趣 |
| 3天 | 0.86 | 轻度衰减 |
| 7天 | 0.70 | 中期兴趣,仍有效 |
| 15天 | 0.47 | 显著衰减 |
| 30天 | 0.22 | 基本忽略,只保留统计特征 |
更新方式上,不要每次实时算全量用户画像,而是用增量更新:每收到一条阅读行为,就把这条新闻的画像向量按衰减后的权重叠加到用户向量上,再用Redis缓存用户向量的当前值。这样既能实时反映用户刚读完一篇科技新闻后的兴趣偏移,又不会把系统拖垮。
3. 召回层设计:行为相似度与内容相似度的双路召回
3.1 基于协同过滤的用户行为召回
召回阶段的目标是从全量新闻库里快速圈定候选集,通常在几百到几千的量级。行为召回有两种实现路径:基于物品的协同过滤(ItemCF)和基于用户的协同过滤(UserCF)。新闻场景下,ItemCF更实用,因为用户的阅读兴趣比购买兴趣更分散。
-- 计算新闻间的共现相似度 SELECT a.news_id AS news_a, b.news_id AS news_b, COUNT(DISTINCT a.user_id) AS cooccur_cnt FROM read_log a JOIN read_log b ON a.user_id = b.user_id WHERE a.news_id != b.news_id AND a.timestamp > unix_timestamp('2024-01-01') GROUP BY a.news_id, b.news_id HAVING cooccur_cnt >= 5这里用至少5个共同阅读用户作为共现阈值,太低了会把偶然共现的噪音带进来。得到新闻之间的共现关系后,用户对候选新闻的评分就是用户读过新闻与候选新闻相似度的加权求和。时间窗口限定在最近3个月,因为新闻的生命周期短,半年前的共现关系对现在没有参考价值。
3.2 基于TF-IDF与实体匹配的内容召回
内容召回的作用是兜底两个场景:新用户没有任何行为日志;以及行为召回覆盖不到的长尾新闻。实现思路是,把用户最近阅读的N篇新闻的画像向量取平均,作为“用户内容偏好向量”,然后与新闻库中每篇新闻的画像向量做余弦相似度计算。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import numpy as np def content_recall(user_recent_vec, news_vectors, top_k=200): # user_recent_vec: 用户最近阅读新闻的平均向量 # news_vectors: 全量新闻向量矩阵,shape = (N, dim) sim_scores = cosine_similarity([user_recent_vec], news_vectors)[0] top_indices = np.argsort(sim_scores)[::-1][:top_k] return top_indices, sim_scores[top_indices]核心参数是top_k,我一般设200,太小会丢失排序阶段的多样性,太大则给排序模型增加无谓计算。向量维度控制在300到500维之间,维度太低区分度不够,太高则存储和计算成本不成比例。实体匹配可以叠加在向量相似度之上,如果候选新闻包含用户画像中的高权重实体,直接加一个固定的分数加成,我习惯加0.1。
3.3 双路召回结果融合策略
双路召回的结果存在重叠与互补,融合时不能简单做并集。常见的做法是给两路结果打上来源标签,行为召回的结果带behavior标签,内容召回的结果带content标签,然后按分数交错排列。但要注意,行为召回的分数和内容召回的分数不在同一个量纲上,直接拼接会让排序模型学到错误的特征分布。
我建议把两路召回各自归一化到0到1区间,然后做加权求和。行为召回权重设为0.6,内容召回权重设为0.4,这是从经验出发的默认值。对于行为很少的用户,内容权重要动态调高,比如行为数量少于10条时,内容权重改为0.7。融合后的候选集进入排序阶段前,还需要去掉用户已经读过的新闻,这步用Bloom Filter处理,省内存且速度快。
4. 排序层实践:特征工程与轻量级点击率预估
4.1 排序特征的分层设计与构造
召回圈定候选集后,排序模型的任务是输出一个精准的点击概率。特征设计比模型选择更重要,一套可用的特征体系至少要覆盖用户侧、物品侧、交互侧三个维度。用户侧包括行为统计特征(近7天阅读量、平均停留时长)、兴趣分布特征(各主题类目占比);物品侧包括新闻热度(近1小时阅读量)、内容质量分(标题长度、图片数量);交互侧则要计算用户画像与新闻画像的余弦相似度。
| 特征类别 | 特征名 | 数据类型 | 说明 |
|---|---|---|---|
| 用户侧 | recent_read_cnt_7d | int | 近7天阅读文章数,反映活跃度 |
| 用户侧 | user_topic_entropy | float | 用户主题熵,低熵代表兴趣集中 |
| 物品侧 | news_hot_score_1h | float | 近1小时阅读量归一化,时效性代表 |
| 交互侧 | cos_sim_user_news | float | 用户向量与新闻向量余弦值 |
| 交互侧 | entity_overlap_cnt | int | 用户高权重实体与新闻实体的交集数 |
全部特征统一做分箱离散化会损失一定精度,我通常保留数值型特征不处理,交给GBDT类模型自动切分,但必须先做缺失值填充。对新用户来说,用户侧特征全部为0时,填充为用户画像的平均值,比如平均阅读量填充为5,主题熵填充为2.0,这样模型能在冷启动场景下学到“平均用户”的基准行为。
4.2 基于LightGBM的CTR预估模型
排序层不一定要上深度模型,在数据量级达到百万级之前,LightGBM就能取得不错的效果,而且训练快、可解释性强。特征重要性和SHAP值能直接告诉运营,哪些因素在驱动推荐结果。
import lightgbm as lgb train_data = lgb.Dataset(X_train, label=y_train) val_data = lgb.Dataset(X_val, label=y_val, reference=train_data) params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 63, # 叶子数,太大易过拟合 "min_data_in_leaf": 100, # 叶子最少样本数,防止学过细模式 "feature_fraction": 0.8, # 特征采样比例 "bagging_fraction": 0.8, "bagging_freq": 1, "lambda_l2": 1.0 } model = lgb.train(params, train_data, num_boost_round=500, valid_sets=[val_data], early_stopping_rounds=50)注意min_data_in_leaf对新闻推荐很重要,因为长尾新闻的样本量极少,如果阈值太低,模型会对小样本类目产生过拟合。feature_fraction设为0.8能降低特征间的共线性影响。Early stopping设定50轮,防过拟合的同时也控制了训练时间,10万级样本大概跑2分钟就能收敛。
4.3 排序结果重排:多样性、新颖度与道德约束
排序模型输出点击率后直接按分数下发的做法,容易让用户看到高度相似的内容。比如用户刷了两篇苹果发布会的新闻,后续几十条全是苹果,用户会迅速厌烦。重排阶段需要显式地控制类目占比,同一个细分类目在连续10条结果中最多出现4条,同一实体的新闻连续展示不能超过2条。
def rerank(candidates, max_cat_repeat=4, max_entity_repeat=2, window=10): result = [] cat_counter = {} entity_counter = {} for news in candidates: cid = news["cat_id"] ent = news["primary_entity"] if cat_counter.get(cid, 0) >= max_cat_repeat: continue if ent and entity_counter.get(ent, 0) >= max_entity_repeat: continue result.append(news) cat_counter[cid] = cat_counter.get(cid, 0) + 1 entity_counter[ent] = entity_counter.get(ent, 0) + 1 if len(result) == window: break return result这里用的是贪心重排,复杂度低且线上延迟可控。max_cat_repeat和max_entity_repeat两个参数直接控制多样性强度,数值调大则点击率可能提升但用户留存下降,调小则内容分散导致点击率下降。需要在AB测试里找平衡点。新闻推荐还有一个特殊要求:已经标记为低质量或包含不实信息的新闻,必须在重排阶段无条件过滤,这个不能用权重处理,要硬性拦截。
5. 线上服务架构与性能优化
5.1 推荐服务的实时链路设计
线下训练好的模型需要部署到线上服务。一个完整的推荐请求链路包含三个环节:读取用户画像、召回候选集、排序输出。用户画像是热点数据,放在Redis里,Key设计为ua:{user_id},带上最后更新时间戳。候选集存储在召回结果缓存中,Key设计为rec:{user_id},每10分钟失效重建一次。
# 推荐服务启动脚本核心配置 redis-cli set ua:u_10293 '{"topic_weight": {...}, "entity_weight": {...}}' EX 600Redis的EX 600表示用户画像缓存10分钟过期,这个时间窗口既能保证实时性,又不会因为频繁重建给上游数据库压力。如果用户刚刚读完一篇文章触发画像更新,也可以主动失效这个Key强制刷新,代价是下一次请求会有轻微的延迟增加。
5.2 冷启动:新用户与突发新闻的处理策略
新用户没有行为日志,画像为空,无法计算相似度。常见做法是给冷启动用户推热门新闻,但这会导致马太效应——头部新闻越来越热,长尾内容失去曝光。我习惯用“试探性推荐”策略:新用户的前10条推荐里混合高热度新闻和长尾新闻,比例7比3,用一两天时间积累用户行为。
突发新闻的处理是新闻推荐系统的独特难点。一篇刚发布的突发新闻,初始热度低、没有用户行为,内容召回和协同过滤都覆盖不到,需要手动或规则化地把这类新闻插入召回结果顶部。如果检测到新闻标题里含有关键词“突发”“快讯”“直播”,且发布10分钟内阅读量增速超过阈值,就在所有用户的候选集里插入该新闻,插入位置控制在第三到第五位,避免完全打乱个性化排序。
5.3 延迟优化与缓存降级
线上推荐请求必须在200毫秒内返回,否则用户体验会明显下降。优化手段按成本从低到高排序:Redis缓存画像、召回结果本地缓存、排序模型特征并行计算。前两个环节在架构上已覆盖,第三个需要把用户侧特征和物品侧特征的获取改成并发请求,Python里用concurrent.futures即可。
from concurrent.futures import ThreadPoolExecutor def fetch_features(user_id, news_ids): with ThreadPoolExecutor(max_workers=8) as executor: user_future = executor.submit(load_user_features, user_id) news_futures = [executor.submit(load_news_features, nid) for nid in news_ids] user_feat = user_future.result() news_feats = [f.result() for f in news_futures] return user_feat, news_feats线程池max_workers=8是个经验值,线程太多会触发GIL竞争,反而降低效率;这里每个新闻特征加载是IO密集型操作,线程池比进程池更合适。缓存降级策略要有预案:Redis挂掉时,直接跳过用户画像读取,用用户最近一次缓存在本地的行为快照代替,这个快照在客户端登录时拉取,降级只损失兴趣实时性,不丢失服务能力。
6. 线上验证技巧:AB测试的配置与效果评估
6.1 AB测试分流与实验配置
推荐系统上线前必须经过AB测试,否则无法判断排序模型改动是真实收益还是随机波动。分流原则是用户维度稳定哈希,保证同一用户始终进同一个实验组,避免用户在不同组之间跳来跳去让指标无法归因。
def hash_assign(user_id, bucket_count=100, experiment_version=5): hash_val = int(hashlib.md5(f"{user_id}:{experiment_version}".encode()).hexdigest()[:8], 16) return hash_val % bucket_count版本号experiment_version是一个关键的实验标识,每次新实验用新的版本号,避免不同期实验复用同一批用户的实验状态导致交叉污染。默认的分桶策略是80%进对照组,20%进实验组,这个比例可以根据业务风险调整,风险越高实验组占比越小。
6.2 离线评估与在线指标的完整视角
离线评估用AUC衡量排序能力时,要特别注意正样本的定义。新闻推荐里用户曝光了没点不算负样本,因为你不知道用户没点是因为不感兴趣还是没看到。我建议只把“曝光且停留超过0.5秒”的样本归为可训练样本,把停留低于0.5秒的全部丢弃。
在线指标分为质量指标和体验指标两层,阅读量、阅读时长和点击率反映推荐质量;人均刷新次数、次日留存率反映整体体验是否受损。点击率提升但人均刷新次数下降的组合,说明推荐结果变“黏人”但视野变窄了,长期看会伤害用户的新鲜感。重排参数的一次调整,至少观察三天数据,不能只看高峰时段的指标。
6.3 系统白盒验证:画像更新与实时推荐自检
模型是否在正确工作,可以在线上做一次白盒实验来验证。选一个测试账号,连续点击三篇关于“新能源车企”的新闻报道,然后查看这个账号在Redis里的画像实体权重是否新增了相关实体;再刷新推荐列表,观察前10条结果的主题集中度。如果画像权重更新了但推荐结果没变化,问题可能出在召回缓存——确认一下rec:{user_id}的过期时间是否配置正确。
用日志回放也能验证全链路:取某用户过去一周的点击序列,分别喂给当前线上版本和新版模型,对比新模型生成结果中用户真实点击新闻的排名位置。排名从第100提升到第30,说明改动有效,可以灰度放量;如果排名没有明显变化,就在特征层面看哪些特征权重影响了排序结果,用模型的feature_importance()输出排查哪个特征没生效。
本文还有配套的精品资源,点击获取