ARTICLE DETAIL

资讯详情

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

Python服饰推荐系统实战:从数据到排序的完整链路

Python服饰推荐系统实战:从数据到排序的完整链路 简介这是一套面向Python初学者与推荐系统爱好者的服饰推荐系统实战项目围绕个性化服装推荐场景综合运用数据采集、机器学习与Web开发技术帮助读者理解从用户行为数据到精准推荐列表的完整链路。资源包共2000个文件以9698张jpg服饰图片和22个csv数据集为主辅以30个py脚本、27个js与8个vue前端文件、26个xml配置及bson、json等数据文件压缩包约225.31MB目录结构清晰便于按模块检索。项目覆盖数据收集与预处理、特征工程、用户画像构建、协同过滤与矩阵分解等推荐算法、模型训练评估以及Flask/Django接口部署等关键环节并配有scrapy爬虫配置与前端页面资源。已有318人学习下载适合希望系统掌握推荐系统构建流程、积累数据驱动项目经验的开发者参考实践。1. 从零搭一套 Python 服饰推荐系统为什么它比你想的更好落地电商后台里堆着几十万条订单运营却还在用「销量 Top 20」给所有用户推同一批衣服这是很多中小团队的真实状态。Python 服饰推荐系统要解决的就是这件事把「谁买过什么」变成「谁可能想买什么」并且用一套能跑在单机或小集群上的代码实现。它适合有 Python 基础、懂一点 pandas 的开发者也适合想从零入门推荐算法的同学——服饰这个品类天然带颜色、尺码、风格、季节属性特征工程空间大做出来的效果比通用商品推荐更容易被业务感知。下面这套方案我按「数据准备 → 召回 → 排序 → 服务化」的路径拆开讲每一步都能单独跑通也能拼成完整链路。2. 数据与特征服饰推荐系统的地基怎么打2.1 三张核心表与字段设计任何推荐系统的起点都是数据。服饰场景下我一般会准备三张表用户行为表、商品属性表、用户画像表。行为表记录user_id、item_id、behavior_type浏览/加购/下单、timestamp商品表记录item_id、category、color、size、price、style画像表记录user_id、age_group、gender、preferred_style。这三张表用 pandas 读进来后第一件事是统一 ID 类型避免后面 merge 时因为 int 和 str 对不上导致空结果。import pandas as pd # 读取三张核心表dtype 显式指定避免 ID 被推断成 float behavior pd.read_csv(behavior.csv, dtype{user_id: str, item_id: str}) item pd.read_csv(item.csv, dtype{item_id: str}) user pd.read_csv(user.csv, dtype{user_id: str}) # 行为表去重同一用户对同一商品同一秒的重复上报只保留一条 behavior behavior.drop_duplicates(subset[user_id, item_id, behavior_type, timestamp]) # 时间戳转 datetime后续做时间衰减要用 behavior[timestamp] pd.to_datetime(behavior[timestamp]) print(behavior.shape, item.shape, user.shape)这段代码的关键在dtype指定和去重。服饰类目下用户反复浏览同一件衣服的概率很高不去重会让热门商品权重虚高。timestamp转成 datetime 是为了后面算「最近 7 天行为」和「时间衰减系数」。参数上drop_duplicates的 subset 必须包含behavior_type否则用户先浏览后下单会被误删一条。2.2 服饰特有的特征颜色、尺码、季节通用推荐系统往往只用 category 和 price但服饰品类里颜色和尺码的区分度极高。一个用户如果连续三次浏览 M 码你给他推 XL 码就是浪费曝光。我一般会把颜色和尺码做 one-hot再和用户历史行为做交叉。季节特征用月份映射3-5 春、6-8 夏、9-11 秋、12-2 冬。这个映射表直接写死在代码里比让模型自己学更稳。# 颜色和尺码 one-hot color_dummies pd.get_dummies(item[color], prefixcolor) size_dummies pd.get_dummies(item[size], prefixsize) item_feat pd.concat([item[[item_id, category, price]], color_dummies, size_dummies], axis1) # 季节映射 season_map {12: winter, 1: winter, 2: winter, 3: spring, 4: spring, 5: spring, 6: summer, 7: summer, 8: summer, 9: autumn, 10: autumn, 11: autumn} behavior[season] behavior[timestamp].dt.month.map(season_map) # 用户偏好的尺码取最近 30 天出现次数最多的尺码 recent behavior[behavior[timestamp] behavior[timestamp].max() - pd.Timedelta(days30)] size_pref recent.merge(item[[item_id, size]], onitem_id) \ .groupby(user_id)[size].agg(lambda x: x.mode().iloc[0] if not x.mode().empty else M) print(size_pref.head())get_dummies生成的列数取决于颜色和尺码的枚举值服饰类目一般颜色 10-20 种、尺码 5-8 种维度可控。size_pref用众数而不是均值因为尺码是离散的均值没有物理意义。如果某个用户最近 30 天没有行为mode()会返回空这里用if not x.mode().empty兜底成 M 码避免后续报错。2.3 行为权重与时间衰减浏览、加购、下单三种行为的信号强度完全不同。我一般设浏览 1 分、加购 3 分、下单 5 分。再叠加时间衰减越近的行为权重越高用指数衰减exp(-λ * days)λ 取 0.1 时大约 7 天前的行为权重降到一半。这个参数没有绝对标准服饰换季快λ 可以取大一点比如 0.15。import numpy as np weight_map {view: 1, cart: 3, order: 5} behavior[base_weight] behavior[behavior_type].map(weight_map) # 时间衰减以数据集中最新时间为基准 max_ts behavior[timestamp].max() behavior[days_ago] (max_ts - behavior[timestamp]).dt.total_seconds() / 86400 behavior[decay] np.exp(-0.15 * behavior[days_ago]) behavior[final_weight] behavior[base_weight] * behavior[decay] print(behavior[[user_id, item_id, behavior_type, final_weight]].head())final_weight就是后续召回和排序的输入。注意days_ago用总秒数除以 86400比直接用.dt.days精度高。如果数据集时间跨度超过一年建议先截断到最近 180 天否则早期行为衰减到接近零白白增加计算量。3. 召回层用 ItemCF 和内容相似度双路兜底3.1 ItemCF 召回共现矩阵怎么算不爆内存ItemCF 的核心是「买了 A 的人也买了 B」。服饰场景下共现矩阵的稀疏度很高直接算全量 item-item 相似度会爆内存。我的做法是先按用户分组取每个用户的行为序列再两两组合生成 item 对。为了控制规模只保留共现次数大于 2 的对。from itertools import combinations from collections import defaultdict # 按用户聚合商品列表 user_items behavior.groupby(user_id)[item_id].apply(list).to_dict() # 统计共现 cooccur defaultdict(int) for items in user_items.values(): unique_items list(set(items)) if len(unique_items) 50: # 异常用户截断 unique_items unique_items[:50] for a, b in combinations(sorted(unique_items), 2): cooccur[(a, b)] 1 # 转成 DataFrame 并过滤低频 cooccur_df pd.DataFrame( [(a, b, c) for (a, b), c in cooccur.items() if c 2], columns[item_a, item_b, co_count] ) print(cooccur_df.shape)combinations生成的是无序对用sorted保证 (A,B) 和 (B,A) 只出现一次。len(unique_items) 50的截断是防止个别爬虫账号或测试账号把矩阵撑爆。co_count 2过滤掉偶然共现服饰类目下这个阈值可以调到 3看数据量决定。3.2 相似度计算与 TopN 截断有了共现次数还要除以两个商品的流行度做归一化否则热门商品会和所有商品都「相似」。公式是co_count / sqrt(count_a * count_b)。算完相似度后每个商品只保留 Top 20 相似商品存成字典供线上查询。# 商品流行度 item_count behavior.groupby(item_id)[user_id].nunique().to_dict() cooccur_df[sim] cooccur_df.apply( lambda r: r[co_count] / np.sqrt(item_count.get(r[item_a], 1) * item_count.get(r[item_b], 1)), axis1 ) # 每个 item 保留 Top20 相似 sim_dict {} for item_a, group in cooccur_df.groupby(item_a): top group.nlargest(20, sim)[[item_b, sim]].values.tolist() sim_dict[item_a] top print(len(sim_dict), sim_dict.get(list(sim_dict.keys())[0]))item_count用nunique而不是count因为同一用户多次浏览同一商品只算一次流行度。nlargest(20)的 20 是经验值线上召回一般取 50-100 个候选这里每个种子商品出 20 个多个种子叠加后足够。sim_dict可以直接用 pickle 存下来线上加载后查表。3.3 内容相似度召回解决冷启动新品没有行为数据ItemCF 召不回。这时候用内容相似度兜底把商品的颜色、尺码、类目、价格分桶做向量算余弦相似度。价格分桶我一般按分位数切 5 段避免高价商品和低价商品被算成相似。from sklearn.preprocessing import OneHotEncoder from sklearn.metrics.pairwise import cosine_similarity # 价格分桶 item[price_bin] pd.qcut(item[price], q5, labelsFalse, duplicatesdrop) # 组合特征 item[content_feat] item[category].astype(str) _ \ item[color].astype(str) _ \ item[size].astype(str) _ \ item[price_bin].astype(str) enc OneHotEncoder() content_matrix enc.fit_transform(item[[content_feat]]) content_sim cosine_similarity(content_matrix) # 转成 Top20 字典 content_sim_dict {} for i, item_id in enumerate(item[item_id]): sim_scores list(enumerate(content_sim[i])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue)[1:21] content_sim_dict[item_id] [(item[item_id].iloc[j], s) for j, s in sim_scores] print(len(content_sim_dict))qcut的duplicatesdrop是防止价格分布太集中导致分桶失败。content_feat用字符串拼接再 one-hot比分开 one-hot 再拼接更省内存。cosine_similarity在商品量小于 10 万时可以直接算超过 10 万建议用稀疏矩阵或近似最近邻库。4. 排序层用 LightGBM 把召回结果排好序4.1 样本构造与负采样召回层给出几百个候选排序层要从中选出 Top 10 展示。训练样本用「曝光未点击」做负样本「点击/加购/下单」做正样本。服饰场景下正负样本比例可能到 1:50需要负采样到 1:5 左右。采样时按时间倒序取最近的负样本避免用太久远的数据。# 假设有曝光日志 exposure exposure pd.read_csv(exposure.csv, dtype{user_id: str, item_id: str}) exposure[label] exposure[clicked].astype(int) # 负采样 pos exposure[exposure[label] 1] neg exposure[exposure[label] 0].sample(nlen(pos) * 5, random_state42) train pd.concat([pos, neg]).sample(frac1, random_state42).reset_index(dropTrue) print(train[label].value_counts())sample(nlen(pos)*5)控制负样本数量random_state固定保证可复现。如果曝光日志里没有clicked字段可以用行为表反推有行为的算正样本没行为的算负样本但要注意曝光未点击和未曝光的区别。4.2 特征拼接与 LightGBM 训练排序特征分三类用户特征年龄、性别、偏好尺码、商品特征类目、颜色、价格、交叉特征用户偏好颜色是否匹配商品颜色。LightGBM 对类别特征支持好直接指定categorical_feature即可。import lightgbm as lgb from sklearn.model_selection import train_test_split # 拼接特征 train train.merge(user, onuser_id, howleft) \ .merge(item_feat, onitem_id, howleft) # 交叉特征用户偏好尺码是否匹配 train train.merge(size_pref.rename(pref_size), onuser_id, howleft) train[size_match] (train[pref_size] train[size]).astype(int) feature_cols [age_group, gender, category, color, size, price, size_match] X train[feature_cols] y train[label] X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42) cat_features [age_group, gender, category, color, size] dtrain lgb.Dataset(X_train, y_train, categorical_featurecat_features) dval lgb.Dataset(X_val, y_val, categorical_featurecat_features) params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1 } model lgb.train(params, dtrain, num_boost_round500, valid_sets[dval], callbacks[lgb.early_stopping(50)]) print(model.best_iteration, model.best_score)num_leaves31是 LightGBM 的常用起点服饰数据特征不多不需要太深。min_data_in_leaf50防止过拟合数据量小于 10 万时调到 100。early_stopping(50)表示验证集 AUC 50 轮不提升就停。categorical_feature里的字段必须是整数编码或字符串LightGBM 会自动处理。4.3 排序结果重排打散与多样性模型打分后直接取 Top 10 会有问题同一类目可能占满整个列表。我一般做类目打散同一类目最多出现 3 个剩下的位置用次优商品补。这个逻辑在服务层做不放进模型。def rerank(scores, item_info, top_k10, max_per_category3): scores sorted(scores, keylambda x: x[1], reverseTrue) result [] category_count defaultdict(int) for item_id, score in scores: cat item_info[item_id][category] if category_count[cat] max_per_category: continue result.append((item_id, score)) category_count[cat] 1 if len(result) top_k: break return result # 示例 item_info item.set_index(item_id)[[category]].to_dict(index) scores [(iid, 0.9 - i * 0.01) for i, iid in enumerate(item[item_id].head(50))] print(rerank(scores, item_info))max_per_category3是经验值服饰类目下可以调到 2 让列表更多样。rerank的时间复杂度是 O(n log n)线上候选几百个时耗时可忽略。如果业务要求强多样性可以再加颜色打散。5. 避坑与排查服饰推荐系统最常见的 5 个翻车现场5.1 现象离线 AUC 0.85线上点击率没涨原因离线样本用了曝光日志但线上召回层没覆盖到那些商品。离线评估的是排序层在「给定候选集」上的表现候选集本身有问题排序再好也没用。解决先看召回覆盖率统计线上曝光商品中有多少来自召回层。如果覆盖率低于 80%优先优化召回别急着调排序模型。5.2 现象新品永远推不出去原因ItemCF 召回依赖行为共现新品没有行为数据召不回排序模型训练样本里新品也少打分偏低。解决内容相似度召回给新品兜底同时在排序层加一个「新品加权」特征比如上架 7 天内的商品加 0.1 分。这个权重别太大否则会伤害整体效果。5.3 现象用户反馈「推的尺码不对」原因size_pref用最近 30 天众数但用户可能最近在给家人买衣服尺码偏好变了。解决把size_pref拆成「自用」和「送礼」两个场景用收货地址或订单备注区分。如果区分不了至少把最近 7 天的尺码偏好单独做一个特征让模型自己学权重。5.4 现象训练时 AUC 很高上线后打分分布和离线不一致原因离线特征用 pandas 算线上用 SQL 或 Redis 算两边逻辑不一致。比如离线days_ago用数据集最新时间线上用当前时间导致衰减系数不同。解决把特征计算逻辑封装成统一函数离线和线上调同一个函数。如果做不到至少写单元测试对比两边输出。5.5 现象服务响应时间超过 200ms原因召回层查sim_dict是内存操作很快但排序层每次都要拼特征、调 LightGBM如果特征从数据库逐条查就会慢。解决用户特征和商品特征预加载到 Redis排序时批量取。LightGBM 用predict的num_threads参数控制并发线上一般设 4 线程再高收益递减。6. 进阶技巧用 FAISS 把召回从 200ms 压到 20ms当商品量超过 10 万sim_dict查表虽然快但内容相似度召回如果每次都要算余弦相似度就慢了。我一般用 FAISS 做向量检索把商品内容向量预训练好存进索引线上查询时直接搜 TopN。下面是一个最小示例。import faiss import numpy as np # 假设 content_matrix 是稀疏矩阵先转 dense vectors content_matrix.toarray().astype(float32) faiss.normalize_L2(vectors) # 归一化后内积等于余弦相似度 # 建索引 dim vectors.shape[1] index faiss.IndexFlatIP(dim) # 内积索引 index.add(vectors) # 查询给一个用户历史商品取平均向量作为 query user_vec vectors[0:1] # 示例 faiss.normalize_L2(user_vec) D, I index.search(user_vec, 20) print(I, D)IndexFlatIP是精确检索商品量小于 100 万时够用。超过 100 万换IndexIVFFlat但需要训练。normalize_L2必须在add和search前都做否则内积不等于余弦。I返回的是向量下标需要提前存好item_id到下标的映射。方案召回耗时召回率适用规模字典查表5ms高 10 万FAISS 精确20ms高 100 万FAISS 近似10ms中 100 万验证方法用离线测试集算召回率对比 FAISS 和字典查表的结果重合度。如果重合度低于 95%检查归一化和索引参数。我自己的习惯是每次上线新召回策略前先跑一遍 A/B 测试观察点击率和转化率别只看离线指标。希望帮到你。本文还有配套的精品资源点击获取
返回列表