【学习排序】 Learning to Rank 中Listwise关于ListNet算法讲解及实现

2023-11-18

    前一篇文章"Learning to Rank中Pointwise关于PRank算法源码实现"讲述了基于点的学习排序PRank算法的实现.该篇文章主要讲述Listwise Approach和基于神经网络的ListNet算法及Java实现.包括:
    1.基于列的学习排序(Listwise)介绍
    2.ListNet算法介绍
    3.ListNet算法Java实现

    LTR中单文档方法是将训练集里每一个文档当做一个训练实例,文档对方法是将同一个查询的搜索结果里任意两个文档对作为一个训练实例,文档列方法是将一个查询里的所有搜索结果列表作为一个训练实例. 

一. 基于列的学习排序(Listwise)介绍

    Listwise方法将一个查询对应的所有搜索结果评分作为一个实例,训练得到一个最优的评分函数.在给出如下数据集中:(数据集介绍详见上一篇文章)

===============================================================================
0 qid:10 1:0.000000 2:0.000000 3:0.000000 ... 45:0.000000 46:0.000000 #docid =
1 qid:10 1:0.031310 2:0.666667 3:0.500000 ... 45:0.448276 46:0.000000 #docid =
1 qid:10 1:0.078682 2:0.166667 3:0.500000 ... 45:1.000000 46:0.000000 #docid =
 0 qid:50 1:0.044248 2:0.400000 3:0.333333 ... 45:0.622951 46:0.000000 #docid = 
2 qid:50 1:0.764381 2:0.200000 3:0.000000 ... 45:0.252874 46:0.000000 #docid =
1 qid:50 1:0.693584 2:0.000000 3:0.000000 ... 45:0.275862 46:0.000000 #docid =
===============================================================================
    基于列的学习排序(Listwise Approach)是将qid=10对应的所有查询文档作为一个实例进行训练,即一个查询及其对应的所有搜索结果评分作为一个实例进行训练;训练得到一个最后评分函数F后,test测试集中一个新的查询,函数F对每一个文档进行打分,之后按照得分顺序由高到低排序即是对应搜索的结果.
    下面介绍一种基于搜索结果排序组合的概率分布情况来训练.如下图:
    参考《这就是搜索引擎:核心技术详解 by:张俊林》第5章


    用户输入查询Q1,假设返回的搜索结果集合里包含A、B和C三个文档,搜索引擎要对搜索结果排序,而3个文档顺序共有6种排列组合方式:ABC、ACB、BAC、BCA、CAB和CBA,每种排列组合都是一种可能的搜索结果排序方法.
    我们可以把函数g设想成最优评分函数(人工打分),对查询Q1来说:文档A得6分,文档B得4分,文档C得3分;我们的任务是找到一个函数,使得其对Q1的搜索结果打分顺序尽可能的接近标准函数g.其中函数f和h就是实际的评分函数,通过比较两个概率之间的KL距离,发现f比h更接近假想的最优函数g.故选择函数f为搜索的评分函数.
    Listwise主要的算法包括:AdaRank、SVM-MAP、ListNet、LambdaMART等.

二. ListNet算法介绍

    Pointwise学习排序是将训练集中的每个文档看作一个样本获取Rank函数,主要解决办法是把分类问题转换为单个文档的分类和回归问题,如PRank.
    Pairwise学习排序(下篇介绍)是将同一个查询中不同的相关标注的两个文档看作一个样本,主要解决思想是把Rank问题转换为二值分类问题,如RankNet.
    Listwise学习排序是将整个文档序列看作一个样本,主要是通过直接优化信息检索的评价方法和定义损失函数两种方法实现.ListNet算法将Luce模型引入到了排序学习方法中来表示文档序列,同时大多数基于神经网络的排序学习算法都是基于Luce模型(Luce模型就是将序列的任意一种排序方式表示成一个概率值)来表示序列的排序方式的.

    ListNet算法参考:
   《Learning to Rank: From Pairwise Approach to Listwise Approach
   《基于神经网络的Listwise排序学习方法的研究》 By:林原


    通过该算法步骤解释如下:
    1.首先输入训练集train.txt数据.{x,y}表示查询号对应的样本文档,包括标注等级Label=y(46维微软数据集共3个等级:0-不相关,1-部分相关,2-全部相关),x表示对应的特征和特征值,需要注意的是x(m)表示m个qid数,每个x(m)中有多个样本文档.
    2.初始化操作.迭代次数T(设置为30次)和Learning rate(ita可以为0.003、0.001、0.03、0.01等),同时初始化权重w.
    3.两层循环操作.第一层是循环迭代次数:for t = 1 to T do;第二层循环是迭代查询总数(qid总数):for i = 1 to m do.
    4.计算该行分数用当前权重w.注意权重w[46]是一维数组,分别对应46个特征值,同时f(w) = w * x.

   5.计算梯度向量delta_w(46个维度).其中计算公式如下:

    其中n(i)表示查询号qid=i对应的总文档数,j表示qid=i的当前文档.x的右上方下标表示对应的qid数,右下方下标表示对应的文档标号.而P是计算概率的函数,如下:

    它表示S1排第一、S2排第二且S3排第三的概率值.这就是使用Luce模型使一个序列的排序方式表示成一个单一的概率值.实际过程中,我们通过使用exp()函数来表示fai.主要保证其值为正、递增.
    但N!的时间复杂度很显然效率很低,所以提出了Top-K概率来解决,即用前k项的排列概率来近似原有的整个序列的概率,通过降低精准度来换取运行时间.
    Top-K概率公式如下:

    在下面的Java代码实现中我采用的是Top-1,即获取当前行文档排第一的概率值.
    6.循环更新权重w.
    7.最后输出w[46]权重,训练过程结束.通过该模型可以进行测试预测排序,test.txt通过该权重进行w*x打分,再进行从高到低排序即可.

    PS:这仅仅是我结合两篇论文后的个人理解,如果有错误或不足之处,欢迎探讨!同时感谢我的同学XP和MT,我们一起探讨和分享才理解了一些ListNet算法及代码.

三. ListNet算法Java实现

    (PS:该部分代码非常感谢我的组长XP和MT,他们在整个编程路上对我帮助是一生的.同时自己也希望以后工作中能找到更多的老师和挚友指导我前行~)
    代码中有详细的注释,按照每个步骤完成.左图是主函数,它主要包括:读取文件并解析数据、写数据、学习排序模型和打分预测,右图是学习排序的核心算法.

  
    代码如下:
package listNet_xiuzhang;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.InputStreamReader;

public class listNet {
	
	//文件总行数(标记数)
	private static int sumLabel;                   
	//特征值 46个 (标号1-46)
	private static double feature[][] = new double[100000][48];                
	//特征值权重 46个 (标号1-46)
	private static double weight [] = new double[48];
	//相关度 其值有0-2三个级别 从1开始记录
	private static int label [] = new int[1000000];
	//查询id 从1开始记录
	private static int qid [] = new int[1000000];
	//每个Qid的doc数量
	private static int doc_ofQid[] = new int[100000]; 

	private static int ITER_NUM=30;     //迭代次数
	private static int weidu=46;        //特征数
	private static int qid_Num=0;       //Qid数量
	private static int tempQid=-1;      //临时Qid数
	private static int tempDoc=0;       //临时doc数
	
	/**  
	 * 函数功能 读取文件
	 * 参数 String filePath 文件路径
	 */
	public static void ReadTxtFile(String filePath) {
        try {
        	String encoding="GBK";
        	File file=new File(filePath);
        	if(file.isFile() && file.exists()) { //判断文件是否存在
        		InputStreamReader read = new InputStreamReader(new FileInputStream(file), encoding); 
                BufferedReader bufferedReader = new BufferedReader(read);
                String lineTxt = null;
                sumLabel =1; //初始化从1记录
                //按行读取数据并分解数据
                while((lineTxt = bufferedReader.readLine()) != null) {
                	String str = null;
                	int lengthLine = lineTxt.length();
                	//获取数据 字符串空格分隔
                	String arrays[] = lineTxt.split(" ");
                	for(int i=0; i<arrays.length; i++) {
                		//获取每行样本的Label值
                		if(i==0) {
                			label[sumLabel] = Integer.parseInt(arrays[0]);
                		} 
                		else if(i>=weidu+2){ //读取至#跳出 0-label 1-qid 2:47-特征
                			continue;
                		}
                		else {
                			String subArrays[] = arrays[i].split(":"); //特征:特征值
                			if(i==1) { //获取qid		
                				//判断是否是新的Qid
                				if(tempQid != Integer.parseInt(subArrays[1])) { 
                					if(tempQid != -1){ //不是第一次出现新Qid
                						//赋值上一个为qid_Num对应的tempDoc个文档
                						doc_ofQid[qid_Num]=tempDoc;    
                						tempDoc=0;
                					}
                					//当tempQid不等于当前qid时下标加1 
                					//相等则直接跳至Doc加1直到不等
                					qid_Num++;
                					tempQid=Integer.parseInt(subArrays[1]);    					
                				}
                				tempDoc++; //新的文档 
                				qid[sumLabel] = Integer.parseInt(subArrays[1]);
                			} 
                			else { //获取46维特征值
                				int number = Integer.parseInt(subArrays[0]); //判断特征
                				double value = Double.parseDouble(subArrays[1]);
                				feature[sumLabel][number] = value; //number数组标号:1-46
                			}
                		}
                	}
                	sumLabel++;
                }
                doc_ofQid[qid_Num]=tempDoc;
                read.close();
        	} else {
        		System.out.println("找不到指定的文件\n");
        	}
        } catch (Exception e) {
            System.out.println("读取文件内容出错");
            e.printStackTrace();
        }
    }

	/**
	 * 学习排序
	 * 训练模型得到46维权重
	 */
	public static void LearningToRank() {
		
		//变量
		double index [] = new double[1000000];
		double tao [] = new double[1000000];
		double yita=0.00003;
		//初始化
		for(int i=0;i<weidu+2;i++) { //从1到136为权重,0和137无用
			weight[i] = (double) 1.0; //权重初值
		}
		System.out.println("training...");				
		//计算权重 学习算法
		for(int iter = 0; iter<ITER_NUM; iter++) //迭代ITER_NUM次
		{ 
			System.out.println("---迭代次数:"+iter);
			int now_doc=0; //全局文档索引
			for(int i=1; i<=qid_Num; i++) //总样qid数  相当于两层循环T和m 
			{ 
				double delta_w[] = new double[weidu+2]; //46个梯度组成的向量
				int doc_of_i=doc_ofQid[i]; //该Qid的文档数
				//得分f(w),一个QID有多个文档,一个文档为一个分,所以一个i对应一个分数数组
				double fw[] = new double[doc_of_i+2];
				
				/* 第一步 算得分数组fw fin */
				for(int k=1;k<=doc_of_i;k++) { //初始化
					fw[k]=0.0;
				}
				for(int k=1;k<=doc_of_i;k++) { //每个文档的得分
					for(int p=1;p<=weidu;p++) {
						fw[k]=fw[k]+weight[p]*feature[now_doc+k][p]; //算出这个文档的分数
					}
				}
				
				/*
				 * 第二步  算梯度delta_w向量
				 * a=Σp*x,a是向量  
				 * b=Σexpf(x),b是数字
				 * c=expf(x)*x,c是向量
				 * 最终结果delta_w是向量
				 */
				double[] a=new double[weidu+2],c=new double[weidu+2];
				for(int k=0;k<weidu+2;k++){a[k]=0.0;} //初始化
				for(int k=0;k<weidu+2;k++){c[k]=0.0;} //初始化
				double b=0.0;
				//算a:----
				for(int k=1; k<=doc_of_i; k++) {
					double p=1.0; //先不topK
					double[] temp=new double[48];
					for(int q=1;q<=weidu;q++) {
						//算P: ----第q个向量排XX的概率是多少
						//分母:
						double fenmu=0.0;
						for(int m=1;m<=doc_of_i;m++) {
							fenmu=fenmu+Math.exp(fw[m]); //所有文档得分
						}
						//top-1  exp(s1) / exp(s1)+exp(s2)+..+exp(sn)
						for(int m=1;m<=doc_of_i;m++) {
							p=p*(Math.exp(fw[m])/fenmu);
						}
						//算积
						temp[q]=temp[q]+p*feature[now_doc+k][q];
					}
					for(int q=1; q<=weidu; q++){			
						a[q]=a[q]+temp[q];
					}	
				} //End a
				//算b:---- fin.
				for(int k=1; k<=doc_of_i; k++){
					b=b+Math.exp(fw[k]);
				}
				//算c:----
				for(int k=1; k<=doc_of_i; k++){
					double[] temp=new double[weidu+2];
					for(int q=1; q<=weidu; q++){			
						temp[q]=temp[q]+Math.exp(fw[k])*feature[now_doc+k][q];
					}
					for(int q=1; q<=weidu; q++){			
						c[q]=c[q]+temp[q];
					}	
				}
				//算梯度:delta_x=-a+1/b*c
				for(int q=1; q<=weidu; q++){
					delta_w[q]= (-1)*a[q] + ((1.0/b)*c[q]);
				}
				//**********
				
				/* 第三步 更新权重 fin. */
				for(int k=1; k<=weidu; k++){
					weight[k]=weight[k]-yita*delta_w[k];
				}
				now_doc=now_doc+doc_of_i; //更新当前文档索引
			}
		} //End 迭代次数
		
		//输出权重
		for(int i=1;i<=weidu;i++) //从1到136为权重,0和137无用
		{
			System.out.println(i+"wei:"+weight[i]);
		}
	}
	
	/**
	 * 输出权重到文件fileModel
	 * @param fileModel
	 */
	public static void WriteFileModel(String fileModel) {
		//输出权重到文件
		try {
			System.out.println("write start.总行数:"+sumLabel);
			FileWriter fileWriter = new FileWriter(fileModel);
			//写数据
			fileWriter.write("## ListNet");
			fileWriter.write("\r\n");
			fileWriter.write("## Epochs = "+ITER_NUM);
			fileWriter.write("\r\n");
			fileWriter.write("## No. of features = 46");
			fileWriter.write("\r\n");
			fileWriter.write("1 2 3 4 5 6 7 8 9 10 ...  39 40 41 42 43 44 45 46");
			fileWriter.write("\r\n");
			fileWriter.write("0");
			fileWriter.write("\r\n");
			for(int k=0; k<weidu; k++){
				fileWriter.write("0 "+k+" "+weight[k+1]);
				fileWriter.write("\r\n");
			}
			fileWriter.close();
			System.out.println("write fin.");
		} catch(Exception e) {
			System.out.println("写文件内容出错");
            e.printStackTrace();
		}
	}
	
	/**
	 * 预测排序
	 * 正规应对test.txt文件进行打分排序
	 * 但我们是在Hadoop实现该打分排序步骤 此函数仅测试train.txt打分
	 */
	public static void PredictRank(String fileScore) {
		//输出得分
		try {
			System.out.println("write start.总行数:"+sumLabel);
			String encoding = "GBK";
			FileWriter fileWriter = new FileWriter(fileScore);
			//写数据
			for(int k=1; k<sumLabel; k++){
				double score=0.0;
				for(int j=1;j<=weidu;j++){
					score=score+weight[j]*feature[k][j];
				}
				fileWriter.write("qid:"+qid[k]+" score:"+score+" label:"+label[k]);
				fileWriter.write("\r\n");
			}	
			fileWriter.close();
			System.out.println("write fin.");	
		} catch(Exception e) {
			System.out.println("写文件内容出错");
            e.printStackTrace();
		}
	}
	
	/**
	 * 主函数
	 */
	public static void main(String args[]) {
		String fileInput = "Fold1\\train.txt";       //训练
		String fileModel = "model_weight.txt";       //输出权重模型
		String fileScore = "score_listNet.txt";      //输出得分
		//第1步 读取文件并解析数据
		System.out.println("read...");
		ReadTxtFile(fileInput);
		System.out.println("read and write well.");
		//第2步 排序计算
		LearningToRank();
		//第3步 输出模型
		WriteFileModel(fileModel);
		//第4步 打分预测排序
		PredictRank(fileScore);
	  }
	
	/*
	 * End
	 */
	
}

