聊天机器人(Chatbot)开发:自然语言处理(NLP)技术栈
2017-12-15 09:14
585 查看
我相信在大多数情况下,聊天机器人的开发者构建自己的自然语言解析器,而不是使用第三方云端API,是有意义的选择。 这样做有很好的战略性和技术性方面的依据,我将向你展示自己实现
为什么要自己做
最简单的实现也很有效
你可以真正用起来的东西
那么要实现一个典型的机器人,你需要什么样的
I’m looking for a cheap Mexican place in the centre.
为了回答这个问题,你需要做两件事:
了解用户的意图(
提取
在之前的文章中,我提到像wit和LUIS这样的工具使得意图分类(
首先,如果你真的想建立一个基于对话软件的业务,那么把用户告诉你的所有东西都传给
其次,本地库是可以深入探索的(
首先,我们将不使用任何库(numpy除外)来构建一个最简单的模型,以便了解它是如何工作的。
我坚信,在机器学习中你唯一能做的就是找到一个好的表示(
我们将使用词向量(
下面的代码基于
实现的就是把整个词表载入内存:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[/code]
现在让我们尝试使用这些词向量来完成第一项任务:在句子
我们将尽可能使用最简单的方法:在句子中寻找与给出的菜系样例最相似的单词。 我们将遍历句子中的单词,并挑选出与参考单词的平均余弦相似度高于某个阈值的单词:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[/code]
让我们试一下例句。
2
3
4
5
6
7
8
9
10
11
12
13
[/code]
令人惊讶的是,上面的代码足以正确地泛化,并根据其与参考词的相似性来挑选
现在来分类用户的意图。 我们希望能够把句子分成“打招呼”,“感谢”,“请求餐馆”,“指定位置”,“拒绝建议”等类别,以便我们可以告诉机器人的后端运行哪些代码。 我们可以通过很多方法通过组合词向量来建立句子的表示,不过再一次,我们决定采用最简单的方法:把词向量加起来。 我知道也许你对这一方法的意义与作用有所质疑,附录中解释了这么做的原因。
我们可以为每个句子创建这些词袋(
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
[/code]
我所展示的解析和分类方法都不是特别鲁棒,所以我们将继续探索更好的方向。 但是,我希望我已经证明,没有什么神秘的,实际上很简单的方法已经可以工作了。
有很多事情我们可以做得更好。 例如,将文本转换为
在这里,我将使用MITIE (MIT信息抽取库)的
有两个类我们可以直接使用。 首先,一个文本分类器(
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[/code]
其次,一个实体识别器(
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[/code]
文本分类器是一个简单的
如果您有兴趣,在github仓库中有相关文献的链接。
正如你所期望的那样,使用像这样的库(或者SpaCy加上你最喜欢的ML库)比起我在开始时发布的实验代码提供了更好的性能。 事实上,根据我的经验,你可以很快地超越
我希望我已经说服你,在构建聊天机器人时创建自己的
感谢 Alex ,Kate, Norman 和 Joey 的阅读草稿!
你怎么可能把一个句子中的单词向量加起来(或平均),就可以作为句子的表示? 这就好像告诉你,一个班上的10个学生,在测试中平均得分为75%,你却试图找出每个人的成绩。 好吧,差不多。 事实证明,这是关于高维几何的那些违反直觉的事情之一。
如果从一组1000个样本中抽取10个向量,那么只要知道平均值,就可以真正地找出你选择哪个向量,如果向量具有足够高的维数(比如说300)。 这归结于
我们对单词向量的长度不感兴趣(它们在上面的代码中被归一化了),所以我们可以把这些向量看作单位球面上的点。 假设我们有一组
问题是,给定
我们可以使用一个叫做度量集中(
回到测试分数的比喻,按这个思路进行思考的话可能会使事情变得更清楚。 现在我们得到的是每个问题的平均分数,而不是平均总分。 现在,你可以从10位学生那里获得平均每个问题的分数,我们所说的是当测试中包含更多问题时,分辨哪些学生变得更容易。 毕竟这不是很违反直觉。
感谢 Alexander Weidauer 。
原文:Do-it-yourself NLP for bot developers
NLP有多么简单。 这篇文章包含3个部分:
为什么要自己做
最简单的实现也很有效
你可以真正用起来的东西
那么要实现一个典型的机器人,你需要什么样的
NLP技术栈? 假设您正在构建一项服务来帮助人们找到餐馆。 你的用户可能会这样说:
I’m looking for a cheap Mexican place in the centre.
为了回答这个问题,你需要做两件事:
了解用户的意图(
intent) :他们正在寻找一家餐厅,而不是说“你好”,“再见”或“感谢”。
提取
cheap,
Mexican和
center作为你的查询字段。
在之前的文章中,我提到像wit和LUIS这样的工具使得意图分类(
Intent Classification)和实体提取(
Entity Extraction)变得非常简单,以至于在参加黑客马拉松期间你就可以快速构建一个聊天机器人。 我是这些云端服务以及背后团队的忠实粉丝,但是并不是说它们适用于任何场景。
1.使用NLP库而不是云端API的三个理由
首先,如果你真的想建立一个基于对话软件的业务,那么把用户告诉你的所有东西都传给Web API是开发中每一个问题的解决方案。
https调用速度很慢,并且始终受到
API设计的限制。
其次,本地库是可以深入探索的(
hackable)。 第三,在自己的数据和用例上,你有机会实现更好的性能。 请记住,通用
API必须在每个问题上都做得很好,而你只需要做好你的工作。
2. 词向量 +启发式 - 复杂性=工作代码
首先,我们将不使用任何库(numpy除外)来构建一个最简单的模型,以便了解它是如何工作的。我坚信,在机器学习中你唯一能做的就是找到一个好的表示(
presentation)。 如果这一点不是很明了,我现在正在写另一篇文章对此进行解释,所以请稍后再回来看看。 重点是,如果你有一个有效的方式来表示你的数据,那么即使是非常简单的算法也能完成这项工作。
我们将使用词向量(
word vector),它是包含几十或几百个浮点数的向量,可以在某种程度上捕捉单词的含义 。 事实上,完全可以做到这一点,而这些模型的训练方式都是非常有趣的。 就我们的目的而言,这意味着我们已经完成了艰苦的工作:像
word2vec或
GloVe这样的词嵌入(
word embedding)是表示文本数据的有力方式。 我决定使用GloVe进行这些实验。 你可以从他们的仓库中下载训练好的模型,我使用了最小维数(50维)的预训练模型。
下面的代码基于
GloVe仓库中的
python示例。
实现的就是把整个词表载入内存:
class Embedding(object): def __init__(self,vocab_file,vectors_file): with open(vocab_file, 'r') as f: words = [x.rstrip().split(' ')[0] for x in f.readlines()] with open(vectors_file, 'r') as f: vectors = {} for line in f: vals = line.rstrip().split(' ') vectors[vals[0]] = [float(x) for x in vals[1:]] vocab_size = len(words) vocab = {w: idx for idx, w in enumerate(words)} ivocab = {idx: w for idx, w in enumerate(words)} vector_dim = len(vectors[ivocab[0]]) W = np.zeros((vocab_size, vector_dim)) for word, v in vectors.items(): if word == '<unk>': continue W[vocab[word], :] = v # normalize each word vector to unit variance W_norm = np.zeros(W.shape) d = (np.sum(W ** 2, 1) ** (0.5)) W_norm = (W.T / d).T self.W = W_norm self.vocab = vocab self.ivocab = ivocab1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[/code]
现在让我们尝试使用这些词向量来完成第一项任务:在句子
I’m looking for a cheap Mexican place in the centre.中提取
Mexican作为菜系名。
我们将尽可能使用最简单的方法:在句子中寻找与给出的菜系样例最相似的单词。 我们将遍历句子中的单词,并挑选出与参考单词的平均余弦相似度高于某个阈值的单词:
def find_similar_words(embed,text,refs,thresh): C = np.zeros((len(refs),embed.W.shape[1])) for idx, term in enumerate(refs): if term in embed.vocab: C[idx,:] = embed.W[embed.vocab[term], :] tokens = text.split(' ') scores = [0.] * len(tokens) found=[] for idx, term in enumerate(tokens): if term in embed.vocab: vec = embed.W[embed.vocab[term], :] cosines = np.dot(C,vec.T) score = np.mean(cosines) scores[idx] = score if (score > thresh): found.append(term) print scores return found1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[/code]
让我们试一下例句。
vocab_file ="/path/to/vocab_file" vectors_file ="/path/to/vectors_file" embed = Embedding(vocab_file,vectors_file) cuisine_refs = ["mexican","chinese","french","british","american"] threshold = 0.2 text = "I want to find an indian restaurant" cuisines = find_similar_words(embed,cuisine_refs,text,threshold) print(cuisines) # >>> ['indian']1
2
3
4
5
6
7
8
9
10
11
12
13
[/code]
令人惊讶的是,上面的代码足以正确地泛化,并根据其与参考词的相似性来挑选
Indian作为菜系类型。 因此,这就是为什么我说,一旦有了好的表示,问题就变得简单了。
现在来分类用户的意图。 我们希望能够把句子分成“打招呼”,“感谢”,“请求餐馆”,“指定位置”,“拒绝建议”等类别,以便我们可以告诉机器人的后端运行哪些代码。 我们可以通过很多方法通过组合词向量来建立句子的表示,不过再一次,我们决定采用最简单的方法:把词向量加起来。 我知道也许你对这一方法的意义与作用有所质疑,附录中解释了这么做的原因。
我们可以为每个句子创建这些词袋(
bag-of-words)向量,并再次使用简单的距离对它们进行分类。 再一次,令人惊讶的是,它已经可以泛化处理之前从未见过的句子了:
import numpy as np def sum_vecs(embed,text): tokens = text.split(' ') vec = np.zeros(embed.W.shape[1]) for idx, term in enumerate(tokens): if term in embed.vocab: vec = vec + embed.W[embed.vocab[term], :] return vec def get_centroid(embed,examples): C = np.zeros((len(examples),embed.W.shape[1])) for idx, text in enumerate(examples): C[idx,:] = sum_vecs(embed,text) centroid = np.mean(C,axis=0) assert centroid.shape[0] == embed.W.shape[1] return centroid def get_intent(embed,text): intents = ['deny', 'inform', 'greet'] vec = sum_vecs(embed,text) scores = np.array([ np.linalg.norm(vec-data[label]["centroid"]) for label in intents ]) return intents[np.argmin(scores)] embed = Embedding('/path/to/vocab','/path/to/vectors') data={ "greet": { "examples" : ["hello","hey there","howdy","hello","hi","hey","hey ho"], "centroid" : None }, "inform": { "examples" : [ "i'd like something asian", "maybe korean", "what mexican options do i have", "what italian options do i have", "i want korean food", "i want german food", "i want vegetarian food", "i would like chinese food", "i would like indian food", "what japanese options do i have", "korean please", "what about indian", "i want some vegan food", "maybe thai", "i'd like something vegetarian", "show me french restaurants", "show me a cool malaysian spot" ], "centroid" : None }, "deny": { "examples" : [ "nah", "any other places ?", "anything else", "no thanks" "not that one", "i do not like that place", "something else please", "no please show other options" ], "centroid" : None } } for label in data.keys(): data[label]["centroid"] = get_centroid(embed,data[label]["examples"]) for text in ["hey you","i am looking for chinese food","not for me"]: print "text : '{0}', predicted_label : '{1}'".format(text,get_intent(embed,text)) # output # >>>text : 'hey you', predicted_label : 'greet' # >>>text : 'i am looking for chinese food', predicted_label : 'inform' # >>>text : 'not for me', predicted_label : 'deny'1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
[/code]
我所展示的解析和分类方法都不是特别鲁棒,所以我们将继续探索更好的方向。 但是,我希望我已经证明,没有什么神秘的,实际上很简单的方法已经可以工作了。
3.你可以实际使用的东西
有很多事情我们可以做得更好。 例如,将文本转换为token而不是仅仅基于空白字符进行拆分。 一种方法是使用SpaCy /textacy的组合来清理和解析文本,并使用scikit-learn来构建模型。
在这里,我将使用MITIE (MIT信息抽取库)的
Python接口来完成我们的任务。
有两个类我们可以直接使用。 首先,一个文本分类器(
Text Classifier):
import sys, os from mitie import * trainer = text_categorizer_trainer("/path/to/total_word_feature_extractor.dat") data = {} # same as before - omitted for brevity for label in training_examples.keys(): for text in training_examples[label]["examples"]: tokens = tokenize(text) trainer.add_labeled_text(tokens,label) trainer.num_threads = 4 cat = trainer.train() cat.save_to_disk("my_text_categorizer.dat") # we can then use the categorizer to predict on new text tokens = tokenize("somewhere that serves chinese food") predicted_label, _ = cat(tokens)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[/code]
其次,一个实体识别器(
Entity Recognizer):
import sys, os from mitie import * sample = ner_training_instance(["I", "am", "looking", "for", "some", "cheap", "Mexican", "food", "."]) sample.add_entity(xrange(5,6), "pricerange") sample.add_entity(xrange(6,7), "cuisine") # And we add another training example sample2 = ner_training_instance(["show", "me", "indian", "restaurants", "in", "the", "centre", "."]) sample2.add_entity(xrange(2,3), "cuisine") sample2.add_entity(xrange(6,7), "area") trainer = ner_trainer("/path/to/total_word_feature_extractor.dat") trainer.add(sample) trainer.add(sample2) trainer.num_threads = 4 ner = trainer.train() ner.save_to_disk("new_ner_model.dat") # Now let's make up a test sentence and ask the ner object to find the entities. tokens = ["I", "want", "expensive", "korean", "food"] entities = ner.extract_entities(tokens) print "\nEntities found:", entities print "\nNumber of entities detected:", len(entities) for e in entities: range = e[0] tag = e[1] entity_text = " ".join(tokens[i] for i in range) print " " + tag + ": " + entity_text # output # >>> Number of entities detected: 2 # >>> pricerange: expensive # >>> cuisine: korean1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[/code]
MITIE库非常复杂,使用多种词嵌入而不单是
GloVe。
文本分类器是一个简单的
SVM,而实体识别器使用结构化
SVM。
如果您有兴趣,在github仓库中有相关文献的链接。
正如你所期望的那样,使用像这样的库(或者SpaCy加上你最喜欢的ML库)比起我在开始时发布的实验代码提供了更好的性能。 事实上,根据我的经验,你可以很快地超越
wit或
LUIS的表现,因为你可以根据自己数据集进行相应的参数调整。
结论
我希望我已经说服你,在构建聊天机器人时创建自己的NLP模块是值得的。 请在下面添加你的想法、建议和问题。 我期待着讨论。 如果你喜欢这个文章,可以在这里赞一下,或者在twitter上 ,那会更好。
感谢 Alex ,Kate, Norman 和 Joey 的阅读草稿!
附录:稀疏恢复(sparse recovery
)
你怎么可能把一个句子中的单词向量加起来(或平均),就可以作为句子的表示? 这就好像告诉你,一个班上的10个学生,在测试中平均得分为75%,你却试图找出每个人的成绩。 好吧,差不多。 事实证明,这是关于高维几何的那些违反直觉的事情之一。如果从一组1000个样本中抽取10个向量,那么只要知道平均值,就可以真正地找出你选择哪个向量,如果向量具有足够高的维数(比如说300)。 这归结于
R³⁰⁰中有很多空间的事实,所以如果随机抽样一对向量,你可以期望它们(几乎)是线性独立的。
我们对单词向量的长度不感兴趣(它们在上面的代码中被归一化了),所以我们可以把这些向量看作单位球面上的点。 假设我们有一组
N个向量
V⊂ℝ^d,它们是单位
d球体上的独立同分布(
iid)。
问题是,给定
V的一个子集
S,如果只知道
x = Σ v _i,我们需要多大的
D才能恢复所有的
S?只有当
x和
v(
v ∉ S)之间的点积足够小,并且
S中的向量有
v · x〜1时,我们才能够恢复原始的数据。
我们可以使用一个叫做度量集中(
concentration of measure)的结果,它告诉我们我们需要什么。 对于单位
d球上的
iid点,任意两点之间点积的期望值
E( v · w )= 1 /√d。 而点积大于a的概率是
P( v · w > a)≤(1-a²)^(d / 2)。 所以我们可以写出概率
ε,即就空间的维度而言,某个向量
v ∉S太靠近向量
v ∈S。 。 这给出了减少
ε失败概率的结果,我们需要
d> S log(NS /ε)。 因此,如果我们想要从总共1000个具有1%容错的10个向量中恢复一个子集,我们可以在138个维度或更高的维度上完成。
回到测试分数的比喻,按这个思路进行思考的话可能会使事情变得更清楚。 现在我们得到的是每个问题的平均分数,而不是平均总分。 现在,你可以从10位学生那里获得平均每个问题的分数,我们所说的是当测试中包含更多问题时,分辨哪些学生变得更容易。 毕竟这不是很违反直觉。
感谢 Alexander Weidauer 。
原文:Do-it-yourself NLP for bot developers
相关文章推荐
- 聊天机器人(Chatbot)开发:自然语言处理(NLP)技术栈
- 聊天机器人(chatbot)终极指南:自然语言处理(NLP)和深度机器学习(Deep Machine Learning)
- 聊天机器人(chatbot)终极指南:自然语言处理(NLP)和深度机器学习(Deep Machine Learning)
- 自然语言处理的主流技术(NLP)
- NLP汉语自然语言处理原理与实践 3 词汇与分词技术
- 自然语言处理技术(NLP)在推荐系统中的应用
- 自然语言处理技术(NLP)在推荐系统中的应用
- 自然语言处理(NLP)学习笔记(二)——NLP技术
- 自然语言处理技术(NLP)在推荐系统中的应用
- NLP 专题论文解读:从 Chatbot 到 NER | PaperDaily #11
- 自然语言处理技术(NLP)在推荐系统中的应用 原2017.06.29人工智能头条 作者: 张相於,58集团算法架构师,转转搜索推荐部负责人,负责搜索、推荐以及算法相关工作。多年来主要从事推荐系统以及机
- NLP入门+实战必读:一文教会你最常见的10种自然语言处理技术(附代码)
- 阿里自然语言处理部总监分享:NLP技术的应用及思考
- NLP自然语言处理相关技术说明及样例(附源码)
- NLP入门+实战必读:一文教会你最常见的10种自然语言处理技术(附代码)
- 聊天机器人教学:使用Dialogflow (API.AI)开发 iOS Chatbot App
- 《NLP汉语自然语言处理原理与实践》第三章 词汇与分词技术
- [NLP]如何打造一个Chatbot
- WebService从零到项目开发使用4—技术研究之Axis2 集成Spring框架