有时系统中用户的行为比较稀少,采集到的样本很稀疏,这样直接导致常用的拟合方法学到的模型存在严重的过拟合问题,即特征之间存在严重的依赖和隔离关系,使得模型无法进一步学习到精准的内在规律。为了解决这一问题,FM模型应运而生,其基本原理是学到特征与特征之间的关系,从而达到更加精准的预测的目的。
引用论文原文的图示,图中的一条样本描述了当前用户的id特征、当前物品的id特征、当前用户对其他物品的打分、时间、当前用户上次对物品的打分,这条样本的label是当前用户对当前物品的打分。
可以看出,整个矩阵比较稀疏,通常意义下LR模型都会为每一维特征分配一个权重,公式如下所示:
y ^ ( x ) = w 0 + ∑ i = 1 n w i x i \hat{y}(x)=w_0+\sum_{i=1}^n w_ix_i y^(x)=w0+i=1∑nwixi
FM 模型与上述公式不同的地方在于其添加了一个 V ( n × k ) V(n\times k) V(n×k)矩阵, V V V的每一行代表的是 x x x的某个特征本身的"特征",具体公式如下所示:
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 \hat{y}(x)=w_0+\sum_{i=1}^n w_ix_i + {\color{Red} \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
示意图如下所示:
这里 ⟨ v i , v j ⟩ = ∑ f = 1 k v i , f v j , f \left \langle v_i,v_j \right \rangle=\sum_{f=1}^kv_{i,f}v_{j,f} ⟨vi,vj⟩=∑f=1kvi,fvj,f,代表的是 x i x_i xi和 x j x_j xj之间的相互关系。而上述红色公式这样设定的原因我个人理解是因为其描述的是两个不同特征之间的关系,同一个特征之间的关系没有学习的意义,即学习的关系如下所示(蓝色为需要学习的领域,白色为不需要学习的领域):
可以看出上述公式的时间复杂度为 O ( k n 2 ) O(kn^2) O(kn2),但是这个时间复杂度可以优化到 O ( k n ) O(kn) O(kn),公式推导如下所示:
上述公式的梯度下降计算公式如下:
∂ y ^ ( x ) ∂ θ = { 1 θ = w 0 x i θ = w i x i ∑ j = 1 n v j , f x j − v i , f x i 2 θ = v i , f \frac{\partial \hat{y}(x)}{\partial \theta} = \left\{\begin{matrix} 1 \ \ \ \ \theta =w_0\\ x_i \ \ \ \ \theta =w_i\\ x_i\sum_{j=1}^n v_{j,f}x_j-v_{i,f}x^2_i \ \ \ \theta=v_{i,f} \end{matrix}\right. ∂θ∂y^(x)=⎩⎨⎧1 θ=w0xi θ=wixi∑j=1nvj,fxj−vi,fxi2 θ=vi,f
乍一看模型反向传播的计算时间复杂度为 O ( n 2 ) O(n^2) O(n2),但后来发现针对 v i , f v_{i,f} vi,f的梯度计算, ∑ j = 1 n v j , f x j \sum_{j=1}^n v_{j,f}x_j ∑j=1nvj,fxj已经在前向传播中计算过一遍,只需要把之前的结果保存下来就行,因而这里只需要计算 v i , f x i 2 v_{i,f}x^2_i vi,fxi2,因而 V V V的反向传播计算时间复杂度依然是线性的。
用最简单的方式来阐述FM前向推导的整体过程:
import tensorflow as tf
X = tf.constant([[1, 2, 3, 4]], dtype=tf.float16)
w_0 = tf.constant([0.5], dtype=tf.float16)
W = tf.constant([[5], [6], [7], [8]], dtype=tf.float16)
V = tf.constant([[1, 2, 3, 4],[1, 2, 3, 4]], dtype=tf.float16)
linear_output = tf.add(w_0, tf.matmul(X, W))
# 公式[1] \sum((X * V^T)^2 - (X^2 * (V^T)^2)) * 0.5
complex_output = tf.multiply(tf.reduce_sum(tf.subtract(tf.pow(tf.matmul(X, tf.transpose(V)), 2), \
tf.matmul(tf.pow(X, 2), tf.pow(tf.transpose(V), 2))), \
axis=1, keep_dims=True), 0.5)
final_output= tf.add(linear_output, complex_output)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
print(complex_output.shape, complex_output.eval())
>>> (1, 1) [[546.]]
在写这段代码之前,确实有些疑惑,代码的实现方式和论文中的公式有所不同,代码中的实现方式有些难以理解,但是当我们仔细展开公式,发现二者确实是一样的,初始化 X X X和 V V V如下所示:
X = [ x 1 , x 2 , x 3 , x 4 ] X = [x_1, x_2, x_3, x_4] X=[x1,x2,x3,x4]
V = [ v 11 v 12 v 13 v 14 v 21 v 22 v 23 v 24 ] V=\begin{bmatrix} v_{11} & v_{12} & v_{13} & v_{14}\\ v_{21} & v_{22} & v_{23} & v_{24} \end{bmatrix} V=[v11v21v12v22v13v23v14v24]
上述FM核心代码的公式[1]的推导如下所示(格式有些乱,但是思路比较清晰):
c o m p l e x _ o u t = 1 2 ∑ f = 1 2 ( ( ∑ i = 1 4 v i , f x i ) 2 − ∑ i = 1 4 v i , f 2 x i 2 ) = 1 2 [ ( v 11 x 1 + v 21 x 2 + v 31 x 3 + v 41 x 4 ) 2 − ( v 11 2 x 1 2 + v 21 2 x 2 2 + v 31 2 x 3 2 + v 41 2 x 4 2 ) ] + 1 2 [ ( v 12 x 1 + v 22 x 2 + v 32 x 3 + v 42 x 4 ) 2 − ( v 12 2 x 1 2 + v 22 2 x 2 2 + v 32 2 x 3 2 + v 42 2 x 4 2 ) ] = 1 2 [ ( v 11 x 1 + v 21 x 2 + v 31 x 3 + v 41 x 4 , v 12 x 1 + v 22 x 2 + v 32 x 3 + v 42 x 4 ) 2 − ( v 11 2 x 1 2 + v 21 2 x 2 2 + v 31 2 x 3 2 + v 41 2 x 4 2 , v 12 2 x 1 2 + v 22 2 x 2 2 + v 32 2 x 3 2 + v 42 2 x 4 2 ) ] = r e d u c e _ s u m ( 1 2 [ ( X ∗ V T ) 2 − ( X 2 ∗ ( V T ) 2 ) ] ) \begin{aligned} complex\_out = \frac{1}{2}\sum_{f=1}^2((\sum_{i=1}^4v_{i,f}x_i)^2-\sum_{i=1}^4v_{i,f}^2x^2_i) \\ =\frac{1}{2}[(v_{11} x_1+v_{21} x_2+v_{31} x_3+v_{41} x_4)^2 \\ -(v_{11}^2x_1^2+v_{21}^2x_2^2+v_{31}^2x_3^2+v_{41}^2x_4^2)] \\ +\frac{1}{2}[(v_{12} x_1+v_{22} x_2+v_{32} x_3+v_{42} x_4)^2- \\ (v_{12}^2x_1^2+v_{22}^2x_2^2+v_{32}^2x_3^2+v_{42}^2x_4^2)] \\ =\frac{1}{2}[(v_{11} x_1+v_{21} x_2+v_{31} x_3+v_{41} x_4, \\ v_{12} x_1+v_{22} x_2+v_{32} x_3+v_{42} x_4)^2 \\ -(v_{11}^2x_1^2+v_{21}^2x_2^2+v_{31}^2x_3^2+v_{41}^2x_4^2, \\ v_{12}^2x_1^2+v_{22}^2x_2^2+v_{32}^2x_3^2+v_{42}^2x_4^2)] \\ = reduce\_sum(\frac{1}{2}[(X*V^T)^2-(X^2*(V^T)^2)]) \end{aligned} complex_out=21f=1∑2((i=1∑4vi,fxi)2−i=1∑4vi,f2xi2)=21[(v11x1+v21x2+v31x3+v41x4)2−(v112x12+v212x22+v312x32+v412x42)]+21[(v12x1+v22x2+v32x3+v42x4)2−(v122x12+v222x22+v322x32+v422x42)]=21[(v11x1+v21x2+v31x3+v41x4,v12x1+v22x2+v32x3+v42x4)2−(v112x12+v212x22+v312x32+v412x42,v122x12+v222x22+v322x32+v422x42)]=reduce_sum(21[(X∗VT)2−(X2∗(VT)2)])
这段时间在看张俊林老师的知乎博客,因而在这里总结下我能够理解的知识点。印象较深的有两点:1. FM模型作为召回模型如何在工程中使用,2. 统一召回和多路召回的优缺点,因而我也抄录部分内容作为自己知识点的扩充。
只考虑User和Item各自的特征
将单个User每一维User相关特征的embedding叠加起来,作为 U U U,并将所有的User的 U U U放入redis中,将单个Item每一维Item相关特征的embedding叠加起来,作为 I I I,并将所有的Item的 I I I放入faiss中,如下图所示:
当一个User请求打过来时,通过这个User对应的 U U U去faiss选出与这个 U U U topK相关的 I I I,具体计算方式即为 U U U和 I I I的点积,而能够这样做的原因恰恰是FM的原理,即在只考虑User和Item各自独立特征时,可以看出如下公式的等价关系(借用上面公式),
∑ i = 1 n ∑ j = i + 1 n ⟨ v i , v j ⟩ x i x j = ∑ i = 1 n ∑ j = i + 1 n ⟨ x i ∗ v i , x j ∗ v j ⟩ = ⟨ ∑ i = 1 n x i ∗ v i , ∑ j = 1 , j ≠ i n x j ∗ v j ⟩ \sum_{i=1}^n\sum_{j=i+1}^n \left \langle v_i,v_j \right \rangle x_i x_j=\sum_{i=1}^n\sum_{j=i+1}^n \left \langle x_i * v_i, x_j * v_j \right \rangle=\left \langle \sum_{i=1}^nx_i*v_i,\sum_{j=1,j\neq i}^nx_j*v_j \right \rangle i=1∑nj=i+1∑n⟨vi,vj⟩xixj=i=1∑nj=i+1∑n⟨xi∗vi,xj∗vj⟩=⟨i=1∑nxi∗vi,j=1,j=i∑nxj∗vj⟩
而如果在这里只考虑User和Item各自的特征,则上述公式可以转化为:
⟨ ∑ i = 1 n x i ∗ v i , ∑ j = 1 , j ≠ i n x j ∗ v j ⟩ = ⟨ ∑ i U i , ∑ j I j ⟩ \left \langle \sum_{i=1}^nx_i*v_i,\sum_{j=1,j\neq i}^nx_j*v_j \right \rangle=\left \langle \sum_iU_i,\sum_jI_j \right \rangle ⟨i=1∑nxi∗vi,j=1,j=i∑nxj∗vj⟩=⟨i∑Ui,j∑Ij⟩
添加context信息
context信息只能够通过线上实时获取,比如用户当时的播放行为等等,这里设context信息为 C C C,相应的做法也很简单,就是将User和Context的向量叠加后得到 U + C U+C U+C,而后去faiss通过内积的方式取出topK相关的 I I I,如下图所示: