阿里云医疗实体关系抽取大赛

1、本项目是基于阿里云比赛开放的医疗数据集去做的实体关系抽取。下面会从数据的详情,模型的选取,模型的训练,模型的验证和模型的预测去讲述。

2、数据准备阶段

  • 1、数据来源是阿里云医疗大赛,选取的是其中一个子任务,医疗实体关系抽取,数据如下:
    在这里插入图片描述
    在这里插入图片描述
  • 2、数据分析
    • 从上面的数据中我们可以发现数据是清洗后的json文件,其中包含对应的需要的文本,标注的三元组信息,三元组按照主体-》关系-》客体去标注的。其中整理后的关系种类也是一个json文件,因为在实体关系抽取中我们目标是计划在文本句子中抽取出一个或n个三元组。较于传统正则去清洗三元组不同,使用模型去实现需要事先定义好这些信息,具体数据格式如下:
      在这里插入图片描述

    3、模型选择

    • 针对Bert在文本上下文语义中的很好效果,采用半指针半标注结构,去实现,通过先把subject传到模型中,将输出去预测出object和predicate,模型的结构如下:
      在这里插入图片描述
    • 模型设计过程如下:基于“半指针-半标注”的方式来做抽取,顺序是先抽取s,然后传入s来抽取o、p,不同的只是将模型的整体架构换成了bert:
    • 1、原始序列转id后,传入bert的编码器,得到编码序列;
    • 2、编码序列接两个二分类器,预测s;
    • 3、根据传入的s,从编码序列中抽取出s的首和尾对应的编码向量;
    • 4、以s的编码向量作为条件,对编码序列做一次条件Layer Norm;
    • 5、条件Layer Norm后的序列来预测该s对应的o、p。

4、模型训练

  • 1、模型的核心代码部分:
    • 1.1模型代码如下:
在这里插入代# 补充输入
subject_labels = Input(shape=(None, 2), name='Subject-Labels')
subject_ids = Input(shape=(2,), name='Subject-Ids')
object_labels = Input(shape=(None, len(predicate2id), 2), name='Object-Labels')

# 加载预训练模型
bert = build_transformer_model(
    config_path=config_path,
    checkpoint_path=checkpoint_path,
    return_keras_model=False,
)

# 预测subject
output = Dense(
    units=2, activation='sigmoid', kernel_initializer=bert.initializer
)(bert.model.output)
subject_preds = Lambda(lambda x: x ** 2)(output)

subject_model = Model(bert.model.inputs, subject_preds)


output = bert.model.layers[-2].get_output_at(-1)
subject = Lambda(extract_subject)([output, subject_ids])
output = LayerNormalization(conditional=True)([output, subject])
output = Dense(
    units=len(predicate2id) * 2,
    activation='sigmoid',
    kernel_initializer=bert.initializer
)(output)
output = Lambda(lambda x: x ** 4)(output)
object_preds = Reshape((-1, len(predicate2id), 2))(output)

object_model = Model(bert.model.inputs + [subject_ids], object_preds)码片
  • 1.2定义损失函数:

class TotalLoss(Loss):
    """subject_loss与object_loss之和,都是二分类交叉熵
    """

    def compute_loss(self, inputs, mask=None):
        subject_labels, object_labels = inputs[:2]
        subject_preds, object_preds, _ = inputs[2:]
        if mask[4] is None:
            mask = 1.0
        else:
            mask = K.cast(mask[4], K.floatx())
        # sujuect部分loss
        subject_loss = K.binary_crossentropy(subject_labels, subject_preds)
        subject_loss = K.mean(subject_loss, 2)
        subject_loss = K.sum(subject_loss * mask) / K.sum(mask)
        # object部分loss
        object_loss = K.binary_crossentropy(object_labels, object_preds)
        object_loss = K.sum(K.mean(object_loss, 3), 2)
        object_loss = K.sum(object_loss * mask) / K.sum(mask)
        # 总的loss
        return subject_loss + object_loss
  • 1.2数据加载:
def load_data(filename):
    D = []
    with open(filename, encoding='utf-8') as f:
        for l in f:
            l = json.loads(l)
            D.append({
                'text': l['text'],
                'spo_list': [(spo['subject'], spo['predicate'], spo['object']['@value'])
                             for spo in l['spo_list']]
            })
    return D
  • 1.3关系语料读取:
with open('CMeIE/53_schemas.json', encoding="utf-8") as f:
    for l in f:
        l = json.loads(l)
        if l['predicate'] not in predicate2id:
            id2predicate[len(predicate2id)] = l['predicate']
            predicate2id[l['predicate']] = len(predicate2id)
print(predicate2id)
  • 1.4输入text进行三元组抽取:
def extract_spoes(text):
    """抽取输入text所包含的三元组
    """
    tokens = tokenizer.tokenize(text, maxlen=maxlen)
    mapping = tokenizer.rematch(text, tokens)
    token_ids, segment_ids = tokenizer.encode(text, maxlen=maxlen)
    token_ids, segment_ids = to_array([token_ids], [segment_ids])
    # 抽取subject
    subject_preds = subject_model.predict([token_ids, segment_ids])
    start = np.where(subject_preds[0, :, 0] > 0.6)[0]
    end = np.where(subject_preds[0, :, 1] > 0.5)[0]
    subjects = []
    for i in start:
        j = end[end >= i]
        if len(j) > 0:
            j = j[0]
            subjects.append((i, j))
    if subjects:
        spoes = []
        token_ids = np.repeat(token_ids, len(subjects), 0)
        segment_ids = np.repeat(segment_ids, len(subjects), 0)
        subjects = np.array(subjects)
        # 传入subject,抽取object和predicate
        object_preds = object_model.predict([token_ids, segment_ids, subjects])
        for subject, object_pred in zip(subjects, object_preds):
            start = np.where(object_pred[:, :, 0] > 0.6)
            end = np.where(object_pred[:, :, 1] > 0.5)
            for _start, predicate1 in zip(*start):
                for _end, predicate2 in zip(*end):
                    if _start <= _end and predicate1 == predicate2:
                        spoes.append(
                            ((mapping[subject[0]][0],
                              mapping[subject[1]][-1]), predicate1,
                             (mapping[_start][0], mapping[_end][-1]))
                        )
                        break
        return [(text[s[0]:s[1] + 1], id2predicate[p], text[o[0]:o[1] + 1])
                for s, p, o, in spoes]
    else:
        return []


  • 1.5模型的验证与指标信息保存:
def evaluate(data):
    """评估函数,计算f1、precision、recall
    """
    X, Y, Z = 1e-10, 1e-10, 1e-10
    f = open('dev_pred.json', 'w', encoding='utf-8')
    w2=open('final_result.txt','a+',encoding='utf-8')
    pbar = tqdm()
    for d in data:
        R = set([SPO(spo) for spo in extract_spoes(d['text'])])
        T = set([SPO(spo) for spo in d['spo_list']])
        X += len(R & T)
        Y += len(R)
        Z += len(T)
        f1, precision, recall = 2 * X / (Y + Z), X / Y, X / Z
        pbar.update()
        pbar.set_description(
            'f1: %.5f, precision: %.5f, recall: %.5f' % (f1, precision, recall)
        )
        s = json.dumps({
            'text': d['text'],
            'spo_list': list(T),
            'spo_list_pred': list(R),
            'new': list(R - T),
            'lack': list(T - R),
        },
            ensure_ascii=False,
            indent=4)
        f.write(s + '\n')
    pbar.close()
    f.close()
    w2.write('F1:'+str(f1)+';'+'Precision:'+str(precision)+';'+'Recall:'+str(recall)+'\n')
    return f1, precision, recall

2、模型训练过程:

  • 实验环境介绍:采用的是tensorflow,keras,12g显存上训练一个星期左右,时间问题,只跑了50个epoch,尝试在自己的1650TI上训练实在是太慢了,不动的,建议服务器去跑,参数量是几个亿左右。迭代次数在1000步以上效果会更好。
  • 训练过程截图如下:
    在这里插入图片描述
    在这里插入图片描述
    3、模型预测:
  • 根据训练好的权重,使用load_wights去加载模型,再去输入文本去预测
    权重已经训练好,需要的可以私信我发给大家。需要数据集的可以邮我:sessioncookies@163.com。
  • 模型的预测结果如下:
    在这里插入图片描述
    在这里插入图片描述
  • 模型的验证结果保存到json如下:
    在这里插入图片描述

5、总结:

根据上述验证结果和预测结果我们发现bert实现实体关系抽取效果很好,最高是可以达到84%的F1值,在足够多的epoch下。欢迎三连啦。关注一波,后期各种干货,下一期将讲解如何利用这些抽取的三元组去使用neo4j通过脚本生成知识图谱,最终完成KBQA问答。

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值