排序學習(learning to rank)中的ranknet pytorch簡單實現


一.理論部分

  理論部分網上有許多,自己也簡單的整理了一份,這幾天會貼在這里,先把代碼貼出,后續會優化一些寫法,這里將訓練數據寫成dataset,dataloader樣式。

  排序學習所需的訓練樣本格式如下:

  

  解釋:其中第二列是query id,第一列表示此query id與這條樣本的相關度(數字越大,表示越相關),從第三列開始是本條樣本的特征向量。

  • RankNet:

         RankNet是屬於pairwise方法,它是將某個query下的所有文檔兩兩組成文檔對,每個文檔對作為一個樣本:

  A.  預測相關性概率:

        

  解釋:對於任一個doc對(Ui,Uj),模型輸出的得分為si和sj,那么根據模型預測Ui比Uj與query更相關的概率。RankNet一般采用神經網絡,sigmoid能提供一個較好的概率評估。

  B.  真實相關性概率:

        

  解釋:真實數據對中的Ui和Uj都包含一個與query相關度的label,比如Ui為3,Uj為1,則Ui比Uj與query更相關,這里是定義Ui比Uj更相關的真實概率。Sij定義為1:Ui比Uj更相關;-1:Uj比Ui更相關;0:Ui與Uj相關度相同。

  C.  代價函數:

        

  解釋:這里使用交叉熵來擬合真實概率與預測概率,兩個分布越接近,交叉熵越小。

  D.  問題:

  問題一:沒有使用排序中的一些評估指標直接作為代價函數,原因是這些指標函數不連續,不好求導,不太好用梯度下降,交叉熵適合梯度下降。

  問題二:在正常訓練時,對每個樣本對{i,j}都會更新一次參數,采用BP時,更新一次需要先前向預測,再誤差后反向傳播,會很慢。

  E.在實際使用中,ranknet采用神經網絡方法進行學習,一般采用的是帶有隱層的神經網絡。學習過程一般使用誤差反向傳播方法來訓練。

  如何訓練呢?這里提供了兩種思路:
1)取一個樣本對(Xi, Xj),首先對Xi帶入神經網絡進行前向反饋,其次將Xj帶入神經網絡進行前向反饋,
然后計算差分結果並進行誤差反向傳播,接着取下一個樣本對。這種方法很直觀,缺點是收斂速度慢。
2)批量訓練。我們可以對同一個排序下的所有文檔pair全部帶入神經網絡進行前向反饋,
然后計算總差分並進行誤差反向傳播,這樣將大大減少誤差反向傳播的次數。

 

  大家可以參考論文《From RankNet to LambdaRank to LambdaMART: An Overview》,這篇論文從RankNet,LambdaRank講到LambdaMart的這三種排序學習方法,后面的都是在前面的基礎上進行改進提出的。基中RankNet來自論文《Learning to Rank using Gradient Descent》,LambdaRank來自論文《Learning to Rank with Non-Smooth Cost Functions》,LambdaMart來自《Selective Gradient Boosting for Effective Learning to Rank》。RankNet與LambdaRank是神經網絡模型,LambdaRank加速了計算和引入了排序的評估指標NDCG,提出了lambda概念。而LambdaMart的核心則是利用了GBDT,即MART,這里每棵樹擬合的不是殘差(平方損失的梯度是殘差,其它損失叫負梯度),而是Lambda這個值,這個值代表這篇文檔在下次迭代時的方向和強度,lambdamart不需要顯式定義損失函數,更加不需要對損失函數求導(因為ndcg非連續),lambda充當了擬合目標,在實際計算時,會為每個文檔計算一個lambda值。

  ......

二.pytorch實現RankNet

 1 import torch
 2 import torch.utils.data as data
 3 import numpy as np
 4 
 5 y_train = []
 6 x_train = []
 7 query_id = []
 8 array_train_x1 = []
 9 array_train_x0 = []
