推荐算法(九)——阿里经典深度兴趣网络 DIN

1 介绍

本文为 推荐系统专栏 的第九篇文章,也是阿里三部曲 DIN、DIEN、DSIN中的第一篇。内容围绕 DIN 的原理及代码展开,后续会出后两篇的详解。

阿里出品的深度兴趣网络 DIN,通过引入 Attention Layer,赋予用户行为不同的重要性权重,获得更具表达能力的用户兴趣表示。

在这里插入图片描述
论文传送门:Deep Interest Network for Click-Through Rate Prediction

代码传送门:DIN

2 原理

2.1 Base Model

介绍 DIN 之前,先了解一下 Base Model,模型结构如下:

在这里插入图片描述
阿里的推荐系统主要用到四组特征:用户画像特征、用户行为特征、候选商品、上下文特征。本文只需关注用户行为特征如何处理即可。

Base 模型的做法是将用户点击的商品序列,简单的进行 SUM Pooling,然后将聚合得到的 embedding 向量,作为用户的兴趣表示。

这种做法的缺陷也很明显,简单的累加无法突出某些商品的重要性。对于与候选商品具有强关联性的 item,应该给予更大的权重,让其在提取用户兴趣时发挥更大的作用。

2.2 DIN

DIN 便是采用这种方式,引入 Activation Unit 为每个商品计算一个重要性权重,再 Pooling 得到兴趣表示。模型结构如下:

在这里插入图片描述
主要关注 Activation Unit 内的权重计算方式,该单元的输入为:用户点击的商品(Inputs from User)、候选商品(Inputs from Ad)。

计算方式:
1 计算点击的商品与候选商品的外积,得到一维embedding;
2 将外积结果与原始输入 Concat 在一起;
3 后面结两层全连接,隐层激活函数使用 PRelu或Dice,输出映射到一维,表示权重分数。

用户点击的多个商品,分别按照以上方式与候选商品计算权重,然后加权再取 SUM Pooling 即可。这样就突出了重要商品发挥的作用,可提取到更精确的用户兴趣表示。

DIN 最大的创新点就是引入了 Activation Unit,这也是与 Base Model 唯一不同的地方。

除此之外,DIN 也提出了另外两个 Trick:

Data Adaptive Activation Function

在这里插入图片描述上图为激活函数的梯度变化曲线,Dice 激活函数是对 PRelu 的改进。因为 Relu、PRelu 的梯度发生变化的点都固定在 x=0 处,神经网络每层的输出往往具有不同分布,所以固定在一处无法适应多样的分布,所以变化点应随着数据的分布自适应调整。

在这里插入图片描述在这里插入图片描述
Dice 的计算方式如上,s表示当前 batch 的数据,p(s) 的计算可以看做是先对 s 进行标准化,即减去均值、除以方差,得到 e 的指数部分,然后进行 sigmoid 变换得到概率值 p(s)。

f(s) 的计算是利用 p(s) 的值对 s 进行平滑,第二部分需要乘上一个权重 a,该权重随模型学习得到。

Mini-batch Aware Regularization

在这里插入图片描述
对 L2 正则化的改进,在进行 SGD 优化的时候,每个 mini-batch 都只会输入部分训练数据,反向传播只针对部分非零特征参数进行训练,添加上 L2 之后,需要对整个网络的所有参数进行训练,计算量非常大。

所以引入该方法,只对每一个 mini-batch 中不为 0 的参数进行梯度更新,以此降低训练开销。

3 总结

优点:

  1. 引入 Attention 机制,更精准的提取用户兴趣;
  2. 引入 Dice 激活函数与,并优化了稀疏场景中的 L2 正则方式。

缺点:

  1. 没有考虑用户点击商品的相对位置信息,后续的 DIEN 也是针对这点进行了改进。

4 代码实践

Layer 搭建:

import tensorflow as tf
from tensorflow.keras.layers import Layer, Dense, BatchNormalization

class Attention(Layer):
    def __init__(self, hidden_units, activation='prelu'):
        super(Attention, self).__init__()
        self.dense_layer = [Dense(i, activation=activation) for i in hidden_units]
        self.out_layer = Dense(1, activation=None)

    def call(self, inputs, **kwargs):
        # query: [None, k]
        # key:   [None, n, k]
        # value: [None, n, k]
        # mask:  [None, n]
        query, key, value, mask = inputs

        query = tf.expand_dims(query, axis=1)         # [None, 1, k]
        query = tf.tile(query, [1, key.shape[1], 1])  # [None, n, k]

        emb = tf.concat([query, key, query*k], axis=-1)  # [None, n, 4*k]
				
        for layer in self.dense_layer:
            emb = layer(emb)  # 前馈
            
        score = self.out_layer(emb)  # [None, n, 1]
        score = tf.squeeze(score, axis=-1)  # [None, n]

        padding = tf.ones_like(score) * (-2**32 + 1)           # [None, n]
        score = tf.where(tf.equal(mask, 0), padding, score)    # [None, n]

        score = tf.nn.softmax(score)
        output = tf.matmul(tf.expand_dims(score, axis=1), value)  # [None, 1, k]
        output = tf.squeeze(output, axis=1)  # [None, k]
        return output

