目录

  • 背景
  • DIN
    • 预处理
    • Activation Unit
    • DNN
  • 论文其他知识点
    • Dice
    • mini-batch aware regularization
    • RelaImpr
  • QA
  • Pytorch实现
    • 模型实现
    • 代码实现
    • 实验
      • 实验参数
      • 实验结果
      • 实验结果可视化
      • 实验结果分析
      • 模型统计性描述
  • 思考题
  • 写在最后
  • 参考资料

背景

 在上文中,我介绍了NPM模型,提到NPM模型通过充分进行二阶特征的合成,从而可以更好地帮助上层的DNN去构建更高阶的特征和非线性的特征和减轻DNN的负担,集成高阶,低阶,非线性的特征,模型可以更准确地得到ctr的估计率。

 但是包括NPM及之前介绍的所有的模型,对所有的用户都一视同仁,即没有针对具体用户考虑用户的喜好,只是单纯地使用一些标注的信息如当前商品的信息,当前设备信息,时间,地点等。如果只是考虑这些信息的话,推荐系统的广告推荐其实无法做到个性化,所以之后的模型就将用户的历史数据考虑在内,如用户的历史行为特征,但这些模型多是简单的将用户的历史行为特征进行向量化之后再通过一个简单的pooling层如sum或者average进行简单的池化。下面是这种模型的结构图:

 这样虽然可以得到用户的历史行为信息,但是因为池化的原因,所以会信息混合而使信息部分缺失或者说弱化信息。此外,也不是所有的历史信息我们都需要。就比如说我是一个厨师,如果我因为一次帮其他人购买机箱而推荐系统经常推荐IT产品给我,那我其实并不会倾向于去购买这些IT产品,但是如果给我推荐一些厨房用品,我则可能更乐意去点击甚至是购买。那么说到这,就能发现这其实就是大家的一种局部性法则,即更倾向于关注自己更喜欢的一部分,那么推荐系统也该如此。推荐系统不应该对所有的历史行为一概而谈,而是应该根据当前的商品的特征,再结合用户的局部历史行为特征,从而帮助广告的rank和推荐,所以就有了今天我要谈论的DIN模型。

DIN

 DIN模型是阿里在18年提出的推荐模型,该模型一改之前模型不考虑用户历史行为的不足,引入用户历史行为特征,通过结合用户历史行为特征,来判断当前商品于历史行为的相似度,从而更好地帮助预测商品的点击率。而DIN模型通过引入LocalActivationUnit来聚焦于局部的历史行为,从而计算当前商品与局部历史行为的相关性,从而得到历史行为的特征向量,再与其他稀疏特征对应的embedding向量和数值特征相连结,最后利用DNN得到最终结果。模型的基本结构图如下:

 下面对整个流程进行解读:

预处理

 对于一般的模型而言,我们使用稀疏特征和数值型特征,然后将稀疏特征通过embedding进行表示,最终和数值型特征进行连结,最后送入DNN中。但是在DIN中,我们考虑用户历史行为特征,需要注意用户历史行为特征可能不止有一组,比如有历史购买商品的列表,也有用户历史点击商品的列表,此外,每个特征组列表中的特征数是不定长的,比如某人A历史购买商品可能有{苹果,梨子,可乐},而某人B历史购买商品可能有{奥利奥,C primer plus,西瓜书,南瓜书},那么很明显,A和B的历史购买物品列表的长度是不定的,但是我们输入进网络的向量要求维度相同,所以此处需要进行padding补全,所以在这里需要对A的历史购买商品列表进行补全,将其补全为{苹果,梨子,可乐,pad},那么这样我们将历史购买物品转换为embedding向量时,就可以得到两个维数相同的4×emb_len的矩阵。但是要注意,因为这里的pad是我们之后补上来的,只起到填充的作用,那么在判断历史商品与当前商品的关系时,我们需要将计算出的padding的影响去除,所以需要添加MASK来表明哪些是padding而哪些不是padding。

 因为不定长的特征组仍是一个特征,但是因其取值有多个,所以在编码的时候,就无法采取one-hot编码,而是采取multi-hot编码。所以不同于one-hot编码经过embedding之后会得到一条embedding向量,multi-hot编码经过embedding之后会得到多条向量。对应到我上面举的A,B的例子就是4条embedding向量。

