理解聊天機器人的工作原理是非常重要的。聊天機器人內部一個非常重要的元件就是文字分類器。我們看一下文字分類器的神經網路(ANN)的內部工作原理。
多層神經網路
我們將會使用2層網路(1個隱層)和一個“詞包”的方法來組織我們的訓練資料。文字分類有3個特點:模式匹配、演算法、神經網路。雖然使用多項樸素貝葉斯演算法的方法非常有效,但是它有3個致命的缺陷:
- 這個演算法輸出一個分數而不是一個概率。我們可以使用概率來忽略特定閾值以下的預測結果。這類似於忽略收音機中的噪聲。
- 這個演算法從一個樣本中學習一個分類中包含什麼,而不是一個分類中不包含什麼。一個分類中不包含什麼的的學習模式往往也很重要。
- 不成比例的大訓練集的分類將會導致扭曲的分類分數,迫使演算法相對於分類規模來調整輸出分數,這並不理想。
和它“天真”的對手一樣,這種分類器並不試圖去理解句子的含義,而僅僅對它進行分類。事實上,所謂的“人工智慧聊天機器人”並不理解語言,但那是另一個故事。
如果你剛接觸人工神經網路,這是它的工作原理。
理解分類演算法,請看這裡。
我們來逐個分析文字分類器的每個部分。我們將按照以下順序:
- 引用需要的庫
- 提供訓練集
- 整理資料
- 迭代:編寫程式碼+測試預測結果+調整模型
- 抽象
程式碼在這裡,我們使用ipython notebook這個在資料科學專案上非常高效的工具。程式碼語法是python。
我們首先匯入自然語言工具包。我們需要一個可靠的方法將句子切分成詞並且將單詞詞幹化處理。
1 2 3 4 5 6 7 |
# use natural language toolkit import nltk from nltk.stem.lancaster import LancasterStemmer import os import json import datetime stemmer = LancasterStemmer() |
下面是我們的訓練集,12個句子屬於3個類別(“意圖”)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 3 classes of training data training_data = [] training_data.append({"class":"greeting", "sentence":"how are you?"}) training_data.append({"class":"greeting", "sentence":"how is your day?"}) training_data.append({"class":"greeting", "sentence":"good day"}) training_data.append({"class":"greeting", "sentence":"how is it going today?"}) training_data.append({"class":"goodbye", "sentence":"have a nice day"}) training_data.append({"class":"goodbye", "sentence":"see you later"}) training_data.append({"class":"goodbye", "sentence":"have a nice day"}) training_data.append({"class":"goodbye", "sentence":"talk to you soon"}) training_data.append({"class":"sandwich", "sentence":"make me a sandwich"}) training_data.append({"class":"sandwich", "sentence":"can you make a sandwich?"}) training_data.append({"class":"sandwich", "sentence":"having a sandwich today?"}) training_data.append({"class":"sandwich", "sentence":"what's for lunch?"}) print ("%s sentences in training data" % len(training_data)) |
1 |
12 sentences in training data |
現在我們可以將資料結構組織為:documents, classes 和words.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
words = [] classes = [] documents = [] ignore_words = ['?'] # loop through each sentence in our training data for pattern in training_data: # tokenize each word in the sentence w = nltk.word_tokenize(pattern['sentence']) # add to our words list words.extend(w) # add to documents in our corpus documents.append((w, pattern['class'])) # add to our classes list if pattern['class'] not in classes: classes.append(pattern['class']) # stem and lower each word and remove duplicates words = [stemmer.stem(w.lower()) for w in words if w not in ignore_words] words = list(set(words)) # remove duplicates classes = list(set(classes)) print (len(documents), "documents") print (len(classes), "classes", classes) print (len(words), "unique stemmed words", words) |
1 2 3 |
12 documents 3 classes ['greeting', 'goodbye', 'sandwich'] 26 unique stemmed words ['sandwich', 'hav', 'a', 'how', 'for', 'ar', 'good', 'mak', 'me', 'it', 'day', 'soon', 'nic', 'lat', 'going', 'you', 'today', 'can', 'lunch', 'is', "'s", 'see', 'to', 'talk', 'yo', 'what'] |
注意每個單詞都是詞根並且小寫。詞根有助於機器將“have”和“having”等同起來。同時我們也不關心大小寫。
我們將訓練集中的每個句子轉換為詞包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# create our training data training = [] output = [] # create an empty array for our output output_empty = [0] * len(classes) # training set, bag of words for each sentence for doc in documents: # initialize our bag of words bag = [] # list of tokenized words for the pattern pattern_words = doc[0] # stem each word pattern_words = [stemmer.stem(word.lower()) for word in pattern_words] # create our bag of words array for w in words: bag.append(1) if w in pattern_words else bag.append(0) training.append(bag) # output is a '0' for each tag and '1' for current tag output_row = list(output_empty) output_row[classes.index(doc[1])] = 1 output.append(output_row) # sample training/output i = 0 w = documents[i][0] print ([stemmer.stem(word.lower()) for word in w]) print (training[i]) print (output[i]) |
1 2 3 |
['how', 'ar', 'you', '?'] [0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [1, 0, 0] |
上面的步驟是文字分類中的一個經典步驟:每個訓練句子被轉化為一個包含0和1的陣列,而不是語料庫中包含獨特單詞的陣列。
1 |
['how', 'are', 'you', '?'] |
被詞幹化為:
1 |
['how', 'ar', 'you', '?'] |
然後轉換為輸入詞包的形式:1代表單詞存在於詞包中(忽略問號?)
1 |
[0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
輸出:第一類
1 |
[1, 0, 0] |
注意:一個句子可以有多個分類,也可以沒有。確保理解上面的內容,仔細閱讀程式碼直到你理解它。
機器學習的第一步是要有乾淨的資料
接下來我們的學習2層神經網路的核心功能。
如果你是人工神經網路新手,這裡是它的工作原理
我們使用numpy,原因是它可以提供快速的矩陣乘法運算。
我們使用sigmoid函式對值進行歸一化,用其導數來衡量錯誤率。通過不斷迭代和調整,直到錯誤率低到一個可以接受的值。
下面我們也實現了bag-of-words函式,將輸入的一個句子轉化為一個包含0和1的陣列。這就是轉換訓練資料,得到正確的轉換資料至關重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import numpy as np import time # compute sigmoid nonlinearity def sigmoid(x): output = 1/(1+np.exp(-x)) return output # convert output of sigmoid function to its derivative def sigmoid_output_to_derivative(output): return output*(1-output) def clean_up_sentence(sentence): # tokenize the pattern sentence_words = nltk.word_tokenize(sentence) # stem each word sentence_words = [stemmer.stem(word.lower()) for word in sentence_words] return sentence_words # return bag of words array: 0 or 1 for each word in the bag that exists in the sentence def bow(sentence, words, show_details=False): # tokenize the pattern sentence_words = clean_up_sentence(sentence) # bag of words bag = [0]*len(words) for s in sentence_words: for i,w in enumerate(words): if w == s: bag[i] = 1 if show_details: print ("found in bag: %s" % w) return(np.array(bag)) def think(sentence, show_details=False): x = bow(sentence.lower(), words, show_details) if show_details: print ("sentence:", sentence, "n bow:", x) # input layer is our bag of words l0 = x # matrix multiplication of input and hidden layer l1 = sigmoid(np.dot(l0, synapse_0)) # output layer l2 = sigmoid(np.dot(l1, synapse_1)) return l2 |
現在我們對神經網路訓練函式進行編碼,創造連線權重。別太激動,這主要是矩陣乘法——來自中學數學課堂。
我們現在準備去構建我們的神經網路模型,我們將連線權重儲存為json檔案。
你應該嘗試不同的“α”(梯度下降引數),看看它是如何影響錯誤率。此引數有助於錯誤調整,並找到最低錯誤率:
synapse_0 += alpha * synapse_0_weight_update
我們在隱藏層使用了20個神經元,你可以很容易地調整。這些引數將隨著於您的訓練資料規模的不同而不同,將錯誤率調整到低於10 ^ – 3是比較合理的。
1 2 3 4 5 6 7 8 9 |
X = np.array(training) y = np.array(output) start_time = time.time() train(X, y, hidden_neurons=20, alpha=0.1, epochs=100000, dropout=False, dropout_percent=0.2) elapsed_time = time.time() - start_time print ("processing time:", elapsed_time, "seconds") |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Training with 20 neurons, alpha:0.1, dropout:False Input matrix: 12x26 Output matrix: 1x3 delta after 10000 iterations:0.0062613597435 delta after 20000 iterations:0.00428296074919 delta after 30000 iterations:0.00343930779307 delta after 40000 iterations:0.00294648034566 delta after 50000 iterations:0.00261467859609 delta after 60000 iterations:0.00237219554105 delta after 70000 iterations:0.00218521899378 delta after 80000 iterations:0.00203547284581 delta after 90000 iterations:0.00191211022401 delta after 100000 iterations:0.00180823798397 saved synapses to: synapses.json processing time: 6.501226902008057 seconds |
synapse.json檔案中包含了全部的連線權重,這就是我們的模型。
一旦連線權重已經計算完成,對於分類來說只需要classify()函式了:大約15行程式碼
備註:如果訓練集有變化,我們的模型需要重新計算。對於非常大的資料集,這需要較長的時間。
現在我們可以生成一個句子屬於一個或者多個分類的概率了。它的速度非常快,這是因為我們之前定義的think()函式中的點積運算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# probability threshold ERROR_THRESHOLD = 0.2 # load our calculated synapse values synapse_file = 'synapses.json' with open(synapse_file) as data_file: synapse = json.load(data_file) synapse_0 = np.asarray(synapse['synapse0']) synapse_1 = np.asarray(synapse['synapse1']) def classify(sentence, show_details=False): results = think(sentence, show_details) results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD ] results.sort(key=lambda x: x[1], reverse=True) return_results =[[classes[r[0]],r[1]] for r in results] print ("%s n classification: %s" % (sentence, return_results)) return return_results classify("sudo make me a sandwich") classify("how are you today?") classify("talk to you tomorrow") classify("who are you?") classify("make me some lunch") classify("how was your lunch today?") print() classify("good day", show_details=True) |
1 2 3 4 5 6 7 8 9 10 11 12 |
<strong>sudo make me a sandwich </strong> [['sandwich', 0.99917711814437993]] <strong>how are you today? </strong> [['greeting', 0.99864563257858363]] <strong>talk to you tomorrow </strong> [['goodbye', 0.95647479275905511]] <strong>who are you? </strong> [['greeting', 0.8964283843977312]] <strong>make me some lunch</strong> [['sandwich', 0.95371924052636048]] <strong>how was your lunch today? </strong> [['greeting', 0.99120883810944971], ['sandwich', 0.31626066870883057]] |
你可以用其它語句、不同概率來試驗幾次,也可以新增訓練資料來改進/擴充套件當前的模型。尤其注意用很少的訓練資料就得到穩定的預測結果。
有一些句子將會產生多個預測結果(高於閾值)。你需要給你的程式設定一個合適的閾值。並非所有的文字分類方案都是相同的:一些預測情況比其他預測需要更高的置信水平。
最後這個分類結果展示了一些內部的細節:
1 2 3 4 5 6 |
found in bag: good found in bag: day sentence: **good day** bow: [0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] good day [['greeting', 0.99664077655648697]] |
從這個句子的詞包中可以看到,有兩個單詞和我們的詞庫是匹配的。同時我們的神經網路從這些 0 代表的非匹配詞語中學習了。
如果提供一個僅僅有一個常用單詞 ‘a’ 被匹配的句子,那我們會得到一個低概率的分類結果A:
1 2 3 4 5 |
found in bag: a sentence: **a burrito! ** bow: [0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] a burrito! [['sandwich', 0.61776860634647834]] |
現在你已經掌握了構建聊天機器人的一些基礎知識結構,它能處理大量不同的意圖,並且對於有限或者海量的訓練資料都能很好的適配。想要為某個意圖新增一個或者多個響應實在輕而易舉,就不必多講了。
Enjoy!
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式