简介:这是一套面向Python初学者与推荐系统爱好者的服饰推荐系统实战项目,围绕个性化服装推荐场景,综合运用数据处理、机器学习与Web开发技术,帮助读者理解数据驱动推荐从数据采集到模型部署的完整链路。压缩包共约2000个文件,整体约225.31MB,其中以9698张jpg服饰图片作为商品视觉素材,配合30个py脚本承载推荐算法与数据处理逻辑,另有csv、json、bson等数据文件用于存储用户行为与商品信息,js、vue、xml等前端文件支撑界面交互,md文档则提供必要的说明。项目涵盖数据收集与预处理、特征工程、用户画像构建、协同过滤与矩阵分解等推荐算法、模型训练评估以及Flask/Django接口部署等关键模块,并借助scikit-learn、surprise等库完成标准化与效果评估。目前已有318人学习下载,适合希望系统掌握推荐系统构建流程、积累完整项目经验的开发者参考实践。
1. 服饰推荐系统到底在推什么:从「猜你喜欢」到可复现的 Python 方案
打开任何一个电商 App,首页那排「猜你喜欢」背后跑的就是推荐系统。而python服饰推荐系统这个标题,说的就是用 Python 把「用户—服饰—行为」这三者串起来,算出一个排序列表,让每个人看到不一样的衣服。它解决的不是「有没有货」的问题,而是「这么多货,先给谁看哪几件」的问题。适合谁做?有订单或点击日志的电商团队、想给服装类目做个性化排序的算法同学、以及拿公开数据集练手的学生。我见过太多人一上来就上深度学习,结果连最基础的协同过滤都没跑通,召回率还不如热门榜单——这篇就把从数据到上线的最小闭环讲清楚,让你少走那段弯路。
服饰推荐和图书、电影推荐有个本质区别:服饰有强属性、强季节、强尺码,还有退货率。一件连衣裙的「颜色、版型、尺码、季节」直接决定它能不能被推给某个用户,而电影没有尺码这回事。所以做服饰推荐,特征工程比模型选型更吃功夫。下面按「数据怎么来 → 召回怎么搭 → 排序怎么排 → 坑在哪 → 怎么验证」这条线走,每一步都给能抄的代码和参数。
2. 数据准备与特征工程:服饰推荐系统的地基怎么打
2.1 三类数据源和它们的获取方式
做推荐系统,数据永远是第一道坎。服饰场景常见的数据源有三类:用户行为日志(点击、加购、收藏、下单、退货)、商品属性表(品类、颜色、尺码、价格、上架时间)、用户画像(性别、年龄段、历史偏好)。如果你手上有生产库,直接写 SQL 抽数;如果没有,用公开数据集起步,比如阿里天池的淘宝用户行为数据集、Amazon Reviews 里的 Clothing 子集。
用 Python 读进来通常长这样:
import pandas as pd # 行为日志:user_id, item_id, behavior_type, timestamp behavior = pd.read_csv("user_behavior.csv", parse_dates=["timestamp"]) # 商品属性:item_id, category, color, size, price items = pd.read_csv("item_attrs.csv") # 用户画像:user_id, gender, age_range users = pd.read_csv("user_profile.csv") # 只保留服饰类目,避免跨类目噪声 clothing_cates = ["连衣裙", "T恤", "牛仔裤", "外套", "卫衣"] items = items[items["category"].isin(clothing_cates)] # 行为加权:下单权重最高,退货给负权重 weight_map = {"click": 1.0, "fav": 2.0, "cart": 3.0, "buy": 5.0, "refund": -3.0} behavior["weight"] = behavior["behavior_type"].map(weight_map)这段代码做了三件事:解析时间戳、过滤服饰类目、给不同行为赋权重。参数说明:weight_map里的数值不是拍脑袋,一般按「点击:收藏:加购:下单 = 1:2:3:5」的经验比例,退货给负权重是为了让模型学到「推了会退」的负信号。如果你的退货率超过 30%,这个负权重还要加大。
2.2 服饰特有的特征:尺码、季节、颜色偏好
通用推荐系统那套 one-hot 编码在服饰场景不够用。你得额外构造几类特征:
| 特征类型 | 具体字段 | 构造方式 | 为什么重要 |
|---|---|---|---|
| 尺码匹配 | user_size, item_size | 用户历史购买尺码众数 vs 商品尺码 | 尺码不对直接退货 |
| 季节匹配 | item_season, 当前月份 | 季节标签 + 月份映射 | 羽绒服夏天推就是浪费曝光 |
| 颜色偏好 | user_color_pref | 用户历史购买颜色分布 | 服饰颜色偏好极强 |
| 价格带 | price_bucket | 按用户历史客单价分桶 | 推太贵直接划走 |
构造尺码匹配特征的代码:
# 用户历史购买尺码众数 user_size = behavior[behavior["behavior_type"] == "buy"].merge( items[["item_id", "size"]], on="item_id" ).groupby("user_id")["size"].agg(lambda x: x.mode()[0] if len(x) > 0 else "M") # 商品尺码与用户尺码是否一致 items["size_match"] = items.apply( lambda row: 1 if user_size.get(row.get("user_id")) == row["size"] else 0, axis=1 )逻辑说明:mode()取众数,因为用户可能买过多个尺码,但最常买的那个才是真实尺码。size_match作为二值特征喂给排序模型,能显著降低退货率。注意:新用户没有历史购买记录,user_size会是 NaN,这时候用品类平均尺码兜底,别直接填 0。
2.3 训练样本的构造:负采样怎么做才不翻车
推荐系统的训练样本不是「用户买了什么」这么简单,而是「正样本 + 负样本」。正样本是用户真实交互过的商品,负样本是用户没交互过的。负采样策略直接决定模型好坏。
常见做法是「按曝光未点击采样」,也就是从用户看到过但没点的商品里抽。但服饰场景有个坑:用户没点可能是因为尺码不全或季节不对,这种负样本会教坏模型。我一般会做两层过滤:
import numpy as np def negative_sampling(behavior, items, neg_ratio=4): positives = behavior[behavior["weight"] > 0][["user_id", "item_id"]] all_items = items["item_id"].unique() negatives = [] for uid, group in behavior.groupby("user_id"): seen = set(group["item_id"]) # 过滤掉尺码缺失、季节不符的商品 candidates = items[ (~items["item_id"].isin(seen)) & (items["size"].notna()) & (items["season"].isin(["春", "夏", "秋", "冬"])) ]["item_id"].values if len(candidates) == 0: continue sampled = np.random.choice(candidates, size=min(neg_ratio * len(group), len(candidates)), replace=False) negatives.extend([(uid, iid) for iid in sampled]) neg_df = pd.DataFrame(negatives, columns=["user_id", "item_id"]) neg_df["label"] = 0 pos_df = positives.copy() pos_df["label"] = 1 return pd.concat([pos_df, neg_df], ignore_index=True)参数说明:neg_ratio=4表示每个正样本配 4 个负样本,这是业界常用的比例,太高会让训练变慢,太低模型学不到区分度。踩坑提醒:负采样一定要在「用户维度」做,不能全局随机抽,否则同一个用户的正负样本分布会偏。
3. 召回层:用 Python 把候选集从百万降到千
3.1 协同过滤召回:ItemCF 在服饰场景的适配
召回层的目标是从全量商品里快速筛出几百个候选,协同过滤是最经典的方案。ItemCF 的核心思想是「买了 A 的人也买了 B」,在服饰场景特别有效,因为搭配购买很常见——买了牛仔裤的人往往也需要腰带。
from collections import defaultdict import math def item_cf_train(behavior, top_k=20): # 构建用户-商品交互矩阵 user_items = behavior[behavior["weight"] > 0].groupby("user_id")["item_id"].apply(set).to_dict() # 计算商品共现 item_sim = defaultdict(dict) item_count = defaultdict(int) for uid, items in user_items.items(): for i in items: item_count[i] += 1 for j in items: if i != j: item_sim[i][j] = item_sim[i].get(j, 0) + 1 # 余弦相似度归一化 for i in item_sim: for j in item_sim[i]: item_sim[i][j] /= math.sqrt(item_count[i] * item_count[j]) # 取 top_k 相似 item_sim[i] = dict(sorted(item_sim[i].items(), key=lambda x: -x[1])[:top_k]) return item_sim def item_cf_recall(user_id, user_items, item_sim, top_n=200): scores = defaultdict(float) for i in user_items.get(user_id, []): for j, sim in item_sim.get(i, {}).items(): scores[j] += sim return sorted(scores.items(), key=lambda x: -x[1])[:top_n]逻辑说明:先算商品共现次数,再用余弦相似度归一化,避免热门商品因为曝光大而霸榜。top_k=20是每个商品保留 20 个最相似邻居,top_n=200是给每个用户召回 200 个候选。参数怎么调:如果候选集不够,加大top_n;如果召回太慢,减小top_k。服饰场景建议top_k在 15~30 之间。
3.2 向量召回:用双塔模型做语义匹配
协同过滤有个硬伤:新商品没有交互数据就召不回。服饰上新快,这个问题很致命。双塔模型(Two-Tower)把用户和商品分别编码成向量,用内积算相似度,能缓解冷启动。
import torch import torch.nn as nn class Tower(nn.Module): def __init__(self, input_dim, hidden_dim=128, output_dim=64): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, output_dim) ) def forward(self, x): return nn.functional.normalize(self.net(x), dim=-1) class TwoTower(nn.Module): def __init__(self, user_dim, item_dim): super().__init__() self.user_tower = Tower(user_dim) self.item_tower = Tower(item_dim) def forward(self, user_feat, item_feat): u = self.user_tower(user_feat) i = self.item_tower(item_feat) return (u * i).sum(dim=-1) # 内积参数说明:hidden_dim=128是中间层维度,output_dim=64是最终向量维度。服饰场景建议输出维度不要低于 32,否则颜色、尺码这些细粒度信息会被压没。训练时用 BPR loss 或 sampled softmax,负样本就用 2.3 节采出来的。
注意:双塔模型上线时要预计算所有商品向量存进向量库(Faiss、Milvus 都行),用户向量实时算,然后做 ANN 检索。别每次请求都跑一遍模型,那样 QPS 撑不住。
3.3 多路召回融合:规则加权还是学习权重
实际系统不会只用一路召回。常见做法是 ItemCF + 双塔 + 热门 + 新品,四路各出 100~200 个候选,合并去重后送排序。融合方式有两种:
- 规则加权:给每路打分归一化后加权求和,权重靠 A/B 实验调。简单、可控,但需要人工调。
- 学习融合:把每路的召回分数作为特征,训练一个 LR 或 GBDT 来学权重。效果更好,但要多维护一个模型。
我一般先上规则加权,权重初始值设成 ItemCF:双塔:热门:新品 = 0.4:0.3:0.2:0.1,跑一周看数据再调。血泪经验:热门召回一定要限流,否则它会挤掉个性化召回的位置,最后整个系统退化成榜单。
4. 排序层:从候选集到最终列表的精细打分
4.1 特征工程:排序模型吃什么
召回层给的是粗排候选,排序层要做精细打分。排序模型的特征分四组:
| 特征组 | 示例字段 | 来源 |
|---|---|---|
| 用户特征 | 性别、年龄段、历史客单价、偏好品类 | 用户画像表 |
| 商品特征 | 品类、价格、颜色、尺码、上架天数 | 商品属性表 |
| 交叉特征 | 用户品类偏好分、尺码匹配、价格差 | 实时计算 |
| 上下文特征 | 当前小时、星期几、是否大促 | 请求上下文 |
交叉特征里最重要的是用户品类偏好分,算法是用户对该品类的历史行为加权求和再归一化:
def user_category_pref(behavior, items): df = behavior.merge(items[["item_id", "category"]], on="item_id") pref = df.groupby(["user_id", "category"])["weight"].sum().reset_index() # 归一化到 0-1 pref["pref_score"] = pref.groupby("user_id")["weight"].transform( lambda x: (x - x.min()) / (x.max() - x.min() + 1e-6) ) return pref[["user_id", "category", "pref_score"]]参数说明:1e-6是防止除零。归一化用 min-max 而不是 z-score,因为推荐场景更关心相对排序,不关心绝对分布。
4.2 模型选型:GBDT 还是 DeepFM
排序模型的选择取决于数据量和特征复杂度:
- 数据量 < 100 万样本:先用 LightGBM,特征工程做好就能打,训练快、可解释。
- 数据量 > 100 万且特征稀疏:上 DeepFM 或 DIN,能自动学交叉特征。
- 服饰场景特殊点:尺码、季节这类硬约束建议做成规则过滤,别全交给模型学。
LightGBM 的训练代码:
import lightgbm as lgb from sklearn.model_selection import train_test_split feature_cols = ["age", "gender", "price", "category_id", "size_match", "pref_score", "hour", "is_promo"] X_train, X_val, y_train, y_val = train_test_split( samples[feature_cols], samples["label"], test_size=0.2, random_state=42 ) params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 63, "min_data_in_leaf": 100, "feature_fraction": 0.8, "bagging_fraction": 0.8, "bagging_freq": 5, "verbose": -1 } dtrain = lgb.Dataset(X_train, y_train) dval = lgb.Dataset(X_val, y_val, reference=dtrain) model = lgb.train(params, dtrain, num_boost_round=500, valid_sets=[dval], callbacks=[lgb.early_stopping(50)])参数说明:learning_rate=0.05配合num_boost_round=500是稳妥组合,num_leaves=63控制树复杂度,min_data_in_leaf=100防止过拟合。early_stopping(50)表示验证集 50 轮不提升就停。踩坑:服饰场景样本不均衡严重(正样本远少于负样本),记得设scale_pos_weight或对负样本降采样。
4.3 重排:多样性和业务规则的最后一公里
排序模型输出的是「相关性」排序,但直接展示会有问题:同款不同色霸屏、全是同一品类、价格全挤在一个区间。重排层就是解决这个的。
常见做法是 MMR(最大边际相关性):
def mmr_rerank(candidates, lambda_param=0.7, top_n=20): selected = [] remaining = candidates.copy() while len(selected) < top_n and remaining: best_score = -float("inf") best_item = None for item in remaining: rel = item["score"] # 与已选列表的最大相似度 max_sim = max([similarity(item, s) for s in selected], default=0) mmr = lambda_param * rel - (1 - lambda_param) * max_sim if mmr > best_score: best_score = mmr best_item = item selected.append(best_item) remaining.remove(best_item) return selected参数说明:lambda_param=0.7表示 70% 看相关性、30% 看多样性。这个值越高越偏向相关性,越低越多样。服饰场景建议 0.6~0.8,太低会推出用户完全不感兴趣的东西。similarity函数可以用品类相同、颜色相同、价格接近来定义。
5. 避坑与排查:服饰推荐系统上线后最容易翻车的五个点
5.1 现象:推荐结果全是爆款,个性化消失
原因:热门商品在召回和排序阶段都有天然优势,样本里正例多,模型学出来就是「推爆款准没错」。解决:在召回层给热门商品降权,排序层加一个「个性化分数」特征,或者在重排层强制插入一定比例的冷门商品。我一般会在重排阶段保留 20% 的位置给「非热门但相关」的商品。
5.2 现象:新用户推荐完全不准
原因:新用户没有历史行为,协同过滤和双塔都召不回合适商品。解决:新用户走「热门 + 品类多样性」兜底策略,同时在前端做引导(选性别、选风格),拿到初始信号后立刻切个性化。注意:别给新用户推高退货率的品类,比如紧身类服饰,尺码问题会直接导致退货。
5.3 现象:季节切换时推荐大面积失效
原因:模型是用历史数据训练的,夏天推羽绒服的样本很少,模型没学到「季节」这个强特征。解决:把季节作为硬过滤规则放在召回前,同时训练时对季节特征做加权。更彻底的做法是按季节分模型,春夏一个、秋冬一个,但维护成本高,小团队用规则过滤就够了。
5.4 现象:A/B 实验指标涨了,但退货率也涨了
原因:模型只优化了点击率或转化率,没考虑退货。推了尺码不全或与用户风格不符的商品,短期转化好看,长期退货吃掉利润。解决:把退货率作为负向指标加入模型目标,或者在重排层过滤掉「高退货风险」商品。血泪经验:服饰类目退货率能到 30%~50%,不把退货纳入优化目标,推荐系统就是给公司挖坑。
5.5 现象:线上服务延迟高,QPS 上不去
原因:召回层用了太多路、排序模型太大、特征实时计算太慢。解决:按这个顺序排查——先看召回层是不是每路都跑了全量,再看排序模型能不能蒸馏成小模型,最后看特征能不能预计算。后悔药:上线前一定要做压测,别等大促当天才发现扛不住。
6. 验证与迭代:怎么判断这套系统真的有用
6.1 离线指标:AUC、Recall@K、NDCG 怎么看
离线评估是上线前的第一道关。常用指标:
| 指标 | 含义 | 服饰场景参考值 |
|---|---|---|
| AUC | 排序模型区分正负样本的能力 | > 0.7 可用,> 0.75 不错 |
| Recall@200 | 召回层前 200 个里有多少真实正样本 | > 0.6 及格 |
| NDCG@20 | 排序列表的质量 | > 0.4 可用 |
| 覆盖率 | 推荐商品占总商品的比例 | > 30% 避免头部集中 |
算 Recall@K 的代码:
def recall_at_k(model, test_data, k=200): hits = 0 total = 0 for user_id, group in test_data.groupby("user_id"): true_items = set(group[group["label"] == 1]["item_id"]) if not true_items: continue pred_items = set(model.recommend(user_id, top_n=k)) hits += len(true_items & pred_items) total += len(true_items) return hits / total if total > 0 else 0参数说明:k=200对应召回层候选数,离线评估要和线上召回数量对齐,否则指标没意义。
6.2 在线实验:A/B 测试的坑和正确姿势
离线指标好不代表线上好,必须做 A/B。常见坑:分流不均匀、实验周期太短、只看点击率不看 GMV。正确姿势是:
- 按用户 ID 哈希分流,保证同一用户始终在同一组
- 实验至少跑 7 天,覆盖工作日和周末
- 核心指标看 GMV 和退货率,辅助指标看点击率和停留时长
- 实验组和对照组样本量要够,用 t 检验判断显著性
6.3 迭代节奏:从周更到日更的路径
推荐系统不是一次做完就完事。我的迭代节奏是:
- 第一周:跑通 ItemCF + 热门召回,排序用 LR,先上线看数据
- 第二周:加双塔召回,排序换 LightGBM,做第一次 A/B
- 第一个月:加实时特征、重排多样性,优化退货率
- 之后:按周迭代特征和模型,按天更新商品向量
最后说个我自己的习惯:每次上线新模型前,我都会用历史数据做一次「回放测试」——把过去一周的请求重新跑一遍新模型,对比新旧排序结果的差异。如果差异超过 40%,说明模型变化太大,得谨慎放量。这个习惯帮我躲过了好几次线上事故。希望帮到你。
本文还有配套的精品资源,点击获取