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 总结
优点:
- 引入 Attention 机制,更精准的提取用户兴趣;
- 引入 Dice 激活函数与,并优化了稀疏场景中的 L2 正则方式。
缺点:
- 没有考虑用户点击商品的相对位置信息,后续的 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 仓库:
希望看完此文的你,能够有所收获!