新闻推荐系统毕业设计:UserCF协同过滤从原理到落地实现
2026/9/23 14:18:39 网站建设 项目流程

简介:这是一套基于协同过滤算法的新闻推荐系统毕业设计项目,面向计算机类专业学生及推荐系统入门开发者,旨在解决从新闻数据采集与清洗、用户行为矩阵构建到个性化推荐算法落地的全流程问题。压缩包共146个文件,整体大小仅577KB,包含Vue前端页面、Java/Scala/Python后端代码、XML与Properties配置、SQL初始化脚本、Dockerfile等,覆盖了前后端交互、推荐服务、数据库存储和容器化部署等模块。目前已有353人学习下载,资源内还提供说明文档、parquet示例数据与辅助脚本,便于对照源码复现实验、撰写毕业设计文档或进行二次扩展。算法上实现了基于用户与基于物品两种协同过滤方式,涉及相似度计算、Top-N推荐、评分矩阵处理等关键步骤;整体代码结构清晰,目录按功能模块拆分,可快速定位训练脚本、数据处理逻辑与前端界面。此外,包内保留数据样例与说明,方便替换数据或调整参数做效果对比,适合作为课程设计或毕设项目参考。

1. 这个毕设题目的真实分量:算法是骨架,数据流才是血肉

新闻推荐系统是推荐算法里最“挑数据”的场景之一。协同过滤本身不复杂,核心就是“物以类聚、人以群分”,但放到新闻场景里,会发现用户兴趣变化快、物品(新闻)生命周期短、冷启动问题比电商严重得多。很多同学把精力全花在调相似度公式上,结果项目一跑起来,死在全是在处理空列表、脏数据和内存溢出——这恰恰是这类毕业设计的真实面貌。

这个题目适合两类人:一类是推荐系统方向、想用Python把协同过滤从原理到落地走通一遍的学生;另一类是已经在做Web开发、想往算法工程方向靠的从业者。它的价值不在“协同过滤”这个词本身,而在于你被迫处理真实数据流:爬新闻、清洗正文、建用户行为表、算相似度矩阵、生成推荐列表、再想办法验证效果。这套链路才是答辩时最值钱的东西,也是面试官真正会问的细节。

本文按照“选型 → 数据准备 → 核心实现 → 排查优化”的顺序展开,最后会给出几个让系统更像“产品”而非“课程作业”的进阶做法。如果你准备动手做这个题目,这篇文章能帮你少走至少两周弯路。

2. 协同过滤在新闻场景下的选型:为什么新闻推荐首选UserCF而不是ItemCF

2.1 UserCF和ItemCF的原理差异与选型依据

协同过滤分两类,基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。两者的出发点完全不同。

UserCF的核心逻辑是:找到与当前用户兴趣最相似的一群“邻居”,把这群邻居喜欢的、而当前用户没看过的物品推荐给他。逻辑顺序是“先找相似的人,再找这些人爱看的东西”。ItemCF则反过来,先建立物品之间的相似度关系,然后推荐“和你之前看过的新闻相似的新闻”。

在电商场景里ItemCF是绝对主流,因为商品是稳定存在的,今天卖T恤明天还在卖,物品相似度矩阵可以离线算好、定期更新。但新闻不一样——一条新闻的生命周期往往只有几小时到三天,早上发生的热点事件,晚上就过时了。在物品快速更替的场景下,ItemCF的问题会集中暴露:新新闻没有历史交互数据,无法计算它与任何旧新闻的相似度;旧新闻即使算好了相似度,等用户来的时候可能已经被下架。更麻烦的是,新闻内容的相似度计算需要高质量分词和语义理解,如果只按标题字面算相似,完全无法识别“国足”和“中国男足”这种同义表达。

所以新闻推荐系统行业里的常见做法是优先UserCF。用户的兴趣在短期内是相对稳定的,一个用户过去三天看了什么类型的新闻,基本决定了他今天想看什么。即使一条新闻刚发布还没有任何点击,只要它被某个相似用户看了,就可以被推荐出去,天然规避了物品冷启动问题。

2.2 相似度计算的选择:为什么这里推荐余弦相似度

确定用UserCF后,下一步是定义“用户相似度”。常见选择有三种:余弦相似度、皮尔逊相关系数、杰卡德相似系数。

