- 使用医疗领域预训练模型ERNIE-Health进行Fine-tune完成中文医疗文本分类
- 通过该案例掌握PaddleNLP的Transformer 、Tokenizer、Dataset 等API 的使用
- 熟悉PaddleNLP的数据处理流程
本案例基于CBLUE数据集, 介绍如下(摘自PaddleNLP):
中文医学语言理解测评(Chinese Biomedical Language Understanding Evaluation,CBLUE)1.0 版本数据集,这是国内首个面向中文医疗文本处理的多任务榜单,涵盖了医学文本信息抽取(实体识别、关系抽取)、医学术语归一化、医学文本分类、医学句子关系判定和医学问答共5大类任务8个子任务。其数据来源分布广泛,包括医学教材、电子病历、临床试验公示以及互联网用户真实查询等。该榜单一经推出便受到了学界和业界的广泛关注,已逐渐发展成为检验AI系统中文医疗信息处理能力的“金标准”。
- CMeEE:中文医学命名实体识别
- CMeIE:中文医学文本实体关系抽取
- CHIP-CDN:临床术语标准化任务
- CHIP-CTC:临床试验筛选标准短文本分类
- CHIP-STS:平安医疗科技疾病问答迁移学习
- KUAKE-QIC:医疗搜索检索词意图分类
- KUAKE-QTR:医疗搜索查询词-页面标题相关性
- KUAKE-QQR:医疗搜索查询词-查询词相关性
更多关于CBLUE数据集的介绍可前往CBLUE官方网站学习~
本次案例将学习的是CBLUE数据集中的CHIP-CDN任务。对于临床术语标准化任务(CHIP-CDN),我们按照 ERNIE-Health 中的方法通过检索将原多分类任务转换为了二分类任务,即给定一诊断原词和一诊断标准词,要求判定后者是否是前者对应的诊断标准词。本项目提供了检索处理后的 CHIP-CDN 数据集(简写CHIP-CDN-2C
),且构建了基于该数据集的example代码。下面就通过代码来开启paddlenlp的学习之旅吧!
本次学习的是医疗文本分类的脚本,我将代码抽象成了以下几块,挑重点去学习。
- 导包
- 定义指标类别
- 添加命令行参数
- 设置随机种子
- 定义评估方法
- 定义训练方法
- 定义主函数
这里最重要的就是5步也就是训练方法,下面具体看看详细的代码。
训练方法
def do_train():
paddle.set_device(args.device)
rank = paddle.distributed.get_rank()
if paddle.distributed.get_world_size() > 1:
paddle.distributed.init_parallel_env()
set_seed(args.seed)
train_ds, dev_ds = load_dataset('cblue',
args.dataset,
splits=['train', 'dev'])
model = ElectraForSequenceClassification.from_pretrained(
'ernie-health-chinese',
num_classes=len(train_ds.label_list),
activation='tanh')
tokenizer = ElectraTokenizer.from_pretrained('ernie-health-chinese')
trans_func = partial(convert_example,
tokenizer=tokenizer,
max_seq_length=args.max_seq_length)
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id, dtype='int64'), # input
Pad(axis=0, pad_val=tokenizer.pad_token_type_id, dtype='int64'
), # segment
Pad(axis=0, pad_val=args.max_seq_length - 1, dtype='int64'), # position
Stack(dtype='int64')): [data for data in fn(samples)]
train_data_loader = create_dataloader(train_ds,
mode='train',
batch_size=args.batch_size,
batchify_fn=batchify_fn,
trans_fn=trans_func)
dev_data_loader = create_dataloader(dev_ds,
mode='dev',
batch_size=args.batch_size,
batchify_fn=batchify_fn,
trans_fn=trans_func)
if args.init_from_ckpt and os.path.isfile(args.init_from_ckpt):
state_dict = paddle.load(args.init_from_ckpt)
state_keys = {
x: x.replace('discriminator.', '')
for x in state_dict.keys() if 'discriminator.' in x
}
if len(state_keys) > 0:
state_dict = {
state_keys[k]: state_dict[k]
for k in state_keys.keys()
}
model.set_dict(state_dict)
if paddle.distributed.get_world_size() > 1:
model = paddle.DataParallel(model)
num_training_steps = args.max_steps if args.max_steps > 0 else len(
train_data_loader) * args.epochs
args.epochs = (num_training_steps - 1) // len(train_data_loader) + 1
lr_scheduler = LinearDecayWithWarmup(args.learning_rate, num_training_steps,
args.warmup_proportion)
# Generate parameter names needed to perform weight decay.
# All bias and LayerNorm parameters are excluded.
decay_params = [
p.name for n, p in model.named_parameters()
if not any(nd in n for nd in ['bias', 'norm'])
]
optimizer = paddle.optimizer.AdamW(
learning_rate=lr_scheduler,
parameters=model.parameters(),
weight_decay=args.weight_decay,
apply_decay_param_fun=lambda x: x in decay_params)
criterion = paddle.nn.loss.CrossEntropyLoss()
if METRIC_CLASSES[args.dataset] is Accuracy:
metric = METRIC_CLASSES[args.dataset]()
metric_name = 'accuracy'
elif METRIC_CLASSES[args.dataset] is MultiLabelsMetric:
metric = METRIC_CLASSES[args.dataset](
num_labels=len(train_ds.label_list))
metric_name = 'macro f1'
else:
metric = METRIC_CLASSES[args.dataset]()
metric_name = 'micro f1'
if args.use_amp:
scaler = paddle.amp.GradScaler(init_loss_scaling=args.scale_loss)
global_step = 0
tic_train = time.time()
total_train_time = 0
for epoch in range(1, args.epochs + 1):
for step, batch in enumerate(train_data_loader, start=1):
input_ids, token_type_ids, position_ids, labels = batch
with paddle.amp.auto_cast(
args.use_amp,
custom_white_list=['layer_norm', 'softmax', 'gelu', 'tanh'],
):
logits = model(input_ids, token_type_ids, position_ids)
loss = criterion(logits, labels)
probs = F.softmax(logits, axis=1)
correct = metric.compute(probs, labels)
metric.update(correct)
if isinstance(metric, Accuracy):
result = metric.accumulate()
elif isinstance(metric, MultiLabelsMetric):
_, _, result = metric.accumulate('macro')
else:
_, _, _, result, _ = metric.accumulate()
if args.use_amp:
scaler.scale(loss).backward()
scaler.minimize(optimizer, loss)
else:
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.clear_grad()
global_step += 1
if global_step % args.logging_steps == 0 and rank == 0:
time_diff = time.time() - tic_train
total_train_time += time_diff
print(
'global step %d, epoch: %d, batch: %d, loss: %.5f, %s: %.5f, speed: %.2f step/s'
% (global_step, epoch, step, loss, metric_name, result,
args.logging_steps / time_diff))
if global_step % args.valid_steps == 0 and rank == 0:
evaluate(model, criterion, metric, dev_data_loader)
if global_step % args.save_steps == 0 and rank == 0:
save_dir = os.path.join(args.save_dir, 'model_%d' % global_step)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
if paddle.distributed.get_world_size() > 1:
model._layers.save_pretrained(save_dir)
else:
model.save_pretrained(save_dir)
tokenizer.save_pretrained(save_dir)
if global_step >= num_training_steps:
return
tic_train = time.time()
if rank == 0 and total_train_time > 0:
print('Speed: %.2f steps/s' % (global_step / total_train_time))
do_train方法中比较重要的部分有:
- 加载数据集load_dataset方法
- 创建数据加载器create_dataloader方法
- 加载模型ElectraForSequenceClassification.from_pretrained方法
- 加载分词器ElectraTokenizer.from_pretrained方法
其实和pytorch大同小异,其他一些地方就不说了,主要学习一下这几个接口的使用。
load_dataset()
目前PaddleNLP内置20余个NLP数据集,涵盖阅读理解,文本分类,序列标注,机器翻译等多项任务。目前提供的数据集可以在 数据集列表 中找到。
以加载msra_ner数据集为例:
from paddlenlp.datasets import load_dataset
train_ds, test_ds = load_dataset("msra_ner", splits=("train", "test"))
load_dataset()
方法会从 paddlenlp.datasets 下找到msra_ner数据集对应的数据读取脚本(默认路径:paddlenlp/datasets/msra_ner.py),并调用脚本中 DatasetBuilder
类的相关方法生成数据集。
生成数据集可以以 MapDataset
和 IterDataset
两种类型返回,分别是对 paddle.io.Dataset
和 paddle.io.IterableDataset
的扩展,只需在 load_dataset()
时设置 lazy
参数即可获取相应类型。Flase
对应返回 MapDataset
,True
对应返回 IterDataset
,默认值为None,对应返回 DatasetBuilder
默认的数据集类型,大多数为 MapDataset
。
关于 MapDataset
和 IterDataset
功能和异同可以参考API文档 datasets。
在此文本分类案例中,加载的是cblue中的子数据集,load_dataset()中提供了一个name参数用来指定想要获取的子数据集。
do_train方法中的加载数据集的时候把name参数省略了,但实际上还是用name实现获取子数据集的。
train_ds, dev_ds = load_dataset('cblue',
args.dataset,
splits=['train', 'dev'])
当然也可以加载自定义数据集,想更深入了解请前往加载内置数据集 食用~
create_dataloader()
该方法包含在utils.py中,具体代码如下:
def create_dataloader(dataset,
mode='train',
batch_size=1,
batchify_fn=None,
trans_fn=None):
if trans_fn:
dataset = dataset.map(trans_fn)
shuffle = True if mode == 'train' else False
if mode == 'train':
batch_sampler = paddle.io.DistributedBatchSampler(dataset,
batch_size=batch_size,
shuffle=shuffle)
else:
batch_sampler = paddle.io.BatchSampler(dataset,
batch_size=batch_size,
shuffle=shuffle)
return paddle.io.DataLoader(dataset=dataset,
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
可以看出它最后返回的时候还是调用的DataLoader方法,前面一些代码主要是根据传进来的参数对数据集dataset和取样器batch_sampler做了一些变化/选择。
OK,那么PaddlePaddle中DataLoader是啥样的呢?往下看!
只说create_dataloader方法中的DataLoader用的到几个参数吧,也就是这几个:
paddle.io.DataLoader(dataset=dataset,
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
DataLoader定义:
DataLoader返回一个迭代器,该迭代器根据 batch_sampler
给定的顺序迭代一次给定的 dataset。
dataset参数:
DataLoader当前支持 map-style
和 iterable-style
的数据集, map-style
的数据集可通过下标索引样本,请参考 paddle.io.Dataset
; iterable-style
数据集只能迭代式地获取样本,类似Python迭代器,请参考 paddle.io.IterableDataset
。这一点和上面的load_dataset()方法对应起来了,通过load_dataset()加载进来的数据集也只有两种类型—— MapDataset
和 IterDataset
两种类型。所以对于dataset参数只需要选择他是用map还是iter类型的就可以了,代码中的trans_fn应该就是做这个事的。
batch_sampler参数:
批采样器的基础实现,用于 paddle.io.DataLoader
中迭代式获取mini-batch的样本下标数组,数组长度与 batch_size
一致。
所有用于 paddle.io.DataLoader
中的批采样器都必须是 paddle.io.BatchSampler
的子类并实现以下方法:
__iter__
: 迭代式返回批样本下标数组。
__len__
: 每epoch中mini-batch数。
参数包含:
-
dataset (Dataset) - 此参数必须是
paddle.io.Dataset
或paddle.io.IterableDataset
的一个子类实例或实现了__len__
的Python对象,用于生成样本下标。默认值为None。 -
sampler (Sampler) - 此参数必须是
paddle.io.Sampler
的子类实例,用于迭代式获取样本下标。dataset
和sampler
参数只能设置一个。默认值为None。 -
shuffle (bool) - 是否需要在生成样本下标时打乱顺序。默认值为False。
-
batch_size (int) - 每mini-batch中包含的样本数。默认值为1。
-
drop_last (bool) - 是否需要丢弃最后无法凑整一个mini-batch的样本。默认值为False。
在create_dataloader中的paddle.io.BatchSampler和paddle.io.DistributedBatchSampler中只用到了三个参数——dataset、batch_size、shuffle,得到实例batch_sampler,包含了样本下标数组的迭代器,然后将它传入DataLoader中去作为采样器。
collate_fn参数:
用过pytorch加载数据集的都应该知道这个参数的作用,就是传入一个函数名,用来将一批中的数据集对齐成相同长度并转成tensor类型数据或对每一批数据做一些其他操作的。
在此案例代码中他是这样使用的:
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id, dtype='int64'), # input
Pad(axis=0, pad_val=tokenizer.pad_token_type_id, dtype='int64'), # segment
Pad(axis=0, pad_val=args.max_seq_length - 1, dtype='int64'), # position
Stack(dtype='int64')): [data for data in fn(samples)]
用了一个lambda表达式,对于每一个batch的samples和fn,调用一次
[data for data in fn(samples)]
其中fn=Tuple(3个Pad、1个Stack),3个pad分别代表input、segment、position用当前批次最大句子长度进行填充,1个Stack代表将当前批次的label顺序堆叠。
不得不说,这种写法很优雅,很装杯,学到了哈哈哈哈~
ElectraForSequenceClassification.from_pretrained()
PaddleNLP加载预训练模型的方式和pytorch差不多,也是用from_pretrained(),只需要往里面传一个模型实例即可。根据官方文档找到对应的模型直接加载就行了。Electra 模型在输出层的顶部有一个线性层,用于序列分类/回归任务,如 GLUE 任务。
ElectraTokenizer.from_pretrained()
与ElectraForSequenceClassification.from_pretrained()配套的Tokenizer,也同pytorch一致,传入路径加载即可。
最后来总结一下使用paddlenlp完成医疗文本分类的流程,详细代码请移步医疗文本分类~
- 导包:参考github代码
- 定义指标类别:对于不同的子数据集及任务,使用不同的指标如Accuracy、MultiLabelsMetric、AccuracyAndF1。
- 添加命令行参数:主要用于接受用户从控制台输入的参数。
- 设置随机种子:用于复现训练和测试结果,方便后续进行调试。
- 定义评估方法:传入model、数据加载器、评价指标和损失函数,得到数据集对应的指标。
- 定义训练方法:指定分布式设置、加载数据集、分词器和模型、使用partial将已经提前得到的tokenizer和max_seq_length先传到convert_example中、定义批量处理方法batchify_fn并使用create_dataloader定义数据加载器、加载预训练模型的checkpoint、定义步数和衰减率等参数、定义优化器、定义损失函数、定义评价指标、开始训练(使用自动混合精度)
- 定义主函数:运行训练方法。
大功告成!主要学习了一下如何用PaddleNLP进行医疗文本分类,get了比较关键的几个api的使用,总结了整体处理流程,官方文档查阅能力+1~