文章来自:深入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
包含个参数
对于参数 wij w i j ,只有当特征 xi x i 和 xj x j 都非 0 0 时,才产生loss,因此需要大量 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
分解为两个相同的矩阵,即
W=VTV
W
=
V
T
V
,其中
V
V
是一个的矩阵,
k
k
是隐向量的维度,通常,于是参数个数由
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) 将平方项减去之后乘上

我们得到了(3)中完全相同的表,于是推导结束
【参数梯度】
为了便于说明,仍然假设
n=4
n
=
4
,
k=3
k
=
3
,对于某个
f
f
,有
上式对 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
,求完之后可以反复使用,求和的复杂度为
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) )
V * np.expand_dims(x**2, axis=1)使用了boardcast
V.shape=(n, k) np.expand_dims(x**2, axis=1).shape=(n, 1)
term2 = V * np.expand_dims(x**2, axis=1)
grad_V = term1 - term2
print( grad_V )
向量化实现,求batch_size个样本X关于V的梯度,复杂度O(kn)
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)
对一个矩阵按行求和,相当于右乘一个全为1的列向量
temp1 = np.sum( A, axis=1, keepdims=True )
temp2 = np.dot( A, np.ones( (batch_size, 1) ) )
print( temp1 )
print( temp2 )
对一个列向量做水平方向tile,相当于右乘一个全为1的行向量
temp1 = np.tile( temp1, (1, k) )
temp2 = np.dot( temp2, np.ones( (1, k) ) )
print( temp1 )
print( temp2 )
将上述两步合并起来,直接对矩阵右乘一个全为1的矩阵
temp = np.dot( A, np.ones( (batch_size, k) ) )
print( temp )