Factorization Machine

文章来自:深入FFM原理与实践

【动机】

特征的交叉是有用的,于是想到构造二次项特征,对应着如下的多项式模型
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

参数包括: w 0 [ w 1 w 2 w n ] W = [ w 12 w 13 w 1 n w 23 w 2 n w n 1 , n ]
其中矩阵 W 包含 ( n 1 ) + ( n 2 ) + + 2 + 1 = n ( n 1 ) 2 个参数

对于参数 w i j ,只有当特征 x i x j 都非 0 时,才产生loss,因此 w i j 需要大量 x i x j 都非0的样本才能进行训练

然而在实际场景中,特征向量 x 往往是高维且稀疏的(由于对cat型变量作one-hot编码),满足“ x i x j 都非零”的样本将会非常少

训练样本的不足,很容易导致参数 w i j 不准确,最终将严重影响模型的性能

【FM思想】

FM借鉴了协同过滤中将rating矩阵分解为user矩阵和item矩阵的方法

在本问题中,将矩阵 W 分解为两个相同的矩阵 V ,即 W = V T V ,其中 V 是一个 n × k 的矩阵, k 是隐向量的维度,通常 k n ,于是参数个数由 n ( n 1 ) 2 个下降到 k n

【FM模型公式】

扫描二维码关注公众号,回复: 3088905 查看本文章

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
参数包括: w 0 [ w 1 w 2 w n ] 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 = 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 ( n 2 ) ,内层计算向量点乘复杂度为 O ( k ) ,于是整个式子的复杂度为 O ( k n 2 )

右式是一个二重求和,内外的复杂度分别为 O ( n ) O ( k ) ,故整个式子的复杂度为 O ( k n )

综上所述,二次项经过化简,计算复杂度由 O ( k n 2 ) 降为 O ( k n )

【二次项化简的推导】

假设 n = 4 k = 3

(1) 展开左式的二重求和符号

(2) 展开向量点乘 v i , v j

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

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

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

我们得到了(3)中完全相同的表,于是推导结束

【参数梯度】

为了便于说明,仍然假设 n = 4 k = 3 ,对于某个 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 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 ) ]

上式对 v i , f 求导,得

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 θ = { 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 j = 1 n v j , f x j 求完之后可以反复使用,求和的复杂度为 O ( n ) ,因此整个模型的训练复杂度为 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 ( 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 ( 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 ( 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 )

猜你喜欢

转载自blog.csdn.net/o0Helloworld0o/article/details/81774126
今日推荐