CHIP-2020 中文医学文本实体关系抽取

来源:这里

这个项目的本质是-----------医学文本的实体关系联合抽取问题,既要解决命名实体识别,又要解决关系分类问题。使用RoFormerV2模型作为预训练模型,GPLinker作为下游模型,在Embedding层添加FGM对抗训练增加模型性能。

一、数据集

  • CHIP-2020-2中文医学文本实体关系抽取数据集

      数据集包含儿科训练语料和百种常见疾病训练语料,儿科训练语料来源于518种儿科疾病,百种常见疾病训练语料来源于109种常见疾病。
      近7.5万三元组数据,2.8万疾病语句和53种定义好的schema。
    
  • 数据集格式

{
   "spo_list" : [
      {
         "Combined" : true,
         "object" : {
            "@value" : "外照射"
         },
         "object_type" : {
            "@value" : "其他治疗"
         },
         "predicate" : "放射治疗",
         "subject" : "慢性胰腺炎",
         "subject_type" : "疾病"
      },
      {
         "Combined" : true,
         "object" : {
            "@value" : "外照射"
         },
         "object_type" : {
            "@value" : "其他治疗"
         },
         "predicate" : "放射治疗",
         "subject" : "非肿瘤性疼痛",
         "subject_type" : "疾病"
      }
   ],
   "text" : "慢性胰腺炎@### 低剂量放射 自1964年起,有几项病例系列报道称外照射 (5-50Gy) 可以有效改善慢性胰腺炎患者的疼痛症状。慢性胰腺炎@从概念上讲,外照射可以起到抗炎和止痛作用,并且已经开始被用于非肿瘤性疼痛的治疗。"
}
临床实践文本可能不提及主题疾病---每句之前增加主题疾病实体,并以@和原文分割
Combined-----三元组来源上下多个句子,拼接在一起
”text”----记录来源文本
SPO表示法。S-头实体,O-尾实体,P-Predicate,即「关系(Relation)」更专业的叫法。
  • 本代码需要将数据处理成如下的格式:
{
   "spo_list" : [
      {
         "Combined" : true,
         "object" : "外照射",
         "object_type" : "其他治疗",
         "predicate" : "放射治疗",
         "subject" : "慢性胰腺炎",
         "subject_type" : "疾病"
      },
      {
         "Combined" : true,
         "object" : "外照射",
         "object_type" : "其他治疗",
         "predicate" : "放射治疗",
         "subject" : "非肿瘤性疼痛",
         "subject_type" : "疾病"
      }
   ],
   "text" : "慢性胰腺炎@### 低剂量放射 自1964年起,有几项病例系列报道称外照射 (5-50Gy) 可以有效改善慢性胰腺炎患者的疼痛症状。慢性胰腺炎@从概念上讲,外照射可以起到抗炎和止痛作用,并且已经开始被用于非肿瘤性疼痛的治疗。"
}
  • schemas格式(数据库的组织和结构)
    介绍:限定待加入知识图谱数据的格式;相当于某个领域内的数据模型,包含了该领域内有意义的概念类型以及这些类型的属性
    作用:规范结构化数据的表达,一条数据必须满足Schema预先定义好的实体对象及其类型,才被允许更新到知识图谱中
... ...

{"subject_type": "疾病", "predicate": "发病部位", "object_type": "部位"}
{"subject_type": "疾病", "predicate": "转移部位", "object_type": "部位"}
{"subject_type": "疾病", "predicate": "外侵部位", "object_type": "部位"}

... ...

  • dataloader单条格式
{'text': text, 'spo_list': [(s, p, o)]}

schema中的predicate不能重复,如有重复要更改,加以区别,否则就算object_type和subject_type不一致,只要predicate一致,就认为是一个关系。数据里的predicate也要同步更改。

二、项目结构

