当前位置:   article > 正文

PaddleNLP学习日记(一)CBLUE医疗文本分类_ernie-health

ernie-health

目标

  •         使用医疗领域预训练模型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的学习之旅吧!

流程 

         本次学习的是医疗文本分类的脚本,我将代码抽象成了以下几块,挑重点去学习。

  1. 导包
  2. 定义指标类别
  3. 添加命令行参数
  4. 设置随机种子
  5. 定义评估方法
  6. 定义训练方法
  7. 定义主函数

         这里最重要的就是5步也就是训练方法,下面具体看看详细的代码。

训练方法

  1. def do_train():
  2. paddle.set_device(args.device)
  3. rank = paddle.distributed.get_rank()
  4. if paddle.distributed.get_world_size() > 1:
  5. paddle.distributed.init_parallel_env()
  6. set_seed(args.seed)
  7. train_ds, dev_ds = load_dataset('cblue',
  8. args.dataset,
  9. splits=['train', 'dev'])
  10. model = ElectraForSequenceClassification.from_pretrained(
  11. 'ernie-health-chinese',
  12. num_classes=len(train_ds.label_list),
  13. activation='tanh')
  14. tokenizer = ElectraTokenizer.from_pretrained('ernie-health-chinese')
  15. trans_func = partial(convert_example,
  16. tokenizer=tokenizer,
  17. max_seq_length=args.max_seq_length)
  18. batchify_fn = lambda samples, fn=Tuple(
  19. Pad(axis=0, pad_val=tokenizer.pad_token_id, dtype='int64'), # input
  20. Pad(axis=0, pad_val=tokenizer.pad_token_type_id, dtype='int64'
  21. ), # segment
  22. Pad(axis=0, pad_val=args.max_seq_length - 1, dtype='int64'), # position
  23. Stack(dtype='int64')): [data for data in fn(samples)]
  24. train_data_loader = create_dataloader(train_ds,
  25. mode='train',
  26. batch_size=args.batch_size,
  27. batchify_fn=batchify_fn,
  28. trans_fn=trans_func)
  29. dev_data_loader = create_dataloader(dev_ds,
  30. mode='dev',
  31. batch_size=args.batch_size,
  32. batchify_fn=batchify_fn,
  33. trans_fn=trans_func)
  34. if args.init_from_ckpt and os.path.isfile(args.init_from_ckpt):
  35. state_dict = paddle.load(args.init_from_ckpt)
  36. state_keys = {
  37. x: x.replace('discriminator.', '')
  38. for x in state_dict.keys() if 'discriminator.' in x
  39. }
  40. if len(state_keys) > 0:
  41. state_dict = {
  42. state_keys[k]: state_dict[k]
  43. for k in state_keys.keys()
  44. }
  45. model.set_dict(state_dict)
  46. if paddle.distributed.get_world_size() > 1:
  47. model = paddle.DataParallel(model)
  48. num_training_steps = args.max_steps if args.max_steps > 0 else len(
  49. train_data_loader) * args.epochs
  50. args.epochs = (num_training_steps - 1) // len(train_data_loader) + 1
  51. lr_scheduler = LinearDecayWithWarmup(args.learning_rate, num_training_steps,
  52. args.warmup_proportion)
  53. # Generate parameter names needed to perform weight decay.
  54. # All bias and LayerNorm parameters are excluded.
  55. decay_params = [
  56. p.name for n, p in model.named_parameters()
  57. if not any(nd in n for nd in ['bias', 'norm'])
  58. ]
  59. optimizer = paddle.optimizer.AdamW(
  60. learning_rate=lr_scheduler,
  61. parameters=model.parameters(),
  62. weight_decay=args.weight_decay,
  63. apply_decay_param_fun=lambda x: x in decay_params)
  64. criterion = paddle.nn.loss.CrossEntropyLoss()
  65. if METRIC_CLASSES[args.dataset] is Accuracy:
  66. metric = METRIC_CLASSES[args.dataset]()
  67. metric_name = 'accuracy'
  68. elif METRIC_CLASSES[args.dataset] is MultiLabelsMetric:
  69. metric = METRIC_CLASSES[args.dataset](
  70. num_labels=len(train_ds.label_list))
  71. metric_name = 'macro f1'
  72. else:
  73. metric = METRIC_CLASSES[args.dataset]()
  74. metric_name = 'micro f1'
  75. if args.use_amp:
  76. scaler = paddle.amp.GradScaler(init_loss_scaling=args.scale_loss)
  77. global_step = 0
  78. tic_train = time.time()
  79. total_train_time = 0
  80. for epoch in range(1, args.epochs + 1):
  81. for step, batch in enumerate(train_data_loader, start=1):
  82. input_ids, token_type_ids, position_ids, labels = batch
  83. with paddle.amp.auto_cast(
  84. args.use_amp,
  85. custom_white_list=['layer_norm', 'softmax', 'gelu', 'tanh'],
  86. ):
  87. logits = model(input_ids, token_type_ids, position_ids)
  88. loss = criterion(logits, labels)
  89. probs = F.softmax(logits, axis=1)
  90. correct = metric.compute(probs, labels)
  91. metric.update(correct)
  92. if isinstance(metric, Accuracy):
  93. result = metric.accumulate()
  94. elif isinstance(metric, MultiLabelsMetric):
  95. _, _, result = metric.accumulate('macro')
  96. else:
  97. _, _, _, result, _ = metric.accumulate()
  98. if args.use_amp:
  99. scaler.scale(loss).backward()
  100. scaler.minimize(optimizer, loss)
  101. else:
  102. loss.backward()
  103. optimizer.step()
  104. lr_scheduler.step()
  105. optimizer.clear_grad()
  106. global_step += 1
  107. if global_step % args.logging_steps == 0 and rank == 0:
  108. time_diff = time.time() - tic_train
  109. total_train_time += time_diff
  110. print(
  111. 'global step %d, epoch: %d, batch: %d, loss: %.5f, %s: %.5f, speed: %.2f step/s'
  112. % (global_step, epoch, step, loss, metric_name, result,
  113. args.logging_steps / time_diff))
  114. if global_step % args.valid_steps == 0 and rank == 0:
  115. evaluate(model, criterion, metric, dev_data_loader)
  116. if global_step % args.save_steps == 0 and rank == 0:
  117. save_dir = os.path.join(args.save_dir, 'model_%d' % global_step)
  118. if not os.path.exists(save_dir):
  119. os.makedirs(save_dir)
  120. if paddle.distributed.get_world_size() > 1:
  121. model._layers.save_pretrained(save_dir)
  122. else:
  123. model.save_pretrained(save_dir)
  124. tokenizer.save_pretrained(save_dir)
  125. if global_step >= num_training_steps:
  126. return
  127. tic_train = time.time()
  128. if rank == 0 and total_train_time > 0:
  129. 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数据集为例:

  1. from paddlenlp.datasets import load_dataset
  2. 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实现获取子数据集的。

  1. train_ds, dev_ds = load_dataset('cblue',
  2. args.dataset,
  3. splits=['train', 'dev'])

当然也可以加载自定义数据集,想更深入了解请前往加载内置数据集 食用~

create_dataloader()

该方法包含在utils.py中,具体代码如下:

  1. def create_dataloader(dataset,
  2. mode='train',
  3. batch_size=1,
  4. batchify_fn=None,
  5. trans_fn=None):
  6. if trans_fn:
  7. dataset = dataset.map(trans_fn)
  8. shuffle = True if mode == 'train' else False
  9. if mode == 'train':
  10. batch_sampler = paddle.io.DistributedBatchSampler(dataset,
  11. batch_size=batch_size,
  12. shuffle=shuffle)
  13. else:
  14. batch_sampler = paddle.io.BatchSampler(dataset,
  15. batch_size=batch_size,
  16. shuffle=shuffle)
  17. return paddle.io.DataLoader(dataset=dataset,
  18. batch_sampler=batch_sampler,
  19. collate_fn=batchify_fn,
  20. return_list=True)

可以看出它最后返回的时候还是调用的DataLoader方法,前面一些代码主要是根据传进来的参数对数据集dataset和取样器batch_sampler做了一些变化/选择。

OK,那么PaddlePaddle中DataLoader是啥样的呢?往下看!

只说create_dataloader方法中的DataLoader用的到几个参数吧,也就是这几个:

  1. paddle.io.DataLoader(dataset=dataset,
  2. batch_sampler=batch_sampler,
  3. collate_fn=batchify_fn,
  4. 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中去作为采样器。

参考paddle.io.BatchSampler

collate_fn参数:

        用过pytorch加载数据集的都应该知道这个参数的作用,就是传入一个函数名,用来将一批中的数据集对齐成相同长度并转成tensor类型数据或对每一批数据做一些其他操作的。

在此案例代码中他是这样使用的:

  1. batchify_fn = lambda samples, fn=Tuple(
  2. Pad(axis=0, pad_val=tokenizer.pad_token_id, dtype='int64'), # input
  3. Pad(axis=0, pad_val=tokenizer.pad_token_type_id, dtype='int64'), # segment
  4. Pad(axis=0, pad_val=args.max_seq_length - 1, dtype='int64'), # position
  5. 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完成医疗文本分类的流程,详细代码请移步医疗文本分类~

  1. 导包:参考github代码
  2. 定义指标类别:对于不同的子数据集及任务,使用不同的指标如Accuracy、MultiLabelsMetric、AccuracyAndF1。
  3. 添加命令行参数:主要用于接受用户从控制台输入的参数。
  4. 设置随机种子:用于复现训练和测试结果,方便后续进行调试。
  5. 定义评估方法:传入model、数据加载器、评价指标和损失函数,得到数据集对应的指标。
  6. 定义训练方法:指定分布式设置、加载数据集、分词器和模型、使用partial将已经提前得到的tokenizer和max_seq_length先传到convert_example中、定义批量处理方法batchify_fn并使用create_dataloader定义数据加载器、加载预训练模型的checkpoint、定义步数和衰减率等参数、定义优化器、定义损失函数、定义评价指标、开始训练(使用自动混合精度)
  7. 定义主函数:运行训练方法。

        大功告成!主要学习了一下如何用PaddleNLP进行医疗文本分类,get了比较关键的几个api的使用,总结了整体处理流程,官方文档查阅能力+1~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/355699
推荐阅读
相关标签
  

闽ICP备14008679号