本章是课程第一阶段的学习里程碑,第一个大课程练习,是对整个第一阶段学习内容的综合应用和自我检验:我们来实现一个简单的对话机器人。
在我们想编程实现一个什么时,一定要先问下自己:这个东西的实质是什么(What is its nature)?
对话机器人的实质是什么呢?Siri 是个著名的对话机器人,我们可以和她闲聊,也可以问她问题和请她帮忙设个闹钟啊啥的:
对话机器人能够读取我们对它说的话(输入),理解其含义,然后做出合理的回应,然后把回应展示(输出)给我们。对话的主题可以各种各样,以 Siri 为例,主要可以分几类:
为了完成这些对话,Siri 包含了几个关键的部件:
这里面每一个都是人工智能领域的大课题,相对来说 speech recognition 和 TTS 这一对引擎比较成熟一些,虽然还有很多提升空间,比如 Siri 自动合成的语音平滑圆润,却还是缺乏真人的生动感(感情色彩),不过对于 Siri 这样的应用,效果已经足够好了。
对话系统(dialog system)则是人工智能重要领域“自然语言处理(natural language processing, NLP)”中一个主要应用场景,其终极目标是创造能够与人自由对话(free talk)的计算机程序。
自由对话已经被证明是极具挑战的,迄今为止,无论是 Apple 的 Siri,还是微软的 Cortana、微软小冰,或者 Google Now,或者小米的小爱同学,都还只是迈出了第一步,离终极目标还差得很远。不过对于特定场景或者特定目的的对话,这些试验产品都正在“开始变得有用”,其背后的技术进步目前已经开始应用在一些非常重要的系统中,比如大型客服系统。
我们自己要尝试实现一个对话机器人,当然不可能做到像 Siri 那么完整,目前能有一点对话系统的感觉就好,同时我们还希望搭好一个架子,让我们可以不断迭代加强这个系统的能力,可以这么去思考:
为了进一步降低难度,我们可以从程序发起的对话开始,也就是把“程序提问题-用户回应-程序再回应”作为一个基本对话单元,这比由用户自由提出话题要容易。
程序设计不是画图纸,是像小孩玩沙子一样,一边尝试一边思考,在必要时才会画图纸。在做更复杂的思考和设计之前可以先随便的玩一下,看看在 Python 里怎么实现“程序提问题-用户回应-程序再回应”这样的基本单元,结合我们之前学过的知识,我们很快可以写出下面这样的代码:
print("Hi, what is your name?")
name = input()
print(f"Hello {name}!")
Hi, what is your name?
Hello Neo!
是不是已经有点对话的样子了?我们可以再写一个类似的,这次引入一点逻辑判断和条件分支:
print("How are you today?")
feeling = input()
if 'good' in feeling:
print("I'm feeling good too")
else:
print("Sorry to hear that")
How are you today?
Sorry to hear that
在这里用各种输入测试几次,会发现一个小 bug:如果用户输入 good 或者 I'm good 这种,代码运行是如我们所想,但是如果用户输入的是 Good,程序会不认,而输出 Sorry to hear that,这显然不合理,源于我们代码中的不严谨,在判断时未考虑大小写问题。修正也很容易,改成下面这样就好了:
print("How are you today?")
feeling = input()
if 'good' in feeling.lower():
print("I'm feeling good too")
else:
print("Sorry to hear that")
How are you today?
I'm feeling good too
继续,这次我们引入一点随机性来增加乐趣:
import random
print("What's your favorite color?")
favcolor = input()
colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'purple']
print(f"You like {favcolor.lower()}? My favorite color is {random.choice(colors)}")
What's your favorite color?
You like blue? My favorite color is purple
这里用了来自 random
模块中的 choice
函数,从列表 colors
中随机抽选一个元素。
经过这一番尝试,我们可以有一些收获:
print()
一句话,然后用 input()
获取用户输入,然后围绕输入内容进行计算,给出一个相关的输出,并 print()
出来;print()
输出太快了,如果能停顿一下再输出感觉会更像对话。最重要的,我们知道基本的交互模式之后,只要愿意,我们可以重复上面的过程,写出好多好多交互剧本,和用户聊上一整天,但是那样好像很枯燥,而且要写好多重复的代码,所以现在我们可以停下来思考怎么把这个过程变得更方便和高效。
回忆我们在前面几章所讲的一些编程的基本思维,尤其是“分而治之”和“责任分离”,把问题分解,每个子问题用一个程序模块来解决,每个程序模块力争把一个问题解决好,并且给出清晰的接口供其他模块使用。
在对话机器人这个课题中,我们可以建立这么几个抽象模块:
print-input-print
流程,可以在一个公共的 Bot 父类中处理;下面我们就来尝试构建这一设计的代码实现。
先来看公共父类 Bot
:
import time
class Bot:
wait = 1
def __init__(self):
self.q = ''
self.a = ''
def _think(self, s):
return s
def run(self):
time.sleep(Bot.wait)
print(self.q)
self.a = input()
time.sleep(Bot.wait)
print(self._think(self.a))
这个定义中我们可以看到前面尝试的成果已经很好的集成进来:
run()
方法就是一轮交互 print-input-print
流程的实现;time
模块中的 sleep()
方法,这个方法让解释器休息指定的秒数;wait
来管理程序说每句话之前要停顿的秒数;self.q
和 self.a
分别代表这一轮对话中程序提出的问题和用户的回答,是两个字符串;_think()
方法就是以用户输入来计算程序回答的核心算法,在 run()
方法中被调用,这个方法一般来说只在 Bot
类内部使用,外部最好不要直接访问,所以名字前面我们加了下划线 _
。有了这个父类,我们就可以一股脑把前面尝试的几轮对话都实现为它不同的子类:
class HelloBot(Bot):
def __init__(self):
self.q = "Hi, what is your name?"
def _think(self, s):
return f"Hello {s}"
class GreetingBot(Bot):
def __init__(self):
self.q = "How are you today?"
def _think(self, s):
if 'good' in s.lower() or 'fine' in s.lower():
return "I'm feeling good too"
else:
return "Sorry to hear that"
import random
class FavoriteColorBot(Bot):
def __init__(self):
self.q = "What's your favorite color?"
def _think(self, s):
colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'purple']
return f"You like {s.lower()}? My favorite color is {random.choice(colors)}"
可以看到,具体对话逻辑的实现就两件事:
__init__()
方法中设定程序发问的问题是什么;_think()
方法中根据用户输入来计算程序回应的内容。下面我们需要一个主流程把上面的这些 Bot
串起来,这是我们的对话系统的主类定义(我们给它起名叫 Garfield
,你可以用你喜欢的名字):
class Garfield:
def __init__(self, wait=1):
Bot.wait = wait
self.bots = []
def add(self, bot):
self.bots.append(bot)
def _prompt(self, s):
print(s)
print()
def run(self):
self._prompt("This is Garfield dialog system. Let's talk.")
for bot in self.bots:
bot.run()
这里面有点新东西,我们稍微解释一下:
self.bots
中,这是一个列表,就是我们在介绍循环的一章提过的一种数据容器,初始化为空列表 []
;add()
方法来往系统中加入 bot,加入方法是用列表的 append()
方法,如上代码中 add(self, bot)
方法定义;wait
统一设定,在初始化对话系统时通过修改类变量 Bot.wait
的值来实现;run()
方法是对话系统运行主方法,它先打印一行提示,然后开始一个一个的运行我们加入的 bot,即循环调用每个 bot
的 run()
方法。然后我们就可以初始化一个 Garfield
实例来具体运行了:
# 创建一个聊天延时 1s 的对话系统
garfield = Garfield(1)
# 向其中加入我们已经定义好的各个对话 bot 的对象实例
garfield.add(HelloBot())
garfield.add(GreetingBot())
garfield.add(FavoriteColorBot())
# 运行之
garfield.run()
This is Garfield dialog system. Let's talk. Hi, what is your name?
Hello Neo How are you today?
I'm feeling good too What's your favorite color?
You like red? My favorite color is yellow
这样我们的对话系统雏形就运行起来了,还有点像回事吧?
以后我们可以定义更多的 bot,每个代表了一轮不一样的对话,然后加入到 Garfield
的对象实例,就可以运行起来了。
你可以把这一节的代码一起放进一个源代码文件保存起来,然后在命令行用 python garfield.py
来运行一下。
有了上面的第一版之后,我们可以随时“增量式(progressively)”更新和优化这个对话系统,比如:
Bot
父类的 run()
方法;Bot
子类;Garfield
和 Bot
类的 run()
方法来实现。这里我们给出两个增强的例子。第一个是给程序输出的对话加上颜色,以区分用户输入的内容,这个修改只涉及 Bot
父类的 run
方法(这就是责任分离的好处)。
要给 Python print()
输出的文字加上颜色和其他格式,可以使用命令行特定的格式串,自己做起来有点复杂,我们用一个第三方的模块 termcolor
,首先打开你的命令行界面运行 pip install termcolor
,然后修改 Bot
父类定义如下:
from time import sleep
from termcolor import colored
class Bot:
wait = 1
def __init__(self):
self.q = ''
self.a = ''
def _think(self, s):
return s
def _format(self, s):
return colored(s, 'blue')
def run(self):
sleep(Bot.wait)
print(self._format(self.q))
self.a = input()
sleep(Bot.wait)
print(self._format(self._think(self.a)))
我们增加了一个内部方法 _format
来对程序输出的文字进行格式化,在其中调用 termcolor
提供的 colored
函数来对字符串上色。程序的其他部分不需要改变,所有 Bot
子类继承父类自动得到这一新特性,你可以重新运行程序来检验一下。
第二个例子是做一个新的 Bot
子类,可以对用户输入的一个四则运算表达式进行计算求值,也就是把对话系统变成一个计算器,你可以试着自己做一做,提示是:可以借助第三方库来进行表达式求值,比如这个 simpleeval。
下面是我们的参考答案。
首先在命令行界面执行 pip install simpleeval
,然后增加一个 Bot
子类的定义如下:
from simpleeval import simple_eval
class CalcBot(Bot):
def __init__(self):
self.q = "Through recent upgrade I can do calculation now. Input some arithmetic expression to try:"
def _think(self, s):
result = simple_eval(s)
return f"Done. Result = {result}"
然后创建 Garfield
实例试试:
garfield = Garfield(1)
garfield.add(HelloBot())
garfield.add(CalcBot())
garfield.run()
This is Garfield dialog system. Let's talk. Hi, what is your name?
Hello Neo
Through recent upgrade I can do calculation now. Input some arithmetic expression to try:
Done. Result = 1008
是不是比想象的还要简单啊?
课程练习 #1 的目标是在上述成果基础上完成下面两个作业。
作业一:改进最后这个 CalcBot
,让它可以反复执行,用户可以一直输入算术表达式求值,直到用户输入 x
q
exit
或者 quit
为止,才跳到下一个 bot 执行。
提示:这个需求改变了 run()
方法的基本流程,所以需要在 CalcBot
中重新实现一个 run()
方法,覆盖掉父类中 run()
的实现;也可以通过修改父类 Bot
实现,但要注意,不要影响已经完成的功能。
作业二:创建一个自己设计的 bot 并将其整合到整个系统中。
提示:这个新的 bot 应该是 Bot 类的子类;可以用上面示范的方法将其装载到对话系统中(Garfield
或者自行定义的类);这个 bot 的话题应该和已有 bot 都不一样,而且实现机制越独特得分越高。
我们在本章实例中运用的程序设计方法叫做 UDB(Understand, Design and Build):
对于所有要解决问题的探索,这个方法基本都适用,格外适合编程,差别仅在问题的规模(难度和复杂度),你可以多多体会这个方法的思维模式,平时多尝试。
本章还运用了前面学到的几乎所有东西,如果需要可以回到前面重新温习,直到可以顺利的完成本章的各个部分。