一、赛题背景
在NLP任务中,经常会出现Multi-Task Learning(多任务学习)这一问题。多任务学习是一种联合学习,多个任务并行学习,结果相互影响。在实际问题中,就是将多个学习任务融合到一个模型中完成。不同的任务会关注到不同的文本分析特征,将多任务联合起来有利于进行模型泛化,缓解深度学习模型容易过拟合的现象。
多任务学习的出发点是多种多样的:
(1)从生物学来看,我们将多任务学习视为对人类学习的一种模拟。为了学习一个新的任务,我们通常会使用学习相关任务中所获得的知识。例如,婴儿先学会识别脸,然后将这种知识用来识别其他物体。
(2)从教学法的角度来看,我们首先学习的任务是那些能够帮助我们掌握更复杂技术的技能。这一点对于学习武术和编程来讲都是非常正确的方法。具一个脱离大众认知的例子,电影Karate Kid中Miyagi先生教会学空手道的小孩磨光地板以及为汽车打蜡这些表明上没关系的任务。然而,结果表明正是这些无关紧要的任务使得他具备了学习空手道的相关的技能。
(3)从机器学习的角度来看,我们将多任务学习视为一种归约迁移(inductive transfer)。归约迁移(inductive transfer)通过引入归约偏置(inductive bias)来改进模型,使得模型更倾向于某些假设。举例来说,常见的一种归约偏置(Inductive bias)是L1正则化,它使得模型更偏向于那些稀疏的解。在多任务学习场景中,归约偏置(Inductive bias)是由辅助任务来提供的,这会导致模型更倾向于那些可以同时解释多个任务的解。接下来我们会看到这样做会使得模型的泛化性能更好。
深度学习中有两种多任务学习模式:参数硬共享机制与参数软共享机制。本文基础参数硬共享机制构建算法模型,并从软件工程的角度构建代码,完成算法的设计方案与落地实施。本次算法是为了完成天池比赛提出的,比赛详细内容请点击这里。我们先对比赛内容做一个简单的介绍:
这次的NLP比赛有三个任务,分别是预测文本是否相关、预测新闻文本分类与情感分析。本次比赛只能使用单模型(单模型的定义:一个任务只能有一个预测函数,所有任务只能使用同一个bert,在计算图中只能有一个bert)完成任务,不能集成多个模型进行预测。即不能使用不同的模型提取文本特征。在此基础上,我们实现基于参数硬共享机制的算法模型,并结合软件工程的思想构建代码,
二、算法设计方案与结构特点
算法最终要落地到具体的代码实现,而在实现过程中就会遇到各种各样的问题。我们的算法模型可以分为两个层,数据层与算法层。数据层向算法层提供接口调取数据,而算法层经过训练与优化产生最终的预测结果。所以我们的设计方案是没有界面层的三层架构,或者说这样可以称作两层架构。算法层是业务逻辑层而数据层是数据访问层,将数据库替换为文件读取,也不妨是一种返璞归真。在两个层的内部,我们使用建造者模式来完成设计。在数据层,由于有三个分类任务,我们需要从三个文件中读取数据,不同文件的数据格式是不一样的,所以我们需要对三个文件做单独处理,这就会产生三个子层。而算法层需要我们提供格式一直的数据,所以我们在数据层需要对这些数据进行整理,将三个部分统一为一个整体,再通过接口传递给算法层。在算法层,又可以分为三个子层,分别是bert、pool与predict,分别完成特征提取、特征池化与分类预测的任务。在算法层我们将分为三个子层去实现,每个子层各自独立实现,再将三个子层结合到一起形成一个整体模型,方便torch框架对模型进行反向传播与优化。在算法层中,每个子层都有不同的实现方案,比如bert可以利用不同的模型实现,不同模型的参数设定与输出不一定完全相同;pool有max、mean、attention等不同的实现方法,这需要不停地调整优化才能得出最终的模型。这要求算法层各个子层相互独立,三个子层组成的算法链完成训练与预测。而在调用中,只会看到一个整理的模型。除了最主要的数据层与算法层,系统中还有日志记录、配置文件读写、参数解析、模型检查点记录等辅助功能,这些功能采取单例的模式,在整个系统中各自只拥有一个实例。
三、算法接口API及视图
我们分为几个类来介绍API接口:
(1)Logger类,继承logging,提供info、debug、warning等接口记录日志。
(2)Config类,提供读写配置文件、配置参数的检查接口。
(3)CheckpointManager类:提供记录模型检查点接口。
(4)JointDataset类:提供数据读取、batch抽取接口。
(5)Ocnli、Tnews、Ocemotion类:提供三个数据集的读取、解析与填充接口。
(6)NLPModel类:提供模型构建、模型训练、模型预测接口。
算法依赖视图:
算法执行流程图:
四、算法核心数据结构设计与源代码的目录文件结构
为了能够最大化利用torch带来的便利,我们在数据层继承了torch中的Dataset类,并实现了__len__()与__getitem__()方法。这样我们就能够继续利用torch的DataLoader类,能够自动产生随机抽取的batch进行算法训练。在batch中,每一条数据由一个dict组成,dict的具体内容如下所示:
{
'id':数据ID,
'input_ids':AutoTokenizer后的向量,
'token_type_ids':标明input_ids中padding的部分,与input_ids长度一致,0表示为paddnig,1表示为原始数据,
'length':长度,
'target':标签,
'task_type_id':任务类型
}
源代码的文件结构目录如下:
五、算法运行环境和技术选型说明
Python:3.8.5
torch: 1.7.1+cu110
cuda: 11.0
tensorboard:2.4.0
tensorflow: 2.3.1
transformers: 4.0.1
硬件环境:建议使用NVIDIA2070以上显卡,2070上运行大概需要一个小时;
操作系统:win10/linux皆可,只要版本较新,可以支持cuda等。
运行环境算是标配没有可以选择的余地,具体的bert模型与pool方法选择还在尝试寻找最优解。
六、举例说明算法运行流程
我们有三个数据集:OCEMOTION_train1128.csv、OCNLI_train1128.csv与TNEWS_train1128.csv,分别有35693、55386与63359条数据。我们简单地从三个数据集中各抽取4条数据作为我们的训练数据来举例说明我们的运行流程。因为OCNLI_train1128.csv数据是对比两句话的相关程度,比另外两个数据集多一句话,所以我们把12条数据AutoTokenizer后,全部padding到统一的长度,即OCNLI_train1128.csv中最长的长度,padding的数字为0,并且使用一个等长的向量标注哪些是原始数据哪些是padding的数据。在完成数据处理之后,将数据组成一个batch交给算法层的模型去处理。首先,bert层会将每一条输入的数据转化为一个768维的特征向量交给pool层。pool层采取self-attention机制,先产生三个12*768维的query、key与value矩阵。query是将batch标签通过一个nn产生,key与value是batch特征通过一个nn产生。然后将12*768维的矩阵升维成12*8*96的矩阵再转化为8*12*96的矩阵作为8个MultiHeadAttentionHeads。接着,基于self-attention机制,通过计算query与key的乘积计算value作为该条数据的特征,交给predict层进行预测。三个任务有各自的predict层,最后的loss通过计算平均loss再进行反向传播。在训练中,学习率通过warm-up的方法进行调整,参数会进行AdamW优化。