本人最近在研究NLP,做了一个简易版的问答系统。
一个问答系统主要包含以下几个模块
- 命名实体识别
- 句法分析
- 实体关系抽取
- 知识图谱的构建
- 知识推理
- 意图识别
今天开个头,以后有时间慢慢写。。。
分词
这边我喜欢用的两个分词包,一个是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 <http://www.example.org/在> ?part. <http://www.example.org/马云> <http://www.example.org/在> ?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
今天暂时先写到这儿吧,问答系统还包含意图识别等比较复杂的主体,后面有时间再详细些出来