赞
踩
目标
数据集
本案例基于CBLUE数据集, 介绍如下(摘自PaddleNLP):
中文医学语言理解测评(Chinese Biomedical Language Understanding Evaluation,CBLUE)1.0 版本数据集,这是国内首个面向中文医疗文本处理的多任务榜单,涵盖了医学文本信息抽取(实体识别、关系抽取)、医学术语归一化、医学文本分类、医学句子关系判定和医学问答共5大类任务8个子任务。其数据来源分布广泛,包括医学教材、电子病历、临床试验公示以及互联网用户真实查询等。该榜单一经推出便受到了学界和业界的广泛关注,已逐渐发展成为检验AI系统中文医疗信息处理能力的“金标准”。
更多关于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方法中比较重要的部分有:
其实和pytorch大同小异,其他一些地方就不说了,主要学习一下这几个接口的使用。
目前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'])
当然也可以加载自定义数据集,想更深入了解请前往加载内置数据集 食用~
该方法包含在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顺序堆叠。
不得不说,这种写法很优雅,很装杯,学到了哈哈哈哈~
PaddleNLP加载预训练模型的方式和pytorch差不多,也是用from_pretrained(),只需要往里面传一个模型实例即可。根据官方文档找到对应的模型直接加载就行了。Electra 模型在输出层的顶部有一个线性层,用于序列分类/回归任务,如 GLUE 任务。
与ElectraForSequenceClassification.from_pretrained()配套的Tokenizer,也同pytorch一致,传入路径加载即可。
总结
最后来总结一下使用paddlenlp完成医疗文本分类的流程,详细代码请移步医疗文本分类~
大功告成!主要学习了一下如何用PaddleNLP进行医疗文本分类,get了比较关键的几个api的使用,总结了整体处理流程,官方文档查阅能力+1~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。