1 项目概述
1.1 原始项目简介
本项目基于用户【深渊上的坑】的项目——使用PaddleNLP识别垃圾邮件(四):用RoBERTa做中文邮件标题分类改编。
原始项目的主要内容为,针对当前企业面临的垃圾邮件问题,尝试使用深度学习的方法,根据邮件的中文标题,判断该邮件是否为垃圾邮件。
1.2 为什么使用Pipeline
特性 | 传统方法 | Pipeline |
---|---|---|
自动化 | 运行一个完整流程需要用户多次操作,来执行多个步骤。 | Pipeline将多个步骤有机的结合成一个整体,实现一次操作便可自动化的运行整个流程的效果。 |
标准化 | 开发前需要商定多个步骤间传递数据的方式。 | Pipeline具有标准化接口,有利于多人合作开发,提升开发效率。 |
高可复用性 | 一个步骤难以在多个流程中使用。 | 所有步骤都被封装成Op,能更好的支持复用,减少开发工作量。 |
可视化 | 无可视化功能,从而难以整体把控全流程。 | Pipeline可以将流程进行图形化展示,从而更易掌控全流程。 |
快速复现 | 实验记录易丢失,导致实验难以复现。 | Pipeline会保存运行记录,可以根据记录中的信息(如本项目中使用的模型类型、模型的超参数等),对实验进行快速复现。 |
产出管理 | 实验的产出难以管理。 | Pipeline会对实验的产出进行系统化的管理。 |
Pipeline的详细介绍可参考Pipeline简介。
Op是Pipeline运行时的基本调度单位,Pipeline中的每一个节点都需要是一个Op。Op的具体介绍请查看Op详细介绍
2 构建并运行Pipeline
2.1 Pipeline构建思路
原始项目做的事情主要有“模型训练”、“模型评估”、“模型预测”。而针对一个原始项目,我们想基于它构建一个Pipeline,最重要的是以下2点。
-
构建Op:构建每个步骤对应的Op。可以设计一个函数来决定Op的运行逻辑,也可以让Op直接调用一个能完成该步骤的脚本文件。由于原始项目中已实现了所有步骤,所以本项目可以直接调用对应的脚本文件。
-
编排Op:设计Op之间的数据传递关系。原始项目中,“模型”以读写文件的形式在3个步骤间传递,那么对应的Op之间也应该合理的传递“模型”。以读写本地文件的方式传递,可拓展性较差,且不便于保存和管理”模型”。为了解决这个问题,我们可以利用Pipeline的特性,以输入Artifact和输出Artifact的形式传递”模型“。
关于Artifact:
Artifact是由Op生成的文件,在实际的Pipeline运行过程中,Op的输入和输出是以“Artifact”的形式保存和传递的。
Op在运行前,会从Artifact仓库把对应的文件下载到指定的输入存储路径下;Op在运行完后,会把指定的输出存储路径下的文件上传到Artifact仓库保存。
这样的机制能更科学的管理实验时运行的产出。更具体的描述可参考这里。
另外,我们也可以根据需要对原始项目的脚本进行优化。比如本项目优化了“模型训练”步骤,使它可以选择待训练模型的类别。
至于具体如何生成Op,以及如何指定Op之间的关系,本文2.3将会介绍。现在我们先看下根据上述的设计思路,最终本项目设计出来的Pipeline的流程是怎样的。
2.2 Pipeline概述
2.2.1 Pipeline流程
根据上述的Pipeline构建思路,本项目的主要流程如上图所示,整个Pipeline由3类Op组成,分别为“模型训练”Op、“模型评估”Op、“模型预测”Op。
-
首先是“模型训练”Op,该Op的功能是根据输入的模型名称参数,在指定的训练数据集上,训练对应的网络模型,目前支持的模型有“rbt3”、“bert_base_chinese”、“chinese_electra_small”、“ernie_tiny”。模型训练完后,会保存训练完的模型,并作为输出Artifact传给下游Op。
-
其次是“模型评估”Op,该Op通过将上游的“模型训练”Op的输出Artifact,即模型,指定为输入Artifact来获取这些训练好的模型,而它的功能是对从上游Op得到的模型进行评估。评估所使用的数据集由用户指定。评估的结果,即Loss和Accuracy,会写入到日志中。同时,该Op会输出一个评估得到一个最好的模型(暂时规定为Accuracy最高的模型),并将其保存下来,作为输出Artifact传给下游Op。
-
最后是“模型预测”Op,该Op会使用从”模型评估“Op得到的模型,在用户指定的预测数据集上进行预测,最终预测的结果写入到日志中。
2.2.2 文件目录
为了更好的理解后面讲到的各个函数的参数的含义,需要先简单浏览一下本案例的文件的目录结构。
点击这里可下载本项目的代码及其他文件。
mail_classification/
├── dataset
│ ├── eval_list.txt
│ ├── test_list.txt
│ └── train_list.txt
├── pipeline.ppl
├── requirements.txt
└── script
├── eval.py
├── predict.py
└── train.py
如上所示,数据集存放在dataset文件夹下,python脚本文件存放在script文件夹下,requirements.txt包含了运行本项目的Pipeline需要的依赖库,本文2.4介绍了如何安装这些依赖库,pipeline.ppl构建了一个Pipeline。
2.3 构建Pipeline
想要构建Pipeline,通常需要先完成2个步骤。首先是构建Op,然后是编排Op。
-
构建Op:本项目首先将模型的“训练”、“评估”、“预测”这三个步骤,分别用一个函数进行封装。这些函数会构建并返回一个Op,而Op能完成对应的步骤。本文称这些函数为Op构建函数。
-
编排Op:有了Op之后,本项目定义了一个函数来编排这些Op。本文称该函数为Pipeline函数。
接下来本文将依次讲解Op构建函数和Pipeline函数,这两个函数通常我们会在ppl文件中定义,如本项目的pipeline.ppl文件。
2.3.1 构建Op
如上文所说,我们需要先构建Pipeline需要的Op。
本项目中,有3类Op,“模型训练”Op、“模型评估”Op、“模型预测”Op,它们分别由函数train()
、evaluate()
、predict()
来构建并返回。
train()函数
train()
函数的功能是根据参数构建并返回一个模型训练Op,这个训练Op会训练用户指定的模型,并将这个模型作为Op的输出,以文件的形式保存下来。train()
函数的代码如下所示:
def train(ds_path, model_name, output_save_dir="output_model_dir",
device="gpu", epoch=1, batch_size=10):
return ScriptOp(name="Model Train Op",
command=["python", "script/train.py"],
arguments=[
"--train_ds", ds_path,
"--model_name", model_name,
"--save_dir", output_save_dir,
"--device", device,
"--epoch", epoch,
"--batch_size", batch_size],
outputs={"model": output_save_dir})
上述代码调用了ScriptOp的构造函数来构建Op,通过command
参数指定该Op完成“模型训练”这个步骤的方式为“运行train.py脚本”,而train.py的主要功能是进行模型训练。
通过arguments
参数可以指定传递给command
命令的参数,这些参数最终传递给train.py,在运行脚本的时候被使用。具体而言,这里通过--train_ds
指定了训练数据集的存放路径,--model_name
指定了训练的模型的类型,--save_dir
指定了模型训练完后保存的路径,最后三个参数是训练相关的设定。
而outputs
指定了当train.py脚本执行完后,Op会将什么路径下的文件以Artifact的形式上传到Artifact仓库。
ScriptOp参数:
ScriptOp是Op的一个类别,这里简单介绍一下ScriptOp的构造函数中比较重要的参数
command
:指定Op执行什么样的命令
arguments
:传递给command命令的参数
outputs
:需赋值一个dict,其中key为输出的名称,value为一个路径,在Op运行完后,路径下的文件会作为输出,被上传到Artifact仓库关于ScripOp构造函数的参数详解或者其他参数,可以参考Pipeline编排详解
另外,关于上文提到的train.py,本项目在原始项目的基础上进行了优化:由原来的固定训练某一种模型,改为可以选择不同的模型类型。这样修改是为了让Op有更好的可复用性。实现方式如下代码所示:
if (args.model_name == "rbt3"):
model = ppnlp.transformers.RobertaForSequenceClassification.from_pretrained('rbt3', num_class=2)
tokenizer = ppnlp.transformers.RobertaTokenizer.from_pretrained('rbt3')
elif (······):
······
上述代码中的args.model_name
就是通过arguments
传递给command
命令,再进而传递给train.py。
除了上述的train()
函数的主要逻辑,它的参数列表如下所示:
参数名称 | 说明 |
---|---|
ds_path | 训练数据集的存储路径。 |
model_name | 模型的名称,用来指定所使用的模型。目前仅支持“rbt3”、“bert_base_chinese”、“chinese_electra_small”、“ernie_tiny”。 |
output_save_dir | Op输出的存放目录。 |
device | 训练时使用的硬件设备。 |
epoch | 训练时设定的epoch。 |
batch_size | 训练时设定的batch size。 |
evaluate()函数
evaluate()
函数的功能是构建并返回一个“模型评估”Op,评估Op的主要功能是,对从上游的“模型训练”Op获取的模型进行评估,将评估结果,即Loss和Accuracy,写入日志中,同时评估Op还会将最好的模型(Accuracy最高)保存下来,并作为Op的输出。
def evaluate(ds_path, model_inputs, output_save_dir="output_model_dir", device="gpu"):
return ScriptOp(name="Model Eval Op",
command=["python", "script/eval.py"],
arguments=["--eval_ds", ds_path,
"--save_dir", output_save_dir,
"--device", device,
"--model_names"] + list(model_inputs.keys()),
inputs=model_inputs,
outputs={"model": output_save_dir})
evaluate()
函数构建Op的逻辑和train()
函数类似,都是通过command
让Op执行一个脚本,也就是eval.py脚本,通过arguments
给命令传递参数。
inputs
参数的作用是指定该Op运行时,有哪些数据以输入Artifact的形式传递给Op。在给该参数赋值时,除了指定输入的名称和内容,还可以选择是否指定输入的存放路径。
关于inputs
和outputs
参数的赋值,更详细的介绍可参考ScriptOp详解
eval.py脚本的主要内容与原始项目中train.py的模型评估部分相似,但有一定优化,优化的点在于,原始项目中只能评估一个模型,而本项目eval.py可以同时评估多个模型。实现的方式是在eval.py脚本中新添加的参数model_names
,用这个参数指定需要评估的模型有哪些。改动后的代码如下所示:
for model_name in args.model_names:
if (model_name == "rbt3"):
model = ppnlp.transformers.RobertaForSequenceClassification.from_pretrained('rbt3', num_class=2)
tokenizer = ppnlp.transformers.RobertaTokenizer.from_pretrained('rbt3')
elif (······):
······
# 通过环境变量获取Artifact的存储路径
model_dir = os.getenv(model_name)
# model_dir是一个表示路径的字符串,如"./rbt3"
state_dict = paddle.load(model_dir + "/model_state.pdparams")
model.set_dict(state_dict)
上述代码中的model_names
是通过ScriptOp构造函数中的arguments
参数传递进来的,它用来指定加载那种类型的模型。加载完的网络结构后,以读取模型文件的方式,加载训练好的模型的参数,用来评估。
model_name
的值为环境变量名,该环境变量的值为待评估模型对应的Artifact的存储路径。具体的方法和原理可参考如下注释。
关于inputs和outputs生成的环境变量:
在本项目中,所有Op的输入,也就是
inputs
,在赋值时都没有指定存放路径,这种情况下,Pipeline会自动生成一个路径用来存放Op的输入。另外,对于所有Op的输入Artifact和输出Artifact,Pipeline会生成环境变量,用来保存这些Artifact的存放路径,用户也就可以通过这些环境变量来获取的Artifact存储路径,进而使用Artifact。环境变量名为Artifact的名称,即赋值时对应的key值。更具体的讲解可查看通过环境变量获取Artifact存储路径
最后,evaluate()
函数的参数如下所示:
参数名称 | 说明 |
---|---|
ds_path | 评估数据集的存储路径。 |
model_inputs | 赋值给inputs 的dict。key为模型的名称,value为要输入的内容。 |
save_dir | 输出模型的存储路径。 |
device | 评估时使用的硬件设备。 |
predict()函数
predict()
函数的功能是构建并返回一个“模型评估”Op,该Op的功能是使用训练好的模型进行预测,并将预测的结果写入到日志中。
def predict(ds_path, model_input, device="gpu"):
return ScriptOp(name="Model Predict Op",
command=["python", "script/predict.py"],
arguments=[
"--predict_ds_path", ds_path,
"--device", device],
inputs=model_input)
predict()
函数构建Op的方式和上述的train()
和evaluate
类似,其中的command
、arguments
、inputs
参数的功能与前面的用法一致,这里不过多赘述。
predict()
函数的参数如下所示:
参数名称 | 说明 |
---|---|
ds_path | 测试数据集的存储路径。 |
model_input | 待预测模型,必须指定模型的存储路径,读取待预测模型时需要用到。 |
device | 预测时使用的硬件设备。 |
2.3.2 编排Op
有了Op之后,我们就可以通过一个Pipeline函数来编排这些Op。
本项目定义的了如下的pipeline_func
函数,该函数可在pipeline.ppl中查看。
def pipeline_func(train_ds_path="dataset/train_list.txt", eval_ds_path="dataset/eval_list.txt",
predict_ds_path="dataset/test_list.txt", device="gpu", epoch=1, batch_size=10):
train_op1 = train(ds_path=train_ds_path,
model_name="rbt3",
output_save_dir="model_train_output/rbt3",
device=device,
epoch=epoch,
batch_size=batch_size)
train_op2 = train(ds_path=train_ds_path,
model_name="chinese_electra_small",
output_save_dir="model_train_output/chinese_electra_small",
device=device,
epoch=epoch,
batch_size=batch_size)
eval_op = evaluate(ds_path=eval_ds_path,
model_inputs={"rbt3": train_op1.outputs["model"],
"chinese_electra_small": train_op2.outputs["model"]},
output_save_dir="model_eval_output",
device=device)
predict_op = predict(ds_path=predict_ds_path,
model_input={"model": eval_op.outputs["model"]},
device=device)
该函数按照2.2.1的流程,先后调用了3种Op构建函数,分别为train()
、evaluate()
、predict()
。
首先我们调用了两次train()
函数,先后构建两个“训练”Op,并分别赋值给train_op1
和train_op2
,这两个训练Op会输出两个训练后的模型。
train_op1
和train_op2
的下游Op是evaluate()
函数构建的eval_op
,它对所有从“训练”Op获得的模型进行评估,并将最好的模型作为输出传输到下游的predict_op
。
train_op1.outputs["model"]
为train_op1
中名称为“model”的输出Artifact,这个名称是在train()
函数中通过outputs
参数指定的。具体原理可参考输出Artifact
predict_op
由predict()
函数构建,它会使用从上游获得的模型在指定的测试数据集上进行预测。
pipeline_func
的参数依次是训练、评估、预测三个数据集的存储路径,Pipeline运行时所用的硬件设备,训练时对epoch和batch size的设定。
2.4 运行Pipeline
编排好一个Pipeline后,我们可以尝试运行Pipeline。
在定义了pipeline_func
的pipeline.ppl文件中输入如下代码:
Run().create(pipeline_func)
这行代码的主要作用是根据用户定义的Pipeline函数,运行对应的Pipeline,pipeline_func
就是本项目定义的Pipeline函数。
在运行前,需要安装requirements.txt中的依赖库,在命令行运行如下命令即可。其他依赖库请根据系统提示进行安装。
pip install -r requirements.txt
2.4.1 在CodeLab运行
我们可以在CodeLab上,选择并跳转到.ppl文件的代码编辑页面,就可以通过界面交互的方式简单快捷的运行Pipeline。
本地调试
可以通过点击“调试”按钮的方式运行Pipeline,如下图所示。
在Pipeline运行时,终端上会先后显示训练的进度、评估的结果、预测的结果,以及其他信息,具体如下所示。
训练Op运行时,终端输出的部分信息:
本地运行
调试过后,如果没有Bug,就可以正常运行了,本地运行和本地调试从结果来看稍有不同,这里暂不展开,有兴趣可以查看本地运行任务。
操作方式如下图所示:
点击运行后,会弹出一个界面,在这个界面可以选择“单次运行”还是“周期运行”。同时还可以指定想要传给Pipeline的参数。具体界面如下图所示:
本地运行的优势之一在于,可以将运行记录保存下来,每一次运行的参数、结果等信息,都会被记录,这有利于用户分析问题,也便于日后的复现。查看运行历史记录的界面在左侧的菜单栏,点击图标即可查看所有的记录,如下图所示:
再点击某一条记录,就可以查看详情。详情页面如下所示:
在CodeLab上,还可以在运行前通过可视化的方式查看Op之间的关系和每个Op的相关信息,具体方式可查看可视化
2.4.2 以命令行的方式运行
除了在CodeLab上以界面交互的方式运行,我们也能在终端以命令行的方式运行Pipeline。
输入如下命令可以以本地调试的方式运行:
ppl run create pipeline.ppl --debug --show-log
其中 pipeline.ppl
就是我们编写Pipeline的文件,--debug
表示以调试的方式运行,--show-log
会将脚本的输出显示在终端上。
同样,也可以不以调试的模式运行,只需去掉--debug
即可,命令如下:
ppl run create pipeline.ppl --show-log
更多命令行的操作,如查看运行记录等,可查看命令行运行
3 自定义数据集和训练模型
3.1 自定义数据集
用户可自行传入参数来指定数据集的存储路径。用户可以自定义数据集的内容。
训练、评估数据集中的数据需要以如下数据表示:
'邮件标题' + '\t' + '0或1' + '\n'
如:
这是一个垃圾邮件标题 0
这是一个正常邮件的标题 1
其中\t
前的部分为数据,后面的0或1为为标注,分别表示垃圾邮件和正常邮件。
测试数据集中的数据需去掉标注部分,格式为:
'邮件标题' + '\n'
如:
这是一个垃圾邮件标题
这是一个正常邮件的标题
3.2 添加训练模型
用户可以自行添加模型训练Op,同时也需要将新添加的模型,添加到模型评估Op的model_inputs
里,从而让新模型也加入模型评估。
如:
# 2个模型
train_op1 = train(ds_path=train_ds_path,
model_name="rbt3",
output_save_dir="model_train_output/rbt3",
device=device,
epoch=epoch,
batch_size=batch_size)
train_op2 = train(ds_path=train_ds_path,
model_name="chinese_electra_small",
output_save_dir="model_train_output/chinese_electra_small",
device=device,
epoch=epoch,
batch_size=batch_size)
eval_op = evaluate(ds_path=eval_ds_path,
model_inputs={"rbt3": train_op1.outputs["model"],
"chinese_electra_small": train_op2.outputs["model"]},
output_save_dir="model_eval_output",
device=device)
# 3个模型
train_op1 = train(ds_path=train_ds_path,
model_name="rbt3",
output_save_dir="model_train_output/rbt3",
device=device,
epoch=epoch,
batch_size=batch_size)
train_op2 = train(ds_path=train_ds_path,
model_name="chinese_electra_small",
output_save_dir="model_train_output/chinese_electra_small",
device=device,
epoch=epoch,
batch_size=batch_size)
train_op3 = train(ds_path=train_ds_path,
model_name="bert_base_chinese",
output_save_dir="model_train_output/bert_base_chinese",
device=device,
epoch=epoch,
batch_size=batch_size)
eval_op = evaluate(ds_path=eval_ds_path,
model_inputs={"rbt3": train_op1.outputs["model"],
"chinese_electra_small": train_op2.outputs["model"],
"bert_base_chinese": train_op3.outputs["model"]},
output_save_dir="model_eval_output",
device=device)
在上面的代码中,第二段代码比第一段多训练了一个”bert_base_chinese“模型,该模型也会参与模型评估步骤。
注:本案例暂不支持模型相同但超参不同的情况。