Skip to content

Commit 66ccc0e

Browse files
committed
[ehealth] fix problems in sequence classification
1 parent 15f0aa8 commit 66ccc0e

File tree

11 files changed

+263
-164
lines changed

11 files changed

+263
-164
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# 使用医疗领域预训练模型Fine-tune完成中文医疗语言理解任务
2+
3+
近年来,预训练语言模型(Pre-trained Language Model,PLM)逐渐成为自然语言处理(Natural Language Processing,NLP)的主流方法。这类模型可以利用大规模的未标注语料进行训练,得到的模型在下游NLP任务上效果明显提升,在通用领域和特定领域均有广泛应用。在医疗领域,早期的做法是在预先训练好的通用语言模型上进行Fine-tune。后来的研究发现直接使用医疗相关语料学习到的预训练语言模型在医疗文本任务上的效果更好,采用的模型结构也从早期的BERT演变为更新的RoBERTa、ALBERT和ELECTRA。
4+
5+
本示例展示了中文医疗预训练模型eHealth([Building Chinese Biomedical Language Models via Multi-Level Text Discrimination](https://arxiv.org/abs/2110.07244))如何Fine-tune完成中文医疗语言理解任务。
6+
7+
## 模型介绍
8+
9+
本项目针对中文医疗语言理解任务,开源了中文医疗预训练模型eHealth(简写`chinese-ehealth`)。eHealth使用了医患对话、科普文章、病历档案、临床病理学教材等脱敏中文语料进行预训练,通过预训练任务设计来学习词级别和句级别的文本信息。该模型的整体结构与ELECTRA相似,包括生成器和判别器两部分。 而Fine-tune过程只用到了判别器模块,由12层Transformer网络组成。
10+
11+
## 数据集介绍
12+
13+
本项目使用了中文医学语言理解测评([Chinese Biomedical Language Understanding Evaluation,CBLUE](https://github.com/CBLUEbenchmark/CBLUE))数据集,[<sup>[1]</sup>](#refer-anchor-cblue)其包括医学文本信息抽取(实体识别、关系抽取)、医学术语归一化、医学文本分类、医学句子关系判定和医学问答共5大类任务8个子任务。
14+
15+
* CMeEE:中文医学命名实体识别
16+
* CMeIE:中文医学文本实体关系抽取
17+
* CHIP-CDN:临床术语标准化任务
18+
* CHIP-CTC:临床试验筛选标准短文本分类
19+
* CHIP-STS:平安医疗科技疾病问答迁移学习
20+
* KUAKE-QIC:医疗搜索检索词意图分类
21+
* KUAKE-QTR:医疗搜索查询词-页面标题相关性
22+
* KUAKE-QQR:医疗搜索查询词-查询词相关性
23+
24+
更多信息可参考CBLUE的[github](https://github.com/CBLUEbenchmark/CBLUE/blob/main/README_ZH.md)。其中对于临床术语标准化任务(CHIP-CDN),我们按照eHealth中的方法通过检索将原多分类任务转换为了二分类任务,即给定一诊断原词和一诊断标准词,要求判定后者是否是前者对应的诊断标准词。本项目提供了检索处理后的CHIP-CDN数据集(简写`CHIP-CDN-2C`),且构建了基于该数据集的example代码。
25+
26+
## 快速开始
27+
28+
### 代码结构说明
29+
30+
以下是本项目主要代码结构及说明:
31+
32+
```text
33+
cblue/
34+
├── README.md # 使用说明
35+
└── train_classification.py # 分类任务训练评估脚本
36+
```
37+
38+
### 模型训练
39+
40+
我们按照任务类别划分,同时提供了8个任务的样例代码。可以运行下边的命令,在训练集上进行训练,并在开发集上进行验证。
41+
42+
```shell
43+
$ unset CUDA_VISIBLE_DEVICES
44+
$ python -m paddle.distributed.launch --gpus "0,1,2,3" train.py --dataset CHIP-CDN-2C --batch_size 256 --max_seq_length 32 --learning_rate 3e-5 --epochs 16
45+
```
46+
47+
### 训练参数设置(Training setup)及结果
48+
49+
| Task | epochs | batch_size | learning_rate | max_seq_length | results |
50+
| --------- | :----: | :--------: | :-----------: | :------------: | :-----: |
51+
| CHIP-STS | 16 | 32 | 1e-4 | 96 | 0.88550 |
52+
| CHIP-CTC | 16 | 32 | 3e-5 | 160 | 0.82790 |
53+
| CHIP-CDN | 16 | 256 | 3e-5 | 32 | 0.76979 |
54+
| KUAKE-QQR | 16 | 32 | 6e-5 | 64 | 0.82364 |
55+
| KUAKE-QTR | 12 | 32 | 6e-5 | 64 | 0.69653 |
56+
| KUAKE-QIC | 4 | 32 | 6e-5 | 128 | 0.81176 |
57+
58+
59+
可支持配置的参数:
60+
61+
* `save_dir`:可选,保存训练模型的目录;默认保存在当前目录checkpoints文件夹下。
62+
* `dataset`:可选,CHIP-CDN-2C CHIP-CTC CHIP-STS KUAKE-QIC KUAKE-QTR KUAKE-QQR,默认为KUAKE-QIC数据集。
63+
* `max_seq_length`:可选,ELECTRA模型使用的最大序列长度,最大不能超过512, 若出现显存不足,请适当调低这一参数;默认为128。
64+
* `batch_size`:可选,批处理大小,请结合显存情况进行调整,若出现显存不足,请适当调低这一参数;默认为32。
65+
* `learning_rate`:可选,Fine-tune的最大学习率;默认为6e-5。
66+
* `weight_decay`:可选,控制正则项力度的参数,用于防止过拟合,默认为0.01。
67+
* `epochs`: 训练轮次,默认为3。
68+
* `valid_steps`: evaluate的间隔steps数,默认100。
69+
* `save_steps`: 保存checkpoints的间隔steps数,默认100。
70+
* `logging_steps`: 日志打印的间隔steps数,默认10。
71+
* `warmup_proption`:可选,学习率warmup策略的比例,如果0.1,则学习率会在前10%训练step的过程中从0慢慢增长到learning_rate, 而后再缓慢衰减,默认为0.1。
72+
* `init_from_ckpt`:可选,模型参数路径,热启动模型训练;默认为None。
73+
* `seed`:可选,随机种子,默认为1000.
74+
* `device`: 选用什么设备进行训练,可选cpu或gpu。如使用gpu训练则参数gpus指定GPU卡号。
75+
* `use_amp`: 是否使用混合精度训练,默认为False。
76+
* `use_ema`: 是否使用Exponential Moving Average预测,默认为False。
77+
78+
### 依赖安装
79+
80+
```shell
81+
pip install xlrd==1.2.0
82+
```
83+
84+
<div id="refer-anchor-cblue"></div>
85+
- [1] CBLUE: A Chinese Biomedical Language Understanding Evaluation Benchmark [pdf](https://arxiv.org/abs/2106.08087) [git](https://github.com/CBLUEbenchmark/CBLUE) [web](https://tianchi.aliyun.com/specials/promotion/2021chinesemedicalnlpleaderboardchallenge)

examples/biomedical/cblue/sequence_classification/README.md

Lines changed: 0 additions & 63 deletions
This file was deleted.

examples/biomedical/cblue/sequence_classification/train.py renamed to examples/biomedical/cblue/train_classification.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from paddlenlp.data import Stack, Tuple, Pad
2828
from paddlenlp.datasets import load_dataset
2929
from paddlenlp.transformers import LinearDecayWithWarmup
30-
from paddlenlp.metrics import MultiLabelsMetric
30+
from paddlenlp.metrics import MultiLabelsMetric, AccuracyAndF1
3131
from paddlenlp.ops.optimizer import ExponentialMovingAverage
3232

3333
from utils import convert_example
@@ -36,30 +36,27 @@
3636
'KUAKE-QIC': Accuracy,
3737
'KUAKE-QQR': Accuracy,
3838
'KUAKE-QTR': Accuracy,
39-
'CHIP-CTC': partial(
40-
MultiLabelsMetric, name='macro'),
41-
'CHIP-STS': partial(
42-
MultiLabelsMetric, name='macro'),
43-
'CHIP-CDN-2C': partial(
44-
MultiLabelsMetric, name='micro')
39+
'CHIP-CTC': MultiLabelsMetric,
40+
'CHIP-STS': MultiLabelsMetric,
41+
'CHIP-CDN-2C': AccuracyAndF1
4542
}
4643

4744
# yapf: disable
4845
parser = argparse.ArgumentParser()
4946
parser.add_argument('--dataset', choices=['KUAKE-QIC', 'KUAKE-QQR', 'KUAKE-QTR', 'CHIP-STS', 'CHIP-CTC', 'CHIP-CDN-2C'],
50-
default='KUAKE-QIC', type=str, help='Dataset for token classfication tasks.')
47+
default='KUAKE-QIC', type=str, help='Dataset for sequence classfication tasks.')
5148
parser.add_argument('--seed', default=1000, type=int, help='Random seed for initialization.')
5249
parser.add_argument('--device', choices=['cpu', 'gpu', 'xpu', 'npu'], default='gpu', help='Select which device to train model, default to gpu.')
53-
parser.add_argument('--epochs', default=3, type=int, help='Total number of training epochs to perform.')
50+
parser.add_argument('--epochs', default=3, type=int, help='Total number of training epochs.')
5451
parser.add_argument('--batch_size', default=32, type=int, help='Batch size per GPU/CPU for training.')
5552
parser.add_argument('--learning_rate', default=6e-5, type=float, help='Learning rate for fine-tuning sequence classification task.')
56-
parser.add_argument('--weight_decay', default=0.01, type=float, help="Weight decay if we apply some.")
57-
parser.add_argument('--warmup_proportion', default=0.1, type=float, help='Linear warmup proportion over the training process.')
53+
parser.add_argument('--weight_decay', default=0.01, type=float, help="Weight decay of optimizer if we apply some.")
54+
parser.add_argument('--warmup_proportion', default=0.1, type=float, help='Linear warmup proportion of learning rate over the training process.')
5855
parser.add_argument('--max_seq_length', default=128, type=int, help='The maximum total input sequence length after tokenization.')
5956
parser.add_argument('--init_from_ckpt', default=None, type=str, help='The path of checkpoint to be loaded.')
6057
parser.add_argument('--logging_steps', default=10, type=int, help='The interval steps to logging.')
6158
parser.add_argument('--save_dir', default='./checkpoint', type=str, help='The output directory where the model checkpoints will be written.')
62-
parser.add_argument('--save_steps', default=100, type=int, help='The interval steps to save checkppoints.')
59+
parser.add_argument('--save_steps', default=100, type=int, help='The interval steps to save checkpoints.')
6360
parser.add_argument('--valid_steps', default=100, type=int, help='The interval steps to evaluate model performance.')
6461
parser.add_argument('--use_ema', default=False, type=bool, help='Use exponential moving average for evaluation.')
6562
parser.add_argument('--use_amp', default=False, type=distutils.util.strtobool, help='Enable mixed precision training.')
@@ -100,9 +97,13 @@ def evaluate(model, criterion, metric, data_loader):
10097
if isinstance(metric, Accuracy):
10198
metric_name = 'accuracy'
10299
result = metric.accumulate()
100+
elif isinstance(metric, MultiLabelsMetric):
101+
metric_name = 'macro f1'
102+
_, _, result = metric.accumulate('macro')
103103
else:
104-
metric_name = metric._name + ' f1'
105-
_, _, result = metric.accumulate(metric._name)
104+
metric_name = 'micro f1'
105+
_, _, _, result, _ = metric.accumulate()
106+
106107
print('eval loss: %.5f, %s: %.5f' % (np.mean(losses), metric_name, result))
107108
model.train()
108109
metric.reset()
@@ -143,7 +144,10 @@ def do_train():
143144
'cblue', args.dataset, splits=['train', 'dev', 'test'])
144145

145146
model = ppnlp.transformers.ElectraForSequenceClassification.from_pretrained(
146-
'chinese-ehealth', num_classes=len(train_ds.label_list))
147+
'chinese-ehealth',
148+
num_classes=len(train_ds.label_list),
149+
activation='tanh',
150+
layer_norm_eps=1e-5)
147151
tokenizer = ppnlp.transformers.ElectraTokenizer.from_pretrained(
148152
'chinese-ehealth')
149153

@@ -152,9 +156,9 @@ def do_train():
152156
tokenizer=tokenizer,
153157
max_seq_length=args.max_seq_length)
154158
batchify_fn = lambda samples, fn=Tuple(
155-
Pad(axis=0, pad_val=tokenizer.pad_token_id), # input
156-
Pad(axis=0, pad_val=tokenizer.pad_token_type_id), # segment
157-
Pad(axis=0, pad_val=args.max_seq_length - 1), # position
159+
Pad(axis=0, pad_val=tokenizer.pad_token_id, dtype='int64'), # input
160+
Pad(axis=0, pad_val=tokenizer.pad_token_type_id, dtype='int64'), # segment
161+
Pad(axis=0, pad_val=args.max_seq_length - 1, dtype='int64'), # position
158162
Stack(dtype='int64')): [data for data in fn(samples)]
159163
train_data_loader = create_dataloader(
160164
train_ds,
@@ -172,7 +176,8 @@ def do_train():
172176
if args.init_from_ckpt and os.path.isfile(args.init_from_ckpt):
173177
state_dict = paddle.load(args.init_from_ckpt)
174178
model.set_dict(state_dict)
175-
model = paddle.DataParallel(model)
179+
if paddle.distributed.get_world_size() > 1:
180+
model = paddle.DataParallel(model)
176181

177182
num_training_steps = len(train_data_loader) * args.epochs
178183

@@ -196,10 +201,13 @@ def do_train():
196201
if METRIC_CLASSES[args.dataset] is Accuracy:
197202
metric = METRIC_CLASSES[args.dataset]()
198203
metric_name = 'accuracy'
199-
else:
204+
elif METRIC_CLASSES[args.dataset] is MultiLabelsMetric:
200205
metric = METRIC_CLASSES[args.dataset](
201206
num_labels=len(train_ds.label_list))
202-
metric_name = metric._name + ' f1'
207+
metric_name = 'macro f1'
208+
else:
209+
metric = METRIC_CLASSES[args.dataset]()
210+
metric_name = 'micro f1'
203211
if args.use_amp:
204212
scaler = paddle.amp.GradScaler(init_loss_scaling=args.scale_loss)
205213
if args.use_ema and rank == 0:
@@ -222,8 +230,10 @@ def do_train():
222230

223231
if isinstance(metric, Accuracy):
224232
result = metric.accumulate()
233+
elif isinstance(metric, MultiLabelsMetric):
234+
_, _, result = metric.accumulate('macro')
225235
else:
226-
_, _, result = metric.accumulate(metric._name)
236+
_, _, _, result, _ = metric.accumulate()
227237

228238
if args.use_amp:
229239
scaler.scale(loss).backward()
@@ -259,11 +269,14 @@ def do_train():
259269
save_dir = os.path.join(args.save_dir, 'model_%d' % global_step)
260270
if not os.path.exists(save_dir):
261271
os.makedirs(save_dir)
262-
model._layers.save_pretrained(save_dir)
272+
if paddle.distributed.get_world_size() > 1:
273+
model._layers.save_pretrained(save_dir)
274+
else:
275+
model.save_pretrained(save_dir)
263276
tokenizer.save_pretrained(save_dir)
264277
tic_train = time.time()
265-
266-
print('Speed: %.2f steps/s' % (global_step / total_train_time))
278+
if rank == 0:
279+
print('Speed: %.2f steps/s' % (global_step / total_train_time))
267280

268281

269282
if __name__ == "__main__":

0 commit comments

Comments
 (0)