要理解它们各自的适用场景,先把用户-新闻交互数据想象成一个矩阵,行为用户、列为新闻。余弦相似度计算的是两个用户向量的夹角,只关注方向、不关注数值大小。皮尔逊相关系数是余弦相似度的改进版——先对每个用户做中心化处理,也就是减去该用户所有评分的均值,再去算余弦。杰卡德相似系数则只看两个集合的交并比,完全没有数值概念。

对于新闻场景,默认用皮尔逊相关系数其实不太合适。原因在于,新闻阅读场景下的“评分”跟电影评分完全不同——电影用户会打分,1到5分分布均匀,中心化后再算相似度是有意义的。但新闻场景的交互通常只是“点没点”“看完没看完”,即使引入阅读时长作为隐式反馈(后面章节会细说),数值分布也极其不均,中心化操作的意义不大。杰卡德相似系数适合处理“收藏/未收藏”这类纯粹的布尔行为,信息量太薄,新闻推荐中很少单独使用。

因此代码层面的默认选择是余弦相似度。它的计算形式简洁,对稀疏向量支持好,配合后续要讲的数据清洗,结果已有足够的区分度。还有一个实际考量:余弦相似度可以不依赖Scikit-learn,只用numpy就能快速实现,这对毕设中的自主性展示是有利的。

3. 构建完整的推荐数据链路:从爬虫到用户行为表的落地做法

3.1 数据来源与基础环境准备

做协同过滤,第一步不是写算法,而是确认“手里有什么数据”。新闻推荐系统毕业设计的数据来源,常见做法有三种:第一种是直接使用公开数据集,比如学术界的新闻推荐数据集(如微软的MIND数据集),数据规范、省时间,但格式相对固定,答辩时容易被问到“这个数据集是你从零处理的吗”;第二种是用爬虫抓取新闻网站的内容,字段灵活、本地化好,但要注意robots协议和抓取频率;第三种是两者结合,先用爬虫跑一周攒真实数据,不够的部分用公开数据集补。

不管选哪种,环境准备是共同的起点。以下命令在Windows和Linux下通用(Windows如果提示python不是内部命令,说明没有勾选“Add Python to PATH”选项,需要卸载重装时勾上)。

# 创建独立的虚拟环境,避免依赖冲突 python -m venv news_rec_env # 进入虚拟环境(Windows) news_rec_env\Scripts\activate # 进入虚拟环境(Linux/Mac) source news_rec_env/bin/activate # 升级pip并安装核心依赖 pip install --upgrade pip pip install numpy pandas scikit-learn pip install Flask flask-cors pip install pymysql SQLAlchemy

在安装依赖时有个注意点:最好不要运行“pip install requests beautifulsoup4 scrapy”等和爬虫相关的安装命令,新闻正文抓取的质量会直接决定后续推荐效果,这里只先装推荐链路本身需要的库。

3.2 用户行为表设计:记录什么字段,决定推荐效果的上限

协同过滤算法的输入是“用户—物品”的交互记录,但新闻场景的交互天然有强时效性,不能只存“用户id、新闻id、时间”三个字段。如果只存最原始的点击记录,后面做特征分析时会发现数据不够用:无法区分用户是认真看完了还是误点,无法定位用户关注的话题领域,连“最近一周活跃用户”这种最基础的统计都要反复join。

按照行业里做推荐的通用做法,我建议至少在MySQL中设计三张表:新闻表(news)、用户表(user)、用户行为表(behavior)。其中用户行为表是整个项目的心脏,字段设计如下。

字段名类型说明
idint自增主键
user_idint用户ID,对应user表
news_idint新闻ID,对应news表
behavior_typevarchar(20)行为类型:click/view/favorite
duration_secondsint阅读时长(秒),0表示未记录
is_readtinyint是否读完,1表示读完
create_timedatetime行为发生时间

这里的几个关键细节:behavior_type不要用数字编码而用字符串,便于后续做规则筛选;duration_seconds和is_read是隐式反馈特征,虽然它们不在协同过滤算法主流程中,但是在数据预处理过滤噪音时会用到;create_time一定不能省,因为新闻推荐必须考虑时间衰减。

3.3 用Python实现从数据库读取到行为矩阵构建

