ARTICLE DETAIL

资讯详情

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

基于协同过滤的新闻推荐系统:Python ItemCF实现与时效性优化

基于协同过滤的新闻推荐系统:Python ItemCF实现与时效性优化 简介本资源是一套基于Python协同过滤算法的新闻推荐系统毕业设计项目适合计算机、软件工程等专业学生用于课程设计、论文参考或求职作品整理。项目从数据抓取与清洗、用户行为建模到相似度计算与Top-N推荐均有覆盖并给出基于用户与基于物品两种协同过滤实现思路。压缩包共146个文件包含Java、Scala、Python后端逻辑及Vue前端页面配套XML配置、SQL脚本、properties配置文件与parquet数据文件等层级分明便于直接阅读和二次开发。整体大小约577KB目前已有353人学习下载。对于希望理解推荐系统落地流程的开发者该资源可提供一套可运行的工程骨架同时通过Dockerfile与YAML环境配置降低部署门槛有助于快速复现实验并进行算法效果评估。1. 新闻推荐系统用协同过滤同样是 Python 代码为什么电商能跑通、新闻会翻车如果你在毕业设计里选了“基于 Python 协同过滤算法的新闻推荐系统”大概率是看中了协同过滤“不需要理解内容、只靠用户行为”的简洁性。这个判断方向是对的用户点击、收藏、阅读时长这类行为数据确实比新闻正文好处理得多也更容易在答辩时讲清楚推荐链路。但新闻场景有一个电商和电影没有的硬约束——物品生命周期极短。昨天的爆款新闻今天就是旧闻一个基于“用户喜欢过什么就推荐什么”的模型如果只做静态相似度计算推荐结果会迅速被时间淘汰。这就引出了这套系统的核心矛盾协同过滤算法负责捕捉用户兴趣但新闻的时效性要求你必须额外处理物品冷启动、兴趣漂移和时间衰减。本文不打算复述协同过滤公式而是给出一个能落地、能跑通、能答辩的完整方案数据怎么造、UserCF 和 ItemCF 怎么选、相似度矩阵怎么处理新闻的时间特性、本地项目怎么一步步构建并验证效果。这套方案适合两类人。一是毕设选题锁定推荐系统的在校生需要一份能自己敲出来、能解释清楚每个参数的业务代码二是刚接触推荐工程的开发者想看看最简单的协同过滤在新闻数据上会踩哪些坑、怎么用最少的人力把离线模型变成在线接口。我会把可运行的 Python 代码、每个模块的参数含义、以及调试时的关键日志位置都写出来你照着敲就能复现。2. 数据和召回设计新闻推荐的第一层漏斗行为日志决定协同过滤的上限2.1 为什么新闻推荐要先做“行为日志”而不是直接找公开数据集很多人在毕设开头会去爬新闻数据然后对着 pandas 发愁只有文章列表没有用户行为协同过滤根本无从下手。协同过滤的输入是“用户-物品”交互矩阵没有交互数据算法就是空转。所以做这个题目第一步不是写算法而是定义行为日志结构。我一般建议自己造一份贴近真实场景的模拟日志。好处有三个一是可控你能精确制造“热门新闻”“长尾新闻”“新用户”“老用户”四类样本方便验证算法行为二是能复现答辩时老师问“数据哪来的”你可以理直气壮说“基于真实业务抽象出的仿真日志”三是避坑公开数据集的物品 ID 往往是商品而非新闻字段语义对不上。模拟日志的核心字段如下字段名类型说明user_idstr用户唯一标识建议格式 U0001news_idstr新闻唯一标识建议格式 N0001包含发布时间信息behavior_typeint1 曝光 / 2 点击 / 3 收藏 / 4 分享推荐训练只用 2 和 3behavior_timestr行为发生时间精确到分钟用于时间衰减计算news_publish_timestr新闻发布时间用于计算新闻年龄用 Python 写一个日志生成脚本控制在 100 行以内能让后续所有模块都有稳定输入。import random import pandas as pd from datetime import datetime, timedelta random.seed(42) def generate_news(n_news500): news_list [] base_time datetime(2024, 5, 1) # 模拟从5月1日开始发布新闻 for i in range(n_news): publish_time base_time timedelta(hoursrandom.randint(0, 24 * 30)) news_list.append({ news_id: fN{i:04d}, publish_ts: publish_time.strftime(%Y-%m-%d %H:%M:%S), # 模拟新闻热度20%的新闻占据80%的点击 hot_rank: 1 if random.random() 0.2 else 0 }) return pd.DataFrame(news_list) def generate_logs(news_df, n_users2000, n_logs50000): logs [] user_ids [fU{i:04d} for i in range(n_users)] base_time datetime(2024, 6, 1) for _ in range(n_logs): user_id random.choice(user_ids) # 80%的概率从热门新闻里选模拟马太效应 hot_pool news_df[news_df[hot_rank] 1] normal_pool news_df[news_df[hot_rank] 0] if random.random() 0.8 and len(hot_pool) 0: news hot_pool.sample(1).iloc[0] else: news normal_pool.sample(1).iloc[0] behavior_time base_time - timedelta(minutesrandom.randint(0, 24 * 60 * 30)) logs.append({ user_id: user_id, news_id: news[news_id], behavior_type: random.choices([1, 2, 3, 4], weights[50, 30, 15, 5])[0], behavior_time: behavior_time.strftime(%Y-%m-%d %H:%M:%S), news_publish_time: news[publish_ts], hot_rank: news[hot_rank] }) return pd.DataFrame(logs) news_df generate_news(500) log_df generate_logs(news_df) log_df.to_csv(user_behavior_log.csv, indexFalse)这段代码里的hot_rank是关键参数。20% 热门新闻占据 80% 点击这是新闻阅读的典型马太分布也是之后 ItemCF 需要做热门惩罚的根源。behavior_type的权重比例同样有讲究曝光最多、点击其次、收藏和分享稀少符合真实漏斗。你用这个数据集跑完整个系统后会发现“热门新闻相似度虚高”的问题那就是hot_rank埋下的雷后面会专门解决。2.2 协同过滤的输入矩阵从行为日志到“用户-新闻”评分矩阵日志生成后不能直接喂给算法。协同过滤的经典输入是评分矩阵行是用户、列是新闻值是行为权重。但新闻场景里“评分”不是用户主动打的星级而是对行为类型的加权换算。常见做法是曝光计 0 分、点击计 1 分、收藏计 3 分、分享计 5 分。换算后做数据透视得到稀疏矩阵。import pandas as pd import numpy as np log_df pd.read_csv(user_behavior_log.csv) behavior_weight {1: 0, 2: 1, 3: 3, 4: 5} log_df[score] log_df[behavior_type].map(behavior_weight) # 只保留有正反馈的行为 valid_logs log_df[log_df[score] 0] # 做时间衰减越久远的行为权重越低半衰期设为7天 import numpy as np from datetime import datetime REF_TIME datetime(2024, 6, 1) valid_logs[behavior_dt] pd.to_datetime(valid_logs[behavior_time]) valid_logs[age_days] (REF_TIME - valid_logs[behavior_dt]).dt.total_seconds() / 86400 valid_logs[decay_score] valid_logs[score] * np.power(0.5, valid_logs[age_days] / 7) rating_matrix valid_logs.pivot_table( indexuser_id, columnsnews_id, valuesdecay_score, aggfuncsum ).fillna(0) print(rating_matrix.shape) print(rating_matrix.sparsity) # 可以自己算非零元素比例时间衰减半衰期7天是最需要调的业务参数。新闻的生命周期通常在 24 到 72 小时如果没有衰减用户一个月前的点击和今天的点击权重一样模型会给你推用户早就不关心的旧闻。半衰期设得越短系统对新闻时效越敏感但对“长周期兴趣”不敏感。我习惯先设 7 天跑一版再对比设 3 天的推荐结果差异。这一步做完你就有了一个标准的协同过滤输入矩阵。需要留意的是aggfuncsum——同一个用户对同一篇新闻可能有多条行为sum 会把点击加收藏的分数累加符合直觉。如果你发现矩阵只有不到 1% 的非零元素不用慌新闻推荐的数据稀疏度通常比电商更严重用户看的新闻本来就少。2.3 UserCF 还是 ItemCF新闻场景的选型逻辑协同过滤分成基于用户UserCF和基于物品ItemCF两条路线选型不能凭喜好。先记住一个结论新闻推荐项目优先用 ItemCF除非你的系统有很强的社交属性。为什么UserCF 的思路是找“和我兴趣相似的用户”把他们看过的新闻推荐给我。这在兴趣长尾、以“关注人”为核心的场景里有效比如微博。但新闻场景有几个硬伤用户行为稀疏两个用户共同点击过的新闻少相似度计算不稳定新闻更新快用户向量变化剧烈昨天相似的人今天可能完全不同更重要的是UserCF 推荐结果偏向热门冷门新闻几乎没有机会被推出去。ItemCF 的思路是“和我看过的那篇新闻相似的新闻”它更稳定一篇新闻的相似物品不会频繁变化也更容易在推荐时做一些业务规则干预比如强制剔除 24 小时前的新闻。但 ItemCF 在新闻场景也有两个必须处理的转折点一是新闻都是短生命周期的不能拿一个月前的同现数据算相似度必须加时间窗口二是热门新闻会被过度推荐需要做热门惩罚。这两个问题是本文后面代码的核心也是你答辩时能拿出来讲的亮点。3. 实现 ItemCF 并引入时间窗口把 Python 协同过滤从教材公式改造成新闻可用3.1 基础相似度计算同现矩阵的朴素实现先把教材上的 ItemCF 写出来再逐步加新闻约束。朴素 ItemCF 分两步统计两篇新闻被同一用户点击的次数得到同现矩阵用余弦相似度把同现次数归一化到 0-1 区间。Python 实现如下。from collections import defaultdict import math def build_item_similarity(rating_matrix): 输入评分矩阵输出物品相似度字典 {news_id: {other_news: sim}} # 第一步统计每个用户点击的新闻列表 user_items {} for user_id, row in rating_matrix.iterrows(): items set(row[row 0].index) # 只取有正反馈的新闻 if len(items) 1: user_items[user_id] items # 第二步统计同现次数 co_occur defaultdict(lambda: defaultdict(int)) item_click_count defaultdict(int) for user_id, items in user_items.items(): for news_id in items: item_click_count[news_id] 1 for other_id in items: if other_id ! news_id: co_occur[news_id][other_id] 1 # 第三步余弦相似度归一化 item_sim {} for news_id, related_items in co_occur.items(): item_sim[news_id] {} norm math.sqrt(item_click_count[news_id]) # 分母的一部分 for other_id, count in related_items.items(): # 余弦公式count / sqrt(click_i * click_j) denominator norm * math.sqrt(item_click_count[other_id]) if denominator 0: sim count / denominator item_sim[news_id][other_id] sim return item_sim这个版本跑出来的结果有一个明显问题sim对热门新闻虚高。新闻 A 被 1000 人看过新闻 B 被 800 人看过A 和 B 的同现值会被余弦公式放得很大即便它们只是因为都排在首页而被动获得的点击。这种假相似度在新闻场景是致命的因为它会把所有冷门新闻都推向热门新闻的阴影里。要解决这个问题得引入两个新闻场景专属的修正项。3.2 新闻场景的修正项热门惩罚和时间窗口第一个修正项是流行度惩罚。常见做法是给热门新闻的权重加一个衰减因子公式调整为sim(i, j) sum(decay_score) / (sqrt(click_i * click_j) alpha * hot_rank_i * hot_rank_j)这里的hot_rank来自第一节日志里的字段alpha控制惩罚强度一般取 0.2 到 0.5。当 i 是热门新闻时分母变大相似度被压低。第二个修正项是时间窗口——只统计近 3 天内的同现行为超出窗口的行为不计入同现矩阵。这相当于给相似度计算加了一个滑动窗口过滤器保证“今天的新闻只和今天的新闻相似”。from collections import defaultdict import math import pandas as pd def build_news_item_similarity(log_df, time_window_days3, alpha0.3): 带时间窗口和热门惩罚的 ItemCF 相似度计算 log_df: 必须包含 user_id, news_id, score, behavior_time 列 # 过滤出时间窗口内的行为 latest_time pd.to_datetime(log_df[behavior_time]).max() window_start latest_time - pd.Timedelta(daystime_window_days) windowed_logs log_df[pd.to_datetime(log_df[behavior_time]) window_start] # 用户-新闻映射 user_items defaultdict(set) item_click_count defaultdict(int) item_hot_rank {} for _, row in windowed_logs.iterrows(): if row[score] 0: continue user_items[row[user_id]].add(row[news_id]) item_click_count[row[news_id]] 1 item_hot_rank[row[news_id]] row.get(hot_rank, 0) # 同现矩阵 co_occur defaultdict(lambda: defaultdict(float)) for user_id, items in user_items.items(): for news_id in items: for other_id in items: if other_id ! news_id: co_occur[news_id][other_id] 1 # 相似度 热门惩罚 item_sim defaultdict(dict) for news_id, related in co_occur.items(): norm_i math.sqrt(item_click_count[news_id]) hot_penalty_i alpha * item_hot_rank.get(news_id, 0) for other_id, co_count in related.items(): norm_j math.sqrt(item_click_count[other_id]) hot_penalty_j alpha * item_hot_rank.get(other_id, 0) denominator norm_i * norm_j hot_penalty_i hot_penalty_j if denominator 0: sim co_count / denominator if sim 0.01: # 过滤噪声相似度 item_sim[news_id][other_id] sim return item_sim参数说明time_window_days是新闻场景最重要的旋钮设 3 天表示只利用三天内的行为计算相似度能保证推荐结果是“本周热闻”而不是“本月旧闻”alpha是热门惩罚强度设 0.3 表示对热门新闻的相似度压低三成。sim 0.01的过滤条件是我踩坑加上的——不设阈值时两篇只有一次同现的冷门新闻会拿到 0.5 以上的相似度产生一堆无意义的推荐对。你可以把阈值从 0.01 微调到 0.05观察推荐列表中被推荐新闻的平均点击率变化。3.3 给目标用户生成 Top-N 推荐从相似度到可展示列表相似度矩阵构建完成后推荐生成就是查表排序。目标用户看过新闻 S就从item_sim[S]里取相似度最高的新闻去掉已经看过的按分数排序返回。但让推荐结果一眼看上去“像回事”还需要加两个过滤规则新闻时效过滤和重复来源过滤。def recommend_for_user(user_id, rating_matrix, item_sim, top_n10): 基于用户历史点击生成推荐 # 用户看过的新闻 user_history set(rating_matrix.loc[user_id][rating_matrix.loc[user_id] 0].index) # 候选池从每篇看过的新闻的相似新闻中收集 candidate_scores defaultdict(float) for news_id in user_history: if news_id not in item_sim: continue for other_id, sim in item_sim[news_id].items(): if other_id in user_history: continue # 以用户对news_id的评分为权重累加相似度 user_score rating_matrix.loc[user_id, news_id] candidate_scores[other_id] sim * user_score # 按分数排序取top_n ranked sorted(candidate_scores.items(), keylambda x: x[1], reverseTrue)[:top_n] return [news_id for news_id, score in ranked]注意sim * user_score这一层加权用户对自己看过的新闻评分越高该新闻的相似新闻越靠前。这模拟了“喜欢程度越高越希望看到类似内容”的直觉。但这个函数有个待优化点——它没有计算推荐分数的新鲜度惩罚。一篇 20 天前的新闻即使和用户历史高度相似也不应该进推荐列表。把新闻发布时间纳入排序是下一章的事也需要你决定离线计算和在线发布的边界。4. 从离线相似度到在线推荐把 Python 脚本改造成可持续运行的推荐服务4.1 相似度矩阵预计算为什么不能每次请求都跑一遍 ItemCF很多初稿代码会把相似度计算和推荐生成写进同一个请求函数用户每次刷新都重算所有新闻的两两相似度。这在 500 篇新闻的 demo 里看不出问题但一旦数据量上千、用户行为上万响应时间会从毫秒级劣化到秒级答辩演示时一卡印象分直接掉。协同过滤的标准工程做法是“离线计算、在线读取”定时任务算出相似度矩阵存入内存或数据库推荐接口只做查表和排序。import json import pickle from datetime import datetime def save_similarity_matrix(item_sim, filepathitem_sim_cache.pkl): 把相似度矩阵持久化供在线服务加载 with open(filepath, wb) as f: pickle.dump(dict(item_sim), f) def load_similarity_matrix(filepathitem_sim_cache.pkl): with open(filepath, rb) as f: return pickle.load(f) # 定时任务入口每天凌晨3点跑一次 def daily_update(): log_df pd.read_csv(user_behavior_log.csv) log_df[score] log_df[behavior_type].map({1: 0, 2: 1, 3: 3, 4: 5}) log_df log_df[log_df[score] 0] item_sim build_news_item_similarity(log_df, time_window_days3, alpha0.3) save_similarity_matrix(item_sim) print(f[{datetime.now()}] similarity matrix updated, {len(item_sim)} items)你可以在命令行用python -c from daily_update import daily_update; daily_update()手动触发也可以挂到系统 crontab。重点是理解这个拆分相似度矩阵属于中间结果对新闻可保留半天到一天对用户没有个性化没必要实时计算。把耗时操作移出请求链路是推荐系统性能优化的第一课。4.2 在线推荐接口用 Flask 暴露一个最小推荐服务新闻推荐的在线部分做两件事读预计算的相似度矩阵根据用户 ID 查其历史行为并生成推荐列表。用 Flask 写一个极简接口方便你在本地验证也方便答辩时演示 HTTP 请求。# app.py from flask import Flask, jsonify, request import pandas as pd from collections import defaultdict import pickle app Flask(__name__) # 全局加载相似度矩阵和评分矩阵避免每次请求都读文件 item_sim pickle.load(open(item_sim_cache.pkl, rb)) rating_matrix pd.read_csv(rating_matrix.csv, index_col0) app.route(/api/recommend, methods[GET]) def recommend(): user_id request.args.get(user_id, ) if not user_id or user_id not in rating_matrix.index: return jsonify({code: 404, msg: user not found, data: []}) user_history set(rating_matrix.loc[user_id][rating_matrix.loc[user_id] 0].index) candidate_scores defaultdict(float) for news_id in user_history: if news_id not in item_sim: continue user_score rating_matrix.loc[user_id, news_id] for other_id, sim in item_sim[news_id].items(): if other_id not in user_history: candidate_scores[other_id] sim * user_score # 过滤掉发布时间超过48小时的新闻需要在日志里带publish_time ranked sorted(candidate_scores.items(), keylambda x: x[1], reverseTrue)[:10] result [{news_id: nid, score: round(score, 4)} for nid, score in ranked] return jsonify({code: 200, user_id: user_id, data: result}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)启动服务后浏览器或命令行访问http://127.0.0.1:5000/api/recommend?user_idU0001就能返回这个用户的个性化推荐。rating_matrix.csv文件需要在日常更新任务里一并导出保证用户最新行为能反映到推荐接口。这里有一个取舍接口读的是内存中的item_sim和rating_matrix这意味着代码变更后需要重启服务才能生效。开发调试可以开debugTrue但答辩演示请务必关闭。4.3 冷启动用户和冷启动新闻推荐系统绕不开的两个兜底策略ItemCF 对老用户有很好的效果但新用户没有任何行为记录查不到相似度新新闻没有任何用户点击也无法进入同现矩阵。这两类冷启动在新闻场景比电商更棘手因为新闻每天都有新增。我的做法是用三层兜底新用户先推热门新闻新新闻先在热门新闻的相似候选中补充“最新发布”队列再配合用户主动选择的兴趣标签把标签匹配的新闻加权混入推荐列表。def recommend_with_cold_start(user_id, rating_matrix, item_sim, news_df, top_n10): if user_id not in rating_matrix.index: # 新用户推荐近24小时热度最高的新闻 cold_start_pool news_df[ (pd.to_datetime(news_df[publish_ts]) pd.Timestamp.now() - pd.Timedelta(hours24)) ].sort_values(hot_rank, ascendingFalse) return cold_start_pool[news_id].head(top_n).tolist() # 老用户正常ItemCF 混入最新新闻 rec_list recommend_for_user(user_id, rating_matrix, item_sim, top_nint(top_n * 0.8)) recent_news news_df[ (pd.to_datetime(news_df[publish_ts]) pd.Timestamp.now() - pd.Timedelta(hours12)) ][news_id].tolist() # 把最新新闻插到推荐列表的2, 4, 6位置 result [] rec_iter iter(rec_list) for i in range(top_n): if i % 2 1 and recent_news: result.append(recent_news.pop(0)) else: try: result.append(next(rec_iter)) except StopIteration: break return result这个插队策略看起来粗暴但符合新闻产品的直觉推荐结果不能全是旧闻要让新内容有曝光机会。参数top_n * 0.8表示个性化结果占八成两成留给新内容。你可以把比例调成 7:3 或 9:1通过观察推荐列表的点击率来定没有固定最优解。5. 避坑与排查新闻推荐系统最常见的 5 个翻车现场5.1 “推荐全是爆款没有个性化”评分归一化出问题现象不同用户调用推荐接口返回的 Top-10 新闻高度重叠几乎都是全站热门。原因原始行为分数直接用behavior_type的权重值没有做用户级归一化。一个读了 50 篇新闻的重度用户的分数天然比只读 3 篇的轻度用户高导致 ItemCF 的加权累加结果偏向“重度用户喜欢的内容”。另一层原因是热门新闻的同现次数天然很大相似度矩阵被热门新闻主导。解决把用户行为分数做最大最小归一化让每个用户的分数向量长度一致同时在相似度计算里把alpha调大比如从 0.3 调到 0.6加大对热门新闻的惩罚。不要一次性两个参数都改不然无法判断是谁起的作用。5.2 相似度矩阵全是 0pivot_table 的默认填充值在捣乱现象item_sim构建完成后打印发现大部分新闻的相似度列表为空。原因pivot_table生成评分矩阵时fillna(0)没有对列名做对齐。如果训练日志里某些新闻只在窗口外出现过它们的列会在过滤窗口行为后被整体丢弃而iterrows()遍历时又可能拿到空行。常见新手错误是直接对rating_matrix做row 0判断忽略了NaN。解决pivot 之前先log_df log_df.dropna(subset[user_id, news_id])构建相似度时用row.fillna(0)再判断。这个坑最气人的地方是代码不报错只是安静地返回空结果排查时优先打印len(user_items)和len(item_click_count)两个中间量。5.3 推荐结果里全是旧闻今天发布的新闻永远出不来现象推荐列表稳定但点进去的新闻都是 5 天前的最近两天发布的新内容完全没有曝光。原因ItemCF 的相似度完全来自行为同现新新闻没有行为没有同现同时时间窗口淘汰了旧行为进一步压缩了新新闻的历史数据来源。这个坑是算法设计的盲区不是代码 bug。解决在推荐生成阶段混入新内容队列正如 4.3 节所示。另一种常见做法是“时间衰减 时间提升”双机制相似度计算时做时间衰减越旧的行为权重越低排序时给新发布的新闻加一个时间提升分发布越新分数加成越高。建议在recommend_for_user的分数后面乘一个time_boost 1 exp(-age/24)让 24 小时内的新闻获得最高加成。5.4 用户相似度出现 1.0 的“完美相似”行为次数太少导致的假象现象某两篇冷门新闻的相似度是 1.0但业务上看起来毫不相关。原因当两篇新闻只被同一个用户点击过同现次数为 1且各自点击次数都是 1 时余弦相似度计算结果就是 1.0。这是小样本的统计假象不是真实相似。解决相似度计算时增加最低同现阈值min_co_occur2或者对低于阈值的相似度直接置 0。我在代码里用if co_count 2: sim 0一行挡住比调alpha更精准。毕设里写清楚这个处理方式是加分项。5.5 A/B 测试做不出来离线指标和在线指标对不上现象离线评测显示 AUC 提升 5%上线后点击率反而下降。原因协同过滤的离线指标如准确率、召回率是在“已有点击行为”上评测的但推荐系统改变的是用户的“可见范围”。大量新推荐用户没看过所以没点击不代表推荐不好——只是 A/B 测试时间太短没有覆盖用户发现新内容的过程。解决毕设不要求真上线但你可以做一个时间序列回测取第 1-7 天数据训练第 8-10 天数据评测观察“推荐曝光后是否带来新点击”。同时记录“推荐列表的新闻平均年龄”如果推荐列表平均年龄小于全站新闻平均年龄说明时效性在起作用。这三个指标一起看比单看准确率靠谱。6. 验证推荐质量离线指标之外的三个可操作检查以及我最后留下的一个习惯你以为写完算法和接口就是终点其实答辩时真正经得起追问的是“怎么验证它有效”。离线评测我建议做两个经典指标准确率推荐列表里被用户点击的比例和召回率用户点击过的新闻里被推荐覆盖的比例。但你更需要的是三个能肉眼判断的技术检查它们能帮你快速定位算法是否正确。第一检查推荐列表的多样性。取同一个用户两天前的推荐和今天的推荐计算 Jaccard 相似度如果超过 0.7说明推荐结果几乎没有变化兴趣漂移没有起作用。第二检查相似度矩阵的热门惩罚是否生效。随机抽一篇热门新闻和一篇冷门新闻打印它们的相似度前 5 名热门新闻的相似度均值应该明显低于冷门新闻。第三检查冷启动渠道是否真实曝光。给一个新建空用户发推荐请求结果应该全是最新新闻而不是报错空列表。def verify_recall_quality(user_id, rec_list, held_out_logs): 简易离线验证推荐列表对用户实际点击的覆盖情况 held_out_logs: 训练窗口之后新产生的用户行为日志 actual_clicked set( held_out_logs[held_out_logs[user_id] user_id][news_id] ) rec_set set(rec_list) if not actual_clicked: return 0.0 recall len(rec_set actual_clicked) / len(actual_clicked) precision len(rec_set actual_clicked) / len(rec_set) return precision, recall这个函数用起来很简单训练时留出最后 2 天的日志不参与相似度计算推荐生成后用这 2 天的真实点击做验证。如果 precision 在 5% 以下别急着改算法先去看看推荐列表里是否混入了大量热门旧闻——大概率是热门惩罚没生效。我在调通第一版时precision 只有 2%排查后发现是hot_rank字段在日志生成脚本里没有传给相似度计算函数导致惩罚项恒为 0。这种低级错误靠看代码很难发现但一打印相似度前几名的hot_rank就原形毕露。最后说一个我留下的习惯。在我自己做的新闻推荐项目里每次跑完离线评测我会把“相似度最高的 5 对新闻”打印成一张表肉眼扫一遍。这比任何指标都直观因为指标会骗人但“科技新闻和娱乐新闻相似度 0.9”这种结果一眼就能看出数据或权重配比有问题。你拿这个习惯去检查 ItemCF 结果能省下大量调参时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表