当前位置:   article > 正文

自然语言处理-应用场景-问答系统(知识图谱)【离线:命名实体识别(BiLSTM+CRF>维特比算法预测)、命名实体审核(BERT+RNN);在线:句子相关性判断(BERT+DNN)】【Flask部署】_nlp 在线医生

nlp 在线医生

一、背景介绍

什么是智能对话系统?

随着人工智能技术的发展, 聊天机器人, 语音助手等应用在生活中随处可见, 比如百度的小度, 阿里的小蜜, 微软的小冰等等. 其目的在于通过人工智能技术让机器像人类一样能够进行智能回复, 解决现实中的各种问题.

在这里插入图片描述


从处理问题的角度来区分, 智能对话系统可分为:

  • 任务导向型: 完成具有明确指向性的任务, 比如预定酒店咨询, 在线问诊等等.
  • 非任务导向型: 没有明确目的, 比如算算术, 播放音乐, 回答问题.

我们的在线医生项目就是任务导向型的智能对话系统.


二、Unit对话API的使用

Unit平台是百度大脑开放的智能对话定制与服务平台, 也是当前最大的中文领域对话开放平台之一. Unit对注册用户提供免费的对话接口服务, 比如中文闲聊API, 百科问答API, 诗句生成API等, 通过这些API我们可以感受一下智能对话的魅力, 同时它也可以作为任务导向型对话系统无法匹配用户输入时的最终选择.

在这里插入图片描述


Unit闲聊API演示:

用户输入 >>> "你好"
Unit回复 >>> "你好,想聊什么呢~"
用户输入 >>> "我想有一个女朋友!"
Unit回复 >>> "我也是想要一个女朋友~"
用户输入 >>> "晚吃啥呢想想"
Unit回复 >>> "想吃火锅"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

调用Unit API的实现过程:

  • 第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人.
  • 第二步: 进行相关配置, 获得请求API接口需要的API Key与Secret Key.
  • 第三步: 在服务器上编写API调用脚本并进行测试.

第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人.

在这里插入图片描述


第二步: 进行相关配置, 获得请求API接口需要的API Key与Secret Key.

  • 点击获取API Key进入百度云应用管理页面.

在这里插入图片描述


  • 点击创建应用, 进入应用信息表单填写页面.

在这里插入图片描述


  • 填写完毕后, 点击立即创建, 成功后会提示创建完毕.

在这里插入图片描述


  • 点击返回应用列表.

在这里插入图片描述


  • 可以看到创建的API Key和Secret Key, 至此创建流程结束.

在这里插入图片描述


第三步: 在服务器上编写API调用脚本并进行测试

import json
import random
import requests

# client_id 为官网获取的AK, client_secret 为官网获取的SK
client_id = "1xhPonkmHqwolDt3GCICLX39"
client_secret = "SRYsfjMGNuW8G265paMXLEjDTjO6O4RC"


def unit_chat(chat_input, user_id="88888"):
    """
    description:调用百度UNIT接口,回复聊天内容
    Parameters
      ----------
      chat_input : str
          用户发送天内容
      user_id : str
          发起聊天用户ID,可任意定义
    Return
      ----------
      返回unit回复内容
    """
    # 设置默认回复内容,  一旦接口出现异常, 回复该内容
    chat_reply = "不好意思,俺们正在学习中,随后回复你。"
    # 根据 client_id 与 client_secret 获取access_token
    url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s" % (
    client_id, client_secret)
    res = requests.get(url)
    access_token = eval(res.text)["access_token"]
    # 根据 access_token 获取聊天机器人接口数据
    unit_chatbot_url = "https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token=" + access_token
    # 拼装聊天接口对应请求发送数据,主要是填充 query 值
    post_data = {
   
                "log_id": str(random.random()),
                "request": {
   
                    "query": chat_input,
                    "user_id": user_id
                },
                "session_id": "",
                "service_id": "S23245",
                "version": "2.0"
            }
    # 将封装好的数据作为请求内容, 发送给Unit聊天机器人接口, 并得到返回结果
    res = requests.post(url=unit_chatbot_url, json=post_data)


    # 获取聊天接口返回数据
    unit_chat_obj = json.loads(res.content)
    # print(unit_chat_obj)
    # 打印返回的结果
    # 判断聊天接口返回数据是否出错 error_code == 0 则表示请求正确
    if unit_chat_obj["error_code"] != 0: return chat_reply
    # 解析聊天接口返回数据,找到返回文本内容 result -> response_list -> schema -> intent_confidence(>0) -> action_list -> say
    unit_chat_obj_result = unit_chat_obj["result"]
    unit_chat_response_list = unit_chat_obj_result["response_list"]
    # 随机选取一个"意图置信度"[+response_list[].schema.intent_confidence]不为0的技能作为回答
    unit_chat_response_obj = random.choice(
       [unit_chat_response for unit_chat_response in unit_chat_response_list if
        unit_chat_response["schema"]["intent_confidence"] > 0.0])
    unit_chat_response_action_list = unit_chat_response_obj["action_list"]
    unit_chat_response_action_obj = random.choice(unit_chat_response_action_list)
    unit_chat_response_say = unit_chat_response_action_obj["say"]
    return unit_chat_response_say


if __name__ == '__main__':
    while True:
        chat_input = input("请输入:")
        print(chat_input)
        chat_reply = unit_chat(chat_input)
        print("用户输入 >>>", chat_input)
        print("Unit回复 >>>", chat_reply)
        if chat_input == 'Q' or chat_input == 'q':
            break

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

代码位置: /data/doctor_online/main_serve/unit.py


调用:

python unit.py

  • 1
  • 2

  • 输出效果:
请输入:你好啊
你好啊
用户输入 >>> 你好啊
Unit回复 >>> 你也好啊~
请输入:今天天气棒棒哒
今天天气棒棒哒
用户输入 >>> 今天天气棒棒哒
Unit回复 >>> 必须的
请输入:晚饭吃点什么?
晚饭吃点什么?
用户输入 >>> 晚饭吃点什么?
Unit回复 >>> 晚饭没吃,减肥
请输入:

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

  • 本章总结:

    • 智能对话系统的相关背景知识:
      • 什么是智能对话系统
      • 从处理问题的目的来区分, 智能对话系统的分类

    • 我们的在线医生项目就是任务导向型的智能对话系统.

    • 学习了Unit平台的相关知识:
      • Unit平台是百度大脑开放的智能对话定制与服务平台, 也是当前最大的中文领域对话开放平台之一.

    • 调用Unit API的实现过程:
      • 第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人.
      • 第二步: 进行相关配置, 获得请求API接口需要的API Key与Secret Key.
      • 第三步: 在服务器上编写API调用脚本并进行测试.

三、在线医生项目介绍

1、在线医生的总体架构

在这里插入图片描述
在这里插入图片描述

整个项目分为: 在线部分和离线部分

  • 在线部分包括:
    • werobot服务模块(微信信息发送),
    • 主要逻辑服务模块,
    • 句子相关模型服务模块(BERT模型-判断用户的前后两句话是否具有逻辑上、语义上的连贯性),
    • 会话管理模块(redis:将用户说的话存入redis缓存服务器,同时读出用户上一句话),
    • 图数据库模块(Neo4j)
    • 规则对话/Unit模块(把要返回给用户的话以一个规则的形式写出去).
  • 离线部分包括:
    • 结构与非结构化数据采集模块,
    • NER命名实体识别模型(BiLSTM+CRF)使用模块,
    • 实体审核模型(用RNN模型来判断实体到底是否与医学相关)使用模块.
  • 在线部分数据流:
    • 从用户请求开始,
    • 通过werobot服务, 在werobot服务内部请求主服务,
    • 在主服务中将调用会话管理数据库redis,
    • 调用句子相关模型服务, 以及调用图数据库,
    • 最后将查询结果输送给对话规则模版或者使用Unit对话API回复.
  • 离线部分数据流:
    • 从数据采集开始, 将获得结构化和非结构化的数据,
    • 对于结构化数据将直接使用实体审核模型进行审核, 然后写入图数据库;
    • 对于非结构化数据, 将使用NER命名实体识别模型(BiLSTM+CRF)进行实体抽取, 然后通过实体审核后再写入图数据库.

2、总体架构中的工具介绍

总体架构中使用的工具:

  • Flask web服务框架
  • Redis数据库
  • Gunicorn服务组件
  • Supervisor服务监控器
  • Neo4j图数据库

2.1 Flask web服务框架:

在这里插入图片描述

Flask框架是当下最受欢迎的python轻量级框架, 也是pytorch官网指定的部署框架. Flask的基本模式为在程序里将一个视图函数分配给一个URL,每当用户访问这个URL时,系统就会执行给该URL分配好的视图函数,获取函数的返回值,其工作过程见图.

在这里插入图片描述

在项目中, Flask框架是主逻辑服务和句子相关模型服务使用的服务框架.

安装:

# 使用pip安装Flask
pip install Flask==1.1.1
  • 1
  • 2

基本使用方法:

# 导入Flask类
from flask import Flask
# 创建一个该类的实例app, 参数为__name__, 这个参数是必需的,
# 这样Flask才能知道在哪里可找到模板和静态文件等东西.
app = Flask(__name__)

# 使用route()装饰器来告诉Flask触发函数的URL
@app.route('/')
def hello_world():
    """请求指定的url后,执行的主要逻辑函数"""
    # 在用户浏览器中显示信息:'Hello, World!'
    return 'Hello, World!'

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

代码位置: /data/doctor_onine/main_serve/app.py

