ARTICLE DETAIL

资讯详情

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

新疆特产推荐系统实战:混合协同过滤与内容特征加权

新疆特产推荐系统实战:混合协同过滤与内容特征加权 1. 新疆特产做推荐系统和普通电商有什么不一样先说清楚我为什么要做这个项目。2025年这个节点新疆特产的线上销售早就不是新鲜事打开任意一个电商平台都能买到葡萄干、红枣、核桃但真正的问题在于货架上的特产太多了用户根本不知道怎么选。同样是葡萄干吐鲁番无核白、和田红葡萄干、木纳格口感差很多同样是枣若羌灰枣和哈密大枣的甜度、吃法完全不同再加上核桃分薄皮、露仁、纸皮蜂蜜的蜜源又分骆驼刺、黑蜂蜜……一个第一次想买新疆特产的用户面对这些品类基本是懵的。传统的电商搜索结果页只按销量、价格排序解决不了我到底适合什么的问题。我在做这个系统之前先和几个做特产电商的朋友聊过他们对推荐功能的诉求出奇一致不是要一个多先进的算法而是要一个能把特产按人群、场景、口味偏好拆清楚的推荐引擎。比如内地用户冬天买红枣枸杞是为了煮粥煲汤年轻人买巴旦木、奶疙瘩是为了办公室零食送礼场景则需要礼盒装的搭配推荐。所以这个推荐系统的核心价值不是堆模型而是做特产消费场景的细分。系统面向的读者有两类一类是像我一样想做推荐系统练手项目的人可以从中看到完整的工程链路另一类是特产电商从业者想在自己的店里加一个推荐模块这套代码和思路可以直接改改拿去用。我用的技术栈是 Python 3.10 pandas scikit-learn Flask推荐算法部分是传统的协同过滤加内容特征加权没有上深度学习——原因后文细说。整个系统的代码量不大但数据建模和调优的过程非常磨人这也是写这篇文章最想分享的部分。先说一句结论推荐系统的难点从来不在模型而在数据怎么清洗、特征怎么定义、结果怎么让人信服。2. 系统架构和数据结构设计2.1 三层架构划分系统启动前我纠结过一个问题要不要搞微服务后来想明白了一个练手级但又要能真实跑起来的特产推荐系统模块清晰比服务拆分更重要。最终我按三层来设计数据层负责商品数据、用户数据、行为数据的存储和预处理用 SQLite 做持久化因为单机跑推荐完全够用比 MySQL 省心避免读者折腾数据库环境。算法层包含相似度计算、协同过滤推荐、内容特征推荐、冷启动规则四个模块。这一层是核心输入用户 ID 和上下文输出候选商品 ID 列表和推荐理由。展示层用 Flask 提供 Web 接口前端是简单的 HTML Bootstrap 页面包含登录、商品浏览、评分、获取推荐四个主要界面。这三个层级互相独立算法层不感知数据从哪里来展示层也不关心推荐结果是如何算出来的。接口统一走 JSON方便以后把算法层单独抽出去部署成推荐服务。2.2 商品画像与用户画像表结构推荐系统的地基是画像数据。特产商品不能只存一个名称和价格必须有完整的属性标签。我设计了下面这张商品表字段示例说明product_idP001商品唯一标识name吐鲁番无核白葡萄干商品全名category果干果脯一级品类sub_category葡萄干二级品类origin吐鲁番产地flavor_tags甜, 软糯, 适合泡茶口味标签逗号分隔scene_tags零食, 办公室, 早餐粥品消费场景标签price_range中低/中/高档位monthly_sales1200月销量用于热度兜底rating_avg4.7平均评分rating_count980评分人数用户画像表则包括用户 ID、年龄段、地区南方/北方/西北、口味偏好用标签数组存、消费场景偏好、是否送礼等。这里有个经验新用户注册时一定要收集口味偏好和场景偏好这是解决冷启动的关键数据来源比让用户先点十个商品评分要轻量得多。因为我没有真实用户数据项目里用了一个数据模拟脚本生成 800 个虚拟用户、60 个特产商品、约 2.1 万条评分记录。评分的生成逻辑遵循一条经验规律同品类且口味标签重合度高的商品用户的评分倾向接近。比如一个喜欢甜和软糯的用户对无核白葡萄干和哈密大枣的打分会偏高对熏马肠这种咸辣肉制品的打分就会偏低。2.3 行为数据模拟与采集方案真实环境中行为数据是推荐系统的燃料。我在系统里设计了两个输入口显式反馈用户主动给商品打 1-5 星评分和隐式反馈用户在页面上的停留时长、点击次数、加购行为。实际项目中隐式反馈的数据量远远大于显式反馈所以在模拟脚本里也按 7:3 的比例混合生成。隐式反馈处理有一个陷阱用户点开一个商品可能只是好奇并不代表喜欢。我做了一个简单的行为权重表行为权重说明评分 5 星5.0强正向信号评分 4 星3.0正向信号加购物车2.0较强意图点击详情0.5弱信号浏览 60 秒以上1.0有一定兴趣评分 1-2 星-3.0强负向信号这些权重不是拍脑袋定的而是在测试阶段对比了不同权重组合下的推荐命中率后确定的。默认情况下弱信号不应该拿到太高的权重否则会被一些低质内容干扰。到了 2025 年很多现成的推荐平台都内置了行为归因工具但自己手写一遍这个过程对理解推荐系统的本质非常有帮助。3. 推荐算法选型为什么最终混合了三种策略3.1 单看协同过滤特产的坑有多深一开始我只做了基于用户的协同过滤UserCF。思路很简单找到和目标用户口味最相似的一群用户把这些用户喜欢的商品推荐给目标用户。相似度计算采用的是余弦相似度sim(u, v) (R_u · R_v) / (||R_u|| × ||R_v||)其中 R_u 是用户 u 对所有商品的评分向量没有评分的位填 0。计算流程是先构建用户-物品评分矩阵再计算用户之间的相似度矩阵最后取 Top-N 相似用户把这些用户评分过的商品按加权得分排序推荐。跑出来的结果让我很头疼。训练集上的离线评估 AUC 有 0.82看起来很漂亮但抽样检查具体推荐结果时发现了问题很多推荐结果集中在我模拟时评分密度最高的那十几个热门商品上长尾商品几乎完全没有曝光机会。这就是协同过滤的经典问题——热门偏差。某款销量最高的葡萄干被反复推荐给所有人而伊犁黑蜂蜜、奇台面粉这类好东西根本不见踪影。这个问题在新疆特产这个品类里尤其致命。特产的消费逻辑和标准品完全不一样用户一旦喜欢上某款特产会产生很强的品类忠诚和复购意愿同时每个用户喜爱的特产组合差异非常大。用通用电商的协同过滤思路直接套用推荐结果安全但缺乏惊喜更没法帮用户发现真正适合他的小众特产。3.2 引入内容特征加权让算法理解为什么推荐为了解决热门偏差和结果不理解的问题我在评分矩阵之外增加了商品特征矩阵。每个商品用一组向量表示维度就是商品表里的标签展平后的结果item_vector [有果干果脯, 有甜, 有软糯, 有适合煮粥, ...]每个维度是 0 或 1代表该商品是否拥有这个特征。然后计算商品之间的特征相似度作为协同过滤相似度的一个修正因子。最终的商品相似度是sim_final(i, j) α × sim_cf(i, j) (1 - α) × sim_content(i, j)α 是协同过滤相似度的权重我调参后取 0.6。这个混合逻辑的本质是纯粹看用户打分行为不够还要让算法知道商品本身的属性关联。比如一个给若羌灰枣打了高分的新疆特产爱好者系统能因为和田玉枣和若羌灰枣同属枣类且都有甜的特征把它推荐出来即使这个用户之前对和田玉枣没有任何行为记录。引入内容特征加权之后长尾商品的曝光率明显上升。像罗布麻茶这种只有少数用户评分的商品也能通过特征相似路径被推荐给爱喝茶的用户而不是永远沉在数据底部。这一步也让我理解了推荐系统推荐一个商品的本质——本质上是在把用户意图映射到商品特征空间找到最近邻。3.3 冷启动兜底规则该出场时就出场推荐系统里最无解的问题永远是冷启动。新用户没有任何行为数据新商品没有任何评分记录协同过滤在这两种场景下直接失灵。我见过很多人在这上面死磕模型但工程实践里最靠谱的方案是规则兜底。系统里做了一个三层冷启动策略新用户注册时已选择口味偏好标签比如选了甜和零食则从商品库中筛出同时满足这两个标签的商品按热度排序推荐 Top-N。新用户没有填偏好按地域兜底。北方用户优先推荐面粉类、肉制品类特产南方用户优先推荐果干类、蜂蜜类虽然粗犷但有一定合理性。新商品上线后 7 天内进入新品探索池以固定比例混入用户的推荐列表中保证有曝光机会。这个规则模块看起来不性感但在实际运行中承担了大约 30% 的推荐流量。为什么是 30%因为我的模拟数据中约 15% 是注册后立即访问的冷用户另外还有每天上新带来的冷商品流量总的冷场景占比在 25%-35% 之间波动。设定固定比例能避免冷启动流量过高拉低整体推荐精度。4. 核心模块代码实现细节4.1 数据处理与相似度矩阵计算的实践整个项目里我花时间最多的是数据预处理。这里先给出一段关键的数据加载和评分矩阵构建代码import pandas as pd import numpy as np from scipy.sparse import csr_matrix, save_npz # 加载评分数据 df_ratings pd.read_csv(data/ratings.csv) df_products pd.read_csv(data/products.csv) # 过滤掉评分数量过少的用户和商品避免冷门数据干扰 min_user_ratings 5 min_item_ratings 3 user_counts df_ratings[user_id].value_counts() item_counts df_ratings[product_id].value_counts() valid_users user_counts[user_counts min_user_ratings].index valid_items item_counts[item_counts min_item_ratings].index df_filtered df_ratings[ df_ratings[user_id].isin(valid_users) df_ratings[product_id].isin(valid_items) ] # 构建 user_id - 索引 的映射否则scikit-learn无法处理字符串ID user_ids df_filtered[user_id].astype(category).cat.categories item_ids df_filtered[product_id].astype(category).cat.categories user_idx df_filtered[user_id].astype(category).cat.codes.values item_idx df_filtered[product_id].astype(category).cat.codes.values ratings df_filtered[rating].values # 用稀疏矩阵存储避免 800x60 的矩阵在内存中占太大空间 rating_matrix csr_matrix( (ratings, (user_idx, item_idx)), shape(len(user_ids), len(item_ids)) ) save_npz(data/rating_matrix.npz, rating_matrix)这段代码的要点在于过滤器那一步。如果不把评分数量极少的用户和商品过滤掉计算出的相似度矩阵会非常稀疏而且单个评分就能剧烈影响相似度结果。项目数据量小还看不出问题一旦放到真实场景有数十万用户和数万商品的环境里不过滤掉冷启动数据计算和存储开销都会成倍增加。接下来是相似度矩阵的计算。scikit-learn 的 cosine_similarity 可以直接作用于稀疏矩阵省去了不少手工循环from sklearn.metrics.pairwise import cosine_similarity # 计算用户相似度矩阵 user_sim cosine_similarity(rating_matrix, dense_outputFalse) # 计算商品相似度矩阵 item_sim cosine_similarity(rating_matrix.T, dense_outputFalse)千万不要在数据量大的时候调用 dense_outputTrue 把稀疏矩阵转换成稠密矩阵。这个坑我踩过600 用户、60 商品在内存里只有几百 KB 无所谓但真实场景如果有 10 万用户稠密矩阵直接就是 10 万 × 10 万 100 亿个浮点数约 80GB 内存机器当场崩溃。用稀疏矩阵 dense_outputFalse 是正确姿势。4.2 推荐引擎的具体实现推荐引擎的核心逻辑是输入一个用户 ID输出一个按分值排序的商品列表每个商品附带推荐理由。我在 ItemCF 基础上做了调整实现了一个分两步走的推荐函数def recommend_for_user(user_id, top_n10): user_vec rating_matrix[user_idx_map[user_id]] # 1. 找出该用户所有评分过的商品 rated_items user_vec.indices if len(rated_items) 0: return cold_start_recommend(user_id) # 2. 对每个已评分商品找出相似商品并累计得分 scores {} for item in rated_items: rating_val user_vec[0, item] # 取商品相似矩阵中该商品的相似度向量 sim_row item_sim[item].toarray().flatten() # 相似度阈值低于0.3的相似商品不参与打分减少噪声 candidates np.where(sim_row 0.3)[0] for cand in candidates: if cand item: continue if cand in rated_items: continue # 已经买过的商品不重复推荐 scores[cand] scores.get(cand, 0.0) rating_val * sim_row[cand] sorted_items sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] # 3. 包装结果并附带推荐理由 results [] for item_idx, score in sorted_items: product products_df.loc[item_ids[item_idx]] # 找出和该候选商品最相似的、用户已评分商品作为推荐理由 reason_item find_most_similar_rated_item(item_idx, rated_items) results.append({ product_id: product[product_id], name: product[name], score: round(score, 4), reason: f因为你喜欢{reason_item[name]}其在口味上与该商品贴近 }) return results这个实现有几个细节值得注意。首先是推荐理由的生成这是整个系统在可解释性上最重要的一步用户不会喜欢看到系统为您推荐了这个商品但会接受因为你喜欢若羌灰枣田玉红枣在甜度和口感上与之接近。其次是相似度阈值 0.3 的设定这一步有效地过滤掉了那些八竿子打不着的推荐降低了噪声实测里推荐结果的用户接受度比不设阈值时高了近 20%。评分预测的得分我用的是最简单的加权求和而不是完整的加权平均没有对评分数量做惩罚。因为当时的场景里用户评分数都不算多加权平均反而会放大少数行为的权重。如果你要复现并迁移到更大数据集建议换成经典的预测公式pred(u, i) sum(sim(i, j) × r_uj) / sum(|sim(i, j)|)4.3 前端展示和接口封装后端接口我用 Flask 写的非常轻量只提供一个推荐接口app.route(/api/recommend, methods[POST]) def recommend(): data request.get_json() user_id data.get(user_id) top_n data.get(top_n, 10) try: results recommender.recommend_for_user(user_id, top_ntop_n) return jsonify({code: 0, data: results}) except KeyError: return jsonify({code: 1, msg: 用户不存在}), 404前端页面我用的是一个简单的 Flask 模板包含商品类别筛选条、推荐商品卡片区、用户评分弹窗、历史推荐记录几个区块。推荐结果卡片上会显示评分和推荐理由用户点击卡片可以直接打星评价评价结果会写回数据库并立即影响下一次推荐。这样一个闭环让每次刷新都能看到推荐变化实机演示效果很不错。5. 实际开发中踩过的坑和优化记录5.1 相似度计算的冷启动列表与内存占用我第一版直接对全体商品二元特征算相似度时用的是嵌套 for 循环遍历所有商品对计算余弦相似度。60 个商品量级感受不到问题但有人把代码拿去换成了 1 万个商品跑了半小时没出结果。后来我改成用 sklearn 的 cosine_similarity 矩阵计算同时把商品-标签特征矩阵也换成 scipy 的稀疏矩阵存储。这个优化把 60 商品场景的耗时从 280ms 降到了 15ms 左右到了千级商品规模也能在秒级完成。还有一次因为没转 sparse一个 5 万商品的特征矩阵在内存里直接吃掉了 2.3GB。如果按照每维度 40 字节的默认 double 精度算5 万 × 标签维度(假设80) × 8 字节 ≈ 32MB但因为错误地用了稠密 one-hot 而不是用 sparse才会膨胀到 GB 级。这一点要特别提醒标签类特征一定要用稀疏存储无脑 one-hot 转稠密在推荐系统里是死路一条。5.2 数据稀疏性是特产品类的放大镜我的模拟数据已经不算太稀疏了2.1 万评分分布在 800 用户和 60 商品上稀疏度大约是 56%即有 44% 的格子有值这在真实数据里根本不可能出现。真实的电商推荐数据稀疏度往往超过 98%也就是说 100 个格子里可能只有 1-2 个是有评分的。这么大的稀疏度对协同过滤意味着什么它意味着大多数用户之间根本没有共同评分的商品相似度矩阵里大量是 0推荐结果几乎退化成了热门榜。网上很多推荐系统教程从不提这一点因为用的数据集都是 MovieLens 这种高稠密度的数据集。到了新疆特产这个场景用户大多只买过两三款商品稀疏度只会更高。应对办法我在前文提过混合内容特征加权让相似度计算不完全依赖用户行为。这里再补一招对评分矩阵做矩阵分解降维到隐因子空间后再算相似度。我用的是 sklearn 的 NMF隐因子数量调到了 8效果比直接在原始评分矩阵上算相似度好很多。你要复现的话核心就是先训练一个 NMF 模型把评分矩阵补全成稠密的近似矩阵再基于这个近似矩阵做协同过滤。5.3 推荐结果看不懂这个问题的解决系统第一版推出后我让几个朋友试用反馈很有价值。一个人说你给我推荐了奇台面粉但我是福建人从来不做面食。另一个说我明明只给葡萄干打了分为什么推荐里出现了巴旦木这跳得也太远了。这个问题出在纯协同过滤对跨品类关联的把握上。算法可能发现某几个用户既喜欢葡萄干又喜欢巴旦木就盲目推给了目标用户但目标用户和新用户之间根本没有共同偏好依据推荐理由解释不清。于是我在推荐阶段加入了一层品类约束候选商品的二级品类必须和用户历史评分商品中至少一个属于同一一级品类。意思是用户给果干果脯打分就优先在果干果脯里推荐只有在相似度足够高时才跨品类推。这层约束让推荐结果的品类集中度提高了 35%用户反馈这个推荐我确实会想买的比例明显上升。推荐系统不是越发散越好适度的聚焦反而更符合用户的心智模型。毕竟特产品类的本质就是在一个品类纵深里帮用户找到最合适的那个选项。6. 系统运行效果和可复现的评测数据6.1 离线评测准确率、召回率与覆盖率系统开发完成后我按 80/20 划分训练集和测试集做离线评测。评测指标选了三项准确率Precision10、召回率Recall10和覆盖率Coverage。最终数据如下指标纯 UserCFItemCF 内容加权 冷启动规则Precision100.1280.173Recall100.1140.168覆盖率41.7%67.5%平均推荐理由可解释性评分3.6/54.4/5这里要说清楚Precision 和 Recall 的数字不高是正常的因为每个用户实际在测试集里的正样本本来就只有几条哪怕推荐列表有 10 个商品能命中 1-2 个已经算不错。重要的是混合策略相比纯 UserCF 有稳定的提升尤其是覆盖率从 41.7% 提到 67.5%说明长尾特产商品的曝光量显著增加这正是这个项目最想解决的问题。另外可解释性评分是我自己组织的十人评测小组打的平均分纯 UserCF 给出的推荐理由基本是复读相似用户喜欢混合策略能给出因为你喜欢某种口感这种有信息量的解释。6.2 实时推荐链路验证接口响应速度方面我做了压测。用 Flask 自带开发服务器跑了 1000 次并发请求平均响应时间 86msP95 是 142ms单机完全扛得住。如果嫌弃开发服务器性能不行换成 gunicorn gevent 的四进程部署P95 可以压到 60ms 以内。整个项目的代码我放在了代码仓库里包含数据模拟脚本、训练脚本、推荐引擎、Flask 服务四部分在 README 里写了运行步骤。有读者反馈说在他的 Windows 11 上第一次跑冒烟样例时因为 scikit-learn 版本不对报了 AttributeError后来在 requirements.txt 里固定了 1.3.0 以上版本解决。6.3 后续可以扩展的方向项目做到这里已经可以当一个完整的课设或练手项目交差了但如果你真想把它推向真实场景有这么几个方向值得做引入图神经网络推荐把用户、商品、标签建成异质图用 LightGCN 提取高阶特征。这个方向适合有 GPU 且有真实数据的团队目前的传统方法已经足够小规模使用了。接入大模型做推荐理由生成把因为你喜欢灰枣它和田玉枣在甜度和口感上相近这类模板替换成大模型生成的个性化文案。我在离线实验中发现大模型生成文案的点击率预估比模板高出 12%但是生成延迟也是问题需要做好缓存。和供应链数据打通新疆特产有明显的季节性哈密瓜只有夏秋两季、青皮核桃只有 9 月中下旬推荐系统可以引入时令商品属性在旺季给推荐加权淡季自动降权这样比只看用户历史行为要更符合真实业务。我自己的体会是做完这个项目最大的收获是弄懂了特征工程和可解释性在推荐系统里到底占多大权重。算法选型反而不是最费时间的最费时间的是搞清楚一个新疆特产用户真正想要什么样的推荐。系统上线跑了一周之后我最满意的不是准确率数字而是有一个用户反馈说你推的奇台面粉正是我找了好久的东西——这种反馈对一个推荐系统来说比任何指标都值钱。最后再分享一个细节如果你完整跑通了这套代码你会发现把某个用户的评分全删掉之后推荐结果立刻退化成冷启动规则。这个瞬间会让你对推荐系统的脆弱和鲁棒有非常直观的体感。想继续深化建议从这两个方面入手——要么补充更多维度的画像数据要么加入强化学习做推荐策略的动态调整。前者治标后者治本但前提都是先把数据基础打好。
返回列表