毕设记录(二)——医疗领域知识图谱实现智能问答与分析服务(后端)

三、后端接口部分

共定义了六个python文件,其中在运行过程中需要启动四个,分别是backdata、countnum、login、chat。config和modules文件是在chat文件中被调用的,因此无需启动。其中前三个启动文件都是和数据库交互的数据,是我在毕设当中自行拓展的内容,chat文件是本次毕设的主要实现功能,实现问答语句的返回,接下来会着重讲解这一部分的逻辑。

与数据库关联接口

以backdata为例,countnum和login写法均与前述一致,可查看源码。

后台管理界面的数据需要连接数据库进行操作,这里使用的是mysql数据库。以下代码展示了详细的配置格式,也是连接mysql数据库的必要格式,注意点已在代码中注释出来。

from flask_cors import CORS
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import text

app=Flask(__name__)
CORS(app)

HOSTNAME = "127.0.0.1"
PORT = 3306
USERNAME = "root"
PASSWORD = "自己的数据库密码"
DATABASE = "backplotform"#和在数据库中要使用的表的表名一致,否则无法正确连接

app.config['SQLALCHEMY_DATABASE_URI'] = f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

#自定义类名
class bakcdata(db.Model):
    #将数据库backplotform表中的变量按照以下格式写到此处
    id=db.Column(db.Integer,primary_key=True)
    request=db.Column(db.Text)
    answer=db.Column(db.Text)
    comment=db.Column(db.Text)
    satisfication=db.Column(db.Integer)
    time = db.Column(db.Text)
    username = db.Column(db.Text)
    
    def __repr__(self):
        return f"<bakcdata {self.id}>"

@app.route('/getrecord',methods=['GET'])
def getrecord():
    with app.app_context():
        records = bakcdata.query.all()
        records_dict = [{'id': record.id, 'request': record.request, 'answer': record.answer,
                     'time': record.time, 'comment': record.comment, 'satisfication': record.satisfication,
                     'username':record.username}
                    for record in records]
        return jsonify(records_dict)

if __name__ == '__main__':
    app.run(port=5001)

接下来解释编写一个接口所需要的详细步骤。

@app.route的括号内定义了两个参数,第一个参数是对前端暴露的接口名,前端部分在请求这个接口时就可以使用这个名称进行调用。第二个参数若为post,则需要接收前端传递过来的数据,并对后端的数据进行修改;若为get,则为直接获取后端的数据,无需任何修改。

def后的函数名建议和暴露的方法名一致,with也为固定写法语句。

中间一系列的写法已通过注释表示。

最后若是对数据库的表项进行删除操作,则在commit之前要使用delete,并传入对应要删除的表项;若要对数据库表增加表项,则需要在commit之前使用add,并传入相应表项。若不对数据库表进行相应的操作,则直接return返回数据给前端即可。

#此处接口的方法设置为post,因此前端传送的数据要对后端的数据库表中进行修改
#该接口实现的是删除功能

@app.route('/deleterecord',methods=['POST'])
def deleterecord():
    with app.app_context():
        # 为什么需要json,因为post请求的数据往往会在request里,
        # 若前端是一个json格式,则调用json,然后通过字典的形式获取相应的值
        # 如果为整型数据,则直接从data项中获取即可

        # 定义一个data,来接收前端传递进行的数据
        data = request.json

        #data数据中含有id,通过get方法来获取
        recordid = data.get('id')
        

        # 通过class名来获取对该表的对象,以此来获取表中的数据
        # 使用数据库查询语句对数据库进行查询
        record = bakcdata.query.get(recordid)
        
        db.session.delete(record)
        db.session.commit()
        
        return jsonify({'message':'删除成功'}),200

实现智能问答语句返回接口

chat为主函数,modules.py和config.py为辅函数

        在实现问答语句的返回的过程中,我定义了一个diseasename变量,用来接收实体识别出来的结果,同时用作对用户提问的疾病进行统计,以用作可视化的图标的数据支撑。

        首先用msg接收前端传递进来的数据sent(这是在前端定义好的变量名称)。下一步便是拿到msg中的问句,调用classifier对其进行意图分类。

#chat函数

@app.route('/index',methods=['GET'])
def index():
    diseasename = []
    msg = request.args.get('sent')
    user_intent = classifier(msg)
    if user_intent in ["greet","goodbye","deny","isbot"]:
        reply = gossip_robot(user_intent)
    elif user_intent == "accept":
        reply = load_user_dialogue_context('wjh')
        reply = reply.get("choice_answer")
    else:
        reply = medical_robot(msg , 'wjh')#reply就是槽位模板,里面有已经填充好的模板信息
        # 用作数据库统计疾病提问的数量
        diseasename = get_disease_name(msg,'wjh')
        if reply["slot_values"]:#如果存在新的疾病实体,那么就需要重新写入日志,用于下一轮对话
            dump_user_dialogue_context('wjh',reply)
        reply = reply.get("replay_answer")
    
    return jsonify({'reply': reply , 'diseasename':diseasename}),200
#moudles.py