启动服务:

python app.py
  • 1

如果在阿里云部署,需要在安全组开放5000端口。服务器防火墙开放5000端口

启动效果: 通过浏览器打开地址http://0.0.0.0:5000可看见打印了’Hello, World’.


2.2 Redis数据库:

在这里插入图片描述

Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API.

在项目中, Redis用于会话管理数据库, 保存用户聊天历史.

安装:

# 使用yum安装redis
yum install redis -y
  • 1
  • 2

基本使用方法:

  • Redis支持四种数据结构的存储: String(字符串), Hash(散列), List(列表), Set(集合), Sorted Set(有序集合).
  • 在这里我们将着重介绍如何在python中使用Hash(散列)进行读写.

安装python中的redis驱动:

# 使用pip进行安装
pip install redis
  • 1
  • 2

启动redis服务:

# 启动redis-server, 这里使用了默认配置, 端口是6379.
redis-server
  • 1
  • 2

在python中使用Hash(散列)进行读写:

# coding=utf-8
# redis配置
REDIS_CONFIG = {
   
     "host": "0.0.0.0",
     "port": 6379
}

# 导入redis驱动
import redis

# 创建一个redis连接池
pool = redis.ConnectionPool( **REDIS_CONFIG)
# 从连接池中初始化一个活跃的连接对象
r = redis.StrictRedis(connection_pool=pool)
# hset表示使用hash数据结构进行数据写入
# uid代表某个用户的唯一标识
uid = "8888"
# key是需要记录的数据描述
key = "该用户最后一次说的话:".encode('utf-8')
# value是需要记录的数据具体内容
value = "再见, 董小姐".encode('utf-8')
r.hset(uid, key, value)


# hget表示使用hash数据结构进行数据读取
result = r.hget(uid, key)
print(result.decode('utf-8'))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

输出效果:

再见, 董小姐
  • 1

2.3 Gunicorn服务组件:

在这里插入图片描述

Gunicorn是一个被广泛使用的高性能的Python WSGI UNIX HTTP服务组件(WSGI: Web Server Gateway Interface),移植自Ruby的独角兽(Unicorn )项目,具有使用非常简单,轻量级的资源消耗,以及高性能等特点.

在项目中, Gunicorn和Flask框架一同使用, 能够开启服务, 处理请求,因其高性能的特点能够有效减少服务丢包率.

安装:

# 使用pip安装gunicorn
pip install gunicorn==20.0.4
  • 1
  • 2

基本使用方法:

# 使用其启动Flask服务:
gunicorn -w 1 -b 0.0.0.0:5000 app:app
# -w 代表开启的进程数, 我们只开启一个进程
# -b 服务的IP地址和端口
# app:app 是指执行的主要对象位置, 在app.py中的app对象
  • 1
  • 2
  • 3
  • 4
  • 5
(base) [root@whx main_server]# gunicorn -w 1 -b 0.0.0.0:5000 app:app
[2021-03-22 11:43:15 +0800] [6266] [INFO] Starting gunicorn 20.0.4
[2021-03-22 11:43:15 +0800] [6266] [INFO] Listening at: http://0.0.0.0:5000 (6266)
[2021-03-22 11:43:15 +0800] [6266] [INFO] Using worker: sync
[2021-03-22 11:43:15 +0800] [6269] [INFO] Booting worker with pid: 6269
  • 1
  • 2
  • 3
  • 4
  • 5

如果使其在后台运行可使用:

# 如果使其在后台运行可使用:
# nohup gunicorn -w 1 -b 0.0.0.0:5000 app:app &
  • 1
  • 2

2.4 Supervisor服务监控

Supervisor是用Python开发的一个client/server服务,是Linux/Unix系统下的一个进程管理工具。它可以很方便的监听、启动、停止、重启一个或多个进程, 并守护这些进程。

作用: * 在项目中, Supervisor用于监控和守护主要逻辑服务和redis数据库服务.

安装:

# 使用yum安装supervisor
yum install supervisor -y
  • 1
  • 2

基本使用方法:

# 编辑配置文件, 指明监控和守护的进程开启命令, 
# 请查看/data/doctor_online/supervisord.conf文件
# 开启supervisor, -c用于指定配置文件
supervisord -c /data/doctor_online/main_server/supervisord.conf
  • 1
  • 2
  • 3
  • 4
# 查看监控的进程状态:
supervisorctl status

# main_server                      RUNNING   pid 31609, uptime 0:32:20
# redis                            RUNNING   pid 31613, uptime 0:32:18
  • 1
  • 2
  • 3
  • 4
  • 5

开启supervisor命令行:supervisorctl

(base) [root@whx main_server]# supervisorctl
main_server                      FATAL     Exited too quickly (process log may have details)
redis                            RUNNING   pid 6017, uptime 0:24:39
supervisor> 
  • 1
  • 2
  • 3
  • 4

FATAL说明该程序没启动或出错

开启main_server服务(Flask Web框架)

supervisor> start main_server
main_server: started
supervisor> status
main_server                      RUNNING   pid 6349, uptime 0:00:05
redis                            RUNNING   pid 6017, uptime 0:24:58
supervisor> exit
(base) [root@whx main_server]# 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

关闭supervisor:supervisorctl shutdown

# 关闭supervisor
(base) [root@whx main_server]# supervisorctl shutdown 
Shut down
(base) [root@whx main_server]# 
  • 1
  • 2
  • 3
  • 4

如果程序都处于STOP状态,则 supervisorctl start all批量开启全部服务,或批量停止所有服务 supervisorctl stop all

(base) [root@whx main_server]# supervisorctl start all
main_server: started
redis: started
(base) [root@whx main_server]# supervisorctl status all
main_server                      RUNNING   pid 2881, uptime 0:00:10
redis                            RUNNING   pid 2882, uptime 0:00:10
(base) [root@whx main_server]# 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

还可以通过浏览器查看可视化监控页面: http://0.0.0.0:9001(如果在阿里云部署,则使用公网IP)

在这里插入图片描述

2.5 Neo4j图数据库

四、离线部分简要分析

1、离线部分架构图:

在这里插入图片描述

离线部分架构展开图:

在这里插入图片描述

离线部分简要分析:

  • 根据架构展开图图,离线部分可分为两条数据流水线,分别用于处理结构化数据和非结构化数据. 这里称它们为结构化数据流水线和非结构化数据流水线.
  • 结构化数据流水线的组成部分:
    • 结构化数据爬虫: 从网页上抓取结构化的有关医学命名实体的内容.
    • 结构化数据的清洗: 对抓取的内容进行过滤和清洗, 以保留需要的部分.
    • 命名实体审核: 对当前命名实体进行审核, 来保证这些实体符合我们的要求.
    • 命名实体写入数据库: 将审核后的命名实体写入数据库之中, 供在线部分使用.
  • 非结构化数据流水线的组成部分:
    • 非结构化数据爬虫: 从网页上抓取非结构化的包含医学命名实体的文本.
    • 非结构化数据清洗: 对非结构化数据进行过滤和清洗, 以保留需要的部分.
    • 命名实体识别: 使用模型从非结构化文本中获取命名实体.
    • 命名实体审核: 对当前命名实体进行审核, 来保证这些实体符合我们的要求.
    • 命名实体写入数据库: 将审核后的命名实体写入数据库之中, 供在线部分使用.

说明:

  • 因为本项目是以AI为核心的项目, 因为结构化与非结构化的数据爬虫和清洗部分的内容这里不做介绍, 但同学们要知道我们的数据来源.

2、结构化数据流水线

需要进行命名实体审核的数据内容:

...
踝部急性韧带损伤.csv
踝部扭伤.csv
踝部骨折.csv
蹄铁形肾.csv
蹼状阴茎.csv
躁狂抑郁症.csv
躁狂症.csv
躁郁症.csv
躯体形式障碍.csv
躯体感染伴发的精神障碍.csv
躯体感染所致精神障碍.csv
躯体感觉障碍.csv
躯体疾病伴发的精神障碍.csv
转换性障碍.csv
转移性小肠肿瘤.csv
转移性皮肤钙化病.csv
转移性肝癌.csv
转移性胸膜肿瘤.csv
转移性骨肿瘤.csv
轮状病毒性肠炎.csv
轮状病毒所致胃肠炎.csv
软产道异常性难产.csv
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 每个csv文件的名字都是一种疾病名.
  • 文件位置: /data/doctor_offline/structured/noreview/

以躁狂症.csv为例, 有如下内容:

躁郁样
躁狂
行为及情绪异常
心境高涨
情绪起伏大
技术狂躁症
攻击行为
易激惹
思维奔逸
控制不住的联想
精神运动性兴奋
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • csv文件的内容是该疾病对应的症状, 每种症状占一行.

  • 文件位置: /data/doctor_offline/structured/noreview/躁狂症.csv

2.1 进行命名实体审核

进行命名实体审核的工作我们这里使用AI模型实现, 包括训练数据集, 模型训练和使用的整个过程, 因此这里内容以独立一章的形成呈现给大家, 具体参见第五章: 命名实体审核任务.

以躁狂症.csv为例, 审核后的内容只剩下一行内容:

躁郁样
  • 1

命名实体审核步骤完成之后,删除审核后的可能存在的空文件:

# Linux 命令-- 删除当前文件夹下的空文件
find ./ -name "*" -type f -size 0c | xargs -n 1 rm -f
  • 1
  • 2

代码位置: 在/data/doctor_offline/structured/reviewed/目录下执行.

2.2 命名实体写入数据库:

将命名实体写入图数据库的原因:写入的数据供在线部分进行查询,根据用户输入症状来匹配对应疾病.

将命名实体写入图数据库代码:

# 引入相关包
import os
import fileinput
from neo4j import GraphDatabase
from config import NEO4J_CONFIG

driver = GraphDatabase.driver( **NEO4J_CONFIG)

def _load_data(path):
    """
    description: 将path目录下的csv文件以指定格式加载到内存
    :param path:  经历了命名实体审核后的疾病对应症状的csv文件
    :return:      返回疾病字典,存储各个疾病以及与之对应的症状的字典
                  {疾病1: [症状1, 症状2, ...], 疾病2: [症状1, 症状2, ...]
    """
    # 获得疾病csv列表
    disease_csv_list = os.listdir(path)
    # 将后缀.csv去掉, 获得疾病列表
    disease_list = list(map(lambda x: x.split(".")[0], disease_csv_list))
   
    # 初始化一个症状列表, 它里面是每种疾病对应的症状列表
    symptom_list = []
    # 遍历疾病csv列表
    for disease_csv in disease_csv_list:
        # 将疾病csv中的每个症状取出存入symptom列表中
        symptom = list(map(lambda x: x.strip(), 
                           fileinput.FileInput(os.path.join(path, disease_csv))))
        # 过滤掉所有长度异常的症状名
        symptom = list(filter(lambda x: 0<len(x)<100, symptom))
        symptom_list.append(symptom)
    # 返回指定格式的数据
    return dict(zip(disease_list, symptom_list))



def write(path):
    """
    description: 将csv数据写入到neo4j, 并形成图谱
    :param path: 数据文件路径
    """
    # 使用_load_data从持久化文件中加载数据
    disease_symptom_dict = _load_data(path)
    # 开启一个neo4j的session
    with driver.session() as session:
        
        for key, value in disease_symptom_dict.items():
            cypher = "MERGE (a:Disease{name:%r}) RETURN a" %key
            session.run(cypher)
            for v in value:
                cypher = "MERGE (b:Symptom{name:%r}) RETURN b" %v
                session.run(cypher)
                cypher = "MATCH (a:Disease{name:%r}) MATCH (b:Symptom{name:%r}) WITH a,b MERGE(a)-[r:dis_to_sym]-(b)" %(key, v)
                session.run(cypher)
        cypher = "CREATE INDEX ON:Disease(name)"	# 在Disease标签的name属性上创建索引
        session.run(cypher)
        cypher = "CREATE INDEX ON:Symptom(name)"	# 在Symptom标签的name属性上创建索引
        session.run(cypher)

if __name__=="__main__":
	# 输入参数path为csv数据所在路径
	path = "/data/doctor_offline/structured/reviewed/"
	write(path)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

代码位置: 在/data/doctor_offline/util/neo4j_util.py

通过可视化管理后台查看写入效果:

在这里插入图片描述

4.3 非结构化数据流水线

需要进行命名实体识别的数据内容:

...
麻疹样红斑型药疹.txt
麻疹病毒肺炎.txt
麻痹性臂丛神经炎.txt
麻风性周围神经病.txt
麻风性葡萄膜炎.txt
黄体囊肿.txt
黄斑囊样水肿.txt
黄斑裂孔性视网膜脱离.txt
黄韧带骨化症.txt
黏多糖贮积症.txt
黏多糖贮积症Ⅰ型.txt
黏多糖贮积症Ⅱ型.txt
黏多糖贮积症Ⅵ型.txt
黏多糖贮积症Ⅲ型.txt
黏多糖贮积症Ⅶ型.txt
黑色丘疹性皮肤病.txt
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 每个txt文件的名字都是一种疾病名.
  • 文件位置: /data/doctor_offline/unstructured/norecognite/
  • 以黑色丘疹性皮肤病.txt为例, 有如下内容:
初呈微小、圆形、皮肤色或黑色增深的丘疹,单个或少数发生于颌部或颊部,皮损逐渐增大增多,数年中可达数百,除眶周外尚分布于面部、颈部和胸上部。皮损大小形状酷似脂溢性角化病及扁平疣鶒。不发生鳞屑,结痂和溃疡,亦无瘙痒及其他主观症状
  • 1
  • txt中是对该疾病症状的文本描述.
  • 文件位置: /data/doctor_offline/unstructured/norecognite/黑色丘疹性皮肤病.txt

4.3.1 进行命名实体识别

进行命名实体识别的工作我们这里使用AI模型实现, 包括模型训练和使用的整个过程, 因此内容以独立一章的形成呈现给大家, 具体内容在第六章: 命名实体识别任务.

4.3.2 进行命名实体审核

同结构化数据流水线中的命名实体审核.

4.3.3 命名实体写入数据库

同结构化数据流水线中的命名实体写入数据库.

五、命名实体审核【离线部分】

1、任务介绍与模型选用

一般在实体进入数据库存储前, 中间都会有一道必不可少的工序, 就是对识别出来的实体进行合法性的检验, 即命名实体(NE)审核任务. 它的检验过程不使用上下文信息, 更关注于字符本身的组合方式来进行判断, 本质上,它是一项短文本二分类问题.

选用的模型及其原因:

  • 针对短文本任务, 无须捕捉长距离的关系, 因此我们使用了传统的RNN模型来解决, 性能和效果可以达到很好的均衡.
  • 短文本任务往往适合使用字嵌入(Word Embedding)的方式, 但是如果你的训练集不是很大,涉及的字数有限, 那么可以直接使用预训练模型的字向量进行表示即可. 我们这里使用了bert-chinese预训练模型来获得中文汉字的向量表示.

2、训练数据集

训练数据集的样式:

1	手内肌萎缩
0	缩萎肌内手
1	尿黑酸
0	酸黑尿
1	单眼眼前黑影
0	影黑前眼眼单
1	忧郁
0	郁忧
1	红细胞寿命缩短
0	短缩命寿胞细红
1	皮肤黏蛋白沉积
0	积沉白蛋黏肤皮
1	眼神异常
0	常异神眼
1	阴囊坠胀痛
0	痛胀坠囊阴
1	动脉血氧饱和度降低
0	低降度和饱氧血脉动
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

数据集的相关解释:

  • 这些训练集中的正样本往往是基于人工审核的标准命名实体</.font>.
  • 数据集中的第一列代表标签, 1为正标签, 代表后面的文字是命名实体. 0为负标签, 代表后面的文字不是命名实体.
  • 数据集中的第二列中的命名实体来源于数据库中的症状实体名字, 它是结构化爬虫抓取的数据. 而非命名实体则是它的字符串反转.
  • 正负样本的比例是1:1.

将数据集加载到内存:

import pandas as pd 
from collections import Counter

# 读取数据
train_data_path = "./train_data.csv"
train_data= pd.read_csv(train_data_path, header=None, sep="\t")

# 打印正负标签比例
print(dict(Counter(train_data[0].values)))

# 转换数据到列表形式
train_data = train_data.values.tolist()
print(train_data[:10])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

代码位置: /data/doctor_offline/review_model/train.py

输出效果:

# 正负标签比例
{1: 5740, 0: 5740}

# 取出10条训练数据查看
[[1, '枕部疼痛'], [0, '痛疼部枕'], [1, '陶瑟征阳性'], [0, '性阳征瑟陶'], [1, '恋兽型性变态'], [0, '态变性型兽恋'], [1, '进食困难'], [0, '难困食进'], [1, '会阴瘘管或窦道形成'], [0, '成形道窦或管瘘阴会']]

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3、BERT中文预训练模型

BERT模型整体架构基于Transformer模型架构, BERT中文预训练模型的解码器和编码器具有12层, 输出层中的线性层具有768个节点, 即输出张量最后一维的维度是768. 它使用的多头注意力机制结构中, 头的数量为12, 模型总参数量为110M. 同时, 它在中文简体和繁体上进行训练, 因此适合中文简体和繁体任务.

3.1 BERT中文预训练模型作用

在实际的文本任务处理中, 有些训练语料很难获得, 他们的总体数量和包含的词汇总数都非常少, 不适合用于训练带有Embedding层的模型, 但这些数据中却又蕴含这一些有价值的规律可以被模型挖掘, 在这种情况下,使用预训练模型对原始文本进行编码是非常不错的选择, 因为预训练模型来自大型语料, 能够使得当前文本具有意义, 虽然这些意义可能并不针对某个特定领域, 但是这种缺陷可以使用微调模型来进行弥补.


3.2 使用BERT中文预训练模型对句子编码【Encoder】

不带头的Bert模型本质可以把其看做是新的word2Vec。

import torch
from transformers import BertModel, BertTokenizer

# 获得对应的字符映射器, 它将把中文的每个字映射成一个数字
# tokenizer = torch.hub.load('huggingface/pytorch-transformers', 'tokenizer', 'bert-base-chinese')
# 通过torch.hub(pytorch中专注于迁移学的工具)获得已经训练好的bert-base-chinese模型
# model =  torch.hub.load('huggingface/pytorch-transformers', 'model', 'bert-base-chinese')

# 使用离线bert模型
tokenizer = BertTokenizer.from_pretrained('/opt/huggingface_pretrained_model/bert-base-chinese')    # 加载bert的分词器
model = BertModel.from_pretrained('/opt/huggingface_pretrained_model/bert-base-chinese')    # 加载bert模型,这个路径文件夹下有bert_config.json配置文件和model.bin模型权重文件


