python问答系统实践

本人最近在研究NLP,做了一个简易版的问答系统。
一个问答系统主要包含以下几个模块

  1. 命名实体识别
  2. 句法分析
  3. 实体关系抽取
  4. 知识图谱的构建
  5. 知识推理
  6. 意图识别

今天开个头,以后有时间慢慢写。。。

分词

这边我喜欢用的两个分词包,一个是jieba,另一个是foolnltk
首先看jieba的用法

raw=open(u'../data/昆仑全本.txt',encoding='gb18030',errors='ignore').read()
text=nltk.text.Text(jieba.lcut(raw))#分词

再看foolnltk

fool.analisi(text)[0]

分词技巧
可以结合NER,提高准确率

def segment(txt):#结合NER的分词
    ner=[i[3] for i in fool.analysis(txt)[1][0]]
    words=[]
    for i in ner:
        txt=txt.replace(i[0],'|||'+i[0]).replace(i[-1],i[-1]+'|||')
    txt=txt.split('|||')
    for i in txt:
        if i in ner:
            words.append(i)
        else:
            for j in fool.analysis(i)[0][0]:
                words.append(j[0])
    return words

命名实体识别(NER)

这里主要用foolnltk

text=u'初中老师:到了高中就没人管你们了,作业爱写不写!高中老师:没写完作业的外面的的柜子上补去,这节课不上了什么时候交齐作业再上!真不知道初中老师上没上过高中,但放心高中老师肯定上过初中“这个点初中老师应该讲过”初中同学:写完作业了么,给我发一张                :王者又要新出一个英雄挺厉害的高中同学:记作业了么,给我发一张                 :这TM詹姆斯不是人太NB了初中和高中的差异还是很大的,上了高中就会觉得初中的自己很幼稚,即便是初三和高一的区别。凭借高中老师的大学回忆录我相信大学会更好(高三刚毕业)'
fool.analisi(text)[1]

输出

out:
[[(2, 5, 'job', '老师'),
  (26, 29, 'job', '老师'),
  (68, 71, 'job', '老师'),
  (82, 85, 'job', '老师'),
  (96, 99, 'job', '老师'),
  (188, 192, 'person', '詹姆斯'),
  (245, 248, 'job', '老师'),
  (262, 266, 'person', '高三刚')]]

句法分析

这一块主要用nltk
用上下文无关法,由下及上
foolnltk可以很方便得查看每个词的词性,所以可以据此构造文法树

def product_grammar(s):#根据词性构造语法
    l=fool.analysis(s)[0][0]
    k={}
    y=[]
    print(l)
    for i in l:
        if i[1] not in y:
            y.append(i[1])
            k.update({i[1]:[i[0]]})
        else:
            k.update({i[1]:k[i[1]]+[i[0]]})
    g=''
    for i,j in k.items():
        t=i+' ->'
        c=0
        for n in j:
            if len(j)>1 and c<=len(j)-2:
                t+='\''+n+'\''+'|'
            else:
                t+='\''+n+'\''
            c+=1
        g+=t+'\n'
    print('文法:',g)
    return g

构造出来的文法长这样:

"""
  S -> NP VP
  VP -> V NP | V NP PP
  PP -> P NP
  V -> "saw" | "ate" | "walked"
  NP -> "John" | "Mary" | "Bob" | Det N | Det N PP
  Det -> "a" | "an" | "the" | "my"
  N -> "man" | "dog" | "cat" | "telescope" | "park"
  P -> "in" | "on" | "by" | "with"
  """

构造出完整的文法,就可以分析句子结构了

def draw_1(s):
    m=s
    l=fool.cut(s)[0]
    print(l)
    p=product_grammar(m)
    grammar = CFG.fromstring("""
    S ->NP V NP U L|NP U NP V L| NP U L V NP|L U NP V NP|L V NP U NP|NP V L U NP
    NP -> N N|r NP|NP A NP|M Q NP|N|NP U NP|A U NP|N NP|NP C NP|NP U|M NP
    VP ->V|V NP|V VP|A VP|VP NP|VP U|VP C VP|VP P|VP uguo
    V -> v|vi|vshi
    N ->n|nr|t|ns|f|nx|nz
    R ->r
    C ->c
    P ->p
    L ->R|R NP
    U ->ude|y
    A ->a|d|ad
    M ->m
    Q ->q
    """+p)
    cp= nltk.ChartParser(grammar)
    tree=cp.parse(l)
    stree=[]
    
    for s in tree:
        st=[]
        #s.draw()
        for i in range(len(s)):
            st.append([s[i].label(),''.join(s[i].leaves())])
        stree.append(st)
    return stree

句法依存关系分析与角色标注

这里用pyltp

text='马云和马化腾是好朋友'
par_model_path = os.path.join(LTP_DATA_DIR, 'parser.model')  # 依存句法分析模型路径,模型名称为`parser.model`
from pyltp import Parser
parser = Parser() # 初始化实例
parser.load(par_model_path)  # 加载模型
words = word
postag= [ i for i in postags ]
arcs = parser.parse(words, postag)  # 句法分析

rely_id = [arc.head for arc in arcs]    # 提取依存父节点id
relation = [arc.relation for arc in arcs]   # 提取依存关系
heads = ['Root' if id == 0 else words[id-1] for id in rely_id]  # 匹配依存父节点词语
sets={}
for i in range(len(words)):
    sets[relation[i]]=[ words[i] ,heads[i]]
    print (relation[i] + '(' + words[i] + ', ' + heads[i] + ')')

parser.release()  # 释放模型
set:
SBV(马云, 是)
LAD(和, 马化腾)
COO(马化腾, 马云)
HED(是, Root)
ATT(好, 朋友)
VOB(朋友, 是)

知识推理

知识推理主要用pyDatalog
示例1

pyDatalog.create_terms('X,Y,Z,father,fatherOf,grandfatherOf')
(grandfatherOf[X] == Z) <= ((fatherOf[X]==Y) & (fatherOf[Y]==Z))
fatherOf["乾隆"] = "雍正"
fatherOf["雍正"] = "康熙"
print(grandfatherOf["乾隆"] == X)
out:
X 
--
康熙

示例2

pyDatalog.create_terms('X,Y,Z,localed,local')
(local[X] == Z) <= ((localed[X]==Y) & (local[Y]==Z))#关系推导式
localed["马云"] = "香港"
localed["香港"] = "中国"
localed["中国"] = "亚洲"
localed["亚洲"] = "北半球"
print(local["马云"] == X)
out:
X  
---
北半球

示例3

from pyDatalog.pyDatalog import load
#load用于加载推理表达式
load("relation(X,'爷爷',Z) <= relation(X,'父亲',Y) & relation(Y,'父亲',Z)")#定义推理表达式
    load("relation(Y,'孙子',X) <= relation(X,'爷爷',Y)")
    load("relation(Y,'丈夫',X) <= relation(X,'妻子',Y)")
    load("relation(Y,'儿子',X) <= relation(X,'父亲',Y)")
    load("relation(Y,'奶奶',Z) <= relation(Y,'爷爷',X)& relation(X,'配偶',Z)")
    load("relation(X,'儿媳',Z) <= relation(X,'孙子',Y)& relation(Y,'母亲',Z)")
    load("relation(X,'亲属',Y) <= relation(X,'孙子',Y)")
    load("relation(X,'亲属',Y) <= relation(X,'母亲',Y)")
    load("relation(X,'亲属',Y) <= relation(X,'奶奶',Y)")
    load("relation(X,'亲属',Y) <= relation(X,'儿子',Y)")
    load("relation(X,'亲属',Y) <= relation(X,'爷爷',Y)")
    load("relation(X,'亲属',Y) <= relation(X,'父亲',Y)")
    load("relation(X,'亲属',Y) <= relation(Y,'亲属',X)")

知识图谱的存储与查询

则利用的数据结构式rdfs
查询语句sparql
python 的rdflib包
用法:
命名空间设置

prefix0 = "http://www.example.org/" # URI的统一前缀
abbr = lambda x: x[len(prefix0):]                  # 取URI的缩写,为了展示的简洁
verbose = lambda x: prefix0+x                      # 恢复缩写为URI的全称

生成一个“图”,并往里面增加知识

g = rdflib.Graph()
def add_data(e1,r,e2,g):
    r=rdflib.URIRef(verbose(r))
    e1=rdflib.URIRef(verbose(e1))
    e2=rdflib.URIRef(verbose(e2))
    g.add((e1,r,e2))
add_data('马云','在','香港',g)
add_data('香港','在','中国',g)

查询

q = "select ?part where { ?o  ?part.   ?part}LIMIT 2"
#相当于问,马云在哪儿?
x = g.query(q)
out:
rdflib.term.URIRef('http://www.example.org/香港')

知识图谱的存储与读取

#存储为Turtle格式
str0 =g.serialize(format='turtle')
open("someFile.ttl","wb").write(str0)

#读取
g = rdflib.Graph()
g.parse("someFile.ttl", format="turtle")
for subj, pred, obj in g:          #从RDF取出三元组
    print(abbr(subj),abbr(pred),abbr(obj))

句子相似度计算

这里有两种方法,一个是编辑距离计算,另一个比较靠谱点的办法是先做词向量(word2vec),然后计算词语余弦值,接着计算句子相似度。
这一块儿有一个很有用的python包gensim

加载别人训练好的词向量

model = KeyedVectors.load_word2vec_format("70000-small.txt")

当然,也可以自己拿一份预料训练词向量,以三国演义这本书为例
定义训练函数

def model_train(train_file_name, save_model_file):  # model_file_name为训练语料的路径,save_model为保存模型名
    # 模型训练,生成词向量
    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
    sentences = word2vec.Text8Corpus(train_file_name)  # 加载语料
    model = gensim.models.Word2Vec(sentences, size=200)  # 训练skip-gram模型; 默认window=5
    model.save(save_model_file)
    model.wv.save_word2vec_format(save_model_name + ".bin", binary=True)   # 以二进制类型保存模型以便重用

切词

# 此函数作用是对初始语料进行分词处理后,作为训练模型的语料
def cut_txt(old_file):
    global cut_file     # 分词之后保存的文件名
    cut_file = old_file + '_cut.txt'

    try:
        fi = open(old_file, 'r', encoding='utf-8')
    except BaseException as e:  # 因BaseException是所有错误的基类,用它可以获得所有错误类型
        print(Exception, ":", e)    # 追踪错误详细信息

    text = fi.read()  # 获取文本内容
    new_text = jieba.cut(text, cut_all=True)  # 精确模式
    str_out = ' '.join(new_text).replace(',', '').replace('。', '').replace('?', '').replace('!', '') \
        .replace('“', '').replace('”', '').replace(':', '').replace('…', '').replace('(', '').replace(')', '') \
        .replace('—', '').replace('《', '').replace('》', '').replace('、', '').replace('‘', '') \
        .replace('’', '').replace('的', '').replace('也', '').replace('我们', '')     # 去掉标点符号
    fo = open(cut_file, 'w', encoding='utf-8')
    fo.write(str_out)

开始训练

fname='三国演义'
cut_txt(fname+'.txt')  # 须注意文件必须先另存为utf-8编码格式
save_model_name = fname+'.model'
if not os.path.exists(save_model_name):     # 判断文件是否存在
    model_train(cut_file, save_model_name)
else:
    print('此训练模型已经存在,不用再次训练')

训练好了就可以加在自己训练的模型了

model_1 = word2vec.Word2Vec.load(save_model_name)

利用已经得到的模型,做词句相似度计算

# 计算某个词的相关词列表
word='笔记本'
y2 = model.most_similar(word, topn=10)  # 10个最相关的
print(u"和"+word+"最相关的词有:\n")
for item in y2:
    print(item[0], item[1])
print("-------------------------------\n")
out:
和笔记本最相关的词有:

笔记本电脑 0.8533223271369934
本本 0.8025031089782715
本子 0.7872854471206665
手提电脑 0.7343267202377319
电脑 0.7327929735183716
台式机 0.7327749133110046
日记本 0.7309284210205078
记事本 0.7292013168334961
笔记 0.696074366569519
平板电脑 0.6858302354812622
-------------------------------

计算句子相似度
定义计算函数

def vector_similarity(s1, s2):
    def sentence_vector(s):
        words = jieba.lcut(s)
        v = np.zeros(200)
        for word in words:
            v += model[word]
        v /= len(words)
        return v
    
    v1, v2 = sentence_vector(s1), sentence_vector(s2)
    return np.dot(v1, v2) / (norm(v1) * norm(v2))

实验

s1 = '马云的生日是?'
s2 = '马云是95年出生的'
vector_similarity(s1, s2)

out:
0.8800125795741638

今天暂时先写到这儿吧,问答系统还包含意图识别等比较复杂的主体,后面有时间再详细些出来,以上内容如对你有用请点赞或收藏,谢谢!

你可能感兴趣的:(NLP,知识图谱,自然语言处理,自然语言处理,知识图谱,知识推理,python,中文分词)