./
├── README.md
├── chinese_roformer-v2-char_L-12_H-768_A-12            roformer-v2 12层base版
│		 ├── bert_config.json                           是BERT在训练时可选调整参数
│		 ├── bert_model.ckpt.data-00000-of-00001
│		 ├── bert_model.ckpt.index						负责模型变量载入
│		 ├── bert_model.ckpt.meta						负责模型变量载入
│		 ├── checkpoint
│		 └── vocab.txt
├── chinese_roformer-v2-char_L-6_H-384_A-6              roformer-v2 12层base版
│		 ├── bert_config.json
│		 ├── bert_model.ckpt.data-00000-of-00001
│		 ├── bert_model.ckpt.index
│		 ├── bert_model.ckpt.meta
│		 ├── checkpoint
│		 └── vocab.txt
├── config.py                                           部分超参数配置
├── data
│		 ├── chip2020                           数据集
│		 │		 ├── 53_schemas.json
│		 │		 ├── train_data.json
│		 │		 └── val_data.json
│		 └── pred                               最佳模型预测样本
│		     ├── val_pred_ep38.json
│		     └── val_pred_ep73.json
├── dataloader.py                                       数据编码器
├── evaluate.py                                         模型评估
├── images                                              数据绘图
│		 ├── train_loss.png
│		 ├── train_loss_base.png
│		 ├── val_f1.png
│		 └── val_f1_base.png
├── log                                                 日志
│		 ├── f1.out
│		 ├── nohup.out
│		 ├── nohup_base.out
│		 ├── train_log.csv
│		 └── train_log_base.csv
├── main.py
├── model.py                                            模型文件
├── path.py                                             项目路径
├── plot.py                                             画图工具
├── predict.py                                          预测文件
├── report                                              模型评估报告
│		 ├── f1.csv
│		 ├── predicate_f1.csv
│		 └── predicate_f1_base.csv
├── schemaloader.py                                     schema加载器
├── test.py                                             token转可读字符
├── train.py                                            训练
├── utils                                               bert4keras工具包,可pip下载
│		 ├── __init__.py
│		 ├── adversarial.py
│		 ├── backend.py
│		 ├── layers.py
│		 ├── models.py
│		 ├── optimizers.py
│		 ├── snippets.py
│		 └── tokenizers.py
└── weights                                             保存的权重
    ├── gplinker_roformer_best.h5
    └── gplinker_roformer_v2_best.h5

10 directories, 51 files

模型中L表示的是transformer的层数,H表示输出的维度,A表示mutil-head attention的个数

三、代码解析

train.py

  • 构建模型:
# 构建模型
#extend_with_exponential_moving_average:返回新的优化器类,加入EMA(权重滑动平均)
AdamEMA = extend_with_exponential_moving_average(Adam, name = 'AdamEMA')
optimizer = AdamEMA(lr = 5e-5)
model.compile(loss = globalpointer_crossentropy, optimizer = optimizer)
model.summary()
adversarial_training(model, 'Embedding-Token', 0.5)

Adam:一阶梯度优化算法,提高计算效率,降低内存需求。
optimizer优化器
model.compile():模型编译的时候把优化器跟损失函数传进去,并返回一个张量作为输出结果。
adversarial_training(): 对抗训练
model.summary(): 输出模型各层的参数状况
CHIP-2020 中文医学文本实体关系抽取_第1张图片

  • 每一轮的工作:
    def on_epoch_end(self, epoch, logs = None):
        global count_model_did_not_improve #记录模型没有变好的次数
        #确定模型参数保存位置
        save_best_path = "{}/{}_{}_best.h5".format(weights_path, event_type, MODEL_TYPE)
        #apply_ema_weights():备份原模型权重,然后将平均权重应用到模型上去
        optimizer.apply_ema_weights()
        f1, precision, recall = evaluate(valid_data, epoch)
        f1_list.append(f1)
        recall_list.append(recall)
        precision_list.append(precision)
        if f1 >= self.best_val_f1:
            self.best_val_f1 = f1
            count_model_did_not_improve = 0
            model.save_weights(save_best_path)
        else:
            count_model_did_not_improve += 1
            print("Early stop count " + str(count_model_did_not_improve) + "/" + str(self.patience))
            #当验证集F1超过5轮没有上升时,停止训练。
            if count_model_did_not_improve >= self.patience:
                self.model.stop_training = True
                print("Epoch %05d: early stopping THR" % epoch)