def get_bert_encode_for_single(text):
    """
    功能: 使用bert-chinese预训练模型对中文文本进行编码
    text: 要进行编码的中文文本
    return : 使用bert编码后的文本张量表示
    """

    # 首先使用字符映射器对每个汉字进行映射
    # 这里需要注意, bert的tokenizer映射后会为结果前后添加开始和结束标记即101和102
    # 这对于多段文本的编码是有意义的, 但在我们这里没有意义, 因此使用[1:-1]对头和尾进行切片
    indexed_tokens = tokenizer.encode(text)[1:-1]
    tokens_tensor = torch.tensor([indexed_tokens])  # 将列表结果封装成tensor张量
    print("tokens_tensor = {0}".format(tokens_tensor))

    # 预测部分需要使得模型不自动求导
    with torch.no_grad():
        model_result= model(tokens_tensor)  # 调用模型获得隐层输出
    print("type(model_result) = {0}".format(type(model_result)))
    last_hidden_state = model_result[0]
    pooler_output = model_result[1]
    last_hidden_state = last_hidden_state[0]    # 模型的输出都是三维张量,第一维是1,使用[0]来进行降维,只提取我们需要的后两个维度的张量
    return last_hidden_state


if __name__ == '__main__':
    text = "你好,周杰伦"
    last_hidden_state = get_bert_encode_for_single(text)
    print("last_hidden_state.shape = {0}----last_hidden_state = {1}".format(last_hidden_state.shape, last_hidden_state))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

代码位置: /data/doctor_offline/review_model/bert_chinese_encode.py

输出效果:

tokens_tensor = tensor([[ 872, 1962,  117, 1453, 3345,  840]])
type(model_result) = <class 'transformers.modeling_outputs.BaseModelOutputWithPoolingAndCrossAttentions'>
last_hidden_state.shape = torch.Size([6, 768])----last_hidden_state = tensor([[ 3.2731e-01, -1.4832e-01, -9.1618e-01,  ..., -4.4088e-01,
         -4.1074e-01, -7.5570e-01],
        [-1.1287e-01, -7.6269e-01, -6.4861e-01,  ..., -8.0478e-01,
         -5.3600e-01, -3.1953e-01],
        [-9.3013e-02, -4.4381e-01, -1.1985e+00,  ..., -3.6624e-01,
         -4.7467e-01, -2.6408e-01],
        [-1.6897e-02, -4.3753e-01, -3.6060e-01,  ..., -3.2451e-01,
         -3.4203e-02, -1.7930e-01],
        [-1.3159e-01, -3.0048e-01, -2.4193e-01,  ..., -4.5756e-02,
         -2.0958e-01, -1.0649e-01],
        [-4.0006e-01, -3.4410e-01, -3.9446e-05,  ...,  1.9081e-01,
          1.7006e-01, -3.6221e-01]])

Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4、构建RNN模型【Decoder】

传统RNN的内部结构图:
在这里插入图片描述

结构解释图:

在这里插入图片描述

内部结构分析:

  • 我们把目光集中在中间的方块部分, 它的输入有两部分, 分别是 h t − 1 h_{t-1} ht1 以及 X t X_{t} Xt, 代表上一时间步的隐层输出, 以及此时间步的输入, 它们进入RNN结构体后, 会"融合"到一起, 这种融合我们根据结构解释可知, 是将二者进行拼接, 形成新的张量 [ h t − 1 , X t ] [h_{t-1}, X_{t}] [ht1,Xt], 之后这个新的张量将通过一个全连接层(线性层), 该层使用tanh作为激活函数, 最终得到该时间步的输出 h t h_t ht, 它将作为下一个时间步的输入和 X t + 1 X_{t+1} Xt+1一起进入结构体. 以此类推.

内部结构过程演示:

在这里插入图片描述

根据结构分析得出内部计算公式:

在这里插入图片描述
激活函数tanh的作用:

  • 用于帮助调节流经网络的值, tanh函数将值压缩在-1和1之间.

在这里插入图片描述

构建RNN模型的代码分析:

import torch
import torch.nn as nn
from torch.functional import F

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):   # input_size: 输入张量最后一维的尺寸大小; hidden_size: 隐层张量最后一维的尺寸大小; output_size: 输出张量最后一维的尺寸大小
        super(RNN, self).__init__()
        self.hidden_size = hidden_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)  # 构建从输入到隐含层的线性变化, 输入尺寸是input_size + hidden_size(这是因为在循环网络中, 每次输入都有两部分组成,分别是此时刻的输入和上一时刻产生的输出); 输出尺寸是hidden_size
        self.i2o = nn.Linear(input_size + hidden_size, output_size) # 构建从输入到输出层的线性变化, 输入尺寸还是input_size + hidden_size, 输出尺寸是output_size.
        self.logSoftmax = nn.LogSoftmax(dim=-1)    # 最后需要对输出做softmax处理, 获得结果.

    def forward(self, input_tensor, hidden_tensor):   # input_tensor: 规定尺寸的输入张量; hidden_tensor:规定尺寸的初始化隐层张量
        combined_tensor = torch.cat((input_tensor, hidden_tensor), 1)  # 首先使用torch.cat将 input_tensor 与 hidden_tensor 进行张量拼接
        hidden_tensor = self.i2h(combined_tensor)  # 通过输入层到隐层变换获得hidden张量
        output_tensor = self.i2o(combined_tensor) # 通过输入到输出层变换获得output张量
        print("output_tensor.shape = {0}----output_tensor = {1}".format(output_tensor.shape, output_tensor))
        softmax_output = F.softmax(input=output_tensor, dim=-1)
        print("softmax_output = ", softmax_output)
        log_softmax_output = self.logSoftmax(output_tensor)   # 对输出进行softmax处理
        return log_softmax_output, hidden_tensor    # 返回输出张量和最后的隐层结果

    def initHidden(self):   # 隐层初始化函数
        return torch.zeros(1, self.hidden_size) # 将隐层初始化成为一个 1*hidden_size 的全0张量


if __name__=="__main__":
    input_size = 768    # Bert不带头模型输出的词向量维度
    hidden_size = 128   # 自定义的RNN模型的隐层向量维度
    n_categories = 2    # 分类问题的总类别数量

    input_tensor = torch.rand(1, input_size)    # 随机模拟一个当前时间步的 x_t
    hidden_tensor = torch.rand(1, hidden_size)  # 随机模拟一个上一时间步的 h_{t-1}

    rnn = RNN(input_size, hidden_size, n_categories)
    log_softmax_output, hidden_tensor = rnn(input_tensor, hidden_tensor)    # 获取当前时间步的输出 log_softmax_output,以及当前时间步的隐层向量 hidden_tensor
    print("log_softmax_output:", log_softmax_output)
    print("hidden_tensor:", hidden_tensor)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

输出效果:

output_tensor.shape = torch.Size([1, 2])----output_tensor = tensor([[-0.3805,  0.2267]], grad_fn=<AddmmBackward>)
softmax_output =  tensor([[0.3527, 0.6473]], grad_fn=<SoftmaxBackward>)
log_softmax_output: tensor([[-1.0421, -0.4349]], grad_fn=<LogSoftmaxBackward>)
hidden_tensor: tensor([[-0.2870,  0.2608, -0.0235,  0.3382, -0.4202,  0.1983, -0.3219, -0.3075,
         -0.1800,  0.1812,  0.3352,  0.5984,  0.0766,  0.2267, -0.0472, -0.0294,
          0.3889, -0.0497,  0.3746,  0.0733,  0.0832,  0.1924,  0.4202,  0.6193,
          0.5166,  0.4753,  0.1640,  0.1890, -0.1762, -0.1737, -0.0665,  0.3903,
         -0.0221, -0.3334,  0.0368,  0.4379,  0.1949, -0.2566,  0.0909, -0.1718,
          0.3051,  0.3585, -0.3049, -0.3326, -0.0524,  0.2685,  0.2129,  0.0034,
          0.0677,  0.2064, -0.3648,  0.0370, -0.0311, -0.0194, -0.3450, -0.1689,
          1.1491,  0.1977, -0.2004, -0.3144, -0.1280, -0.4351, -0.3791,  0.0955,
         -0.2828,  0.1291,  0.2379,  0.3214,  0.1803, -0.3894,  0.3599,  0.0908,
          0.1693,  0.1310, -0.0266, -0.0401,  0.1506, -0.5393,  0.1613,  0.1090,
         -0.6135,  0.0540,  0.6531,  0.0088, -0.2246, -0.7439, -0.5373,  0.4964,
          0.1570, -0.3024, -0.4857, -0.0315,  0.1187,  0.0443, -0.0596,  0.0199,
         -0.1884,  0.4038, -0.7042,  0.3909, -0.1443, -0.3280,  0.1886,  0.1337,
         -0.0474, -0.0806, -0.0389,  0.5661,  0.1962,  0.1437, -0.0869, -0.4625,
         -0.0970, -0.5744, -0.0508, -0.0936, -0.0883, -0.3739, -0.0337,  0.1410,
          0.0648,  0.0885, -0.2135,  0.1781, -0.3301,  0.3941,  0.0368, -0.5758]],
       grad_fn=<AddmmBackward>)

Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

代码位置: /data/doctor_offline/review_model/RNN_MODEL.py

torch.cat演示:

>>> x = torch.randn(2, 3)
>>> x
tensor([[ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497]])
>>> torch.cat((x, x, x), 0)
tensor([[ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497],
        [ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497],
        [ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497]])
>>> torch.cat((x, x, x), 1)
ensor([[ 0.6580, -1.0969, -0.4614,  0.6580, -1.0969, -0.4614,  0.6580,-1.0969, -0.4614],
       [-0.1034, -0.5790,  0.1497, -0.1034, -0.5790,  0.1497, -0.1034,-0.5790,  0.1497]])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

5、进行模型训练

进行模型训练的步骤:

  • 第一步: 构建随机选取数据函数.
  • 第二步: 构建模型训练函数.
  • 第三步: 构建模型验证函数.
  • 第四步: 调用训练和验证函数.
  • 第五步: 绘制训练和验证的损失和准确率对照曲线.
  • 第六步: 模型保存.

5.1 第一步: 构建随机选取数据函数

将数据集加载到内存获得的train_data

# 导入bert中文编码的预训练模型
from bert_chinese_encode import get_bert_encode_for_single
# 二、随机选取数据函数
# 1、构建随机选取数据函数
def randomTrainingExample(train_data):  # train_data: 训练集的列表形式数据
    category, line = random.choice(train_data)  # 从train_data随机选择一条数据
    line_tensor = get_bert_encode_for_single(line)  # 将里面的文字使用bert进行编码, 获取编码后的tensor类型数据
    category_tensor = torch.tensor([int(category)]) # 将分类标签封装成tensor
    return category, line, category_tensor, line_tensor # 返回四个结果

# 2、测试随机选取数据函数
for i in range(10): # 选择10条数据进行查看
    category, line, category_tensor, line_tensor = randomTrainingExample(train_data.values.tolist())
    print('category =', category, '/ line =', line, '/category_tensor=', category_tensor, '/line_tensor.shape=', line_tensor.shape)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

输出效果:

category = 1 / line = 指甲类似云母 /category_tensor= tensor([1]) /line_tensor.shape= torch.Size([6, 768])
category = 1 / line = 良性假肥大型肌营养不良症 /category_tensor= tensor([1]) /line_tensor.shape= torch.Size([12, 768])
category = 0 / line = 斑紫 /category_tensor= tensor([0]) /line_tensor.shape= torch.Size([2, 768])
category = 1 / line = 肾小管酸化功能障碍 /category_tensor= tensor([1]) /line_tensor.shape= torch.Size([9, 768])
category = 0 / line = 变病外膜硬髓颈 /category_tensor= tensor([0]) /line_tensor.shape= torch.Size([7, 768])
category = 0 / line = 厥昏性过一 /category_tensor= tensor([0]) /line_tensor.shape= torch.Size([5, 768])
category = 0 / line = 宽变端侧近骨股 /category_tensor= tensor([0]) /line_tensor.shape= torch.Size([7, 768])
category = 1 / line = 仰颈时吞咽困难 /category_tensor= tensor([1]) /line_tensor.shape= torch.Size([7, 768])
category = 0 / line = 象危经神主自 /category_tensor= tensor([0]) /line_tensor.shape= torch.Size([6, 768])
category = 1 / line = 面下部头痛 /category_tensor= tensor([1]) /line_tensor.shape= torch.Size([5, 768])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

代码位置: /data/doctor_offline/review_model/train.py

5.2 第二步: 构建模型训练函数

# 选取损失函数为NLLLoss()
criterion = nn.NLLLoss()
# 学习率为0.005
learning_rate = 0.005


def train(category_tensor, line_tensor):
    """模型训练函数, category_tensor代表类别张量, line_tensor代表编码后的文本张量"""
    # 初始化隐层 
    hidden = rnn.initHidden()
    # 模型梯度归0
    rnn.zero_grad()
    # 遍历line_tensor中的每一个字的张量表示
    for i in range(line_tensor.size()[0]):
        # 然后将其输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此需要拓展一个维度, 循环调用rnn直到最后一个字
        output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
    # 根据损失函数计算损失, 输入分别是rnn的输出结果和真正的类别标签
    loss = criterion(output, category_tensor)
    # 将误差进行反向传播
    loss.backward()

    # 更新模型中所有的参数
    for p in rnn.parameters():
        # 将参数的张量表示与参数的梯度乘以学习率的结果相加以此来更新参数
        p.data.add_(-learning_rate, p.grad.data)

    # 返回结果和损失的值
    return output, loss.item()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

代码位置: /data/doctor_offline/review_model/train.py

5.3 第三步: 模型验证函数

def valid(category_tensor, line_tensor):
    """模型验证函数, category_tensor代表类别张量, line_tensor代表编码后的文本张量"""
    # 初始化隐层
    hidden = rnn.initHidden()
    # 验证模型不自动求解梯度
    with torch.no_grad():
        # 遍历line_tensor中的每一个字的张量表示    
        for i in range(line_tensor.size()[0]):
            # 然后将其输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此需要拓展一个维度, 循环调用rnn直到最后一个字
            output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)      
        # 获得损失
        loss = criterion(output, category_tensor)
     # 返回结果和损失的值
    return output, loss.item()

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

代码位置: /data/doctor_offline/review_model/train.py

5.4 第四步: 调用训练和验证函数

构建时间计算函数:

import time
import math

def timeSince(since):
    "获得每次打印的训练耗时, since是训练开始时间"
    # 获得当前时间
    now = time.time()
    # 获得时间差,就是训练耗时
    s = now - since
    # 将秒转化为分钟, 并取整
    m = math.floor(s / 60)
    # 计算剩下不够凑成1分钟的秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

代码位置: /data/doctor_offline/review_model/train.py

输入参数:

# 假定模型训练开始时间是10min之前
since = time.time() - 10*60
  • 1
  • 2

调用:

period = timeSince(since)
print(period)
  • 1
  • 2

输出效果:

10m 0s
  • 1

调用训练和验证函数并打印日志

# 设置迭代次数为50000步
n_iters = 50000

# 打印间隔为1000步
plot_every = 1000


# 初始化打印间隔中训练和验证的损失和准确率
train_current_loss = 0
train_current_acc = 0
valid_current_loss = 0
valid_current_acc = 0


# 初始化盛装每次打印间隔的平均损失和准确率
all_train_losses = []
all_train_acc = []
all_valid_losses = []
all_valid_acc = []

# 获取开始时间戳
start = time.time()


# 循环遍历n_iters次 
for iter in range(1, n_iters + 1):
    # 调用两次随机函数分别生成一条训练和验证数据
    category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
    category_, line_, category_tensor_, line_tensor_ = randomTrainingExample(train_data)
    # 分别调用训练和验证函数, 获得输出和损失
    train_output, train_loss = train(category_tensor, line_tensor)
    valid_output, valid_loss = valid(category_tensor_, line_tensor_)
    # 进行训练损失, 验证损失,训练准确率和验证准确率分别累加
    train_current_loss += train_loss
    train_current_acc += (train_output.argmax(1) == category_tensor).sum().item()
    valid_current_loss += valid_loss
    valid_current_acc += (valid_output.argmax(1) == category_tensor_).sum().item()
    # 当迭代次数是指定打印间隔的整数倍时
    if iter % plot_every == 0:
        # 用刚刚累加的损失和准确率除以间隔步数得到平均值
        train_average_loss = train_current_loss / plot_every
        train_average_acc = train_current_acc/ plot_every
        valid_average_loss = valid_current_loss / plot_every
        valid_average_acc = valid_current_acc/ plot_every
        # 打印迭代步, 耗时, 训练损失和准确率, 验证损失和准确率
        print("Iter:", iter, "|", "TimeSince:", timeSince(start))
        print("Train Loss:", train_average_loss, "|", "Train Acc:", train_average_acc)
        print("Valid Loss:", valid_average_loss, "|", "Valid Acc:", valid_average_acc)
        # 将结果存入对应的列表中,方便后续制图
        all_train_losses.append(train_average_loss)
        all_train_acc.append(train_average_acc)
        all_valid_losses.append(valid_average_loss)
        all_valid_acc.append(valid_average_acc)
        # 将该间隔的训练和验证损失及其准确率归0
        train_current_loss = 0
        train_current_acc = 0
        valid_current_loss = 0
        valid_current_acc = 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

代码位置: /data/doctor_offline/review_model/train.py

输出效果:

Iter: 1000 | TimeSince: 0m 56s
Train Loss: 0.6127021567507527 | Train Acc: 0.747
Valid Loss: 0.6702297774022868 | Valid Acc: 0.7
Iter: 2000 | TimeSince: 1m 52s
Train Loss: 0.5190641692602076 | Train Acc: 0.789
Valid Loss: 0.5217500487511397 | Valid Acc: 0.784
Iter: 3000 | TimeSince: 2m 48s
Train Loss: 0.5398398997281778 | Train Acc: 0.8
Valid Loss: 0.5844468013737023 | Valid Acc: 0.777
Iter: 4000 | TimeSince: 3m 43s
Train Loss: 0.4700755337187358 | Train Acc: 0.822
Valid Loss: 0.5140456306522071 | Valid Acc: 0.802
Iter: 5000 | TimeSince: 4m 38s
Train Loss: 0.5260879981063878 | Train Acc: 0.804
Valid Loss: 0.5924804099237979 | Valid Acc: 0.796
Iter: 6000 | TimeSince: 5m 33s
Train Loss: 0.4702717279043861 | Train Acc: 0.825
Valid Loss: 0.6675750375208704 | Valid Acc: 0.78
Iter: 7000 | TimeSince: 6m 27s
Train Loss: 0.4734503294042624 | Train Acc: 0.833
Valid Loss: 0.6329268293256277 | Valid Acc: 0.784
Iter: 8000 | TimeSince: 7m 23s
Train Loss: 0.4258338176879665 | Train Acc: 0.847
Valid Loss: 0.5356959595441066 | Valid Acc: 0.82
Iter: 9000 | TimeSince: 8m 18s
Train Loss: 0.45773495503464817 | Train Acc: 0.843
Valid Loss: 0.5413714128659645 | Valid Acc: 0.798
Iter: 10000 | TimeSince: 9m 14s
Train Loss: 0.4856756244019302 | Train Acc: 0.835
Valid Loss: 0.5450502399195044 | Valid Acc: 0.813
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