对于one-hot编码,得到的向量就如[1, 0, 0, 0], 但是对于multi-hot编码,得到的向量就如[1, 1, 0, 0],即一条向量中可以出现不止一个1。

Activation Unit

 那么此时得到了特征组对应的embedding组表示,DIN如何去计算当前商品与历史购物之间的关联性呢?DIN提出了Activation Unit结构。

 Activation Unit以历史购物特征组对应的embedding和当前商品的embedding表示作为输入,利用DNN来计算每个历史商品与当前商品的关联性。这个关联性在这里其实就是历史商品embedding相加时的权重。即设历史购买商品这个特征组最终的特征表示为f(\mathbf{x}),历史购买商品列表为[a_1, a_2, \cdots, a_N],历史购买商品对应的embedding为E_{a_i},每个embedding的权重为\alpha_i,那么有:

f(\mathbf{x}) = \sum_{i=1}^N \alpha_iE_{a_i} \qquad i=1,2,\cdots,N

(上式就是pooling的过程)

 在这里,Activation Unit得到的就是最终的\alpha_i

 Activation Unit将输入历史购买商品的Embedding和当前商品的embedding做外积之后与两个输入进行拼接,传入DNN中,最终输出每个历史购买商品与当前商品的关联性,即权重\alpha_i。体现在代码中则是把当前商品的embedding扩充到历史购买商品的Embedding同样大小,再求出两者的差和元素积,最终在最后一个维度上进行连结,最终送入DNN。下面是Activation Unit的结构图:

论文在这里提出一点,即所有的权重加起来的和并不是1,原因是这样更加可以体现用户的偏爱程度。

特别注意,一个历史行为特征组和一个当前行为对应,即是一对一的关系,比如历史购买商品和当前商品之间一一对应,历史用户id与当前用户id之间一一对应。

DNN

 在Activation Unit中得到历史特征组对应的embeddings之后(这里使用embeddings是因为历史特征组可能不止有一个),与稀疏特征对应的embedding和数值型特征进行连结,最终通过DNN输入ctr的点击率。

论文其他知识点

Dice

 Dice激活函数可以基于数据的分布进行动态的调整的,其是PRelu激活函数的一种改进,PRelu函数数学表达形式如下:

f(s) =
\begin{cases}
s \qquad if \ s > 0\\\alpha s \quad \ if \ s \leq 0
\end{cases}
= p(s) \cdot s + (1 – p(s)) \cdot \alpha s

 其中,对于PRelu的p(s)而言,其函数图像如下:

 而这里在x=0处,体现为硬矫正的特性,而Dice函数的型式正好可以解决这个问题。Dice函数的f(x)和PRelu相同,而p(s)如下:

p(s) = \frac{1}{1+e^{-\frac{s-E[s]}{\sqrt{Var[s]+\epsilon}}}}

p(s)的函数图像如下:

 其中E[s]Var[s]为一个batch的均值和方差,由于考虑整个batch,所以Dice可以根据样本数据的分布进行自适应。

mini-batch aware regularization

 论文中指出,在大数据稀疏特征输入的情况下进行L2正则化会带来巨大的资源消耗也会极大地增加。原因在于数据的稀疏性,模型的参数数量就会巨大,每次对所有的参数进行更新就需要计算资源和时间,基于此,作者提出了mini-batch aware regularization,其主旨在于用每个batch进行参数的更新时,只更新在batch中特征的值不为0的特征的参数。

 mini-batch aware regularization的惩罚项的表达式为:

L_2(w) = \mid\mid W \mid\mid _2^2 = \sum_{j=1}^K||w_j||^2_2 = \sum_{(\mathbf{x},y)\in S}\sum_{j=1}^K\frac{\mathbf{I}(\mathbf{x_j}\neq 0)}{\mathbf{n}_j}||w_j||^2_2

 在这里体现为S中每个样本中每个特征不为0时,这个参数的惩罚才被算进总惩罚项中,其中S代表所有样本的总集合,如果为一个小batch的话,设batch为S,则公式变为:

L_2(w) = \sum_{i=1}^B\sum_{j=1}^K\sum_{(\mathbf{x},y)\in S}\frac{\mathbf{I}(\mathbf{x_j}\neq 0)}{\mathbf{n}_j}||w_j||^2_2

emsp;在论文中做一个模糊处理,即只要一个batch中任意一个样本的某个特征的值不为0,就这整个batch都更新这个特征的参数,形式如下:

L_2(w) \approx \sum_{i=1}^B\sum_{j=1}^K\frac{\alpha_{mj}}{\mathbf{n}_j}||w_j||^2_2

 其中\alpha_{mj}=max_{(x,y)\in B_m}\mathbf{I}(x_j\neq 0),表示只要有一个样本的这个特征的值为1,\alpha_{mj}就为1。

Relaimpr

 在效果评估上,论文提出了RelaImpr的概念,在我个人看来其实是一种相对提升的指标,从数学形式上就可见一斑

RelaImpr = (\frac{AUC(current_model)-0.5}{AUC(base_model) – 0.5} – 1) × 100%

 其中AUC(current_model)是指当前模型的AUC得分,AUC(base_model)是基础模型的AUC得分。之所以减去一个0.5是因为考虑到当分类器随机分类是,AUC得分为0.5,所以在这里减去0.5。

QA

 参考CTR论文笔记[5]:Deep Interest Network并结合本人理解给出两个QA。

 Q1:为什么论文中在Activation Unit中要将两个输入求差和求外积?

 Answer:在这里引入外积的目的是为了特征交叉,对于ReLU而言,其无法实现特征的交叉,即非线性的变化,而sigmoid函数也无法带来明显的特征交叉(参考sigmoid的函数的形式),那么利用外积就可以进行特征的混合交叉,从而利用DNN最终输出得到权重。而求差的目的是为了提供更多信息。

 Q2:为什么论文里认为权重不适用softmax做归一化?

 Answer:softmax的目的是为了扩大影响,即大的越大,小的越小,但是在DIN中,我们仍希望保留历史行为特征的影响力比例,不希望经过softmax之后某个历史行为的影响力由10%变为0.1%这种情况,这种情况会导致用户推荐的范围变窄。

Pytorch实现

 尝试使用pytorch进行复现,全部代码可在我的GitHub找到,点此进入我的GitHub

模型实现

  1. Activation Unit:内含DNN网络,在forward函数中将queries,keys,
    queries-keys,queries \odot keys在最后一个维度上进行连结,最终送入DNN中进行输出。

  2. Dice:初始化一个参数alpha,先进行batchnorm操作,再利用torch自带的Sigmoid按照公式计算forward输出即可

代码实现

import torch
import torch.nn as nn

class Dice(nn.Module):
    def __init__(self, feature_num):
        super(Dice, self).__init__()
        self.alpha = nn.Parameter(torch.ones(1))
        self.feature_num = feature_num
        self.bn = nn.BatchNorm1d(self.feature_num, affine=False)
        self.sigmoid = nn.Sigmoid()
    def forward(self, x):
        x_norm = self.bn(x)
        x_p = self.sigmoid(x_norm)

        return self.alpha * (1.0-x_p) * x + x_p * x



