【Pipeline】基于Pipeline实现垃圾邮件识别

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点。

  1. 构建Op:构建每个步骤对应的Op。可以设计一个函数来决定Op的运行逻辑,也可以让Op直接调用一个能完成该步骤的脚本文件。由于原始项目中已实现了所有步骤,所以本项目可以直接调用对应的脚本文件。

  2. 编排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。

  1. 首先是“模型训练”Op,该Op的功能是根据输入的模型名称参数,在指定的训练数据集上,训练对应的网络模型,目前支持的模型有“rbt3”、“bert_base_chinese”、“chinese_electra_small”、“ernie_tiny”。模型训练完后,会保存训练完的模型,并作为输出Artifact传给下游Op。

  2. 其次是“模型评估”Op,该Op通过将上游的“模型训练”Op的输出Artifact,即模型,指定为输入Artifact来获取这些训练好的模型,而它的功能是对从上游Op得到的模型进行评估。评估所使用的数据集由用户指定。评估的结果,即Loss和Accuracy,会写入到日志中。同时,该Op会输出一个评估得到一个最好的模型(暂时规定为Accuracy最高的模型),并将其保存下来,作为输出Artifact传给下游Op。

  3. 最后是“模型预测”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。

  1. 构建Op:本项目首先将模型的“训练”、“评估”、“预测”这三个步骤,分别用一个函数进行封装。这些函数会构建并返回一个Op,而Op能完成对应的步骤。本文称这些函数为Op构建函数

  2. 编排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_dirOp输出的存放目录。
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。在给该参数赋值时,除了指定输入的名称和内容,还可以选择是否指定输入的存放路径。

关于inputsoutputs参数的赋值,更详细的介绍可参考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类似,其中的commandargumentsinputs参数的功能与前面的用法一致,这里不过多赘述。

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_op1train_op2,这两个训练Op会输出两个训练后的模型。

train_op1train_op2的下游Op是evaluate()函数构建的eval_op,它对所有从“训练”Op获得的模型进行评估,并将最好的模型作为输出传输到下游的predict_op

train_op1.outputs["model"]train_op1中名称为“model”的输出Artifact,这个名称是在train()函数中通过outputs参数指定的。具体原理可参考输出Artifact

predict_oppredict()函数构建,它会使用从上游获得的模型在指定的测试数据集上进行预测。

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,如下图所示。

CodeLab运行Pipeline

在Pipeline运行时,终端上会先后显示训练的进度、评估的结果、预测的结果,以及其他信息,具体如下所示。

训练Op运行时,终端输出的部分信息:

运行截图1

运行截图2

运行截图3

本地运行

调试过后,如果没有Bug,就可以正常运行了,本地运行和本地调试从结果来看稍有不同,这里暂不展开,有兴趣可以查看本地运行任务

操作方式如下图所示:

本地运行

点击运行后,会弹出一个界面,在这个界面可以选择“单次运行”还是“周期运行”。同时还可以指定想要传给Pipeline的参数。具体界面如下图所示:

本地运行界面

本地运行的优势之一在于,可以将运行记录保存下来,每一次运行的参数、结果等信息,都会被记录,这有利于用户分析问题,也便于日后的复现。查看运行历史记录的界面在左侧的菜单栏,点击图标即可查看所有的记录,如下图所示:

Pipeline运行记录

再点击某一条记录,就可以查看详情。详情页面如下所示:

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“模型,该模型也会参与模型评估步骤。

注:本案例暂不支持模型相同但超参不同的情况。

【Pipeline】基于Pipeline实现垃圾邮件识别

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值