有了行为表后,协同过滤的第一步是从数据库读出原始行为数据,加工成UserCF的标准输入——一个用户-物品评分矩阵。具体做法如下(这里用一个简单的示例数据表结构来说明核心写法,实际表名和字段请按自己的表调整)。

import pandas as pd import numpy as np from sqlalchemy import create_engine # 数据库连接配置 DB_CONFIG = { 'host': 'localhost', 'port': 3306, 'user': 'root', 'password': 'your_password', 'database': 'news_recommend' } # 创建数据库引擎 engine = create_engine( f"mysql+pymysql://{DB_CONFIG['user']}:{DB_CONFIG['password']}" f"@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}?charset=utf8mb4" ) # 提取行为数据 def load_behavior_data(): query = """ SELECT user_id, news_id, behavior_type, duration_seconds, is_read, create_time FROM behavior WHERE create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) """ df = pd.read_sql(query, engine) print(f"已加载行为记录: {len(df)} 条") return df # 行为转评分:不同类型赋予不同权重 def behavior_to_rating(df): # 先复制一份,避免修改原始数据 df = df.copy() # 评分规则:完整阅读给1.0,仅点击给0.6,收藏给1.2 rating_map = { 'read': 1.0, 'click': 0.6, 'favorite': 1.2 } # 应用评分映射 df['rating'] = df.apply( lambda row: rating_map.get(row['behavior_type'], 0.5), axis=1 ) # 阅读时长大于30秒的加权 df.loc[(df['duration_seconds'] > 30) & (df['is_read'] == 1), 'rating'] *= 1.1 # 只保留评分大于0的记录 df = df[df['rating'] > 0] # 聚合:同一个用户对同一篇新闻的多次行为取最大值 rating_matrix = df.groupby(['user_id', 'news_id'])['rating'].max().reset_index() return rating_matrix

逻辑说明:behavior_to_rating函数的核心思路是把用户的不同行为映射成分值,然后聚合生成标准的行为矩阵。评分映射表是项目里重点调优的参数——0.6/1.0/1.2这组值表示点击权重最低、收藏最高。这个设计遵循一个直观业务逻辑:收藏代表用户主动表达兴趣,比被动点击意图强得多。

参数说明:drop_duplicates和groupby操作在这里起着行为去重的作用,防止同一用户反复刷新同一页面导致权重累积。SQL查询里的INTERVAL 7 DAY是时间窗口参数,新闻推荐一般只取近7天行为。新闻生命周期短,超过7天的行为对当前推荐基本没有参考价值,如果不做时间过滤,算法会把上周的热点当成现在的兴趣,严重拉低推荐效果。

4. 核心实现:手写UserCF推荐引擎的完整步骤

4.1 构建用户相似度矩阵

从行为矩阵计算用户相似度矩阵,是UserCF的核心步骤。有了上一步构建的评分矩阵,这里可以用Python独立实现整个流程,不依赖现成的推荐库。这样做事后的可解释性好,答辩时能被问到细节都能答上来。

import numpy as np import pandas as pd from collections import defaultdict class UserCF: def __init__(self): self.user_sim_matrix = {} self.user_items = {} self.item_users = {} self.user_ratings = {} def fit(self, rating_df): """ rating_df: DataFrame with columns [user_id, news_id, rating] """ # 按用户分组,构建用户-物品字典 self.user_items = defaultdict(dict) self.item_users = defaultdict(dict) for _, row in rating_df.iterrows(): user_id = row['user_id'] item_id = row['news_id'] rating = row['rating'] self.user_items[user_id][item_id] = rating self.item_users[item_id][user_id] = rating self.user_ratings = dict(self.user_items) print(f"用户数量: {len(self.user_items)}, 新闻数量: {len(self.item_users)}") # 计算用户间相似度 self._calc_user_similarity() def _calc_user_similarity(self): """计算用户相似度矩阵,采用余弦相似度""" # 构建用户-物品评分矩阵(稀疏表示) user_ids = list(self.user_items.keys()) n_users = len(user_ids) user_index = {uid: idx for idx, uid in enumerate(user_ids)} # 构建用户向量矩阵 user_vector = {} for uid, items in self.user_items.items(): vector = np.zeros(len(self.item_users)) for item_id, rating in items.items(): item_idx = list(self.item_users.keys()).index(item_id) vector[item_idx] = rating user_vector[uid] = vector # 计算两两相似度 self.user_sim_matrix = {} for i in range(n_users): uid = user_ids[i] self.user_sim_matrix[uid] = {} vi = user_vector[uid] for j in range(i+1, n_users): vj = user_vector[user_ids[j]] # 余弦相似度公式 dot_product = np.dot(vi, vj) norm_i = np.linalg.norm(vi) norm_j = np.linalg.norm(vj) if norm_i == 0 or norm_j == 0: sim = 0.0 else: sim = dot_product / (norm_i * norm_j) self.user_sim_matrix[uid][user_ids[j]] = sim self.user_sim_matrix[user_ids[j]][uid] = sim

