Python-机器学习蓝图-三-

45 阅读1小时+

Python 机器学习蓝图(三)

原文:Python Machine Learning Blueprints

协议:CC BY-NC-SA 4.0

九、构建聊天机器人

想象一下,你独自坐在一个安静宽敞的房间里。你的右边是一张小桌子,上面放着一叠白色打印纸和一支黑色钢笔。在你面前的似乎是一个巨大的红色立方体,有一个微小的开口——略小于一个邮件槽的大小。插槽正上方的铭文邀请你写下一个问题,并通过插槽传递。碰巧你会说普通话;所以,你用普通话在其中一张纸上写下你的问题,并插入到开头。几分钟过去了,慢慢地,一个答案出现了。它也是用中文写的,并且是你可能期望的那种答案。你问了什么?*你是人还是电脑?*反应如何?为什么是的,是的我是

这个思想实验基于哲学家约翰·塞尔的《中国房间论》。实验的前提是,如果房间里有一个人不会说中文,但有一套规则允许他们将英文字符完美地映射为中文字符,那么在提问者看来,他们可能理解中文,而实际上对中文没有任何理解。塞尔的论点是,不能说产生可理解输出的算法程序理解了输出。他们缺少一个头脑。他的思维实验试图对抗强大的人工智能的思想,或者说人类大脑本质上只是一个湿机器的概念。塞尔不相信人工智能可以说是有意识的,不管它的行为在外部观察者看来有多复杂。

塞尔在 1980 年发表了这个实验。31 年后,Siri 将在 iPhone 4S 上发布。对于任何使用过 Siri 的人来说,很明显,在我们可能面临与我们交谈的代理是否有头脑的不确定性之前,我们还有很长的路要走(尽管我们可能会怀疑我们认识的人是否有头脑)。尽管这些代理或聊天机器人在过去表现出笨拙,但该领域正在迅速发展。

在本章中,我们将学习如何从头开始构建聊天机器人。在此过程中,我们将了解该领域的更多历史及其未来前景。

我们将在本章中讨论以下主题:

  • 图灵测试
  • 聊天机器人的历史
  • 聊天机器人的设计
  • 构建聊天机器人

图灵测试

在塞尔的《中国房间》问世 30 年前,艾伦·图灵提出了这样一个问题:机器能思考吗?在他的一篇比较著名的论文中。作为一个实际的天才,他决定不正面解决这个问题,而是把它放在其他人的思维的问题的框架中。这个问题问,我们如何真正知道别人也有和我们一样的想法?因为我们只能观察他们的行为——而不能观察他们的内心活动——我们必须相信他们和我们一样。图灵提出,如果一台机器可以表现得好像它是智能的,那么我们应该这样看待它。简单来说,这就是图灵测试。通过测试意味着让人类相信机器也是人类。

此时你可能会想,*有没有一个程序曾经成功通过图灵测试?*答案是真的没有官方的图灵测试。最接近官方测试的是罗布纳奖。这是一项每年举行一次的竞赛,向那些在评委团看来最有说服力的聊天机器人颁发适度的奖项。如果这听起来严谨和学术,它已经被描述为任何事情。据说罗布纳本人就是这样的人物,经常公开表示他对葡萄酒、大麻和妓女的喜爱。据说他对比赛的赞助是他完全蔑视任何形式的劳动的结果。

希望此时,你已经意识到任何关于通过图灵测试的说法都比真正的科学更壮观。尽管如此,图灵最初的概念是一个有用的构造。

聊天机器人的历史

我还记得有一天,父亲让我坐在他的 IBM XT 前,看着它黑白闪烁的显示器,让我和它说话。对话大致如下:

你好。你今天感觉怎么样?

嗨。这是谁?

你好...你今天怎么样?

我很好。你没告诉我你是谁。

我们应该讨论你,而不是我。

我们应该吗?为什么呢?

让我们稍微改变一下焦点...说说你的家庭吧。

你是不可能与之交谈的。

你能详细说明一下吗?

你要杀了我。

也许你想让我杀了你。

好吧,你吓到我了。

也许你真的在说你自己?

什么??这说不通。

这个问题的答案对你有什么帮助?

结束

非常有趣。

退出

感谢您与我交谈。

#####@@....

我正在和一个名叫伊莱扎的程序交谈。伊莱扎是约瑟夫·韦森鲍姆于 20 世纪 60 年代中期在麻省理工学院人工智能实验室创作的,旨在模仿一位罗格里安心理治疗师的反应。尽管在任何深度检查时都近乎滑稽,但该程序能够让一些用户相信他们在和真人聊天——考虑到它只有不到 200 行代码,使用随机化和正则表达式来鹦鹉学舌地回答,这是一个了不起的壮举。即使在今天,这个简单的节目仍然是流行文化的主要内容。如果你问 Siri ELISA 是谁,她会告诉你她是一个朋友,也是一个出色的精神病医生。

如果伊莱扎是聊天机器人的早期例子,从那以后我们看到了什么?近年来,新聊天机器人激增。其中最引人注目的是 Cleverbot。

Cleverbot 于 1997 年通过网络向世界发布。从那以后的几年里,这个机器人已经积累了数亿次转换,与早期的聊天机器人不同,正如它的名字所暗示的那样,Cleverbot 似乎随着每次转换变得更加智能。虽然算法工作的确切细节很难找到,但据说它的工作原理是通过记录数据库中的所有对话,并通过识别数据库中最相似的问题和答案来找到最合适的答案。

我编了一个无意义的问题,如下所示,你可以看到它在字符串匹配方面找到了与我的问题的对象相似的东西:

我坚持说:

我又得到了一些东西...相似吗?

您还会注意到,话题可以贯穿整个对话。作为回应,我被要求更详细地解释我的回答。这似乎是让克莱伯特变得聪明的原因之一。

虽然向人类学习的聊天机器人可能相当有趣,但它们也有黑暗的一面。

