4.【Python】分類演算法—Factorization Machine(FM,因子分解機)

不知道叫什麼呢發表於2020-12-15

4.【Python】分類演算法—Factorization Machine(FM,因子分解機)

現實生活中存在大量非線性可分的分類問題,而Logistic Regression演算法只能解決線性可分的二分類問題。Factorization Machine作為Logistic Regression的改進,可解決非線性分類問題。


為了解決非線性分類的問題,第一種方法是採用核函式將非線性可分問題近似為線性可分問題;第二種方法是對Logistic Regression演算法進行擴充,例如Factorization Machine(FM,因子分解機),是一種基於矩陣分解的機器學習演算法。


前言

FM演算法是對Logistic Regression演算法的擴充套件,包含Logistic Regression的非線性項,還包含非線性的交叉項,可以利用矩陣分解的方法對交叉項進行學習,進而確定係數。


一、FM模型

1.因子分解機FM模型

對於二分類即度為2的模型,前兩部分屬於傳統的線性模型,第三部分是兩個非線性互異特徵向量的關係。
在這裡插入圖片描述
⟨ V i , V j ⟩ \left \langle V_{i},V_{j} \right \rangle Vi,Vj表示的是兩個大小為k的向量 ⟨ V i ⟩ \left \langle V_{i} \right \rangle Vi和向量 ⟨ V j ⟩ \left \langle V_{j} \right \rangle Vj的點積。 ⟨ V i ⟩ \left \langle V_{i} \right \rangle Vi為係數矩陣V的第i維向量,且k的大小為FM演算法的度。
在這裡插入圖片描述

2.損失函式

這裡主要用於二分類問題,在二分類中一般採用logit loss作為優化損失函式,表示式如下:
在這裡插入圖片描述

二、交叉項的處理

1.交叉項係數

FM演算法是在基本線性迴歸模型上引入交叉項,若物件為稀疏資料,則可能出現樣本中沒有交叉特徵向量 x i x_{i} xi的情況,不能對交叉項係數進行估計,所以引入向量的點積對交叉項係數 w i , j w_{i,j} wi,j進行估計。
在這裡插入圖片描述
在這裡插入圖片描述
其中,
在這裡插入圖片描述

在這裡插入圖片描述

2.模型求解

對交叉項進行單獨求解,將其化解為形如 ( ( a + b + c ) 2 − a 2 − b 2 − c 2 ) / 2 ((a+b+c)^{2}-a^2-b^2-c^2)/2 ((a+b+c)2a2b2c2)/2,前半部分為交叉項的變式,後半部分為0向量。
在這裡插入圖片描述

三、FM演算法的求解

前面介紹的演算法一般採用梯度下降法,但是在求解迭代過程中一般需要全部資料進行訓練學習,所以在資料量較大時採用隨機梯度下降法。

1.隨機梯度下降法求解模型引數

隨機梯度下降法(Stochastic Gradient Descent)在迭代過程中根據一個資料樣本對模型引數進行調整,其優化過程為:
在這裡插入圖片描述
在採用隨機梯度下降法進行學習時,主要對損失函式求偏導數:
在這裡插入圖片描述
其中 ∂ y ^ ∂ θ \frac{\partial \widehat{y}}{\partial \theta } θy 為:
在這裡插入圖片描述

2.利用隨機梯度下降法訓練FM模型引數

主要步驟為先求初始化權重w和交叉項的係數矩陣V,然後對每個樣本進行訓練求權重和交叉項,最後直到滿足條件才進行終止。

首先定義梯度下降法函式stocGradAscent,函式輸入為特徵dataMatrix、樣本標籤classLabels、係數矩陣維數k、隨機梯度下降法的最大迭代次數max_iter、隨機梯度下降法的學習率alpha,函式輸出為一次項的權重 w 0 w_{0} w0、常數項的權重 w i w_{i} wi、交叉項的係數矩陣V。

程式碼如下:

def stocGradAscent(dataMatrix,classLabels,k,max_iter,alpha):
    '''利用隨機梯度下降法訓練FM模型
    input:dataMatrix(mat)特徵
            classLabels(mat)標籤
            k(int)v的維數
            max_iter(int)最大迭代次數
            alpha(float)學習率
    output:w0(float),w(mat),v(mat):權重

    '''
    m,n = np.shape(dataMatrix)
    #1.初始化引數
    w = np.zeros((n,1))
    w0 = 0 #偏置項
    v= initialize_v(n,k) #初始化v

    #2.訓練
    for it in range(max_iter):
        print("iteration:", it)
        for x in range(m):  #隨機優化,對每一個樣本而言的
            inter_1 = dataMatrix[x] * v
            inter_2 = np.multiply(dataMatrix[x], dataMatrix[x]) * np.multiply(v,v) #multiply對應元素相乘
            #完成交叉項
            interaction = np.sum(np.multiply(inter_1, inter_1) - inter_2) / 2
            p = w0 + dataMatrix[x] * w + interaction  #計算預測的輸出
            loss = sigmoid(classLabels[x] * p[0, 0] - 1)

			#常數項權重的修正
            w0 = w0 - alpha * loss * classLabels[x]
            for i in range(n):
                if dataMatrix[x, i] != 0 :
                	#一次項權重的修正
                    w[i, 0] = w[i, 0] - alpha * loss * classLabels[x] * dataMatrix[x, i]

                    for j in range(k):
                    	#交叉項係數矩陣的修正
                        v[i,j] = v[i,j] - alpha * loss * classLabels[x] * (dataMatrix[x,i] * inter_1[0,j] - v[i,j] * dataMatrix[x,i] * dataMatrix[x,i])

        #計算損失函式的值
        if it %1000 == 0:
            print("\t----iter:", it, ", cost:", getCost(getPrediction(np.mat(dataTrain), w0, w, v), classLabels))

    #3.返回最終的FM模型的引數
    return w0, w, v

在初始化權重時,為簡化步驟將所有權重都化為0。在對交叉項係數矩陣初始化時,使用正態分佈對其進行初始化,定義initialize_v函式如下所示。

程式碼如下:

from random import normalvariate #正態分佈

#初始化交叉項的權重
def initialize_v(n, k):
    '''初始化交叉項
    input: n(int)特徵的個數
            k(int)FM模型的超引數
    output: v(mat):交叉項的係數權重
    '''
    v = np.mat(np.zeros(n, k))

    for i in range(n):
        for j in range(k):
            #利用正態分佈生成每一個權重
            v[i, j] = normalvariate(0, 0.2)
    return v

其中損失函式採用sigmoid函式,定義sigmoid函式如下。

程式碼如下:

#定義sigmoid函式
def sigmoid(inx):
    return 1.0/(1 + np.exp(-inx))

在每完成1000次迭代以後,計算一次當前的損失值,定義計算損失的函式getCost如下。

程式碼如下:

#計算損失函式的值
#定義getcost函式
def getCost(predict, classLabels):
    '''計算預測準確性
    input: predict(list)預測值
            classLabels(list)標籤值
    output: error(float)計算損失函式的值
    '''
    m = len(predict)
    error = 0.0
    for i in range(m):
        error -= np.log(sigmoid(predict[i] * classLabels[i]))
    return error

三、FM演算法實踐

採用非線性可分的資料集作為訓練資料,主要分為兩個部分,一為利用訓練資料集對模型進行訓練,二為利用測試集對資料進行預測。

1.訓練FM模型

FM訓練模型的主函式如下,首先利用loadDataSet函式匯入訓練資料,然後利用stocGradAscent函式訓練模型,其後利用getPrediction函式對訓練資料進行預測,利用getAccuracy函式將預測值和樣本進行比較得到準確率,最後利用save_model函式儲存模型。

定義匯入訓練資料的loadDataSet函式,程式碼如下:

def loadDataSet(data):
    '''匯入訓練資料
    input: data(string)訓練資料
    output: dataMat(list)特徵
            labelMat(list)標籤
    '''
    dataMat = []
    labelMat = []
    fr = open(data) #開啟檔案
    for line in fr.readlines():
        lines = line.strip().split("\t")
        lineArr = []

        for i in range(len(lines) - 1):
            lineArr.append(float(lines[i]))
        dataMat.append(lineArr)
        labelMat.append(float(lines[-1]) * 2 - 1) #轉換成{-1,1}
    fr.close()
    return dataMat,labelMat

由於FM模型中使用的是log損失函式,在匯入標籤的過程中,需要將原始資料範圍{0,1}轉換為{-1,1}。

定義預測訓練樣本的getPrediction函式,程式碼如下:

def getPrediction(dataMatrix, w0, w, v):
    '''得到預測值
    input: dataMatrix(mat)特徵
            w(int)常數項權重
            w0(int)一次項權重
            v(float)交叉項權重
    output: result(list)預測的結果
    '''
    m = np.shape(dataMatrix)[0]
    result = []
    for x in range(m):
        inter_1 = dataMatrix[x] * v
        inter_2 = np.multiply(dataMatrix[x], dataMatrix[x]) * np.multiply(v, v)  # multiply對應元素相乘
        # 完成交叉項
        interaction = np.sum(np.multiply(inter_1, inter_1) - inter_2) / 2
        p = w0 + dataMatrix[x] * w + interaction  # 計算預測的輸出
        pre = sigmoid(p[0, 0])
        result.append(pre)
    return result

定義getAccuracy函式評價模型預測效果,程式碼如下:

def getAccuracy(predict, classLabels):
    '''計算預測準確性
    input: predict(list)預測值
            classLabels(list)標籤
    output: float(error) / allItem(float)錯誤率
    '''
    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

定義save_model函式儲存FM模型的偏置項和權重,程式碼如下:

def save_model(file_name, w0, w, v):
    '''儲存訓練好的模型
    input: file_name(string):儲存的檔名
            w0(float):偏置項
            w(mat): 一次項的權重
            v(mat): 交叉項的權重
    '''
    f = open(file_name, "w")
    #1.儲存w0
    f.write(str(w0) + "\n")
    #2.儲存一次項的權重
    w_array = []
    m = np.shape(w)[0]
    for i in range(m):
        w_array.append(str(w[i, 0]))
    f.write("\t".join(w_array) + "\n")
    #3.儲存交叉項的權重
    m1, n1 = np.shape(v)
    for i in range(m1):
        v_tmp = []
        for j in range(n1):
            v_tmp.append(str(v[i, j]))
        f.write("\t".join(v_tmp) + "\n")
    f.close()

完整程式碼如下:

# conding:UTF-8
import numpy as np
from random import normalvariate #正態分佈


def loadDataSet(data):
    '''匯入訓練資料
    input: data(string)訓練資料
    output: dataMat(list)特徵
            labelMat(list)標籤
    '''
    dataMat = []
    labelMat = []
    fr = open(data) #開啟檔案
    for line in fr.readlines():
        lines = line.strip().split("\t")
        lineArr = []

        for i in range(len(lines) - 1):
            lineArr.append(float(lines[i]))
        dataMat.append(lineArr)
        labelMat.append(float(lines[-1]) * 2 - 1) #轉換成{-1,1}
    fr.close()
    return dataMat,labelMat


#初始化交叉項的權重
def initialize_v(n, k):
    '''初始化交叉項
    input: n(int)特徵的個數
            k(int)FM模型的超引數
    output: v(mat):交叉項的係數權重
    '''
    v = np.mat(np.zeros(n, k))

    for i in range(n):
        for j in range(k):
            #利用正態分佈生成每一個權重
            v[i, j] = normalvariate(0, 0.2)
    return v

#定義sigmoid函式
def sigmoid(inx):
    return 1.0/(1 + np.exp(-inx))

#計算損失函式的值
#定義getcost函式
def getCost(predict, classLabels):
    '''計算預測準確性
    input: predict(list)預測值
            classLabels(list)標籤值
    output: error(float)計算損失函式的值
    '''
    m = len(predict)
    error = 0.0
    for i in range(m):
        error -= np.log(sigmoid(predict[i] * classLabels[i]))
    return error


def stocGradAscent(dataMatrix,classLabels,k,max_iter,alpha):
    '''利用隨機梯度下降法訓練FM模型
    input:dataMatrix(mat)特徵
            classLabels(mat)標籤
            k(int)v的維數
            max_iter(int)最大迭代次數
            alpha(float)學習率
    output:w0(float),w(mat),v(mat):權重

    '''
    m,n = np.shape(dataMatrix)
    #1.初始化引數
    w = np.zeros((n,1))
    w0 = 0 #偏置項
    v= initialize_v(n,k) #初始化v

    #2.訓練
    for it in range(max_iter):
        print("iteration:", it)
        for x in range(m):  #隨機優化,對每一個樣本而言的
            inter_1 = dataMatrix[x] * v
            inter_2 = np.multiply(dataMatrix[x], dataMatrix[x]) * np.multiply(v,v) #multiply對應元素相乘
            #完成交叉項
            interaction = np.sum(np.multiply(inter_1, inter_1) - inter_2) / 2
            p = w0 + dataMatrix[x] * w + interaction  #計算預測的輸出
            loss = sigmoid(classLabels[x] * p[0, 0] - 1)

            w0 = w0 - alpha * loss * classLabels[x]
            for i in range(n):
                if dataMatrix[x, i] != 0 :
                    w[i, 0] = w[i, 0] - alpha * loss * classLabels[x] * dataMatrix[x, i]

                    for j in range(k):
                        v[i,j] = v[i,j] - alpha * loss * classLabels[x] * (dataMatrix[x,i] * inter_1[0,j] - v[i,j] * dataMatrix[x,i] * dataMatrix[x,i])

        #計算損失函式的值
        if it %1000 == 0:
            print("\t----iter:", it, ", cost:", getCost(getPrediction(np.mat(dataTrain), w0, w, v), classLabels))

    #3.返回最終的FM模型的引數
    return w0, w, v


