推薦系統——FFM模型點擊率CTR預估(代碼,數據流動詳細過程)


前言:主要記錄,在推薦系統利用FFM模型,進行CTR預估的時候,離散化特征需要嵌入,field之間的特征交叉是怎么計算的?記錄了數據流動的每一個過程。

FMM是在FM的基礎上改進的,理論部分未作過多解釋。(內容有不足之處,請大家指正批評)

參考:github:pytorch-fm

一、公式:

FFM模型定義如下:

class FieldAwareFactorizationMachineModel(torch.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):
        """
        :param x: Long tensor of size ``(batch_size, num_fields)``
        """
        ffm_term = torch.sum(torch.sum(self.ffm(x), dim=1), dim=1, keepdim=True)
        x = self.linear(x) + ffm_term
return torch.sigmoid(x.squeeze(1))

  self.linear 是為了求出

 

  self.ffm是為了求出

 

  self.linear(x) + ffm_term表示兩部分相加

 設樣本有三個field,num_dield = 3,field1取值有10種情況,field2取值有20種情況,field3取值有10種情況

那么field_dims=[10,20,10],令嵌入維度embed_dim=4

在forward中,由於一次讀取的是batch_size個數據,設batch_size=5

那么x = [[1,3,1], [1,7,1], [2,10,2], [3,10,3], [4,11,2]]   x的shape為:batch_size*num_field=5*3

二、FeaturesLinear(field_dims)

接下來看一下FeaturesLinear(field_dims)是怎么實現的

class FeaturesLinear(torch.nn.Module):

    def __init__(self, field_dims, output_dim=1):
        super().__init__()
        self.fc = torch.nn.Embedding(sum(field_dims), output_dim)
        self.bias = torch.nn.Parameter(torch.zeros((output_dim,)))
        self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long) 

    def forward(self, x):  
        """
        :param x: Long tensor of size ``(batch_size, num_fields)``
        """
        print("FeaturesLinear x=",x)
        x = x + x.new_tensor(self.offsets,dtype=np.long).unsqueeze(0)  
        print("FeaturesLinear return=", torch.sum(self.fc(x), dim=1) + self.bias)
        return torch.sum(self.fc(x), dim=1) + self.bias 
sum(field_dims) = 10+20+10 = 40, output_dim嵌入維度默認為1
self.fc = torch.nn.Embedding(sum(field_dims), output_dim)相當於構建了一個索引字典,索引為1到40,每個索引對應一個長度為output_dim=1的向量
bias就是公式中的w0
為什么需要self.offsets,是這樣的:
  以樣本[1,3,1]為例,one-hot編碼過后其實是
  [1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,0,0,0,0,0,0,0,0,0]
  那么“1”所在的位置對應的索引分別為1、13、31.那該怎么得到這個索引呢,offsets發揮作用了,

