推荐算法(一)——FM因式分解机

1 介绍

FMFactorization Machines因子分解机)早在2010年提出,作为逻辑回归模型的改进版,拟解决在稀疏数据的场景下模型参数难以训练的问题。并且考虑了特征的二阶交叉,弥补了逻辑回归表达能力差的缺陷。

FM 作为推荐算法广泛应用于推荐系统及计算广告领域,通常用于预测点击率 CTR(click-through rate)和转化率 CVR(conversion rate)。

2 原理

逻辑回归为普通的线性模型,优点是复杂度低、方便求解,但缺点也很明显,没有考虑特征之间的交叉,表达能力有限。

在这里插入图片描述
FM在线性模型的基础上添加了一个多项式,用于描述特征之间的二阶交叉。

在这里插入图片描述

  • n n n 表示一个样本的特征的个数(类别特征 onehot 之后的维度),两两交互可得到 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2 个交叉项;
  • w i j w_{ij} wij 是组合对应的权重,用于表征这对组合的重要性,多项式要学习的参数即为 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2 w w w 系数。

2.1 存在的问题:

参数 w i j w_{ij} wij 学习困难,因为对 w i j w_{ij} wij 进行更新时,求得的梯度对应为 x i x j x_{i}x_{j} xixj,当且仅当 x i x_{i} xi x j x_{j} xj 都非0时参数才会得到更新。

但是经过 onehot 处理的数据非常稀疏,能够保证两者都非0的组合较少,导致大部分参数 w w w 难以得到充分训练。

2.2 解决方法:

作者对每个特征分量 x i x_{i} xi 引入 k k k 维(k<<n) 辅助向量 v i = ( v i 1 , v i 2 , . . . , v i k ) v_{i}=(v_{i1},v_{i2},...,v_{ik}) vi=(vi1,vi2,...,vik),每个特征对应一个总共 n n n 个向量,然后利用向量内积的结果 v i v j T v_{i}v_{j}^{T} vivjT 来表示原来的组合参数 w i j w_{ij} wij.
在这里插入图片描述
于是,原式变成了如下形式:(尖括号表示内积)

在这里插入图片描述

这样要学习的参数从 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2 w i j w_{ij} wij 系数变成了元素个数为 n ∗ k n*k nk V V V 矩阵,因为 k < < n k<<n k<<n, 所以该做法降低了训练复杂度。

在这里插入图片描述

此外,引入辅助向量削弱了参数间的独立性,因为对于 x i x_{i} xi 的隐向量 v i v_{i} vi,任何包含 x i x_{i} xi 的特征组合,只要 x i x_{i} xi 本身不为0,都可对 v i v_{i} vi 进行更新,同理每个隐向量都能得到充分的学习,这样就解决了数据稀疏带来的难以训练问题。

2.3 复杂度分析

为了进一步降低算法复杂度,对多次项进行如下化简:
在这里插入图片描述
在这里插入图片描述
对需要训练的参数 θ \theta θ 求梯度得:

在这里插入图片描述
重点关注 v i f v_{if} vif 的梯度, v i f v_{if} vif 表示 x i x_{i} xi 的隐向量,因为梯度项 ∑ j = 1 n v j , f x j \sum_{j=1}^{n} v_{j, f} x_{j} j=1nvj,fxj 中不包含 i i i,只与 f f f 有关,因此只要一次性求出所有的 f f f ∑ j = 1 n v j , f x j \sum_{j=1}^{n} v_{j, f} x_{j} j=1nvj,fxj 的值(复杂度 O ( n k ) O(nk) O(nk)),在求每个参数的梯度时都可复用该值。

当已知 ∑ j = 1 n v j , f x j \sum_{j=1}^{n} v_{j, f} x_{j} j=1nvj,fxj 时计算每个参数梯度的复杂度是 O ( 1 ) O(1) O(1),因此训练FM模型的复杂度也是 O ( n k ) O(nk) O(nk)

化简之后,FM的复杂度从 O ( n 2 k ) O(n^{2}k) O(n2k) 降到线性的 O ( n k ) O(nk) O(nk),更利于上线使用。

2.4 扩展到多维 FM:

在这里插入图片描述
将二阶交叉项转换成多阶交叉项,进一步提升模型的表达能力。跟二阶交叉项相同,多阶交叉项也可从 O ( n l k ) O(n^{l}k) O(nlk) 复杂度降到线性的 O ( n k ) O(nk) O(nk),具有非常好的性质。

3 代码

理论结合代码食用更佳, 代码中会加入充分注释,以易理解。(整体代码可参考Github仓库)

FM 层代码:

(将 FM 封装成 Layer,随后在搭建 Model 时直接调用即可)

import tensorflow as tf
import tensorflow.keras.backend as K

