前言:主要記錄,在推薦系統利用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)
得到的結果為(這就是第一部分,FieldAwareFactorizationMachineModel中self.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