1 任務背景:
本次實驗擬設計一個智能問答系統,並應當保證該智能問答系統可以回答5個及其以上的問題。由於本實驗室目前正在使用知識圖譜搭建問答系統,故而這裡將使用知識圖譜的方式構建該智能問答系統。這裡將構建一個關於歌曲信息的問答系統。以「晴天」為例,本系統應當能夠回答晴天的歌詞是什麼,晴天是哪首專輯的歌曲,該專輯是哪一年發行的,該專輯對應的歌手是誰,該歌手的的基本信息是什麼。
本系統的環境配置過程以及全部代碼均已上傳Github。下面的文章主要介紹的是系統總體結構以及部分代碼解析。
2 系統總體工作流程圖
在搭建系統之前,第一步的任務是準備數據。這裡的準備的數據包括周杰倫的姓名,個人簡介,出生日期,以及發行的所有專輯名字,《葉惠美》專輯的名字,簡介以及發行日期,《以父之名》、《晴天》的歌曲名和歌詞。
準備好數據之後,將數據整理成RDF文檔的格式。這裡採用手工的方式在protégé中構建本體以及知識圖譜。本體作為模式層,這裡聲明了三個類,包括歌手類、專輯類和歌曲類;聲明了四種關係,也叫objectProperty,包括include,include_by,release和release_by。其中include和include_by聲明為inverseOf關係,表示專輯和歌曲之間的包含和被包含的關係。Release和release_by聲明為inverseOf關係表示歌手和專輯之間的發行和被發行的關係;聲明了8種數據屬性,也叫DataProperty,分別為singer_name,singer_birthday,singer_introduction,album_name,album_introduction,album_release_date,song_name和song_content。將上述準備好的數據以individual和dataProperty的形式寫進知識圖譜。至此,就準備好了我們的RDF/OWL文件了。
接著,為了使用RDF查詢語言SPARQL做後續的查詢操作,這裡使用Apache Jena的TDB和Fuseki組件。TDB是Jena用於存儲RDF的組件,是屬於存儲層面的技術。Fuseki是Jena提供的SPARQL伺服器,也就是SPARQL endpoint。這一步中,首先利用Jena將RDF文件轉換為tdb數據。接著對fuseki進行配置並打開SPARQL伺服器,就可以通過查詢語句完成對知識圖譜的查詢。
最後,將自然語言問題轉換成SPARQL查詢語句。首先使用結巴分詞將自然語言問題進行分詞以及詞性標註。這裡將專輯名字和歌曲名字作為外部詞典以保證正確的分詞和詞性標註。以「葉惠美」為例,結巴分詞將「葉惠美」標註為nr,即人名,這裡「葉惠美」作為專輯名字應該標註為nz,即專有名詞。對於不同類型的問題,我們將問題匹配給不同的查詢語句生成函數從而得到正確的查詢語句。將查詢語句作為請求參數和Fuseki伺服器通信就能得到相應的問題結果。上述工作流程圖如圖2-1所示。
圖2-1 系統工作流程圖
3 系統實現
3.1 系統實現工具和環境
使用protégé構建知識庫的本體和知識圖譜。首先是定義模式層,包括class,objectProperty和dataProperty。以歌曲類為例,其RDF代碼為:
<!-- http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#歌曲 --> <owl:Class rdf:about="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#歌曲"> <rdfs:subClassOf> <owl:Restriction> <owl:onProperty rdf:resource="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#song_content"/> <owl:allValuesFrom rdf:resource="http://www.w3.org/2001/XMLSchema#string"/> </owl:Restriction> </rdfs:subClassOf> <rdfs:subClassOf> <owl:Restriction> <owl:onProperty rdf:resource="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#song_name"/> <owl:allValuesFrom rdf:resource="http://www.w3.org/2001/XMLSchema#string"/> </owl:Restriction> </rdfs:subClassOf> </owl:Class>
可以看出class 「歌曲」包含了song_name和song_content兩個dataProperty。以include的關係為例,其RDF代碼如下:
<!-- http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#include --> <owl:ObjectProperty rdf:about="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#include"> <owl:inverseOf rdf:resource="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#include_by"/> <rdfs:domain rdf:resource="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#專輯"/> <rdfs:range rdf:resource="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#歌曲"/> </owl:ObjectProperty>
可以看出該關係和include_by是inverseOf的關係,其關係主語是專輯,賓語是歌曲。以singer_introduction的dataProperty為例,其RDF代碼如下:
<!-- http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#singer_introduction --> <owl:DatatypeProperty rdf:about="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#singer_introduction"> <rdfs:domain rdf:resource="http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#歌手"/> <rdfs:range rdf:resource="http://www.w3.org/2001/XMLSchema#string"/> </owl:DatatypeProperty>
可以看出其主語是歌手類,其賓語是字元串。
接著是數據層。圖3-1和圖3-2是系統的知識圖譜可視化的結果。可以看出,「七里香」是一個專輯,「東風破」是一首歌曲。
圖3-1專輯知識圖譜可視化
圖3-2 歌曲知識圖譜可視化
使用Feseki啟動SPARQL伺服器,可以在localhost:3030中實現數據的查詢。以查詢「以父之名」的專輯為例,圖3-3展示其查詢代碼和查詢結果,可以看出如下語句可以得到查詢結果「葉惠美」。
圖3-3 Feseki平台的查詢情況
3.2 自然語言處理核心代碼分析
使用結巴分詞將自然語言句子實現分詞和詞性標註。其核心代碼如下:
import jieba import jieba.posseg as pseg class Word(object): def __init__(self, token, pos): self.token = token self.pos = pos class Tagger: def __init__(self, dict_paths): # TODO 載入外部詞典 for p in dict_paths: jieba.load_userdict(p) @staticmethod def get_word_objects(sentence): # 把自然語言轉為Word對象 return [Word(word.encode(utf-8), tag) for word, tag in pseg.cut(sentence)]
這段代碼可以將句子作為一個輸入,輸出句子的分子和詞性,以「葉惠美是什麼發布的?」為例,可以得到以下結果:
圖3-4 結巴分詞示意圖
將句子作為參數傳遞給Rule對象,根據關鍵字匹配相一致的查詢語句的生成函數,Rule對象和關鍵字匹配的代碼如下:
from refo import finditer, Predicate, Star, Any, Disjunction import re
class W(Predicate): def __init__(self, token=".*", pos=".*"): self.token = re.compile(token + "$") self.pos = re.compile(pos + "$") super(W, self).__init__(self.match)
def match(self, word): m1 = self.token.match(word.token.decode("utf-8")) m2 = self.pos.match(word.pos) return m1 and m2
class Rule(object): def __init__(self, condition_num, condition=None, action=None): assert condition and action self.condition = condition self.action = action self.condition_num = condition_num
def apply(self, sentence): matches = [] for m in finditer(self.condition, sentence): i, j = m.span() matches.extend(sentence[i:j]) return self.action(matches), self.condition_num # TODO 定義關鍵詞 pos_person = "nr" pos_song = "nz" pos_album = "nz" person_entity = (W(pos=pos_person)) song_entity = (W(pos=pos_song)) album_entity = (W(pos=pos_album)) singer = (W("歌手") | W("歌唱家") | W("藝術家") | W("藝人") | W("歌星")) album = (W("專輯") | W("合輯") | W("唱片")) song = (W("歌") | W("歌曲")) birth = (W("生日") | W("出生") + W("日期") | W("出生")) english_name = (W("英文名") | W("英文") + W("名字")) introduction = (W("介紹") | W("是") + W("誰") | W("簡介")) song_content = (W("歌詞") | W("歌") | W("內容")) release = (W("發行") | W("發布") | W("發表") | W("出")) when = (W("何時") | W("時候")) where = (W("哪裡") | W("哪兒") | W("何地") | W("何處") | W("在") + W("哪"))
# TODO 問題模板/匹配規則 """ 1.周杰倫的專輯都有什麼? 2.晴天的歌詞是什麼? 3.周杰倫的生日是哪天? 4.以父之名是哪個專輯裡的歌曲? 5.葉惠美是哪一年發行的? """ rules = [ Rule(condition_num=2, condition=person_entity + Star(Any(), greedy=False) + album + Star(Any(), greedy=False), action=QuestionSet.has_album), Rule(condition_num=2, condition=song_entity + Star(Any(), greedy=False) + song_content + Star(Any(), greedy=False), action=QuestionSet.has_content), Rule(condition_num=2, condition=person_entity + Star(Any(), greedy=False) + introduction + Star(Any(), greedy=False), action=QuestionSet.person_inroduction), Rule(condition_num=2, condition=song_entity + Star(Any(), greedy=False) + album + Star(Any(), greedy=False), action=QuestionSet.stay_album), Rule(condition_num=2, condition=song_entity + Star(Any(), greedy=False) + release + Star(Any(), greedy=False), action=QuestionSet.release_album), ]
匹配成功後,通過action動作出發相對應的函數能夠生成相對應的查詢語句。以查詢「以父之名是哪個專輯的歌曲?」為例,其生成查詢語句的代碼如下:
# TODO SPARQL前綴和模板 SPARQL_PREXIX = u""" PREFIX owl: <http://www.w3.org/2002/07/owl#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> PREFIX : <http://www.semanticweb.org/張濤/ontologies/2019/1/untitled-ontology-32#> """ SPARQL_SELECT_TEM = u"{prefix} " + u"SELECT {select} WHERE {{ " + u"{expression} " + u"}} " class QuestionSet: @staticmethod def stay_album(word_object): # 以父之名是哪個專輯的歌曲 select = u"?x" sparql = None
for w in word_object: if w.pos == pos_song: e = u" :{song} :include_by ?o." u" ?o :album_name ?x.".format(song=w.token.decode(utf-8)) sparql = SPARQL_SELECT_TEM.format(prefix=SPARQL_PREXIX, select=select, expression=e) break return sparql 最後,將得到的查詢語句作為請求參數和SPARQL伺服器進行通信並對得到的結果進行解析就能得到我們想要的答案,其核心代碼如下: from SPARQLWrapper import SPARQLWrapper, JSON from collections import OrderedDict
class JenaFuseki: def __init__(self, endpoint_url=http://localhost:3030/jay_kbqa/sparql): self.sparql_conn = SPARQLWrapper(endpoint_url)
def get_sparql_result(self, query): self.sparql_conn.setQuery(query) self.sparql_conn.setReturnFormat(JSON) return self.sparql_conn.query().convert()
@staticmethod def parse_result(query_result): """ 解析返回的結果 :param query_result: :return: """ try: query_head = query_result[head][vars] query_results = list() for r in query_result[results][bindings]: temp_dict = OrderedDict() for h in query_head: temp_dict[h] = r[h][value] query_results.append(temp_dict) return query_head, query_results except KeyError: return None, query_result[boolean]
以上就是將自然語言轉換成SPARQL查詢語言並與Feseki進行通信的核心代碼。
3.3 代碼運行結果
這裡分別從歌手的簡介,專輯的發行時間,歌手的所有專輯,歌曲屬於哪個專輯以及歌曲的歌詞等5類問題做問答,均能達到良好的表現效果。其問答情況如圖3-5所示。
圖3-5 問答系統運行結果實例
最後:
代碼部分參考了:
配置部分參考了:
https://blog.csdn.net/keyue123/article/details/85266355?blog.csdn.net 推薦閱讀: