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

真語發表於2021-07-15

目錄

      樸素貝葉斯分類(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 ....] 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]嗎
胰島素的副作用 胰島素/的/副作用 [DRUG]的副作用
糖尿病吃什麼藥 糖尿病/吃什麼/藥 [DISEASE]吃什麼藥
糖尿病高血糖怎麼治 糖尿病/高血糖/怎麼/治 [DISEASE][DISEASE]怎麼治
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(問句列表),模型

批量預測:問句 -->提取特徵(槽值替換 --> 分詞 --> 轉換為向量)--> 模型預測

儲存結果到result.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=',')

result.csv

title classes
糖尿病可以吃草莓嗎 飲食
胰島素的副作用 用藥情況
糖尿病吃什麼藥 用藥治療
糖尿病高血糖怎麼治 治療

相關文章