分解机(Factorization Machines,FM)推荐算法
- 在推荐系统中,特征经过one-hot编码后,数据矩阵往往是十分稀疏的,而原本分类任务中最强的单模型——SVM又对高维稀疏的数据十分的不敏感,无法找到一个好的分割超平面。
- 大量的研究和实际数据分析结果表明:某些特征之间的关联信息(相关度)对事件结果的的发生会产生很大的影响。
- 为了解决数据稀疏的情况下,特征组合的问题,FM产生。(当 x i , x j x_i,x_j xi,xj同时非0时,特征组合才有意义)
- 普通的线性模型,各个特征都是独立考虑的,没有考虑到特征之间的相关性,为了描述特征间的多样性,进而采用多项式模型。
二阶多项式模型(degree=2)
回归模型
- 二阶多项式模型的表达式: 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 ⏟ 交叉项(组合特征) \hat{y}(x) := \underbrace {w_0 + \sum_{i=1}^{n} w_i x_i }_{\text{线性回归}} + \underbrace {\sum_{i=1}^{n} \sum_{j=i+1}^{n} w_{ij} x_i x_j}_{\text{交叉项(组合特征)}} \qquad y^(x):=线性回归 w0+i=1∑nwixi+交叉项(组合特征) i=1∑nj=i+1∑nwijxixj其中,n表示样本的特征数量, x i x_i xi 表示第i个特征。 w 0 ∈ R , w = { w 1 , w 2 , ⋯ , w n } ∈ R n , w i j ∈ R n × n w_0 \in R,\; w = \{w_1, w_2, \cdots, w_n\}\in R^n, w_{ij} \in R^{n \times n} w0∈R,w={w1,w2,⋯,wn}∈Rn,wij∈Rn×n
- 从上面的多项式可以看出,特征组合部分特征相关的参数有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)个。但是在数据很稀疏的情况下, x i , x j x_i,x_j xi,xj同时不为0的情况非常少,训练样本不充分,学到的参数就不是充分统计量结果,导致参数 w i j w_{ij} wij不准确,而这会严重影响模型预测的效果和稳定性。这样使得 w i j w_{ij} wij无法通过训练得出。
- 根据矩阵分解的启发,如果把多项式模型中二阶交叉项参数 w i j w_{ij} wij组成一个对称矩阵 W W W(对角元素设为正实数),那么这个矩阵就可以分解为 W = V V T V ∈ R n × k W=VV^T\;\;V \in R^{n \times k} W=VVTV∈Rn×k V V V称为系数矩阵,其中第i行对应着第i维特征的隐向量。即每个特征都由一个长度为k的隐变量替代。
FM模型
- 将每个交叉项参数 w i j w_{ij} wij用隐向量的内积< v i , v j v_i,v_j vi,vj>表示,是FM模型的核心思想。 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}(\mathbf{x}) := w_0 + \sum_{i=1}^{n} w_i x_i + \sum_{i=1}^{n} \sum_{j=i+1}^{n} \langle \mathbf{v}_i, \mathbf{v}_j \rangle x_i x_j \qquad 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 <v_i,v_j>=\sum ^k_{f=1}v_{if}v_{jf} <vi,vj>=f=1∑kvifvjf
- 线性模型+交叉项:交叉项表示组合特征,从而建立特征与结果之间的非线性关系。
- 交叉项系数 → 隐向量内积:对每一个特征分量 x i x_i xi引入隐向量 v i = ( v i , 1 , v i , 2 , ⋯ , v i , k ) \mathbf{v}_i = (v_{i,1}, v_{i,2},\cdots,v_{i,k}) vi=(vi,1,vi,2,⋯,vi,k),将每个 w i j w_{ij} wij用隐向量的内积< v i , v j v_i,v_j vi,vj>表示,然后对 w i j w_{ij} wij进行矩阵求解。
- 隐向量的长度k称为超参数,用k个描述特征的因子来表示第i维特征,二阶交叉项的参数由n⋅n个减少到n⋅k个,远少于二阶多项式模型中的参数数量。
参数学习
-
FM模型的复杂度为 O ( k n 2 ) O(kn^2) O(kn2),但是通过等价转换,可以将FM的二次项化简,其复杂度可优化到 O ( k n ) O(kn) O(kn)
∑ 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 ⟯ \sum_{i=1}^{n} \sum_{j=i+1}^{n} {\langle \mathbf{v}_i, \mathbf{v}_j \rangle} x_i x_j = \frac{1}{2} \sum_{f=1}^{k} {\left \lgroup \left(\sum_{i=1}^{n} v_{i,f} x_i \right)^2 - \sum_{i=1}^{n} v_{i,f}^2 x_i^2\right \rgroup} \qquad i=1∑nj=i+1∑n⟨vi,vj⟩xixj=21f=1∑k⎩⎪⎪⎪⎪⎪⎧(i=1∑nvi,fxi)2−i=1∑nvi,f2xi2⎭⎪⎪⎪⎪⎪⎫
-
转换后替换原FM模型 y ^ ( x ) : = w 0 + ∑ i = 1 n w i x i + 1 2 ∑ f = 1 k ⟮ ( ∑ i = 1 n v i , f x i ) 2 − ∑ i = 1 n v i , f 2 x i 2 ⟯ \hat{y}(\mathbf{x}) := w_0 + \sum_{i=1}^{n} w_i x_i + \frac{1}{2} \sum_{f=1}^{k} {\left \lgroup \left(\sum_{i=1}^{n} v_{i,f} x_i \right)^2 - \sum_{i=1}^{n} v_{i,f}^2 x_i^2\right \rgroup} \qquad\qquad\qquad y^(x):=w0+i=1∑nwixi+21f=1∑k⎩⎪⎪⎪⎪⎪⎧(i=1∑nvi,fxi)2−i=1∑nvi,f2xi2⎭⎪⎪⎪⎪⎪⎫
-
如果用随机梯度下降(Stochastic Gradient Descent)法学习模型参数。 ∂ ∂ θ y ( x ) = { 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 (交叉项) \frac{\partial}{\partial \theta} y(\mathbf{x}) = \left \{ \begin{array}{ll} 1 &; \text{if}\; \theta\; \text{is}\; w_0 \qquad \text{(常数项)} \\ x_i &; \text{if}\; \theta\; \text{is}\; w_i \;\qquad \text{(线性项)} \\ x_i \sum_{j=1}^{n} v_{j,f} x_j - v_{i,f} x_i^2, &; \text{if}\; \theta\; \text{is}\; v_{i,f} \qquad \text{(交叉项)} \end{array} \right. \qquad\quad ∂θ∂y(x)=⎩⎨⎧1xixi∑j=1nvj,fxj−vi,fxi2,;ifθisw0(常数项);ifθiswi(线性项);ifθisvi,f(交叉项)其中, v j , f v_{j,f} vj,f是隐向量 v j v_j vj的第 f f f个元素。模型参数一共有1+n+nk个。因此,FM参数训练的时间复杂度为O(kn)。
FM总结
- FM降低了交叉项参数学习不充分的影响
参数学习由之前学习交叉项参数 w i j w_{ij} wij,转变为学习n个单特征对应k维隐向量的过程。很明显,单特征参数(k维隐向量 v i v_i vi)的学习要比交叉项参数 w i j w_{ij} wij学习得更充分。
如果我们还是用 w i j w_{ij} wij的方式去学习<女性,汽车>,那么只有500个样本,而用单特征参数 v i v_i vi的方式去学习就会有大于30000个样本。因此,可以说FM降低了因数据稀疏,导致交叉项参数学习不充分的影响。 - FM提升了模型预估能力
样本中没有 < 男 性 , 化 妆 品 > <男性,化妆品> <男性,化妆品>交叉特征,即没有男性看化妆品广告的数据。如果用多项式模型来建模,对应的交叉项参数是学不出来的,因为数据中没有对应的共现交叉特征,那么多项式模型就不能对出现的男性看化妆品广告场景给出准确地预估。
由于FM学习的参数就是单特征的隐向量,那么男性看化妆品广告的预估结果可以用 ⟨ v 男 性 , v 化 妆 品 ⟩ ⟨v男性,v化妆品⟩ ⟨v男性,v化妆品⟩得到。这样,即便训练集中没有出现男性看化妆品广告的样本,FM模型仍然可以用来预估,提升了预估能力。 - FM提升了参数学习效率
这个显而易见,参数个数由 n 2 + n + 1 n^2+n+1 n2+n+1变为 n k + n + 1 nk+n+1 nk+n+1个,模型训练复杂度也由 O ( m n 2 ) O(mn^2) O(mn2)变为 O ( m n k ) O(mnk) O(mnk)。m为训练样本数。对于训练样本和特征数而言,都是线性复杂度。
此外FM模型本身是在多项式模型基础上对参数的计算做了调整,因此也有人把FM模型称为多项式的广义线性模型。 - FM模型对稀疏数据有更好的学习能力,通过交互项可以学习特征之间的关联关系,并且保证了学习效率和预估能力。
pyFM封装了实现FM基本操作的pylibfm
工具包xLearn也封装了FM和FFM
# -*- coding: utf-8 -*-
from __future__ import division
from math import exp
from numpy import *
from random import normalvariate # 正态分布
from sklearn import preprocessing
import numpy as np
'''
data : 数据的路径
feature_potenital : 潜在分解维度数
alpha : 学习速率
iter : 迭代次数
_w,_w_0,_v : 拆分子矩阵的weight
with_col : 是否带有columns_name
first_col : 首列有价值的feature的index
'''
class fm(object):
def __init__(self):
self.data = None
self.feature_potential = None
self.alpha = None
self.iter = None
self._w = None
self._w_0 = None
self.v = None
self.with_col = None
self.first_col = None
def min_max(self, data):
self.data = data
min_max_scaler = preprocessing.MinMaxScaler()
return min_max_scaler.fit_transform(self.data)
def loadDataSet(self, data, with_col=True, first_col=2):
# 我就是闲的蛋疼,明明pd.read_table()可以直接度,非要搞这样的,显得代码很长,小数据下完全可以直接读嘛,唉~
self.first_col = first_col
dataMat = []
labelMat = []
fr = open(data)
self.with_col = with_col
if self.with_col:
N = 0
for line in fr.readlines():
# N=1时干掉列表名
if N > 0:
currLine = line.strip().split()
lineArr = []
featureNum = len(currLine)
for i in range(self.first_col, featureNum):
lineArr.append(float(currLine[i]))
dataMat.append(lineArr)
labelMat.append(float(currLine[1]) * 2 - 1)
N = N + 1
else:
for line in fr.readlines():
currLine = line.strip().split()
lineArr = []
featureNum = len(currLine)
for i in range(2, featureNum):
lineArr.append(float(currLine[i]))
dataMat.append(lineArr)
labelMat.append(float(currLine[1]) * 2 - 1)
return mat(self.min_max(dataMat)), labelMat
def sigmoid(self, inx):
# return 1.0/(1+exp(min(max(-inx,-10),10)))
return 1.0 / (1 + exp(-inx))
# 得到对应的特征weight的矩阵
def fit(self, data, feature_potential=8, alpha=0.01, iter=100):
# alpha是学习速率
self.alpha = alpha
self.feature_potential = feature_potential
self.iter = iter
# dataMatrix用的是mat, classLabels是列表
dataMatrix, classLabels = self.loadDataSet(data)
print('dataMatrix:',dataMatrix.shape)
print('classLabels:',classLabels)
k = self.feature_potential
m, n = shape(dataMatrix)
# 初始化参数
w = zeros((n, 1)) # 其中n是特征的个数
w_0 = 0.
v = normalvariate(0, 0.2) * ones((n, k))
for it in range(self.iter): # 迭代次数
# 对每一个样本,优化
for x in range(m):
# 这边注意一个数学知识:对应点积的地方通常会有sum,对应位置积的地方通常都没有,详细参见矩阵运算规则,本处计算逻辑在:http://blog.csdn.net/google19890102/article/details/45532745
# xi·vi,xi与vi的矩阵点积
inter_1 = dataMatrix[x] * v
# xi与xi的对应位置乘积 与 xi^2与vi^2对应位置的乘积 的点积
inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v) # multiply对应元素相乘
# 完成交叉项,xi*vi*xi*vi - xi^2*vi^2
interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
# 计算预测的输出
p = w_0 + dataMatrix[x] * w + interaction
print('classLabels[x]:',classLabels[x])
print('预测的输出p:', p)
# 计算sigmoid(y*pred_y)-1
loss = self.sigmoid(classLabels[x] * p[0, 0]) - 1
if loss >= -1:
loss_res = '正方向 '
else:
loss_res = '反方向'
# 更新参数
w_0 = w_0 - self.alpha * loss * classLabels[x]
for i in range(n):
if dataMatrix[x, i] != 0:
w[i, 0] = w[i, 0] - self.alpha * loss * classLabels[x] * dataMatrix[x, i]
for j in range(k):
v[i, j] = v[i, j] - self.alpha * loss * classLabels[x] * (
dataMatrix[x, i] * inter_1[0, j] - v[i, j] * dataMatrix[x, i] * dataMatrix[x, i])
print('the no %s times, the loss arrach %s' % (it, loss_res))
self._w_0, self._w, self._v = w_0, w, v
def predict(self, X):
if (self._w_0 == None) or (self._w == None).any() or (self._v == None).any():
raise NotFittedError("Estimator not fitted, call `fit` first")
# 类型检查
if isinstance(X, np.ndarray):
pass
else:
try:
X = np.array(X)
except:
raise TypeError("numpy.ndarray required for X")
w_0 = self._w_0
w = self._w
v = self._v
m, n = shape(X)
result = []
for x in range(m):
inter_1 = mat(X[x]) * v
inter_2 = mat(multiply(X[x], X[x])) * multiply(v, v) # multiply对应元素相乘
# 完成交叉项
interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
p = w_0 + X[x] * w + interaction # 计算预测的输出
pre = self.sigmoid(p[0, 0])
result.append(pre)
return result
def getAccuracy(self, data):
dataMatrix, classLabels = self.loadDataSet(data)
w_0 = self._w_0
w = self._w
v = self._v
m, n = shape(dataMatrix)
allItem = 0
error = 0
result = []
for x in range(m):
allItem += 1
inter_1 = dataMatrix[x] * v
inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v) # multiply对应元素相乘
# 完成交叉项
interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
p = w_0 + dataMatrix[x] * w + interaction # 计算预测的输出
pre = self.sigmoid(p[0, 0])
result.append(pre)
if pre < 0.5 and classLabels[x] == 1.0:
error += 1
elif pre >= 0.5 and classLabels[x] == -1.0:
error += 1
else:
continue
# print(result)
value = 1 - float(error) / allItem
return value
class NotFittedError(Exception):
"""
Exception class to raise if estimator is used before fitting
"""
pass
if __name__ == '__main__':
fm()