C++_写一个聊天机器人

注:本文翻译自http://www.codeproject.com/articles/36106/chatbot-tutorial#preprocessing

简介

聊天机器人就是一种能和你用相同的自然语言交流的电脑程序,这便意味着机器人的能力是由回复的质量来决定的。根据这个定义我们就可以用几行代码来完成一个非常基础的机器人。下面就是我们的第一个机器人(下面的所有的代码都会用c++完成,阅读下面的代码需要读者熟悉STL)。这个机器人也用下面的语言完成了:Java, Visual Basic, C#, Pascal, Prolog and Lisp(地址就在原帖里)

//
// Program Name: chatterbot1
// Description: this is a very basic example of a chatterbot program
//描述:这是一个非常基础的聊天机器人
//
// Author: Gonzales Cenelia
//

#include <iostream>
#include <string>
#include <ctime>

int main()
{
    std::string Response[] = {
        "I HEARD YOU!",
        "SO, YOU ARE TALKING TO ME.",
        "CONTINUE, I’M LISTENING.",
        "VERY INTERESTING CONVERSATION.",
        "TELL ME MORE..."
    };

    srand((unsigned) time(NULL));

    std::string sInput = "";
    std::string sResponse = "";

    while(1) {
        std::cout << ">";
        std::getline(std::cin, sInput);
        int nSelection = rand() % 5;
        sResponse = Response[nSelection];
        std::cout << sResponse << std::endl;
    }

    return 0;
}

就像上面的程序一样,写一个能和人交流的小程序并不需要多少行代码,但是写一个能够读懂人说什么并恰当的回复的机器人是非常困单的。在第一代计算机产生甚至更早的时候,科学家就把写一个能够读懂人类语言的机器人作为一个非常长远的目标。在1951年,英国数学家Alan Turing提出了一个问题“可以让机器思考吗”。同时,他也提出了著名的“图灵测试”,图灵测试就是让测试者分别于一台计算机和一个人对话,测试者判断出哪一个是真的人。如今,“罗布纳奖”便是颁给与人类反应最难区别的计算机,如果能够骗过大部分的测试者,那么将会赢得100000¥的奖金。迄今为止还没有计算机能够通过这项测试。主要原因之一便是电脑程序总是有回复错的趋势(他们经常回复与上下文无关的话)。这便意味着人们能够很容易的判断出他是在跟一个人还是一个机器人交流。
所有试图与人类进行对话的程序的祖先是Eliza,他最初的版本是由MIT的教授Joseph Weizenbaum在1966年完成的。
聊天机器人通常属于“weak AI”领域而不是以制造一个和人类一样甚至比人类更聪明的机器人为目标的“strong AI”。但是这并不意味着聊天机器人没有钱能。在人工智能领域,写一个以人类的方式与人类交流的软件就是一个极大的进步。聊天机器人 对热衷于人工智能领域的人来说是非常容易理解的(写一个聊天机器人仅仅需要一些普通的技巧)。所以,对于那些想要进军人工智能领域的程序员来说,写一个聊天机器人是一个非常好的起点。

现在,让我们再来看看刚才的程序都存在什么问题

好吧,这里面有很多问题。首先,这个程序并没有师徒理解人说了什么,相反,它仅仅是从数据库随机选取了一个回复来回复人输入的句子。其次,这个机器人经常回复相同的句子,造成这个现象的原因之一便是它的数据库太小了(仅仅只有5个句子)。另一个原因就是我们没有实施一些措施来空着这个无法预料的行为。

那我们如何把这个机器人从随机回复变成一个能够理解用户输入并做出智能回复的机器人呢

这个问题的回答很简单,就是使用关键词。
关键词可以是一个句子(不必是完整的一个)或者甚至是一个单词,程序可以通过识别这些关键词来做出相应的反应(例如:在屏幕上显示)。在下一个程序中,我们将会写一个知识库或数据库,它包括了一些关键词和一些与这些关键词有关的回复。
现在,我们知道了如何提升我们的第一个“聊天机器人”并让他更聪明。下面就是第二个机器人程序,我们叫他“chatterbot2”

