背景介绍
- 什么是智能对话系统?
- 随着人工智能技术的发展, 聊天机器人, 语音助手等应用在生活中随处可见, 比如百度的小度, 阿里的小蜜, 微软的小冰等等.
其目的在于通过人工智能技术让机器像人类一样能够进行智能回复, 解决现实中的各种问题.
- 从处理问题的角度来区分, 智能对话系统可分为:
- 任务导向型: 完成具有明确指向性的任务, 比如预定酒店咨询, 在线问诊等等.
- 非任务导向型: 没有明确目的, 比如算算术, 播放音乐, 回答问题.
Unit对话API的使用
- Unit平台的相关知识:
- Unit平台是百度大脑开放的智能对话定制与服务平台, 也是当前最大的中文领域对话开放平台之一. Unit对注册用户提供免费的对话接口服务,比如中文闲聊API, 百科问答API, 诗句生成API等, 通过这些API我们可以感受一下智能对话的魅力,同时它也可以作为任务导向型对话系统无法匹配用户输入时的最终选择.
- Unit闲聊API演示
用户输入 >>> "你好"
Unit回复 >>> "你好,想聊什么呢~"
用户输入 >>> "我想有一个女朋友!"
Unit回复 >>> "我也是想要一个女朋友~"
用户输入 >>> "晚吃啥呢想想"
Unit回复 >>> "想吃火锅"
- 调用Unit API的实现过程:
- 第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人.
- 第二步: 进行相关配置, 获得请求API接口需要的API Key与Secret Key.
- 第三步: 在服务器上编写API调用脚本并进行测试.
- 第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人
https://ai.baidu.com/tech/unit - 第二步: 进行相关配置, 获得请求API接口需要的API Key与Secret Key.
- 第三步: 在服务器上编写API调用脚本并进行测试
import json
import random
import requests
# client_id 为官网获取的AK, client_secret 为官网获取的SK
client_id = "uryd9RRIXmz6xO7cdvCv3nuo"
client_secret = "UTp2EqpWtb4ApZoIezrmfpKPDE21lNg0"
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": "S71326",
"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("请输入闲聊内容或q(Q)退出:")
if chat_input == 'Q' or chat_input == 'q':
break
chat_reply = unit_chat(chat_input)
print("用户输入 >>>", chat_input)
print("Unit回复 >>>", chat_reply)
在线医生需求分析
架构图分析:
- 整个项目分为: 在线部分和离线部分
- 在线部分包括: werobot服务模块, 主要逻辑服务模块, 句子相关模型服务模块, 会话管理模块(redis),
图数据库模块以及规则对话/Unit模块. 离线部分包括: 结构与非结构化数据采集模块, NER模型使用模块, 以及实体审核模型使用模块. - 在线部分数据流: 从用户请求开始, 通过werobot服务, 在werobot服务内部请求主服务,在主服务中将调用会话管理数据库redis, 调用句子相关模型服务, 以及调用图数据库, 最后将查询结果输送给对话规则模版或者使用Unit对话API回复.
- 离线部分数据流: 从数据采集开始, 将获得结构化和非结构化的数据, 对于结构化数据将直接使用实体审核模型进行审核, 然后写入图数据库;对于非结构化数据, 将使用NER模型进行实体抽取, 然后通过实体审核后再写入图数据库.
工具
Flask web服务框架
- 安装
pip install flask==2.0.2
- 测试代码
# 导入
from flask import Flask
# 创建一个该类的实例app 参数为 __name__ 这个参数是必需的
# 只有传入这个参数,Flask才能知道哪里找到模板和静态文件
app = Flask(__name__)
# 使用route()装饰器来告诉flask触发函数的url
@app.route("/")
def hello_world():
return "hello world"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
redis
- 安装
yum install redis -y
- python中的redis驱动
pip install redis
- 启动redis服务
redis-server
- 测试代码
# coding=utf-8
REDIS_CONFIG = {
"host": "0.0.0.0",
"port": 6379
}
import redis
# 创建一个redis连接池
pool = redis.ConnectionPool(**REDIS_CONFIG)
# 创建一个活跃对象
r = redis.StrictRedis(connection_pool=pool)
# 利用r.hset()写入数据,传入三个参数uid,key,value
# uid 用户唯一标识
uid = "8888"
# key是需要记录的数据的描述信息
key = "该用户最后一次说的话:".encode("utf-8")
# value是真实需要记录的数据内容
value = "再见,董小姐".encode("utf-8")
r.hset(uid, key, value)
# 利用r.hget()读取数据
result = r.hget(uid, key)
print(result.decode("utf-8"))
Gunicorn服务组件
- 安装
pip install gunicorn==20.0.4
- 使用
Supervisor服务监控
- 安装
yum install supervisor
- 使用方法
Neo4j图数据库
![在这里插入图片描述](
- 安装
第一步: 准备环境
sudo su
$ wget --no-check-certificate -O - https://debian.neo4j.org/neotechnology.gpg.key | sudo apt-key add -
$ echo 'deb http://debian.neo4j.org/repo stable/' > /etc/apt/sources.list.d/neo4j.list
$ apt update
$ apt install neo4j
sudo apt-get install openjdk-8-jdk
java -version
sudo su
wget -O - https://debian.neo4j.org/neotechnology.gpg.key | sudo apt-key add -
echo 'deb https://debian.neo4j.org/repo stable/' | sudo tee -a /etc/apt/sources.list.d/neo4j.list
sudo apt-get update
第二步: apt-get安装
sudo apt-get install neo4j=3.1.4 #企业版
sudo apt-get install neo4j-enterprise=1:3.5.12 #社区版
第三步: 修改配置文件默认在/etc/neo4j/neo4j.conf
# 数据库的存储库存储位置、日志位置等
dbms.directories.data=/var/lib/neo4j/data
dbms.directories.plugins=/var/lib/neo4j/plugins
dbms.directories.certificates=/var/lib/neo4j/certificates
dbms.directories.logs=/var/log/neo4j
dbms.directories.lib=/usr/share/neo4j/lib
dbms.directories.run=/var/run/neo4j
# 导入的位置
dbms.directories.import=/var/lib/neo4j/import
# 初始化内存大小
dbms.memory.heap.initial_size=512m
# Bolt 连接地址
dbms.connector.bolt.enabled=true
dbms.connector.bolt.tls_level=OPTIONAL
dbms.connector.bolt.listen_address=192.168.81.129:7687
安装完成进入cypher-shell,进入后可通过CALL dbms.changePassword(‘new password’)修改新密码,否则不能写入数据,初始用户名和密码都为neo4j
第四步: 启动neo4j数据库
# 启动命令
neo4j start
# 终端显示如下, 代表启动成功
Active database: graph.db
Directories in use:
home: /usr/neo4j
config: /etc/neo4j
logs: /var/log/neo4j
plugins: /var/lib/neo4j/plugins
import: /var/lib/neo4j/import
data: /var/lib/neo4j/data
certificates: /var/lib/neo4j/certificates
run: /var/run/neo4j
Starting Neo4j.
如果密码错误:
- Cypher使用
create命令: 创建图数据中的节点.
# 创建命令格式:
# 此处create是关键字, 创建节点名称node_name, 节点标签Node_Label, 放在小括号里面()
# 后面把所有属于节点标签的属性放在大括号'{}'里面, 依次写出属性名称:属性值, 不同属性用逗号','分隔
# 例如下面命令创建一个节点e, 节点标签是Employee, 拥有id, name, salary, deptnp四个属性:
CREATE (e:Employee{id:222, name:'Bob', salary:6000, deptnp:12})
match命令: 匹配(查询)已有数据
# match命令专门用来匹配查询, 节点名称:节点标签, 依然放在小括号内, 然后使用return语句返回查询结果, 和SQL很相似.
MATCH (e:Employee) RETURN e.id, e.name, e.salary, e.deptno
merge命令: 若节点存在, 则等效与match命令; 节点不存在, 则等效于create命令
MERGE (e:Employee {id:146, name:'Lucer', salary:3500, deptno:16})
然后再次用merge查询, 发现数据库中的数据并没有增加, 因为已经存在相同的数据了, merge匹配成功
MERGE (e:Employee {id:146, name:'Lucer', salary:3500, deptno:16})
使用create创建关系: 必须创建有方向性的关系, 否则报错
# 创建一个节点p1到p2的有方向关系, 这个关系r的标签为Buy, 代表p1购买了p2, 方向为p1指向p2
CREATE (p1:Profile1)-[r:Buy]->(p2:Profile2)
使用merge创建关系: 可以创建有/无方向性的关系.
# 创建一个节点p1到p2的无方向关系, 这个关系r的标签为miss, 代表p1-miss-p2, 方向为相互的
MERGE (p1:Profile1)-[r:miss]-(p2:Profile2)
where命令: 类似于SQL中的添加查询条件
# 查询节点Employee中, id值等于123的那个节点
MATCH (e:Employee) WHERE e.id=123 RETURN e
delete命令: 删除节点/关系及其关联的属性
# 注意: 删除节点的同时, 也要删除关联的关系边
MATCH (c1:CreditCard)-[r]-(c2:Customer) DELETE c1, r, c2
sort命令: Cypher命令中的排序使用的是order by
# 匹配查询标签Employee, 将所有匹配结果按照id值升序排列后返回结果
MATCH (e:Employee) RETURN e.id, e.name, e.salary, e.deptno ORDER BY e.id
# 如果要按照降序排序, 只需要将ORDER BY e.salary改写为ORDER BY e.salary DESC
MATCH (e:Employee) RETURN e.id, e.name, e.salary, e.deptno ORDER BY e.salary DESC
toUpper()函数: 将一个输入字符串转换为大写字母
MATCH (e:Employee) RETURN e.id, toUpper(e.name), e.salary, e.deptno
toLower()函数: 讲一个输入字符串转换为小写字母
MATCH (e:Employee) RETURN e.id, toLower(e.name), e.salary, e.deptno
substring()函数: 返回一个子字符串
# 输入字符串为input_str, 返回从索引start_index开始, 到end_index-1结束的子字符串
substring(input_str, start_index, end_index)
# 示例代码, 返回员工名字的前两个字母
MATCH (e:Employee) RETURN e.id, substring(e.name,0,2), e.salary, e.deptno
replace()函数: 替换掉子字符串
# 输入字符串为input_str, 将输入字符串中符合origin_str的部分, 替换成new_str
replace(input_str, origin_str, new_str)
# 示例代码, 将员工名字替换为添加后缀_HelloWorld
MATCH (e:Employee) RETURN e.id, replace(e.name,e.name,e.name + "_HelloWorld"), e.salary, e.deptno
count()函数: 返回由match命令匹配成功的条数
# 返回匹配标签Employee成功的记录个数
MATCH (e:Employee) RETURN count( * )
max()函数: 返回由match命令匹配成功的记录中的最大值
# 返回匹配标签Employee成功的记录中, 最高的工资数字
MATCH (e:Employee) RETURN max(e.salary)
min()函数: 返回由match命令匹配成功的记录中的最小值
# 返回匹配标签Employee成功的记录中, 最低的工资数字
MATCH (e:Employee) RETURN min(e.salary)
sum()函数: 返回由match命令匹配成功的记录中某字段的全部加和值
# 返回匹配标签Employee成功的记录中, 所有员工工资的和
MATCH (e:Employee) RETURN sum(e.salary)
avg()函数: 返回由match命令匹配成功的记录中某字段的平均值
# 返回匹配标签Employee成功的记录中, 所有员工工资的平均值
MATCH (e:Employee) RETURN avg(e.salary)
创建索引: 使用create index on来创建索引
# 创建节点Employee上面属性id的索引
CREATE INDEX ON:Employee(id)
删除索引: 使用drop index on来删除索引
# 删除节点Employee上面属性id的索引
DROP INDEX ON:Employee(id)
- 在Python中使用neo4j
安装
pip install neo4j-driver
配置类
# 设置neo4j图数据库的配置信息
NEO4J_CONFIG = {
"uri": "bolt://192.168.81.129:7687",
"auth": ("neo4j", "cgneo4j"),
"encrypted": False
}
from neo4j import GraphDatabase
# 关于neo4j数据库的用户名,密码信息已经配置在同目录下的config.py文件中
from config import NEO4J_CONFIG
driver = GraphDatabase.driver(**NEO4J_CONFIG)
# 直接用python代码形式访问节点Company, 并返回所有节点信息
with driver.session() as session:
cypher = "CREATE(c:Company) SET c.name='在线医生' RETURN c.name"
record = session.run(cypher)
result = list(map(lambda x: x[0], record))
print("result:", result)
result: ['在线医生']
- 事务
如果一组数据库操作要么全部发生要么一步也不执行,我们称该组处理步骤为一个事务, 它是数据库一致性的保证
def _some_operations(tx, cat_name, mouse_name):
tx.run("MERGE (a:Cat{name: $cat_name})"
"MERGE (b:Mouse{name: $mouse_name})"
"MERGE (a)-[r:And]-(b)",
cat_name=cat_name, mouse_name=mouse_name)
with driver.session() as session:
session.write_transaction(_some_operations, "Tom", "Jerry")
离线部分
结构化数据流水线
需要进行命名实体审核的数据内容
...
踝部急性韧带损伤.csv
踝部扭伤.csv
踝部骨折.csv
蹄铁形肾.csv
蹼状阴茎.csv
躁狂抑郁症.csv
躁狂症.csv
躁郁症.csv
躯体形式障碍.csv
躯体感染伴发的精神障碍.csv
躯体感染所致精神障碍.csv
躯体感觉障碍.csv
躯体疾病伴发的精神障碍.csv
转换性障碍.csv
转移性小肠肿瘤.csv
转移性皮肤钙化病.csv
转移性肝癌.csv
转移性胸膜肿瘤.csv
转移性骨肿瘤.csv
轮状病毒性肠炎.csv
轮状病毒所致胃肠炎.csv
软产道异常性难产.csv
...
以躁狂症.csv为例, 有如下内容
躁郁样
躁狂
行为及情绪异常
心境高涨
情绪起伏大
技术狂躁症
攻击行为
易激惹
思维奔逸
控制不住的联想
精神运动性兴奋
删除审核后的可能存在的空文件
# Linux 命令-- 删除当前文件夹下的空文件
find ./ -name "*" -type f -size 0c | xargs -n 1 rm -f
命名实体写入数据库
写入的数据供在线部分进行查询,根据用户输入症状来匹配对应疾病
# 引入相关包
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), openhook= fileinput.hook_encoded('utf-8'))))
# 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)"
session.run(cypher)
cypher = "CREATE INDEX ON:Symptom(name)"
session.run(cypher)
# 输入参数path为csv数据所在路径
path = "/data/doctor_offline/structured/reviewed/"
write(path)
非结构化数据流水线
需要进行命名实体识别的数据内容
...
麻疹样红斑型药疹.txt
麻疹病毒肺炎.txt
麻痹性臂丛神经炎.txt
麻风性周围神经病.txt
麻风性葡萄膜炎.txt
黄体囊肿.txt
黄斑囊样水肿.txt
黄斑裂孔性视网膜脱离.txt
黄韧带骨化症.txt
黏多糖贮积症.txt
黏多糖贮积症Ⅰ型.txt
黏多糖贮积症Ⅱ型.txt
黏多糖贮积症Ⅵ型.txt
黏多糖贮积症Ⅲ型.txt
黏多糖贮积症Ⅶ型.txt
黑色丘疹性皮肤病.txt
...
以黑色丘疹性皮肤病.txt为例
初呈微小、圆形、皮肤色或黑色增深的丘疹,单个或少数发生于颌部或颊部,皮损逐渐增大增多,数年中可达数百,除眶周外尚分布于面部、颈部和胸上部。皮损大小形状酷似脂溢性角化病及扁平疣鶒。不发生鳞屑,结痂和溃疡,亦无瘙痒及其他主观症状
命名实体识别
训练数据集
- 训练数据集的样式
1 手内肌萎缩
0 缩萎肌内手
1 尿黑酸
0 酸黑尿
1 单眼眼前黑影
0 影黑前眼眼单
1 忧郁
0 郁忧
1 红细胞寿命缩短
0 短缩命寿胞细红
1 皮肤黏蛋白沉积
0 积沉白蛋黏肤皮
1 眼神异常
0 常异神眼
1 阴囊坠胀痛
0 痛胀坠囊阴
1 动脉血氧饱和度降低
0 低降度和饱氧血脉动
- 将数据集加载到内存
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: 5740, 0: 5740}
# 取出10条训练数据查看
[[1, '枕部疼痛'], [0, '痛疼部枕'], [1, '陶瑟征阳性'], [0, '性阳征瑟陶'], [1, '恋兽型性变态'], [0, '态变性型兽恋'], [1, '进食困难'], [0, '难困食进'], [1, '会阴瘘管或窦道形成'], [0, '成形道窦或管瘘阴会']]
BERT中文预训练模型
- 使用BERT中文预训练模型对句子编码
import torch
import torch.nn as nn
# 通过torch.hub(pytorch中专注于迁移学的工具)获得已经训练好的bert-base-chinese模型
model = torch.hub.load('huggingface/pytorch-transformers', 'model', 'bert-base-chinese')
# 获得对应的字符映射器, 它将把中文的每个字映射成一个数字
tokenizer = torch.hub.load('huggingface/pytorch-transformers', 'tokenizer', 'bert-base-chinese')
def get_bert_encode_for_single(text):
"""
description: 使用bert-chinese编码中文文本
:param text: 要进行编码的文本
:return: 使用bert编码后的文本张量表示
"""
# 首先使用字符映射器对每个汉字进行映射
# 这里需要注意, bert的tokenizer映射后会为结果前后添加开始和结束标记即101和102
# 这对于多段文本的编码是有意义的, 但在我们这里没有意义, 因此使用[1:-1]对头和尾进行切片
indexed_tokens = tokenizer.encode(text)[1:-1]
# 之后将列表结构转化为tensor
tokens_tensor = torch.tensor([indexed_tokens])
print(tokens_tensor)
# 使模型不自动计算梯度
with torch.no_grad():
# 调用模型获得隐层输出
encoded_layers, _ = model(tokens_tensor)
# 输出的隐层是一个三维张量, 最外层一维是1, 我们使用[0]降去它.
print(encoded_layers.shape)
encoded_layers = encoded_layers[0]
return encoded_layers
text = "你好, 周杰伦"
outputs = get_bert_encode_for_single(text)
print(outputs)
print(outputs.shape)
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.3012e-02, -4.4381e-01, -1.1985e+00, ..., -3.6624e-01,
-4.7467e-01, -2.6408e-01],
[-1.6896e-02, -4.3753e-01, -3.6060e-01, ..., -3.2451e-01,
-3.4204e-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.8532e-05, ..., 1.9081e-01,
1.7006e-01, -3.6221e-01]])
torch.Size([6, 768])
bert 预训练模型地址
PRETRAINED_VOCAB_ARCHIVE_MAP = {
'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-vocab.txt",
'bert-large-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased-vocab.txt",
'bert-base-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-cased-vocab.txt",
'bert-large-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased-vocab.txt",
'bert-base-multilingual-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-uncased-vocab.txt",
'bert-base-multilingual-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-cased-vocab.txt",
'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese-vocab.txt",
}
PRETRAINED_MODEL_ARCHIVE_MAP = {
'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased.tar.gz",
'bert-large-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased.tar.gz",
'bert-base-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-cased.tar.gz",
'bert-large-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased.tar.gz",
'bert-base-multilingual-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-uncased.tar.gz",
'bert-base-multilingual-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-cased.tar.gz",
'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese.tar.gz",
}
构建RNN模型
- 构建RNN模型
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
"""初始化函数中有三个参数,分别是输入张量最后一维的尺寸大小,
隐层张量最后一维的尺寸大小, 输出张量最后一维的尺寸大小"""
super(RNN, self).__init__()
# 传入隐含层尺寸大小
self.hidden_size = hidden_size
# 构建从输入到隐含层的线性变化, 这个线性层的输入尺寸是input_size + hidden_size
# 这是因为在循环网络中, 每次输入都有两部分组成,分别是此时刻的输入xt和上一时刻产生的输出ht-1.
# 这个线性层的输出尺寸是hidden_size
self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
# 构建从输入到输出层的线性变化, 这个线性层的输入尺寸还是input_size + hidden_size
# 这个线性层的输出尺寸是output_size.
self.i2o = nn.Linear(input_size + hidden_size, output_size)
# 最后需要对输出做softmax处理, 获得结果.
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden):
"""在forward函数中, 参数分别是规定尺寸的输入张量, 以及规定尺寸的初始化隐层张量"""
# 首先使用torch.cat将input与hidden进行张量拼接
combined = torch.cat((input, hidden), 1)
# 通过输入层到隐层变换获得hidden张量
hidden = self.i2h(combined)
# 通过输入到输出层变换获得output张量
output = self.i2o(combined)
# 对输出进行softmax处理
output = self.softmax(output)
# 返回输出张量和最后的隐层结果
return output, hidden
def initHidden(self):
"""隐层初始化函数"""
# 将隐层初始化成为一个1xhidden_size的全0张量
return torch.zeros(1, self.hidden_size)
input_size = 768
hidden_size = 128
n_categories = 2 # ner审核通过或者不通过
input = torch.rand(1, input_size)
hidden = torch.rand(1, hidden_size)
from RNN_MODEL import RNN
rnn = RNN(input_size, hidden_size, n_categories)
outputs, hidden = rnn(input, hidden)
print("outputs:", outputs)
print("hidden:", hidden)
outputs: tensor([[-0.7858, -0.6084]], grad_fn=<LogSoftmaxBackward>) # [1, 2]
hidden: tensor([[-4.8444e-01, -5.9609e-02, 1.7870e-01,
-1.6553e-01, ... , 5.6711e-01]], grad_fn=<AddmmBackward>)) # [1, 128]
- 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]])
模型训练
- 第一步: 构建随机选取数据函数
import pandas as pd
import random
from bert_chinese_encode import get_bert_encode_for_single
import torch
# 读取数据
train_data_path = './train_data.csv'
train_data = pd.read_csv(train_data_path, header = None, sep = '\t', encoding = 'utf-8')
trian_data = train_data.values.tolist()
def randomTrainingExample(train_data):
"""随机选取数据函数, train_data是训练集的列表形式数据"""
# 从train_data随机选择一条数据
category, line = random.choice(train_data)
# 将里面的文字使用bert进行编码, 获取编码后的tensor类型数据
line_tensor = get_bert_encode_for_single(line)
# 将分类标签封装成tensor
category_tensor = torch.tensor([int(category)])
# 返回四个结果
return category, line, category_tensor, line_tensor
- 第二步: 构建模型训练函数
# 选取损失函数为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()
- 第三步: 模型验证函数
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()
- 第四步: 调用训练和验证函数
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)
# 假定模型训练开始时间是10min之前
since = time.time() - 10*60
period = timeSince(since)
print(period)
10m 0s
调用训练和验证函数并打印日志
# 设置迭代次数为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
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
- 第五步: 绘制训练和验证的损失和准确率对照曲线
plt.title(“your title name”, y=-0.1)设置y位置可以将title设置在图像下方
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")
损失对照曲线一直下降, 说明模型能够从数据中获取规律,正在收敛, 准确率对照曲线中验证准确率一直上升,最终维持在0.98左右
- 第六步: 模型保存
# 保存路径
MODEL_PATH = './BERT_RNN.pth'
# 保存模型参数
torch.save(rnn.state_dict(), MODEL_PATH)
模型使用
- 模型预测的实现过程
import os
import torch
import torch.nn as nn
# 导入RNN模型结构
from RNN_MODEL import RNN
# 导入bert预训练模型编码函数
from bert_chinese_encode import get_bert_encode_for_single
# 预加载的模型参数路径
MODEL_PATH = './BERT_RNN.pth'
# 隐层节点数, 输入层尺寸, 类别数都和训练时相同即可
n_hidden = 128
input_size = 768
n_categories = 2
# 实例化RNN模型, 并加载保存模型参数
rnn = RNN(input_size, n_hidden, n_categories)
rnn.load_state_dict(torch.load(MODEL_PATH))
def _test(line_tensor):
"""模型测试函数, 它将用在模型预测函数中, 用于调用RNN模型并返回结果.它的参数line_tensor代表输入文本的张量表示"""
# 初始化隐层张量
hidden = rnn.initHidden()
# 与训练时相同, 遍历输入文本的每一个字符
for i in range(line_tensor.size()[0]):
# 将其逐次输送给rnn模型
output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
# 获得rnn模型最终的输出
return output
def predict(input_line):
"""模型预测函数, 输入参数input_line代表需要预测的文本"""
# 不自动求解梯度
with torch.no_grad():
# 将input_line使用bert模型进行编码
output = _test(get_bert_encode_for_single(input_line))
# 从output中取出最大值对应的索引, 比较的维度是1
_, topi = output.topk(1, 1)
# 返回结果数值
return topi.item()
input_line = "点瘀样尖针性发多"
result = predict(input_line)
print("result:", result)
result: 0
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]]))
- 模型批量预测的实现过程
def batch_predict(input_path, output_path):
"""批量预测函数, 以原始文本(待识别的命名实体组成的文件)输入路径
和预测过滤后(去除掉非命名实体的文件)的输出路径为参数"""
# 待识别的命名实体组成的文件是以疾病名称为csv文件名,
# 文件中的每一行是该疾病对应的症状命名实体
# 读取路径下的每一个csv文件名, 装入csv列表之中
csv_list = os.listdir(input_path)
# 遍历每一个csv文件
for csv in csv_list:
# 以读的方式打开每一个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_line = fr.readline()
# 使用模型进行预测
res = predict(input_line)
# 如果结果为1
if res:
# 说明审核成功, 写入到输出csv中
fw.write(input_line + "\n")
else:
pass
input_path = "/data/doctor_offline/structured/noreview/"
output_path = "/data/doctor_offline/structured/reviewed/"
batch_predict(input_path, output_path)
在输出路径下生成与输入路径等数量的同名csv文件, 内部的症状实体是被审核的可用实体