def getPrediction(dataMatrix, w0, w, v):
    '''得到預測值
    input: dataMatrix(mat)特徵
            w(int)常數項權重
            w0(int)一次項權重
            v(float)交叉項權重
    output: result(list)預測的結果
    '''
    m = np.shape(dataMatrix)[0]
    result = []
    for x in range(m):
        inter_1 = dataMatrix[x] * v
        inter_2 = np.multiply(dataMatrix[x], dataMatrix[x]) * np.multiply(v, v)  # multiply對應元素相乘
        # 完成交叉項
        interaction = np.sum(np.multiply(inter_1, inter_1) - inter_2) / 2
        p = w0 + dataMatrix[x] * w + interaction  # 計算預測的輸出
        pre = sigmoid(p[0, 0])
        result.append(pre)
    return result


def getAccuracy(predict, classLabels):
    '''計算預測準確性
    input: predict(list)預測值
            classLabels(list)標籤
    output: float(error) / allItem(float)錯誤率
    '''
    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


def save_model(file_name, w0, w, v):
    '''儲存訓練好的模型
    input: file_name(string):儲存的檔名
            w0(float):偏置項
            w(mat): 一次項的權重
            v(mat): 交叉項的權重
    '''
    f = open(file_name, "w")
    #1.儲存w0
    f.write(str(w0) + "\n")
    #2.儲存一次項的權重
    w_array = []
    m = np.shape(w)[0]
    for i in range(m):
        w_array.append(str(w[i, 0]))
    f.write("\t".join(w_array) + "\n")
    #3.儲存交叉項的權重
    m1, n1 = np.shape(v)
    for i in range(m1):
        v_tmp = []
        for j in range(n1):
            v_tmp.append(str(v[i, j]))
        f.write("\t".join(v_tmp) + "\n")
    f.close()


if __name__ == '__main__':
    #1.匯入訓練資料
    print("-------1.load data-------")
    dataTrain,labelTrain = loadDataSet("data.txt")
    print("-------2.learning--------")
    #2.利用隨機梯度訓練FM模型
    w0, w, v = stocGradAscent(np.mat(dataTrain), labelTrain, 3, 10000, 0.01)
    predict_result = getPrediction(np.mat(dataTrain), w0, w, v) #計算訓練的準確性
    print("-------training error: %f " %(1 - getAccuracy(predict_result, labelTrain)))
    print("-------3.save model------")
    #3.儲存訓練好的FM模型
    save_model("weights", w0, w, v)


2.FM測試模型

上述將FM分類模型訓練完成後,將係數都儲存在weights檔案中,建立FM_test.py檔案。

FM測試模型主要是對新資料進行預測,主要步驟為:定義loadDataSet函式匯入測試資料,定義loadModel函式匯入訓練模型資料,定義save_result模型儲存最終的預測結果。

主函式程式碼如下:

if __name__ == '__main__':
    #1.匯入測試資料
    dataTest = loadDataTest("test_data.txt")
    #2.匯入FM訓練模型
    w0, w, v = loadModel("weights")
    #3.預測
    result = getPrediction(dataTest, w0, w, v)
    #4.儲存最終的預測結果
    save_result = ("predict_result", result)

函式loadDataTest的輸入為測試資料的位置,輸出為測試資料的特徵,程式碼如下:

def loadDataSet(data):
    '''匯入測試資料
    input: data(string)訓練資料
    output: dataMat(list)特徵
    '''
    dataMat = []
    fr = open(data) #開啟檔案
    for line in fr.readlines():
        lines = line.strip().split("\t")
        lineArr = []

        for i in range(len(lines)):
            lineArr.append(float(lines[i]))
        dataMat.append(lineArr)

    fr.close()
    return dataMat