//
// Program Name: chatterbot2
// Description: this is an improved version
// of the previous chatterbot program "chatterbot1"
// this one will try a little bit more to understand what the user is trying to say
//
// Author: Gonzales Cenelia
//

#pragma warning(disable: 4786)

#include <iostream>
#include <string>
#include <vector>
#include <ctime>

const int MAX_RESP = 3;

typedef std::vector<std::string> vstring;

vstring find_match(std::string input);
void copy(char *array[], vstring &v);


typedef struct {
    char *input;
    char *responses[MAX_RESP];
}record;

record KnowledgeBase[] = {
    {"WHAT IS YOUR NAME", 
    {"MY NAME IS CHATTERBOT2.",
     "YOU CAN CALL ME CHATTERBOT2.",
     "WHY DO YOU WANT TO KNOW MY NAME?"}
    },

    {"HI", 
    {"HI THERE!",
     "HOW ARE YOU?",
     "HI!"}
    },

    {"HOW ARE YOU",
    {"I'M DOING FINE!",
    "I'M DOING WELL AND YOU?",
    "WHY DO YOU WANT TO KNOW HOW AM I DOING?"}
    },

    {"WHO ARE YOU",
    {"I'M AN A.I PROGRAM.",
     "I THINK THAT YOU KNOW WHO I'M.",
     "WHY ARE YOU ASKING?"}
    },

    {"ARE YOU INTELLIGENT",
    {"YES,OFCORSE.",
     "WHAT DO YOU THINK?",
     "ACTUALY,I'M VERY INTELLIGENT!"}
    },

    {"ARE YOU REAL",
    {"DOES THAT QUESTION REALLY MATERS TO YOU?",
     "WHAT DO YOU MEAN BY THAT?",
     "I'M AS REAL AS I CAN BE."}
    }
};

size_t nKnowledgeBaseSize = sizeof(KnowledgeBase)/sizeof(KnowledgeBase[0]);


int main() {
    srand((unsigned) time(NULL));

    std::string sInput = "";
    std::string sResponse = "";

    while(1) {
        std::cout << ">";
        std::getline(std::cin, sInput);
        vstring responses = find_match(sInput);
        if(sInput == "BYE") {
            std::cout << "IT WAS NICE TALKING TO YOU USER, SEE YOU NEXTTIME!" << std::endl;  
            break;
        } 
        else if(responses.size() == 0)  {
            std::cout << "I'M NOT SURE IF I  UNDERSTAND WHAT YOU  ARE TALKING ABOUT." << std::endl;
        }
        else {
            int nSelection = rand()  % MAX_RESP;
            sResponse =   responses[nSelection]; std::cout << sResponse << std::endl; 
        } 
    } 

    return 0;
}

// make a  search for the  user's input 
// inside the database of the program 
vstring find_match(std::string  input) { 
    vstring result;
    for(int i = 0; i < nKnowledgeBaseSize;  ++i) {  
        if(std::string(KnowledgeBase[i].input) == input) { 
            copy(KnowledgeBase[i].responses, result); 
            return result;
        } 
    } 
    return result; 
}

void copy(char  *array[], vstring &v) { 
    for(int i = 0;  i < MAX_RESP; ++i) {
        v.push_back(array[i]);
    }
}

现在,这个程序可以理解一些类似于“what is your name”,“are you intelligent”等等的句子,并且可以从他的回复列表里选择一个合适的句子来显示到屏幕上。Chatterbot2能够根据人输入的句子来选择一个合适的回复,而不是像以前的版本一样仅仅是随机回复而不管人在说什么。
为了解决程序无法找到一个合适的关键词的问题,我们通过回复“I’M NOT SURE IF I UNDERSTAND WHAT YOU ARE TALKING ABOUT.”来确保这个程序更像是一个人。

我们还可以做什么来让这个程序更好?