几年前,微软在推特上发布了一个名为 Tay 的聊天机器人。人们被邀请问泰问题,泰会根据她的性格做出回应。微软显然将机器人编程为一个 19 岁的美国女孩。她本打算成为你的虚拟闺蜜;唯一的问题是,她开始在推特上发布极端种族主义的言论。

由于这些令人难以置信的煽动性推文,微软被迫将 Tay 从推特上拉下来并发表道歉。

"As many of you know by now, on Wednesday we launched a chatbot called Tay. We are deeply sorry for the unintended offensive and hurtful tweets from Tay, which do not represent who we are or what we stand for, nor how we designed Tay. Tay is now offline and we'll look to bring Tay back only when we are confident we can better anticipate malicious intent that conflicts with our principles and values." -March 25, 2016 Official Microsoft Blog

显然,想要在未来将聊天机器人释放到野外的品牌应该从这次失败中吸取教训,并计划让用户试图操纵它们来展示人类最糟糕的行为。

毫无疑问,品牌正在拥抱聊天机器人。从脸书到塔可钟,每个人都参与了这场比赛。

见证玉米机器人:

是的,这是真的。而且,尽管有一些障碍,像 Tay 一样,UI 的未来很有可能看起来很像 TacoBot。最后一个例子甚至可能有助于解释为什么。

Quartz 最近推出了一款将新闻转化为对话的应用。与其把一天的故事列成一个简单的清单,不如像从朋友那里得到消息一样聊天:

推特的项目经理大卫·加斯卡在《媒体》上的一篇帖子中描述了他使用该应用的经历。他描述了对话性质如何唤起通常只在人际关系中触发的感情:

"Unlike a simple display ad, in a conversational relationship with my app I feel like I owe something to it: I want to click. At the most subconscious level I feel the need to reciprocate and not let the app down: "The app has given me this content. It's been very nice so far and I enjoyed the GIFs. I should probably click since it's asking nicely."

如果这种体验是普遍的——我预计也是如此——这可能是广告界的下一件大事,我毫不怀疑广告利润将推动用户界面设计:

"The more the bot acts like a human, the more it will be treated like a human." -Mat Webb, Technologist and Co-Author of Mind Hacks

在这一点上,你可能很想知道这些东西是如何工作的,所以让我们继续吧!

聊天机器人的设计

最初的 ELIZA 应用是 200 多行代码。Python NLTK 实现同样很短。节选自 NLTK 网站(www.nltk.org/_modules/nl…):

从代码中可以看到,输入文本被解析,然后与一系列正则表达式进行匹配。一旦输入匹配,就会返回随机响应(有时会回显部分输入)。所以,比如说,我需要一个玉米卷会引发这样的反应,*真的能帮你得到一个玉米卷吗?*显然,答案是肯定的,而且,幸运的是,我们已经发展到了技术可以为你提供一个的地步(祝福你,TacoBot),但这仍然是早期。令人震惊的是,有些人真的相信 ELIZA 是一个真正的人。

但是更先进的机器人呢?它们是如何建造的?

令人惊讶的是,你可能遇到的大多数聊天机器人甚至都没有使用机器学习(ML);它们是所谓的基于 T4 检索的模型。这意味着响应是根据问题和上下文预先定义的。这些机器人最常见的架构是一种叫做人工智能标记语言 ( AIML )的东西。AIML 是一个基于 XML 的模式,用于表示在给定用户输入的情况下机器人应该如何交互。这实际上只是伊莱扎工作方式的更高级版本。

让我们看看如何使用 AIML 生成响应。首先,对所有输入进行预处理,使其规范化。这意味着当你输入 Waaazzup 时???映射到什么是向上。这个预处理步骤将无数种表达同一件事的方式汇集到一个输入中,这个输入可以违反一个规则。标点符号和其他无关的输入在这一点上也被删除。一旦完成,输入将与适当的规则相匹配。以下是一个示例模板:

<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之前的一个或多个单词,然后在输出模板中重复这些单词。如果用户输入Dance for me!,响应将是I'm a bot. I don't dance. Ever

正如你所看到的,这些规则并没有创造出任何接近真实智力的东西,但是有一些技巧可以强化这种错觉。其中一个更好的是能够产生以某个话题为条件的反应。

例如,这里有一个调用主题的规则:

<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.05…的论文,描述了基于序列到序列模型的神经网络的构建。这种类型的模型将输入序列(如 ABC )映射到输出序列(如 XYZ )。例如,这些输入和输出可能是从一种语言到另一种语言的翻译。就他们在这里的工作而言,培训数据不是语言翻译,而是技术支持成绩单和电影对话。虽然这两种模式的结果都很有趣,但占据头条的是基于电影模式的互动。

以下是本文中的互动示例:

这些都不是由人类明确编码的,也没有按照要求出现在训练集中,然而,看着这些,就像和人类说话一样可怕。但让我们看看更多:

请注意,模特的回应似乎是性别知识( heshe )、 place (英国)和职业(选手)。甚至意义、伦理和道德的问题都是公平的游戏:

如果那份成绩单没有让你感到一丝寒意,你可能已经是某种人工智能了。

我衷心推荐阅读整篇论文。它并没有过度的技术化,它肯定会让你看到技术的发展方向。

我们已经谈了很多关于聊天机器人的历史、类型和设计,但是现在让我们继续构建我们自己的聊天机器人。对此,我们将采取两种方法。第一个将使用我们在前面看到的技术,余弦相似性,第二个将利用序列到序列的学习。

构建聊天机器人

现在,在了解了聊天机器人的可能性之后,你很可能想要构建最好的、最先进的、谷歌级别的机器人,对吗?好吧,暂时不要想这些,因为我们要从相反的事情开始。我们要建造史上最可怕的机器人!

这听起来可能令人失望,但如果你的目标只是建造一些非常酷和吸引人的东西(不需要花费数小时来建造),这是一个很好的开始。