class LocalActivationUnit(nn.Module):
    def __init__(self, len_num, emb_len=8):
        super(LocalActivationUnit, self).__init__()
        self.linear1 = nn.Linear(4*emb_len, 256)
        self.linear2 = nn.Linear(256, 128)
        self.linear3 = nn.Linear(128, 64)
        self.linear4 = nn.Linear(64, 1)
        self.dice1 = Dice(len_num)
        self.dice2 = Dice(len_num)
        self.dice3 = Dice(len_num)


    def forward(self, keys, query): # query(B, len, emb_len), key(B, 1, emb_len)
        queries = query.repeat(1, keys.shape[1], 1)
        output = torch.cat([queries, keys, queries-keys, queries*keys], dim=-1) # (B, len, emb_len*4)
        output = self.dice1(self.linear1(output))
        output = self.dice2(self.linear2(output))
        output = self.dice3(self.linear3(output))
        output = self.linear4(output) # (B, len ,1)

        return output.squeeze(-1)


class AttentionPoolingLayer(nn.Module):
    def __init__(self, len_num, emb_len):
        super(AttentionPoolingLayer, self).__init__()
        self.local_activation_unit = LocalActivationUnit(len_num, emb_len)

    def forward(self, keys, query): # query(B, 1, emb_len), keys(B, len, emb_len)
        query = query.unsqueeze(1)
        key_mask = torch.not_equal(keys[:, :, 0], 0) # (B, len)
        attention_score = self.local_activation_unit(keys, query) # (B, len)
        paddings = torch.zeros_like(attention_score)
        outputs = torch.where(key_mask, attention_score, paddings) # (B, len)
        outputs = outputs.unsqueeze(dim=1) # (B, 1, len)

        outputs = torch.matmul(outputs, keys) # (B, 1, emb_len)
        outputs = outputs.squeeze(dim=1) # (B, emb_len)
        return outputs




class DIN(nn.Module):
    def __init__(self, sparse_cols, dense_cols, varlen_cols, behavior_list, behavior_hist_list, tuple_list, emb_len=8):
        super(DIN, self).__init__()
        self.sparse_cols = sparse_cols
        self.dense_cols = dense_cols
        self.varlen_cols = varlen_cols
        self.sparse_col_len = len(self.sparse_cols)
        self.dense_col_len = len(self.dense_cols)
        self.varlen_col_len = len(self.varlen_cols)
        self.sparse_tuple_list = tuple_list[0]
        self.varlen_tuple_list = tuple_list[1]
        self.behavior_list = behavior_list
        self.behavior_hist_list = behavior_hist_list
        self.emb_len = emb_len

        self.attention_pool_layer = AttentionPoolingLayer(self.varlen_col_len, self.emb_len)

        self.linear1 = nn.Linear(self.dense_col_len+self.emb_len*self.sparse_col_len+self.emb_len, 200)
        self.linear2 = nn.Linear(200, 80)
        self.linear3 = nn.Linear(80, 1)
        self.sigmoid = nn.Sigmoid()
        self.dice1 = Dice(200)
        self.dice2 = Dice(80)
        self.dropout1 = nn.Dropout(0.5)
        self.dropout2 = nn.Dropout(0.3)

        # sparse features embedding
        self.sparse_embeddings = nn.ModuleList()
        self.varlen_embeddings = nn.ModuleList()
        for fc in self.sparse_tuple_list:
            self.sparse_embeddings.append(nn.Embedding(fc.vocab_size, fc.emb_len))
        for fc in self.varlen_tuple_list:
            self.varlen_embeddings.append(nn.Embedding(fc.vocab_size+1, fc.emb_len, padding_idx=0))


    def forward(self, x):
        sparse_x = x[:, :self.sparse_col_len]
        varlen_x = x[:, self.sparse_col_len:self.sparse_col_len+self.varlen_col_len]
        dense_x = x[:, self.sparse_col_len+self.varlen_col_len:] # (B, dense_col_len)

        # sparse embedding
        sparse_embedding_input = self.sparse_embeddings[0](sparse_x[:, 0].long())
        for i in range(1, self.sparse_col_len):
            sparse_embedding_input = torch.cat([sparse_embedding_input, self.sparse_embeddings[i](sparse_x[:, i].long())], dim=1) # (B, emb_len*sparse_col_len)

        # varlen embedding
        varlen_embedding_input = self.varlen_embeddings[0](varlen_x[:, :].long()) # (B, varlen_feature[0]_num, emb_len))
        varlen_input = self.attention_pool_layer(varlen_embedding_input, self.sparse_embeddings[3](sparse_x[:, 3].long())) # movie_id是第3列, varlen_intput:(B, emb_len)

        final_input = torch.cat([dense_x, sparse_embedding_input, varlen_input], dim=1)

        final_output = self.dropout1(self.dice1(self.linear1(final_input)))
        final_output = self.dropout2(self.dice2(self.linear2(final_output)))
        final_output = self.sigmoid(self.linear3(final_output))

        final_output = torch.cat([1-final_output, final_output], dim=1)
        return final_output

