利用 TensorFlow 实现上下文的 Chat-bots
在我们的日常聊天中,情景才是最重要的。我们将使用 TensorFlow 构建一个聊天机器人框架,并且添加一些上下文处理机制来使得机器人更加智能。
你是否想过一个问题,为什么那么多的聊天机器人会缺乏会话情景功能?
鉴于上下文在所有的对话场景中的重要性,那么又该如何加入这个特性?
接下来,我们将创建一个聊天机器人的框架,并且以一个岛屿轻便摩托车租赁店为例子,建立一个对话模型。这个小企业的聊天机器人需要处理一些关于租赁时间,租赁选项等的简单问题。我们也希望这个机器人可以处理一些上下文的信息,比如查询同一天的租赁信息。如果可以解决这个问题,那么我们将节约很多的时间。
关于构建聊天机器人,我们通过以下三部进行:
我们会利用 TensorFlow 来编写对话意图模型。
接下啦,我们将构建一个处理对话的聊天机器人框架。
最后,我们将介绍如何将上下文信息合并到我们的响应式处理器中。
在模型中,我们将使用 tflearn 框架,这是一个 TensorFlow 的高层 API,并且我们将使用 IPython 作为开发工具。
1. 我们会利用 TensorFlow 来编写对话意图模型。
完整的 notebook 文档,可以点击这里。
对于一个聊天机器人框架,我们需要定义一个会话意图的结构。最简单方便的方式是使用一个 JSON 格式的文件,如下所示:
每个会话意图包含:
标签(唯一的名称)
模式(我们的神经网络文本分类器需要分类的句子)
回应(一个将被用作回应的句子)
稍后,我们也会添加一些基本的上下文元素。
首先,我们来导入一些我们需要的包:
# things we need for NLPimport nltkfrom nltk.stem.lancaster import LancasterStemmerstemmer = LancasterStemmer()# things we need for Tensorflowimport numpy as npimport tflearnimport tensorflow as tfimport random
如果你还不了解 TensorFlow,那么可以学习一下这个教程或者这个教程。
# import our chat-bot intents fileimport jsonwith open('intents.json') as json_data: intents = json.load(json_data)
代码中的 JSON 文件可以这里下载,接下来我们可以开始组织代码的文件,数据和分类器。
words = [] classes = [] documents = [] ignore_words = ['?']# loop through each sentence in our intents patternsfor intent in intents['intents']: for pattern in intent['patterns']: # tokenize each word in the sentence w = nltk.word_tokenize(pattern) # add to our words list words.extend(w) # add to documents in our corpus documents.append((w, intent['tag'])) # add to our classes list if intent['tag'] not in classes: classes.append(intent['tag'])# stem and lower each word and remove duplicateswords = [stemmer.stem(w.lower()) for w in words if w not in ignore_words]words = sorted(list(set(words)))# remove duplicatesclasses = sorted(list(set(classes))) print (len(documents), "documents") print (len(classes), "classes", classes) print (len(words), "unique stemmed words", words)
我们创建了一个文件列表(每个句子),每个句子都是由一些词干组成,并且每个文档都属于一个特定的类别。
27 documents9 classes ['goodbye', 'greeting', 'hours', 'mopeds', 'opentoday', 'payments', 'rental', 'thanks', 'today']44 unique stemmed words ["'d", 'a', 'ar', 'bye', 'can', 'card', 'cash', 'credit', 'day', 'do', 'doe', 'good', 'goodby', 'hav', 'hello', 'help', 'hi', 'hour', 'how', 'i', 'is', 'kind', 'lat', 'lik', 'mastercard', 'mop', 'of', 'on', 'op', 'rent', 'see', 'tak', 'thank', 'that', 'ther', 'thi', 'to', 'today', 'we', 'what', 'when', 'which', 'work', 'you']
比如,词干 tak
将和 take
,taking
,takers
等匹配。在实际过程中,我们可以删除一些无用的条目,但在这里已经足够了。
不幸的是,这种数据结构不能在 TensorFlow 中使用,我们需要进一步将这个数据进行转换:从单词转换到数字的张量。
# create our training data training = [] output = [] # create an empty array for our output output_empty = [0] * len(classes) # training set, bag of words for each sentencefor doc in documents: # initialize our bag of words bag = [] # list of tokenized words for the pattern pattern_words = doc[0] # stem each word pattern_words = [stemmer.stem(word.lower()) for word in pattern_words] # create our bag of words array for w in words: bag.append(1) if w in pattern_words else bag.append(0) # output is a '0' for each tag and '1' for current tag output_row = list(output_empty) output_row[classes.index(doc[1])] = 1 training.append([bag, output_row]) # shuffle our features and turn into np.array random.shuffle(training) training = np.array(training) # create train and test lists train_x = list(training[:,0]) train_y = list(training[:,1])
请注意,我们的数据顺序已经被打乱了。 TensorFlow 会选取其中的一些数据作为测试数据,用来测试训练的模型的准确度。
如果我们观察单个的 x
向量和 y
向量,那么这就是一个词袋模型,一个表示需要匹配的模式,一个表示匹配的目标。
train_x example: [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1] train_y example: [0, 0, 1, 0, 0, 0, 0, 0, 0]
接下来,我们来构建我们的模型。
# reset underlying graph datatf.reset_default_graph()# Build neural networknet = tflearn.input_data(shape=[None, len(train_x[0])])net = tflearn.fully_connected(net, 8)net = tflearn.fully_connected(net, 8)net = tflearn.fully_connected(net, len(train_y[0]), activation='softmax')net = tflearn.regression(net)# Define model and setup tensorboardmodel = tflearn.DNN(net, tensorboard_dir='tflearn_logs')# Start training (apply gradient descent algorithm)model.fit(train_x, train_y, n_epoch=1000, batch_size=8, show_metric=True)model.save('model.tflearn')
这个模型使用的是 2 层神经网络模型,跟这篇文章中的是一样的。
我们完成了这部分的工作,现在需要保存我们的模型和文档, 以便在后续的代码中可以使用它们。
# save all of our data structuresimport pickle pickle.dump( {'words':words, 'classes':classes, 'train_x':train_x, 'train_y':train_y}, open( "training_data", "wb" ) )
构建我们的聊天机器人框架
这部分,完整的代码在这里。
我们将构建一个简单的状态机来处理响应,并且使用我们的在上一部分中提到的意图模型来作为我们的分类器。如果你想了解聊天机器人的工作原理,那么可以点击这里。
我们需要导入和上一部分相同的包,然后 un-pickle 我们的模型和句子,正如我们在上一部分中操作的。请记住,我们的聊天机器人框架与我们的模型是分开构建的 —— 除非意图模式改变了,那么我们需要重新运行我们的模型,否则不需要重构模型。如果拥有数百种意图和数千种模式,模型可能需要几分钟的时间才能构建完成。
# restore all of our data structuresimport pickledata = pickle.load( open( "training_data", "rb" ) ) words = data['words'] classes = data['classes'] train_x = data['train_x'] train_y = data['train_y'] # import our chat-bot intents fileimport jsonwith open('intents.json') as json_data: intents = json.load(json_data)
接下来,我们需要导入刚刚利用 TensorFlow(tflearn 框架)训练好的模型。请注意,你第一步还是需要去定义 TensorFlow 模型结构,正如我们在第一部分中做的那样。
# load our saved modelmodel.load('./model.tflearn')
在我们开始处理对话意图之前,我们需要一种从用户输入数据生词词袋的方法。而这个方法,跟我们前面所使用的方法是相同的。
def clean_up_sentence(sentence): # tokenize the pattern sentence_words = nltk.word_tokenize(sentence) # stem each word sentence_words = [stemmer.stem(word.lower()) for word in sentence_words] return sentence_words# return bag of words array: 0 or 1 for each word in the bag that exists in the sentencedef bow(sentence, words, show_details=False): # tokenize the pattern sentence_words = clean_up_sentence(sentence) # bag of words bag = [0]*len(words) for s in sentence_words: for i,w in enumerate(words): if w == s: bag[i] = 1 if show_details: print ("found in bag: %s" % w) return(np.array(bag))
p = bow("is your shop open today?", words) print (p) [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0]
现在,我们可以开始构建我们的响应处理器了。
ERROR_THRESHOLD = 0.25def classify(sentence): # generate probabilities from the model results = model.predict([bow(sentence, words)])[0] # filter out predictions below a threshold results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD] # sort by strength of probability results.sort(key=lambda x: x[1], reverse=True) return_list = [] for r in results: return_list.append((classes[r[0]], r[1])) # return tuple of intent and probability return return_listdef response(sentence, userID='123', show_details=False): results = classify(sentence) # if we have a classification then find the matching intent tag if results: # loop as long as there are matches to process while results: for i in intents['intents']: # find a tag matching the first result if i['tag'] == results[0][0]: # a random response from the intent return print(random.choice(i['responses'])) results.pop(0)
传递给 response() 的每个句子都会被分类。我们分类器使用 model.predict()
函数来进行类别预测,这个方法非常快。模型返回的概率值和我们定义的意图是一直的,用来生成潜在的响应列表。
如果一个或多个分类结果高于阈值,那么我们会选取出一个与意图匹配的标签,然后处理。我们将我们的分类列表作为一个堆栈,并从这个堆栈中寻找一个适合的匹配,直到找到一个最好的或者直到堆栈变空。
我们来举一个例子,模型会返回最有可能的标签和其概率。
classify('is your shop open today?')[('opentoday', 0.9264171123504639)]
请注意,“is your shop open today?” 不是这个意图中的任何模式:“pattern : ["Are you open today?", "When do you open today?", "What are your hours today?"]”。但是,“open” 和 “today” 术语对我们的模式是非常有用的(他们在选择意图时,有决定性的作用)。
我们现在从用户的输入数据中产生一个结果:
response('is your shop open today?')Our hours are 9am-9pm every day
再来一些例子:
response('do you take cash?')We accept VISA, Mastercard and AMEXresponse('what kind of mopeds do you rent?')We rent Yamaha, Piaggio and Vespa mopedsresponse('Goodbye, see you later')Bye! Come back again soon.
接下来让我们结合一些基础的语境来设计一个聊天机器人,比如拖车租赁聊天机器人。
语境
我们想处理的是一个关于租赁摩托车的问题,并询问一些有关租金的事。对于用户问题的理解应该是非常容易的,语境非常清晰。如果用户询问 “today”,那么上下文的租赁信息就是进入时间框架,那么最好你还能指定是哪一个自行车,这样交流起来就不会浪费时间。
为了实现这一点,我们需要在框架中再加入一个概念 “state” 。这需要一个数据结构来维护这个新的概念和原来的意图。
因为我们需要我们的状态机是一个非常容易的维护,恢复和复制等等操作,所以我们需要把数据都保存在一个诸如字典的数据结构中,这是非常重要的。
接下来,我们给出基本语境的回复过程:
# create a data structure to hold user contextcontext = {} ERROR_THRESHOLD = 0.25def classify(sentence): # generate probabilities from the model results = model.predict([bow(sentence, words)])[0] # filter out predictions below a threshold results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD] # sort by strength of probability results.sort(key=lambda x: x[1], reverse=True) return_list = [] for r in results: return_list.append((classes[r[0]], r[1])) # return tuple of intent and probability return return_listdef response(sentence, userID='123', show_details=False): results = classify(sentence) # if we have a classification then find the matching intent tag if results: # loop as long as there are matches to process while results: for i in intents['intents']: # find a tag matching the first result if i['tag'] == results[0][0]: # set context for this intent if necessary if 'context_set' in i: if show_details: print ('context:', i['context_set']) context[userID] = i['context_set'] # check if this intent is contextual and applies to this user's conversation if not 'context_filter' in i or \ (userID in context and 'context_filter' in i and i['context_filter'] == context[userID]): if show_details: print ('tag:', i['tag']) # a random response from the intent return print(random.choice(i['responses'])) results.pop(0)
我们的上下文状态是一个字典,它将包含每个用户的状态。我将为每个用户使用一个唯一的标识(例如,cell#)。这允许我们的框架和状态机同事维护多个用户的状态。
# create a data structure to hold user contextcontext = {}
我们在意图处理流程中,添加了上下文信息,具体如下:
if i['tag'] == results[0][0]: # set context for this intent if necessary if 'context_set' in i: if show_details: print ('context:', i['context_set']) context[userID] = i['context_set'] # check if this intent is contextual and applies to this user's conversation if not 'context_filter' in i or \ (userID in context and 'context_filter' in i and i['context_filter'] == context[userID]): if show_details: print ('tag:', i['tag']) # a random response from the intent return print(random.choice(i['responses']))
如果一个意图想要设置上下文信息,那么我们可以这样做:
{“tag”: “rental”, “patterns”: [“Can we rent a moped?”, “I’d like to rent a moped”, … ], “responses”: [“Are you looking to rent today or later this week?”], “context_set”: “rentalday” }
如果另一个意图想要与上下文进行关联,那么可以这样做:
{“tag”: “today”, “patterns”: [“today”], “responses”: [“For rentals today please call 1–800-MYMOPED”, …], “context_filter”: “rentalday” }
以这种方法构建的信息库,如果用户只是输入 "today" 而没有上下文信息,那么这个 “today” 的用户意图是不会被处理的。如果用户输入的 "today" 是对我们的一个时间回应,即触动了意图标签 "rental" ,那么这个意图将会被处理。
response('we want to rent a moped')Are you looking to rent today or later this week?response('today')Same-day rentals please call 1-800-MYMOPED
我们上下文信息也改变了:
context{'123': 'rentalday'}
我们定义我们的 "greeting" 意图用来清除上下文语境信息,这就像我们打招呼一样,标志着我们要开启一个新的对话。我们还添加了 "show_details" 参数,用来帮助我们看到程序里面的信息。
response("Hi there!", show_details=True) context: ''tag: greeting Good to see you again
让我们再次尝试输入 "今天" 这个词,一些有趣的事情就发生了。
response('today') We're open every day from 9am-9pm classify('today') [('today', 0.5322513580322266), ('opentoday', 0.2611265480518341)]
首先,我们对没有上下文信息的 "today" 的回应是不同的。我们的分类产生了 2 个合适的意图,但 "opentoday" 被选中了。所以这个随机性就比较大,上下文信息很重要!
response("thanks, your great")Happy to help!
现在需要考虑的事情就是如何将对话放置到具体语境中了。
状态处理
没错,你的机器人将会成为你的私人机器人了,不再是那么大众化。除非你想要重建状态,重新加载你的模型和文档 —— 每次调用你的机器人框架,你都会需要加载一个模型状态。
这不是那么困难,你可以在自己的进程中运行一个有状态的聊天机器人框架,并使用 RPC(远程过程调用)或 RMI(远程方法调用)调用它,我推荐使用 Pyro
用户界面(客户端)通常是无状态的,例如:HTTP 或 SMS。
你的聊天机器人客户端将通过 Pyro 函数进行调用,你的状态服务将由它处理,是不是很赞。
这里有一个手把手教你如何构建一个 Twilio SMS 机器人客户端的方法,这里是一个构建 Facebook 机器人的方法。
不要将状态存储在局部变量中
所有状态信息都必须放在诸如字典之类的数据结构中,易于持久化,重新加载或者以原子状态进行复制。
每个用户的对话和上下文语境都会保存在用户 ID 下面,这个ID必须是唯一的。
我们会复制有些用户的对话信息来进行场景分析,如果这些信息被保存在临时变量中,那么就非常难来处理,这是一个最大的考虑。
所以,现在你已经学会了如何去构建一个聊天机器人框架,一个使它能记住上下文信息的机器人,已经如何分析文本。未来的聊天机器人也都是能分析上下文语境的,这是一个大趋势。
我们联想到意图的构建会影响上下文的对话反应,所以我们可以创建各种各样的会话环境。
快去动手试试吧!
作者:chen_h
链接:https://www.ft12.com/
來源:FT12短网址
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。