一.理论部分

  理论部分网上有许多,自己也简单的整理了一份,这几天会贴在这里,先把代码贴出,后续会优化一些写法,这里将训练数据写成dataset,dataloader样式。

  排序学习所需的训练样本格式如下:

  排序学习(learning to rank)中的ranknet pytorch简单实现

  解释:其中第二列是query id,第一列表示此query id与这条样本的相关度(数字越大,表示越相关),从第三列开始是本条样本的特征向量。

  • RankNet:

         RankNet是属于pairwise方法,它是将某个query下的所有文档两两组成文档对,每个文档对作为一个样本:

  A.  预测相关性概率:

        排序学习(learning to rank)中的ranknet pytorch简单实现

  解释:对于任一个doc对(Ui,Uj),模型输出的得分为si和sj,那么根据模型预测Ui比Uj与query更相关的概率。RankNet一般采用神经网络,sigmoid能提供一个较好的概率评估。

  B.  真实相关性概率:

        排序学习(learning to rank)中的ranknet pytorch简单实现

  解释:真实数据对中的Ui和Uj都包含一个与query相关度的label,比如Ui为3,Uj为1,则Ui比Uj与query更相关,这里是定义Ui比Uj更相关的真实概率。Sij定义为1:Ui比Uj更相关;-1:Uj比Ui更相关;0:Ui与Uj相关度相同。

  C.  代价函数:

        排序学习(learning to rank)中的ranknet pytorch简单实现

  解释:这里使用交叉熵来拟合真实概率与预测概率,两个分布越接近,交叉熵越小。

  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)