日萌社
人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度學習實戰(不定時更新)
在線聊天的總體架構與工具介紹:Flask web、Redis、Gunicorn服務組件、Supervisor服務監控器、Neo4j圖數據庫
linux 安裝 neo4j、linux 安裝 Redis、supervisor 安裝
在線部分:werobot服務、主要邏輯服務、句子相關模型服務、BERT中文預訓練模型+微調模型(目的:比較兩句話text1和text2之間是否有關聯)、模型在Flask部署
離線部分+在線部分:命名實體審覈任務RNN模型、命名實體識別任務BiLSTM+CRF模型、BERT中文預訓練+微調模型、werobot服務+flask
命名實體審覈任務RNN模型
bert_chinese_encode
import torch
import torch.nn as nn
# 導入bert的模型
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):
"""
功能: 使用bert-chinese預訓練模型對中文文本進行編碼
text: 要進行編碼的中文文本
return : 編碼後的張量
"""
# 首先使用字符映射器對每個漢子進行映射
# bert中的tokenizer映射後會加入開始和結束的標記, 101, 102, 這兩個標記對我們不需要,採用切片的方式去除
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)
# 模型的輸出都是三維張量,第一維是1,使用[0]來進行降維,只提取我們需要的後兩個維度的張量
encoded_layers = encoded_layers[0]
return encoded_layers
if __name__ == '__main__':
text = "你好,周杰倫"
outputs = get_bert_encode_for_single(text)
# print(outputs)
# print(outputs.shape)
RNN_MODEL
import torch
import torch.nn as nn
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
# 構建第一個線性層, 輸入尺寸是input_size + hidden_size,因爲真正進入全連接層的張量是X(t) + h(t-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, input1, hidden1):
# 首先要進行輸入張量的拼接, 將X(t)和h(t-1)拼接在一起
combined = torch.cat((input1, hidden1), 1)
# 讓輸入經過隱藏層獲得hidden
hidden = self.i2h(combined)
# 讓輸入經過輸出層獲得output
output = self.i2o(combined)
# 讓output經過softmax層
output = self.softmax(output)
# 返回兩個張量,output, hidden
return output, hidden
def initHidden(self):
# 將隱藏層初始化爲一個[1, hidden_size]的全0張量
return torch.zeros(1, self.hidden_size)
train
import pandas as pd
from collections import Counter
import random
from bert_chinese_encode import get_bert_encode_for_single
import torch
import torch.nn as nn
import math
import time
import matplotlib.pyplot as plt
# 讀取數據
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_data1[:10])
def randomTrainingExample(train_data):
# 隨機選取數據, train_data是訓練集的列表形式的數據
category, line = random.choice(train_data)
# 首先將文字部分利用bert進行編碼
line_tensor = get_bert_encode_for_single(line)
# 將分類標籤封裝成tensor
category_tensor = torch.tensor([int(category)])
# 依次將讀取出來的原始數據,以及封裝後的tensor返回
return category, line, category_tensor, line_tensor
# for i in range(10):
# category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
# print('category = ', category, ' / line = ', line)
# 編寫RNN類的代碼
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
# 構建第一個線性層, 輸入尺寸是input_size + hidden_size,因爲真正進入全連接層的張量是X(t) + h(t-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, input1, hidden1):
# 首先要進行輸入張量的拼接, 將X(t)和h(t-1)拼接在一起
combined = torch.cat((input1, hidden1), 1)
# 讓輸入經過隱藏層獲得hidden
hidden = self.i2h(combined)
# 讓輸入經過輸出層獲得output
output = self.i2o(combined)
# 讓output經過softmax層
output = self.softmax(output)
# 返回兩個張量,output, hidden
return output, hidden
def initHidden(self):
# 將隱藏層初始化爲一個[1, hidden_size]的全0張量
return torch.zeros(1, self.hidden_size)
# 選取損失函數爲nn.NLLLoss()
criterion = nn.NLLLoss()
hidden_size = 128
# 預訓練模型bert輸出的維度
input_size = 768
n_categories = 2
rnn = RNN(input_size, hidden_size, n_categories)
# 把學習率設定爲0.005
learning_rate = 0.005
def train(category_tensor, line_tensor):
# category_tensor: 代表類別的張量, line_tensor: 代表經過bert編碼後的文本張量
# 初始化隱藏層
hidden = rnn.initHidden()
# 訓練前一定要將梯度歸零
rnn.zero_grad()
# 遍歷line_tensor中的每一個字符的張量
for i in range(line_tensor.size()[0]):
# 傳入rnn中的參數必須是二維張量,如果不是,需要擴展維度 unsqueeze(0)
output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
# 調用損失函數, 輸入分別是rnn預測的結果和真實的類別標籤
loss = criterion(output, category_tensor)
# 開啓反向傳播
loss.backward()
# 爲大家顯示的更新模型中的所有參數
for p in rnn.parameters():
# 利用梯度下降法更新, add_()功能是參數的梯度乘以學習率,然後結果相加來更新參數
p.data.add_(-learning_rate, p.grad.data)
return output, loss.item()
def valid(category_tensor, line_tensor):
# category_tensor: 類別標籤的張量, line_tensor: 經過了bert編碼後的文本張量
# 初始化隱藏層
hidden = rnn.initHidden()
# 注意: 驗證函數中要保證模型不自動求導
with torch.no_grad():
# 遍歷文本張量中的每一個字符的bert編碼
for i in range(line_tensor.size()[0]):
# 注意: 輸入rnn的參數必須是二維張量,如果不足,利用unsqueeze()來進行擴展
output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
loss = criterion(output, category_tensor)
return output, loss.item()
def timeSince(since):
# 功能:獲取每次打印的時間消耗, since是訓練開始的時間
# 獲取當前的時間
now = time.time()
# 獲取時間差, 就是時間消耗
s = now - since
# 獲取時間差的分鐘數
m = math.floor(s/60)
# 獲取時間差的秒數
s -= m*60
return '%dm %ds' % (m, s)
# 設置訓練的迭代次數
n_iters = 1000
# 設置打印間隔爲100
plot_every = 100
# 初始化訓練和驗證的損失,準確率
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")
# 模型的保存,首先給定保存的路徑
MODEL_PATH = './BERT_RNN.pth'
torch.save(rnn.state_dict(), MODEL_PATH)
predict
# 導入若干包
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]):
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模型進行編碼,然後將張量傳輸給_test()函數
output = _test(get_bert_encode_for_single(input_line))
# 從output中取出最大值對應的索引,比較的維度是1
_, topi = output.topk(1, 1)
return topi.item()
# 編寫批量預測的函數
def batch_predict(input_path, output_path):
# 功能: 批量預測函數
# input_path: 以原始文本的輸入路徑(等待進行命名實體審覈的文件)
# output_path: 預測後的輸出文件路徑(經過命名實體審覈通過的所有數據)
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_lines = fr.readlines()
for input_line in input_lines:
# 調用預測函數,利用RNN模型進行審覈
res = predict(input_line)
if res:
# 如果res==1, 說明通過了審覈
fw.write(input_line + "\n")
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)
neo4j_write
import os
import fileinput
from neo4j import GraphDatabase
from config import NEO4J_CONFIG
driver = GraphDatabase.driver( **NEO4J_CONFIG)
# 導入數據的函數
def _load_data(path):
"""
功能:將path參數目錄下的csv文件以指定的格式加載到內存中
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 = []
for disease_csv in disease_csv_list:
# 將一個疾病文件中所有的症狀提取到一個列表中
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):
"""
功能: 將csv數據全部寫入neo4j圖數據庫中
path: 經歷了命名實體審覈後,所有的疾病-症狀的csv文件
"""
# 導入數據成爲字典類型
disease_symptom_dict = _load_data(path)
# 開啓一個會話,進行數據庫的操作
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)
# 創建Disease節點的索引
cypher = "CREATE INDEX ON:Disease(name)"
session.run(cypher)
# 創建Symptom節點的索引
cypher = "CREATE INDEX ON:Symptom(name)"
session.run(cypher)
if __name__ == '__main__':
path = "./structured/reviewed/"
write(path)
命名實體識別任務BiLSTM+CRF模型
loader_data
# 導入包
import numpy as np
import torch
import torch.utils.data as Data
# 創建生成批量訓練數據的函數
def load_dataset(data_file, batch_size):
'''
data_file: 代表待處理的文件
batch_size: 代表每一個批次樣本的數量
'''
# 將train.npz文件帶入到內存中
data = np.load(data_file)
# 分別提取data中的特徵和標籤
x_data = data['x_data']
y_data = data['y_data']
# 將數據封裝成Tensor張量
x = torch.tensor(x_data, dtype=torch.long)
y = torch.tensor(y_data, dtype=torch.long)
# 將數據再次封裝
dataset = Data.TensorDataset(x, y)
# 求解一下數據的總量
total_length = len(dataset)
# 確認一下將80%的數據作爲訓練集, 剩下的20%的數據作爲測試集
train_length = int(total_length * 0.8)
validation_length = total_length - train_length
# 利用Data.random_split()直接切分數據集, 按照80%, 20%的比例進行切分
train_dataset, validation_dataset = Data.random_split(dataset=dataset, lengths=[train_length, validation_length])
# 將訓練數據集進行DataLoader封裝
# dataset: 代表訓練數據集
# batch_size: 代表一個批次樣本的數量, 若數據集的總樣本數無法被batch_size整除, 則最後一批數據的大小爲餘數,
# 若設置另一個參數drop_last=True, 則自動忽略最後不能被整除的數量
# shuffle: 是否每隔批次爲隨機抽取, 若設置爲True, 代表每個批次的數據樣本都是從數據集中隨機抽取的
# num_workers: 設置有多少子進程負責數據加載, 默認爲0, 即數據將被加載到主進程中
# drop_last: 是否把最後一個批次的數據(指那些無法被batch_size整除的餘數數據)忽略掉
train_loader = Data.DataLoader(dataset=train_dataset, batch_size=batch_size,
shuffle=True, num_workers=2, drop_last=False)
validation_loader = Data.DataLoader(dataset=validation_dataset, batch_size=batch_size,
shuffle=True, num_workers=2, drop_last=False)
# 將兩個數據生成器封裝成一個字典類型
data_loaders = {'train': train_loader, 'validation': validation_loader}
# 將兩個數據集的長度也封裝成一個字典類型
data_size = {'train': train_length, 'validation': validation_length}
return data_loaders, data_size
# 批次的大小
BATCH_SIZE = 32
# 訓練數據集的文件路徑
DATA_FILE = './data/total.npz'
if __name__ == '__main__':
data_loader, data_size = load_dataset(DATA_FILE, BATCH_SIZE)
print('data_loader:', data_loader, '\ndata_size:', data_size)
preprocess_data
import json
import numpy as np
# 創建訓練數據集, 從原始訓練文件中將中文字符進行數字化編碼, 同時也將標籤進行數字化的編碼
def create_train_data(train_data_file, result_file, json_file, tag2id, max_length=100):
'''
train_data_file: 原始訓練文件
result_file: 處理後的結果文件
json_file: 中文字符向id的映射表, 也是一個文件char_to_id.json
tag2id: 標籤向id的映射表, 提前已經寫好了
'''
# 導入json格式的中文字符向id的映射表
char2id = json.load(open(json_file, mode='r', encoding='utf-8'))
char_data, tag_data = [], []
# 打開原始訓練文件
with open(train_data_file, mode='r', encoding='utf-8') as f:
# 初始化一條語句數字化編碼後的列表
char_ids = [0] * max_length
tag_ids = [0] * max_length
idx = 0
# 遍歷文件中的每一行
for line in f.readlines():
# char \t tag
line = line.strip('\n').strip()
# 如果不是空行, 並且當前語句的長度沒有超過max_length,則進行字符到id的映射
if line and len(line) > 0 and idx < max_length:
ch, tag = line.split('\t')
# 如果當前字符在映射表中,則直接映射爲對應的id值
if char2id.get(ch):
char_ids[idx] = char2id[ch]
# 否則直接用"UNK"的id值進行賦值, 代表的是未知的字符
else:
char_ids[idx] = char2id['UNK']
# 將標籤對應的id值進行數字化編碼映射
tag_ids[idx] = tag2id[tag]
idx += 1
# 如果是空行, 或者當前語句的長度超過了max_length
else:
# 如果當前語句的長度超過了max_length,直接將[0: max_length]的部分直接進行結果賦值
if idx <= max_length:
char_data.append(char_ids)
tag_data.append(tag_ids)
# 遇到空行, 說明當前一條完整的語句已經結束了, 需要將初始化列表進行清零操作, 爲了下一個句子的迭代做準備
char_ids = [0] * max_length
tag_ids = [0] * max_length
idx = 0
# 將數字化編碼後的數據封裝成numpy的數組類型, 數字化編碼採用int32
x_data = np.array(char_data, dtype=np.int32)
y_data = np.array(tag_data, dtype=np.int32)
# 直接利用np.savez()將數據存儲成.npz類型的文件
np.savez(result_file, x_data=x_data, y_data=y_data)
print("create_train_data Finished!".center(100, "-"))
json_file = './data/char_to_id.json'
# 參數2:標籤碼錶對照字典
tag2id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 參數3:訓練數據文件路徑
train_data_file = './data/total.txt'
# 參數4:創建的npz文件保路徑(訓練數據)
result_file = './data/total.npz'
if __name__ == '__main__':
create_train_data(train_data_file, result_file, json_file, tag2id)
bilstm_crf
import torch
import torch.nn as nn
import torch.optim as optim
# 添加幾個輔助函數, 爲log_sum_exp()服務
def to_scalar(var):
# 返回一個python float類型的值
return var.view(-1).data.tolist()[0]
def argmax(vec):
# 返回列的維度上最大值的下標, 而且下標是一個標量float類型
_, idx = torch.max(vec, 1)
return to_scalar(idx)
def log_sum_exp(vec):
# 求向量中的最大值
max_score = vec[0, argmax(vec)]
# 構造一個最大值的廣播變量
max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])
# 先減去最大值, 再求解log_sum_exp, 最終的返回值上再加上max_score
return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
# 函數sentence_map()完成中文文本信息的數字編碼, 將中文語句變成數字化張量
def sentence_map(sentence_list, char_to_id, max_length):
# 首先對一個批次的所有語句按照句子的長短進行排序, 這個操作並非必須
sentence_list.sort(key=lambda x: len(x), reverse=True)
# 定義一個最終存儲結果特徵張量的空列表
sentence_map_list = []
# 循環遍歷一個批次內所有的語句
for sentence in sentence_list:
# 採用列表生成式來完成中文字符到id值的映射
sentence_id_list = [char_to_id[c] for c in sentence]
# 長度不夠max_length的部分用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)
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim,
num_layers, batch_size, sequence_length):
'''
vocab_size: 單詞總數量
tag_to_ix: 標籤到id的映射字典
embedding_dim: 詞嵌入的維度
hidden_dim: 隱藏層的維度
num_layers: 堆疊的LSTM層數
batch_size: 批次的大小
sequence_length: 語句的最大長度
'''
# 繼承函數的初始化
super(BiLSTM_CRF, self).__init__()
# 設置單詞的總數量
self.vocab_size = vocab_size
# 設置標籤到id的映射字典
self.tag_to_ix = tag_to_ix
# 設置標籤的總數
self.tagset_size = len(tag_to_ix)
# 設置詞嵌入的維度
self.embedding_dim = embedding_dim
# 設置隱藏層的維度
self.hidden_dim = hidden_dim
# 設置LSTM層數
self.num_layers = num_layers
# 設置批次的大小
self.batch_size = batch_size
# 設置語句的長度
self.sequence_length = sequence_length
# 構建詞嵌入層, 兩個參數分別單詞總數量, 詞嵌入維度
self.word_embeds = nn.Embedding(vocab_size, embedding_dim)
# 構建雙向LSTM層, 輸入參數包括詞嵌入維度, 隱藏層大小, LSTM層數, 是否雙向標誌
self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=self.num_layers, bidirectional=True)
# 構建全連線性層, 一端對接BiLSTM, 另一端對接輸出層, 注意輸出層維度是tagset_size
self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)
# 初始化轉移矩陣, 注意轉移矩陣的維度[tagset_size, tagset_size]
self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))
# 任何合法的句子不會轉移到"START_TAG",設置爲-10000
# 任何合法的句子不會從"STOP_TAG"繼續轉移, 設置爲-10000
self.transitions.data[tag_to_ix["<START>"], :] = -10000
self.transitions.data[:, tag_to_ix["<STOP>"]] = -10000
# 初始化隱藏層, 利用類中的函數init_hidden()來完成
self.hidden = self.init_hidden()
def init_hidden(self):
# 爲了符合LSTM的要求, 返回h0, c0, 這兩個張量擁有相同的shape
# shape: [2 * num_layers, batch_size, hidden_dim // 2]
return (torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2),
torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim //2))
# 在類中將文本信息經過詞嵌入層, BiLSTM層, 線性層的處理, 最終輸出句子的張量
def _get_lstm_features(self, sentence):
self.hidden = self.init_hidden()
# 讓sentence經歷詞嵌入層
embeds = self.word_embeds(sentence).view(self.sequence_length, self.batch_size, -1)
# 將詞嵌入層的輸出, 進入BiLSTM層, LSTM輸入的兩個參數: 詞嵌入後的張量, 隨機初始化的隱藏層張量
lstm_out, self.hidden = self.lstm(embeds, self.hidden)
# 保證輸出張量的形狀:[sequence_length, batch_size, hidden_dim]
lstm_out = lstm_out.view(self.sequence_length, self.batch_size, self.hidden_dim)
# 最後經過線性層的處理, 得到最後輸出張量的shape: [sequence_length, batch_size, tagset_size]
lstm_feats = self.hidden2tag(lstm_out)
return lstm_feats
def _forward_alg(self, feats):
# 初始化一個alphas張量, 代表轉移矩陣的起始位置
init_alphas = torch.full((1, self.tagset_size), -10000)
# 僅僅將"START_TAG"賦值爲0, 代表着接下來的矩陣轉移只能從START_TAG開始
init_alphas[0][self.tag_to_ix["<START>"]] = 0
# 將初始化的init_alphas賦值爲前向計算變量, 爲了後續在反向傳播求導的時候可以自動更新參數
forward_var = init_alphas
# 輸入進來的feats - shape:[20, 8, 7], 爲了後續按句子爲單位進行計算, 需要將batch_size放在第一個維度上
feats = feats.transpose(1, 0)
# 初始化一個最終的結果張量
result = torch.zeros((1, self.batch_size))
idx = 0
# 遍歷每一行文本, 總共循環batch_size次
for feat_line in feats:
# feats: [8, 20, 7], feat_line: [20, 7]
# 遍歷每一行, 每一個feat代表一個time_step
for feat in feat_line:
# 當前的time_step,初始化一個前向計算張量
alphas_t = []
# 每一個時間步, 遍歷所有可能的轉移標籤, 進行累加計算
for next_tag in range(self.tagset_size):
# 構造發射分數的廣播張量
emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)
# 當前時間步, 轉移到next_tag標籤的轉移分數
trans_score = self.transitions[next_tag].view(1, -1)
# 將前向計算矩陣, 發射矩陣, 轉移矩陣累加
next_tag_var = forward_var + trans_score + emit_score
# 計算log_sum_exp()的值, 並添加進alphas_t列表中
alphas_t.append(log_sum_exp(next_tag_var).view(1))
# 將列表張量轉換爲二維張量
forward_var = torch.cat(alphas_t).view(1, -1)
# 添加最後一步轉移到"STOP_TAG"的分數, 就完成了整條語句的分數計算
terminal_var = forward_var + self.transitions[self.tag_to_ix["<STOP>"]]
# 將terminal_var放進log_sum_exp()中進行計算, 得到一條樣本語句最終的分數
alpha = log_sum_exp(terminal_var)
# 將得分添加進最終的結果列表中, 作爲整個函數的返回結果
result[0][idx] = alpha
idx += 1
return result
def _score_sentence(self, feats, tags):
'''
feats: [20, 8, 7], 經歷了_get_lstm_features()處理後的特徵張量
tags: [8, 20], 代表的是訓練語句真實的標籤矩陣
'''
# 初始化一個0值的tensor,爲後續的累加做準備
score = torch.zeros(1)
# 要在tags矩陣的第一列添加,這一列全部都是START_TAG
temp = torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix["<START>"]), dtype=torch.long)
tags = torch.cat((temp, tags), dim=1)
# 將傳入的feats形狀轉變爲[batch_size, sequence_length, tagset_size]
feats = feats.transpose(1, 0)
# 初始化最終的結果分數張量, 每一個句子得到一個分數
result = torch.zeros((1, self.batch_size))
idx = 0
# 遍歷所有的語句特徵向量
for feat_line in feats:
# 此處feat_line: [20, 7]
# 遍歷每一個時間步, 注意: 最重要的區別在於這裏是在真實標籤tags的指導下進行的轉移矩陣和發射矩陣的累加分數求和
for i, feat in enumerate(feat_line):
score = score + self.transitions[tags[idx][i+1], tags[idx][i]] + feat[tags[idx][i+1]]
# 遍歷完當前語句所有的時間步之後, 最後添加上"STOP_TAG"的轉移分數
score = score + self.transitions[self.tag_to_ix["<STOP>"], tags[idx][-1]]
# 將該條語句的最終得分添加進結果列表中
result[0][idx] = score
idx += 1
score = torch.zeros(1)
return result
def _viterbi_decode(self, feats):
# 根據傳入的語句特徵feats,推斷出標籤序列
# 初始化一個最佳路徑結果的存放列表
result_best_path = []
# 將輸入的張量形狀變爲 [batch_size, sequence_length, tagset_size]
feats = feats.transpose(1, 0)
# 對批次中的每一個語句進行遍歷, 每個語句產生一個最優的標註序列
for feat_line in feats:
backpointers = []
# 初始化前向傳播的張量, 同時設置START_TAG等於0, 約束了合法的序列只能從START_TAG開始
init_vvars = torch.full((1, self.tagset_size), -10000)
init_vvars[0][self.tag_to_ix["<START>"]] = 0
# 將初始化的變量賦值給forward_var, 在第i個time_step中, 張量forward_var保存的是第i-1個time_step的viterbi張量
forward_var = init_vvars
# 遍歷從i=0, 到序列最後一個time_step, 每一個時間步
for feat in feat_line:
# 初始化保存當前time_step的回溯指針
bptrs_t = []
# 初始化保存當前tme_step的viterbi變量
viterbivars_t = []
# 遍歷所有可能的轉移標籤
for next_tag in range(self.tagset_size):
# next_tag_var[i]保存了tag_i在前一個time_step的viterbi變量
# 通過前向傳播張量forward_var加上從tag_i轉移到next_tag的轉移分數, 賦值給next_tag_var
# 注意: 在這裏不去加發射矩陣的分數, 因爲發射矩陣分數一致, 不影響求最大值下標
next_tag_var = forward_var + self.transitions[next_tag]
# 將最大的標籤所對應的id加入到當前time_step的回溯列表中
best_tag_id = argmax(next_tag_var)
bptrs_t.append(best_tag_id)
viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))
# 此處再將發射矩陣的分數feat添加上來, 繼續賦值給forward_var, 作爲下一個time_step的前向傳播變量
forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
# 將當前time_step的回溯指針添加進當前樣本行的總體回溯指針中
backpointers.append(bptrs_t)
# 最後加上轉移到STOP_TAG的分數
terminal_var = forward_var + self.transitions[self.tag_to_ix["<STOP>"]]
best_tag_id = argmax(terminal_var)
# 根據回溯指針, 解碼最佳路徑
best_path = [best_tag_id]
# 從後向前回溯最佳路徑
for bptrs_t in reversed(backpointers):
# 通過第i個time_step得到的最佳id, 找到第i-1個time_step的最佳id
best_tag_id = bptrs_t[best_tag_id]
best_path.append(best_tag_id)
# 將START_TAG去除掉
start = best_path.pop()
# print(start)
# 確認一下最佳路徑的第一個標籤是START_TAG
# if start != self.tag_to_ix["<START>"]:
# print(start)
assert start == self.tag_to_ix["<START>"]
# 因爲是從後向前進行回溯, 所以在此對列表進行逆序操作得到從前向後的真實路徑
best_path.reverse()
# 將當前這一行的樣本結果添加到最終的結果列表中
result_best_path.append(best_path)
return result_best_path
# 對數似然函數, 輸入兩個參數: 數字化編碼後的張量, 和真實的標籤
# 注意: 這個函數是未來真實訓練中要用到的損失函數, 虛擬化的forward()
def neg_log_likelihood(self, sentence, tags):
# 第一步先得到BiLSTM層的輸出特徵張量
feats = self._get_lstm_features(sentence)
# feats: [20, 8, 7]代表一個批次8個樣本, 每個樣本長度20, 每一個字符映射成7個標籤。每一個word映射到7個標籤的概率, 發射矩陣。
# feats本質上就是發射矩陣
# forward_score, 代表公式推導中損失函數loss的第一項
forward_score = self._forward_alg(feats)
# gold_score, 代表公式推導中損失函數loss的第二項
gold_score = self._score_sentence(feats, tags)
# 注意: 在這裏,通過forward_score和gold_score的差值作爲loss,進行梯度下降的優化求解訓練模型
# 按行求和的時候, 在torch.sum()函數中, 需要設置dim=1;同理, 如果要按列求和, 需要設置dim=0
return torch.sum(forward_score - gold_score, dim=1)
# 編寫正式的forward()函數, 注意應用場景是在預測的時候, 模型訓練的時候並沒有用到forward()函數
def forward(self, sentence):
# 首先獲取BiLSTM層的輸出特徵, 得到發射矩陣
lstm_feats = self._get_lstm_features(sentence)
# 通過維特比算法直接解碼出最優路徑
result_sequence = self._viterbi_decode(lstm_feats)
return result_sequence
# 開始字符和結束字符
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 標籤和序號的對應碼錶
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 詞嵌入的維度
EMBEDDING_DIM = 200
# 隱藏層神經元的數量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
""" 在僅運行當前文件進行測試時,設置SENTENCE_LENGTH爲20 """
# 設置最大語句限制長度
# SENTENCE_LENGTH = 20
SENTENCE_LENGTH = 100
# 默認神經網絡的層數
NUM_LAYERS = 1
# 初始化的字符和序號的對應碼錶
# char_to_id = {"雙": 0, "肺": 1, "見": 2, "多": 3, "發": 4, "斑": 5, "片": 6,
# "狀": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}
'''
model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM,
hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, sequence_length=SENTENCE_LENGTH)
print(model)
'''
sentence_list = [
"確診瀰漫大b細胞淋巴瘤1年",
"反覆咳嗽、咳痰40年,再發伴氣促5天。",
"生長髮育遲緩9年。",
"右側小細胞肺癌第三次化療入院",
"反覆氣促、心悸10年,加重伴胸痛3天。",
"反覆胸悶、心悸、氣促2多月,加重3天",
"咳嗽、胸悶1月餘, 加重1周",
"右上肢無力3年, 加重伴肌肉萎縮半年"
]
# 真實標籤數據, 對應爲tag_to_ix中的數字標籤
tag_list = [
[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 將標籤轉爲標量tags
tags = torch.tensor(tag_list, dtype=torch.long)
char_to_id = {"<PAD>": 0}
""" 在僅運行當前文件進行測試時,設置SENTENCE_LENGTH爲20 """
if __name__ == '__main__':
for sentence in sentence_list:
for c in sentence:
# 如果當前字符不在映射字典中, 追加進字典
if c not in char_to_id:
char_to_id[c] = len(char_to_id)
# 首先利用char_to_id完成中文文本的數字化編碼
sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
# print("sentence_sequence:\n", sentence_sequence)
# 構建類的實例, 去得到語句的特徵張量
model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM,
hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE,
sequence_length=SENTENCE_LENGTH)
# 調用類內部的_get_lstm_features()函數, 得到特徵張量
# sentence_features = model._get_lstm_features(sentence_sequence)
# print("sentence_features:\n", sentence_features)
# 定義優化器
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
for epoch in range(1):
model.zero_grad()
# feats = model._get_lstm_features(sentence_sequence)
# forward_score = model._forward_alg(feats)
# print(forward_score)
# gold_score = model._score_sentence(feats, tags)
# print(gold_score)
# result_tag = model._viterbi_decode(feats)
# print(result_tag)
loss = model.neg_log_likelihood(sentence_sequence, tags)
print(loss)
loss.backward()
optimizer.step()
result = model(sentence_sequence)
print(result)
evaluate_model
import torch
import torch.nn as nn
# 評估模型的準確率, 召回率, F1等指標
def evaluate(sentence_list, true_tag, predict_tag, id2char, id2tag):
'''
sentence_list: 文本向量化後的句子張量
true_tag: 真實的標籤
predict_tag: 預測的標籤
id2tag: id值到中文字符的映射表
id2tag: id值到tag標籤的映射表
'''
# 初始化真實的命名實體, 預測的命名實體, 接下來比較兩者的異同來評估指標
true_entities, true_entity = [], []
predict_entities, predict_entity = [], []
# 逐條的遍歷批次中的所有語句
for line_num, sentence in enumerate(sentence_list):
# 遍歷一條樣本語句中的每一個字符編碼(這裏面都是數字化之後的編碼)
for char_num in range(len(sentence)):
# 如果編碼等於0, 表示PAD, 說明後續全部都是填充的0, 可以跳出當前for循環
if sentence[char_num] == 0:
break
# 依次提取真實的語句字符, 真實的樣本標籤, 預測的樣本標籤
char_text = id2char[sentence[char_num]]
true_tag_type = id2tag[true_tag[line_num][char_num]]
predict_tag_type = id2tag[predict_tag[line_num][char_num]]
# 先對真實的標籤進行命名實體的匹配
# 如果第一個字符是"B", 表示一個實體的開始, 將"字符/標籤"的格式添加進實體列表中
if true_tag_type[0] == "B":
true_entity = [char_text + "/" + true_tag_type]
# 如果第一個字符是"I", 表示處於一個實體的中間
# 如果真實的命名實體列表非空, 並且最後一個添加進去的標籤類型和當前的標籤類型一樣, 則繼續添加
# 意思就是比如true_entity = ["中/B-Person", "國/I-Person"], 此時"人/I-Person"就可以進行添加
elif true_tag_type[0] == "I" and len(true_entity) != 0 and true_entity[-1].split("/")[1][1:] == true_tag_type[1:]:
true_entity.append(char_text + "/" + true_tag_type)
# 如果第一個字符是"O", 並且true_entity非空, 表示一個命名實體已經匹配結束
elif true_tag_type[0] == "O" and len(true_entity) != 0:
true_entity.append(str(line_num) + "_" + str(char_num))
# 將匹配結束的一個命名實體加入到最終的真實實體列表中
true_entities.append(true_entity)
# 清空true_entity,爲了下一個命名實體的匹配做準備
true_entity = []
# 除了上述3種情況, 說明當前沒有匹配出任何的實體, 則清空true_entity, 繼續下一輪匹配
else:
true_entity = []
# 對預測的標籤進行命名實體的匹配
# 如果第一個字符是"B", 表示一個實體的開始, 將"字符/標籤"的格式添加進實體列表中
if predict_tag_type[0] == "B":
predict_entity = [char_text + "/" + predict_tag_type]
# 如果第一個字符是"I", 表示處於一個實體的中間
# 如果預測命名實體列表非空, 並且最後一個添加進去的標籤類型和當前的標籤類型一樣, 則繼續添加
elif predict_tag_type[0] == "I" and len(predict_entity) != 0 and predict_entity[-1].split("/")[1][1:] == predict_tag_type[1:]:
predict_entity.append(char_text + "/" + predict_tag_type)
# 如果第一個字符是"O", 並且predict_entity非空, 表示一個完整的命名實體已經匹配結束了
elif predict_tag_type[0] == "O" and len(predict_entity) != 0:
predict_entity.append(str(line_num) + "_" + str(char_num))
# 將這個匹配結束的預測命名實體添加到最終的預測實體列表中
predict_entities.append(predict_entity)
# 清空predict_entity, 爲下一個命名實體的匹配做準備
predict_entity = []
# 除了上述3種情況, 說明當前沒有匹配出任何的實體, 則清空predict_entity, 繼續下一輪的匹配
else:
predict_entity = []
# 遍歷所有預測出來的實體列表, 只有那些在真實命名實體列表中的實體纔是正確的預測
acc_entities = [entity for entity in predict_entities if entity in true_entities]
# 計算正確實體的個數, 預測實體的個數, 真實實體的個數
acc_entities_length = len(acc_entities)
predict_entities_length = len(predict_entities)
true_entities_length = len(true_entities)
# 至少爭取預測了一個實體的情況下, 才計算準確率, 召回率, F1值
if acc_entities_length > 0:
accuracy = float(acc_entities_length / predict_entities_length)
recall = float(acc_entities_length / true_entities_length)
f1_score = 2.0 * accuracy * recall / (accuracy + recall)
return accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_length
else:
return 0, 0, 0, acc_entities_length, predict_entities_length, true_entities_length
# 真實標籤數據
tag_list = [
[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 預測標籤數據
predict_tag_list = [
[0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0],
[3, 4, 0, 3, 4, 0, 0, 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 編碼與字符對照字典
id2char = {0: '<PAD>', 1: '確', 2: '診', 3: '彌', 4: '漫', 5: '大', 6: 'b', 7: '細', 8: '胞', 9: '淋', 10: '巴', 11: '瘤', 12: '1', 13: '年', 14: '反', 15: '復', 16: '咳', 17: '嗽', 18: '、', 19: '痰', 20: '4', 21: '0', 22: ',', 23: '再', 24: '發', 25: '伴', 26: '氣', 27: '促', 28: '5', 29: '天', 30: '。', 31: '生', 32: '長', 33: '育', 34: '遲', 35: '緩', 36: '9', 37: '右', 38: '側', 39: '小', 40: '肺', 41: '癌', 42: '第', 43: '三', 44: '次', 45: '化', 46: '療', 47: '入', 48: '院', 49: '心', 50: '悸', 51: '加', 52: '重', 53: '胸', 54: '痛', 55: '3', 56: '悶', 57: '2', 58: '多', 59: '月', 60: '餘', 61: ' ', 62: '周', 63: '上', 64: '肢', 65: '無', 66: '力', 67: '肌', 68: '肉', 69: '萎', 70: '縮', 71: '半'}
# 編碼與標籤對照字典
id2tag = {0: 'O', 1: 'B-dis', 2: 'I-dis', 3: 'B-sym', 4: 'I-sym'}
# 輸入的數字化sentences_sequence, 由下面的sentence_list經過映射函數sentence_map()轉化後得到
sentence_list = [
"確診瀰漫大b細胞淋巴瘤1年",
"反覆咳嗽、咳痰40年,再發伴氣促5天。",
"生長髮育遲緩9年。",
"右側小細胞肺癌第三次化療入院",
"反覆氣促、心悸10年,加重伴胸痛3天。",
"反覆胸悶、心悸、氣促2多月,加重3天",
"咳嗽、胸悶1月餘, 加重1周",
"右上肢無力3年, 加重伴肌肉萎縮半年"
]
# 添加中文字符的數字化編碼函數
def sentence_map(sentence_list, char_to_id, max_length=100):
sentence_list.sort(key=lambda x: len(x), reverse=True)
sentence_map_list = []
for sentence in sentence_list:
sentence_id_list = [char_to_id[c] for c in sentence]
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)
char_to_id = {"<PAD>": 0}
SENTENCE_LENGTH = 20
for sentence in sentence_list:
for c in sentence:
if c not in char_to_id:
char_to_id[c] = len(char_to_id)
if __name__ == '__main__':
sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_length = evaluate(sentence_sequence.tolist(), tag_list, predict_tag_list, id2char, id2tag)
print("accuracy:", accuracy,
"\nrecall:", recall,
"\nf1_score:", f1_score,
"\nacc_entities_length:", acc_entities_length,
"\npredict_entities_length:", predict_entities_length,
"\ntrue_entities_length:", true_entities_length)
train
# 導入包
import json
import time
from tqdm import tqdm
import matplotlib.pyplot as plt
import torch
import torch.optim as optim
from torch.autograd import Variable
# 導入之前編寫好的包, 包括類, 數據集加載, 評估函數
from 項目一.AI_doctor.doctor_offline.ner_model.bilstm_crf import BiLSTM_CRF
from 項目一.AI_doctor.doctor_offline.ner_model.loader_data import load_dataset
from 項目一.AI_doctor.doctor_offline.ner_model.evaluate_model import evaluate
# 訓練模型的函數
def train(data_loader, data_size, batch_size, embedding_dim, hidden_dim,
sentence_length, num_layers, epochs, learning_rate, tag2id,
model_saved_path, train_log_path,
validate_log_path, train_history_image_path):
'''
data_loader: 數據集的加載器, 之前已經通過load_dataset完成了構造
data_size: 訓練集和測試集的樣本數量
batch_size: 批次的樣本個數
embedding_dim: 詞嵌入的維度
hidden_dim: 隱藏層的維度
sentence_length: 文本限制的長度
num_layers: 神經網絡堆疊的LSTM層數
epochs: 訓練迭代的輪次
learning_rate: 學習率
tag2id: 標籤到id的映射字典
model_saved_path: 模型保存的路徑
train_log_path: 訓練日誌保存的路徑
validate_log_path: 測試集日誌保存的路徑
train_history_image_path: 訓練數據的相關圖片保存路徑
'''
# 將中文字符和id的對應碼錶加載進內存
char2id = json.load(open("./data/char_to_id.json", mode="r", encoding="utf-8"))
# 初始化BiLSTM_CRF模型
model = BiLSTM_CRF(vocab_size=len(char2id), tag_to_ix=tag2id,
embedding_dim=embedding_dim, hidden_dim=hidden_dim,
batch_size=batch_size, num_layers=num_layers,
sequence_length=sentence_length)
# 定義優化器, 使用SGD作爲優化器(pytorch中Embedding支持的GPU加速爲SGD, SparseAdam)
# 參數說明如下:
# lr: 優化器學習率
# momentum: 優化下降的動量因子, 加速梯度下降過程
# optimizer = optim.SGD(params=model.parameters(), lr=learning_rate, momentum=0.85, weight_decay=1e-4)
optimizer = optim.Adam(params=model.parameters(), lr=learning_rate, betas=(0.9, 0.999), eps=1e-8, weight_decay=1e-4)
# 設定優化器學習率更新策略
# 參數說明如下:
# optimizer: 優化器
# step_size: 更新頻率, 每過多少個epoch更新一次優化器學習率
# gamma: 學習率衰減幅度,
# 按照什麼比例調整(衰減)學習率(相對於上一輪epoch), 默認0.1
# 例如:
# 初始學習率 lr = 0.5, step_size = 20, gamma = 0.1
# lr = 0.5 if epoch < 20
# lr = 0.05 if 20 <= epoch < 40
# lr = 0.005 if 40 <= epoch < 60
# scheduler = optim.lr_scheduler.StepLR(optimizer=optimizer, step_size=5, gamma=0.8)
# 初始化存放訓練中損失, 準確率, 召回率, F1等數值指標
train_loss_list = []
train_acc_list = []
train_recall_list = []
train_f1_list = []
train_log_file = open(train_log_path, mode="w", encoding="utf-8")
# 初始化存放測試中損失, 準確率, 召回率, F1等數值指標
validate_loss_list = []
validate_acc_list = []
validate_recall_list = []
validate_f1_list = []
validate_log_file = open(validate_log_path, mode="w", encoding="utf-8")
# 利用tag2id生成id到tag的映射字典
id2tag = {v:k for k, v in tag2id.items()}
# 利用char2id生成id到字符的映射字典
id2char = {v:k for k, v in char2id.items()}
# 按照參數epochs的設定來循環epochs次
for epoch in range(epochs):
# 在進度條打印前, 先輸出當前所執行批次
tqdm.write("Epoch {}/{}".format(epoch + 1, epochs))
# 定義要記錄的正確總實體數, 識別實體數以及真實實體數
total_acc_entities_length, \
total_predict_entities_length, \
total_gold_entities_length = 0, 0, 0
# 定義每batch步數, 批次loss總值, 準確度, f1值
step, total_loss, correct, f1 = 1, 0.0, 0, 0
# 開啓當前epochs的訓練部分
for inputs, labels in tqdm(data_loader["train"]):
# 將數據以Variable進行封裝
inputs, labels = Variable(inputs), Variable(labels)
# 在訓練模型期間, 要在每個樣本計算梯度前將優化器歸零, 不然梯度會被累加
optimizer.zero_grad()
# 此處調用的是BiLSTM_CRF類中的neg_log_likelihood()函數
loss = model.neg_log_likelihood(inputs, labels)
# 獲取當前步的loss, 由tensor轉爲數字
step_loss = loss.data
# 累計每步損失值
total_loss += step_loss
# 獲取解碼最佳路徑列表, 此時調用的是BiLSTM_CRF類中的forward()函數
best_path_list = model(inputs)
# 模型評估指標值獲取包括:當前批次準確率, 召回率, F1值以及對應的實體個數
step_acc, step_recall, f1_score, acc_entities_length, \
predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),
labels.tolist(),
best_path_list,
id2char,
id2tag)
# 訓練日誌內容
'''
log_text = "Epoch: %s | Step: %s " \
"| loss: %.5f " \
"| acc: %.5f " \
"| recall: %.5f " \
"| f1 score: %.5f" % \
(epoch, step, step_loss, step_acc, step_recall,f1_score)
'''
# 分別累計正確總實體數、識別實體數以及真實實體數
total_acc_entities_length += acc_entities_length
total_predict_entities_length += predict_entities_length
total_gold_entities_length += gold_entities_length
# 對損失函數進行反向傳播
loss.backward()
# 通過optimizer.step()計算損失, 梯度和更新參數
optimizer.step()
# 記錄訓練日誌
# train_log_file.write(log_text + "\n")
step += 1
# 獲取當前epochs平均損失值(每一輪迭代的損失總值除以總數據量)
epoch_loss = total_loss / data_size["train"]
# 計算當前epochs準確率
if total_predict_entities_length > 0:
total_acc = total_acc_entities_length / total_predict_entities_length
# 計算當前epochs召回率
if total_gold_entities_length > 0:
total_recall = total_acc_entities_length / total_gold_entities_length
# 計算當前epochs的F1值
total_f1 = 0
if total_acc + total_recall != 0:
total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)
log_text = "Epoch: %s " \
"| mean loss: %.5f " \
"| total acc: %.5f " \
"| total recall: %.5f " \
"| total f1 scroe: %.5f" % (epoch, epoch_loss,
total_acc,
total_recall,
total_f1)
print(log_text)
# 當前epochs訓練後更新學習率, 必須在優化器更新之後
# scheduler.step()
# 記錄當前epochs訓練loss值(用於圖表展示), 準確率, 召回率, f1值
train_loss_list.append(epoch_loss)
train_acc_list.append(total_acc)
train_recall_list.append(total_recall)
train_f1_list.append(total_f1)
train_log_file.write(log_text + "\n")
# 定義要記錄的正確總實體數, 識別實體數以及真實實體數
total_acc_entities_length, \
total_predict_entities_length, \
total_gold_entities_length = 0, 0, 0
# 定義每batch步數, 批次loss總值, 準確度, f1值
step, total_loss, correct, f1 = 1, 0.0, 0, 0
# 開啓當前epochs的驗證部分
with torch.no_grad():
for inputs, labels in tqdm(data_loader["validation"]):
# 將數據以 Variable 進行封裝
inputs, labels = Variable(inputs), Variable(labels)
# 此處調用的是 BiLSTM_CRF 類中的 neg_log_likelihood 函數
# 返回最終的 CRF 的對數似然結果
try:
loss = model.neg_log_likelihood(inputs, labels)
except:
continue
# 獲取當前步的 loss 值,由 tensor 轉爲數字
step_loss = loss.data
# 累計每步損失值
total_loss += step_loss
# 獲取解碼最佳路徑列表, 此時調用的是BiLSTM_CRF類中的forward()函數
best_path_list = model(inputs)
# 模型評估指標值獲取: 當前批次準確率, 召回率, F1值以及對應的實體個數
step_acc, step_recall, f1_score, acc_entities_length, \
predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),
labels.tolist(),
best_path_list,
id2char,
id2tag)
# 訓練日誌內容
'''
log_text = "Epoch: %s | Step: %s " \
"| loss: %.5f " \
"| acc: %.5f " \
"| recall: %.5f " \
"| f1 score: %.5f" % \
(epoch, step, step_loss, step_acc, step_recall,f1_score)
'''
# 分別累計正確總實體數、識別實體數以及真實實體數
total_acc_entities_length += acc_entities_length
total_predict_entities_length += predict_entities_length
total_gold_entities_length += gold_entities_length
# 記錄驗證集損失日誌
# validate_log_file.write(log_text + "\n")
step += 1
# 獲取當前批次平均損失值(每一批次損失總值除以數據量)
epoch_loss = total_loss / data_size["validation"]
# 計算總批次準確率
if total_predict_entities_length > 0:
total_acc = total_acc_entities_length / total_predict_entities_length
# 計算總批次召回率
if total_gold_entities_length > 0:
total_recall = total_acc_entities_length / total_gold_entities_length
# 計算總批次F1值
total_f1 = 0
if total_acc + total_recall != 0.0:
total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)
log_text = "Epoch: %s " \
"| mean loss: %.5f " \
"| total acc: %.5f " \
"| total recall: %.5f " \
"| total f1 scroe: %.5f" % (epoch, epoch_loss,
total_acc,
total_recall,
total_f1)
print(log_text)
# 記錄當前批次驗證loss值(用於圖表展示)準確率, 召回率, f1值
validate_loss_list.append(epoch_loss)
validate_acc_list.append(total_acc)
validate_recall_list.append(total_recall)
validate_f1_list.append(total_f1)
validate_log_file.write(log_text + "\n")
# 保存模型
torch.save(model.state_dict(), model_saved_path)
# 將loss下降歷史數據轉爲圖片存儲
save_train_history_image(train_loss_list,
validate_loss_list,
train_history_image_path,
"Loss")
# 將準確率提升歷史數據轉爲圖片存儲
save_train_history_image(train_acc_list,
validate_acc_list,
train_history_image_path,
"Acc")
# 將召回率提升歷史數據轉爲圖片存儲
save_train_history_image(train_recall_list,
validate_recall_list,
train_history_image_path,
"Recall")
# 將F1上升歷史數據轉爲圖片存儲
save_train_history_image(train_f1_list,
validate_f1_list,
train_history_image_path,
"F1")
print("train Finished".center(100, "-"))
# 按照傳入的不同路徑, 繪製不同的訓練曲線
def save_train_history_image(train_history_list,
validate_history_list,
history_image_path,
data_type):
# 根據訓練集的數據列表, 繪製折線圖
plt.plot(train_history_list, label="Train %s History" % (data_type))
# 根據測試集的數據列表, 繪製折線圖
plt.plot(validate_history_list, label="Validate %s History" % (data_type))
# 將圖片放置在最優位置
plt.legend(loc="best")
# 設置x軸的圖標爲輪次Epochs
plt.xlabel("Epochs")
# 設置y軸的圖標爲參數data_type
plt.ylabel(data_type)
# 將繪製好的圖片保存在特定的路徑下面, 並修改圖片名字中的"plot"爲對應的data_type
plt.savefig(history_image_path.replace("plot", data_type))
plt.close()
# 參數1:批次大小
BATCH_SIZE = 8
# 參數2:訓練數據文件路徑
train_data_file_path = "./data/total.npz"
# 參數3:加載 DataLoader 數據
data_loader, data_size = load_dataset(train_data_file_path, BATCH_SIZE)
# 參數4:記錄當前訓練時間(拼成字符串用)
time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime(time.time()))
# 參數5:標籤碼錶對照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 參數6:訓練文件存放路徑
model_saved_path = "model/bilstm_crf_state_dict_%s.pt" % (time_str)
# 參數7:訓練日誌文件存放路徑
train_log_path = "log/train_%s.log" % (time_str)
# 參數8:驗證打印日誌存放路徑
validate_log_path = "log/validate_%s.log" % (time_str)
# 參數9:訓練歷史記錄圖存放路徑
train_history_image_path = "log/bilstm_crf_train_plot_%s.png" % (time_str)
# 參數10:字向量維度
EMBEDDING_DIM = 300
# 參數11:隱層維度
HIDDEN_DIM = 128
# 參數12:句子長度
SENTENCE_LENGTH = 100
# 參數13:堆疊 LSTM 層數
NUM_LAYERS = 1
# 參數14:訓練批次
EPOCHS = 3
# 參數15:初始化學習率
LEARNING_RATE = 0.05
if __name__ == '__main__':
train(data_loader, data_size, BATCH_SIZE, EMBEDDING_DIM, HIDDEN_DIM,
SENTENCE_LENGTH, NUM_LAYERS, EPOCHS, LEARNING_RATE, tag_to_id,
model_saved_path, train_log_path, validate_log_path,
train_history_image_path)
predict
import os
import torch
import json
from bilstm_crf import BiLSTM_CRF
def singel_predict(model_path, content, char_to_id_json_path, batch_size, embedding_dim,
hidden_dim, num_layers, sentence_length, offset, target_type_list, tag2id):
char_to_id = json.load(open(char_to_id_json_path, mode="r", encoding="utf-8"))
# 將字符串轉爲碼錶id列表
char_ids = content_to_id(content, char_to_id)
# 處理成 batch_size * sentence_length 的 tensor 數據
# 定義模型輸入列表
model_inputs_list, model_input_map_list = build_model_input_list(content,
char_ids,
batch_size,
sentence_length,
offset)
# 加載模型
model = BiLSTM_CRF(vocab_size=len(char_to_id),
tag_to_ix=tag2id,
embedding_dim=embedding_dim,
hidden_dim=hidden_dim,
batch_size=batch_size,
num_layers=num_layers,
sequence_length=sentence_length)
# 加載模型字典
model.load_state_dict(torch.load(model_path))
tag_id_dict = {v: k for k, v in tag_to_id.items() if k[2:] in target_type_list}
# 定義返回實體列表
entities = []
with torch.no_grad():
for step, model_inputs in enumerate(model_inputs_list):
prediction_value = model(model_inputs)
# 獲取每一行預測結果
for line_no, line_value in enumerate(prediction_value):
# 定義將要識別的實體
entity = None
# 獲取當前行每個字的預測結果
for char_idx, tag_id in enumerate(line_value):
# 若預測結果 tag_id 屬於目標字典數據 key 中
if tag_id in tag_id_dict:
# 取符合匹配字典id的第一個字符,即B, I
tag_index = tag_id_dict[tag_id][0]
# 計算當前字符確切的下標位置
current_char = model_input_map_list[step][line_no][char_idx]
# 若當前字標籤起始爲 B, 則設置爲實體開始
if tag_index == "B":
entity = current_char
# 若當前字標籤起始爲 I, 則進行字符串追加
elif tag_index == "I" and entity:
entity += current_char
# 當實體不爲空且當前標籤類型爲 O 時,加入實體列表
if tag_id == tag_to_id["O"] and entity:
# 滿足當前字符爲O,上一個字符爲目標提取實體結尾時,將其加入實體列表
entities.append(entity)
# 重置實體
entity = None
return entities
def content_to_id(content, char_to_id):
# 定義字符串對應的碼錶 id 列表
char_ids = []
for char in list(content):
# 判斷若字符不在碼錶對應字典中,則取 NUK 的編碼(即 unknown),否則取對應的字符編碼
if char_to_id.get(char):
char_ids.append(char_to_id[char])
else:
char_ids.append(char_to_id["UNK"])
return char_ids
def build_model_input_list(content, char_ids, batch_size, sentence_length, offset):
# 定義模型輸入數據列表
model_input_list = []
# 定義每個批次句子 id 數據
batch_sentence_list = []
# 將文本內容轉爲列表
content_list = list(content)
# 定義與模型 char_id 對照的文字
model_input_map_list = []
# 定義每個批次句子字符數據
batch_sentence_char_list = []
# 判斷是否需要 padding
if len(char_ids) % sentence_length > 0:
# 將不足 batch_size * sentence_length 的部分填充0
padding_length = (batch_size * sentence_length
- len(char_ids) % batch_size * sentence_length
- len(char_ids) % sentence_length)
char_ids.extend([0] * padding_length)
content_list.extend(["#"] * padding_length)
# 迭代字符 id 列表
# 數據滿足 batch_size * sentence_length 將加入 model_input_list
for step, idx in enumerate(range(0, len(char_ids) + 1, sentence_length)):
# 起始下標,從第一句開始增加 offset 個字的偏移
start_idx = 0 if idx == 0 else idx - step * offset
# 獲取長度爲 sentence_length 的字符 id 數據集
sub_list = char_ids[start_idx:start_idx + sentence_length]
# 獲取長度爲 sentence_length 的字符數據集
sub_char_list = content_list[start_idx:start_idx + sentence_length]
# 加入批次數據集中
batch_sentence_list.append(sub_list)
# 批量句子包含字符列表
batch_sentence_char_list.append(sub_char_list)
# 每當批次長度達到 batch_size 時候,將其加入 model_input_list
if len(batch_sentence_list) == batch_size:
# 將數據格式轉爲 tensor 格式,大小爲 batch_size * sentence_length
model_input_list.append(torch.tensor(batch_sentence_list))
# 重置 batch_sentence_list
batch_sentence_list = []
# 將 char_id 對應的字符加入映射表中
model_input_map_list.append(batch_sentence_char_list)
# 重置批字符串內容
batch_sentence_char_list = []
# 返回模型輸入列表
return model_input_list, model_input_map_list
# 參數1:待識別文本
content = "本病是由DNA病毒的單純皰疹病毒所致。人類單純皰疹病毒分爲兩型," \
"即單純皰疹病毒Ⅰ型(HSV-Ⅰ)和單純皰疹病毒Ⅱ型(HSV-Ⅱ)。" \
"Ⅰ型主要引起生殖器以外的皮膚黏膜(口腔黏膜)和器官(腦)的感染。" \
"Ⅱ型主要引起生殖器部位皮膚黏膜感染。" \
"病毒經呼吸道、口腔、生殖器黏膜以及破損皮膚進入體內," \
"潛居於人體正常黏膜、血液、唾液及感覺神經節細胞內。" \
"當機體抵抗力下降時,如發熱胃腸功能紊亂、月經、疲勞等時," \
"體內潛伏的HSV被激活而發病。"
# 參數2:模型保存文件路徑
model_path = "./model/bilstm_crf_state_dict_20200129_210417.pt"
# 參數3:批次大小
BATCH_SIZE = 8
# 參數4:字向量維度
EMBEDDING_DIM = 300
# 參數5:隱層維度
HIDDEN_DIM = 128
NUM_LAYERS = 1
# 參數6:句子長度
SENTENCE_LENGTH = 100
# 參數7:偏移量
OFFSET = 10
# 參數8:標籤碼錶對照字典
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 參數9:字符碼錶文件路徑
char_to_id_json_path = "./data/char_to_id.json"
# 參數10:預測結果存儲路徑
prediction_result_path = "prediction_result"
# 參數11:待匹配標籤類型
target_type_list = ["sym"]
entities = singel_predict(model_path,
content,
char_to_id_json_path,
BATCH_SIZE,
EMBEDDING_DIM,
HIDDEN_DIM,
NUM_LAYERS,
SENTENCE_LENGTH,
OFFSET,
target_type_list,
tag_to_id)
# print("entities:\n", entities)
# 構建批量文本預測的函數
def batch_predict(data_path, model_path, char_to_id_json_path, batch_size, embedding_dim, hidden_dim, sentence_length,
offset, target_type_list, prediction_result_path, tag_to_id):
# data_path: 待預測的批量文本所在的文件夾路徑
# 遍歷文件夾下的所有文件
for fn in os.listdir(data_path):
# 拼接出完整的文件路徑
fullpath = os.path.join(data_path, fn)
# 定義輸出實體結果的文件
entities_file = open(os.path.join(prediction_result_path, fn), mode="w", encoding="utf-8")
# 打開文件進行預測
with open(fullpath, mode="r", encoding="utf-8") as f:
# 讀取文件的內容
content = f.readline()
# 通過單文本預測函數進行預測
entities = single_predict(model_path, content, char_to_id_json_path, batch_size, embedding_dim,
hidden_dim, sentence_length, offset, target_type_list, tag_to_id)
# 將預測出的實體寫入到結果文件中
entities_file.write("\n".join(entities))
print("batch_predict Finished.".center(100, "-"))
data_path = "origin_data"
# 進行批量預測函數的調用
batch_predict(data_path, model_path, cahr_to_id_json_path, BATCH_SIZE, EMBEDDING_DIM, HIDDEN_DIM, SENTENCE_LENGTH,
OFFSET, target_type_list, prediction_result_path, tag_to_id)
BERT中文預訓練+微調模型
bert_chinese_encode
import torch
import torch.nn as nn
# 使用torch.hub加載bert中文模型的字映射器
tokenizer = torch.hub.load('huggingface/pytorch-transformers', 'tokenizer', 'bert-base-chinese')
# 使用torch.hub加載bert中文模型
model = torch.hub.load('huggingface/pytorch-transformers', 'model', 'bert-base-chinese')
# 編寫獲取bert編碼的函數
def get_bert_encode(text_1, text_2, mark=102, max_len=10):
'''
功能: 使用bert中文模型對輸入的文本進行編碼
text_1: 代表輸入的第一句話
text_2: 代表輸入的第二句話
mark: 分隔標記, 是bert預訓練模型tokenizer的一個自身特殊標記, 當輸入兩個文本的時候, 有中間的特殊分隔符, 102
max_len: 限制的最大語句長度, 如果大於max_len, 進行截斷處理, 如果小於max_len, 進行0填充的處理
return: 輸入文本的bert編碼
'''
# 第一步使用tokenizer進行兩個文本的字映射
indexed_tokens = tokenizer.encode(text_1, text_2)
# 接下來要對兩個文本進行補齊, 或者截斷的操作
# 首先要找到分隔標記的位置
k = indexed_tokens.index(mark)
# 第二步處理第一句話, 第一句話是[:k]
if len(indexed_tokens[:k]) >= max_len:
# 長度大於max_len, 進行截斷處理
indexed_tokens_1 = indexed_tokens[:max_len]
else:
# 長度小於max_len, 需要對剩餘的部分進行0填充
indexed_tokens_1 = indexed_tokens[:k] + (max_len - len(indexed_tokens[:k])) * [0]
# 第三步處理第二句話, 第二句話是[k:]
if len(indexed_tokens[k:]) >= max_len:
# 長度大於max_len, 進行截斷處理
indexed_tokens_2 = indexed_tokens[k:k+max_len]
else:
# 長度小於max_len, 需要對剩餘的部分進行0填充
indexed_tokens_2 = indexed_tokens[k:] + (max_len - len(indexed_tokens[k:])) * [0]
# 接下來將處理後的indexed_tokens_1和indexed_tokens_2進行相加合併
indexed_tokens = indexed_tokens_1 + indexed_tokens_2
# 需要一個額外的標誌列表, 來告訴模型那部分是第一句話, 哪部分是第二句話
# 利用0元素來表示第一句話, 利用1元素來表示第二句話
# 注意: 兩句話的長度都已經被我們規範成了max_len
segments_ids = [0] * max_len + [1] * max_len
# 利用torch.tensor將兩個列表封裝成張量
tokens_tensor = torch.tensor([indexed_tokens])
segments_tensor = torch.tensor([segments_ids])
# 利用模型進行編碼不求導
with torch.no_grad():
# 使用bert模型進行編碼, 傳入參數tokens_tensor和segments_tensor, 最終得到模型的輸出encoded_layers
encoded_layers, _ = model(tokens_tensor, token_type_ids=segments_tensor)
return encoded_layers
text_1 = "人生該如何起頭"
text_2 = "改變要如何起手"
encoded_layers = get_bert_encode(text_1, text_2)
print(encoded_layers)
print(encoded_layers.shape)
finetuning_net
import torch
import torch.nn as nn
import torch.nn.functional as F
# 創建微調模型類Net
class Net(nn.Module):
def __init__(self, char_size=20, embedding_size=768, dropout=0.2):
'''
char_size: 輸入句子中的字符數量, 因爲在bert繼承中規範化後的句子長度爲10, 所以這裏面等於兩個句子的長度2*char_size
embedding_size: 字嵌入的維度, 因爲使用了bert中文模型, 而bert的嵌入維度是768, 因此這裏的詞嵌入維度也要是768
dropout: 爲了防止過擬合, 網絡中引入dropout層, dropout爲置0的比例, 默認等於0.2
'''
super(Net, self).__init__()
# 將傳入的參數變成類內部的變量
self.char_size = char_size
self.embedding_size = embedding_size
# 實例化Dropout層
self.dropout = nn.Dropout(p=dropout)
# 定義第一個全連接層
self.fc1 = nn.Linear(char_size * embedding_size, 8)
# 定義第二個全連接層
self.fc2 = nn.Linear(8, 2)
def forward(self, x):
# 首先要對輸入的張量形狀進行變化, 要滿足匹配全連接層
x = x.view(-1, self.char_size * self.embedding_size)
# 使用Dropout層
x = self.dropout(x)
# 將x輸入進第一個全連接層
x = F.relu(self.fc1(x))
# 再次使用Dropout層
x = self.dropout(x)
# 將x輸入進第二個全連接層
x = F.relu(self.fc2(x))
return x
embedding_size = 768
char_size = 20
dropout = 0.2
x = torch.randn(1, 20, 768)
net = Net(char_size, embedding_size, dropout)
res = net(x)
print(res)
train
import pandas as pd
from sklearn.utils import shuffle
from functools import reduce
from collections import Counter
from bert_chinese_encode import get_bert_encode
import torch
import torch.nn as nn
# 定義數據加載器構造函數
def data_loader(data_path, batch_size, split=0.2):
'''
data_path: 訓練數據的路徑
batch_size: 訓練集和驗證集的批次大小
split: 訓練集和驗證集的劃分比例
return: 訓練數據生成器, 驗證數據的生成器, 訓練數據的大小, 驗證數據的大小
'''
# 首先讀取數據
data = pd.read_csv(data_path, header=None, sep="\t")
# 打印一下整體數據集上正負樣本的數量
print("數據集的正負樣本數量:")
print(dict(Counter(data[0].values)))
# 要對讀取的數據進行散亂順序的操作
data = shuffle(data).reset_index(drop=True)
# 劃分訓練集和驗證集
split_point = int(len(data) * split)
valid_data = data[:split_point]
train_data = data[split_point:]
# 保證驗證集中的數據總數至少能夠滿足一個批次
if len(valid_data) < batch_size:
raise("Batch size or split not match!")
# 定義獲取每個批次數據生成器的函數
def _loader_generator(data):
# data: 訓練數據或者驗證數據
# 以每個批次大小的間隔來遍歷數據集
for batch in range(0, len(data), batch_size):
# 初始化batch數據的存放張量列表
batch_encoded = []
batch_labels = []
# 逐條進行數據的遍歷
for item in data[batch: batch + batch_size].values.tolist():
# 對每條數據進行bert預訓練模型的編碼
encoded = get_bert_encode(item[1], item[2])
# 將編碼後的每條數據放進結果列表中
batch_encoded.append(encoded)
# 將標籤放入結果列表中
batch_labels.append([item[0]])
# 使用reduce高階函數將列表中的數據轉換成模型需要的張量形式
# encoded的形狀[batch_size, 2 * max_len, embedding_size]
encoded = reduce(lambda x, y: torch.cat((x, y), dim=0), batch_encoded)
labels = torch.tensor(reduce(lambda x, y: x + y, batch_labels))
# 以生成器的方式返回數據和標籤
yield (encoded, labels)
return _loader_generator(train_data), _loader_generator(valid_data), len(train_data), len(valid_data)
data_path = "./train_data.csv"
batch_size = 32
max_len = 10
train_data_labels, valid_data_labels, train_data_length, valid_data_length = data_loader(data_path, batch_size)
# print(next(train_data_labels))
# print(next(valid_data_labels))
# print("train_data_length:", train_data_length)
# print("valid_data_length:", valid_data_length)
from finetuning_net import Net
import torch.optim as optim
# 初始化若干參數
embedding_size = 768
char_size = 2 * max_len
# 實例化微調網絡
net = Net(embedding_size, char_size)
# 定義交叉熵損失函數
criterion = nn.CrossEntropyLoss()
# 定義優化器
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
def train(train_data_labels):
# train_data_labels: 代表訓練數據和標籤的生成器對象
# return: 整個訓練過程的平均損失和, 正確標籤數量的累加和
# 初始化損失變量和準確數量
train_running_loss = 0.0
train_running_acc = 0.0
# 遍歷數據生成器
for train_tensor, train_labels in train_data_labels:
# 首先將優化器的梯度歸零
optimizer.zero_grad()
# 將訓練數據傳入模型得到輸出結果
train_outputs = net(train_tensor)
# 計算當前批次的平均損失
train_loss = criterion(train_outputs, train_labels)
# 累加損失
train_running_loss += train_loss.item()
# 訓練模型, 反向傳播
train_loss.backward()
# 優化器更新模型參數
optimizer.step()
# 將該批次樣本中正確的預測數量進行累加
train_running_acc += (train_outputs.argmax(1) == train_labels).sum().item()
# 整個循環結束後, 訓練完畢, 得到損失和以及正確樣本的總量
return train_running_loss, train_running_acc
def valid(valid_data_labels):
# valid_data_labels: 代表驗證數據和標籤的生成器對象
# return: 整個驗證過程中的平均損失和和正確標籤的數量和
# 初始化損失值和正確標籤數量
valid_running_loss = 0.0
valid_running_acc = 0
# 循環遍歷驗證數據集的生成器
for valid_tensor, valid_labels in valid_data_labels:
# 測試階段梯度不被更新
with torch.no_grad():
# 將特徵輸入網絡得到預測張量
valid_outputs = net(valid_tensor)
# 計算當前批次的損失值
valid_loss = criterion(valid_outputs, valid_labels)
# 累加損失和
valid_running_loss += valid_loss.item()
# 累加正確預測的標籤數量
valid_running_acc += (valid_outputs.argmax(1) == valid_labels).sum().item()
# 返回整個驗證過程中的平均損失和, 累加的正確標籤數量
return valid_running_loss, valid_running_acc
epochs = 20
# 定義每個輪次的損失和準確率的列表初始化, 用於未來畫圖
all_train_losses = []
all_valid_losses = []
all_train_acc = []
all_valid_acc = []
for epoch in range(epochs):
# 打印輪次
print("Epoch:", epoch + 1)
# 首先通過數據加載函數, 獲得訓練數據和驗證數據的生成器, 以及對應的訓練樣本數和驗證樣本數
train_data_labels, valid_data_labels, train_data_len, valid_data_len = data_loader(data_path, batch_size)
# 調用訓練函數進行訓練
train_running_loss, train_running_acc = train(train_data_labels)
# 調用驗證函數進行驗證
valid_running_loss, valid_running_acc = valid(valid_data_labels)
# 計算平均損失, 每個批次的平均損失之和乘以批次樣本數量, 再除以本輪次的樣本總數
train_average_loss = train_running_loss * batch_size / train_data_len
valid_average_loss = valid_running_loss * batch_size / valid_data_len
# 計算準確率, 本輪次總的準確樣本數除以本輪次的總樣本數
train_average_acc = train_running_acc / train_data_len
valid_average_acc = valid_running_acc / valid_data_len
# 接下來將4個值添加進畫圖的列表中
all_train_losses.append(train_average_loss)
all_valid_losses.append(valid_average_loss)
all_train_acc.append(train_average_acc)
all_valid_acc.append(valid_average_acc)
# 打印本輪次的訓練損失, 準確率, 以及驗證損失, 準確率
print("Train Loss:", train_average_loss, "|", "Train Acc:", train_average_acc)
print("Valid Loss:", valid_average_loss, "|", "Valid Acc:", valid_average_acc)
print("Finished Training.")
# 導入畫圖的工具包
import matplotlib.pyplot as plt
from matplotlib.pyplot import MultipleLocator
# 創建第一張畫布
plt.figure(0)
# 繪製訓練損失曲線
plt.plot(all_train_losses, label="Train Loss")
# 繪製驗證損失曲線, 同時將顏色設置爲紅色
plt.plot(all_valid_losses, color="red", label="Valid Loss")
# 定義橫座標間隔對象, 間隔等於1, 代表一個輪次一個座標點
x_major_locator = MultipleLocator(1)
# 獲得當前座標圖的句柄
ax = plt.gca()
# 在句柄上設置橫座標的刻度間隔
ax.xaxis.set_major_locator(x_major_locator)
# 設置橫座標取值範圍
plt.xlim(1, epochs)
# 將圖例放在左上角
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")
# 定義橫座標間隔對象, 間隔等於1, 代表一個輪次一個座標點
x_major_locator = MultipleLocator(1)
# 獲得當前座標圖的句柄
ax = plt.gca()
# 在句柄上設置橫座標的刻度間隔
ax.xaxis.set_major_locator(x_major_locator)
# 設置橫座標的取值範圍
plt.xlim(1, epochs)
# 將圖例放在左上角
plt.legend(loc='upper left')
# 保存圖片
plt.savefig("./acc.png")
# 保存模型時間
time_ = int(time.time())
# 設置保存路徑和模型名稱
MODEL_PATH = './model/BERT_net_%d.pth' % time_
# 保存模型
torch.save(rnn.state_dict(), MODEL_PATH)
app
# 導入若干工具包
from flask import Flask
from flask import request
app = Flask(__name__)
import torch
# 導入中文預訓練模型的編碼函數
from bert_chinese_encode import get_bert_encode
# 導入微調網絡模型
from finetuning_net import Net
# 設置訓練好的模型路徑
MODEL_PATH = "./model/BERT_net.pth"
# 定義實例化的模型參數
embedding_size = 768
char_size = 20
dropout = 0.2
# 初始化微調模型
net = Net(embedding_size, char_size, dropout)
# 加載已經訓練好的模型
net.load_state_dict(torch.load(MODEL_PATH))
# 因爲是在線部分, 所以採用評估模式, 本質是模型參數不發生變化
net.eval()
# 定義服務請求的路徑和方式
@app.route('/v1/recognition/', methods=["POST"])
def recognition():
# 首先接收數據
text_1 = request.form['text1']
text_2 = request.form['text2']
# 對原始文本進行編碼
inputs = get_bert_encode(text_1, text_2, mark=102, max_len=10)
# 使用微調模型進行預測
outputs = net(inputs)
# 從預測張量中獲取結果
_, predicted = torch.max(outputs, 1)
# 返回字符串類型的結果
return str(predicted.item())
werobot服務+flask
app
# 服務框架使用Flask
# 導入相關的包
from flask import Flask
from flask import request
app = Flask(__name__)
# 導入發送http請求的requests工具
import requests
# 導入redis
import redis
# 導入json工具
import json
# 導入已經編寫好的Unit API文件
from unit import unit_chat
# 導入操作neo4j數據庫的工具
from neo4j import GraphDatabase
# 從配置文件config.py導入需要的若干配置信息
# 導入neo4j的相關信息
from config import NEO4J_CONFIG
# 導入redis的相關信息
from config import REDIS_CONFIG
# 導入句子相關模型服務的請求地址
from config import model_serve_url
# 導入句子相關模型服務的超時時間
from config import TIMEOUT
# 導入規則對話模型的加載路徑
from config import reply_path
# 導入用戶對話信息保存的過期時間
from config import ex_time
# 建立redis的連接池
pool = redis.ConnectionPool( **REDIS_CONFIG)
# 初始化neo4j的驅動對象
_driver = GraphDatabase.driver( **NEO4J_CONFIG)
# 查詢neo4j圖數據的函數
def query_neo4j(text):
''''
功能: 根據用戶對話文本中可能存在的疾病症狀, 來查詢圖數據庫, 返回對應的疾病名稱
text: 用戶輸入的文本語句
return: 用戶描述的症狀所對應的的疾病名稱列表
'''
# 開啓一個會話session來操作圖數據庫
with _driver.session() as session:
# 構建查詢的cypher語句, 匹配句子中存在的所有症狀節點
# 保存這些臨時的節點, 並通過關係dis_to_sym進行對應疾病名稱的查找, 返回找到的疾病名稱列表
cypher = "MATCH(a:Symptom) WHERE(%r contains a.name) WITH \
a MATCH(a)-[r:dis_to_sym]-(b:Disease) RETURN b.name LIMIT 5" %text
# 通過會話session來運行cypher語句
record = session.run(cypher)
# 從record中讀取真正的疾病名稱信息, 並封裝成List返回
result = list(map(lambda x: x[0], record))
return result
# 主要邏輯服務類Handler類
class Handler(object):
def __init__(self, uid, text, r, reply):
'''
uid: 用戶唯一標識uid
text: 標識該用戶本次輸入的文本信息
r: 代表redis數據庫的一個鏈接對象
reply: 規則對話模板加載到內存中的對象(字典對象)
'''
self.uid = uid
self.text = text
self.r = r
self.reply = reply
# 編寫非首句處理函數, 該用戶不是第一句問話
def non_first_sentence(self, previous):
'''
previous: 代表該用戶當前語句的上一句文本信息
'''
# 嘗試請求語句模型服務, 如果失敗, 打印錯誤信息
# 在此處打印信息, 說明服務已經可以進入到首句處理函數中
print("準備請求句子相關模型服務!")
try:
data = {"text1": previous, "text2": self.text}
# 直接向語句服務模型發送請求
result = requests.post(model_serve_url, data=data, timeout=TIMEOUT)
# 如果回覆爲空, 說明服務暫時不提供信息, 轉去百度機器人回覆
if not result.text:
return unit_chat(self.text)
# 此處打印信息, 說明句子相關模型服務請求成功且不爲空
print("句子相關模型服務請求成功, 返回結果爲:", result.text)
except Exception as e:
print("模型服務異常:", e)
return unit_chat(self.text)
# 此處打印信息, 說明程序已經準備進行neo4j數據庫查詢
print("騎牛模型服務後, 準備請求neo4j查詢服務!")
# 查詢圖數據庫, 並得到疾病名稱的列表結果
s = query_neo4j(self.text)
# 此處打印信息, 說明已經成功獲得了neo4j的查詢結果
print("neo4j查詢服務請求成功, 返回結果是:", s)
# 判斷如果結果爲空, 繼續用百度機器人回覆
if not s:
return unit_chat(self.text)
# 如果結果不是空, 從redis中獲取上一次已經回覆給用戶的疾病名稱
old_disease = self.r.hget(str(self.uid), "previous_d")
# 如果曾經回覆過用戶若干疾病名稱, 將新查詢的疾病和已經回覆的疾病做並集, 再次存儲
# 新查詢的疾病, 要和曾經回覆過的疾病做差集, 這個差集再次回覆給用戶
if old_disease:
# new_disease是本次需要存儲進redis數據庫的疾病, 做並集得來
new_disease = list(set(s) | set(eval(old_disease)))
# 返回給用戶的疾病res, 是本次查詢結果和曾經的回覆結果之間的差集
res = list(set(s) - set(eval(old_disease)))
else:
# 如果曾經沒有給該用戶的回覆疾病, 則存儲的數據和返回給用戶的數據相同, 都是從neo4j數據庫查詢返回的結果
res = new_disease = list(set(s))
# 將new_disease存儲進redis數據庫中, 同時覆蓋掉之前的old_disease
self.r.hset(str(self.uid), "previous_d", str(new_disease))
# 設置redis數據的過期時間
self.r.expire(str(self.uid), ex_time)
# 此處打印信息, 說明neo4j查詢後已經處理完了redis任務, 開始使用規則對話模板
print("使用規則對話模板進行返回對話的生成!")
# 將列表轉化爲字符串, 添加進規則對話模板中返回給用戶
if not res:
return self.reply["4"]
else:
res = ",".join(res)
return self.reply["2"] %res
# 編碼首句請求的代碼函數
def first_sentence(self):
# 此處打印信息, 說明程序邏輯進入了首句處理函數, 並且馬上要進行neo4j查詢
print("該用戶近期首次發言, 不必請求模型服務, 準備請求neo4j查詢服務!")
# 直接查詢neo4j圖數據庫, 並得到疾病名稱列表的結果
s = query_neo4j(self.text)
# 此處打印信息, 說明已經成功完成了neo4j查詢服務
print("neo4j查詢服務請求成功, 返回結果:", s)
# 判斷如果結果爲空列表, 再次訪問百度機器人
if not s:
return unit_chat(self.text)
# 將查詢回來的結果存儲進redis, 並且做爲下一次訪問的"上一條語句"previous
self.r.hset(str(self.uid), "previous_d", str(s))
# 設置數據庫的過期時間
self.r.expire(str(self.uid), ex_time)
# 將列表轉換爲字符串, 添加進規則對話模板中返回給用戶
res = ",".join(s)
# 此處打印信息, 說明neo4j查詢後有結果並且非空, 接下來將使用規則模板進行對話生成
print("使用規則對話生成模板進行返回對話的生成!")
return self.reply["2"] %res
# 定義主要邏輯服務的主函數
# 首先設定主要邏輯服務的路由和請求方法
@app.route('/v1/main_serve/', methods=["POST"])
def main_serve():
# 此處打印信息, 說明werobot服務成功的發送了請求
print("已經進入主要邏輯服務, werobot服務正常運行!")
# 第一步接收來自werobot服務的相關字段, uid: 用戶唯一標識, text: 用戶輸入的文本信息
uid = request.form['uid']
text = request.form['text']
# 從redis連接池中獲得一個活躍的連接
r = redis.StrictRedis(connection_pool=pool)
# 獲取該用戶上一次說的話(注意: 可能爲空)
previous = r.hget(str(uid), "previous")
# 將當前輸入的text存入redis, 作爲下一次訪問時候的"上一句話"
r.hset(str(uid), "previous", text)
# 此處打印信息, 說明redis能夠正常讀取數據和寫入數據
print("已經完成了初次會話管理, redis運行正常!")
# 將規則對話模板的文件Load進內存
reply = json.load(open(reply_path, "r"))
# 實例化Handler類
handler = Handler(uid, text, r, reply)
# 如果上一句話存在, 調用非首句服務函數
if previous:
return handler.non_first_sentence(previous)
# 如果上一句話不存在, 調用首句服務函數
else:
return handler.first_sentence()
'''
if __name__ == '__main__':
text = "我最近腹痛!"
result = query_neo4j(text)
print("疾病列表:", result)
'''
config
# 設置redis相關的配置信息
REDIS_CONFIG = {
"host": "0.0.0.0",
"port": 6379,
"decode_responses":True
}
# 設置neo4j圖數據庫的配置信息
NEO4J_CONFIG = {
"uri": "bolt://0.0.0.0:7687",
"auth": ("neo4j", "Itcast2019"),
"encrypted": False
}
# 設置句子相關服務的請求地址
model_serve_url = "http://0.0.0.0:5001/v1/recognition/"
# 設置服務的超時時間
TIMEOUT = 2
# 設置規則對話的模板加載路徑
reply_path = "./reply.json"
# 用戶對話信息保存的過期時間
ex_time = 36000
unit
# -*- coding: utf-8 -*-
import json
import random
import requests
# client_id 爲官網獲取的AK, client_secret 爲官網獲取的SK
client_id = "xxxxxxx"
client_secret = "xxxxxxx"
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 獲取 token 的 url https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s
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 獲取聊天機器人接口數據 https://aip.baidubce.com/rpc/2.0/unit/service/chat?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"
}
# 將聊天接口對應請求數據轉爲 json 數據
#request_post_data = json.dumps(post_data)
res = requests.post(url=unit_chatbot_url, json=post_data)
# print(res.content)
# 獲取聊天接口返回數據
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__":
#chat_input = "今晚吃啥呢想想"
#chat_reply = unit_chat(chat_input)
#print("用戶輸入 >>>", chat_input)
#print("Unit回覆 >>>", chat_reply)
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
wr
# 導入werobot和發送請求的requests
import werobot
import requests
# 設定主要邏輯服務的請求URL
url = "http://xxx.xxx.xxx.xxx:5000/v1/main_serve/"
# 設定服務超時的時間
TIMEOUT = 3
# 聲明微信訪問的請求
robot = werobot.WeRoBot(token="doctoraitoken")
# 設置所有請求的入口
@robot.handler
def doctor(message, session):
try:
# 獲取用戶的Id
uid = message.source
try:
# 檢查session, 判斷用戶是否第一次發言
if session.get(uid, None) != "1":
# 將添加{uid: "1"}
session[uid] = "1"
# 返回用戶一個打招呼的話
return '您好, 我是智能客服小艾, 有什麼需要幫忙的嗎?'
# 獲取message中的用戶發言內容
text = message.content
except:
# 有時候會出現特殊情況, 用戶很可能取消關注後來又再次關注
# 直接通過session判斷, 會發現該用戶已經不是第一次發言, 執行message.content語句
# 真實情況是該用戶登錄後並沒有任何的發言, 獲取message.content的時候就會報錯
# 在這種情況下, 我們也通過打招呼的話回覆用戶
return '您好, 我是智能客服小艾, 有什麼需要幫忙的嗎?'
# 向主邏輯服務發送請求, 獲得發送的數據體
data = {"uid": uid, "text": text}
# 利用requests發送請求
res = requests.post(url, data=data, timeout=TIMEOUT)
# 將返回的文本內容返回給用戶
return res.text
except Exception as e:
print("出現異常:", e)
return "對不起, 機器人客服正在休息..."
# 讓服務監聽在0.0.0.0:80
robot.config["HOST"] = "0.0.0.0"
robot.config["PORT"] = 80
robot.run()
supervisord.conf
; author: zhoumingzhen
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Notes:
; - Shell expansion ("~" or "$HOME") is not supported. Environment
; variables can be expanded using this syntax: "%(ENV_HOME)s".
; - Quotes around values are not supported, except in the case of
; the environment= options as shown below.
; - Comments must have a leading space: "a=b ;comment" not "a=b;comment".
; - Command will be truncated if it looks like a config file comment, e.g.
; "command=bash -c 'foo ; bar'" will truncate to "command=bash -c 'foo ".
[unix_http_server]
file=/tmp/supervisor.sock ; the path to the socket file
;chmod=0700 ; socket file mode (default 0700)
;chown=nobody:nogroup ; socket file uid:gid owner
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)
[inet_http_server] ; inet (TCP) server disabled by default
port=0.0.0.0:9001 ; ip_address:port specifier, *:port for all iface
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)
[supervisord]
logfile=./log/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
loglevel=info ; log level; default info; others: debug,warn,trace
pidfile=./log/supervisord.pid ; supervisord pidfile; default supervisord.pid
;nodaemon=true ; start in foreground if true; default false
minfds=1024 ; min. avail startup file descriptors; default 1024
minprocs=200 ; min. avail process descriptors;default 200
;umask=022 ; process file creation umask; default 022
;user=chrism ; default is current user, required if root
;identifier=supervisor ; supervisord identifier, default is 'supervisor'
;directory=/tmp ; default is not to cd during start
;nocleanup=true ; don't clean up tempfiles at start; default false
;childlogdir=/tmp ; 'AUTO' child log dir, default $TEMP
;environment=KEY="value" ; key value pairs to add to environment
;strip_ansi=false ; strip ansi escape codes in logs; def. false
; The rpcinterface:supervisor section must remain in the config file for
; RPC (supervisorctl/web interface) to work. Additional interfaces may be
; added by defining them in separate [rpcinterface:x] sections.
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
; The supervisorctl section configures how supervisorctl will connect to
; supervisord. configure it match the settings in either the unix_http_server
; or inet_http_server section.
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
serverurl=http://0.0.0.0:9001 ; use an http:// url to specify an inet socket
;username=chris ; should be same as in [*_http_server] if set
;password=123 ; should be same as in [*_http_server] if set
;prompt=mysupervisor ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history ; use readline history if available
; The sample program section below shows all possible program subsection values.
; Create one or more 'real' program: sections to be able to control them under
; supervisor.
[program:main_server]
command=gunicorn -w 1 -b 0.0.0.0:5000 app:app ; the program (relative uses PATH, can take args)
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1 ; number of processes copies to start (def 1)
;directory=/tmp ; directory to cwd to before exec (def no cwd)
;umask=022 ; umask for process (default None)
;priority=999 ; the relative start priority (default 999)
;autostart=true ; start at supervisord start (default: true)
;startsecs=1 ; # of secs prog must stay up to be running (def. 1)
;startretries=3 ; max # of serial start failures when starting (default 3)
;autorestart=unexpected ; when to restart if exited after running (def: unexpected)
;exitcodes=0,2 ; 'expected' exit codes used with autorestart (default 0,2)
stopsignal=QUIT ; signal used to kill process (default TERM)
;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
stopasgroup=false ; send stop signal to the UNIX process group (default false)
killasgroup=false ; SIGKILL the UNIX process group (def false)
;user=chrism ; setuid to this UNIX account to run the program
;redirect_stderr=true ; redirect proc stderr to stdout (default false)
stdout_logfile=./log/main_server_out ; stdout log path, NONE for none; default AUTO
stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10 ; # of stdout logfile backups (0 means none, default 10)
;stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
;stdout_events_enabled=false ; emit events on stdout writes (default false)
stderr_logfile=./log/main_server_error ; stderr log path, NONE for none; default AUTO
stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10 ; # of stderr logfile backups (0 means none, default 10)
;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
;stderr_events_enabled=false ; emit events on stderr writes (default false)
;environment=A="1",B="2" ; process environment additions (def no adds)
;serverurl=AUTO ; override serverurl computation (childutils)
[program:redis]
command=redis-server