Amaze UI Logo

收集文章: 2001

浏览人数: 1333014



利用 TensorFlow 实现上下文的 Chat-bots 转发

利用 TensorFlow 实现上下文的 Chat-bots

在我们的日常聊天中,情景才是最重要的。我们将使用 TensorFlow 构建一个聊天机器人框架,并且添加一些上下文处理机制来使得机器人更加智能。


“Whole World in your Hand” — Betty Newman-Maguire (http://www.bettynewmanmaguire.ie/)

你是否想过一个问题,为什么那么多的聊天机器人会缺乏会话情景功能?

鉴于上下文在所有的对话场景中的重要性,那么又该如何加入这个特性?

接下来,我们将创建一个聊天机器人的框架,并且以一个岛屿轻便摩托车租赁店为例子,建立一个对话模型。这个小企业的聊天机器人需要处理一些关于租赁时间,租赁选项等的简单问题。我们也希望这个机器人可以处理一些上下文的信息,比如查询同一天的租赁信息。如果可以解决这个问题,那么我们将节约很多的时间。

关于构建聊天机器人,我们通过以下三部进行:

  1. 我们会利用 TensorFlow 来编写对话意图模型。

  2. 接下啦,我们将构建一个处理对话的聊天机器人框架。

  3. 最后,我们将介绍如何将上下文信息合并到我们的响应式处理器中。

在模型中,我们将使用 tflearn 框架,这是一个 TensorFlow 的高层 API,并且我们将使用IPython 作为开发工具。

1. 我们会利用 TensorFlow 来编写对话意图模型。

完整的 notebook 文档,可以点击这里

对于一个聊天机器人框架,我们需要定义一个会话意图的结构。最简单方便的方式是使用一个 JSON 格式的文件,如下所示:


chat-bot intents

每个会话意图包含:

  • 标签(唯一的名称)

  • 模式(我们的神经网络文本分类器需要分类的句子)

  • 回应(一个将被用作回应的句子)

稍后,我们也会添加一些基本的上下文元素。

首先,我们来导入一些我们需要的包:

# 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 将和 taketakingtakers 等匹配。在实际过程中,我们可以删除一些无用的条目,但在这里已经足够了。

不幸的是,这种数据结构不能在 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 层神经网络模型,跟这篇文章中的是一样的。


interactive build of a model in tflearn

我们完成了这部分的工作,现在需要保存我们的模型和文档, 以便在后续的代码中可以使用它们。

# 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必须是唯一的。

我们会复制有些用户的对话信息来进行场景分析,如果这些信息被保存在临时变量中,那么就非常难来处理,这是一个最大的考虑。


所以,现在你已经学会了如何去构建一个聊天机器人框架,一个使它能记住上下文信息的机器人,已经如何分析文本。未来的聊天机器人也都是能分析上下文语境的,这是一个大趋势。

我们联想到意图的构建会影响上下文的对话反应,所以我们可以创建各种各样的会话环境。

快去动手试试吧!


反对(0) 支持(0)


评论