中文医疗问答系统项目

项目概述

该项目基于开源医疗数据,实现对疾病的相关知识查询,主要包含四部分内容:
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
两 OOOOOOO

使用的是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
  1. 然后规定自己想要识别的问句意图类型,借助大模型构建意图相关的问句,我这里构建了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
numpy
1.22.4
pandas1.5.1
py2neo
2021.2.4
scikit_learn1.2.0
torch
1.10.0
torch1.12.0
torchtext
0.11.0
tqdm==4.64.1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值