函式loadModel輸出的是FM訓練模型中的引數包括偏置項 w 0 w_{0} w0,一次項的權重 w i w_{i} wi,交叉項的權重V,程式碼如下:

def loadModel(model_file):
    '''匯入FM模型
    input: model_file(string)FM模型
    output:w0, np.mat(w).T, np.mat(v) FM模型的引數
    '''
    f = open(model_file)
    line_index = 0
    w0 = 0.0
    w = []
    v = []
    for line in f.readlines():
        lines = line.strip().split("\t")
        if line_index == 0:  #w0
            w0 = float(lines[0].strip())
        elif line_index == 1: #w
            for x in lines:
                w.append(float(x.strip()))
        else:
            v_tmp = []
            for x in lines:
                v_tmp.append(float(x.strip()))
            v.append()
        line_index += 1
    f.close()
    return w0, np.mat(w).T, np.mat(v)

函式save_result將FM測試模型的預測結果儲存到檔案中,程式碼如下:

def save_result(file_name, result):
    '''儲存最終的預測結果
    input: file_name(string)需要儲存的檔名
            result(mat):對測試資料的預測結果
    '''
    f = open(file_name, "w")
    f.write("\n".join(str(x) for x in result))
    f.close()

完整程式碼如下:

# conding:UTF-8
import numpy as np


def loadDataTest(data):
    '''匯入測試資料
    input: data(string)訓練資料
    output: dataMat(list)特徵
    '''
    dataMat = []
    fr = open(data) #開啟檔案
    for line in fr.readlines():
        lines = line.strip().split("\t")
        lineArr = []

        for i in range(len(lines)):
            lineArr.append(float(lines[i]))
        dataMat.append(lineArr)

    fr.close()
    return dataMat

def loadModel(model_file):
    '''匯入FM模型
    input: model_file(string)FM模型
    output:w0, np.mat(w).T, np.mat(v) FM模型的引數
    '''
    f = open(model_file)
    line_index = 0
    w0 = 0.0
    w = []
    v = []
    for line in f.readlines():
        lines = line.strip().split("\t")
        if line_index == 0:  #w0
            w0 = float(lines[0].strip())
        elif line_index == 1: #w
            for x in lines:
                w.append(float(x.strip()))
        else:
            v_tmp = []
            for x in lines:
                v_tmp.append(float(x.strip()))
            v.append()
        line_index += 1
    f.close()
    return w0, np.mat(w).T, np.mat(v)


#定義sigmoid函式
def sigmoid(inx):
    return 1.0/(1 + np.exp(-inx))

def getPrediction(dataMatrix, w0, w, v):
    '''得到預測值
    input: dataMatrix(mat)特徵
            w(int)常數項權重
            w0(int)一次項權重
            v(float)交叉項權重
    output: result(list)預測的結果
    '''
    m = np.shape(dataMatrix)[0]
    result = []
    for x in range(m):
        inter_1 = dataMatrix[x] * v
        inter_2 = np.multiply(dataMatrix[x], dataMatrix[x]) * np.multiply(v, v)  # multiply對應元素相乘
        # 完成交叉項
        interaction = np.sum(np.multiply(inter_1, inter_1) - inter_2) / 2
        p = w0 + dataMatrix[x] * w + interaction  # 計算預測的輸出
        pre = sigmoid(p[0, 0])
        result.append(pre)
    return result


def save_result(file_name, result):
    '''儲存最終的預測結果
    input: file_name(string)需要儲存的檔名
            result(mat):對測試資料的預測結果
    '''
    f = open(file_name, "w")
    f.write("\n".join(str(x) for x in result))
    f.close()


if __name__ == '__main__':
    #1.匯入測試資料
    dataTest = loadDataTest("test_data.txt")
    #2.匯入FM訓練模型
    w0, w, v = loadModel("weights")
    #3.預測
    result = getPrediction(dataTest, w0, w, v)
    #4.儲存最終的預測結果
    save_result("predict_result", result)


總結

本文主要介紹了Factorization Machine(FM,因子分解機)分類演算法,與Softmax 和Logistic分類演算法不同的是,此演算法可用於非線性分類,因此在函式模型上多了一項交叉項表示非線性向量的互動。python的實現過程主要分為兩部分——訓練和測試。

參考文獻:《Python機器學習演算法》

相關文章