假设目标物品为“衬衫”,同时用户的历史行为是“衣着”、“数码”、“水果”等多个类型交叉的序列。依据个人生活经验,决定“衬衫”是否符合用户兴趣或者应该推荐怎样的“衬衫”,大概率来自用户“衣着”相关行为中的信息,因此在处理用户行为序列时,应该对“衣着”相关信息赋予更高的权重,这就是DIN算法做的事情。
传统处理用户行为序列时的方式为”一视同仁“,即将行为序列中商品的embedding表示进行Sum Pooling操作,这样不能很好的区分行为序列中不同类型的行为对目标物品的影响。
关键知识点
DIN算法中增加Activation Unit来得到注意力得分,从而对不同的行为给予不同的权重;
Activation Unit 的理解
实现功能:输入候选商品和行为序列商品的embedding表示,经过处理后输出注意力得分;如图;
假设候选商品为”短裙A“,行为序列中”长裙X“和”书C“的注意力得分中”长裙X“会得到更高的分数;
Inputs from User:用户行为序列中Goods N的embedding表示;
Inputs from Ad:候选Ad的embedding表示;
Out Product Layer:计算点击商品和候选商品的embedding外积,得到一维embedding表示;
PRelu / Dice:隐层的激活函数使用PRelu或者Dice,如下图所示;
整体算法逻辑理解
阿里的推荐系统主要用到四组特征:用户画像特征、用户行为特征、候选商品、上下文特征。本文只需关注用户行为特征如何处理即可;
Base Model
上图中Base Model中重点关注User Behaviors即可,其处理方法是将用户点击的商品序列进行简单的SUM Pooling,将其聚合得到Embedding向量作为用户的兴趣表示;
上述做法的缺陷是在于简单的累加无法突出某些商品的重要性。通过个人经验判断,对于与候选商品具有强关联性的items,应该赋予更大的权重;
Deep Interest Network
上图中在SUM Pooling之前,引入了Activation Unit为每个商品计算一个重要性权重,再与商品Embedding 表示相乘后进行SUM Pooling。
Activation Unit
Inputs from Ad:候选Ad的embedding表示;
Inputs from User:用户行为序列中Goods N的embedding表示;
Out Product Layer:计算点击商品和候选商品的embedding外积,得到一维embedding表示;
PRelu / Dice:隐层的激活函数使用PRelu或者Dice,如下图所示;
计算方式:
- 计算点击商品和候选商品的外积,得到Embeding表示;
- 将外积Embedding表示和原输入中点击商品、候选商品Embedding表示进行Concat;
- 后两层为全连接,且隐层激活函数使用PRelu或Dice,输出映射到一维,表示为权重分数;
Data Adaptive Activation Function
上图中Dice激活函数是对PReLU函数的改进,因为ReLU、PReLU的梯度变化都固定在x=0处,但神经网络每层的输入往往具有不同的分布,即固定一处无法适应多样的分布,所以变化点应该随数据的分布自适应的进行调整;计算公式如下:
Dice 的计算方式如上, 表示当前batch的数据
的计算可以看作是先对 进行标准化,即减去均值,除以方差,得到 的指数部分,然后进行 变换得到概率值 ;
的计算是利用 的值对 进行平滑,第二部分需乘上一个权重 ,该权重随模型学习得;
相关细节
- 上图 Model 中使用了 Goods ID、Shop ID、Cate ID三者的 Embedding 的 Concat 结果。而不是直接使用 Goods ID。原因是使用 Goods ID 通过点积计算相关度效果不佳,因此尝试更粗粒度的 Shop ID、Cate ID,使得其既有泛化性又有特异性(可尝试多种粗粒度);
- 注意在使用Goods ID、Shop ID、Cate ID 时,Goods ID 和对应的 Goods ID发生作用,并不是混合在一起作用
- Attention 相关运算的算子选择:除了以上使用的余弦函数,还可以点乘等操作,具体需要考虑学习充分度以及梯度弥散等问题;
- Attention Score 是否进行归一化:在阿里中实际应用中,归一化后效果并没有差,且可以让向量更加稳定,便于学习。实际应用中可以尝试后判断是否归一化;
- 对L2正则化的改进,只对每一个Mini-batch中不为0的参数进行梯度更新,以此来降低训练开销;因为在进行SGD优化时,每个Mini-batch都只会输入部分训练数据,反向传播也只针对部分非零特征参数进行训练,但是添加L2后,需要整个网络的所有参数进行训练,计算量会很大,所以引入上述L2正则化的改进来降低开销;
优缺点
优点
- 引入Attention机制,可以更精准的提取用户兴趣;
- 引入Dice激活函数,适应于不同的数据分布;
- 优化稀疏场景中的L2正则方式,降低训练开销;
缺点
- 没有考虑用户点击商品的相对位置,后续的DIEN也是针对这点进行改进;
代码实现
class Attention(Layer): def __init__(self, hidden_units, activation='prelu'): """ :param hidden_units: 隐层 :param activation: 激活函数(prelu、dice);Dice需要自主实现 """ super(Attention, self).__init__() self.hidden_units = hidden_units if activation == 'dice': self.activation_layer = [Dice() for _ in range(len(self.hidden_units))] elif activation == 'prelu': self.activation_layer = [Dense(i, activation=PReLU()) for i in hidden_units] self.out_layer = Dense(1, activation=None) def call(self, inputs, *args, **kwargs): """ query: [None, k]; item embedding layer key: [None, n ,k]; seq embedding layer value: [None, n, k] mask: [None, n] """ query, key, value, mask = inputs # expand_dims 用于增加维度 query = tf.expand_dims(query, axis=1) # [None, 1, k] # tile对应维度复制几次 query = tf.tile(query, [1, key.shape[1], 1]) # [None, n, k] """ e.g tf.tile([1,2], [2]) = [1,2,1,2] tf.tile([[1,2][3,4]], [2,3]) = [[1,2,1,2,1,2], [3,4,3,4,3,4], [1,2,1,2,1,2], [3,4,3,4,3,4]] """ # 激活单元中连接层操作,包括原输入、元素乘、元素减操作进行concat emb = tf.concat([query, key, query - key, query * key], axis=-1) # [None, n, 4*k] for i in range(len(self.hidden_units)): emb = self.activation_layer[i](emb) score = self.out_layer(emb) # [None, n, 1] score = tf.squeeze(score, axis=-1) # 移除大小为1的维度 [None, n] padding = tf.ones_like(score) * (-2 ** 32 + 1) # [None, n] # tf.where: return if tf.equal(mask, 0) padding else score score = tf.where(tf.equal(mask, 0), padding, score) # [None, n] score = tf.nn.softmax(score) # [None, n] output = tf.matmul(tf.expand_dims(score, axis=1), value) # [None, 1, k] output = tf.squeeze(output, axis=1) # [None, k] return output
class DIN(Model): def __init__(self, feature_columns, behavior_feature_list, att_hidden_units=(80, 40), dnn_hidden_units=(256, 128, 64), att_attention='prelu', dnn_activation='prelu', dnn_dropout=0.0): """ :param feature_columns: feature columns dict :param behavior_feature_list: 行为特征的列表 :param att_hidden_units: activation units 隐层单元数 :param dnn_hidden_units: dnn层的隐层单元数 :param att_attention: 隐层的激活函数 :param dnn_activation: dnn的激活函数 :param dnn_dropout: dropout参数 """ super(DIN, self).__init__() # dense feature & sparse feature self.dense_feature_columns, self.sparse_feature_columns = feature_columns self.behavior_feature_list = behavior_feature_list # 除去behavior feature的数量记录 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 list 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] # activation layer self.att_layer = Attention(att_hidden_units, att_attention) self.bn_layer = BatchNormalization(trainable=True) self.dense_layer = [Dense(unit, activation=PReLU() if dnn_activation == 'prelu' else Dice()) for unit in dnn_hidden_units] self.dropout = Dropout(dnn_dropout) self.out_layer = Dense(1, activation=None) def call(self, inputs, training=None, mask=None): """ dense_inputs: empty / (None, dense_num) sparse_inputs: empty / (None, other_sparse_num) history_seq: (None, n, k); n=len(seq); k=embed_dim candidate_item: (None, k); k=embed_dim """ # inputs >> concat other features embedding dense_inputs = tf.concat([inputs[feat['feat']] for feat in self.dense_feature_columns], axis=-1) sparse_inputs = tf.concat([inputs[feat['feat']] for feat in self.sparse_feature_columns if feat['feat'] not in self.behavior_feature_list], axis=-1) 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) # dense & sparse inputs embedding concat # behaviors input >> attention embedding history_seq = tf.transpose([inputs[feat['feat']] for feat in self.sparse_feature_columns if feat['feat'] in self.behavior_feature_list], [1, 2, 0]) candidate_item = tf.stack(inputs['movie_id']) seq_embed = tf.concat([layer(history_seq[:, :, i]) for i, layer in enumerate(self.embed_seq_layers)], axis=-1) # (None, n, k) item_embed = tf.concat([layer(candidate_item[:, i]) for i, layer in enumerate(self.embed_seq_layers)], axis=-1) # (None, k) mask = tf.cast(tf.not_equal(history_seq[:, :, 0], 0), dtype=tf.float32) # Mask操作,用来遮盖序列中空值 att_emb = self.att_layer([item_embed, seq_embed, seq_embed, mask]) # all embedding concat if self.dense_num > 0 or self.other_sparse_num > 0: # 若其它特征不为empty 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) if __name__ == '__main__': behavior_features = ['movies_seq'] feature_dict, (X_train, y_train), (X_test, y_test) = create_movies_dataset(0.3, 10) features = X_train.columns X = {feat: list(X_train[feat].values) for feat in features} model = DIN(feature_dict, behavior_features) model = compile_fit(model, X, y_train) tmp = {} for feat in features: if feat in behavior_features: tmp[feat] = list(X_test[feat].values) else: tmp[feat] = [[i] for i in X_test[feat]] pre = model(tmp) pre = [1 if x > 0.5 else 0 for x in pre] print('AUC: ', accuracy_score(y_test, pre))