天天看點

Python實作 利用樸素貝葉斯模型(NBC)進行問句意圖分類

利用樸素貝葉斯模型實作問句意圖分類。創新點:槽值替換。思路:自己建構資料集和類别,再對問句進行分詞,槽值替換,去停用詞,根據特征詞建構one-hot向量後,調用skearn子產品的樸素貝葉斯接口模組化,對問句進行分類。

目錄

      樸素貝葉斯分類(NBC)

      程式簡介

      分類流程

      字典(dict)構造:用于jieba分詞和槽值替換

      資料集建構

      代碼分析

      另外:點選右下角魔法陣上的【顯示目錄】,可以導航~~

樸素貝葉斯分類(NBC)

        這篇部落格的重點不在于樸素貝葉斯分類的原理,而在于怎麼用樸素貝葉斯分類器解決實際問題。是以這邊我就簡單介紹以下我們使用的模型。

        NBC模型所需估計的參數很少,對缺失資料不太敏感,算法也比較簡單。貝葉斯方法是以貝葉斯原理為基礎,使用機率統計的知識對樣本資料集進行分類。它假設特征條件之間互相獨立,先通過已給定的訓練集,以特征詞之間獨立作為前提假設,學習從輸入到輸出的聯合機率分布,再基于學習到的模型,輸入X求出使得後驗機率最大的輸出Y。

程式簡介

      這是一個糖尿病醫療智能問答的子子產品。目的是把糖尿病患者提出的問題對其意圖進行分類,以便後續根據知識圖譜對問句進行回答。

      對于意圖分類這個子子產品,我的思路是自己建構資料集和類别,再對問句進行分詞,槽值替換,去停用詞,根據特征詞建構one-hot向量後,調用skearn子產品的樸素貝葉斯接口模組化,對問句進行分類。

      要解決這個意圖分類問題,有以下幾點需要注意:

  1. 在缺少大量而準确的資料集的情況下,深度學習模型并不是一個很好的選擇,沒有足夠多的樣本,難以訓練好那麼多的參數。于是我在這裡選擇樸素貝葉斯的方法。
  2. 由于缺少現成的資料集,我自己建構了資料集。這裡有些講究:每個類别最好數量均衡;每個類别要注意包含一些關鍵詞,各個關鍵詞數量均衡,搭配均衡。
  3. “一型糖尿病可以吃草莓嗎?“ 與 ”二型糖尿病可以吃蘋果嗎?“在意圖上是一個類别的問題,然而重疊的特征詞似乎隻有“可以”,“吃”,“嗎”,而這些有點類似于停用詞了。但我們人類能看出它們是一個意圖,是因為“一型糖尿病”,“二型糖尿病”,“草莓”,“蘋果”這些實詞。但我們不可能在資料集裡窮舉所有的病名和水果名甚至其它食物,那如何讓它們被識别為一類呢?是以這裡我進行了槽值替換。将所有病名都替換為”[DISEASE]";所有蔬菜水果都替換為"[FOOD]"。替換後再統計詞頻進行訓練,效果就比較好。

     最終我實作了這樣一個分類器,它的接口為

  • 輸入:糖尿病患者提出的問句
  • 輸出:問句的意圖分類

     github位址:https://github.com/PengJiazhen408/Naive-Bayesian-Classifier

分類流程

問句-->槽值替換-->分詞-->根據vocab.txt 生成特征向量 --> 模型預測生成标簽 --> 标簽轉換為類别(中文)

如:

問句 槽值替換後 分詞後 one-hot 特征詞向量 标簽 類别
糖尿病可以吃草莓嗎 [DISEASE]可以吃[FOOD]嗎 [/DISEASE/]/可以/吃/[/FOOD/]/嗎 [1, 0, 0, 0, 0, 0, 0, 0, 1 ....] 2 飲食
胰島素的副作用 [DRUG]的副作用 [/DRUG/]/的/副作用 [0, 0, 0, 1, 0, 0, 0, 0, 0 ....] 5 用藥情況
糖尿病吃什麼藥 [DISEASE]吃什麼藥 [/DISEASE/]/吃什麼/藥 [1, 0, 0, 0, 0, 0, 0, 0, 0 ....] 3 用藥治療
糖尿病高血糖怎麼治 [DISEASE][DISEASE]怎麼治 [/DISEASE/]/[/DISEASE/]/怎麼/治 [2, 0, 0, 0, 0, 0, 0, 1, 0 ....] 治療

字典(dict)構造:用于jieba分詞和槽值替換

檔案名 内容 例子
category.txt 食物集合名 如:海鮮,早餐等
check.txt 檢查項目名 如:測血糖,抽血等
department.txt 科室名 如:内分泌科,兒科等
disease.txt 疾病名 如:糖尿病,一型糖尿病等
drug.txt 藥物名 如:胰島素,二甲雙胍等
food.txt 食物和水果 如:蘋果,綠豆等
style.txt 生活方式名 如:運動,跑步,洗澡等
symptom.txt 症狀名 如:頭疼,腹瀉等

格式

  1. 一個詞一行
  2. 每個檔案第一個詞是類别名,如[DISEASE],[DRUG] 用于槽值替換

資料集建構

  1. 訓練集(train_data.txt): 各類50例
  2. 測試集(test_data.txt): 各類5或10例
  3. 格式:問句 類别(中文)
  4. 注意事項:每個類别要注意包含一些關鍵詞,各個關鍵詞數量均衡,搭配均衡

代碼分析

共有4個主程式:

  • data_pro.py     處理原始資料集,生成類别檔案,将類别轉換為數字标簽并存為檔案
  • extract.py        根據訓練資料,經過槽值替換,分詞,人工篩選等步驟,生成 vocab.txt 
  • main.py           模型訓練,測試,單句預測
  • predict.py        根據訓練好的模型,批量預測

data_pro.py:  由train_data.txt, test_data.txt生成 class.txt, train.txt, test.txt

 導入子產品

from utils import open_data
from random import shuffle
import os           

加載原始資料集(train_data.txt, test_data.txt)

def open_data(path):
    contents, labels = [], []
    with open(path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        for line in lines:
            lin = line.strip()
            if not lin:
                continue
            content, label = lin.split('\t')
            contents.append(content)
            labels.append(label)
    return contents, labels

# 加載原始資料
x_train, c_train = open_data('data_orig/train_data.txt')
x_test, c_test = open_data('data_orig/test_data.txt')           

對類别清單去重,生成class.txt

# 識别所有類,生成類别清單和字典,并儲存類别清單
if not os.path.exists('data'):
    os.mkdir('data')
class_list = list(set(c_train))
with open('data/class.txt', 'w', encoding='utf-8') as f:
    f.writelines(content+'\n' for content in class_list)
class_dict = {}
for i, item in enumerate(class_list):
    class_dict[item] = str(i)           

将每個問句的類别(中文)轉換為标簽(數字)

打亂資料集,儲存在train.txt, test.txt

def pro_data(x, c, str):
    y = [class_dict[i] for i in c]
    all_data = list(zip(x, y))
    shuffle(all_data)
    x[:], y[:] = zip(*all_data)
    folder = 'data/'
    save_sample(folder, str, x, y)
    return x, y
  
def save_sample(data_folder, str, x, y):
    path = data_folder + str + '.txt'
    with open(path, 'w', encoding='utf-8') as f:
        for i in range(len(x)):
            content = x[i] + '\t' + y[i] + '\n'
            f.write(content)
  
# 類别轉換為标簽, 打亂順序, 儲存
x_train, y_train = pro_data(x_train, c_train, 'train')
x_test, y_test = pro_data(x_test, c_test, 'test')           

extract.py: 由train.txt, stopwords.txt 生成 特征清單 vocab.txt

加載train.txt,提取其所有問句;加載stopwords.txt,生成停用詞表

def open_data(path):
    contents, labels = [], []
    with open(path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        for line in lines:
            lin = line.strip()
            if not lin:
                continue
            content, label = lin.split('\t')
            contents.append(content)
            labels.append(label)
    return contents, labels

# 加載問句清單和停用詞表
questions, _ = open_data('data/train.txt')
stopwords = [line.strip() for line in open("data/stopwords.txt", 'r', encoding="utf-8").readlines()]           

對問句槽值替換。步驟:

  1. 加載dict中的檔案,生成同義詞表的字典,key:每個詞,value:該詞所在檔案第一個詞
  2. 問句分詞
  3. 周遊問句的每個詞,用同義詞表進行替換
  4. 傳回替換後的句子
原句 分詞 替換
糖尿病/可以/吃/草莓/嗎 DISEASE]可以吃[FOOD]嗎
胰島素/的/副作用
糖尿病/吃什麼/藥
糖尿病/高血糖/怎麼/治
def load_jieba():
    # jieba加載詞典
    for _, _, filenames in os.walk('dict'):
        for filename in filenames:
            jieba.load_userdict(os.path.join('dict', filename))
    # jieba分詞時,對下列這些詞繼續往下分
    del_words = ['糖尿病人', '常用藥', '藥有', '感冒藥', '特效藥', '止疼藥', '中成藥', '中藥', '止痛藥', '降糖藥', '單藥', '喝啤酒',
                 '西藥', '怎樣才能', '要測', '要驗', '能測', '能驗', '喝酒', '喝奶', '吃糖', '喝牛奶', '吃肉', '茶好', '吃水果']
    # jieba分詞時,不要把下列這些詞分開
    add_words = ['DISEASE', 'SYMPTOM', 'CHECK', 'FOOD', 'STYLE', 'CATEGORY', '會不會', '能不能', '可不可以', '是不是', '要不要',
                 '應不應該', '啥用', '什麼用', '吃什麼', '喝什麼']
    for word in del_words:
        jieba.del_word(word)
    for word in add_words:
        jieba.add_word(word)           
def synonym_sub(question):
    # dict檔案夾中的每個檔案是一個同義詞表
    # 1讀取同義詞表:并生成一個字典。
    combine_dict = {}
    for _, _, filenames in os.walk('dict'):
        for filename in filenames:
            fpath = os.path.join('dict', filename)
            # 加載同義詞
            synonyms = []
            with open(fpath, 'r', encoding='utf-8') as f:
                lines = f.readlines()
                for line in lines:
                    synonyms.append(line.strip())
            for i in range(1, len(synonyms)):
                combine_dict[synonyms[i]] = synonyms[0]
    # with open('synonym.txt', 'w', encoding='utf-8') as f:
    #     f.write(str(combine_dict))

    # 2将語句切分
    seg_list = jieba.cut(question, cut_all=False)
    temp = "/".join(seg_list)
    # print(temp)

    # 3
    final_sentence = ""
    for word in temp.split("/"):
        if word in combine_dict:
            word = combine_dict[word]
            final_sentence += word
        else:
            final_sentence += word
    # print(final_sentence)
    return final_sentenc           
load_jieba()
# 槽值替換
for i in range(len(questions)):
    questions[i] = synonym_sub(questions[i])           

分詞,統計各個詞詞頻,按照詞頻從大到小對詞語排序,生成詞語清單

詞語清單除去停用詞表中的詞語;除去長度為1的詞;加上對分類有用的長度為1的詞,如 ‘吃’,‘藥’,‘治’等

将詞語清單(也即特征清單)儲存在vocab.txt

# 分詞
words = jieba.cut("\n".join(questions), cut_all=False)
print(words)

# 統計詞頻
word_count = {}
stopwords = [line.strip() for line in open("data/stopwords.txt", 'r', encoding="utf-8").readlines()]
for word in words:
    if word not in stopwords:
        if len(word) == 1:
            continue
        word_count[word] = word_count.get(word, 0) + 1

items = list(word_count.items())
items.sort(key=lambda x: x[1], reverse=True)

# vocab中添加的單字
single = ['吃', '喝', '藥', '能', '治', '啥', '病', '查', '測', '檢', '驗', '酒', '奶', '糖']
with open('data/vocab.txt', 'w', encoding='utf-8') as f:
    for item in items:
        f.write(item[0]+'\n')
    for word in single:
        f.write(word + '\n')           

停用詞表構造:人工篩選出vocab.txt中對分類無意義的詞,加入到stopwords.txt中。再重複運作extract.py

main.py: 模型訓練,測試,預測

導入子產品,預設路徑

from utils import *
from sklearn.naive_bayes import MultinomialNB
from sklearn import metrics
import numpy as np
from termcolor import colored
import matplotlib.pyplot as plt
import seaborn as sns
import pickle
import pandas as pd

data_dir = 'data'           

加載train.txt, test.txt, class.txt

# 加載訓練集 測試集 類别
q_train, y_train = open_data(os.path.join(data_dir,'train.txt'))
q_test, y_test = open_data(os.path.join(data_dir, 'test.txt'))
classes = [line.strip() for line in open(os.path.join(data_dir, 'class.txt'), 'r', encoding="utf-8").readlines()]           

模型訓練:訓練集問句-->槽值替換-->分詞-->根據vocab.txt 生成特征向量 --> (特征向量,标簽) 作為模型輸入 --> 訓練模型

def qes2wb(questions):
    # 加載vocab
    data_folder = 'data/'
    vocab = [line.strip() for line in open(data_folder+'vocab.txt', 'r', encoding="utf-8").readlines()]

    # 問句,槽值替換後, 分詞,轉換為詞袋向量
    vecs = []
    load_jieba()
    for question in questions:
        sen = synonym_sub(question)
        # print(sen)
        words = list(jieba.cut(sen, cut_all=False))
        # print('/'.join(words))
        vec = [words.count(v) for v in vocab]
        vecs.append(vec)
    return vecs           
# 轉為詞袋模型
x_train = qes2wb(q_train)

# 模組化
model = MultinomialNB()
model.fit(x_train, y_train)

# 儲存模型
with open('MultinomialNB.pkl', 'wb') as f:
    pickle.dump(model, f)           

模型測試:測試集問句-->槽值替換-->分詞-->根據vocab.txt 生成特征向量 --> 模型預測生成預測标簽 

def qes2wb(questions):
    # 加載vocab
    data_folder = 'data/'
    vocab = [line.strip() for line in open(data_folder+'vocab.txt', 'r', encoding="utf-8").readlines()]

    # 問句,槽值替換後, 分詞,轉換為詞袋向量
    vecs = []
    load_jieba()
    for question in questions:
        sen = synonym_sub(question)
        # print(sen)
        words = list(jieba.cut(sen, cut_all=False))
        # print('/'.join(words))
        vec = [words.count(v) for v in vocab]
        vecs.append(vec)
    return vecs           
# 轉為詞袋模型
x_test = qes2wb(q_test)

# 測試
p_test = model.predict(x_test)
y_test = np.array(y_test)
p_test = np.array(p_test)
           

模型評價:預測标簽與真實标簽比對,輸出評價名額,分析 bad cases, 可視化混淆矩陣

# 輸出模型測試結果
print(metrics.classification_report(y_test, p_test, target_names=classes))
# for i, c in enumerate(classes):
#     print("%d: %s" % (i, c), end='\t')
# print('\n')
# print(metrics.classification_report(y_test, p_test))

# 輸出錯誤
errors = []
for i in range(len(y_test)):
    if y_test[i] != p_test[i]:
        errors.append((y_test[i], q_test[i], p_test[i]))
print('---Bad Cases---')
for y, q, p in sorted(errors):
    print('Truth: %-20s Query: %-30s Predict: %-20s' % (classes[int(y)], q, classes[int(p)]))

# 混淆矩陣
# 用來正常顯示中文标簽
plt.rcParams['font.sans-serif'] = ['SimHei']
# 用來正常顯示負号
plt.rcParams['axes.unicode_minus'] = False
# 計算混淆矩陣
confusion = metrics.confusion_matrix(y_test, p_test)
plt.clf()
plt.title('分類混淆矩陣')
sns.heatmap(confusion, square=True, annot=True, fmt='d', cbar=False,
            xticklabels=classes,
            yticklabels=classes,
            linewidths=0.1, cmap='YlGnBu_r')
plt.ylabel('實際')
plt.xlabel('預測')
plt.xticks(rotation=-13)
plt.savefig('分類混淆矩陣.png', dpi=100)           

模型評價結果:

precision    recall  f1-score   support
    
        治療       0.91      1.00      0.95        10
    檢查項目       0.91      1.00      0.95        10
        飲食       0.90      0.90      0.90        10
    用藥治療       1.00      1.00      1.00        10
      并發症       1.00      1.00      1.00        10
    用藥情況       0.77      1.00      0.87        10
  遺傳與傳染       1.00      1.00      1.00        10
        病因       0.83      1.00      0.91         5
        其它       0.89      0.80      0.84        10
        症狀       1.00      0.50      0.67        10
  保健與護理       1.00      1.00      1.00        10

    accuracy                           0.92       105
   macro avg       0.93      0.93      0.92       105
weighted avg       0.93      0.92      0.92       105           
---Bad Cases---
Truth         Query                       Predict
保健與護理    糖尿病如何降低血糖          遺傳與傳染
病因          糖尿病是如何造成的          保健與護理
其它          壓力大會引起糖尿病嗎        病因
其它          糖尿病并發症咋辦            并發症
其它          糖尿病并發症應注意什麼      并發症
其它          糖尿病并發症怎麼治          治療
其它          糖尿病并發症的病因          并發症           
Python實作 利用樸素貝葉斯模型(NBC)進行問句意圖分類

單句預測:輸入問句-->提取特征(槽值替換 --> 分詞 --> 轉換為向量)--> 模型預測 --> 輸出預測的意圖

def qes2wb(questions):
    # 加載vocab
    data_folder = 'data/'
    vocab = [line.strip() for line in open(data_folder+'vocab.txt', 'r', encoding="utf-8").readlines()]

    # 問句,槽值替換後, 分詞,轉換為詞袋向量
    vecs = []
    load_jieba()
    for question in questions:
        sen = synonym_sub(question)
        # print(sen)
        words = list(jieba.cut(sen, cut_all=False))
        # print('/'.join(words))
        vec = [words.count(v) for v in vocab]
        vecs.append(vec)
    return vecs           
# 單句預測
while True:
    query = input(colored('請咨詢:', 'green'))
    x_query = qes2wb([query])
    # print(x_query)
    p_query = model.predict(x_query)
    # print(p_query)
    print('意圖: ' + classes[int(p_query[0])])           

預測結果:

請咨詢:糖尿病可以吃草莓嗎
意圖: 飲食

請咨詢:胰島素的副作用
意圖: 用藥情況

請咨詢:糖尿病吃什麼藥
意圖: 用藥治療

請咨詢:糖尿病高血糖怎麼治
意圖: 治療           

predict.py:  批量預測

from utils import *
import pandas as pd
import pickle
import os

classes_dir = 'data'
data_dir = 'predict_data'           

 加載class.txt(類别),question.csv(問句清單),模型

# 加載類别和問句
classes = [line.strip() for line in open(os.path.join(classes_dir, 'class.txt'), 'r', encoding="utf-8").readlines()]
sentence_csv = pd.read_csv(os.path.join(data_dir, 'question.csv'), sep='\t', names=['title'])
sentences = sentence_csv['title'].tolist()

# 加載模型
if os.path.exists('MultinomialNB.pkl'):
    with open('MultinomialNB.pkl', 'rb') as f:
        model = pickle.load(f)
else:
    raise Exception("Please run main.py first!")

# 預測
x_query = qes2wb(sentences)
p_query = model.predict(x_query)
results = [classes[int(p)] for p in p_query]

# 儲存結果
dataframe = pd.DataFrame({'title': sentences, 'classes': results})
dataframe.to_csv(os.path.join(data_dir, 'result.csv'), index=False, sep=',')           
title classes