離線部分+在線部分:命名實體審覈任務RNN模型、命名實體識別任務BiLSTM+CRF模型、BERT中文預訓練+微調模型、werobot服務+flask

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度學習實戰(不定時更新)


智能對話系統:Unit對話API

在線聊天的總體架構與工具介紹:Flask web、Redis、Gunicorn服務組件、Supervisor服務監控器、Neo4j圖數據庫

linux 安裝 neo4jlinux 安裝 Redissupervisor 安裝

neo4j圖數據庫:Cypher

neo4j圖數據庫:結構化數據流水線、非結構化數據流水線

命名實體審覈任務:BERT中文預訓練模型

命名實體審覈任務:構建RNN模型

命名實體審覈任務:模型訓練

命名實體識別任務:BiLSTM+CRF part1

命名實體識別任務:BiLSTM+CRF part2

命名實體識別任務:BiLSTM+CRF part3

在線部分: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 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章