AFM 网络介绍与源码浅析
前言
分享一篇上上个星期看过的论文, 记录下重点吧~
广而告之
可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中;
Attentional Factorization Machines (AFM)
文章信息
- 论文标题: Attentional Factorization Machines:
Learning the Weight of Feature Interactions via Attention Networks - 论文地址: https://www.ijcai.org/Proceedings/2017/0435.pdf
- 代码地址: https://github.com/hexiangnan/attentional_factorization_machine, 这是作者提供的代码; 另外在 https://github.com/princewen/tensorflow_practice/blob/master/recommendation/Basic-AFM-Demo/AFM.py 我看到了另外的精彩的实现, 后面分析实现时用的是这个代码.
- 发表时间: IJCAI 2017
- 论文作者: Jun Xiao, Hao Ye, Xiangnan He, Hanwang Zhang, Fei Wu, Tat-Seng Chua
- 作者单位: Zhejiang University, National University of Singapore
请注意
后面在博文中介绍的代码主要以 https://github.com/princewen/tensorflow_practice/blob/master/recommendation/Basic-AFM-Demo/AFM.py 中的为主; 原作者的代码结构也是类似的, 所以没有关系.
核心观点
文章首先介绍了 FM 的不足, 即对不同的特征交叉
⟨
v
i
,
v
j
⟩
x
i
x
j
\langle v_i, v_j\rangle x_ix_j
⟨vi,vj⟩xixj 使用的是同一个权重 1, 针对这个不足, 提出了自己的解决方案, 即不同的特征组合对输出的贡献程度是不同的; 具体的实现是引入了 Pair-wise Interaction Layer 和 Attention-based Pooling Layer;
前者输入的
m
m
m 个向量, 产生
m
×
(
m
−
1
)
2
\frac{m\times (m - 1)}{2}
2m×(m−1) 个交叉的向量, 和 FM 不同的是, FM 得到的是
v
i
v_i
vi 和
v
j
v_j
vj 的内积,但是 Pair-wise Interaction Layer 得到的是
v
i
v_i
vi 和
v
j
v_j
vj 的 element-wise product (哈达玛积), 因此该层的输出是一组向量;
之后这组向量再输入到 Attention-based Pooling Layer 中, 以学习不同特征组合的自适应权重
α
\alpha
α. 为了防止样本稀疏导致某些
α
\alpha
α 无法被训练到以及考虑到泛化性, Attention 层还使用了一个网络层对来自 Pair-wise Interaction Layer 的向量进行了处理, 再根据 Softmax 获得每个交叉特征的权重
α
\alpha
α.
核心观点介绍
Pair-wise Interaction Layer
输入特征 x i x_i xi 经过 Embedding Layer 后, 得到每个特征对应的 embedding, 结果表示为:
E = { v i x i } i ∈ X \mathcal{E}=\left\{\mathbf{v}_{i} x_{i}\right\}_{i \in \mathcal{X}} E={vixi}i∈X
其中 v i ∈ R k \mathbf{v}_{i} \in \mathbb{R}^{k} vi∈Rk, 即特征的大小为 k k k. Pair-wise Interaction Layer 的作用是将这 m m m 个输入的特征进行两两交叉, 从而共生成 m × ( m − 1 ) 2 \frac{m\times (m - 1)}{2} 2m×(m−1) 个输出结果; 和 FM 不同的是, 该层对特征交叉的方式不是进行特征间的内积, 而是对两个特征进行 element-wise product, 也就是求哈达马积:
f P I ( E ) = { ( v i ⊙ v j ) x i x j } ( i , j ) ∈ R x f_{P I}(\mathcal{E})=\left\{\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}\right\}_{(i, j) \in \mathcal{R}_{x}} fPI(E)={(vi⊙vj)xixj}(i,j)∈Rx
其中 R x = { ( i , j ) } i ∈ X , j ∈ X , j > i \mathcal{R}_{x}=\{(i, j)\}_{i \in \mathcal{X}, j \in \mathcal{X}, j>i} Rx={(i,j)}i∈X,j∈X,j>i.
代码实现也很简单:
## embeddings 是特征对应的 emb, 大小为 B * F * K
## F 为 field 的大小, 即 field_size
## K 为 embedding 的大小
element_wise_product_list = []
for i in range(field_size):
for j in range(i+1, field_size):
element_wise_product_list.append(tf.multiply(embeddings[:,i,:], embeddings[:,j,:])) # B * K
element_wise_product = tf.stack(element_wise_product_list) # (F * F - 1 / 2) * B * K
element_wise_product = tf.transpose(self.element_wise_product,perm=[1,0,2],name='element_wise_product') # B * (F * F - 1 / 2) * K
(另外产生
m
×
(
m
−
1
)
2
\frac{m\times (m - 1)}{2}
2m×(m−1) 个交叉特征还看到一种写法, 用了 tf.gather
, 在 PNN 的实现中可以看到; 关于 PNN 可以去参看我的博客: Product-based Neural Network (PNN) 介绍与源码浅析; 不行了, 笑哭 , 强行打了一个广告 🤣🤣🤣)
Attention-based Pooling Layer
该层对 Pair-wise Interaction Layer 产生的 m × ( m − 1 ) 2 \frac{m\times (m - 1)}{2} 2m×(m−1) 个交叉特征进行 Attention:
f A t t ( f P I ( E ) ) = ∑ ( i , j ) ∈ R x a i j ( v i ⊙ v j ) x i x j f_{A t t}\left(f_{P I}(\mathcal{E})\right)=\sum_{(i, j) \in \mathcal{R}_{x}} a_{i j}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j} fAtt(fPI(E))=(i,j)∈Rx∑aij(vi⊙vj)xixj
其中 α i j \alpha_{ij} αij 为 Attention 分数; 但上式的一个问题是, 如果样本集中 x i x_i xi 和 x j x_j xj 没有同时出现, 那么 α i j \alpha_{ij} αij 可能就无法得到更新 (这个问题其实非常类似 FM 要处理的问题, 比如在引入二阶交叉特征时, 直接采用下式:
y ( x ) = w 0 + ∑ i = 1 n w i x i + ∑ i = 1 n ∑ j = i + 1 n w i j x i x j y(x)=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n} \sum_{j=i+1}^{n} w_{i j} x_{i} x_{j} y(x)=w0+i=1∑nwixi+i=1∑nj=i+1∑nwijxixj
可能会存在某些 w ^ i j \hat{w}_{i j} w^ij 无法得到更新, 所以 FM 采用矩阵分解的思路, 将公式转换成:
y ( x ) = w 0 + ∑ i = 1 n w i x i + ∑ i = 1 n ∑ j = i + 1 n ⟨ v i , v j ⟩ x i x j y(x)=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n} \sum_{j=i+1}^{n}\left\langle v_{i}, v_{j}\right\rangle x_{i} x_{j} y(x)=w0+i=1∑nwixi+i=1∑nj=i+1∑n⟨vi,vj⟩xixj
从而解决了因样本稀疏而导致的参数无法及时更新的问题, 更为具体的讨论可以查看 FM 算法介绍以及 libFM 源码简析)
因此, 作者最后考虑的是, 引入 MLP 来对 α \alpha α 进行学习, 公式如下:
a i j ′ = h T ReLU ( W ( v i ⊙ v j ) x i x j + b ) a i j = exp ( a i j ′ ) ∑ ( i , j ) ∈ R x exp ( a i j ′ ) \begin{aligned} a_{i j}^{\prime} &=\mathbf{h}^{T} \operatorname{ReLU} \left(\mathbf{W}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}+\mathbf{b}\right) \\ a_{i j} &=\frac{\exp \left(a_{i j}^{\prime}\right)}{\sum_{(i, j) \in \mathcal{R}_{x}} \exp \left(a_{i j}^{\prime}\right)} \end{aligned} aij′aij=hTReLU(W(vi⊙vj)xixj+b)=∑(i,j)∈Rxexp(aij′)exp(aij′)
其中 W ∈ R t × k , b ∈ R t , h ∈ R t \mathbf{W} \in \mathbb{R}^{t \times k}, \mathbf{b} \in \mathbb{R}^{t}, \mathbf{h} \in \mathbb{R}^{t} W∈Rt×k,b∈Rt,h∈Rt 为 Attention 网络的参数, t t t 表示 Attention 网络隐藏层的大小;
最终的输出表示如下, 主要 Attention 得到交叉特征间的加权求和的结果后, 再与 p \bm{p} p 进行内积, 其中 p ∈ R k \bm{p}\in\mathbb{R}^{k} p∈Rk, 和特征的大小一样.
y ^ A F M ( x ) = w 0 + ∑ i = 1 n w i x i + p T ∑ i = 1 n ∑ j = i + 1 n a i j ( v i ⊙ v j ) x i x j \hat{y}_{A F M}(\mathbf{x})=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\mathbf{p}^{T} \sum_{i=1}^{n} \sum_{j=i+1}^{n} a_{i j}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j} y^AFM(x)=w0+i=1∑nwixi+pTi=1∑nj=i+1∑naij(vi⊙vj)xixj
下面看看 Attention-based Pooling Layer 的实现代码:
## field_size 为 Field 的个数, 记为 F
## num_interactions 为交叉特征的个数 (F - 1) * F / 2
## 为了方便, 我后面简记为 P, 表示 pair 的个数; 即 P = (F - 1) * F / 2
num_interactions = int(field_size * (field_size - 1) / 2)
## wx+b -> relu(wx+b) -> h*relu(wx+b)
## element_wise_product 为 Pair-wise Interaction Layer 的输出结果
## 大小为 B * P * K, 其中 K 为 embedding 的大小
## weights['attention_w'] 为 Attention 网络的权重, 大小为 (K, A),
## 其中 A 表示 attention size, 使用论文中的数学符号, 表示的是 t
## weights['attention_b'] 为 Attention 网络的偏置, 大小为 (A,)
## 那么 attention_wx_plus_b 的结果为 wx + b
attention_wx_plus_b = tf.reshape(
tf.add(tf.matmul(
tf.reshape(element_wise_product, # B * P * K
shape=(-1, embedding_size) # (B * P) * K
),
weights['attention_w'] # (K, A)
),
weights['attention_b'] # (A,)
),
shape=[-1,num_interactions, attention_size]
) # N * P * A
## weights['attention_h'] 表示公式中的 h, 大小为 (A,)
## reduce_sum 相当于做 h 和 relu(Wx + b) 的内积
attention_exp = tf.exp(tf.reduce_sum(
tf.multiply(
tf.nn.relu(attention_wx_plus_b),
weights['attention_h']),
axis=2, keep_dims=True)
) # N * P * 1
## 求 sum(alpha)
attention_exp_sum = tf.reduce_sum(attention_exp,
axis=1, keep_dims=True) # N * 1 * 1
## 相当于求 softmax(alpha)
attention_out = tf.div(attention_exp, attention_exp_sum,
name='attention_out') # N * P * 1
## 完成对 P 个特征的加权求和
attention_x_product = tf.reduce_sum(
tf.multiply(attention_out, # N * P * 1
element_wise_product # N * P * K
),
axis=1, name='afm') # N * K
## 输出之前还要和 p 向量进行内积
## weights['attention_p'] 大小为 (K,)
## 注意这里使用的是 matmul, 表示矩阵的乘法
attention_part_sum = tf.matmul(attention_x_product, # N * K
weights['attention_p'] # K
) # N * 1
总结
OK, 下午 5 点前写完了, 出门吃烧烤! 😁😁😁 罪恶的周末~