model.py

  • 加载模型,训练结果
def get_model():
    # 加载预训练模型
    base = build_transformer_model(
        config_path = config_path,
        checkpoint_path = checkpoint_path,
        model = MODEL_TYPE,
        return_keras_model = False
    )
    
    # 预测结果
    #SetLearningRate:层的一个包装,用来设置当前层的学习率
    entity_output = SetLearningRate(GlobalPointer(heads = 2, head_size = 64, kernel_initializer = "he_normal"),
                                    10, True)(base.model.output)
    head_output = SetLearningRate(GlobalPointer(
        heads = len(predicate2id), head_size = 64, RoPE = False, tril_mask = False, kernel_initializer = "he_normal"
    ), 10, True)(base.model.output)
    tail_output = SetLearningRate(GlobalPointer(
        heads = len(predicate2id), head_size = 64, RoPE = False, tril_mask = False, kernel_initializer = "he_normal"
    ), 10, True)(base.model.output)
    outputs = [entity_output, head_output, tail_output]
    
    #input输入层,output输出层
    model = keras.models.Model(base.model.inputs, outputs)
    return model

1.调用bert4keras中的build_transformer_model来选择使用的模型

checkpoint_path = checkpoint_path
checkpoint_path = BASE_CKPT_NAME
BASE_CKPT_NAME = proj_path + “/chinese_roformer-v2-char_L-12_H-768_A-12/bert_model.ckpt”

TensorFlow模型会保存在后缀为.ckpt的文件中。
保存后在save这个文件夹中实际会出现3个文件,因为TensorFlow会将计算图的结构和图上参数取值分开保存。
model.ckpt.meta文件保存了TensorFlow计算图的结构,可以理解为神经网络的网络结构model.ckpt文件保存了TensorFlow程序中每一个变量的取值
checkpoint文件保存了一个目录下所有的模型文件列表(也可以看作权重)

  • 设置当前层的学习率
class SetLearningRate:
    """层的一个包装,用来设置当前层的学习率
    """
    
    def __init__(self, layer, lamb, is_ada = False):
        self.layer = layer
        self.lamb = lamb  # 学习率比例
        self.is_ada = is_ada  # 是否自适应学习率优化器
    
    def __call__(self, inputs):
        with K.name_scope(self.layer.name):
            if not self.layer.built:
                input_shape = K.int_shape(inputs)
                self.layer.build(input_shape)
                self.layer.built = True
                if self.layer._initial_weights is not None:
                    self.layer.set_weights(self.layer._initial_weights)
        for key in ['kernel', 'bias', 'embeddings', 'depthwise_kernel', 'pointwise_kernel', 'recurrent_kernel', 'gamma',
                    'beta']:
            if hasattr(self.layer, key):
                weight = getattr(self.layer, key)
                if self.is_ada:
                    lamb = self.lamb  # 自适应学习率优化器直接保持lamb比例
                else:
                    lamb = self.lamb ** 0.5  # SGD(包括动量加速),lamb要开平方
                K.set_value(weight, K.eval(weight) / lamb)  # 更改初始化
                setattr(self.layer, key, weight * lamb)  # 按比例替换
        return self.layer(inputs)

四、运行时的报错:

  1. AttributeError: module ‘keras.engine.base_layer’ has no attribute ‘Node’

  2. 版本问题
    CHIP-2020 中文医学文本实体关系抽取_第2张图片

  3. KeyError: ‘roformer’
    解决
    因为bert4keras模块的版本不对应------pip install bert4keras==0.11.1
    源代码中的utils文件夹其实就是bert4keras库,但是没有更新到最新的,所以导致找不到roformer

  4. tensorflow.python.framework.errors_impl.NotFoundError: Key bert/embeddings/LayerNorm/beta not found in checkpoint
    bert4keras版本出错

  5. 如何用gpu去跑数据
    安装tensorflow-gpu并且注意与cuda的版本问题

  6. tensorflow.python.framework.errors_impl.ResourceExhaustedError: 2 root error(s) found.
    找到两个root错误

     cuda+cuDNN+TensorFlow版本不一致导致
     显存分配问题,更改为动态分配内存就可以解决。
     ensorflow或者numpy版本引起-----tensorflow是1.14.0版本。应该使用pip uninstall numpy卸载所有的numpy,再安装1.16.4即可
     (以上方法都没用)
     后来发现漏了上一行报错tensorflow.python.framework.errors_impl.ResourceExhaustedError: OOM when allocating tensor with shape[32,256,55,55]”
     ResourceExhaustedError:资源耗尽错误
     减少批处理Batch 的大小32---->16
    
  7. FileNotFoundError: [Errno 2] Unable to create file (unable to open file: name = ‘/root/autodl-tmp/chip2020_relation_extraction-gplinker/weights/gplinker_roformer_v2_best.h5’, errno = 2, error message = ‘No such file or directory’, flags = 13, o_flags = 242)

    报错的中文解释是:没有这样的文件和目录
    也就是说写的不应该是文件,而是文件的路径或者说是目录,否则系统根本找不到所指的东西。

    问题解决
    明确写好文件对应的位置
    (其实后来发现是,给的源文件中少了weights的文件夹,添加后)

  8. AttributeError: ‘str’ object has no attribute ‘decode’
    解决
    一般是因为str的类型本身不是bytes,所以不能解码
    问题原因:可能是h5py模块的版本过高,导致无法加载h5文件------------pip install h5py==2.10.0
    有时候有环境配置里很复杂,
    用pip install ‘h5py<3.0.0’ -i https://pypi.tuna.tsinghua.edu.cn/simple好一点

  9. Linux实时将所有输出重定向到文件

     nohup unbuffer command > file.out 2>&1 &
     unbuffer命令需要额外安装expect-devel,用来实时刷新。
     nohup命令用来忽略所有挂断(SIGHUP)信号,让你的程序即使在用户注销后依然继续运行。
     command是任何一段你想要执行的shell命令。
     > file.out 代表将command运行结果重定向到当前目录下的file.out文件中(如果要每次运行的结果追加到file.out后面,可以用>>而不是>)。
     2 >&1表示将标准错误输出cerr的所有输出也都重定向到标准输出cout中,这样file.out中就会记录command命令运行过程中所有标准输出。
     最后一个&表示后台运行该command。 
    

10.File “/root/miniconda3/envs/simbert/lib/python3.7/site-packages/tensorflow/python/client/session.py”, line 1458, in call
run_metadata_ptr)
tensorflow.python.framework.errors_impl.InternalError: 2 root error(s) found.
(0) Internal: Blas xGEMMBatched launch failed : a.shape=[12,234,64], b.shape=[12,64,234], m=234, n=234, k=64, batch_size=12
[[{{node Transformer-0-MultiHeadSelfAttention_1/einsum/MatMul}}]]
[[global_pointer_5_1/truediv/_1251]]
(1) Internal: Blas xGEMMBatched launch failed : a.shape=[12,234,64], b.shape=[12,64,234], m=234, n=234, k=64, batch_size=12
[[{{node Transformer-0-MultiHeadSelfAttention_1/einsum/MatMul}}]]
解决
很玄学,相同代码之前没问题,后来训练了一次mt5后出现问题,以为是内存爆炸,然后使用A500GPU还是不行,(A500不行怀疑是算力不匹配),然后换回最开始的代码在2080ti上跑行了

你可能感兴趣的:(自然语言处理,人工智能,python)