5.5 第五步: 绘制训练和验证的损失和准确率对照曲线

import matplotlib.pyplot as plt

plt.figure(0)
plt.plot(all_train_losses, label="Train Loss")
plt.plot(all_valid_losses, color="red", label="Valid Loss")
plt.legend(loc='upper left')
plt.savefig("./loss.png")


plt.figure(1)
plt.plot(all_train_acc, label="Train Acc")
plt.plot(all_valid_acc, color="red", label="Valid Acc")
plt.legend(loc='upper left')
plt.savefig("./acc.png")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

代码位置: /data/doctor_offline/review_model/train.py

训练和验证损失对照曲线:
在这里插入图片描述

训练和验证准确率对照曲线:

在这里插入图片描述

分析:

  • 损失对照曲线一直下降, 说明模型能够从数据中获取规律,正在收敛, 准确率对照曲线中验证准确率一直上升,最终维持在0.98左右.

5.6 模型保存

# 保存路径
MODEL_PATH = './BERT_RNN.pth'
# 保存模型参数
torch.save(rnn.state_dict(), MODEL_PATH)
  • 1
  • 2
  • 3
  • 4

代码位置: /data/doctor_offline/review_model/train.py

输出效果:

  • 在/data/doctor_offline/review_model/路径下生成BERT_RNN.pth文件.

5.7 模型训练、评估、绘画、保存完整代码

import random
import torch
import time
import math
from torch import nn
import torch.optim as optim
import pandas as pd
from collections import Counter
import matplotlib.pyplot as plt
from bert_chinese_encode import get_bert_encode_for_single  # 导入bert中文编码的预训练模型
from RNN_MODEL import RNN

# 训练命名实体审核模型

# 一、读取训练数据
train_data_path = './train_data.csv'
train_data = pd.read_csv(train_data_path, header=None, sep='\t')
print("正负标签比例: ", dict(Counter(train_data[0].values)))  # 打印一下正负标签比例
train_data = train_data.values.tolist()
print("打印若干数据展示: ", train_data[:2]) #  [[1, '手掌软硬度异常'], [0, '常异度硬软掌手']]


# 二、随机选取数据函数
# 1、构建随机选取数据函数
def randomTrainingExample(train_data):  # train_data: 训练集的列表形式数据
    category, line = random.choice(train_data)  # 从train_data随机选择一条数据
    line_tensor = get_bert_encode_for_single(line)  # 将里面的文字使用bert进行编码, 获取编码后的tensor类型数据
    category_tensor = torch.tensor([int(category)]) # 将分类标签封装成tensor
    return category, line, category_tensor, line_tensor # 返回四个结果

# 2、测试随机选取数据函数
for i in range(10): # 选择10条数据进行查看
    category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
    print('category =', category, '/ line =', line, '/category_tensor=', category_tensor, '/line_tensor.shape=', line_tensor.shape)


# 三、模型训练函数
rnn = RNN(input_size=768, hidden_size=128, output_size=2)  # input_size: Bert不带头模型输出的词向量维度; hidden_size: 自定义的RNN模型的隐层向量维度; output_size: 分类问题的总类别数量
criterion = nn.NLLLoss()    # 实例化损失函数,选取损失函数为NLLLoss()
optimizer = optim.Adam(rnn.parameters())
learning_rate = 0.005   # 学习率为0.005

# 2、构建训练函数
def train(category_tensor, line_tensor):    # category_tensor: 代表类别标签张量, line_tensor: 代表编码后的文本张量
    hidden = rnn.initHidden()   # 初始化隐层
    rnn.zero_grad() # 模型梯度归0    【optimizer.zero_grad()】
    for i in range(line_tensor.size()[0]):  # 遍历line_tensor中的每一个字符的词向量
        output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)   # 将当前字符的词向量、上一时间步的隐层张量输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此当前字符的词向量需要拓展一个维度, 循环调用rnn直到最后一个字
    loss = criterion(output, category_tensor)   # 根据损失函数计算损失, 输入分别是rnn的输出的类别标签结果和真正的类别标签
    loss.backward() # 将误差进行反向传播
    for p in rnn.parameters():  # 更新模型中所有的参数【此步一般用torch.optim实例化的优化器代替当前更新梯度的方法 optimizer.step() 】
        p.data.add_(-learning_rate, p.grad.data)    # 利用梯度下降法更新参数,add_()功能:原地更新【将“参数的张量”p.data与“参数的梯度乘以学习率的结果 (-learning_rate)×p.grad.data”相加以此来更新参数】
    return output, loss.item()  # 返回结果和损失的值

# 四、模型验证函数
def valid(category_tensor, line_tensor):   # category_tensor: 代表类别标签张量, line_tensor: 代表编码后的文本张量
    hidden = rnn.initHidden()   # 初始化隐藏层
    with torch.no_grad():   # 注意: 验证函数中要保证模型不自动求导
        for i in range(line_tensor.size()[0]):  # 遍历line_tensor中的每一个字符的词向量
            output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)   # 将当前字符的词向量、上一时间步的隐层张量输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此当前字符的词向量需要拓展一个维度, 循环调用rnn直到最后一个字
        loss = criterion(output, category_tensor)  # 根据损失函数计算损失, 输入分别是rnn的输出的类别标签结果和真正的类别标签
    return output, loss.item()

# 五、时间工具函数【获取每次打印的时间消耗, since是训练开始的时间】
def timeSince(since):
    now = time.time()   # 获取当前的时间
    s = now - since # 获取时间差, 就是时间消耗
    m = math.floor(s/60)    # 获取时间差的分钟数
    s -= m*60   # 获取时间差的秒数
    return '%dm %ds' % (m, s)


if __name__=="__main__":
    n_iters = 1000  # 设置训练的迭代次数
    plot_every = 100    # 设置打印间隔为1000
    # 初始化训练和验证的损失,准确率
    train_current_loss = 0
    train_current_acc = 0
    valid_current_loss = 0
    valid_current_acc = 0
    # 为后续的画图做准备,存储每次打印间隔之间的平均损失和平均准确率
    all_train_loss = []
    all_train_acc = []
    all_valid_loss = []
    all_valid_acc = []
    start = time.time() # 获取整个训练的开始时间
    # 进入主循环,遍历n_iters次
    for iter in range(1, n_iters + 1):
        # 分别调用两次随机获取数据的函数,分别获取训练数据和验证数据
        category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
        category_, line_, category_tensor_, line_tensor_ = randomTrainingExample(train_data)
        # 分别调用训练函数,和验证函数,得到输出和损失
        train_output, train_loss = train(category_tensor, line_tensor)
        valid_output, valid_loss = valid(category_tensor_, line_tensor_)
        # 累加训练的损失,训练的准确率,验证的损失,验证的准确率
        train_current_loss += train_loss
        train_current_acc += (train_output.argmax(1) == category_tensor).sum().item()
        valid_current_loss += valid_loss
        valid_current_acc += (valid_output.argmax(1) == category_tensor_).sum().item()
        # 每隔plot_every次数打印一下信息
        if iter % plot_every == 0:
            train_average_loss = train_current_loss / plot_every
            train_average_acc = train_current_acc / plot_every
            valid_average_loss = valid_current_loss / plot_every
            valid_average_acc = valid_current_acc / plot_every
            # 打印迭代次数,时间消耗,训练损失,训练准确率,验证损失,验证准确率
            print("Iter:", iter, "|", "TimeSince:", timeSince(start))
            print("Train Loss:", train_average_loss, "|", "Train Acc:", train_average_acc)
            print("Valid Loss:", valid_average_loss, "|", "Valid Acc:", valid_average_acc)
            # 将损失,准确率的结果保存起来,为后续的画图使用
            all_train_loss.append(train_average_loss)
            all_train_acc.append(train_average_acc)
            all_valid_loss.append(valid_average_loss)
            all_valid_acc.append(valid_average_acc)
            # 将每次打印间隔的训练损失,准确率,验证损失,准确率,归零操作
            train_current_loss = 0
            train_current_acc = 0
            valid_current_loss = 0
            valid_current_acc = 0

    plt.figure(0)
    plt.plot(all_train_loss, label="Train Loss")
    plt.plot(all_valid_loss, color="red", label="Valid Loss")
    plt.legend(loc="upper left")
    plt.savefig("./loss.png")

    plt.figure(1)
    plt.plot(all_train_acc, label="Train Acc")
    plt.plot(all_valid_acc, color="red", label="Valid Acc")
    plt.legend(loc="upper left")
    plt.savefig("./acc.png")

    torch.save(rnn.state_dict(), './BERT_RNN.pth')  # 模型的保存
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133

6、使用模型预测【命名实体审核】

将 /doctor_offline/structured/noreview文件夹中的每个文件(文件名为疾病名称)里的症状通过命名实体审核模型(BERT_RNN.pth)进行审核。

模型预测的实现过程:

import os
import torch
import torch.nn as nn

#将 /doctor_offline/structured/noreview文件夹中的每个文件(文件名为疾病名称)里的症状通过命名实体审核模型(BERT_RNN.pth)进行审核。

from RNN_MODEL import RNN   # 导入RNN模型结构
from bert_chinese_encode import get_bert_encode_for_single  # 导入bert预训练模型编码函数
MODEL_PATH = './BERT_RNN.pth'   # 预加载的模型参数路径

# 隐层节点数, 输入层尺寸, 类别数都和训练时相同即可
input_size = 768
hidden_size = 128
n_categories = 2

# 实例化RNN模型, 并加载保存模型参数
rnn = RNN(input_size=input_size, hidden_size=hidden_size, output_size=n_categories)  # input_size: Bert不带头模型输出的词向量维度; hidden_size: 自定义的RNN模型的隐层向量维度; output_size: 分类问题的总类别数量
rnn.load_state_dict(torch.load(MODEL_PATH))

# 模型测试函数, 它将用在模型预测函数中, 用于调用RNN模型并返回结果
def decoder(encoder_output_line_embedding): # line_tensor代表输入文本的张量表示
    hidden = rnn.initHidden()   # 初始化隐层张量
    for i in range(encoder_output_line_embedding.size()[0]):  # 与训练时相同, 遍历输入文本的每一个字符
        output, hidden = rnn(encoder_output_line_embedding[i].unsqueeze(0), hidden)   # 将其逐次输送给rnn模型
    return output   # 获得rnn模型最终的输出

# 模型预测函数
def predict(input_line):    # 输入参数input_line代表需要预测的文本
    with torch.no_grad():   # 不自动求解梯度
        encoder_output_line_embedding = get_bert_encode_for_single(input_line) # 将input_line字符串使用bert模型进行编码得到词向量输出
        decoder_output = decoder(encoder_output_line_embedding)
        _, topi = decoder_output.topk(1, 1) # 从decoder_output中取出最大值对应的索引, 比较的维度是1
        return topi.item()  # 返回结果数值

# 批量预测函数【待识别的命名实体组成的文件是以疾病名称为csv文件名,文件中的每一行是该疾病对应的症状命名实体】
def batch_predict(input_path, output_path): # input_path: 以原始文本(待识别的命名实体组成的文件)输入路径; output_path: 预测过滤后(去除掉非命名实体的文件)的输出路径
    csv_list = os.listdir(input_path)   # 读取路径下的每一个csv文件名, 装入csv列表之中
    # 遍历每一个csv文件
    for csv in csv_list:
        print("csv = ", csv)
        with open(os.path.join(input_path, csv), "r") as fr:    # 以读的方式打开每一个csv文件
            with open(os.path.join(output_path, csv), "w") as fw:   # 再以写的方式打开输出路径的同名csv文件
                input_lines = fr.readlines()  # 读取csv文件的每一行
                for input_line in input_lines:
                    input_line = input_line.strip()
                    res = predict(input_line)   # 使用模型进行预测
                    if res: # 如果结果为1
                        print("input_line = {0}, res = {1}".format(input_line, res))
                        fw.write(input_line + "\n") # 说明审核成功, 写入到输出csv中
                    else:
                        pass

if __name__=="__main__":
    # 模型预测函数【单样本测试】
    input_line = "点瘀样尖针性发多"
    result = predict(input_line)
    print("result:", result)
    # 批量预测函数
    input_path = "/data/doctor_offline/structured/noreview/"
    output_path = "/data/doctor_offline/structured/reviewed/"
    batch_predict(input_path, output_path)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

tensor.topk演示:

>>> tr = torch.randn(1, 2)
>>> tr
tensor([[-0.1808, -1.4170]])
>>> tr.topk(1, 1)
torch.return_types.topk(values=tensor([[-0.1808]]), indices=tensor([[0]]))
  • 1
  • 2
  • 3
  • 4
  • 5

代码位置: /data/doctor_offline/review_model/predict.py

输出效果:

  • 在输出路径下生成与输入路径等数量的同名csv文件, 内部的症状实体是被审核的可用实体.

六、命名实体识别(Named Entity Recognition,NER)【离线部分】

1、命名实体识别介绍

命名实体识别(Named Entity Recognition,NER)就是从一段自然语言文本中找出相关实体,并标注出其位置以及类型。是信息提取, 问答系统, 句法分析, 机器翻译等应用领域的重要基础工具, 在自然语言处理技术走向实用化的过程中占有重要地位. 包含行业, 领域专有名词, 如人名, 地名, 公司名, 机构名, 日期, 时间, 疾病名, 症状名, 手术名称, 软件名称等。具体可参看如下示例图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1 命名实体识别的作用

  • 识别专有名词, 为文本结构化提供支持.
  • 主体识别, 辅助句法分析.
  • 实体关系抽取, 有利于知识推理.

1.2 命名实体识别常用方法:基于规则(正则表达式)、基于模型(BiLSTM+CRF)

基于规则: 针对有特殊上下文的实体, 或实体本身有很多特征的文本, 使用规则的方法简单且有效. 比如抽取文本中物品价格, 如果文本中所有商品价格都是“数字+元”的形式, 则可以通过正则表达式”\d*.?\d+元”进行抽取. 但如果待抽取文本中价格的表达方式多种多样, 例如“一千八百万”, “伍佰贰拾圆”, “2000万元”, 遇到这些情况就要修改规则来满足所有可能的情况. 随着语料数量的增加, 面对的情况也越来越复杂, 规则之间也可能发生冲突, 整个系统也可能变得不可维护. 因此基于规则的方式比较适合半结构化或比较规范的文本中的进行抽取任务, 结合业务需求能够达到一定的效果.

  • 优点: 简单, 快速.
  • 缺点: 适用性差, 维护成本高后期甚至不能维护.

基于模型: 从模型的角度来看, 命名实体识别问题实际上是序列标注问题。序列标注问题指的是模型的输入是一个序列, 包括文字, 时间等, 输出也是一个序列. 针对输入序列的每一个单元, 输出一个特定的标签. 以中文分词任务进行举例, 例如输入序列是一串文字: “我是中国人”, 输出序列是一串标签: “OOBII”, 其中"BIO"组成了一种中文分词最基础的标签体系: B表示这个字是词的开始, I表示词的中间到结尾, O表示其他类型词(也可以用更多的字母来表示标签体系,“BIO”是最基础的一种标签体系). 因此我们可以根据输出序列"OOBII"进行解码, 得到分词结果"我\是\中国人".

  • 序列标注问题涵盖了自然语言处理中的很多任务, 包括语音识别, 中文分词, 机器翻译, 命名实体识别等, 而常见的序列标注模型包括HMM, CRF, RNN, LSTM, GRU等模型.
  • 其中在命名实体识别技术上, 目前主流的命名实体识别技术是通过 “BiLSTM+CRF” 模型进行序列标注, 也是项目中要用到的模型.

1.3 医学文本特征

在这里插入图片描述

  • 简短精炼
  • 形容词相对较少
  • 泛化性相对较小
  • 医学名词错字率比较高
  • 同义词、简称比较多

2、BiLSTM介绍

所谓的BiLSTM,就是(Bidirectional LSTM)双向LSTM. 单向的LSTM模型只能捕捉到从前向后传递的信息, 而双向的网络可以同时捕捉正向信息和反向信息, 使得对文本信息的利用更全面, 效果也更好.

在BiLSTM网络最终的输出层后面增加了一个线性层, 用来将BiLSTM产生的隐藏层输出结果投射到具有某种表达标签特征意义的区间, 具体如下图所示:

在这里插入图片描述
BiLSTM模型实现:

  • 第一步: 实现类的初始化和网络结构的搭建.
  • 第二步: 实现文本向量化的函数.
  • 第三步: 实现网络的前向计算.

2.1 第一步: 实现类的初始化和网络结构的搭建

本段代码构建类BiLSTM, 完成初始化和网络结构的搭建。

总共3层:

  • 词嵌入层,
  • 双向LSTM层,
  • 全连接线性层
# 本段代码构建类BiLSTM, 完成初始化和网络结构的搭建
# 总共3层: 词嵌入层, 双向LSTM层, 全连接线性层
import torch
import torch.nn as nn


class BiLSTM(nn.Module):
    """
    description: BiLSTM 模型定义
    """
    def __init__(self, vocab_size, tag_to_id, input_feature_size, hidden_size,
                 batch_size, sentence_length, num_layers=1, batch_first=True):
        """
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_id:           标签与 id 对照
        :param input_feature_size:  字嵌入维度( 即LSTM输入层维度 input_size )
        :param hidden_size:         隐藏层向量维度
        :param batch_size:          批训练大小
        :param sentence_length      句子长度
        :param num_layers:          堆叠 LSTM 层数
        :param batch_first:         是否将batch_size放置到矩阵的第一维度
        """
        # 类继承初始化函数
        super(BiLSTM, self).__init__()
        # 设置标签与id对照
        self.tag_to_id = tag_to_id
        # 设置标签大小, 对应BiLSTM最终输出分数矩阵宽度
        self.tag_size = len(tag_to_id)
        # 设定LSTM输入特征大小, 对应词嵌入的维度大小
        self.embedding_size = input_feature_size
        # 设置隐藏层维度, 若为双向时想要得到同样大小的向量, 需要除以2
        self.hidden_size = hidden_size // 2
        # 设置批次大小, 对应每个批次的样本条数, 可以理解为输入张量的第一个维度
        self.batch_size = batch_size
        # 设定句子长度
        self.sentence_length = sentence_length
        # 设定是否将batch_size放置到矩阵的第一维度, 取值True, 或False
        self.batch_first = batch_first
        # 设置网络的LSTM层数
        self.num_layers = num_layers
        
        # 构建词嵌入层: 字向量, 维度为总单词数量与词嵌入维度
        # 参数: 总体字库的单词数量, 每个字被嵌入的维度
        self.embedding = nn.Embedding(vocab_size, self.embedding_size)

        # 构建双向LSTM层: BiLSTM (参数: input_size      字向量维度(即输入层大小),
        #                               hidden_size     隐藏层维度,
        #                               num_layers      层数,
        #                               bidirectional   是否为双向,
        #                               batch_first     是否批次大小在第一位)
        self.bilstm = nn.LSTM(input_size=input_feature_size,
                              hidden_size=self.hidden_size,
                              num_layers=num_layers,
                              bidirectional=True,
                              batch_first=batch_first)

        # 构建全连接线性层: 将BiLSTM的输出层进行线性变换
        self.linear = nn.Linear(hidden_size, self.tag_size)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

