ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

农产品智能推荐系统:协同过滤、ALS矩阵分解与线上重排

农产品智能推荐系统:协同过滤、ALS矩阵分解与线上重排 简介这份西南财经大学学士学位毕业论文围绕协同过滤算法在农产品智能推荐系统中的应用展开适合计算机科学、数据科学、人工智能等专业的本科生与研究生以及正在准备毕业设计、论文开题的研究者参考。论文先梳理推荐系统与协同过滤的研究现状再分别讲解基于用户和基于物品的协同过滤原理并讨论冷启动、数据稀疏与计算复杂度等典型问题随后结合农产品的季节性、地域性和新鲜度特征说明用户画像构建、推荐策略设计与评价指标选择最后给出实验设计、结果分析和优化方向。压缩包内仅含1个docx文件约32KB属于结构完整的学位论文正文不含额外素材或代码脚本适合直接用于阅读、引用与框架参考。目前已有302人学习下载可帮助读者快速掌握协同过滤的实现思路、算法优缺点辨析和农产品推荐场景的落地方法也为毕业论文的章节组织与实验设计提供可借鉴的样本。1. 农产品智能推荐系统里协同过滤算法到底解决什么问题一家做社区生鲜的平台后台常年挂着两千个左右的 SKU首页能给到的坑位只有十二个。运营每天早上对着 Excel 手排排到最后还是那几样鸡蛋、土豆、西红柿。用户端的表现就是打开首页没有新鲜感复购靠的是价格补贴而不是推荐。农产品推荐和服饰、图书那类目最大的不同在于三点一是强季节性六月的西瓜和十二月的羊蝎子是两套完全不同的商品池二是短保鲜期临期品的推荐价值必须被放大三是复购周期短用户在一周内可能对同一商品产生多次行为噪声和信号交织在一起。协同过滤算法在农产品智能推荐系统里的位置就是把「谁和谁像」「谁买过什么」这两件事算清楚用用户群体的历史行为补足单用户的兴趣判断。这套方案最适合已经有稳定订单流水的生鲜、社区团购、农批电商团队日均行为日志在十万条以上时收益最明显数据量太小的团队先做规则兜底更划算。2. 从用户行为到评分矩阵农产品协同过滤的数据建模与相似度选型2.1 把浏览、加购、下单、复购折成偏好分农产品几乎没有显式评分。用户不会给一斤白菜打四星能拿到的只有行为流浏览、加入购物车、下单、复购。把这些行为直接当二元信号用会丢掉强度差异——浏览十次没下单和直接下单含义完全不同。常见做法是给每类行为配一个基础权重再叠加时间衰减让近两周的行为比三个月前的行为更有话语权。import pandas as pd import numpy as np # 行为流字段user_id, item_id, action, ts logs pd.read_csv(behavior.csv, parse_dates[ts]) now logs[ts].max() ACTION_W {view: 1.0, cart: 3.0, order: 5.0, repurchase: 8.0} HALF_LIFE 14 # 半衰期单位天 logs[weight] logs[action].map(ACTION_W) logs[days_ago] (now - logs[ts]).dt.total_seconds() / 86400 logs[score] logs[weight] * np.power(0.5, logs[days_ago] / HALF_LIFE) # 同一用户对同一商品多次行为取最大值而不是求和 mat logs.groupby([user_id, item_id])[score].max().reset_index()逻辑说明weight控制行为强度cart给 3 是因为加购比浏览的购买意图明确得多HALF_LIFE14是农产品的经验值生鲜复购快超过两周的行为参考价值明显下降如果做的是米面粮油这类长周期品类把它调到 45 到 60 更合适。最后一步用max而不是sum是关键一个用户反复浏览同一款水果三十次求和会把分数刷到远超真实下单用户导致召回被少数高频活跃用户带偏。如果业务上有明确的负反馈退款、差评、连续曝光未点击可以额外给一个 0.3 到 0.5 的负权重让这类商品在对应用户的向量里被压低。2.2 用户-商品矩阵的构建与稀疏度诊断拿到打分表之后要做的第一件事不是训练模型而是看稀疏度。农产品的品类集中度高用户行为往往集中在头部几十个商品上长尾商品只有零星交互这直接决定了模型能不能跑起来。n_users mat[user_id].nunique() n_items mat[item_id].nunique() sparsity 1 - len(mat) / (n_users * n_items) print(f用户 {n_users}商品 {n_items}交互 {len(mat)}稀疏度 {sparsity:.5f})判断标准比较直接稀疏度低于 0.99协同过滤通常能跑出可用结果高于 0.999UserCF 会大面积失效因为两个用户之间几乎找不到共同购买项。这时候要么退回到 ItemCF要么直接上矩阵分解。还要单独统计每个商品的交互数分布如果前 5% 的商品吃掉了 70% 以上的交互就必须在训练前做长尾处理否则模型学出来的就是一份「爆款排行榜」。2.3 相似度怎么选余弦、皮尔逊、调整余弦在农产品数据上的差别相似度函数不是随手挑一个它决定了「像」这个字的定义。相似度计算核心农产品场景适配注意点余弦相似度向量夹角看方向不看绝对值隐式行为打分适合高频短周期未中心化热门商品普遍偏高皮尔逊相关系数先减用户均值再算相关有显式评分的场景共同评分项少于 3 项时方差为零调整余弦先减物品均值冷门商品也需要公平曝光计算量比余弦大杰卡德相似度集合交并比只有买/没买的二元数据丢失购买频次信息在农产品推荐里隐式行为权重差异大复购 8 分、浏览 1 分余弦相似度是最省事也最稳的起点。如果发现推荐结果里全是鸡蛋、土豆这类全民商品换成调整余弦往往能改善长尾曝光代价是每次更新相似度矩阵的计算时间会涨一截商品数上万时要配合 TopN 截断。2.4 UserCF 还是 ItemCF由复购周期决定维度UserCFItemCF相似度更新频率用户兴趣变化快需高频更新商品关联相对稳定可离线算可解释性弱难给运营一句话说清强「买过 X 的人常买 Y」适合的农产品场景社区团购、拼团、限时秒杀生鲜标品、日常复购品稀疏数据下的表现用户数远大于商品数时更差商品数可控时更稳农产品平台的商品数通常在几千级别用户数是商品数的几十倍这个比例天然偏向 ItemCF。而且运营需要一个能拿去做文案的解释ItemCF 给出的「搭配购买」直接能写成推荐理由。from scipy.sparse import csr_matrix from sklearn.preprocessing import normalize users mat[user_id].astype(category) items mat[item_id].astype(category) R csr_matrix((mat[score], (users.cat.codes, items.cat.codes))) # ItemCF转置后按行 L2 归一化内积即余弦相似度 item_user normalize(R.T, norml2, axis1) sim (item_user item_user.T).toarray() np.fill_diagonal(sim, 0) # 自己和自己相似度置零 # TopN 截断只保留每个商品最相近的 50 个邻居 TOPN 50 sim_trunc np.zeros_like(sim) for i in range(sim.shape[0]): idx np.argpartition(sim[i], -TOPN)[-TOPN:] sim_trunc[i, idx] sim[i, idx]normalize这一步不能省内积只有在向量都是单位长度时才等于余弦相似度。TOPN50是精度和内存的折中商品数到两万以上时完整的相似度矩阵会吃掉几个 G 内存截断后能压到几百兆。截断会损失一部分召回实践中用 50 到 100 之间通常够用。3. 协同过滤算法的核心实现ALS 矩阵分解与 SVD 在农产品数据上的落地3.1 SVD 和 ALS 的选型显式评分还是隐式行为Surprise 库里的 SVD 是带偏置的 FunkSVD专为显式评分设计它假设缺失值是未知而不是零。农产品场景里用户没买过某个商品不代表不喜欢只是没机会看到这种情况下把缺失值当零更合理。所以选型判断很简单如果平台有明确的星级评分比如团购团长给商品的评价用 SVD如果是纯行为流用 implicit 的 ALS。两者的差别在损失函数上ALS 对每个观测值引入一个置信度行为越多置信度越高未观测到的项权重压得很低正好对应「没买过可能是没曝光」这个假设。3.2 用 Surprise 的 SVD 跑通最小可复现示例先用小数据集验证流程能不能跑通再上生产规模。from surprise import Dataset, Reader, SVD from surprise.model_selection import train_test_split from surprise import accuracy # df 需要 user_id、item_id、rating 三列 reader Reader(rating_scale(1, 5)) data Dataset.load_from_df(df[[user_id, item_id, rating]], reader) trainset, testset train_test_split(data, test_size0.2, random_state42) algo SVD( n_factors64, # 隐向量维度 n_epochs30, # 迭代轮数 lr_all0.005, # 学习率 reg_all0.02, # 正则化系数 random_state42, ) algo.fit(trainset) preds algo.predict(testset) accuracy.rmse(preds)n_factors64是先验起点农产品的品类维度不高蔬菜、水果、肉禽、水产、粮油等十几大类32 到 64 基本够再大容易把噪声也学进去。reg_all0.02控制过拟合如果离线 RMSE 一直抖先把它调到 0.05 再看。n_epochs30之后收益递减明显超过 50 轮基本是在记训练集。这套流程适合做基线但要接线上还得换成 ALS。3.3 implicit 的 ALS农产品隐式反馈的主力模型import implicit import numpy as np from scipy.sparse import csr_matrix # user_item行用户列商品值2.1 算出的偏好分 user_item csr_matrix((mat[score], (users.cat.codes, items.cat.codes))) model implicit.als.AlternatingLeastSquares( factors64, regularization0.05, iterations20, alpha40, random_state42, ) model.fit(user_item) # 给用户 0 推荐 10 个商品过滤掉已交互的 ids, scores model.recommend(0, user_item[0], N10, filter_already_liked_itemsTrue)alpha是 ALS 里最需要调的参数它决定「有过一次行为」被当成多强的偏好40 是隐式反馈的常用值调到 100 以上会把一次随手浏览也当成强烈兴趣推荐结果会变得极端。regularization0.05偏大一点是有意的农产品数据噪声重正则强一些能让推荐更稳。iterations20观察下来 15 到 25 之间就能收敛再堆轮数是浪费机器。filter_already_liked_itemsTrue必须开否则推荐里会混进用户刚买过的商品直接影响点击率。参数速查参数建议范围作用调过头的后果factors32–128隐向量维度过大过拟合商品间区分度反而下降regularization0.01–0.1L2 正则强度过大推荐趋同人人都是爆款alpha10–40观测值置信度放大过大把偶然浏览当强偏好iterations15–30交替最小二乘轮数收益递减纯耗算力3.4 把季节窗口写进训练集按月份分片建模农产品最大的特殊性在季节。用全年数据训练一个模型会把夏天爱买西瓜、冬天爱囤火锅料的信号平均掉最后推荐出来的东西对谁都不够贴。SEASON_MONTHS { spring: [3, 4, 5], summer: [6, 7, 8], autumn: [9, 10, 11], winter: [12, 1, 2], } for season, months in SEASON_MONTHS.items(): sub logs[logs[ts].dt.month.isin(months)] # 每个季节单独训一个 ALS线上按当前月份路由到对应模型 # 同时保留一个全量模型做兜底避免刚换季时数据量不足分片会带来一个副作用刚进入某个月份时该季节的样本量可能偏少模型不稳定。常见做法是保留一份全量模型在季节模型的训练样本低于某个阈值时自动 fallback 到全量。另外季节分片只在商品有强季节性时收益明显如果平台主打的是米面油这类全年品分片反而会稀释数据得不偿失。4. 冷启动、稀疏与季节性农产品智能推荐系统的协同过滤排错4.1 新用户和新商品冷启动的三条兜底路径新用户没有任何交互协同过滤给不出分数。硬要推荐只能用热门榜效果等于没做推荐。农产品的冷启动其实有天然抓手配送地址本身就是极强的先验。三条兜底路径按优先级排第一用收货地址的社区/城市维度做地域热门召回南方城市优先推叶菜和水产北方城市优先推面点和根茎类第二用当前月份做季节召回六月推西瓜、荔枝十二月推柑橘、羊肉第三如果平台有注册时选择的饮食偏好是否吃辣、是否素食用它做标签过滤。三路结果融合后再按商品毛重和客单价做一次平衡避免全是低价引流品。新商品的冷启动反过来用 ItemCF计算新商品与已有商品的属性相似度品类、产地、价格带、包装规格借用最相似商品的 embedding 作为初始向量上线后随着交互累积再逐步切换到真实向量。4.2 稀疏与长尾为什么你的推荐总在推那几个爆款推荐结果被爆款垄断通常不是模型的错是数据处理阶段就埋了雷。先做一个诊断item_cnt mat.groupby(item_id).size().sort_values(ascendingFalse) head_share item_cnt.head(int(len(item_cnt) * 0.05)).sum() / item_cnt.sum() print(f前 5% 商品占据交互比例{head_share:.2%})这个值超过 70% 就说明头部效应严重。处理手段是把商品交互次数做逆频率加权让冷门商品的每次交互在训练中更有分量item_freq mat.groupby(item_id)[item_id].transform(count) mat[score_weighted] mat[score] / np.power(item_freq, 0.3)指数 0.3 是温和降权调到 0.5 以上会让长尾商品冲得太前点击率下滑。另一个必须做的动作是给推荐结果加类目多样性约束同一一级类目最多占坑位数的一半否则十有八九全是水果。4.3 离线评估PrecisionK、RecallK、NDCGK 的代码实现离线指标不能只看 RMSE。RMSE 衡量的是打分准不准业务关心的是 TopN 排得对不对。import numpy as np def ndcg_at_k(ranked, truth, k10): ranked模型给出的有序商品列表truth测试集里真实交互的商品集合 dcg sum(1.0 / np.log2(i 2) for i, it in enumerate(ranked[:k]) if it in truth) idcg sum(1.0 / np.log2(i 2) for i in range(min(len(truth), k))) return dcg / idcg if idcg 0 else 0.0 def precision_at_k(ranked, truth, k10): hit len(set(ranked[:k]) set(truth)) return hit / k def recall_at_k(ranked, truth, k10): hit len(set(ranked[:k]) set(truth)) return hit / len(truth) if truth else 0.0NDCGK对位置敏感排第一和排第十的贡献差很多最贴近首页坑位的真实价值。评估时必须用时间切分而不是随机切分用前 80% 时间的行为训练后 20% 时间的行为测试。随机切分会让模型看到未来数据离线指标虚高上线后打脸。指标参考阈值说明Precision100.08–0.20生鲜类目普遍偏低能到 0.15 已不错Recall100.10–0.30与用户历史购买广度强相关NDCG100.15–0.35主要看趋势不必强求绝对值覆盖率 30%低于此值说明推荐过度集中4.4 线上常见故障与排查表现象可能原因排查动作推荐位全是鸡蛋、土豆爆款主导看覆盖率指标开逆频率降权新用户推荐为空无交互记录检查地域季节兜底是否生效离线涨、线上 CTR 不涨训练集含未来信息改时间切分排查特征泄漏结果一整天不变离线调度未触发检查 T1 任务的依赖和告警换季后还在推过季商品季节路由未切换核对月份路由表和模型版本同一用户刷两次结果不同召回未做去重稳定固定随机种子加结果缓存5. 从离线分数到线上坑位协同过滤推荐结果的重排技巧5.1 多路召回分数的归一化与加权融合协同过滤只是召回的一路通常还要并上热门榜、地域榜、季节榜。不同来源的分数不在一个量纲上直接相加没有意义先做 min-max 归一化再按权重融合def normalize(scores): lo, hi min(scores), max(scores) if hi - lo 1e-9: return [0.5] * len(scores) return [(s - lo) / (hi - lo) for s in scores] final {} for src, w in [(cf, 0.5), (region, 0.2), (season, 0.2), (hot, 0.1)]: ids, sc recall[src] for i, s in zip(ids, normalize(sc)): final[i] final.get(i, 0) w * s ranked sorted(final.items(), keylambda x: -x[1])[:50]权重不是拍脑袋定的用小流量 AB 实验扫一遍协同过滤的权重通常落在 0.4 到 0.6 之间。低于 0.3 时个性化基本失效等于在做热门榜。5.2 用 Faiss 把向量召回做到毫秒级ALS 训完会得到用户和商品的隐向量线上用 Faiss 做近邻检索比遍历全量商品快几个数量级import faiss import numpy as np item_vecs np.asarray(model.item_factors, dtypefloat32) faiss.normalize_L2(item_vecs) # 归一化后内积等价于余弦 index faiss.IndexFlatIP(item_vecs.shape[1]) index.add(item_vecs) query user_vec.reshape(1, -1).astype(float32) faiss.normalize_L2(query) scores, ids index.search(query, 50) # 取 Top50 候选商品数超过十万时把IndexFlatIP换成IndexIVFFlat用nlist控制倒排桶数量召回率损失通常在 2% 以内查询延迟能降到毫秒级。5.3 按保鲜期重排一个农产品独有的技巧这是农产品推荐相对其他类目最容易做出差异的地方。协同过滤算的是「用户想不想买」但生鲜还有一层约束「这批货还能放几天」。把临期库存的紧迫度作为加权项叠上去def rerank_shelf_life(cands, base_score, shelf_life, stock_age, alpha0.15): cands: 候选商品 id 列表 base_score: 协同过滤融合后的归一化分数 shelf_life: {商品id: 保质期天数} stock_age: {商品id: 当前批次已入库天数} alpha: 临期加权强度建议 0.1-0.25 out [] for cid, s in zip(cands, base_score): remain max(shelf_life[cid] - stock_age[cid], 0) urgency 1.0 - remain / shelf_life[cid] out.append((cid, s alpha * urgency)) return sorted(out, keylambda x: -x[1])alpha必须设上限。调到 0.3 以上首页会变成临期清仓专区短期 GMV 好看用户信任掉得很快。稳妥的做法是给临期商品单独开一个坑位或一个「今日特惠」模块而不是让它挤进主推荐流。上线前用离线回放验证一遍拿过去两周的日志重跑看临期加权的推荐是否真的带来更多实际成交而不是只有点击没有下单。本文还有配套的精品资源点击获取
返回列表