因為eg filed_dims=[10, 20,10],那麽offsets=[0,10,30],把[1, 3, 1] + [0, 10, 20] = [1, 13, 31]
因為輸入的x = [[1,3,1], [1,7,1], [2,10,2], [3,10,3], [4,11,2]]
所以通過加上offsets之后 x變為了[1,13,31],[1,17,31],[2,20,32],[3,20,33],[4,21,32]]
self.fc(x)的會得到一個batch_size*num_field*output_dim的tensor(不清楚可以查看pytorch中embdding的用法), 它長這樣:
tensor([[[-0.3187],
         [-0.1316],
         [ 0.1061]],

        [[-0.3187],
         [ 0.1420],
         [ 0.1061]],

        [[-0.2323],
         [ 0.1549],
         [ 0.2619]],

        [[ 0.2500],
         [ 0.1549],
         [ 0.0837],

        [[ 0.1705,
         [ 0.2401],
         [ 0.2619]]]
[1,13,31]對應[[-0.3187], [-0.1316], [ 0.1061]],大小為num_filed*output_dim=3*1
[1,17,31]對應[[-0.3187], [ 0.1420], [ 0.1061]],大小為num_filed*output_dim=3*1
torch.sum(self.fc(x), dim=1)得到一個batch_size*output_dim大小的張量:
對於[1,13,31]就是把[[-0.3187], [-0.1316], [ 0.1061]]第dim=1維的數據相加.
變成為[-0.3187]+[-0.1316]+ [0.1061]=[-0.3534]
所以torch.sum(self.fc(x), dim=1)的結果為
[[-0.3534], [*], [*], [*], [*]]  # 后面四個*值自己加
最后加上bias
 

三、FieldAwareFactorizationMachine(field_dims, embed_dim)

 看看FieldAwareFactorizationMachine(field_dims, embed_dim)是怎么實現的

field_dims=[10, 20, 10], 設embed_dim=4

x+offsset = [1,13,31],[1,17,31],[2,20,32],[3,20,33],[4,21,32]]

 

class FieldAwareFactorizationMachine(torch.nn.Module):

    def __init__(self, field_dims, embed_dim):
        super().__init__()
        self.num_fields = len(field_dims)   
        self.embeddings = torch.nn.ModuleList([
            torch.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:
            torch.nn.init.xavier_uniform_(embedding.weight.data)

    def forward(self, x):
        """
        :param x: Long tensor of size ``(batch_size, num_fields)``
        """
        x = x + x.new_tensor(self.offsets, dtype=np.long).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][:, i] * xs[i][:, j])
        ix = torch.stack(ix, dim=1)
self.embeddings = torch.nn.ModuleList([ torch.nn.Embedding(sum(field_dims), embed_dim) for _ in range(self.num_fields) ])
因為在FFM中,每一維特征 xi,針對其它特征的每一種field fj,都會學習一個隱向量 v_i,fj,所以有多少個field們就要構建多少個torch.nn.Embedding層
(不太會用術語解釋,相當於總的特征為sum(field_dims)=40, 嵌入維度embed_dim=4,共有num_field=3個field,所以就要構建3個embedding,
當i=1,f_{j}=2,得到v_{1,2},表示特征1對第2個field的一個長度為4的向量)
offset的作用和上文提到的一樣
torch.nn.init.xavier_uniform_(embedding.weight.data)是一種初始化嵌入層權重的方法

xs = [self.embeddings[i](x) for i in range(self.num_fields)] 
將會的到長度為num_field=3的列表,列表中的每一個元素,大小都為batch_size*num_field*embed_dim=5*3*4
形如:(
[1,13,31],[1,17,31],[2,20,32],[3,20,33],[4,21,32]]
列表的第1個元素分別記錄着當前batch中的所有樣本各自的特征對第1個field1的隱向量
列表的第2個元素分別記錄着當前batch中的所有樣本各自的特征對第2個field2的隱向量
列表的第3個元素分別記錄着當前batch中的所有樣本各自的特征對第3個field3的隱向量
 
 
FieldAwareFactorizationMachine_xs= 
[tensor([[[-0.3187, -0.2215, 0.2950, -0.2186], # 特征“1”對field1的隱向量 [-0.1316, 0.1353, 0.3162, 0.0994], # 特征“13”對field1的隱向量 [ 0.1061, 0.0932, -0.3512, -0.2172]], # 特征“31”對field1的隱向量
      # 記錄着第一個樣本[1,13,31]的“1”號,“13”號、“31”特征對第一個field1的隱向量 [[
-0.3187, -0.2215, 0.2950, -0.2186], # 特征“1”對fieled1的隱向量 [ 0.1420, -0.0538, -0.2896, -0.1630], # 特征”17“對field1的隱向量 [ 0.1061, 0.0932, -0.3512, -0.2172]], # 特征“31”對field1的隱向量
# 記錄着第二個樣本[1, 17, 31]的“1”號、“17”號、“31”號特征對第一個filed1的影響量” [[
-0.2323, -0.0702, 0.1226, -0.0558], [ 0.1549, 0.3265, 0.1930, -0.0248], [ 0.2619, -0.2355, -0.1781, 0.1442]], [[ 0.2500, -0.2571, -0.3023, 0.2887], [ 0.1549, 0.3265, 0.1930, -0.0248], [ 0.0837, 0.1860, 0.0337, -0.3686]], [[ 0.1705, 0.1915, 0.2721, 0.0653], [ 0.2401, 0.3042, 0.1146, 0.3081], [ 0.2619, -0.2355, -0.1781, 0.1442]]],
grad_fn=<EmbeddingBackward>),
tensor([[[-0.2026, 0.0910, -0.1647, -0.0428], # 特征“1”對field2的隱向量 [ 0.1085, 0.2459, 0.2358, -0.0501], # 特征“13”對field2的隱向量 [ 0.2694, 0.0325, 0.2198, 0.2486]], # 特征“31”對field2的隱向量
# 記錄着第一個樣本[1, 13, 31]的“1”號、“13”號、“31”號特征對field2的隱向量 [[
-0.2026, 0.0910, -0.1647, -0.0428], [ 0.1493, 0.2111, 0.0914, -0.1304], [ 0.2694, 0.0325, 0.2198, 0.2486]], [[ 0.0288, -0.3006, 0.0826, 0.3179], [-0.1215, -0.3026, -0.2408, -0.2218], [-0.1306, -0.2992, -0.2194, -0.3114]], [[ 0.2275, -0.0470, 0.0298, 0.0510], [-0.1215, -0.3026, -0.2408, -0.2218], [ 0.0844, -0.3333, 0.3446, -0.0249]], [[-0.3259, -0.0525, 0.2875, -0.2050], [ 0.2183, 0.0466, 0.3299, 0.1833], [-0.1306, -0.2992, -0.2194, -0.3114]]], grad_fn=<EmbeddingBackward>),
tensor([[[-0.0756, -0.1417, 0.0075, 0.0632], # 特征“1”對field3的隱向量 [-0.0770, 0.2010, 0.0051, 0.0050], # 特征“13”對field3的隱向量 [-0.1394, 0.0776, 0.2685, -0.1017]], # 特征“31”對field3的隱向量
# 記錄着第一個樣本[1, 13, 31]的“1”號、“13”號、“31”號特征對field3的隱向量 [[
-0.0756, -0.1417, 0.0075, 0.0632], [-0.1809, 0.0321, 0.1205, 0.1586], [-0.1394, 0.0776, 0.2685, -0.1017]], [[-0.3561, 0.2795, 0.3210, -0.0522], [-0.1674, 0.1584, 0.1336, 0.1036], [-0.0826, 0.2853, 0.2323, 0.1982]], [[ 0.0778, -0.3036, -0.1546, 0.2859], [-0.1674, 0.1584, 0.1336, 0.1036], [ 0.2655, 0.1352, 0.0962, 0.1214]], [[-0.0497, 0.1356, 0.0720, 0.0554], [-0.1741, -0.0329, -0.3503, -0.0485], [-0.0826, 0.2853, 0.2323, 0.1982]]], grad_fn=<EmbeddingBackward>)]