class FM_layer(tf.keras.layers.Layer):
    def __init__(self, k, w_reg, v_reg):
        super(FM_layer, self).__init__()
        self.k = k	 # 隐向量vi的维度
        self.w_reg = w_reg  # 权重w的正则项系数
        self.v_reg = v_reg  # 权重v的正则项系数

    def build(self, input_shape): # 需要根据input来定义shape的变量,可在build里定义)
        self.w0 = self.add_weight(name='w0', shape=(1,), # shape:(1,)
                                 initializer=tf.zeros_initializer(),
                                 trainable=True,)
        self.w = self.add_weight(name='w', shape=(input_shape[-1], 1), # shape:(n, 1)
                                 initializer=tf.random_normal_initializer(), # 初始化方法
                                 trainable=True, # 参数可训练
                                 regularizer=tf.keras.regularizers.l2(self.w_reg)) # 正则化方法
        self.v = self.add_weight(name='v', shape=(input_shape[-1], self.k), # shape:(n, k)
                                 initializer=tf.random_normal_initializer(),
                                 trainable=True,
                                 regularizer=tf.keras.regularizers.l2(self.v_reg))

    def call(self, inputs, **kwargs):
    	# inputs维度判断,不符合则抛出异常
        if K.ndim(inputs) != 2:
            raise ValueError("Unexpected inputs dimensions %d, expect to be 2 dimensions" % (K.ndim(inputs)))
		
		# 线性部分,相当于逻辑回归
        linear_part = tf.matmul(inputs, self.w) + self.w0   #shape:(batchsize, 1)
        # 交叉部分——第一项
        inter_part1 = tf.pow(tf.matmul(inputs, self.v), 2)  #shape:(batchsize, self.k)
        # 交叉部分——第二项
        inter_part2 = tf.matmul(tf.pow(inputs, 2), tf.pow(self.v, 2)) #shape:(batchsize, k)
        # 交叉结果
        inter_part = 0.5*tf.reduce_sum(inter_part1 - inter_part2, axis=-1, keepdims=True) #shape:(batchsize, 1)
		# 最终结果
        output = linear_part + inter_part
        return tf.nn.sigmoid(output) #shape:(batchsize, 1)

定义好了 FM 层,模型搭建就简单了,Model 代码如下:

class FM(tf.keras.Model):
    def __init__(self, k, w_reg=1e-4, v_reg=1e-4):
        super(FM, self).__init__()
        self.fm = FM_layer(k, w_reg, v_reg) # 调用写好的FM_layer

    def call(self, inputs, training=None, mask=None):
        output = self.fm(inputs)  # 输入FM_layer得到输出
        return output 

数据处理代码: 实验数据点我

import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split

def create_criteo_dataset(file_path, test_size=0.3):
    data = pd.read_csv(file_path)
    dense_features = ['I' + str(i) for i in range(1, 14)]  # 数值特征
    sparse_features = ['C' + str(i) for i in range(1, 27)] # 类别特征

    # 缺失值填充
    data[dense_features] = data[dense_features].fillna(0)
    data[sparse_features] = data[sparse_features].fillna('-1')

    # 归一化(数值特征)
    data[dense_features] = MinMaxScaler().fit_transform(data[dense_features])
    # onehot编码(类别特征)
    data = pd.get_dummies(data)

    #数据集划分
    X = data.drop(['label'], axis=1).values
    y = data['label']
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size)
    return (X_train, y_train), (X_test, y_test)

模型训练代码:

from model import FM
from utils import create_criteo_dataset

import tensorflow as tf
from tensorflow.keras import optimizers, losses, metrics
from sklearn.metrics import accuracy_score

if __name__ == '__main__':
    file_path = 'data\train.txt' # 修改为自己的路径
    (X_train, y_train), (X_test, y_test) = create_criteo_dataset(file_path, test_size=0.2)
    k = 8	
    w_reg = 1e-5
    v_reg = 1e-5

    model = FM(k, w_reg, v_reg)
    optimizer = optimizers.SGD(0.01)

    summary_writer = tf.summary.create_file_writer('.\tensorboard') # tensorboard可视化文件路径
    for epoch in range(100):
        with tf.GradientTape() as tape:
            y_pre = model(X_train)  # 前馈得到预测值
            loss = tf.reduce_mean(losses.binary_crossentropy(y_true=y_train, y_pred=y_pre))  # 与真实值计算loss值
            print('epoch: {} loss: {}'.format(epoch, loss.numpy())) 
	        grad = tape.gradient(loss, model.variables)	# 根据loss计算模型参数的梯度
	        optimizer.apply_gradients(grads_and_vars=zip(grad, model.variables)) # 将梯度应用到对应参数上进行更新
        # 需要tensorboard记录的变量(不需要可视化可将该模块注释掉)
        with summary_writer.as_default():
            tf.summary.scalar("loss", loss, step=epoch)
    #评估
    pre = model(X_test)
    pre = [1 if x>0.5 else 0 for x in pre]
    print("AUC: ", accuracy_score(y_test, pre))  # AUC: 0.772

tensorboard的可视化结果如下: (可继续加大 epoch 数)

在这里插入图片描述

4 总结

看到这,相信你对 FM 也有了基本的了解。下面是对优缺点的总结:

优点:

  1. 将二阶交叉特征考虑进来,提高模型的表达能力;
  2. 引入隐向量,缓解了数据稀疏带来的参数难训练问题;
  3. 模型复杂度保持为线性,并且改进为高阶特征组合时,仍为线性复杂度,有利于上线应用。

缺点:

  1. 虽然考虑了特征的交叉,但是表达能力仍然有限,不及深度模型;
  2. 同一特征 x i x_{i} xi 与不同特征组合使用的都是同一隐向量 v i v_{i} vi,违反了特征与不同特征组合可发挥不同重要性的事实。

下篇文章将会是 FM 算法的改进版本 FFM,敬请期待…

写在最后

一直都想开一个推荐系统专栏,记录一些经典推算法的原理介绍及代码实践。但忙于学业,一直拖到了现在才产出专栏的第一篇文章,之后陆续发表其他算法的文章.

如果你想系统的学习推荐算法,原理可参考我的知乎专栏,代码的实现可参考Github仓库

码字不易,读完此文的你,如果感觉有收获,就请点个赞吧~

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值