实验

实验参数

参数
epochs 400
optimizer Adam
lr 0.00005
loss_fn CrossEntropyLoss
metric Accuracy
batch_size 64

实验结果

epoch 1 loss 0.009938 train acc 0.673913
epoch 2 loss 0.008233 train acc 0.820290
epoch 3 loss 0.007977 train acc 0.828986
epoch 4 loss 0.007947 train acc 0.828261
epoch 5 loss 0.007866 train acc 0.832609
...
epoch 396 loss 0.007583 train acc 0.838406
epoch 397 loss 0.007574 train acc 0.838406
epoch 398 loss 0.007540 train acc 0.838406
epoch 399 loss 0.007602 train acc 0.838406
epoch 400 loss 0.007588 train acc 0.837681

实验结果可视化


准确率迭代图


损失迭代图

实验结果分析

 总得来说模型损失是下降的而准确率是上升的,但观察loss下降的速度,发现损失下降较快,应该是发生了过拟合,可以增加dropout的概率来缓解这种情况。

模型描述性统计

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
         Embedding-1                    [-1, 8]              32
         Embedding-2                    [-1, 8]              24
         Embedding-3                    [-1, 8]              32
         Embedding-4                    [-1, 8]           1,672
         Embedding-5                    [-1, 8]              80
         Embedding-6                [-1, 50, 8]           1,680
         Embedding-7                    [-1, 8]           1,672
            Linear-8              [-1, 50, 256]           8,448
       BatchNorm1d-9              [-1, 50, 256]               0
          Sigmoid-10              [-1, 50, 256]               0
             Dice-11              [-1, 50, 256]               0
           Linear-12              [-1, 50, 128]          32,896
      BatchNorm1d-13              [-1, 50, 128]               0
          Sigmoid-14              [-1, 50, 128]               0
             Dice-15              [-1, 50, 128]               0
           Linear-16               [-1, 50, 64]           8,256
      BatchNorm1d-17               [-1, 50, 64]               0
          Sigmoid-18               [-1, 50, 64]               0
             Dice-19               [-1, 50, 64]               0
           Linear-20                [-1, 50, 1]              65
LocalActivationUnit-21                   [-1, 50]               0
AttentionPoolingLayer-22                    [-1, 8]               0
           Linear-23                  [-1, 200]          10,000
      BatchNorm1d-24                  [-1, 200]               0
          Sigmoid-25                  [-1, 200]               0
             Dice-26                  [-1, 200]               0
          Dropout-27                  [-1, 200]               0
           Linear-28                   [-1, 80]          16,080
      BatchNorm1d-29                   [-1, 80]               0
          Sigmoid-30                   [-1, 80]               0
             Dice-31                   [-1, 80]               0
          Dropout-32                   [-1, 80]               0
           Linear-33                    [-1, 1]              81
          Sigmoid-34                    [-1, 1]               0
