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。
- 从上面的数据中我们可以发现数据是清洗后的json文件,其中包含对应的需要的文本,标注的三元组信息,三元组按照主体-》关系-》客体去标注的。其中整理后的关系种类也是一个json文件,因为在实体关系抽取中我们目标是计划在文本句子中抽取出一个或n个三元组。较于传统正则去清洗三元组不同,使用模型去实现需要事先定义好这些信息,具体数据格式如下:
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问答。