逻辑说明:fit方法是训练入口,输入上一步生成的rating_df,完成user_items和item_users两个字典的构建,然后计算相似度。这里的核心是_calc_user_similarity方法,它把每个用户的评分行为变成一个稀疏向量,向量的每个维度对应一篇新闻。这样两个用户的相似度就是两个向量的余弦值,数值越接近1表示兴趣越一致,越接近0表示几乎无交集。

参数说明:这段代码里最容易出性能问题的是“将物品转成向量”的过程——每构建一个用户向量都要遍历item_users.keys(),这样的低效做法在小数据集上没问题,数据量到十万级之后跑一次要几分钟。如果你打算用真实爬取数据,建议用scipy.sparse中的csr_matrix构建矩阵,代码更简洁,运行效率也更高。这里保留遍历写法,是为了让初学者能看清每一步在做什么。

4.2 生成TopN推荐列表

相似度矩阵只是“中间结果”,最终要给用户输出的是一个按分数排序的新闻id列表。这一步的逻辑是:找到和目标用户最相似的K个用户,把这K个用户的行为汇总(排除目标用户已读内容),按推荐分值排序取前N条。

def recommend(self, user_id, top_k=10, top_n=10): """ 为目标用户生成推荐列表 user_id: 目标用户ID top_k: 取前K个相似用户 top_n: 最终推荐的新闻数量 """ # 如果该用户不在训练集中,返回空列表 if user_id not in self.user_items: return [] # 获取当前用户的已读新闻(用于过滤) read_items = set(self.user_items[user_id].keys()) # 获取相似度最高的K个用户 if user_id not in self.user_sim_matrix: return [] sim_users = sorted( self.user_sim_matrix[user_id].items(), key=lambda x: x[1], reverse=True )[:top_k] # 候选物品得分表 item_scores = defaultdict(float) item_sim_sum = defaultdict(float) # 遍历相似用户,累加推荐分值 for sim_user, sim_score in sim_users: for item_id, rating in self.user_items[sim_user].items(): # 跳过当前用户已读过的新闻 if item_id in read_items: continue # 加权打分:相似度 * 该用户对新闻的评分 item_scores[item_id] += sim_score * rating item_sim_sum[item_id] += sim_score # 归一化:除以相似度之和,消除K值影响 for item_id in item_scores: item_scores[item_id] /= item_sim_sum[item_id] # 按分数排序取TopN ranked_items = sorted(item_scores.items(), key=lambda x: x[1], reverse=True) return [(item_id, score) for item_id, score in ranked_items[:top_n]]

逻辑说明:推荐部分的三个关键操作是——过滤、加权、归一化。过滤是为了避免“推荐用户已经看过的新闻”,这是协同过滤最容易犯的低级错误;加权是相似度与评分的乘积,相似用户贡献越高,他对新闻的评分在最终排序中占的比重就越大;归一化(除以item_sim_sum)解决的是“某些用户行为特别多、导致他推荐的新闻总是排最前”的问题。

参数说明:top_k=10表示参与推荐的邻居数量,这个值建议从5到30做网格搜索。top_k太小,推荐的视野太窄,系统只围绕几个最相似的用户转;top_k太大,把相似度很低的人都拉进来,推荐结果会被“大众口味”淹没。top_n一般取10或20,新闻客户端首屏放10条,下拉刷新再放10条,符合用户浏览习惯。

4.3 用Flask快速暴露成推荐接口

毕业设计要演示效果,最直观的落地方式是写一个轻量Web接口。用Flask写一个简单的服务,把上面的UserCF类封装成HTTP接口,前端页面直接调用。这是新闻推荐系统的标准集成方式,核心代码如下。

