300行python代码从零开始构建基于知识图谱的电影问答系统4-用户问题预处理

今天冷得,我在五月份穿了三件衣服,你敢相信。。。

这篇主要介绍对用户问题的处理,也就是从获取用户问题到明白用户意图这个过程,主要涉及到命名实体识别(这个任务简单,我就用词性标注来代替了),问题分类,以及填充问题模板这几个部分。介绍的时候,可能会用一些代码来说明,但是下面列出来的代码并不完整,完整的代码请参照github。这些代码只是辅助理解整个过程,这样去看代码的时候才容易理清函数之间的来龙去脉。再说明下,这个系统适合那些刚入门没多久,通过实现一个小东西来练练手的同学,而这篇教程,可以看作是代码的说明书,帮助理解代码,主要还是梳理逻辑。

关键信息抽取

这部分就是通过命名实体识别来获取用户问题中的人名,电影名等信息,而在实验过程中,在这一部分我们其实用的词性标注,因为我发现,词性标注工具可以把人名标注出来,如下使用jieba进行词性标注时的含义:

1
nr	人名	名词代码 n和“人(ren)”的声母并在一起。

所以,只要我们把问题词性标注后,找到对应的nr对应的单词,那就是人名啦,电影名称的识别和这个差不多。这就是这部分的主要思路,接下来就看下代码是怎么实现的。

  • 用户问题词性标注

功能的实现就是下面这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def question_posseg(self):
jieba.load_userdict("./data/userdict3.txt")
clean_question = re.sub("[\s+\.\!\/_,$%^*(+\"\')]+|[+——()?【】“”!,。?、~@#¥%……&*()]+","",self.raw_question)
self.clean_question=clean_question
question_seged=jieba.posseg.cut(str(clean_question))
result=[]
question_word, question_flag = [], []
for w in question_seged:
temp_word=f"{w.word}/{w.flag}"
result.append(temp_word)
# 预处理问题
word, flag = w.word,w.flag
question_word.append(str(word).strip())
question_flag.append(str(flag).strip())
assert len(question_flag) == len(question_word)
self.question_word = question_word
self.question_flag = question_flag
print(result)
return result

jieba的词性标注和分词是同步进行的,所以如果分词不准确的话,那么词性标注往往也会出错,比如说电影《卧虎藏龙》,被jieba分词分为:

1
卧虎  藏龙

这样肯定就不能识别出卧虎藏龙这个实体了啊,人名也是同样的道理,如果把一个演员的姓名分开了,也就不能标注出这样一个人名了,于是我就加了一个自定义字典,把所涉及到的所有电影,所有人名都加到了这个字典中,这样以来,jieba分词的时候就会参考字典里面的词来进行分词和词性标注了,自定义词典的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
太极气功 15 nm
功夫小子 15 nm
大师 15 nm
...
陈雅伦 15 nr
李修贤 15 nr
黄锦燊 15 nr
潘恒生 15 nr
林熙蕾 15 nr
锺丽缇 15 nr
...
恐怖 15 ng
动作 15 ng
喜剧 15 ng
历史 15 ng

这样涉及到的电影名,人名以及电影类型就不会被jieba分词分开了,此外还需要去掉问题中的特殊字符,这些操作搞完了后就可以进行分词和词性标注了,处理后把结果返回即可。

  • 问题分类和模板填充

接下来是对用户的问题进行分类,获取对应问题模板,从而明白用户的意图,首先要使用前面介绍的根据用户习惯构造的各种各样的问题来训练一个分类器,我这里使用的sklearn里面的贝叶斯分类器。
首先是组织训练数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 获取训练数据
def read_train_data(self):
train_x=[]
train_y=[]
file_list=getfilelist("./data/question/")
# 遍历所有文件
for one_file in file_list:
# 获取文件名中的数字
num = re.sub(r'\D', "", one_file)
# 如果该文件名有数字,则读取该文件
if str(num).strip()!="":
# 设置当前文件下的数据标签
label_num=int(num)
# 读取文件内容
with(open(one_file,"r",encoding="utf-8")) as fr:
data_list=fr.readlines()
for one_line in data_list:
word_list=list(jieba.cut(str(one_line).strip()))
# 将这一行加入结果集
train_x.append(" ".join(word_list))
train_y.append(label_num)
return train_x,train_y

接着是训练多分类贝叶斯分类器模型,并返回:

1
2
3
4
5
6
7
8
9
# 训练并测试模型-NB
def train_model_NB(self):
X_train, y_train = self.train_x, self.train_y
self.tv = TfidfVectorizer()

train_data = self.tv.fit_transform(X_train).toarray()
clf = MultinomialNB(alpha=0.01)
clf.fit(train_data, y_train)
return clf

利用训练好的模型来对新问题进行分类:

1
2
3
4
5
6
7
# 预测
def predict(self,question):
question=[" ".join(list(jieba.cut(question)))]
test_data=self.tv.transform(question).toarray()
y_predict = self.model.predict(test_data)[0]
# print("question type:",y_predict)
return y_predict

返回用户问题所属的类别编号,这个编号也就对应一个问题模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:nm 评分
1:nm 上映时间
2:nm 类型
3:nm 简介
4:nm 演员列表
5:nnt 介绍
6:nnt ng 电影作品
7:nnt 电影作品
8:nnt 参演评分 大于 x
9:nnt 参演评分 小于 x
10:nnt 电影类型
11:nnt nnr 合作 电影列表
12:nnt 电影数量
13:nnt 出生日期

数字和模板的对应关系可以提前存到一个字典中,当预测出编号后,直接通过这个编号作为字典的key值,这样就查询出问题模板了。比如预测的结果是2,则对应的问题模板 $nm 类型$,表示询问某部电影的类型,再结合前一阶段获取的电影名字,则可以组成一个新的问题,比如:

1
卧虎藏龙 类型

上面对涉及到的重点部分进行了介绍,这里小结一下,当遇到一个新问题时:

1
2
3
4
1、得到原始问题;
2、抽取问题中的关键信息,比如从问题“张学友的个人介绍”中抽取出张学友这个人名;
3、使用分类模型来预测问题类别,预测出模板编号,查询字典得到问题模板;如dict[5]:nnt 介绍;
4、替换模板中的抽象内容,得到:张学友 介绍。

大体思路是这样。