推薦系統實踐 0x0c FM系列(LR/FM/FFM)


邏輯回歸(LR)

在介紹FM系列之前,我想首先簡單介紹一下邏輯回歸。通常來說,邏輯回歸模型能夠綜合利用更多的信息,如用戶、物品、上下文等多種不同的特征,生成更為全面的結果。另外,邏輯回歸將推薦問題看成一個分類問題。通過預測正樣本的概率對物品進行排序,這里的正樣本可以是用戶觀看了某個視頻,也可以是用戶點擊了某個商品,或者用戶播放了某個音樂等等。邏輯回歸模型將推薦問題轉換成了CTR(click throught rate)預估的問題。

步驟

一般來說,邏輯回歸模型的推薦過程分成以下幾步:

  1. 將用戶年齡、性別等信息,商品名稱、屬性等信息,以及上下文等信息轉換成數值型特征向量。
  2. 將邏輯回歸作為優化目標,利用樣本數據對邏輯回歸模型進行訓練,調整模型內部參數。
  3. 在模型服務階段,將特征向量的輸入到模型當中,得到用戶“點擊”等正反饋的概率。
  4. 按照正反饋的概率對物品進行排序,得到推薦列表。

這里的邏輯回歸也使用了梯度下降的算法。這里我推薦一篇文章專門介紹邏輯回歸的數學原理,感興趣的讀者可以繼續閱讀。另外特別要說明的事是,邏輯回歸是分類模型,不是回歸模型。

優點

  • 有着具體的數學含義作為支撐。由於CTR模型符合伯努利分布,所以使用邏輯回歸作為CTR模型符合邏輯規律。
  • 可解釋性強,能夠通過權重對各個因素進行定位,給出結果的可解釋性原因。
  • 實際工程需要。由於易於並行化、模型簡單以及訓練開銷小等特點,邏輯回歸受到了廣泛認可。

局限

  • 表達能力不強,無法進行特征交叉、特征篩選等操作等

POLY2

POLY2是最簡單的特征交叉的算法,直接對特征進行暴力組合,看看它的數學形式就能知道

\[\mathrm{POLY2}(w,x)=\sum_{j_1=1}^{n-1}\sum_{j_2=j_1+1}^{n}w_{h(j_1,j_2)}x_{j_1}x_{j_2} \]

直接對特征進行兩兩交叉,並對交叉后的特征組合賦予權重。POLY2仍然是線性模型,訓練方法與邏輯回歸模型並無區別。

局限

  1. 對於很多互聯網數據,通常使用的是one-hot編碼,無選擇的特征交叉使得特征向量更加稀疏,對於權重缺乏有效訓練,甚至無法收斂。
  2. 權重參數直接上升了一個數量級,計算量難以接受

Factorization Machines(FM)

為了解決POLY2的局限,FM模型使用了兩個向量內積取代了單一的權重系數。FM模型為每個特征學習了一個隱權重向量,在做特征交叉時使用兩個特征隱向量的內積作為交叉特征的權重。如以下公式:

\[\mathrm{FM}(w,x)=\sum_{j_1=1}^{n-1}\sum_{j_2=j_1+1}^{n}(w_{j_1}w_{j_2})x_{j_1}x_{j_2} \]

FM引入特征隱向量與矩陣分解中的隱向量有異曲同工之妙。通過引入特征隱向量的方式,把POLY2當中\(n^2\)級別的權重參數降低到了\(nk\),極大地降低了訓練開銷。

另外,由於特征隱向量的存在,使得模型具備了計算特征組合權重的能力,如家具,蔬菜兩種特征中的一個訓練樣本,(桌子,西紅柿),就不需要同時出現桌子和西紅柿才能學習這種特征組合。另外,當出現新的樣本事也能通過計算過的特征隱向量進行在線服務。

同樣的,FM也可以使用梯度下降法進行學習,不失實時性和靈活性。我們看一下PyTorch版本的FM是如何實現的吧。

import torch as torch
import torch.nn as nn
import numpy as np
import torch.nn.functional as F


class FeaturesLinear(nn.Module):

    def __init__(self, field_dims, output_dim=1):
        super(FeaturesLinear, self).__init__()
        print("field_dims: ", field_dims)
        self.fc = nn.Embedding(sum(field_dims), output_dim)
        self.bias = nn.Parameter(torch.zeros((output_dim,)))
        # accumulation add function to sparse the categories like:[1,3,4,7]==>[1,4,8,15]
        self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)

    def forward(self, x):
        """
          to change the category Serial number to ordered number
          like we got x = [2, 4] means category_1's id is 2, and category_2's id is 4
          assume field_dims like [3, 8], category_1 has 3 ids, category_2 has 8 ids. ==> offsets=[0, 3]
          x = [0 + 2, 4 + 3] ==> [2, 7]
        """
        x = x + x.new_tensor(self.offsets).unsqueeze(0)
        return torch.sum(self.fc(x), dim=1)+self.bias


