赞
踩
什么是智能对话系统?
随着人工智能技术的发展, 聊天机器人, 语音助手等应用在生活中随处可见, 比如百度的小度, 阿里的小蜜, 微软的小冰等等. 其目的在于通过人工智能技术让机器像人类一样能够进行智能回复, 解决现实中的各种问题.
从处理问题的角度来区分, 智能对话系统可分为:
我们的在线医生项目就是任务导向型的智能对话系统.
Unit平台是百度大脑开放的智能对话定制与服务平台, 也是当前最大的中文领域对话开放平台之一. Unit对注册用户提供免费的对话接口服务, 比如中文闲聊API, 百科问答API, 诗句生成API等, 通过这些API我们可以感受一下智能对话的魅力, 同时它也可以作为任务导向型对话系统无法匹配用户输入时的最终选择.
Unit闲聊API演示:
用户输入 >>> "你好"
Unit回复 >>> "你好,想聊什么呢~"
用户输入 >>> "我想有一个女朋友!"
Unit回复 >>> "我也是想要一个女朋友~"
用户输入 >>> "晚吃啥呢想想"
Unit回复 >>> "想吃火锅"
调用Unit 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
代码位置: /data/doctor_online/main_serve/unit.py
调用:
python unit.py
请输入:你好啊
你好啊
用户输入 >>> 你好啊
Unit回复 >>> 你也好啊~
请输入:今天天气棒棒哒
今天天气棒棒哒
用户输入 >>> 今天天气棒棒哒
Unit回复 >>> 必须的
请输入:晚饭吃点什么?
晚饭吃点什么?
用户输入 >>> 晚饭吃点什么?
Unit回复 >>> 晚饭没吃,减肥
请输入:
本章总结:
整个项目分为: 在线部分和离线部分
总体架构中使用的工具:
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
如果在阿里云部署,需要在安全组开放5000端口。服务器防火墙开放5000端口
启动效果: 通过浏览器打开地址http://0.0.0.0:5000可看见打印了’Hello, World’.
Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API.
在项目中, Redis用于会话管理数据库, 保存用户聊天历史.
安装:
# 使用yum安装redis
yum install redis -y
基本使用方法:
安装python中的redis驱动:
# 使用pip进行安装
pip install redis
启动redis服务:
# 启动redis-server, 这里使用了默认配置, 端口是6379.
redis-server
在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'))
输出效果:
再见, 董小姐
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对象
(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
如果使其在后台运行可使用:
# 如果使其在后台运行可使用:
# nohup gunicorn -w 1 -b 0.0.0.0:5000 app:app &
Supervisor是用Python开发的一个client/server服务,是Linux/Unix系统下的一个进程管理工具。它可以很方便的监听、启动、停止、重启一个或多个进程, 并守护这些进程。
作用: * 在项目中, Supervisor用于监控和守护主要逻辑服务和redis数据库服务.
安装:
# 使用yum安装supervisor
yum install supervisor -y
基本使用方法:
# 编辑配置文件, 指明监控和守护的进程开启命令,
# 请查看/data/doctor_online/supervisord.conf文件
# 开启supervisor, -c用于指定配置文件
supervisord -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
(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>
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]#
关闭supervisor:supervisorctl shutdown
# 关闭supervisor
(base) [root@whx main_server]# supervisorctl shutdown
Shut down
(base) [root@whx main_server]#
如果程序都处于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]#
还可以通过浏览器查看可视化监控页面: http://0.0.0.0:9001(如果在阿里云部署,则使用公网IP)
离线部分架构展开图:
离线部分简要分析:
说明:
需要进行命名实体审核的数据内容:
... 踝部急性韧带损伤.csv 踝部扭伤.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
进行命名实体审核的工作我们这里使用AI模型实现, 包括训练数据集, 模型训练和使用的整个过程, 因此这里内容以独立一章的形成呈现给大家, 具体参见第五章: 命名实体审核任务.
以躁狂症.csv为例, 审核后的内容只剩下一行内容:
躁郁样
命名实体审核步骤完成之后,删除审核后的可能存在的空文件:
# 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)))) # 过滤掉所有长度异常的症状名 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)
代码位置: 在/data/doctor_offline/util/neo4j_util.py
通过可视化管理后台查看写入效果:
需要进行命名实体识别的数据内容:
... 麻疹样红斑型药疹.txt 麻疹病毒肺炎.txt 麻痹性臂丛神经炎.txt 麻风性周围神经病.txt 麻风性葡萄膜炎.txt 黄体囊肿.txt 黄斑囊样水肿.txt 黄斑裂孔性视网膜脱离.txt 黄韧带骨化症.txt 黏多糖贮积症.txt 黏多糖贮积症Ⅰ型.txt 黏多糖贮积症Ⅱ型.txt 黏多糖贮积症Ⅵ型.txt 黏多糖贮积症Ⅲ型.txt 黏多糖贮积症Ⅶ型.txt 黑色丘疹性皮肤病.txt ...
初呈微小、圆形、皮肤色或黑色增深的丘疹,单个或少数发生于颌部或颊部,皮损逐渐增大增多,数年中可达数百,除眶周外尚分布于面部、颈部和胸上部。皮损大小形状酷似脂溢性角化病及扁平疣鶒。不发生鳞屑,结痂和溃疡,亦无瘙痒及其他主观症状
进行命名实体识别的工作我们这里使用AI模型实现, 包括模型训练和使用的整个过程, 因此内容以独立一章的形成呈现给大家, 具体内容在第六章: 命名实体识别任务.
同结构化数据流水线中的命名实体审核.
同结构化数据流水线中的命名实体写入数据库.
一般在实体进入数据库存储前, 中间都会有一道必不可少的工序, 就是对识别出来的实体进行合法性的检验, 即命名实体(NE)审核任务. 它的检验过程不使用上下文信息, 更关注于字符本身的组合方式来进行判断, 本质上,它是一项短文本二分类问题.
选用的模型及其原因:
训练数据集的样式:
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])
代码位置: /data/doctor_offline/review_model/train.py
输出效果:
# 正负标签比例
{1: 5740, 0: 5740}
# 取出10条训练数据查看
[[1, '枕部疼痛'], [0, '痛疼部枕'], [1, '陶瑟征阳性'], [0, '性阳征瑟陶'], [1, '恋兽型性变态'], [0, '态变性型兽恋'], [1, '进食困难'], [0, '难困食进'], [1, '会阴瘘管或窦道形成'], [0, '成形道窦或管瘘阴会']]
BERT模型整体架构基于Transformer模型架构, BERT中文预训练模型的解码器和编码器具有12层, 输出层中的线性层具有768个节点, 即输出张量最后一维的维度是768. 它使用的多头注意力机制结构中, 头的数量为12, 模型总参数量为110M. 同时, 它在中文简体和繁体上进行训练, 因此适合中文简体和繁体任务.
在实际的文本任务处理中, 有些训练语料很难获得, 他们的总体数量和包含的词汇总数都非常少, 不适合用于训练带有Embedding层的模型, 但这些数据中却又蕴含这一些有价值的规律可以被模型挖掘, 在这种情况下,使用预训练模型对原始文本进行编码是非常不错的选择, 因为预训练模型来自大型语料, 能够使得当前文本具有意义, 虽然这些意义可能并不针对某个特定领域, 但是这种缺陷可以使用微调模型来进行弥补.
不带头的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))
代码位置: /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
传统RNN的内部结构图:
结构解释图:
内部结构分析:
内部结构过程演示:
根据结构分析得出内部计算公式:
激活函数tanh的作用:
构建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)
输出效果:
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
代码位置: /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]])
进行模型训练的步骤:
将数据集加载到内存获得的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)
输出效果:
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])
代码位置: /data/doctor_offline/review_model/train.py
# 选取损失函数为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
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
训练和验证损失对照曲线:
训练和验证准确率对照曲线:
分析:
# 保存路径
MODEL_PATH = './BERT_RNN.pth'
# 保存模型参数
torch.save(rnn.state_dict(), MODEL_PATH)
代码位置: /data/doctor_offline/review_model/train.py
输出效果:
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') # 模型的保存
将 /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)
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
输出效果:
命名实体识别(Named Entity Recognition,NER)就是从一段自然语言文本中找出相关实体,并标注出其位置以及类型。是信息提取, 问答系统, 句法分析, 机器翻译等应用领域的重要基础工具, 在自然语言处理技术走向实用化的过程中占有重要地位. 包含行业, 领域专有名词, 如人名, 地名, 公司名, 机构名, 日期, 时间, 疾病名, 症状名, 手术名称, 软件名称等。具体可参看如下示例图:
基于规则: 针对有特殊上下文的实体, 或实体本身有很多特征的文本, 使用规则的方法简单且有效. 比如抽取文本中物品价格, 如果文本中所有商品价格都是“数字+元”的形式, 则可以通过正则表达式”\d*.?\d+元”进行抽取. 但如果待抽取文本中价格的表达方式多种多样, 例如“一千八百万”, “伍佰贰拾圆”, “2000万元”, 遇到这些情况就要修改规则来满足所有可能的情况. 随着语料数量的增加, 面对的情况也越来越复杂, 规则之间也可能发生冲突, 整个系统也可能变得不可维护. 因此基于规则的方式比较适合半结构化或比较规范的文本中的进行抽取任务, 结合业务需求能够达到一定的效果.
基于模型: 从模型的角度来看, 命名实体识别问题实际上是序列标注问题。序列标注问题指的是模型的输入是一个序列, 包括文字, 时间等, 输出也是一个序列. 针对输入序列的每一个单元, 输出一个特定的标签. 以中文分词任务进行举例, 例如输入序列是一串文字: “我是中国人”, 输出序列是一串标签: “OOBII”, 其中"BIO"组成了一种中文分词最基础的标签体系: B表示这个字是词的开始, I表示词的中间到结尾, O表示其他类型词(也可以用更多的字母来表示标签体系,“BIO”是最基础的一种标签体系). 因此我们可以根据输出序列"OOBII"进行解码, 得到分词结果"我\是\中国人".
所谓的BiLSTM,就是(Bidirectional LSTM)双向LSTM. 单向的LSTM模型只能捕捉到从前向后传递的信息, 而双向的网络可以同时捕捉正向信息和反向信息, 使得对文本信息的利用更全面, 效果也更好.
在BiLSTM网络最终的输出层后面增加了一个线性层, 用来将BiLSTM产生的隐藏层输出结果投射到具有某种表达标签特征意义的区间, 具体如下图所示:
BiLSTM模型实现:
本段代码构建类BiLSTM, 完成初始化和网络结构的搭建。
总共3层:
# 本段代码构建类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)
代码实现位置: /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
调用:
# 初始化模型
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)
输出效果:
BiLSTM(
(embedding): Embedding(14, 200)
(bilstm): LSTM(200, 50, batch_first=True, bidirectional=True)
(linear): Linear(in_features=100, out_features=5, bias=True)
)
将句子中的每一个字符映射到码表中,比如:
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)
代码实现位置: /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
调用:
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)
输出效果:
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]])
将句子利用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
代码实现位置: /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
调用:
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)
输出效果:
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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。