Python 机器学习蓝图第二版(三)
原文:
annas-archive.org/md5/da2aba0c7496af58c8b1b0c62d741e1a译者:飞龙
第九章:构建一个聊天机器人
想象一下,假设你正独自坐在一个安静宽敞的房间里。右侧有一张小桌子,上面放着一叠白色打印纸和一支黑色的钢笔。你面前似乎有一个大红色的立方体,顶部有一个小孔——大小略小于邮筒的孔。洞口上方的铭文邀请你写下一个问题并把它穿过孔口。碰巧你会说普通话,于是你在纸张上写下你的问题,并将其放入孔中。几秒钟后,慢慢地,一个回答出现了。它同样是中文写的,正是你可能期待的那种回答。那么,你问了什么?你是人还是计算机? 回答是什么?是的,是的,我是。
这个思想实验基于哲学家约翰·赛尔的“中文房间”论证。实验的前提是,如果房间里有一个不会说中文的人,但他有一套规则,能够将英文字符完美地映射到中文字符上,那么他可能看起来像是理解中文,尽管他实际上并不懂中文。赛尔的论点是,产生可理解输出的算法程序不能说自己“理解”这一输出。它们缺乏意识。他的思想实验旨在反对强人工智能的观点,或认为人类大脑本质上只是一种湿机器的观点。赛尔不认为无论 AI 的行为看起来多么复杂,都能被称为拥有意识。
赛尔于 1980 年发布了这个实验。31 年后,Siri 将在 iPhone 4S 上发布。对于使用过 Siri 的人来说,显然我们还有很长的路要走,才可能面临是否我们正在与一个具有意识的代理交流的不确定性(尽管我们可能会对那些已知为人类的人的意识产生怀疑)。尽管这些代理,或聊天机器人,过去表现得笨拙,但该领域正在迅速发展。
在本章中,我们将学习如何从零开始构建一个聊天机器人。在这个过程中,我们还会了解该领域的历史及其未来前景。
本章将涵盖以下主题:
-
图灵测试
-
聊天机器人的历史
-
聊天机器人设计
-
构建聊天机器人
图灵测试
在塞尔的中文房间理论提出的 30 年前,艾伦·图灵在他的一篇较为著名的论文中提出了问题,机器能思考吗? 作为一位实践天才,他决定不是直接面对这个问题,而是将其置于他人心智问题的框架内。这个问题是:我们如何才能真正知道其他人有像我们一样的心智? 由于我们只能观察他们的行为,而无法看到他们内心的工作机制,我们必须信任他们像我们一样。图灵提出,如果一台机器能够表现得像智能一样,那么我们应该把它看作是智能的。这,简而言之,就是图灵测试。通过测试意味着让人类相信这台机器是一个人类。
你现在可能在想,有程序成功通过过图灵测试吗? 答案是,实际上并没有官方的图灵测试。最接近官方测试的是洛布纳奖。这是一个每年举办的比赛,奖励那些在评委眼中最能表现得像人的聊天机器人。如果这听起来很严谨和学术,实际上并非如此。传闻洛布纳本人是个相当有个性的人,经常公开表达自己对葡萄酒、大麻和妓女的喜爱。据说,他资助这项比赛完全是因为他对任何形式的劳动都抱有极大的厌恶。
希望到这个时候,你已经意识到,关于通过图灵测试的任何说法,更多的是一种表演,而非真正的科学。尽管如此,图灵的原始概念依然是一个有用的构建。
聊天机器人的历史
我依然记得那天,我父亲让我坐在他的 IBM XT 前,那台黑绿相间、屏幕闪烁的显示器前,叫我与它对话。我们的对话大概是这样的:
你好,今天感觉怎么样?
嗨,这是谁?
嗨,今天怎么样?
我很好。你没告诉我你是谁。
我们应该讨论的是你,而不是我。
我们应该?为什么?
我们换个话题吧... 说说你的家人。
你简直无法进行对话。
你能详细说明一下吗?
你快把我搞死了。
也许你希望我在杀了你?
好吧,你让我有点害怕。
也许你真的是在说你自己?
什么??这毫无意义。
这个回答能对你有帮助吗?
结束
非常有趣。
退出
谢谢你和我交谈。
#####@@....
我所说的那个 it 是一个名为 ELIZA 的程序。ELIZA 是由约瑟夫·韦伊泽恩鲍姆(Joseph Weizenbaum)在 1960 年代中期编写于麻省理工学院人工智能实验室的,旨在模仿罗杰斯式心理治疗师的回应。尽管在深入研究时几乎显得滑稽,但这个程序能够让一些用户相信他们正在与真正的人类交谈,这是一个了不起的成就,考虑到它仅仅是使用随机化和正则表达式来模仿回复的 200 行代码。即使在今天,这个简单的程序仍然是流行文化的重要组成部分。如果你问 Siri 谁是 ELIZA,她会告诉你她是你的朋友和一位杰出的心理医生。
如果 ELIZA 是聊天机器人的早期示例,那么自那时以来我们看到了什么?近年来,新型聊天机器人如雨后春笋般涌现,其中最引人注目的是 Cleverbot。
Cleverbot 在 1997 年通过网络发布到世界上。多年来,该机器人已累积了数亿次对话,与早期的聊天机器人不同,正如其名称所示,Cleverbot 似乎随着每次对话变得更加智能。尽管其算法的确切细节难以找到,据说它通过记录所有对话并在数据库中查找最相似的问题和回答来工作,以找到最合适的回应。
我编造了一个无意义的问题,如下所示,你可以看到它在字符串匹配方面找到了与我的问题对象相似的内容:
我坚持说:
而且,我又得到了类似的东西...
你还会注意到,话题可以在对话中持续存在。作为回应,我被要求详细阐述并证明我的答案。这似乎是使 Cleverbot 变得聪明的其中一点。
尽管能从人类那里学到东西的聊天机器人可能相当有趣,但它们也可能有更黑暗的一面。
几年前,微软在 Twitter 上发布了一个名为 Tay 的聊天机器人。人们被邀请向 Tay 提问,而 Tay 则会根据其 个性 回应。微软显然将该机器人编程为看起来像一个 19 岁的美国女孩。她旨在成为你的虚拟 闺蜜;唯一的问题是,她开始发布极端种族主义言论。
由于这些令人难以置信的煽动性推文,微软被迫将 Tay 从 Twitter 下线,并发布了道歉声明。
"正如你们许多人现在所知道的,我们在周三推出了一个名为 Tay 的聊天机器人。我们对 Tay 不经意的冒犯性和伤人的推文深表歉意,这些推文不代表我们是谁或我们的立场,也不代表我们设计 Tay 的方式。Tay 现在已下线,只有当我们有信心能更好地预测与我们原则和价值观相冲突的恶意意图时,我们才会考虑重新启动 Tay。"
-2016 年 3 月 25 日 官方微软博客
很明显,未来那些希望将聊天机器人投入市场的品牌应该从这次的失败中吸取教训,并计划好让用户尝试操控它们,展示人类最糟糕的行为。
毋庸置疑,品牌们正在拥抱聊天机器人。从 Facebook 到 Taco Bell,每个品牌都在加入这场游戏。
见证 TacoBot:
是的,它真的是个现实存在的东西。尽管像 Tay 这样的失败让人跌倒,但未来的用户界面很可能会像 TacoBot 那样。最后的一个例子甚至可能帮助解释其中的原因。
Quartz 最近推出了一款将新闻转化为对话的应用。与其将当天的新闻按平铺方式展示,它让你参与一场对话,就像是从朋友那里获取新闻一样:
Twitter 的项目经理 David Gasca 在 Medium 上发布了一篇文章,描述了他使用该应用的体验。他讲述了这种对话式的设计如何唤起通常只在人与人关系中才会触发的情感:
“与简单的展示广告不同,在与我的应用建立对话关系时,我感觉自己欠它什么:我想要点击。在最潜意识的层面,我感到需要回报,不想让应用失望:‘应用给了我这个内容。到目前为止非常好,我很喜欢这些 GIF。我应该点击一下,因为它很有礼貌地请求了。’”
如果这种体验是普遍的——我相信是——这可能会成为广告的下一个大趋势,我毫不怀疑广告利润将推动用户界面设计的发展:
“机器人越像人类,就越会被当作人类对待。”
-Mat Webb,技术专家,Mind Hacks 的合著者
到这时,你可能迫不及待地想知道这些东西是如何工作的,那我们就继续吧!
聊天机器人的设计
原始的 ELIZA 应用程序大约是 200 行代码。Python 的 NLTK 实现也同样简短。以下是 NLTK 网站上的一段摘录(www.nltk.org/_modules/nltk/chat/eliza.html):
从代码中可以看到,输入文本首先被解析,然后与一系列正则表达式进行匹配。一旦输入匹配成功,系统会返回一个随机的回应(有时会回响部分输入内容)。所以,像 我需要一个塔可 这样的输入会触发一个回应:你真的需要一个塔可吗? 显然,答案是“是的”,而且幸运的是,我们已经发展到技术可以提供它(感谢你,TacoBot),但那时仍是初期阶段。令人震惊的是,有些人真的相信 ELIZA 是一个真实的人类。
那么更先进的机器人呢?它们是如何构建的?
令人惊讶的是,大多数你可能遇到的聊天机器人甚至没有使用机器学习(ML);它们被称为基于检索的模型。这意味着回答是根据问题和上下文预先定义的。这些机器人的最常见架构是被称为人工智能标记语言(AIML)的东西。AIML 是一种基于 XML 的模式,用于表示机器人在接收到用户输入时应如何互动。它其实就是 ELIZA 工作方式的更高级版本。
让我们看看如何使用 AIML 生成回应。首先,所有输入都会被预处理以进行标准化。这意味着当你输入Waaazzup???时,它会被映射为WHAT IS UP。这个预处理步骤将表达相同意思的多种说法归纳为一个输入,这个输入可以通过一个规则进行匹配。在这一过程中,标点符号和其他无关的输入也会被去除。一旦完成这些处理,输入就会与相应的规则进行匹配。以下是一个样例模板:
<category>
<pattern>WHAT IS UP</pattern>
<template>The sky, duh. Pfft. Humans...</template>
</category>
这就是基本设置,但你也可以在其中加入通配符、随机化和优先级方案。例如,以下模式使用了通配符匹配:
<category>
<pattern>* FOR ME<pattern>
<template>I'm a bot. I don't <star/>. Ever.</template>
</category>
在这里,*通配符匹配FOR ME之前的一个或多个单词,然后在输出模板中重复这些单词。如果用户输入的是为我跳舞!,则回复将是我是一个机器人。我不跳舞。永远不。
正如你所看到的,这些规则并不能产生任何接近真实智能的东西,但有一些技巧可以增强这种错觉。其中一个更好的技巧是能够生成基于话题的回应。
例如,这里有一个引发话题的规则:
<category>
<pattern>I LIKE TURTLES</pattern>
<template>I feel like this whole <set name="topic">turtle</set> thing could be a problem. What do you like about them? </template>
</category>
一旦话题确定,特定于该上下文的规则可以进行匹配:
<topic name="turtles">
<category>
<pattern>* SHELL IS *</pattern>
<template>I dislike turtles primarily because of their shells. What other creepy things do you like about turtles? </template>
</category>
<category>
<pattern>* HIDE *</pattern>
<template>I wish, like a turtle, that I could hide from this conversation. </template>
</category>
</topic>
让我们看看这种互动可能是什么样子的:
我喜欢乌龟!
我觉得这整个乌龟的事儿可能会是个问题。你喜欢它们什么呢?
我喜欢它们如何躲进壳里。
我希望,像乌龟一样,我能躲避这场对话。
你可以看到,贯穿整个对话的连贯性增加了一定的现实感。
你可能在想,在这个深度学习的时代,这种技术不可能是最先进的,没错。虽然大多数聊天机器人是基于规则的,但下一代聊天机器人正在崭露头角,它们基于神经网络。
在 2015 年,谷歌的 Oriol Vinyas 和 Quoc Le 发表了一篇论文,arxiv.org/pdf/1506.05869v1.pdf,描述了基于序列到序列模型构建神经网络的过程。这种类型的模型将输入序列(如ABC)映射到输出序列(如XYZ)。这些输入和输出可能是不同语言之间的翻译。例如,在他们的研究中,训练数据并不是语言翻译,而是技术支持记录和电影对话。尽管这两个模型的结果都很有趣,但基于电影模型的互动却成为了头条新闻。
以下是论文中的一些示例互动:
这些内容没有被人类明确编码,也不在训练集里,如问题所要求的。然而,看着这些,感觉像是在和一个人对话,真让人不寒而栗。接下来我们来看看更多内容:
注意,模型正在响应看起来像是性别(他,她)、地点(英格兰)和职业(运动员)的知识。即使是关于意义、伦理和道德的问题也是可以探讨的:
如果这个对话记录没有让你感到一丝寒意,那你很可能已经是某种人工智能了。
我强烈推荐通读整篇论文。它并不太技术性,但肯定会让你看到这项技术的未来发展方向。
我们已经讨论了很多关于聊天机器人的历史、类型和设计,但现在我们来开始构建我们自己的聊天机器人。我们将采用两种方法。第一种将使用我们之前看到的余弦相似度技术,第二种将利用序列到序列学习。
构建聊天机器人
现在,既然已经看到聊天机器人的潜力,你可能想要构建最好的、最先进的、类似 Google 级别的机器人,对吧?好吧,先把这个想法放在一边,因为我们现在将从做完全相反的事情开始。我们将构建一个最糟糕、最糟糕的机器人!
这听起来可能让人失望,但如果你的目标只是构建一些非常酷且吸引人的东西(而且不需要花费数小时来构建),这是一个很好的起点。
我们将利用从 Cleverbot 的真实对话中获取的训练数据。这些数据是从notsocleverbot.jimrule.com收集的。这个网站非常适合,因为它收录了人们与 Cleverbot 进行的最荒谬的对话。
让我们来看一下 Cleverbot 与用户之间的示例对话:
虽然你可以自由使用我们在前几章中介绍的网页抓取技术来收集数据,但你也可以在本章的 GitHub 仓库中找到一个.csv格式的数据。
我们将再次从 Jupyter Notebook 开始。我们将加载、解析并检查数据。首先,我们将导入 pandas 库和 Python 的正则表达式库re。我们还将设置 pandas 的选项,扩大列宽,以便更好地查看数据:
import pandas as pd
import re
pd.set_option('display.max_colwidth',200)
现在,我们将加载我们的数据:
df = pd.read_csv('nscb.csv')
df.head()
上面的代码会产生以下输出:
由于我们只对第一列——对话数据感兴趣,因此我们将只解析这一列:
convo = df.iloc[:,0]
convo
上面的代码会产生以下输出:
你应该能看出我们有用户和Cleverbot之间的互动,且任一方都可以发起对话。为了获得我们所需的格式,我们必须将数据解析为问答对。我们不一定关注谁说了什么,而是关注如何将每个回答与每个问题匹配。稍后你会明白为什么。现在,让我们对文本进行一些正则表达式的魔法处理:
clist = []
def qa_pairs(x):
cpairs = re.findall(": (.*?)(?:$|\n)", x)
clist.extend(list(zip(cpairs, cpairs[1:])))
convo.map(qa_pairs);
convo_frame = pd.Series(dict(clist)).to_frame().reset_index()
convo_frame.columns = ['q', 'a']
上述代码生成了以下输出:
好的,这里有很多代码。刚才发生了什么?我们首先创建了一个列表来存储问题和回答的元组。然后我们通过一个函数将我们的对话拆分成这些对,使用了正则表达式。
最后,我们将所有这些放入一个 pandas DataFrame 中,列标为 q 和 a。
接下来我们将应用一些算法魔法,来匹配与用户输入问题最相似的问题:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
vectorizer = TfidfVectorizer(ngram_range=(1,3))
vec = vectorizer.fit_transform(convo_frame['q'])
在前面的代码中,我们导入了 tf-idf 向量化库和余弦相似度库。然后我们使用训练数据创建了一个 tf-idf 矩阵。现在我们可以利用这个矩阵来转换我们自己的新问题,并衡量它们与训练集中现有问题的相似度。现在就让我们做这个:
my_q = vectorizer.transform(['Hi. My name is Alex.'])
cs = cosine_similarity(my_q, vec)
rs = pd.Series(cs[0]).sort_values(ascending=False)
top5 = rs.iloc[0:5]
top5
上述代码生成了以下输出:
我们在这里看到了什么?这是我提出的问题与最相似的五个问题之间的余弦相似度。在左侧是索引,右侧是余弦相似度。让我们来看一下这些:
convo_frame.iloc[top5.index]['q']
这会产生以下输出:
正如你所看到的,没有完全相同的,但确实有一些相似之处。
现在让我们来看看这个回答:
rsi = rs.index[0]
rsi
convo_frame.iloc[rsi]['a']
上述代码生成了以下输出:
好的,我们的机器人似乎已经有了个性。让我们更进一步。
我们将创建一个方便的函数,这样我们就能轻松地测试多个陈述:
def get_response(q):
my_q = vectorizer.transform([q])
cs = cosine_similarity(my_q, vec)
rs = pd.Series(cs[0]).sort_values(ascending=False)
rsi = rs.index[0]
return convo_frame.iloc[rsi]['a']
get_response('Yes, I am clearly more clever than you will ever be!')
这会产生以下输出:
我们显然已经创造了一个怪物,所以我们会继续:
get_response('You are a stupid machine. Why must I prove anything to
you?')
这会产生以下输出:
我很享受这个过程。让我们继续:
get_response('Did you eat tacos?')
get_response('With beans on top?')
get_response('What else do you like to do?')
get_response('What do you like about it?')
get_response('Me, random?')
get_response('I think you mean you\'re')
令人惊讶的是,这可能是我一段时间以来经历过的最棒的对话之一,无论是机器人还是其他。
尽管这是一个有趣的小项目,但现在让我们进入一个更高级的建模技术:序列到序列建模。
聊天机器人序列到序列建模
对于接下来的任务,我们将利用在第八章中讨论的几个库,使用卷积神经网络对图像进行分类,TensorFlow 和 Keras。如果你还没有安装它们,可以通过 pip 安装。
我们还将使用本章前面讨论的那种高级建模方法;它是一种深度学习方法,叫做序列到序列建模。这种方法常用于机器翻译和问答应用,因为它可以将任何长度的输入序列映射到任何长度的输出序列:
François Chollet 在 Keras 博客中有一个很好的关于这种模型的介绍:blog.keras.io/a-ten-minute-introduction-to-sequence-to-sequence-learning-in-keras.html。值得一读。
我们将大量使用他的示例代码来构建我们的模型。尽管他的示例使用的是机器翻译(英语到法语),但我们将重新利用它来进行问答,并使用我们的 Cleverbot 数据集:
- 设置导入项:
from keras.models import Model
from keras.layers import Input, LSTM, Dense
import numpy as np
- 设置训练参数:
batch_size = 64 # Batch size for training.
epochs = 100 # Number of epochs to train for.
latent_dim = 256 # Latent dimensionality of the encoding space.
num_samples = 1000 # Number of samples to train on.
我们将从这些开始。我们可以检查模型的成功,并根据需要进行调整。
数据处理的第一步是将数据获取到正确的格式,然后进行向量化。我们将一步步进行:
input_texts = []
target_texts = []
input_characters = set()
target_characters = set()
这将为我们的提问和回答(目标)创建列表,并为我们的问题和答案中的单个字符创建集合。该模型实际上是通过一次生成一个字符来工作的:
- 让我们将问题和回答对的字符数限制为 50 个或更少。这将有助于加速训练:
convo_frame['q len'] = convo_frame['q'].astype('str').apply(lambda
x: len(x))
convo_frame['a len'] = convo_frame['a'].astype('str').apply(lambda
x: len(x))
convo_frame = convo_frame[(convo_frame['q len'] < 50)&
(convo_frame['a len'] < 50)]
- 让我们设置输入文本和目标文本列表:
input_texts = list(convo_frame['q'].astype('str'))
target_texts = list(convo_frame['a'].map(lambda x: '\t' + x +
'\n').astype('str'))
上述代码将数据格式化为正确的格式。请注意,我们向目标文本中添加了制表符(\t)和换行符(\n)。这些将作为解码器的开始和停止标记。
- 让我们看看输入文本和目标文本:
input_texts
上述代码生成了以下输出:
target_texts
上述代码生成了以下输出:
现在让我们看看这些输入和目标字符集:
input_characters
上述代码生成了以下输出:
target_characters
上述代码生成了以下输出:
接下来,我们将对传入模型的数据做一些额外的准备。尽管数据可以是任意长度并且返回的长度也可以是任意的,但我们需要将数据填充到最大长度,以便模型可以正常工作:
input_characters = sorted(list(input_characters))
target_characters = sorted(list(target_characters))
num_encoder_tokens = len(input_characters)
num_decoder_tokens = len(target_characters)
max_encoder_seq_length = max([len(txt) for txt in input_texts])
max_decoder_seq_length = max([len(txt) for txt in target_texts])
print('Number of samples:', len(input_texts))
print('Number of unique input tokens:', num_encoder_tokens)
print('Number of unique output tokens:', num_decoder_tokens)
print('Max sequence length for inputs:', max_encoder_seq_length)
print('Max sequence length for outputs:', max_decoder_seq_length)
上述代码生成了以下输出:
接下来,我们将使用独热编码对数据进行向量化:
input_token_index = dict(
[(char, i) for i, char in enumerate(input_characters)])
target_token_index = dict(
[(char, i) for i, char in enumerate(target_characters)])
encoder_input_data = np.zeros(
(len(input_texts), max_encoder_seq_length, num_encoder_tokens),
dtype='float32')
decoder_input_data = np.zeros(
(len(input_texts), max_decoder_seq_length, num_decoder_tokens),
dtype='float32')
decoder_target_data = np.zeros(
(len(input_texts), max_decoder_seq_length, num_decoder_tokens),
dtype='float32')
for i, (input_text, target_text) in enumerate(zip(input_texts, target_texts)):
for t, char in enumerate(input_text):
encoder_input_data[i, t, input_token_index[char]] = 1\.
for t, char in enumerate(target_text):
# decoder_target_data is ahead of decoder_input_data by one
# timestep
decoder_input_data[i, t, target_token_index[char]] = 1\.
if t > 0:
# decoder_target_data will be ahead by one timestep
# and will not include the start character.
decoder_target_data[i, t - 1, target_token_index[char]] =
1\.
让我们来看一下其中一个向量:
Decoder_input_data
上面的代码生成了以下输出:
从上图中,你会注意到我们有一个对字符数据进行独热编码的向量,这将在我们的模型中使用。
现在我们设置好我们的序列到序列模型的编码器和解码器 LSTM:
# Define an input sequence and process it.
encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]
# Set up the decoder, using `encoder_states` as initial state.
decoder_inputs = Input(shape=(None, num_decoder_tokens))
# We set up our decoder to return full output sequences,
# and to return internal states as well. We don't use the
# return states in the training model, but we will use them in
# inference.
decoder_lstm = LSTM(latent_dim, return_sequences=True,
return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs,
initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
然后我们继续讲解模型本身:
# Define the model that will turn
# `encoder_input_data` & `decoder_input_data` into `decoder_target_data`
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
# Run training
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data],
decoder_target_data,
batch_size=batch_size,
epochs=epochs,
validation_split=0.2)
# Save model
model.save('s2s.h5')
在上面的代码中,我们使用编码器和解码器的输入以及解码器的输出来定义模型。然后我们编译它,训练它,并保存它。
我们将模型设置为使用 1,000 个样本。在这里,我们还将数据按 80/20 的比例分为训练集和验证集。我们还将训练周期设为 100,因此它将运行 100 个周期。在一台标准的 MacBook Pro 上,这大约需要一个小时才能完成。
一旦该单元运行,以下输出将被生成:
下一步是我们的推理步骤。我们将使用这个模型生成的状态作为输入,传递给下一个模型以生成我们的输出:
# Next: inference mode (sampling).
# Here's the drill:
# 1) encode input and retrieve initial decoder state
# 2) run one step of decoder with this initial state
# and a "start of sequence" token as target.
# Output will be the next target token
# 3) Repeat with the current target token and current states
# Define sampling models
encoder_model = Model(encoder_inputs, encoder_states)
decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
[decoder_inputs] + decoder_states_inputs,
[decoder_outputs] + decoder_states)
# Reverse-lookup token index to decode sequences back to
# something readable.
reverse_input_char_index = dict(
(i, char) for char, i in input_token_index.items())
reverse_target_char_index = dict(
(i, char) for char, i in target_token_index.items())
def decode_sequence(input_seq):
# Encode the input as state vectors.
states_value = encoder_model.predict(input_seq)
# Generate empty target sequence of length 1\.
target_seq = np.zeros((1, 1, num_decoder_tokens))
# Populate the first character of target sequence with the start character.
target_seq[0, 0, target_token_index['\t']] = 1\.
# Sampling loop for a batch of sequences
# (to simplify, here we assume a batch of size 1).
stop_condition = False
decoded_sentence = ''
while not stop_condition:
output_tokens, h, c = decoder_model.predict(
[target_seq] + states_value)
# Sample a token
sampled_token_index = np.argmax(output_tokens[0, -1, :])
sampled_char = reverse_target_char_index[sampled_token_index]
decoded_sentence += sampled_char
# Exit condition: either hit max length
# or find stop character.
if (sampled_char == '\n' or
len(decoded_sentence) > max_decoder_seq_length):
stop_condition = True
# Update the target sequence (of length 1).
target_seq = np.zeros((1, 1, num_decoder_tokens))
target_seq[0, 0, sampled_token_index] = 1\.
# Update states
states_value = [h, c]
return decoded_sentence
for seq_index in range(100):
# Take one sequence (part of the training set)
# for trying out decoding.
input_seq = encoder_input_data[seq_index: seq_index + 1]
decoded_sentence = decode_sequence(input_seq)
print('-')
print('Input sentence:', input_texts[seq_index])
print('Decoded sentence:', decoded_sentence)
上面的代码生成了以下输出:
如你所见,我们模型的结果相当重复。但是我们仅使用了 1,000 个样本,并且响应是一个字符一个字符生成的,所以这实际上已经相当令人印象深刻。
如果你想获得更好的结果,可以使用更多样本数据和更多训练周期重新运行模型。
在这里,我提供了一些我从更长时间的训练中记录下来的较为幽默的输出:
摘要
在本章中,我们对聊天机器人领域进行了全面的探索。很明显,我们正处于这类应用程序爆炸性增长的前夕。对话式用户界面的革命即将开始。希望本章能激励你创建自己的聊天机器人,如果没有,也希望你对这些应用的工作原理及其如何塑造我们的未来有了更丰富的理解。
我会让应用程序说出最后的结论:
get_response("This is the end, Cleverbot. Say goodbye.")
第十章:构建推荐引擎
就像许多事情一样,它源于挫败感和烈酒。那是一个星期六,两个年轻人再次陷入了没有约会的困境。当他们坐下来倒酒、分享苦恼时,这两个哈佛大学的新生开始构思一个想法。如果他们不再依赖随机机会来遇到合适的女孩,而是能利用计算机算法呢?
他们认为,匹配人们的关键是创建一组问题,提供每个人在第一次尴尬约会时真正想了解的信息。通过使用这些问卷来匹配人们,你可以消除那些最好避免的约会。这个过程将是超级高效的。
这个想法是将他们的新服务推向波士顿及全国各地的大学生。简而言之,他们确实做到了这一点。
不久之后,他们所构建的数字匹配服务获得了巨大的成功。它吸引了全国媒体的关注,并在接下来的几年里生成了数万次匹配。事实上,这家公司如此成功,以至于最终被一家更大的公司收购,该公司希望利用其技术。
如果你认为我在谈论OkCupid,那你就错了——而且错得有点远,大约错了 40 年。我说的这家公司从 1965 年开始就做了这些事——那时候,匹配计算是通过 IBM 1401 主机上的穿孔卡来完成的。完成计算甚至需要三天时间。
但奇怪的是,OkCupid和它 1965 年的前身兼容性研究公司(Compatibility Research, Inc.)之间有一种联系。兼容性研究的共同创始人是杰夫·塔尔(Jeff Tarr),他的女儿詹妮弗·塔尔(Jennifer Tarr)是OkCupid共同创始人克里斯·科因(Chris Coyne)的妻子。真是个小世界。
那么,为什么这一切和构建推荐引擎的章节有关系呢?因为很可能这实际上是第一个推荐引擎。而尽管大多数人通常把推荐引擎视为用来寻找相关产品、音乐和电影的工具,这些是人们可能会喜欢的,但最初的版本是用来寻找潜在的伴侣的。作为思考这些系统如何工作的模型,它提供了一个很好的参考框架。
本章我们将探索推荐系统的不同种类。我们将看到它们是如何商业化实施的,以及它们是如何运作的。最后,我们将实现自己的推荐引擎,用来查找 GitHub 上的仓库。
本章将涵盖以下主题:
-
协同过滤
-
基于内容的过滤
-
混合系统
-
构建推荐引擎
协同过滤
2012 年初,一则新闻报道了一个男人的故事,他来到明尼阿波利斯的 Target 商店投诉送到他家中的一本优惠券书。实际上,他对这些优惠券非常生气,这些优惠券是寄给他女儿的,而她当时是一名高中生。虽然这看起来像是对一项潜在的省钱机会的奇怪反应,但得知这些优惠券只针对产前维生素、尿布、婴儿配方奶粉、婴儿床等产品时,可能会改变你的看法。
经理在听到投诉后,深感抱歉。事实上,他感到十分难过,以至于几天后他再次打电话进行跟进并解释事情是如何发生的。但在经理还没开始道歉之前,父亲开始向经理道歉。事实证明,他的女儿实际上已经怀孕了,而且她的购物习惯暴露了这一点。
揭露她的算法很可能至少部分基于一种在推荐引擎中使用的算法,叫做协同过滤。
那么,什么是协同过滤?
协同过滤基于这样一个理念:在世界的某个地方,你有一个品味的双胞胎——一个与自己在评价星际大战的好坏以及真爱至上的糟糕程度上有相同看法的人。
其核心理念是,你对一组物品的评分方式非常类似于另一个人——这个双胞胎——对这些物品的评分方式,但你们每个人又分别对其他物品进行了评分,而这些物品对方并没有评分。由于你们的品味相似,推荐可以基于你们的双胞胎对某些你没有评分的高分物品,或者基于你对某些他没有评分的高分物品进行生成。从某种意义上讲,这就像是数字化的配对,但结果是你会喜欢的歌曲或产品,而不是实际的人。
因此,在我们怀孕的高中女生的例子中,当她购买了正确的无香料润肤霜、棉花球和维生素补充剂组合时,她很可能与那些后来购买婴儿床和尿布的人配对了。
让我们通过一个例子来看看这在实际中是如何运作的。
我们将从所谓的效用矩阵开始。这与词-文档矩阵类似,但我们将代表的是产品和用户,而不是词汇和文档。
在这里,我们假设我们有客户A-D,以及一组他们根据 0 到 5 的评分标准对产品的评价:
| 客户 | 斯纳奇薯片 | 顺滑润肤霜 | 达夫啤酒 | 更佳水 | XX 大型生活足球衫 | 雪白棉花
尿布 | 迪斯波索尿布 |
| A | 4 | 5 | 3 | 5 | |||
|---|---|---|---|---|---|---|---|
| B | 4 | 4 | 5 | ||||
| C | 2 | 2 | 1 | ||||
| D | 5 | 3 | 5 | 4 |
我们之前看到,当我们想要找到相似的项目时,可以使用余弦相似度。我们就在这里尝试一下。我们将找到最像用户 A 的用户。由于我们有一个稀疏向量,其中包含许多未评分的项目,我们需要为这些缺失值输入一些内容。我们这里就使用 0。我们从比较用户 A 和用户 B 开始:
from sklearn.metrics.pairwise import cosine_similarity
cosine_similarity(np.array([4,0,5,3,5,0,0]).reshape(1,-1),\
np.array([0,4,0,4,0,5,0]).reshape(1,-1))
上述代码的结果是以下输出:
如你所见,两者的相似度评分并不高,这也有道理,因为他们没有共同的评分项。
现在我们来看一下用户 C 与用户 A 进行比较:
cosine_similarity(np.array([4,0,5,3,5,0,0]).reshape(1,-1),\
np.array([2,0,2,0,1,0,0]).reshape(1,-1))
上述代码的结果是以下输出:
在这里,我们看到他们之间有很高的相似度评分(记住,1 是完美相似),尽管他们对相同的产品给出了截然不同的评分。为什么会得到这样的结果呢?问题出在我们选择使用 0 来表示未评分的产品。它被视为这些未评分产品的强烈(负向)一致性。0 在这种情况下并不是中立的。
那么,我们该如何解决这个问题呢?
我们可以做的,不仅仅是对缺失值使用 0,而是重新调整每个用户的评分,使得平均评分为 0,或者说是中立的。我们通过将每个用户的评分减去该用户所有评分的平均值来实现这一点。例如,对于用户 A,其平均值为 17/4,即 4.25。然后,我们从用户 A 提供的每个评分中减去这个平均值。
完成这一步后,我们继续为每个用户计算平均值,并将其从每个用户的评分中减去,直到所有用户都被处理完。
这个过程将生成如下表格。你会注意到每个用户的行总和为 0(这里忽略四舍五入问题):
| 顾客 | Snarky's 土豆片 | SoSo 顺滑 乳液 | Duffly 啤酒 | BetterTap 水 | XXLargeLivin' 橄榄球球衣 | Snowy 棉花
Balls | Disposos' 尿布 |
| A | -.25 | .75 | -1.25 | .75 | |||
|---|---|---|---|---|---|---|---|
| B | -.33 | -.33 | .66 | ||||
| C | .33 | .33 | -.66 | ||||
| D | .75 | -1.25 | .75 | -.25 |
现在让我们在我们重新调整过的数据上尝试余弦相似度。我们将再次进行用户 A 与用户 B 和 C 的比较。
首先,让我们将用户 A 与用户 B 进行比较:
cosine_similarity(np.array([-.25,0,.75,-1.25,.75,0,0])\
.reshape(1,-1),\
np.array([0,-.33,0,-.33,0,.66,0])\
.reshape(1,-1))
上述代码的结果是以下输出:
现在让我们尝试将用户 A 和 C 进行比较:
cosine_similarity(np.array([-.25,0,.75,-1.25,.75,0,0])\
.reshape(1,-1),\
np.array([.33,0,.33,0,-.66,0,0])\
.reshape(1,-1))
上述代码的结果是以下输出:
我们可以看到,A 和 B 之间的相似度略有增加,而 A 和 C 之间的相似度则大幅下降。这正是我们希望看到的结果。
这个中心化过程,除了帮助我们处理缺失值外,还具有一个副作用,帮助我们处理评分严格或宽松的用户,因为现在每个人的评分都是围绕 0 进行中心化的。你可能会注意到,这个公式等同于皮尔逊相关系数,就像那个系数一样,数值范围在-1到1之间。
预测产品评分
现在,让我们利用这个框架来预测某个产品的评分。我们将示例限定为三位用户,用户X、用户Y和用户Z。我们将预测用户X未评分的产品,但用户Y和用户Z已经对其进行了评分,而且他们与X非常相似。
我们将从每个用户的基础评分开始,如下表所示:
| 客户 | Snarky's Potato Chips | SoSo Smooth Lotion | Duffly Beer | BetterTap Water | XXLargeLivin' Football Jersey | Snowy Cotton
球 | Disposos' Diapers |
| X | 4 | 3 | 4 | ||||
|---|---|---|---|---|---|---|---|
| Y | 3.5 | 2.5 | 4 | 4 | |||
| Z | 4 | 3.5 | 4.5 | 4.5 |
接下来,我们将对评分进行中心化处理:
| 客户 | Snarky's Potato Chips | SoSo Smooth Lotion | Duffly Beer | BetterTap Water | XXLargeLivin' Football Jersey | Snowy Cotton
球 | Disposos' Diapers |
| X | .33 | -.66 | .33 | ? | |||
|---|---|---|---|---|---|---|---|
| Y | 0 | -1 | .5 | .5 | |||
| Z | -.125 | -.625 | .375 | .375 |
现在,我们想知道用户X可能给Disposos' Diapers的评分是多少。通过使用用户Y和用户Z的评分,我们可以根据他们的中心化余弦相似度计算加权平均值来得到这个评分。
我们先计算出这个数值:
user_x = [0,.33,0,-.66,0,33,0]
user_y = [0,0,0,-1,0,.5,.5]
cosine_similarity(np.array(user_x).reshape(1,-1),\
np.array(user_y).reshape(1,-1))
前面的代码产生了以下输出:
现在,让我们计算用户Z的数值:
user_x = [0,.33,0,-.66,0,33,0]
user_z = [0,-.125,0,-.625,0,.375,.375]
cosine_similarity(np.array(user_x).reshape(1,-1),\
np.array(user_z).reshape(1,-1))
前面的代码产生了以下输出:
所以,现在我们已经得到了用户X与用户Y(0.42447212)和用户Z(0.46571861)之间的相似度值。
将所有内容整合在一起,我们通过每个用户与X的相似度加权评分,然后再按总相似度进行除法,如下所示:
(.42447212 * (4) + .46571861 * (4.5) ) / (.42447212 + .46571861) = 4.26
我们可以看到,用户X对Disposos' Diapers的预期评分是 4.26(最好发送一个优惠券!)
目前为止,我们只看了用户对用户的协同过滤方法,但还有一种方法可以使用。在实际应用中,这种方法优于用户对用户的过滤;它被称为项目对项目过滤。该方法的工作原理是:与其基于过去的评分将每个用户与其他相似的用户进行匹配,不如将每个已评分的项目与所有其他项目进行比较,以找到最相似的项目,再次使用中心化余弦相似度。
让我们看看这个是如何工作的。
我们又有了一个效用矩阵;这次,我们将查看用户对歌曲的评分。用户在列上,歌曲在行上,结果如下:
| 实体 | U1 | U2 | U3 | U4 | U5 |
|---|---|---|---|---|---|
| S1 | 2 | 4 | 5 | ||
| S2 | 3 | 3 | |||
| S3 | 1 | 5 | 4 | ||
| S4 | 4 | 4 | 4 | ||
| S5 | 3 | 5 |
现在,假设我们想知道用户 3 会给歌曲 5 打多少分。我们不再寻找相似的用户,而是根据这些歌曲在不同用户中的评分相似度,寻找相似的歌曲。
让我们看一个例子。
首先,我们通过对每首歌的行进行居中,并计算与我们的目标行S5的余弦相似度,结果如下:
| 实体 | U1 | U2 | U3 | U4 | U5 | CntrdCoSim |
|---|---|---|---|---|---|---|
| S1 | -1.66 | .33 | 1.33 | .98 | ||
| S2 | 0 | 0 | 0 | |||
| S3 | -2.33 | 1.66 | .66 | .72 | ||
| S4 | 0 | 0 | 0 | 0 | ||
| S5 | -1 | ? | 1 | 1 |
你可以看到最右列是通过每一行与行S5的中心余弦相似度计算出来的。
我们接下来需要选择一个数字,k,即我们用来对用户 3 打分的最近邻数量。在这个简单的示例中,我们使用k = 2。
你可以看到歌曲S1和歌曲S3是最相似的,因此我们将使用这两首歌曲以及用户 3 对S1和S3的评分(分别为 4 和 5)。
现在让我们来计算评分:
(.98 * (4) + .72 * (5)) / (.98 + .72) = 4.42
所以,根据这个物品到物品的协同过滤,我们可以看到用户 3 可能会对歌曲S5给出非常高的评分,计算结果为 4.42。
之前,我说过用户到用户过滤不如物品到物品过滤有效。这是为什么呢?
很有可能你有一些朋友非常喜欢你也喜欢的东西,但每个人还有其他一些兴趣领域,而这些领域是对方完全没有兴趣的。
例如,也许你们俩都喜欢权力的游戏,但你的朋友还喜欢挪威死亡金属。然而,你宁愿死掉也不愿听挪威死亡金属。如果你们在许多方面相似——排除死亡金属——通过用户到用户的推荐,你仍然会看到许多包含火焰、斧头、骷髅和重击等词语的乐队推荐。而通过物品到物品的过滤,最有可能的是,你将避免这些建议。
到目前为止,我们一直将用户和物品视为一个整体进行比较,但现在让我们看看另一种方法,它将用户和物品分解为可能称为特征篮子的东西。
基于内容的过滤
作为一名音乐人,Tim Westergren 曾在路上度过多年时间,聆听其他有才华的音乐人,思考为什么他们始终无法成功突破。尽管他们的音乐很好——与无线电广播上播放的音乐一样出色——然而,他们似乎从未迎来大爆发。他想,这一定是因为他们的音乐从未出现在足够多合适的人面前。
Tim 最终辞去了音乐人工作,转而担任电影配乐作曲家。在那里,他开始认为每一段音乐都有独特的结构,可以被分解成组成部分——一种音乐 DNA 的形式。
思考了一段时间后,他开始考虑围绕构建音乐基因组的这个想法成立一家公司。他向一位曾经创建并出售过公司朋友介绍了这个概念。朋友非常喜欢 Tim 的这个想法。事实上,他喜欢到开始帮助 Tim 编写商业计划书,并为项目筹集初期的资金。这项计划得到了推进。
在接下来的几年里,他们雇佣了一支小型的音乐家团队,细致地为超过百万首音乐作品制定了近 400 个独特的音乐特征。每个特征都用 0 到 5 分的评分标准手工评分(或者说是用耳朵评分可能更为恰当)。每首三到四分钟的歌曲需要将近半小时才能分类完毕。
特征包括主唱的声音沙哑程度或节奏的每分钟节拍数等。他们的第一个原型花了近一年时间才完成。这个原型完全是用 Excel 和 VBA 宏构建的,单单返回一个推荐就需要近四分钟的时间。但最终,它成功了,并且效果非常好。
这家公司现在被称为 Pandora 音乐,可能你要么听说过它,要么使用过它的产品,因为它在全球有数百万的日活跃用户。毫无疑问,这是基于内容的过滤的一个成功案例。
与基于内容的过滤方法将每首歌视为一个不可分割的单元不同,这些歌曲变成了特征向量,可以使用我们的老朋友余弦相似度进行比较。
另一个好处是,不仅歌曲可以被分解成特征向量,听众的偏好也可以被分解。每个听众的品味档案也会变成该空间中的一个向量,这样就可以在他们的品味档案和歌曲之间进行衡量。
对 Tim Westergren 来说,这就是魔力所在,因为与许多基于流行度的推荐不同,这个系统的推荐是基于固有的结构相似性做出的。也许你从未听说过歌曲X,但如果你喜欢歌曲Y,那么你也应该喜欢歌曲X,因为它在基因上几乎是相同的。这就是基于内容的过滤。
混合系统
我们现在已经看了两种主要的推荐系统,但你应该知道,在任何大规模的生产环境中,你可能会看到结合这两种方式的推荐系统。这被称为混合系统,混合系统的偏好原因在于,它们有助于消除使用单一系统时可能出现的缺点。这两个系统结合在一起,创造了一个更强大的解决方案。
让我们来看看每种类型的优缺点。
协同过滤
协同过滤的优点如下:
- 不需要手动制作特征
缺点如下:
-
如果没有大量的商品和用户,效果不好
-
当商品数量远远超过可以购买的数量时,会出现稀疏性问题
基于内容的过滤
基于内容的过滤的优点如下:
- 它不需要大量的用户
缺点如下:
-
定义正确的特征可能是一个挑战
-
缺乏偶然性
正如你所看到的,当你还没有建立起庞大的用户基础时,基于内容的过滤是一个更好的选择,但随着你的发展,增加协同过滤可以帮助将更多的偶然性引入推荐中。
现在你已经熟悉了推荐引擎的类型和工作原理,让我们开始构建一个自己的推荐引擎。
构建推荐引擎
我喜欢偶然发现一些非常有用的 GitHub 仓库。你可以找到从手工策划的机器学习教程到可以节省你大量代码行数的库,特别是在使用 Elasticsearch 时。问题是,找到这些库比应该的更难。幸运的是,我们现在有了足够的知识,可以通过 GitHub API 来帮助我们找到这些代码宝藏。
我们将使用 GitHub API 来基于协同过滤创建一个推荐引擎。计划是获取我一段时间内标记的所有仓库,然后获取这些仓库的所有创建者,查看他们标记了哪些仓库。一旦完成,我们将找到与我(或你,如果你为自己的仓库运行此过程,我建议这样做)最相似的用户。一旦找到最相似的用户,我们就可以使用他们标记的,而我没有标记的仓库来生成推荐列表。
让我们开始吧:
- 我们将导入所需的库:
import pandas as pd
import numpy as np
import requests
import json
-
你需要在 GitHub 上注册一个帐户,并标记一些仓库,这样才能使你的 GitHub 账号生效,但实际上你不需要注册开发者程序。你可以从你的个人资料中获得一个授权令牌,这将允许你使用 API。你也可以用这段代码使它正常工作,但由于限制太多,无法在我们的示例中使用。
-
要为 API 创建一个令牌,请访问以下网址:
github.com/settings/tokens。在这里,你会看到右上角有一个按钮,像这样:
- 你需要点击那个生成新令牌的按钮。点击之后,你需要选择权限,我选择了仅限 public_repo。然后,最后复制它给你的令牌,并在以下代码中使用。确保将其包含在引号内:
myun = YOUR_GITHUB_HANDLE
mypw = YOUR_PERSONAL_TOKEN
- 我们将创建一个函数,提取你标星的每个仓库的名字:
my_starred_repos = []
def get_starred_by_me():
resp_list = []
last_resp = ''
first_url_to_get = 'https://api.github.com/user/starred'
first_url_resp = requests.get(first_url_to_get, auth=(myun,mypw))
last_resp = first_url_resp
resp_list.append(json.loads(first_url_resp.text))
while last_resp.links.get('next'):
next_url_to_get = last_resp.links['next']['url']
next_url_resp = requests.get(next_url_to_get, auth=(myun,mypw))
last_resp = next_url_resp
resp_list.append(json.loads(next_url_resp.text))
for i in resp_list:
for j in i:
msr = j['html_url']
my_starred_repos.append(msr)
这里发生了很多事情,但本质上,我们在查询 API 以获取我们自己的标星仓库。GitHub 使用分页,而不是一次性返回所有内容。因此,我们需要检查每个响应中返回的.links。只要还有“下一页”链接可调用,我们就会继续这样做。
- 我们只需要调用我们创建的那个函数:
get_starred_by_me()
- 然后,我们可以看到所有被标星的仓库的完整列表:
my_starred_repos
前面的代码将输出类似以下内容:
- 我们需要解析出我们标星的每个库的用户名,以便我们可以获取他们标星的库:
my_starred_users = []
for ln in my_starred_repos:
right_split = ln.split('.com/')[1]
starred_usr = right_split.split('/')[0]
my_starred_users.append(starred_usr)
my_starred_users
这将产生类似以下的输出:
- 现在我们已经获取了所有我们标星的用户的用户名,我们需要获取他们标星的所有仓库。以下函数将实现这一功能:
starred_repos = {k:[] for k in set(my_starred_users)}
def get_starred_by_user(user_name):
starred_resp_list = []
last_resp = ''
first_url_to_get = 'https://api.github.com/users/'+ user_name +'/starred'
first_url_resp = requests.get(first_url_to_get, auth=(myun,mypw))
last_resp = first_url_resp
starred_resp_list.append(json.loads(first_url_resp.text))
while last_resp.links.get('next'):
next_url_to_get = last_resp.links['next']['url']
next_url_resp = requests.get(next_url_to_get, auth=(myun,mypw))
last_resp = next_url_resp
starred_resp_list.append(json.loads(next_url_resp.text))
for i in starred_resp_list:
for j in i:
sr = j['html_url']
starred_repos.get(user_name).append(sr)
这个函数的工作方式与我们之前调用的函数几乎相同,但它调用的是不同的端点。它将把他们标星的仓库添加到我们稍后会用到的字典中。
- 现在让我们调用它。根据每个用户标星的仓库数量,它可能需要几分钟才能运行完成。实际上,我有一个用户标星了超过 4,000 个仓库:
for usr in list(set(my_starred_users)):
print(usr)
try:
get_starred_by_user(usr)
except:
print('failed for user', usr)
前面的代码将输出类似以下内容:
注意,在调用它之前,我将标星用户列表转成了集合。我注意到有一些重复项,是因为一个用户标星了多个仓库,所以按这些步骤操作来减少额外的调用是很有意义的:
- 现在我们需要构建一个特征集,包含我们所有标星的用户标星的所有仓库:
repo_vocab = [item for sl in list(starred_repos.values()) for item in sl]
- 我们将把它转换为集合,去除可能存在的重复项,这些重复项可能是多个用户标星了相同的仓库:
repo_set = list(set(repo_vocab))
- 让我们看看这会生成多少内容:
len(repo_vocab)
前面的代码应该会输出类似以下内容:
我标星了 170 个仓库,此外,这些仓库的用户们一共标星了超过 27,000 个独立的仓库。你可以想象,如果我们再往外推一步,会看到多少仓库。
现在我们拥有了完整的特征集或仓库词汇,我们需要对每个用户进行操作,创建一个二进制向量,对于每个他们标星的仓库赋值1,对于每个没有标星的仓库赋值0:
all_usr_vector = []
for k,v in starred_repos.items():
usr_vector = []
for url in repo_set:
if url in v:
usr_vector.extend([1])
else:
usr_vector.extend([0])
all_usr_vector.append(usr_vector)
我们刚才做的事情是检查每个用户是否标星了我们仓库词汇中的每个仓库。如果标星了,他们会得到1,如果没有,他们会得到0。
到目前为止,我们为每个用户(总共 170 个用户)创建了一个包含 27,098 个项目的二进制向量。现在,让我们将其放入一个DataFrame中。行索引将是我们标星过的用户句柄,列将是仓库词汇:
df = pd.DataFrame(all_usr_vector, columns=repo_set, index=starred_repos.keys())
df
上面的代码将生成类似以下的输出:
接下来,为了与其他用户进行比较,我们需要将我们自己的行添加到这个框架中。在这里,我添加了我的用户句柄,但你应该添加你自己的:
my_repo_comp = []
for i in df.columns:
if i in my_starred_repos:
my_repo_comp.append(1)
else:
my_repo_comp.append(0)
mrc = pd.Series(my_repo_comp).to_frame('acombs').T
mrc
上面的代码将生成类似以下的输出:
我们现在需要添加适当的列名,并将其与我们的其他DataFrame进行拼接:
mrc.columns = df.columns
fdf = pd.concat([df, mrc])
fdf
上面的代码将生成类似以下的输出:
你可以在上面的截图中看到,我已经被加入到了DataFrame中。
从这里开始,我们只需要计算自己与其他我们标星过的用户之间的相似度。我们现在就用pearsonr函数来做这个计算,我们需要从 SciPy 导入这个函数:
from scipy.stats import pearsonr
sim_score = {}
for i in range(len(fdf)):
ss = pearsonr(fdf.iloc[-1,:], fdf.iloc[i,:])
sim_score.update({i: ss[0]})
sf = pd.Series(sim_score).to_frame('similarity')
sf
上面的代码将生成类似以下的输出:
我们刚才所做的是将我们的向量(DataFrame中的最后一个)与其他所有用户的向量进行比较,以生成一个居中的余弦相似度(皮尔逊相关系数)。由于某些用户没有标星任何仓库,因此有些值必然为NaN,这会导致在计算时除以零:
- 现在让我们对这些值进行排序,返回最相似用户的索引:
sf.sort_values('similarity', ascending=False)
上面的代码将生成类似以下的输出:
所以,我们得到了最相似的用户,因此我们可以利用这些用户来推荐我们可能喜欢的仓库。让我们看看这些用户以及他们标星了哪些我们可能喜欢的仓库。
- 你可以忽略第一个具有完美相似度分数的用户;那是我们自己的仓库。向下看,三个最接近的匹配是用户 6、用户 42 和用户 116。我们来看看每一个:
fdf.index[6]
上面的代码将生成类似以下的输出:
- 让我们看看这个用户是谁以及他们的仓库。通过
github.com/cchi,我可以看到这个仓库属于以下用户:
这个人实际上是 Charles Chi,我以前在彭博社的同事,所以这不是什么意外。我们来看看他标星了哪些内容:
- 有几种方法可以做到这一点;我们可以使用代码,或者直接点击他们头像下方的星标。我们这次做两者,比较一下并确保结果一致。首先,让我们通过代码来操作:
fdf.iloc[6,:][fdf.iloc[6,:]==1]
这将产生以下输出:
- 我们看到他加星了 30 个仓库。让我们将这些仓库与 GitHub 网站上的仓库进行比较:
-
在这里我们看到它们是相同的,你会注意到可以标识出我们都加星的仓库:它们是那些标记为 Unstar 的。
-
不幸的是,仅凭 30 个加星的仓库,推荐的仓库数量并不多。
-
与我相似的下一个用户是 42,Artem Golubin:
fdf.index[42]
上述代码将产生以下输出:
以及他下面的 GitHub 资料:
这是他加星的仓库:
-
Artem 加星了超过 500 个仓库,所以那里肯定有一些推荐。
-
最后,让我们来看一下第三位最相似的用户:
fdf.index[116]
这将产生以下输出:
这位用户,Kevin Markham,加星了大约 60 个仓库:
我们可以在下面的图像中看到加星的仓库:
这无疑是生成推荐的肥沃土壤。现在,让我们就做这件事;我们将使用这三个链接来生成一些推荐:
- 我们需要收集他加星的仓库链接,而我没有加星的仓库。我们将创建一个
DataFrame,包含我加星的仓库以及与我最相似的三位用户:
all_recs = fdf.iloc[[6,42,116,159],:]
all_recs.T
上述代码将产生以下输出:
- 如果它看起来全是零,不用担心;这是一个稀疏矩阵,所以大部分值为 0。让我们看看是否有我们都加星的仓库:
all_recs[(all_recs==1).all(axis=1)]
上述代码将产生以下输出:
- 正如你所见,我们似乎都热衷于 scikit-learn 和机器学习的仓库——这不令人惊讶。让我们看看他们可能都加星了哪些我错过的仓库。我们将先创建一个不包含我的框架,然后查询其中共同加星的仓库:
str_recs_tmp = all_recs[all_recs[myun]==0].copy()
str_recs = str_recs_tmp.iloc[:,:-1].copy()
str_recs[(str_recs==1).all(axis=1)]
上述代码产生以下输出:
- 好的,看起来我没有错过任何明显的内容。让我们看看是否有至少三位用户中有两位加星的仓库。为了找到这些仓库,我们将对行进行求和:
str_recs.sum(axis=1).to_frame('total').sort_values(by='total', ascending=False)
上述代码将产生类似以下的输出:
这看起来很有前景。有很多很好的机器学习和人工智能的存储库,老实说,我很羞愧以前没收藏 fuzzywuzzy,因为我经常用到它。
到目前为止,我必须说我对结果感到印象深刻。这些确实是我感兴趣的存储库,我会去查看它们。
到目前为止,我们已经通过协同过滤生成了推荐,并且通过聚合做了一些轻量的附加过滤。如果我们想更进一步,可以根据推荐所收到的总星标数来排序。这可以通过再次调用 GitHub API 来实现。GitHub 有一个端点提供这一信息。
我们可以做的另一件事是增加一层基于内容的过滤。这就是我们之前讨论的混合化步骤。我们需要从我们自己的存储库中创建一组特征,来表示我们感兴趣的内容类型。一种方法是通过对我们已收藏的存储库名称及其描述进行分词,来创建一组特征。
这是我收藏的存储库:
正如你想象的那样,这将生成一组我们可以用来筛选通过协同过滤找到的存储库的词特征。这将包括许多词,如 Python、机器学习 和 数据科学。这可以确保即使是与我们不太相似的用户,也能提供基于我们自己兴趣的推荐。同时,这也会减少推荐的偶然性,这一点需要考虑。也许有些东西与我目前拥有的完全不同,我会喜欢看到。这个可能性是存在的。
那么,基于内容的过滤步骤在 DataFrame 中会是什么样子的呢?列将是词特征(n-grams),行则是通过协同过滤步骤生成的存储库。我们只需要再次运行相似度处理,使用我们自己的存储库进行比较。
总结
在本章中,我们了解了推荐引擎。我们学习了当前使用的两种主要系统:协同过滤和基于内容的过滤。我们还学习了如何将这两者结合使用,形成一个混合系统。我们还讨论了每种系统的优缺点。最后,我们一步步了解了如何使用 GitHub API 从零开始构建推荐引擎。
我希望你能按照本章的指导,构建你自己的推荐引擎,并找到对你有用的资源。我知道我已经发现了许多我一定会使用的东西。祝你在旅程中好运!
第十一章:下一步是什么?
到目前为止,我们已经使用机器学习(ML)实现了各种任务。机器学习领域有许多进展,随着时间的推移,其应用领域也在不断增加。
在本章中,我们将总结在前几章中执行的项目。
项目总结
让我们从第一章,Python 机器学习生态系统开始。
在第一章,我们开始了 Python 机器学习的概述。我们从机器学习的工作流程入手,包括数据获取、检查、准备、建模评估和部署。然后我们学习了每个工作流程步骤所需的各种 Python 库和函数。最后,我们设置了机器学习环境来执行这些项目。
第二章,构建一个寻找低价公寓的应用程序,顾名思义,基于构建一个应用程序来寻找低价公寓。最初,我们列出了数据来寻找所需位置的公寓来源。然后,我们检查了数据,并在准备和可视化数据后,进行了回归建模。线性回归是一种监督式机器学习(ML)。在这个上下文中,监督式意味着我们为训练集提供输出值。
接着,我们花剩余的时间按照我们的选择探索其他选项。我们创建了一个应用程序,使寻找合适的公寓变得更加轻松。
在第三章,构建一个寻找便宜机票的应用程序,我们构建了一个类似于第二章,构建一个寻找低价公寓的应用程序,但目的是寻找便宜的机票。我们首先在网上获取了机票价格。我们使用了当下流行的技术之一——网页抓取,来获取机票价格数据。为了解析我们的 Google 页面的 DOM,我们使用了Beautifulsoup库。接着,我们使用异常检测技术来识别离群的机票价格。通过这样做,我们可以找到更便宜的机票,并且通过 IFTTT 收到实时文本提醒。
在第四章,使用逻辑回归预测 IPO 市场,我们探讨了 IPO 市场的运作方式。首先,我们讨论了什么是首次公开募股(IPO),以及研究告诉我们关于这个市场的情况。接着,我们讨论了多种策略,这些策略可以用来预测 IPO 市场。这包括数据清洗和特征工程。然后,我们使用逻辑回归对数据进行了二分类分析。最后,我们评估了获得的最终模型。
我们还了解到,影响我们模型的特征包括来自随机森林分类器的特征重要性。这能更准确地反映某个特征的实际影响。
第五章,创建自定义新闻推送,主要面向那些对全球新闻充满兴趣的新闻爱好者。通过创建一个自定义新闻推送,你可以决定哪些新闻更新会出现在你的设备上。在本章中,你学习了如何构建一个能够理解你新闻偏好的系统,并每天向你发送量身定制的新闻简报。我们从使用 Pocket 应用创建一个监督训练集开始,然后利用 Pocket API 来获取故事内容。我们使用 Embedly API 来提取故事正文。
接着,我们学习了自然语言处理(NLP)和支持向量机(SVMs)的基础知识。我们将If This Then That(IFTTT)与 RSS 源和 Google Sheets 结合使用,以便我们能够通过通知、电子邮件等方式保持最新。最后,我们设置了一个每日个人新闻简报。我们使用 Webhooks 通道发送POST请求。
该脚本每四小时运行一次,从 Google Sheets 中提取新闻故事,通过模型处理这些故事,生成电子邮件,发送POST请求到 IFTTT,通知我们预测可能感兴趣的故事,最后,它会清空电子表格中的旧故事,以确保下次邮件只发送新的故事。这就是我们如何获取个性化新闻推送的方式。
在第六章,判断你的内容是否会病毒式传播,我们考察了一些最受分享的内容,并试图找出这些内容与人们不太愿意分享的内容之间的共同元素。本章开始时提供了关于病毒式传播的定义。我们还研究了关于病毒式传播的研究结果。
然后,正如我们在其他章节中所做的那样,我们将会获取共享的计数和内容。我们使用了一个来自现已关闭的名为ruzzit.com的网站收集的数据集。该网站在运营时,追踪了最受分享的内容,这正是我们这个项目所需要的。接着,我们探索了可分享性的特征,其中包括探索图片数据、聚类、探索标题以及分析故事的内容。
最后,也是最重要的一部分,是构建预测内容评分模型。我们使用了一种名为随机森林回归的算法。我们构建的模型没有任何错误。然后,我们评估了这个模型,并添加了一些特性来增强它。
在第七章,使用机器学习预测股市,我们学会了如何建立和测试一个交易策略。我们还学到了如何不去做这件事。在尝试设计自己的系统时,有无数的陷阱需要避免,这几乎是一个不可能完成的任务,但它也可以非常有趣,有时甚至能带来利润。话虽如此,不要做愚蠢的事情,比如冒着自己负担不起的风险去投资。
当你准备好冒险投资时,不妨学习一些技巧和窍门,以避免损失太多。谁喜欢在生活中失败——无论是钱财还是游戏?
我们主要集中精力在股票和股市上。最初,我们分析了市场类型,然后研究了股市。在冒险之前,有一些先验知识总是更好的。我们通过关注技术方面开始制定策略。我们回顾了过去几年中的标准普尔 500 指数,并使用 pandas 导入我们的数据。这使我们能够访问多个股票数据来源,包括 Yahoo!和 Google。
然后我们建立了回归模型。我们从一个非常基础的模型开始,只使用股票的前一天收盘值来预测第二天的收盘价,并使用支持向量回归来构建它。最后,我们评估了模型的表现以及所执行的交易。
在 Siri 随 iPhone 4S 发布之前,我们就有了广泛应用于多种场景的聊天机器人。在第九章,构建聊天机器人中,我们学习了图灵测试及其起源。然后我们看了一个叫做 ELIZA 的程序。如果 ELIZA 是聊天机器人的早期示例,那么我们从那时以来又见到了什么?近年来,新的聊天机器人层出不穷,其中最著名的就是 Cleverbot。
然后,我们看到了有趣的部分:设计这些聊天机器人。
那么更先进的机器人呢?它们是如何构建的?
令人惊讶的是,大多数你可能遇到的聊天机器人并没有使用机器学习;它们是所谓的基于检索的模型。这意味着响应是根据问题和上下文预定义的。这些机器人最常见的架构是被称为人工智能标记语言(AIML)的东西。AIML 是一种基于 XML 的架构,用于表示机器人在用户输入的情况下应该如何互动。它实际上只是 ELIZA 工作方式的一个更高级版本。
最后,我们进行了聊天机器人中的序列到序列建模。这种方法在机器翻译和问答应用中经常使用,因为它允许我们将任意长度的输入序列映射到任意长度的输出序列。
在第八章,使用卷积神经网络进行图像分类中,我们学习了使用 Keras 构建卷积神经网络(CNN)来分类 Zalando 研究数据集中的图像。
我们从提取图像的特征开始。然后,使用卷积神经网络(CNN),我们理解了网络拓扑结构、各种卷积层和滤波器,以及最大池化层的原理。
尝试构建更深层次的模型,或者对我们在模型中使用的许多超参数进行网格搜索。像评估其他模型一样评估你的分类器的表现——尝试构建混淆矩阵,了解我们预测得好的类别以及我们不太擅长的类别!
在第十章,构建推荐引擎,我们探索了不同种类的推荐系统。我们了解了它们是如何在商业中实现的,以及它们是如何工作的。然后,我们为寻找 GitHub 仓库实现了自己的推荐引擎。
我们从协同过滤开始。协同过滤基于这样一个观点:在这个世界的某个地方,你有一个品味的“替身”——某个人对星球大战的评价和对真爱至上的看法与你完全相同。
接着,我们还学习了基于内容的过滤和混合系统是什么。
最后,我们使用 GitHub API 创建了一个基于协同过滤的推荐引擎。计划是获取我在一段时间内所有的星标仓库,并通过这些仓库的创建者,找出他们星标的仓库。这样,我们就能找出哪些用户的星标仓库与我最相似。
总结
本章只是一个小小的回顾,带你回顾我们实施过的所有项目。
希望你喜欢阅读这本书,并且这些实践能帮助你以类似的方式创建自己的项目!