• 从零开始的FM(1)


    一、CTR中的特征交互

    在计算广告和推荐系统中,CTR预估(click-through rate)是非常重要的一个环节,判断一个商品的是否进行推荐需要根据CTR预估的点击率来进行。在进行CTR预估时,除了单特征外,往往要对特征进行组合。
    普通的线性模型,我们都是将各个特征独立考虑的,并没有考虑到特征与特征之间的相互关系。但实际上,大量的特征之间是有关联的。最简单的以电商为例,一般女性用户看化妆品服装之类的广告比较多,而男性更青睐各种球类装备。那很明显,女性这个特征与化妆品类服装类商品有很大的关联性,男性这个特征与球类装备的关联性更为密切。如果我们能将这些有关联的特征找出来,显然是很有意义的。
    FM,完成了上述特征交互。

    二、从线性模型到FM

    1、线性模型

    [y = w_0 + sum_{i=1}^{n} w_ix_i ag{1} ]

    一般的线性模型没有考虑特征间的关联。

    2、多项式模型

    [y = w_0 + sum_{i=1}^{n} w_ix_i + sum_{i=1}^{n-1}sum_{j=i+1}^{n}w_{ij}x_ix_j ag{2} ]

    上式中,n表示样本的特征数量,(x_i)表示第(i)个特征的值。注意:一般而言,特征只会和其它特征组合,而不会和自己组合。所以(j)的取值不会和(i)有重叠。组合部分的特征相关参数共有(n(n−1)/2)

    3、多项式模型存在的问题

    CTR数据集是数据稀疏的,其稀疏性原因:类别特征需要进行one-hot编码。假设淘宝或者京东上的item为100万,如果对item这个维度进行one-hot编码,光这一个维度数据的稀疏度就是百万分之一。

    数据稀疏带来的问题:(w_i)(w_j)都不为0的情况非常少,这样就导致(w_{ij})无法训练得出。即:没有出现交互的特征组合,不能对对应的参数进行估计。

    4、问题解决思路

    由3可知,当(x_i)为0,(w_{ij})就无法训练更新,
    利用矩阵分解的思路,对二阶交叉特征的系数进行调整,让系数不在是独立无关的,从而减少模型独立系数的数量。
    (x_i)引入一个辅助向量,由隐向量(v_i=(v_{i1},v_{i2},⋯,v_{ik}))来代表(x_i)(k)为隐向量维度。

    得到隐向量embedding矩阵(V):

    [V= left( egin{array}{cccc} v_{11} & v_{12} &cdots &v_{1k} \ v_{21} & v_{22} & cdots &v_{2k}\ vdots & vdots &&vdots\ v_{n1} & v_{n2} & cdots & v_{nk} end{array} ight)_{n imes k} = left( egin{array}{cccc} V1 \ V2 \ vdots \ Vn end{array} ight) ]

    (V)的第(j)行就是第(j)个特征的隐向量。

    这样(w_{ij})变为:

    [hat{W}=VV^T = left( egin{array}{cccc} V1 \ V2 \ vdots \ Vn end{array} ight) left( egin{array}{cccc} V1 & V2 &cdots &V_n end{array} ight) ]

    为何可以完成上述矩阵分解:

    对于任意对称的正定矩阵(W),只要(k)足够大,一定存在一个矩阵(V),使得(W=VV^T)
    首先,(hat{W})的每个元素都是两个向量的内积,所以一定是对称的。
    第二,如何保证(hat{W})的正定性:我们只关心互异特征分量之间的相互关系,因此(hat{W})的对角线元素是任意取值的,只需将它们取得足够大(例如保证行元素满足严格对角占优),就可以保证(hat{W})的正定性。
    理论上,我们要求参数(k)取得足够大,但是,在高度稀疏的数据场景中,由于没有足够的样本来估计复杂的交互矩阵,因为(k)通常应取得很小。

    此时,公式(2)就变为:

    [y = w_0 + sum_{i=1}^{n} w_ix_i + sum_{i=1}^{n-1}sum_{j=i+1}^{n}<V_i,V_j>x_ix_j ag{3} ]

    其中(<V_i,V_j>)表示两个(k)维向量的点乘:

    [<V_i,V_j> = sum_{f=1}^{k} v_{i,f}v_{j,f} ]

    三、FM公式解析

    1、复杂度分析

    FM模型公式(3)需要估计的参数包括:

    [w_0 in R, w in R^n,V in R^{n imes k} ]

    共有(1+n+nk)个。其中,(w_0)为整体的偏置量,(w)对特征向量的各个分量的强度进行建模,(V)对特征向量中任意两个分量之间的关系进行了建模。

    其时间复杂度:(O(kn^2))

    [{frac{n(n+1)}{2}[k+(k-1)+2] + frac{n(n-1)}{2}-1} = O(kn^2) ]

    花括号分别对应二阶项的加法项和乘法项。

    2、二阶项公式变换

    通过变换降低其时间复杂度。


    如上图所示,FM二阶项为图中绿色部分。而全图包含了 (2*绿色部分 + 对角线)
    所以:

    [ egin{align} sum_{i=1}^{n-1}{sum_{j=i+1}^{n}{<v_i,v_j>x_ix_j}} &= frac{1}{2}sum_{i=1}^{n}{sum_{j=1}^{n}{<v_i,v_j>x_ix_j}} - frac{1}{2} {sum_{i=1}^{n}{<v_i,v_i>x_ix_i}} \ &= frac{1}{2} left( sum_{i=1}^{n}{sum_{j=1}^{n}{sum_{f=1}^{k}{v_{i,f}v_{j,f}x_ix_j}}} - sum_{i=1}^{n}{sum_{f=1}^{k}{v_{i,f}v_{i,f}x_ix_i}} ight) \ &= frac{1}{2}sum_{f=1}^{k}{left[ left( sum_{i=1}^{n}{v_{i,f}x_i} ight) cdot left( sum_{j=1}^{n}{v_{j,f}x_j} ight) - sum_{i=1}^{n}{v_{i,f}^2 x_i^2} ight]} \ &= frac{1}{2}sum_{f=1}^{k}{left[ left( sum_{i=1}^{n}{v_{i,f}x_i} ight)^2 - sum_{i=1}^{n}{v_{i,f}^2 x_i^2} ight]} end{align} ]

    变换之后的时间复杂度为:(O(kn))

    3、导数和损失函数

    经过变换,最终,FM模型公式为:

    [hat{y} = w_0 + sum_{i=1}^{n} w_ix_i + frac{1}{2}sum_{f=1}^{k}{[ ( sum_{i=1}^{n}{v_{i,f}x_i} )^2 - sum_{i=1}^{n}{v_{i,f}^2 x_i^2} ]} ag{4} ]

    3.1 导数

    对三个参数的导数为:

    [ egin{equation} frac{partial hat{y}(x)}{partial heta} = left{egin{array} {lr} 1, & if heta is omega_{0} \ x_{i}, & if heta is omega_i \ x_{i} sum_{j=1}^{n}{v_{j,f} x_{j} - v_{i,f} x_{i}^2} & if heta is v_{i,f} end{array} ight. end{equation} ag{5} ]

    (w_0,w)的求导很好理解,对(v_{i,f})的求导,其推导过程:

    [egin{align} frac{partial frac{1}{2}sum_{f=1}^k[(sum_{i=1}^nv_{i,f}x_i)^2-sum_{i=1}^nv_{i,f}^2x_i^2]}{partial v_{i,f}} &= frac{1}{2}[frac{partial ((sum_{i=1}^nv_{i,f}x_i)^2) }{partial v_{i,f}} frac{partial (sum_{i=1}^nv_{i,f}x_i) }{partial v_{i,f}} - frac{partial (sum_{i=1}^nv_{i,f}^2x_i^2) }{partial v_{i,f}}] \ &= (sum_{j=1}^n v_{j,f}x_j)*x_i - v_{i,f}x_i^2 \ &= x_isum_{j=1}^nv_{j,f}x_j-v_{i,f}x_i^2 end{align} ]

    为何(i)变成了(j),前半部分求和和i无关,换个符号以便区分。

    3.2 损失函数

    FM用于CTR时,借助于LR,相当于二分类任务,使用交叉熵损失函数。

    [hat{h} = heta (hat{y}) = frac{1}{1+e^{-hat{y}}} ]

    (hat{y})为FM模型的输出,(hat{h})为加上sigmoid函数之后的模型最终输出。
    1、当(h in {0,1}),用于CTR,则交叉熵损失函数为:

    [Loss=-sum_{i=1}^m [h^{(i)}log hat{y}^{(i)} + (1-h^{(i)})log(1-hat{h}^{(i)}] ]

    2、当 (h in {-1,1}),用于二分类则交叉熵损失函数,等价于Logit loss为:

    [Loss = -sum_{i=1}^m lndelta(y^{(i)}hat{y}^{(i)}) ]

    3.3 随机梯度下降过程的参数更新

    (h in {-1,1}) 为例,三个参数导数为:

    [egin{align} frac{partial L}{partial heta} &= -frac{1}{delta(yhat{y})}delta(yhat{y})cdot [1-delta(yhat{y}]cdot y cdot frac{partial hat{y}}{partial heta} \ &= [delta(yhat{y}-1]cdot y cdot frac{partial hat{y}}{partial heta} end{align} ]

    可知,在求梯度过程中,三个参数都有([delta(yhat{y}-1]cdot y)这一公共部分。
    参数更新公式:

    [hat{ heta} = heta - alpha cdot frac{partial L}{partial heta} ]

    四、python实现FM二分类

    已知模型公式和参数更新公式,如何转换成对应的python代码

    1、参数初始化

    w_0 = 0.
    w = zeros((n, 1))  # 其中n是特征的个数
    v = normalvariate(0, 0.2) * ones((n, k)) # embedding矩阵V
    

    2、前项传播:公式转换为矩阵乘法

    假设样本矩阵为dataMatrix,其形状为(m*n),表示m个样本,每个样本有n个特征。则 dataMatrix[i] 为第i个样本。
    classLabels为标签列表。classLabels[i]为第i个样本的真实label。

    一阶项,相乘然后求和,变为矩阵点积。对应点积的地方通常会有sum,对应位置积的地方通常都没有

    linear_part = dataMatrix[x] * w
    

    二阶交叉项:第一部分相乘求和,变为矩阵点积。第二部分,先各自完成对应元素相乘,然后矩阵点积。

    # xi·vi,xi与vi的矩阵点积
    inter_1 = dataMatrix[i] * v # x,int。表示第几个样本的特征向量
    # xi与xi的对应位置乘积   与   xi^2与vi^2对应位置的乘积    的点积
    inter_2 = multiply(dataMatrix[i], dataMatrix[i]) * multiply(v, v)  # multiply对应元素相乘
    # 完成交叉项,xi*vi*xi*vi - xi^2*vi^2
    interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.
    

    模型输出:偏置项+一阶+二阶。注意:这里没有sigmoid。

    p = w_0 + dataMatrix[x] * w + interaction
    

    3、参数更新

    1、计算sigmoid(y*pred_y)-1

    tmp = sigmoid(classLabels[x] * p[0, 0]) - 1
    

    为何这里使用p[0,0],因为p的类型为:<class 'numpy.matrix'>,其值形如:[[-0.48939694]]。所以p[0,0]得到具体数值。

    2、三个参数的更新规则:随机梯度下降,每个样本x训练完成,进行更新。

    w_0 = w_0 + alpha * tmp * classLabels[x]
    # 由于w有n个值,每个值迭代更新
     for i in range(n):
         if dataMatrix[x, i] != 0:
               w[i, 0] = w[i, 0] + alpha * tmp * classLabels[x] * dataMatrix[x, i]
               for j in range(k): # V的第i行有k个值,也是每个值迭代更新
                   v[i, j] = v[i, j] + alpha * tmp * classLabels[x] * (dataMatrix[x, i] * inter_1[0, j] - v[i, j] * dataMatrix[x, i] * dataMatrix[x, i])
    

    4、代码完成

    1、数据集预处理

    皮马人糖尿病数据集:https://github.com/susanli2016/Machine-Learning-with-Python/blob/master/diabetes.csv
    进行训练集、测试集切分

    import numpy as np
    import random
    
    # 数据集切分
    def loadData(fileName,ratio):    # ratio,训练集与测试集比列
        trainingData = []
        testData = []
        with open(fileName) as txtData:
            lines = txtData.readlines()
            for line in lines:
                lineData = line.strip().split(',')
                if random.random() < ratio:           #数据集分割比例
                    trainingData.append(lineData)     #训练数据集列表
                else:
                    testData.append(lineData)         #测试数据集列表
                np.savetxt('./data/diabetes_train.txt', trainingData, delimiter=',',fmt='%s')
                np.savetxt('./data/diabetes_test.txt', testData, delimiter=',',fmt='%s')
        return trainingData,testData
    
    diabetes_file = './data/diabetes.csv'
    trainingData, testData = loadData(diabetes_file,0.8)
    

    2、FM实现

    # FM二分类
    # diabetes皮马人糖尿病数据集
    # coding:UTF-8
    
    from numpy import *
    from random import normalvariate  # 正态分布
    from datetime import datetime
    import pandas as pd
    import numpy as np
    
    
    # 处理数据
    def preprocessData(data):
        feature = np.array(data.iloc[:, :-1])  # 取特征(8个特征)
        label = data.iloc[:, -1].map(lambda x: 1 if x == 1 else -1)  # 取标签并转化为 +1,-1
    
        # 将数组按行进行归一化
        zmax, zmin = feature.max(axis=0), feature.min(axis=0)  # 特征的最大值,特征的最小值
        feature = (feature - zmin) / (zmax - zmin)
        label = np.array(label)
    
        return feature, label
    
    
    def sigmoid(inx):
        return 1.0 / (1 + np.exp(-inx))
    
    
    # 训练FM模型
    def FM(dataMatrix, classLabels, k, iter, alpha):
        '''
        :param dataMatrix:  特征矩阵
        :param classLabels: 标签矩阵
        :param k:           v的维数
        :param iter:        迭代次数
        :return:            常数项w_0, 一阶特征系数w, 二阶交叉特征系数v
        '''
        # dataMatrix用的是matrix, classLabels是列表
        m, n = shape(dataMatrix)  # 矩阵的行列数,即样本数m和特征数n
    
        # 初始化参数
        w = zeros((n, 1))  # 一阶特征的系数
        w_0 = 0  # 常数项
        v = normalvariate(0, 0.2) * ones((n, k))  # 即生成辅助向量(n*k),用来训练二阶交叉特征的系数
    
        for it in range(iter):
            for x in range(m):  # 随机优化,每次只使用一个样本
                # 二阶项的计算
                inter_1 = dataMatrix[x] * v  # 每个样本(1*n)x(n*k),得到k维向量(FM化简公式大括号内的第一项)
                inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v)  # 二阶交叉项计算,得到k维向量(FM化简公式大括号内的第二项)
                interaction = sum(multiply(inter_1, inter_1) - inter_2) / 2.  # 二阶交叉项计算完成(FM化简公式的大括号外累加)
    
                p = w_0 + dataMatrix[x] * w + interaction  # 计算预测的输出,即FM的全部项之和
                tmp = 1 - sigmoid(classLabels[x] * p[0, 0])  # tmp迭代公式的中间变量,便于计算
                w_0 = w_0 + alpha * tmp * classLabels[x]
    
                for i in range(n):
                    if dataMatrix[x, i] != 0:
                        w[i, 0] = w[i, 0] + alpha * tmp * classLabels[x] * dataMatrix[x, i]
                        for j in range(k):
                            v[i, j] = v[i, j] + alpha * tmp * classLabels[x] * (
                                    dataMatrix[x, i] * inter_1[0, j] - v[i, j] * dataMatrix[x, i] * dataMatrix[x, i])
    
            # 计算损失函数的值
            if it % 10 == 0:
                loss = getLoss(getPrediction(mat(dataMatrix), w_0, w, v), classLabels)
                print("第{}次迭代后的损失为{}".format(it, loss))
    
        return w_0, w, v
    
    
    # 损失函数
    def getLoss(predict, classLabels):
        m = len(predict)
        loss = 0.0
        for i in range(m):
            loss -= log(sigmoid(predict[i] * classLabels[i]))
        return loss
    
    
    # 预测
    def getPrediction(dataMatrix, w_0, w, v):
        m = np.shape(dataMatrix)[0]
        result = []
        for x in range(m):
            inter_1 = dataMatrix[x] * v
            inter_2 = multiply(dataMatrix[x], dataMatrix[x]) * multiply(v, v)  # multiply对应元素相乘
            # 完成交叉项
            interaction = np.sum(multiply(inter_1, inter_1) - inter_2) / 2.
            p = w_0 + dataMatrix[x] * w + interaction  # 计算预测的输出
            pre = sigmoid(p[0, 0])
            result.append(pre)
        return result
    
    
    # 评估预测的准确性
    def getAccuracy(predict, classLabels):
        m = len(predict)
        allItem = 0
        error = 0
        for i in range(m):  # 计算每一个样本的误差
            allItem += 1
            if float(predict[i]) < 0.5 and classLabels[i] == 1.0:
                error += 1
            elif float(predict[i]) >= 0.5 and classLabels[i] == -1.0:
                error += 1
            else:
                continue
    
        return float(error) / allItem
    
    
    if __name__ == '__main__':
        trainData = './data/diabetes_train.txt'
        testData = './data/diabetes_test.txt'
        train = pd.read_csv(trainData)
        test = pd.read_csv(testData)
        dataTrain, labelTrain = preprocessData(train)
        dataTest, labelTest = preprocessData(test)
        date_startTrain = datetime.now()
    
        print("开始训练")
        w_0, w, v = FM(mat(dataTrain), labelTrain, 4, 100, 0.01)
        print("w_0:", w_0)
        print("w:", w)
        print("v:", v)
        predict_train_result = getPrediction(mat(dataTrain), w_0, w, v)  # 得到训练的准确性
        print("训练准确性为:%f" % (1 - getAccuracy(predict_train_result, labelTrain)))
        date_endTrain = datetime.now()
        print("训练用时为:%s" % (date_endTrain - date_startTrain))
    
        print("开始测试")
        predict_test_result = getPrediction(mat(dataTest), w_0, w, v)  # 得到测试的准确性
        print("测试准确性为:%f" % (1 - getAccuracy(predict_test_result, labelTest)))
    

    python版fm代码解析:
    1、损失函数,输入m个样本,损失函数累加。
    2、预测时候的输出使用了sigmoid。这样是FM模型和sigmoid进行了分割,分为了两个阶段,因为如前面所说,在参数更新过程中,使用了logit loss作为损失函数,而这个损失函数的输入为sigmoid之前的fm模型的原始输出。但是到了准确率评估阶段,需要将fm模型的原始输出经过sigmoid,这样就可以以0.5为阈值,进行正负例区分。
    3、预测准确率:预测值大于0.5为正例,小于0.5为负例。

    ps: 公式到代码的转换:累加和变矩阵乘法,试想,矩阵乘法里就是一行*一列,然后累加和。

    参考文献

    1、FM因子分解机的原理、公式推导、Python实现和应用
    2、FM算法解析及Python实现
    3、分解机(Factorization Machines)推荐算法原理
    4、深度学习遇上推荐系统(一)--FM模型理论和实践

  • 相关阅读:
    台式机+笔记本的扩展模式+远程登录设置
    Hadoop 集群搭建以及脚本撰写
    Python 入门学习(三)
    1056 Mice and Rice
    1057 Stack
    1058 A+B in Hogwarts
    1059 Prime Factors
    使用熔断器仪表盘监控
    使用熔断器防止服务雪崩
    创建服务消费者(Feign)
  • 原文地址:https://www.cnblogs.com/leimu/p/14570332.html
Copyright © 2020-2023  润新知