AFM AFM提出的动机 AFM的全称是Attentional Factorization Machines, 从模型的名称上来看是在FM的基础上加上了注意力机制,FM是通过特征隐向量的内积来对交叉特征进行建模,从公式中可以看出所有的交叉特征都具有相同的权重也就是1,没有考虑到不同的交叉特征的重要性程度: $$ y{fm} = w0+\sum{i=1}^nwixi+\sum{i=1}^{n}\sum{i+1}^n\lt vi,vj\gt xixj $$
AFM的全称是Attentional Factorization Machines, 从模型的名称上来看是在FM的基础上加上了注意力机制,FM是通过特征隐向量的内积来对交叉特征进行建模,从公式中可以看出所有的交叉特征都具有相同的权重也就是1,没有考虑到不同的交叉特征的重要性程度:
如何让不同的交叉特征具有不同的重要性就是AFM核心的贡献,在谈论AFM交叉特征注意力之前,对于FM交叉特征部分的改进还有FFM,其是考虑到了对于不同的其他特征,某个指定特征的隐向量应该是不同的(相比于FM对于所有的特征只有一个隐向量,FFM对于一个特征有多个不同的隐向量)。
FM二阶交叉项:所有非零特征对应的隐向量两两点积再求和,输出的是一个数值
AFM二阶交叉项(无attention):所有非零特征对应的隐向量两两对应元素乘积,然后再向量求和,输出的还是一个向量。
上述写法是为了更好的与FM进行对比,下面将公式变形方便与原论文中保持一致。首先是特征的隐向量。从上图中可以看出,作者对数值特征也对应了一个隐向量,不同的数值乘以对应的隐向量就可以得到不同的隐向量,相对于onehot编码的特征乘以1还是其本身(并没有什么变化),其实就是为了将公式进行统一。虽然论文中给出了对数值特征定义隐向量,但是在作者的代码中并没有发现有对数值特征进行embedding的过程(原论文代码链接)具体原因不详。
按照论文的意思,特征的embedding可以表示为:\varepsilon = {v_ix_i},经过Pair-wise Interaction Layer输出可得:
R_x表示的是有效特征集合。此时的f_{PI}(\varepsilon)表示的是一个向量集合,所以需要先将这些向量集合聚合成一个向量,然后在转换成一个数值:
上式中的求和部分就是将向量集合聚合成一个维度与隐向量维度相同的向量,通过向量p再将其转换成一个数值,b表示的是偏置。
从开始介绍Pair-wise Interaction Layer到现在解决的一个问题是,如何将使用哈达玛积得到的交叉特征转换成一个最终输出需要的数值,到目前为止交叉特征之间的注意力权重还没有出现。在没有详细介绍注意力之前先感性的认识一下如果现在已经有了每个交叉特征的注意力权重,那么交叉特征的输出可以表示为:
就是在交叉特征得到的新向量前面乘以一个注意力权重\alpha_{ij}, 那么这个注意力权重如何计算得到呢?
对于神经网络注意力相关的基础知识大家可以去看一下邱锡鹏老师的《神经网络与深度学习》第8章注意力机制与外部记忆。这里简单的叙述一下使用MLP实现注意力机制的计算。假设现在有n个交叉特征(假如维度是k),将nxk的数据输入到一个kx1的全连接网络中,输出的张量维度为nx1,使用softmax函数将nx1的向量的每个维度进行归一化,得到一个新的nx1的向量,这个向量所有维度加起来的和为1,每个维度上的值就可以表示原nxk数据每一行(即1xk的数据)的权重。用公式表示为:
使用softmax归一化可得:
这样就得到了AFM二阶交叉部分的注意力权重,如果将AFM的一阶项写在一起,AFM模型用公式表示为:
AFM从最终的模型公式可以看出与FM的模型公式是非常相似的,所以也可以和FM一样应用于不同的任务,例如分类、回归及排序(不同的任务的损失函数是不一样的),AFM也有对防止过拟合进行处理:
加上正则参数之后的回归任务的损失函数表示为:
def AFM(linear_feature_columns, dnn_feature_columns): # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型 dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns) # 将linear部分的特征中sparse特征筛选出来,后面用来做1维的embedding linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns)) # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式 # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层 input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values()) # linear_logits由两部分组成,分别是dense特征的logits和sparse特征的logits linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns) # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型 # embedding层用户构建FM交叉部分和DNN的输入部分 embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False) # 将输入到dnn中的sparse特征筛选出来 att_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns)) att_logits = get_attention_logits(sparse_input_dict, att_sparse_feature_columns, embedding_layers) # B x (n(n-1)/2) # 将linear,dnn的logits相加作为最终的logits output_logits = Add()([linear_logits, att_logits]) # 这里的激活函数使用sigmoid output_layers = Activation("sigmoid")(output_logits) model = Model(input_layers, output_layers) return model
关于每一块的细节,这里就不解释了,在我们给出的GitHub代码中,我们已经加了非常详细的注释,大家看那个应该很容易看明白, 为了方便大家的阅读,我们这里还给大家画了一个整体的模型架构图,帮助大家更好的了解每一块以及前向传播(画的图不是很规范,先将就看一下,后面我们会统一在优化一下这个手工图)。
下面是一个通过keras画的模型结构图,为了更好的显示,数值特征和类别特征都只是选择了一小部分,画图的代码也在github中。