10 
11 def extract_features(toks):
12     # 獲取features
13     features = []
14     for tok in toks:
15         features.append(float(tok.split(":")[1]))
16     return features
17 
18 def extract_query_data(tok):
19     #獲取queryid documentid
20     query_features = [tok.split(":")[1]] #qid
21     return query_features
22 
23 def get_format_data(data_path):
24     with open(data_path, 'r', encoding='utf-8') as file:
25         for line in file:
26             data, _, comment = line.rstrip().partition("#")
27             toks = data.split()
28             y_train.append(int(toks[0])) #相關度
29             x_train.append(extract_features(toks[2:])) # doc features
30             query_id.append(extract_query_data(toks[1])) #qid
31 
32 def get_pair_doc_data(y_train, query_id):
33     #兩兩組合pair
34     pairs = []
35     tmp_x0 = []
36     tmp_x1 = []
37     for i in range(0, len(query_id) - 1):
38         for j in range(i + 1, len(query_id)):
39             #每個query下的文檔
40             if query_id[i][0] != query_id[j][0]:
41                 break
42             #使用不同相關度的文檔pair
43             if (query_id[i][0] == query_id[j][0]) and (y_train[i] != y_train[j]):
44                 #將最相關的放在前面,保持文檔pair中第一個doc比第二個doc與query更相關
45                 if y_train[i] > y_train[j]:
46                     pairs.append([i,j])
47                     tmp_x0.append(x_train[i])
48                     tmp_x1.append(x_train[j])
49                 else:
50                     pairs.append([j,i])
51                     tmp_x0.append(x_train[j])
52                     tmp_x1.append(x_train[i])
53     #array_train_x0里和array_train_x1里對應的下標元素,保持前一個元素比后一個元素更相關
54     array_train_x0 = np.array(tmp_x0)
55     array_train_x1 = np.array(tmp_x1)
56     print('fond {} doc pairs'.format(len(pairs)))
57     return len(pairs), array_train_x0, array_train_x1
58 
59 class Dataset(data.Dataset):
60     '''
61     torch.utils.data.Dataset 是一個表示數據集的抽象類. 你自己的數據集一般應該繼承Dataset, 並且重寫下面的方法:
62     __len__使用len(dataset) 可以返回數據集的大小
63     __getitem__ 支持索引, 以便於使用 dataset[i] 可以 獲取第i個樣本(0索引)
64     數據集創建一個數據集類. 我們使用 __init__方法來初始化, 使用 __getitem__根據索引讀取樣本.
65     這樣可以使內存高效利用, 因為我們並不需要在內存中一次存儲所有圖片, 而是按需讀取.
66     '''
67     def __init__(self, data_path):
68         # 解析訓練數據
69         get_format_data(data_path)
70         # pair組合
71         self.datasize, self.array_train_x0, self.array_train_x1 = get_pair_doc_data(y_train, query_id)
72 
73     def __getitem__(self, index):
74         data1 = torch.from_numpy(self.array_train_x0[index]).float()
75         data2 = torch.from_numpy(self.array_train_x1[index]).float()
76         return data1, data2
77 
78     def __len__(self):
79         return self.datasize
80 
81 def get_loader(data_path, batch_size, shuffle, num_workers):
82     dataset = Dataset(data_path)
83     data_loader = torch.utils.data.DataLoader(
84         dataset=dataset,
85         batch_size = batch_size,
86         shuffle = shuffle,
87         num_workers=num_workers
88     )
89     return data_loader
 1 import torch
 2 import torch.nn as nn
 3 import torch.optim as optim
 4 import numpy as np
 5 import os
 6 
 7 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 8 
 9 class RankNet(nn.Module):
10     def __init__(self, inputs, hidden_size, outputs):
11         super(RankNet, self).__init__()
12         self.model = nn.Sequential(
13             nn.Linear(inputs, hidden_size),
14             #nn.Dropout(0.5),
15             nn.ReLU(inplace=True),
16             #nn.LeakyReLU(0.2,  inplace=True),#inplace為True,將會改變輸入的數據 ,否則不會改變原輸入,只會產生新的輸出
17             nn.Linear(hidden_size, outputs),
18             #nn.Sigmoid()
19         )
20         self.sigmoid = nn.Sigmoid()
21 
22     def forward(self, input_1, input_2):
23         result_1 = self.model(input_1) #預測input_1得分
24         result_2 = self.model(input_2) #預測input_2得分
25         pred = self.sigmoid(result_1 - result_2) #input_1比input_2更相關概率
26         return pred
27 
28     def predict(self, input):
29         result = self.model(input)
30         return result
31 
32 def train():
33     # 超參
34     inputs = 38
35     hidden_size = 10
36     outputs = 1
37     learning_rate = 0.2
38     num_epochs = 100
39     batch_size = 100
40 
41     model = RankNet(inputs, hidden_size, outputs).to(device)
42     #損失函數和優化器
43     criterion = nn.BCELoss()
44     optimizer = optim.Adadelta(model.parameters(), lr = learning_rate)
45 
46     base_path = os.path.abspath(os.path.join(os.getcwd(), '..'))
47     base_path = os.path.dirname(base_path)
48     data_path = base_path + '/goods_data/train/train_result.txt'
49 
50     data_loader = get_loader(data_path, batch_size, False, 4)
51     total_step = len(data_loader)
52     #  這里使用batch size的方式,並非每次傳入一對docs進行前向和后向傳播
53     # (tips:還有一種是將每個query下的所有docs對作為batch輸入到網絡中進行前向和后向,但是這里沒法用到Dataset和DataLoader)
54     for epoch in range(num_epochs):
55         for i, (data1, data2) in enumerate(data_loader):
56             print('Epoch [{}/{}], Step [{}/{}]'.format(epoch, num_epochs, i, total_step))
57             data1 = data1.to(device)
58             data2 = data2.to(device)
59             label_size = data1.size()[0]
60             pred = model(data1, data2)
61             loss = criterion(pred, torch.from_numpy(np.ones(shape=(label_size, 1))).float().to(device))
62             optimizer.zero_grad()
63             loss.bachward()
64             optimizer.step()
65         if i % 10 == 0:
66             print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'
67                   .format(epoch + 1, num_epochs, i + 1, total_step, loss.item()))
68 
69     torch.save(model.state_dict(), 'model.ckpt')
70 
71 def test():
72     #test data
73     base_path = os.path.abspath(os.path.join(os.getcwd(), '..'))
74     base_path = os.path.dirname(base_path)
75     test_path = base_path + '/goods_data/test/test_result.txt'
76 
77     # 超參
78     inputs = 38
79     hidden_size = 10
80     outputs = 1
81     model = RankNet(inputs, hidden_size, outputs).to(device)
82     model.load_state_dict(torch.load('model.ckpt'))
83 
84     with open(test_path, 'r', encoding='utf-8') as f:
85         features = []
86         for line in f:
87             toks = line.split()
88             feature = []
89             for tok in toks[2:]:
90                 _, value = tok.split(":")
91                 feature.append(float(value))
92             features.append(feature)
93         features = np.array(features)
94     features = torch.from_numpy(features).float().to(device)
95     predict_score = model.predict(features)

 


免責聲明!

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



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