from flask import Flask, jsonify, request from user_cf import UserCF # 假设上面代码保存为user_cf.py app = Flask(__name__) # 全局模型实例 model = None @app.route('/api/recommend', methods=['GET']) def recommend_news(): """推荐接口,接收user_id参数""" user_id = request.args.get('user_id', type=int) if not user_id: return jsonify({'code': 400, 'msg': '缺少user_id参数', 'data': []}), 400 try: # 调用模型生成推荐 rec_list = model.recommend(user_id, top_k=10, top_n=10) # 拼接推荐结果 recommendations = [] for news_id, score in rec_list: # 这里需要查news表获取标题和url recommendations.append({ 'news_id': news_id, 'score': round(score, 4) # 实际项目中在这里联查news表,返回title和url }) return jsonify({'code': 200, 'msg': 'success', 'data': recommendations}) except Exception as e: return jsonify({'code': 500, 'msg': str(e), 'data': []}), 500 @app.route('/health') def health_check(): return jsonify({'status': 'ok'}) if __name__ == '__main__': # 实际运行时先加载数据、训练模型 # from data_prepare import load_behavior_data, behavior_to_rating # df = load_behavior_data() # rating_df = behavior_to_rating(df) # model = UserCF() # model.fit(rating_df) app.run(host='0.0.0.0', port=5000, debug=True)

逻辑说明:这里的weight参数传递方式我在接口层做了简化处理,实际中建议每个接口可接收用户id、返回条数两个参数。需要特别注意的是Flask里debug=True会让每次请求时如果修改了Python代码自动重启服务,带来了方便,但在生产环境必须置为False。

参数说明:port=5000是Flask默认端口,如果被占用可以换成5001或8080。在最终演示时,前端页面通过GET请求访问http://localhost:5000/api/recommend?user_id=1,直接拿到JSON格式的推荐结果,不管是写个简单的HTML页面展示,还是用Vue前后端分离,接口都是通用的。

5. 避坑指南:新闻推荐系统最常见的5个坑和对应解法

5.1 数据稀疏导致相似度全为0

现象:训练完成之后,发现大部分用户之间相似度为0,推荐结果为空或者只有点击率最高的那几篇新闻。

原因:新闻场景交互天然稀疏。假设系统里有1000个用户、5000篇新闻,每个用户平均只看过20篇,那么任意两个用户的共同阅读量可能只有0到2篇,侥幸非零,余弦相似度数值也极低,推荐效果趋近于随机。

解决:把人作为中心把数据变稠密——把新闻按频道或类别聚合,从“用户-新闻”矩阵变成“用户-频道”矩阵,频道数量可能只有十几个,稀疏度立刻降下来。具体做法是在news表加category字段,聚合成类别评分后再跑UserCF,但这会牺牲推荐新颖性(只能推频道内的热门新闻,无法跨频道发现)。更稳妥的做法是保留两层推荐:UserCF在“用户-类别”层面计算相似用户,然后在相似用户的行为里挑选新闻候选。这样大类相似度计算稳定,小类新闻推荐有惊喜。

5.2 相似度矩阵占用内存过大

现象:代码跑起来没有任何报错,但内存占用迅速飙升,训练到一半直接被系统杀掉(Process finished with exit code 137,Killed)。

原因:UserCF需要存储用户间两两相似度。当用户数达到10000时,相似度矩阵理论上存储约5000万个浮点数,每个float64占8字节,相当于400MB内存;用户数到50000时直接超过8GB,笔记本扛不住。

解决:三层处理方案。第一,只保留TopN最相似的邻居,每个用户只存相似度最高的30个用户信息,矩阵从O(N)降到O(N×K),同时还能防止冷门用户拖慢计算速度;第二,用scipy.sparse.csr_matrix稀疏存储矩阵,只记录非零元素,因为新闻场景里大多数用户对之间根本没有共同阅读记录,稀疏度轻松超过99%;第三,数据库侧先做时间过滤,只保留近7天行为数据参与计算,而不是把所有历史数据都加载进内存。实际项目中我一般三层同时做。

5.3 新用户和新新闻的冷启动问题