注意以下代碼就是求:

ix = list()
        for i in range(self.num_fields - 1):
            for j in range(i + 1, self.num_fields):
                ix.append(xs[j][:, i] * xs[i][:, j])

按理說一共有40個特征,為什么循環是這樣寫的,好像應該這樣

i取1,j應該取2到40;

i取2,j應該取3到40;

……

i取39,j應該取40;

# 注意:FFM模型中不同field的特征之間,eg:“1”與"9"不交叉,因為1,和9屬於[1,2,……,10]在同一field1。同理[11,12,……,30]field2之間不交叉,[31,32,……,40]field3之間不交叉
# 考慮field1、field2、field3之間互相交叉
# 以第一個樣本[1,13,31]為例,雖然每個embeddings[i]層都有[1,2,……,40]的嵌入向量
# 但是[1,13,31]只有這1、13、31這三個位置的值不為0,就算1與20交叉過后,v_{1,f_{20}}*v_{20,f_{1}}*x_{1}*x_{20}=0,對最終的計算結果沒有貢獻
# 就只考慮了1,13,和31之間的相互交叉
# 所以我們要計算的就是x1與x13、x1與x31,x13與x31之間的交叉,得到的交叉特征共有3項。field1*field2;field1*field3;field2*field3;
(如果,num_field=4的話,交叉特征共有6項:field1*field2;field1*field3;field1*field4;field2*field3;field2*field4;field3*field4;)

 

 

 

通過FieldAwareFactorizationMachine_xs的結果我們可以看到,第一個樣本的特征對field1、field2、field3的隱向量分別為
[[-0.3187, -0.2215,  0.2950, -0.2186],  # 特征“1”對field1的隱向量
 [-0.1316,  0.1353,  0.3162,  0.0994], # 特征“13”對field1的隱向量
 [ 0.1061,  0.0932, -0.3512, -0.2172]], # 特征“31”對field1的隱向量

[[-0.2026,  0.0910, -0.1647, -0.0428], # 特征“1”對field2的隱向量
 [ 0.1085,  0.2459,  0.2358, -0.0501], # 特征“13”對field2的隱向量
 [ 0.2694,  0.0325,  0.2198,  0.2486]], # 特征“31”對field2的隱向量

[[-0.0756, -0.1417,  0.0075,  0.0632], # 特征“1”對field3的隱向量
 [-0.0770,  0.2010,  0.0051,  0.0050], # 特征“13”對field3的隱向量
  [-0.1394,  0.0776,  0.2685, -0.1017]], # 特征“31”對field3的隱向量

 