我们将利用从与 Cleverbot 的一系列真实对话中获得的培训数据。数据来自notsocleverbot.jimrule.com。这个网站是完美的,因为它让人们提交了他们与 Cleverbot 最荒谬的对话。

让我们看一下 Cleverbot 和网站用户之间的对话示例:

虽然您可以自由使用我们在前面章节中使用的网页抓取技术来收集数据,但是您可以在本章的 GitHub repo 中找到数据的.csv

我们将在 Jupyter 笔记本中重新开始。我们将加载、解析和检查数据。我们将首先导入熊猫和 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 

前面的代码产生以下输出:

你应该能看出我们在用户克莱伯之间有互动,并且两者都可以发起对话。为了以我们需要的格式获得数据,我们必须将它解析成问题和响应对。我们不一定关心谁说了什么,而是匹配每个问题的每个回答。一会儿你就会明白为什么了。现在让我们对文本进行一点正则表达式魔法:

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'] 

前面的代码产生以下输出:

好的,有很多代码。刚刚发生了什么?我们首先创建了一个列表来保存我们的问答元组。然后,我们通过一个函数传递我们的对话,使用正则表达式将它们分成几对。

最后,我们将其全部设置到熊猫数据框中,其中的列分别标记为qa

我们现在将应用一点算法魔法来匹配与用户输入的问题最接近的问题:

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') 

值得注意的是,这可能是我有一段时间以来最好的对话之一,不管是不是机器人。

虽然这是一个有趣的小项目,但现在让我们转向使用序列到序列建模的更高级的建模技术。

聊天机器人的序列对序列建模

对于下一个任务,我们将利用在第 8 章中讨论的几个库,使用卷积神经网络、TensorFlow 和 Keras 对图像进行分类。如果你还没有安装,两者都可以pip安装。

我们还将使用本章前面讨论的高级建模类型;这是一种叫做序列对序列建模的深度学习。这在机器翻译和问答应用中经常使用,因为它允许我们将任意长度的输入序列映射到任意长度的输出序列:

Source: blog.keras.io/a-ten-minut…

Francois Chollet 在 Keras 的博客上对这种类型的模型有一个很好的介绍:https://blog . Keras . io/a-十分钟介绍序列对序列学习 in-keras.html 。值得一读。

我们将大量使用他的示例代码来构建我们的模型。虽然他的例子使用了机器翻译,即英语到法语,但我们将使用我们的 Cleverbot 数据集将其重新用于问答:

  1. 设置导入:
from keras.models import Model 
from keras.layers import Input, LSTM, Dense 
import numpy as np 
  1. 设置培训参数:
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() 

这为我们的问题和答案(目标)创建了列表,也为我们的问题和答案中的单个字符创建了集合。这个模型实际上将通过一次生成一个字符来工作:

  1. 让我们将问答对限制在 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)] 
  1. 让我们设置输入和目标文本列表:
input_texts = list(convo_frame['q'].astype('str')) 
target_texts = list(convo_frame['a'].map(lambda x: '\t' + x + 
                    '\n').astype('str')) 

前面的代码以正确的格式获取我们的数据。请注意,我们在目标文本中添加了一个制表符(\t)和一个换行符(\n)。这将作为解码器的开始和停止标记。

  1. 让我们看看输入文本和目标文本:
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 

上述代码生成以下输出:

从上图中,你会注意到我们有一个字符数据的单热编码向量,它将用于我们的模型中。

我们现在设置序列到序列模型编码器和解码器 LSTMs:

# 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') 

在前面的代码中,我们使用编码器和解码器输入以及解码器输出定义了模型。然后我们编译它,调整它,并保存它。

我们将模型设置为使用 1000 个样本。在这里,我们还将数据分为 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) 

上述代码生成以下输出:

如您所见,我们模型的结果相当重复。但是我们只使用了 1000 个样本,每次产生一个字符的响应,所以这实际上是相当令人印象深刻的。

如果您想要更好的结果,请使用更多的样本数据和更多的时期重新运行模型。

在这里,我提供了一些我从更长时间的训练中注意到的更幽默的输出:

摘要

在这一章中,我们全面参观了 chatbot 景观。很明显,我们正处于这类应用爆炸的风口浪尖。对话式 UI 革命即将开始。希望这一章已经启发你创建自己的机器人,但如果没有,我们希望你对这些应用如何工作以及它们将如何塑造我们的未来有更丰富的理解。

我会让应用说出最后的话:

get_response("This is the end, Cleverbot. Say goodbye.") 

十、构建推荐引擎

像许多事情一样,它诞生于沮丧和僵硬的鸡尾酒。那是一个星期六,两个年轻人又一次陷入了没有约会对象的困境。当他们坐在那里倒饮料,分享悲伤时,这两位哈佛新生开始充实一个想法。如果他们可以使用计算机算法,而不是依靠随机的机会遇到合适的女孩呢?

他们认为,匹配人的关键是创造一组问题,提供每个人在第一次尴尬约会中真正想要的信息。通过匹配使用这些问卷的人,你可以排除那些最好避免的日期。这个过程会非常高效。

这个想法是向波士顿和全国各地的大学生推销他们的新服务。很快,他们就这么做了。

不久之后,他们建立的数字婚介服务取得了巨大成功。它受到了全国媒体的关注,并在接下来的几年里产生了数万场比赛。该公司非常成功,事实上,它最终被一家更大的公司收购,该公司希望使用其技术。

如果你认为我说的是 OkCupid ,那你就错了——而且会推迟 40 年。我所说的这家公司从 1965 年开始做所有这些事情——当时在 IBM 1401 主机上使用穿孔卡片进行计算匹配。仅仅运行计算也花了三天时间。

但奇怪的是,OkCupid 和它 1965 年的前身兼容性研究公司有联系。兼容性研究公司的联合创始人是杰夫·塔尔,他的女儿詹妮弗·塔尔是 OkCupid 联合创始人克里斯·科恩的妻子。世界真小。

