智能問答系統(對話系統)的應用是非常普遍的,比如說客服,前台機器人,講解機器人等很多場景都可能會用到FAQ問答系統。所謂的FAQ就是 frequently asked questions,也就是說在某個場景下,算法可以回答一些比較常見的問題。
對數據方提供的冬奧會知識數據進行集成,輸入端接受以自然語言輸入的問題(比如使用中文詢問“中國在索契冬奧會獲得金牌數目”),輸出端輸出該問題對應的回答。
數據包有兩個:標注數據1和標注數據2。標注數據1中的數據為單層問題,標注數據2中的數據為疊加問題。
問句:哪一屆奧運會的金牌總數最多?
分詞:哪 一屆 奧運會 的 金牌 總數 最 多 ?
# 1問句類型:Which多選一
# 1領域類型:Competition比賽
# 1語義類型:Calculation計算
# 2問句類型:NA
# 2領域類型:NA
# 2語義類型:NA
問句:中國奧運第一人值得尊敬嗎?
分詞:中國 奧運 第一 人 值得 尊敬 嗎 ?
# 1問句類型:Who
# 1領域類型:Competition
# 1語義類型:Factoid
# 2問句類型:Whether
# 2領域類型:Competition
# 2語義類型:Opinion
對於一個完整的對話系統FAQ的構建,第一步要做的是對輸入的問題進行預處理。預處理需要做的事情主要為刪除無用文字,去除停用詞,並將問題切分成一個個中文詞語。
第二部就是將處理之後的語料進行向量化。常見的向量化方法有詞頻向量化、word2vec、tf-idf 等方法。向量化之後,每一個問題對應的都為一個高維向量,當有詢問問題輸入時,先將問題預處理、向量化,然後和數據集中的數據進行比對,輸出相似度最高的問題的答案,這就是檢索式對話系統的大致框架。
查看測試集,發現有的問題沒有答案,所以預處理的第一步就是將沒有答案的問題刪除。預處理的第二步就是將不屬於中文的文本刪除(包括各種標點符號)。預處理的第三步是將修正後的文本進行詞語的切分,從而將一整段話切分為一個個詞語。
使用CountVectorizer對每一條語料進行詞頻矩陣的生成,從而完成語料的空間向量化。
將數據庫中的語料與輸入問題的向量化後的向量進行相似度比對。這裡我們采用的是余弦相似度比對算法。比對完成之後,將得分最高的語料的答案返回。
以下是整個處理過程的具體實現:
對非中文無用數據的清理,需要將以下類別的數據從訓練集和測試集中清除:
html = re.compile('<.*?>')
http = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[[email protected]&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-f
A-F]))+')
src = re.compile(r'\b(?!src|href)\w+=[\'\"].*?[\'\"](?=[\s\>])')
space = re.compile(r'[\\][n]')
ids = re.compile('[(]["微信"]?id:(.*?)[)]')
wopen = re.compile('window.open[(](.*?)[)]')
english = re.compile('[a-zA-Z]')
others= re.compile(u'[^\u4e00-\u9fa5\u0041-\u005A\u0061-\u007A\u0030-\u0039\u3002\uFF1F\uFF01\uFF0C\u3001\uFF1B\uFF1A\u300C\u300D\u300E\u300F\u2018\u2019\u201C\u201D\uFF08\uFF09\u3014\u3015\u3010\u3011\u2014\u2026\u2013\uFF0E\u300A\u300B\u3008\u3009\!\@\#\$\%\^\&\*\(\)\-\=\[\]\{\}\\\|\;\'\:\"\,\.\/\<\>\?\/\*\+\_"\u0020]+')
需要將html鏈接、數據來源、用戶名、英語字符和其他單個非中文字符清除,采用以上正則表達式描述需要刪除的類別,配合sub命令將其從訓練集和數據集中刪除。由於本次的數據來源質量較好,該步對源數據的處理很少。
數據劃分有兩種方式:保留停用詞和去除停用詞。數據劃分使用的庫為jieba分詞,具體的操作如下:
Question.txt和Answer.txt分別存放了問題和答案,預處理後輸出到QuestionSeg.txt和AnswerSeg.txt。
inputQ = open('Question.txt', 'r', encoding='gbk')
outputQ = open('QuestionSeg.txt', 'w', encoding='gbk')
inputA = open('Answer.txt', 'r', encoding='gbk')
outputA = open('AnswerSeg.txt', 'w', encoding='gbk')
先來看保留停用詞的劃分方式:
def segmentation(sentence):
sentence_seg = jieba.cut(sentence.strip())
out_string = ''
for word in sentence_seg:
out_string += word
out_string += " "
return out_string
for line in inputQ:
line_seg = segmentation(line)
outputQ.write(line_seg + '\n')
outputQ.close()
inputQ.close()
for line in inputA:
line_seg = segmentation(line)
outputA.write(line_seg + '\n')
outputA.close()
inputA.close()
接下來看不保留停用詞的劃分方式,測試結果表明保留停用詞劃分效果更好,查閱資料
顯示原因為CountVectorizer目前版本對停用詞的優化更佳。
def stopword_list():
stopwords = [line.strip() for line in open('stopword.txt', encoding='utf-8').readlines()]
return stopwords
def seg_with_stop(sentence):
sentence_seg = jieba.cut(sentence.strip())
stopwords = stopword_list()
out_string = ''
for word in sentence_seg:
if word not in stopwords:
if word != '\t':
out_string += word
out_string += " "
return out_string
for line in inputQ:
line_seg = seg_with_stop(line)
outputQ.write(line_seg + '\n')
outputQ.close()
inputQ.close()
for line in inputA:
line_seg = seg_with_stop(line)
outputA.write(line_seg + '\n')
outputA.close()
inputA.close()
經過以上兩步,我們已經將文本成功分割為獨立的中文詞語,接下來需要統計每個詞出現的頻率及分布。
stopwords = [line.strip() for line in open('stopword.txt',encoding='utf-8').readlines()]
首先需要獲得停用詞表。這裡我們使用的是百度停用詞表、哈工大停用詞表、中文停用詞表等多個詞表的綜合結果。
CountVectorizer是通過fit_transform函數將文本中的詞語轉換為詞頻矩陣,矩陣元素a[i][j] 表示j詞在第i個文本下的詞頻。即各個詞語出現的次數,通過get_feature_names()可看到所有文本的關鍵字,通過toarray()可看到詞頻矩陣的結果。
count_vec = CountVectorizer()
先對文本內容進行詞頻統計。需要說明的是,由於代碼第一版優化不佳,沒有進行對向量化數據的保存,在每次輸入問題之後都需要將全體語料進行向量化的重新演算,導致查詢時間較長。
余弦相似度量:計算個體間的相似度。
相似度越小,距離越大。相似度越大,距離越小。
假設有3個物品,item1,item2和item3,用向量表示分別為:
item1[1,1,0,0,1],
item2[0,0,1,2,1],
item3[0,0,1,2,0],
即五維空間中的3個點。用歐式距離公式計算item1、itme2之間的距離,以及item2和item3之間的距離,分別是:
由此可得出item1和item2相似度小,兩個之間的距離大(距離為7),item2和itme3相似度大,兩者之間的距離小(距離為1)。
余弦相似度算法:一個向量空間中兩個向量夾角間的余弦值作為衡量兩個個體之間差異的大小,余弦值接近1,夾角趨於0,表明兩個向量越相似,余弦值接近於0,夾角趨於90度,表明兩個向量越不相似。
基於余弦相似度算法,我們將輸入的問題的向量與數據庫中的語料的向量進行一一比對,輸出相似度最高(最接近1)的語料的答案。
余弦相似度計算函數:
def count_cos_similarity(vec_1, vec_2):
if len(vec_1) != len(vec_2):
return 0
s = sum(vec_1[i] * vec_2[i] for i in range(len(vec_2)))
den1 = math.sqrt(sum([pow(number, 2) for number in vec_1]))
den2 = math.sqrt(sum([pow(number, 2) for number in vec_2]))
return s / (den1 * den2)
計算兩個語句的余弦相似度
def cos_sim(sentence1, sentence2):
sentences = [sentence1, sentence2]
vec_1 = count_vec.fit_transform(sentences).toarray()[0] #輸入問題向量化
vec_2 = count_vec.fit_transform(sentences).toarray()[1] #語料庫向量化
return count_cos_similarity(vec_1, vec_2)
def get_answer(sentence1):
sentence1 = segmentation(sentence1)
score = []
for idx, sentence2 in enumerate(open('QuestionSeg.txt', 'r')):
# print('idx: {}, sentence2: {}'.format(idx, sentence2))
# print('idx: {}, cos_sim: {}'.format(idx, cos_sim(sentence1, sentence2)))
score.append(cos_sim(sentence1, sentence2))
if len(set(score)) == 1:
print('暫時無法找到您想要的答案。')
else:
index = score.index(max(score))
file = open('Answer.txt', 'r').readlines()
print(file[index])
while True:
sentence1 = input('請輸入您需要問的問題(輸入q退出):\n')
if sentence1 == 'q':
break
else:
get_answer(sentence1)
為了提高問答系統的性能,這次實驗中我們沒有對數據集進行分割,而使將全部的問題都用來訓練模型。
我們針對幾個角度刁鑽的問題進行測試,結果如下:
針對高相似度問題的測試:
原問題及答案及測試結果截圖:
普萊西德湖冬奧會比賽時,僅能容納3000人的冰場,一下子湧進了多少人?
答案:7000
由此可見,檢索系統對於相似度很高的問題之間仍然可以做出明確的區分。
部分缺失問題的提問:
原問題及答案及測試結果截圖:
普萊西德湖冬奧會美、加兩隊比賽時,第一場美國隊以l:多少敗北?
答案:2
年科蒂納丹佩佐冬季奧運會的項目數是什麼?
答案:4項運動、8個大項、24個小項
年因斯布魯克冬奧會第一次參賽的有什麼?
答案:朝鮮民主主義人民共和國、印度和蒙古