一.理論部分
理論部分網上有許多,自己也簡單的整理了一份,這幾天會貼在這里,先把代碼貼出,后續會優化一些寫法,這里將訓練數據寫成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)