ARTICLE DETAIL

资讯详情

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

基于Python协同过滤的淘宝商铺推荐系统实现与调优

基于Python协同过滤的淘宝商铺推荐系统实现与调优 简介面向电商推荐系统学习者和Web开发者的完整项目源码基于Python、Django与Vue.js实现淘宝商铺个性化推荐。项目采用Scrapy爬虫采集商品及用户行为数据通过基于物品的协同过滤算法计算相似度并生成推荐列表前后端分离架构覆盖从数据抓取、存储到推荐展示的完整流程。压缩包共122个文件核心为40个Python源码及36个编译后的pyc文件含后端逻辑、算法实现与爬虫脚本另有26个JavaScript文件和7个CSS文件支撑前端界面附带SQLite数据库、配置文件、说明文档及页面图标等静态资源整体仅1.77MB结构紧凑便于下载与部署。目前已有378人学习适合用于课程设计、毕业设计或电商推荐算法入门实践。通过这套资源可快速掌握Scrapy采集流程、Django接口开发、Vue组件化页面搭建及协同过滤算法落地要点还可基于现有代码扩展功能或替换数据集直接复用其目录设计思路。1. 一个 python 压缩包装着一套能跑通的淘宝商铺推荐系统打开这个python基于协同过滤的淘宝商铺推荐系统.zip本质上是拿到一个完整的教学型推荐系统项目Python 写的数据处理脚本、协同过滤推荐引擎、以及一份用于演示的淘宝商铺数据样本。它的作用是让你在本地用pandas和numpy就能复现“用户浏览/收藏/购买商铺”到“给用户推荐相似商铺”的全链路适合正在做毕设、准备转行推荐系统方向、或者想给电商项目加一个“猜你喜欢”模块的开发者。和商品推荐不一样商铺推荐的粒度更粗、行为更稀疏这套系统的核心在于用 item-based 协同过滤把“商铺-用户”矩阵的相似度算出来然后做 Top-N 推荐。它在设计上也相当克制数据量控制在单机内存能跑动的级别没有吹分布式没有硬凑深度学习老老实实走“相似度计算—评分预测—排序输出”的经典路线。2. 协同过滤选型为什么商铺推荐更适合 ItemCF 而不是 UserCF2.1 先从“人找店”和“店找人”的落地场景说起淘宝商铺推荐的业务诉求非常直接用户没有明确的搜索词系统根据他过往的行为猜他接下来想逛哪家店。这里有两个物理事实用户数量远大于商铺数量流量集中在头部店铺用户行为极其稀疏多数用户一个月只产生几十条商铺行为。如果在这条场景上用 UserCF要实时维护“和你兴趣相似的人”的集合在线模块改一个用户的画像就要重算邻居关系行为特征一变就失效。而 ItemCF 先离线把商铺的相似度矩阵算好在线阶段只需要查表用户对哪些店有正向行为直接取这些店的相似商铺作为候选再按用户自己的偏好强度加权排序。这个流程非常适合“商铺为推荐主体”的场景。另一个直接原因在于可解释性。UserCF 给出的推荐理由是“和你相似的用户喜欢这家店”用户想看穿这层关系很费力而 ItemCF 给的解释是“你收藏过的 X 号店铺和这家店有很高的相似度”在电商 App 的推荐卡片上直接就能展示为“看了又看”这一栏。实际部署时线上推荐流里用户对“理由可感知”的反馈率提升非常明显这也是业内把 ItemCF 作为召回层标配的原因。最后还需要考虑冷启动恢复能力一个只有一两次交互的新用户UserCF 几乎找不准邻居但 ItemCF 只需要他点过的一家店就能顺着相似商铺链路给出候选。2.2 相似度计算的两种底座余弦相似度与皮尔逊相关系数商铺相似度是整个系统的心脏。最常用的计算方式是余弦相似度。把每个商铺表示为一个向量向量的维度是所有用户向量的每个值是用户对该商铺的行为强度两个商铺之间的相似度就是两个向量夹角的余弦值。公式是cos(A, B) (A·B) / (|A| * |B|)在 Python 里用numpy一行可以算完import numpy as np def cosine_similarity(vec_a, vec_b): dot np.dot(vec_a, vec_b) norm_a np.linalg.norm(vec_a) norm_b np.linalg.norm(vec_b) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b)这段代码里最关键的是分母的模长归一化。淘宝商铺数据里有些店铺被几万人标记过“收藏”有些店铺只有几十个人收藏如果不除以模长热门商铺和任何商铺的相似度都会虚高直接导致推荐列表全被头部大店刷屏。除以模长之后相似度描述的是“方向一致性”而不是“规模大小”这才能把长尾的小众商铺也拉进候选池。另外注意那个norm_a 0的判断数据清洗不彻底时向量可能全为 0直接除会得到 nan这个兜底务必保留。皮尔逊相关系数的差别在于它额外做了均值中心化公式是sim Σ((x - x̄)(y - ȳ)) / sqrt(Σ((x - x̄)²) * Σ((y - ȳ)²))。它的好处是能消除用户打分尺度不同带来的偏差比如 A 用户习惯给小店都打 1 分、给旗舰店打 5 分B 用户刚好反过来余弦相似度会认为这两个用户没有共性皮尔逊认为减去均值之后趋势一致。因此当你的“行为强度”字段不只是 0/1而是带有明确的浏览、收藏、加购、购买多级权重时我优先使用皮尔逊。商铺推荐里行为字段天然就是多级的所以通常的做法是两种都算一遍召回结果差异大的商铺单独排查。2.3 用 Python 直接手写相似度矩阵不调第三方推荐库很多初学者一上来就pip install surprise或者implicit但这两个库的 API 是围绕“评分预测”设计的拿来做商铺 Top-N 推荐需要做不少数据格式适配。更麻烦的是如果你交上去的代码别人跑不起来环境问题比算法问题更致命。我一般建议在pandas和numpy环境下把相似度矩阵手写出来这版代码既能在 Jupyter 里复现也能直接扔进 Flask 接口做服务。用pandas构造一个“商铺-用户”的评分矩阵import pandas as pd # data.csv 字段: user_id, shop_id, behavior, weight df pd.read_csv(data/shop_behavior.csv) # 行为权重映射浏览1分、收藏2分、加购3分、下单4分 behavior_weight {view: 1, collect: 2, cart: 3, buy: 4} df[score] df[behavior].map(behavior_weight) # 构造商铺-用户矩阵行是商铺列是用户空位补0 shop_user_matrix df.pivot_table( indexshop_id, columnsuser_id, valuesscore, fill_value0 ) print(shop_user_matrix.shape)pivot_table会把缺失的交互填成 0这在数学上等价于“该用户对该商铺没有行为”符合矩阵补零的直觉。但要留意一个细节如果样本中某个用户只有一条“浏览”记录他的向量就会极度稀疏和任何商铺算出来的余弦相似度都会被这一个非零值主导。后续在避坑章节会展开讲这类稀疏噪声怎么处理。至于为什么不用 Spark、不用分布式原因很简单这套代码在 10 万级用户、1 万级商铺的数据规模下内存占用不超过 2GB单机跑完整条链路只需要几分钟。只有当商铺数量到百万级、交互记录到亿级才需要考虑用 Spark 重写相似度计算。热词里提到的“基于 Spark 的电商系统推荐”是另一条路线本文不展开。3. 从原始行为日志到商铺评分矩阵数据清洗的三道关卡3.1 行为数据的去重策略保留最强的那个行为而不是全部保留淘宝商铺推荐的数据入口通常是埋点日志同一对用户商铺可能同时存在浏览、收藏、加购、支付四种行为。直接把这四行都塞进 pivot_table同一个用户在矩阵的同一点上会被后来者覆盖结果随行顺序漂移推荐结果变得不可复现。常规做法是聚合成“用户对商铺的最强行为”即如果既有浏览又有购买只保留购买这一条。这样既避免了重复也让评分矩阵的物理含义变得清晰# 按 user_id, shop_id 分组取权重最大的行为作为该对的最终得分 df[score] df[behavior].map(behavior_weight) df_dedup ( df.sort_values(score, ascendingFalse) .drop_duplicates(subset[user_id, shop_id], keepfirst) ) # 过滤掉完全没有区分度的账号行为数少于5的用户保留意义不大 user_active_count df_dedup.groupby(user_id).size() active_users user_active_count[user_active_count 5].index df_filtered df_dedup[df_dedup[user_id].isin(active_users)]drop_duplicates先按分数升序排列再按用户商铺去重keepfirst留下的就是分数最高的那条。这里有第二个关键过滤行为数少于 5 的用户直接剔除。他们可能是刚注册的测试号也可能是爬虫扫店留下的垃圾行为这些用户会让相似度矩阵出现大量只有一个非零值的行拉低所有相关计算的稳定性。行为数阈值的设置需要根据数据规模调整1 万条以下建议阈值设为 310 万条以上可以提到 10。3.2 商铺维度的频次裁剪头腰部店铺建索引尾部店铺不参与相似度计算日志里大量商铺只有个位数的交互记录参与矩阵运算后不仅拖慢速度还会在相似度计算时产生“共现次数过低”的伪相似。两个只被同一个用户点过的店铺余弦相似度可能高达 1.0但这完全是偶然共现不具备推荐价值。所以要做商铺侧裁剪只有交互次数超过阈值通常取 20的店铺才进入相似度矩阵其他商铺在推荐阶段用“热门兜底”策略覆盖。shop_count df_filtered.groupby(shop_id).size() keep_shops shop_count[shop_count 20].index df_use df_filtered[df_filtered[shop_id].isin(keep_shops)] shop_user_matrix df_use.pivot_table( indexshop_id, columnsuser_id, valuesscore, fill_value0 ) print(f有效商铺数: {shop_user_matrix.shape[0]}, 有效用户数: {shop_user_matrix.shape[1]})这个裁剪是刚性的尾部商铺虽然数量多但对推荐系统整体质量的贡献趋近于零而且会引入大量噪声相似对。保持候选池在 2000 到 5000 个商铺是参与交互运算的合理区间计算相似度矩阵的复杂度是 O(n²·m)n 是商铺数m 是用户数把 n 从 5 万砍到 3000运算量直接降两个数量级。3.3 训练集和测试集的切分按时间切而不是按行随机切推荐系统评估最忌讳随机切分因为推荐天然依赖时间顺序用 3 月份的数据训练去预测 5 月份的行为才贴近线上真实。如果随机切分测试集里会混进训练集同期的行为评估指标虚高。常见的做法是时间戳字段存在的话按 80/20 的时间比例把交互记录一份为二前 80% 时间段内的行为作为训练矩阵后 20% 时间段内的行为作为验证。df_use[ts] pd.to_datetime(df_use[ts]) split_ts df_use[ts].quantile(0.8) train_df df_use[df_use[ts] split_ts] test_df df_use[df_use[ts] split_ts] # 测试集里只保留训练矩阵中存在过的用户和商铺保证可计算性 train_users set(train_df[user_id]) train_shops set(train_df[shop_id]) test_df test_df[ test_df[user_id].isin(train_users) test_df[shop_id].isin(train_shops) ]用quantile(0.8)切分的好处是不用写死具体日期数据更新后阈值自动跟着分布走。最后一步过滤测试集里的新用户和新商铺是因为协同过滤没有对完全陌生的实体做过学习把它们留在测试集里只会让指标难看不等于算法变差。真正的冷启动评估要单独做不混在这条链路里。4. ItemCF 推荐引擎的实现从相似商铺库到个性化 Top-N 列表4.1 构建商铺相似度矩阵用字典存储稀疏相似对更高效用pandas.DataFrame存商铺相似度矩阵会浪费大量内存因为大部分商铺对的相似度是 0 或者低到无意义。实际工程常用字典套字典的结构外层键是商铺 id内层是“邻居商铺 id → 相似度值”。计算流程是遍历每个商铺取用户向量再和其余商铺做余弦计算。直接双重 for 循环在 3000 个商铺规模下是九百万次向量点积性能压力不小。常见做法是先转成numpy数组用矩阵乘法一次性求所有对的点积。import numpy as np matrix shop_user_matrix.values shop_ids shop_user_matrix.index.tolist() # 归一化后做矩阵乘法点积结果就是余弦相似度 norm_matrix matrix / np.linalg.norm(matrix, axis1, keepdimsTrue) sim_matrix np.dot(norm_matrix, norm_matrix.T) # 只保留相似度大于 0.1 的邻居稀疏化存储 sim_dict {} n len(shop_ids) for i in range(n): neighbors {} for j in range(n): if i j: continue score sim_matrix[i, j] if score 0.1: neighbors[shop_ids[j]] round(float(score), 4) sim_dict[shop_ids[i]] neighborsnp.linalg.norm(axis1, keepdimsTrue)先做行归一化然后一次矩阵乘法就把所有商铺对的余弦相似度算完比 for 循环快一到两个数量级。相似度阈值 0.1 是一个经验值调大了召回变少调小了噪声对会进入候选。数据清洗质量高的情况下可以放到 0.15但稀疏数据下 0.1 更稳。注意round(float(score), 4)是为了压缩存储也给后续排序减少无意义的浮点尾数干扰。4.2 召回与打分为什么只用“强行为商铺”做种子而不是全量历史在线推荐的时候如果把用户所有交互过的商铺都作为种子去召回计算量会随用户历史长度线性膨胀而且浏览过但没产生兴趣的弱信号商铺会把候选列表带偏。常规策略是只取分数不低于 2即收藏及以上的商铺作为种子浏览行为只作为冷启动的备用信号。这样既保证召回基于真实兴趣又控制了计算规模def recommend_for_user(user_id, top_n10): # 取该用户在矩阵中的评分向量 if user_id not in shop_user_matrix.columns: return [] user_vector shop_user_matrix[user_id] # 种子商铺行为分数 2 且相似度字典中存在邻居 seed_shops user_vector[user_vector 2].index.tolist() if not seed_shops: # 无强行为时降级取最近浏览过的3个商铺做种子 seed_shops user_vector[user_vector 0].index.tolist()[:3] if not seed_shops: return [] score_bucket {} for shop in seed_shops: for neighbor, sim in sim_dict.get(shop, {}).items(): if neighbor in seed_shops: continue # 已交互过的商铺不再推荐 # 权重 用户对种子的偏好分 * 商铺相似度 score_bucket[neighbor] score_bucket.get(neighbor, 0) user_vector[shop] * sim # 按累计得分排序取前 top_n ranked sorted(score_bucket.items(), keylambda x: x[1], reverseTrue) return [shop_id for shop_id, _ in ranked[:top_n]]打分逻辑只看种子商铺对候选商铺的“加权贡献”用户对种子店铺的偏好分数越高、种子与候选的相似度越高候选得分越大。这里有一个很关键的工程决定不做归一化。如果不除以种子数量历史行为多的用户分数天然偏高排序本身不受影响。但如果你想把分数对外展示为百分比或星级就需要按种子数量和最大可能得分做缩放。代码里user_vector[shop] * sim的乘积在数值上常小于 1多次累加结果稳定阈值不是必需品。4.3 三大必调参数相似度阈值、种子行为阈值与邻居候选数量参数调优是推荐系统里玄学最多的环节但商铺推荐这个场景里有效的参数其实只有三个。第一个是相似度阈值上文代码里的0.1决定候选池大小。调到 0.06 时召回量几乎翻倍但准确率下降调到 0.2 时推荐结果趋于保守全是和种子店铺几乎雷同的店。第二个是种子行为阈值即user_vector 2这个过滤条件。收藏起步是合理的如果把浏览也算进来阈值降到 1推荐列表会偏向热门流量店铺因为浏览行为的信号噪声比太低。第三个是 top_n 裁剪不要直接对所有相似邻居累加分数先对每个种子的邻居按相似度取前 30 个再进入累计能显著降低长尾商品的偶然共现干扰NEIGHBOR_LIMIT 30 for shop in seed_shops: # 先给当前种子的邻居按相似度排序截断到前30 sorted_neighbors sorted( sim_dict.get(shop, {}).items(), keylambda x: x[1], reverseTrue )[:NEIGHBOR_LIMIT] for neighbor, sim in sorted_neighbors: if neighbor in seed_shops: continue score_bucket[neighbor] score_bucket.get(neighbor, 0) user_vector[shop] * simNEIGHBOR_LIMIT不要设置太大30 到 50 之间是实测比较舒服的区间。选太大则长尾噪声进入打分太小则热门大店垄断相似邻居。实际调试时可以分别把 20、30、50 跑一遍推荐结果看推荐列表的多样性变化。判断多样性的粗略方式是看推荐结果中店铺的类目分布如果 10 条推荐里 8 条都是同一类目说明邻居截断过大相似度矩阵已经被头部类目主导。5. 避坑zip 伪加密、稀疏矩阵与冷启动三类翻车现场5.1 zip 伪加密解压提示需要密码但项目原本没有密码拿到python基于协同过滤的淘宝商铺推荐系统.zip后很多人在解压这一关就卡住了。WinRAR 提示输入密码但 README 里没说密码的事。很大概率不是真的加密而是 zip 伪加密。伪加密是文件头里的“加密标志位”被改成了加密状态但数据本身没有加密用常规工具解压会误报。解决方法是修改文件头对应字节或者直接换用 7-Zip 打开它在遇到伪加密时会尝试忽略标志位强制解压。如果你用 Python 读取zipfile模块对这个场景也会抛RuntimeError: File is encrypted这时可以检查ZipInfo.flag_bits的 bit 0import zipfile with zipfile.ZipFile(shop_recommend_system.zip) as zf: for info in zf.infolist(): # flag_bits 第0位为1表示加密伪加密则数据区未加密 is_encrypted (info.flag_bits 0x1) 1 print(info.filename, encrypted flag:, is_encrypted)如果是伪加密直接用zf.extractall()通常能正常解出原始文件因为 Python 的zipfile只检查 flag 位不校验真正是否加密。如果真的加密了且你确定这个包来自公开渠道优先检查下载页面附件说明或 README 首行注释教学项目普遍不会真的加密码。为文件强制做暴力破解没有必要花的时间远超重新下载的代价。5.2 整个推荐列表全是热门店铺调低相似度阈值却没有任何改善现象给用户推荐的 10 家店里至少有 7 家是平台头部大店完全看不到个性化味道。原因有两个一是数据裁剪阶段没有过滤“公共行为用户”二是归一化前没有做 IDF 降权。大店拥有大量用户交互和任何其他商铺计算相似度时公共用户多导致得分天然偏高。解决在计算余弦相似度时引入“商铺流行度惩罚”方式是对交互数取对数进行降权# 商铺流行度惩罚因子交互越多的店权重越低 shop_popularity df_use.groupby(shop_id).size() popularity_penalty 1 / np.log(shop_popularity 1) # 对矩阵的每一行乘以惩罚因子再做归一化 matrix_penalized shop_user_matrix.values * popularity_penalty.values.reshape(-1, 1) matrix_penalized matrix_penalized / np.linalg.norm(matrix_penalized, axis1, keepdimsTrue) sim_matrix np.dot(matrix_penalized, matrix_penalized.T)这个惩罚因子的直观理解是热门店的相似度被压缩到原来的三分之一小众店铺的相似度相对抬升。副作用是热门店之间的强相似关系被削弱偶尔会出现“推荐结果和用户收藏毫无关系”的错觉所以惩罚因子不能过度1 / log(n1)这个非线性折中通常够用。5.3 冷启动用户一个用户只有一条浏览记录推荐结果完全随机协同过滤对这类用户的输出本来就是不可用的因为没有任何偏好信号可依赖。不要试图在协同过滤层解决冷启动它做不到。正确的做法是做一个降级策略对该用户直接返回商铺热度榜 Top-N并且每次请求随机偏移几位避免所有冷启动用户看到完全相同的列表。在服务层加一条分支逻辑if seed_shops_count 0: # 冷启动降级返回热门商铺榜并加随机偏移 hot_shops ( df_use.groupby(shop_id) .agg({user_id: nunique}) .reset_index() .sort_values(user_id, ascendingFalse) ) offset random.randint(0, 5) return hot_shops[shop_id].tolist()[offset:offset top_n]这个偏移技巧是线上常用的小把戏既保证了曝光内容的质量下限又制造了个性化差异。需要记住边界冷启动降级逻辑放在用户维度判断而不是商铺维度判断。5.4 相似商铺共现次数过低两个店铺只被同一个用户交互过相似度却是 1.0这条是数据稀疏导致的经典翻车。两个店铺因为同一个用户的行为余弦相似度被算出来是满值 1.0。放到推荐列表里就成了莫名其妙的关联推荐。解决手段是给相似度乘一个“共现惩罚系数”两个商铺共同交互的用户数越少相似度越不可信。推荐在相似度计算后加一层过滤# co_occurrence: 两个商铺共同被多少个用户交互过 # 在两个商铺的交互用户集合交集不为空时统计 from collections import defaultdict user_to_shops defaultdict(set) for uid, sid in zip(df_use[user_id], df_use[shop_id]): user_to_shops[uid].add(sid) co_count defaultdict(int) for uid, shops in user_to_shops.items(): shop_list list(shops) for i in range(len(shop_list)): for j in range(i 1, len(shop_list)): co_count[(shop_list[i], shop_list[j])] 1 co_count[(shop_list[j], shop_list[i])] 1计算完共现数后把相似度乘以一个缩放系数共现次数小于 3 的对直接丢弃。这个操作能去掉大量由偶然共现引起的“幽灵相似对”代价是需要多一层集合运算数据量大时耗时明显但对最终推荐质量的提升非常显著。6. 离线评估用准确率与召回率判断你的系统值不值得上线推荐系统的口碑是跑评估跑出来的不是调参调出来的。对商铺 Top-N 推荐最直接的评估指标是PrecisionN和RecallN。对测试集中的每个用户取他的真实行为商铺集合作为 ground truth看推荐列表里有多少命中。下面的脚本基于已经切好的test_df计算hit_count 0 total_recommend 0 total_relevant 0 for uid in test_df[user_id].unique(): true_shops set(test_df[test_df[user_id] uid][shop_id].tolist()) if not true_shops: continue rec_shops recommend_for_user(uid, top_n10) if not rec_shops: continue hits len(set(rec_shops) true_shops) hit_count hits total_recommend len(rec_shops) total_relevant len(true_shops) precision hit_count / total_recommend recall hit_count / total_relevant print(fPrecision10: {precision:.4f}, Recall10: {recall:.4f})这样算出来的是全局宏平均如果用户的真实行为数量极不均等建议再按用户分别算 PN 后取均值避免大行为量用户主导结果。对于这套商铺推荐系统在清洗干净的数据上P10 在 0.1 到 0.3 之间算正常R10 在 0.05 到 0.2 之间。不要期待更高的数值因为商铺粒度下用户可选择的店太多了模糊性本身就是业务的一部分。除了这两个指标还可以加一个覆盖率推荐结果里的商铺数除以总商铺数覆盖率低于 10% 说明推荐结果集中在头部个性化不足。最后用第一个用户的推荐列表做一次人工抽样打印商铺名称和来源种子店确认推荐链路的业务合理性。我自己的习惯是每个推荐项目先跑通评估脚本再开始调参数否则调了三天可能方向都是反的。希望帮到你。本文还有配套的精品资源点击获取
返回列表