class FeaturesEmbedding(nn.Module):

    def __init__(self, field_dims, embed_dim):
        super(FeaturesEmbedding, self).__init__()
        self.embedding = nn.Embedding(sum(field_dims), embed_dim)
        self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)
        nn.init.xavier_uniform_(self.embedding.weight.data)

    def forward(self, x):
        x = x + x.new_tensor(self.offsets).unsqueeze(0)
        return self.embedding(x)

class FactorizationMachine(nn.Module):
    def __init__(self, reduce_sum=True):
        super(FactorizationMachine, self).__init__()
        self.reduce_sum = reduce_sum

    def forward(self, x):
        """
             $\frac{1}{2}\sum_{k=1}^{K}[(\sum_{i=1}^{n}v_{ik}x_i)^2-\sum_{i=1}^{n}v_{ik}^2x_i^2]$
        :param x: float tensor of size (batch_size, num_fields, embed_dim)
        :return:
        """
        square_of_sum = torch.sum(x, dim=1) ** 2
        sum_of_square = torch.sum(x ** 2, dim=1)
        ix = square_of_sum - sum_of_square
        if self.reduce_sum:
            ix = torch.sum(ix, dim=1, keepdim=True)
        return 0.5 * ix
import torch.nn.functional as F
from base import BaseModel
import torch as torch
import torch.nn as nn

from model.layers import *


class FM(BaseModel):

    def __init__(self, field_dims=None, embed_dim=None):
        super().__init__()
        self.linear = FeaturesLinear(field_dims)
        self.embedding = FeaturesEmbedding(field_dims, embed_dim)
        self.fm = FactorizationMachine(reduce_sum=True)

    def forward(self, x):
        x = self.linear(x) + self.fm(self.embedding(x))
        x = torch.sigmoid(x.squeeze(1))
        return x

Field-aware Factorization Machine(FFM)

還是為了解決數據特征系數的問題,FFM在FM的基礎上進一步改進,在模型中引入域的概念,即field。將同一個域的特征單獨進行one-hot,因此在FFM中,每一維特征都會針對其他特征的每個域,分別學習一個隱變量,該隱變量不僅與特征相關,也與域相關。

\[\mathrm{FFM}(w,x)=\sum_{j_1=1}^{n-1}\sum_{j_2=j_1+1}^{n}(w_{j_1,f_2}w_{j_2,f_1})x_{j_1}x_{j_2} \]

按照我的理解,引入特征域的概念實際上是希望每種特征都能夠針對性對其他特征有更合適的權重,也就是學習域與域之間的權重分布,作為特征隱變量。但是與此同時,計算復雜度從\(nk\)上升到了\(n^2k\),在實際應用中需要在效果和工程投入進行權衡。

我們看一下相關代碼:

class FieldAwareFactorizationMachine(nn.Module):
    def __init__(self, field_dims, embed_dim):
        super().__init__()
        self.num_fields = len(field_dims)
        self.embeddings = nn.ModuleList([
            nn.Embedding(sum(field_dims), embed_dim) for _ in range(self.num_fields)
        ])
        self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)
        for embedding in self.embeddings:
            nn.init.xavier_uniform_(embedding.weight.data)

    def forward(self, x):
        x = x + x.new_tensor(self.offsets).unsqueeze(0)
        xs = [self.embeddings[i](x) for i in range(self.num_fields)]
        ix = list()
        for i in range(self.num_fields-1):
            for j in range(i+1, self.num_fields):
                ix.append(xs[j][:, j] * xs[i][:, j])
        ix = torch.stack(ix, dim=1)
        return ix
from model.layers import *


class FFM(nn.Module):

    def __init__(self, field_dims, embed_dim):
        super().__init__()
        self.linear = FeaturesLinear(field_dims)
        self.ffm = FieldAwareFactorizationMachine(field_dims, embed_dim)

    def forward(self, x):
        ffm_term = torch.sum(torch.sum(self.ffm(x), dim=1), dim=1, keepdim=True)
        x = self.linear(x) + ffm_term
        return x.squeeze(1)

參考

深度學習推薦系統 王喆編著
【機器學習】邏輯回歸(非常詳細)
Github:ottsion/deeplite


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM