\documentclass[a4paper]{ctexart} \usepackage[margin=1in]{geometry} \usepackage{booktabs} \usepackage{hyperref} \usepackage{graphicx} \usepackage[numbers]{gbt7714} \RequirePackage[outputdir=./latex-output]{minted} \setlength{\belowcaptionskip}{1em} \title{《深度学习》实验报告} \author{姓名:岳锦鹏\qquad 学号:10213903403\qquad 专业:统计学-计算机\qquad 学院:统计学院} \date{2024年6月8日} \ctexset { section = { name = {,、}, format += \raggedright, number = \chinese{section}, }, subsection = { % name = {(,)}, number = \arabic{subsection} } } \begin{document} \maketitle \section{实验环境} \noindent requirements.txt: \begin{minted}[frame=leftline, framesep=1em, framerule=1pt]{python} numpy paddlepaddle-gpu scikit-learn tqdm \end{minted} 这些代码库的作用是什么已经显而易见了,其中 \mintinline{Python}{scikit-learn (sklearn)} 只是用来分训练验证集的。 \section{实验过程} \subsection{实验思路} \subsubsection{确定首次召回个数} 先尝试了一下完全不使用baseline的方法,直接从全部的几千个文档中召回3个文档,发现效果完全不如baseline,于是想到了采用二次召回,第一次采用baseline的方法,从几千个文档中召回一部分,第二次再从召回的这些文档中选出3个。关于第一次召回多少,我首先做了按照baseline的方式直接计算余弦相似度的top k召回率的实验,结果如 表 \ref{first retrieve num} 所示,可以看到当首次召回数量达到500个时已经有很高的召回率了,所以后续的实验都在首次召回500个文章下进行。 \begin{table}[h] \centering \caption{直接计算余弦相似度的召回率} \label{first retrieve num} \begin{tabular}{cc} \toprule Recall@100 & 0.75 \\ Recall@500 & 0.93 \\ Recall@5000 & 0.99 \\ \bottomrule \end{tabular} \end{table} \subsubsection{二次召回方案选择} 从500个里召回3个,仍然是一个复杂的任务,尝试过几种方案: \begin{enumerate} \item 将其当做分类问题,输入一个查询,500个文章,输出这500个文章的概率。缺点是效率可能较低,而且实验后发现效果也不好; \item 双塔编码(cross encoder)和point wise 对比学习\cite{jianshu},给定一个查询和一个文章,模型给出一个得分,对于查询和对应的文章(正例),得分应该更高;对于查询和不对应的文章(负例),得分应该更低,这里的负例一般是从文章库中随机选取。实验后发现不管正负例的比例是多少,效果都不好; \item 单塔编码,query通过查询编码器,fact通过文章编码器,之后计算编码后的向量的余弦相似度,如果不使用对比学习会导致模型把所有的文章都编码得非常相似,所以这里还是需要使用对比学习。那么如何选取负例?实验过还是从文章库中随机选取负例,效果还是不好,于是查找资料,发现了这样一篇文章:\cite{aistudio},里面提到了In-batch Negatives策略,即将一个批次内其他样本都作为负例,尝试后发现效果有很大提升,从baseline的0.2121提升到了0.4059。 \end{enumerate} 最终选择了单塔编码的方案,并且使用了In-batch Negatives策略。 \subsection{数据预处理部分} 先使用 \mintinline{Python}{sklearn} 中的 \mintinline{Python}{train_test_split} 按8:2的比例分出训练集和验证集。对于训练集和验证集,先把首次召回500个的工作全部完成,即对于每一个查询,先召回500个文章,存放在内存中,训练时在这500 $\times $ num of queries 个样本里训练。 \subsection{模型构建} 这里需要做的就是在单塔编码中,如何编码查询,如何编码文章。其实非常简单,就是多层全连接层,使用ReLU作为激活函数,并且使用了残差连接,如图 \ref{model structure} 所示。 \begin{figure}[h] \centering \includegraphics[width=1\linewidth]{模型结构图.png} \caption{模型结构图}\label{model structure} (图中的N表示batch size,EMB表示嵌入维度。) \end{figure} 由于query和fact的每一行是对应的,所以将query和fact通过编码器后的向量,在对角线的位置表示查询和文档对应时的相似度(正例),而非对角线的位置表示非对应时的相似度(负例),所以此矩阵应该和单位矩阵相近,所以可以用交叉熵损失。在实际代码中使用了小于1的margin来代替1形成对角阵。 这里只使用简单的线性层加残差连接,是因为试了很多种结构,比如更深的带残差块的线性层,比如Transformer(Decoder only),效果都没有简单的线性层效果好。 \section{实验结果} (以下召回率都是指Recall@3) % 训练集和验证集比例为8:2,batch size设为1024,训练370轮,优化器为Adam,在验证集上的召回率为0.48165760869565216,提交后(在测试集上)的召回率为0.452148。 % 不区分训练集和验证集,batch size设为4096,训练1300轮,优化器为Adam,提交后(在测试集上)的召回率为0.529622。 % 不区分训练集和验证集,batch size设为2048,训练2500步(iter steps)(不是轮(epochs)了),优化器为Adam,提交后(在测试集上)的召回率为0.523275。 % 不区分训练集和验证集,batch size设为2048,训练2600步,优化器为AdamW,提交后(在测试集上)的召回率为0.523926。 % 不区分训练集和验证集,batch size设为2048,训练5100步,优化器为AdamW,将模型参数与输入输出的数据类型从float32改为float64,提交后(在测试集上)的召回率为0.584635。 结果如表 \ref{hyper-parameters} 所示。 \begin{table}[h] \centering \caption{尝试不同的超参数}\label{hyper-parameters} \begin{tabular}{cccccccc} \toprule 序号 & 是否有验证集 & batch size & epoch / step & 优化器 & 数据类型 & 验证集召回率 & 测试集召回率 \\ \midrule 0 & \multicolumn{4}{c}{baseline 无需训练} & float32 & / & 0.212077 \\ 1 & 是 & 1024 & 370 epochs & Adam & float32 & 0.481658 & 0.452148 \\ 2 & 否 & 4096 & 1300 epochs & Adam & float32 & / & 0.529622 \\ 3 & 否 & 2048 & 2500 steps & Adam & float32 & / & 0.523275 \\ 4 & 否 & 2048 & 2600 steps & AdamW & float32 & / & 0.523926 \\ 5 & 否 & 2048 & 5100 steps & AdamW & float64 & / & 0.584635 \\ 6 & 否 & 8192 & 9500 steps & AdamW & float64 & / & \textbf{0.606283} \\ \bottomrule \end{tabular} \end{table} \bibliography{ref} \end{document}