class Dice(Layer):
    def __init__(self):
        super(Dice, self).__init__()
        self.bn_layer = BatchNormalization()
        self.alpha = self.add_weight(name='alpha', shape=(1,), trainable=True)

    def call(self, inputs, **kwargs):
        x = self.bn_layer(inputs)
        x = tf.nn.sigmoid(x)
        output = x * inputs + (1-x) * self.alpha * inputs
        return output
        

Model 搭建:

from layer import Attention, Dice

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Embedding, Dense, BatchNormalization, Input, PReLU, Dropout
from tensorflow.keras.regularizers import l2

class DIN(Model):
    def __init__(self, feature_columns, behavior_feature_list, att_hidden_units, ffn_hidden_units,
                 att_activation='prelu', ffn_activation='prelu', maxlen=40, dnn_dropout=0.0):
        super(DIN, self).__init__()
        
        self.maxlen = maxlen
        self.dense_feature_columns, self.sparse_feature_columns = feature_columns

        self.other_sparse_num = len(self.sparse_feature_columns) - len(behavior_feature_list)
        self.dense_num = len(self.dense_feature_columns)
        self.behavior_num = len(behavior_feature_list)

        # other sparse embedding
        self.embed_sparse_layers = [Embedding(feat['feat_onehot_dim'], feat['embed_dim'])
                                       for feat in self.sparse_feature_columns
                                           if feat['feat'] not in behavior_feature_list]
                                           
        # behavior embedding layers, item id and category id
        self.embed_seq_layers = [Embedding(feat['feat_onehot_dim'], feat['embed_dim'])
                                    for feat in self.sparse_feature_columns
                                      if feat['feat'] in behavior_feature_list]

        self.att_layer = Attention_Layer(att_hidden_units, att_activation)
        self.bn_layer = BatchNormalization(trainable=True)
        self.dense_layer = [Dense(unit, activation=PReLU() if ffn_activation=='prelu' else Dice())\
             				for unit in ffn_hidden_units]
             				
        self.dropout = Dropout(dnn_dropout)
        self.out_layer = Dense(1, activation=None)

    def call(self, inputs, training=None):
        # dense_inputs:  empty/(None, dense_num)
        # sparse_inputs: empty/(None, other_sparse_num)
        # history_seq:  (None, n, k)
        # candidate_item: (None, k)
        dense_inputs, sparse_inputs, history_seq, candidate_item = inputs

        # dense & sparse inputs embedding
        other_feat = tf.concat(
        	[layer(sparse_inputs[:, i]) for i, layer in enumerate(self.embed_sparse_layers)],
            axis=-1
        )
        other_feat = tf.concat([other_feat, dense_inputs], axis=-1)

        # history_seq & candidate_item embedding
        seq_embed = tf.concat(  # (None, n, k)
        	[layer(history_seq[:, :, i]) for i, layer in enumerate(self.embed_seq_layers)],
        	axis=-1
        )   
        item_embed = tf.concat(  # (None, k)
        	[layer(candidate_item[:, i]) for i, layer in enumerate(self.embed_seq_layers)],
        	axis=-1
        )   

        # one_hot之后第一维是1的token,为填充的0,需要mask
        mask = tf.cast(tf.not_equal(history_seq[:, :, 0], 0), dtype=tf.float32)   # (None, n)
        att_emb = self.attention_layer([item_embed, seq_embed, seq_embed, mask])  # (None, k)

        # 若其他特征不为empty
        if self.dense_len>0 or self.other_sparse_len>0:
            emb = tf.concat([att_emb, item_embed, other_feat], axis=-1)
        else:
            emb = tf.concat([att_emb, item_embed], axis=-1)

        emb = self.bn_layer(emb)
        for layer in self.dense_layer:
            emb = layer(emb)

        emb = self.dropout(emb)
        output = self.out_layer(emb)
        return tf.nn.sigmoid(output)  # (None, 1)
        

写在最后

下一篇预告: 推荐算法(十)——阿里深度兴趣进化网络 DIEN

推荐算法Github 仓库:

Recommend-System-tf2.0

希望看完此文的你,能够有所收获!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值