现象:新注册用户没有任何行为记录,模型对其输出空列表;当天新发布的新闻没有曝光机会,永远进不了行为表。

原因:协同过滤的本质是从历史行为中总结模式,没有历史就无模式可言。这是算法的先天缺陷,不是代码bug。

解决:新用户冷启动常用“热门兜底”策略——当UserCF返回结果为空或相似用户少于3个时,直接推荐当前时间窗口内点击量最高的新闻列表,保证接口永远有返回。避免了前端页面“推荐区空白”的尴尬。新新闻那边问题更棘手,新闻生命周期太短,等它积累了足够点击量就已经过时了。所以行业里对新闻推荐有专门的做法:对当天发布不足24小时的新新闻,用基于内容的推荐作为补充——提取新闻关键词与用户历史阅读的关键词做匹配,冷启动问题就从协同过滤层面转移到了内容层面,需要引入分词(jieba)和关键词抽取。这一段逻辑要写进毕设论文的创新点里,很加分。

5.4 中文分词不规范导致相似度计算失真

现象:新闻标题中包含英文单词或中文分词结果混乱,算出来的新闻相似度不可用,甚至把完全无关的新闻判定为高度相似。

原因:新闻文本往往是中英混合(比如“iPhone 17发布”“AI大模型”),直接按空格切分根本得不到有效token,必须做中文分词。毕设常用的是jieba库,但默认词典不够全,人名、领域词经常被切碎。

解决:在数据预处理阶段增加分词步骤。先加载自定义词典(领域特有词汇:大模型、新能源、算力租赁等),再对标题和摘要做分词去停用词处理。分词结果不要直接用于协同过滤,而是存到news表的keyword_field字段,作为冷启动和后续内容推荐的特征来源。这里必须去掉“根据标题正文泛泛而谈”的水分,落成一个具体的函数,我在项目里一般写一个news_preprocess.py脚本,内容就是加载词典、分词、存库,约30行代码。

5.5 评分权重设置不合理导致推荐结果单一

现象:推荐列表里全是收藏或看完的长新闻,把短视频类和快讯类内容全都挤掉了。用户明明经常看短平快的快讯,但推荐结果里一篇都看不到。

原因:如果收藏的权重设得太高(比如1.5甚至2.0),算法会严重偏向那些容易引发收藏的深度长文,而新闻阅读场景的大量真实行为是快速浏览,这类行为权重被压低之后,用户的真实兴趣特征被扭曲了。

解决:切忌“拍脑袋定权重”,不要只拿一组值跑到底。正确做法是调参后看行为分布:跑完一批验证集数据后,统计推荐列表里behavior_type的分布比例,和训练集里的比例对比。当推荐列表里“收藏行为”的占比被放大到训练集的2倍以上,说明权重设定偏了;这时把收藏权重从1.2下调到1.0,把点击权重从0.6上调到0.8,直到比例重新回到合理区间。这组参数是典型的“看似不起眼、实际决定成败”的细节。不同的新闻客户端、不同的用户群体,这组权重值差异很大,没有万能参数。

6. 让系统更像产品:引入时间衰减与评估指标的两个进阶做法

6.1 时间衰减:让算法理解“新闻是易腐品”

入门版的UserCF把近7天行为全部视为同等重要,但现实是用户昨天看了科技新闻、今天刷了一下午体育赛事,那么明天推荐应该更偏向体育还是科技?答案是体育。用户兴趣在短时间内有强连续性,但也存在快速偏移。

时间衰减的常见做法是在推荐打分时加上一个随“行为发生时间和当前时间间隔”衰减的系数。代码层面,在之前fit方法处理rating_df的时候增加一个时间衰减公式:

import datetime import math def apply_time_decay(rating_df, half_life_days=3): """时间衰减函数:行为越旧,权重越低 half_life_days: 半衰期天数,3天表示3天前的行为权重衰减为一半 """ df = rating_df.copy() now = datetime.datetime.now() # 确保时间列是datetime类型 df['create_time'] = pd.to_datetime(df['create_time']) # 计算行为发生到当前的间隔(单位:天) df['age_days'] = (now - df['create_time']).dt.total_seconds() / 86400.0 # 指数衰减公式:权重 = 0.5 ^ (age / half_life) df['decay_weight'] = df['age_days'].apply( lambda age: math.pow(0.5, age / half_life_days) ) # 修正评分 df['rating'] = df['rating'] * df['decay_weight'] return df