首先,这个程序会不断的重复一些相同的话,我们可以创建一种机制来避免这种情况。
我们可以把这个程序上次回复的句子用一个字符串“sPrevResponse”来储存,当机器人要回复的后判断是否与这个字符串相同,如果相同我们就选择一个新的。
另一个问题就是这个程序限制了用户的输入,比如如果现在你全用小写字母输入,即使数据库中含有一个和这个输入匹配的回复,这个机器人也不会做出反应。另外,如果句子中含有像“!;,。”的标点符号也会阻碍程序对输入的理解。所以在程序进入数据库进行检索的时候,我们需要处理一下用户输入。如果数据库中的关键词是大写字母,我们可以用一个函数把输入的字母全部变成大写。然后用另一个函数把句子内的标点符号和额外的空格剔除。现在我们便有了足够的材料来写第三代聊天机器人了。

现在的版本还有那些弱势?

很明显,如今的版本还有许多限制。最明显的就是这个程序为了做出一个回复必须要求用户的输入与关键词一字不差。假设如果你问他“what is your name again”,这个程序不会理解你想对他说什么,这是因为他无法找到一个匹配的回复内容。但是一个令人吃惊的事实却是他能理解”what is your name”

我们如何解决这个问题

至少有两种方法来解决这个问题。
最明显的结局方案就是采用更加灵活的方式来匹配关键字。为了让这个变成一种可能我们需要做的就是判断用户输入中是否含有这些关键字,这样我们就不会有以前的那些闲着了。
另一种方法比较麻烦。使用了”字符串模糊搜索“的概念。 为了应用这个方法,我们一开始需要把输入的句子和关键词分成一个个的单词,然后我们创建两个向量,一个用来存放输入的单词,另一个用来存放关键词的单词,然后我们就可以用“Levenshtein distance”来来测量两个向量之间的差距。(如果我们要使这个方法有效,我们需要创建一个额外的关键字来代表被分割的关键字)

(注:编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。)

现在你有了两种解决该问题的方法,事实上我们可以结合这两种方法并选择在合适的时机使用他们中的一个。
最后仍有一个你可能会注意到的问题,那就是你可以用一个句子不断的跟机器人对话,然后程序就没有句子来回复了。我们需要修正这个问题。
现在我们可以写第四代机器人了。我们可以把它简单的称作“chatterbot4”。
你可以看到,第四代机器人的代码和第三代机器人的代码非常类似,但是也有一些不同,特别的,在数据库中匹配关键字变得更加灵活了。
那么,接下来干嘛?别担心:仍然有许多东西等待完善。

在chatterbot4我们还可以做什么来让他变得更好

这里有一些建议

  • 这个机器人的代码如今已经变得越来越长了,所以我们可以尝试吧这个机器人封装到一个类里。
  • 这个机器人的数据库还是太小了,它还是不能和人类聊天,所以我们需要继续扩大这个数据库。
  • 有的时候用户可能会只按了回车键,我们要解决这种情况
  • 有时使用者会用几个非常类似的句子来捉弄这个机器人,我们要把这种情况归为使用者在不停的重复
  • 最后,我们需要一个方法来对关键字排序,如果我们有数种选择,我们需要选择一个最好的。

现在,我们可以写第五代机器人——“chatterbot5”
在进行下一步之前,你应该尝试编译并运行这段代码, 这样你就能理解他是如何工作并看到其中的变化。如果你看完了这个代码,你会发现现在的聊天机器人一斤封装到了一个类里,同时,在这个版本的机器人里也添加了一些新的功能。

下面我们来看一下这个第五代机器人的新功能