但是为什么这些都与构建推荐引擎的章节有关呢?因为这很可能是第一次。虽然大多数人倾向于将推荐引擎视为寻找他们可能欣赏的密切相关的产品或音乐和电影的工具,但最初的化身是寻找潜在的伴侣。作为思考这些系统如何工作的模型,它提供了一个很好的参考框架。

在本章中,我们将探讨不同种类的推荐系统。我们将看看它们是如何在商业上实现的,以及它们是如何工作的。最后,我们将实现自己的推荐引擎来寻找 GitHub 存储库。

我们将在本章中讨论以下主题:

  • 协同过滤
  • 基于内容的过滤
  • 混合系统
  • 构建推荐引擎

协同过滤

2012 年初,一个故事发生了,一个男人走进明尼阿波利斯的塔吉特百货商店,抱怨一本书的优惠券被送到他家。事实上,他对这些优惠券相当恼火,这些优惠券是寄给当时还是高中生的女儿的。尽管这看起来像是对潜在省钱机会的奇怪反应,但了解到优惠券只针对产前维生素、尿布、婴儿配方奶粉、婴儿床等产品可能会改变你的看法。

经理听到投诉后,连连道歉。事实上,他感觉很糟糕,几天后他打电话跟进,解释这是怎么发生的。但是在经理还没来得及开始道歉的时候,父亲就开始向经理道歉。事实证明,他的女儿实际上已经怀孕了,她的购物习惯已经把她出卖了。

送走她的算法可能是基于——至少部分是基于——一种在推荐引擎中使用的算法,称为协同过滤

那么,什么是协同过滤呢?

协同过滤是基于这样一种想法:在世界的某个地方,你有一个味觉二重身——一个对《星球大战》有多好和《T2》有多糟糕有着相同想法的人。

这个想法是,你给一些物品打分的方式和另一个人,这个二重身给它们打分的方式非常相似,但是你们每个人都给其他人没有给的物品打分。因为你已经确定了你的品味是相似的,推荐可以从你的二重身评价很高但你没有评价的项目中产生,反之亦然。这在某种程度上很像数字婚介,但结果是你喜欢的歌曲或产品,而不是真实的人。

因此,以我们怀孕的高中生为例,当她购买了无气味乳液、棉球和维生素补充剂的正确组合时,她很可能发现自己与后来继续购买婴儿床和尿布的人配对了。

让我们通过一个例子来看看这在实践中是如何工作的。

我们将从所谓的效用矩阵开始。这类似于术语-文档矩阵,但是,我们将代表产品和用户,而不是术语和文档。

在这里,我们假设我们有客户 A-D 和一组产品,他们已经从 0 到 5 进行了评级:

| 客户 | 斯纳克薯片 | 搜搜爽滑T2乳液 | 杜飞T2啤酒 | 自来水 自来水 | 【xxlargelivin】 泽西岛 | 雪天T2棉花T5丸子 | 一次性尿布T2尿布 | | A | four | | five | three | five | | | | B | | four | | four | | five | | | C | Two | | Two | | one | | | | D | | five | | three | | five | four |

我们之前已经看到,当我们想要找到相似的项目时,我们可以使用余弦相似度。让我们在这里试试。我们会找到最像用户 A 的用户。因为我们有一个包含许多未分级项目的稀疏向量,我们将不得不为那些丢失的值输入一些东西。我们这里用 0。我们将从比较用户和用户开始:

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(忽略此处的舍入问题):

| 客户 | 斯纳克薯片 | 搜搜爽滑T2乳液 | 杜飞T2啤酒 | 自来水 自来水 | 【xxlargelivin】 泽西岛 | 雪天T2棉花T5丸子 | 一次性尿布T2尿布 | | A | -.25 | | .75 | -1.25 | .75 | | | | B | | -.33 | | -.33 | | .66 | | | C | .33 | | .33 | | -.66 | | | | D | | .75 | | -1.25 | | .75 | -.25 |

现在让我们在新的中心数据上尝试余弦相似性。我们再来做用户 A 对比用户 BC

首先,我们来比较一下用户 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)) 

前面的代码产生以下输出:

现在让我们在用户 AC 之间进行尝试:

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)) 

前面的代码产生以下输出:

我们可以看到的是 AB 的相似度略有增加,而 AC 的相似度则大幅下降。这正是我们所希望的。

这个居中过程,除了帮助我们处理缺失的值之外,还有一个好处,那就是帮助我们处理困难或容易的评分者,因为现在每个人都以平均值 0 为中心。你可能会注意到,这个公式相当于皮尔逊相关系数,就像那个系数一样,数值介于-11之间。

预测产品的评级

现在让我们利用这个框架来预测产品的评级。我们将我们的例子限制为三个用户,人 X ,人 Y ,人 Z 。我们来预测一个产品的评级,人 X 没有评级,但是人 YZ 非常相似的人 X 评级。

我们将从每个用户的基本评分开始,如下表所示:

| 客户 | 斯纳克薯片 | 搜搜爽滑T2乳液 | 杜飞T2啤酒 | 自来水 自来水 | 【xxlargelivin】 泽西岛 | 雪天T2棉花T5丸子 | 一次性尿布T2尿布 | | X | | four | | three | | four | | | Y | | Three point five | | Two point five | | four | four | | Z | | four | | Three point five | | Four point five | Four point five |

接下来,我们将集中评分:

| 客户 | 斯纳克薯片 | 搜搜爽滑T2乳液 | 杜飞T2啤酒 | 自来水 自来水 | 【xxlargelivin】 泽西岛 | 雪天T2棉花T5丸子 | 一次性尿布T2尿布 | | X | | .33 | | -.66 | | .33 | ? | | Y | | Zero | | -1 | | .5 | .5 | | Z | | -.125 | | -.625 | | .375 | .375 |

现在,我们想知道用户 X 可能会给德普索斯的尿布什么评分。使用来自用户 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德普索斯纸尿裤的预期评分为 4.26。(最好发个优惠券!)

到目前为止,我们只关注了用户对用户的协同过滤,但还有另一种方法可以使用。在实践中,该方法优于用户对用户过滤;叫做项对项过滤。方法是这样的:不是根据过去的评分将每个用户与其他相似的用户进行匹配,而是将每个评分的项目与所有其他项目进行比较,以找到最相似的项目,同样使用中心余弦相似度。

让我们看看这是如何工作的。

同样,我们有一个效用矩阵;这一次,我们来看看用户对歌曲的评分。用户沿着列,歌曲沿着行,如下所示:

| 实体 | U1 | U2 | U3 | U4 | U5 | | S1 | Two | | four | | five | | S2 | | three | | three | | | S3 | one | | five | | four | | S4 | | four | four | four | | | S5 | three | | | | five |

现在,假设我们想知道用户 3 将为歌曲 5 分配的等级。我们将根据用户对歌曲的评价来寻找相似的歌曲,而不是寻找相似的用户。

我们来看一个例子。

首先,我们以每首歌曲行为中心,计算每首与我们的目标行的余弦相似度,即 S5 ,如下所示:

| 实体 | U1 | U2 | U3 | U4 | U5 | CNT rdcosim | | S1 | -1.66 | | .33 | | One point three three | .98 | | S2 | | Zero | | Zero | | Zero | | S3 | -2.33 | | One point six six | | .66 | .72 | | S4 | | Zero | Zero | Zero | | Zero | | S5 | -1 | | ? | | one | one |

您可以看到,最右边的列是用每一行的中心余弦相似度对行 S5 计算的。

我们接下来需要选择一个数字, k ,这是我们将用于为用户 3 评价歌曲的最近邻居的数量。我们在简单的例子中使用 k = 2

你可以看到歌曲 S1 和歌曲 S3 最相似,所以我们将使用这两个以及用户 3 对 S1S3 的评分(分别为 4 和 5)。

现在让我们计算一下评级:

*(. 98 *(4)+. 72 (5))/(. 98+. 72)= 4.42

因此,基于这种项目到项目的协同过滤,我们可以看到,根据我们的计算,用户 3 很可能会将歌曲 S5 评为非常高的 4.42。

前面我说过,用户对用户的过滤不如项目对项目的过滤有效。为什么会这样?

很有可能你的朋友真的很喜欢你喜欢的一些东西,但是你们每个人都有其他感兴趣的领域,而另一个人绝对没有兴趣。

比如,或许你们俩都爱权力的游戏,但你的朋友也爱挪威死亡金属。然而,你宁愿死也不愿听挪威的死亡金属。如果你在用户对用户的推荐中,在很多方面都很相似——不包括死亡金属,你仍然会看到很多关于乐队名字的推荐,这些名字包含了像燃烧的斧头头骨大头棒这样的词。有了逐项过滤,你很可能就不用提那些建议了。

到目前为止,在进行比较时,我们已经将用户和项目视为一个单一的实体,但是现在让我们继续看另一种方法,该方法将我们的用户和项目分解成所谓的功能篮

基于内容的过滤

作为一名音乐家,蒂姆·韦斯特格伦多年来一直在路上听其他有才华的音乐家演奏,想知道为什么他们永远无法取得成功。他们的音乐很好——就像你在收音机里听到的一样好——然而,不知何故,他们就是没有抓住他们的大突破。他想这一定是因为他们的音乐从来没有在足够多的合适的人面前出现过。

蒂姆最终辞去了音乐人的工作,转而从事电影配乐作曲家的工作。正是在那里,他开始认为每首音乐都有一个独特的结构,可以分解成组成部分——一种音乐基因的形式。

考虑了一番后,他开始考虑围绕这个建立音乐基因组的想法创建一家公司。他的一个朋友提出了这个概念,他之前创建并出售了一家公司。朋友喜欢蒂姆的想法。事实上,以至于他开始帮助他写一份商业计划,并为这个项目筹集初始资金。这是一次尝试。

在接下来的几年里,他们雇佣了一小群音乐家,他们为 100 多万首音乐精心编纂了近 400 种不同的音乐特征。每一个特征都是用手在 0 到 5 分的等级上评定的(或者用耳朵来说是更好的方式)。每首三四分钟的歌都要花将近半个小时来分类。

这些特色包括主唱的声音有多沙哑,或者节奏有多快。他们的第一个原型花了将近一年的时间才完成。完全在 Excel 中使用 VBA 宏构建,仅返回一条推荐就花了将近四分钟。但最终,它奏效了,而且效果很好。

这家公司现在被称为潘多拉音乐,你可能听说过它,或者使用过它的产品,因为它在世界各地有数百万的日常用户。毫无疑问,这是一个基于内容过滤的成功例子。

不像在基于内容的过滤中那样,将每首歌曲视为一个不可分割的单元,而是将歌曲作为特征向量,可以使用我们的朋友余弦相似度进行比较。

另一个好处是,不仅歌曲会被分解成特征向量,听众也会被分解成特征向量。每个听众的口味特征成为这个空间中的一个向量,因此可以在他们的口味特征和歌曲本身之间进行测量。

对蒂姆·韦斯特格伦来说,这就是神奇之处,因为与其像许多推荐一样依赖音乐的流行度,不如说这个系统的推荐是基于内在的结构相似性。也许你没听过歌 X ,但是如果你是宋立科 Y ,那么你应该是宋立科 X ,因为它的基因几乎一模一样。这就是基于内容的过滤。

混合系统

我们现在已经研究了推荐系统的两种主要形式,但是您应该知道,在任何大规模生产环境中,您都有可能看到利用这两种形式的推荐。这就是所谓的混合动力系统,混合动力系统之所以更受欢迎,是因为它们有助于消除单独使用任何一种系统时可能存在的缺点。这两个系统共同创造了一个更强大的解决方案。