================================================================
Total params: 81,018
Trainable params: 81,018
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.70
Params size (MB): 0.31
Estimated Total Size (MB): 1.01
----------------------------------------------------------------

思考题

 在实际应用中,模型的输入会包括:

  • 用户的画像特征,例如性别、年龄、学历等;
  • 用户的行为序列数据,例如点击商品的行为序列、购买商品的行为序列;
  • 候选商品的画像特征,例如品类、品牌等;
  • 上下文特征,例如设备终端、时间、地点等;

 DIN模型的不足:用户的兴趣一直在不断的变化,而DIN并没有能捕捉到用户不断变化的兴趣,而是简单通过用户过去的历史购买记录进行推荐,而不能针对不断变化的用户兴趣做真正的“下一次”推荐。总结而言就是DIN忽略了序列信息,没有挖掘出真正的依赖关系。(参考自AI上推荐 之 DIEN模型(序列模型与推荐系统的花火碰撞)

 举个例子,当某个用户过去很长的一段时间经常购买零食,但是最近没有买零食了,就算我们这个时候机智地为就近时间段赋予更高的权重,但是如果过去购买的零食过多或者“最近”不购买零食的时间过短,此时DIN从全部的购买历史考量,会有较大几率推荐零食。

写在最后

 在这里写一些参与此处datawhale deep recommendation system活动的总结。

 在本次学习的过程中,我渐渐产生了一个疑问“为什么这些模型,在平时做二分类问题的时候没有被经常使用呢?”。一直很疑惑这个问题,直到我一次在看资料时,注意到稀疏特征的字眼(当然,这个稀疏特征的点一直在各种资料里被提到,但是我一直没有注意到),我才恍然大悟。推荐系统最大的特点之一就是其核心数据多为类别型数据且多为稀疏的特征,基于巨量稀疏特征的情况,才开发出这么多模型,而同样的,这些模型的核心就在于处理稀疏特征,进行稀疏特征的表示,稀疏特征之间的交互等等。这让我意识到这应该就是工程中具体问题具体讨论的思路吧。所以说做比赛的话,其实比赛不仅仅是一个磨练机器学习/深度学习技巧的地方,而是提供机会去近距离接触实际工程,实际业务的机会,因为比赛提供的背景其实都是真正生活中要处理的问题,当做这些比赛的时候,如果切实地从比赛背后的问题出发,才能更好地去匹配上问题,去解决问题。做特征工程也是这种思想,特征工程从具体的背景出发。

 比如像kaggle的泰坦尼克号的训练赛,在做特征工程的时候会从人名出发,从人名中挖掘出姓氏,挖掘出性别,挖掘出阶级。而这些特征,其实就很可能会影响泰坦尼克号中不同人的生还。(其他的例子我就不举了🤣)

 这种具体问题具体考虑的做法是我在本次活动中逐渐认识到的一点。此外,本次活动,因为博客会打分,所以我在写博客的时候就很认真,不希望自己比其他人差,所以在每次得分出来后,我就积极学习优秀博客,不断改善自己的博客。也逐渐形成了自己的博客的风格,即先介绍概念(完全靠自己的理解),再介绍一下小知识点和问题,最后实现模型的复现,给出实验结果(只有实验结果才能证明有效性)。我觉得养成一种写博客的习惯还蛮不错的,包括写博客的习惯和风格,都是自己的一种进步,能看到自己的进步(比如从写的东西的质量和数量上)还是蛮高兴的。

 最后感谢datawhale组织提供了这次学习机会,拥抱开源,热爱开源。

参考资料

  1. datawhale深度推荐系统
  2. AI上推荐 之 AFM与DIN模型(当推荐系统遇上了注意力机制)
  3. AI上推荐 之 DIEN模型(序列模型与推荐系统的花火碰撞)
  4. 阿里巴巴DIN模型详解
  5. CTR论文笔记[5]:Deep Interest Network
  6. DIN论文

0 条评论

发表评论