这个函数的核心改动就一行——原有评分乘以decay_weight,实现“越近的行为权重越大”的效果。half_life_days=3是一个直观的入门设定,可调范围在2到7之间。新闻推荐比电商推荐对时间更敏感,半衰期设3天比较符合阅读习惯。实现中时间复杂度没有变化,只是多了一列计算,对性能几乎无影响,推荐效果肉眼可见地提升。

6.2 评估推荐效果:这种毕业设计怎么量化说明“我的系统是有效的”

新闻推荐系统的评估比电商更困难——用户没有显式评分,缺乏标准答案。毕设答辩时如果只说“效果好”,没有任何数据支撑,这一块会被追问到很难看。推荐系统行业里最常用的离线评估指标有三个:准确率(Precision)、召回率(Recall)、覆盖率(Coverage)。但在新闻推荐场景里,最常用也最易解释的是前两个。

做法是把行为数据按时间切成训练集和测试集,比如用前5天数据训练,用第6天“用户真实点击了哪些新闻”作为答案,然后让模型给这些用户预测第6天可能看的新闻。如果模型推荐的10条新闻里,有2条是用户真实实际点击过的,那Precision@10就是0.2。

def evaluate(model, train_df, test_df, top_n=10): """计算推荐结果的精确率和召回率""" # 按用户分组构建测试集的真实点击 test_user_items = test_df.groupby('user_id')['news_id'].apply(set).to_dict() hit_count = 0 # 推荐命中的总次数 rec_count = 0 # 总推荐条数 test_item_count = 0 # 测试集总条数 for user_id, actual_items in test_user_items.items(): # 模型只推荐训练中出现过的用户 if user_id not in model.user_items: continue # 生成推荐 rec_items = model.recommend(user_id, top_k=10, top_n=top_n) rec_item_ids = set([item[0] for item in rec_items]) # 统计命中 hits = rec_item_ids & actual_items hit_count += len(hits) rec_count += len(rec_item_ids) test_item_count += len(actual_items) precision = hit_count / rec_count if rec_count > 0 else 0 recall = hit_count / test_item_count if test_item_count > 0 else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0 return { 'precision': round(precision, 4), 'recall': round(recall, 4), 'f1': round(f1, 4) }

逻辑说明:这套evaluate方法实现了Precision和Recall的基本计算逻辑——Precision衡量的是“推荐了10条,有几条是用户真正看的”,Recall衡量的是“用户实际看了10条,我推荐出来了其中几条”。在实际毕业论文的实验章节中,需要跑多组不同参数对比结果:top_k取5/10/20,时间窗口取3/5/7天,评分权重取几组不同设置,然后把对比数据做成表格,这就是最有说服力的实验章节素材。

6.3 一个亲测有效的参数组合(和它的局限)

综合上面的讨论,对新闻推荐场景下入门快速跑通的默认参数,我一般推荐这组:

参数项推荐值说明
相似度公式余弦相似度数据稀疏时比皮尔逊更稳定
top_k10邻居数量,建议搜索5~30
top_n10推荐数量,也可与前端翻页联动
时间窗口7天配合半衰期=3天的时间衰减
点击/阅读/收藏权重0.6 / 1.0 / 1.2可调,收藏不宜过高
相似用户数阈值3低于该阈值时启用热门兜底

这组参数我在好几个项目上验证过,逻辑上限明确:它们适合“新闻门户+短生命周期内容”的通用场景,但如果你的毕设定位是某个垂直领域(比如金融资讯或体育新闻),就没有参考价值——垂直领域的用户行为模式完全不同,必须从头调参。

做这类毕业设计最怕的不是算法复杂,而是数据链路只通到一半:爬了新闻、建了表、模型能跑出来,但问到“为什么推荐这条给这个用户”就答不上来。所以我的习惯是:哪怕时间再紧,也要把推荐结果的中间过程留在代码里——哪个相似用户贡献了这条推荐、他的权重是多少,全部能打印出来看。一个能自解释的推荐系统,比一个精度高0.5个百分点的黑匣子,在毕业答辩时的价值高得多。希望这些内容能帮你在动手时少踩一些我已经踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询