- select_response(): 这个函数从回复的列表里选择一个回复,为了增加程序的随机性增加了一个新的函数,这个函数在调用函数。see_drandom_generator()后会打乱回复的链表
- save_prev_input(): 这个函数在用户下次输入之前把现在的输入储存到一个变量中(m_sPrevInput)。
- void save_prev_response(): 这个函数用来储存上次机器人回复的内容。
- void save_prev_event(): 这个函数把现在的事件(m_sEvent)储存到变量(m_sPrevEvent)中,当程序检测出用户输入空内容或用户不断重复或机器人产生重复的时候,都会触发一个事件。
- void set_event(std::string str):设置当前的事件
- void save_input():备份现在的输入到变量m_sInputBackup中。
- void set_input(std::string str): 储存当前的输入。
- void restore_input():从备份的输入变量(m_sInputBackup)取出变量到现在的变量(m_sInput)中。
- void print_response():输出回复的内容
- void preprocess_input():对输入的内容做一定的处理,例如除去标点符号,出去空格,全部转换到大写。
- bool bot_repeat(): 判定机器人是否要回复相同的内容。
- bool user_repeat(): 判断用户是否重复输入
- bool bot_understand(): 判断机器人能否看懂用户的输入
- bool null_input(): 判断用户现在的输入(m_sInput)是否为空
- bool null_input_repetition(): 判断用户是否重复输入空内容。
- bool user_want_to_quit(): 判断用户是否想要退出对话。
- bool same_event(): 判断当前事件与以前的时间是否相同。
- bool no_response(): 判断机器人是否无法对用户输入作出回复。
- bool same_input():判断当前输入与上一次输入是否相同。
- bool similar_input(): 判断当前的输入和上一次输入是否类似。
- void get_input(): 获得用户输入。
- void respond(): 处理所有的回复,所以这个函数控制了这个程序的行为。
- find_match(): 根据用户的输入找到回复。
- void handle_repetition(): 处理程序产生的重复。
- handle_user_repetition():处理用户不断重复的情况。
- void handle_event(std::string str): 处理普通的事件。

你可以清楚地看到,“chatterbot5”比“chatterbot4”有了更多的函数,并且每一个函数都被封装成类CBot的一个方法。但是这个程序仍然后很多的提升。

Chattebot5引入了“state”的概念,在这个新版本的聊天机器人,几个不同的事件之间可能存在着交叉。例如,当用户输入了空内容,聊天机器人会把他自己置于“NULL INPUT**”状态,如果用户继续输入空内容时,就会进入“REPETITION T1**” 状态。

现在的机器人比以前的机器人使用了更大的数据库,但是这仍然是微不足道的,因为现在的主流聊天机器人的数据库至少10000行甚至更多。所以这也是我们下一个版本的机器人的主要目标。

不管怎样,我们先关注如今机器人的一些问题

当前的问题是什么?

“keyword boundary”问题,假设用户输入了”I think not“,机器人会在数据库中寻找匹配的句子,他可能会找到”Hi“。这是因为因为用户的输入包含了“think”,很明显,这并不是我们希望的行为。

我们应该如何避免

很简单,我们可以在数据库中的关键字的前后都填一个空格,我们可以在函数“”find_match() ”中应用这个变化。

我们还可以添加一些其他的功能么

当然可以,现在的机器人在开始一个对话之前不会说任何的内容。我们可以再机器人的数据库中添加一个新的状态,让他一开始就可以输出一些内容到屏幕上。新的状态可以被称作”SIGNON**”。

关键词排名

在以前的每一版机器人中,我们都添加了一些功能来使它更加的真实。在第七个机器人版本中我们将引入”关键词排名”的方法。当有不止一个的关键词符合用户的输入时,通过关键字词排名从数据库中选出最好的回复。例如,当用户的输入为”what is your name”时,会有两个关键词匹配用户的输入,一个是”WHAT”,另一个是”WAHT IS YOUR NAME”,很明显,后者最匹配。
这种方法将被应用于第七版的机器人中。

等价关键词

前几个版本的数据库仅仅是一个关键词对应回复,但是如果几个意思相近的关键词对应相同的回复,那程序将变得更加高效。例如,”what is your name”与”Can you please tell me your name”有着相同的意思,所以我们就没有必要为他们单独做一个记录,而是将他们合并到一起。我们可以通过改变结构体来实现这个目的。

  1. 首先确保机器人有一些与合适的关键词对应的回复模板。回复模板是一种为聊天机器人产生新的回复骨架。通常我们在回复中使用通配符来表明这是一个模板,在以往的例子中,我们时候”(so, you think that*)”模板来构建回复。在新的程序中,我们可以通过原始的输入的一部分来替换通配符
阅读更多
文章标签: c++
个人分类: C++
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