def classifier(text):
    """
    判断是否是闲聊意图,以及是什么类型闲聊
    """
    
    """
    这里的clf_model是先前使用bert模型训练好的意图识别模型,
    使用predict方法可以获取预测到的置信度
    """
    return clf_model.predict(text)

        在进行意图识别的过程中,调用模型进行预测时会同步返回一个置信度,当意图置信度达到一定阈值时(>=0.8),可以查询该意图下的答案,也即会直接执行else部分;当意图置信度较低时(0.4~0.8),会进行一次意图增强(accept)的提问,若用户进行了肯定的回答,则置信度会被提升至0.8,按照识别的实体进行恢复,若用户拒绝了回答,则置信度会被修改至小于0.4的某一个值,按照拒绝回答的模板进行回复。

        意图的识别会分为两类,一类是闲聊意图,另一类是增强意图。若识别为闲聊意图:问好(greet)、再见(goodbye)、肯定(isbot)、拒绝(deny),则需要触发gossip_robot函数,使其调用config中配置好的回复模板。

         若意图识别为闲聊意图,则会调用以下代码按分类进行回复。

#modules.py

def gossip_robot(intent):
    return random.choice(
         gossip_corpus.get(intent)
    )


#config.py

gossip_corpus = {
    "greet":[
            "你好呀",
            "你好,我是智能医疗诊断机器人,有什么可以帮助你吗",
            "你好,你可以问我一些关于疾病诊断的问题哦"
        ],
    "goodbye":[
            "再见啦,很高兴为您服务,欢迎再次使用,祝您身体健康!"
        ],
    "deny":[
            "很抱歉没帮到您",
            "那您可以试着问我其他问题哟"
        ],
    "isbot":[
            "我是小智,你的智能健康顾问",
            "你可以叫我小智哦~",
            "我是医疗诊断机器人小智"
        ],
}

       在说明意图增强前,我们需要知道多轮对话是如何实现的。本项目定义了13种问题类型,包括定义、病因、预防措施、临床表现和相关病症等,每种提问类型都会对应一个语义槽,我们需要将识别出来的实体填入相应问题的语义槽当中。下面展示一个语义槽,其余问题类型都是相似的,只需要将识别出来的实体填入Disease中,这就要借助utils文件夹中的json_utils.py文件,其里面的dump_user_dialogue_context函数。

#config.py

semantic_slot = {
    "病因":{
        "slot_list" : ["Disease"],
        "slot_values":None,
        "cql_template" : "MATCH(p:疾病) WHERE p.name='{Disease}' RETURN p.cause",
        "reply_template" : "'{Disease}' 疾病的原因是:\n",
        "ask_template" : "您问的是疾病 '{Disease}' 的原因吗?",
        "intent_strategy" : "",
        "deny_response":"您说的我有点不明白,您可以换个问法问我哦~"
    },
}   

        以下为utils文件夹中json_utlis.py文件。

        第一个函数是对json文件的修改,也即将识别出来的的实体填写在json文件中。修改的json文件往往是当识别出了新的实体后需要调用这个函数。在chat函数中,若为问答意图,则需要调用dump_user_dialogue_context函数,将识别出来的实体的填入到相应问题的模板中。

        第二个函数是调用json文件,获取其填充好的语义槽,这往往用于意图增强时需要调用上一轮对话。

#实现多轮对话的关键
#将 Python 数据结构转换为 JSON 格式的字符串
def dump_user_dialogue_context(user,data):
    path = os.path.join(LOGS_DIR,'{}.json'.format(str(user)))#则是根据用户标识 user 构建的文件名(以用户标识为名的 JSON 文件)。
    with open(path,'w',encoding='utf8') as f:
        f.write(json.dumps(data, sort_keys=True, indent=4, 
                separators=(', ', ': '),ensure_ascii=False))

def load_user_dialogue_context(user):
    path = os.path.join(LOGS_DIR,'{}.json'.format(str(user)))
    if not os.path.exists(path):
        return {"choice_answer":"非常抱歉,我不理解你的意思。","slot_values":None}
    else:
        with open(path,'r',encoding='utf8') as f:
            data = f.read()
            return json.loads(data)

以下为wjh.json文件中的内容(存储上一轮对话的json文件),识别的插槽名为:“颅脑损伤”。

