因子分解机模型(Factorization Machines)
FM模型的基本原理
FM模型的引入
country | festival | checked |
---|---|---|
China | Chinese new year | 1 |
China | Thanksgiving | 0 |
American | Thanksgiving | 1 |
从以上表格可以看出,American”与“Thanksgiving”、“China”与“Chinese New Year”这样的关联特征,对用户的点击有着正向的影响。换句话说,来自“China”的用户很可能会在“Chinese New Year”有大量的浏览、购买行为,而在“Thanksgiving”却不会有特别的消费行为。
再比如,用户更常在饭点的时间下载外卖app,因此,引入两个特征的组合是非常有意义的。
FM模型的方程
首先我们定义特征的组合含义如下:
(1)一阶特征:即单个特征,不产生新特征,如 x 1 x_{1} x1
(2)二阶特征:即两个特征组合产生的新特征,如 x 1 x 2 x_{1}x_{2} x1x2
(3)高阶特征:即两个以上的特征组合产生的新特征,如 x 1 x 2 x 3 x_{1}x_{2}x_{3} x1x2x3
一阶特征的线性回归方程:
y ^ = w 0 + ∑ i = 1 n w i x i \hat{y}=w_{0}+\sum_{i=1}^{n}{w_{i}x_{i}} y^=w0+i=1∑nwixi
由于一阶线性回归模型过于简单,欠拟合现象严重;于是我们引入二阶线性回归方程:
y ^ = 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}=w_{0}+\sum_{i=1}^{n}{w_{i}x_{i}}+\sum_{i=1}^{n}{\sum_{j=i+1}^{n}{w_{ij}x_{i}x_{j}}} y^=w0+i=1∑nwixi+i=1∑nj=i+1∑nwijxixj
参数说明:
x i x j x_{i}x_{j} xixj:表示两个互异特征组合的二阶特征
w i j w_{ij} wij:表示二阶特征的交叉项系数
但上述二阶线性回归模型存在致命缺陷:
在互联网场景下用户的行为记录是极其稀疏的,而训练二阶特征组合系数 w i j w_{ij} wij,需要大量特征分量 x i x_{i} xi和 x j x_{j} xj都非零的样本;显然在互联网场景下无法满足。
为了克服模型无法在稀疏数据场景下学习二阶特征系数 w i j w_{ij} wij的问题,我们可以引用矩阵分解模型中使用的思想,将每个特征用k维的隐向量来表示。
例如,可以将i特征用隐向量表示为 V i V_{i} Vi,将j特征用隐向量表示为 V j V_{j} Vj,而将 w i j w_{ij} wij表示为隐向量 V i V_{i} Vi和 V j V_{j} Vj的内积,即:
w i j = < V i , V j > = ∑ f = 1 k V i f ∗ V j f w_{ij}=<V_{i},V_{j}>=\sum_{f=1}^{k}{V_{if}*V_{jf}} wij=<Vi,Vj>=f=1∑kVif∗Vjf
因此,二阶线性回归方程可如下表示:
y ^ = 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}=w_{0}+\sum_{i=1}^{n}{w_{i}x_{i}}+\sum_{i=1}^{n}{\sum_{j=i+1}^{n}{<V_{i},V_{j}>x_{i}x_{j}}} y^=w0+i=1∑nwixi+i=1∑nj=i+1∑n<Vi,Vj>xixj
参数说明:
w 0 w_{0} w0:FM模型的偏置量 1个
w i w_{i} wi:FM模型的第 i i i个特征的一阶权重系数 n n n个
V i V_{i} Vi: FM模型第 i i i个特征的二阶权重系数 n ∗ k n*k n∗k个
参数的总个数: 1 + n + n ∗ k 1+n+n*k 1+n+n∗k
原因:
我们采用将 w i j w_{ij} wij分解的方法,使得不同的特征对不再是完全独立的,而它们的关联性可以用隐式因子表示,这将使得有更多的数据可以用于模型参数的学习。比如 x i , x j x_{i},x_{j} xi,xj与 x i , x k x_{i},x_{k} xi,xk的参数分别为 < V i , V j > <V_{i},V_{j}> <Vi,Vj>和 < V i , V k > <V_{i},V_{k}> <Vi,Vk>,它们都可以用来学习 V i V_{i} Vi,更一般的,包含 x i x j ≠ 0 & i ≠ j x_{i}x_{j}\ne0 \And i\ne{j} xixj=0&i=j的所有样本都可以用来学习 V i V_{i} Vi,很大程度上避免了数据稀疏的影响
二阶特征项公式简化
参数训练
最优化目标函数
对于分类问题,损失函数可以取logit逻辑函数:
l o s s ( y ^ , y ) = l o g ( 1 + e − y ^ y ) loss(\hat{y}, y)=log(1+e^{-\hat{y}y}) loss(y^,y)=log(1+e−y^y)
说明:logit损失函数可以把分类错误的损失值放大,分类正确的损失值缩小,从而更加关注那些被分错的对象
根据损失函数我们可以构造目标函数:
o b j = ∑ i = 1 m l o g ( 1 + e − y i ^ y i ) obj=\sum_{i=1}^{m}{log(1+e^{-\hat{y_{i}}y_{i}})} obj=i=1∑mlog(1+e−yi^yi)
因此最优化目标函数:
θ ∗ = a r g m i n ∑ i = 1 m l o g ( 1 + e − y i ^ y i ) \theta^{*}=argmin\sum_{i=1}^{m}{log(1+e^{-\hat{y_{i}}y_{i}})} θ∗=argmini=1∑mlog(1+e−yi^yi)
最优化目标函数求偏导
目标函数对模型参数的偏导数通式:
∂ l o s s ( y i ^ ( X ⃗ ) , y i ) ∂ θ = ∂ l o s s ( y i ^ ( X ⃗ ) , y i ) ∂ y i ^ ( X ⃗ ) ∂ y i ^ ( X ⃗ ) ∂ θ \frac{\partial loss(\hat{y_{i}}(\vec{X}),y_{i})}{\partial\theta}=\frac{\partial loss(\hat{y_{i}}(\vec{X}),y_{i})}{\partial\hat{y_{i}}(\vec{X})}\frac{\partial\hat{y_{i}}(\vec{X})}{\partial\theta} ∂θ∂loss(yi^(X),yi)=∂yi^(X)∂loss(yi^(X),yi)∂θ∂yi^(X)
其中:
∂ l o s s ( y i ^ ( X ⃗ ) , y i ) ∂ y i ^ ( X ⃗ ) = − y i 1 + e y i ^ y i \frac{\partial loss(\hat{y_{i}}(\vec{X}),y_{i})}{\partial\hat{y_{i}}(\vec{X})}=\frac{-y_{i}}{1+e^{\hat{y_{i}}y_{i}}} ∂yi^(X)∂loss(yi^(X),yi)=1+eyi^yi−yi
对于FM模型而言,优化的参数为: θ ∗ = { w 0 , w , V } \theta^{*} = \{w_{0}, \mathbf{w}, \mathbf{V}\} θ∗={
w0,w,V},则FM模型方程对各个参数 θ ∗ \theta^{*} θ∗的偏导数:
FM模型的python实现
说明:本程序使用的数据集在文章末尾的百度云链接里
注意:
1.sigmoid函数中指数运算溢出的问题
2.logit损失函数中指数运算溢出问题
溢出问题的解决方法请看解决sigmoid/softmax指数运算溢出问题及python实现
3.描绘图像时字符的乱码问题
import numpy as np
import pandas as pd
import time
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
# 解决字符显示乱码问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 将预测值映射到0~1区间,解决指数函数的溢出问题
def sigmoid(x):
if x >= 0:
return 1 / (1 + np.exp(-x))
else:
return np.exp(x) / (1 + np.exp(x))
# 计算logit损失函数
def logit(y, y_hat):
z = y * y_hat
if z >= 0:
return np.log(1 + np.exp(-z))
else:
return np.log(1 + np.exp(z)) - z
# 计算logit损失函数的外层偏导数(不对y_hat本身求导)
def df_logit(y, y_hat):
return sigmoid(-y * y_hat) * (-y)
'''
# FM的模型方程:LR线性组合+特征交叉项组合 = 一阶线性组合 + 二阶线性组合
参数说明:
w_0:FM模型的偏执系数
W:FM模型的一阶特征组合权重系数 n *1
V:FM模型的二阶特征组合权重系数 n * k
'''
def FM(X_i, w_0, W, V):
# 样本X_i的特征分量xi和xj的2阶交叉项组合系数wij = xi和xj对应的隐向量Vi和Vj的内积
# 向量形式:Wij=<Vi,Vj> * xi * xj
interaction = np.sum((X_i.dot(V)) ** 2 - (X_i ** 2).dot(V ** 2)) / 2
y_hat = w_0 + X_i.dot(W) + interaction
return y_hat[0]
# SGD更新FM模型的参数列表,[w_0, W, V]
def FM_SGD(X, y, k=2, alpha=0.02, iter=45):
# m:用户数量,n:特征数量
m, n = np.shape(X)
# w_0,W参数初始化
w_0, W = 0, np.zeros((n, 1))
# 参数V初始化: V=(n, k)~N(0,1)
V = np.random.normal(loc=0, scale=1, size=(n, k))
# SGD结束标识
flag = 1
# 前一次迭代的总损失值
loss_total_old = 0
# FM模型的参数列表[w_0, W, V]
all_FM_params = []
# SGD开始时间
st = time.time()
# SGD结束条件1:满足最大迭代次数
for step in range(iter):
# 本次迭代的总损失值
loss_total_new = 0
# 遍历训练集
for i in range(m):
# 计算第i用户的预测值
y_hat = FM(X[i], w_0=w_0, W=W, V=V)
loss_total_new += logit(y[i], y_hat)
# logit损失函数的外层偏导数
df_loss = df_logit(y[i], y_hat)
# logit损失函数对w_0的偏导数
df_w0_loss = df_loss
# 更新参数w_0
w_0 = w_0 - alpha * df_w0_loss
# 遍历所有特征
for j in range(n):
# 若第i个用户在第j个特征取值为0, 则不执行参数更新
if X[i, j] == 0:
continue
# logit损失函数对Wij的偏导数
df_Wij_loss = df_loss * X[i, j]
# 更新参数W[j]
W[j] = W[j] - alpha * df_Wij_loss
# 遍历k维隐向量Vj
for f in range(k):
# logit损失函数对Vjf的偏导数
df_Vjf_loss = df_loss * X[i, j] * (X[i].dot(V[:, f]) - X[i, j] * V[j, f])
# 更新参数V[j, f]
V[j, f] = V[j, f] - alpha * df_Vjf_loss
# SGD结束条件2:损失值过小,跳出
if loss_total_new < 1e-2:
flag = 2
all_FM_params.append([w_0, W, V])
print("the total step:%d\n the loss is:%.6f" % ((step+1), loss_total_new))
break
# 第一次迭代,不计算前后损失值之差
if step == 0:
loss_total_old = loss_total_new
continue
# SGD结束条件3:前后损失值之差过小,跳出
if (loss_total_old - loss_total_new) < 1e-5:
flag = 3
all_FM_params.append([w_0, W, V])
print("the total step:%d\n the loss is:%.6f" % ((step + 1), loss_total_new))
break
else:
loss_total_old = loss_total_new
if step % 10 == 0:
print("the step is :%d\t the loss is:%.6f" % ((step+1), loss_total_new))
all_FM_params.append([w_0, W, V])
# SGD结束时间
et = time.time()
print("the total time:%.4f\nthe type of jump out:%d" % ((et - st), flag))
return all_FM_params
# FM模型进行预测
def FM_predict(X, w_0, W, V, ):
m = X.shape[0]
# sigmoid函数阙值设置
predicts, threshold = [], 0.5
# 遍历测试集
for i in range(m):
# X[i]的预测值
y_hat = FM(X_i=X[i], w_0=w_0, W=W, V=V)
# 分类非线性映射
predicts.append(-1 if sigmoid(y_hat) < threshold else 1)
return np.array(predicts)
# 计算预测准确度
def accuracy_score(Y, predicts):
# 预测准确数量
hits_count = 0
for i in range(Y.shape[0]):
if Y[i] == predicts[i]:
hits_count += 1
score_acc = hits_count / Y.shape[0]
return score_acc
# 根据FM模型每次迭代得到的参数[w_0, W, V]描绘预测准确率及损失值变化曲线
def draw_research(all_FM_params, X_train, y_train, X_test, y_test):
# loss_total_all记录使用每次迭代参数计算的损失值
# acc_total_all记录使用每次迭代参数计算的预测准确度
loss_total_all, acc_total_all = [], []
# 遍历每次迭代生成的参数值
for w_0, W, V in all_FM_params:
loss_total = 0
# 计算使用某一参数得到的总损失
for i in range(X_train.shape[0]):
loss_total += logit(y=y_train[i], y_hat=FM(X_i=X_train[i], w_0=w_0, W=W, V=V))
loss_total_all.append(loss_total / X_train.shape[0])
acc_total_all.append(accuracy_score(Y=y_test, predicts=FM_predict(X=X_test, w_0=w_0, W=W, V=V)))
# 描绘训练集损失值的变化曲线
plt.plot(np.arange(len(all_FM_params)), loss_total_all, color='#FF4040', label='训练集的损失值')
plt.plot(np.arange(len(all_FM_params)), acc_total_all, color='#4876FF', label='测试集的预测准确率')
plt.xlabel("SGD迭代次数")
plt.title("FM模型:二阶互异特征组合")
# 给图像加图例
plt.legend()
plt.show()
if __name__ == '__main__':
# 产生一个随机序列
np.random.seed(123)
data = pd.read_csv("D:\\YSA\\dataFile\\xg.csv", sep=',')
# 将数据集中的Class特征中的值0映射为-1,1映射为1
data['Class'] = data['Class'].map({
0: -1, 1: 1})
# 切分数据集为训练集、测试集
X_train, X_test, y_train, y_test = train_test_split(data.iloc[:, :-1].values, data.iloc[:, -1].values, test_size=0.3, random_state=123)
# 训练集归一化处理
X_train = MinMaxScaler().fit_transform(X_train)
# 测试集归一化处理
X_test = MinMaxScaler().fit_transform(X_test)
# FM模型的参数列表[w_0, W, V]
all_FM_params = FM_SGD(X_train, y_train, k=2, alpha=0.01, iter=45)
# 最终的参数值w_0, W, V
w_0, W, V = all_FM_params[-1]
# 测试集的预测结果
predicts = FM_predict(X_test, w_0=w_0, W=W, V=V)
# 预测的准确度
acc = accuracy_score(Y=y_test, predicts=predicts)
print("测试集的预测准确度:%04f" % acc)
draw_research(all_FM_params, X_train, y_train, X_test, y_test)
数据集
数据集在这里
提取码:nbil