项目概述
该项目基于开源医疗数据,实现对疾病的相关知识查询,主要包含四部分内容:
1.基于医疗数据构建知识图谱
2.基于BILSTM-CRF模型实现医疗命名实体识别
3.基于textCNN模型实现医疗问句意图识别
4.构建槽位,基于2,3识别的结果实现cypher语句查询,支持多轮对话
项目代码结构:
参考链接:
模型:
https://www.bilibili.com/video/BV1ev4y1o7zj/?spm_id_from=333.999.0.0&vd_source=b2efa4eda8beee73e0aca2b5ef1909bc
https://blog.csdn.net/Tink1995/article/details/104708678
构建知识图谱:https://www.bilibili.com/video/BV11c411g7Sr/?spm_id_from=333.337.search-card.all.click&vd_source=b2efa4eda8beee73e0aca2b5ef1909bc
数据集链接
通过网盘分享的文件:数据
链接: https://pan.baidu.com/s/1_O-AB46RKuxyFrX8tqphKQ?pwd=fmi3 提取码: fmi3
话不多说,先看效果:
一些简单问题是能回答的,但是还有很多缺陷,主要是模型的精度,这与数据集也有一点的关系,目前只做到了这种程度。下面是正文:
1.构建知识图谱–build_medical_graph
定义三元组,三元组即实体-关系-实体的形式。
在该项目中定义了8个实体:药物、菜谱、食物、检查、科室、药企、疾病、症状
11中关系:
- | - |
---|---|
疾病 | 忌吃食物 |
疾病 | 宜吃食物 |
疾病 | 推荐吃食物 |
疾病 | 通用药品 |
疾病 | 热门药品 |
疾病 | 检查 |
疾病 | 药物 |
疾病 | 症状 |
疾病 | 并发症 |
疾病 | 科室 |
科室 | 科室 |
根据实体和关系就能构建出三元组数据,简单介绍一下程序:
首先创建MedicalExtractor类,初始化neo4j数据库、实体列表以及关系列表
然后,从数据集medical.json中读取数据,经过处理后转化为三元组的形式。原始数据为json文件,读出之后提取相应的key和value即可。下图函数包含处理过程,太长就不放出来了。
然后就是往数据库中写入节点和关系。
数据写入时间根据个人电脑配置,花费的时间不同,需要等上一段时间,顺利的话neo4j中会看到如下的情况,这个样子基本上就是导入完成了:
主函数运行:
2.命名实体识别-build_NER_model
命名实体识别的目的是为了获取问句中的关键词,比如什么是心脏病,那么心脏病就是我们需要获取的疾病实体,借助已经标注的数据可以进行训练来识别。一般情况下,如果需要根据自己的知识库以及具体业务需求来构建数据集,需要自己手动进行标注,实体标注软件有很多,我自己用过label studio这个标注工具,网上有很多安装教程,需要的话可以了解一下。这里因为我们用的开源数据,所以不需要标注阶段。
文件框架如下:
BiLSTM_CRF.py:模型结构代码
biLSTMCRF_data_process.py:数据预处理代码
biLSTMCRF_model_train.py:模型训练代码
model_predict.py:预测代码
NER_model_config.py:配置相关
cache:包括训练好的模型、哈工大停用词、词表、标签表
data:包括测试集、训练集、验证集
NER使用的是开源的数据,在网上可以找的,数据格式如下:
便 B_disease
秘 I_disease
两 O
个 O
多 O
月 O
不 O
清 O
楚 O
使用的是BIO标注,每个字都有对应的标签,需要我们自己处理成模型能够读取的形式。
基本模型和训练过程我是参考这位大佬的代码,链接如下:
逐行讲解BiLSTM+CRF实现命名实体识别(NER)
写的很好的一篇文章,大家可以去看看~,我在这里就不细说,主要说一下数据处理的过程。
首先是建立词表和标签表,因为原始数据是文本,我们需要将其转化为模型可以识别的数字才行,
def get_vocab_label():
# path = './data/train.txt'
# 加载数据以及保存词表、标签表的路径
vocab_path = './cache/vocab.pkl'
label_map_path = './cache/label_map.pkl'
stopwords_path = './cache/hit_stopwords.txt'
# 判断有没有建立过词表,省的重复建立浪费时间,保存为json格式
if os.path.exists(label_map_path):
with open(label_map_path, 'r', encoding='utf-8') as file:
label_map = json.load(file)
if os.path.exists(vocab_path):
with open(vocab_path, 'rb') as fp:
vocab = pickle.load(fp)
else:
# 数据集路径
train_path = './data/train.txt'
test_path = './data/test.txt'
dev_path = './data/dev.txt'
# 加载数据
lines = []
with open(train_path, 'r', encoding='utf-8') as file:
lines.extend(file.readlines())
with open(test_path, 'r', encoding='utf-8') as file:
lines.extend(file.readlines())
with open(dev_path, 'r', encoding='utf-8') as file:
lines.extend(file.readlines())
# 加载数据集
stopwords = open(stopwords_path, encoding='utf-8').read().split('\n')
# PAD:在一个batch中不同长度的序列用该字符补齐,padding
# UNK:当验证集活测试机出现词表以外的词时,用该字符代替。unknown
vocab = {'PAD': 0, 'UNK': 1}
label_map = {}
# 将字符存入词表
for i in range(len(lines)):
content = lines[i].split(' ')
try:
word = content[0]
if word in stopwords: # 如果该词为停用词,跳过
continue
# 去停用词属于对数据进行清洗,除去我们不需要的噪声词
if word not in vocab: # 如果不是停用词存入到字典中,value为当前字典长度,这样每个字符都有独立的编号
vocab[word] = len(vocab)
# 同样的道理,建立标签索引
label = content[1]
if label.startswith(('B_','I_')):
label = label.split('\n')
try:
if label_map[label[0]]:
continue
except KeyError:
label_map[label[0]] = len(label_map)
except:
continue
label_map['O'] = len(label_map)
# 对于BiLSTM+CRF网络,需要增加开始和结束标签,以增强其标签约束能力
START_TAG = "<START>"
STOP_TAG = "<STOP>"
label_map[START_TAG] = len(label_map)
label_map[STOP_TAG] = len(label_map)
# 保存
with open(label_map_path, 'w', encoding='utf-8') as fp:
json.dump(label_map, fp, indent=4)
with open(vocab_path, 'wb') as fp:
pickle.dump(vocab, fp)
vocab_inv = {v: k for k, v in vocab.items()}
label_map_inv = {v: k for k, v in label_map.items()}
return vocab, vocab_inv, label_map,label_map_inv
定义dataset类,这里就是与数据导入模型时的前处理相关
class Mydataset(data.Dataset):
def __init__(self,path,batch_size):
self.path = path
# 获取带BIO标注的数据
self.data = get_text_label(path)
# 获取词表
self.vocab,self.vocab_inv,self.label_map,self.label_map_inv = get_vocab_label()
self.batch_size = batch_size
self.get_points()
# getitem和len是dataset类中的方法,在处理自定义数据集时需要重写该方法
def __getitem__(self, item):
# 将data中的中文和label中的英文根据中文词表以及实体标签索引变为数字索引
text = self.data[0][self.points[item]:self.points[item+1]]
label = self.data[1][self.points[item]:self.points[item+1]]
label = [s.replace("\n", "") for s in label] # 删除换行符\n
t = [self.vocab.get(t, self.vocab['UNK']) for t in text]
l = [self.label_map[l] for l in label]
return t,l
def __len__(self):
return len(self.points) - 1
# 文本长度填充
def collect_fn(self,batch):
# 文本长度小于最大文本长度的需要进行填充(还可以自己规定长度,短的填充、长的阶段,这里偷个懒)
_text = []
_label = []
seq_len = []
for _t,_l in batch:
_text.append(_t)
_label.append(_l)
seq_len.append(len(_t))
# 最大长度作为填充的标准
max_len = max(seq_len)
# 转化为tensor张量
text = torch.tensor([t + [self.vocab['PAD']] * (max_len - len(t)) for t in _text],dtype=torch.long)
label = torch.tensor([l + [self.label_map['O']] * (max_len - len(l)) for l in _label],dtype=torch.long)
seq_len = torch.tensor(seq_len,dtype=torch.long)
return text,label,seq_len
def get_text_label(path):
text_label_path = path
stopwords_path = './cache/hit_stopwords.txt'
stopwords = open(stopwords_path, encoding='utf-8').read().split('\n')
texts = []
label = []
with open(text_label_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
for line in lines:
content = line.split(' ')
if content[0] == '\n':
continue
if content[0] in stopwords:
continue
else:
texts.append(content[0])
label.append(content[1])
return texts,label
预处理的主体部分就包括这两段程序,剩余训练和预测都比较常规,可以参考上面的链接,最大的问题时该数据集实体标签存在严重的不平衡问题,这也导致了最终模型精度也一般,大家如果有什么提升的方法可以尝试一下。
最终结果:
从结果就可以看出有的标签预测的F1比较高,有的比较低,如果解决了数据不平衡问题应该能提升许多。
3.意图识别
意图识别的目的识别出用户问句的意图是什么,如什么是心脏病,那意图就是问的心脏病的定义,再比如心脏病怎么治疗,那问的就是治疗方法,这就是意图识别的目的。所以我们需要先规定我们的模型能够识别哪些意图,然后再构建我们的数据集。起初我用了开源的数据集(天池发布的中文医疗识别竞赛数据集)对比了textCNN\fasttest\NB三个模型,结果模型精度都只有50%左右,效果比较差,因为该数据集噪声比较大,并且数据极度不平衡,难度比较大,下面是其中一段数据集:
text,label
丰胸的方法有哪些,治疗方案
还差一点就进子宫了,其他
散光能好吗,疾病表述
我的脂肪瘤是否存在恶变的倾向?,疾病表述
白癜风不断恶化的原因,病因分析
吃了紧急避孕药需要注意休息吗,因为工作缘故,需要上夜班,会有影响吗,注意事项
25岁已经感觉脸部松弛了怎么办,治疗方案
小孩的眉毛剪了会长吗?,其他
172的身高还能长高吗?,其他
冻疮用三金冻疮酊有效果么?,功效作用
可以看出数据比较杂散,想挑战一下的可以去官方细看一下数据集的相关描述,这里我就不再介绍了。
既然开源数据比较难,那么我们就需要自己构建数据集了,现在GPT、文心一言、通义千问等大模型热度正高,我们可以借助大模型帮我们构建数据集。我构建数据集的思路如下:
1.首先,提取出上述从命名实体识别章节数据集中提取出disease实体,即build_medical_graph/data/temp/entity_extract.csv文件,内容如下:
entity,label
便秘,disease
前列腺癌,disease
孩子,crowd
痣,disease
iga肾病,disease
附睾炎,disease
肺癌,disease
水痘,disease
- 然后规定自己想要识别的问句意图类型,借助大模型构建意图相关的问句,我这里构建了12个意图。如下图所示:
上面就是构建的问句文本类型,具体内容如下:
[disease]的定义是什么?-定义
[disease]怎么解释?-定义
啥病叫[disease]?-定义
[disease]是啥病?-定义
解释一下[disease]这个病。-定义
[disease]的定义能告诉我吗?-定义
啥是[disease]的定义?-定义
[disease]的定义是啥样的?-定义
给我[disease]的定义。-定义
解释一下[disease]这个术语。-定义
能否详细阐述一下[disease]的定义?-定义
[disease]的具体定义和特征是什么?-定义
我们只需要根据第一步的疾病名称填充到问句模板中即可,最终得到的问句数据集在all_questions.csv文件中,内容如下:
text,label
便秘应该怎么预防?, 预防措施
便秘要怎么避免才好?, 预防措施
怎么做才能不被便秘缠上?, 预防措施
便秘来了,我该怎么做才能防住它?, 预防措施
有什么办法能防止便秘?, 预防措施
便秘高发期,我要怎么保护自己?, 预防措施
为了不被便秘感染,我该怎么做?, 预防措施
打疫苗能预防便秘吗?, 预防措施
在便秘多发的时候,有什么好办法防住它?, 预防措施
在构造了数据集之后,我们只需要将数据集转化我们需要的形式,然后导入到模型中进行训练即可,NLU模型的文件结构如下:
cahce:主要保存搜狗的预训练词向量、词表和标签表、停用词以及训练的模型
data:dev和train是开源数据、self_train和self_test是自己构建的数据集,借助train_test_split模块进行划分。
NLU_model_config:配置相关
predict:预测
pocess:数据前处理,将数据转化为模型能读取的形式
textCNN_model:textCNN模型代码
预处理阶段代码如下:
class DataProcess:
def __init__(self,batch_size):
# 停用词
stopwords_path = './cache/hit_stopwords.txt'
self.stopwords = open(stopwords_path,encoding='utf-8').read().split('\n')
print('-' * 10 + '停用词加载完成' + '-' * 10)
# 加载预训练词向量
self.pre_trained_name = 'sgns.sogou.word'
self.pre_trained_path = '.\\cache'
self.vectors = torchtext.vocab.Vectors(name=self.pre_trained_name, cache=self.pre_trained_path)
print('-' * 10 + '预训练词向量加载完成' + '-' * 10)
# 定义Field
self.TEXT = torchtext.legacy.data.Field(sequential=True, lower=True, tokenize=self.cut,init_token='<sos>', eos_token='<eos>', pad_token='<pad>', unk_token='<unk>')
self.LABEL = torchtext.legacy.data.LabelField(sequential=False,dtype=torch.long)
# 加载数据
# start_time = time.time()
fields = [('text', self.TEXT),('label', self.LABEL)]
self.train_dataset, self.val_dataset = torchtext.legacy.data.TabularDataset.splits(
path='.\data',
format='csv',
skip_header=True, # 是否跳过表头
train='self_train.csv',
validation='self_test.csv',
fields=fields, # 定义数据对应的表头
)
# end_time = time.time()
print('-' * 10 + '数据加载完成' + '-' * 10)
# print('数据加载所用时间%s' % (end_time - start_time))
# 创建词表
if os.path.exists(',/cache//text_vocab.pt'):
self.TEXT.vocab = torch.load('./cache/text_vocab.pt')
self.LABEL.vocab = torch.load('./cache/label_vocab.pt')
print('-' * 10 + '词表加载完成' + '-' * 10)
else:
self.TEXT.build_vocab(self.train_dataset, self.val_dataset, vectors=self.vectors, min_freq=2)
self.LABEL.build_vocab(self.train_dataset, self.val_dataset)
print('-' * 10 + '词表创建完成' + '-' * 10)
torch.save(self.TEXT.vocab, './cache/text_vocab.pt')
torch.save(self.LABEL.vocab, './cache/label_vocab.pt')
# 重新创建迭代器
self.train_iter, self.val_iter = torchtext.legacy.data.BucketIterator.splits(
(self.train_dataset, self.val_dataset),
batch_sizes=(batch_size, batch_size, batch_size),
sort_key=lambda x: len(x.text)
)
def cut(self, sentence):
return [token for token in jieba.lcut(sentence) if token not in self.stopwords]
这里用了torchtext模块来处理问句数据集,这里一定要注意torchtext版本的安装以及pytorch版本,最好是安装对应版本,后面我会列出我的各种库版本。
textCNN_model代码如下:
def save(model, save_dir, steps):
if not os.path.isdir(save_dir):
os.makedirs(save_dir)
save_path = 'bestmodel_steps{}.pt'.format(steps)
save_bestmodel_path = os.path.join(save_dir, save_path)
torch.save(model.state_dict(), save_bestmodel_path)
def dev_eval(dev_iter, model):
model.eval()
corrects, avg_loss = 0, 0
all_pre = []
all_labels = []
with torch.no_grad():
for batch in dev_iter:
feature, target = batch.text, batch.label
if torch.cuda.is_available():
feature, target = feature.cuda(), target.cuda()
logits = model(feature)
loss = F.cross_entropy(logits, target)
avg_loss += loss.item()
pre = torch.max(logits, 1)[1].view(target.size()).data
corrects += (pre == target.data).sum()
all_pre.extend(pre.cpu().numpy())
all_labels.extend(target.data.cpu().numpy())
size = len(dev_iter.dataset)
avg_loss /= size
accuracy = 100.0 * corrects / size
print('\nEvaluation - loss: {:.6f} acc: {:.4f}%({}/{}) \n'.format(avg_loss,
accuracy,
corrects,
size))
# 打印分类报告
print("Classification Report:")
print(classification_report(all_labels, all_pre, digits=3))
return accuracy
def train(train_iter, dev_iter, model):
if torch.cuda.is_available(): # 判断是否有GPU,如果有把模型放在GPU上训练,速度质的飞跃
model.cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) # 梯度下降优化器,采用Adam
steps = 0
best_acc = 0
last_step = 0
model.train()
for epoch in range(1, epochs + 1):
print(f'---------------------eopch:{epoch}-----------------------------')
for batch in tqdm(train_iter):
feature, target = batch.text, batch.label
# print(feature, target)
# exit()
if torch.cuda.is_available(): # 如果有GPU将特征更新放在GPU上
feature, target = feature.cuda(), target.cuda()
optimizer.zero_grad() # 将梯度初始化为0,每个batch都是独立训练地,因为每训练一个batch都需要将梯度归零
logits = model(feature)
loss = F.cross_entropy(logits, target) # 计算损失函数 采用交叉熵损失函数
loss.backward() # 反向传播
optimizer.step() # 放在loss.backward()后进行参数的更新
optimizer.zero_grad()
steps += 1
dev_acc = dev_eval(dev_iter, model)
if dev_acc > best_acc:
best_acc = dev_acc
# last_step = steps
print('Saving best model, acc: {:.4f}%\n'.format(best_acc))
torch.save(model.state_dict(), save_dir)
# else:
# if steps - last_step >= early_stopping:
# print('\n提前停止于 {} steps, acc: {:.4f}%'.format(last_step, best_acc))
# raise KeyboardInterrupt
class TextCNN(nn.Module):
def __init__(self,
class_num, # 最后输出的种类数
filter_sizes, # 卷积核的长也就是滑动窗口的长
filter_num, # 卷积核的数量
vocabulary_size, # 词表的大小
embedding_dimension, # 词向量的维度
vectors, # 词向量
dropout): # dropout率
super(TextCNN, self).__init__() # 继承nn.Module
chanel_num = 1 # 通道数,也就是一篇文章一个样本只相当于一个feature map
self.embedding = nn.Embedding(vocabulary_size, embedding_dimension) # 嵌入层
self.embedding = self.embedding.from_pretrained(vectors) # 嵌入层加载预训练词向量
self.convs = nn.ModuleList(
[nn.Conv2d(chanel_num, filter_num, (fsz, embedding_dimension)) for fsz in filter_sizes]) # 卷积层
self.dropout = nn.Dropout(dropout) # dropout
self.fc = nn.Linear(len(filter_sizes) * filter_num, class_num) # 全连接层
def forward(self, x):
# x维度[句子长度,一个batch中所包含的样本数] 例:[3451,128]
x = self.embedding(x) # 经过嵌入层之后x的维度,[一个batch中所包含的样本数,句子长度,词向量维度] 例:[128,3451,300]
x = x.permute(1, 0, 2) # permute函数将样本数和句子长度换一下位置,[一个batch中所包含的样本数,句子长度,词向量维度] 例:[128,3451,300]
x = x.unsqueeze(1) # # conv2d需要输入的是一个四维数据,所以新增一维feature map数 unsqueeze(1)表示在第一维处新增一维,[一个batch中所包含的样本数,一个样本中的feature map数,句子长度,词向量维度] 例:[128,1,3451,300]
# 添加填充
max_filter_size = max(self.convs, key=lambda conv: conv.kernel_size[0]).kernel_size[0]
pad_size = max_filter_size - 1
x = F.pad(x, (0, 0, pad_size, pad_size), mode='constant', value=0)
x = [conv(x) for conv in self.convs] # 与卷积核进行卷积
x = [sub_x.squeeze(3) for sub_x in x] # squeeze(3)判断第三维是否是1,如果是则压缩
x = [F.relu(sub_x) for sub_x in x] # ReLU激活函数激活
x = [F.max_pool1d(sub_x, sub_x.size(2)) for sub_x in x] # 池化层
x = [sub_x.squeeze(2) for sub_x in x] # 判断第二维是否为1,若是则压缩
x = torch.cat(x, 1) # 进行拼接
x = self.dropout(x) # 去除掉一些神经元防止过拟合
logits = self.fc(x) # 全接连层
return logits
if __name__ == "__main__":
dp = DataProcess(batch_size)
LABEL = dp.LABEL
TEXT = dp.TEXT
train_iter = dp.train_iter
dev_iter = dp.val_iter
class_num = len(LABEL.vocab) # 类别数目
vocab_size = len(TEXT.vocab) # 词表大小
embedding_dim = TEXT.vocab.vectors.size()[-1] # 词向量维度
vectors = TEXT.vocab.vectors # 词向量
textcnn_model = TextCNN(class_num=class_num,
filter_sizes=filter_size,
filter_num=filter_num,
vocabulary_size=vocab_size,
embedding_dimension=embedding_dim,
vectors=vectors,
dropout=dropout)
print(10 * '*' + '开始训练' + '*' * 10)
train(train_iter=train_iter, dev_iter=dev_iter, model=textcnn_model)
这里就是常规的训练阶段,没什么好细说的,注意输入数据的形式就好了。
4. 问答逻辑
在通过NER和NLU获取问句中的疾病实体和意图后,就需要对应相应的问答模板中,然后在数据库中搜索出我们需要的答案。
我把运行所需要的文件都放在了根目录下的cache文件中了,以便调用。
slot_config:槽位形式
slot_dict = {
"定义": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases) WHERE p.name='{Disease}' RETURN p.desc",
"reply_template": "'{Disease}' 是这样的:\n",
"ask_template": "您是想问 '{Disease}' 的定义吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"病因": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases) WHERE p.name='{Disease}' RETURN p.cause",
"reply_template": "'{Disease}' 疾病的原因是:\n",
"ask_template": "您是想问 '{Disease}' 的原因吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"预防措施": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases) WHERE p.name='{Disease}' RETURN p.prevent",
"reply_template": "关于 '{Disease}' 疾病您可以这样预防:\n",
"ask_template": "您是想问 '{Disease}' 的预防措施吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"临床表现": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases)-[r:has_symptom]->(q:symptoms) WHERE p.name='{Disease}' RETURN q.name",
"reply_template": "'{Disease}' 疾病的病症表现一般是这样的:\n",
"ask_template": "您是想问 '{Disease}' 的症状表现吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"相关病症": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases)-[r:acompany_with]->(q:diseases) WHERE p.name='{Disease}' RETURN q.name",
"reply_template": "'{Disease}' 疾病的具有以下并发疾病:\n",
"ask_template": "您是想问 '{Disease}' 的并发疾病吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"治疗方法": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": ["MATCH(p:diseases) WHERE p.name='{Disease}' RETURN p.cure_way",
"MATCH(p:diseases)-[r:recommand_drug]->(q) WHERE p.name='{Disease}' RETURN q.name",
"MATCH(p:diseases)-[r:recommand_recipes]->(q) WHERE p.name='{Disease}' RETURN q.name"],
"reply_template": "'{Disease}' 疾病的治疗方式、可用的药物、推荐菜肴有:\n",
"ask_template": "您是想问 '{Disease}' 的治疗方法吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"科室": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases)-[r:cure_department]->(q:departments) WHERE p.name='{Disease}' RETURN q.name",
"reply_template": "得了 '{Disease}' 可以挂这个科室哦:\n",
"ask_template": "您是想问 '{Disease}' 要挂什么科室吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"传染性": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases) WHERE p.name='{Disease}' RETURN p.easy_get",
"reply_template": "'{Disease}' 较为容易感染这些人群:\n",
"ask_template": "您是想问 '{Disease}' 会感染哪些人吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"治愈率": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases) WHERE p.name='{Disease}' RETURN p.cured_prob",
"reply_template": "得了'{Disease}' 的治愈率为:",
"ask_template": "您是想问 '{Disease}' 的治愈率吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"治疗时间": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases) WHERE p.name='{Disease}' RETURN p.cure_lasttime",
"reply_template": "疾病 '{Disease}' 的治疗周期为:",
"ask_template": "您是想问 '{Disease}' 的治疗周期吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"化验/体检方案": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases)-[r:need_check]->(q:checks) WHERE p.name='{Disease}' RETURN q.name",
"reply_template": "得了 '{Disease}' 需要做以下检查:\n",
"ask_template": "您是想问 '{Disease}' 要做什么检查吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
"禁忌": {
"slot_list": ["Disease"],
"slot_values": None,
"cql_template": "MATCH(p:diseases)-[r:not_eat]->(q:foods) WHERE p.name='{Disease}' RETURN q.name",
"reply_template": "得了 '{Disease}' 切记不要吃这些食物哦:\n",
"ask_template": "您是想问 '{Disease}' 不可以吃的食物是什么吗?",
"intent_strategy": "",
"deny_response": "很抱歉没有检索到相关的知识~"
},
}
这里也是参考了上述链接中的内容,大家可以根据自己的需要进行修改。
run.py是运行文件,包括了问答逻辑代码,以及词相似度匹配代码
graph = Graph('neo4j://localhost:7687', auth=("neo4j", "12345678"), name="neo4j")
unrecognized_replay = unrecognized['replay_answer']
def entity_similarity(entity_type, text):
cql = "MATCH (n:{entity_type}) RETURN n.name".format(entity_type=entity_type + 's')
entities = graph.run(cql).data()
max_sim_entity = ''
max_lev_distance = 0
for i, item in enumerate(entities):
# 计算Levenshtein距离
val = list(item.values())[0]
lev_distance = difflib.SequenceMatcher(None, text, val).ratio()
if lev_distance > 0.6 and lev_distance < 1:
if lev_distance > max_lev_distance:
print(val)
max_lev_distance = lev_distance
max_sim_entity = val
return max_sim_entity
def robot_answer(text, old_entity=None, old_intent=None):
"二次问话后的回答情景"
if text == "是的":
user_intent = old_intent
entity_info = old_entity
elif text == "不是":
return unrecognized_replay
else:
# 意图识别
user_intent = nlu_predict(text)
user_intent = user_intent.strip(' ')
# 实体识别
entity_info = ner_predict(text)
try:
slot_info = slot_dict[user_intent]
except KeyError: # 没有识别到意图
return unrecognized_replay
cql_template = slot_info.get('cql_template')
deny_response = slot_info.get('deny_response')
ask_template = slot_info.get('ask_template')
reply_template = slot_info.get('reply_template')
if not entity_info: # 实体不存在,但是有意图,可能是二次提问的情况
if user_intent:
entity_info = old_entity # 将上次提问的实体赋予本次问话中
# print(user_intent, entity_info)
if entity_info:
entity_type = entity_info[0][1]
if entity_type != 'disease': # 目前只能根据疾病来回答问题
return unrecognized_replay
entity = entity_info[0][0]
if isinstance(cql_template, list): # 多查询语句
res = []
for q in cql_template:
cql = q.format(Disease=entity)
data = graph.run(cql).data()
res.extend([list(item.values())[0] for item in data if list(item.values())[0] != None])
else: # 单查询语句
cql = cql_template.format(Disease=entity)
# print(cql)
data = graph.run(cql).data()
res = [list(item.values())[0] for item in data if list(item.values())[0] != None]
if not res: # 没有检索到答案
# 文本相似度匹配
sim_entity = entity_similarity(entity_type, entity)
# 二次确认相似实体
reply = ask_template.format(Disease=sim_entity)
entity_info[0][0] = sim_entity
return [reply, entity_info, user_intent]
else:
answer = "、".join([str(i) for i in res])
reply_template = reply_template.format(Disease=entity)
reply = reply_template + answer
return [reply, entity_info, user_intent]
else:
return unrecognized_replay
if __name__ == "__main__":
old_entity = ''
old_intent = ''
while True:
text = input('请输入问句:')
if old_entity:
reply = robot_answer(text, old_entity, old_intent)
else:
reply = robot_answer(text)
if isinstance(reply, list):
answer = reply[0]
old_entity = reply[1]
old_intent = reply[2]
else:
answer = reply
old_entity = ''
old_intent = ''
print(answer)
回答逻辑还存在许多漏洞,不建议大家直接使用,可以自己自行修改~
以上就是整个项目的思路介绍,其实问题还是存在不少的:
1.NER模型精度不够,会识别错,需要提高精度,大家可以尝试在数据预处理、数据不平衡问题处理、其他模型几个方向来尝试;
2. 问句数据集很理想,不够口语化,也不太符合实际情况,可以搜集较多的相关问句构建自己数据集;
3. 问答系统逻辑不够完善;
4. 系统只回答疾病实体相关的内容,无法根据现象描述来返回对应的疾病;
当然,主要是帮助大家整理怎么构建一个简单问答系统的思路,除了这种基于深度学习的问答系统。还有基于模板匹配的问答系统,这个大家可以根据自己的实际业务来选择合适的方法,我这里的内容仅供参考
github:
https://github.com/zhaosong123843/KQAS_medical/tree/main
附上环境依赖包:
jieba0.42.1
numpy1.22.4
pandas1.5.1
py2neo2021.2.4
scikit_learn1.2.0
torch1.10.0
torch1.12.0
torchtext0.11.0
tqdm==4.64.1