V(1,2)是“1”號特征對field2的隱向量,對應數據[-0.2026,  0.0910, -0.1647, -0.0428]
V(13,1)“13”號特征對field1的隱向量,對應數據[-0.1316, 0.1353, 0.3162, 0.0994]
ix最終得到的結果是一個長度為3(這個3是交叉特征的數目,不是num_field的數目)的列表
[tensor([[ 0.0267, 0.0123, -0.0521, -0.0043],
#         [-0.0288, -0.0049,  0.0477,  0.0070],
#         [ 0.0045, -0.0982,  0.0159, -0.0079],
#         [ 0.0352, -0.0154,  0.0057, -0.0013],
#         [-0.0783, -0.0160,  0.0329, -0.0632]], grad_fn=<MulBackward0>), tensor([[-0.0080, -0.0132, -0.0026, -0.0137],
#         [-0.0080, -0.0132, -0.0026, -0.0137],
#         [-0.0933, -0.0658, -0.0572, -0.0075],
#         [ 0.0065, -0.0565, -0.0052, -0.1054],
#         [-0.0130, -0.0319, -0.0128,  0.0080]], grad_fn=<MulBackward0>), tensor([[-0.0207, 0.0065, 0.0011, 0.0012],
#         [-0.0487,  0.0010,  0.0265,  0.0394],
#         [ 0.0219, -0.0474, -0.0293, -0.0323],
#         [-0.0141, -0.0528,  0.0460, -0.0026],
#         [ 0.0227,  0.0099,  0.0768,  0.0151]], grad_fn=<MulBackward0>)]
[ 0.0267,  0.0123, -0.0521, -0.0043] 表示,特征“1”和特征“13”交叉
我們來驗證一下:
V(1,2)*V(13,1)
=[-0.2026,  0.0910, -0.1647, -0.0428].*[-0.1316,  0.1353,  0.3162,  0.0994]
=[ 0.0267,  0.0123, -0.0521, -0.0043]  # 是正確的
同理:
[-0.0080, -0.0132, -0.0026, -0.0137]表示
特征“1”和特征“31”交叉
[-0.0207,  0.0065,  0.0011,  0.0012]表示,特征“13”和特征“31”交叉

 

分別記錄着[1, 13, 31]這個樣本,特征交叉過后的結果

 再進行一次

        ix = torch.stack(ix, dim=1)

得到的結果為(這就是第一部分,FieldAwareFactorizationMachineModelself.ffm(x)的返回值):

#tensor(
#       [[[ 0.0267, 0.0123, -0.0521, -0.0043], # [-0.0080, -0.0132, -0.0026, -0.0137], # [-0.0207, 0.0065, 0.0011, 0.0012]],
#
#         [[-0.0288, -0.0049,  0.0477,  0.0070],
#          [-0.0080, -0.0132, -0.0026, -0.0137],
#          [-0.0487,  0.0010,  0.0265,  0.0394]],
#
#         [[ 0.0045, -0.0982,  0.0159, -0.0079],
#          [-0.0933, -0.0658, -0.0572, -0.0075],
#          [ 0.0219, -0.0474, -0.0293, -0.0323]],
#
#         [[ 0.0352, -0.0154,  0.0057, -0.0013],
#          [ 0.0065, -0.0565, -0.0052, -0.1054],
#          [-0.0141, -0.0528,  0.0460, -0.0026]],
#
#         [[-0.0783, -0.0160,  0.0329, -0.0632],
#          [-0.0130, -0.0319, -0.0128,  0.0080],
#          [ 0.0227,  0.0099,  0.0768,  0.0151]]], grad_fn=<StackBackward>)

第一個樣本最終交叉特征的結果就匯總記錄到了

# [[ 0.0267, 0.0123, -0.0521, -0.0043], # [-0.0080, -0.0132, -0.0026, -0.0137], # [-0.0207, 0.0065, 0.0011, 0.0012]]

四、兩部分相加

現在linear部分和交叉特征ffm部分的結果都得到了,回到代碼,我們看看他是怎么相加的,(黑體下划線的代碼

class FieldAwareFactorizationMachineModel(torch.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):
        """
        :param x: Long tensor of size ``(batch_size, num_fields)``
        """ ffm_term = torch.sum(torch.sum(self.ffm(x), dim=1), dim=1, keepdim=True) x = self.linear(x) + ffm_term return torch.sigmoid(x.squeeze(1))

我們可以看到ffm_term將會得到一個batch_size*1的tensor

而 self.linear(x)得到是batch_size*output_dim=5*1的tensor

兩部分可以直接相加,最后消除x長度為1的維度x.squeeze(1),

在經過一個sigmoid,就得到了大小為batch_size的一個tensor,tensor中的每一個元素都在0到1之間,這就是這屋個樣本模型計算出來是否會被點擊的概率。

然后根據他們真實的標簽“1”或者“0”,計算logloss就可以了,這樣一個batch_size的計算過程就結束了。

 

不清楚的地方或不足之處,請留言互相交流。

 

1, 17, 31


免責聲明!

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



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