文章来自:深入FFM原理与实践
【动机】
特征的交叉是有用的,于是想到构造二次项特征,对应着如下的多项式模型
y(x)=w0+∑i=1nwixi+∑i=1n∑j=i+1nwijxixj 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
参数包括: w0 w 0 , ⎡⎣⎢⎢⎢⎢w1w2⋮wn⎤⎦⎥⎥⎥⎥ [ w 1 w 2 ⋮ w n ] , W=⎡⎣⎢⎢⎢⎢⎢⎢⎢−w12−w13w23−⋯⋯⋯−w1nw2n⋮wn−1,n−⎤⎦⎥⎥⎥⎥⎥⎥⎥ W = [ − w 12 w 13 ⋯ w 1 n − w 23 ⋯ w 2 n − ⋯ ⋮ − w n − 1 , n − ]
其中矩阵 W W 包含 (n−1)+(n−2)+⋯+2+1=n(n−1)2 ( n − 1 ) + ( n − 2 ) + ⋯ + 2 + 1 = n ( n − 1 ) 2 个参数
对于参数 wij w i j ,只有当特征 xi x i 和 xj x j 都非 0 0 时,才产生loss,因此 wij w i j 需要大量 xi x i 和 xj x j 都非0的样本才能进行训练
然而在实际场景中,特征向量 x x 往往是高维且稀疏的(由于对cat型变量作one-hot编码),满足“ xi x i 和 xj x j 都非零”的样本将会非常少
训练样本的不足,很容易导致参数 wij w i j 不准确,最终将严重影响模型的性能
【FM思想】
FM借鉴了协同过滤中将rating矩阵分解为user矩阵和item矩阵的方法
在本问题中,将矩阵 W W 分解为两个相同的矩阵 V V ,即 W=VTV W = V T V ,其中 V V 是一个 n×k n × k 的矩阵, k k 是隐向量的维度,通常 k≪n k ≪ n ,于是参数个数由 n(n−1)2 n ( n − 1 ) 2 个下降到 kn k n 个
【FM模型公式】
y(x)=w0+∑i=1nwixi+∑i=1n∑j=i+1n⟨vi,vj⟩xixj 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
参数包括: w0 w 0 , ⎡⎣⎢⎢⎢⎢w1w2⋮wn⎤⎦⎥⎥⎥⎥ [ w 1 w 2 ⋮ w n ] , Vn×k=⎡⎣⎢⎢⎢⎢−v1−−v2−⋮−vn−⎤⎦⎥⎥⎥⎥=⎡⎣⎢⎢⎢⎢⎢v1,1v2,1⋮vn,1v1,2v2,2⋮vn,2⋯⋯⋱⋯v1,kv2,k⋮vn,k⎤⎦⎥⎥⎥⎥⎥ V n × k = [ − v 1 − − v 2 − ⋮ − v n − ] = [ v 1 , 1 v 1 , 2 ⋯ v 1 , k v 2 , 1 v 2 , 2 ⋯ v 2 , k ⋮ ⋮ ⋱ ⋮ v n , 1 v n , 2 ⋯ v n , k ]
【二次项化简】
∑i=1n∑j=i+1n⟨vi,vj⟩xixj=12∑f=1k⎡⎣(∑i=1nvi,fxi)2−∑i=1nv2i,fx2i⎤⎦ ∑ i = 1 n ∑ j = i + 1 n ⟨ v i , v j ⟩ x i x j = 1 2 ∑ f = 1 k [ ( ∑ i = 1 n v i , f x i ) 2 − ∑ i = 1 n v i , f 2 x i 2 ]
左式外层的二重求和复杂度为 O(n2) O ( n 2 ) ,内层计算向量点乘复杂度为 O(k) O ( k ) ,于是整个式子的复杂度为 O(kn2) O ( k n 2 )
右式是一个二重求和,内外的复杂度分别为 O(n) O ( n ) 和 O(k) O ( k ) ,故整个式子的复杂度为 O(kn) O ( k n )
综上所述,二次项经过化简,计算复杂度由 O(kn2) O ( k n 2 ) 降为 O(kn) O ( k n )
【二次项化简的推导】
假设 n=4 n = 4 , k=3 k = 3
(1) 展开左式的二重求和符号

(2) 展开向量点乘 ⟨vi,vj⟩ ⟨ v i , v j ⟩

(3) 按照分量 f=1,2,3 f = 1 , 2 , 3 分类,拆分为 3 3 个子表

(4) 另一方面,构造如下式子,展开之后得到下表

(4) 将平方项减去之后乘上 1/2 1 / 2

