ARTICLE DETAIL

资讯详情

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

基于Python的新疆特产推荐系统:ItemCF协同过滤设计与实现

基于Python的新疆特产推荐系统:ItemCF协同过滤设计与实现 每年答辩季都能看到一堆“基于XXX的推荐系统设计与实现”的题目但坦白讲能把这个题目真正做实、做透、讲清楚的项目并不多。我自己做完“基于Python的新疆特产推荐系统的设计与实现”这个项目之后最大的体会是推荐系统听起来是个高大上的算法问题真正拿到手里绝大部分工作量其实在数据处理、相似度计算和业务逻辑缝合上。这篇博文就把我整个项目从需求拆解到代码落地、再到踩坑复盘的过程完整写出来适合正在做毕设或课设的同学也适合想用Python快速搭一个能跑通的推荐系统Demo的开发者参考。你能从里面拿走的不只是代码思路还有一套完整的设计与实现方法论。1. 项目需求与整体设计思路1.1 为什么选择“新疆特产”作为推荐场景新疆特产本身的品类多、价格跨度大、信息不对称严重这个特点天然适合做推荐系统。我一开始列了一下特产清单像是若羌红枣、吐鲁番葡萄干、薄皮核桃、巴旦木、库尔勒香梨、阿克苏苹果、伊犁薰衣草制品、罗布麻茶、黑枸杞、和田大枣这些单品之间的用户偏好差异非常大。有人偏好干果类有人偏好鲜果类还有人只对养生类产品感兴趣。如果平台只是把所有特产堆在一个页面上用户从里面找到自己想要的东西成本非常高。尤其是像红枣这种商品若羌枣、和田枣、哈密枣在口感和价格上差别很大没有辅助决策工具用户基本靠猜。推荐系统在这里面解决的问题本质上是一个“信息过滤”问题。它通过分析用户的历史行为包括浏览、收藏、评分、购买记录把符合用户偏好的特产优先展示出来。我在项目里把需求拆成了三层基础的数据管理、核心的推荐引擎、上层的Web交互展示。1.2 功能需求拆解我落地的时候把系统分成了六大功能模块用户模块注册、登录、个人信息管理商品模块特产信息录入、分类管理、详情展示行为模块记录用户的浏览、评分、收藏行为推荐模块基于物品的协同过滤推荐、热门推荐、分类偏好推荐展示模块推荐列表页、商品详情页、用户评分页管理模块后台数据统计和基础配置这里面最核心、也最容易被低估的是行为模块。很多第一次做推荐系统的同学把精力全放在算法上结果数据库里根本没有用户行为数据推荐引擎跑起来没有输入纯属白搭。所以我从一开始就确定了数据先行原则先把用户行为数据结构和生成逻辑定好再去写算法代码。因为推荐系统的输入是行为数据输出是推荐列表输入端如果没想清楚后端算法再漂亮也只是一堆死代码。1.3 技术选型思路这个项目为什么选Python不用多说数据处理和推荐算法这块Python生态实在太成熟。我在技术选型上做了如下选择Web框架Flask。轻量、容易写、适合课程设计和快速原型搭建设计数据处理pandas、numpy处理评分矩阵和相似度计算非常顺手推荐算法scikit-learn的cosine_similarity也可以自己手写方便答辩时候讲原理数据库SQLite用于开发MySQL用于部署演示。对推荐系统来说数据量不大时SQLite足够前端Jinja2模板 Bootstrap不用单独搞前后端分离降低整个项目的复杂度选择Flask而不是Django是因为这个项目的主体不是Web业务而是推荐算法和数据处理。Flask足够轻能把核心精力放在推荐链路上。不过如果你喜欢更规范化的工程结构Django也可以但我觉得对毕设/课设来说Flask会更清晰直接。2. 推荐算法选型为什么主用ItemCF2.1 三种主流推荐算法横向对比推荐系统的核心算法方向主要就三类基于内容的推荐、协同过滤推荐、混合推荐。协同过滤里面又分基于用户的UserCF和基于物品的ItemCF我把它们的适用场景整理成了一张对比表方便后来人快速判断该选哪种。算法类型核心思路优点缺点适用场景基于内容用物品本身的特征建立画像推荐相似物品不需要其他用户数据无冷启动问题特征提取难容易推荐重复内容新闻、文章推荐UserCF找兴趣相似的用户推荐那些用户喜欢的物品推荐结果有惊喜性能发现新品类用户量大时计算开销大冷启动差新闻类、社交类平台ItemCF找物品之间的相似度推荐用户喜欢物品的相似物品解释性强推荐结果稳定适合电商难以给用户推荐全新品类电商、视频、购物平台我做这个项目的时候一开始其实想过用UserCF因为网上很多教程都拿电影推荐数据集来讲用户对电影的评分数据比较稠密。但这个项目是特产电商场景用户数量少行为数据稀疏UserCF算出来邻居用户往往不准确。而且特产推荐更讲究“看这个商品的人还看了什么”这个场景天然适合ItemCF。2.2 ItemCF在特产推荐中的天然优势ItemCF的核心逻辑是给你推荐那些和你喜欢过的物品相似的物品。比如你给“若羌红枣”打过分系统就会去找到和若羌红枣相似度高的“和田大枣”“灰枣”把这些推荐给你。这个逻辑在电商场景里解释性特别强。用户看到推荐结果第一反应是“对我喜欢红枣所以推荐红枣是合理的”这提高了用户对推荐结果的信任度。另一个优势是物品相似度矩阵可以离线计算。特产商品的种类一般来说不会特别多不像商品SKU动辄百万级所以我可以在系统空闲时把相似度矩阵算好存到数据库或者缓存里用户请求时直接查表响应速度非常快。我最终选型的方案是以ItemCF为主推荐算法用基于特产分类的内容推荐做冷启动兜底再用热门特产做平台级兜底。三者的关系是用户有行为记录就走ItemCF行为太少但注册时选了偏好分类就走内容推荐一无所有就按热度推荐。2.3 相似度计算与评分预测的数学原理ItemCF里面最核心的是物品相似度的计算。最常用的方法是余弦相似度公式如下similarity(i, j) cos(向量i, 向量j) (向量i · 向量j) / (||向量i|| * ||向量j||)这里物品的向量是“用户-物品评分矩阵”中的一列。比如有100个用户物品“若羌红枣”的向量就可以表示成这100个用户对它的评分没评过的位置补0。两个商品如果总是被同一批用户喜欢它们的向量方向就接近余弦相似度就高。余弦相似度的好处是它不受评分尺度的影响。有的用户习惯打高分有的用户习惯打低分但对同一个物品向量来说我们更关心的是它在不同用户之间的相对关系这一点余弦相似度处理得比欧氏距离好。评分预测我用的是加权平均公式prediction(user, item) sum( sim(item, item_i) * rating(user, item_i) ) / sum( sim(item, item_i) )其中sim是物品与物品的相似度rating是用户对已评物品的评分。这个方法很好理解如果一个物品跟用户喜欢的另一个物品很像那用户对这个物品的打分预测就偏向那个相似物品的评分。2.4 冷启动问题怎么兜底冷启动是推荐系统里最经典的问题就是新用户或者新商品没有任何行为数据时推荐系统完全无法工作。我这个项目里用了三层策略第一层新用户注册时选择感兴趣的品类比如说“干果”“鲜果”“养生茶饮”“手工艺品”系统根据品类做基于内容的推荐把对应品类的热门商品推荐给用户。第二层用户有了一定的评分行为后用ItemCF提供个性化推荐。第三层如果品类偏好也没有就按平台整体热度推荐把浏览量和评分最高的前10个特产展示出来。这三层策略不是互相替代而是按数据的丰富程度逐级递进。我在代码里写了一个推荐入口函数先判断行为数量再决定走哪条推荐链路后面我会详细讲实现。3. 数据准备与数据库设计3.1 数据从哪来爬虫、公开数据与模拟数据的取舍做推荐系统的同学问得最多的一个问题就是推荐算法学会了到哪里去找数据这个问题我在做项目时也纠结了很久。最理想的数据来源当然是真实的电商平台数据但现实中很难拿到用户隐私级别的行为数据。我自己尝试过爬取公开的电商展示数据比如特产的名称、分类、价格、图片链接、销量这部分是商品基础数据可以用爬虫去采集。但用户评分、用户行为这些数据是平台核心资产不可能公开给你。所以我的方案是商品基础信息用手工整理加公开数据补充用户行为数据用程序模拟生成。模拟生成不是乱编而是按照一定的用户画像逻辑来生成。比如我设计了“偏好干果型用户”“偏好健康养生型用户”“价格敏感型用户”等几类每类用户对对应分类的特产打分偏高对无关分类打分偏低模拟真实行为模式。这样做的原因是要让推荐算法有规律可学。如果评分是纯随机生成的那任何推荐算法都学不到有效规律系统跑起来效果一定差。模拟数据虽然不能完全等于真实数据但至少能验证推荐链路的正确性。3.2 数据库表结构设计这个项目我设计了四张核心表分别是用户表、商品表、评分表和行为日志表。下面给出核心字段设计用户表字段名类型说明user_idINTEGER主键usernameVARCHAR用户名password_hashVARCHAR密码哈希preference_typeVARCHAR偏好品类用于冷启动推荐created_atDATETIME注册时间商品表字段名类型说明item_idINTEGER主键nameVARCHAR特产名称categoryVARCHAR分类如干果、鲜果、养生茶饮等priceFLOAT价格descriptionTEXT描述image_urlVARCHAR图片地址stockINTEGER库存评分表字段名类型说明idINTEGER主键user_idINTEGER用户IDitem_idINTEGER商品IDratingFLOAT评分1-5created_atDATETIME评分时间行为日志表记录浏览和收藏行为这个表我主要用于数据统计和后续扩展如果只是做简单推荐评分表就够用了。3.3 数据清洗的几个关键点数据清洗虽然不起眼但直接影响推荐效果我在这上面踩过坑。主要做了三件事第一去重。商品名称可能因为不同商家叫法不同出现“若羌红枣一级”和“若羌红枣特级”这种两个商品其实是同类的情况我通过名称归一化处理去掉等级、规格这类修饰词再按分类合并。第二处理缺失值。评分表里如果有为空的数据直接丢弃或者用该物品的平均分填充。我选择的是平均分填充这样可以保留数据量同时不会对相似度计算造成太大偏差。第三评分归一化。有些人打分手松全给5分有些人手紧最高只给4分。为了消除这种“评分尺度偏差”我做了每个用户评分减去个人平均分的处理然后再构建评分矩阵。这一步对余弦相似度的计算结果影响很大因为评分尺度被拉伸开了物品向量的区分度也变高了。4. 推荐引擎与Web系统的核心实现4.1 离线相似度矩阵计算我先把评分数据从数据库加载进来用pandas构建“用户-物品”评分矩阵然后调用sklearn的cosine_similarity计算物品相似度。核心代码如下可以直接抄走改改。import pandas as pd from sklearn.metrics.pairwise import cosine_similarity # 从SQLite读取评分数据 ratings pd.read_sql_query(SELECT user_id, item_id, rating FROM ratings, conn) # 构建用户-物品评分矩阵缺失位置填0 user_item_matrix ratings.pivot_table( indexuser_id, columnsitem_id, valuesrating, fill_value0 ) # 计算物品相似度矩阵 item_sim_matrix cosine_similarity(user_item_matrix.T) # 转成DataFrame方便查询 item_sim_df pd.DataFrame( item_sim_matrix, indexuser_item_matrix.columns, columnsuser_item_matrix.columns ) # 保存到本地文件接口调用时直接加载 item_sim_df.to_pickle(item_sim_df.pkl)这段代码里有个细节需要注意pivot_table里fill_value0意思是用户没评过分的物品位置补0。但在余弦相似度里“没评分补0”其实等于把“不喜欢”和“没评过”混为一谈了。严谨的做法是只计算两个物品共同被评分的用户集合但为了简化并利用成熟的库函数补0是当前最常见的思路。要是追求更精确的结果可以手写皮尔逊相关系数只统计共同评分项代价是代码稍长答辩时候如果问起来能答上来就没事。4.2 推荐函数的核心逻辑相似度矩阵算好之后推荐逻辑就简单清晰了。我给用户推荐物品的流程是找出用户已经评分过的物品列表找出所有用户没评分过的物品遍历这些未评分物品加权计算预测评分按预测评分排序取TopN直接上代码import pickle import pandas as pd # 加载离线计算好的相似度矩阵 item_sim_df pd.read_pickle(item_sim_df.pkl) def recommend_items(user_id, top_n10): # 取该用户已评分的物品 user_rated ratings[ratings[user_id] user_id] if user_rated.empty: # 没有行为时走热门/分类推荐 return cold_start_recommend(user_id, top_n) all_items item_sim_df.columns user_unrated [it for it in all_items if it not in user_rated[item_id].values] scores {} for item in user_unrated: pred_score 0.0 sim_sum 0.0 for _, row in user_rated.iterrows(): rated_item row[item_id] sim item_sim_df.loc[item, rated_item] # 跳过相似度为0的物品提高计算效率 if sim 0: continue pred_score sim * row[rating] sim_sum sim if sim_sum 0: scores[item] pred_score / sim_sum # 按预测评分倒序排列取前N个 sorted_items sorted(scores.items(), keylambda x: x[1], reverseTrue) return [item_id for item_id, _ in sorted_items[:top_n]]这个实现逻辑非常直白嵌套了两层循环时间复杂度是O(未评分数 * 已评分数)。对特产这种小规模商品集几百个商品体量完全跑得动。如果你的数据集到了几万级别需要改成矩阵运算用numpy一次性算所有候选物品的预测分那样代码会复杂很多。项目答辩时面试官如果问性能优化你把这一点说到位说明你是真考虑过扩展性的。4.3 冷启动与热门兜底实现冷启动函数我分成了两种情况注册时选了偏好品类和没选品类直接用热门榜。def cold_start_recommend(user_id, top_n10): # 从数据库拿到用户的偏好品类 user get_user_by_id(user_id) if user and user[preference_type]: # 按偏好品类取商品按评分均值排序 items get_items_by_category(user[preference_type]) items items.sort_values(avg_rating, ascendingFalse) return items[item_id].head(top_n).tolist() else: # 无偏好时取全站热门TopN popular get_popular_items() return popular[item_id].head(top_n).tolist()热门商品的计算可以很简单就是一个按平均评分和浏览量的综合热度公式。我用的热度公式是hot_score 0.7 * avg_rating 0.3 * log10(views 1)加log10是因为浏览量的数量级远大于评分直接加会被浏览量带偏取对数可以压缩量纲差异。4.4 Flask接口与页面展示后端推荐逻辑完成后我用Flask封装成Web接口模板渲染推荐结果页面。from flask import Flask, render_template, request app Flask(__name__) app.route(/recommend) def recommend_api(): user_id request.args.get(user_id, typeint) if not user_id: return 缺少user_id参数, 400 rec_ids recommend_items(user_id, top_n12) items [get_item_by_id(i) for i in rec_ids] return render_template(recommend.html, itemsitems) app.route(/) def index(): popular get_popular_items() return render_template(index.html, itemspopular) if __name__ __main__: app.run(debugTrue, port5000)前端用Bootstrap做了个简洁的卡片列表每个特产展示图片、名称、价格和推荐理由。推荐理由这里我加了一个小亮点从相似度矩阵中取出用户已评分物品里相似度最高的那个生成一段“因为您喜欢若羌红枣所以为您推荐和田大枣相似度达82%”这样的解释。可解释性对推荐系统来说很重要也是答辩的加分项因为推荐不仅要把东西推给用户还要让用户理解为什么推。4.5 系统整体工作流程我把整条链路串起来从用户进入系统到看到推荐列表大概是这样一个过程用户登录后前端页面请求/recommend接口Flask检查用户是否有评分行为有评分行为加载离线相似度矩阵计算候选物品预测分排序取TopN没有评分行为但注册填了偏好按偏好品类热门推荐都是空按平台热门推荐推荐结果渲染成Web页面返回前端展示这个流程的好处是每层都有兜底用户在系统里任何时候都不会看到空推荐页。这一点很重要因为推荐系统的体验底线是必须有内容而不是推荐得有多准。5. 实际开发中遇到的问题与排查实录5.1 冷启动测试翻车新用户看到一片空白我第一次调试的时候注册了一个新账号进入推荐页结果页面是白屏。后来查了下发现是recommend_items里的冷启动分支没有兜住“用户注册时跳过偏好选择”的情况用户对象为空访问用户preference_type时报了AttributeError。这个问题提醒我用户行为统计和兜底逻辑一定要层层判断。后来我把冷启动函数改成了上面的三段式判断彻底堵住空数据入口。测试用例里专门加了一条注册新用户后直接调用推荐接口断言返回结果不为空。5.2 数据稀疏导致相似度矩阵全是0我模拟了50个用户、60个商品的评分数据但每个用户平均只评了3个商品结果发现商品相似度矩阵里大部分值都是0。这意味着推荐结果基本是靠少数几个有共同评分的商品撑起来的效果很不稳定。解决办法有两个方向。一个是“减稀疏”降低商品数量、提高用户评分数量比如让每个用户至少评8-10个商品保证共同评分项存在。另一个是“加平滑”当两个物品共同评分用户数少于阈值时强制把相似度置为0避免随机噪声干扰推荐结果。def smooth_similarity(matrix, min_common3): # matrix是物品相似度矩阵 # 统计两个物品被共同评分的用户数 common_counts matrix.copy() common_counts[common_counts 0] 1 common_counts common_counts common_counts.T # 共同评分用户数少于阈值的相似度置0 matrix[common_counts min_common] 0 return matrix这段代码的核心思想是相似度要有足够的数据支撑才可信。用矩阵乘法算共同评分用户数比用双重循环快很多。数据量大的时候这个优化效果非常明显。5.3 推荐结果过于大众化缺少个性化用热门推荐兜底跑了几轮之后发现推荐列表里永远是那几款销量最高的干果。虽然点击率不低但每个用户看到的都是同一批东西根本谈不上“个性化”。我加了一个“惊喜度”策略在推荐列表的末尾插入一个用户从未互动过、但相似度给到一定阈值的冷门品类商品。具体做法是在最终排序结果里把第9和第10个位置用低热度但高相似度的商品替换打破“永远只推热门”的僵局。这一点在答辩时被老师专门问过问如何平衡相关性和多样性。我把用位置混排的方式讲清楚之后老师给了个不错的评价说至少说明不是照搬教程代码。5.4 相似度计算内存爆了有一次我把商品数量扩到1万用户扩到5000直接构建用户-物品矩阵去算相似度结果内存飙升程序卡死。原因很好理解1万乘1万的相似度矩阵float64类型占的内存大约是800MB这还只是物品相似度的一半。全部的矩阵运算和临时对象内存妥妥爆掉。应对方案有三个限制参与计算的物品范围只对同品类或共现次数高的物品对计算相似度用scipy的稀疏矩阵保存只保留非零相似度条目相似度离线分块计算算完存数据库不一次性加载所有数据对特产推荐这种场景我最终选择的就是限制品类范围每个物品只跟同分类下的其他物品计算相似度。这样的结果不仅省内存而且相似度本身也更有业务意义。不同分类的物品就算余弦相似度高比如“红枣”和“葡萄干”对用户来说也可能没那么强关联反而容易误导推荐。5.5 常见问题速查表问题现象可能原因排查方向推荐列表为空冷启动分支没兜底检查用户是否有行为数据检查偏好品类是否为空相似度全是0数据太稀疏共同评分用户太少增加模拟评分量或提高最小共同评分阈值推荐结果全是大热门缺少个性化数据里热门商品噪声过大加入惊喜度混排策略限制热门商品权重程序内存溢出物品数过大相似度矩阵过密用稀疏矩阵或限定同分类计算评分预测偏差大用户评分尺度不一致做用户评分归一化处理推荐结果解释不清缺少推荐理由记录最大相似度来源生成可读解释文本6. 项目扩展与后续优化方向这个项目做完之后我复盘了一下完全有空间往下深挖。一个方向是做实时推荐。现在的相似度矩阵是离线算好的用户行为变化后不会立刻反映到推荐结果里。如果要支持实时性可以考虑把用户最近行为放到Redis里每次请求时只对最近评过的物品做增量相似度取数再和离线结果加权融合。另一个方向是引入混合推荐。ItemCF虽然适合电商但用户如果只评了红枣它就只会推荐红枣的亲戚很难跨界推荐薰衣草枕头或者黑枸杞这种新品类。这个时候需要加入基于内容的分类特征或者用简单的矩阵分解把潜在特征挖掘出来给用户推一些“看起来不一样但其实会喜欢”的商品。SVD矩阵分解就是一个不错的进阶方向sklearn里有TruncatedSVD可以直接用。还有一个很实用的方向是推荐理由的生成和交互反馈。我现在已经加了“因为您喜欢A所以推荐B”的解释如果再做一个“赞一下/不感兴趣”的反馈按钮把反馈数据回写到行为表里就能形成一个推荐-反馈-优化的闭环这个系统就真的从Demo走向产品了。写在最后的一点实际体会项目做完我最大的收获不是把ItemCF背熟了而是真正理解了推荐系统是个“数据决定上限”的事情。算法只决定了你能跑多快、算多准而数据的完整性、清洗的干净程度、冷启动兜底的设计决定了系统能不能用、好不好用。在做这个项目之前我以为推荐系统最难的算法部分做完才发现最麻烦的其实是把用户行为、商品信息、相似度矩阵这三层数据给理顺。如果你们也在做类似的推荐系统项目我的建议是先用20个用户、30个商品、每个用户评10个商品把整条链路跑通再逐步扩到真实数据规模。小数据把所有逻辑验证好了后面扩数据只是时间和机器的事。先把链路走通你会少走很多弯路。最后的最后分享一个小经验代码里推荐结果正确与否不要只看生成列表对不对一定要看中间数据比如相似度矩阵、预测分排名把中间结果打印出来检查一遍。很多你以为的“推荐失败”其实都是前面的数据问题中间过程一眼就能看出来。这个习惯陪我避开了至少一半的坑。
返回列表