让我们来看看每种类型的优缺点。

协同过滤

协同过滤的优点如下:

  • 没有必要手工制作特征

缺点如下:

  • 如果没有大量的项目和用户,就不能很好地工作
  • 当物品的数量远远超过可以购买的数量时,就会变得稀疏

基于内容的过滤

基于内容的过滤的优点如下:

  • 它不需要大量用户

缺点如下:

  • 定义正确的特性可能是一个挑战
  • 缺乏意外的发现

正如您所看到的,当您还没有建立起庞大的用户群时,基于内容的过滤是一个更好的选择,但是随着您的成长,添加协作过滤可以帮助在推荐中引入更多的意外发现。

既然您已经熟悉了推荐引擎的类型和内部工作方式,让我们开始构建自己的推荐引擎。

构建推荐引擎

我喜欢偶然发现一个非常有用的 GitHub 存储库。你可以找到包含所有内容的存储库,从手工策划的机器学习教程到使用 Elasticsearch 时可以为你节省几十行代码的库。问题是,找到这些库比它应该的要困难得多。幸运的是,我们现在已经知道如何利用 GitHub API 来帮助我们找到这些代码宝石。

我们将使用 GitHub API 创建一个基于协同过滤的推荐引擎。我们的计划是获得我长期以来主演的所有存储库,然后让这些存储库的所有创建者找到他们主演的存储库。一旦这样做了,我们会发现哪些用户和我最相似(或者你,如果你是为你自己的存储库运行这个,我建议)。一旦我们有了最相似的用户,我们就可以使用他们主演的存储库来生成一组推荐,而我没有。

让我们开始吧:

  1. 我们将导入所需的库:
import pandas as pd 
import numpy as np 
import requests 
import json 
  1. 您需要在 GitHub 上开设一个帐户,并在许多存储库中打上星号,这样才能为您的 GitHub 句柄工作,但您实际上不需要注册开发人员程序。您可以从您的配置文件中获得授权令牌,这将允许您使用该应用编程接口。您也可以让它与这段代码一起工作,但是限制太多,无法让它用于我们的示例。

  2. 要创建一个与应用编程接口一起使用的令牌,请访问位于github.com/settings/to…的以下网址。在那里,您将在右上角看到一个按钮,如下所示:

  1. 您需要单击生成新令牌按钮。完成后,您需要选择权限,我选择了 public_repo。最后,复制它给你的令牌,用于下面的代码。请务必用引号将两者括起来:
myun = YOUR_GITHUB_HANDLE 
mypw = YOUR_PERSONAL_TOKEN 
  1. 我们将创建一个函数,该函数将提取您所标记的每个存储库的名称:
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。只要有下一个要调用的链接,我们就会继续这样做。

  1. 我们只需要调用我们创建的函数:
get_starred_by_me() 
  1. 然后,我们可以看到星标存储库的完整列表:
my_starred_repos 

前面的代码将产生类似如下的输出:

  1. 我们需要解析出我们标记的每个库的用户名,以便我们可以检索它们标记的库:
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 

这将导致类似以下的输出:

  1. 现在我们已经有了所有我们主演的用户的句柄,我们将需要检索他们主演的所有存储库。下面的函数就可以做到这一点:
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) 

该函数的工作方式与我们之前调用的函数几乎相同,但调用了不同的端点。它会将他们的星形存储库添加到我们稍后将使用的字典中。

  1. 现在就这么说吧。运行可能需要几分钟时间,这取决于每个用户所加入的存储库的数量。我实际上有一个超过 4000 个存储库:
for usr in list(set(my_starred_users)): 
    print(usr) 
    try: 
        get_starred_by_user(usr) 
    except: 
        print('failed for user', usr) 

前面的代码将产生类似如下的输出:

请注意,在我调用之前,我已经将明星用户列表变成了一个集合。我注意到在一个用户句柄下由多个存储库导致的一些重复,因此遵循以下步骤来减少额外的调用是有意义的:

  1. 我们现在需要构建一个功能集,其中包括我们主演过的每个人的所有明星资料库:
repo_vocab = [item for sl in list(starred_repos.values()) for item in sl] 
  1. 我们将把它转换成一个集合来删除可能存在于同一存储库中的多个用户的重复项:
repo_set = list(set(repo_vocab)) 
  1. 让我们看看会产生多少:
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

此时,我们为每个用户提供了 27,098 个项目的二进制向量——全部 170 个。现在让我们把这个放入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,因为它们没有用星号标出存储库,因此导致计算中被零除:

  1. 现在让我们对这些值进行排序,以返回最相似用户的索引:
sf.sort_values('similarity', ascending=False) 

前面的代码将产生类似如下的输出:

所以我们有了,这些是最相似的用户,因此我们可以用来推荐我们可能喜欢的存储库。让我们来看看这些用户,看看他们有哪些我们可能会喜欢的明星。

  1. 你可以忽略第一个拥有完美相似度分数的用户;那是我们自己的仓库。沿着列表往下,三个最接近的匹配是用户 6、用户 42 和用户 116。让我们看看每一个:
fdf.index[6] 

前面的代码将产生类似如下的输出:

  1. 让我们看看这是谁以及他们的存储库。从github.com/cchi我可以看到这个仓库属于以下哪个用户:

这其实是我以前在的同事迟浩田,所以这并不奇怪。让我们看看他主演了什么:

  1. 有几种方法可以做到这一点;我们可以使用我们的代码,或者直接点击他们图片下面的星星。让我们为这一个做两个,只是为了比较,并确保一切匹配。首先,让我们通过代码来实现:
fdf.iloc[6,:][fdf.iloc[6,:]==1] 

