
简介面向电商会员运营、推荐算法及用户增长从业者这套方案以分层聚类与深度兴趣网络DIN为核心将用户画像、会员分群与推荐系统打通解决会员分群粗放、权益同质化、触达不精准等核心痛点。文档共175页含45个大章节压缩包为单个PDF文件约11.01MB支持目录章节跳转及阅读器左侧书签大纲定位。内容从电商会员数据体系构建、特征工程、数据预处理讲起逐步深入到聚类算法原理、参数调优与有效性验证再覆盖DIN网络结构、注意力机制、时序行为建模、损失函数设计以及数据标注、模型训练与优化器等环节穿插SHAP特征权重分析、AdamW优化器选择等实践要点。目前已有87人学习适合算法工程师、数据分析师与运营策略人员参考可帮助读者快速建立从用户分群到差异化权益与触达策略的完整落地框架。1. 一个标题里藏着两套模型先把DeepSeek电商会员运营拆开看某品牌电商的会员项目做到第三个版本时我意识到RFM分层的天花板不是算法而是假设。用户的会员等级半年变一次但兴趣每个星期都在变一个本该收到母婴券的用户可能因为家里宠物出生已经连续一周浏览猫粮。标题里这个方案的本质就是把「会员是谁」和「会员此刻想要什么」拆成两套模型来处理分层聚类算法负责把用户切进稳定的人群域深度兴趣网络负责在候选权益上给每个人打分最后用DeepSeek把分数翻译成权益文案与差异化触达。适合正在做会员精细化运营、被静态分群和统一券包困扰的算法和运营团队也适合想了解DIN如何落到非推荐场景的工程师。2. 分层聚类算法与深度兴趣网络选型先搞清楚两个模型的分工2.1 为什么分层聚类比KMeans更合适不预设簇数才能留下小人群会员特征矩阵通常包含最近一次购买间隔、180天订单数、GMV、品类占比、券核销率等。KMeans的第一个问题是必须提前指定K而这在业务上很难回答第二个问题是对噪声和初始中心敏感跑两次结果可能差很远。在电商会员数据里高频高价值用户只占一小部分剩下大量中低频用户里还混着「季节性爆发用户」和「新客尝鲜用户」KMeans容易把这两类并进同一个大簇运营拿到分群后仍然只能群发。我一般会先把样本用MiniBatchKMeans粗分到200个中间簇再对这些簇中心做凝聚层次聚类AgglomerativeClustering用Ward方差最小化做连接准则。这样既避开了全量样本两两距离矩阵的内存爆炸又保留了一层可以随时换切分位的能力。业务上想从5段调到8段不需要重跑全量只改层级的n_clusters即可。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.cluster import MiniBatchKMeans, AgglomerativeClustering # features: 只含数值型会员特征不含member_id scaler StandardScaler() X scaler.fit_transform(features.drop(columns[member_id])) # 第一步粗分到200个小簇 mbk MiniBatchKMeans(n_clusters200, batch_size4096, random_state42) coarse_ids mbk.fit_predict(X) # 第二步在粗簇中心上做层次聚类得到5-8个会员域 centers mbk.cluster_centers_ agg AgglomerativeClustering(n_clusters7, metriceuclidean, linkageward) meta_labels agg.fit_predict(centers) # 第三步把粗簇映射到最终会员域 coarse_to_segment {i: meta_labels[i] for i in range(len(meta_labels))} member_segment np.array([coarse_to_segment[c] for c in coarse_ids])参数说明batch_size4096控制每次更新聚类中心用的样本数为了在内存和迭代速度之间取平衡n_clusters200不是最终分群数而是中间层太小会损失人群边界太大会让层次聚类退化成对噪声聚类metriceuclidean与linkageward是固定搭配因为Ward方法本身基于欧氏距离计算簇内方差增量。这里最容易被忽略的是特征标准化如果GMV单位是元、间隔单位是天直接丢进Ward会让量纲大的特征主导距离。提示高基数离散特征不要直接放进层次聚类。比如会员等级、渠道来源这样的标签可以先做成目标编码或embedding降维否则Ward会把“渠道相同”误判成“行为相似”。2.2 深度兴趣网络(DIN)的注意力机制候选权益与历史兴趣的相关性深度兴趣网络DIN最早出现在广告推荐场景核心贡献是「局部激活单元」。它不对所有历史行为做平均池化而是先计算候选物品与每个历史行为的相关系数再按相关系数做加权求和。放到会员权益场景里候选对象从商品换成权益历史序列换成过去90天浏览过的品类、点击过的券、兑换过的积分商品。假设用户最近点过「无门槛券」「运费险」和「奶粉品类券」现在候选权益是一张「满199减30」美妆券DIN会算出美妆券和历史行为的相关性很低加权结果被压低如果候选是「母婴品类免运费券」历史里的奶粉和运费险都会被激活打分自然高。这个逻辑比直接把所有行为embedding求平均要细腻得多。import torch import torch.nn as nn import torch.nn.functional as F class LocalActivationUnit(nn.Module): def __init__(self, embedding_dim): super().__init__() # 拼接候选、行为、候选-行为、候选*行为四个向量 self.fc1 nn.Linear(embedding_dim * 4, 36) self.fc2 nn.Linear(36, 1) def forward(self, candidate, behavior_seq): B, T, E behavior_seq.shape cand candidate.unsqueeze(1).expand(B, T, E) concat torch.cat([ cand, behavior_seq, cand - behavior_seq, cand * behavior_seq ], dim-1) out torch.relu(self.fc1(concat)) return torch.sigmoid(self.fc2(out)) # [B, T, 1] class DIN(nn.Module): def __init__(self, n_item, embedding_dim, hidden_dim): super().__init__() self.item_emb nn.Embedding(n_item, embedding_dim) self.au LocalActivationUnit(embedding_dim) self.mlp nn.Sequential( nn.Linear(embedding_dim * 2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1) ) def forward(self, candidate, behavior_seq, seq_mask): cand_emb self.item_emb(candidate) # [B, E] bhv_emb self.item_emb(behavior_seq) # [B, T, E] weight self.au(cand_emb, bhv_emb).squeeze(-1) # [B, T] weight weight.masked_fill(~seq_mask, 0.0) # padding位置不参与 weight_sum weight.sum(dim-1, keepdimTrue).clamp(min1e-8) pooled (weight.unsqueeze(-1) * bhv_emb).sum(dim1) / weight_sum out torch.cat([cand_emb, pooled], dim-1) return self.mlp(out)seq_mask是形状为[B, T]的布尔张量标记行为序列中哪些位置真实存在。masked_fill把padding位置权重置0避免无效行为拉低注意力。拼接cand - behavior_seq和cand * behavior_seq不是拍脑袋乘法能刻画两个embedding之间的二阶交叉减法能表达“负向偏移”比如用户最近对某个品类明显冷淡。2.3 DeepSeek在这里补什么位置文本特征与触达文案的生成分层聚类和DIN都是判别模型只能回答“我该给谁发什么”。但会员运营还有两块数据它们吃不了一是售后评价、咨询工单这类非结构化文本二是发给用户的那句push文案。DeepSeek在这个方案里承担两类活一类是把用户留言转成结构化标签喂给分群特征另一类是根据权益类型和用户兴趣自动生成多条触达文案替代运营手写。很多团队验证大模型能力时习惯先接IDE插件比如在Codex或Claude Code里试试DeepSeek但那只验证了对话质量。真正上生产要按DeepSeek开放平台的API协议来接入且要考虑数据是否能出域。数据不出域的业务场景常见做法是本地部署DeepSeek再暴露OpenAI兼容接口给下游调用。我建议把DeepSeek放在异步任务里不要在用户请求链路上同步等待生成结果。from openai import OpenAI client OpenAI( base_urlYOUR_ENDPOINT, # 本地部署或API网关地址 api_keyYOUR_API_KEY, timeout10, ) response client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是电商会员运营助手只输出JSON。}, {role: user, content: 把这条售后评价归类为价格敏感/物流不满/质量问题/赠品需求输出:{\reason\:\\}} ], temperature0.2 ) print(response.choices[0].message.content)temperature0.2是为了打标任务保持稳定如果把参数调高同一个评价可能被归到不同类特征分布会引入额外抖动。调用超时设为10秒宁可放弃单条文本标签也不能让离线跑批卡死。3. 从数据到样本搭建会员分层与DIN训练集的正确姿势3.1 会员360标签表和权益交互日志怎么取分群和DIN要使用同一批会员但特征设计不一样。分群特征要稳定适合取近180天累积量DIN训练需要“行为序列候选权益反馈标签”行为序列截断到观察日之前反馈标签放在观察日之后否则会出现未来信息泄露。我常用快照日方式抽数比如今天想预测未来30天的核销行为观察日设为T训练样本的序列特征只取T-180到T标签取T到T30。下面是Hive/Spark SQL风格的取数示例注意recency_days用快照日减去最后一次下单日。WITH member_base AS ( SELECT member_id, datediff(2024-12-31, max(order_date)) AS recency_days, count(DISTINCT order_id) AS order_cnt_180d, sum(order_amt) AS gmv_180d, count(DISTINCT CASE WHEN category 母婴 THEN order_id END) AS baby_order_cnt, count(DISTINCT CASE WHEN category 宠物 THEN order_id END) AS pet_order_cnt FROM dwd_order_d WHERE order_date BETWEEN date_sub(2024-12-31, 180) AND 2024-12-31 GROUP BY member_id ), interaction AS ( SELECT member_id, collect_list(coupon_type) AS coupon_action_seq, collect_list(category_id) AS category_view_seq, collect_list(event_time) AS event_time_seq FROM dwd_member_interaction_log WHERE event_date BETWEEN date_sub(2024-12-31, 90) AND 2024-12-31 GROUP BY member_id ) SELECT a.member_id, a.recency_days, a.order_cnt_180d, a.gmv_180d, b.coupon_action_seq, b.category_view_seq FROM member_base a LEFT JOIN interaction b ON a.member_id b.member_id;collect_list会把同一个会员的行为按聚合顺序放进数组后续在Python里按event_time_seq排序并截断到最近50条。这里要特别留意不要把当天的行为当特征又当标签建议把观察日设置为0点特征区间严格小于观察日标签区间大于观察日。3.2 DeepSeek API如何调用把评价文本变成可入模标签会员评价文本通常散落在订单评论表和客服工单表。传统做法是通过关键词匹配打标比如出现“太慢了”就归为物流不满但“包装很好但快递太慢”会被关键词拆碎。用DeepSeek做这件事输出要限制成JSON便于直接落到特征表。import json import pandas as pd from openai import OpenAI client OpenAI(base_urlYOUR_ENDPOINT, api_keyYOUR_API_KEY, timeout5) def tag_review(text: str) - dict: try: resp client.chat.completions.create( modeldeepseek-chat, messages[{ role: user, content: ( 从[价格敏感,物流不满,质量问题,售后效率,赠品需求]中选择1-3个标签 f返回JSON字典不要其他解释。评论文本:{text} ) }], temperature0.0, ) return json.loads(resp.choices[0].message.content) except Exception: return {reason: unknown} reviews[tags] reviews[review_content].map(tag_review)这段代码在Demo里能跑但生产环境不要每条同步调用。我会用消息队列把评价文本发给WorkerWorker批量调DeepSeek结果写回标签宽表调用失败的消息进入重试队列。temperature0.0和解析失败默认unknown都很重要前者保证标签可复现后者保证单条异常不会掐断整个管道。3.3 特征标准化、缺失值处理与分群样本的平衡分层聚类对缺失值非常敏感因为Ward依赖欧氏距离缺失值如果被简单填0会把“没有某行为”和“行为为0”混为一谈。我一般按特征业务含义决定填充方式特征组字段示例处理方式价值类gmv_180d, order_cnt_180dlog1p后标准化压制长尾时效类recency_days, last_active_hour缺失代表新客填窗口最大值品类偏好baby_order_cnt, pet_order_cnt转占订单比避免数量级差异文本标签price_sensitive_flag, logistics_flagone-hot缺失填0序列特征coupon_action_seq, category_view_seq映射id序列截断到50并padding分群样本不用刻意做平衡聚类是无监督方法保持自然分布才能反映真实人群结构。但DIN是有监督训练负样本要采样通常正样本来自“曝光后点击或核销”负样本来自“曝光未点击”加上一部分未曝光的随机权益比例控制在1比3到1比5。负样本多了会让模型打分整体偏低后面做阈值选择时要重新校准。4. 用代码把分层聚类和深度兴趣网络串成会员权益模型4.1 聚类K值选择轮廓系数只是参考业务承接力才是硬约束在分层聚类中K的选择要看两个东西第一个是轮廓系数第二个是最小簇人数占比。轮廓系数高不代表业务能用比如某个簇只有几千人运营不可能为它单独设计一套权益包反过来一个簇占60%人群说明聚类没有区分度。实际操作时我会在5到12之间扫一遍然后输出每个候选K的最小簇占比。from sklearn.metrics import silhouette_score for k in range(5, 13): agg AgglomerativeClustering(n_clustersk, metriceuclidean, linkageward) labels agg.fit_predict(centers) s silhouette_score(centers, labels) min_ratio pd.Series(labels).value_counts(normalizeTrue).min() print(fk{k}, silhouette{s:.3f}, min_ratio{min_ratio:.2%})经验上我会要求min_ratio不低于1%同时每个簇在业务上能一句话解释清楚。比如“高价低频但偏好宠物粮”和“高频低价偏好临期商品”是两类可以运营的人群“其他”这种名字一次都不该出现。4.2 深度兴趣网络排序权益样本组织、损失函数和训练周期模型结构沿用2.2的DIN本节关心的是训练数据怎么喂。需要构造四列member_id、candidate_benefit_id、behavior_seq、label。behavior_seq是最近50次交互行为的权益/商品id序列label表示用户是否在表现期内核销、点击或复购。行为序列和候选权益要使用同一套id映射不能一个用商品id、一个用券id。train_df build_sample( observation_start2024-10-01, observation_end2024-12-31, label_window30 ) seq_ids torch.tensor(train_df[behavior_seq].tolist(), dtypetorch.long) cand_ids torch.tensor(train_df[candidate_benefit_id].tolist(), dtypetorch.long) seq_mask torch.tensor(train_df[seq_mask].tolist(), dtypetorch.bool) labels torch.tensor(train_df[label].values, dtypetorch.float32) model DIN(n_itemlen(item_id_mapping), embedding_dim16, hidden_dim64) optimizer torch.optim.Adam(model.parameters(), lr0.001) loss_fn nn.BCEWithLogitsLoss() for epoch in range(3): optimizer.zero_grad() logit model(cand_ids, seq_ids, seq_mask).squeeze(-1) loss loss_fn(logit, labels) loss.backward() optimizer.step()embedding_dim16对商品和权益规模在几十万的场景足够如果品类很少8维也行。hidden_dim64控制MLP表达力过大会在小样本上过拟合。训练3个epoch是保守选择会员权益数据通常比商品点击量少两个量级模型很快收敛后端要靠early stopping盯着验证集AUC而不是把epoch拉长。4.3 两级权益分配分群决定候选池DIN决定个体排序如果让DIN直接对全量权益池打分候选集很容易到几十万离线训练和线上查询成本都不可控。更稳妥的做法是两级结构先用分层聚类结果把用户映射到某个会员域每个域配置一个几十到上百权益的候选池然后DIN只对这个候选池打分。分群解决“该人群普遍要什么”DIN解决“这个人此刻更可能回应哪一张券”。def recommend_benefit(member_id: str, member_features: dict): segment load_segment(member_id) candidate_pool segment_benefit_pool[segment] scores din_score(member_features, candidate_pool) ranked scores.sort_values(score, ascendingFalse) # 预算控制单用户权益成本不能超过剩余预算 affordable ranked[ranked[expected_cost] load_remain_budget(member_id)] if affordable.empty: return rule_fallback(member_id, segment) return affordable.iloc[0][benefit_id]rule_fallback是一个必须存在的兜底逻辑当DIN打分后所有候选权益都超出预算就退回该会员域的默认低门槛券。两级结构还有一个额外好处运营调整权益池不需要重新训练DIN只改分群和候选池映射关系。5. 差异化会员权益与触达策略模型打分之后才是运营真正的主场5.1 权益不是「满减金额」把权益拆成原子券包运营经常把权益简化成“满减券”但影响用户感知的是门槛、品类、有效期、发放渠道的组合。同一张“满199减30”加上“限母婴品类”和“3天有效”之后对母婴人群的感知度完全不同。做差异化权益的第一件事是把权益拆成原子属性权益属性可选值策略作用权益类型无门槛券、满减券、运费险、赠品、积分翻倍决定基础吸引力适用品类全品类、母婴、宠物、美妆、食品圈定相关人群门槛0元、满99、满199影响核销门槛与客单价有效期3天、7天、15天控制紧迫感触达渠道App Push、短信、企微、站内信决定触达成本与疲劳度对高价值但最近30天未活跃的用户我会给无门槛券加7天有效期文案突出“专属”“回来看看”对高频低客单用户给满减券但门槛贴近他们平时客单的1.2倍。聚类在这里的最大价值是让运营不用逐个人设规则而是按会员域批量配置。5.2 触达频控与渠道优先级规则写在模型前面模型打分会给出“给谁发”但如果三个模型都在推同一个用户用户一天收到六条push退订率会立刻上升。触达策略里的第一条规则不是“高评分优先”而是“今日已被触达超过N次的用户直接跳过”。def touch_decision(member_id: str, benefit_id: str, score: float) - str: fatigue_7d load_touch_count(member_id, window_days7) if fatigue_7d 3: return skip preferred_channel load_preferred_channel(member_id) if score 0.7: return fsend:{preferred_channel}:immediate elif score 0.4: return fsend:app_push:next_morning else: return skip这段规则把高频用户拦截在模型之前避免“模型越聪明打扰越多”的失控。渠道优先级也需要分人群企微适合高价值用户短信适合流失召回App Push适合日常触达。每次触达都要打上campaign_id后续实验才能拆分清楚是文案影响了核销还是触达时机影响了打开。5.3 用DeepSeek生成权益文案并做成可A/B的变量同一个权益文案和利益点可以有多种组合。我会让DeepSeek一次生成5条候选文案每条不超过20字然后把文案选项随机下发到A/B实验组。prompt f 为会员生成一条权益触达文案。 权益无门槛运费券 适用人群最近30天未活跃但历史客单高于500元的会员 用户近期兴趣宠物用品 要求不超过18个字突出宠物用品相关权益不使用“限时抢购”这类夸张词不要出现折扣数字。 resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.8, ) print(resp.choices[0].message.content)temperature0.8在文案场景是有意调高让多条候选之间有足够差异方便A/B测试找出有效句式。文案层和权益层必须拆开测一组用DeepSeek生成的文案常规券另一组用人工文案同一张券才能判断LLM是否真正带来增量。6. 上线后排错与验证DeepSeek服务器繁忙、簇漂移与增量衡量6.1 DeepSeek服务器繁忙、超时与降级策略DeepSeek接入后最先遇到的问题不是效果而是“DeepSeek服务器繁忙,请稍后再试”。文案生成是离线任务可以重试但必须防抖用指数退避重试重试次数不超过3次超过后直接落到模板文案不能阻塞整个触达任务。from tenacity import retry, stop_after_attempt, wait_exponential from openai import OpenAIError retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, max8), retryretry_if_exception_type((OpenAIError, TimeoutError)), ) def call_deepseek_for_copy(prompt: str) - str: resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.8, timeout8, ) return resp.choices[0].message.content生产环境我还会加一层Redis缓存同一权益同一人群的文案直接复用。线上每个小时的调用配额固定超配额时宁可少发几条也不要让大量任务堆积在重试队列里。6.2 分层聚类的簇数漂移与用户生命周期迁移每个月重跑分群后簇编号会变运营看到的“第3类人群”可能和上月不是同一批人。我会用上一期的聚类中心和标签训练一个KNN分类器对本月样本做映射这样能保持人群口径连续。from sklearn.neighbors import KNeighborsClassifier from scipy.stats import ks_2samp # last_centers: 上一期每个簇的中心 knn KNeighborsClassifier(n_neighbors3) knn.fit(last_centers, list(range(len(last_centers)))) this_month_segment knn.predict(this_month_X) # 对每个稳定簇做分布漂移检测 for feat in [gmv_180d, recency_days, pet_order_ratio]: p_value ks_2samp(last_month[feat], this_month[feat]).pvalue if p_value 0.01: print(f{feat} 分布显著变化需要人工复核簇定义)KS检验只能提示特征分布变了不能自动告诉你该不该重新聚类。我会把漂移告警发给运营而不是直接重训模型不然每次重训出来的分群都没法做历史同比。6.3 触达实验的因果推断核销率不是增量ARPU才是验证权益策略是否有效的唯一方法是做对照实验。在同一会员域内按member_id哈希分桶实验组发权益对照组不发两组都不做其他特殊运营干预。结果表里至少要保留曝光数、核销数、支付金额三个字段。SELECT t.group_id, count(DISTINCT t.member_id) AS members, sum(CASE WHEN t.coupon_used_flag 1 THEN 1 ELSE 0 END) / count(DISTINCT t.member_id) AS coupon_use_rate, sum(t.paid_amount) / count(DISTINCT t.member_id) AS arpu FROM experiment_result t GROUP BY t.group_id;只看核销率很容易被“用户本来就要买”的自选择欺骗。净增量应当对比实验组和对照组的ARPU差再减去权益成本。如果实验组ARP高于对照组但客诉率或退订率同时上升这个策略也不能放量。报告周期至少要覆盖两个完整会员月才能避开发薪日、平台大促和品类季节性的干扰。本文还有配套的精品资源点击获取