task01:AFM阅读
(这是我第一次在csdn上写文章,目的是完成参加Datawhale组织的一个很好的活动的任务,第一次发也是对我的一种挑战)
1. AFM提出的动机
AFM(Attentional Factorization Machines)是在FM的基础上加上了注意力机制,而FM是通过特征隐向量的内积来对交叉特征进行建模,公式如下:
y
f
m
=
w
0
+
∑
i
=
1
n
w
i
x
i
+
∑
i
=
1
n
∑
i
+
1
n
<
v
i
,
v
j
>
x
i
x
j
y_{fm} = w_0+\sum_{i=1}^nw_ix_i+\sum_{i=1}^{n}\sum_{i+1}^n\lt v_i,v_j\gt x_ix_j
yfm=w0+i=1∑nwixi+i=1∑ni+1∑n<vi,vj>xixj
如何让不同的交叉特征具有不同的重要性就是AFM核心的贡献,这也是我所理解的注意力机制(目前在做attention在语义分割方面的神经网络,所以我理解的attention机制就是类似于人眼,对某一块比较注意,则具有不同的权重)。
2. AFM模型原理
(该图转载于Datawhale所提供的学习资料,表示的是AFM交叉特征部分的模型结构)
AFM最核心的两个点分别是Pair-wise Interaction Layer和Attention-based Pooling。Pair-wise Interaction Layer将输入的向量两两计算哈达玛积(两个向量对应元素相乘,得到的还是一个向量,类似于向量中的内积,但是不进行最后的加和),再将得到的交叉特征向量组输入到Attention-based Pooling层。
2.1 Pair-wise Interaction Layer
AFM二阶交叉项(无attention):所有非零特征对应的隐向量两两对应元素乘积,然后再向量求和,输出的还是一个向量。
∑
i
=
1
n
∑
i
+
1
n
(
v
i
⊙
v
j
)
x
i
x
j
\sum_{i=1}^{n}\sum_{i+1}^n (v_i \odot v_j) x_ix_j
i=1∑ni+1∑n(vi⊙vj)xixj
上述写法是为了更好的与FM进行对比,下面给出论文中真正的写法:
按照论文的意思,特征的embedding可以表示为:
ε
=
v
i
x
i
\varepsilon = {v_ix_i}
ε=vixi,经过Pair-wise Interaction Layer输出可得:
f
P
I
(
ε
)
=
{
(
v
i
⊙
v
j
)
x
i
x
j
}
i
,
j
∈
R
x
f_{PI}(\varepsilon)=\{(v_i \odot v_j) x_ix_j\}_{i,j \in R_x}
fPI(ε)={(vi⊙vj)xixj}i,j∈Rx
R
x
R_x
Rx表示的是有效特征集合。此时的
f
P
I
(
ε
)
f_{PI}(\varepsilon)
fPI(ε)表示的是一个向量集合,所以需要先将这些向量集合聚合成一个向量,然后在转换成一个数值:
y
^
=
p
T
∑
(
i
,
j
)
∈
R
x
(
v
i
⊙
v
j
)
x
i
x
j
+
b
\hat{y} = p^T \sum_{(i,j)\in R_x}(v_i \odot v_j) x_ix_j + b
y^=pT(i,j)∈Rx∑(vi⊙vj)xixj+b
上式中的求和部分就是将向量集合聚合成一个维度与隐向量维度相同的向量,通过向量
p
p
p再将其转换成一个数值,b表示的是偏置。
从开始介绍Pair-wise Interaction Layer到现在解决的一个问题是,如何将使用哈达玛积得到的交叉特征转换成一个最终输出需要的数值,到目前为止交叉特征之间的注意力权重还没有出现。在没有详细介绍注意力之前先感性的认识一下如果现在已经有了每个交叉特征的注意力权重,那么交叉特征的输出可以表示为:
y
^
=
p
T
∑
(
i
,
j
)
∈
R
x
α
i
j
(
v
i
⊙
v
j
)
x
i
x
j
+
b
\hat{y} = p^T \sum_{(i,j)\in R_x}\alpha_{ij}(v_i \odot v_j) x_ix_j + b
y^=pT(i,j)∈Rx∑αij(vi⊙vj)xixj+b
就是在交叉特征得到的新向量前面乘以一个注意力权重
α
i
j
\alpha_{ij}
αij, 那么这个注意力权重如何计算得到呢?
2.2 Attention-based Pooling
我之前阅读过attention论文,这里给出attention大致的原理:这里的Q,K,V都是原向量乘上训练后的三个矩阵所得出的。
下面是datawhale给出的理解:假设现在有n个交叉特征(假如维度是k),将nxk的数据输入到一个kx1的全连接网络中,输出的张量维度为nx1,使用softmax函数将nx1的向量的每个维度进行归一化,得到一个新的nx1的向量,这个向量所有维度加起来的和为1,每个维度上的值就可以表示原nxk数据每一行(即1xk的数据)的权重。用公式表示为:
α
i
j
′
=
h
T
R
e
L
U
(
W
(
v
i
⊙
v
j
)
x
i
x
j
+
b
)
\alpha_{ij}' = h^T ReLU(W(v_i \odot v_j)x_ix_j + b)
αij′=hTReLU(W(vi⊙vj)xixj+b)
使用softmax归一化可得:
α
i
j
=
e
x
p
(
α
i
j
′
)
∑
(
i
,
j
)
∈
R
x
e
x
p
(
α
i
j
′
)
\alpha_{ij} = \frac{exp(\alpha_{ij}')}{\sum_{(i,j)\in R_x}exp(\alpha_{ij}')}
αij=∑(i,j)∈Rxexp(αij′)exp(αij′)
这样就得到了AFM二阶交叉部分的注意力权重,如果将AFM的一阶项写在一起,AFM模型用公式表示为:
y
^
a
f
m
(
x
)
=
w
0
+
∑
i
=
1
n
w
i
x
i
+
p
T
∑
(
i
,
j
)
∈
R
x
α
i
j
(
v
i
⊙
v
j
)
x
i
x
j
+
b
\hat{y}_{afm}(x) = w_0+\sum_{i=1}^nw_ix_i+p^T \sum_{(i,j)\in R_x}\alpha_{ij}(v_i \odot v_j) x_ix_j + b
y^afm(x)=w0+i=1∑nwixi+pT(i,j)∈Rx∑αij(vi⊙vj)xixj+b
2.3 AFM模型训练
AFM从最终的模型公式可以看出与FM的模型公式是非常相似的,所以也可以和FM一样应用于不同的任务,例如分类、回归及排序(不同的任务的损失函数是不一样的),AFM也有对防止过拟合进行处理:
- 在Pair-wise Interaction Layer层的输出结果上使用dropout防止过拟合,因为并不是所有的特征组合对预测结果都有用,所以随机的去除一些交叉特征,让剩下的特征去自适应的学习可以更好的防止过拟合。(这一点我不是很理解,如果有大佬看到的话能指点我一下,实在感谢)
- 对Attention-based Pooling层中的权重矩阵 W W W使用L2正则化(没有使用dropout的原因是经过试验,发现加入dropout会使得模型训练不稳定而且性能不好)
加上正则参数之后的回归任务的损失函数表示为:
L
=
∑
x
∈
T
(
y
^
a
f
m
(
x
)
−
y
(
x
)
)
2
+
λ
∣
∣
x
∣
∣
2
L = \sum_{x\in T} (\hat{y}_{afm}(x) - y(x))^2 + \lambda ||x||^2
L=x∈T∑(y^afm(x)−y(x))2+λ∣∣x∣∣2
3. AFM代码实现
- linear part: 这部分是有关于线性计算,也就是FM的前半部分 w 1 x 1 + w 2 x 2... w n x n + b w1x1+w2x2...wnxn+b w1x1+w2x2...wnxn+b的计算。对于这一块的计算,我们用了一个get_linear_logits函数实现,后面再说,总之通过这个函数,我们就可以实现上面这个公式的计算过程,得到linear的输出
- dnn part: 这部分是后面交叉特征的那部分计算,这一部分需要使用注意力机制来将所有类别特征的embedding计算注意力权重,然后通过加权求和的方式将所有交叉之后的特征池化成一个向量,最终通过一个映射矩阵 p p p将向量转化成一个logits值
- 最终将linear部分与dnn部分相加之后,通过sigmoid激活得到最终的输出
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
下面是Datawhale组织所提供的一个整体的模型架构图,能帮助大家更好的了解每一块以及前向传播
下面是一个通过keras画的模型结构图,为了更好的显示,数值特征和类别特征都只是选择了一小部分,画图的代码也在github中。
4. task01总结
这是第一次在csdn上写博客,也算是勇敢地迈出了第一步,更加感谢datawhale组织的这次活动,并且,这两天的课比较多,所以第一次task学习的内容比较少并且所加入的自己的思考太少了,下一次一定要花更多时间来阅读学习。