
简介这是一套面向Python初学者与推荐系统爱好者的服饰推荐系统实战项目围绕个性化服装推荐场景完整呈现从数据采集到模型部署的全流程。项目综合运用pandas、numpy、scikit-learn等库完成数据清洗与特征工程结合K-Means聚类、协同过滤、矩阵分解等算法构建用户画像与推荐模型并借助Flask或Django搭建API接口适合希望理解数据驱动决策系统的开发者练手。资源包共约2000个文件压缩后225.31MB其中9698张jpg图片与22个csv数据文件构成服饰样本与行为数据集30个py脚本承载核心算法逻辑另有js、vue、xml等前端与配置文件支撑界面交互bson文件则用于存储匹配与商品数据。目前已有318人学习下载可帮助读者掌握推荐系统各模块的实现思路与工程组织方式。1. 从一堆 bson 文件说起这套 Python 服饰推荐系统到底能跑出什么如果你拿到手的是一份只有.babelrc、scrapy.cfg、global.css和几个.bson文件的压缩包第一反应大概率是懵的——没有 README没有 requirements连个入口脚本都找不到。我拆过不少这种「半成品」推荐系统资源包这套 Python 服饰推荐系统的骨架其实很清晰它用 Scrapy 做数据采集把抓到的服饰商品和用户行为落成 BSONBinary JSON再用 Python 侧的推荐算法做召回和排序。yoho_items.bson和clothings.bson是商品侧的两份数据快照yoho_valid_matchings.bson则是验证集里的用户-商品匹配关系clothing.csv大概率是从 BSON 导出的扁平化样本方便直接喂给 pandas 或 surprise。它解决的不是「从零搭一个推荐引擎」的问题而是「我已经有了一批服饰行为数据怎么快速验证协同过滤和矩阵分解到底有没有效果」。适合两类人一是想拿真实服饰数据练手推荐算法的 Python 学习者二是需要快速搭一个推荐 baseline 再迭代的从业者。下面我按「数据怎么读 → 特征怎么建 → 模型怎么训 → 坑在哪」的顺序把这套资源拆开讲透。2. 读懂 BSON 与 CSV数据加载、字段探查与格式转换2.1 为什么这套资源用 BSON 而不是直接给 CSVBSON 是 MongoDB 的二进制序列化格式优势在于能保留嵌套结构和类型信息。服饰推荐场景里一件衣服的字段往往不是扁平的——它可能有category、brand、tags数组、price区间、color列表。如果直接存 CSV嵌套字段要么被拍平丢失层级要么被塞进一个字符串里后续解析全靠正则维护成本极高。BSON 能原样保留这些结构读出来就是 Python 的 dict 或 list省掉一层解析逻辑。但 BSON 也有代价不能像 CSV 那样用 Excel 直接打开看调试时得写代码。所以资源包里同时给了clothing.csv这是把 BSON 里最常用的扁平字段导出来的一份样本方便快速做数据探查和画图。常见做法是BSON 作为「原始数据源」保留完整信息CSV 作为「分析视图」用于快速迭代。2.2 用 Python 读取 BSON 并转成 DataFrame读取 BSON 需要pymongo里的bson模块或者直接用bson独立包。下面这段代码把yoho_items.bson和clothings.bson读进来转成 DataFrame 并打印字段结构。import bson import pandas as pd def load_bson(path): 读取 BSON 文件返回 list[dict] with open(path, rb) as f: # bson.decode_all 一次性解码所有文档 data bson.decode_all(f.read()) return data # 加载商品侧两份数据 yoho_items load_bson(yoho_items.bson) clothings load_bson(clothings.bson) print(fyoho_items 文档数: {len(yoho_items)}) print(fclothings 文档数: {len(clothings)}) print(yoho_items 首条字段:, list(yoho_items[0].keys())) print(clothings 首条字段:, list(clothings[0].keys())) # 转 DataFrame嵌套字段先保留为 object df_items pd.DataFrame(yoho_items) df_cloth pd.DataFrame(clothings) print(df_items.dtypes)逻辑说明bson.decode_all接收 bytes 返回文档列表比逐条decode快很多。转 DataFrame 后嵌套的 list/dict 字段会变成 object 类型这是正常的后续特征工程再单独处理。参数上唯一要注意的是文件路径——BSON 文件没有编码参数二进制读取即可。2.3 验证集 matchings 的读取与负样本构造yoho_valid_matchings.bson存的是验证阶段的用户-商品匹配关系通常是正样本用户真实交互过的商品。推荐系统评估必须有负样本否则召回率、AUC 都算不出来。常见做法是从用户未交互的商品里随机采样作为负样本比例控制在 1:4 到 1:10 之间。import random matchings load_bson(yoho_valid_matchings.bson) print(匹配记录数:, len(matchings)) print(首条匹配:, matchings[0]) # 假设每条记录含 user_id 和 item_id user_item_set {(m[user_id], m[item_id]) for m in matchings} all_items {item[item_id] for item in yoho_items} def negative_sampling(user_id, pos_items, all_items, ratio4): 为单个用户采样负样本 neg [] candidates list(all_items - pos_items) n min(len(pos_items) * ratio, len(candidates)) neg random.sample(candidates, n) return [(user_id, iid, 0) for iid in neg] # 示例为第一个用户构造负样本 uid matchings[0][user_id] pos {m[item_id] for m in matchings if m[user_id] uid} neg_samples negative_sampling(uid, pos, all_items) print(f用户 {uid} 正样本 {len(pos)} 条负样本 {len(neg_samples)} 条)这里的关键参数是ratio服饰场景下用户交互稀疏负样本比例太高会让模型偏向「预测为负」太低则学不到区分度。我一般先用 1:4 跑一版看验证集上的召回再调。注意random.sample在候选集很大时会有性能问题生产环境建议用 numpy 的np.random.choice配合布尔掩码。3. 特征工程与用户画像从原始字段到可训练矩阵3.1 服饰场景下的特征拆解思路服饰推荐和电影、图书推荐最大的区别在于风格、季节、尺码、颜色这些属性对用户决策的影响远大于「评分」本身。用户买一件衣服往往不是因为「这件衣服评分高」而是因为「它是我的尺码、符合我常买的风格、当季能穿」。所以特征工程要围绕这几类展开用户侧历史购买品类分布、价格带偏好、品牌偏好、活跃时段商品侧品类、品牌、价格、颜色、季节标签、上架时长交互侧点击、收藏、加购、购买权重递增资源包里的clothing.csv就是把这些字段扁平化后的结果可以直接用 pandas 做分组统计。3.2 用 pandas 构建用户-品类偏好矩阵下面这段代码从 CSV 出发统计每个用户在各品类上的购买次数构建偏好矩阵。这是后续协同过滤和矩阵分解的输入基础。# 读取扁平化样本 df pd.read_csv(clothing.csv) print(df.columns.tolist()) print(df.head()) # 假设字段为 user_id, item_id, category, price, action # 只保留购买行为权重设为 1 df_buy df[df[action] buy].copy() # 用户-品类偏好矩阵 user_cat df_buy.groupby([user_id, category]).size().unstack(fill_value0) print(偏好矩阵形状:, user_cat.shape) # 归一化每个用户的行为除以自己的总次数 user_cat_norm user_cat.div(user_cat.sum(axis1), axis0) print(user_cat_norm.head()) # 价格带偏好分箱后统计 df_buy[price_bin] pd.cut(df_buy[price], bins[0, 100, 300, 600, 1000, 9999], labels[100, 100-300, 300-600, 600-1000, 1000]) user_price df_buy.groupby([user_id, price_bin]).size().unstack(fill_value0) print(价格带偏好形状:, user_price.shape)逻辑说明groupby().size().unstack()是构建共现矩阵的标准操作fill_value0保证稀疏位置补零。归一化那一步很重要——不归一化的话购买次数多的用户会在相似度计算里占主导导致推荐结果偏向重度用户。pd.cut的分箱边界要根据实际价格分布调整我给的这组是服饰电商的常见区间你可以先用df[price].describe()看分位数再定。3.3 用 K-Means 做用户聚类与画像标签有了偏好矩阵就可以对用户做聚类给每个用户打上「风格标签」。K-Means 是最直接的选择scikit-learn的KMeans类几行就能跑。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 用品类偏好矩阵做聚类 X user_cat_norm.values scaler StandardScaler() X_scaled scaler.fit_transform(X) # 聚类数先用 8可通过肘部法调整 kmeans KMeans(n_clusters8, random_state42, n_init10) labels kmeans.fit_predict(X_scaled) # 把簇标签写回用户 user_cluster pd.Series(labels, indexuser_cat_norm.index, namecluster) print(user_cluster.value_counts()) # 查看每个簇的品类偏好均值人工解读画像 cluster_profile user_cat_norm.groupby(user_cluster).mean() print(cluster_profile.round(3))参数上n_clusters是核心8 只是起点实际要用肘部法或轮廓系数选。n_init10表示跑 10 次不同初始化取最优避免陷入局部最优。random_state固定后结果可复现调试阶段必加。聚类完一定要看cluster_profile如果某个簇的偏好分布和整体均值几乎一样说明这个簇没意义得减少聚类数或换特征。提示服饰数据的品类字段往往有几十个类别直接 one-hot 会让矩阵非常稀疏。常见做法是先按销量或出现频次做一次筛选只保留 Top 20-30 个品类其余归为「其他」。4. 推荐算法落地协同过滤、矩阵分解与评估指标4.1 基于物品的协同过滤相似度计算与 TopN 召回协同过滤是推荐系统的 baseline服饰场景下基于物品的 CFItem-CF通常比基于用户的更稳因为商品数量相对可控且商品相似度变化比用户兴趣慢。核心是算物品-物品相似度矩阵然后根据用户历史交互的物品召回相似物品。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 构建用户-物品交互矩阵购买为 1 user_item df_buy.groupby([user_id, item_id]).size().unstack(fill_value0) user_item_bin (user_item 0).astype(int) # 物品-物品余弦相似度 item_sim cosine_similarity(user_item_bin.T) item_sim_df pd.DataFrame(item_sim, indexuser_item_bin.columns, columnsuser_item_bin.columns) def recommend_item_cf(user_id, user_item_bin, item_sim_df, topn10): 基于物品相似度为用户召回 TopN if user_id not in user_item_bin.index: return [] # 用户交互过的物品 interacted user_item_bin.loc[user_id] interacted_items interacted[interacted 0].index.tolist() # 聚合相似度得分 scores item_sim_df[interacted_items].sum(axis1) # 排除已交互 scores scores.drop(interacted_items, errorsignore) return scores.sort_values(ascendingFalse).head(topn).index.tolist() # 示例 recs recommend_item_cf(user_item_bin.index[0], user_item_bin, item_sim_df) print(推荐物品:, recs)逻辑说明cosine_similarity接收的是「物品×用户」矩阵所以要对user_item_bin转置。聚合得分时用sum是最简单的也可以用加权相似度高的物品贡献更大。drop那一步必须做否则会把用户已经买过的物品再推一遍这是最常见的翻车点之一。4.2 矩阵分解用 surprise 做 SVD 训练与预测协同过滤在稀疏数据上表现有限矩阵分解能把用户和物品映射到低维隐向量空间泛化能力更强。surprise库是 Python 里做这件事最省心的选择内置了 SVD、NMF、SlopeOne 等算法。from surprise import Dataset, Reader, SVD from surprise.model_selection import train_test_split from surprise import accuracy # 准备数据user_id, item_id, rating # 服饰场景没有显式评分用购买次数或行为权重代替 df_surprise df_buy.groupby([user_id, item_id]).size().reset_index(namerating) reader Reader(rating_scale(1, df_surprise[rating].max())) data Dataset.load_from_df(df_surprise[[user_id, item_id, rating]], reader) trainset, testset train_test_split(data, test_size0.2, random_state42) # SVD 训练 algo SVD(n_factors50, n_epochs20, lr_all0.005, reg_all0.02, random_state42) algo.fit(trainset) # 预测与评估 predictions algo.test(testset) rmse accuracy.rmse(predictions) mae accuracy.mae(predictions) print(fRMSE: {rmse:.4f}, MAE: {mae:.4f})参数说明n_factors是隐向量维度50 是常用起点数据量大可以加到 100-200n_epochs是迭代轮数20 轮通常够用但要看 RMSE 是否还在下降lr_all是学习率0.005 偏保守训练慢但稳reg_all是正则化系数防止过拟合稀疏数据上可以适当调大。rating_scale要按实际评分范围设如果评分是 1-5 就写(1, 5)这里用最大购买次数动态设定。4.3 评估指标除了 RMSE 还要看召回率和覆盖率RMSE 衡量的是评分预测误差但推荐系统真正关心的是「推的东西用户会不会点/买」。所以还要算 TopN 召回率和覆盖率。def precision_recall_at_k(predictions, k10, threshold3.5): 计算 TopK 的精确率和召回率 from collections import defaultdict user_est_true defaultdict(list) for uid, iid, true_r, est, _ in predictions: user_est_true[uid].append((est, true_r)) precisions, recalls {}, {} for uid, user_ratings in user_est_true.items(): user_ratings.sort(keylambda x: x[0], reverseTrue) n_rel sum((true_r threshold) for (_, true_r) in user_ratings) n_rec_k sum((est threshold) for (est, _) in user_ratings[:k]) n_rel_and_rec_k sum((true_r threshold) and (est threshold) for (est, true_r) in user_ratings[:k]) precisions[uid] n_rel_and_rec_k / n_rec_k if n_rec_k ! 0 else 0 recalls[uid] n_rel_and_rec_k / n_rel if n_rel ! 0 else 0 precision sum(precisions.values()) / len(precisions) recall sum(recalls.values()) / len(recalls) return precision, recall prec, rec precision_recall_at_k(predictions, k10, threshold3.5) print(fPrecision10: {prec:.4f}, Recall10: {rec:.4f})threshold是判定「相关」的评分门槛服饰场景下如果评分是购买次数3.5 可能偏高要根据分布调整。覆盖率则是推荐出的不同物品数除以总物品数反映推荐是否集中在少数爆款上——覆盖率太低说明系统多样性差用户容易审美疲劳。5. 避坑与排查这套资源跑不起来时先看这几条5.1 BSON 文件读取报 InvalidBSON现象bson.decode_all抛InvalidBSON: invalid message size或直接返回空列表。原因通常是文件不完整下载中断或文件本身是 BSON 的变体格式比如 MongoDB 的 dump 格式带额外头信息。解决先用ls -lh看文件大小是否和资源描述一致再用xxd file.bson | head看前几个字节正常 BSON 文档以 4 字节长度开头。如果是 dump 格式需要用mongorestore而不是直接bson.decode_all。5.2 用户-物品矩阵过于稀疏导致相似度为 0现象Item-CF 算出来的相似度矩阵几乎全是 0推荐结果为空。原因服饰数据里单个用户交互的物品数很少两个物品共同被同一用户交互的概率极低。解决降低相似度计算的粒度从「物品级」降到「品类级」或「品牌级」先算品类相似度再映射回物品或者引入内容特征颜色、风格标签做补充相似度不要只依赖行为共现。5.3 surprise 训练时 RMSE 不下降或震荡现象algo.fit后 RMSE 一直在 1.0 以上或者每轮波动很大。原因评分尺度设置错误比如实际评分是 0-1但rating_scale设成了 1-5或者学习率太大导致跳过最优解。解决先用df_surprise[rating].describe()确认评分范围rating_scale严格按实际范围设学习率从 0.001 开始试n_epochs加到 50 看趋势。另外检查是否有用户或物品只出现一次surprise 对冷启动样本的处理需要额外配置。5.4 负样本采样引入位置偏差现象离线评估指标很好但上线后点击率很低。原因负样本是从全体物品里随机采的但真实场景中用户看到的候选集是经过召回和排序的随机负样本太「容易区分」模型学到的区分度在真实候选集上不成立。解决负样本尽量从「曝光未点击」里采而不是从全体物品里随机采。如果资源包里没有曝光日志至少按物品热度做加权采样让热门物品更可能成为负样本。5.5 CSV 与 BSON 字段对不上导致 merge 失败现象用clothing.csv的item_id去 merge BSON 里的商品数据结果大量 NaN。原因CSV 导出时可能做了字段重命名或 ID 截断BSON 里的_id是 ObjectId 类型和 CSV 里的字符串 ID 不是同一套。解决先分别打印两边的 ID 样例确认格式如果是 ObjectId用str(oid)转成字符串再 merge如果 ID 体系完全不同需要找资源包里是否有映射表没有的话只能以其中一份为准重建关联。6. 进阶技巧把离线模型接上 Flask 接口做在线推荐离线跑通只是第一步真正要验证这套系统有没有用得把它变成一个能调用的服务。我一般用 Flask 包一层把训练好的 SVD 模型和 Item-CF 相似度矩阵加载到内存对外暴露一个/recommend接口。下面是最小可运行版本。from flask import Flask, request, jsonify import pickle app Flask(__name__) # 启动时加载模型和相似度矩阵 with open(svd_model.pkl, rb) as f: svd_model pickle.load(f) with open(item_sim.pkl, rb) as f: item_sim_df pickle.load(f) app.route(/recommend, methods[GET]) def recommend(): user_id request.args.get(user_id) topn int(request.args.get(topn, 10)) if not user_id: return jsonify({error: user_id required}), 400 # 先用 Item-CF 召回候选 candidates recommend_item_cf(user_id, user_item_bin, item_sim_df, topn50) # 再用 SVD 对候选打分排序 scored [] for iid in candidates: pred svd_model.predict(user_id, iid) scored.append((iid, pred.est)) scored.sort(keylambda x: x[1], reverseTrue) return jsonify({ user_id: user_id, recommendations: [{item_id: iid, score: round(s, 4)} for iid, s in scored[:topn]] }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)逻辑说明这是「召回排序」的两阶段结构Item-CF 负责从全量物品里快速筛出 50 个候选SVD 负责对这 50 个精排。pickle加载的模型要在离线阶段先pickle.dump保存好。debugFalse是生产环境必须的调试模式会暴露堆栈信息且有安全风险。接口参数topn做了默认值处理避免调用方不传时报错。验证方法上我会用curl或 Postman 打几轮请求看返回的 item_id 是否在用户历史交互之外以及不同用户的推荐结果是否有区分度。如果所有用户返回的 Top10 几乎一样说明召回阶段就退化了得回去检查相似度矩阵和用户历史。注意Flask 自带的开发服务器不能扛并发真要上线得用 gunicorn 或 uwsgi 起多 worker。模型文件如果很大加载到每个 worker 会吃内存常见做法是单独起一个模型服务Flask 只做转发。从那以后我每次拿到这种「只有数据文件和配置文件」的资源包都强制先走一遍「读数据 → 看字段 → 跑通最小召回 → 再谈模型」的流程绝不跳过数据探查直接上算法。这套 Python 服饰推荐系统的价值不在于算法多先进而在于它给了一份真实的服饰行为数据让你能把协同过滤、矩阵分解、评估指标这条链路完整跑一遍。希望帮到你。本文还有配套的精品资源点击获取