1.1 背景介绍
- 学习目标:
- 了解智能对话系统的相关背景知识.
- 掌握使用Unit对话API.
- 什么是智能对话系统?
- 随着人工智能技术的发展, 聊天机器人, 语音助手等应用在生活中随处可见, 比如百度的小度, 阿里的小蜜, 微软的小冰等等. 其目的在于通过人工智能技术让机器像人类一样能够进行智能回复, 解决现实中的各种问题.
- 从处理问题的角度来区分, 智能对话系统可分为:
- 任务导向型: 完成具有明确指向性的任务, 比如预定酒店咨询, 在线问诊等等.
- 非任务导向型: 没有明确目的, 比如算算术, 播放音乐, 回答问题.
- 我们的在线医生项目就是任务导向型的智能对话系统.
1.2 Unit对话API的使用
- 学习目标:
- 了解Unit平台的相关知识.
- 掌握调用Unit API的实现过程.
- Unit平台的相关知识:
- Unit平台是百度大脑开放的智能对话定制与服务平台, 也是当前最大的中文领域对话开放平台之一. Unit对注册用户提供免费的对话接口服务, 比如中文闲聊API, 百科问答API, 诗句生成API等, 通过这些API我们可以感受一下智能对话的魅力, 同时它也可以作为任务导向型对话系统无法匹配用户输入时的最终选择.
- Unit闲聊API演示:
用户输入 >>> "你好"
Unit回复 >>> "你好,想聊什么呢~"
用户输入 >>> "我想有一个女朋友!"
Unit回复 >>> "我也是想要一个女朋友~"
用户输入 >>> "晚吃啥呢想想"
Unit回复 >>> "想吃火锅"
- 调用Unit API的实现过程:
- 第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人.
- 第二步: 进行相关配置, 获得请求API接口需要的API Key与Secret Key.
- 第三步: 在服务器上编写API调用脚本并进行测试.
- 第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人.
ai.baidu.com
- 第二步: 进行相关配置, 获得请求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
- 代码位置: /data/doctor_online/main_serve/unit.py
- 调用:
python unit.py
- 输出效果:
请输入:你好啊
你好啊
用户输入 >>> 你好啊
Unit回复 >>> 你也好啊~
请输入:今天天气棒棒哒
今天天气棒棒哒
用户输入 >>> 今天天气棒棒哒
Unit回复 >>> 必须的
请输入:晚饭吃点什么?
晚饭吃点什么?
用户输入 >>> 晚饭吃点什么?
Unit回复 >>> 晚饭没吃,减肥
请输入:
-
本章总结:
- 学习了智能对话系统的相关背景知识:
- 什么是智能对话系统
- 从处理问题的目的来区分, 智能对话系统的分类
- 我们的在线医生项目就是任务导向型的智能对话系统.
- 学习了Unit平台的相关知识:
- Unit平台是百度大脑开放的智能对话定制与服务平台, 也是当前最大的中文领域对话开放平台之一.
- 学习了调用Unit API的实现过程:
- 第一步: 注册登录百度账户, 进入Unit控制台创建自己的机器人.
- 第二步: 进行相关配置, 获得请求API接口需要的API Key与Secret Key.
- 第三步: 在服务器上编写API调用脚本并进行测试.
- 学习了智能对话系统的相关背景知识:
2.1 在线医生的总体架构
- 学习目标:
- 了解在线医生项目的总体架构
- 项目整体架构图:
- 架构图分析:
- 整个项目分为: 在线部分和离线部分
- 在线部分包括: werobot服务模块, 主要逻辑服务模块, 句子相关模型服务模块, 会话管理模块(redis), 图数据库模块以及规则对话/Unit模块.
- 离线部分包括: 结构与非结构化数据采集模块, NER模型使用模块, 以及实体审核模型使用模块.
- 在线部分数据流: 从用户请求开始, 通过werobot服务, 在werobot服务内部请求主服务, 在主服务中将调用会话管理数据库redis, 调用句子相关模型服务, 以及调用图数据库, 最后将查询结果输送给对话规则模版或者使用Unit对话API回复.
- 离线部分数据流: 从数据采集开始, 将获得结构化和非结构化的数据, 对于结构化数据将直接使用实体审核模型进行审核, 然后写入图数据库; 对于非结构化数据, 将使用NER模型进行实体抽取, 然后通过实体审核后再写入图数据库.
2.2 总体架构中的工具介绍
- 学习目标:
- 了解总体架构中使用了哪些工具.
- 掌握总体架构中各个工具的简介, 作用, 安装和基本使用方法.
- 总体架构中使用的工具:
- Flask web服务框架
- Redis数据库
- Gunicorn服务组件
- Supervisor服务监控器
- Neo4j图数据库
- Flask web服务框架:
- 简介:
* Flask框架是当下最受欢迎的python轻量级框架, 也是pytorch官网指定的部署框架. Flask的基本模式为在程序里将一个视图函数分配给一个URL,每当用户访问这个URL时,系统就会执行给该URL分配好的视图函数,获取函数的返回值,其工作过程见图.
- 作用:
* 在项目中, Flask框架是主逻辑服务和句子相关模型服务使用的服务框架.
- 安装:
# 使用pip安装Flask
pip install Flask==1.1.1
- 基本使用方法:
# 导入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)
- 代码位置: /data/doctor_onine/main_serve/app.py
- 启动服务:
python app.py
- 启动效果:
* 通过浏览器打开地址http://127.0.0.1:5000/可看见打印了’Hello, World’.
- Redis数据库:
window下使用redis
先在redis目录下cmd: redis-server.exe redis.windows.conf 启动redis服务
再在redis目录下另开cmd,输入redis-cli -h 127.0.0.1 -p 6379 -a 密码
- 简介:
* Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API.
- 作用:
* 在项目中, Redis用于会话管理数据库, 保存用户聊天历史.
- 安装:
# 使用yum安装redis
yum install redis -y
- 基本使用方法:
* Redis支持四种数据结构的存储: String(字符串), Hash(散列), List(列表), Set(集合), Sorted Set(有序集合).
* 在这里我们将着重介绍如何在python中使用Hash(散列)进行读写.
- 安装python中的redis驱动:
# 使用pip进行安装
pip install redis
- 启动redis服务:
# 启动redis-server, 这里使用了默认配置, 端口是6379.
redis-server
- 在python中使用Hash(散列)进行读写:
如果redis 设置了密码
需要
REDIS_CONFIG = {
‘host’:‘127.0.0.1’,
‘port’:6379,
‘password’:‘123456’
}
否则报错:Authentication required.
# 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'))
- 输出效果:
再见, 董小姐
- Gunicorn服务组件:
- 简介:
* Gunicorn是一个被广泛使用的高性能的Python WSGI UNIX HTTP服务组件(WSGI: Web Server Gateway Interface),移植自Ruby的独角兽(Unicorn )项目,具有使用非常简单,轻量级的资源消耗,以及高性能等特点.
- 作用:
* 在项目中, Gunicorn和Flask框架一同使用, 能够开启服务, 处理请求,因其高性能的特点能够有效减少服务丢包率.
- 安装:
# 使用pip安装gunicorn
pip install gunicorn==20.0.4
- 基本使用方法:
# 使用其启动Flask服务:
gunicorn -w 1 -b 0.0.0.0:5000 app:app
# -w 代表开启的进程数, 我们只开启一个进程
# -b 服务的IP地址和端口
# app:app 是指执行的主要对象位置, 在app.py中的app对象
# 如果使其在后台运行可使用:
# nohup gunicorn -w 1 -b 0.0.0.0:5001 app:app &
- Supervisor服务监控:
- 简介:
* Supervisor是用Python开发的一个client/server服务,是Linux/Unix系统下的一个进程管理工具。它可以很方便的监听、启动、停止、重启一个或多个进程, 并守护这些进程。
- 作用:
* 在项目中, Supervisor用于监控和守护主要逻辑服务和redis数据库服务.
- 安装:
# 使用yum安装supervisor
yum install supervisor -y
- 基本使用方法:
# 编辑配置文件, 指明监控和守护的进程开启命令,
# 请查看/data/doctor_online/supervisord.conf文件
# 开启supervisor, -c用于指定配置文件
sueprvisord -c /data/doctor_online/main_server/supervisord.conf
# 查看监控的进程状态:
supervisorctl status
# main_server RUNNING pid 31609, uptime 0:32:20
# redis RUNNING pid 31613, uptime 0:32:18
# 关闭supervisor
supervisorctl shutdown
- 还可以通过浏览器查看可视化监控页面: http://0.0.0.0:9001
- Neo4j图数据库:
- 因为在项目中, Neo4j图数据库作为核心的存储和查询数据库, 后续课件中对其进行详细的介绍.
-
本章总结:
- 学习了架构图分析:
- 整个项目分为: 在线部分和离线部分
- 在线部分包括: werobot服务模块, 主要逻辑服务模块, 句子相关模型服务模块, 会话管理模块(redis), 图数据库模块以及规则对话/Unit模块.
- 离线部分包括: 结构与非结构化数据采集模块, NER模型使用模块, 以及实体审核模型使用模块.
- 学习了总体架构中使用的工具:
- Flask web服务框架
- Redis数据库
- Gunicorn服务组件
- Supervisor服务监控器
- Neo4j图数据库
- Flask web服务框架:
- 作用: 在项目中, Flask框架是主逻辑服务和句子相关模型服务使用的服务框架.
- Redis数据库:
- 作用: 在项目中, Redis用于会话管理数据库, 保存用户聊天历史.
- Gunicorn服务组件:
- 作用: 在项目中, Gunicorn和Flask框架一同使用, 能够开启服务, 处理请求,因其高性能的特点能够有效减少服务丢包率.
- Supervisor服务监控:
- 作用: 在项目中, Supervisor用于监控和守护主要逻辑服务和redis数据库服务.
- 学习了架构图分析:
3.1 neo4j简介
- 学习目标:
- 了解neo4j图数据库的简介, 版本说明.
- 了解节点, 关系,属性,标签的有关概念.
警告: ERROR! Neo4j cannot be started using java version 1.7.0_75
解决: java JDK下载
并重新配置java JDK 环境变量后,可以正常启动neo4j
浏览器登录http://localhost:7474/browser/ 初始账号密码均为:neo4j
若出现问题:“ NotFoundError:无法在’Node’上执行’removeChild’:要删除的节点不是该节点的子节点。”并且应用程序无法恢复。 更换chrome或者其他浏览器
- neo4j简介:
- neo4j是由Java实现的开源NoSQL图数据库.自从2003年开始研发, 到2007年发布第一版, 最新版本为3.3.5, neo4j现如今已经被各行各业的数十万家公司和组织采用.
- neo4j实现了专业数据库级别的图数据模型的存储. 与普通的图处理或内存级数据库不同, neo4j提供了完整的数据库特性, 包括ACID事物的支持, 集群支持, 备份与故障转移等. 这使其适合于企业级生产环境下的各种应用.
- neo4j的版本说明:
- 企业版: 需要高额的付费获得授权, 提供高可用, 热备份等性能.
- 社区开源版: 免费使用, 但只能单点运行.
- neo4j图形数据库的有关概念:
- 节点
* 节点是主要的数据元素, 节点通过关系连接到其他节点, 节点可以具有一个或多个属性
(即存储为键/值对的属性), 节点有一个或多个标签, 用于描述其在图表中的作用. 示例: Person>节点.
* 可以将节点类比为关系型数据库中的表, 对应的标签可以类比为不同的表名, 属性就是表中的列.
- 关系
* 关系连接两个节点, 关系是方向性的, 关系可以有一个或多个属性(即存储为键/值对的
属性).
- 属性
* 属性是命名值, 其中名称(或键)是字符串, 属性可以被索引和约束, 可以从多个属性创
建复合索引.
- 标签
* 标签用于组节点到集, 节点可以具有多个标签, 对标签进行索引以加速在图中查找节点.
3.2 neo4j图数据库的安装
- 学习目标:
- 掌握neo4j图数据库的安装流程及其可视化后台的登陆…
- neo4j图数据库的安装流程:
- 第一步: 将neo4j安装信息载入到yum检索列表.
- 第二步: 使用yum install命令安装.
- 第三步: 修改配置文件内容 /etc/neo4j/neo4j.conf.
- 第四步: 启动neo4j数据库.
- 第一步: 将neo4j安装信息载入到yum检索列表
cd /tmp
wget http://debian.neo4j.org/neotechnology.gpg.key
rpm --import neotechnology.gpg.key
cat <<EOF> /etc/yum.repos.d/neo4j.repo
# 写入下面内容
[neo4j]
name=Neo4j RPM Repository
baseurl=http://yum.neo4j.org/stable
enabled=1
gpgcheck=1
- 第二步: 使用yum install命令安装
yum install neo4j-3.3.5
- 第三步: 修改配置文件默认在/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=0.0.0.0:7687
- 第四步: 启动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.
- neo4j的可视化管理后台登陆:
- 访问地址: http://0.0.0.0:7474.
- ConnectURL: bolt://0.0.0.0:7687
- Username: neo4j
- Password: neo4j (默认)
-
小节总结:
- 学习了neo4j图数据库的安装流程:
- 第一步: 将neo4j安装信息载入到yum检索列表.
- 第二步: 使用yum install命令安装.
- 第三步: 修改配置文件内容 /etc/neo4j/neo4j.conf.
- 第四步: 启动neo4j数据库.
- 学习了neo4j的可视化管理后台登陆:
- 访问地址: http://0.0.0.0:7474.
- ConnectURL: bolt://0.0.0.0:7687
- Username: neo4j
- Password: neo4j (默认)
- 学习了neo4j图数据库的安装流程:
3.3 Cypher介绍与使用
- 学习目标
- 了解Cypher的基本概念.
- 掌握Cypher的基本命令和语法.
- Cypher的基本概念:
- Cypher是neo4j图数据的查询语言, 类似于mysql数据库的sql语句, 但是它允许对图形进行富有表现力和有效的查询和更新.
- Cypher的基本命令和语法:
- create命令
- match命令
- merge命令
- relationship关系命令
- where命令
- delete命令
- sort命令
- 字符串函数
- 聚合函数
- index索引命令
- 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()函数
- toLower()函数
- substring()函数
- replace()函数
- 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()函数
- max()函数
- min()函数
- sum()函数
- avg()函数
- 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)
- 效果:
- 索引index
- Neo4j支持在节点或关系属性上的索引, 以提高查询的性能.
- 可以为具有相同标签名称的所有节点的属性创建索引.
- 创建索引: 使用create index on来创建索引.
- 演示:
# 创建节点Employee上面属性id的索引
CREATE INDEX ON:Employee(id)
- 效果:
- 删除索引: 使用drop index on来删除索引.
- 演示:
# 删除节点Employee上面属性id的索引
DROP INDEX ON:Employee(id)
- 效果:
-
小节总结:
- 学习了Cypher的基本概念:
- Cypher是neo4j图数据的查询语言, 类似于mysql数据库的sql语句, 但是它允许对图形进行富有表现力和有效的查询和更新.
- Cypher的基本命令和语法:
- create命令
- match命令
- merge命令
- relationship关系命令
- where命令
- delete命令
- sort命令
- 字符串函数
- 聚合函数
- index索引命令
- create命令: 创建图数据中的节点.
- CREATE (e:Employee{id:222, name:‘Bob’, salary:6000, deptnp:12})
- match命令: 匹配(查询)已有数据.
- MATCH (e:Employee) RETURN e.id, e.name, e.salary, e.deptno
- merge命令: 若节点存在, 则等效与match命令; 节点不存在, 则等效于create命令.
- MERGE (e:Employee {id:145, name:‘Lucy’, salary:7500, deptno:12})
- 使用create创建关系: 必须创建有方向性的关系, 否则报错.
- CREATE (p1:Profile1)-[r:Buy]->(p2:Profile2)
- 使用merge创建关系: 可以创建有/无方向性的关系.
- MERGE (p1:Profile1)-[r:miss]-(p2:Profile2)
- where命令: 类似于SQL中的添加查询条件.
- MATCH (e:Employee) WHERE e.id=123 RETURN e
- delete命令: 删除节点/关系及其关联的属性.
- MATCH (c1:CreditCard)-[r]-(c2:Customer) DELETE c1, r, c2
- sort命令: Cypher命令中的排序使用的是order by.
- MATCH (e:Employee) RETURN e.id, e.name, e.salary, e.deptno ORDER BY e.id
- 字符串函数:
- toUpper()函数
- toLower()函数
- substring()函数
- replace()函数
- 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()函数: 返回一个子字符串.
- MATCH (e:Employee) RETURN e.id, substring(e.name,0,2), e.salary, e.deptno
- replace()函数: 替换掉子字符串.
- MATCH (e:Employee) RETURN e.id, replace(e.name,e.name,e.name + “_HelloWorld”), e.salary, e.deptno
- 聚合函数
- count()函数
- max()函数
- min()函数
- sum()函数
- avg()函数
- count()函数: 返回由match命令匹配成功的条数.
- MATCH (e:Employee) RETURN count( * )
- max()函数: 返回由match命令匹配成功的记录中的最大值.
- MATCH (e:Employee) RETURN max(e.salary)
- min()函数: 返回由match命令匹配成功的记录中的最小值.
- MATCH (e:Employee) RETURN min(e.salary)
- sum()函数: 返回由match命令匹配成功的记录中某字段的全部加和值.
- MATCH (e:Employee) RETURN sum(e.salary)
- avg()函数: 返回由match命令匹配成功的记录中某字段的平均值.
- MATCH (e:Employee) RETURN avg(e.salary)
- 索引index
- Neo4j支持在节点或关系属性上的索引, 以提高查询的性能.
- 可以为具有相同标签名称的所有节点的属性创建索引.
- 创建索引: 使用create index on来创建索引.
- CREATE INDEX ON:Employee(id)
- 删除索引: 使用drop index on来删除索引.
- DROP INDEX ON:Employee(id)
- 学习了Cypher的基本概念:
3.4 在Python中使用neo4j
- 学习目标
- 了解python中neo4j-driver的相关知识.
- 掌握neo4j中事务概念和操作方法.
- neo4j-driver简介:
- neo4j-driver是一个python中的package, 作为python中neo4j的驱动, 帮助我们在python程序中更好的使用图数据库.
- neo4j-driver的安装:
pip install neo4j-driver
- neo4j-driver使用演示:
config.py
# 设置neo4j图数据库的配置信息
NEO4J_CONFIG = {
"uri": "bolt://127.0.0.1:7687",
"auth": ("username", "password"),
"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")
- 输出效果:
查询多个节点多个属性
match(c:Cat) - [r] - (m:Mouse) return c.name, m.name
-
小节总结:
- 学习了neo4j-driver简介:
- neo4j-driver是一个python中的package, 作为python中neo4j的驱动, 帮助我们在python程序中更好的使用图数据库.
- 学习了neo4j-driver的安装和使用方法.
- 学习了事务的概念:
- 如果一组数据库操作要么全部发生要么一步也不执行,我们称该组处理步骤为一个事务, 它是数据库一致性的保证.
- 学习了如何使用事务来向图数据库中写入数据.
- 学习了neo4j-driver简介:
4.1 离线部分简要分析
- 学习目标:
- 了解离线部分的数据流水线以及组成部分.
- 了解各个组成部分的作用.
- 离线部分架构图:
- 离线部分架构展开图:
- 离线部分简要分析:
- 根据架构展开图图,离线部分可分为两条数据流水线,分别用于处理结构化数据和非结构化数据. 这里称它们为结构化数据流水线和非结构化数据流水线.
- 结构化数据流水线的组成部分:
- 结构化数据爬虫: 从网页上抓取结构化的有关医学命名实体的内容.
- 结构化数据的清洗: 对抓取的内容进行过滤和清洗, 以保留需要的部分.
- 命名实体审核: 对当前命名实体进行审核, 来保证这些实体符合我们的要求.
- 命名实体写入数据库: 将审核后的命名实体写入数据库之中, 供在线部分使用.
- 非结构化数据流水线的组成部分:
- 非结构化数据爬虫: 从网页上抓取非结构化的包含医学命名实体的文本.
- 非结构化数据清洗: 对非结构化数据进行过滤和清洗, 以保留需要的部分.
- 命名实体识别: 使用模型从非结构化文本中获取命名实体.
- 命名实体审核: 对当前命名实体进行审核, 来保证这些实体符合我们的要求.
- 命名实体写入数据库: 将审核后的命名实体写入数据库之中, 供在线部分使用.
- 说明:
- 因为本项目是以AI为核心的项目, 因为结构化与非结构化的数据爬虫和清洗部分的内容这里不做介绍, 但同学们要知道我们的数据来源.
4.2 结构化数据流水线
- 学习目标:
- 了解需要进行命名实体审核的数据内容.
- 掌握结构化数据流水线中命名实体审核的过程.
- 掌握结构化数据流水线中命名实体写入的过程.
- 需要进行命名实体审核的数据内容:
...
踝部急性韧带损伤.csv
踝部扭伤.csv
踝部骨折.csv
蹄铁形肾.csv
蹼状阴茎.csv
躁狂抑郁症.csv
躁狂症.csv
躁郁症.csv
躯体形式障碍.csv
躯体感染伴发的精神障碍.csv
躯体感染所致精神障碍.csv
躯体感觉障碍.csv
躯体疾病伴发的精神障碍.csv
转换性障碍.csv
转移性小肠肿瘤.csv
转移性皮肤钙化病.csv
转移性肝癌.csv
转移性胸膜肿瘤.csv
转移性骨肿瘤.csv
轮状病毒性肠炎.csv
轮状病毒所致胃肠炎.csv
软产道异常性难产.csv
...
- 每个csv文件的名字都是一种疾病名.
- 文件位置: /data/doctor_offline/structured/noreview/
- 以躁狂症.csv为例, 有如下内容:
躁郁样
躁狂
行为及情绪异常
心境高涨
情绪起伏大
技术狂躁症
攻击行为
易激惹
思维奔逸
控制不住的联想
精神运动性兴奋
- csv文件的内容是该疾病对应的症状, 每种症状占一行.
- 文件位置: /data/doctor_offline/structured/noreview/躁狂症.csv
- 进行命名实体审核:
- 进行命名实体审核的工作我们这里使用AI模型实现, 包括训练数据集, 模型训练和使用的整个过程, 因此这里内容以独立一章的形成呈现给大家, 具体参见[第五章: 命名实体审核任务].
- 删除审核后的可能存在的空文件:
# Linux 命令-- 删除当前文件夹下的空文件
find ./ -name "*" -type f -size 0c | xargs -n 1 rm -f
- 代码位置: 在/data/doctor_offline/structured/reviewed/目录下执行.
- 命名实体写入数据库:
- 将命名实体写入图数据库的原因:
* 写入的数据供在线部分进行查询,根据用户输入症状来匹配对应疾病.
- 将命名实体写入图数据库代码:
# 引入相关包
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)
fileinput UnicodeDecodeError: gbk codec cant decode byte 0x80 in position 2: illegal multibyte sequence
解决: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))))
- 输出效果:
* 通过可视化管理后台查看写入效果.
MATCH(a:Disease) - [r:dis_to_sym] - (b:Symptom) RETURN a, r, b LIMIT 25
# 或者
MATCH p = () -[r : dis_to_sym] - () RETURN p LIMIT 25
4.3 非结构化数据流水线
- 学习目标:
- 了解需要进行命名实体识别的数据内容.
- 掌握非结构化数据流水线中命名实体识别的过程.
- 掌握非结构化数据流水线中命名实体审核的过程.
- 掌握非结构化数据流水线中命名实体写入的过程.
- 需要进行命名实体识别的数据内容:
...
麻疹样红斑型药疹.txt
麻疹病毒肺炎.txt
麻痹性臂丛神经炎.txt
麻风性周围神经病.txt
麻风性葡萄膜炎.txt
黄体囊肿.txt
黄斑囊样水肿.txt
黄斑裂孔性视网膜脱离.txt
黄韧带骨化症.txt
黏多糖贮积症.txt
黏多糖贮积症Ⅰ型.txt
黏多糖贮积症Ⅱ型.txt
黏多糖贮积症Ⅵ型.txt
黏多糖贮积症Ⅲ型.txt
黏多糖贮积症Ⅶ型.txt
黑色丘疹性皮肤病.txt
...
- 每个txt文件的名字都是一种疾病名.
- 文件位置: /data/doctor_offline/unstructured/norecognite/
- 以黑色丘疹性皮肤病.txt为例, 有如下内容:
初呈微小、圆形、皮肤色或黑色增深的丘疹,单个或少数发生于颌部或颊部,皮损逐渐增大增多,数年中可达数百,除眶周外尚分布于面部、颈部和胸上部。皮损大小形状酷似脂溢性角化病及扁平疣鶒。不发生鳞屑,结痂和溃疡,亦无瘙痒及其他主观症状
- txt中是对该疾病症状的文本描述.
- 文件位置: /data/doctor_offline/unstructured/norecognite/黑色丘疹性皮肤病.txt
- 进行命名实体识别:
- 进行命名实体识别的工作我们这里使用AI模型实现, 包括模型训练和使用的整个过程, 因此内容以独立一章的形成呈现给大家, 具体内容在[第六章: 命名实体识别任务]
- 进行命名实体审核:
- 同4.2 结构化数据流水线中的命名实体审核.
- 命名实体写入数据库:
- 同4.2 结构化数据流水线中的命名实体写入数据库.
-
本章总结:
- 学习了离线部分的数据流水线以及组成部分.
- 根据架构展开图图,离线部分可分为两条数据流水线,分别用于处理结构化数据和非结构化数据. 这里称它们为结构化数据流水线和非结构化数据流水线.
- 结构化数据流水线的组成部分:
- 结构化数据爬虫: 从网页上抓取结构化的有关医学命名实体的内容.
- 结构化数据的清洗: 对抓取的内容进行过滤和清洗, 以保留需要的部分.
- 命名实体审核: 对当前命名实体进行审核, 来保证这些实体符合我们的要求.
- 命名实体写入数据库: 将审核后的命名实体写入数据库之中, 供在线部分使用.
- 非结构化数据流水线的组成部分:
- 非结构化数据爬虫: 从网页上抓取非结构化的包含医学命名实体的文本.
- 非结构化数据清洗: 对非结构化数据进行过滤和清洗, 以保留需要的部分.
- 命名实体识别: 使用模型从非结构化文本中获取命名实体.
- 命名实体审核: 对当前命名实体进行审核, 来保证这些实体符合我们的要求.
- 命名实体写入数据库: 将审核后的命名实体写入数据库之中, 供在线部分使用.
- 学习了需要进行命名实体审核的数据内容.
- 学习了结构化/非结构化数据流水线中命名实体审核的过程.
- 学习了结构化/非结构化数据流水线中命名实体写入的过程.
- 学习了需要进行命名实体识别的数据内容.
- 非结构化数据流水线中命名实体识别的过程.
- 学习了离线部分的数据流水线以及组成部分.
5.1 任务介绍与模型选用
- 学习目标:
- 了解命名实体审核任务的相关知识.
- 了解选用的模型及其原因.
- NE审核任务:
- 一般在实体进入数据库存储前, 中间都会有一道必不可少的工序, 就是对识别出来的实体进行合法性的检验, 即命名实体(NE)审核任务. 它的检验过程不使用上下文信息, 更关注于字符本身的组合方式来进行判断, 本质上,它是一项短文本二分类问题.
- 选用的模型及其原因:
- 针对短文本任务, 无须捕捉长距离的关系, 因此我们使用了传统的RNN模型来解决, 性能和效果可以达到很好的均衡.
- 短文本任务往往适合使用字嵌入的方式, 但是如果你的训练集不是很大,涉及的字数有限, 那么可以直接使用预训练模型的字向量进行表示即可. 我们这里使用了bert-chinese预训练模型来获得中文汉字的向量表示.
5.2 训练数据集
- 学习目标:
- 了解训练数据集的样式及其相关解释.
- 掌握将数据集加载到内存中的过程.
- 训练数据集的样式:
1 手内肌萎缩
0 缩萎肌内手
1 尿黑酸
0 酸黑尿
1 单眼眼前黑影
0 影黑前眼眼单
1 忧郁
0 郁忧
1 红细胞寿命缩短
0 短缩命寿胞细红
1 皮肤黏蛋白沉积
0 积沉白蛋黏肤皮
1 眼神异常
0 常异神眼
1 阴囊坠胀痛
0 痛胀坠囊阴
1 动脉血氧饱和度降低
0 低降度和饱氧血脉动
- 数据集的相关解释:
- 这些训练集中的正样本往往是基于人工审核的标准命名实体.
- 数据集中的第一列代表标签, 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])
- 代码位置: /data/doctor_offline/review_model/train.py
- 输出效果:
# 正负标签比例
{1: 5740, 0: 5740}
# 取出10条训练数据查看
[[1, '枕部疼痛'], [0, '痛疼部枕'], [1, '陶瑟征阳性'], [0, '性阳征瑟陶'], [1, '恋兽型性变态'], [0, '态变性型兽恋'], [1, '进食困难'], [0, '难困食进'], [1, '会阴瘘管或窦道形成'], [0, '成形道窦或管瘘阴会']]
- 小节总结:
- 学习了训练数据集的样式及其相关解释.
- 学习了将数据集加载到内存中的过程.
5.3 BERT中文预训练模型
- 学习目标:
- 了解BERT中文预训练模型的有关知识和作用.
- 掌握使用BERT中文预训练模型对句子编码的过程.
- BERT中文预训练模型:
- BERT模型整体架构基于Transformer模型架构, BERT中文预训练模型的解码器和编码器具有12层, 输出层中的线性层具有768个节点, 即输出张量最后一维的维度是768. 它使用的多头注意力机制结构中, 头的数量为12, 模型总参数量为110M. 同时, 它在中文简体和繁体上进行训练, 因此适合中文简体和繁体任务.
- BERT中文预训练模型作用:
- 在实际的文本任务处理中, 有些训练语料很难获得, 他们的总体数量和包含的词汇总数都非常少, 不适合用于训练带有Embedding层的模型, 但这些数据中却又蕴含这一些有价值的规律可以被模型挖掘, 在这种情况下,使用预训练模型对原始文本进行编码是非常不错的选择, 因为预训练模型来自大型语料, 能够使得当前文本具有意义, 虽然这些意义可能并不针对某个特定领域, 但是这种缺陷可以使用微调模型来进行弥补.
- 使用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
- 代码位置: /data/doctor_offline/review_model/bert_chinese_encode.py
- 输入参数:
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])
注意:torch.hub.load无法下载时,利用迅雷等通过链接中 的地址进行下载model.bin config.json vocab.txt并更改名称
from transformers import BertModel, BertTokenizer
import torch
from transformers import BertModel, BertTokenizer
# 通过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')
#
model = BertModel.from_pretrained('./bert-base-chinese')
tokenizer = BertTokenizer.from_pretrained('./bert-base-chinese/')
def get_bert_encode_for_single(text):
"""
使用bert-base-chinese对中文文本进行编码
:param text: 进行编码的中文文本
:return: 编号后的张量
"""
# 使用字符映射器对每个汉字进行映射
# 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)
print('encoded_layers_shape:{}'.format(encoded_layers.shape))
# 模型的输出均为三维张量,第一维为1,只提取后两个维度张量,需要[0]来降维
encoded_layers = encoded_layers[0]
return encoded_layers
if __name__ == '__main__':
text = '你好, 周杰伦'
outputs = get_bert_encode_for_single(text)
print('outputs:{}'.format(outputs))
print('outputs.shape:{}'.format(outputs.shape))
tensor([[ 872, 1962, 117, 1453, 3345, 840]])
encoded_layers_shape:torch.Size([1, 6, 768])
outputs: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.3014e-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.4204e-02, -1.7930e-01],
[-1.3159e-01, -3.0048e-01, -2.4193e-01, ..., -4.5757e-02,
-2.0958e-01, -1.0649e-01],
[-4.0006e-01, -3.4410e-01, -3.9786e-05, ..., 1.9081e-01,
1.7006e-01, -3.6221e-01]])
outputs.shape:torch.Size([6, 768])
-
小节总结:
- 学习了BERT中文预训练模型的有关知识:
- BERT模型整体架构基于Transformer模型架构, BERT中文预训练模型的解码器和编码器具有12层, 输出层中的线性层具有768个节点, 即输出张量最后一维的维度是768. 它使用的多头注意力机制结构中, 头的数量为12, 模型总参数量为110M. 同时, 它在中文简体和繁体上进行训练, 因此适合中文简体和繁体任务.
- 学习了BERT中文预训练模型的作用:
- 在实际的文本任务处理中, 有些训练语料很难获得, 他们的总体数量和包含的词汇总数都非常少, 不适合用于训练带有Embedding层的模型, 但这些数据中却又蕴含这一些有价值的规律可以被模型挖掘, 在这种情况下, 使用预训练模型对原始文本进行编码是非常不错的选择, 因为预训练模型来自大型语料, 能够使得当前文本具有意义, 虽然这些意义可能并不针对某个特定领域, 但是这种缺陷可以使用微调模型来进行弥补.
- 学习了使用BERT中文预训练模型对句子编码的函数: get_bert_encode_for_single(text)
- 学习了BERT中文预训练模型的有关知识:
5.4 构建RNN模型
- 学习目标:
- 学习RNN模型的内部结构及计算公式.
- 掌握RNN模型的实现过程.
- 传统RNN的内部结构图:
- 结构解释图:
- 内部结构分析:
* 我们把目光集中在中间的方块部分, 它的输入有两部分, 分别是h(t-1)以及x(t), 代表上一时间步的隐层输出, 以及此时间步的输入, 它们进入RNN结构体后, 会"融合"到一起, 这种融合我们根据结构解释可知, 是将二者进行拼接, 形成新的张量[x(t), h(t-1)], 之后这个新的张量将通过一个全连接层(线性层), 该层>使用tanh作为激活函数, 最终得到该时间步的输出h(t), 它将作为下一个时间步的>输入和x(t+1)一起进入结构体. 以此类推.
- 内部结构过程演示:
- 根据结构分析得出内部计算公式:
- 激活函数tanh的作用:
* 用于帮助调节流经网络的值, tanh函数将值压缩在-1和1之间.
- 构建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)
- 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]])
- 代码位置: /data/doctor_offline/review_model/RNN_MODEL.py
- 实例化参数:
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]
- 小节总结:
- 学习了RNN模型的内部结构及计算公式.
- 学习并实现了RNN模型的类: class RNN(nn.Module).
5.5 进行模型训练
- 学习目标:
- 了解进行模型训练的步骤.
- 掌握模型训练中每个步骤的实现过程.
- 进行模型训练的步骤:
- 第一步: 构建随机选取数据函数.
- 第二步: 构建模型训练函数.
- 第三步: 构建模型验证函数.
- 第四步: 调用训练和验证函数.
- 第五步: 绘制训练和验证的损失和准确率对照曲线.
- 第六步: 模型保存.
- 第一步: 构建随机选取数据函数
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
- 代码位置: /data/doctor_offline/review_model/train.py
- 输入参数:
# 将数据集加载到内存获得的train_data
- 调用:
# 选择10条数据进行查看
for i in range(10):
category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
print('category =', category, '/ line =', line)
- 输出效果:
category = 1 / line = 触觉失调
category = 0 / line = 颤震性理生
category = 0 / line = 征压血高娠妊
category = 1 / line = 食欲减退
category = 0 / line = 血淤道肠胃
category = 0 / line = 形畸节关
category = 0 / line = 咳呛水饮
category = 0 / line = 症痣巨
category = 1 / line = 昼盲
category = 1 / line = 眼神异常
- 第二步: 构建模型训练函数
# 选取损失函数为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()
- 代码位置: /data/doctor_offline/review_model/train.py
- 第三步: 模型验证函数
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()
- 代码位置: /data/doctor_offline/review_model/train.py
- 第四步: 调用训练和验证函数
- 构建时间计算函数:
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)
- 代码位置: /data/doctor_offline/review_model/train.py
- 输入参数:
# 假定模型训练开始时间是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
- 代码位置: /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
- 第五步: 绘制训练和验证的损失和准确率对照曲线
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")
- 代码位置: /data/doctor_offline/review_model/train.py
- 训练和验证损失对照曲线:
- 训练和验证准确率对照曲线:
- 分析:
* 损失对照曲线一直下降, 说明模型能够从数据中获取规律,正在收敛, 准确率对照曲线中验证准确率一直上升,最终维持在0.98左右.
- 第六步: 模型保存
# 保存路径
MODEL_PATH = './BERT_RNN.pth'
# 保存模型参数
torch.save(rnn.state_dict(), MODEL_PATH)
- 代码位置: /data/doctor_offline/review_model/train.py
- 输出效果:
* 在/data/doctor_offline/review_model/路径下生成BERT_RNN.pth文件.
- 小节总结:
- 学习了进行模型训练的步骤:
- 第一步: 构建随机选取数据函数.
- 第二步: 构建模型训练函数.
- 第三步: 构建模型验证函数.
- 第四步: 调用训练和验证函数.
- 第五步: 绘制训练和验证的损失和准确率对照曲线.
- 第六步: 模型保存.
- 学习了进行模型训练的步骤:
5.6 模型使用
- 学习目标:
- 掌握模型预测的实现过程.
- 掌握模型批量预测的实现过程.
- 模型预测的实现过程:
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()
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]]))
- 代码位置: /data/doctor_offline/review_model/predict.py
- 输入参数:
input_line = "点瘀样尖针性发多"
- 调用:
result = predict(input_line)
print("result:", result)
- 输出效果:
result: 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
- 代码位置: /data/doctor_offline/review_model/predict.py
- 输入参数:
input_path = "/data/doctor_offline/structured/noreview/"
output_path = "/data/doctor_offline/structured/reviewed/"
- 调用:
batch_predict(input_path, output_path)
- 输出效果:
- 在输出路径下生成与输入路径等数量的同名csv文件, 内部的症状实体是被审核的可用实体.
- 小节总结:
- 学习并实现了模型预测的函数: predict(input_line).
- 学习并实现了模型批量预测的函数: batch_predict(input_path, output_path)