• 排序学习(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)
  • 相关阅读:
    网络编程--requests模块
    python连接oracle--cx_Oracle模块
    操作excel
    操作mysql数据库
    枚举--enumerate
    邮箱
    匿名函数
    使用spring security jwt 、spring security oauth2权限控制遇到的坑记录
    基于element-ui定义自己的Menu 菜单组件并以component方式去定渲染
    VUE同域情况下父窗体打开子窗口,关闭子窗口时刷新父窗体
  • 原文地址:https://www.cnblogs.com/little-horse/p/10468311.html
Copyright © 2020-2023  润新知