这将产生以下输出:

  1. 我们看到了 30 个星形存储库。让我们把这些和 GitHub 网站上的进行比较:

  1. 在这里,我们可以看到它们是相同的,你会注意到你可以识别我们都主演过的存储库:它们被标记为 Unstar。
  2. 不幸的是,只有 30 个星标存储库,没有太多的存储库来生成推荐。
  3. 相似性方面的下一个用户是 42,Artem Golubin:
fdf.index[42] 

前面的代码产生以下输出:

下面是他的 GitHub 简介:

这里我们看到他主演的仓库:

  1. Artem 已经主演了 500 多个存储库,所以肯定有一些推荐可以在那里找到。
  2. 最后,让我们看看第三个最相似的用户:
fdf.index[116] 

这将产生以下输出:

这个用户,凯文·马卡姆,已经主演了大约 60 个存储库:

我们可以在下图中看到星形存储库:

这绝对是产生推荐的沃土。让我们现在就这样做;让我们利用这三个链接提出一些建议:

  1. 我们需要收集他们主演的和我没有主演的资料库的链接。我们将创建一个DataFrame,其中包含我主演的存储库以及与我最相似的三个用户:
all_recs = fdf.iloc[[6,42,116,159],:] 
all_recs.T 

前面的代码将产生以下输出:

  1. 如果看起来全是零,不用担心;这是一个稀疏矩阵,所以大多数都是 0。让我们看看是否有我们都主演过的存储库:
all_recs[(all_recs==1).all(axis=1)] 

前面的代码将产生以下输出:

  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)] 

上述代码产生以下输出:

  1. 好吧,看起来我没有错过什么特别明显的东西。让我们看看是否有至少三分之二的用户主演的存储库。为了找到这一点,我们将对行进行求和:
str_recs.sum(axis=1).to_frame('total').sort_values(by='total', ascending=False) 

前面的代码将产生类似如下的输出:

这看起来很有希望。有很多好的语言和人工智能库,老实说,我很惭愧我从来没有主演过模糊的东西,因为我经常使用它。

在这一点上,我不得不说我对结果印象深刻。这些肯定是我感兴趣的存储库,我会去看看。

到目前为止,我们已经使用协作过滤生成了推荐,并使用聚合进行了一些轻度的附加过滤。如果我们想走得更远,我们可以根据他们收到的星星总数来订购推荐。这可以通过再次调用 GitHub API 来实现。有一个端点可以提供这些信息。

我们可以做的另一件事是增加一层基于内容的过滤来改善结果。这是我们之前讨论的杂交步骤。我们需要从我们自己的存储库中创建一组特性,这些特性指示了我们感兴趣的事物的类型。实现这一点的一种方法是创建一个特性集,方法是将我们标记的存储库的名称及其描述标记化。

下面是我的星标存储库:

正如您可能想象的那样,这将生成一组单词特征,我们可以使用它们来检查我们使用协作过滤找到的那些库。这将包括许多单词,如 Python机器学习数据科学。这将确保与我们不太相似的用户仍然提供基于我们自己兴趣的推荐。这也会降低这些建议的意外收获,这是需要考虑的事情。也许有什么不一样的东西是我现在很想看到的。这当然是有可能的。

就数据帧而言,基于内容的过滤步骤会是什么样子?列将是单词特征(n-gram),行将是我们的协同过滤步骤生成的存储库。我们将使用自己的存储库再次运行相似性过程进行比较。

摘要

在本章中,我们学习了推荐引擎。我们了解了目前使用的两种主要类型的系统:协作过滤和基于内容的过滤。我们还学习了如何将它们一起用于形成混合系统。我们还讨论了每种系统的优缺点。最后,我们一步一步地学习了如何使用 GitHub API 从头开始构建推荐引擎。

希望大家利用本章的指导构建自己的推荐引擎,希望大家找到对自己有用的资源。我知道我已经找到了一些我肯定会用到的东西。祝你旅途好运!

十一、下一步是什么?

到目前为止,我们已经使用机器学习 ( ML )来实现各种任务。ML 领域有许多进步,随着时间的推移,它的应用领域也在增加。

在本章中,我们将总结我们在前几章中执行的项目。

项目摘要

先说第一章Python 机器学习生态系统

在第一章中,我们从 Python 的 ML 概述开始。我们从 ML 工作流程开始,包括获取、检查、准备、建模评估和部署。然后,我们研究了工作流每个步骤所需的各种 Python 库和函数。最后,我们建立了 ML 环境来执行项目。

第二章构建一个寻找低价公寓的应用,顾名思义,就是基于构建一个寻找低价公寓的应用。最初,我们列出了我们的数据,以找到所需位置公寓的来源。然后,我们检查数据,在准备和可视化数据后,我们执行回归建模。线性回归是监督最大似然估计的一种。在这种情况下,Supervised 仅仅意味着我们为训练集提供输出值。

然后,我们根据自己的选择,用剩下的时间探索各种选择。我们创建了一个应用,让找到合适的公寓变得稍微容易一点。

第 3 章构建一个寻找廉价机票的应用中,我们构建了一个类似于 第 2 章构建一个寻找低价公寓的应用中的应用,但是寻找廉价机票。我们从网上寻找机票价格开始。我们使用了一种趋势技术,网页抓取,来检索飞机票价的数据。为了解析谷歌页面的 DOM,我们使用了Beautifulsoup库。然后,我们使用异常检测技术来识别异常票价。通过这样做,可以找到更便宜的机票,我们将使用 IFTTT 接收实时文本警报。

第 4 章用逻辑回归预测 IPO 市场中,我们研究了 IPO 市场是如何运作的。首先,我们讨论了什么是首次公开募股 ( 首次公开募股)以及研究告诉我们的这个市场。之后,我们讨论了一些可以用来预测 IPO 市场的策略。它涉及数据清理和功能工程。然后,我们使用逻辑回归对数据进行二元分类分析。然后,我们评估作为输出获得的最终模型。

我们还理解,对我们的模型有影响的特征包括来自随机森林分类器的特征重要性。这更准确地反映了给定功能的真实影响。

