Python 真实世界的数据科学(九)
三十一、新闻文章聚类
在前面的大多数章节中,我们都是在了解数据的基础上进行数据挖掘的。 通过使用目标类,我们可以了解我们的变量如何在训练阶段对这些目标进行建模。 我们有目标要针对的这种学习类型称为监督学习。 在本章中,我们考虑了没有这些目标的情况。 这是无监督学习,更多的是探索性任务。 无需使用我们的模型进行分类,无监督学习的目标更多是关于探索数据以寻找见解。
在本章中,我们着眼于对新闻文章进行聚类以发现数据中的趋势和模式。 我们研究如何使用链接聚合网站显示不同的新闻故事,从不同的网站提取数据。
本章涵盖的关键概念包括:
- 从任意网站获取文本
- 使用 Reddit API 收集有趣的新闻故事
- 用于无监督数据挖掘的聚类分析
- 从文档中提取主题
- 在线学习以更新模型而无需重新训练
- 集群整合以结合不同的模型
获得新闻文章
在本章中,我们将构建一个系统,该系统获取新闻文章的实时供稿并将其分组,其中各组的主题相似。 您可以在数周(或更长时间)内运行系统,以查看在这段时间内趋势如何变化。
我们的系统将从流行的链接聚合网站 reddit 开始,该网站存储指向其他网站的链接列表以及用于讨论的评论部分。 reddit 上的链接分为几类链接,称为 subreddits。 有一些专门针对特定电视节目,有趣图像和许多其他事物的版本。 我们感兴趣的是新闻的分类。 我们将在本章中使用/r/worldnews子目录,但该代码应与任何其他子目录一起使用。
在本章中,我们的目标是下载受欢迎的故事,然后将它们聚类以查看出现的任何主要主题或概念。 这将使我们能够深入了解流行的焦点,而无需手动分析数百个单个故事。
使用 Web API 获取数据
在前面的几章中,我们使用基于 Web 的 API 提取数据。 例如,在第 7 章,“使用图形挖掘”发现要遵循的帐户中,我们使用了 Twitter 的 API 提取数据。 收集数据是数据挖掘管道中的关键部分,基于 Web 的 API 是一种收集各种主题数据的绝妙方法。
使用基于 Web 的 API 收集数据时,需要考虑三件事:授权方法,速率限制和 API 端点。
授权方法允许数据提供者知道谁在收集数据,以便确保它们受到适当的速率限制,并且可以跟踪数据访问。 对于大多数网站,个人帐户通常足以开始收集数据,但是某些网站会要求您创建一个正式的开发人员帐户来获得此访问权限。
速率限制适用于数据收集,特别是免费服务。 使用 API时要注意规则,这一点很重要,因为它们会并且确实会在网站之间发生变化。 Twitter 的 API 限制为每 15 分钟 180 个请求(取决于特定的 API 调用)。 稍后我们将看到 Reddit 允许每分钟 30 个请求。 其他网站设置了每日限制,而其他网站则是按秒限制。 即使在网站内部,不同的 API 调用也存在巨大差异。 例如,Google Maps 的限制较小,每个资源的 API 限制不同,每小时请求数量的配额也不同。
注意
如果您发现自己正在创建需要更多请求和更快响应的应用或正在运行的实验,则大多数 API 提供商都制定了允许进行更多调用的商业计划。
API 端点是用于提取信息的实际 URL。 这些内容因网站而异网站。 通常,基于 Web 的 API 将遵循 RESTful 接口(表示状态传输的缩写)。 RESTful 接口通常使用与 HTTP 相同的操作:GET,POST 和 DELETE 是最常见的操作。 例如,要检索有关资源的信息,我们可以使用以下 API 端点: www.dataprovider.com/api/resourc… 。
为了使获得和信息,我们仅向该 URL 发送一个 HTTP GET 请求。 这将返回具有给定类型和 ID 的资源信息。 尽管实现上存在一些差异,但大多数 API 都遵循此结构。 大多数具有 API 的网站都会对其进行适当记录,从而为您提供可检索的所有 API 的详细信息。
首先,我们设置参数以连接到服务。 为此,您需要使用开发人员密钥进行 Reddit。 为了获得此密钥,请登录这个页面网站并转到这个页面。 从此处单击**,您是开发人员吗? 创建一个应用…**并填写表格,将类型设置为脚本。 您将获得客户端 ID 和一个秘密,可以将其添加到新的 IPython Notebook 中:
CLIENT_ID = "<Enter your Client ID here>"
CLIENT_SECRET = "<Enter your Client Secret here>"
Reddit 还要求您在使用 API时将用户代理设置为包含用户名的唯一字符串。 创建一个唯一标识您的应用的用户代理字符串。 我使用书名chapter 10和版本号 0.1 来创建我的用户代理,但是它可以是您喜欢的任何字符串。 请注意,不这样做会导致您的连接受到严重的速率限制:
USER_AGENT = "python:<your unique user agent> (by /u/<your reddit username>)"
此外,您将需要使用用户名和密码登录 reddit。 如果您还没有,请注册一个新的帐户(这是免费的,您也不需要使用个人信息进行验证)。
注意
您将需要密码来完成下一步,因此在与他人共享代码以将其删除之前,请务必小心。 如果您不输入密码,请将其设置为none,系统将提示您输入密码。 但是,由于 IPython Notebook 的工作方式,您需要将其输入到启动 IPython 服务器的命令行终端中,而不是输入 Notebook 本身。 如果无法执行此操作,则需要在脚本中进行设置。 IPython Notebook 的开发人员正在开发一个插件来解决此问题,但在编写本文时尚不可用。
现在让我们创建用户名和密码:
USERNAME = "<your reddit username>"
PASSWORD = "<your reddit password>"
接下来,我们将创建一个函数来记录此信息。 reddit 登录 API 将返回一个可用于进一步连接的令牌,这是该功能的结果。 代码如下:
def login(username, password):
首先,如果您不想将密码添加到脚本中,可以将其设置为None,系统将提示您,如前所述。 代码如下:
if password is None:
password = getpass.getpass("Enter reddit password for user {}: ".format(username))
将用户代理设置为唯一值非常重要,否则连接可能会受到严格限制。 代码如下:
headers = {"User-Agent": USER_AGENT}
接下来,我们设置一个 HTTP 授权对象以允许我们在 reddit 的服务器上登录:
client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET)
要登录,我们向access_token端点发出 POST 请求。 我们发送的数据是我们的用户名和密码,以及在此示例中设置为password的授予类型:
post_data = {"grant_type": "password", "username": username, "password": password}
最后,我们使用requests库发出登录请求(通过 HTTP POST 请求完成)并返回结果,该结果是值的字典。 这些值之一是我们将来需要的令牌。 代码如下:
response = requests.post("https://www.reddit.com/api/v1/access_token", auth=client_auth, data=post_data, headers=headers)
return response.json()
我们现在可以调用我们的函数来获取令牌:
token = login(USERNAME, PASSWORD)
token对象只是一个字典,但它包含access_token字符串,我们将在以后的请求中将其传递给。 它还包含其他信息,例如令牌的范围(将是所有内容)以及令牌的到期时间,例如:
{'access_token': '<semi-random string>', 'expires_in': 3600, 'scope': '*', 'token_type': 'bearer'}
Reddit 作为数据源
Reddit( www.reddit.com )是一个链接聚合的网站,全球数百万人使用,尽管英文版本是以美国为中心。 任何用户都可以向他们认为有趣的网站提供链接,以及该链接的标题。 然后,其他用户可以赞成,表明他们喜欢该链接,或者赞成,表明他们不喜欢该链接。 投票最高的链接将移至页面顶部,而投票较低的链接则不会显示。 较旧的链接会随着时间的流逝而被删除(取决于它拥有多少票)。 拥有推荐的故事的用户获得称为业力的积分,从而鼓励他们仅提交好故事。
Reddit 还允许非链接内容,称为自发布。 这些包含标题和提交者输入的一些文本。 这些用于提问和开始讨论,但不计入一个人的业力。 在本章中,我们将仅考虑基于链接的帖子,而不考虑基于评论的帖子。
帖子分为网站的不同部分,称为 subreddits。 subreddit 是相关帖子的集合。 当用户提交指向 reddit 的链接时,他们可以选择要加入的子 reddit。 Subreddit 有其自己的管理员,并且对于该 Subreddit 的有效内容有自己的规则。
默认情况下,帖子按热门排序,这取决于帖子的年龄,支持的数量和已收到的支持的数量。 还有新,它只为您提供最近发布的故事(因此包含很多垃圾邮件和不良帖子),以及热门,是给定投票率最高的故事 时间段。 在本章中,我们将使用热门,它将为我们提供最近的高质量故事(新中确实有许多劣质链接)。
使用我们先前创建的令牌,我们现在可以从 subreddit 获取链接集。 为此,我们将使用/r/<subredditname> API 端点,该端点默认情况下返回热门故事。 我们将使用/r/worldnews subreddit:
subreddit = "worldnews"
上一个端点的 URL 使我们可以创建完整的 URL,可以使用字符串格式设置该 URL:
url = "https://oauth.reddit.com/r/{}".format(subreddit)
接下来,我们需要设置标题。 这样做有两个原因:允许我们使用之前收到的授权令牌,并设置用户代理以阻止我们的请求受到严格限制。 代码如下:
headers = {"Authorization": "bearer {}".format(token['access_token']),
"User-Agent": USER_AGENT}
然后,像之前的一样,我们使用requests库进行调用,确保设置了标头:
response = requests.get(url, headers=headers)
对此调用json()将产生一个 Python 字典,其中包含 Reddit 返回的信息。 它将包含来自给定 subreddit 的前 25 个结果。 我们可以通过遍历此响应中的故事来获得标题。 故事本身存储在词典的data键下。 代码如下:
for story in result['data']['children']:
print(story['data']['title'])
获取数据
我们的数据集将包含/r/worldnews subreddit 的热门列表中的帖子。 我们在上一节中看到了如何连接到 reddit 以及如何下载链接。 综上所述,我们将创建一个函数,该函数将提取给定 subreddit 中每个项目的标题,链接和得分。
我们将遍历 subreddit,一次最多获取 100 个故事。 我们也可以进行分页以获得更多结果。 在 reddit 阻止我们之前,我们可以阅读大量页面,但我们会将其限制为 5 页。
由于我们的代码将反复调用 API,因此请务必对调用进行速率限制。 为此,我们需要sleep函数:
from time import sleep
我们的函数将接受一个 subreddit 名称和一个授权令牌。 尽管我们将默认设置为 5:
def get_links(subreddit, token, n_pages=5):
然后,我们创建一个列表以将故事存储在:
stories = []
我们在第 7 章中发现了要使用图形挖掘进行跟踪的帐户,即分页如何适用于 Twitter API。 我们将光标与返回的结果一起发送,并随请求发送。 然后,Twitter 将使用此光标来获取结果的下一页。 除了调用参数after之外,reddit API 的功能几乎完全相同。 我们第一页不需要它,因此我们最初将其设置为none。 我们将在结果的第一页之后将其设置为有意义的值。 代码如下:
after = None
然后,我们迭代要返回的页面数:
for page_number in range(n_pages):
在循环内部,我们像以前一样初始化 URL 结构:
headers = {"Authorization": "bearer {}".format(token['access_token']),
"User-Agent": USER_AGENT}
url = "https://oauth.reddit.com/r/{}?limit=100".format(subreddit)
从第二个循环开始,我们需要设置after参数(否则,我们将只获得同一页结果的多个副本)。 该值将在循环的上一个迭代中设置-第一个循环为第二个循环设置 after 参数,依此类推。 如果存在,则将其附加到 URL 的末尾,告诉 reddit 获取下一页数据。 代码如下:
if after:
url += "&after={}".format(after)
然后,像以前一样,我们使用请求库进行调用,然后使用json()将结果转换为 Python 字典:
response = requests.get(url, headers=headers)
result = response.json()
此结果将为我们在下一次循环迭代时提供after参数,我们现在可以将其设置如下:
after = result['data']['after']
然后,我们睡眠 2 秒钟以避免超出 API 限制:
sleep(2)
作为循环中的最后一个动作,我们从返回的结果中获取每个故事,并将它们添加到我们的stories列表中。 我们不需要所有数据,仅获得标题,URL 和分数。 代码如下:
stories.extend([(story['data']['title'], story['data']['url'], story['data']['score'])
for story in result['data']['children']])
最后(在循环之外),我们返回找到的所有故事:
return stories
调用stories函数是传递授权令牌和 subreddit 名称的简单情况:
stories = get_links("worldnews", token)
返回的结果应包含标题,URL 和 500 个故事,我们现在将使用它们从所得网站中提取实际文本。
从任意网站提取文本
我们从 reddit 获得的链接转到由许多不同组织运营的任意网站。 更难的是,这些页面被设计为人类而非计算机程序读取。 尝试获取这些结果的实际内容/故事时,这可能会引起问题,因为现代网站在后台运行了很多工作。 调用 JavaScript 库,应用样式表,使用 AJAX 加载广告,在侧边栏中添加额外的内容,并进行其他各种操作以使现代网页成为复杂的文档。 这些功能使现代 Web 成为现实,但是很难自动从中获取良好的信息!
在任意网站中查找故事
首先,我们将从每个链接下载完整的网页,并将其存储在raw子文件夹下的数据文件夹中。 稍后我们将处理这些信息以提取有用的信息。 这种结果缓存确保我们在工作时不必连续下载网站。 首先,我们设置数据文件夹路径:
import os
data_folder = os.path.join(os.path.expanduser("~"), "Data", "websites", "raw")
我们将使用 MD5 哈希为文章创建唯一的文件名,因此我们将导入hashlib来执行此操作。 hash函数是将某些输入(在我们的示例中为包含标题的字符串)转换为看似随机的字符串的函数。 相同的输入将始终返回相同的输出,但是略有不同的输入将返回截然不同的输出。 从哈希值到原始值也是不可能的,这使其成为单向函数。 代码如下:
import hashlib
我们将简单地跳过所有失败的网站下载。 为了确保这样做不会丢失太多信息,我们对发生的错误数进行了简单的计数。 我们将抑制发生的任何错误,这可能导致系统性的问题,禁止下载。 如果此错误计数器过高,我们可以查看这些错误是什么并尝试修复它们。 例如,如果计算机无法访问 Internet,则所有 500 次下载都将失败,您应该在继续之前解决此问题!
如果下载没有错误,则输出应为零:
number_errors = 0
接下来,我们遍历每个故事:
for title, url, score in stories:
然后,我们通过散列标题为文章创建唯一的输出文件名。 reddit 中的标题不必唯一,这意味着两个故事可能具有相同的标题,因此在我们的数据集中会发生冲突。 为了获得唯一的文件名,我们只需使用 MD5 算法对文章的 URL 进行哈希处理即可。 尽管已知 MD5 存在一些问题,但在我们的场景中不太可能发生问题(冲突),即使它确实发生了,我们也不必担心太多,也不必担心太多 如果确实发生碰撞。
output_filename = hashlib.md5(url.encode()).hexdigest()
fullpath = os.path.join(data_folder, output_filename + ".txt")
接下来,我们下载实际页面并将其保存到我们的output文件夹中:
try:
response = requests.get(url)
data = response.text
with open(fullpath, 'w') as outf:
outf.write(data)
如果在获取网站时出错,我们只需跳过此网站并继续前进。 该代码将在 95%的网站上运行,这对于我们的应用已经足够了,因为我们正在寻找总体趋势而非精确性。 请注意,有时您确实希望获得 100%的响应,并且应该调整代码以容纳更多的错误。 获得最终 5%到 10%网站的代码将变得更加复杂。 然后,我们捕获可能发生的任何错误(这是 Internet,许多事情可能出错),增加错误计数,然后继续。
except Exception as e:
number_errors += 1
print(e)
如果发现发生太多错误,请将print(e)行更改为仅键入raise。 这将导致调用异常,从而使您可以调试问题。
现在,子文件夹中有很多网站。 看完这些页面(在文本编辑器中打开创建的文件)之后,您可以看到其中存在内容,但是有 HTML,JavaScript,CSS 代码以及其他内容。 由于我们仅对故事本身感兴趣,因此我们现在需要一种从这些不同的网站中提取此信息的方法。
全部放在一起
在获得原始数据之后,我们需要在每个数据中找到故事。 有一些在线来源使用数据挖掘来实现此目的。 您可以在第 13 章中找到它们。 尽管使用它们可以得到更好的准确性,但是很少需要使用这样的复杂算法。 这是数据挖掘的一部分-知道何时使用它,何时不使用。
首先,我们在raw子文件夹中获得每个文件名的列表:
filenames = [os.path.join(data_folder, filename)
for filename in os.listdir(data_folder)]
接下来,我们为要提取的纯文本版本创建一个output文件夹:
text_output_folder = os.path.join(os.path.expanduser("~"), "Data",
"websites", "textonly")
接下来,我们开发代码以从文件中提取文本。 我们将使用lxml库来解析 HTML 文件,因为该库具有处理某些格式错误的表达式的良好 HTML 解析器。 代码如下:
from lxml import etree
提取文本的实际代码基于三个步骤。 首先,我们遍历 HTML 文件中的每个节点,并提取其中的文本。 其次,我们跳过任何包含 JavaScript,样式或注释的节点,因为这不太可能包含我们感兴趣的信息。 第三,我们确保内容至少包含 100 个字符。 这是一个很好的基准,但是可以进行改进以获得更准确的结果。
如前所述,我们对脚本,样式或注释不感兴趣。 因此,我们创建了一个列表来忽略这些类型的节点。 此列表中具有类型的任何节点都不会被视为包含故事。 代码如下:
skip_node_types = ["script", "head", "style", etree.Comment]
现在,我们将创建一个将 HTML 文件解析为lxml etree的函数,然后我们将创建另一个解析该树以查找文本的函数。 第一个功能非常简单; 只需使用lxml库的 HTML 文件解析功能打开文件并创建树。 代码如下:
def get_text_from_file(filename):
with open(filename) as inf:
html_tree = lxml.html.parse(inf)
return get_text_from_node(html_tree.getroot())
在该函数的最后一行,我们调用getroot()函数来获取树的根节点,而不是完整的etree。 这使我们可以编写文本提取函数以接受任何节点,因此可以编写递归函数。
此函数将在任何子节点上调用自身以从中提取文本,然后返回任何子节点文本的串联。
如果传递此函数的节点没有任何子节点,则仅从中返回文本。 如果没有任何文本,我们只返回一个空字符串。 请注意,我们还在此处检查第三个条件-文本至少 100 个字符长。 代码如下:
def get_text_from_node(node):
if len(node) == 0:
# No children, just return text from this item
if node.text and len(node.text) > 100:
return node.text
else:
return ""
至此,我们知道该节点具有子节点,因此我们在每个子节点上递归调用此函数,然后在它们返回时加入结果。 代码如下:
results = (get_text_from_node(child) for child in node
if child.tag not in skip_node_types)
return "\n".join(r for r in results if len(r) > 1)
返回结果的最终条件是停止返回空行(例如,当节点没有子节点也没有文本时)。
现在,我们可以通过遍历原始 HTML 页面,在每个原始 HTML 页面上调用文本提取函数并将结果保存到纯文本子文件夹来在所有原始 HTML 页面上运行此代码:
for filename in os.listdir(data_folder):
text = get_text_from_file(os.path.join(data_folder, filename))
with open(os.path.join(text_output_folder, filename), 'w') as outf:
outf.write(text)
您可以通过打开“纯文本”子文件夹中的每个文件并检查其内容来手动评估结果。 如果发现太多结果包含非故事内容,请尝试增加最小 100 个字符的限制。 如果仍然无法获得满意的结果,或者需要更好的结果,请尝试使用第 13 章中列出的更复杂的方法。
分组新闻文章
本章的目的是通过将新闻文章进行聚类或分组来发现新闻文章中的趋势。 为此,我们将使用 k-means 算法,这是一种最初于 1957 年开发的经典机器学习算法。
聚类是一种无监督的学习技术,我们使用聚类算法来探索数据。 我们的数据集包含大约 500 个故事,要逐一检查每个故事将非常艰巨。 即使我们使用汇总统计信息,仍然是很多数据。 通过使用聚类,我们可以将相似的故事分组在一起,并且我们可以独立地探索每个聚类中的主题。
当我们没有清晰的数据目标类集时,我们将使用聚类技术。 从这个意义上讲,聚类算法的学习方向不大。 他们根据某种功能学习,而与数据的基本含义无关。 因此,选择良好的功能至关重要。 在监督学习中,如果选择较差的功能,则学习算法可以选择不使用那些功能。 例如,支持向量机将对那些在分类中没有用的特征给予很小的重视。 但是,通过聚类,所有功能都将用于最终结果中,即使这些功能无法为我们提供所需的答案。
在对真实数据进行聚类分析时,最好先了解哪种功能将适用于您的方案。 在本章中,我们将使用词袋模型。 我们正在寻找基于主题的组,因此我们将使用基于主题的功能为文档建模。 我们知道这些功能有效是因为其他人在问题的监督版本中所做的工作。 相反,如果要执行基于作者身份的聚类,则将使用诸如 第 9 章,“作者身份归因”实验中发现的功能。
k-均值算法
k 均值聚类算法使用迭代过程找到最能代表数据的质心。 该算法从一组预定义的质心开始,这些质心通常是从训练数据中获取的数据点。 k 均值中的k是要查找的质心数以及该算法将找到多少个簇。 例如,将k设置为3将在数据集中找到三个聚类。
k 均值分为两个阶段:分配和更新。
在分配步骤中,我们为数据集中的每个样本设置了标签,将其链接到最近的质心。 对于每个最接近质心 1 的样本,我们为其分配标签 1。对于每个最接近质心 2 的样本,我们为每个k重心分配一个标签 2,依此类推。 这些标签形成聚类,因此我们说每个带有标签 1 的数据点都在聚类 1 中(仅此时,因为分配可以随着算法的运行而变化)。
在更新步骤中,我们采用每个聚类并计算质心,质心是该聚类中所有样本的平均值。
然后,算法在分配步骤和更新步骤之间进行迭代; 每次执行更新步骤时,每个质心都会移动少量。 这将导致分配稍有变化,从而导致质心在下一次迭代中移动少量。 重复此过程,直到达到某个停止标准为止。 通常在经过一定数量的迭代后或质心的总运动非常低时停止。 该算法在某些情况下也可以完成,这意味着群集是稳定的,分配不会更改,质心也不会更改。
在下图中,对随机创建的数据集执行了 k 均值,但是数据中包含三个簇。 星形代表质心的起始位置,这些质心是通过从数据集中选取随机样本随机选择的。 在 k 均值算法的 5 次迭代中,质心移至三角形表示的位置。
k 均值算法因其数学特性和历史意义而着迷。 它是一种(大约)只有一个参数的算法,在发现后的 50 多年中,它是相当有效且经常使用的算法。
scikit-learn 中有一个 k-means 算法,我们从cluster子包中导入该算法:
from sklearn.cluster import KMeans
我们还导入了CountVectorizer类的近亲表弟TfidfVectorizer。 此向量化器将根据每个术语的出现次数对每个术语的计数进行加权。许多文档中出现的术语的权重较低(通过将值除以出现在其中的文档数的对数)。 对于许多文本挖掘应用,使用这种类型的加权方案可以相当可靠地提高性能。 代码如下:
from sklearn.feature_extraction.text import TfidfVectorizer
然后,我们建立了用于分析的管道。 这有两个步骤。 首先是应用向量化器,其次是应用 k-means 算法。 代码如下:
from sklearn.pipeline import Pipeline
n_clusters = 10
pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)),
('clusterer', KMeans(n_clusters=n_clusters))
])
max_df参数设置为 0.4 的较低值,表示忽略 40%以上的文档中出现的任何单词。 此参数对于删除功能词本身无价值的意义非常大。
注意
删除 40%以上的文档中出现的任何单词都会删除功能单词,这对于我们在第 9 章和“作者身份”中看到的工作来说,这种类型的预处理非常无用。
然后,我们拟合并预测该管道。 到目前为止,在该模块中,我们已经针对分类任务执行了多次此过程,但是这里有所不同-我们没有将数据集的目标类提供给 fit 函数。 这就是使它成为无监督学习任务的原因! 代码如下:
pipeline.fit(documents)
labels = pipeline.predict(documents)
labels变量现在包含每个样本的簇号。 带有相同标签的样品被称为属于同一类。 应当注意,集群标签本身是没有意义的:集群 1 和 2 与集群 1 和 3 不再相似。
我们可以使用Counter类查看每个簇中放置了多少样本:
from collections import Counter
c = Counter(labels)
for cluster_number in range(n_clusters):
print("Cluster {} contains {} samples".format(cluster_number, c[cluster_number]))
结果中的许多(请记住,您的数据集将与我的完全不同)由一个包含大多数实例的大型群集,几个中等群集以及一些仅包含一个或两个实例的群集组成。 在许多群集应用中,这种不平衡是很正常的。
评估结果
聚类主要是的探索性分析,因此难以有效评估聚类算法的结果。 一种直接的方法是根据算法尝试学习的标准来评估算法。
注意
如果有测试集,则可以根据它评估聚类。 有关更多详细信息,请访问这个页面。
对于 k 均值算法,开发质心时使用的标准是最小化每个样本到其最近质心的距离。 这称为算法的惯性,可以从对其进行调用的任何KMeans实例中检索出来:
pipeline.named_steps['clusterer'].inertia_
我的数据集上的结果是 343.94。 不幸的是,这个值本身是毫无意义的,但是我们可以使用它来确定应该使用多少个群集。 在前面的示例中,我们将n_clusters设置为 10,但这是最佳值吗? 下面的代码运行 10 次 k-means 算法,其中n_clusters的每个值从 2 到 20。每次运行时,它记录结果的惯性。
我们仅将n_clusters的每个值拟合X矩阵一次,以(大幅)提高此代码的速度:
inertia_scores = []
n_cluster_values = list(range(2, 20))
for n_clusters in n_cluster_values:
cur_inertia_scores = []
X = TfidfVectorizer(max_df=0.4).fit_transform(documents)
for i in range(10):
km = KMeans(n_clusters=n_clusters).fit(X)
cur_inertia_scores.append(km.inertia_)
inertia_scores.append(cur_inertia_scores)
inertia_scores变量现在包含 2 到 20 之间的每个n_clusters值的惯性分数列表。我们可以对其进行绘图,以了解该值如何与n_clusters相互作用:
总体而言,随着簇数的增加,惯性的值应随着改进的减少而减小,这可以从这些结果中大致看到。 值 6 到 7 之间的增加仅是由于选择质心的随机性,这直接影响最终结果的质量。 尽管如此,总的趋势是(在这些结果中;您的结果可能会有所不同),大约是 6 个簇是最后一次惯性发生重大改善。
此后,尽管很难对诸如此类的模糊标准做出具体规定,但对惯性仅作了些微改进。 寻找这种类型的图案称为弯头规则,因为我们正在图中寻找弯头式弯头。 一些数据集的弯头更明显,但是不能保证甚至出现此功能(某些图形可能很平滑!)。
基于此分析,我们将n_clusters设置为 6,然后重新运行该算法:
n_clusters = 6
pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)),
('clusterer', KMeans(n_clusters=n_clusters))
])
pipeline.fit(documents)
labels = pipeline.predict(documents)
从集群中提取主题信息
现在我们将目光聚焦在集群上,以尝试发现每个集群中的主题。 我们首先从特征提取步骤中提取term列表:
terms = pipeline.named_steps['feature_extraction'].get_feature_names()
我们还设置了另一个计数器来计算每个类的大小:
c = Counter(labels)
遍历每个集群,我们像以前一样打印集群的大小。 在评估结果时,请记住群集的大小,这一点很重要-一些群集仅具有一个样本,因此并不表示总体趋势。 代码如下:
for cluster_number in range(n_clusters):
print("Cluster {} contains {} samples".format(cluster_number, c[cluster_number]))
接下来(仍然在循环中),我们遍历此集群的最重要术语。 为此,我们从质心中选取五个最大的值,方法是找到质心本身中具有最高值的要素。 代码如下:
print(" Most important terms")
centroid = pipeline.named_steps['clusterer'].cluster_centers_[cluster_number]
most_important = centroid.argsort()
然后,我们打印出最重要的五个术语:
for i in range(5):
我们在这一行中使用i的取反,因为我们的most_important数组首先以最低值排序:
term_index = most_important[-(i+1)]
然后,我们为该值打印等级,术语和得分:
print(" {0}) {1} (score: {2:.4f})".format(i+1, terms[term_index], centroid[term_index]))
结果可以很好地指示当前趋势。 在我的结果(2015 年 3 月)中,这些分类对应于健康问题,中东紧张局势,朝鲜紧张局势和俄罗斯事务。 这些是这段时间新闻频繁出现的主要主题-尽管多年来一直没有改变!
使用聚类算法作为转换器
作为的补充说明,关于 k 均值算法(和任何聚类算法)的一个有趣特性是可以将其用于特征约简。 有许多方法可以减少要素的数量(或创建新的要素以将数据集嵌入到要素上),例如主成分分析,潜在语义索引等。 这些算法中的许多问题之一是它们通常需要大量的计算能力。
在前面的示例中,术语列表中有 14,000 多个条目-这是一个很大的数据集。 我们的 k 均值算法将其转换为六个簇。 然后,我们可以通过将每个质心的距离作为特征来创建特征数量少得多的数据集。 代码如下:
为此,我们在 KMeans 实例上调用 transform 函数。 我们的管道适合于此目的,因为它的末尾有一个 k-means 实例:
X = pipeline.transform(documents)
这将在流水线的最后一步调用转换方法,这是 k 均值的一个实例。 这将导致具有六个特征的矩阵,并且样本数与文档的长度相同。
然后,您可以对结果执行自己的第二级聚类,如果有目标值,则可以将其用于分类。 为此,可能的工作流程是使用监督数据执行某些特征选择,使用聚类将特征数量减少到更易于管理的数量,然后在分类算法(例如 SVM)中使用结果。
聚类合奏
在第 3 章,“用决策树”预测运动优胜者中,我们研究了使用随机森林算法的分类集成,该集成是许多低质量, 基于分类器。 也可以使用聚类算法进行组装。 这样做的关键原因之一是平滑算法多次运行的结果。 正如我们之前看到的,根据初始质心的选择,运行 k 均值的结果会有所不同。 可以通过多次运行算法然后组合结果来减少差异。
集成还可以减少选择参数对最终结果的影响。 大多数聚类算法对为该算法选择的参数值非常敏感。 选择稍有不同的参数将导致不同的群集。
证据积累
作为基本合奏,我们可以首先对数据进行多次聚类并记录每次运行的标签。 然后,我们记录每对样本在一个新矩阵中聚集在一起的次数。 这是证据累积聚类(EAC)算法的本质。
EAC 有两个主要步骤。 第一步是使用低级聚类算法(例如 k 均值)对数据进行多次聚类,并在每次迭代中记录样本在同一聚类中的频率。 该存储在coassociation矩阵中。 第二步是对生成的coassociation矩阵执行聚类分析,该分析使用另一种称为层次聚类的聚类算法执行。 该具有有趣的属性,因为它在数学上与找到将所有节点链接在一起并删除弱链接的树相同。
通过遍历每个标签并记录两个样本具有相同标签的地方,我们可以从标签数组中创建一个coassociation矩阵。 我们使用 SciPy 的csr_matrix,它是一种稀疏矩阵:
from scipy.sparse import csr_matrix
我们的函数定义带有一组标签:
def create_coassociation_matrix(labels):
然后,我们记录每个匹配项的行和列。 我们在列表中进行这些操作。 稀疏矩阵通常只是记录非零值位置的列表的集合,csr_matrix是这种稀疏矩阵的示例:
rows = []
cols = []
然后,我们遍历每个单独的标签:
unique_labels = set(labels)
for label in unique_labels:
我们寻找具有该标签的所有样品:
indices = np.where(labels == label)[0]
对于带有前面标签的每对样本,我们将两个样本的位置记录在列表中。 代码如下:
for index1 in indices:
for index2 in indices:
rows.append(index1)
cols.append(index2)
在所有循环之外,我们然后创建数据,每次将两个样本一起列出时,该数据的值就是 1。 通过注意总共在我们的标签集中有多少个匹配项,可以得出 1 的数量。 代码如下:
data = np.ones((len(rows),))
return csr_matrix((data, (rows, cols)), dtype='float')
要从标签中获取关联矩阵,我们只需调用此函数:
C = create_coassociation_matrix(labels)
从这里,我们可以将这些矩阵的多个实例加在一起。 这使我们可以合并多次 k 均值的结果。 打印出 C(只需将 C 输入一个新的单元格并运行它)将告诉您其中有多少个单元格具有非零值。 在我的情况下,大约一半的单元格中都有值,因为我的聚类结果具有较大的聚类(聚类越多,非零值的数量越少)。
下一步涉及协同关联矩阵的层次聚类。 我们将通过在此矩阵上找到最小的生成树并删除权重低于给定阈值的边来做到这一点。
在图理论中,生成树是图上将所有节点连接在一起的一组边。 最小生成树(MST)只是具有最低总权重的生成树。 对于我们的应用,图中的节点是数据集中的样本,边缘权重是这两个样本聚在一起的次数,即,来自我们的关联矩阵的值。
在下图中,显示了六个节点的图形上的 MST。 图中的节点可以在 MST 中多次使用。 生成树的唯一标准是所有节点都应连接在一起。
为了计算 MST,我们使用 SciPy 的minimum_spanning_tree函数,该函数在sparse程序包中找到:
from scipy.sparse.csgraph import minimum_spanning_tree
可以直接在我们的关联函数返回的稀疏矩阵上调用mst函数:
mst = minimum_spanning_tree(C)
但是,在我们的关联矩阵C中,较高的值表示样本聚集在一起的频率更高,即相似度值。 相反,minimum_spanning_tree将输入视为距离,但得分较高。 由于这个原因,我们改为根据协关联矩阵的求反来计算最小生成树:
mst = minimum_spanning_tree(-C)
上一个函数的结果是一个矩阵的大小与协关联矩阵的大小相同(行和列的数量与数据集中的样本数相同),仅保留了 MST 中的边,所有其他边均被删除。
然后,我们删除任何权重小于预定义阈值的节点。 为此,我们在 MST 矩阵的边缘上进行迭代,删除所有小于特定值的边缘。 我们不能仅凭一个在关联矩阵中的迭代来测试这一点(值将为 1 或 0,因此没有太多需要处理的)。 因此,我们将首先创建额外的标签,创建协关联矩阵,然后将两个矩阵加在一起。 代码如下:
pipeline.fit(documents)
labels2 = pipeline.predict(documents)
C2 = create_coassociation_matrix(labels2)
C_sum = (C + C2) / 2
然后,我们计算 MST 并删除这两个标签中未出现的任何边:
mst = minimum_spanning_tree(-C_sum)
mst.data[mst.data > -1] = 0
我们想要切掉的阈值是两个聚类中都不存在的任何边缘,即值为 1。但是,当我们否定协关联矩阵时,我们也必须否定该阈值。
最后,我们找到所有连接的分量,这是在去除低权重的边缘之后找到仍通过边缘连接的所有样本的一种简单方法。 返回的第一个值是连接的组件数(即簇数),第二个是每个样本的标签。 代码如下:
from scipy.sparse.csgraph import connected_components
number_of_clusters, labels = connected_components(mst)
在我的数据集中,我获得了八个聚类,这些聚类与以前大致相同。 鉴于我们仅使用了两次 k 均值迭代,这不足为奇。 使用更多的 k 均值迭代(如我们在下一节中所做的那样)将导致更多的方差。
工作原理
在 k 均值算法中,使用每个特征时均不考虑其权重。 从本质上讲,所有功能均假定为相同比例。 我们在第 2 章,“用 scikit-learn 估计器分类”中看到了不具有缩放功能的问题。 结果是 k-means 正在寻找圆形簇,如以下屏幕截图所示:
正如我们在前面的屏幕截图中所看到的,并不是所有的集群都具有这种形状。 蓝色簇是圆形的,并且是 k 均值非常擅长的类型。 红色簇是椭圆形。 k 均值算法可以通过某种特征缩放来拾取这种形状的聚类。 第三个簇甚至不是凸的-k 均值将很难发现,这是一个奇怪的形状。
EAC 算法通过将特征重新映射到新的空间来工作,实质上是使用与上一节中使用 k 均值进行特征约简的原理相同的原理将 k 均值算法的每次运行转换为变形器。 但是,在这种情况下,我们仅使用实际标签,而不使用到每个质心的距离。 这是在关联矩阵中记录的数据。
结果是,EAC 现在只在乎事物之间的距离,而不必关心它们在原始特征空间中的放置方式。 围绕未缩放的功能仍然存在问题。 特征缩放很重要,并且无论如何都应该进行(我们在本章中使用tf-idf进行了缩放,这导致特征值具有相同的缩放比例)。
通过在 SVM 中使用内核,我们在第 9 章,“作者身份”中看到了类似的转换形式。 这些转换非常强大,对于复杂的数据集应牢记。
实施
综合所有这些,我们现在可以创建一个适合 scikit-learn 接口的简单聚类算法,该接口执行 EAC 中的所有步骤。 首先,我们使用 scikit-learn 的ClusterMixin创建该类的基本结构:
from sklearn.base import BaseEstimator, ClusterMixin
class EAC(BaseEstimator, ClusterMixin):
我们的参数是第一步(创建协关联矩阵)要执行的 k 均值聚类数,要在其中截止的阈值以及在每个 k 均值聚类中要找到的聚类数。 我们设置n_clusters的范围是为了在我们的 k 均值迭代中获得很多方差。 通常,从整体上讲,差异是一件好事; 没有它,解决方案将不会比单个聚类更好(也就是说,高方差并不表示整体会更好)。 代码如下:
def __init__(self, n_clusterings=10, cut_threshold=0.5, n_clusters_range=(3, 10)):
self.n_clusterings = n_clusterings
self.cut_threshold = cut_threshold
self.n_clusters_range = n_clusters_range
接下来是我们的EAC类的fit函数:
def fit(self, X, y=None):
然后,我们使用 k 均值执行低级聚类,并对每次迭代所得的关联矩阵求和。 我们在生成器中执行此操作以节省内存,仅在需要它们时才创建协关联矩阵。 在此生成器的每次迭代中,我们使用数据集创建一个新的单 k 均值,然后为其创建协关联矩阵。 我们使用sum将它们加在一起。 代码如下:
C = sum((create_coassociation_matrix(self._single_clustering(X))
for i in range(self.n_clusterings)))
和以前一样,我们创建 MST,删除小于给定阈值的任何边(如前所述,适当取负值),然后找到连接的组件。 与 scikit-learn 中的任何fit函数一样,我们需要返回 self 以使类有效地在管道中工作。 代码如下:
mst = minimum_spanning_tree(-C)
mst.data[mst.data > -self.cut_threshold] = 0
self.n_components, self.labels_ = connected_components(mst)
return self
然后,我们编写函数以对单个迭代进行聚类。 为此,我们使用 NumPy 的randint函数和我们的n_clusters_range参数随机选择了多个聚类,以设置可能值的范围。 然后,我们使用 k 均值对数据集进行聚类和预测。 这里的返回值将是来自 k 均值的标签。 代码如下:
def _single_clustering(self, X):
n_clusters = np.random.randint(*self.n_clusters_range)
km = KMeans(n_clusters=n_clusters)
return km.fit_predict(X)
现在,我们可以像以前一样通过建立管道并使用 EAC(以前使用KMeans实例作为管道的最后阶段)来在以前的代码上运行此代码。 代码如下:
pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)),
('clusterer', EAC())
])
在线学习
在某些情况下,在我们开始学习之前,我们没有训练所需的所有数据。 有时,我们在等待新数据到达,也许我们拥有的数据太大而无法容纳到内存中,或者在做出预测后我们收到了额外的数据。 在这种情况下,在线学习是随时间推移训练模型的一种选择。
在线学习简介
在线学习是随着新数据的到来而对模型进行的增量更新。 支持在线学习的算法可以一次训练一个或几个样本,并在新样本到达时进行更新。 相反,非在线算法要求立即访问所有数据。 标准 k 均值算法就是这样,到目前为止我们在本模块中看到的大多数算法都是这样。
在线版本的算法仅用几个样本就可以部分更新其模型。 神经网络是一种以在线方式工作的算法的标准示例。 当将新样本提供给神经网络时,网络中的权重会根据学习率进行更新,该学习率通常是一个非常小的值,例如 0.01。 这意味着任何单个实例都只会对模型进行较小(但希望有所改善)的更改。
神经网络也可以以批处理模式进行训练,其中一次给出一组样本,并且训练是一步完成的。 在批处理模式下,算法更快,但使用更多的内存。
同样地,我们可以在单个或少量样本后稍微更新 k 均值质心。 为此,我们在 k 均值算法的更新步骤中将学习率应用于质心运动。 假设样本是从总体中随机选择的,则质心应趋向于它们在标准,离线和 k 均值算法中的位置。
在线学习与基于流的学习有关; 但是,有一些重要的区别。 在线学习能够在模型中使用较旧的样本后对其进行审查,而基于流的机器学习算法通常仅一次通过,也就是说,只有一次机会查看每个样本。
实施
scikit-learn 软件包包含MiniBatchKMeans算法,该算法允许在线学习。 该类实现partial_fit函数,该函数获取一组样本并更新模型。 相反,调用fit()将删除以前的所有训练,仅在新数据上重新拟合模型。
MiniBatchKMeans遵循与 scikit-learn 中其他算法相同的聚类格式,因此创建和使用它与其他算法非常相似。
因此,我们可以通过使用TfIDFVectorizer从数据集中提取特征来创建矩阵X,然后从中进行采样以逐步更新模型。 代码如下:
vec = TfidfVectorizer(max_df=0.4)
X = vec.fit_transform(documents)
然后,我们导入MiniBatchKMeans并创建一个实例:
from sklearn.cluster import MiniBatchKMeans
mbkm = MiniBatchKMeans(random_state=14, n_clusters=3)
接下来,我们将从X矩阵中随机采样以模拟来自外部源的数据。 每次输入一些数据时,我们都会更新模型:
batch_size = 10
for iteration in range(int(X.shape[0] / batch_size)):
start = batch_size * iteration
end = batch_size * (iteration + 1)
mbkm.partial_fit(X[start:end])
然后,我们可以通过要求实例进行预测来获取原始数据集的标签:
labels = mbkm.predict(X)
不过,在此阶段,我们无法在管道中执行此操作,因为TfIDFVectorizer不是在线算法。 为了解决这个问题,我们使用HashingVectorizer。 HashingVectorizer类巧妙地使用了散列算法,从而大大减少了计算词袋模型的内存。 我们只记录那些名称的哈希,而不是记录特征名称(如文档中的单词)。 这使我们甚至在查看数据集之前,就可以知道我们的功能,因为它是所有可能散列的集合。 这是一个非常大的数字,通常约为 2 ^ 18。 使用稀疏矩阵,我们甚至可以很容易地存储和计算这种大小的矩阵,因为很大一部分矩阵的值为 0。
当前,Pipeline类不允许在在线学习中使用它。 在不同的应用中存在一些细微差别,这意味着没有一种明显的“一刀切”的方法可以实施。 相反,我们可以创建自己的Pipeline子类,使我们可以将其用于在线学习。 我们首先从Pipeline派生我们的类,因为我们只需要实现一个函数:
class PartialFitPipeline(Pipeline):
我们创建一个类函数partial_fit,它接受输入矩阵和一个可选的类集(尽管本实验中不需要这些类):
def partial_fit(self, X, y=None):
我们之前介绍过的管道是一组转换,其中一步的输入是上一步的输出。 为此,我们将第一个输入设置为X矩阵,然后遍历每个转换器以转换此数据:
Xt = X
for name, transform in self.steps[:-1]:
然后,我们转换当前数据集并继续进行迭代,直到完成最后一步(在本例中为聚类算法):
Xt = transform.transform(Xt)
然后,我们在最后一步调用partial_fit函数并返回结果:
return self.steps[-1][1].partial_fit(Xt, y=y)
现在,我们可以与HashingVectorizer一起创建一个管道,以在在线学习中使用MiniBatchKMeans。 除了使用我们的新类PartialFitPipeline和HashingVectorizer外,此过程与本章其余部分所用的过程相同,只是一次只能容纳几个文档。 代码如下:
pipeline = PartialFitPipeline([('feature_extraction', HashingVectorizer()),
('clusterer', MiniBatchKMeans(random_state=14, n_clusters=3))
])
batch_size = 10
for iteration in range(int(len(documents) / batch_size)):
start = batch_size * iteration
end = batch_size * (iteration + 1)
pipeline.partial_fit(documents[start:end])
labels = pipeline.predict(documents)
不过,仍存在一些缺点。 首先,我们无法轻松地找出每个单词中最重要的单词。 我们可以通过拟合另一个CountVectorizer并获取每个单词的哈希值来解决此问题。 然后,我们通过哈希而不是单词来查找值。 这有点麻烦,并且会破坏使用HashingVectorizer的内存。 此外,我们无法使用之前使用的max_df参数,因为它要求我们知道功能的含义并随时间对其进行计数。
在线进行训练时,我们也无法使用tf-idf加权。 可以对此进行近似并应用这样的加权,但这又是一个麻烦的方法。 HashingVectorizer仍然是一种非常有用的算法,并且大量使用了哈希算法。
三十二、使用深度学习分类图像中的对象
我们在第 8 章,“和神经网络”打败验证码中使用了基本的神经网络。 该地区最近的大量研究已使该基础设计取得了许多重大进展。 如今,神经网络的研究正在许多领域中创建一些最先进,最准确的分类算法。
这些进步来自计算能力的提高,这使我们能够训练更大,更复杂的网络。 但是,这些进步远不只是简单地为该问题投入更多的计算能力。 新的算法和层类型大大提高了性能,超出了计算能力。
在本章中,我们将研究确定图像中表示的对象。 像素值将用作输入,然后神经网络将自动找到有用的像素组合以形成更高级别的特征。 然后将这些用于实际分类。 总体而言,在本章中,我们将研究以下内容:
- 分类图像中的对象
- 不同类型的深度神经网络
- Theano,Lasagne 和 nolearn; 库来构建和训练神经网络
- 使用 GPU 来提高算法速度
对象分类
计算机视觉正成为未来技术的重要组成部分。 例如,我们将在未来五年内使用自动驾驶汽车(如果相信一些传闻,则可能会更快)。 为了实现这一目标,汽车的计算机必须能够看到周围的物体:障碍物,其他交通状况和天气状况。
尽管我们可以很容易地检测到是否存在障碍物(例如使用雷达),但知道该物体是什么也很重要。 如果是动物,它可能会移开。 如果它是建筑物,它将完全不会移动,我们需要绕开它。
应用场景和目标
在本章中,我们将构建一个系统,该系统将图像作为输入并预测其中的对象是什么。 我们将扮演汽车视觉系统的角色,环视道路或道路两侧的任何障碍物。 图像具有以下形式:
该数据集来自一个流行的数据集 CIFAR-10。 它包含 60,000 张图像,这些图像的宽度为 32 像素,高度为 32 像素,每个像素都有一个红绿蓝(RGB)值。 数据集已经分为训练和测试,尽管直到完成训练后我们才使用测试数据集。
注意
CIFAR-10 数据集可从以下位置下载。 下载 python 版本,该版本已转换为 NumPy 数组。
打开一个新的 IPython Notebook,我们可以看到数据的样子。 首先,我们设置数据文件名。 我们只担心第一个批次开始,并在最后增加到完整的数据集大小。
import os
data_folder = os.path.join(os.path.expanduser("~"), "Data", "cifar-10-batches-py")
batch1_filename = os.path.join(data_folder, "data_batch_1")
接下来,我们创建一个可以读取批处理中存储的数据的函数。 批处理已使用 pickle 保存,pickle 是用于保存对象的 python 库。 通常,我们只需在文件上调用pickle.load即可获取对象。 但是,此数据有一个小问题:它已保存在 Python 2 中,但是我们需要在 Python 3 中打开它。为了解决此问题,我们将编码设置为拉丁语(即使我们 正在以字节模式打开它):
import pickle
# Bigfix thanks to: http://stackoverflow.com/questions/11305790/pickle-incompatability-of-numpy-arrays-between-python-2-and-3
def unpickle(filename):
with open(filename, 'rb') as fo:
return pickle.load(fo, encoding='latin1')
使用此功能,我们现在可以加载批处理数据集:
batch1 = unpickle(batch1_filename)
该批处理是一个字典,其中包含 NumPy 数组中的实际数据,相应的标签和文件名,最后是一个注释,说明该批处理是什么(例如,这是训练 5 的第 1 批)。
我们可以通过使用批次数据键中的索引来提取图像:
image_index = 100
image = batch1['data'][image_index]
图像数组是一个 NumPy 数组,具有 3,072 个条目(从 0 到 255)。每个值都是图像中特定位置的红色,绿色或蓝色强度。
图像的格式与 matplotlib 通常使用的格式不同(用于显示图像),因此,要显示图像,我们首先需要对阵列进行整形并旋转矩阵。 训练我们的神经网络并不重要(我们将以适合数据的方式定义网络),但是为了 matplotlib 的缘故,我们确实需要对其进行转换:
image = image.reshape((32,32, 3), order='F')
import numpy as np
image = np.rot90(image, -1)
现在我们可以使用 matplotlib 显示图像:
%matplotlib inline
from matplotlib import pyplot as plt
plt.imshow(image)
显示结果图像,一条船:
该图像的分辨率非常差-仅 32 像素宽和 32 像素高。 尽管如此,大多数人还是会看着图像并看到一条船。 我们可以让计算机做同样的事情吗?
您可以更改图像索引以显示不同的图像,以了解数据集的属性。
在本章中,我们项目的目的是建立一个分类系统,该系统可以拍摄这样的图像并预测其中的对象。
用例
在许多情况下都使用计算机视觉。
在线地图网站(例如 Google Maps)出于多种原因使用计算机视觉。 原因之一是自动模糊他们发现的任何面孔,以便为作为街景功能一部分的被拍照人员提供一定的隐私保护。
人脸检测还用于许多行业。 现代相机会自动检测人脸,以提高所拍摄照片的质量(用户最常希望将焦点对准可见的脸)。 人脸检测也可以用于识别。 例如,Facebook 自动识别照片中的人物,从而轻松标记朋友。
正如我们之前所述,自动驾驶汽车高度依赖计算机视觉来识别其路径并避免障碍。 计算机视觉是不仅在自动驾驶汽车研究中要解决的关键问题之一,不仅是供消费者使用,还包括采矿和其他行业。
其他行业也在使用计算机视觉,包括仓库自动检查货物是否有缺陷。
航天工业也在使用计算机视觉,以帮助自动化数据收集。 这对于有效利用航天器至关重要,因为从地球向火星上的漫游者发送信号可能会花费很长时间,并且在某些时候是不可能的(例如,当两个行星彼此不面对时)。 随着我们越来越频繁地从更远的距离开始处理天基飞行器,绝对有必要提高这些航天器的自主性。
以下屏幕截图显示了由 NASA 设计和使用的火星探测器; 它充分利用了计算机视觉:
深度神经网络
我们在第 8 章和“用神经网络”击败 CAPTCHAs 中使用的神经网络具有一些出色的理论特性。 例如,只需要一个隐藏层即可学习任何映射(尽管中间层的大小可能需要非常大)。 神经网络在 1970 年代和 1980 年代是非常活跃的研究领域,因此不再使用这些网络,特别是与其他分类算法(例如支持向量机)相比。 主要问题之一是运行许多神经网络所需的计算能力比其他算法还要多,而且比许多人可以访问的能力还要多。
另一个问题是训练网络。 虽然反向传播算法已经有一段时间了,但是它在较大的网络中存在问题,需要在权重确定之前进行大量的训练。
这些问题中的每一个都已在近期得到解决,从而导致了神经网络的流行。 现在比 30 年前更容易获得计算能力,并且训练算法的进步意味着我们现在可以随时使用该能力。
直觉
在第 8 章,“和神经网络”中,将深度神经网络与更基本的神经网络区分开来。 当神经网络具有两个或多个隐藏层时,它被认为是。 在实践中,无论是在每层的节点数还是在层数上,深度神经网络通常都大得多。 虽然 2000 年代中期的一些研究集中在非常大量的层上,但是更智能的算法正在减少所需的实际层数。
神经网络基本上将非常基本的特征作为输入-在计算机视觉的情况下,它是简单的像素值。 然后,随着数据的组合和通过网络的推送,这些基本功能将合并为更复杂的功能。 有时,这些功能对人类意义不大,但它们代表了计算机进行分类所需的样本方面。
实施
由于它们的规模,实现这些深度神经网络可能非常具有挑战性。 一个不好的实现比一个好的实现要花费更长的时间,并且由于内存的使用甚至可能根本无法运行。
神经网络的基本实现可能首先创建一个节点类,然后将它们的集合收集到一个层类中。 然后,使用 Edge 类的实例将每个节点连接到下一层中的节点。 这种实现是基于类的实现,很好地展示了网络是如何工作的,但对于大型网络而言效率太低。
神经网络的核心只是简单的矩阵数学表达式。 一个网络与下一个网络之间的连接权重可以表示为值矩阵,其中行表示第一层中的节点,列表示第二层中的节点(有时也使用此矩阵的转置) 。 该值是一层与下一层之间的边缘的权重。 然后可以将网络定义为这些权重矩阵的集合。 除了节点之外,我们还向每层添加一个偏差项,该偏差项基本上是一个始终位于并连接到下一层中每个神经元的节点。
这种见解使我们能够使用数学运算来构建,训练和使用神经网络,而不是创建基于类的实现。 这些数学运算非常棒,因为已经编写了许多很棒的高度优化的代码库,我们可以使用它们尽可能高效地执行这些计算。
我们在第 8 章,“用神经网络”击败 CAPTCHAs 中使用的PyBrain库确实包含用于神经网络的简单卷积层。 但是,它没有为我们提供此应用所需的某些功能。 但是,对于更大,更自定义的网络,我们需要一个库,该库可以为我们提供更多功能。 因此,将使用Lasagne和nolearn库。 该库在Theano库上运行,该库是用于数学表达式的有用工具。
在本章中,我们将从使用Lasagne实现基本的神经网络开始,以介绍概念。 然后,我们将使用nolearn在第 8 章,“和神经网络”预测图像中的哪个字母上复制实验。 最后,我们将使用复杂得多的卷积神经网络对 CIFAR 数据集进行图像分类,这还将包括在 GPU 而不是 CPU 上运行该算法以提高性能。
Theano 简介
Theano 是一个库,可让您构建数学表达式并运行它们。 尽管该似乎与我们通常编写程序的并没有什么不同,但在 Theano 中,我们定义了要执行的功能,而不是其计算方式。 这使 Theano 可以优化表达式的评估并执行延迟计算-仅在需要时才实际计算表达式,而在定义它们时才进行计算。
许多程序员每天都不使用这种类型的编程,但是大多数程序员都与相关的系统交互。 关系数据库,特别是基于 SQL 的数据库,使用了称为声明式范式的概念。 尽管程序员可能使用 WHERE 子句在数据库上定义了 SELECT 查询,但数据库会对此进行解释并根据多种因素(例如是否 ] WHERE 子句位于主键上,数据存储的格式以及其他因素上。 程序员定义他们想要的东西,然后系统确定如何做。
注意
您可以使用 pip pip3 install Theano安装 Theano。
使用 Theano,我们可以定义许多用于标量,数组和矩阵的函数,以及其他数学表达式。 例如,我们可以创建一个函数来计算直角三角形的斜边的长度:
import theano
from theano import tensor as T
首先,我们定义两个输入 a 和 b。 这些是简单的数值,因此我们将它们定义为标量:
a = T.dscalar()
b = T.dscalar()
然后,我们定义输出c。 这是一个基于a和b值的表达式:
c = T.sqrt(a ** 2 + b ** 2)
请注意,此处c不是函数或值,它只是给定a和b的表达式。 还要注意a和b没有实际值-这是一个代数表达式,而不是绝对值。 为了对此进行计算,我们定义一个函数:
f = theano.function([a,b], c)
该基本上告诉 Theano 创建一个函数,该函数将a和b的值作为输入,并根据给定的值计算返回c作为输出。 例如,f(3, 4)返回5。
尽管这个简单的示例似乎没有比 Python 强大的功能,但我们现在可以在其他代码部分和其余映射中使用函数或数学表达式c。 另外,虽然我们在定义函数之前就定义了c,但是直到调用函数之前,才进行实际的计算。
千层面的介绍
Theano 并不是构建神经网络的库。 以类似的方式,NumPy 并不是执行机器学习的库; 它只是执行繁重的任务,通常在其他库中使用。 Lasagne 是一个这样的库,它是专门为构建神经网络而设计的,使用 Theano 进行计算。
Lasagne 实现了许多现代类型的神经网络层,以及用于构建它们的构建块。
其中包括:
- 网络中的网络层:这些是小型的神经网络,比传统的神经网络层更易于解释。
- 脱落层:这些在训练过程中随机脱落的单元,可防止过度拟合,这是神经网络中的主要问题。
- 噪声层:这些层将噪声引入神经元。 再次,解决过度拟合的问题。
在本章中,我们将使用convolution layers(用来模拟人类视觉工作方式的图层)。 他们使用连接神经元的小集合,这些神经元仅分析输入值的一部分(在这种情况下为图像)。 这允许网络处理标准更改,例如处理图像的翻译。 在基于视觉的实验中,卷积层处理的变化示例是平移图像。
相比之下,传统的神经网络通常连接紧密,一层中的所有神经元都连接到下一层中的所有神经元。
卷积网络是在lasagne.layers.Conv1DLayer和lasagne.layers.Conv2DLayer类中实现的。
注意
在撰写本文时,Lasagne 尚未正式发布,也没有发布在pip上。 您可以从github安装它。 在新文件夹中,使用以下命令下载源代码存储库:
git clone https://github.com/Lasagne/Lasagne.git
在创建的 Lasagne 文件夹中,然后可以使用以下命令安装该库:
sudo python3 setup.py install
请参阅这个页面了解安装说明。
神经网络使用卷积层(通常仅使用卷积神经网络)以及pooling层,它们在特定区域内获得最大输出。 这样可以减少由图像的微小变化引起的噪声,并减少(或下采样)信息量。 这具有减少后续层中需要完成的工作量的额外好处。
Lasagne 还实现了这些池化层,例如在lasagne.layers.MaxPool2DLayer类中。 与卷积层一起,我们拥有构建卷积神经网络所需的所有工具。
在 Lasagne 中构建神经网络比仅使用 Theano 构建神经网络容易。 为了展示这些原理,我们将实现一个基于 Iris 数据集的基本网络,我们在第 1 章,“数据挖掘入门”中看到了该数据集。 Iris 数据集非常适合测试新算法,甚至包括深度神经网络等复杂算法。
首先,打开一个新的 IPython Notebook。 在本章的后面,我们将返回加载了 CIFAR 数据集的 Notebook。
首先,我们加载数据集:
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data.astype(np.float32)
y_true = iris.target.astype(np.int32)
由于 Lasagne 的工作方式,我们需要更加明确地说明数据类型。 这就是为什么我们将类转换为int32(它们在原始数据集中存储为int64)的原因。
然后,我们分为训练和测试数据集:
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y_true, random_state=14)
接下来,我们通过创建不同的层来构建我们的网络。 我们的数据集包含四个输入变量和三个输出类。 这给了我们第一层和最后一层的大小,但没有中间的层。 玩弄这个数字会得出不同的结果,值得追踪不同的值以查看会发生什么。
我们从创建一个输入层开始,该输入层具有与数据集相同数量的节点。 我们可以指定一个批处理大小(该值为 10),这使 Lasagne 可以在训练中进行一些优化:
import lasagne
input_layer = lasagne.layers.InputLayer(shape=(10, X.shape[1]))
接下来,我们创建我们的隐藏层。 该层的输入来自我们的输入层(指定为第一个参数),该输入层具有 12 个节点,并使用 S 型非线性,这在第 8 章,“通过神经网络击败验证码”中;
hidden_layer = lasagne.layers.DenseLayer(input_layer, num_units=12, nonlinearity=lasagne.nonlinearities.sigmoid)
接下来,我们有一个输出层,它从具有三个节点(与类数相同)的隐藏层获取输入,并使用 softmax 非线性。 Softmax 是,通常用于神经网络的最后一层:
output_layer = lasagne.layers.DenseLayer(hidden_layer, num_units=3,
nonlinearity=lasagne.nonlinearities.softmax)
在千层面的用法中,此输出层是我们的network。 当我们在其中输入样本时,它将查看该输出层并获得输入到其中的层(第一个参数)。 这将以递归方式持续进行,直到到达输入层为止,该输入层将样本应用于自身,因为它没有输入层。 输入层中神经元的激活然后被馈送到其调用层(在我们的情况下为hidden_layer),然后一直传播到输出层。
为了训练我们的网络,我们现在需要定义一些训练功能,它们是基于 Theano 的功能。 为此,我们需要定义 Theano 表达式和用于训练的函数。 我们首先为输入样本,网络给定的输出和实际输出创建变量:
import theano.tensor as T
net_input = T.matrix('net_input')
net_output = output_layer.get_output(net_input)
true_output = T.ivector('true_output')
现在,我们可以定义损失函数,该函数告诉训练函数如何改善网络-它尝试根据此函数训练网络以最大程度地减少损失。 我们将使用的损失是分类交叉熵,它是对诸如我们这样的分类数据的度量。 这是网络给定的输出和我们期望的实际输出的函数:
loss = T.mean(T.nnet.categorical_crossentropy(net_output, true_output))
接下来,我们定义将改变网络权重的函数。 为此,我们从网络中获取所有参数,并创建一个函数(使用 Lasagne 提供的辅助函数),该函数可以调整权重以最大程度地减少损失。
all_params = lasagne.layers.get_all_params(output_layer)
updates = lasagne.updates.sgd(loss, all_params, learning_rate=0.1)
最后,我们创建基于 Theano 的功能来执行此训练,并获得网络的输出以进行测试:
import theano
train = theano.function([net_input, true_output], loss, updates=updates)
get_output = theano.function([net_input], net_output)
然后,我们可以在训练数据上调用训练函数,以对网络进行一次迭代训练。 这涉及获取每个样本,计算其预测类别,将这些预测与预期类别进行比较,并更新权重以最小化损失函数。 然后,我们执行这 1000 次,在这些迭代中逐步训练我们的网络:
for n in range(1000):
train(X_train, y_train)
接下来,我们可以通过计算输出的 F 分数进行评估。 首先,我们获得以下输出:
y_output = get_output(X_test)
注意
请注意,get_output是我们从神经网络获得的 Theano 函数,这就是为什么我们不需要将网络作为参数添加到此代码行的原因。
此结果y_output是最终输出层中每个神经元的激活。 通过发现哪个神经元具有最高的激活来创建实际的预测:
import numpy as np
y_pred = np.argmax(y_output, axis=1)
现在,y_pred是类预测的数组,就像我们在分类任务中所习惯的那样。 现在,我们可以使用以下预测来计算 F 分数:
from sklearn.metrics import f1_score
print(f1_score(y_test, y_pred))
结果是令人印象深刻的完美-1.0! 这意味着所有分类在测试数据中都是正确的:很好的结果(尽管这是一个更简单的数据集)。
如我们所见,虽然仅使用 Lasagne 即可开发和训练网络,但可能会有些尴尬。 为了解决这个问题,我们将使用nolearn,这是一个软件包,可以进一步将该过程包装在可通过 scikit-learn API 方便转换的代码中。
使用 nolearn 实现神经网络
nolearn软件包提供了千层面的包装。 我们无法通过在 Lasagne 中手动构建神经网络来进行的微调,但是该代码更具可读性且易于管理。
nolearn包实现了您可能想要构建的普通类型的复杂神经网络。 如果您想获得比nolearn更大的控制权,可以恢复使用 Lasagne,但要付出更多的训练和建设过程的代价。
要开始使用nolearn,我们将重新实现在第 8 章,“使用神经网络打败 CAPTCHA”的示例,以预测图像中代表了哪个字母。 我们将重新创建在第 8 章,“使用神经网络击败 CAPTCHA”中的密集神经网络。 首先,我们需要在笔记本中再次输入数据集构建代码。 有关此代码的功能的描述,请参见第 8 章和“用神经网络”击败验证码:
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from skimage.transform import resize
from skimage import transform as tf
from skimage.measure import label, regionprops
from sklearn.utils import check_random_state
from sklearn.preprocessing import OneHotEncoder
from sklearn.cross_validation import train_test_split
def create_captcha(text, shear=0, size=(100, 24)):
im = Image.new("L", size, "black")
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(r"Coval.otf", 22)
draw.text((2, 2), text, fill=1, font=font)
image = np.array(im)
affine_tf = tf.AffineTransform(shear=shear)
image = tf.warp(image, affine_tf)
return image / image.max()
def segment_image(image):
labeled_image = label(image > 0)
subimages = []
for region in regionprops(labeled_image):
start_x, start_y, end_x, end_y = region.bbox
subimages.append(image[start_x:end_x,start_y:end_y])
if len(subimages) == 0:
return [image,]
return subimages
random_state = check_random_state(14)
letters = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
shear_values = np.arange(0, 0.5, 0.05)
def generate_sample(random_state=None):
random_state = check_random_state(random_state)
letter = random_state.choice(letters)
shear = random_state.choice(shear_values)
return create_captcha(letter, shear=shear, size=(20, 20)), letters.index(letter)
dataset, targets = zip(*(generate_sample(random_state) for i in range(3000)))
dataset = np.array(dataset, dtype='float')
targets = np.array(targets)
onehot = OneHotEncoder()
y = onehot.fit_transform(targets.reshape(targets.shape[0],1))
y = y.todense().astype(np.float32)
dataset = np.array([resize(segment_image(sample)[0], (20, 20)) for sample in dataset])
X = dataset.reshape((dataset.shape[0], dataset.shape[1] * dataset.shape[2]))
X = X / X.max()
X = X.astype(np.float32)
X_train, X_test, y_train, y_test = \
train_test_split(X, y, train_size=0.9, random_state=14)
神经网络是层的集合。 在nolearn中实现一个层是组织这些层的外观的情况,就像在 PyBrain 中一样。 我们在第 8 章,“使用神经网络击败 CAPTCHAs”中的神经网络使用了完全连接的密集层。 这些是在 nolearn中实现的,这意味着我们可以在此处复制我们的基本网络结构。 首先,我们创建由输入层,密集隐藏层和密集输出层组成的层:
from lasagne import layers
layers=[
('input', layers.InputLayer),
('hidden', layers.DenseLayer),
('output', layers.DenseLayer),
]
然后,我们导入一些需求,我们将在使用它们时进行解释:
from lasagne import updates
from nolearn.lasagne import NeuralNet
from lasagne.nonlinearities import sigmoid, softmax
接下来,我们定义神经网络,将其表示为 scikit-learn-compatible 估计器:
net1 = NeuralNet(layers=layers,
请注意,我们还没有关闭括号,这是有意的。 此时,我们从每层的大小开始输入神经网络的参数:
input_shape=X.shape,
hidden_num_units=100,
output_num_units=26,
此处的参数与图层匹配。 换句话说,input_shape参数首先在我们的层中找到已输入名称的层,其工作方式与在管道中设置参数的方式几乎相同。
接下来,我们定义非线性。 同样,我们将sigmoid用于隐藏层,并将softmax用于输出层:
hidden_nonlinearity=sigmoid,
output_nonlinearity=softmax,
接下来,我们将使用偏置节点,这些节点是始终在隐藏层中打开的节点。 偏置节点对于训练网络很重要,因为它们允许神经元的激活来更具体地训练他们的问题。 举一个简化的例子,如果我们的预测始终偏离 4,则可以添加-4 的偏差以消除该偏差。 我们的偏差节点允许这样做,权重的训练决定了所使用的偏差量。
偏差是作为一组权重给出的,这意味着它的大小必须与该偏差附加到的图层的大小相同:
hidden_b=np.zeros((100,), dtype=np.float32),
接下来,我们定义网络将如何训练。 nolearn软件包没有与第 8 章和“用神经网络”击败 CAPTCHA 所使用的训练机制完全相同,因为它没有衰减权重的方法 。 但是,它确实具有动量,我们将使用它,以及高学习率和低动量值:
update=updates.momentum,
update_learning_rate=0.9,
update_momentum=0.1,
接下来,我们将该问题定义为回归问题。 在执行分类任务时,这可能看起来很奇怪。 但是,输出是实值,优化它们是一个回归问题,在训练中比尝试对分类进行优化要好得多:
regression=True,
最后,我们将训练的最大纪元数设置为 1,000,这非常适合良好的训练,而又不需要花费很长时间进行训练(对于此数据集;其他数据集可能需要或多或少的训练):
max_epochs=1000,
现在,我们可以关闭神经网络构造函数的括号;
)
接下来,我们在训练数据集上训练网络:
net1.fit(X_train, y_train)
现在我们可以评估经过训练的网络。 为此,我们获得网络的输出,并且与 Iris 示例一样,我们需要执行argmax来通过选择最高激活值来获得实际分类:
y_pred = net1.predict(X_test)
y_pred = y_pred.argmax(axis=1)
assert len(y_pred) == len(X_test)
if len(y_test.shape) > 1:
y_test = y_test.argmax(axis=1)
print(f1_score(y_test, y_pred))
结果同样令人印象深刻,这在我的机器上又是一个完美的成绩。 但是,由于nolearn程序包具有某些无法在此阶段直接控制的随机性,因此的结果可能会有所不同。
GPU 优化
神经网络的规模会很大。 这对内存使用有一些影响。 但是,有效的结构(例如稀疏矩阵)意味着我们通常不会遇到将神经网络拟合到内存中的问题。
当神经网络变大时,主要的问题是它们需要很长时间才能计算。 此外,一些数据集和神经网络将需要进行许多训练,才能很好地适应该数据集。 我们将在本章中训练的神经网络在功能强大的计算机上每个纪元要花费 8 分钟以上的时间,并且我们预计将运行数十个纪元,甚至可能数百个纪元。 一些较大的网络可能需要几个小时才能训练一个时期。 为了获得最佳性能,您可能正在考虑数千个训练周期。
数学显然不能在这里给出很好的结果。
一个积极的方面是,神经网络的核心是充满浮点运算。 由于神经网络训练主要由矩阵运算组成,因此还有许多运算可以并行执行。 这些因素意味着在 GPU 上进行计算是加快训练速度的有吸引力的选择。
何时使用 GPU 进行计算
GPU 最初旨在渲染图形以供显示。 这些图形使用矩阵和这些矩阵上的数学方程式表示,然后将其转换为我们在屏幕上看到的像素。 此过程涉及大量并行计算。 尽管现代 CPU 可能具有许多核心(您的计算机可能具有 2、4 甚至 16 个甚至更多!),但 GPU 具有数千个专门为图形设计的小型核心。
因此,CPU 更适合用于顺序任务,因为内核往往更快一些,并且诸如访问计算机内存等任务效率更高。 老实说,让 CPU 做繁重的工作也更容易。 几乎每个机器学习库都默认使用 CPU,并且在使用 GPU 进行计算之前还涉及其他工作。 但是,好处可能非常明显。
因此,GPU 更适合用于其中许多可以同时执行的数字小操作的任务。 许多机器学习任务就是这样,通过使用 GPU 来提高效率。
使您的代码在 GPU 上运行可能会令人沮丧。 这在很大程度上取决于您拥有的 GPU 类型,如何配置,您的操作系统以及您是否准备对计算机进行一些低级更改。
有以下三种主要途径:
- 首先是查看您的计算机,为您的 GPU 和操作系统搜索工具和驱动程序,浏览其中的许多教程,然后找到适合您情况的教程。 是否有效取决于您的系统。 就是说,这种情况比几年前容易得多,有了更好的工具和驱动程序可以执行支持 GPU 的计算。
- 第二种方法是选择一个系统,找到有关设置的良好文档,然后购买一个匹配的系统。 这将更好地工作,但可能会相当昂贵-在大多数现代计算机中,GPU 是最昂贵的部件之一。 如果您想从系统中获得出色的性能,那就尤其如此-您将需要一个非常好的 GPU,这可能会非常昂贵。
- 第三种途径是使用已为此目的配置的虚拟机。 例如,马库斯·贝辛格(Markus Beissinger)已经创建了一个在亚马逊网络服务上运行的系统。 该系统将花费您运行的钱,但价格却比新计算机便宜得多。 根据您所在的位置,所获得的确切系统以及使用量的多少,您每小时的收入可能会少于 1 美元,而且往往要少得多。 如果您在 Amazon Web Services 中使用竞价型实例,则可以以每小时几美分的价格运行它们(尽管您将需要开发代码以分别在竞价型实例上运行)。
如果您负担不起虚拟机的运行成本,建议您使用当前系统来研究第一个途径。 您还可以从家人或不断更新计算机的朋友那里获得良好的二手 GPU(游戏玩家朋友对此非常有用!)。
在 GPU 上运行我们的代码
将采用本章的第三种方法,并基于 Markus Beissinger 的基本系统创建一个虚拟机。 这将在亚马逊的 EC2 服务上运行。 还有许多其他的 Web 服务要使用,每个过程都将略有不同。 在本节中,我将概述 Amazon 的过程。
如果要使用自己的计算机并将其配置为运行启用 GPU 的计算,请随时跳过此部分。
注意
您可以在这个页面。
首先,在以下位置转到到 AWS 控制台:
console.aws.amazon.com/console/hom…
使用您的 Amazon 帐户登录。 如果您没有,则将提示您创建一个,然后继续进行操作。
单击启动实例,然后在右上角的下拉菜单中选择 N. California 作为您的位置。
单击社区 AMI,然后搜索ami-b141a2f5,这是 Markus Beissinger 创建的机器。 然后,单击选择。 在下一个屏幕上,选择 g2.2xlarge 作为机器类型,然后单击 Review and Launch。 在下一个屏幕上,单击启动。
此时,您将需要付费,因此请记住在完成处理后关闭计算机。 您可以转到 EC2 服务,选择机器,然后将其停止。 您无需为未运行的计算机付费。
系统将提示您有关如何连接到实例的一些信息。 如果您以前从未使用过 AWS,则可能需要创建一个新的密钥对以安全地连接到您的实例。 在这种情况下,给您的密钥对命名,下载pem文件,并将其存储在安全的地方-如果丢失,您将无法再次连接到您的实例!
单击 Connect,以获取有关使用pem文件连接到您的实例的信息。 最可能的情况是,您将通过以下命令使用ssh:
ssh -i <certificante_name>.pem ubuntu@<server_ip_address>
设置环境
将连接到实例后,可以安装更新的Lasagne和nolearn软件包。
首先,为本章前面概述的Lasagne克隆git存储库:
git clone https://github.com/Lasagne/Lasagne.git
为了在此机器上构建该库,我们需要 Python 3 的setuptools,我们可以通过apt-get进行安装,这是 Ubuntu 安装应用和库的方法。 我们还需要 NumPy 的开发库。 在虚拟机的命令行中运行以下命令:
sudo apt-get install python3-pip python3-numpy-dev
接下来,我们安装千层面。 首先,我们转到源代码目录,然后运行setup.py来构建和安装它:
cd Lasagne
sudo python3 setup.py install
注意
为简便起见,我们已经安装了Lasagne,并将以系统级软件包的形式安装nolearn。 对于那些想要更便携的解决方案的人,我建议使用virtualenv安装这些软件包。 它将允许您在同一台计算机上使用不同的 python 和库版本,并使将代码移动到新计算机变得更加容易。 有关更多信息,请参见这个页面。
千层面建好后,我们现在可以安装nolearn。 转到主目录,并遵循相同的步骤,但nolearn软件包除外:
cd ~/
git clone https://github.com/dnouri/nolearn.git
cd nolearn
sudo python3 setup.py install
我们的系统即将建立。 我们需要安装 scikit-learn 和 scikit-image 以及 matplotlib。 我们可以使用pip3完成所有这些操作。 作为对此的依赖,我们还需要scipy和matplotlib软件包,它们目前尚未安装在本机上。 我建议使用apt-get而不是pip3的 scipy 和 matplotlib,因为在某些情况下使用pip3安装它可能会很痛苦:
sudo apt-get install python3-scipy python3-matplotlib
sudo pip3 install scikit-learn scikit-image
接下来,我们需要将代码添加到计算机上。 有很多方法可以将该文件保存到计算机上,但是最简单的方法之一就是复制并粘贴内容。
首先,打开我们之前使用的 IPython Notebook(在您的计算机上,而不是在 Amazon 虚拟机上)。 在笔记本电脑上本身是一个菜单。 单击文件,然后单击下载为。 选择 Python 并将其保存到您的计算机。 此过程将 IPython Notebook 中的代码下载为可从命令行运行的 python 脚本。
打开此文件(在某些系统上,您可能需要右键单击并使用文本编辑器打开)。 选择所有内容并将其复制到剪贴板。
在 Amazon 虚拟机上,移至主目录并使用新文件名打开nano:
cd ~/
nano chapter11script.py
将打开nano程序,这是一个命令行文本编辑器。
打开此程序,将剪贴板的内容粘贴到此文件中。 在某些系统上,可能需要使用 ssh 程序的文件选项,而不是按 Ctrl +V进行粘贴。
在nano中,按 Ctrl +O将文件保存在磁盘上,然后按 Ctrl +X退出程序。
您还需要字体文件。 最简单的方法是从原始位置重新下载。 为此,请输入以下内容:
wget http://openfontlibrary.org/img/downloads/bretan/680bc56bbeeca95353ede363a3744fdf/bretan.zip
sudo apt-get install unzip
unzip -p bretan.zip Coval.otf > Coval.otf
这只会解压缩一个Coval.otf文件(此 zip 文件夹中有很多我们不需要的文件)。
仍在虚拟机中时,可以使用以下命令运行该程序:
python3 chapter11script.py
该程序将像在 IPython Notebook 中一样运行,并且结果将打印到命令行。
结果应该与以前相同,但是神经网络的实际训练和测试将更快。 请注意,在程序的其他方面并没有那么快—我们没有编写使用 GPU 的 CAPTCHA 数据集创建过程,因此我们无法在那里获得加速。
注意
您可能希望关闭 Amazon 虚拟机以节省一些钱。 我们将在本章结尾使用它来运行我们的主要实验,但是将首先在您的主计算机上开发代码。
应用
现在回到您的主机,打开我们在本章中创建的第一个 IPython Notebook-我们用来加载 CIFAR 数据集的笔记本。 在这个主要实验中,我们将获取 CIFAR 数据集,创建一个深度卷积神经网络,然后在基于 GPU 的虚拟机上运行它。
获取数据
首先,我们将获取 CIFAR 图像并使用它们创建数据集。 与以前不同,我们将保留像素结构,即。 在行和列中。 首先,将所有批次加载到列表中:
import numpy as np
batches = []
for i in range(1, 6):
batch_filename = os.path.join(data_folder, "data_batch_{}".format(i))
batches.append(unpickle(batch1_filename))
break
最后一行,即中断,是测试代码—这将大大减少训练示例的数量,使您可以快速查看代码是否正常运行。 在测试代码正常工作后,我会提示您删除此行。
接下来,通过将这些批次彼此堆叠来创建数据集。 我们使用 NumPy 的vstack,可以将其可视化为在行末添加行:
X = np.vstack([batch['data'] for batch in batches])
然后,我们将数据集规范化为 0 到 1 的范围,然后将类型强制为 32 位浮点型(这是启用 GPU 的虚拟机可以运行的唯一数据类型):
X = np.array(X) / X.max()
X = X.astype(np.float32)
然后,我们对这些类进行相同的操作,除了执行hstack,这类似于将列添加到数组的末尾。 然后,我们使用OneHotEncoder将其转换为单热阵列:
from sklearn.preprocessing import OneHotEncoder
y = np.hstack(batch['labels'] for batch in batches).flatten()
y = OneHotEncoder().fit_transform(y.reshape(y.shape[0],1)).todense()
y = y.astype(np.float32)
接下来,我们将数据集分为训练和测试集:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
接下来,我们调整数组的形状以保留原始数据结构。 原始数据为 32 x 32 像素的图像,每个像素具有 3 个值(红色,绿色和蓝色值);
X_train = X_train.reshape(-1, 3, 32, 32)
X_test = X_test.reshape(-1, 3, 32, 32)
现在,我们有了一个熟悉的训练和测试数据集,以及每个目标类。 现在,我们可以构建分类器。
创建神经网络
我们将使用nolearn包来构建神经网络,因此将遵循类似于第 8 章和“用神经网络”复制击败 CAPTCHA 的复制实验的模式。 。
首先,我们创建神经网络的各层:
from lasagne import layers
layers=[
('input', layers.InputLayer),
('conv1', layers.Conv2DLayer),
('pool1', layers.MaxPool2DLayer),
('conv2', layers.Conv2DLayer),
('pool2', layers.MaxPool2DLayer),
('conv3', layers.Conv2DLayer),
('pool3', layers.MaxPool2DLayer),
('hidden4', layers.DenseLayer),
('hidden5', layers.DenseLayer),
('output', layers.DenseLayer),
]
最后三层使用密集层,但在此之前,我们将卷积层与池化层结合使用。 我们有三套。 另外,我们(必须)从输入层开始。 这总共给了我们 10 层。 和以前一样,可以很容易地从数据集中计算出第一层和最后一层的大小,尽管我们的输入大小将具有与数据集相同的形状,而不仅仅是相同数量的节点/输入。
开始构建我们的神经网络(请记住不要关闭括号):
from nolearn.lasagne import NeuralNet
nnet = NeuralNet(layers=layers,
添加输入形状。 此处的形状类似于数据集的形状(每个像素三个值和一个 32 x 32 像素图像)。 第一个值,无,是nolearn使用的默认批处理大小-它将立即训练此数量的样本,从而减少了算法的运行时间。 将其设置为 None 将删除此硬编码值,从而使我们在运行算法时更具灵活性:
input_shape=(None, 3, 32, 32),
注意
要更改批处理大小,您将需要创建BatchIterator实例。 对此参数感兴趣的人可以在这个页面上查看文件的源,跟踪batch_iterator_train和batch_iterator_test 参数,以及如何在此文件的NeuralNet类中设置它们。
接下来,我们设置卷积层的大小。 这里没有严格的规则,但是我发现以下值是一个很好的起点。
conv1_num_filters=32,
conv1_filter_size=(3, 3),
conv2_num_filters=64,
conv2_filter_size=(2, 2),
conv3_num_filters=128,
conv3_filter_size=(2, 2),
filter_size参数决定卷积层所查看图像的窗口大小。 另外,我们设置池化层的大小:
pool1_ds=(2,2),
pool2_ds=(2,2),
pool3_ds=(2,2),
然后,我们设置两个隐藏的密集层(倒数第三层和倒数第二层)的大小,以及输出层的大小,即我们数据集中的类数;
hidden4_num_units=500,
hidden5_num_units=500,
output_num_units=10,
我们也使用softmax为最后一层设置非线性。
output_nonlinearity=softmax,
我们还设置了学习速度和动力。 根据经验,随着样本数量的增加,学习率应降低:
update_learning_rate=0.01,
update_momentum=0.9,
像以前一样,我们将回归设置为True,并且将训练时期的数量设置为较低,因为此网络需要很长时间才能运行。 成功运行之后,增加时期数将得到更好的模型,但是您可能需要等待一两天(或更长时间!)才能进行训练:
regression=True,
max_epochs=3,
最后,我们将详细程度设置为等于 1,这将使我们打印出每个时期的结果。 这使我们能够知道模型的进度以及模型仍在运行。 另一个功能是,它告诉我们每个纪元运行所花费的时间。 这是非常一致的,因此您可以通过将该值乘以剩余历元数来计算出训练剩余时间,从而可以很好地估算出等待训练完成所需的时间:
verbose=1)
全部放在一起
现在我们有了网络,我们可以使用我们的训练数据集对其进行训练:
nnet.fit(X_train, y_train)
即使减小了数据集的大小并减少了纪元的数量,该仍需要花费相当长的时间才能运行。 代码完成后,您可以像以前一样对其进行测试:
from sklearn.metrics import f1_score
y_pred = nnet.predict(X_test)
print(f1_score(y_test.argmax(axis=1), y_pred.argmax(axis=1)))
结果将是可怕的,应该如此! 我们对网络的训练不是很多,仅进行了几次迭代,仅处理了五分之一的数据。
首先,返回并删除在创建数据集时放入的折线(位于批处理循环中)。 这将使代码可以训练所有样本,而不仅仅是其中一些样本。
接下来,将神经网络定义中的时期数更改为 100。
现在,我们将脚本上传到我们的虚拟机。 与以前一样,单击文件 | 以Python格式下载,并将脚本保存在计算机上的某个位置。 启动并连接到虚拟机,然后像您之前所做的那样上传脚本(我叫我的脚本chapter11cifar.py-如果您的命名不同,则只需更新以下代码)。
接下来,我们需要将数据集放在虚拟机上。 最简单的方法是转到虚拟机并键入:
wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
这将下载数据集。 下载完成后,您可以通过首先创建该文件夹,然后将数据解压缩到其中,将数据提取到Data文件夹中:
mkdir Data
tar -zxf cifar-10-python.tar.gz -C Data
最后,我们可以使用以下示例运行示例:
python3 chapter11cifar.py
您会注意到的第一件事是急剧的加速。 在我的家用计算机上,每个纪元花费了 100 秒钟以上才能运行。 在启用 GPU 的虚拟机上,每个纪元仅需 16 秒! 如果我们尝试在计算机上运行 100 个纪元,则将花费近 3 个小时,而在虚拟机上仅需 26 分钟。
这种巨大的加速使追踪不同模型的速度更快。 通常在试用机器学习算法的情况下,单个算法的计算复杂性并不太重要。 算法可能需要几秒钟,几分钟或几小时才能运行。 如果仅运行一个模型,则此训练时间不太可能太大,尤其是大多数机器学习算法的预测速度很快时,这就是大多数使用机器学习模型的地方。
但是,当您有多个参数要运行时,您突然需要训练成千上万个参数略有不同的模型-突然地,这些速度的增加就变得越来越重要。
经过 100 个历时的训练,历时 26 分钟,您将获得最终结果的打印输出:
0.8497
还不错! 我们可以增加训练的次数来进一步改善这种情况,或者我们可以尝试更改参数。 也许更多的隐藏节点,更多的卷积层或其他密集层。 在千层面中也可以尝试其他类型的图层; 尽管通常来说,卷积层更适合视觉。
三十三、使用大数据
数据量正以指数速度增长。 当今的系统正在生成和记录有关客户行为,分布式系统,网络分析,传感器以及许多其他来源的信息。 当前移动数据的大趋势正在推动当前的增长,而下一个大事物– 物联网(IoT)–将进一步提高增长率。
这对数据挖掘意味着什么是一种新的思维方式。 需要改进或放弃运行时间较长的复杂算法,而能够处理更多样本的简单算法越来越受欢迎。 例如,虽然支持向量机是很好的分类器,但是某些变体很难在非常大的数据集上使用。 相反,在这些情况下,更简单的算法(例如逻辑回归)可以更轻松地进行管理。
在本章中,我们将研究以下内容:
- 大数据挑战与应用
- MapReduce 范例
- Hadoop MapReduce
- mrjob,一个可在亚马逊基础设施上运行 MapReduce 程序的 python 库
大数据
什么使大数据与众不同? 大多数大数据支持者谈论大数据的四个 V:
- 量:我们生成和存储的数据量正在以越来越高的速度增长,对未来的预测通常仅暗示进一步的增长。 今天的数千兆字节大小的硬盘驱动器将在几年内转变为 EB 级硬盘驱动器,并且网络吞吐量流量也将不断增加。 信噪比可能非常困难,因为重要数据丢失在大量不重要的数据中。
- 速度:尽管与体积有关,但数据的速度也在增加。 现代汽车有数百个传感器,这些传感器将数据流传输到计算机中,并且需要在亚秒级的水平上分析来自这些传感器的信息,以操作汽车。 这不仅仅是在数据量中寻找答案的情况; 这些答案通常需要迅速得出。
- 品种:具有清晰定义列的漂亮数据集只是我们如今拥有的数据集的一小部分。 考虑一个社交媒体帖子,该帖子可能包含文本,照片,用户提及,喜欢,评论,视频,地理信息和其他字段。 仅仅忽略这些数据中不适合您模型的部分将导致信息丢失,但是整合信息本身可能非常困难。
- 准确性:随着数据量的增加,可能很难确定是否已正确收集数据-是过时,嘈杂,包含异常值,还是通常是否有用 完全没有 当人们无法可靠地验证数据本身时,很难信任数据。 外部数据集也越来越多地被合并为内部数据集,从而带来了更多与数据准确性相关的麻烦。
这四个主要 V(其他人提出了其他 V)概述了大数据为何与大量数据不同的原因。 在这些规模上,处理数据的工程问题通常更加困难,更不用说分析了。 尽管有很多蛇油销售人员高估了使用大数据的能力,但很难否认工程挑战和大数据分析的潜力。
我们使用的算法是先将数据集加载到内存中,然后再处理内存中的版本。 这在计算速度方面带来了很大的好处,因为在内存中数据上进行计算要比必须在使用之前加载样本快得多。 此外,内存中的数据使我们可以对数据进行多次迭代,从而改进我们的模型。
在大数据中,我们无法将数据加载到内存中,从很多方面来说,这是一个确定问题是否是大数据的好定义-如果数据可以容纳在计算机的内存中,那么您就不会在处理 大数据问题。
应用场景和目标
在公共和私营部门中都有许多大数据用例。
人们使用基于大数据的系统的最常见体验是在 Internet 搜索中,例如 Google。 要运行这些系统,需要在一秒钟之内对数十亿个网站进行搜索。 进行基于文本的基本搜索将不足以解决此类问题。 仅存储所有这些网站的文本是一个大问题。 为了处理查询,需要为此应用专门创建和实现新的数据结构和数据挖掘方法。
大数据还用于许多其他科学实验中,例如大型强子对撞机(下图所示),它的长度超过 17 公里,包含 1.5 亿个传感器,每秒监视数亿个粒子碰撞。 来自该实验的数据非常庞大,经过过滤过程之后,每天创建 25 PB(如果不使用过滤,则每年将有 1.5 亿 PB)。 对如此庞大的数据进行分析已经获得了有关我们宇宙的惊人见解,但这一直是工程和分析领域的重大挑战。
各国政府也越来越多地使用大数据来跟踪人口,企业以及有关其国家的其他方面。 跟踪数百万人和数十亿的互动(例如业务交易或医疗保健支出)导致许多政府组织需要大数据分析。
交通管理是世界上许多国家/地区的特别关注的国家,它们正在使用数百万个传感器来跟踪交通,以确定最拥挤的道路,并预测新道路对交通水平的影响。
大型零售组织正在使用大数据来改善客户体验并降低成本。 这涉及到预测客户需求以拥有正确的库存水平,用他们可能想要购买的产品向客户推销,以及跟踪交易以寻找趋势,模式和潜在的欺诈行为。
其他大型企业也正在利用大数据来自动化其业务并改善其产品。 这包括利用分析来预测其行业的未来趋势并跟踪外部竞争对手。 大型企业还使用分析来管理自己的员工,从而跟踪员工以寻找员工可能离开公司的迹象,以便在他们离开之前进行干预。
信息安全部门还通过监视网络流量,利用大数据来查找大型网络中的恶意软件感染。 这可能包括寻找异常的流量模式,恶意软件传播的证据以及其他异常情况。 高级持久威胁(APT)是另一个问题,有意识的攻击者会将他们的代码隐藏在大型网络中,从而在很长一段时间内窃取信息或造成破坏。 查找 APT 通常是对许多计算机进行取证检查的情况,对于人类而言,完成这项任务只需要很长时间才能有效地执行自己的任务。 Analytics(分析)可帮助自动化和分析这些取证图像以发现感染。
大数据正在越来越多的部门和应用中使用,并且这种趋势可能只会持续下去。
MapReduce
有许多概念可以对大数据执行数据挖掘和常规计算。 最受欢迎的模型之一是 MapReduce 模型,该模型可用于任意大型数据集的常规计算。
MapReduce 起源于 Google,当时该公司在开发时就考虑了分布式计算。 它还引入了容错能力和可伸缩性改进。 MapReduce 的“原始”研究报告于 2004 年发表,自那时以来,已有成千上万的项目,实现和应用在使用它。
尽管该概念与许多先前的概念相似,但 MapReduce 已成为大数据分析的主要内容。
直觉
MapReduce 有两个主要步骤:Map 步骤和 Reduce 步骤。 这些建立在将函数映射到列表并减少结果的函数编程概念上。 为了解释这个概念,我们将开发代码,该代码将遍历列表列表并产生这些列表中所有数字的总和。
MapReduce 范式中还包含混洗和合并步骤,我们将在后面看到。
首先,“映射”步骤采用一个函数并将其应用于列表中的每个元素。 返回的结果是一个大小相同的列表,该函数的结果应用于每个元素。
要打开一个新的 IPython Notebook,首先创建一个列表列表,每个子列表中都有数字:
a = [[1,2,1], [3,2], [4,9,1,0,2]]
接下来,我们可以使用 sum 函数执行映射。 此步骤将求和函数应用于a的每个元素:
sums = map(sum, a)
尽管 sums 是一个生成器(实际值直到我们要求时才计算),但上述步骤大约等于以下代码:
sums = []
for sublist in a:
results = sum(sublist)
sums.append(results)
减少步骤要复杂一些。 它涉及将函数应用于返回结果的每个元素以及某个初始值。 我们从初始值开始,然后将给定函数应用于该初始值和第一个值。 然后,我们将给定函数应用于结果和下一个值,依此类推。
我们首先创建一个将两个数字加在一起的函数。
def add(a, b):
return a + b
然后我们执行归约。 reduce 的签名是 reduce(函数,序列和初始),其中函数在每个步骤应用于序列。 第一步,将初始值用作第一个值,而不是列表的第一个元素:
from functools import reduce
print(reduce(add, sums, 0))
结果25是和列表中每个值的总和,因此是原始数组中每个元素的总和。
上面的代码等于以下代码:
initial = 0
current_result = initial
for element in sums:
current_result = add(current_result, element)
在这个琐碎的示例中,我们的代码可以大大简化,但是真正的收益来自于分布计算。 例如,如果我们有一百万个子列表,并且每个子列表包含一百万个元素,则可以在许多计算机上分布此计算。
为此,我们分配了地图步骤。 对于列表中的每个元素,我们将其以及功能说明发送到计算机。 然后,这台计算机将结果返回到我们的主计算机(主机)。
然后,主机将结果发送到计算机进行缩减步骤。 在上百万个子列表的示例中,我们会将一百万个作业发送到不同的计算机(同一台计算机在完成我们的第一份工作后可能会被重用)。 返回的结果只是一个一百万个数字的列表,然后我们计算它们的总和。
结果是,即使我们的原始数据中包含一万亿个数字,也没有计算机需要存储超过一百万个数字。
字数示例
MapReduce 的实现比仅使用 map and reduce 步骤要复杂一些。 这两个步骤都使用键来调用,这允许分离数据和跟踪值。
映射函数采用键和值对,并返回键+值对的列表。 输入和输出的键不一定相互关联。 例如,对于执行单词计数的 MapReduce 程序,输入键可能是示例文档的 ID 值,而输出键可能是给定的单词。 输入值将是文档的文本,输出值将是每个单词的频率:
from collections import defaultdict
def map_word_count(document_id, document):
我们首先计算每个单词的频率。 在这个简化的示例中,尽管有更好的选择,我们将文档拆分为空白以获取单词:
counts = defaultdict(int)
for word in document.split():
counts[word] += 1
然后我们产生每个单词,计数对。 这里的单词是关键,计数是 MapReduce 术语中的值:
for word in counts:
yield (word, counts[word])
通过使用单词作为关键字,我们可以执行随机播放步骤,该步骤将每个关键字的所有值分组:
def shuffle_words(results):
首先,我们将每个单词的结果计数汇总到计数列表中:
records = defaultdict(list)
然后,我们迭代 map 函数返回的所有结果;
for results in results_generators:
for word, count in results:
records[word].append(count)
接下来,我们产生每个单词以及在数据集中获得的所有计数:
for word in records:
yield (word, records[word])
最后一步是 reduce 步骤,该步骤采用一个键值对(在这种情况下,该值始终是一个列表)并生成一个键值对。 在我们的示例中,键是单词,输入列表是在随机播放步骤中产生的计数列表,输出值是计数之和:
def reduce_counts(word, list_of_counts):
return (word, sum(list_of_counts))
为了了解这一点,我们可以使用 scikit-learn 中提供的 20 个新闻组数据集:
from sklearn.datasets import fetch_20newsgroups
dataset = fetch_20newsgroups(subset='train')
documents = dataset.data
然后,我们应用地图步骤。 我们在此处使用枚举为我们自动生成文档 ID。 尽管它们在此应用中并不重要,但这些键在其他应用中却很重要。
map_results = map(map_word_count, enumerate(documents))
此处的实际结果只是一个生成器,没有实际计数。 就是说,这是一个生成(单词,计数)对的生成器。
接下来,我们执行洗牌步骤对这些字数进行排序:
shuffle_results = shuffle_words(map_results)
从本质上讲,这是 MapReduce 的工作。 但是,它仅在单个线程上运行,这意味着我们无法从 MapReduce 数据格式中获得任何好处。 在下一节中,我们将开始使用 Hadoop(MapReduce 的开源提供程序)开始从这种范例中获取好处。
Hadoop MapReduce
Hadoop 是 Apache 的开源工具集,其中包括 MapReduce 的实现。 在许多情况下,它是许多人事实上使用的实现。 该项目由 Apache 小组(负责著名的 Web 服务器)管理。
Hadoop 生态系统非常复杂,具有大量工具。 我们将使用的主要组件是 Hadoop MapReduce。 Hadoop 中包含的其他用于处理大数据的工具如下:
- Hadoop 分布式文件系统(HDFS):此是一种文件系统,可以在许多计算机上存储文件,目的是在提供高带宽的同时,防止硬件故障。
- YARN:此是用于调度应用和管理计算机集群的方法。
- Pig:这是一种用于 MapReduce 的高级编程语言。 Hadoop MapReduce 以 Java 实施,Pig 位于 Java 实施之上,允许您以其他语言(包括 Python)编写程序。
- 配置单元:此用于管理数据仓库和执行查询。
- HBase:此是 Google 的 BigTable(分布式数据库)的实现。
这些工具都解决了进行大数据实验(包括数据分析)时出现的不同问题。
也有非基于 Hadoop 的 MapReduce 实现,以及其他具有类似目标的项目。 此外,许多云提供商都使用基于 MapReduce 的系统。
应用
在此应用中,我们将研究根据使用不同单词来预测作家的性别。 我们将为此使用朴素贝叶斯方法,并在 MapReduce 中进行训练。 最终模型不需要 MapReduce,尽管我们可以使用 Map 步骤来执行-也就是说,在列表中的每个文档上运行预测模型。 这是 MapReduce 中用于数据挖掘的常见 Map 操作,而 reduce 步骤仅组织了预测列表,因此可以将其追溯到原始文档。
我们将使用亚马逊的基础设施来运行我们的应用,从而使我们能够利用其计算资源。
获取数据
我们将使用的数据是一组博客文章,其中标有年龄,性别,行业(即工作)以及有趣的是星号。 这些数据是 2004 年 8 月从这个页面收集的,在超过 60 万个帖子中有 1.4 亿个单词。 每个博客可能只由一个人撰写,并做了一些工作来验证这一点(尽管我们永远无法确定)。 帖子还与发布日期匹配,这使它成为非常丰富的数据集。
要获取数据,请转到这个页面并单击下载语料库。 从那里将文件解压缩到计算机上的目录。
数据集由一个博客组织成一个文件,文件名给出了类。 例如,文件名之一如下:
1005545.male.25.Engineering.Sagittarius.xml
文件名用句点分隔,字段如下:
- Blogger ID:此一个简单的 ID 值,用于组织身份。
- 性别:此是男性还是女性,并且所有博客都被标识为这两个选项之一(此数据集中不包含其他选项)。
- 年龄:给出了确切的年龄,但故意存在一些差距。 存在的年龄在 13-17、23-27 和 33-48(含)范围内。 出现差距的原因是允许将博客按年龄段划分,因为将 18 岁的写作与 19 岁的写作分开很难,而且年龄本身可能已经过时了。
- 行业:在中,是 40 个不同行业之一,包括科学,工程,艺术和房地产。 另外,还包括不知名行业的 indUnk。
- 星号:此是 12 个占星术星号之一。
所有值都是自我报告的,这意味着标签可能存在错误或不一致,但被认为是最可靠的-如果人们想以这种方式保护自己的隐私,则可以选择不设置值。
单个文件为pseudo-XML格式,包含<Blog>标签和<post>标签序列。 每个<post>标签都以<date>标签开头。 尽管我们可以将其解析为 XML,但逐行解析它要简单得多,因为文件不是完全格式正确的 XML,存在一些错误(主要是编码问题)。 要阅读文件中的帖子,我们可以使用循环遍历行。
我们设置了一个测试文件名,以便可以看到它的作用:
import os
filename = os.path.join(os.path.expanduser("~"), "Data", "blogs", "1005545.male.25.Engineering.Sagittarius.xml")
首先,我们创建一个列表,该列表将让我们存储每个帖子:
all_posts = []
然后,我们打开文件以读取:
with open(filename) as inf:
然后,我们设置一个标志,指示我们当前是否在帖子中。 当找到表示帖子开始的<post>标签时,将其设置为True;当找到结束的</post>标签时,将其设置为False;
post_start = False
然后,我们创建一个列表来存储当前帖子的行:
post = []
然后,我们遍历文件的每一行并删除空白:
for line in inf:
line = line.strip()
如前所述,如果我们找到开始的<post>标签,则表明我们在新帖子中。 同样,使用 close </post>标签:
if line == "<post>":
post_start = True
elif line == "</post>":
post_start = False
当我们找到结束</post>标记时,我们还将记录到目前为止找到的完整帖子。 然后,我们也开始一个新的“当前”帖子。 该代码与上一行的缩进级别相同:
all_posts.append("\n".join(post))
post = []
最后,当该行不是 end 标记的开始,而是在发布中时,我们将当前行的文本添加到当前发布中:
elif post_start:
post.append(line)
如果我们不在当前帖子中,则只需忽略该行。
然后,我们可以获取每个帖子的文本:
print(all_posts[0])
我们也可以找出该作者创建的帖子数:
print(len(all_posts))
朴素贝叶斯预测
现在,我们将要实现能够处理我们的数据集的朴素贝叶斯算法(从技术上讲,是贝叶斯算法的简化版本,没有许多更复杂的实现具有的功能)。
mrjob 程序包
mrjob 软件包使我们能够创建 MapReduce 作业,这些作业可以轻松地传输到 Amazon 的基础架构。 虽然 mrjob 听起来像是 Men 先生系列儿童读物中的可笑的补充,但它实际上代表 Map Reduce Job。 这是一个很棒的包装; 但是,在撰写本文时,Python 3 支持仍不成熟,这对于我们稍后将讨论的 Amazon EMR 服务是正确的。
注意
您可以使用以下命令为 Python 2 版本安装 mrjob:
sudo pip2 install mrjob
请注意,pip用于版本 2,而不用于版本 3。
本质上,mrjob 提供了大多数 MapReduce 作业所需的标准功能。 它最惊人的功能是,您可以编写相同的代码,在没有 Hadoop 的情况下在本地计算机上进行测试,然后推送到 Amazon 的 EMR 服务或另一台 Hadoop 服务器。
尽管无法神奇地使大问题变小,但这使测试代码变得更加容易-请注意,任何本地测试都使用数据集的子集而不是整个大型数据集。
提取博客文章
我们首先要创建一个 MapReduce 程序,该程序将从每个博客文件中提取每个帖子,并将它们存储为单独的条目。 由于我们对帖子作者的性别感兴趣,因此我们也将其提取出来并与帖子一起存储。
我们无法在 IPython Notebook 中执行此操作,因此请打开 Python IDE 进行开发。 如果您没有 Python IDE(例如 PyCharm),则可以使用文本编辑器。 我建议寻找具有语法高亮显示的 IDE。
注意
如果仍然找不到良好的 IDE,则可以在 IPython Notebook 中编写代码,然后单击文件 |。 下载为 | Python。 将此文件保存到目录并按照第 11 章,“使用深度学习”对图像中的对象进行分类中所述运行它。
为此,我们将需要os和re库,因为我们将获取环境变量,并且还将使用正则表达式进行单词分隔:
import os
import re
然后,我们导入MRJob类,该类将从我们的 MapReduce 作业中继承:
from mrjob.job import MRJob
然后,我们创建一个新类,该子类继承了 MRJob:
class ExtractPosts(MRJob):
与以前一样,我们将使用类似的循环从文件中提取博客文章。 接下来将定义的映射功能将在每一行中工作,这意味着我们必须在映射功能之外跟踪不同的帖子。 因此,我们将post_start和post类变量,而不是函数内部的变量:
post_start = False
post = []
然后,我们定义我们的 mapper 函数-这将文件中的一行作为输入并产生博客文章。 这些行保证可以从同一作业文件中订购。 这使我们可以使用上述类变量来记录当前的帖子数据:
def mapper(self, key, line):
在开始收集博客文章之前,我们需要获取博客作者的性别。 尽管我们通常不将文件名用作 MapReduce 作业的一部分,但对它的需求非常强烈(如本例所示),因此该功能可用。 当前文件存储为环境变量,我们可以使用以下代码行获取该文件:
filename = os.environ["map_input_file"]
然后,我们分割文件名以获取性别(这是第二个标记):
gender = filename.split(".")[1]
我们从行的开头和结尾删除空格(这些文档中有很多空格),然后像以前一样进行基于帖子的跟踪;
line = line.strip()
if line == "<post>":
self.post_start = True
elif line == "</post>":
self.post_start = False
与其像以前那样将存储在列表中,不如将它们存储在列表中,而是生成它们。 这使 mrjob 可以跟踪输出。 我们同时提供性别和职位,以便我们可以记录每条记录匹配的性别。 此函数的其余部分与上述循环的定义方式相同:
yield gender, repr("\n".join(self.post))
self.post = []
elif self.post_start:
self.post.append(line)
最后,在函数和类之外,我们设置脚本以在从命令行调用该 MapReduce 作业时运行该脚本:
if __name__ == '__main__':
ExtractPosts.run()
现在,我们可以使用以下 shell 命令运行此 MapReduce 作业。 请注意,我们使用的是 Python 2,而不是 Python 3。
python extract_posts.py <your_data_folder>/blogs/51* --output-dir=<your_data_folder>/blogposts –no-output
第一个参数<your_data_folder>/blogs/51*(只需记住将<your_data_folder >更改为数据文件夹的完整路径),将获得数据样本(所有以 51 开头的文件,只有 11 个文档)。 然后,我们将输出目录设置为新文件夹,并将其放置在 data 文件夹中,并指定不输出流数据。 如果没有最后一个选项,则运行时输出的数据将显示在命令行中,这对我们不是很有帮助,并且会大大降低计算机的速度。
运行脚本,很快就会提取每个博客帖子并将其存储在我们的output文件夹中。 该脚本仅在本地计算机上的单个线程上运行,因此我们根本无法获得加速,但是我们知道代码可以运行。
现在,我们可以在output文件夹中查找结果。 创建了一堆文件,每个文件在单独的行中包含每个博客文章,并在其前面加上博客作者的性别。
训练朴素贝叶斯
现在我们已经提取了博客文章,我们可以在其上训练我们的朴素贝叶斯模型。 直觉是我们记录了某个单词被特定性别书写的可能性。 为了对新样本进行分类,我们将概率相乘并找到最可能的性别。
此代码的目标是要输出一个文件,该文件列出了语料库中的每个单词以及该单词针对每个性别的出现频率。 输出文件将如下所示:
"'ailleurs" {"female": 0.003205128205128205}
"'air" {"female": 0.003205128205128205}
"'an" {"male": 0.0030581039755351682, "female": 0.004273504273504274}
"'angoisse" {"female": 0.003205128205128205}
"'apprendra" {"male": 0.0013047113868622459, "female": 0.0014172668603481887}
"'attendent" {"female": 0.00641025641025641}
"'autistic" {"male": 0.002150537634408602}
"'auto" {"female": 0.003205128205128205}
"'avais" {"female": 0.00641025641025641}
"'avait" {"female": 0.004273504273504274}
"'behind" {"male": 0.0024390243902439024}
"'bout" {"female": 0.002034152292059272}
第一个值是单词,第二个值是字典,将性别映射到该单词在该性别文字中的出现频率。
在 Python IDE 或文本编辑器中打开一个新文件。 我们将再次需要 mrjob 的os和re库以及NumPy和MRJob。 我们还需要 itemgetter,因为我们将对字典进行排序:
import os
import re
import numpy as np
from mrjob.job import MRJob
from operator import itemgetter
我们还将需要 MRStep,它概述了 MapReduce 作业中的步骤。 我们以前的工作只有一步,即定义为映射函数,然后定义为归约函数。 这项工作将包含三个步骤,我们先进行 Map,Reduce,然后再次进行 Map 和 Reduce。 直觉与我们在前面各章中使用的管道相同,其中一步的输出是下一步的输入:
from mrjob.step import MRStep
然后,我们创建单词搜索正则表达式并进行编译,从而使我们能够找到单词边界。 这种正则表达式比我们在前几章中使用的简单拆分功能要强大得多,但是如果您要寻找更准确的单词拆分器,我建议像在第 6 章中一样使用 NLTK,[ “使用朴素贝叶斯”的社交媒体见解:
word_search_re = re.compile(r"[\w']+")
我们为训练定义了一个新班级:
class NaiveBayesTrainer(MRJob):
我们定义了MapReduce工作的步骤。 有两个步骤。 第一步将提取单词出现概率。 第二步将比较两个性别,并将每个性别的概率输出到我们的输出文件中。 在每个 MRStep 中,我们定义映射器和化简器函数,它们是NaiveBayesTrainer类中的类函数(我们将在接下来编写这些函数):
def steps(self):
return [
MRStep(mapper=self.extract_words_mapping,
reducer=self.reducer_count_words),
MRStep(reducer=self.compare_words_reducer),
]
第一个功能是第一步的映射器功能。 此功能的目标是获取每个博客帖子,获取该帖子中的所有单词,然后记录出现的情况。 我们需要单词的频率,因此我们将返回1 / len(all_words),这允许我们稍后对频率值求和。 这里的计算并不完全正确-我们还需要针对文档数进行标准化。 但是,在此数据集中,类的大小是相同的,因此我们可以方便地忽略它,而对最终版本的影响很小。
我们还将输出该帖子作者的性别,因为稍后我们将需要它:
def extract_words_mapping(self, key, value):
tokens = value.split()
gender = eval(tokens[0])
blog_post = eval(" ".join(tokens[1:]))
all_words = word_search_re.findall(blog_post)
all_words = [word.lower() for word in all_words]
all_words = word_search_re.findall(blog_post)
all_words = [word.lower() for word in all_words]
for word in all_words:
yield (gender, word), 1\. / len(all_words)
提示
对于本示例,我们在前面的代码中使用eval来简化从文件中博客帖子的解析。 不建议这样做。 而是使用 JSON 之类的格式正确存储和解析文件中的数据。 具有对数据集的访问权限的恶意使用会将代码插入这些令牌中,并使该代码在您的服务器上运行。
在第一步的归约器中,我们将每个性别和单词对的频率相加。 我们也将关键字更改为单词,而不是组合,因为这允许我们在使用最终训练的模型时按单词搜索(尽管,我们仍然需要输出性别以备后用);
def reducer_count_words(self, key, frequencies):
s = sum(frequencies)
gender, word = key
yield word, (gender, s)
的最后一步不需要映射器功能,因此我们不需要添加一个映射器功能。 数据将作为identity mapper类型直接传递。 但是,归约器将组合给定单词下每个性别的频率,然后输出单词和频率字典。
这为我们提供了朴素贝叶斯实现所需的信息:
def compare_words_reducer(self, word, values):
per_gender = {}
for value in values:
gender, s = value
per_gender[gender] = s
yield word, per_gender
最后,当文件作为脚本运行时,我们设置代码来运行该模型;
if __name__ == '__main__':
NaiveBayesTrainer.run()
然后,我们可以运行此脚本。 该脚本的输入是前一个提取程序后脚本的输出(如果您愿意,我们可以在同一 MapReduce 作业中将它们实际上作为不同的步骤);
python nb_train.py <your_data_folder>/blogposts/
--output-dir=<your_data_folder>/models/
--no-output
输出目录是一个文件夹,该文件夹将存储一个包含此 MapReduce 作业的输出的文件,这将是我们运行 Naive Bayes 分类器所需的概率。
全部放在一起
现在,我们可以使用这些概率运行朴素贝叶斯分类器。 我们将在 IPython Notebook 中进行此操作,然后可以返回使用 Python 3(phe!)。
首先,查看上一个 MapReduce 作业中指定的 models 文件夹。 如果输出是多个文件,我们可以通过在 models 目录中使用命令行功能将文件彼此附加在一起来合并文件:
cat * > model.txt
如果这样做,则需要使用model.txt作为模型文件名更新以下代码。
回到我们的笔记本,我们首先导入脚本所需的一些标准导入:
import os
import re
import numpy as np
from collections import defaultdict
from operator import itemgetter
我们再次重新定义词搜索正则表达式-如果您是在实际应用中执行此操作,则建议将其集中化。 以相同的方式提取单词进行训练和测试非常重要:
word_search_re = re.compile(r"[\w']+")
接下来,我们创建一个从给定文件名加载模型的函数:
def load_model(model_filename):
模型参数将采用字典字典的形式,其中第一个键是单词,内部字典将每个性别映射为一个概率。 我们使用defaultdicts,如果不存在值,它将返回零;
model = defaultdict(lambda: defaultdict(float))
然后,我们打开模型并解析每一行;
with open(model_filename) as inf:
for line in inf:
该行分为两部分,用空格隔开。 第一个是单词本身,第二个是概率词典。 对于每个,我们对它们运行eval以获得实际值,该实际值是使用repr存储在前面的代码中的:
word, values = line.split(maxsplit=1)
word = eval(word)
values = eval(values)
然后,我们将值跟踪到模型中的单词:
model[word] = values
return model
接下来,我们加载实际模型。 您可能需要更改模型文件名,该文件名将位于最后一个 MapReduce 作业的输出dir中;
model_filename = os.path.join(os.path.expanduser("~"), "models", "part-00000")
model = load_model(model_filename)
例如,我们可以看到男性和女性在单词i(所有单词在 MapReduce 作业中都变为小写)的用法上的差异:
model["i"]["male"], model["i"]["female"]
接下来,我们创建一个可以使用该模型进行预测的函数。 在此示例中,我们将不使用 scikit-learn 接口,而仅创建一个函数。 我们的函数将模型和文档作为参数,并返回最可能的性别:
def nb_predict(model, document):
我们首先创建一个字典,将每个性别映射到计算出的概率:
probabilities = defaultdict(lambda : 1)
我们从文档中提取每个单词:
words = word_search_re.findall(document)
然后,我们遍历单词并在数据集中找到每种性别的概率:
for word in set(words):
probabilities["male"] += np.log(model[word].get("male", 1e-15))
probabilities["female"] += np.log(model[word].get("female", 1e-15))
然后,我们按性别将其值排序,获得最高价值,并将其返回为我们的预测:
most_likely_genders = sorted(probabilities.items(), key=itemgetter(1), reverse=True)
return most_likely_genders[0][0]
重要的是要注意,我们使用np.log来计算概率。 朴素贝叶斯模型中的概率通常很小。 在许多统计值中都必须乘以较小的值,这会导致下溢错误,其中计算机的精度不够好,只会使整个值变为 0。在这种情况下,这将导致两个性别的可能性为零 ,导致错误的预测。
为了解决这个问题,我们使用对数概率。 对于两个值 a 和 b,log(a,b)等于log(a) + log(b)。 小概率的对数为负值,但相对较大。 例如,log(0.00001)约为-11.5。 这意味着我们可以乘以对数概率并以相同的方式比较值,而不是乘以实际概率并冒下溢错误的风险(数字越大表示可能性越高)。
使用对数概率的的一个问题是它们不能很好地处理零值(尽管乘以零概率也不能)。 这是因为未定义 log(0)。 在朴素贝叶斯的某些实现中,所有计数都加 1 以消除此问题,但是还有其他方法可以解决此问题。 这是平滑值的简单形式。 在我们的代码中,如果对于给定的性别没有看到这个词,我们只会返回一个很小的值。
回到我们的预测功能,我们可以通过复制数据集中的帖子来进行测试:
new_post = """ Every day should be a half day. Took the afternoon off to hit the dentist, and while I was out I managed to get my oil changed, too. Remember that business with my car dealership this winter? Well, consider this the epilogue. The friendly fellas at the Valvoline Instant Oil Change on Snelling were nice enough to notice that my dipstick was broken, and the metal piece was too far down in its little dipstick tube to pull out. Looks like I'm going to need a magnet. Damn you, Kline Nissan, daaaaaaammmnnn yooouuuu.... Today I let my boss know that I've submitted my Corps application. The news has been greeted by everyone in the company with a level of enthusiasm that really floors me. The back deck has finally been cleared off by the construction company working on the place. This company, for anyone who's interested, consists mainly of one guy who spends his days cursing at his crew of Spanish-speaking laborers. Construction of my deck began around the time Nixon was getting out of office.
"""
然后,我们使用以下代码进行预测:
nb_predict(model, new_post)
对于此示例,最终的预测结果是男性,是正确的。 当然,我们绝不会在单个样本上测试模型。 我们使用以 51 开头的文件来训练该模型。 样本不多,所以我们不能指望准确性太高。
我们应该做的第一件事是训练更多样本。 我们将对以 6 或 7 开头的任何文件进行测试,然后对其余文件进行训练。
在命令行和 Blog 文件夹所在的数据文件夹(cd <your_data_folder)中,将 Blog 数据的副本创建到新文件夹中。
为我们的训练集创建一个文件夹:
mkdir blogs_train
从训练集中将所有以 6 或 7 开头的文件移到测试集中:
cp blogs/4* blogs_train/
cp blogs/8* blogs_train/
然后,为我们的测试集创建一个文件夹:
mkdir blogs_test
将以6或7开头的任何文件从训练集中移入测试集中:
cp blogs/6* blogs_test/
cp blogs/7* blogs_test/
我们将对训练集中的所有文件重新运行博客提取。 但是,这是一个比我们的系统更适合云基础架构的大型计算。 因此,我们现在将解析作业移至 Amazon 的基础架构。
和以前一样,在命令行上运行以下命令。 唯一的区别是我们在不同的输入文件文件夹上进行训练。 在运行以下代码之前,请删除博客文章和模型文件夹中的所有文件:
python extract_posts.py ~/Data/blogs_train --output-dir=/home/bob/Data/blogposts –no-output
python nb_train.py ~/Data/blogposts/ --output-dir=/home/bob/models/ --no-output
此处的代码将需要更长的时间才能运行。
我们将在测试集中的任何博客文件上进行测试。 要获取文件,我们需要提取它们。 我们将使用extract_posts.py MapReduce 作业,但将文件存储在单独的文件夹中:
python extract_posts.py ~/Data/blogs_test --output-dir=/home/bob/Data/blogposts_testing –no-output
回到 IPython Notebook,我们列出了所有输出的测试文件:
testing_folder = os.path.join(os.path.expanduser("~"), "Data", "blogposts_testing")
testing_filenames = []
for filename in os.listdir(testing_folder):
testing_filenames.append(os.path.join(testing_folder, filename))
对于每个文件,我们提取性别和文档,然后调用预测函数。 因为有很多文档,所以我们在生成器中执行此操作,并且我们不想使用过多的内存。 生成器产生实际性别和预测性别:
def nb_predict_many(model, input_filename):
with open(input_filename) as inf:
# remove leading and trailing whitespace
for line in inf:
tokens = line.split()
actual_gender = eval(tokens[0])
blog_post = eval(" ".join(tokens[1:]))
yield actual_gender, nb_predict(model, blog_post)
然后,我们记录整个数据集中的预测和实际性别。 我们在这里的预测是男性还是女性。 为了使用 scikit-learn 的f1_score函数,我们需要将它们变成 1 和 0。 为此,如果性别是男性,我们记录为 0,如果性别是 1,则记录为 1。 为此,我们使用布尔测试,查看性别是否为女性。 然后,我们使用NumPy将这些布尔值转换为int:
y_true = []
y_pred = []
for actual_gender, predicted_gender in nb_predict_many(model, testing_filenames[0]):
y_true.append(actual_gender == "female")
y_pred.append(predicted_gender == "female")
y_true = np.array(y_true, dtype='int')
y_pred = np.array(y_pred, dtype='int')
现在,我们使用 scikit-learn 中的F1分数来测试的质量:
from sklearn.metrics import f1_score
print("f1={:.4f}".format(f1_score(y_true, y_pred, pos_label=None)))
0.78 的结果还不错。 我们可能可以通过使用更多数据来改善此问题,但是要做到这一点,我们需要转向可以处理数据的功能更强大的基础架构。
关于亚马逊 EMR 基础设施的训练
我们正在使用 Amazon 的 Elastic Map Reduce(EMR)基础结构来运行我们的解析和模型构建作业。
为了做到这一点,我们首先需要在亚马逊的存储云中创建一个存储桶。 为此,请转到这个页面在 Web 浏览器中打开 Amazon S3 控制台,然后单击创建存储桶。 记住存储桶的名称,因为稍后我们将需要它。
右键单击新存储桶,然后选择Properties。 然后,更改权限,向所有人授予完全访问权限。 通常,这不是一个好的安全做法,建议您在完成本章后更改访问权限。
左键单击存储桶以将其打开,然后单击创建文件夹。 将文件夹命名为blogs_train。 我们将训练数据上传到此文件夹以在云上进行处理。
在您的计算机上,我们将使用 Amazon 的 AWS CLI,这是用于在 Amazon 的云上进行处理的命令行界面。
要安装,请使用以下命令:
sudo pip2 install awscli
按照这个页面上的说明设置此程序的凭据。
现在,我们要将数据上传到我们的新存储桶中。 首先,我们要创建数据集,该数据集不是所有博客都以 6 或 7 开头的博客。有更多优雅的方法可以完成此复制,但没有一个跨平台的方法值得推荐。 相反,只需复制所有文件,然后从训练数据集中删除以 6 或 7 开头的文件:
cp -R ~/Data/blogs ~/Data/blogs_train_large
rm ~/Data/blogs_train_large/6*
rm ~/Data/blogs_train_large/7*
接下来,将数据上传到您的 Amazon S3 存储桶。 请注意,这将花费一些时间,并且会使用大量的上传数据(几百兆字节)。 对于互联网连接速度较慢的用户,在连接速度较快的位置进行此操作可能值得;
aws s3 cp ~/Data/blogs_train_large/ s3://ch12/blogs_train_large --recursive --exclude "*" --include "*.xml"
我们将使用 mrjob 连接到 Amazon 的 EMR,它可以为我们处理所有事情。 它只需要我们的凭据即可。 按照这个页面上的说明,使用您的 Amazon 凭证设置 mrjob。
完成此操作后,我们只需稍稍更改 mrjob 运行即可在 Amazon EMR 上运行。 我们只是通过-r开关告诉 mrjob 使用emr,然后将s3容器设置为输入和输出目录。 即使这将在亚马逊的基础设施上运行,也将需要相当长的时间才能运行。
python extract_posts.py -r emr s3://ch12gender/blogs_train_large/ --output-dir=s3://ch12/blogposts_train/ --no-output
python nb_train.py -r emr s3://ch12/blogposts_train/ --output-dir=s3://ch12/model/ --o-output
注意
您还需要为使用付费。 这只有几美元,但是如果您要继续运行作业或在更大的数据集上执行其他作业,请记住这一点。 我做了很多工作,被收取了大约 20 美元。 仅运行这少数几个就应该少于$ 4。 但是,您可以转到这个页面来检查余额并设置价格提醒。
不需要blogposts_train和model文件夹存在-它们将由 EMR 创建。 实际上,如果它们存在,您将得到一个错误。 如果要重新运行,只需将这些文件夹的名称更改为新名称,但要记住将两个命令都更改为相同的名称(即,第一个命令的输出目录是第二个命令的输入目录)。
注意
如果您不耐烦,您可以在一段时间后随时停止第一份工作,而仅使用到目前为止收集的训练数据即可。 我建议离职至少 15 分钟,可能至少一个小时。 但是,您不能停止第二份工作并获得良好的结果。 第二份工作大概需要第一份工作的两倍到三倍。
您现在可以返回s3控制台,并从存储桶中下载输出模型。 将其保存在本地,我们可以返回 IPython Notebook 并使用新模型。 我们在此处重新输入代码-仅突出显示差异,以更新到我们的新模型:
aws_model_filename = os.path.join(os.path.expanduser("~"), "models", "aws_model")
aws_model = load_model(aws_model_filename)
y_true = []
y_pred = []
for actual_gender, predicted_gender in nb_predict_many(aws_model, testing_filenames[0]):
y_true.append(actual_gender == "female")
y_pred.append(predicted_gender == "female")
y_true = np.array(y_true, dtype='int')
y_pred = np.array(y_pred, dtype='int')
print("f1={:.4f}".format(f1_score(y_true, y_pred, pos_label=None)))
使用额外的数据(0.81),结果要好得多。
注意
如果一切按计划进行,则您可能要从 Amazon S3 中删除存储桶-您需要支付存储费用。