推荐算法-AFM
推荐算法-AFM,这篇文章也是在FM的基础上做工作。这篇文章是针对特征之间组合时,不同的特征都是用同样的向量去做。即每一个特征和其它的特征进行组合时,都是采用同一个向量,缺乏不同特征之间的关联性不同,应该采用不同的向量。解决这个问题的一个思路就是FFM,即每一个特征针对每一个的field生成一个特征向量,即在进行特征组合时,采用不同的向量表示去做。本文是解决这个问题的另一种思路,也就是对不同的特征组合赋予不同的权值,而且这个权值是可学习的,体现了模型对不同的特征组合的关注度不同。我的理解是对与最后的分类贡献程度较高的特征组合,会赋予较高的权值。
说到Attention,在cv、nlp、推荐这三个领域都有使用,可见Attention的作用之大。有人在cv领域使用Attention,可以找出深度模型作出最后判决所依据的区域,在nlp领域通过Attention可以找出关键词。在推荐中,可以为不同的特征组合赋予权值。总体的思路就是模型对不同的特征组合关注度是不同的。
AFM网络结构
从上图中可以看出,前半部分AFM的模型结构和NFM的基本一致,都是先把特征映射到固定的长度,然后计算FM部分,即上面的Pair-wise Interaction Layer部分。但是后面的就不一样了,NFM是直接在FM后面接入了一个NN层,而AFM是生成了一个Attention-based Pooling,即生成一组权重向量,然后再和Pair-wise部分对应相乘。AFM没有继续对这部分特征进行深层神经网络的学习,或许引入NN会进一步提升模型的精度。
FM的公式大家应该还记得,这里先回归一下:
attention就是对上面
V
i
,
V
j
x
i
x
j
V_{i},V_{j}x_{i}x_{j}
Vi,Vjxixj部分进行加权。最终AFM的公式表达为:
y
A
F
M
=
w
0
+
∑
i
=
1
n
w
i
x
i
+
P
T
∑
i
=
1
∑
j
=
i
+
1
a
i
j
<
V
i
,
V
j
>
x
i
x
j
y_{AFM}=w_{0}+\sum_{i=1}^{n}w_{i}x_{i}+P^{T}\sum_{i=1}\sum_{j=i+1}a_{ij}<V_{i},V_{j}>x_{i}x_{j}
yAFM=w0+i=1∑nwixi+PTi=1∑j=i+1∑aij<Vi,Vj>xixj
相比于原始的FM公式,AFM部分多了一组权重系数
a
i
j
a_{ij}
aij和向量
P
T
P^{T}
PT。我们只需要知道这两个数据的表达式,就可以实现AFM了。
a
i
j
′
=
h
T
R
e
L
U
(
W
<
V
i
,
V
j
>
x
i
x
j
+
b
)
a_{ij}^{'}=h^{T}ReLU(W<V_{i},V_{j}>x_{i}x_{j}+b)
aij′=hTReLU(W<Vi,Vj>xixj+b) ,
a
i
j
=
e
x
p
(
a
i
j
′
)
∑
i
,
j
e
x
p
(
a
i
j
′
)
a_{ij}=\frac{exp(a_{ij}^{'})}{\sum_{i,j}exp(a_{ij}^{'})}
aij=∑i,jexp(aij′)exp(aij′)
上面这两个公式表达了
a
i
j
a_{ij}
aij的计算方式,第一个公式中又引入了参数
w
、
h
w、h
w、h,因为两个特征相乘之后得到的特征长度不变,仍为embeddingSIze,w是要根据这部分特征计算attention部分的权重,因此w的大小为
K
∗
A
K*A
K∗A K为embedding部分的长度,A为attention部分的长度,
h
h
h部分的长度也为
A
A
A。第二个公式其实就是softmax表达式。其实最后就是对
p
a
i
r
s
=
f
i
e
l
d
S
I
z
e
∗
(
f
i
e
l
d
S
I
z
e
−
1
)
/
2
pairs=fieldSIze*(fieldSIze-1)/2
pairs=fieldSIze∗(fieldSIze−1)/2个特征组合分别赋予权重,即上面
a
i
j
a_{ij}
aij的长度为pairs。
AFM模型构建
模型构建部分就比较简单了,其实就是先把embedding部分的权重构建出来,然后再把Attention的权重构建出来就可以了。
&emsp 权重构建,embedding部分就是[featureSIze, embeddingSize],以及一次项部分[featureSize,1]。接下来就是Attention部分的权重了,Attention部分的参数有
W
、
h
、
P
、
b
W、h、P、b
W、h、P、b,他们的shape分别为:
W
:
[
e
m
b
e
d
d
i
n
g
S
i
z
e
,
a
t
t
e
n
t
i
o
n
S
i
z
e
]
W: [embeddingSize, attentionSize]
W:[embeddingSize,attentionSize]
b
:
[
a
t
t
e
n
t
i
o
n
S
i
z
e
,
]
b: [attentionSize, ]
b:[attentionSize,]
h
:
[
a
t
t
e
n
t
i
o
n
S
i
z
e
,
]
h: [attentionSize, ]
h:[attentionSize,]
p
:
[
a
t
t
e
n
t
i
o
n
S
i
z
e
,
1
]
p: [attentionSize, 1]
p:[attentionSize,1]
然后我们可以构建权重了:
def _initWeights(self):
weights = dict()
# embedding
weights['feature_embedding'] = tf.Variable(tf.random_normal(shape=[self.featureSize, self.embeddingSize],
mean=0.0, stddev=1.0), name='feature_embedding')
weights['feature_bias'] = tf.Variable(tf.random_normal(shape=[self.featureSize, 1], mean=0.0, stddev=1.0),
name='feature_bias')
weights['bias'] = tf.Variable(tf.constant(0.1), name='bias')
# attention
# w: K * A
# b: A
# h: A
# p: K * 1
weights['attention_w'] = tf.Variable(tf.random_normal(shape=[self.embeddingSize, self.attentionSize], mean=0.0,
stddev=1.0), name='attention_w')
weights['attention_b'] = tf.Variable(tf.random_normal(shape=[self.attentionSize, ], mean=0.0, stddev=1.0),
name='attention_b')
weights['attention_h'] = tf.Variable(tf.random_normal(shape=[self.attentionSize, ], mean=0.0, stddev=1.0),
name='attention_h')
weights['attention_p'] = tf.Variable(tf.random_normal(shape=[self.embeddingSize, 1]))
return weights
计算图的构建,首先是计算embedding部分,然后是计算attention部分的权重。
一些输入的设置:
self.weights = self._initWeights()
self.featureIndex = tf.placeholder(shape=[None, None], dtype=tf.int32, name='featureIndex')
self.featureValue = tf.placeholder(shape=[None, None], dtype=tf.float32, name='featureValue')
self.label = tf.placeholder(shape=[None, 1], dtype=tf.float32, name='label')
# self.dropoutKeep = tf.placeholder(shape=[None, ], dtype=tf.float32, name='dropoutKeep')
self.trainPhrase = tf.placeholder(dtype=tf.bool, name='trainPhrase')
embedding部分:
# embedding
self.embedding = tf.nn.embedding_lookup(self.weights['feature_embedding'], self.featureIndex) # N * F * K
featureValue = tf.reshape(self.featureValue, shape=[-1, self.fieldSize, 1])
self.embedding = tf.multiply(self.embedding, featureValue) # N*F*K
接下来我们就要计算特征之间的组合了,两两组合,这次是采用的for循环了,在PNN中我们是先找出来每一个特征组合pair的索引,然后利用tf.gather来把这些向量找出来,最后实现相乘。特征向量组合的的代码:
<
V
i
,
V
j
>
x
i
x
j
<V_{i},V_{j}>x_{i}x_{j}
<Vi,Vj>xixj
<
V
i
,
V
j
>
<V_{i},V_{j}>
<Vi,Vj>得到的是一个长度为embeddingSize的向量
elementWiseProduct = []
for i in range(0, self.fieldSize-1):
for j in range(i+1, self.fieldSize):
elementWiseProduct.append(tf.multiply(self.embedding[:, i, :], self.embedding[:, j, :]))
self.elementWiseProduct = tf.stack(elementWiseProduct) # f*(f-1)/2 * N * k
self.elementWiseProduct = tf.transpose(self.elementWiseProduct, [1, 0, 2], ) # N * (f*(f-1)/2) * K
此时self.elementWiseProduct的shape为
N
∗
f
∗
(
f
−
1
)
/
2
∗
K
N * f*(f-1)/2 * K
N∗f∗(f−1)/2∗K f为fieldSize,k为embeddingSize。
然后就是计算
W
<
V
i
,
V
j
>
x
i
x
j
+
b
W<V_{i},V_{j}>x_{i}x_{j}+b
W<Vi,Vj>xixj+b,W的shape为
K
∗
A
K*A
K∗A,因此首先要把self.elementWiseProduct的shape调整一下。
self.numPairs = int(self.fieldSize * (self.fieldSize-1)/2)
self.wxPlusB = tf.matmul(tf.reshape(self.elementWiseProduct, shape=[-1, self.embeddingSize]), # (N*pairs) * K
self.weights['attention_w']) + self.weights['attention_b'] # (N*pairs) * A
self.wxPlusB = tf.reshape(self.wxPlusB, shape=[-1, self.numPairs, self.attentionSize]) # N*pairs*A
因为
W
W
W和
<
V
i
i
,
V
j
>
<Vi_{i},V_{j}>
<Vii,Vj>相乘是矩阵相乘,所以要遵循矩阵相乘时尺寸的要求。计算完之后再对其进行shape的调整,调整为
N
∗
p
a
i
r
s
∗
A
N*pairs*A
N∗pairs∗A。
然后就是计算
a
i
j
′
=
h
T
R
e
L
U
(
W
<
V
i
,
V
j
>
x
i
x
j
+
b
)
a_{ij}^{'}=h^{T}ReLU(W<V_{i},V_{j}>x_{i}x_{j}+b)
aij′=hTReLU(W<Vi,Vj>xixj+b),
self.attentionExp = tf.exp(tf.reduce_sum(tf.multiply(tf.nn.relu(self.wxPlusB), self.weights['attention_h']),
axis=2, keep_dims=True)) # N*pairs*1
self.attentionExpSum = tf.reduce_sum(self.attentionExp, axis=1, keep_dims=True) # N*1*1
第一行代码是计算
a
i
j
′
a_{ij}^{'}
aij′第二行代码是对所有的
a
i
j
′
a_{ij}^{'}
aij′求和,即为后面的softmax计算
a
i
j
a_{ij}
aij做准备。上面这两行的keep_dims还是要加的,如果不加的话,在某一维度上进行求和,求和之后得到的结果就会少一个维度。
计算
a
i
j
a_{ij}
aij,
self.attentionOut = tf.div(self.attentionExp, self.attentionExpSum) # N*pairs*1
接下来就是对每一对特征组合进行权重分配了,
self.attention_x_product = tf.reduce_sum(tf.multiply(self.attentionOut, self.elementWiseProduct), axis=1) # N*K
self.attentionPartSum = tf.matmul(self.attention_x_product, self.weights['attention_p']) # N*1
最后把一次项以及偏置项计算出来加到一起就可以了,
# first order
self.yFirstOrder = tf.nn.embedding_lookup(self.weights['feature_bias'], self.featureIndex) # N*F
self.yFirstOrder = tf.multiply(self.yFirstOrder, featureValue) # N*F*1
self.yFirstOrder = tf.reduce_sum(self.yFirstOrder, axis=2) # N*F
# bias
self.yBias = self.weights['bias'] * tf.ones_like(self.label)
self.out = tf.add_n([tf.reduce_sum(self.yFirstOrder, axis=1, keep_dims=True),
self.attentionPartSum, self.yBias], name='out_afm')
然后就是模型的loss的设置、以及初始化等问题了。因为该模型没有全连接层,所以模型部分就是这样。
这篇文章就是对FM的二次项部分进行了加权操作,没有太大的改进,上一篇NFM是对FM的输出向量进行了几层全连接操作,思路都比较简单。不知道这样的模型在实际生产环境中是否有使用。
参考
https://www.comp.nus.edu.sg/~xiangnan/papers/ijcai17-afm.pdf
https://www.jianshu.com/p/83d3b2a1e55d