文章来自:深入FFM原理与实践
【动机】
特征的交叉是有用的,于是想到构造二次项特征,对应着如下的多项式模型
参数包括:
,
,
其中矩阵
包含
个参数
对于参数 ,只有当特征 和 都非 时,才产生loss,因此 需要大量 和 都非0的样本才能进行训练
然而在实际场景中,特征向量 往往是高维且稀疏的(由于对cat型变量作one-hot编码),满足“ 和 都非零”的样本将会非常少
训练样本的不足,很容易导致参数
不准确,最终将严重影响模型的性能
【FM思想】
FM借鉴了协同过滤中将rating矩阵分解为user矩阵和item矩阵的方法
在本问题中,将矩阵
分解为两个相同的矩阵
,即
,其中
是一个
的矩阵,
是隐向量的维度,通常
,于是参数个数由
个下降到
个
【FM模型公式】
参数包括:
,
,
【二次项化简】
左式外层的二重求和复杂度为 ,内层计算向量点乘复杂度为 ,于是整个式子的复杂度为
右式是一个二重求和,内外的复杂度分别为 和 ,故整个式子的复杂度为
综上所述,二次项经过化简,计算复杂度由
降为
【二次项化简的推导】
假设 ,
(1) 展开左式的二重求和符号

(2) 展开向量点乘

(3) 按照分量
分类,拆分为
个子表

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

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

我们得到了(3)中完全相同的表,于是推导结束
【参数梯度】
为了便于说明,仍然假设
,
,对于某个
,有
上式对 求导,得
FM模型各个参数的梯度如下
对于某个
,
求完之后可以反复使用,求和的复杂度为
,因此整个模型的训练复杂度为
【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,复杂度为
的计算方法】
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,复杂度为
的计算方法】
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的梯度,复杂度
】
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 )