NLU意圖識別的流程說明
基於智能問答的業務流程,所謂的NLU意圖識別就是針對已知的訓練語料(如語料格式為\((x,y)\)格式的元組列表,其中\(x\)為訓練語料,\(y\)為期望輸出類別或者稱為意圖)采用選定的算法構建一個模型,而后基於構建的模型對未知的文本進行分類。流程梳理如下:
- 准備訓練數據,按照固定的格式進行;
- 抽取所需要的特征,形成特征向量;
- 抽取的特征向量與對應的期望輸出(也就是目標label)一起輸入到機器學習算法中,訓練出一個預測模型;
- 對新到的數據采取同樣的特征抽取,得到用於預測的特征向量;
- 使用訓練好的預測模型,對處特征處理后的新數據進行預測,並返回結果。
從流程梳理看,NLU的意圖識別從根本上看是有監督的機器學習,即基於給定的人工篩選數據進行特征處理,構建模型用於預測。處理流程圖如下所示:
針對NLU意圖識別原理的例子說明
基於上述說明的流程,用一個例子來進行原理說明。
場景
小明需要訂購一張從上海到北京的機票(意圖:訂機票),在訂票的過程中想要了解下北京的天氣情況(意圖:查天氣),看是否需要准備一把雨傘;同時考慮初到北京對地方不熟悉,希望協助訂餐(意圖:訂餐)。
准備數據
根據第一部分介紹的流程,需要先准備上述場景涉及意圖的訓練數據,以備模型構建使用,我們准備如下的幾條數據(此處為了描述方便,每個意圖我們僅提供一條數據進行說明,在實際處理中每個意圖至少需要2條語料):
- 幫我查詢明天到北京的機票。---------> 訂機票
- 北京明天是否有雨?---------> 查天氣
- 幫我定個烤鴨送到酒店。---------> 訂餐
在第一條的語料“幫我查詢明天到北京的機票。”中,我們可以通過實體抽取提取出“時間:明天、目的地:北京”,通過隱含條件可以推算出出發地為上海(基於用戶當前的定位數據等),這部分信息可以作為對話過程中的關鍵信息收集起來,作為后續的訂票主要信息。因此處我們主要介紹意圖識別,對於實體提取的一些處理流程不做具體的介紹。
特征提取
特征的提取是為了方便后續的計算,在中文文本處理中,常用的特征處理有詞袋模型(bag of word)、Tf-idf、SVD奇異值分解等,這里為了方便說明,我們采用詞袋模型(bag of word)。特征權重的計算方法采用簡單的\(1/n\)進行,其中\(n\)為所有語料中該詞出現的個數。
針對上文的數據,首先構造詞典,利用jieba分詞,對上述語料進行分詞,而后統計各個詞的權重,結果如下所示:
"幫我查詢明天到北京的機票。"分詞結果如下:
['幫', '我', '查詢', '明天', '到', '北京', '的', '機票', '。']
"北京明天是否有雨?"分詞結果如下:
['北京', '明天', '是否', '有雨', '?']
"幫我定個烤鴨送到酒店。"分詞結果如下:
['幫', '我定', '個', '烤鴨', '送到', '酒店', '。']
整合后的詞典及詞權重如下所示:
['幫':0.5, '我':1.0, '查詢':1.0, '明天':0.5, '到':1.0, '北京':0.5, '的':1.0, '機票':1.0, '。':0.5, '是否':1.0, '有雨':1.0, '?':1.0, '我定':1.0, '個':1.0, '烤鴨':1.0, '送到':1.0, '酒店':1.0]
模型訓練
基於上步構造的詞典,對樣本數據進行特征權重構造(也就是模型構建的過程,如果是使用神經網絡等深度學習技術的話,就是通過樣本數據學習各個特征的權重及偏置),如下所示:
"幫我查詢明天到北京的機票。"特征如下:
['幫':0.5, '我':1.0, '查詢':1.0, '明天':0.5, '到':1.0, '北京':0.5, '的':1.0, '機票':1.0, '。':0.5]
"北京明天是否有雨?"特征如下:
['北京':0.5, '明天':1.0, '是否':1.0, '有雨':1.0, '?':1.0]
"幫我定個烤鴨送到酒店。"特征如下:
['幫':0.5, '我定':1.0, '個':1.0, '烤鴨':1.0, '送到':1.0, '酒店':1.0, '。':0.5]
新數據特征提取
在上述幾步流程中,我們已經通過准備的訓練數據構建好了預測模型,針對新的數據,需要經過同樣的特征提取流程獲取新數據的特征向量。
比如新數據為:查詢機票。
結巴分詞結果如下:
['查詢', '機票', '。']
基於構造的詞典,獲得各個特征的權重如下:
['查詢':1.0, '機票':1.0, '。':0.5]
模型預測
通過模型計算新數據與各個類別(意圖)的得分(為了方便說明,這里直接比對新數據與各個類別數據特征匹配上的個數,而后計算相關的權重得分),如下所示:
訂機票意圖特征命中:查詢、機票、。
得分:1.0(查詢)+1.0(機票)+0.5(。)=2.5
查天氣意圖特征命中:
得分:0.0(查詢)+0.0(機票)+0.0(。)=0.0
訂餐意圖特征命中:。
得分:0.0(查詢)+0.0(機票)+0.5(。)=0.5
采用得分最高作為最終意圖,則新數據意圖為“訂機票”。
備注:在上述的處理過程中,出現了很多的得分為0項,在實際的處理過程中會做平滑處理,常用的平滑處理有\(add-k smoothing、Good-turning\)等平滑方法。
NLU識別引擎中使用的pipline分析
上面是簡單的描述了文本分類模型的構建及模型使用的介紹,在實際的場景處理中會比較復雜,本節針對我們在使用RASA框架的NLU模塊的一個文本處理pipline進行流程分析說明。
pipline如下所示:
language: "zh"
pipeline:
- name: JiebaTokenizer
- name: CRFEntityExtractor
- name: EntitySynonymMapper
- name: CountVectorsFeaturizer
- name: EmbeddingIntentClassifier
意圖識別的三個流程涉及JiebaTokenizer(分詞)、CountVectorsFeaturizer(特征向量表示)、EmbeddingIntentClassifier(分類)三個過程,我們主要對上述三個進行說明。
JiebaTokenizer 分詞
分詞組件這里主要是使用的一個開源分詞jieba分詞,通過結巴分詞將我們的訓練語料或者是傳入的用戶語句進行分詞處理,獲取分詞后的結果。
關注下,語料經過jieba分詞后會得到一個詞在該條語料中的開始start、結束end位置信息,在最終返回給其他算法處理時僅返回詞條在訓練語料的開始start位置,結束end信息后續會通過開始start+len(word)的方式獲得。
分詞的示例:
import jieba
text = "幫我查詢明天到北京的機票。"
tokenized = jieba.tokenize(text)
# tokens = [Token(word, start) for (word, start, end) in tokenized]
print(list(tokenized))
[('幫', 0, 1), ('我', 1, 2), ('查詢', 2, 4), ('明天', 4, 6), ('到', 6, 7), ('北京', 7, 9), ('的', 9, 10), ('機票', 10, 12), ('。', 12, 13)]
CountVectorsFeaturizer 特征向量表示
CountVectorsFeaturizer 是一種基於特征的tf表示的向量標識方法,其核心思想與上述示例中的基本一致,也是通過對訓練集數據經過分詞后構建詞典,而后針對每一條訓練文本統計其特征的相關tf,形成特征向量表示(或者認為是詞頻矩陣)。這里需要注意下,CountVectorsFeaturizer 中有一些默認參數,會對分詞后的數據進行一些處理,比如針對英文的一些大小寫轉換、針對中文的單字過濾、停用詞過濾等操作。
我們仍然使用上文示例中出現的3條語料進行CountVectorsFeaturizer 的表示,示例代碼采用sklearn中的CountVectorizer,如下所示:
from sklearn.feature_extraction.text import CountVectorizer
# "幫 我 查詢 明天 到 北京 的 機票" 為輸入列表元素,即代表一個文章的經過分詞后的詞,這里為了便於說明去除了其他一些信息,每個語料為一條信息
texts = ["幫 我 查詢 明天 到 北京 的 機票", "北京 明天 是否 有雨", "幫 我定 個 烤鴨 送到 酒店"]
# 創建詞袋模型
cv = CountVectorizer()
# 詞袋模型構建
cv_fit = cv.fit_transform(texts)
# 打印所有的訓練語料形成的詞典中的詞
print(cv.get_feature_names())
結果: ['北京', '我定', '明天', '是否', '有雨', '機票', '查詢', '烤鴨', '送到', '酒店']
# 打印所有的訓練語料形成的詞典及該詞在詞典中的標號
print(cv.vocabulary_)
結果: {'查詢': 6, '明天': 2, '北京': 0, '機票': 5, '是否': 3, '有雨': 4, '我定': 1, '烤鴨': 7, '送到': 8, '酒店': 9}
# 打印出特征的詞頻標識
print(cv_fit)
結果: (0, 5) 1 0:texts中的第0個元素; 5:詞典中順序為5的詞,即“機票”; 1:詞頻
(0, 0) 1
(0, 2) 1
# 結果轉化為稀疏舉證標識
print(cv_fit.toarray())
結果: [[1 0 1 0 0 1 1 0 0 0]
[1 0 1 1 1 0 0 0 0 0]
[0 1 0 0 0 0 0 1 1 1]]
上述示例中單條語料中沒有出現重復的詞,我們對第一條語料增加一個“北京”對比看下,結果如下所示:
from sklearn.feature_extraction.text import CountVectorizer
# "幫 我 查詢 明天 到 北京 的 機票" 為輸入列表元素,即代表一個文章的字符串
texts = ["幫 我 查詢 明天 到 北京 的 機票 北京", "北京 明天 是否 有雨", "幫 我定 個 烤鴨 送到 酒店"]
# 創建詞袋模型
cv = CountVectorizer()
cv_fit = cv.fit_transform(texts)
# print(cv.get_feature_names())
# print(cv.vocabulary_)
# print(cv_fit)
print(cv_fit.toarray())
結果:[[2 0 1 0 0 1 1 0 0 0]
[1 0 1 1 1 0 0 0 0 0]
[0 1 0 0 0 0 0 1 1 1]]
經過處理后就將我們所提供的訓練文本轉換成了特征的向量表示形式,這些特征向量在傳入到EmbeddingIntentClassifier中與各條語料的類別標識一同進行訓練成模型。
EmbeddingIntentClassifier 分類(模型構建)
rasa框架是通過集成TensorFlow來進行模型構建的,一些細節進行了封裝,為了說明清晰,這里撇開TensorFlow框架進行分析。
同樣使用上文處理好的稀疏向量為例:
三條語料:幫我查詢明天到北京的機票。 北京明天是否有雨? 幫我定個烤鴨送到酒店。 其對應的特征向量如下所示:
[[1 0 1 0 0 1 1 0 0 0]
[1 0 1 1 1 0 0 0 0 0]
[0 1 0 0 0 0 0 1 1 1]]
三條語料的意圖分別為:訂機票、查天氣、訂餐
基於上述的特征數據,我們構建一個4層的神經網絡,其中第一層為輸入層,其接收我們處理后的特征向量數據,根據上述示例,每個語料有10個向量值,則我們的第一層輸入層對應有10個神經元;最后一層為輸出層,也就是結構層,我們這里有三個意圖分類,則我們的輸出神經元對應有3個;中間兩層時隱藏層,我們可以根據需要進行設計。
備注:神經網絡的主要思想可以看做是通過大量的訓練樣本,自動學習一個模擬函數,進而對未知數據進行預測。或者說神經網絡使用樣本數據自動推斷出每一類的的特征規則,然后應用的新的位置數據上,進而達到分類的目的。
經過上述說明后,設計如下的神經網絡結構:
輸入層較多,我們僅畫出部分表示,層與層之間采用全連接的方式,激活函數我們選擇使用sigmoid函數。
針對上述的神經網絡結構,每一個神經元的結構如下所示:
其中\(x_1,x_2,...,x_{10}\)就是我們上文處理后的特征向量,我們上文示例是10維度的,一般的情況下輸出的維度都比較大,\(z=w_1x_1+w_2x_2+...+w_{10}x_{10}+b\)為加權輸入,輸出則為激活函數作用在加權輸入\(z\)上,即\(y=\sigma(z)\).
上述我們已經構建了一個4層的神經網絡,那該神經網絡如何與我們的分類結合在一起呢?在數學上,我們一般將這類問題歸納為優化問題,也就是有了訓練數據與相關的數據標識,則可以通過設計相關的優化函數進行。比如我們在上文已經將語料 “幫我查詢明天到北京的機票。” 標識成一個1X10的向量表示 [1 0 1 0 0 1 1 0 0 0],我們設計了一個模擬函數\(y=y(x)\)標識對應的期望輸出,根據上文示例,這個輸出是一個3維的向量,對於語料 “幫我查詢明天到北京的機票。” 我們期望的輸出應該是\(y(x)=(1,0,0)^T\),如何求出這個模擬函數(一般情況下則是求相關的權重和偏置)則是我們模型構建的過程,即上面說的最優化問題。
針對我們上面是設計的神經網絡,我們選擇二次代價函數(也稱為均方誤差代價函數):
模型的構建過程就是基於訓練語料對上述代價函數進行最優化的過程,最終得到\(y(x)\)函數的權重及偏置,則在新的用戶數據請求到來后,直接進行計算則可以得到相應的分類結果。
注:上述最優化過程在深度學習中最常用的是反向傳播算法,這塊內容因為涉及的細節后數學公式推導較多,可以參考我整理的筆記 神經網絡的幾點記錄 、反向傳播的四個基本方程
上述流程即是使用RASA的NLU進行模型構建及模型預測的流程,其思想與上述中的例子所講基本上類似。