四. 总结

    上面的代码我更希望你关注的是ListNet在训练模型过程中的代码,也就是通过train.txt获取得到46维的权重的模型.通过该模型你可以对test.txt进行打分(权重*特征值)排序,而上面的代码仅是对train.txt进行了简单的打分操作,那时因为我们的作业是基于Hadoop或Spark分布式处理基础上的.所以该部分由其他同学完成.
    同时你也可以通过开源的RankLib或罗磊同学的ListNet算法进行学习,地址如下:
    http://sourceforge.net/projects/minorthird/
    http://code.google.com/p/learning-to-rank-listnet/

    http://people.cs.umass.edu/~vdang/ranklib.html
    最后我们使用开源的MAP和NDCG@r简单对该算法进行了性能评估,同时附上Hadoop上的运行截图(MapReduce只找到了PRank的一张截图).

 
  
    希望文章对大家有所帮助,同时我是根据论文写出的Java代码,如果有错误或不足之处,还请海涵~同时欢迎提出问题,我对机器学习和算法的了解还是初学,但是会尽力答复.同时发现该部分的代码真的很少,所以才写了这样一些文章,后面还准备写写Pairwise和Map\NDCG评价.
    (By:Eastmount 2015-2-5 夜10点  http://blog.csdn.net/eastmount/article/)


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【学习排序】 Learning to Rank 中Listwise关于ListNet算法讲解及实现 的相关文章

  • C语言的每日一题(一)

    前言 今天开始 我将开启我的每日一题之路 作者 一颗带有痣的毛 期待你的关注 一 三位数反转 题目 输入一个三位数 分离出它的百位 十位和个位 反转后输入 样例输入 127 样例输出 721 首先我们来分析一下这一题的思路 这一题的主要思路
  • 九鼎Study210嵌入式开发板刷机笔记

    本刷机笔记是基于S5PV210的九鼎Study210嵌入式开发板 准备 电脑通过串口线连接到开发板的UART2 靠近电源 与裸机不同 usb线连接到开发板OTG端口 开发板接入电源 串口助手打开 开机 只要连接成功 无论开发板是否启动了ub
  • Android系统控件使用的踩坑日志

    1 ListView在乐视 三星手机上出现崩溃 修复listview在部分手机上崩溃 java lang IndexOutOfBoundsException Invalid index 0 size is 0 at java util Ar

随机推荐

  • 【ARM 常见汇编指令学习 3 -- ARM64 无符号位域提取指令 UBFX】

    文章目录 ARM64 无符号位域提取指令 上篇文章 ARM 常见汇编指令学习 2 存储指令 STP 与 LDP 下篇文章 ARM 常见汇编指令学习 4 ARM64 比较指令 cbnz 与 b ne 区别 ARM64 无符号位域提取指令 在代
  • 图片上传提交前,图片显示在页面上

    lt div class upload box gt lt b gt 上传图片 lt input type file name file id file accept image onchange imgChange this gt lt
  • MMSegmentation笔记07:使用MMDeploy部署

    1 安装MMDeploy 首先要安装MMDeploy库 直接在对应环境下执行命令 pip install mmdeploy 1 2 0 然后在对应项目下安装MMDeploy源码 在cmd中执行 git clone https github
  • 求建议:二本计算机专业,考研还是就业做开发?

    安老师好 我是一个大连某二本院校的在读生 专业是计算机 日语强化 上大学以来一直关注着您的公众号 读了很多您的文章 学到很多东西 也受到过您的鼓舞 随您一同成长进步 目前我已是大三下 站在第二个人生岔路口 正为各种浮在空中的想法烦恼不已 身
  • C++ 读取文件和写入文件

    C 读取文件 有两种方式可以实现读文件的功能 第一种 用fread实现 这种方法适合读取各种结构化的数据包 include
  • 良心推荐:12个免费学习网站,赶紧收藏

    01 Oeasy http oeasy org Oeasy 是一个完全免费的综合视频教程网站 非常良心实用 它提供的视频教程非常丰富并且质量很高 包括 PS 教程 手机摄影教程 Ai 做图教程 Excel 教程 Word 教程 PPT 教程
  • python 安装第三方库,超时报错--Read timed out.(安装numpy时会出现)

    近期在安装numpy时 总是会出现Read timed out这个错误 经查是由于python在安装三方库时设置的时间限制 一般我们使用的命令为 pip install XXXX XXXX为你即将要安装的三方库 此时可能会出现以下错误 Re
  • 解决OpenCV重装后Python下import cv2错误问题(Ubuntu系统)

    本文是博主解决问题过程中所产生的一些想法 分享出来 希望能帮到一些朋友 不足之处 还望各位大佬不吝赐教 前提 博主在Ubuntu服务器上安装了OpenCV4 0版本 Python环境下可以运行 如图1 某天在部署YOLO3的时候 程序跳出e
  • git 错误error: failed to push some refs to

    今天使用VSCODE 学习node js 想在git上push代码 于是在git上建立了一个私有的长裤 连接后push代码时提示如下错误 error failed to push some refs to git github com Up
  • 树型权限管理插件:jQuery Tree Multiselect详细使用指南

    树型权限管理插件 jQuery Tree Multiselect详细使用指南 1 认识jQuery Tree Multiselect 这个插件允许用户以树型的形式来呈现列表复选框的选择 多用于权限管理中用于分配不同的权限 使用文档 请参考
  • unity2D横版游戏教程-1 让人物动起来

    在看完了麦扣老师的小狐狸的视频后 为了巩固练习 我另外找一个素材包来练手 顺便写成一个教程让自己更加熟练 同时当自己遗忘时可以回头查阅 另外 这个教程我写的非常详细 萌新也可以学习 有什么不对的地方或者有疑问的地方 欢迎指正和咨询 首先导入
  • python可视化——matplotlib画图颜色控制

    这里cd整理了python中matplotlib库中color可用的颜色 之前有人整理的是图片版 实际用的时候不怎么方便 因策这里整理了表格 颜色可以选择 方便使用 关于matplotlib中颜色的使用 有很多方式 1 用RGB或者RGBA
  • 给 MSYS2 添加中科大的源

    最近一段时间不知怎么的 使用默认的 MSYS2 源升级软件或是安装新软件的特别的慢 所以就翻了翻国内的几个开源软件的镜像库 发现中科大的库里就有 MSYS2 所以就研究了一下 给 MSYS2 添加了中科大的源 简单的说 msys64 etc
  • 领域驱动设计:DDD重构中台业务模型

    文章目录 如何避免重复造轮子 如何构建中台业务模型 如何避免重复造轮子 要避免重复建设 就要理解中台的理念和思想 中台是企业级能力复用平台 复用 用白话说就是重复使用 就是要避免重复造轮子的事情 中台的设计思想与 高内聚 低耦合 的设计原则
  • Oracle入门笔记(四)——Oracle表约束和表间关系

    Oracle表约束和表间关系 1 Oracle数据表要求 2 Oracle数据表约束 3表间关系 3 1一对一关系 3 1一对多关系 3 1多对多关系 1 Oracle数据表要求 1 必须以字母开头 2 长度不能超过30字符 3 避免使用关
  • 软件测试项目管理系统(STM)

    1 产品介绍 凯云软件测试项目管理系统 Software Testing Management System 简称 STM 是为企业软件测试部门以及第三方软件测试机构打造的统一工作平台 该系统提供规范的测试流程 支持被测件接收 测试需求分析
  • 若依RuoYi-Vue代码学习一---若依如何基于OncePerRequestFilter【Spring Security的过滤器】验证的token与用户信息

    文章目录 一 通过token获取用户信息 二 验证token过期 一 通过token获取用户信息 代码位置 com ruoyi framework security filter JwtAuthenticationTokenFilter 直
  • https://isux.tencent.com/svg-animate.html(svg动画)

    初学SVG的时候 感觉那一坨一坨的代码难读难懂 现在回过头仔细想想 是因为那时候看文档缺少一些具体的实例 导致学习起来很枯燥 如今SVG已经在前端各个领域都有所作为 无论是项目里的应用还是demo都所处可见 这里我就用一个实例的写法带新同学
  • Android 上传头像功能第三方框架Boxing与TakePhoto使用总结

    刚做了个人中心的上传头像功能 就来总结一下做的过程 一开始就直接去github上找一个第三方框架接入 省点事 先是听了朋友的推荐去看了一下bilibili开源的一款Boxing的框架 但是使用起来发现不太符合我的需求 最后还是使用TakeP
  • 【学习排序】 Learning to Rank 中Listwise关于ListNet算法讲解及实现

    前一篇文章 Learning to Rank中Pointwise关于PRank算法源码实现 讲述了基于点的学习排序PRank算法的实现 该篇文章主要讲述Listwise Approach和基于神经网络的ListNet算法及Java实现 包括