因子分解机模型FM原理与python实现

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=1nwixi
由于一阶线性回归模型过于简单,欠拟合现象严重;于是我们引入二阶线性回归方程:
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=1nwixi+i=1nj=i+1nwijxixj
参数说明:
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=1kVifVjf
因此,二阶线性回归方程可如下表示:
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=1nwixi+i=1nj=i+1n<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 nk
参数的总个数: 1 + n + n ∗ k 1+n+n*k 1+n+nk
原因
我们采用将 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+ey^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=1mlog(1+eyi^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=1mlog(1+eyi^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^yiyi
对于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

参考

1.FM模型的算法思想
2.『我爱机器学习』FM、FFM与DeepFM

猜你喜欢

转载自blog.csdn.net/shiaiao/article/details/109130094