实验和完整代码
完整代码实现和jupyter运行:https://github.com/Myolive-Lin/RecSys--deep-learning-recommendation-system/tree/main
引言
在电商与广告推荐场景中,用户兴趣的多样性和动态变化是核心挑战。传统推荐模型(如Embedding & MLP)通过池化操作将用户历史行为压缩为固定长度的向量,导致用户兴趣表示过于静态化。为解决这一问题,阿里巴巴团队提出了深度兴趣网络(Deep Interest Network, DIN) ,通过自适应注意力机制动态建模用户兴趣
,显著提升了点击率预测(CTR)的精度。本文将深入解析DIN的数学模型、结构设计及训练优化技术。
2. 问题定义与特征体系
特征结构(见表1):
类别 | 特征组示例 | 维度 | 编码类型 |
---|---|---|---|
用户画像 | 性别、年龄 | 低维(~10) | One-hot |
用户行为 | 浏览商品ID | 高维(~10⁹) | Multi-hot |
广告属性 | 商品ID、类目ID | 高维(~10⁷) | One-hot |
上下文 | 时间、页面位置 | 低维(~10) | One-hot |
用户行为特征(如visited_goods_ids
)是多热编码(Multi-hot) 向量,包含丰富的兴趣信息,但传统池化方法无法捕捉其与候选广告的动态相关性。
这些特征的维度和稀疏性对模型的设计提出了巨大挑战。DIN 的创新之处在于如何有效地处理这些特征,并动态地捕捉用户兴趣。
3. Deep Interest Network(DIN)架构
DIN 的核心创新在于如何动态地建模用户兴趣。在基础模型中,用户兴趣的表示是通过 Pooling 层固定下来的,对于同一个用户,其兴趣向量在面对不同广告时保持不变。然而,这种表示方式无法捕捉用户兴趣的多样性和动态性。DIN 通过引入局部激活单元(Local Activation Unit),解决了这一问题。
3.1 局部激活单元
DIN 的关键在于局部激活单元的设计。它通过计算用户历史行为与候选广告的相关性,动态地生成用户兴趣的表示。具体来说,局部激活单元的输出是一个加权和:
v U ( A ) = f ( v A , e 1 , e 2 , … , e H ) = ∑ j = 1 H a ( e j , v A ) e j v_U(A) = f(v_A, e_1, e_2, \dots, e_H) = \sum_{j=1}^{H} a(e_j, v_A) e_j vU(A)=f(vA,e1,e2,…,eH)=j=1∑Ha(ej,vA)ej
其中, { e 1 , e 2 , … , e H } \{e_1, e_2, \dots, e_H\} {e1,e2,…,eH} 是用户行为的嵌入向量, v A v_A vA 是广告的嵌入向量, a ( ⋅ ) a(\cdot) a(⋅) 是一个前馈网络,用于计算激活权重。与传统的注意力机制不同,DIN 放弃了对权重进行归一化的操作,以保留用户兴趣的强度信息。
例如,假设一个用户的历史行为中有 90% 是服装相关,10% 是电子产品相关。当候选广告是 T 恤时,它会激活大部分与服装相关的行为,因此 v U v_U vU 的值会更大,表示用户对 T 恤的兴趣更强。
3.2 架构设计
DIN 的架构在基础模型的基础上引入了局部激活单元。对于用户行为特征,DIN 使用局部激活单元动态计算用户兴趣的表示,而其他部分的结构保持不变。这种设计使得用户兴趣的表示能够根据不同的广告动态变化,从而更准确地捕捉用户的兴趣。
架构如下:
王喆老师也给出DIN的另外一种形式如:
4. 基础模型:Embedding & MLP
以下是其基础架构——Embedding 和多层感知机(MLP)。
4.1 Embedding 层
由于输入特征是高维稀疏的二进制向量,Embedding 层的作用是将它们转换为低维密集的表示。对于第 i 个特征组 t i t_i ti,其 Embedding 字典 W i W_i Wi 的维度为 D × K i D \times K_i D×Ki,其中 D D D 是嵌入向量的维度, K i K_i Ki 是特征组的维度。Embedding 操作遵循表查找机制:
- 如果 t i t_i ti 是 one-hot 向量,且第 j 个元素为 1,则嵌入表示为 e i = w i j e_i = w_{ij} ei=wij。
- 如果 t i t_i ti 是 multi-hot 向量,且第 i 1 , i 2 , … , i k i_1, i_2, \dots, i_k i1,i2,…,ik 个元素为 1,则嵌入表示为一组向量 { e i 1 , e i 2 , … , e i k } \{e_{i1}, e_{i2}, \dots, e_{ik}\} {ei1,ei2,…,eik}。
4.2 Pooling 层和 Concat 层
由于用户行为的数量不同,multi-hot 特征向量的非零值数量会因实例而异。为了将这些可变长度的嵌入向量转换为固定长度的向量,通常使用 Pooling 层。最常见的 Pooling 方法是求和(sum pooling)和平均(average pooling):
e i = pooling ( e i 1 , e i 2 , … , e i k ) e_i = \text{pooling}(e_{i1}, e_{i2}, \dots, e_{ik}) ei=pooling(ei1,ei2,…,eik)
通过 Pooling 层,所有特征组的嵌入向量被转换为固定长度的向量,然后通过 Concat 层拼接在一起,形成最终的特征表示。
4.3 MLP 和损失函数
MLP 是一个多层感知机网络,用于自动学习特征的组合。其目标函数通常采用负对数似然函数:
L = − 1 N ∑ ( x , y ) ∈ S [ y log p ( x ) + ( 1 − y ) log ( 1 − p ( x ) ) ] L = -\frac{1}{N} \sum_{(x,y) \in S} \left[ y \log p(x) + (1 - y) \log (1 - p(x)) \right] L=−N1(x,y)∈S∑[ylogp(x)+(1−y)log(1−p(x))]
其中, S S S 是训练集, x x x 是输入特征, y ∈ { 0 , 1 } y \in \{0, 1\} y∈{0,1} 是标签, p ( x ) p(x) p(x) 是网络输出的点击概率。
5. 训练技术
在阿里巴巴的广告系统中,商品和用户的数量达到了数亿级别。训练如此大规模的深度网络是一个巨大的挑战。DIN 提出了两种重要的训练技术,以应对这一挑战。
5.1 小批量感知正则化(Mini-batch Aware Regularization)
由于亿级参数使用l2的消耗过大
传统的正则化方法(如
ℓ
2
\ell_2
ℓ2 和
ℓ
1
\ell_1
ℓ1)难以直接应用。DIN 提出了一种小批量感知正则化方法,它只计算每个小批量中出现的特征参数的
ℓ
2
\ell_2
ℓ2 范数。具体来说:
L 2 ( W ) ≈ ∑ j = 1 K ∑ m = 1 B α m j n j ∥ w j ∥ 2 L_2(W) \approx \sum_{j=1}^{K} \sum_{m=1}^{B} \frac{\alpha_{mj}}{n_j} \|w_j\|^2 L2(W)≈j=1∑Km=1∑Bnjαmj∥wj∥2
其中, n j n_j nj表示在样本中特征id j 出现的数量, α m j \alpha_{mj} αmj 表示在第 m 个小批量中是否存在特征 j。B表示mini-batches数量, B m B_m Bm 表示第m个 mini-batch。这种方法大大减少了计算量,同时有效地防止了过拟合。
数。
5.2 数据自适应激活函数(Dice)
在深度学习中,激活函数的选择对模型的性能至关重要。DIN 提出了一种数据自适应激活函数 Dice,它通过输入数据的均值和方差动态调整激活函数的行为:
f ( s ) = p ( s ) ⋅ s + ( 1 − p ( s ) ) ⋅ α s f(s) = p(s) \cdot s + (1 - p(s)) \cdot \alpha s f(s)=p(s)⋅s+(1−p(s))⋅αs
其中,
p ( s ) = 1 1 + e − s − E [ s ] Var [ s ] + ϵ p(s) = \frac{1}{1 + e^{-\frac{s - E[s]}{\sqrt{\text{Var}[s] + \epsilon}}}} p(s)=1+e−Var[s]+ϵs−E[s]1
Dice 的设计使得激活函数能够根据输入数据的分布动态调整,从而更好地适应不同层的输入分布。
6实验
原论文使用的Amazon Dataset,但是由于已经失效,于是使用DIN模型pytorch代码逐行细讲 给出的Amazon-100k数据集进行处理(注:免责声明: 本文使用的数据集为 Amazon-100k 数据集,该数据集来源不明确,且未能找到该数据集的原始出处。请读者在使用该数据集时注意遵循相应的版权和数据使用规定。
)
label | userID | itemID | cateID | hist_item_list | hist_cate_list |
---|---|---|---|---|---|
0 | AZPJ9LUT0FEPY | B00AMNNTIA | Literature & Fiction | [0307744434, 0062248391, 0470530707, 097892462…] | [Books, Books, Books, Books, Books] |
1 | AZPJ9LUT0FEPY | 0800731603 | Books | [0307744434, 0062248391, 0470530707, 097892462…] | [Books, Books, Books, Books, Books] |
0 | A2NRV79GKAU726 | B003NNV10O | Russian | [0814472869, 0071462074, 1583942300, 081253836…] | [Books, Books, Books, Books, Baking, Books, Books, Books] |
1 | A2NRV79GKAU726 | B000UWJ91O | Books | [0814472869, 0071462074, 1583942300, 081253836…] | [Books, Books, Books, Books, Baking, Books, Books, Books] |
0 | A2GEQVDX2LL4V3 | 0321334094 | Books | [0743596870, 0374280991, 1439140634, 0976475731] | [Books, Books, Books, Books] |
… | … | … | … | … | … |
0 | A3CV7NJJC20JTB | 098488789X | Books | [034545197X, 0765326396, 1605420832, 1451648448] | [Books, Books, Books, Books] |
1 | A3CV7NJJC20JTB | 0307381277 | Books | [034545197X, 0765326396, 1605420832, 1451648448] | [Books, Books, Books, Books] |
0 | A208PSIK2APSKN | 0957496184 | Books | [0515140791, 147674355X, B0055ECOUA, B007JE1B1…] | [Books, Books, Bibles, Literature & Fiction, Literature & Fiction] |
1 | A208PSIK2APSKN | 1480198854 | Books | [0515140791, 147674355X, B0055ECOUA, B007JE1B1…] | [Books, Books, Bibles, Literature & Fiction, Literature & Fiction] |
0 | A1GRLKG8JA19OA | B0095VGR4I | Literature & Fiction | [031612091X, 0399163832, 1442358238, 1118017447] | [Books, Books, Books, Books] |
使用Ordinal Encoding处理后得到:
使用Ordinal_itemID,Ordinal_cateID经过Embedding后拼接成的向量来表示目标向量
label | Ordinal_userID | Ordinal_itemID | Ordinal_cateID | Ordinal_hist_item_list | Ordinal_hist_cate_list |
---|---|---|---|---|---|
0 | 44917 | 68074 | 419 | [206424, 142847, 182786, 69605, 197011] | [386, 386, 386, 386, 386] |
1 | 44917 | 101880 | 386 | [206424, 142847, 182786, 69605, 197011] | [386, 386, 386, 386, 386] |
0 | 19804 | 6163 | 264 | [78315, 2890, 54255, 137135, 124338] | [386, 386, 300, 386, 386] |
1 | 19804 | 21400 | 386 | [78315, 2890, 54255, 137135, 124338] | [386, 386, 300, 386, 386] |
0 | 17385 | 220405 | 386 | [0, 30271, 97772, 12556, 137554] | [0, 386, 386, 386, 386] |
… | … | … | … | … | … |
0 | 28177 | 210244 | 386 | [0, 98594, 185606, 19190, 15365] | [0, 386, 386, 386, 386] |
1 | 28177 | 16915 | 386 | [0, 98594, 185606, 19190, 15365] | [0, 386, 386, 386, 386] |
0 | 12193 | 23175 | 386 | [24471, 189598, 130748, 33111, 100134] | [386, 386, 365, 419, 419] |
1 | 12193 | 114216 | 386 | [24471, 189598, 130748, 33111, 100134] | [386, 386, 365, 419, 419] |
0 | 5675 | 32074 | 419 | [0, 18220, 48157, 191849, 146917] | [0, 386, 386, 386, 386] |
将上述数据进行数据集拆分和训练,训练结果如下:
如上图所示,模型在第3个epoch时已经开始出现拟合现象。通过对比原论文的图表(下图),可以看到在该论文的实验中,Test Loss在第2个epoch时便开始上升,AUC也呈现出收敛的趋势。然而,由于数据集的差异,并且博主使用的正则化方法为原始的L2正则化,两个实验的结果不能直接进行比较。如果有任何错误或者不准确的地方,还请指正,谢谢!
7. torch实现
https://github.com/zhougr1993/DeepInterestNetwork 论文中也有使用tensorflow实现版本
正则化方面使用的是l2
数据集使用的是DIN模型pytorch代码逐行细讲提到的amazon-books-100k,但博主找了挺久未找到原官方出处,这里就不提供下载地址
Dice激活函数
class Dice(nn.Module):
def __init__(self, alpha=0.0, epsilon=1e-8):
"""
初始化 Dice 激活函数。
:param alpha: 可学习的参数,用于控制负值部分的缩放。
:param epsilon: 一个小常数,用于数值稳定性。
"""
super(Dice, self).__init__()
self.alpha = nn.Parameter(torch.tensor(alpha)) # 可学习参数
self.epsilon = epsilon
def forward(self, x):
"""
前向传播函数。
:param x: 输入张量,形状为 (batch_size, ...)。
:return: 经过 Dice 激活后的张量。
"""
# 计算输入 x 的均值和方差
mean = x.mean(dim=0, keepdim=True) # 沿 batch 维度计算均值,保留维度
var = x.var(dim=0, keepdim=True, unbiased=False) # 沿 batch 维度计算方差,不使用无偏估计
# 计算控制函数 p(s)
p_s = 1 / (1 + torch.exp(-(x - mean) / torch.sqrt(var + self.epsilon)))
# 应用 Dice 激活函数
output = p_s * x + (1 - p_s) * self.alpha * x
return output
LocalActivationUnit
Attention块计算
#定义激活单元(Activation Unit)
class LocalActivationUnit(nn.Module):
def __init__(self, embedding_dim, hidden_units = [36], dropout = 0.5):
super(LocalActivationUnit, self).__init__()
layers = []
input_dim = embedding_dim *2 * 3 #开始的输入维度为 cate和behavior的cat所以得乘2
for dim in hidden_units:
layers.append(nn.Linear(input_dim, dim))
layers.append(Dice())
layers.append(nn.Dropout(dropout))
input_dim = dim
layers.append(nn.Linear(input_dim, 1))
self.mlp = nn.Sequential(*layers)
def forward(self, behavior_embeds, target_embed, mask):
"""
Args:
behavior_embeds: 历史行为序列嵌入 (batch_size, seq_len, embed_dim)
target_embed: 候选物品嵌入 (batch_size, embed_dim)
mask: 序列填充掩码 (batch_size, seq_len)
Returns:
注意力权重 (batch_size, seq_len)
"""
seq_len = behavior_embeds.size(1)
# 扩展候选物品嵌入以匹配序列长度
target_embed = target_embed.unsqueeze(1).expand(-1, seq_len, - 1) #-1:表示保留该维度的原始大小,seq_len:表示将第 1 维(即新增的维度)扩展到与 behavior_embeds 的序列长度一致。
#拼接行为序列和候选物品嵌入
concat_embeds = torch.cat([behavior_embeds, target_embed, target_embed - behavior_embeds], dim = -1) #最后一个维度进行拼接
#计算每一个序列的权重
scores = self.mlp(concat_embeds).squeeze(-1) #(batch_size, seq_len, 1) 转换为 (batch_size, seq_len)
#应用sigmoid获取权重
weights = torch.sigmoid(scores) #(batch_size, seq_len)
if mask is not None:
weights = weights * mask #(batch_size, seq_len)
return weights
DIN模型
class DIN(nn.Module):
"""
Args:
user_feat_dims: 用户特征维度字典
item_feat_dims: 总物品特征维度字典
context_feat_dims: 上下文特征维度字典, 如果没有上下文特征,可以设置为 None
embedding_dim: 嵌入维度
mlp_dims: MLP隐藏层维度列表
dropout: Dropout概率
Output:
预测结果这里并没有使用sigmoid激活,而是通过softmax输出两个类别的概率
"""
def __init__(self, user_feat_dims, item_feat_dims, context_feat_dims,
embedding_dim=8, mlp_dims=[200, 80], dropout=0.2):
super(DIN, self).__init__()
# 用户特征嵌入层
self.user_embeddings = nn.ModuleDict({
feat: nn.Embedding(dim, embedding_dim)
for feat, dim in user_feat_dims.items()
})
# 物品特征嵌入层
self.item_embeddings = nn.ModuleDict({
feat: nn.Embedding(dim, embedding_dim)
for feat, dim in item_feat_dims.items()
})
# 上下文特征层
self.context_embeddings = None
self.context_embed_size = 0
if context_feat_dims is not None:
self.context_embeddings = nn.ModuleDict({
feat: nn.Embedding(dim, embedding_dim)
for feat, dim in context_feat_dims.items()
})
self.context_embed_size = len(context_feat_dims) * embedding_dim
# 注意力单元层
self.attention = LocalActivationUnit(embedding_dim, hidden_units=[80, 40], dropout=dropout)
# 计算MLP输入维度
user_embed_size = len(user_feat_dims) * embedding_dim
item_embed_size = len(item_feat_dims) * embedding_dim
mlp_input_dim = user_embed_size + item_embed_size + self.context_embed_size + embedding_dim*2 # 最后一个是user_interest维度,因为使用了item_id和cate_id 所以维度是2 * embedding_dim
# MLP层
mlp_layers = []
input_dim = mlp_input_dim
for hidden_dim in mlp_dims:
mlp_layers.append(nn.Linear(input_dim, hidden_dim))
mlp_layers.append(Dice())
mlp_layers.append(nn.Dropout(dropout))
input_dim = hidden_dim
mlp_layers.append(nn.Linear(input_dim, 2))
self.mlp = nn.Sequential(*mlp_layers)
def forward(self, user_features, target_item_features, context_features, hist_behavior_seq, hist_cate_seq):
"""
Args:
user_features: 用户特征字典
target_item_features: 目标物品特征字典
context_features: 上下文特征字典
hist_behavior_seq: 历史行为序列,形状为(batch_size, seq_len)
hist_cate_seq: 历史行为类别列表,形状为(batch_size, seq_len)
Returns:
预测结果
"""
# 生成序列掩码,因为数据中做了对齐,没有数据的部分值已被填为了0
if hist_behavior_seq is not None:
# 确保 hist_behavior_seq 是布尔张量
mask = (hist_behavior_seq != 0).float()
else:
mask = None
# 用户特征嵌入
user_embeds = [
self.user_embeddings[feat](user_features[feat]) for feat in user_features
]
user_embeds = torch.cat(user_embeds, dim=1) # (batch_size, user_embed_size)
# 目标特征嵌入
item_embeds = [
self.item_embeddings[feat](target_item_features[feat]) for feat in target_item_features
]
target_item_embed = torch.cat(item_embeds, dim=1) # (batch_size, item_embed_size)
# 上下文特征嵌入,如果没有上下文特征,就用0填充
context_embed = torch.zeros(len(user_embeds), self.context_embed_size).to(user_embeds.device)
if self.context_embeddings is not None:
context_embeds = [
self.context_embeddings[feat](context_features[feat]) for feat in context_features
]
context_embed = torch.cat(context_embeds, dim=1) # (batch_size, context_embed_size)
# 历史行为序列处理
if len(hist_behavior_seq) > 0 and len(hist_cate_seq) > 0:
hist_behavior_embed = self.item_embeddings['Ordinal_itemID'](hist_behavior_seq) # (batch_size, seq_len, item_embed_size)
hist_cate_embed = self.item_embeddings['Ordinal_cateID'](hist_cate_seq) # (batch_size, seq_len, item_embed_size)
hist_embed = torch.cat([hist_behavior_embed, hist_cate_embed], dim=2) # (batch_size, seq_len, item_embed_size*2)
weights = self.attention(hist_embed, target_item_embed, mask) # (batch_size, seq_len)
user_interest = torch.sum(hist_embed * weights.unsqueeze(-1), dim=1) # (batch_size, item_embed_size * 2)
else:
user_interest = torch.zeros(len(user_embeds), self.item_embeddings['Ordinal_itemID'].embedding_dim).to(user_embeds.device)
# 拼接特征
combined = torch.cat([user_embeds, target_item_embed, context_embed, user_interest], dim=1) # (batch_size, mlp_input_dim)
# 通过MLP获取预测结果
logit = self.mlp(combined).squeeze(-1)
output = torch.softmax(logit, dim=1)
return output
Reference
Guorui Zhou, Chengru Song, Xiaoqiang Zhu, Ying Fan, Han Zhu, Xiao Ma, Yanghui Yan, Junqi Jin, Han Li, and Kun Gai. “Deep Interest Network for Click-Through Rate Prediction.” In Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining, 1-9. ACM, 2018.
王喆《深度学习推荐系统》
推荐系统中的注意力机制——阿里深度兴趣网络(DIN)
DIN模型pytorch代码逐行细讲