代码实现位置: /data/doctor_offline/ner_model/bilstm.py

输入参数:

# 参数1:码表与id对照
char_to_id = {
   "双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6,
              "状": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}

# 参数2:标签码表对照
tag_to_id = {
   "O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}

# 参数3:字向量维度
EMBEDDING_DIM = 200

# 参数4:隐层维度
HIDDEN_DIM = 100

# 参数5:批次大小
BATCH_SIZE = 8

# 参数6:句子长度
SENTENCE_LENGTH = 20

# 参数7:堆叠 LSTM 层数
NUM_LAYERS = 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

调用:

# 初始化模型
model = BiLSTM(vocab_size=len(char_to_id),
               tag_to_id=tag_to_id,
               input_feature_size=EMBEDDING_DIM,
               hidden_size=HIDDEN_DIM,
               batch_size=BATCH_SIZE,
               sentence_length=SENTENCE_LENGTH,
               num_layers=NUM_LAYERS)
print(model)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

输出效果:

BiLSTM(
  (embedding): Embedding(14, 200)
  (bilstm): LSTM(200, 50, batch_first=True, bidirectional=True)
  (linear): Linear(in_features=100, out_features=5, bias=True)
)
  • 1
  • 2
  • 3
  • 4
  • 5

2.2 第二步:实现文本向量化的函数(将中文文本中的每个字映射为序列化的序号)

将句子中的每一个字符映射到码表中,比如:
char_to_id = {“双”: 0, “肺”: 1, “见”: 2, “多”: 3, “发”: 4, “斑”: 5, “片”: 6, “状”: 7, “稍”: 8, “高”: 9, “密”: 10, “度”: 11, “影”: 12, “。”: 13…}

# 本函数实现将中文文本映射为数字化的张量
def sentence_map(sentence_list, char_to_id, max_length):
    """
    description: 将句子中的每一个字符映射到码表中
    :param sentence: 待映射句子, 类型为字符串或列表
    :param char_to_id: 码表, 类型为字典, 格式为{"字1": 1, "字2": 2}
    :return: 每一个字对应的编码, 类型为tensor
    """
    # 字符串按照逆序进行排序, 不是必须操作
    sentence_list.sort(key=lambda c:len(c), reverse=True)
    # 定义句子映射列表
    sentence_map_list = []
    for sentence in sentence_list:
        # 生成句子中每个字对应的 id 列表
        sentence_id_list = [char_to_id[c] for c in sentence]
        # 计算所要填充 0 的长度
        padding_list = [0] * (max_length-len(sentence))
        # 组合
        sentence_id_list.extend(padding_list)
        # 将填充后的列表加入句子映射总表中
        sentence_map_list.append(sentence_id_list)
    # 返回句子映射集合, 转为标量
    return torch.tensor(sentence_map_list, dtype=torch.long)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

代码实现位置: /data/doctor_offline/ner_model/bilstm.py

输入参数:

# 参数1:句子集合
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"]

# 参数2:码表与id对照
char_to_id = {
   "<PAD>":0}	# 初始化的码表

# 参数3:句子长度
SENTENCE_LENGTH = 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

调用:

if __name__ == '__main__':
    for sentence in sentence_list:
        # 获取句子中的每一个字
        for _char in sentence:
            # 判断是否在码表 id 对照字典中存在
            if _char not in char_to_id:
                # 加入字符id对照字典
                char_to_id[_char] = len(char_to_id)

    # 将句子转为 id 并用 tensor 包装
    sentences_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    print("sentences_sequence:\n", sentences_sequence)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

输出效果:

sentences_sequence:
tensor([[14, 15, 16, 17, 18, 16, 19, 20, 21, 13, 22, 23, 24, 25, 26, 27, 28, 29, 30,  0],
        [14, 15, 26, 27, 18, 49, 50, 12, 21, 13, 22, 51, 52, 25, 53, 54, 55, 29, 30,  0],
        [14, 15, 53, 56, 18, 49, 50, 18, 26, 27, 57, 58, 59, 22, 51, 52, 55, 29, 0,  0],
        [37, 63, 64, 65, 66, 55, 13, 22, 61, 51, 52, 25, 67, 68, 69, 70, 71, 13, 0,  0],
        [37, 38, 39,  7,  8, 40, 41, 42, 43, 44, 45, 46, 47, 48,  0,  0,  0,  0, 0,  0],
        [16, 17, 18, 53, 56, 12, 59, 60, 22, 61, 51, 52, 12, 62,  0,  0,  0,  0, 0,  0],
        [ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13,  0,  0,  0,  0,  0, 0,  0],
        [31, 32, 24, 33, 34, 35, 36, 13, 30,  0,  0,  0,  0,  0,  0,  0,  0,  0, 0,  0]])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.3 第三步: 实现网络的前向计算

将句子利用BiLSTM进行特征计算,分别经过Embedding->BiLSTM->Linear,获得发射矩阵(emission scores)。

BiLSTM层的输出维度是tag_size, 也就是每个单词 w i w_i wi 映射到 各个tag的发射概率值。

# 本函数实现类BiLSTM中的前向计算函数forward()
def forward(self, sentences_sequence):
    """
    description: 将句子利用BiLSTM进行特征计算,分别经过Embedding->BiLSTM->Linear,获得发射矩阵(emission scores)
    :param sentences_sequence: 句子序列对应的编码,
                               若设定 batch_first 为 True,
                               则批量输入的 sequence 的 shape 为(batch_size, sequence_length)
    :return:    返回当前句子特征,转化为 tag_size 的维度的特征
    """
    # 初始化隐藏状态值
    h0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)
    # 初始化单元状态值
    c0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)
    # 生成字向量, shape 为(batch, sequence_length, input_feature_size)
    # 注:embedding cuda 优化仅支持 SGD 、 SparseAdam
    input_features = self.embedding(sentences_sequence)
    
    # 将字向量与初始值(隐藏状态 h0 , 单元状态 c0 )传入 LSTM 结构中
    # 输出包含如下内容:
    # 1, 计算的输出特征,shape 为(batch, sentence_length, hidden_size)
    #    顺序为设定 batch_first 为 True 情况, 若未设定则 batch 在第二位
    # 2, 最后得到的隐藏状态 hn , shape 为(num_layers * num_directions, batch, hidden_size)
    # 3, 最后得到的单元状态 cn , shape 为(num_layers * num_directions, batch, hidden_size)
    output, (hn, cn) = self.bilstm(input_features, (h0, c0))
    # 将输出特征进行线性变换,转为 shape 为 (batch, sequence_length, tag_size) 大小的特征
    sequence_features = self.linear(output)
    # 输出线性变换为 tag 映射长度的特征
    return sequence_features
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

代码实现位置: /data/doctor_offline/ner_model/bilstm.py

输入参数:

# 参数1:标签码表对照
tag_to_id = {
   "O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}

# 参数2:字向量维度
EMBEDDING_DIM = 200

# 参数3:隐层维度
HIDDEN_DIM = 100

# 参数4:批次大小
BATCH_SIZE = 8

# 参数5:句子长度
SENTENCE_LENGTH = 20

# 参数6:堆叠 LSTM 层数
NUM_LAYERS = 1

char_to_id = {
   "<PAD>":0}
SENTENCE_LENGTH = 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

调用:

if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)

    model = BiLSTM(vocab_size=len(char_to_id), tag_to_id=tag_to_id, input_feature_size=EMBEDDING_DIM, \
    hidden_size=HIDDEN_DIM, batch_size=BATCH_SIZE, sentence_length=SENTENCE_LENGTH, num_layers=NUM_LAYERS)

    sentence_features = model(sentence_sequence)
    print("sequence_features:\n", sentence_features)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

输出效果:

sequence_features:
tensor([[[ 4.0880e-02, -5.8926e-02, -9.3971e-02,  8.4794e-03, -2.9872e-01],
         [ 2.9434e-02, -2.5901e-01, -2.0811e-01,  1.3794e-02, -1.8743e-01],
         [-2.7899e-02, -3.4636e-01,  1.3382e-02,  2.2684e-02, -1.2067e-01],
         [-1.9069e-01, -2.6668e-01, -5.7182e-02,  2.1566e-01,  1.1443e-01],
       	  								...
         [-1.6844e-01, -4.0699e-02,  2.6328e-02,  1.3513e-01, -2.4445e-01],
         [-7.3070e-02,  1.2032e-01,  2.2346e-01,  1.8993e-01,  8.3171e-02],
         [-1.6808e-01
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/580116
推荐阅读
相关标签
  

闽ICP备14008679号