第 5 章创建一个定制的新闻提要,主要是为那些对了解全球发生的事情感兴趣的狂热新闻读者准备的。通过创建自定义新闻源,您可以决定在设备上获取哪些新闻更新。在这一章中,你学会了如何建立一个系统来理解你对新闻的品味,并且每天都会给你发一份量身定制的时事通讯。我们从用 Pocket 应用创建一个受监督的训练集开始,然后利用 Pocket API 来检索故事。我们使用了嵌入式应用编程接口来提取故事主体。

然后,我们学习了自然语言处理 ( NLP )和支持向量机 ( 支持向量机)的基础知识。我们将If This Then That(iftt)与 RSS 提要和 Google sheets 集成在一起,这样我们就可以随时了解通知、电子邮件等信息。最后,我们建立了一个每日个人通讯。我们使用网络钩子频道发送了一个POST请求。

该脚本每四个小时运行一次,从 Google Sheets 中提取新闻故事,通过模型运行这些故事,通过向 IFTTT 发送POST请求来生成一封电子邮件,以获取预测感兴趣的故事,然后,最后,它将清除电子表格中的故事,以便在下一封电子邮件中只发送新故事。这就是我们获得个性化新闻的方式。

第 6 章 断言你的内容是否会像病毒一样传播中,我们研究了一些最常分享的内容,并试图找出区别这些内容和人们不太愿意分享的内容的共同因素。这一章首先解释了病毒的确切含义。我们还研究了研究告诉我们的病毒特性。

然后,正如我们在其余章节中所做的那样,我们将获取共享的计数和内容。我们使用的数据集是从一个名为ruzzit.com的网站上收集的。这个网站,当它活跃的时候,随着时间的推移,跟踪最多共享的内容,这正是我们这个项目所需要的。然后我们探索了共享性的特征,包括探索图像数据、聚类、探索标题和探索故事内容。

最后,但也是最重要的一部分是构建预测性内容评分模型。我们使用了一种叫做随机森林回归的算法。我们建立了一个零误差的模型。然后,我们对模型进行了评估,并添加了一些特性来增强它。

第 7 章使用机器学习预测股市中,我们学习了如何构建和测试交易策略。我们也学会了如何而不是去做。当试图设计自己的系统时,有无数的陷阱需要避免,这几乎是一项不可能的任务,但它可能会很有趣,有时甚至会有利可图。也就是说,不要做傻事,比如拿你输不起的钱去冒险。

当你准备拿你的钱冒险时,你不妨学习一些技巧和窍门,以避免损失太多。谁喜欢在生活中失败——是为了钱还是为了游戏?

我们主要把注意力集中在股票和股市上。首先分析了市场的类型,然后对股票市场进行了研究。在冒任何风险之前,最好有一些先验知识。我们开始通过关注技术方面来发展我们的战略。在过去的几年里,我们走遍了标准普尔 500,用熊猫来输入我们的数据。这让我们可以访问几个股票数据来源,包括雅虎!和谷歌。

然后我们建立了回归模型。我们从一个非常基本的模型开始,仅使用股票的先前收盘价来预测第二天的收盘价,并使用支持向量回归机来构建它。最后,我们评估了我们的模型和交易的表现。

早在 Siri 与 iPhone 4S 一起发布之前,我们就有了广泛应用于多个应用的聊天机器人。在第 9 章构建聊天机器人中,我们了解了图灵测试及其起源。然后我们看了一个叫 ELIZA 的程序。如果伊莱扎是聊天机器人的早期例子,从那以后我们看到了什么?近年来,新聊天机器人激增——其中最引人注目的是 Cleverbot。

然后,我们看了有趣的部分:设计这些聊天机器人。

但是更先进的机器人呢?它们是如何建造的?

令人惊讶的是,你可能遇到的大多数聊天机器人都不用 ML;它们被称为基于检索的模型。这意味着响应是根据问题和上下文预先定义的。这些机器人最常见的架构是一种叫做人工智能标记语言 ( AIML )的东西。AIML 是一个基于 XML 的模式,用于表示在给定用户输入的情况下机器人应该如何交互。这实际上只是伊莱扎工作方式的更高级版本。

最后,我们为聊天机器人做了序列到序列的建模。这在机器翻译和问答应用中经常使用,因为它允许我们将任意长度的输入序列映射到任意长度的输出序列。

第 8 章使用卷积神经网络对图像进行分类中,我们考虑构建一个卷积神经网络 ( CNN )来使用 Keras 对 Zalando Research 数据集中的图像进行分类。

我们从提取图像的特征开始。然后,使用 CNNs,我们了解了网络拓扑、各种卷积层和滤波器,以及什么是最大池层。

尝试构建更深层次的模型,或者在模型中使用的许多超参数上进行网格搜索。评估你的分类器的性能,就像评估任何其他模型一样——试着构建一个混淆矩阵来理解我们预测的好的类和我们没有预测的强的类!

第 10 章构建推荐引擎中,我们探索了不同种类的推荐系统。我们看到了它们是如何在商业上实现的,以及它们是如何工作的。然后我们实现了自己的推荐引擎来寻找 GitHub 存储库。

我们从协同过滤开始。协同过滤是基于这样一个想法,在世界的某个地方,你有一个味觉二重身——一个对《星球大战》有多好,《T2》有多糟糕,《真爱至上》有同样感觉的人。

然后我们还研究了什么是基于内容的过滤和混合系统。

最后,我们使用 GitHub API 创建了一个基于协同过滤的推荐引擎。我们的计划是获得我长期以来主演的所有存储库,然后让这些存储库的所有创建者找出他们主演的存储库。这使我们能够发现哪些用户的星标存储库与我的最相似。

摘要

这一章只是一个小回顾,带你回顾我们实施的所有项目。

我希望你喜欢读这本书,并且执行将帮助你以类似的方式创建你自己的项目!