{
    "ask_template": "您问的是疾病 '{Disease}' 的治疗方法吗?", 
    "choice_answer": "'颅脑损伤' 的主要治疗药物有:\n甘油氯化钠注射液、胞磷胆碱钠氯化钠注射液", 
    "cql_template": "MATCH(p:疾病)-[r:recommand_drug]->(q) WHERE p.name='{Disease}' RETURN q.name", 
    "deny_response": "没有理解您说的意思哦~", 
    "intent_strategy": "clarify", 
    "replay_answer": "您问的是疾病 '颅脑损伤' 的治疗方法吗?", 
    "reply_template": "'{Disease}' 的主要治疗药物有:\n", 
    "slot_list": [
        "Disease"
    ], 
    "slot_values": {
        "Disease": "颅脑损伤"
    }
}

        当用户输入的问句省略了疾病主语时,如下图中“不能吃哪些食物?”,此时不能够直接分析出实体,那么此时意图识别为增强意图,则首先会调用utils文件夹中的json_utils.py 中的load_user_dialogue_context方法,调用上一轮对话中识别出来的语义槽。若无法加载到上一轮存储对话的文件,则返回无法回答的语句,也即上一轮未能识别出正确的实体。若能够找到,则读取data并返回,再获取其中的choice_answer返回最终的结果,也即后端会返回“您是想问‘高血压’不可以吃的食物是什么吗?”。返回问句中使用的“高血压”就是从json文件中调用的上一轮对话存储的结果。

        若意图识别为诊断意图,则调用封装好的medical_robot进行文本解析,并利用填充好的语义槽对问句进行组装然后返回最终回复答案。其中semantic_parser为文本的实体解析,根据其中的置信度返回相应的回复策略。而get_answer根据相应的意图,组装不同的cypher语句来查询返回最终的回复语句。

def medical_robot(text,user):
    """
    如果确定是诊断意图则使用该方法进行诊断问答
    """
    semantic_slot = semantic_parser(text,user)
    answer = get_answer(semantic_slot)
    return answer
def intent_classifier(text):
    #该方法内部会调用训练好的模型来解析文本的意图,获取相应的概率和名称
    result = bert_intent_recognize(text)
    if result != -1:
        return result['data']
    else:
        return -1

def slot_recognizer(text):
    #该方法可以获取识别出来的实体
    result = medical_ner(text)
    if result != -1:
        return result['data']
    else:
        return -1 

def semantic_parser(text,user):
    """
    对文本进行解析
    intent = {"name":str,"confidence":float}
    """
    intent_rst = intent_classifier(text)#会获取相应的名称和概率
    slot_rst = slot_recognizer(text)
    if intent_rst==-1 or slot_rst==-1 or intent_rst.get("name")=="其他":
        return semantic_slot.get("unrecognized")

    slot_info = semantic_slot.get(intent_rst.get("name"))

    # 填槽:实际上就是要去调用模型的一个过程,将从用户输入文本中提取的实体或信息填入预定义的槽位中
    slots = slot_info.get("slot_list")#从槽信息中获取槽列表,获取识别的实体疾病。

    slot_values = {}#创建一个空字典,用于存储槽的值
    for slot in slots:
        slot_values[slot] = None
        for ent_info in slot_rst:
            for e in ent_info["entities"]:
                if slot.lower() == e['type']:
                    slot_values[slot] = entity_link(e['word'],e['type'])

    last_slot_values = load_user_dialogue_context(user)["slot_values"]#将填充完的槽值存入槽信息中。
    
    
    for k in slot_values.keys():
        if slot_values[k] is None:
            slot_values[k] = last_slot_values.get(k,None)
        
    slot_info["slot_values"] = slot_values

    # 根据意图强度来确认回复策略,这里会选择相应的直接进行回复
    conf = intent_rst.get("confidence")
    if conf >= intent_threshold_config["accept"]:
        slot_info["intent_strategy"] = "accept"
    elif conf >= intent_threshold_config["deny"]:
        slot_info["intent_strategy"] = "clarify"
    else:
        slot_info["intent_strategy"] = "deny"

    return slot_info
def get_answer(slot_info):
    """
    根据语义槽获取答案回复
    """
    #获取相应的语义槽变量,具体的内容可以查看config中的语义槽
    cql_template = slot_info.get("cql_template")
    reply_template = slot_info.get("reply_template")
    ask_template = slot_info.get("ask_template")
    slot_values = slot_info.get("slot_values")
    strategy = slot_info.get("intent_strategy")

    if not slot_values:
        return slot_info

    #根据回复策略来组装cypher语句
    if strategy == "accept":
        cql = []
        if isinstance(cql_template,list):
            for cqlt in cql_template:
                cql.append(cqlt.format(**slot_values))
        else:
            cql = cql_template.format(**slot_values)
        answer = neo4j_searcher(cql)
        if not answer:
            slot_info["replay_answer"] = "抱歉,未找到相关结果,请重新提问!"
        else:
            pattern = reply_template.format(**slot_values)
            slot_info["replay_answer"] = pattern + answer
    elif strategy == "clarify":
        # 澄清用户是否问该问题
        pattern = ask_template.format(**slot_values)
        slot_info["replay_answer"] = pattern
        # 得到肯定意图之后需要给用户回复的答案
        cql = []
        if isinstance(cql_template,list):
            for cqlt in cql_template:
                cql.append(cqlt.format(**slot_values))
        else:
            cql = cql_template.format(**slot_values)
        answer = neo4j_searcher(cql)
        if not answer:
            slot_info["replay_answer"] = "抱歉,未找到相关结果,请重新提问!"
        else:
            pattern = reply_template.format(**slot_values)
            slot_info["choice_answer"] = pattern + answer
    elif strategy == "deny":
        slot_info["replay_answer"] = slot_info.get("deny_response")
    
    return slot_info

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值