我们得到了(3)中完全相同的表,于是推导结束
【参数梯度】
为了便于说明,仍然假设 n=4 n = 4 , k=3 k = 3 ,对于某个 f f ,有
12⎡⎣(∑i=14vi,fxi)2−∑i=14v2i,fx2i⎤⎦=12[(v1,fx1+v2,fx2+v3,fx3+v4,fx4)2−(v21,fx21+v22,fx22+v23,fx23+v24,fx24)] 1 2 [ ( ∑ i = 1 4 v i , f x i ) 2 − ∑ i = 1 4 v i , f 2 x i 2 ] = 1 2 [ ( v 1 , f x 1 + v 2 , f x 2 + v 3 , f x 3 + v 4 , f x 4 ) 2 − ( v 1 , f 2 x 1 2 + v 2 , f 2 x 2 2 + v 3 , f 2 x 3 2 + v 4 , f 2 x 4 2 ) ]
上式对 vi,f v i , f 求导,得
∂y∂vi,f=12[2(v1,fx1+v2,fx2+v3,fx3+v4,fx4)xi−2vi,fx2i]=(v1,fx1+v2,fx2+v3,fx3+v4,fx4)xi−vi,fx2i=xi∑j=14vj,fxj−vi,fx2i ∂ y ∂ v i , f = 1 2 [ 2 ( v 1 , f x 1 + v 2 , f x 2 + v 3 , f x 3 + v 4 , f x 4 ) x i − 2 v i , f x i 2 ] = ( v 1 , f x 1 + v 2 , f x 2 + v 3 , f x 3 + v 4 , f x 4 ) x i − v i , f x i 2 = x i ∑ j = 1 4 v j , f x j − v i , f x i 2
FM模型各个参数的梯度如下
∂y∂θ=⎧⎩⎨⎪⎪⎪⎪⎪⎪⎪⎪1xixi∑j=1nvj,fxj−vi,fx2iif θ is w0if θ is wiif θ is vi,f ∂ y ∂ θ = { 1 if θ is w 0 x i if θ is w i x i ∑ j = 1 n v j , f x j − v i , f x i 2 if θ is v i , f
对于某个 f f , ∑j=1nvj,fxj ∑ j = 1 n v j , f x j 求完之后可以反复使用,求和的复杂度为 O(n) O ( n ) ,因此整个模型的训练复杂度为 O(kn) O ( k n )
【FM模型缺点】
本质上为线性模型,没有考虑Field-aware
【loss function及梯度代码】
import numpy as np
seed = 0
np.random.seed( seed )
n, k, batch_size = 4, 3, 5
V = np.random.rand( n, k )
x = np.random.rand( n )
X = np.tile( x, (batch_size, 1) )
【非向量化实现,求1个样本x
的loss,复杂度为 O(kn2) O ( k n 2 ) 的计算方法】
loss = 0
for i in range(n):
for j in range(i+1, n):
v_i, v_j = V[i, :], V[j, :]
loss += np.dot( v_i, v_j ) * x[i] * x[j]
print( 'loss =', loss )
【非向量化实现,求1个样本x
的loss,复杂度为 O(kn) O ( k n ) 的计算方法】
loss = 0
for f in range(k):
term1, term2 = 0, 0
for i in range(n):
term1 += V[i, f] * x[i]
term2 += V[i, f] ** 2 * x[i] ** 2
loss += term1 ** 2 - term2
loss /= 2
print( 'loss =', loss )
【向量化实现,求batch_size个样本X
的loss】
loss = 1/2 * np.sum( np.dot( X, V ) ** 2 - np.dot( X ** 2, V ** 2 ), axis=1 )
loss = np.mean( loss )
print( 'loss =', loss )
【非向量化实现,求1个样本x
关于V的梯度,复杂度 O(kn) O ( k n ) 】
grad_V = np.zeros_like(V)
for f in range(k):
temp = 0
for j in range(n):
temp += V[j, f] * x[j]
for i in range(n):
grad_V[i, f] = x[i] * temp - V[i, f] * x[i]**2
print( grad_V )
【向量化实现,求1个样本x关于V的梯度,复杂度O(kn)】
temp = np.dot(x, V)
term1 = np.dot( np.expand_dims(x, axis=1), np.expand_dims(temp, axis=0) )
term2 = V * np.expand_dims(x**2, axis=1)
grad_V = term1 - term2
print( grad_V )
term1 = np.dot( X.T, np.dot(X, V) )
term2 = V * np.dot( (X**2).T, np.ones( (batch_size, k) ) )
grad_V = term1 - term2
print( grad_V / batch_size )
def compute_loss( V, X ):
loss = 1/2 * np.sum( np.dot( X, V ) * 2 - np.dot( X * 2, V ** 2 ), axis=1 )
loss = np.mean( loss )
return loss
grad_V = np.zeros_like(V)
epsilon = 1e-4
for i in range( V.shape[0] ):
for j in range( V.shape[1] ):
epsilon_vec = np.zeros_like(V)
epsilon_vec[i, j] += epsilon
grad_V[i, j] = ( compute_loss( V+epsilon_vec, X ) - compute_loss( V-epsilon_vec, X ) ) / ( 2 * epsilon )
print( grad_V )
【题外话】
A = np.random.rand(n, batch_size)
temp1 = np.sum( A, axis=1, keepdims=True )
temp2 = np.dot( A, np.ones( (batch_size, 1) ) )
print( temp1 )
print( temp2 )
temp1 = np.tile( temp1, (1, k) )
temp2 = np.dot( temp2, np.ones( (1, k) ) )
print( temp1 )
print( temp2 )
temp = np.dot( A, np.ones( (batch_size, k) ) )
print( temp )