排序学习(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
import torch
import torch.utils.data as data
import numpy as np y_train = []
x_train = []
query_id = []
array_train_x1 = []
array_train_x0 = [] def extract_features(toks):
# 获取features
features = []
for tok in toks:
features.append(float(tok.split(":")[1]))
return features def extract_query_data(tok):
#获取queryid documentid
query_features = [tok.split(":")[1]] #qid
return query_features def get_format_data(data_path):
with open(data_path, 'r', encoding='utf-8') as file:
for line in file:
data, _, comment = line.rstrip().partition("#")
toks = data.split()
y_train.append(int(toks[0])) #相关度
x_train.append(extract_features(toks[2:])) # doc features
query_id.append(extract_query_data(toks[1])) #qid def get_pair_doc_data(y_train, query_id):
#两两组合pair
pairs = []
tmp_x0 = []
tmp_x1 = []
for i in range(0, len(query_id) - 1):
for j in range(i + 1, len(query_id)):
#每个query下的文档
if query_id[i][0] != query_id[j][0]:
break
#使用不同相关度的文档pair
if (query_id[i][0] == query_id[j][0]) and (y_train[i] != y_train[j]):
#将最相关的放在前面,保持文档pair中第一个doc比第二个doc与query更相关
if y_train[i] > y_train[j]:
pairs.append([i,j])
tmp_x0.append(x_train[i])
tmp_x1.append(x_train[j])
else:
pairs.append([j,i])
tmp_x0.append(x_train[j])
tmp_x1.append(x_train[i])
#array_train_x0里和array_train_x1里对应的下标元素,保持前一个元素比后一个元素更相关
array_train_x0 = np.array(tmp_x0)
array_train_x1 = np.array(tmp_x1)
print('fond {} doc pairs'.format(len(pairs)))
return len(pairs), array_train_x0, array_train_x1 class Dataset(data.Dataset):
'''
torch.utils.data.Dataset 是一个表示数据集的抽象类. 你自己的数据集一般应该继承Dataset, 并且重写下面的方法:
__len__使用len(dataset) 可以返回数据集的大小
__getitem__ 支持索引, 以便于使用 dataset[i] 可以 获取第i个样本(0索引)
数据集创建一个数据集类. 我们使用 __init__方法来初始化, 使用 __getitem__根据索引读取样本.
这样可以使内存高效利用, 因为我们并不需要在内存中一次存储所有图片, 而是按需读取.
'''
def __init__(self, data_path):
# 解析训练数据
get_format_data(data_path)
# pair组合
self.datasize, self.array_train_x0, self.array_train_x1 = get_pair_doc_data(y_train, query_id) def __getitem__(self, index):
data1 = torch.from_numpy(self.array_train_x0[index]).float()
data2 = torch.from_numpy(self.array_train_x1[index]).float()
return data1, data2 def __len__(self):
return self.datasize def get_loader(data_path, batch_size, shuffle, num_workers):
dataset = Dataset(data_path)
data_loader = torch.utils.data.DataLoader(
dataset=dataset,
batch_size = batch_size,
shuffle = shuffle,
num_workers=num_workers
)
return data_loader
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import os device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') class RankNet(nn.Module):
def __init__(self, inputs, hidden_size, outputs):
super(RankNet, self).__init__()
self.model = nn.Sequential(
nn.Linear(inputs, hidden_size),
#nn.Dropout(0.5),
nn.ReLU(inplace=True),
#nn.LeakyReLU(0.2, inplace=True),#inplace为True,将会改变输入的数据 ,否则不会改变原输入,只会产生新的输出
nn.Linear(hidden_size, outputs),
#nn.Sigmoid()
)
self.sigmoid = nn.Sigmoid() def forward(self, input_1, input_2):
result_1 = self.model(input_1) #预测input_1得分
result_2 = self.model(input_2) #预测input_2得分
pred = self.sigmoid(result_1 - result_2) #input_1比input_2更相关概率
return pred def predict(self, input):
result = self.model(input)
return result def train():
# 超参
inputs = 38
hidden_size = 10
outputs = 1
learning_rate = 0.2
num_epochs = 100
batch_size = 100 model = RankNet(inputs, hidden_size, outputs).to(device)
#损失函数和优化器
criterion = nn.BCELoss()
optimizer = optim.Adadelta(model.parameters(), lr = learning_rate) base_path = os.path.abspath(os.path.join(os.getcwd(), '..'))
base_path = os.path.dirname(base_path)
data_path = base_path + '/goods_data/train/train_result.txt' data_loader = get_loader(data_path, batch_size, False, 4)
total_step = len(data_loader)
# 这里使用batch size的方式,并非每次传入一对docs进行前向和后向传播
# (tips:还有一种是将每个query下的所有docs对作为batch输入到网络中进行前向和后向,但是这里没法用到Dataset和DataLoader)
for epoch in range(num_epochs):
for i, (data1, data2) in enumerate(data_loader):
print('Epoch [{}/{}], Step [{}/{}]'.format(epoch, num_epochs, i, total_step))
data1 = data1.to(device)
data2 = data2.to(device)
label_size = data1.size()[0]
pred = model(data1, data2)
loss = criterion(pred, torch.from_numpy(np.ones(shape=(label_size, 1))).float().to(device))
optimizer.zero_grad()
loss.bachward()
optimizer.step()
if i % 10 == 0:
print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'
.format(epoch + 1, num_epochs, i + 1, total_step, loss.item())) torch.save(model.state_dict(), 'model.ckpt') def test():
#test data
base_path = os.path.abspath(os.path.join(os.getcwd(), '..'))
base_path = os.path.dirname(base_path)
test_path = base_path + '/goods_data/test/test_result.txt' # 超参
inputs = 38
hidden_size = 10
outputs = 1
model = RankNet(inputs, hidden_size, outputs).to(device)
model.load_state_dict(torch.load('model.ckpt')) with open(test_path, 'r', encoding='utf-8') as f:
features = []
for line in f:
toks = line.split()
feature = []
for tok in toks[2:]:
_, value = tok.split(":")
feature.append(float(value))
features.append(feature)
features = np.array(features)
features = torch.from_numpy(features).float().to(device)
predict_score = model.predict(features)
排序学习(learning to rank)中的ranknet pytorch简单实现的更多相关文章
- Learning to rank相关的pointwise,pairwise,listwise
论文分享--- >Learning to Rank: From Pairwise Approach to Listwise Approach 学习排序 Learning to Rank 小结 [ ...
- Learning to rank 介绍
PS:文章主要转载自CSDN大神hguisu的文章"机器学习排序": http://blog.csdn.net/hguisu/article/details/79 ...
- Learning to rank的讲解,单文档方法(Pointwise),文档对方法(Pairwise),文档列表方法(Listwise)
学习排序(Learning to Rank) LTR(Learning torank)学习排序是一种监督学习(SupervisedLearning)的排序方法.LTR已经被广泛应用到文本挖掘的很多领域 ...
- [笔记]Learning to Rank算法介绍:RankNet,LambdaRank,LambdaMart
之前的博客:http://www.cnblogs.com/bentuwuying/p/6681943.html中简单介绍了Learning to Rank的基本原理,也讲到了Learning to R ...
- Learning to Rank算法介绍:RankNet,LambdaRank,LambdaMart
之前的博客:http://www.cnblogs.com/bentuwuying/p/6681943.html中简单介绍了Learning to Rank的基本原理,也讲到了Learning to R ...
- 排序学习实践---ranknet方法
要: 1 背景 随着移动互联网的崛起,越来越多的用户开始习惯于从手机完成吃.喝.玩.乐.衣.食.住.行等各个方面的需求.打开手机,点开手淘.美团等APP,商品玲玲满目,而让用户将所有商品一页 ...
- Learning to Rank之RankNet算法简介
排序一直是信息检索的核心问题之一, Learning to Rank(简称LTR)用机器学习的思想来解决排序问题(关于Learning to Rank的简介请见我的博文Learning to Rank ...
- 推荐排序---Learning to Rank:从 pointwise 和 pairwise 到 listwise,经典模型与优缺点
转载:https://blog.csdn.net/lipengcn/article/details/80373744 Ranking 是信息检索领域的基本问题,也是搜索引擎背后的重要组成模块. 本文将 ...
- 【RS】List-wise learning to rank with matrix factorization for collaborative filtering - 结合列表启发排序和矩阵分解的协同过滤
[论文标题]List-wise learning to rank with matrix factorization for collaborative filtering (RecSys '10 ...
随机推荐
- mybatis插入数据返回主键
原来之前一直用错了... keyProperty是表示将返回的主键设置为该方法参数的对应属性中去,而不是用返回值的形式的去获取.
- 怎样使用 CSS 清除 input 输入框聚焦选中时的蓝色边框?
input 输入框的聚焦选中时的边框是由 outline 属性控制的, 直接使用: input { outline: none } 即可. 如下:
- prefixOverrides使用注意是事项
不可以prefixOverrides=',' 否则执行的sql格式可能为 id ,time ,name... 导致sql报错,或者执行结果出错
- 非常有用的pointer-events属性
介绍 pointer-events是css3的一个属性,指定在什么情况下元素可以成为鼠标事件的target(包括鼠标的样式) 属性值 pointer-events属性有很多值,但是对于浏览器来说,只有 ...
- 用帝国cms 反馈内容的时候自动发送邮箱开发流程
开发流程是这样的. 第一步:先找好一个发送的邮箱.以及一个接受的邮箱.这里是 测试的,我都用的qq邮箱,帝国cms后台配置如下: 注意:这里的SMTP服务器是腾讯的,如果你用163的或者其他的请自行百 ...
- vue中params-解决换路由不刷新问题
因为依赖路由的params参数获取写在created生命周期里面,因为相同路由二次甚至多次加载的关系 没有达到监听,退出页面再进入另一个文章页面并不会运行created组件生命周期,导致文章数据还是第 ...
- vue的数据代理
1. vue数据代理: data对象的所有属性的操作(读/写)由vm对象来代理操作2. 好处: 通过vm对象就可以方便的操作data中的数据3. 实现: 1). 通过Object.defineProp ...
- vue-cli3.x创建项目vue create hello-world
在git中输入指令vue create hello-world,没反应,因为vue-cli的版本问题,必须3.x版本才能使用这个指令于是按照官网的提示升级vue vue-cli从2.x版本升级到3.x ...
- DataSnap跨域
- 逆向破解 H.Koenig 遥控器 Part 1
逆向破解 H.Koenig 遥控器(Part 1) 最近我正在尝试一研究些自动吸尘器机器人.iRobot公司的Roomba貌似是该领域的领导者,但是作为实验来讲的话这些东西真是太昂贵了,我也找不到 ...