• PaddleNLP学习日记(一)CBLUE医疗文本分类


    目标

    •         使用医疗领域预训练模型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~

  • 相关阅读:
    云原生k8s之管理工具kubectl详解(一)
    Android managed configurations(设置受管理的配置)
    Elasticsearch入门及整合springboot
    Java高级 设计模式
    浅析 vSAN 磁盘组架构和缓存盘的“消亡”
    ​Base64编码知识详解 ​
    跟羽夏学 Ghidra ——调试
    30天Python入门(第十七天:深入了解Python中的异常处理)
    SQL必需掌握的100个重要知识点:组合 WHERE 子句
    算法记录|笔试中遇到的题
  • 原文地址:https://blog.csdn.net/doubleguy/article/details/127502113