深度学习探索指南-全-

深度学习探索指南(全)

原文:Grokking Deep Learning

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章. 介绍深度学习:为什么你应该学习它

本章内容

  • 你为什么应该学习深度学习

  • 你为什么应该阅读这本书

  • 你需要开始学习的内容

“不要担心你在数学上的困难。我可以向你保证,我的困难仍然更大。”

阿尔伯特·爱因斯坦

欢迎来到 掌握深度学习

你即将学习到本世纪最有价值的技能之一!

我非常激动你能在这里!你也应该如此!深度学习代表了机器学习和人工智能之间令人兴奋的交汇点,对社会和产业产生了非常重大的颠覆。本书中讨论的方法正在改变你周围的世界。从优化你的汽车引擎到决定你在社交媒体上查看的内容,它无处不在,它强大,而且幸运的是,它很有趣!

你为什么应该学习深度学习

它是智能增量自动化的强大工具

从时间的开始,人类一直在建造更好的工具来理解和控制我们周围的环境。深度学习是今天这个创新故事的最新篇章。

也许使这一章节如此引人入胜的是,这个领域更多的是一种 思维 创新,而不是 机械 创新。与机器学习的姊妹领域一样,深度学习试图 逐步自动化智能。在过去的几年里,它在这一努力中取得了巨大的成功和进步,超过了计算机视觉、语音识别、机器翻译和其他许多任务的前期记录。

这尤其非凡,因为深度学习似乎使用 大量基于大脑的算法(神经网络)在众多领域实现这些成就。尽管深度学习仍然是一个积极发展的领域,面临着许多挑战,但最近的发展已经引起了巨大的兴奋:也许我们不仅发现了一个伟大的工具,而且打开了我们自己心智的窗口。

深度学习有潜力显著自动化熟练劳动力

如果按照当前的趋势以不同的速度外推,围绕深度学习潜在影响的炒作相当多。尽管许多预测都过于乐观,但我认为有一个值得你考虑:工作替代。我认为这个论断与众不同,因为即使深度学习的创新今天停止,它对全球熟练劳动力的影响也将是巨大的。呼叫中心操作员、出租车司机和低级商业分析师是深度学习可以提供低成本替代方案的引人注目的例子。

幸运的是,经济并不是一夜间就会转变;但就许多方面而言,我们已经超过了担忧的点,考虑到当前技术的力量。我希望这本书能够让你(以及你认识的人)从可能面临颠覆的行业过渡到一个充满增长和繁荣的行业:深度学习。

这本书既有趣又富有创意。通过尝试模拟智能和创造力,你会发现很多关于人类本质的事情。

个人的经历是,我之所以对深度学习感兴趣,是因为它非常迷人。它是人类和机器之间一个惊人的交汇点。解开思考、推理和创造的含义是启发性的、吸引人的,并且对我来说,是鼓舞人心的。考虑一下拥有包含所有已绘制的绘画的数据集,然后使用它来教机器如何像莫奈一样绘画。不可思议的是,这是可能的,看到它是如何工作的,真是令人难以置信地酷。

学习这个会很难吗?

你需要工作多长时间才能有“有趣”的回报?

这是我最喜欢的问题。我对“有趣”的回报的定义是见证我所构建的东西学习的过程。看到你亲手创造的东西做这样的事情,真是令人惊叹。如果你也有同样的感觉,那么答案很简单。在第三章的几页之后,你将创建你的第一个神经网络。在此之前,唯一需要做的工作就是阅读这里和那里之间的页面。

在第三章之后,你可能想知道,在你记住了小块代码并继续读到第四章的中段之后,下一个有趣的回报就会发生。每一章都会这样进行:记住前一章的小段代码,阅读下一章,然后体验新学习神经网络带来的回报。

你为什么应该阅读这本书

它有独特的低门槛。

你应该阅读这本书的原因和我写这本书的原因是一样的。我不知道还有其他资源(书籍、课程、大型博客系列)能够教授深度学习而不需要假设对数学有高级知识(数学领域的大学学位)。

请不要误解:用数学来教授它确实有很好的理由。毕竟,数学是一种语言。当然,用这种语言教授深度学习是更有效的,但我认为,为了成为一个熟练的知识渊博的从业者,对深度学习背后的“如何”有一个牢固的理解,并不绝对需要假设对数学有高级知识。

那么,为什么你应该用这本书来学习深度学习呢?因为我将假设你具有高中水平的数学背景(并且可能有些生疏),并且在我们前进的过程中解释你需要知道的其他所有内容。还记得乘法吗?还记得 x-y 图(上面有线的正方形)吗?太棒了!你会没事的。

这将帮助你理解框架(如 Torch、TensorFlow 等)内部的结构

深度学习教育材料(如书籍和课程)主要有两大类。一类是围绕如何使用流行的框架和代码库(如 Torch、TensorFlow、Keras 等)展开的。另一类是围绕教授深度学习本身,也就是这些主要框架的内部科学。最终,了解两者都是重要的。就像如果你想成为一名纳斯卡赛车手:你需要了解你驾驶的特定车型(框架)以及驾驶(科学/技能)。但仅仅学习框架就像在不知道什么是手动变速器的情况下学习第六代雪佛兰 SS 的优点和缺点。这本书是关于教你深度学习是什么,这样你就可以准备好学习一个框架。

所有与数学相关的材料都将通过直观的类比来支撑

每当我遇到野外的数学公式时,我会采取两步方法。第一步是将它的方法转化为对现实世界的直观类比。我几乎从不直接接受公式:我会将其分解为部分,每个部分都有自己的故事。这本书也将采取这种方法。每当遇到数学概念时,我会提供一个替代的类比,说明公式实际上在做什么。

“一切都应该尽可能简单,但不能过于简单。”

归功于阿尔伯特·爱因斯坦

引言章节之后的所有内容都是“基于项目”的

如果在学习新事物时有一件事让我讨厌,那就是不得不质疑我所学习的内容是否有用或相关。如果有人教我关于锤子的所有知识,但实际上并没有真正伸出手帮我钉钉子,那么他们实际上并没有真正教我如何使用锤子。我知道会有一些没有连接的点,如果我带着锤子、一盒钉子和一些两英寸乘四英寸的木板被扔到现实世界中,我不得不做一些猜测。

这本书是关于在你了解它们的作用之前,先给你木材、钉子和锤子*。每节课都是关于拿起工具并用它们来建造东西,我们在进行中解释事物是如何工作的。这样,你不会带着关于你将使用的各种深度学习工具的事实清单离开;你将有能力使用它们来解决问题。此外,你将理解最重要的部分:何时以及为什么每个工具适合你想要解决的问题。有了这些知识,你将能够追求研究或工业领域的事业。

你需要开始的地方

安装 Jupyter Notebook 和 NumPy Python 库

我最喜欢的工作地点是 Jupyter Notebook。对我来说,学习深度学习(对我来说)最重要的部分是能够在网络训练时停止它,并彻底拆解每一个部分来看它是什么样子。这是 Jupyter Notebook 极其有用的地方。

对于 NumPy 来说,这本书没有遗漏任何内容的最有说服力的案例可能是我们只会使用一个矩阵库。这样,你将理解如何一切工作,而不仅仅是如何调用一个框架。这本书从零开始教授深度学习,从汤到坚果。

这些两个工具的安装说明可以在Jupyterjupyter.orgNumPynumpy.org找到。我将使用 Python 2.7 构建示例,但我已经测试了它们在 Python 3 上的兼容性。为了方便安装,我还推荐使用 Anaconda 框架:docs.continuum.io/anaconda/install

通过高中数学

一些数学假设对于这本书来说过于深入,但我的目标是假设你只理解基本的代数来教授深度学习。

找一个你感兴趣的个人问题

这可能看起来像是一个可选的“需求”来开始。我想它可能是,但说真的,我强烈、强烈推荐找到一个。我所认识的在这个领域取得成功的人都有一些他们试图解决的问题。学习深度学习只是解决其他一些有趣任务的“依赖”。

对于我来说,是使用 Twitter 来预测股市。这只是我觉得非常有趣的事情。这是驱使我坐下来阅读下一章并构建下一个原型的原因。

事实上,这个领域是如此的新兴,变化如此之快,以至于如果你用接下来的几年时间追逐一个项目使用这些工具,你会发现你比想象中更快地成为该特定问题的领先专家。对我来说,追逐这个想法让我从几乎对编程一无所知到在约 18 个月内获得了一个对冲基金的研究资助,并应用了我所学到的知识!对于深度学习来说,有一个你着迷的问题,涉及到使用一个数据集来预测另一个数据集,这是关键催化剂!去找一个吧!

你可能需要一些 Python 知识

Python 是我首选的教学库,但我会在网上提供一些其他的库

Python 是一种非常直观的语言。我认为它可能是迄今为止最广泛采用且直观易读的语言。此外,Python 社区对简洁的热爱是无与伦比的。出于这些原因,我想在所有示例中都坚持使用 Python(我在使用 Python 2.7)。在本书的可下载源代码中,可在www.manning.com/books/grokking-deep-learninggithub.com/iamtrask/Grokking-Deep-Learning找到,我在网上提供了各种其他语言的示例。

你应该有多少编码经验?

阅读 Python Codecademy 课程(www.codecademy.com/learn/python)。如果你能阅读目录并熟悉其中提到的术语,那么你就准备好了!如果不熟悉,那么就参加这门课程,完成后再回来。这门课程设计为初学者课程,制作得非常精良。

摘要

如果你手头有一本 Jupyter 笔记本,并且对 Python 的基础知识感到舒适,那么你就准备好进入下一章了!提前提醒一下,第二章将是最后一章,主要基于对话(不涉及构建内容)。它的目的是让你对人工智能、机器学习以及最重要的深度学习中的高级词汇、概念和领域有所了解。

第二章. 基本概念:机器是如何学习的?

本章

  • 深度学习、机器学习和人工智能是什么?

  • 参数模型非参数模型是什么?

  • 监督学习无监督学习是什么?

  • 机器如何学习?

“机器学习将在五年内导致每一次成功的 IPO 胜利。”

埃里克·施密特,谷歌执行董事长,云计算平台会议主题演讲,2016 年

深度学习是什么?

深度学习是机器学习方法的一个子集

深度学习是机器学习的一个子集,机器学习是一个致力于研究和发展能够学习的机器(有时目标是最终实现通用人工智能)的领域。

在工业界,深度学习被用于解决计算机视觉(图像)、自然语言处理(文本)和自动语音识别(音频)等各个领域的实际任务。简而言之,深度学习是机器学习工具箱中的一种方法子集,主要使用人工神经网络,这是一种类算法,其灵感来源于人脑。

注意到在这个图中,并非所有深度学习都集中在追求通用人工智能(如电影中的感知机器)上。许多这种技术的应用都是用来解决工业中各种各样的问题。本书旨在专注于教授深度学习的基础知识,无论是前沿研究还是工业应用,都有助于为这两种情况做好准备。

机器学习是什么?

“一个研究领域,它赋予计算机在没有明确编程的情况下学习的能力。”

归功于亚瑟·萨缪尔

由于深度学习是机器学习的一个子集,那么机器学习是什么?最普遍地说,它就是其名称所暗示的。机器学习是计算机科学的一个子领域,其中机器学习执行那些它们没有被明确编程的任务。简而言之,机器观察到一个模式,并试图以某种方式直接或间接地模仿它。

我将直接和间接模仿与机器学习的两种主要类型相提并论:监督学习无监督学习。监督机器学习是两个数据集之间模式的直接模仿。它总是试图将一个输入数据集转换成一个输出数据集。这可以是一种非常强大和有用的能力。考虑以下例子(输入数据集加粗,输出数据集斜体):

  • 使用图像的像素来检测存在不存在

  • 使用你喜欢的电影来预测你可能喜欢的更多电影

  • 使用某人的话语来预测他们是否快乐悲伤

  • 使用天气传感器数据来预测降雨****概率

  • 使用汽车发动机传感器来预测最佳的调整****设置

  • 使用新闻数据来预测明天的股价

  • 使用一个输入数字来预测其两倍大小的数字

  • 使用原始音频文件来预测音频的转录

这些都是监督机器学习任务。在所有情况下,机器学习算法都试图模仿两个数据集之间的模式,以便它能够使用一个数据集来预测另一个数据集。对于这些例子中的任何一个,想象一下,如果你只有输入数据集,就有能力预测输出数据集。这种能力将是深远的。

监督机器学习

监督学习转换数据集

监督学习是将一个数据集转换为另一个数据集的方法。例如,如果你有一个名为“周一股票价格”的数据集,它记录了过去 10 年每周一每只股票的价格,以及另一个名为“周二股票价格”的数据集,它记录了同一时间段内的价格,一个监督学习算法可能会尝试使用一个来预测另一个。

图片

如果你成功地在 10 年的周一和周二数据上训练了监督机器学习算法,那么你就可以根据前一个周一的股票价格预测未来任何一天的周二股票价格。我鼓励你停下来思考一下这一点。

监督机器学习是应用人工智能(也称为窄 AI)的精髓。它有助于将你已知的知识作为输入,快速转化为你想要知道的知识。这使得监督机器学习算法能够在看似无限多的方式上扩展人类智能和能力。

大多数使用机器学习的工作结果都是训练某种类型的监督分类器。即使是未监督的机器学习(你将在下一刻了解更多关于它的信息)通常也是为了帮助开发一个准确的监督机器学习算法。

图片

在本书的剩余部分,你将创建能够接受可观察、可记录的输入数据,并通过扩展,可知的数据,并将其转换为需要逻辑分析的有价值输出数据算法。这就是监督机器学习的力量。

未监督机器学习

未监督学习对数据进行分组

未监督学习与监督学习有一个共同属性:它将一个数据集转换为另一个数据集。但它转换成的数据集不是事先已知或理解的。与监督学习不同,没有“正确答案”是你试图让模型复制的。你只需告诉未监督算法“在这个数据中找到模式,并告诉我关于它们的信息。”

例如,将数据集聚类成组是一种无监督学习。聚类将一系列数据点转换成一系列聚类标签。如果它学习了 10 个聚类,这些标签通常是数字 1-10。每个数据点将根据它所在的聚类分配一个数字。因此,数据集从一系列数据点变成一系列标签。为什么标签是数字?算法没有告诉你这些聚类的名称。它怎么能知道呢?它只是说:“嘿科学家!我找到了一些结构。看起来你的数据中有些组。这里它们是!”

图片

我有个好消息!这种聚类的想法,你可以作为无监督学习的定义,可靠地保存在你的脑海中。尽管无监督学习有许多形式,所有形式的无监督学习都可以被视为一种聚类形式。你将在本书后面的内容中了解更多。

图片

查看这个例子。尽管算法没有告诉这些聚类的名称,你能想出它是如何聚类这些单词的吗?(答案:1 == cute 和 2 == delicious。)稍后,我们将解开其他形式的无监督学习也是聚类形式的原因,以及这些聚类对于监督学习是有用的。

参数化与非参数化学习

过于简化:试错学习与计数和概率

最后两页将所有机器学习算法分为两组:监督学习和无监督学习。现在,我们将讨论另一种将相同的机器学习算法分为两组的方法:参数化和非参数化。所以,如果我们考虑我们的小型机器学习云,它有两个设置:

图片

如您所见,实际上有四种不同的算法可供选择。一个算法要么是无监督的,要么是监督的,要么是参数化的,要么是非参数化的。而前一个关于监督的部分是关于学习到的模式类型,参数化则是关于学习是如何存储的,通常,通过扩展,也是关于学习方法。首先,让我们看看参数化与非参数化的正式定义。据记录,关于确切差异的讨论仍然存在。

参数化模型的特点是具有固定数量的参数,而非参数化模型的参数数量是无限的(由数据决定)。

例如,假设问题是将一个正方体放入正确的(正方形)孔中。有些人(如婴儿)只是把它塞进所有的孔,直到它适合某个地方(参数化)。然而,一个青少年可能会数一数边的数量(四条),然后寻找具有相同数量的孔(非参数化)。参数化模型倾向于使用试错法,而非参数化模型倾向于计数。让我们更深入地了解一下。

监督参数化学习

过于简化:使用旋钮的试错学习

监督参数学习机器是具有固定数量旋钮的机器(这就是参数部分),学习是通过旋转旋钮来进行的。输入数据进来,根据旋钮的角度进行处理,并转换为预测

学习是通过调整旋钮到不同的角度来完成的。如果您试图预测红袜队是否会赢得世界系列赛,那么这个模型首先会收集数据(例如体育统计数据,如胜负记录或平均每名球员的脚趾数量)并做出预测(例如 98%的机会)。接下来,模型会观察红袜队是否真的获胜。在知道他们是否获胜后,学习算法会更新旋钮,以便在下一次看到相同或类似输入数据时做出更准确的预测。

如果球队的胜负记录是一个好的预测因素,它可能会“提高”胜负记录旋钮。相反,如果这个数据点不是一个好的预测因素,它可能会“降低”平均每名球员的脚趾数量旋钮。这就是参数模型学习的方式!

注意,模型所学习的一切都可以通过任何给定时间旋钮的位置来捕捉。您也可以将这种学习模型视为一种搜索算法。您通过尝试配置、调整它们并重新尝试来“搜索”适当的旋钮配置。

进一步注意,试错的概念并不是正式的定义,但它是非参数模型的一个常见(但有例外)属性。当存在任意(但固定)数量的旋钮可以旋转时,需要一定程度的搜索来找到最佳配置。这与非参数学习形成对比,非参数学习通常是基于计数,并且(或多或少)在发现新的计数对象时添加新的旋钮。让我们将监督参数学习分解为其三个步骤。

第 1 步:预测

为了说明监督参数学习,让我们继续使用体育类比,尝试预测红袜队是否会赢得世界系列赛。正如之前提到的,第一步是收集体育统计数据,将它们通过机器,并预测红袜队获胜的概率。

第 2 步:与真实模式比较

第二步是将预测结果(98%)与您关注的模式(例如红袜队是否获胜)进行比较。遗憾的是,他们输了,所以比较是这样的

  • 预测:98% > 事实:0%

这一步认识到,如果模型预测了 0%,它就能完美地预测球队即将到来的失败。您希望机器准确,这导致了第 3 步。

第 3 步:学习模式

这一步通过研究模型在预测时多少次错过了(98%)以及输入数据是什么(体育统计数据)来调整旋钮。然后,根据输入数据调整旋钮,以便做出更准确的预测。

理论上,下次这个步骤看到相同的体育统计数据时,预测值将低于 98%。请注意,每个旋钮代表预测对不同类型输入数据的敏感性。这就是你在“学习”时改变的内容。

无监督参数学习

无监督参数学习采用了一种非常相似的方法。让我们从高层次的角度来梳理一下步骤。记住,无监督学习完全是关于数据分组。无监督参数学习使用旋钮来分组数据。但在这种情况下,通常为每个组提供几个旋钮,每个旋钮将输入数据的亲和力映射到特定的组(有例外和细微差别——这是一个高层次描述)。让我们来看一个例子,假设你想要将数据分成三个组。

主场或客场 # 球迷数量
主队 客队 主队 客队 客队 客队 100k 50k 100k 99k 50k 10k 11k

在数据集中,我已经识别出三个你可能会希望参数模型找到的簇。它们通过格式化为组 1组 2组 3来表示。让我们将第一个数据点通过一个训练好的无监督模型传播,如下所示。注意,它最强烈地映射到组 1

每个组的机器试图将输入数据转换为一个介于 0 到 1 之间的数字,告诉我们输入数据是该组的概率是成员的概率。这些模型在训练方式和最终属性上存在很大差异,但就高层次而言,它们调整参数以将输入数据转换为其所属的组(们)。

非参数学习

过于简化的说法:基于计数的方法

非参数学习是一类算法,其中参数的数量基于数据(而不是预定义的)。这使得它适用于一般以某种方式计数的方法,从而根据数据中计数项的数量增加参数的数量。例如,在监督设置中,一个非参数模型可能会计算特定颜色的街灯导致汽车“行驶”的次数。在只计算了几个例子之后,这个模型就能够预测中间的灯总是(100%)导致汽车行驶,而右侧的灯只有时(50%)导致汽车行驶。

注意到这个模型将有三个参数:三个计数表示每种颜色的灯光开启和汽车行驶的次数(可能除以总观察次数)。如果有五个灯光,就会有五个计数(五个参数)。使这个简单模型成为非参数模型的特点是参数的数量根据数据变化(在这种情况下,是灯光的数量)。这与参数模型形成对比,参数模型从一组固定的参数开始,更重要的是,科学家可以根据自己的意愿调整模型参数的数量或减少(无论数据如何)。

仔细观察的人可能会质疑这个想法。之前的参数模型似乎为每个输入数据点都有一个旋钮。大多数参数模型仍然需要基于数据中的类别数量某种形式的输入。因此,你可以看到参数和非参数算法之间存在一个灰色区域。即使是参数算法也多少会受到数据中类别数量的影响,即使它们没有明确地计数模式。

这也说明了“参数”是一个通用术语,仅指用于建模模式的数字集合(没有任何关于这些数字如何使用的限制)。计数是参数。权重是参数。计数或权重的归一化变体是参数。相关系数可以是参数。这个术语指的是用于建模模式的数字集合。实际上,深度学习是一类参数模型。我们在这本书中不会进一步讨论非参数模型,但它们是一类有趣且强大的算法。

摘要

在本章中,我们深入探讨了机器学习的各种风味。你了解到机器学习算法要么是监督的,要么是无监督的,要么是参数的,要么是非参数的。此外,我们还探讨了是什么使得这四组不同的算法各具特色。你了解到监督机器学习是一类算法,其中你学会根据另一个数据集预测一个数据集,而无监督学习通常将单个数据集分组为各种类型的聚类。你了解到参数算法具有固定数量的参数,而非参数算法根据数据集调整其参数数量。

深度学习使用神经网络进行监督学习和无监督预测。到目前为止,我们一直停留在概念层面,帮助你在这个领域找到自己的位置。在下一章,你将构建你的第一个神经网络,所有随后的章节都将基于项目。所以,拿出你的 Jupyter 笔记本,让我们开始吧!

第三章. 神经预测简介:前向传播

本章

  • 一个简单的网络进行预测

  • 神经网络是什么,它做什么?

  • 使用多个输入进行预测

  • 使用多个输出进行预测

  • 使用多个输入和输出进行预测

  • 在预测上进行预测

“我尽量避免参与预测业务。这很容易让人看起来像个白痴。”

沃伦·埃利斯,漫画书作家、小说家和编剧

第 1 步:预测

本章是关于预测的

在上一章中,你学习了关于预测、比较、学习的范例。在本章中,我们将深入探讨第一步:预测。你可能记得预测步骤看起来很像这样:

图片

在本章中,你将了解神经网络预测的这三个不同部分在内部是如何工作的。让我们从第一个开始:数据。在你的第一个神经网络中,你将一次预测一个数据点,如下所示:

图片

以后你会发现,你一次处理的点数数量对网络的外观有重大影响。你可能想知道,“我如何选择一次传播多少点数?”答案是取决于你认为神经网络是否可以用你给它的数据准确预测。

例如,如果我在尝试预测照片中是否有猫,我肯定需要一次性向我的网络展示图像的所有像素。为什么?好吧,如果我只给你一个像素的图像,你能分类图像中是否含有猫吗?我也不能!(顺便说一句,这是一个一般性的规则:总是向网络提供足够的信息,其中“足够的信息”被宽泛地定义为人类可能需要做出相同预测所需的信息量。)

现在让我们先跳过网络。实际上,只有在你理解了输入和输出数据集的形状之后,你才能创建一个网络(现在,“形状”意味着“列数”或“你一次处理的点数数量”)。让我们专注于对棒球队能否获胜的单一预测:

图片

现在你已经知道你想要输入一个数据点并输出一个预测,你可以创建一个神经网络。因为你只有一个输入数据点和输出数据点,所以你将构建一个将输入点映射到输出点的单一控制网络。(抽象地说,这些“控制”实际上被称为权重,从现在起我将这样称呼它们。)所以,不再多言,这是你的第一个神经网络,它有一个单一的权重将输入“脚趾数量”映射到输出“赢?”:

图片

如你所见,这个网络一次只接受一个数据点(棒球队伍中每名球员的平均脚趾数量)并输出一个预测(它认为队伍是否会赢)。

一个简单的神经网络进行预测

让我们从最简单的神经网络开始

图片

图片

图片

图片

什么是神经网络?

这是你的第一个神经网络

要启动神经网络,打开 Jupyter 笔记本并运行以下代码:

weight = 0.1

def neural_network(input, weight):      *1*

    prediction = input * weight         *1*

    return prediction                   *1*
  • 1 网络*

现在,运行以下代码:

number_of_toes = [8.5, 9.5, 10, 9]      *1*

input = number_of_toes[0]               *1*

pred = neural_network(input,weight)     *1*
print(pred)                             *1*
  • 1 如何使用网络进行预测*

你刚刚制作了你的第一个神经网络,并使用它进行了预测!恭喜!最后一行打印了预测(pred)。它应该是 0.85。那么,神经网络是什么?现在,它是一个或多个可以乘以输入数据的权重来做出预测

什么是输入数据?

它是在现实世界中的某个地方记录的数字。它通常是容易知道的事情,比如今天的温度、棒球运动员的打击率,或者昨天的股价。

什么是预测?

预测是神经网络告诉你的内容,给定输入数据,例如“给定温度,今天人们穿运动服的可能性是0%”或“给定棒球运动员的打击率,他击出全垒打的概率是30%”或“给定昨天的股价,今天的股价将是101.52”。

这个预测总是正确吗?

有时候,神经网络会犯错误,但它可以从错误中学习。例如,如果它预测得过高,它将调整其权重以在下一次预测得更低,反之亦然。

网络是如何学习的?

尝试和错误!首先,它尝试做出预测。然后,它查看预测是否过高或过低。最后,它改变权重(向上或向下)以在下一次看到相同输入时更准确地预测。

这个神经网络做什么?

它将输入乘以一个权重。它通过一定的量“缩放”输入

在上一节中,你使用神经网络进行了第一次预测。最简单的神经网络形式使用乘法的力量。它取一个输入数据点(在这种情况下,8.5)并将其与权重相乘。如果权重是 2,那么神经网络将加倍输入。如果权重是 0.01,那么网络将输入除以 100。正如你所看到的,一些权重值使输入变大,而其他值使输入变小

图片

神经网络的界面很简单。它接受一个input变量作为信息和一个weight变量作为知识,并输出一个prediction。你将看到的每一个神经网络都是这样工作的。它使用权重中的知识来解释输入数据中的信息。后来的神经网络将接受更大、更复杂的inputweight值,但这个相同的根本前提始终是正确的。

图片

在这种情况下,信息是比赛前棒球队的平均脚趾数。请注意以下几点。首先,神经网络没有访问任何除了一个实例之外的信息。如果在这次预测之后,你输入number_of_toes[1],网络不会记得它在最后一个时间步长中做出的预测。神经网络只知道你作为输入提供的内容。它会忘记其他所有内容。稍后,你将学习如何通过一次输入多个输入来给神经网络一个“短期记忆”。

图片

另一种思考神经网络权重值的方法是将其视为网络输入和预测之间的敏感性度量。如果权重非常高,那么即使是最微小的输入也能产生一个非常大的预测!如果权重非常小,那么即使大的输入也会产生小的预测。这种敏感性类似于音量。“提高权重”放大了相对于输入的预测:权重就像一个音量旋钮!

图片

在这种情况下,神经网络实际上是在对number_of_toes变量应用一个音量旋钮。理论上,这个音量旋钮可以告诉你,根据球队每个球员的平均脚趾数,球队获胜的可能性。这可能或可能不奏效。说实话,如果球队成员的平均脚趾数为 0,他们可能会打得非常糟糕。但棒球比这要复杂得多。在下一节中,你将同时提供多份信息,以便神经网络可以做出更明智的决策。

注意,神经网络不仅预测正数——它们还可以预测负数,甚至可以将负数作为输入。也许你想预测今天人们穿大衣的概率。如果温度是-10 摄氏度,那么一个负权重将预测人们穿大衣的概率很高。

图片

使用多个输入进行预测

神经网络可以结合多个数据点的智能

之前的神经网络能够接受一个数据点作为输入,并基于该数据点做出一个预测。也许你一直在想,“平均脚趾数真的能作为一个好的预测指标吗?”如果是这样,你就有发现了。如果你能一次给网络提供比每个球员的平均脚趾数更多的信息,会怎样?在这种情况下,理论上,网络应该能够做出更准确的预测。好吧,实际上,网络可以一次接受多个输入数据点。看看下一个预测:

图片

图片

图片

图片

多个输入:这个神经网络做什么?

它将三个输入乘以三个旋钮权重,并将它们相加。T- 这是一个加权求和

在上一节的结尾,你意识到你简单神经网络的限制因素:它只是一个数据点的音量旋钮。在示例中,这个数据点是棒球队伍每位球员的平均脚趾数。你了解到,为了做出准确的预测,你需要构建能够同时结合多个输入的神经网络。幸运的是,神经网络完全能够做到这一点。

这个新的神经网络可以一次接受每个预测的多个输入。这使得网络能够结合各种形式的信息,做出更明智的决策。但使用权重的根本机制并没有改变。你仍然需要将每个输入通过其自身的音量旋钮。换句话说,你需要将每个输入乘以其自身的权重。

这里的新特性是,由于你有多个输入,你必须分别求和它们的预测值。因此,你需要将每个输入乘以其相应的权重,然后将所有局部预测值相加。这被称为输入的加权求和,或简称为加权求和。有些人也将加权求和称为点积,你将会看到。

一个相关的提醒

神经网络的接口很简单:它接受一个input变量作为信息和一个weights变量作为知识,并输出一个预测。

同时处理多个输入的新需求证明了使用新工具的必要性。这个工具被称为向量,如果你一直在你的 Jupyter 笔记本中跟随,你已经使用过它了。向量不过是一个数字列表。在示例中,input是一个向量,weights也是一个向量。你能在之前的代码中找到更多的向量吗?(还有三个。)

实际上,向量在你想执行涉及数字组的操作时非常有用。在这种情况下,你正在执行两个向量之间的加权求和(点积)。你正在取两个长度相等的向量(inputweights),根据其位置(input中的第一个位置乘以weights中的第一个位置,以此类推)乘以每个数字,然后将结果相加。

任何时间当你对两个长度相等的向量进行数学运算,并且根据它们在向量中的位置配对值(再次强调:位置 0 与 0 配对,1 与 1 配对,以此类推),这被称为逐元素运算。因此,逐元素加法是将两个向量相加,而逐元素乘法是两个向量的乘法。

挑战:向量数学

能够操作向量是深度学习的一个基石技术。看看你是否能编写执行以下操作的函数:

  • def elementwise_multiplication(vec_a, vec_b)

  • def elementwise_addition(vec_a, vec_b)

  • def vector_sum(vec_a)

  • def vector_average(vec_a)

然后,看看你是否可以使用这两种方法之一来执行点积!

图片

点积(加权求和)如何以及为什么工作背后的直觉是真正理解神经网络如何进行预测的最重要部分之一。简单地说,点积给出了两个向量之间的“相似度”概念。考虑以下例子:

|

a = [ 0, 1, 0, 1]
b = [ 1, 0, 1, 0]
c = [ 0, 1, 1, 0]
d = [.5, 0,.5, 0]
e = [ 0, 1,-1, 0]

|

w_sum(a,b) = 0
w_sum(b,c) = 1
w_sum(b,d) = 1
w_sum(c,c) = 2
w_sum(d,d) = .5
w_sum(c,e) = 0

|

最大的加权求和(w_sum(c,c))是在完全相同的向量之间。相比之下,因为ab没有重叠的权重,它们的点积为零。也许最有趣的加权求和是在ce之间,因为e有一个负权重。这个负权重抵消了它们之间的正相似性。但是,e与自身的点积会产生数字 2,尽管有负权重(双重否定转为正数)。让我们熟悉点积操作的各项属性。

有时可以将点积的性质等同于逻辑“与”。考虑ab

a = [ 0, 1, 0, 1]
b = [ 1, 0, 1, 0]

如果你问a[0] AND b[0]是否都有值,答案是“否”。如果你问a[1] AND b[1]是否都有值,答案仍然是“否”。因为这对于所有四个值都是“总是”成立的,所以最终分数等于 0。每个值都未通过逻辑“与”。

b = [ 1, 0, 1, 0]
c = [ 0, 1, 1, 0]

然而,bc有一个共享值的列。它通过了逻辑“与”,因为b[2]c[2]都有权重。这一列(仅这一列)导致分数上升到 1。

c = [ 0, 1, 1, 0]
d = [.5, 0,.5, 0]

幸运的是,神经网络也能够模拟部分“与”运算。在这种情况下,cdbc共享相同的列,但由于d在那里只有 0.5 的权重,最终分数只有 0.5。我们在神经网络建模概率时利用了这一属性。

d = [.5, 0,.5, 0]
e = [-1, 1, 0, 0]

在这个类比中,负权重往往意味着逻辑“非”运算符,因为任何与负权重配对的正权重都会导致分数下降。此外,如果两个向量都具有负权重(例如w_sum(e,e)),那么神经网络将执行“双重否定”并添加权重。此外,有些人可能会说这是“与”之后的“或”,因为如果任何一行显示权重,分数就会受到影响。因此,对于w_sum(a,b),如果(a[0] AND b[0])“或”(a[1] AND b[1]),依此类推,那么w_sum(a,b)返回一个正分数。此外,如果其中一个值是负数,那么该列就得到一个“非”。

有趣的是,这为我们提供了一种阅读权重的粗略语言。让我们读几个例子,好吗?这些假设你在执行w_sum(input,weights),并且这些if语句的“然后”是一个抽象的“然后给出高分”:

weights = [ 1, 0, 1] => if input[0] OR input[2]

weights = [ 0, 0, 1] => if input[2]

weights = [ 1, 0, -1] => if input[0] OR NOT input[2]

weights = [ -1, 0, -1] => if NOT input[0] OR NOT input[2]

weights = [ 0.5, 0, 1] => if BIG input[0] or input[2]

注意在最后一行中,weight[0] = 0.5 表示相应的 input[0] 必须更大才能补偿较小的权重。正如我提到的,这是一种非常粗略的近似语言。但当我试图在脑海中想象底层的运作时,我发现它非常有用。这将在未来对你有很大帮助,尤其是在以越来越复杂的方式组合网络时。

基于这些直觉,当神经网络进行预测时,这意味着什么?粗略地说,这意味着网络根据输入与权重的相似度给出高分数。注意在下面的示例中,nfans 在预测中被完全忽略,因为与之相关的权重是 0。最敏感的预测器是 wlrec,因为其权重是 0.2。但高分的主导力量是脚趾的数量(ntoes),不是因为权重最高,而是因为输入与权重的组合远远是最高的。

这里有一些额外的要点需要注意,以供进一步参考。你不能随机排列权重:它们需要处于特定的位置。此外,权重的值和输入的值都会决定对最终得分的整体影响。最后,负权重将导致某些输入减少最终预测(反之亦然)。

多个输入:完整的可运行代码

这个例子中的代码片段组合在以下代码中,该代码创建并执行了一个神经网络。为了清晰起见,我使用 Python 的基本属性(列表和数字)将所有内容都写出来了。但有一种更好的方法,我们将在未来开始使用。

之前的代码

def w_sum(a,b):

    assert(len(a) == len(b))

    output = 0

    for i in range(len(a)):
        output += (a[i] * b[i])

    return output

weights = [0.1, 0.2, 0]

def neural_network(input, weights):

    pred = w_sum(input,weights)

    return pred

toes = [8.5, 9.5, 9.9, 9.0]
wlrec = [0.65, 0.8, 0.8, 0.9]
nfans = [1.2, 1.3, 0.5, 1.0]

input = [toes[0],wlrec[0],nfans[0]]    *1*
pred = neural_network(input,weights)
print(pred)
  • 1 输入对应于赛季第一场比赛的每个条目。

有一个名为 NumPy 的 Python 库,代表“数值 Python”。它具有创建向量和执行常见函数(如点积)的高效代码。无需多言,以下是相同的代码,使用 NumPy 实现。

NumPy 代码

import numpy as np

weights = np.array([0.1, 0.2, 0])

def neural_network(input, weights):

    pred = input.dot(weights)

    return pred

toes = np.array([8.5, 9.5, 9.9, 9.0])
wlrec = np.array([0.65, 0.8, 0.8, 0.9])
nfans = np.array([1.2, 1.3, 0.5, 1.0])

input = np.array([toes[0],wlrec[0],nfans[0]])    *1*
pred = neural_network(input,weights)
print(pred)
  • 1 输入对应于赛季第一场比赛的每个条目。

两个网络都应该输出 0.98。注意在 NumPy 代码中,你不需要创建一个 w_sum 函数。相反,NumPy 有一个可以调用的 dot 函数(简称“点积”)。你将来会使用的许多函数都有 NumPy 的对应版本。

使用多个输出进行预测

神经网络也可以仅使用单个输入进行多次预测

可能比多个输入更简单的一种增强是多输出。预测发生的方式与有三个独立单权重神经网络的预测相同。

在这种设置中,最重要的评论是要注意三个预测是完全独立的。与具有多个输入和单个输出的神经网络不同,那里的预测无疑是相互关联的,这个网络真正地表现为三个独立的组件,每个组件接收相同的数据输入。这使得网络易于实现。

图片

图片

图片

使用多个输入和输出进行预测

神经网络可以根据多个输入预测多个输出

最后,构建具有多个输入或输出的网络的方式可以组合起来构建一个具有多个输入多个输出的网络。就像之前一样,一个权重将每个输入节点连接到每个输出节点,并且预测以通常的方式进行。

图片

图片

图片

图片

多个输入和输出:它是如何工作的?

它对输入执行三个独立的加权求和以做出 - 三个预测

你可以从两个角度看待这个架构:将其视为每个输入节点出来的三个权重,或者每个输出节点进入的三个权重。目前,我发现后者更有益。将这个神经网络视为三个独立的点积:三个独立的输入加权求和。每个输出节点对其输入进行自己的加权求和并做出预测。

图片

图片

图片

如前所述,我们选择将这个网络视为一系列加权求和。因此,之前的代码创建了一个名为vect_mat_mul的新函数。这个函数遍历每个权重的行(每一行都是一个向量)并使用w_sum函数进行预测。它实际上执行了三个连续的加权求和,并将它们的预测存储在一个名为output的向量中。这里有很多权重在飞舞,但这并不比其他你见过的网络更高级。

我想使用这个向量列表一系列加权求和的逻辑来介绍两个新概念。看看步骤 1 中的weights变量?它是一个向量列表。向量列表被称为矩阵。正如其名所示。常用的函数使用矩阵。其中之一被称为向量-矩阵乘法。一系列加权求和正是这样:你取一个向量并与矩阵中的每一行进行点积。^([*]) 你将在下一节中发现,NumPy 有特殊的函数来帮助。

^*

如果你熟悉线性代数,更正式的定义是将权重存储/处理为列向量而不是行向量。这将在稍后纠正。

在预测上进行预测

神经网络可以堆叠!

如以下图所示,你也可以将一个网络的输出作为输入提供给另一个网络。这会导致连续两次向量-矩阵乘法。你可能还不清楚为什么会有这样的预测;但有些数据集(如图像分类)包含过于复杂的模式,无法用一个单权重矩阵来表示。稍后,我们将讨论这些模式的特点。现在,只需知道这是可能的就足够了。

下面的列表展示了如何使用一个方便的 Python 库 NumPy 来执行上一节中用代码编写的相同操作。使用像 NumPy 这样的库可以使你的代码更快、更容易阅读和编写。

NumPy 版本

import numpy as np

# toes % win # fans
ih_wgt = np.array([
            [0.1, 0.2, -0.1], # hid[0]
            [-0.1,0.1, 0.9], # hid[1]
            [0.1, 0.4, 0.1]]).T # hid[2]

# hid[0] hid[1] hid[2]
hp_wgt = np.array([
            [0.3, 1.1, -0.3], # hurt?
            [0.1, 0.2, 0.0], # win?
            [0.0, 1.3, 0.1] ]).T # sad?

weights = [ih_wgt, hp_wgt]

def neural_network(input, weights):

    hid = input.dot(weights[0])
    pred = hid.dot(weights[1])
    return pred

toes = np.array([8.5, 9.5, 9.9, 9.0])
wlrec = np.array([0.65,0.8, 0.8, 0.9])
nfans = np.array([1.2, 1.3, 0.5, 1.0])

input = np.array([toes[0],wlrec[0],nfans[0]])

pred = neural_network(input,weights)
print(pred)

NumPy 快速入门

NumPy 为你做了几件事情。让我们揭示这个魔法的秘密

到目前为止,在本章中,我们讨论了两种新的数学工具:向量和矩阵。你还了解到了在向量和矩阵上发生的不同操作,包括点积、逐元素乘法和加法以及向量-矩阵乘法。对于这些操作,你已经编写了可以在简单的 Python list 对象上操作的 Python 函数。

在短期内,你需要继续编写和使用这些函数,以确保你完全理解它们内部的运作。但现在我已经提到了 NumPy 和几个主要操作,我想快速概述一下基本的 NumPy 使用方法,以便你为过渡到仅使用 NumPy 的章节做好准备。让我们再次从基础知识开始:向量和矩阵。

在 NumPy 中,你可以以多种方式创建向量和矩阵。大多数神经网络中常用的技术在前面的代码中列出。请注意,创建向量和矩阵的过程是相同的。如果你只创建了一行的矩阵,你就是在创建一个向量。并且,正如数学中通常的做法一样,你通过列出 (rows,columns) 来创建矩阵。我之所以这么说,只是为了让你记住顺序:行在前,列在后。让我们看看你可以对这些向量和矩阵执行的一些操作:

print(a * 0.1)       *1*
print(c * 0.2)       *2*
print(a * b)         *3*
print(a * b * 0.2)   *4*
print(a * c)         *5*

print(a * e)         *6*
  • 1 将向量 a 中的每个数乘以 0.1

  • 2 将矩阵 c 中的每个数乘以 0.2

  • 3 在 a 和 b 之间逐元素相乘(列配对)

  • 4 逐元素相乘,然后乘以 0.2

  • 5 对矩阵 c 的每一行执行逐元素乘法,因为 c 的列数与 a 相同

  • 6 因为 a 和 e 的列数不同,这会导致“值错误:操作数无法一起广播...”

import numpy as np

a = np.array([0,1,2,3])   *1*
b = np.array([4,5,6,7])   *2*
c = np.array([[0,1,2,3],  *3*
              [4,5,6,7]])

d = np.zeros((2,4))       *4*
e = np.random.rand(2,5)   *5*

print(a)
print(b)
print(c)
print(d)
print(e)
  • 1 向量

  • 2 另一个向量

  • 3 矩阵

  • 4 2×4 的零矩阵

  • 5 随机 2×5 矩阵,数值介于 0 和 1 之间

输出

[0 1 2 3]
[4 5 6 7]
[[0 1 2 3]
 [4 5 6 7]]
[[ 0\.  0\.  0\.  0.]
 [ 0\.  0\.  0\.  0.]]
[[ 0.22717119  0.39712632
0.0627734   0.08431724
0.53469141]
 [ 0.09675954  0.99012254
0.45922775  0.3273326
0.28617742]]

现在运行所有之前的代码。第一部分“起初令人困惑但最终美妙”的魔法应该已经可见了。当你用*函数乘以两个变量时,NumPy 会自动检测你正在处理什么类型的变量,并试图弄清楚你所说的操作。这可能会非常方便,但有时会使 NumPy 代码变得难以阅读。确保你在进行过程中跟踪每个变量的类型。

对于任何逐元素操作(+-*/)的一般规则是,两个变量必须有相同的列数,或者其中一个变量必须只有一个列。例如,print(a * 0.1)将一个向量乘以一个单个数字(一个标量)。NumPy 会说,“哦,我敢打赌我应该在执行向量-标量乘法,”然后它将标量(0.1)乘以向量中的每个值。这看起来与print(c * 0.2)完全一样,但 NumPy 知道c是一个矩阵。因此,它执行标量-矩阵乘法,将c中的每个元素乘以 0.2。因为标量只有一个列,你可以将它乘以任何东西(或除、加或减)。

接下来是print(a * b)。NumPy 首先确定它们都是向量。因为这两个向量都没有只有一个列,NumPy 会检查它们是否有相同数量的列。它们确实有,所以 NumPy 知道要根据它们在向量中的位置逐个元素相乘。加法、减法和除法也是同样的道理。

print(a * c)可能是最难以捉摸的。a是一个有四个列的向量,而c是一个(2 × 4)矩阵。它们都没有只有一个列,所以 NumPy 会检查它们是否有相同数量的列。它们确实有,所以 NumPy 会将向量ac的每一行相乘(就像它在每一行上执行逐元素向量乘法一样)。

再次,最令人困惑的部分是,如果你不知道哪些是标量、向量或矩阵,所有这些操作看起来都是一样的。当你“阅读 NumPy”时,你实际上在做两件事:阅读操作并跟踪每个操作的形状(行数和列数)。这需要一些练习,但最终它会变得像本能一样。让我们看看 NumPy 中矩阵乘法的几个例子,并注意每个矩阵的输入和输出形状。

a = np.zeros((1,4))
b = np.zeros((4,3))

c = a.dot(b)      *1*
print(c.shape)    *2*
  • 1 长度为 4 的向量

  • 2 4 行 3 列的矩阵

输出

(1,3)

使用dot函数时有一个黄金法则:如果你将两个你要“点积”的变量的(rows,cols)描述放在一起,相邻的数字应该总是相同的。在这种情况下,你正在将(1,4)与(4,3)进行点积。它运行正常并输出(1,3)。从变量形状的角度来看,你可以这样想,无论你是点积向量还是矩阵:它们的形状(行数和列数)必须对齐。左矩阵的列必须等于右矩阵的行,这样(a,b).dot(b,c) = (a,c)

a = np.zeros((2,4))         *1*
b = np.zeros((4,3))         *2*

c = a.dot(b)
print(c.shape)              *3*

e = np.zeros((2,1))         *4*
f = np.zeros((1,3))         *5*

g = e.dot(f)
print(g.shape)              *6*

h = np.zeros((5,4)).T       *7* *8*
i = np.zeros((5,6))         *9*

j = h.dot(i)
print(j.shape)              *10*

h = np.zeros((5,4))         *11*
i = np.zeros((5,6))         *12*
j = h.dot(i)
print(j.shape)              *13*
  • 1 2 行 4 列的矩阵

  • 2 4 行 3 列的矩阵

  • 3 输出 (2,3)

  • 4 2 行 1 列的矩阵

  • 5 1 行 3 列的矩阵

  • 6 输出 (2,3)

  • 7 抛出错误;.T 翻转矩阵的行和列。

  • 8 4 行 5 列的矩阵

  • 9 6 行 5 列的矩阵

  • 10 输出 (4,6)

  • 11 5 行 4 列的矩阵

  • 12 5 行 6 列的矩阵

  • 13 抛出错误

摘要

为了进行预测,神经网络执行输入的重复加权求和

你在本章中看到了越来越多种类的神经网络。我希望你清楚,相对较少的简单规则被反复使用,以创建更大、更先进的神经网络。网络智能取决于你赋予它的权重值。

本章我们所做的一切都是所谓的前向传播的形式,其中神经网络接收输入数据并做出预测。之所以称为前向传播,是因为你正在将激活信号正向传播通过网络。在这些例子中,激活是指所有不是权重且对每个预测都是唯一的数字。

在下一章,你将学习如何设置权重,以便你的神经网络做出准确的预测。正如预测是基于几个简单的技术,这些技术被重复/堆叠在一起一样,权重学习也是一个由许多简单技术组成的系列,这些技术在一个架构中被多次组合。那里见!

第四章。神经网络学习简介:梯度下降

在本章中

  • 神经网络能做出准确的预测吗?

  • 为什么要测量误差?

  • 热和冷学习

  • 从误差中计算方向和数量

  • 梯度下降

  • 学习就是减少误差

  • 导数及其如何用于学习

  • 离散度和 alpha

“对假设有效性的唯一相关检验是其预测与经验的比较。”

米尔顿·弗里德曼,《正面经济学论文集》(芝加哥大学出版社,1953 年)

预测、比较和学习

在第三章中,你学习了“预测、比较、学习”这一范式,并且我们深入探讨了第一步:预测。在这个过程中,你学到了许多东西,包括神经网络的主要部分(节点和权重)、数据集如何融入网络(匹配一次进入的数据点的数量),以及如何使用神经网络进行预测。

也许这个过程引发了这样的问题:“我们如何设置权重值,以便网络能够准确预测?”回答这个问题是本章的主要内容,因为我们涵盖了范式中的下一步:比较学习

比较

比较提供了一个衡量预测“错过”多少的度量

一旦你做出了预测,下一步就是评估你的表现如何。这听起来可能是一个简单的概念,但你很快会发现,想出一个好的方法来衡量误差是深度学习中最重要且最复杂的问题之一。

你可能一直在做测量误差的事情,而你可能没有意识到这一点。也许你(或你认识的人)放大了较大的误差,而忽略了非常小的误差。在本章中,你将学习如何用数学方法教会网络这样做。你还将了解到误差始终是正的!我们将考虑一个类比,即弓箭手射中靶心:无论射击是低了一英寸还是高了一英寸,误差仍然是 1 英寸。在神经网络的比较步骤中,你需要考虑这些属性来衡量误差。

提前提醒,在本章中,我们只评估了一种简单的误差测量方法:均方误差。这只是评估神经网络准确性的许多方法之一。

这一步将给你一个关于你错过了多少的感觉,但这还不足以进行学习。比较逻辑的输出是一个“热或冷”类型的信号。给定一些预测,你会计算一个误差度量,它会说“很多”或“很少”。它不会告诉你为什么你错过了,你错过了哪个方向,或者你应该做什么来纠正错误。它更多或更少地说“大错”、“小错”或“完美预测”。关于错误应该做什么,将在下一步,学习中解决。

学习

学习告诉每个权重如何改变以减少误差

学习的关键在于错误归因,或者说找出每个权重在产生错误中扮演了什么角色的艺术。这是深度学习的指责游戏。在本章中,我们将用许多页面来探讨深度学习指责游戏中最流行的版本:梯度下降

最终,这会导致为每个权重计算一个数字。这个数字代表该权重应该更高或更低,以便减少错误。然后你将根据这个数字移动权重,任务就完成了。

比较:你的网络是否做出了好的预测?

让我们来测量错误并找出答案!

在你的 Jupyter 笔记本中执行以下代码。它应该打印0.3025

goal_pred 变量是什么?

input一样,goal_pred是在现实世界中记录的数字。但它通常是难以观察的东西,比如“给定温度,有多少百分比的人确实穿了运动服”;或者“给定他的打击率,击球手是否击出了全垒打”。

为什么错误要平方?

想象一个射箭手射中靶心。当箭射高了 2 英寸,射箭手错过了多少?当箭射低了 2 英寸,射箭手错过了多少?两种情况下,射箭手都只错过了 2 英寸。将“你错过了多少”平方的主要原因在于,它迫使输出为正数(pred - goal_pred)在某些情况下可能是负数,与实际错误不同

平方不会使大错误(>1)更大,小错误(<1)更小吗?

呃 ... 这是一种有点奇怪的测量错误的方法,但结果证明,放大大错误和减少小错误是可以接受的。稍后,你将使用这个错误来帮助网络学习,你更希望它关注大错误,而不是过多地担心小错误。好父母也是如此:如果错误足够小(比如铅笔断了),他们实际上会忽略错误;但如果错误很大(比如车祸),他们可能会非常严厉。你明白为什么平方是有价值的吗?

为什么要测量错误?

测量错误简化了问题

训练神经网络的目的是做出正确的预测。这正是你想要的。在前面章节提到的最实用的情况下,你希望网络能够接受你容易计算的输入(今天的股价)并预测难以计算的事情(明天的股价)。这就是神经网络有用的原因。

结果表明,将knob_weight改为使网络正确预测goal_prediction比将其改为使error == 0要复杂一些。以这种方式看待问题有一些更简洁的地方。最终,这两个陈述说的是同一件事,但试图将错误降到 0似乎更直接。

测量错误的不同方法优先考虑错误的方式不同

如果现在这有点牵强,那没关系,但回想一下我之前说的:通过平方错误,小于 1 的数字会变得更小,而大于 1 的数字会变得更大。你将改变我所说的纯错误pred - goal_pred),使得大的错误变得非常大,而小的错误迅速变得无关紧要。

通过这种方式测量错误,你可以将大错误优先于小错误。当你有相当大的纯错误(比如说,10)时,你会告诉自己你有非常大的错误(102 == 100);而相比之下,当你有小的纯错误(比如说,0.01)时,你会告诉自己你有非常小的错误(0.012 == 0.0001)。你明白我说的优先级是什么吗?这只是修改你认为的错误,以便放大大的错误,而大量忽略小的错误。

相反,如果你取的是错误的绝对值而不是平方错误,你就不会有这种优先级。错误就只是纯错误的正值——这很好,但不同。关于这一点,稍后还会详细说明。

你为什么只想看到的错误?

最终,你将处理数百万个input -> goal_prediction对,我们仍然希望做出准确的预测。因此,你将尝试将平均误差降低到 0。

如果错误可以是正的也可以是负的,这就带来了问题。想象一下,如果你试图让神经网络正确预测两个数据点——两个input -> goal_prediction对。如果第一个有 1000 的错误,第二个有-1000 的错误,那么平均错误将是!你会让自己误以为你预测得完美,但实际上每次都差了 1000!这会很糟糕。因此,你希望每个预测的错误始终是的,这样在平均时它们就不会意外地相互抵消。

神经学习最简单的形式是什么?

使用热冷方法进行学习

最后,学习实际上就是关于一件事:调整knob_weight向上或向下,以减少错误。如果你一直这样做,错误降到 0,你就完成了学习!你怎么知道是向上还是向下转动旋钮?嗯,你尝试向上和向下转动,看看哪一个可以减少错误!哪一个减少了错误,就用来更新knob_weight。这很简单但很有效。你一次又一次地这样做,最终错误等于 0,这意味着神经网络正在以完美的准确性进行预测。

热冷学习

热冷学习意味着调整权重以查看哪个方向可以最大程度地减少错误,然后在这个方向上移动权重,重复这个过程,直到错误达到 0。

这最后五步是热冷学习的一次迭代。幸运的是,这个迭代本身就让我们非常接近正确答案(新的错误仅为 0.004)。但在正常情况下,我们可能需要重复这个过程很多次才能找到正确的权重。有些人可能需要训练他们的网络数周或数月才能找到足够好的权重配置。

这揭示了神经网络学习实际上是什么:一个搜索问题。你正在搜索最佳可能的权重配置,以便网络的错误降到 0(并做出完美预测)。与其他所有形式的搜索一样,你可能找不到你想要的,即使你找到了,也可能需要一些时间。接下来,我们将使用热冷学习进行稍微困难一些的预测,以便你可以看到这个搜索过程!

热冷学习

这可能是最简单的学习形式

在你的 Jupyter 笔记本中执行以下代码。(新的神经网络修改用粗体标出。)此代码尝试正确预测 0.8:

weight = 0.5
input = 0.5
goal_prediction = 0.8

step_amount = 0.001                                        *1*

for iteration in range(1101):                              *2*

    prediction = input * weight
    error = (prediction - goal_prediction) ** 2

    print("Error:" + str(error) + " Prediction:" + str(prediction))

    up_prediction = input * (weight + step_amount)         *3*
    up_error = (goal_prediction - up_prediction) ** 2

    down_prediction = input * (weight - step_amount)       *4*
    down_error = (goal_prediction - down_prediction) ** 2

    if(down_error < up_error):
        weight = weight - step_amount                      *5*

    if(down_error > up_error):
        weight = weight + step_amount                      *6*
  • 1 每次迭代移动权重多少

  • 2 多次重复学习,以便错误可以持续减小。

  • 3 尝试向上!

  • 4 尝试向下!

  • 5 如果向下更好,就向下走!

  • 6 如果向上更好,就向上走!

当我运行这段代码时,我看到了以下输出:

Error:0.3025 Prediction:0.25
Error:0.30195025 Prediction:0.2505
            ....
Error:2.50000000033e-07 Prediction:0.7995
Error:1.07995057925e-27 Prediction:0.8       *1*
  • 1 最后一步正确预测了 0.8!

热冷学习的特点

这很简单

热冷学习法很简单。在做出预测之后,你预测两次更多,一次稍微增加权重,再次稍微减少权重。然后根据哪个方向给出了更小的错误来移动权重。重复足够多次后,最终将错误减少到 0。

为什么我迭代了正好 1,101 次?

例子中的神经网络在经过恰好那么多次迭代后达到了 0.8。如果你超过这个范围,它将在 0.8 和略高于或略低于 0.8 之间来回摆动,导致左页底部打印的错误日志不那么美观。你可以随意尝试。

问题 1:它不够高效

你必须预测多次才能更新单个knob_weight。这似乎非常低效。

问题 2:有时无法预测确切的预期目标

设置step_amount后,除非完美的权重正好在n*step_amount处,否则网络最终会超过一些小于step_amount的数字。当它这样做时,它将开始交替在goal_prediction的每一侧之间。将step_amount设置为 0.2 来观察这个效果。如果你将step_amount设置为 10,你真的会破坏它。当我尝试这样做时,我看到了以下输出。它从未接近 0.8!

Error:0.3025 Prediction:0.25
Error:19.8025 Prediction:5.25
Error:0.3025 Prediction:0.25
Error:19.8025 Prediction:5.25
Error:0.3025 Prediction:0.25
....
.... repeating infinitely...

真正的问题是,即使你知道移动weight的正确方向,你也不知道正确的数量。相反,你随机选择一个固定的值(step_amount)。此外,这个数量与error没有任何关系。无论error是大是小,step_amount都是相同的。所以,冷热学习有点令人沮丧。它效率低下,因为对于每个weight更新,你需要预测三次,而且step_amount是任意的,这可能会阻止你学习正确的weight值。

如果你能有一种方法来计算每个weight的方向和数量,而无需反复进行预测,会怎么样?

从误差计算方向和数量

让我们来测量误差并找到方向和数量!

在你的 Jupyter 笔记本中执行此代码:

你在这里看到的是一种称为梯度下降的更高级的学习形式。这种方法允许你(在单行代码中,这里用粗体显示)计算你应该改变weight以减少error方向数量

方向和数量是什么?

direction_and_amount代表你想要如何改变weight。第一部分1是我所说的纯误差,等于(pred - goal_pred)。(关于这一点,稍后会有更多介绍。)第二部分2是乘以input进行的缩放、负反转和停止操作,它修改了纯误差,使其准备好更新weight

纯误差是什么?

纯误差是(pred - goal_pred),它表示你错过的原始方向和数量。如果这是一个数,你预测得太高,反之亦然。如果这是一个数,你错得很多,等等。

缩放、负反转和停止是什么?

这三个属性共同作用,将纯误差转换为你要改变weight的绝对数量。它们通过解决纯误差不足以对weight进行良好修改的三个主要边缘情况来实现这一点。

停止是什么?

停止是乘以input后对纯误差产生的第一个(也是最简单)的影响。想象一下将 CD 播放器插入你的立体声音响。如果你把音量开到最大,但 CD 播放器是关着的,音量变化就不会起作用。停止在神经网络中解决了这个问题。如果input是 0,那么它将迫使direction_and_amount也变为 0。当input为 0 时,你不会学习(改变音量),因为没有东西可以学习。每个weight值都有相同的error,移动它没有区别,因为pred始终为 0。

负反转是什么?

这可能是最困难和最重要的效果。通常(当 input 为正时),向上移动 weight 会使预测向上移动。但如果 input 为负,那么突然 weight 的方向就改变了!当 input 为负时,向上移动 weight 会使预测向下移动。它是相反的!你该如何解决这个问题?嗯,如果 input 为负,将纯错误乘以 input 将会反转 direction_and_amount 的符号。这是负反转,确保即使 input 为负,weight 也能向正确的方向移动。

什么是缩放?

缩放是乘以 input 后对纯错误产生的第三个影响。从逻辑上讲,如果 input 很大,你的 weight 更新也应该很大。这更多的是一个副作用,因为它经常失控。稍后,你将使用 alpha 来处理这种情况。

当你运行前面的代码时,你应该看到以下输出:

Error:0.3025 Prediction:0.25
Error:0.17015625 Prediction:0.3875
Error:0.095712890625 Prediction:0.490625
                    ...

Error:1.7092608064e-05 Prediction:0.79586567925
Error:9.61459203602e-06 Prediction:0.796899259437
Error:5.40820802026e-06 Prediction:0.797674444578     *1*
  • 1 最后的步骤正确地接近了 0.8!

在这个例子中,你在一个简化的环境中看到了梯度下降的作用。接下来,你将在其更自然的环境中看到它。一些术语可能会有所不同,但我会用一种使其更明显适用于其他类型网络(如具有多个输入和输出的网络)的方式编写代码。

梯度下降的一次迭代

这对一个训练示例(输入->真实)对进行权重更新

图片

图片

图片

delta 是衡量这个节点错过多少的度量。真实预测是 1.0,而网络的预测是 0.85,所以网络低估了 0.15。因此,delta 是负值 0.15。

梯度下降和这种实现之间的主要区别是新的变量 delta。它是节点过高或过低的原始量。你首先计算你希望输出节点有多大的不同,然后才计算 direction_and_amount 来改变 weight(在第 4 步中,现在重命名为 weight_delta):

图片

weight_delta 是衡量一个权重导致网络错过多少的度量。你通过将权重的输出节点 delta 乘以权重的 input 来计算它。因此,你通过 缩放 输出节点 deltainput 来创建每个 weight_delta。这考虑了上述三个 direction_and_amount 的属性:缩放、负反转和停止。

图片

在使用它来更新 weight 之前,你将 weight_delta 乘以一个小的数字 alpha。这让你可以控制网络学习的速度。如果它学得太快,它可能会过于激进地更新权重并超出范围。(关于这一点稍后会有更多讨论。)请注意,权重更新与热学习和冷学习产生了相同的变化(小的增加)。

学习就是减少错误

你可以修改权重以减少错误

将前几页的代码组合起来,我们现在有以下内容:

weight, goal_pred, input = (0.0, 0.8, 0.5)

for iteration in range(4):

    pred = input * weight              *1*
    error = (pred - goal_pred) ** 2    *1*
    delta = pred - goal_pred
    weight_delta = delta * input
    weight = weight - weight_delta
    print("Error:" + str(error) + " Prediction:" + str(pred))
  • 1 最后的步骤正确地接近了 0.8!

学习的黄金法则

这种方法调整每个weight的正确方向和正确数量,以便将error减少到 0。

你所要做的一切就是找出正确的方向和数量来修改weight,以便error下降。秘密在于prederror的计算。注意你在error计算中使用pred。让我们用生成它的代码替换pred变量:

error = ((input * weight) - goal_pred) ** 2

这根本不会改变error的值!它只是将两行代码合并,并直接计算error。记住,inputgoal_prediction分别固定在 0.5 和 0.8(你在网络开始训练之前设置了它们)。所以,如果你用它们的值替换变量名,秘密就变得明显了:

error = ((0.5 * weight) - 0.8) ** 2

秘密

对于任何inputgoal_prederrorweight之间定义了一种精确的关系,这是通过结合predictionerror公式找到的。在这种情况下:

error = ((0.5 * weight) - 0.8) ** 2

假设你增加了weight 0.5。如果errorweight之间存在精确的关系,你应该能够计算出这也将如何移动error。如果你想将error移动到特定的方向,这能实现吗?

图片

此图表示根据前一个公式中的关系,每个weight的每个error值。注意它形成了一个漂亮的碗形。黑色圆点位于当前weighterror的点。虚线圆圈是你想要到达的地方(error等于 0)。

关键要点

斜率指向碗的底部(最低error),无论你在碗的哪个位置。你可以使用这个斜率来帮助神经网络减少错误。

让我们观察学习过程中的几个步骤

我们最终能找到碗底吗?

weight, goal_pred, input = (0.0, 0.8, 1.1)

for iteration in range(4):
    print("-----\nWeight:" + str(weight))
    pred = input * weight
    error = (pred - goal_pred) ** 2
    delta = pred - goal_pred
    weight_delta = delta * input
    weight = weight - weight_delta
    print("Error:" + str(error) + " Prediction:" + str(pred))
    print("Delta:" + str(delta) + " Weight Delta:" + str(weight_delta))

图片

图片

图片

图片

图片

为什么这能工作?权重增量(weight_delta)到底是什么?

让我们回顾一下并讨论函数。什么是函数?你如何理解它?

考虑这个函数:

def my_function(x):
   return x * 2

函数接收一些数字作为输入,并给出另一个数字作为输出。正如你可以想象的那样,这意味着函数定义了输入数字和输出数字之间的一种关系。也许你也能看出学习函数的能力为什么如此强大:它让你能够将一些数字(比如图像像素)转换为其他数字(比如图像包含猫的概率)。

每个函数都有你可能称之为动态部分的部分:你可以调整或改变这些部分来使函数生成的输出不同。考虑一下前一个例子中的my_function。问问自己,“是什么控制着这个函数的输入和输出之间的关系?”答案是,2。对于下面的函数,也提出同样的问题:

error = ((input * weight) - goal_pred) ** 2

是什么控制着input和输出(error)之间的关系?有很多东西在起作用——这个函数稍微复杂一些!goal_predinput**2weight以及所有括号和代数运算(加法、减法等等)都在计算误差中发挥作用。调整其中任何一个都会改变误差。这一点很重要。

作为一种思维练习,考虑将goal_pred改为减少误差。这很愚蠢,但完全可行。在现实生活中,你可能会称这(设定目标为你的能力所及)为“放弃”。你是在否认你犯了错误!这也不行。

如果你改变input直到error降到 0 会怎样?嗯,这就像是你想看到的世界而不是实际的世界。你正在改变输入数据,直到你预测你想预测的东西(这大致是inceptionism的工作方式)。

现在考虑改变 2,或者加法、减法或乘法。这只是在最初改变计算error的方式。如果误差计算实际上不能给出你遗漏了多少(如几页前提到的正确属性)的良好度量,那么这种计算是没有意义的。这也不行。

剩下什么?唯一剩下的变量是weight。调整它不会改变你对世界的看法,也不会改变你的目标,更不会破坏你的误差度量。改变weight意味着函数符合数据中的模式。通过迫使函数的其他部分保持不变,你迫使函数正确地模拟数据中的某些模式。它只允许修改网络预测的方式。

总结一下:你修改误差函数的特定部分,直到error值降到 0。这个误差函数是通过一系列变量计算得出的,其中一些你可以改变(权重),而另一些你不能(输入数据、输出数据和误差逻辑):

weight = 0.5
goal_pred = 0.8
input = 0.5

for iteration in range(20):
    pred = input * weight
    error = (pred - goal_pred) ** 2
    direction_and_amount = (pred - goal_pred) * input
    weight = weight - direction_and_amount

    print("Error:" + str(error) + " Prediction:" + str(pred))

关键要点

你可以修改pred计算中的任何东西,除了input

我们将在本书的剩余部分(以及许多深度学习研究人员将花费他们余生的时间)尝试所有你能想象到的关于那个pred计算的方法,以便它能做出良好的预测。学习就是自动改变预测函数,使其做出良好的预测——即,使后续的error降到 0。

现在你已经知道你可以改变什么,那么你如何着手进行改变呢?这就是关键所在。这就是机器学习,对吧?在下一节中,我们将详细讨论这一点。

对一个概念的狭隘视野

概念:学习就是调整权重以将误差降至 0

到目前为止,在本章中,我们一直在强调学习实际上只是调整weight以将error降至 0 的想法。这是秘密配方。说实话,知道如何做到这一点完全关乎理解weighterror之间的关系。如果你理解这种关系,你就可以知道如何调整weight以减少error

我所说的“理解关系”是什么意思呢?嗯,理解两个变量之间的关系就是理解一个变量如何改变另一个变量。在这种情况下,你真正追求的是这两个变量之间的敏感性。敏感性是方向和数量的另一种说法。你想要知道errorweight的敏感性。你想要知道当你改变weight时,error变化的方向和数量。这就是目标。到目前为止,你已经看到了两种不同的方法,试图帮助你理解这种关系。

当你调整weight(热学习和冷学习)并研究它对error的影响时,你实际上是在实验性地研究这两个变量之间的关系。这就像走进一个有 15 个不同未标记的开关的房间。你开始打开和关闭它们,以了解它们与房间内各种灯光的关系。你做了同样的事情来研究weighterror之间的关系:你上下调整weight,观察它如何改变error。一旦你知道了关系,你就可以使用两个简单的if语句将weight移动到正确的方向:

if(down_error < up_error):
    weight = weight - step_amount

if(down_error > up_error):
    weight = weight + step_amount

现在,让我们回到之前结合了prederror逻辑的公式。正如提到的,它们在errorweight之间定义了一个精确的关系:

error = ((input * weight) - goal_pred) ** 2

这段代码,女士们先生们,是秘密。这是一个公式。这是errorweight之间的关系。这种关系是精确的。它是可计算的。它是普遍的。它现在是,将来也始终是。

现在,你如何使用这个公式来知道如何改变weight,以便error向特定方向移动?才是正确的问题。停下。我恳求你。停下并欣赏这个时刻。这个公式是这两个变量之间的精确关系,现在你将找出如何改变一个变量,以使另一个变量向特定方向移动。

事实上,有一种方法可以用于任何公式。你会用它来减少误差。

一个有杆从里面伸出来的箱子

想象一下,你坐在一个纸箱前,这个纸箱有两个圆形杆从两个小孔中伸出。蓝色杆从箱子中伸出 2 英寸,红色杆从箱子中伸出 4 英寸。想象一下,我告诉你这些杆是连接的,但我不会告诉你连接的方式。你必须通过实验来找出答案。

所以,你拿起蓝色的杆子,向里推 1 英寸,然后观察,在你推的过程中,红色的杆子也向盒子里移动了 2 英寸。然后,你把蓝色的杆子拉回 1 英寸,红色的杆子又跟着拉出,拉出 2 英寸。你学到了什么?好吧,似乎红色和蓝色杆子之间存在一种关系。无论你移动蓝色杆子多少,红色杆子都会移动两倍的距离。你可能会说以下是真的:

red_length = blue_length * 2

实际上,对于“当我拉这部分时,另一部分移动了多少?”有一个正式的定义,它被称为导数,它实际上意味着“当我拉杆 Y 时,杆 X 移动了多少?”

在红色和蓝色杆子的例子中,“当我拉蓝色时红色移动了多少”的导数是 2。仅仅是 2。为什么是 2?这是由公式确定的乘法关系:

red_length = blue_length * 2     *1*
  • 1 导数

注意,你总是有两个变量之间的导数。你总是在寻找当你改变一个变量时,另一个变量是如何移动的。如果导数是正的,那么当你改变一个变量时,另一个将朝着相同的方向移动。如果导数是负的,那么当你改变一个变量时,另一个将朝着相反的方向移动。

考虑几个例子。因为red_length相对于blue_length的导数是 2,这两个数会朝着同一方向移动。更具体地说,红色会以两倍于蓝色的速度在同一方向上移动。如果导数是-1,红色将以相同的数量朝相反方向移动。因此,给定一个函数,导数表示当你改变另一个变量时,一个变量变化的方向和数量。这正是我们一直在寻找的。

导数:取两个

你对它们还是有点不确定?让我们从另一个角度来考虑

我听说人们有两种解释导数的方式。一种方式是关于理解当你移动另一个变量时,函数中的一个变量是如何变化的。另一种方式是说导数是直线或曲线上某一点的斜率。实际上,如果你取一个函数并绘制它(画出它),你绘制的线的斜率与“当你改变另一个变量时,一个变量变化了多少”是同一回事。让我通过绘制我们最喜欢的函数来展示给你看:

error = ((input * weight) - goal_pred) ** 2

记住,goal_predinput是固定的,所以你可以重写这个函数:

error = ((0.5 * weight) - 0.8) ** 2

因为只剩下两个变量会改变(其余的都是固定的),你可以计算每个权重及其对应的误差。让我们把它们画出来。

如你所见,图表看起来像一个大 U 形曲线。注意,中间也有一个点,其中error等于 0。注意,在那个点右边,线的斜率是正的,而在那个点左边,线的斜率是负的。也许更有趣的是,离目标权重越远,斜率越陡。

这些是有用的属性。斜率的符号给你方向,斜率的陡峭程度给你数量。你可以使用这两个属性来帮助找到目标权重

即使现在,当我看那条曲线时,我也很容易失去对它所代表的含义的跟踪。这类似于学习中的冷热法。如果你尝试了所有可能的权重值并绘制出来,你就会得到这条曲线。

关于导数的显著之处在于,它们可以超越计算误差的大公式(本节开头),看到这条曲线。你可以计算任何权重值的线的斜率(导数)。然后你可以使用这个斜率(导数)来确定哪个方向可以减少误差。更好的是,根据斜率的陡峭程度,你可以至少得到一些关于你距离斜率为零的优化点的距离的想法(尽管这不是一个精确的答案,你会在后面学到更多)。

图片

你真正需要知道的是

使用导数,你可以从任何公式中挑选任意两个变量,并了解它们是如何相互作用的

看看这个巨大的函数

y = (((beta * gamma) ** 2) + (epsilon + 22 - x)) ** (1/2)

关于导数,你需要知道的是。对于任何函数(即使是这个巨大的函数),你都可以挑选任意两个变量,并理解它们之间的关系。对于任何函数,你都可以挑选两个变量,像我们之前做的那样,将它们绘制在 x-y 图上。对于任何函数,你都可以挑选两个变量,并计算当你改变其中一个变量时,另一个变量会发生多少变化。因此,对于任何函数,你可以学习如何改变一个变量,以便你可以将另一个变量移动到某个方向。对不起,我再次强调这个观点,但重要的是你要在内心深处知道这一点。

总结:在这本书中,你将构建神经网络。神经网络实际上就是一件事情:你用来计算误差函数的一组权重。对于任何误差函数(无论多么复杂),你都可以计算任何权重与网络的最终误差之间的关系。有了这些信息,你可以改变神经网络中的每个权重,将误差降低到 0——这正是你将要做的。

你真正不需要知道的是

微积分

所以,学习如何从任何函数中取任意两个变量并计算它们之间关系的方法,大约需要大学三个学期的时间。说实话,如果你上完这三个学期是为了学习如何进行深度学习,你将只会用到你所学内容的一小部分。实际上,微积分只是关于记住并练习每个可能函数的每个可能的导数规则。

在这本书中,我将做我在现实生活中通常做的事情(因为我懒惰——我是说,高效):在参考表中查找导数。你需要知道的是导数代表什么。它是函数中两个变量之间的关系,这样你就可以知道当你改变另一个变量时,一个变量会改变多少。它只是两个变量之间的敏感性。

我知道这有很多信息要说,“它是两个变量之间的敏感性”,但确实是。请注意,这可以包括正敏感性(当变量一起移动时),负敏感性(当它们朝相反方向移动时),以及零敏感性(当一个变量保持固定,无论你对另一个变量做什么)。例如,y = 0 * x。移动 xy 总是 0。

关于导数就说到这里。让我们回到梯度下降。

如何使用导数进行学习

权重变化量是你的导数

误差和误差的导数以及权重的区别是什么?误差是你错过多少的衡量标准。导数定义了每个权重和你的错过之间的关系。换句话说,它告诉改变权重对误差的贡献有多大。所以,现在你知道这个,你是如何使用它来将误差移动到特定方向的呢?

你已经学会了函数中两个变量之间的关系,但你如何利用这种关系呢?实际上,这是非常直观的。再次查看误差曲线。黑色点代表权重开始的地方:(0.5)。虚线圆圈是你想要它去的地方:目标权重。你看到连接黑色点的虚线吗?那就是斜率,也就是导数。它告诉你在这个曲线上,当你改变权重时,误差会改变多少。注意它是向下指的:它是一个负斜率。

图片

直线或曲线的斜率始终指向直线或曲线最低点的相反方向。所以,如果你有一个负斜率,你增加权重以找到误差的最小值。看看吧。

那么,你是如何使用导数来找到误差最小值(误差图中的最低点)的呢?你移动到斜率的相反方向——导数的相反方向。你可以取每个权重值,计算它与误差的导数(这样你比较的是两个变量:权重和误差),然后改变权重,使其与斜率的相反方向。这样就会移动到最小值。

记住目标:你试图找出改变权重的方向和数量,以便误差下降。导数给出了函数中任何两个变量之间的关系。你使用导数来确定任何权重和误差之间的关系。然后你将权重移动到导数的相反方向,以找到最低的权重。哇!神经网络就学会了。

这种学习方法(寻找误差最小值)被称为梯度下降。这个名字应该看起来很直观。你将权重值移动到梯度的相反方向,从而将误差减少到 0。通过相反,我的意思是当你有一个负梯度时,增加权重,反之亦然。就像重力一样。

看起来熟悉吗?

weight = 0.0
goal_pred = 0.8
input = 1.1

for iteration in range(4):
    pred = input * weight
    error = (pred - goal_pred) ** 2
    delta = pred - goal_pred
    weight_delta = delta * input        *1*
    weight = weight - weight_delta

    print("Error:" + str(error) + " Prediction:" + str(pred))
  • 1 导数(误差随权重变化的速度)

打破梯度下降

只给我代码!

weight = 0.5
goal_pred = 0.8
input = 0.5

for iteration in range(20):
    pred = input * weight
    error = (pred - goal_pred) ** 2
    delta = pred - goal_pred
    weight_delta = input * delta
    weight = weight - weight_delta
    print("Error:" + str(error) + " Prediction:" + str(pred))

当我运行这段代码时,我看到了以下输出:

Error:0.3025 Prediction:0.25
Error:0.17015625 Prediction:0.3875
Error:0.095712890625 Prediction:0.490625
                    ...

Error:1.7092608064e-05 Prediction:0.79586567925
Error:9.61459203602e-06 Prediction:0.796899259437
Error:5.40820802026e-06 Prediction:0.797674444578

现在它已经起作用了,让我们来破坏它。尝试调整起始权重goal_predinput数字。你可以将它们都设置为几乎任何值,神经网络将找出如何使用权重根据输入预测输出。看看你是否能找到神经网络无法预测的组合。我发现尝试破坏某物是了解它的好方法。

让我们尝试将input设置为 2,但仍然尝试让算法预测 0.8。会发生什么?看看输出:

Error:0.04 Prediction:1.0
Error:0.36 Prediction:0.2
Error:3.24 Prediction:2.6
            ...

Error:6.67087267987e+14 Prediction:-25828031.8
Error:6.00378541188e+15 Prediction:77484098.6
Error:5.40340687069e+16 Prediction:-232452292.6

哇!这不是你想要的。预测爆炸了!它们在负数和正数之间交替,每一步都离真实答案越来越远。换句话说,每次更新权重都会过度校正。在下一节中,你将了解如何对抗这种现象。

可视化过度校正

发散

有时候神经网络的价值会爆炸。哎呀?

究竟发生了什么?误差爆炸是由你使输入变大的事实引起的。考虑你是如何更新权重的:

weight = weight - (input * (pred - goal_pred))

如果输入足够大,即使误差很小,这也可能导致权重更新很大。当你有一个大的权重更新和小的误差时会发生什么?网络会过度校正。如果新的误差更大,网络会过度校正得更多。这导致了你之前看到的称为发散的现象。

如果你有一个大的输入,预测对权重的变化非常敏感(因为pred = input * weight)。这可能导致网络过度校正。换句话说,即使权重仍然从 0.5 开始,该点的导数非常陡峭。看看图中 U 形误差曲线有多紧?

这真的很直观。你是如何预测的?通过将输入乘以权重。所以,如果输入很大,权重的微小变化会导致预测的变化。误差对权重非常敏感。换句话说,导数非常大。你是如何使它变小的?

引入 alpha

这是防止过度校正权重更新的最简单方法

你试图解决的问题是什么?如果输入太大,那么权重更新可能会过度纠正。症状是什么?当你过度纠正时,新的导数在幅度上甚至比开始时更大(尽管符号将是相反的)。

停下来思考一下。再次看看上一节中的图表,以了解症状。第二步离目标更远,这意味着导数的幅度更大。这导致第三步比第二步更远离目标,神经网络就这样继续下去,表现出发散。

症状就是这种过度调整。解决方案是将权重更新乘以一个分数以使其更小。在大多数情况下,这涉及到将权重更新乘以一个介于 0 和 1 之间的单个实数,称为alpha。注意:这不会对核心问题产生影响,即输入较大。它也会减少不太大的输入的权重更新。

对于即使是最新技术的神经网络,找到合适的 alpha 通常是通过猜测来完成的。你观察错误随时间的变化。如果它开始发散(上升),那么 alpha 太高了,你就减小它。如果学习进展得太慢,那么 alpha 太低了,你就增加它。有其他方法比简单的梯度下降更有效地解决这个问题,但梯度下降仍然非常受欢迎。

代码中的 alpha

我们的“alpha”参数在哪里发挥作用?

你刚刚了解到 alpha 减少了权重更新,以防止它过度调整。这对代码有什么影响?嗯,你根据以下公式更新权重:

weight = weight - derivative

考虑到 alpha 的变化相对较小,如以下所示。注意,如果 alpha 很小(比如说,0.01),它将大大减少权重更新,从而防止它过度调整:

weight = weight - (alpha * derivative)

这很简单。让我们将 alpha 安装到本章开头的小型实现中,并在input = 2 的地方运行它(之前这不起作用):

图片

哇!现在最小的神经网络又可以做出良好的预测了。我是怎么知道将 alpha 设置为 0.1 的呢?说实话,我试了,而且它有效。尽管过去几年深度学习取得了疯狂的发展,但大多数人只是尝试几个 alpha 的量级(10,1,0.1,0.01,0.001,0.0001),然后从那里调整以查看哪个效果最好。这更多的是艺术而不是科学。有更多高级的方法可以到达那里,但现在,尝试各种 alpha,直到找到一个似乎效果很好的。玩一玩。

记忆

是时候真正学习这些内容了

这可能听起来有点过于激烈,但我无法强调我从这个练习中发现的价值的多少:尝试从记忆中在 Jupyter 笔记本(或者如果你必须的话,一个.py 文件)中构建上一节中的代码。我知道这可能看起来有些过度,但(就我个人而言)直到我能够完成这个任务,我都没有在神经网络上有“顿悟”的时刻。

为什么这会起作用呢?首先,唯一知道你是否已经从本章中获取了所有必要信息的方法就是尝试从你的脑海中复现它。神经网络有很多小的移动部件,很容易遗漏其中一个。

这对于本书的其余部分为什么很重要呢?在接下来的章节中,我会以更快的速度引用本章讨论的概念,这样我就可以有更多的时间来讲解新的材料。当我提到“将你的 alpha 参数化添加到权重更新中”这样的内容时,你能够立即识别出我指的是本章中的哪些概念,这是至关重要的。

也就是说,我个人以及许多过去接受过我关于这个主题建议的人,通过记忆神经网络代码的小片段获得了巨大的益处。

第五章. 一次学习多个权重:泛化梯度下降

本章内容

  • 多输入梯度下降学习

  • 冻结一个权重:它做什么?

  • 多输出梯度下降学习

  • 多输入和输出梯度下降学习

  • 可视化权重值

  • 可视化点积

“你不会通过遵循规则来学习走路。你是通过做和摔倒来学习的。”

理查德·布兰森,mng.bz/oVgd

多输入梯度下降学习

梯度下降也适用于多输入

在上一章中,你学习了如何使用梯度下降来更新权重。在这一章中,我们将或多或少地揭示如何使用相同的技巧来更新包含多个权重的网络。让我们先从深入浅出开始,好吗?以下图表显示了具有多个输入的网络如何学习。

图片

图片

图片

这张图没有什么新东西。每个 weight_delta 都是通过将其输出 delta 乘以其 input 来计算的。在这种情况下,因为三个权重共享相同的输出节点,它们也共享该节点的 delta。但由于它们的 input 值不同,权重有不同的 weight_delta。注意,你可以重用之前的 ele_mul 函数,因为你在 weights 中的每个值都乘以相同的值 delta

图片

多输入梯度下降解释

简单易行,理解起来非常迷人

当与单权重神经网络并排放置时,多输入的梯度下降在实践中似乎相当明显。但涉及的属性非常迷人,值得讨论。首先,让我们并排看看它们。

图片

图片

在输出节点生成 delta 之前,单输入和多输入的梯度下降是相同的(除了我们在第三章中研究的预测差异)。你做出预测,并以相同的方式计算 errordelta。但问题仍然存在:当你只有一个 weight 时,你只有一个 input(一个 weight_delta 生成)。现在你有三个。你怎么生成三个 weight_delta

你如何将单个 delta(在节点上)转换为三个 weight_delta 值?

记住 deltaweight_delta 的定义和目的。delta 是衡量你希望节点值差异多少的度量。在这种情况下,你通过节点值和希望节点值是多少之间的直接减法来计算它(预测 - 真实)。正 delta 表示节点的值太高,负 delta 表示太低。

delta

你希望节点值高多少或低多少的度量,以便在给定当前训练示例的情况下完美预测。

weight_delta,另一方面,是减少node_delta的方向和数量的一个估计,通过导数推断得出。你是如何将delta转换成weight_delta的?你将delta乘以权重的input

weight_delta

基于导数估计你应该移动权重以减少node_delta的方向和数量,考虑到缩放、负反转和停止。

从单个权重的角度来考虑,如右图所示:

  • delta: 嘿,inputs——是的,你们三个。下次,预测得高一点。

  • 单个权重: 嗯:如果我的input是 0,那么我的权重就不会起作用,我就不会改变任何东西(停止)。如果我的input是负数,那么我就会想减少我的权重而不是增加它(负反转)。但我的input是正的,而且相当大,所以我猜测我的个人预测对聚合输出非常重要。我将大大提高我的权重来补偿(缩放)。

图片

单个权重增加了其值。

那三个属性/陈述真正说了什么?它们都(停止、负反转和缩放)观察了权重在delta中的作用如何受到其input的影响。因此,每个weight_delta都是delta的一种输入修改版本。

这又把我们带回了最初的问题:如何将一个(节点)delta转换成三个weight_delta值?嗯,因为每个权重都有一个独特的输入和一个共享的delta,你使用每个相应权重的input乘以delta来创建每个相应的weight_delta。让我们看看这个过程是如何实施的。

在接下来的两个图中,你可以看到为之前单个输入架构和新的多输入架构生成weight_delta变量。也许最容易看到它们多么相似的方法是阅读每个图底部的伪代码。注意,多权重版本将delta(0.14)乘以每个输入来创建各种weight_delta。这是一个简单的过程。

图片

图片

图片

图片

最后一步几乎与单个输入网络相同。一旦你有了weight_delta值,你将它们乘以alpha并从权重中减去。这实际上是之前相同的过程,只是在多个权重上重复,而不是单个权重。

让我们观察几个学习步骤

def neural_network(input, weights):
  out = 0
  for i in range(len(input)):
    out += (input[i] * weights[i])
  return out

def ele_mul(scalar, vector):
  out = [0,0,0]
  for i in range(len(out)):
    out[i] = vector[i] * scalar
  return out

toes = [8.5, 9.5, 9.9, 9.0]
wlrec = [0.65, 0.8, 0.8, 0.9]
nfans = [1.2, 1.3, 0.5, 1.0]

win_or_lose_binary = [1, 1, 0, 1]
true = win_or_lose_binary[0]

alpha = 0.01
weights = [0.1, 0.2, -.1]
input = [toes[0],wlrec[0],nfans[0]]

for iter in range(3):

  pred = neural_network(input,weights)

  error = (pred - true) ** 2
  delta = pred - true

  weight_deltas=ele_mul(delta,input)

  print("Iteration:" + str(iter+1))
  print("Pred:" + str(pred))
  print("Error:" + str(error))
  print("Delta:" + str(delta))
  print("Weights:" + str(weights))
  print("Weight_Deltas:")
  print(str(weight_deltas))
  print(
  )

  for i in range(len(weights)):
    weights[i]-=alpha*weight_deltas[i]

图片

我们可以为每个权重制作三个单独的错误/权重曲线,每个权重一个。和之前一样,这些曲线的斜率(虚线)反映了weight_delta值。注意,a比其他曲线更陡。为什么与其他共享相同输出deltaerror测量的weight_delta对于a来说更陡?因为ainput值显著高于其他,因此导数更高。

图片

图片

这里有一些额外的收获。大部分的学习(权重变化)都是在具有最大输入a的权重上进行的,因为输入会显著改变斜率。这并不一定在所有情况下都是有益的。一个名为归一化的子领域有助于鼓励在所有权重上学习,尽管有如这种数据集特征。这种显著的斜率差异迫使我将alpha设置得低于我想要的(0.01 而不是 0.1)。尝试将alpha设置为 0.1:你是否看到a导致它发散?

冻结一个权重:它有什么作用?

这个实验在理论上有点高级,但我认为它是理解权重如何相互影响的一个很好的练习。你将再次进行训练,但权重a将永远不会调整。你将尝试仅使用权重bcweights[1]weights[2])来学习训练示例。

def neural_network(input, weights):
  out = 0
  for i in range(len(input)):
    out += (input[i] * weights[i])
  return out

def ele_mul(scalar, vector):
  out = [0,0,0]
  for i in range(len(out)):
    out[i] = vector[i] * scalar
  return out

toes = [8.5, 9.5, 9.9, 9.0]
wlrec = [0.65, 0.8, 0.8, 0.9]
nfans = [1.2, 1.3, 0.5, 1.0]

win_or_lose_binary = [1, 1, 0, 1]
true = win_or_lose_binary[0]

alpha = 0.3
weights = [0.1, 0.2, -.1]
input = [toes[0],wlrec[0],nfans[0]]

for iter in range(3):
  pred = neural_network(input,weights)

  error = (pred - true) ** 2
  delta = pred - true

  weight_deltas=ele_mul(delta,input)
  weight_deltas[0] = 0

  print("Iteration:" + str(iter+1))
  print("Pred:" + str(pred))
  print("Error:" + str(error))
  print("Delta:" + str(delta))
  print("Weights:" + str(weights))
  print("Weight_Deltas:")
  print(str(weight_deltas))
  print(
  )

  for i in range(len(weights)):
    weights[i]-=alpha*weight_deltas[i]

图片

你可能很惊讶地看到a仍然找到了碗底。为什么?好吧,曲线是每个单独的权重相对于全局错误的度量。因此,因为error是共享的,当一个权重找到碗底时,所有权重都会找到碗底。

这是一个极其重要的教训。首先,如果你用bc权重收敛(达到error = 0),然后尝试训练aa就不会移动。为什么?error = 0,这意味着weight_delta是 0。这揭示了神经网络的一个潜在有害特性:a可能是一个强大的输入,具有大量的预测能力,但如果网络意外地发现如何在没有它的情况下准确预测训练数据,那么它将永远不会学会将其纳入其预测中。

图片

注意a是如何找到碗底的。不是黑色点在移动,而是曲线似乎向左移动。这意味着什么?黑色点只有在权重更新时才能水平移动。因为在这个实验中a的权重被冻结了,所以点必须保持固定。但是error显然降到了 0。

这告诉你了图表的真正含义。实际上,这些都是四维形状的二维切片。三个维度是权重值,第四个维度是错误。这个形状被称为错误平面,信不信由你,它的曲率是由训练数据决定的。为什么是这样?

图片

error 由训练数据决定。任何网络都可以有任意的 weight 值,但给定任何特定的权重配置的 error 值是由数据 100%决定的。你已经看到输入数据的陡峭程度是如何影响 U 形曲线的(在几次场合)。你真正试图用神经网络做到的是找到这个大错误平面上的最低点,这里的最低点指的是最低的 error。有趣,对吧?我们稍后会回到这个想法,所以现在先把它记下来。

多输出梯度下降学习

神经网络也可以仅使用单个输入进行多次预测

也许这看起来有点明显。你以相同的方式计算每个 delta,然后将它们都乘以相同的单个输入。这变成了每个权重的 weight_delta。到目前为止,我希望你已经清楚,一个简单的机制(随机梯度下降)被一致地用于在各种架构中进行学习。

图片

图片

图片

图片

多输入多输出的梯度下降

梯度下降可以推广到任意大的网络

图片

图片

图片

图片

这些权重学习了什么?

每个权重都试图减少错误,但它们在总体上学习了什么?

恭喜!这是本书中我们开始转向第一个真实世界数据集的部分。幸运的是,它具有历史意义。

它被称为修改后的国家标准与技术研究院(MNIST)数据集,它由一些年前高中生和美国人口普查局员工手写的数字组成。有趣的是,这些手写数字是人们手写的黑白图像。每个数字图像都附有他们实际写的数字(0-9)。在过去几十年里,人们一直使用这个数据集来训练神经网络读取人类手写,而今天,你将要做同样的工作。

每个图像只有 784 个像素(28 × 28)。鉴于你有 784 个像素作为输入和 10 个可能的标签作为输出,你可以想象神经网络的形状:每个训练示例包含 784 个值(每个像素一个),所以神经网络必须有 784 个输入值。很简单,对吧?你调整输入节点的数量以反映每个训练示例中的数据点数量。你想要预测 10 个概率:每个数字一个。给定一个输入绘制,神经网络将产生这 10 个概率,告诉你哪个数字最有可能被绘制。

图片

你如何配置神经网络以产生 10 个概率?在上一节中,你看到了一个可以一次接受多个输入并基于该输入做出多个预测的神经网络图。你应该能够修改这个网络,使其具有正确数量的输入和输出,以适应新的 MNIST 任务。你需要调整它,使其有 784 个输入和 10 个输出。

在 MNISTPreprocessor 笔记本中有一个脚本,用于预处理 MNIST 数据集,并将前 1000 张图像和标签加载到两个名为imageslabels的 NumPy 矩阵中。你可能想知道:“图像是二维的。我如何将(28 × 28)像素加载到一个平坦的神经网络中?”目前,答案是简单的:将图像展平成一个 1 × 784 的向量。你将取像素的第一行,并将其与第二行、第三行等拼接起来,直到你有一个包含每张图像的像素列表(784 个像素长)。

此图表示新的 MNIST 分类神经网络。它与你之前用多个输入和输出训练的网络最相似。唯一的区别是输入和输出的数量,这已经大幅增加。此网络有 784 个输入(每个 28 × 28 图像中的像素一个)和 10 个输出(每个可能的数字一个)。

如果这个网络能够完美预测,它将接受图像的像素(比如,下一个图中的 2)并在正确的输出位置(第三个)预测 1.0,在其他所有位置预测 0。如果它能够对数据集中的所有图像都这样做,它将没有错误。

图片

图片

在训练过程中,网络将调整输入和预测节点之间的权重,以便在训练中使error值接近 0。但这意味着什么?修改大量权重以学习整体模式是什么意思?

可视化权重值

在神经网络研究中(尤其是对于图像分类器)的一个有趣且直观的实践是将权重可视化成图像。如果你看这个图,你就会明白为什么。

每个输出节点都有来自每个像素的权重。例如,2?节点有 784 个输入权重,每个权重映射像素和数字 2 之间的关系。

这个关系是什么?嗯,如果权重高,这意味着模型认为该像素和数字 2 之间存在高度的相关性。如果数字非常低(负数),那么网络认为该像素和数字 2 之间的相关性非常低(甚至可能是负相关性)。

图片

如果你将权重打印成与输入数据集图像相同形状的图像,你可以看到哪些像素与特定的输出节点相关性最高。在我们的例子中,使用 2 和 1 的权重分别创建的两个图像中,出现了非常模糊的 2 和 1。亮区代表高权重,暗区代表负权重。中性颜色(如果你在电子书中阅读,则是红色)代表权重矩阵中的 0。这表明网络通常知道 2 和 1 的形状。

为什么会这样?这把我们带回到了点积的课程。让我们快速回顾一下。

可视化点积(加权求和)

回想一下点积是如何工作的。它们取两个向量,逐元素相乘,然后对输出求和。考虑以下例子:

a = [ 0, 1, 0, 1]
b = [ 1, 0, 1, 0]

    [ 0, 0, 0, 0] -> 0    *1*
  • 1

首先,你需要将 ab 中的每个元素相互相乘,在这种情况下,创建了一个全为 0 的向量。这个向量的和也是 0。为什么?因为这两个向量没有共同点。

c = [ 0, 1, 1, 0]            b = [ 1, 0, 1, 0]
d = [.5, 0,.5, 0]            c = [ 0, 1, 1, 0]

cd 之间的点积返回更高的分数,因为具有正值的列之间存在重叠。两个相同向量的点积通常会导致更高的分数。结论是:点积是衡量两个向量之间相似性的粗略度量。

这对权重和输入意味着什么?嗯,如果 weight 向量与 2 的 input 向量相似,那么它将输出一个高分,因为这两个向量相似。相反,如果 weight 向量与 2 的 input 向量不相似,它将输出一个低分。你可以在以下图中看到这一点。为什么最高分(0.98)比较低分(0.01)高?

图片

摘要

梯度下降是一种通用学习算法

本章最重要的潜台词可能是梯度下降是一个非常灵活的学习算法。如果你以某种方式组合权重,可以计算误差函数和 delta,梯度下降可以告诉你如何移动权重以减少误差。本书的剩余部分将探讨梯度下降对不同的权重组合和误差函数有用的不同类型。下一章也不例外。

第六章. 构建您的第一个深度神经网络:反向传播简介

本章内容

  • 街灯问题

  • 矩阵和矩阵关系

  • 全局、批量和随机梯度下降

  • 神经网络学习相关性

  • 过拟合

  • 创建您自己的相关性

  • 反向传播:长距离错误归因

  • 线性与非线性的比较

  • 有时相关性的秘密

  • 您的第一个深度网络

  • 代码中的反向传播:整合一切

“O 深思考计算机,”他说,“我们为你设计的任务是这个。我们想让你告诉我们……”他停顿了一下,“答案。”

道格拉斯·亚当斯,《银河系漫游指南》

街灯问题

这个玩具问题考虑了网络如何学习整个数据集

想象一下你正在一个外国国家的街角。当你接近时,你抬头发现街灯很陌生。你怎么知道什么时候可以过马路?

图片

你可以通过解读街灯来判断是否安全过马路。但在这个情况下,你不知道如何解读它。哪些灯光组合表示是时候了?哪些表示是时候了?为了解决这个问题,你可能会坐在街角几分钟,观察每个灯光组合与周围人选择走或停之间的相关性。你坐下并记录以下模式:

图片

好吧,第一个灯没有人走过。这时你可能在想,“哇,这个模式可以是任何东西。左边的灯或右边的灯可能与停止相关,或者中央的灯可能与行走相关。”没有办法知道。让我们再取一个数据点:

图片

人们走了,所以这个灯的某些东西改变了信号。你唯一确定的是最右边的灯似乎没有指示一个方向或另一个方向。也许它无关紧要。让我们收集另一个数据点:

图片

现在你正在取得进展。这次只有中间的灯改变了,你得到了相反的模式。工作假设是中间的灯表示人们感到安全可以走。在接下来的几分钟里,你记录以下六个灯光模式,注意人们是走还是停。你注意到整体上有模式吗?

图片

如预期的那样,中间(交叉)灯与是否安全过马路之间存在完美的相关性。你是通过观察所有单个数据点并寻找相关性来学习这个模式的。这就是你要训练神经网络去做的。

准备数据

神经网络不读街灯

在前面的章节中,你学习了监督算法。你了解到它们可以将一个数据集转换成另一个数据集。更重要的是,它们可以将你知道的数据集转换成你想知道的数据集。

如何训练一个监督神经网络?你向它展示两个数据集,并要求它学习如何将一个转换成另一个。回想一下街灯问题。你能识别出两个数据集吗?哪一个你总是知道?哪一个你想要知道?

你确实有两个数据集。一方面,你有六个街灯状态。另一方面,你有六个关于人们是否行走的观察。这些就是这两个数据集。

你可以训练神经网络将你所知道的数据库转换为你想要知道的数据库。在这个特定的现实世界例子中,你知道任何给定时间的街灯状态,而你想要知道是否安全过马路。

为了准备这些数据供神经网络使用,你首先需要将其分成这两组(你所知道的和你要知道的)。注意,如果你交换了哪个数据集在哪个组中,你可以尝试反向操作。对于某些问题,这可行。

矩阵及其矩阵关系

将街灯转换为数学

数学不理解街灯。如前所述,你想要教会神经网络将街灯模式转换为正确的停止/行走模式。这里的关键词是模式。你真正想要做的是以数字的形式模仿街灯的模式。让我给你展示一下我的意思。

注意,这里显示的数字模式模仿了街灯以 1 和 0 的形式的模式。每个灯光都有一个列(总共有三列,因为有三个灯光)。还要注意的是,有六行代表六个不同的观察到的街灯。

这种由 1 和 0 组成的结构被称为矩阵。行和列之间的关系在矩阵中很常见,尤其是在数据矩阵(如街灯)中。

在数据矩阵中,惯例是给每个记录的例子一个单独的。同样,惯例是给每个被记录的事物一个单独的。这使得矩阵易于阅读。

因此,一列包含了一个事物被记录的每一个状态。在这种情况下,一列包含了一个特定灯光的每一个开/关状态的记录。每一行包含了一个特定时间点上每个灯光的同时状态。这同样是常见的。

良好的数据矩阵完美地模仿外部世界

数据矩阵不必全是 1 和 0。如果街灯是调光开关,并且以不同的强度打开和关闭,会怎样?也许街灯矩阵看起来会更像这样:

矩阵 A 是完全有效的。它模仿了现实世界中(街灯)存在的模式,因此你可以要求计算机解释它们。以下矩阵仍然有效吗?

矩阵 (B) 有效的。它充分捕捉了各种训练示例(行)和灯光(列)之间的关系。注意 Matrix A * 10 == Matrix B (A * 10 == B)。这意味着这些矩阵是 标量倍数 的关系。

矩阵 A 和 B 都包含相同的基础模式

重要的启示是,存在无数个矩阵可以完美地反映数据集中的路灯模式。甚至下一个展示的也是完美的。

重要的是要认识到,基础模式并不等同于矩阵。它是矩阵的 属性。实际上,它是这三个矩阵(A、B 和 C)的属性。模式是这些矩阵所 表达 的。模式也存在于路灯中。

这个 输入数据模式 是你希望神经网络学习转换成 输出数据模式 的。但为了学习输出数据模式,你还需要以矩阵的形式捕捉这种模式,如下所示。

注意,你可以反转 1 和 0,输出矩阵仍然可以捕捉数据中存在的底层 STOP/WALK 模式。你知道这一点,因为无论你将 1 分配给 WALK 还是 STOP,你仍然可以将 1 和 0 解码成底层的 STOP/WALK 模式。

生成的矩阵被称为 无损表示,因为你可以完美地在你的停止/行走笔记和矩阵之间进行转换。

在 Python 中创建一个矩阵或两个

将矩阵导入 Python

你已经将路灯模式转换成了一个矩阵(只包含 1 和 0 的矩阵)。现在让我们在 Python 中创建这个矩阵(以及更重要的是,其基础模式),以便神经网络可以读取它。Python 的 NumPy 库(在第三章中介绍)正是为了处理矩阵而构建的。让我们看看它的实际应用:

import numpy as np
streetlights = np.array( [ [ 1, 0, 1 ],
                            [ 0, 1, 1 ],
                            [ 0, 0, 1 ],
                            [ 1, 1, 1 ],
                            [ 0, 1, 1 ],
                            [ 1, 0, 1 ] ] )

如果你是一个常规的 Python 用户,这段代码中应该有一些东西会令你印象深刻。矩阵只是一个列表的列表。它是一个数组的数组。什么是 NumPy?NumPy 实际上只是一个用于数组的数组的高级包装器,它提供了特殊的、以矩阵为导向的函数。让我们也创建一个 NumPy 矩阵来输出数据:

walk_vs_stop = np.array( [ [ 0 ],
                           [ 1 ],
                           [ 0 ],
                           [ 1 ],
                           [ 1 ],
                           [ 0 ] ] )

你想让神经网络做什么?将 streetlights 矩阵学习转换成 walk_vs_stop 矩阵。更重要的是,你希望神经网络将 streetlights 具有相同基础模式的任何矩阵转换成一个包含 walk_vs_stop 基础模式的矩阵。关于这一点稍后详细说明。让我们先尝试使用神经网络将 streetlights 转换成 walk_vs_stop

构建神经网络

你现在已经学习了几个章节关于神经网络的内容。你有一个新的数据集,你将创建一个神经网络来解决它。以下是一些示例代码,用于学习第一个路灯模式。这应该看起来很熟悉:

import numpy as np
weights = np.array([0.5,0.48,-0.7])
alpha = 0.1

streetlights = np.array( [ [ 1, 0, 1 ],
                           [ 0, 1, 1 ],
                           [ 0, 0, 1 ],
                           [ 1, 1, 1 ],
                           [ 0, 1, 1 ],
                           [ 1, 0, 1 ] ] )

walk_vs_stop = np.array( [ 0, 1, 0, 1, 1, 0 ] )

input = streetlights[0]                  *1*
goal_prediction = walk_vs_stop[0]        *2*

for iteration in range(20):
    prediction = input.dot(weights)
    error = (goal_prediction - prediction) ** 2
    delta = prediction - goal_prediction
    weights = weights - (alpha * (input * delta))

    print("Error:" + str(error) + " Prediction:" + str(prediction))
  • 1 [1,0,1]

  • 2 等于 0(停止)

这个代码示例可能会让你回忆起在第三章中学到的几个细微差别。首先,dot函数的使用是一种在两个向量之间执行点积(加权求和)的方法。但第三章中没有包括 NumPy 矩阵执行元素级加法和乘法的方法:

import numpy as np

a = np.array([0,1,2,1])
b = np.array([2,2,2,3])

print(a*b)             *1*
print(a+b)             *2*
print(a * 0.5)         *3*
print(a + 0.5)         *4*
  • 1 元素级乘法

  • 2 元素级加法

  • 3 向量-标量乘法

  • 4 向量-标量加法

NumPy 使这些操作变得简单。当你将两个向量之间的+号时,它做你期望的事情:将两个向量相加。除了这些不错的 NumPy 运算符和新的数据集之外,这里显示的神经网络与之前构建的相同。

学习整个数据集

神经网络只学习了一个路灯。难道我们不想让它学习所有的路灯吗?

到目前为止,在这本书中,你已经训练了能够学习如何模拟单个训练示例(输入 -> 目标预测对)的神经网络。但现在你正在尝试构建一个神经网络,告诉你是否可以过马路。你需要它知道不止一个路灯。你该如何做到这一点?你可以一次性在所有路灯上训练它:

import numpy as np

weights = np.array([0.5,0.48,-0.7])
alpha = 0.1

streetlights = np.array( [[ 1, 0, 1 ],
                          [ 0, 1, 1 ],
                          [ 0, 0, 1 ],
                          [ 1, 1, 1 ],
                          [ 0, 1, 1 ],
                          [ 1, 0, 1 ] ] )

walk_vs_stop = np.array( [ 0, 1, 0, 1, 1, 0 ] )

input = streetlights[0]                *1*
goal_prediction = walk_vs_stop[0]      *2*

for iteration in range(40):
    error_for_all_lights = 0
    for row_index in range(len(walk_vs_stop)):
        input = streetlights[row_index]
        goal_prediction = walk_vs_stop[row_index]

        prediction = input.dot(weights)

        error = (goal_prediction - prediction) ** 2
        error_for_all_lights += error

        delta = prediction - goal_prediction
        weights = weights - (alpha * (input * delta))
        print("Prediction:" + str(prediction))
    print("Error:" + str(error_for_all_lights) + "\n")

                     Error:2.6561231104
                     Error:0.962870177672
                     ...
                     Error:0.000614343567483
                     Error:0.000533736773285
  • 1 [1,0,1]

  • 2 等于 0(停止)

完整、批量和随机梯度下降

随机梯度下降一次更新一个示例的权重

事实上,这种一次学习一个示例的想法是梯度下降的一个变体,称为随机梯度下降,并且它是可以用来学习整个数据集的一小部分方法之一。

随机梯度下降是如何工作的?正如你在上一个示例中看到的,它对每个训练示例分别进行预测和权重更新。换句话说,它首先处理第一个路灯,尝试预测它,计算权重变化量,并更新权重。然后它继续处理第二个路灯,依此类推。它多次迭代整个数据集,直到找到对所有训练示例都适用的工作权重配置。

(Full) 梯度下降一次更新一个数据集的权重

如在第四章介绍中所述,学习整个数据集的另一种方法是梯度下降(或平均/完整梯度下降)。不是为每个训练示例更新一次权重,网络计算整个数据集上的平均权重变化量,只在计算完整平均时改变权重。

批量梯度下降在 n 个示例后更新权重

这将在稍后更详细地介绍,但还有一个第三种配置,它在随机梯度下降和完整梯度下降之间取了一个折中。不是在仅一个示例或整个示例数据集之后更新权重,而是选择一个批量大小(通常在 8 到 256 之间)的示例,之后更新权重。

我们将在本书的后面部分进一步讨论这个问题,但到目前为止,认识到之前的例子创建了一个神经网络,通过逐个训练每个示例,可以学习整个街灯数据集。

神经网络学习相关性

最后一个神经网络学到了什么?

你刚刚完成了一个单层神经网络的训练,用于识别街灯模式并判断是否安全过马路。让我们暂时从神经网络的视角来看一下。神经网络并不知道它正在处理街灯数据。它所试图做的只是识别哪个输入(在三个可能的输入中)与输出相关。通过分析网络的最终权重位置,它正确地识别了中间的灯。

图片

注意中间的权重非常接近 1,而左侧和右侧的权重非常接近 0。从高层次来看,所有用于学习的迭代、复杂过程实际上完成了一件非常简单的事情:网络识别了中间输入和输出之间的相关性。相关性位于权重被设置为高数值的地方。相反,输出方面的随机性出现在左侧和右侧的权重(权重值非常接近 0)。

网络是如何识别相关性的呢?好吧,在梯度下降的过程中,每个训练示例都对权重施加了上压力下压力。平均而言,中间权重有更多的上压力,而其他权重有更多的下压力。压力从何而来?为什么不同权重的压力不同?

上下压力

它来自数据

每个节点都在尝试根据输入正确预测输出。在大多数情况下,每个节点在尝试这样做时都会忽略所有其他节点。唯一的交叉通信发生在所有三个权重必须共享相同的误差度量。权重更新不过是将这个共享的误差度量乘以每个相应的输入。

你为什么要这样做?神经网络学习的关键部分是错误归因,这意味着给定一个共享的误差,网络需要找出哪些权重做出了贡献(以便进行调整),哪些权重没有做出贡献(因此可以保持不变)。

图片

考虑第一个训练示例。因为中间输入是 0,所以对于这个预测来说,中间权重完全无关紧要。无论权重是多少,它都会乘以 0(输入)。因此,无论错误是过高还是过低,都可以归因于左侧和右侧的权重。

考虑第一个训练示例的压力。如果网络应该预测 0,而两个输入是 1,这将导致错误,这会驱动权重值趋向于 0

权重压力表有助于描述每个训练示例对每个相应权重的影响。+表示它有向 1 的压力,-表示它有向 0 的压力。零(0)表示没有压力,因为输入数据点是 0,所以该权重不会改变。注意,最左侧的权重有两个负号和一个正号,所以平均而言,权重将趋向于 0。中间的权重有三个正号,所以平均而言,权重将趋向于 1。

每个单独的权重都试图补偿错误。在第一个训练示例中,最右侧和最左侧的输入与期望输出之间存在不相关性。这导致这些权重承受向下压力。

这种现象在所有六个训练示例中都发生,通过向 1 的压力奖励相关性,通过向 0 的压力惩罚不相关性。平均而言,这导致网络找到中间权重和输出之间的相关性成为主要的预测力量(输入加权平均中的最重权重),使网络非常准确。

底线

预测是输入的加权总和。学习算法通过向上压力(趋向于 1)奖励与输出相关的输入,同时通过向下压力惩罚与输出不相关的输入。输入的加权总和通过将不相关的输入加权到 0,找到了输入和输出之间的完美相关性。

你体内的数学家可能有点不舒服。向上压力和向下压力几乎不是精确的数学表达式,而且有很多边缘情况,这种逻辑不成立(我们将在下一部分讨论)。但你会发现,这是一个极其有价值的近似,允许你暂时忽略梯度下降的所有复杂性,只需记住学习奖励相关性与更大的权重(或者更普遍地说,学习在两个数据集之间找到相关性)。

边缘情况:过拟合

有时候相关性是偶然发生的

再次考虑训练数据中的第一个示例。如果最左侧的权重是 0.5,而最右侧的权重是-0.5 怎么办?他们的预测将等于 0。网络将完美预测。但它并没有学会如何安全地预测街灯(这些权重在现实世界中会失败)。这种现象被称为过拟合

深度学习的最大弱点:过拟合

错误在所有权重之间共享。如果特定的权重配置意外地在预测和输出数据集之间创建了完美的相关性(即error等于 0),而没有给予最佳输入最重的权重,神经网络将停止学习

如果没有其他训练示例,这个致命的缺陷将使神经网络瘫痪。其他训练示例做了什么?好吧,让我们看看第二个训练示例。它将最右侧权重向上推,同时不改变最左侧权重。这打破了第一个示例中停止学习的平衡。只要你不只训练第一个示例,其余的训练示例将帮助网络避免陷入任何单个训练示例存在的这些边缘情况配置。

非常重要。神经网络非常灵活,可以找到许多不同的权重配置,这些配置可以正确预测训练数据的一个子集。如果你在这个神经网络上训练前两个训练示例,它很可能会在它对其他训练示例不起作用的地方停止学习。本质上,它记住了这两个训练示例,而不是找到将泛化到任何可能的街灯配置的相关性

如果你只训练在两个街灯上,并且网络只找到这些边缘情况配置,它可能无法告诉你当它看到训练数据中没有的街灯时是否安全过马路。

关键要点

你在深度学习中最面临的挑战是说服你的神经网络泛化而不是仅仅记忆。你还会再次看到这一点。

边缘情况:冲突的压力

有时相关性会自我斗争

考虑以下权重压力表的最右侧列。你看到了什么?

这一列似乎向上和向下的压力时刻数量相等。但网络正确地将这个(最右侧)权重推至 0,这意味着向下的压力时刻必须大于向上的。这是如何工作的?

左侧和中间的权重有足够的信号自行收敛。左侧权重降至 0,中间权重向 1 移动。随着中间权重的不断升高,正例的错误持续减少。但当它们接近最佳位置时,最右侧权重的去相关性变得更加明显。

让我们考虑一个极端的例子,其中左侧和中间权重分别完美地设置为 0 和 1。网络会发生什么?如果右侧权重高于 0,那么网络预测过高;如果右侧权重低于 0,网络预测过低。

随着其他节点学习,它们吸收了一些错误;它们吸收了部分相关性。它们使网络以适度的相关性进行预测,这减少了错误。其他权重随后只尝试调整它们的权重,以正确预测剩余的内容。

在这种情况下,因为中间权重有持续的信号来吸收所有的关联性(由于中间输入和输出之间的 1:1 关系),当你想要预测 1 时的错误变得非常小,但预测 0 时的错误变得很大,推动中间权重向下。

并非总是能如预期般成功。

在某些方面,你有点幸运。如果中间节点没有如此完美地相关,网络可能难以沉默最右侧的权重。稍后你将学习到正则化,它迫使具有冲突压力的权重移动向 0。

作为预览,正则化是有优势的,因为如果一个权重向上和向下的压力相等,它对任何事情都没有帮助。它对任何方向都没有帮助。本质上,正则化的目的是说,只有具有真正强关联性的权重才能保留;其他所有东西都应该被沉默,因为它们在增加噪声。这有点像自然选择,作为副作用,它会导致神经网络训练更快(迭代次数更少),因为最右侧的权重存在正负压力的问题。

在这种情况下,因为最右侧的节点没有明确的关联性,网络会立即开始将其驱动向 0。如果没有正则化(就像你之前训练的那样),你不会在学习到最右侧的输入是无用的,直到左侧和中间部分开始找出它们的模式。关于这一点,稍后还会详细说明。

如果网络在数据的一个输入列和输出列之间寻找关联性,那么神经网络会如何处理以下数据集?

图片

任何输入列和输出列之间都没有关联性。每个权重都有相等向上的压力和向下的压力。这个数据集对神经网络来说是一个真正的问题。

以前,你可以解决具有向上和向下压力的输入数据点,因为其他节点会开始解决正面的或负面的预测,将平衡节点吸引到向上或向下。但在这个情况下,所有输入在正负压力之间都是平衡的。你该怎么办?

学习间接关联

如果你的数据没有关联性,就创建具有关联性的中间数据!

以前,我描述神经网络为一个在输入和输出数据集之间寻找关联性的工具。我想稍微细化一下这个描述。实际上,神经网络在它们的输入和输出之间寻找关联性。

你将输入层的值设置为输入数据的单独行,并尝试训练网络,使得输出层等于输出数据集。奇怪的是,神经网络并不了解数据。它只是在输入层和输出层之间寻找关联性。

图片

不幸的是,这是一个新的没有输入和输出之间相关性的街灯数据集。解决方案很简单:使用两个这样的网络。第一个将创建一个与输出有有限相关性的中间数据集,第二个将使用这种有限的相关性来正确预测输出。

因为输入数据集与输出数据集不相关,所以你会使用输入数据集来创建一个中间数据集,这个中间数据集与输出数据集确实相关。这有点像作弊。

创建相关性

这是一张新神经网络的图片。你基本上是将两个神经网络堆叠在一起。中间层的节点(layer_1)代表中间数据集。目标是训练这个网络,即使输入数据集(layer_0)和输出数据集(layer_2)之间没有相关性,你创建的layer_1数据集使用layer_0将与layer_2相关。

图片

注意:这个网络仍然只是一个函数。它有一系列以特定方式收集在一起的权重。此外,梯度下降仍然有效,因为你可以计算出每个权重对误差的贡献,并调整它以将误差减少到 0。这正是你将要做的。

堆叠神经网络:综述

第三章简要提到了堆叠神经网络。让我们回顾一下

当你看到下面的架构时,预测发生的方式正如我所说的“堆叠神经网络”时你可能预期的那样。第一个较低网络的输出(layer_0layer_1)是第二个较高网络的输入(layer_1layer_2)。这些网络的预测与之前看到的是相同的。

图片

当你开始思考这个神经网络是如何学习的时候,你已经知道了很多。如果你忽略较低的权重,并认为它们的输出是训练集,那么神经网络的顶部(layer_1layer_2)就像之前章节中训练的网络一样。你可以使用所有相同的学习逻辑来帮助它们学习。

你还不理解的部分是如何更新layer_0layer_1之间的权重。它们使用什么作为它们的误差度量?你可能还记得第五章,缓存的/归一化的误差度量被称为delta。在这种情况下,你需要弄清楚如何知道layer_1delta值,这样它们可以帮助layer_2做出准确的预测。

反向传播:长距离误差归因

加权平均误差

layer_1layer_2 的预测是什么?它是 layer_1 上值的加权平均值。如果 layer_2 比预期高 x 量,你怎么知道哪些 layer_1 上的值导致了错误?那些具有 更高权重 (weights_1_2) 的值贡献更多。那些从 layer_1layer_2较低权重 的值贡献较少。

考虑一个极端情况。假设 layer_1layer_2 的最左侧权重为零。这个 layer_1 节点导致了网络错误的程度是多少?

这太简单了,几乎有点好笑。从 layer_1layer_2 的权重正好描述了每个 layer_1 节点对 layer_2 预测的贡献程度。这意味着这些权重也正好描述了每个 layer_1 节点对 layer_2 错误的贡献程度。

你如何使用 layer_2 上的 delta 来确定 layer_1 上的 delta?你将其乘以 layer_1 的相应权重。这就像预测逻辑的反向。这种移动 delta 信号的过程被称为 反向传播

反向传播:为什么这行得通?

加权平均 delta

在第五章(chapter 5)的神经网络中,delta 变量告诉你这个节点的值在下一次应该改变的方向和量。所有反向传播能让你做的只是说,“嘿,如果你想使这个节点比现在高 x 量,那么这四个先前的节点每个都需要比现在高/低 x*weights_1_2 量,因为这些权重将预测放大了 weights_1_2 倍。”

当反向使用时,weights_1_2 矩阵会按适当的比例放大错误。它放大错误,这样你知道每个 layer_1 节点应该向上或向下移动多少。

一旦你知道这一点,你就可以像之前一样更新每个权重矩阵。对于每个权重,将其输出 delta 乘以其输入 value,并按这个量调整权重(或者你可以用 alpha 进行缩放)。

线性与非线性

这可能是书中最难理解的概念。让我们慢慢来

我将向你展示一个现象。结果证明,你需要再添加一个部件才能使这个神经网络进行训练。让我们从两个角度来分析。第一个角度将展示为什么神经网络没有这个部件就无法训练。换句话说,首先我会向你展示为什么神经网络目前是出问题的。然后,一旦你添加了这个部件,我会向你展示它是如何解决这个问题。现在,先看看这个简单的代数式:

1 * 10 * 2 = 100              1 * 0.25 * 0.9 = 0.225
5 * 20 = 100                  1 * 0.225 = 0.225

这里是关键点:对于任何两次乘法,我都可以使用一次乘法来完成相同的事情。结果证明,这是不好的。看看下面的例子:

这两个图显示了两个训练示例,一个输入是 1.0,另一个输入是-1.0。底线是:对于你创建的任何三层网络,都有一个具有相同行为的两层网络。堆叠两个神经网络(正如你目前所知道的)不会给你带来任何额外的力量。两个连续的加权求和只是单个加权求和的一个更昂贵的版本。

为什么神经网络仍然不起作用

如果你像现在这样训练三层网络,它不会收敛

问题:对于任何两个连续的加权求和的输入,都存在一个具有完全相同行为的单个加权求和。三层网络能做的,两层网络也能做。

在修复之前,让我们谈谈中间层(layer_1)。现在,每个节点(四个中的每一个)都从每个输入接收一个权重。让我们从相关性的角度来考虑这个问题。中间层中的每个节点都订阅了与每个输入节点一定量的相关性。如果一个输入到中间层的权重是 1.0,那么它订阅了该节点运动的 100%。如果该节点上升 0.3,中间节点将跟随。如果连接两个节点的权重是 0.5,中间层中的每个节点都订阅了该节点运动的 50%。

中间节点唯一摆脱特定输入节点相关性的方法就是它从另一个输入节点订阅额外的相关性。对这个神经网络没有任何新的贡献。每个隐藏节点都从输入节点订阅一小部分相关性。

中间节点无法为对话添加任何内容;它们无法拥有自己的相关性。它们与各种输入节点或多或少都有相关性。

图片

但因为你知道在新数据集中,任何输入与输出之间都没有相关性,那么中间层如何帮助呢?它混合了一堆已经无用的相关性。你真正需要的是中间层能够选择性地与输入相关联。

你希望中间层有时与输入相关联,有时不相关。这给了它自己的相关性。这使得中间层有机会不仅仅总是与一个输入保持x%的相关性,与另一个输入保持y%的相关性。相反,它可以在想要的时候与一个输入保持x%的相关性,但在其他时候完全不相关。这被称为条件相关性有时相关性

有时相关性的秘诀

当值低于 0 时关闭节点

这可能看起来太简单而不起作用,但考虑一下:如果一个节点的值下降到 0 以下,通常节点仍然会与输入保持相同的相关性。它只是碰巧是负值。但如果你在节点为负时将其关闭(将其设置为 0),那么它在与输入相关时总是具有零相关性

这意味着什么?节点现在可以有选择地挑选和选择它想要与什么相关联的时刻。这允许它说,“当右输入关闭时,让我与左输入完美相关。”它是如何做到这一点的呢?好吧,如果左输入的权重是 1.0,而右输入的权重是一个巨大的负数,那么同时打开左输入和右输入将导致节点始终为 0。但如果只有左输入打开,节点将采用左输入的值。

这在以前是不可能的。以前,中间节点要么总是与输入相关联,要么总是不相关。现在它可以是有条件的。现在它可以为自己发声了。

解决方案:通过在任何中间节点为负时将其关闭,你允许网络有时从各种输入中订阅相关性。这对于两层神经网络是不可能的,因此为三层网络增加了力量。

这种“如果节点为负,则将其设置为 0”的逻辑的术语是非线性。没有这种调整,神经网络是线性的。没有这种技术,输出层只能从两层网络中已有的相关性中进行选择。它正在订阅输入层的一部分,这意味着它不能解决新的街灯数据集。

非线性有很多种。但这里讨论的,在许多情况下,是最好的选择。它也是最简单的。(它被称为relu。)

值得注意的是,大多数其他书籍和课程都说连续的矩阵乘法是线性变换。我觉得这不太直观。这也使得理解非线性如何贡献以及为什么选择一个而不是另一个(我们稍后会讨论)变得更困难。它说,“没有非线性,两次矩阵乘法可能就是 1。”我的解释,虽然不是最简洁的答案,但是对为什么需要非线性的一种直观解释。

快速休息一下

那最后一部分可能感觉有点抽象,这是完全可以接受的

事情是这样的。前几章使用的是简单的代数,所以所有东西最终都基于根本简单的工具。这一章开始基于你之前学到的前提。以前,你学到了这样的课程:

你可以计算出错误与任何一个权重之间的关系,这样你就知道改变权重是如何影响错误的。然后你可以利用这一点将错误降低到 0。

那是一个巨大的教训。但现在我们正在超越它。因为我们已经解决了为什么它会起作用的原因,所以你可以直接接受这个陈述。下一个重要的教训出现在本章的开头:

通过一系列训练示例调整权重以减少误差,最终是在寻找输入层和输出层之间的相关性。如果不存在相关性,则误差永远不会达到 0。

这是一个更大的教训。这主要意味着你现在可以暂时忘记之前的教训。你不需要它。现在你专注于相关性。要点是,你不能一次想太多。接受每一个教训,让自己相信它。当它是对更细粒度教训的更简洁总结(更高的抽象)时,你可以放下细粒度,专注于理解更高的总结。

这类似于一个专业的游泳者、骑自行车者或类似的运动员,他们需要结合对许多小课程的综合流畅知识。一个击球手挥棒击球,学习了成千上万的小课程,最终达到一个伟大的挥棒。但当他走到击球区时,他不会想到所有这些。他的动作是流畅的——甚至是无意识的。学习这些数学概念也是如此。

神经网络寻找输入和输出之间的相关性,你不再需要担心它是如何发生的。你只需要知道它确实发生了。现在我们正在建立在这个想法的基础上。让自己放松,相信你已经学到的东西。

您的第一个深度神经网络

这是如何进行预测的方法

以下代码初始化权重并执行前向传播。新代码是粗体

import numpy as np

np.random.seed(1)

def relu(x):                                           *1*
    return (x > 0) * x

alpha = 0.2
hidden_size = 4

streetlights = np.array( [[ 1, 0, 1 ],
                          [ 0, 1, 1 ],
                          [ 0, 0, 1 ],
                          [ 1, 1, 1 ] ] )

walk_vs_stop = np.array([[ 1, 1, 0, 0]]).T

weights_0_1 = 2*np.random.random((3,hidden_size)) - 1  *2*
weights_1_2 = 2*np.random.random((hidden_size,1)) - 1

layer_0 = streetlights[0]
layer_1 = relu(np.dot(layer_0,weights_0_1))
layer_2 = np.dot(layer_1,weights_1_2)                  *3*
  • 1 此函数将所有负数设置为 0。

  • 2 现在有两套权重连接三个层(随机初始化)

  • 3 层 _1 的输出通过 relu 函数处理,其中负值变为 0。这是下一层,层 _2 的输入。

对于代码的每一部分,请跟随图示。输入数据进入layer_0。通过dot函数,信号从layer_0沿着权重向上传递到layer_1(在每个四个layer_1节点上执行加权求和)。然后,layer_1上的这些加权求和通过relu函数传递,该函数将所有负数转换为 0。然后对最终节点,layer_2执行最终的加权求和。

代码中的反向传播

你可以学习每个权重对最终误差的贡献量

在上一章的结尾,我提出一个断言,即记住两层神经网络代码将非常重要,这样你就可以在我引用更高级的概念时快速轻松地回忆它。这就是记忆的重要性所在。

以下列表是新的学习代码,并且你一定要识别和理解前几章中提到的部分。如果你迷失了方向,就去 第五章,记住代码,然后回来。这将在某一天产生重大影响。

import numpy as np

np.random.seed(1)

def relu(x):
    return (x > 0) * x                                                   *1*

def relu2deriv(output):
    return output>0                                                      *2*

alpha = 0.2
hidden_size = 4

weights_0_1 = 2*np.random.random((3,hidden_size)) - 1
weights_1_2 = 2*np.random.random((hidden_size,1)) - 1

for iteration in range(60):
   layer_2_error = 0
   for i in range(len(streetlights)):
      layer_0 = streetlights[i:i+1]
      layer_1 = relu(np.dot(layer_0,weights_0_1))
      layer_2 = np.dot(layer_1,weights_1_2)

      layer_2_error += np.sum((layer_2 - walk_vs_stop[i:i+1]) ** 2)

      layer_2_delta = (walk_vs_stop[i:i+1] - layer_2)
      layer_1_delta=layer_2_delta.dot(weights_1_2.T)*relu2deriv(layer_1) *3*

      weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
      weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)

   if(iteration % 10 == 9):
      print("Error:" + str(layer_2_error))
  • 1 当 x > 0 时返回 x;否则返回 0

  • 2 当输入大于 0 时返回 1;否则返回 0

  • 3 这一行通过将 layer_2_delta 乘以其连接权重 _1_2,根据 layer_2 的 delta 计算出 layer_1 的 delta。

信不信由你,唯一真正新的代码是加粗的部分。其他所有内容在本质上都与前面的页面相同。relu2deriv 函数在 output > 0 时返回 1;否则返回 0。这是 relu 函数的 斜率(即 导数)。它起着重要的作用,你将在下一刻看到。

记住,目标是 错误归因。这是关于弄清楚每个权重对最终错误贡献了多少。在第一个(两层)神经网络中,你计算了一个 delta 变量,它告诉你希望输出预测变得更高或更低。看看这里的代码。你以相同的方式计算 layer_2_delta。没有什么新的。(再次,如果你忘记了这部分是如何工作的,请回到 第五章。)

现在你已经知道了最终预测应该向上或向下移动多少(delta),你需要弄清楚每个中间(layer_1)节点应该向上或向下移动多少。这些实际上是 中间预测。一旦你有了 layer_1delta,你就可以使用之前相同的过程来计算权重更新(对于每个权重,将其输入值乘以其输出 delta 并将 weight 值增加这么多)。

你如何计算 layer_1delta?首先,做显而易见的事情:将输出 delta 乘以连接到它的每个权重。这给出了每个权重对那个错误的贡献的加权。还有一件事需要考虑。如果 relulayer_1 节点的输出设置为 0,那么它没有对错误做出贡献。当这种情况发生时,你应该也将该节点的 delta 设置为 0。将每个 layer_1 节点乘以 relu2deriv 函数可以实现这一点。relu2deriv 要么是 1,要么是 0,这取决于 layer_1 的值是否大于 0。

一次反向传播迭代

如你所见,反向传播是关于计算中间层的 delta,以便你可以执行梯度下降。要做到这一点,你需要取 layer_2layer_1 的加权平均 delta(由它们之间的权重加权)。然后关闭(设置为 0)没有参与前向预测的节点,因为它们不可能对错误做出贡献。

将所有内容整合在一起

下面是你可以运行的自我依赖程序(运行时输出如下)

import numpy as np

np.random.seed(1)

def relu(x):
    return (x > 0) * x         *1*

def relu2deriv(output):
    return output>0            *2*

streetlights = np.array( [[ 1, 0, 1 ],
                          [ 0, 1, 1 ],
                          [ 0, 0, 1 ],
                          [ 1, 1, 1 ] ] )

walk_vs_stop = np.array([[ 1, 1, 0, 0]]).T

alpha = 0.2
hidden_size = 4

weights_0_1 = 2*np.random.random((3,hidden_size)) - 1
weights_1_2 = 2*np.random.random((hidden_size,1)) - 1

for iteration in range(60):
   layer_2_error = 0
   for i in range(len(streetlights)):
      layer_0 = streetlights[i:i+1]
      layer_1 = relu(np.dot(layer_0,weights_0_1))
      layer_2 = np.dot(layer_1,weights_1_2)

      layer_2_error += np.sum((layer_2 - walk_vs_stop[i:i+1]) ** 2)

      layer_2_delta = (layer_2 - walk_vs_stop[i:i+1])
      layer_1_delta=layer_2_delta.dot(weights_1_2.T)*relu2deriv(layer_1)

      weights_1_2 -= alpha * layer_1.T.dot(layer_2_delta)
      weights_0_1 -= alpha * layer_0.T.dot(layer_1_delta)

   if(iteration % 10 == 9):
      print("Error:" + str(layer_2_error))
  • 1 当 x 大于 0 时返回 x;否则返回 0

  • 2 当输入大于 0 时返回 1;否则返回 0

Error:0.634231159844
Error:0.358384076763
Error:0.0830183113303
Error:0.0064670549571
Error:0.000329266900075
Error:1.50556226651e-05

深度网络为什么重要?

创建具有相关性的“中间数据集”有什么意义?

考虑这里显示的猫图片。进一步考虑,我有一个包含有猫和无猫图片的数据集(并且我已将它们标记为这样)。如果我想训练一个神经网络,使其从像素值预测图片中是否有猫,那么两层网络可能存在问题。

就像在最后一个街灯数据集中一样,单个像素与图片中是否有猫无关。只有像素的不同配置与是否有猫相关。

这就是深度学习的本质。深度学习全部在于创建中间层(数据集),其中每个中间层的节点代表不同输入配置的存在或不存在。

这样,对于猫图像数据集,不需要单个像素与照片中是否有猫相关联。相反,中间层将尝试识别可能与猫相关联(例如耳朵、猫眼或猫毛)或不相关联的像素配置。许多类似猫的配置的存在将给最终层提供所需的信息(相关性),以便正确预测猫的存在或不存在。

信不信由你,你可以使用三层网络并继续堆叠更多的层。一些神经网络有数百层,每个节点都在检测不同输入数据的配置中扮演其角色。本书的其余部分将致力于研究这些层中的不同现象,以探索深度神经网络的全部力量。

为了达到这个目的,我必须提出与第五章相同的挑战:记住之前的代码。为了使接下来的章节可读,你需要非常熟悉代码中的每个操作。除非你能从记忆中构建一个三层神经网络,否则不要越过这一点!

第七章. 如何在头脑中和纸上描绘神经网络

在本章中

  • 相关性总结

  • 简化可视化

  • 观察网络预测

  • 用字母而不是图片来可视化

  • 连接变量

  • 可视化工具的重要性

“数字有一个重要的故事要讲。它们依赖于你给予它们一个清晰而有说服力的声音。”

斯蒂芬·弗,IT 创新者、教师和顾问

是时候简化了

总是考虑所有事情是不切实际的。心理工具可以帮助

第六章以一个相当令人印象深刻的代码示例结束。仅仅神经网络就包含了 35 行非常密集的代码。阅读它时,很明显有很多事情在进行;并且这段代码包括超过 100 页的概念,当结合在一起时,可以预测是否安全过马路。

我希望你在每一章中都能继续通过记忆来重建这些示例。随着示例的增大,这个练习就不再仅仅是记住代码的具体字母,而是更多地关于记住概念,然后根据这些概念重建代码。

在本章中,这种在你的脑海中构建高效概念的结构正是我想讨论的。尽管它不是一个架构或实验,但它可能是我能给你提供的最重要的价值。在这种情况下,我想展示我是如何在我的脑海中以高效的方式总结所有的小课程,以便我能做诸如构建新的架构、调试实验以及在新的问题和新的数据集上使用架构之类的事情。

让我们先回顾一下你到目前为止学到的概念

这本书从小的课程开始,然后在它们之上构建抽象层。我们首先讨论了机器学习背后的思想。然后我们进步到单个线性节点(或神经元)如何学习,然后是水平组神经元(层)和垂直组(层堆叠)。在这个过程中,我们讨论了学习实际上只是将错误减少到 0,我们使用微积分来发现如何改变网络中的每个权重,以帮助将错误移动到 0 的方向。

接下来,我们讨论了神经网络如何搜索(有时创建)输入和输出数据集之间的相关性。这个最后的想法使我们能够忽略之前关于单个神经元行为的课程,因为它简洁地总结了之前的课程。神经元、梯度、层堆叠等总和导致一个单一的想法:神经网络寻找并创建相关性。

保留这个相关性概念而不是之前较小的概念对于学习深度学习很重要。否则,很容易被神经网络的复杂性所淹没。让我们为这个想法起一个名字:相关性总结

相关性总结

这是向更高级的神经网络前进的合理关键

相关性总结

神经网络试图找到输入层和输出层之间的直接和间接相关性,这些层分别由输入和输出数据集决定。

在 10,000 英尺的高度上,这是所有神经网络都在做的事情。鉴于神经网络实际上只是一系列通过层连接的矩阵,让我们稍微放大一下,考虑一下任何特定的权重矩阵正在做什么。

局部相关性总结

任何一组权重都会优化以学习如何将其输入层与输出层所说的内容相关联。

当你只有两层(输入和输出)时,权重矩阵知道输出层基于输出数据集所说的内容。它寻找输入和输出数据集之间的相关性,因为它们被捕获在输入和输出层中。但当你有多个层时,这会变得更加复杂,记得吗?

全局相关性总结

一个早期层所说的内容可以通过将后续层所说的内容乘以它们之间的权重来确定。这样,后续层可以告诉早期层它们需要的信号类型,最终找到与输出的相关性。这种交叉通信被称为反向传播

当全局相关性教给每一层它应该是什么时,局部相关性可以在局部优化权重。当最终层的神经元说,“我需要稍微高一点”时,它就会继续告诉它前面层的所有神经元,“嘿,前一层,给我更高的信号。”然后它们会告诉它们前面的神经元,“嘿,给我们更高的信号。”这就像一场巨大的电话游戏——游戏结束时,每一层都知道它的哪些神经元需要更高或更低,然后局部相关性总结接管,相应地更新权重。

之前过于复杂的可视化

在简化心理图的同时,让我们也简化一下可视化。

在这一点上,我预计你脑海中神经网络的可视化就像这里显示的图片(因为这是我们用的那个)。输入数据集在layer_0中,通过权重矩阵(一堆线条)连接到layer_1,依此类推。这是一个有用的工具,用于学习如何将权重集合和层组合起来学习一个函数。

但向前推进,这幅图过于详细。考虑到相关性总结,你已经知道你不再需要担心单个权重的更新方式。后续层已经知道如何与早期层通信,并告诉它们,“嘿,我需要更高的信号”或“嘿,我需要更低的信号”。说实话,你不再关心权重值,只关心它们是否按预期行为,以适当的方式捕捉相关性,并实现泛化。

为了反映这种变化,让我们在纸上更新可视化。我们还将做一些其他事情,这些事情在以后会变得有意义。正如你所知,神经网络是一系列权重矩阵。当你使用网络时,你也会创建与每一层对应的向量。

在这个图中,权重矩阵是节点之间的线条,向量是节点条带。例如,weights_1_2是一个矩阵,weights_0_1是一个矩阵,layer_1是一个向量。

在后面的章节中,我们将以越来越有创意的方式排列向量和矩阵,因此,而不是显示每个节点通过每个权重连接的细节(如果我们有 500 个节点在layer_1中,这会变得难以阅读),让我们从一般的角度来思考。让我们把它们看作任意大小的向量和矩阵。

图片

简化的可视化

神经网络就像乐高积木,每一块积木都是一个向量或矩阵

在接下来的内容中,我们将以人们用乐高积木搭建新结构的方式构建新的神经网络架构。关于相关性总结的妙处在于,导致它的所有片段(反向传播、梯度下降、alpha、dropout、小批量等)并不依赖于乐高积木的特定配置。无论你如何拼接矩阵序列,用层将其粘合在一起,神经网络都会通过修改输入层和输出层之间的权重来尝试学习数据中的模式。

为了反映这一点,我们将使用右边的部件构建所有神经网络。条带是向量,方框是矩阵,圆圈是单个权重。请注意,方框可以看作是“向量的向量”,水平或垂直排列。

图片

图片

主要收获

左边的图片仍然提供了构建神经网络所需的所有信息。你知道所有层和矩阵的形状和大小。当你知道相关性总结及其所有内容时,之前的细节就不再必要了。但我们还没有完成:我们可以进一步简化。

进一步简化

矩阵的维度由层决定

在上一节中,你可能已经注意到了一个模式。每个矩阵的维度(行数和列数)与其前后层的维度有直接关系。因此,我们可以进一步简化可视化。

考虑右边的可视化。我们仍然有构建神经网络所需的所有信息。我们可以推断出weights_0_1是一个(3 × 4)的矩阵,因为前一层(layer_0)有三个维度,下一层(layer_1)有四个维度。因此,为了使矩阵足够大,以便有一个单独的权重将layer_0中的每个节点与layer_1中的每个节点连接起来,它必须是一个(3 × 4)的矩阵。

这使我们开始思考使用相关性总结的神经网络。所有这些神经网络要做的就是调整权重,以找到layer_0layer_2之间的相关性。它将使用本书中提到的所有方法来完成这项工作。但是,输入层和输出层之间权重和层的不同配置对网络是否成功找到相关性(以及/或找到相关性的速度)有重大影响。

神经网络中层的特定配置和权重被称为其架构,我们将在本书的剩余大部分内容中讨论各种架构的优缺点。正如相关性总结提醒我们的,神经网络调整权重以找到输入层和输出层之间的相关性,有时甚至在隐藏层中发明相关性。不同的架构使信号通道更容易发现相关性

好的神经网络架构使信号通道,以便更容易发现相关性。伟大的架构还过滤噪声,以帮助防止过拟合。

大多数关于神经网络的研究都是关于寻找新的架构,这些架构可以更快地找到相关性,并且更好地泛化到未见过的数据。我们将在本书的剩余大部分内容中讨论新的架构。

让我们看看这个网络如何预测

让我们想象街灯示例中的数据流经系统

在图 1 中,从街灯数据集中选择了一个单个数据点。layer_0被设置为正确的值。

在图 2 中,对layer_0执行了四个不同的加权求和。这四个加权求和由weights_0_1执行。提醒一下,这个过程被称为向量矩阵乘法。这四个值被存入layer_1的四个位置,并通过relu函数(将负值设置为 0)传递。为了清楚起见,layer_1中从左数第三个值原本会是负数,但relu函数将其设置为 0。

如图 3 所示,最终步骤执行了layer_1的加权平均,再次使用向量矩阵乘法过程。这产生了数字 0.9,这是网络的最终预测。

复习:向量矩阵乘法

向量矩阵乘法执行多个加权求和的向量。矩阵必须具有与向量值相同的行数,以便矩阵中的每一列执行一个独特的加权求和。因此,如果矩阵有四列,将生成四个加权求和。每个求和的加权根据矩阵的值执行。

使用字母而不是图片进行可视化

所有这些图片和详细解释实际上只是简单的代数

正如我们为矩阵和向量定义了简单的图示,我们也可以用字母的形式进行相同的可视化。

如何用数学来可视化一个 矩阵?选择一个大写字母。我尽量选择一个容易记住的,比如 W 代表“权重”。小写的 0 表示它可能是几个 W 中的一个。在这种情况下,网络有两个。也许令人惊讶的是,我可以选择任何大写字母。小写的 0 是一个额外的标识,让我可以调用所有的权重矩阵 W,以便区分它们。这是你的可视化;让它容易记住。

如何用数学来可视化一个 向量?选择一个小写字母。为什么我选择了字母 l?嗯,因为我有一堆层向量,我觉得 l 容易记住。为什么我选择称之为 l-zero?因为我有多个层,让所有它们都变成 l 并编号,而不是为每一层想新字母,看起来很合适。这里没有错误答案。

如果这样在数学中可视化矩阵和向量,那么网络中的所有部分看起来是什么样子?在右侧,你可以看到一组变量指向神经网络各自的区域。但定义它们并不显示它们之间的关系。让我们通过向量-矩阵乘法来组合这些变量。

链接变量

字母可以组合起来表示函数和操作

向量-矩阵乘法很简单。为了可视化两个字母相互乘法,将它们并排放置。例如:

代数 翻译
l[0]W[0] “使用层 0 向量与权重矩阵 0 进行向量-矩阵乘法。”
l[1]W[1] “取层 1 向量并与权重矩阵 1 进行向量-矩阵乘法。”

你甚至可以加入像 relu 这样的任意函数,使用看起来几乎与 Python 代码完全相同的符号。这是疯狂直观的东西。

l[1] = relu(l[0]W[0]) “为了创建层 1 向量,取层 0 向量并与权重矩阵 0 进行向量-矩阵乘法;然后对输出执行 relu 函数(将所有负数设置为 0)。”
l[2] = l[1]W[1] “为了创建层 2 向量,取层 1 向量并与权重矩阵 1 进行向量-矩阵乘法。”

如果你注意到,层 2 的代数包含了层 1 作为输入变量。这意味着你可以通过链接它们来用一个表达式表示整个 神经网络

l[2] = relu(l[0]W[0])W[1] 因此,前向传播步骤中的所有逻辑都可以包含在这个公式中。注意:这个公式中包含了向量和矩阵具有正确维度的假设。

所有东西并排在一起

让我们在一个地方看到可视化、代数公式和 Python 代码。

我认为在这个页面上不需要太多对话。花一分钟时间看看通过这四种不同的方式看到的前向传播的每一部分。我希望你能真正理解前向传播,并通过从不同角度观察,在一个地方理解架构。

可视化工具的重要性

我们将研究新的架构

在接下来的章节中,我们将以一些创造性的方式将这些矢量和矩阵结合起来。我描述每个架构的能力完全取决于我们是否有一个共同的语言来描述它们。因此,请在你能够清楚地看到前向传播如何操作这些矢量和矩阵,以及如何用各种形式描述它们之前,不要超出这一章。

关键要点

良好的神经网络架构能够有效地引导信号,使得相关性易于发现。优秀的架构还能够过滤噪声,帮助防止过拟合。

如前所述,神经网络架构控制着信号在网络中的流动方式。你创建这些架构的方式将影响网络检测相关性的方式。你会发现,你需要创建能够最大化网络关注存在有意义相关性的区域的架构,并最小化网络关注包含噪声的区域的架构。

但是不同的数据集和领域具有不同的特性。例如,图像数据与文本数据具有不同类型的信号和噪声。尽管神经网络可以在许多情况下使用,但由于它们定位特定类型相关性的能力,不同的架构将更适合不同的问题。因此,在接下来的几章中,我们将探讨如何修改神经网络以特别寻找你正在寻找的相关性。那里见!

第八章。学习信号和忽略噪声:正则化和批处理简介

本章内容

  • 过拟合

  • Dropout

  • 批量梯度下降

“用四个参数我可以画一头大象,用五个参数我甚至可以让它扭动它的鼻子。”

约翰·冯·诺伊曼,数学家、物理学家、计算机科学家和通才

MNIST 上的三层网络

让我们回到 MNIST 数据集,并尝试使用新的网络进行分类

在前面的几章中,你已经了解到神经网络建模相关性。隐藏层(三层网络中的中间层)甚至可以创建中间相关性来帮助解决问题(似乎是凭空而来)。你怎么知道网络正在创建好的相关性呢?

当我们讨论具有多个输入的随机梯度下降时,我们进行了一个实验,其中我们冻结了一个权重,然后要求网络继续训练。在训练过程中,点就像找到了碗底。你看到权重被调整以最小化错误。

当我们将权重冻结时,冻结的权重仍然找到了碗底。由于某种原因,碗移动了,使得冻结的权重值变得最优。此外,如果我们解冻权重进行更多训练,它就不会学习。为什么?嗯,错误已经下降到 0。对于网络来说,再也没有什么可以学习的了。

这就引出了一个问题,如果冻结的权重的输入对预测现实世界中的棒球胜利很重要怎么办?如果网络已经找到了一种准确预测训练数据集中比赛的方法(因为这就是网络所做的事情:最小化错误),但它却忘记了包含一个有价值的输入怎么办?

不幸的是,这种现象——过拟合——在神经网络中非常普遍。我们可以说它是神经网络的宿敌;神经网络的表达能力越强(更多层和权重),就越容易过拟合。在研究中,人们不断发现需要更强大层级的任务,但随后又必须进行大量问题解决以确保网络不过拟合。

在本章中,我们将研究正则化的基础知识,这是对抗神经网络过拟合的关键。为此,我们将从最强大的神经网络(具有relu隐藏层的三层网络)开始,在最具挑战性的任务(MNIST 数字分类)上进行。

首先,按照下面的步骤训练网络。你应该看到与列出的相同的结果。唉,网络学会了完美地预测训练数据。我们应该庆祝吗?

import sys, numpy as np
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

images, labels = (x_train[0:1000].reshape(1000,28*28) \
                                        255, y_train[0:1000])
one_hot_labels = np.zeros((len(labels),10))

for i,l in enumerate(labels):
    one_hot_labels[i][l] = 1
labels = one_hot_labels

test_images = x_test.reshape(len(x_test),28*28) / 255
test_labels = np.zeros((len(y_test),10))
for i,l in enumerate(y_test):
    test_labels[i][l] = 1

np.random.seed(1)
relu = lambda x:(x>=0) * x        *1*
relu2deriv = lambda x: x>=0       *2*
alpha, iterations, hidden_size, pixels_per_image, num_labels = \
                                              (0.005, 350, 40, 784, 10)
weights_0_1 = 0.2*np.random.random((pixels_per_image,hidden_size)) - 0.1
weights_1_2 = 0.2*np.random.random((hidden_size,num_labels)) - 0.1

for j in range(iterations):
    error, correct_cnt = (0.0, 0)

    for i in range(len(images)):
        layer_0 = images[i:i+1]
        layer_1 = relu(np.dot(layer_0,weights_0_1))
        layer_2 = np.dot(layer_1,weights_1_2)
        error += np.sum((labels[i:i+1] - layer_2) ** 2)
        correct_cnt += int(np.argmax(layer_2) == \
                                        np.argmax(labels[i:i+1]))
        layer_2_delta = (labels[i:i+1] - layer_2)
        layer_1_delta = layer_2_delta.dot(weights_1_2.T)\
                                    * relu2deriv(layer_1)
        weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
        weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)

    sys.stdout.write("\r"+ \
                     " I:"+str(j)+ \
                     " Error:" + str(error/float(len(images)))[0:5] +\
                     " Correct:" + str(correct_cnt/float(len(images))))
  • 1 当 x 大于 0 时返回 x;否则返回 0

  • 2 当输入大于 0 时返回 1;否则返回 0

....
I:349 Error:0.108 Correct:1.0

嗯,这很简单

神经网络完美地学会了预测所有 1000 张图像

在某些方面,这是一个真正的胜利。神经网络能够从 1,000 张图像的数据集中学习,将每个输入图像与正确的标签相关联。

它是如何做到这一点的?它逐个遍历每张图像,做出预测,然后略微更新每个权重,以便下一次预测更好。在所有图像上这样做足够长时间后,网络最终达到了能够正确预测所有图像的状态。

这里有一个不那么明显的问题:神经网络在它之前从未见过的图像上的表现会怎样?换句话说,它在它训练的 1,000 张图像之外的图像上的表现会怎样?MNIST 数据集包含的图像比训练的 1,000 张图像多得多;让我们试试。

在之前的代码笔记本中有两个变量:test_imagestest_labels。如果你执行以下代码,它将在这些图像上运行神经网络并评估网络对它们的分类效果:

if(j % 10 == 0 or j == iterations-1):
  error, correct_cnt = (0.0, 0)

  for i in range(len(test_images)):

        layer_0 = test_images[i:i+1]
        layer_1 = relu(np.dot(layer_0,weights_0_1))
        layer_2 = np.dot(layer_1,weights_1_2)

        error += np.sum((test_labels[i:i+1] - layer_2) ** 2)
        correct_cnt += int(np.argmax(layer_2) == \
                                        np.argmax(test_labels[i:i+1]))
  sys.stdout.write(" Test-Err:" + str(error/float(len(test_images)))[0:5] +\
             " Test-Acc:" + str(correct_cnt/float(len(test_images))))
  print()
Error:0.653 Correct:0.7073

网络的表现非常糟糕!它的准确率只有 70.7%。为什么它在这些新的测试图像上的表现如此糟糕,尽管它在训练数据上学会了以 100%的准确率进行预测?真奇怪。

这个 70.7%的数字被称为测试准确率。这是神经网络在它没有训练过的数据上的准确率。这个数字很重要,因为它模拟了如果你尝试在现实世界中使用神经网络(这给网络只有它之前没有见过的图像)时,神经网络的表现会怎样。这是重要的分数。

记忆与泛化

记忆 1,000 张图像比泛化到所有图像要容易

让我们再次考虑神经网络是如何学习的。它调整每个矩阵中的每个权重,以便网络能够更好地处理特定输入并做出特定预测。也许一个更好的问题可能是,“如果我们用 1,000 张图像来训练它,它学会了完美预测,为什么它还要在其他图像上工作呢?”

如你所预期,当完全训练好的神经网络应用于新图像时,它只有在新的图像几乎与训练数据中的图像完全相同的情况下才能保证表现良好。为什么?因为神经网络学会了将输入数据转换为输出数据,只针对非常特定的输入配置。如果你给它一些看起来不熟悉的东西,它将随机预测。

这使得神经网络变得有点没有意义。一个只在你训练它的数据上工作的神经网络的目的是什么?你已经知道这些数据点的正确分类。神经网络只有在处理你不知道答案的数据时才有用。

事实上,有一种方法可以对抗这种情况。在这里,我已经打印出了神经网络在训练过程中(每 10 次迭代)的训练测试准确率。你注意到什么有趣的东西了吗?你应该看到更好的网络的线索:

I:0 Train-Err:0.722 Train-Acc:0.537 Test-Err:0.601 Test-Acc:0.6488
I:10 Train-Err:0.312 Train-Acc:0.901 Test-Err:0.420 Test-Acc:0.8114
I:20 Train-Err:0.260 Train-Acc:0.93 Test-Err:0.414 Test-Acc:0.8111
I:30 Train-Err:0.232 Train-Acc:0.946 Test-Err:0.417 Test-Acc:0.8066
I:40 Train-Err:0.215 Train-Acc:0.956 Test-Err:0.426 Test-Acc:0.8019
I:50 Train-Err:0.204 Train-Acc:0.966 Test-Err:0.437 Test-Acc:0.7982
I:60 Train-Err:0.194 Train-Acc:0.967 Test-Err:0.448 Test-Acc:0.7921
I:70 Train-Err:0.186 Train-Acc:0.975 Test-Err:0.458 Test-Acc:0.7864
I:80 Train-Err:0.179 Train-Acc:0.979 Test-Err:0.466 Test-Acc:0.7817
I:90 Train-Err:0.172 Train-Acc:0.981 Test-Err:0.474 Test-Acc:0.7758
I:100 Train-Err:0.166 Train-Acc:0.984 Test-Err:0.482 Test-Acc:0.7706
I:110 Train-Err:0.161 Train-Acc:0.984 Test-Err:0.489 Test-Acc:0.7686
I:120 Train-Err:0.157 Train-Acc:0.986 Test-Err:0.496 Test-Acc:0.766
I:130 Train-Err:0.153 Train-Acc:0.99 Test-Err:0.502 Test-Acc:0.7622
I:140 Train-Err:0.149 Train-Acc:0.991 Test-Err:0.508 Test-Acc:0.758
                                ....
I:210 Train-Err:0.127 Train-Acc:0.998 Test-Err:0.544 Test-Acc:0.7446
I:220 Train-Err:0.125 Train-Acc:0.998 Test-Err:0.552 Test-Acc:0.7416
I:230 Train-Err:0.123 Train-Acc:0.998 Test-Err:0.560 Test-Acc:0.7372
I:240 Train-Err:0.121 Train-Acc:0.998 Test-Err:0.569 Test-Acc:0.7344
I:250 Train-Err:0.120 Train-Acc:0.999 Test-Err:0.577 Test-Acc:0.7316
I:260 Train-Err:0.118 Train-Acc:0.999 Test-Err:0.585 Test-Acc:0.729
I:270 Train-Err:0.117 Train-Acc:0.999 Test-Err:0.593 Test-Acc:0.7259
I:280 Train-Err:0.115 Train-Acc:0.999 Test-Err:0.600 Test-Acc:0.723
I:290 Train-Err:0.114 Train-Acc:0.999 Test-Err:0.607 Test-Acc:0.7196
I:300 Train-Err:0.113 Train-Acc:0.999 Test-Err:0.614 Test-Acc:0.7183
I:310 Train-Err:0.112 Train-Acc:0.999 Test-Err:0.622 Test-Acc:0.7165
I:320 Train-Err:0.111 Train-Acc:0.999 Test-Err:0.629 Test-Acc:0.7133
I:330 Train-Err:0.110 Train-Acc:0.999 Test-Err:0.637 Test-Acc:0.7125
I:340 Train-Err:0.109 Train-Acc:1.0 Test-Err:0.645 Test-Acc:0.71
I:349 Train-Err:0.108 Train-Acc:1.0 Test-Err:0.653 Test-Acc:0.7073

神经网络中的过拟合

如果你过度训练神经网络,它可能会变得更差!

由于某种原因,测试准确率在前 20 次迭代中上升,然后在网络训练得越来越多时(在此期间训练准确率仍在提高)缓慢下降。这在神经网络中很常见。让我通过一个类比来解释这个现象。

想象你正在为一个常见的餐叉制作模具,但不是用它来制作其他叉子,而是想用它来识别某个特定的餐具是否是叉子。如果一个物体能放入模具,你就会得出结论说这个物体是叉子,如果不能,你就会得出结论说它不是叉子。

假设你开始制作这个模具,你从一个湿的粘土块和一个装满三叉叉子、勺子和刀的大桶开始。然后你反复将所有叉子压入模具的同一位置以创建一个轮廓,这有点像一团糊状的叉子。你反复将所有叉子放入粘土中,数百次。当你让粘土干燥后,你会发现没有勺子或刀能放入这个模具,但所有叉子都能。太棒了!你做到了。你正确地制作了一个只能适合叉子形状的模具。

但如果有人给你一个四叉叉子会发生什么?你看看你的模具,注意到粘土中有一个特定的三细叉轮廓。四叉叉子不合适。为什么?它仍然是一个叉子。

这是因为粘土没有在四叉叉子上塑形。它只塑形了三叉叉子。这样,粘土就过度拟合了,只能识别它“训练”过的形状类型。

这正是你刚才在神经网络中看到的相同现象。这甚至比你想象的更接近。一种看待神经网络权重的方法是将其视为一个高维形状。随着训练的进行,这个形状塑造着数据的形状,学习区分不同的模式。不幸的是,测试数据集中的图像与训练数据集中的模式略有不同。这导致网络在许多测试示例上失败。

一个更正式的过度拟合神经网络的定义是:一个神经网络学会了数据集中的噪声,而不是仅基于真实信号做出决策。

过度拟合的来源

什么导致了神经网络过度拟合?

让我们稍微改变一下这个场景。再次想象那块新鲜的粘土(未成型的)。如果你只把一个叉子压进去会怎样?假设粘土非常厚,它不会有之前模具(印制多次)那么多的细节。因此,它只会是一个非常一般的叉子形状。这个形状可能与三叉和四叉的叉子都兼容,因为它仍然是一个模糊的印痕。

假设这个信息,随着你印制更多的叉子,模具在测试数据集上变得更差,因为它学到了更多关于训练数据集的详细信息。这导致它拒绝那些与它在训练数据中反复看到的哪怕是一点点不同的图像。

这些图像中的详细信息是什么,与测试数据不兼容?在分叉的类比中,它就是叉子上的叉数。在图像中,这通常被称为噪声。在现实中,它要复杂一些。考虑这两张狗的照片。

任何使这些照片在捕捉“狗”的本质之外变得独特的东西都包含在噪声这个术语中。在左边的图片中,枕头和背景都是噪声。在右边的图片中,狗中间的空黑部分也是一种噪声。实际上,是边缘告诉你这是一只狗;中间的黑色区域并没有告诉你任何东西。在左边的图片中,狗的中间部分有狗的毛茸茸的质感和颜色,这有助于分类器正确地识别它。

你如何让神经网络只训练在信号(狗的本质)上,而忽略噪声(与分类无关的其他东西)?一种方法就是提前停止。结果证明,大量的噪声都存在于图像的细粒度细节中,而大部分的信号(对于物体)都存在于图像的一般形状和可能的颜色中。

最简单的正则化:提前停止

当网络开始变差时停止训练

你如何让神经网络忽略细粒度细节,只捕捉数据中存在的通用信息(如狗的一般形状或 MNIST 数字的一般形状)?你不让网络训练足够长的时间来学习它。

在分叉模具的例子中,需要多次印制许多叉子才能创造出三叉叉的完美轮廓。最初几次的印制通常只能捕捉到叉子的浅轮廓。对于神经网络来说也是如此。因此,提前停止是成本最低的正则化形式,如果你处于困境中,它可能非常有效。

这把我们带到了本章的主题:正则化。正则化是模型对新数据点泛化的方法的一个子领域(而不是仅仅记住训练数据)。它是帮助神经网络学习信号并忽略噪声的方法的一个子集。在这种情况下,它是一套你可以使用的工具,以创建具有这些特性的神经网络。

正则化

正则化是用于鼓励学习模型泛化的方法的一个子集,通常通过增加模型学习训练数据细粒度细节的难度来实现。

接下来的问题可能是,你如何知道何时停止?唯一真正知道的方法是将模型运行在不在训练数据集中的数据上。这通常是通过使用第二个测试数据集,称为验证集来完成的。在某些情况下,如果你使用测试集来决定何时停止,你可能会对测试集过拟合。一般来说,你不使用它来控制训练。相反,你使用验证集。

行业标准正则化:Dropout

方法:在训练过程中随机关闭神经元(将它们设置为 0)

这种正则化技术听起来很简单。在训练过程中,你随机将网络中的神经元设置为 0(通常是在反向传播期间同一节点的 delta,但技术上你不必这样做)。这导致神经网络仅使用神经网络的随机子集进行训练。

信不信由你,这种正则化技术通常被广泛接受为大多数网络的首选、最先进的正则化技术。其方法简单且成本低廉,尽管它背后的为什么它有效的原因要复杂一些。

为什么 Dropout 有效(可能过于简化

Dropout 通过随机训练网络的小部分子集,每次只训练一小部分,使得大网络表现得像一个小网络,而小网络不会过拟合。

结果表明,神经网络越小,它越不容易过拟合。为什么?好吧,小神经网络没有太多的表达能力。它们不能抓住更细粒度的细节(噪声),这些细节往往是过拟合的来源。它们只能捕捉到大的、明显的、高级特征。

这个关于空间容量的概念非常重要,需要牢记在心。可以这样想。还记得粘土的比喻吗?想象一下,如果粘土是由像一角硬币大小的粘土石头制成的,那么这种粘土能够做出一个好的叉子印吗?当然不能。这些石头就像权重。它们围绕着数据形成,捕捉你感兴趣的模式。如果你只有几个较大的石头,它们就不能捕捉细微的细节。每个石头基本上是由叉子的大部分推动,或多或少地平均形状(忽略细小的褶皱和角落)。

现在,想象一下由非常细小的沙子制成的粘土。它由数百万个小石头组成,可以填充叉子的每一个角落。这就是大神经网络通常用来对数据集过拟合的表达能力。

如何获得大神经网络的强大能力,同时具有小神经网络的抗过拟合能力?将大神经网络中的节点随机关闭。当你将一个大神经网络只使用其中的一小部分时,它会表现得像一个小神经网络。但是,当你随机地在数百万个不同的子网络中这样做时,整个网络的总体表达能力仍然保持不变。这不是很酷吗?

Dropout 为什么有效:集成工作

Dropout 是一种训练多个网络并取平均的方法

需要记住的是:神经网络总是从随机状态开始。这有什么关系呢?因为神经网络通过试错来学习,这最终意味着每个神经网络的学习可能同样有效,但没有任何两个神经网络是完全相同的(除非它们由于某种随机或故意的原因从完全相同的状态开始)。

这有一个有趣的特点。当你过拟合两个神经网络时,没有两个神经网络会以完全相同的方式过拟合。过拟合只会持续到每个训练图像都可以被完美预测,此时错误率等于 0,网络停止学习(即使你继续迭代)。但由于每个神经网络都是从随机预测开始,然后调整其权重以做出更好的预测,因此每个网络不可避免地会犯不同的错误,导致不同的更新。这最终导致一个核心概念:

虽然大型、未正则化的神经网络可能会过拟合噪声,但它们不太可能过拟合到相同的噪声。

为什么它们不会过拟合到相同的噪声呢?因为它们是从随机状态开始的,一旦它们学会了足够多的噪声来区分训练集中的所有图像,就会停止训练。MNIST 网络只需要找到几个随机像素,这些像素恰好与输出标签相关联,以实现过拟合。但与此相对的是,也许一个更加重要的概念:

神经网络,尽管它们是随机生成的,但仍然首先学习最大的、最广泛的特点,然后再学习关于噪声的更多内容。

吸取的教训是:如果你训练 100 个神经网络(所有初始化都是随机的),它们各自倾向于锁定不同的噪声,但具有相似的广泛信号。因此,当它们犯错时,它们通常会犯不同的错误。如果你允许它们平等投票,它们的噪声往往会相互抵消,只揭示它们共同学习的内容:信号

Dropout 在代码中的应用

这里是如何在现实世界中使用 dropout 的

在 MNIST 分类模型中,让我们在隐藏层中添加 dropout,这样在训练过程中将有 50%的节点被随机关闭。你可能惊讶地发现这只是在代码中做了三行改动。以下是来自之前神经网络逻辑的一个熟悉的片段,其中添加了 dropout 掩码:

i = 0
layer_0 = images[i:i+1]
dropout_mask = np.random.randint(2,size=layer_1.shape)

layer_1 *= dropout_mask * 2
layer_2 = np.dot(layer_1, weights_1_2)

error += np.sum((labels[i:i+1] - layer_2) ** 2)

correct_cnt += int(np.argmax(layer_2) == \
             np.argmax(labels[i+i+1]))

layer_2_delta = (labels[i:i+1] - layer_2)
layer_1_delta = layer_2_delta.dot(weights_1_2.T)\
             * relu2deriv(layer_1)

layer_1_delta *= dropout_mask

weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)

要在层(在这种情况下,layer_1)上实现 dropout,将layer_1的值乘以一个由 1s 和 0s 组成的随机矩阵。这会随机关闭layer_1中的节点,将它们设置为等于 0。请注意,dropout_mask使用所谓的50%伯努利分布,这意味着 50%的时间,dropout_mask中的每个值都是 1,而(1 - 50% = 50%)的时间,它是 0。

接下来是可能显得有些奇特的事情。你将layer_1乘以 2。你为什么要这样做?记住,layer_2将对layer_1执行加权求和。尽管它是加权的,但它仍然是对layer_1的值的求和。如果你关闭layer_1中一半的节点,这个和将减半。因此,layer_2将增加对layer_1的敏感性,有点像当音量太低而听不清楚时,一个人会靠近收音机。但在测试时间,当你不再使用 dropout 时,音量会恢复到正常。这会干扰layer_2监听layer_1的能力。你需要通过将layer_1乘以(1 / 打开的节点百分比)来对此进行对抗。在这种情况下,那就是 1/0.5,等于 2。这样,layer_1在训练和测试时的音量是相同的,尽管有 dropout。

import numpy, sys
np.random.seed(1)
def relu(x):
   return (x >= 0) * x      *1*

def relu2deriv(output):
   return output >= 0       *2*

alpha, iterations, hidden_size = (0.005, 300, 100)
pixels_per_image, num_labels = (784, 10)

weights_0_1 = 0.2*np.random.random((pixels_per_image,hidden_size)) - 0.1
weights_1_2 = 0.2*np.random.random((hidden_size,num_labels)) - 0.1

for j in range(iterations):
   error, correct_cnt = (0.0,0)
   for i in range(len(images)):
      layer_0 = images[i:i+1]
      layer_1 = relu(np.dot(layer_0,weights_0_1))
      dropout_mask = np.random.randint(2, size=layer_1.shape)
      layer_1 *= dropout_mask * 2
      layer_2 = np.dot(layer_1,weights_1_2)

      error += np.sum((labels[i:i+1] - layer_2) ** 2)
      correct_cnt += int(np.argmax(layer_2) == \
                                      np.argmax(labels[i:i+1]))
      layer_2_delta = (labels[i:i+1] - layer_2)
      layer_1_delta = layer_2_delta.dot(weights_1_2.T) * relu2deriv(layer_1)
      layer_1_delta *= dropout_mask

      weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
      weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)

   if(j%10 == 0):
      test_error = 0.0
      test_correct_cnt = 0

      for i in range(len(test_images)):
           layer_0 = test_images[i:i+1]
           layer_1 = relu(np.dot(layer_0,weights_0_1))
           layer_2 = np.dot(layer_1, weights_1_2)

           test_error += np.sum((test_labels[i:i+1] - layer_2) ** 2)
           test_correct_cnt += int(np.argmax(layer_2) == \
                                     np.argmax(test_labels[i:i+1]))

      sys.stdout.write("\n" + \
           "I:" + str(j) + \
           " Test-Err:" + str(test_error/ float(len(test_images)))[0:5] +\
           " Test-Acc:" + str(test_correct_cnt/ float(len(test_images)))+\
           " Train-Err:" + str(error/ float(len(images)))[0:5] +\
           " Train-Acc:" + str(correct_cnt/ float(len(images))))
  • 1 如果 x 大于 0 则返回 x;否则返回 0

  • 2 对于输入大于 0 的情况返回 1

在 MNIST 上评估 Dropout

如果你记得之前的内容,没有 dropout 的神经网络之前在测试准确率达到 81.14%后下降到 70.73%的准确率完成训练。当你添加 dropout 时,神经网络反而表现出这种行为:

I:0 Test-Err:0.641 Test-Acc:0.6333 Train-Err:0.891 Train-Acc:0.413
I:10 Test-Err:0.458 Test-Acc:0.787 Train-Err:0.472 Train-Acc:0.764
I:20 Test-Err:0.415 Test-Acc:0.8133 Train-Err:0.430 Train-Acc:0.809
I:30 Test-Err:0.421 Test-Acc:0.8114 Train-Err:0.415 Train-Acc:0.811
I:40 Test-Err:0.419 Test-Acc:0.8112 Train-Err:0.413 Train-Acc:0.827
I:50 Test-Err:0.409 Test-Acc:0.8133 Train-Err:0.392 Train-Acc:0.836
I:60 Test-Err:0.412 Test-Acc:0.8236 Train-Err:0.402 Train-Acc:0.836
I:70 Test-Err:0.412 Test-Acc:0.8033 Train-Err:0.383 Train-Acc:0.857
I:80 Test-Err:0.410 Test-Acc:0.8054 Train-Err:0.386 Train-Acc:0.854
I:90 Test-Err:0.411 Test-Acc:0.8144 Train-Err:0.376 Train-Acc:0.868
I:100 Test-Err:0.411 Test-Acc:0.7903 Train-Err:0.369 Train-Acc:0.864
I:110 Test-Err:0.411 Test-Acc:0.8003 Train-Err:0.371 Train-Acc:0.868
I:120 Test-Err:0.402 Test-Acc:0.8046 Train-Err:0.353 Train-Acc:0.857
I:130 Test-Err:0.408 Test-Acc:0.8091 Train-Err:0.352 Train-Acc:0.867
I:140 Test-Err:0.405 Test-Acc:0.8083 Train-Err:0.355 Train-Acc:0.885
I:150 Test-Err:0.404 Test-Acc:0.8107 Train-Err:0.342 Train-Acc:0.883
I:160 Test-Err:0.399 Test-Acc:0.8146 Train-Err:0.361 Train-Acc:0.876
I:170 Test-Err:0.404 Test-Acc:0.8074 Train-Err:0.344 Train-Acc:0.889
I:180 Test-Err:0.399 Test-Acc:0.807 Train-Err:0.333 Train-Acc:0.892
I:190 Test-Err:0.407 Test-Acc:0.8066 Train-Err:0.335 Train-Acc:0.898
I:200 Test-Err:0.405 Test-Acc:0.8036 Train-Err:0.347 Train-Acc:0.893
I:210 Test-Err:0.405 Test-Acc:0.8034 Train-Err:0.336 Train-Acc:0.894
I:220 Test-Err:0.402 Test-Acc:0.8067 Train-Err:0.325 Train-Acc:0.896
I:230 Test-Err:0.404 Test-Acc:0.8091 Train-Err:0.321 Train-Acc:0.894
I:240 Test-Err:0.415 Test-Acc:0.8091 Train-Err:0.332 Train-Acc:0.898
I:250 Test-Err:0.395 Test-Acc:0.8182 Train-Err:0.320 Train-Acc:0.899
I:260 Test-Err:0.390 Test-Acc:0.8204 Train-Err:0.321 Train-Acc:0.899
I:270 Test-Err:0.382 Test-Acc:0.8194 Train-Err:0.312 Train-Acc:0.906
I:280 Test-Err:0.396 Test-Acc:0.8208 Train-Err:0.317 Train-Acc:0.9
I:290 Test-Err:0.399 Test-Acc:0.8181 Train-Err:0.301 Train-Acc:0.908

不仅网络在得分 82.36%时达到峰值,它也没有那么严重地过拟合,最终以 81.81%的测试准确率完成训练。注意,dropout 也减缓了Training-Acc的上升速度,之前它直接升到 100%并保持在那里。

这应该指向 dropout 真正是什么:它是噪声。它使网络在训练数据上训练变得更加困难。这就像在腿上绑着重物跑马拉松。训练起来更难,但当你为一场难度更大的比赛脱下重物时,你最终会跑得相当快,因为你训练的是一件更困难的事情。

批量梯度下降

这里有一种提高训练速度和收敛率的方法

在本章的背景下,我想简要应用几章前引入的一个概念:小批量随机梯度下降。我不会过多地详细介绍,因为这主要是神经网络训练中被默认接受的东西。此外,它是一个简单的概念,即使是最先进的神经网络也不会变得更加复杂。

之前我们一次训练一个训练示例,在每个示例之后更新权重。现在,让我们一次训练 100 个训练示例,对所有 100 个示例的平均权重更新进行平均。接下来显示训练/测试输出,然后是训练逻辑的代码。

I:0 Test-Err:0.815 Test-Acc:0.3832 Train-Err:1.284 Train-Acc:0.165
I:10 Test-Err:0.568 Test-Acc:0.7173 Train-Err:0.591 Train-Acc:0.672
I:20 Test-Err:0.510 Test-Acc:0.7571 Train-Err:0.532 Train-Acc:0.729
I:30 Test-Err:0.485 Test-Acc:0.7793 Train-Err:0.498 Train-Acc:0.754
I:40 Test-Err:0.468 Test-Acc:0.7877 Train-Err:0.489 Train-Acc:0.749
I:50 Test-Err:0.458 Test-Acc:0.793 Train-Err:0.468 Train-Acc:0.775
I:60 Test-Err:0.452 Test-Acc:0.7995 Train-Err:0.452 Train-Acc:0.799
I:70 Test-Err:0.446 Test-Acc:0.803 Train-Err:0.453 Train-Acc:0.792
I:80 Test-Err:0.451 Test-Acc:0.7968 Train-Err:0.457 Train-Acc:0.786
I:90 Test-Err:0.447 Test-Acc:0.795 Train-Err:0.454 Train-Acc:0.799
I:100 Test-Err:0.448 Test-Acc:0.793 Train-Err:0.447 Train-Acc:0.796
I:110 Test-Err:0.441 Test-Acc:0.7943 Train-Err:0.426 Train-Acc:0.816
I:120 Test-Err:0.442 Test-Acc:0.7966 Train-Err:0.431 Train-Acc:0.813
I:130 Test-Err:0.441 Test-Acc:0.7906 Train-Err:0.434 Train-Acc:0.816
I:140 Test-Err:0.447 Test-Acc:0.7874 Train-Err:0.437 Train-Acc:0.822
I:150 Test-Err:0.443 Test-Acc:0.7899 Train-Err:0.414 Train-Acc:0.823
I:160 Test-Err:0.438 Test-Acc:0.797 Train-Err:0.427 Train-Acc:0.811
I:170 Test-Err:0.440 Test-Acc:0.7884 Train-Err:0.418 Train-Acc:0.828
I:180 Test-Err:0.436 Test-Acc:0.7935 Train-Err:0.407 Train-Acc:0.834
I:190 Test-Err:0.434 Test-Acc:0.7935 Train-Err:0.410 Train-Acc:0.831
I:200 Test-Err:0.435 Test-Acc:0.7972 Train-Err:0.416 Train-Acc:0.829
I:210 Test-Err:0.434 Test-Acc:0.7923 Train-Err:0.409 Train-Acc:0.83
I:220 Test-Err:0.433 Test-Acc:0.8032 Train-Err:0.396 Train-Acc:0.832
I:230 Test-Err:0.431 Test-Acc:0.8036 Train-Err:0.393 Train-Acc:0.853
I:240 Test-Err:0.430 Test-Acc:0.8047 Train-Err:0.397 Train-Acc:0.844
I:250 Test-Err:0.429 Test-Acc:0.8028 Train-Err:0.386 Train-Acc:0.843
I:260 Test-Err:0.431 Test-Acc:0.8038 Train-Err:0.394 Train-Acc:0.843
I:270 Test-Err:0.428 Test-Acc:0.8014 Train-Err:0.384 Train-Acc:0.845
I:280 Test-Err:0.430 Test-Acc:0.8067 Train-Err:0.401 Train-Acc:0.846
I:290 Test-Err:0.428 Test-Acc:0.7975 Train-Err:0.383 Train-Acc:0.851

注意,训练准确率比之前更加平滑。持续进行平均权重更新会在训练过程中产生这种现象。实际上,单个训练示例在生成的权重更新方面非常嘈杂。因此,平均它们会使学习过程更加平滑。

import numpy as np
np.random.seed(1)

def relu(x):
    return (x >= 0) * x       *1*

def relu2deriv(output):
    return output >= 0        *2*

batch_size = 100
alpha, iterations = (0.001, 300)
pixels_per_image, num_labels, hidden_size = (784, 10, 100)

weights_0_1 = 0.2*np.random.random((pixels_per_image,hidden_size)) - 0.1
weights_1_2 = 0.2*np.random.random((hidden_size,num_labels)) - 0.1

for j in range(iterations):
    error, correct_cnt = (0.0, 0)
    for i in range(int(len(images) / batch_size)):
        batch_start, batch_end = ((i * batch_size),((i+1)*batch_size))

        layer_0 = images[batch_start:batch_end]
        layer_1 = relu(np.dot(layer_0,weights_0_1))
        dropout_mask = np.random.randint(2,size=layer_1.shape)
        layer_1 *= dropout_mask * 2
        layer_2 = np.dot(layer_1,weights_1_2)

        error += np.sum((labels[batch_start:batch_end] - layer_2) ** 2)
        for k in range(batch_size):
            correct_cnt += int(np.argmax(layer_2[k:k+1]) == \
                    np.argmax(labels[batch_start+k:batch_start+k+1]))

            layer_2_delta = (labels[batch_start:batch_end]-layer_2) \
                                                            /batch_size
            layer_1_delta = layer_2_delta.dot(weights_1_2.T)* \
                                                     relu2deriv(layer_1)
            layer_1_delta *= dropout_mask

            weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
            weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)

    if(j%10 == 0):
        test_error = 0.0
        test_correct_cnt = 0

        for i in range(len(test_images)):
            layer_0 = test_images[i:i+1]
            layer_1 = relu(np.dot(layer_0,weights_0_1))
            layer_2 = np.dot(layer_1, weights_1_2)
  • 1 如果 x 大于 0 则返回 x

  • 2 当输入大于 0 时返回 1

当你运行这段代码时,你首先会注意到它运行得更快。这是因为每个np.dot函数现在一次执行 100 个向量点积。CPU 架构以这种方式批量执行点积要快得多。

然而,这里还有更多的事情发生。请注意,alpha现在比之前大 20 倍。你可以出于一个有趣的原因增加它。想象一下,如果你试图使用一个非常不稳定的指南针找到一个城市。如果你低头看了一下,得到了一个航向,然后跑了 2 英里,你很可能会偏离航线。但如果你低头看了 100 次航向,然后取平均值,跑 2 英里可能会让你大致朝正确的方向前进。

因为这个例子取了一个噪声信号的平均值(100 个训练示例中的平均权重变化),它可以采取更大的步长。你通常会看到批量大小从 8 到高达 256。通常,研究人员会随机选择数字,直到他们找到一个似乎工作良好的batch_size/alpha对。

摘要

本章讨论了两种几乎适用于任何神经网络架构以提高准确性和训练速度的最常用方法。在接下来的章节中,我们将从适用于几乎所有神经网络的通用工具集转向针对特定类型现象建模的有利架构。

第九章:建模概率和非线性:激活函数

在本章中

什么是激活函数?

标准隐藏激活函数

  • Sigmoid

  • Tanh

标准输出激活函数

  • Softmax

激活函数安装说明

“我知道 2 和 2 等于 4——如果我能证明这一点,我也会很高兴——但我必须说,如果通过某种方式我能把 2 和 2 变成 5,那会给我带来更大的快乐。”

乔治·戈登·拜伦,致安 abella Milbanke 的信,1813 年 11 月 10 日

什么是激活函数?

它是在预测期间应用于层的神经元的函数

激活函数是在预测期间应用于层的神经元的函数。这应该看起来非常熟悉,因为你一直在使用一个名为relu的激活函数(如这里所示的三层神经网络)。relu函数的作用是将所有负数转换为 0。

简单来说,激活函数是任何可以取一个数字并返回另一个数字的函数。但是,宇宙中有无限多的函数,并不是所有的函数都适合作为激活函数。

激活函数有几个约束条件。使用这些约束之外的函数通常是个坏主意,你很快就会看到。

图片

约束 1:函数必须是连续的,并且在定义域内是无限的

正确激活函数的第一个约束是它必须对任何输入都有一个输出数字。换句话说,你不应该能够输入一个没有输出的数字。

稍显过度,但看看左侧的函数(四条不同的线)是否没有为每个x值定义y值?它只在四个点上定义。这将是一个糟糕的激活函数。然而,右侧的函数是连续的,并且在定义域内是无限的。对于任何输入(x),你都可以计算出输出(y)。

图片

约束 2:好的激活函数是单调的,永远不会改变方向

第二个约束是函数必须是 1:1 的。它永远不能改变方向。换句话说,它必须始终增加或始终减少。

例如,看看以下两个函数。这些形状回答了“给定x作为输入,函数描述的y值是什么?”的问题。左侧的函数(y = x * x)不是一个理想的激活函数,因为它既不是始终增加也不是始终减少。

你怎么判断呢?注意,有许多情况下两个x的值对应一个y的值(除了 0 以外的所有值都如此)。然而,右侧的函数始终在增加!没有任何两个x的值会有相同的y值:

图片

这个特定的约束在技术上并不是一个要求。与有缺失值的函数(非连续的)不同,你可以优化非单调的函数。但考虑多个输入值映射到相同输出值的含义。

当你在神经网络中学习时,你正在寻找正确的权重配置以产生特定的输出。如果有多个正确答案,这个问题可能会变得更加困难。如果有多种方式可以得到相同的输出,那么网络就有多个可能的完美配置。

一个乐观主义者可能会说:“嘿,这太好了!如果答案可以在多个地方找到,那么你更有可能找到正确的答案!”而一个悲观主义者可能会说:“这太糟糕了!现在你没有正确的方向去减少错误,因为你可以在任何方向上前进,理论上都可以取得进步。”

不幸的是,悲观主义者所识别的现象更为重要。对于这个主题的高级研究,更多地了解凸优化与非凸优化;许多大学(和在线课程)都有专门针对这些问题的课程。

约束 3:好的激活函数是非线性的(它们会弯曲或转向)

第三个约束需要回顾一下第六章。还记得“有时相关性”吗?为了创建它,你必须允许神经元选择性地与输入神经元相关联,使得一个来自一个输入到神经元的非常负的信号可以减少它与任何输入的相关性(在relu的情况下,通过迫使神经元降至 0)。

实际上,这种现象是由任何曲线函数促进的。另一方面,看起来像直线的函数会放大进入的加权平均值。放大某物(乘以一个常数,如 2)不会影响神经元与其各种输入的相关性。它会使表示的集体相关性更响亮或更微弱。但激活函数不允许一个权重影响神经元与其他权重的相关性。你真正想要的是“选择性”的相关性。给定一个具有激活函数的神经元,你希望一个进入的信号能够增加或减少神经元与所有其他进入信号的相关性。所有曲线都做到了这一点(程度不同,你将会看到)。

因此,这里左边显示的函数被认为是线性函数,而右边的是非线性的,通常会成为更好的激活函数(尽管有例外,我们稍后会讨论)。

图片

约束 4:好的激活函数(及其导数)应该能够高效地计算

这个很简单。你将频繁地调用这个函数(有时是数十亿次),所以你不想让它计算得太慢。许多最近的激活函数之所以受欢迎,是因为它们易于计算,尽管牺牲了它们的表达能力(relu就是这样一个很好的例子)。

标准隐藏层激活函数

在无限可能的功能中,哪些是最常用的?

即使有这些限制,也应该清楚,可以使用无限(可能还是超越无限的?)数量的函数作为激活函数。在过去的几年里,最先进的激活函数取得了很大的进展。但仍然只有相对较少的激活函数能够满足大部分激活需求,而且大多数情况下对它们的改进都是微小的。

sigmoid 是最基本的激活函数

sigmoid很棒,因为它可以平滑地将无限多的输入压缩到 0 和 1 之间的输出。在许多情况下,这让你可以解释任何单个神经元的输出作为一个概率。因此,人们既在隐藏层也在输出层使用这种非线性。

图片

tanh 对于隐藏层比 sigmoid 更好

关于tanh的酷之处在于。还记得建模选择性相关性吗?好吧,sigmoid提供不同程度的正相关性。这很好。tanhsigmoid相同,只是它的范围在-1 和 1 之间!

这意味着它也可以引入一些负相关性。尽管它对输出层(除非你预测的数据在-1 和 1 之间)并不是特别有用,但负相关性的这一方面在隐藏层中非常强大;在许多问题上,tanh在隐藏层中会比sigmoid表现得更好。

图片

标准输出层激活函数

选择最好的一个取决于你试图预测什么

结果表明,对隐藏层激活函数的最佳选择可能与输出层激活函数的最佳选择大不相同,尤其是在分类方面。总的来说,输出层有三种主要类型。

配置 1:预测原始数据值(无激活函数)

这可能是最直接但最不常见的一种输出层。在某些情况下,人们希望训练一个神经网络将一个数字矩阵转换成另一个数字矩阵,其中输出的范围(最低值和最高值之间的差异)不是概率。一个例子可能是根据周围州的温度预测科罗拉多州的平均温度。

这里要关注的主要是确保输出非线性可以预测正确的答案。在这种情况下,sigmoidtanh 会被认为是不合适的,因为它强制每个预测都在 0 和 1 之间(你想要预测任何温度,而不仅仅是 0 到 1 之间的温度)。如果我要训练一个网络来做这个预测,我非常可能会在没有输出激活函数的情况下训练网络。

配置 2:预测无关的 yes/no 概率(sigmoid)

你通常希望在一个神经网络中做出多个二元概率。我们在第五章的“具有多个输入和输出的梯度下降”部分中这样做过,根据输入数据预测球队是否会赢,是否有伤害,以及球队的士气(快乐或悲伤)。

作为旁白,当一个神经网络有隐藏层时,同时预测多个事物可能是有益的。通常,当网络预测一个标签时,它会学到一些对其他标签有用的东西。例如,如果网络在预测球队是否会赢得球赛方面非常出色,那么相同的隐藏层很可能对预测球队是否会高兴或悲伤也非常有用。但是,如果没有这个额外信号,网络可能很难预测快乐或悲伤。这通常因问题而异很大,但值得记住。

在这些情况下,最好使用 sigmoid 激活函数,因为它为每个输出节点分别建模单独的概率。

配置 3:预测哪个概率(softmax)

在神经网络中,最常见的情况是预测许多标签中的一个。例如,在 MNIST 数字分类器中,你想要预测图像中是哪个数字。你知道提前图像不可能包含超过一个数字。你可以用 sigmoid 激活函数训练这个网络,并声明最高的输出概率是最可能的。这会工作得相当好。但有一个激活函数来模拟“越有可能是一个标签,其他标签的可能性就越小”的想法会更好。

我们为什么喜欢这种现象?考虑权重更新的方式。假设 MNIST 数字分类器应该预测图像是 9。也假设原始加权总和进入最终层(在应用激活函数之前)的值如下:

图片

网络的原始输入到最后一层预测除了节点 9 以外的每个节点为 0,而节点 9 预测为 100。你可能称之为完美。让我们看看当这些数字通过 sigmoid 激活函数运行时会发生什么:

图片

奇怪的是,网络现在似乎不太确定:9 仍然是最高,但网络似乎认为有 50%的可能性它可能是其他任何数字。真奇怪!另一方面,softmax对输入的解释非常不同:

图片

这看起来很棒。不仅 9 是最高的,而且网络甚至没有怀疑它可能是其他任何可能的 MNIST 数字。这看起来可能像是sigmoid的理论缺陷,但当你反向传播时,它可能会有严重的后果。考虑在sigmoid输出上如何计算均方误差。从理论上讲,网络预测得几乎完美,对吧?当然,它不会反向传播很多错误。但sigmoid不是这样:

图片

看看所有的错误!尽管网络预测得完美,但这些权重将进行大规模的权重更新。为什么?为了sigmoid达到 0 错误,它不仅必须预测真实输出的最高正数;它还必须预测其他所有地方都是 0。而softmax问,“哪个数字似乎最适合这个输入?”sigmoid说,“你最好相信它只有数字 9,并且与其他 MNIST 数字没有共同之处。”

核心问题:输入具有相似性

不同的数字具有特征。让网络相信这是好事

MNIST 数字并不都是完全不同的:它们有重叠的像素值。平均的 2 与平均的 3 有很多共同之处。

为什么这很重要?嗯,作为一条一般规则,相似的输入会产生相似的输出。当你拿一些数字并将它们乘以一个矩阵时,如果起始数字很相似,那么结束数字也会很相似。

图片

考虑这里显示的 2 和 3。如果我们正向传播 2,并且不小心有一部分概率流向了标签 3,这对网络来说意味着什么,以至于它认为这是一个大错误并做出大的权重更新?它会惩罚网络,使其不能通过除了与 2s 独家相关的特征之外的其他任何特征来识别 2。它会惩罚网络,使其基于,比如说,顶部曲线来识别 2。为什么?因为 2 和 3 在图像顶部的曲线是相同的。使用sigmoid训练会惩罚网络尝试根据这个输入预测 2,因为这样做它就会寻找与 3s 相同的输入。因此,当出现 3 时,2 标签会得到一些概率(因为图像的一部分看起来像 2)。

会有什么副作用?大多数图像在图像中间共享很多像素,所以网络会开始尝试专注于边缘。考虑右侧显示的 2 检测节点权重。

看看图像中间有多混乱?最重的权重是图像边缘 2 的端点。一方面,这些可能是 2 的最佳单个指标,但最好的整体是网络能够看到整个形状。这些单个指标可能会被稍微偏离中心或倾斜错误方向的 3 意外触发。网络没有学习到 2 的真正本质,因为它需要学习 2 而不是 1,不是 3,不是 4,等等。

图片

我们希望输出激活函数不会惩罚相似的标签。相反,我们希望它关注所有可能指示任何潜在输入的信息。而且,softmax的概率总和总是等于 1。你可以将任何单个预测解释为预测是特定标签的全局概率。softmax在理论和实践中都表现更好。

softmax计算

softmax将每个输入值进行指数化,然后除以层的总和

让我们看看之前神经网络假设输出值的softmax计算。我将再次在这里展示,这样你可以看到softmax的输入:

图片

要在整个层上计算softmax,首先将每个值进行指数化。对于每个值x,计算ex次幂(e是一个特殊的数,大约是 2.71828...)。e``^x的值显示在右侧。

图片

注意,它将每个预测转换成正数,其中负数变成了非常小的正数,而大数变成了非常大的数。(如果你听说过指数增长,那可能就是在谈论这个函数或一个非常相似的函数。)

图片

简而言之,所有的 0 都变成了 1(因为 1 是e^x的 y 截距),而 100 变成了一个巨大的数字(2 后面跟着 43 个 0)。如果有任何负数,它们变成了介于 0 和 1 之间的数。下一步是将这一层的所有节点相加,然后将这一层的每个值除以这个总和。这实际上使得除了标签 9 的值之外的所有数字都变成了 0。

图片

softmax的好处是,网络预测的某个值越高,它预测的其他值就越低。它增加了所谓的衰减的“尖锐度”。它鼓励网络以非常高的概率预测一个输出。

要调整它执行这一操作的积极性,在指数化时使用比e稍高或稍低的数字。较小的数字会导致较低的衰减,而较大的数字会导致较高的衰减。但大多数人只是坚持使用e

激活函数安装说明

你如何将你最喜欢的激活函数添加到任何层中?

现在我们已经涵盖了各种激活函数,并解释了它们在神经网络隐藏层和输出层中的有用性,让我们谈谈将激活函数正确安装到神经网络中的方法。幸运的是,你已经在你的第一个深度神经网络中看到了如何使用非线性的例子:你向隐藏层添加了relu激活函数。将其添加到正向传播相对简单。你取了layer_1将有的值(没有激活),并对每个值应用了relu函数:

layer_0 = images[i:i+1]
layer_1 = relu(np.dot(layer_0,weights_0_1))
layer_2 = np.dot(layer_1,weights_1_2)

这里有一些术语需要记住。层的输入指的是非线性之前的值。在这种情况下,layer_1的输入是np.dot(layer_0,weights_0_1)。这不要与前面的层layer_0混淆。

在正向传播中向层添加激活函数相对简单。但在反向传播中正确补偿激活函数则要微妙得多。

在第六章中,我们进行了一个有趣的操作来创建layer_1_delta变量。无论relu如何将layer_1的值强制设为 0,我们也会将delta乘以 0。当时的推理是,“因为layer_1的值为 0 对输出预测没有影响,它也不应该对权重更新有任何影响。它不负责错误。”这是更微妙属性的一种极端形式。考虑relu函数的形状。

对于正数,relu的斜率正好是 1。对于负数,relu的斜率正好是 0。修改这个函数的输入(通过一个非常小的量)如果它预测的是正值,将产生 1:1 的效果,如果它预测的是负值,将产生 0:1 的效果(没有效果)。这个斜率是衡量relu的输出在输入变化时将如何变化的一个指标。

由于此时delta的目的在于告诉前面的层“下次让我的输入更高或更低”,这个delta非常有用。它修改了从后续层反向传播回来的delta,以考虑这个节点是否对错误有贡献。

因此,在反向传播时,为了生成layer_1_delta,将来自layer_2的反向传播deltalayer_2_delta.dot(weights_1_2.T))乘以在正向传播中预测的点处的relu斜率。对于某些delta,斜率是 1(正值),而对于其他delta,斜率是 0(负值):

error += np.sum((labels[i:i+1] - layer_2) ** 2)

correct_cnt += int(np.argmax(layer_2) == \
                                np.argmax(labels[i:i+1]))

layer_2_delta = (labels[i:i+1] - layer_2)
layer_1_delta = layer_2_delta.dot(weights_1_2.T)\
                            * relu2deriv(layer_1)

weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)

def relu(x):
    return (x >= 0) * x       *1*

def relu2deriv(output):
    return output >= 0        *2*
  • 1 如果 x > 0,则返回 x;否则返回 0

  • 2 如果输入 > 0,则返回 1;否则返回 0

relu2deriv是一个特殊函数,它可以接受relu的输出并计算在该点relu的斜率(它对输出向量中的所有值都这样做)。这引发了一个问题,如何对其他不是relu的非线性进行调整?考虑relusigmoid

这些图中的重要之处在于斜率是输入的微小变化对输出影响程度的一个指标。你想要修改输入的delta(来自下一层),以考虑在此节点之前更新权重是否会有任何影响。记住,最终目标是调整权重以减少误差。这一步鼓励网络在调整权重将几乎没有效果的情况下保持权重不变。它是通过乘以斜率来实现的。对于sigmoid来说,也是如此。

将差分乘以斜率

要计算层差分(layer_delta),需要将反向传播的差分乘以层的斜率

layer_1_delta[0]表示为了减少网络(对于特定的训练示例)的误差,层 1 的第一个隐藏节点应该比现在高多少或低多少。当没有非线性时,这是layer_2的加权平均delta

图片

但神经元上delta的最终目标是通知权重它们是否应该移动。如果移动它们没有任何效果,它们(作为一个组)应该保持不变。对于relu来说,这是显而易见的,它要么开启要么关闭。sigmoid可能更为微妙。

图片

考虑一个单一的sigmoid神经元。当输入从任一方向接近 0 时,sigmoid对输入变化的敏感性会逐渐增加。但非常正的和非常负的输入接近斜率接近 0。因此,当输入变得非常正或非常负时,对输入权重的小幅变化对神经元在此训练示例中的误差变得不那么相关。更广泛地说,许多隐藏节点对于准确预测 2(可能它们只用于 8)是不相关的。你不应该过多地调整它们的权重,因为这样可能会损害它们在其他地方的有用性。

相反,这也产生了一种粘性的概念。那些在一个方向上(对于相似的训练示例)之前被大量更新的权重会自信地预测一个高值或低值。这些非线性特性有助于使偶尔的错误训练示例难以破坏多次被强化的智能。

将输出转换为斜率(导数)

大多数优秀的激活函数都能将它们的输出转换为斜率。(- (效率提升!)

现在你已经知道向层添加激活函数会改变该层的delta计算方式,让我们讨论一下行业是如何高效地做到这一点的。必要的新的操作是计算所使用的非线性的导数。

大多数非线性(所有流行的非线性)使用一种计算导数的方法,这可能会让熟悉微积分的你们感到惊讶。大多数优秀的激活函数不是以通常的方式在曲线上的某个点上计算导数,而是有一种方法,可以通过层的前向传播的 输出 来计算导数。这已经成为计算神经网络中导数的标准做法,并且非常方便。

以下是一个小型表格,列出了你迄今为止看到的函数及其导数。输入 是 NumPy 向量(对应于层的输入)。输出 是层的预测。导数 是对应于每个节点激活导数导数的向量导数。真实值 是真实值的向量(通常对于正确标签位置为 1,其他地方为 0)。

函数 前向传播 反向传播 delta
relu ones_and_zeros = (输入 > 0) 输出 = 输入 * ones_and_zeros mask = 输出 > 0 导数 = 输出 * mask
sigmoid 输出 = 1 / (1 + np.exp(-输入)) 导数 = 输出 * (1 - 输出)
tanh 输出 = np.tanh(输入) 导数 = 1 - (输出²)
softmax temp = np.exp(输入) 输出 /= np.sum(temp) temp = (输出 - 真实值) 输出 = temp / len(真实值)

注意,softmaxdelta 计算是特殊的,因为它只用于最后一层。理论上还有更多的事情发生,但我们没有时间在这里讨论。现在,让我们在 MNIST 分类网络中安装一些更好的激活函数。

升级 MNIST 网络

让我们将 MNIST 网络升级,以反映你所学的知识。

理论上,tanh 函数应该是一个更好的隐藏层激活函数,而 softmax 应该是一个更好的输出层激活函数。当我们测试它们时,它们确实达到了更高的分数。但事情并不总是像看起来那么简单。

为了正确调整这些新激活函数的网络,我不得不进行一些调整。对于 tanh,我必须减小输入权重的标准差。记住,你随机初始化权重。np.random.random 创建一个在 0 和 1 之间随机分布的随机矩阵。通过乘以 0.2 并减去 0.1,你将这个随机范围重新缩放为 -0.1 和 0.1 之间。这对于 relu 工作得很好,但对于 tanh 来说则不太理想。tanh 喜欢有一个更窄的随机初始化范围,所以我将其调整为 -0.01 和 0.01 之间。

我还移除了错误计算,因为我们还没有准备好进行这项工作。技术上讲,softmax 最好与一个称为 交叉熵 的错误函数一起使用。这个网络正确地计算了用于此误差测量的 layer_2_delta,但由于我们还没有分析为什么这个误差函数是有利的,所以我移除了计算它的代码。

最后,就像对神经网络所做的几乎所有改动一样,我不得不重新审视alpha调整。我发现为了在 300 次迭代内达到良好的分数,需要更高的alpha值。哇哦!正如预期的那样,网络达到了更高的测试准确率,达到了 87%。

import numpy as np, sys
np.random.seed(1)

from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

images, labels = (x_train[0:1000].reshape(1000,28*28)\
                                              / 255, y_train[0:1000])
one_hot_labels = np.zeros((len(labels),10))
for i,l in enumerate(labels):
    one_hot_labels[i][l] = 1
labels = one_hot_labels

test_images = x_test.reshape(len(x_test),28*28) / 255
test_labels = np.zeros((len(y_test),10))
for i,l in enumerate(y_test):
    test_labels[i][l] = 1

def tanh(x):
    return np.tanh(x)
def tanh2deriv(output):
    return 1 - (output ** 2)
def softmax(x):
    temp = np.exp(x)
    return temp / np.sum(temp, axis=1, keepdims=True)

alpha, iterations, hidden_size = (2, 300, 100)
pixels_per_image, num_labels = (784, 10)
batch_size = 100

weights_0_1 = 0.02*np.random.random((pixels_per_image,hidden_size))-0.01
weights_1_2 = 0.2*np.random.random((hidden_size,num_labels)) - 0.1

for j in range(iterations):
    correct_cnt = 0
    for i in range(int(len(images) / batch_size)):
        batch_start, batch_end=((i * batch_size),((i+1)*batch_size))
        layer_0 = images[batch_start:batch_end]
        layer_1 = tanh(np.dot(layer_0,weights_0_1))
        dropout_mask = np.random.randint(2,size=layer_1.shape)
        layer_1 *= dropout_mask * 2
        layer_2 = softmax(np.dot(layer_1,weights_1_2))

        for k in range(batch_size):
            correct_cnt += int(np.argmax(layer_2[k:k+1]) == \
                          np.argmax(labels[batch_start+k:batch_start+k+1]))
        layer_2_delta = (labels[batch_start:batch_end]-layer_2)\
                                           / (batch_size * layer_2.shape[0])
        layer_1_delta = layer_2_delta.dot(weights_1_2.T) \
                                                       * tanh2deriv(layer_1)
        layer_1_delta *= dropout_mask

        weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
        weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)
    test_correct_cnt = 0

    for i in range(len(test_images)):

        layer_0 = test_images[i:i+1]
        layer_1 = tanh(np.dot(layer_0,weights_0_1))
        layer_2 = np.dot(layer_1,weights_1_2)
        test_correct_cnt += int(np.argmax(layer_2) == \
                                               np.argmax(test_labels[i:i+1]))
    if(j % 10 == 0):
        sys.stdout.write("\n"+ "I:" + str(j) + \
         " Test-Acc:"+str(test_correct_cnt/float(len(test_images)))+\
         " Train-Acc:" + str(correct_cnt/float(len(images))))

I:0 Test-Acc:0.394 Train-Acc:0.156   I:150 Test-Acc:0.8555 Train-Acc:0.914
I:10 Test-Acc:0.6867 Train-Acc:0.723 I:160 Test-Acc:0.8577 Train-Acc:0.925
I:20 Test-Acc:0.7025 Train-Acc:0.732 I:170 Test-Acc:0.8596 Train-Acc:0.918
I:30 Test-Acc:0.734 Train-Acc:0.763  I:180 Test-Acc:0.8619 Train-Acc:0.933
I:40 Test-Acc:0.7663 Train-Acc:0.794 I:190 Test-Acc:0.863 Train-Acc:0.933
I:50 Test-Acc:0.7913 Train-Acc:0.819 I:200 Test-Acc:0.8642 Train-Acc:0.926
I:60 Test-Acc:0.8102 Train-Acc:0.849 I:210 Test-Acc:0.8653 Train-Acc:0.931
I:70 Test-Acc:0.8228 Train-Acc:0.864 I:220 Test-Acc:0.8668 Train-Acc:0.93
I:80 Test-Acc:0.831 Train-Acc:0.867  I:230 Test-Acc:0.8672 Train-Acc:0.937
I:90 Test-Acc:0.8364 Train-Acc:0.885 I:240 Test-Acc:0.8681 Train-Acc:0.938
I:100 Test-Acc:0.8407 Train-Acc:0.88 I:250 Test-Acc:0.8687 Train-Acc:0.937
I:110 Test-Acc:0.845 Train-Acc:0.891 I:260 Test-Acc:0.8684 Train-Acc:0.945
I:120 Test-Acc:0.8481 Train-Acc:0.90 I:270 Test-Acc:0.8703 Train-Acc:0.951
I:130 Test-Acc:0.8505 Train-Acc:0.90 I:280 Test-Acc:0.8699 Train-Acc:0.949
I:140 Test-Acc:0.8526 Train-Acc:0.90 I:290 Test-Acc:0.8701 Train-Acc:0.94

第十章。关于边缘和角落的神经网络学习:卷积神经网络简介

本章内容

  • 在多个地方重用权重

  • 卷积层

“卷积神经网络中使用的池化操作是一个大错误,而它之所以能如此有效,简直是一场灾难。”

来自 Reddit 的“问我任何问题”活动,作者:杰弗里·辛顿

在多个地方重用权重

如果你需要在多个地方检测到相同的特征,请使用相同的权重!

神经网络中最大的挑战是过拟合,当神经网络记住数据集而不是学习有用的抽象,这些抽象可以推广到未见过的数据。换句话说,神经网络学习根据数据集中的噪声进行预测,而不是依赖于基本信号(还记得关于泥土中嵌入叉子的类比吗?)。

图片

过拟合通常是由于学习特定数据集所需的参数过多。在这种情况下,网络有如此多的参数,以至于它可以记住训练数据集中每个细微的细节(神经网络:“啊,我看到我们又有图像编号 363 了。这是编号 2。”),而不是学习高级抽象(神经网络:“嗯,它有一个弯曲的顶部,左下角有一个漩涡,右边有一个尾巴;它一定是 2。”)。当神经网络有很多参数但训练示例不多时,过拟合很难避免。

我们在第八章中详细介绍了这个主题,当时我们探讨了正则化作为对抗过拟合的手段。但正则化并不是唯一的技术(甚至不是最理想的技术)来防止过拟合。

正如我提到的,过拟合关注的是模型中权重数量与学习这些权重所需的数据点数量之间的比率。因此,有一种更好的方法来对抗过拟合。当可能时,最好使用某种松散定义的结构

结构是指你选择性地在神经网络中选择重用权重以实现多个目的,因为我们相信相同的模式需要在多个地方被检测到。正如你将看到的,这可以显著减少过拟合,并导致更精确的模型,因为它减少了权重与数据的比率。

图片

但是,通常移除参数会使模型的表达能力降低(更难学习模式),如果你在重用权重的地方足够聪明,模型可以同样具有表达能力,但更能抵抗过拟合。也许令人惊讶的是,这种技术也往往使模型变得更小(因为需要存储的实际参数更少)。神经网络中最著名和最广泛使用的结构称为卷积,当用作层时,称为卷积层

卷积层

在每个位置都重复使用很多非常小的线性层,而不是一个单一的大的层

卷积层背后的核心思想是,而不是有一个大型的、密集的线性层,其中每个输入都连接到每个输出,你反而有很多非常小的线性层,通常少于 25 个输入和一个输出,你可以在每个输入位置使用这些层。每个微型层被称为卷积核,但实际上它不过是一个具有少量输入和一个输出的婴儿线性层。

图片

这里展示了一个单一的 3 × 3 卷积核。它将在当前位置预测,然后向右移动一个像素,再次预测,然后再次向右移动,以此类推。一旦它扫描了整个图像,它将向下移动一个像素,然后向左扫描,重复直到它在图像中的每个可能位置都进行了预测。结果将是一个较小的核预测正方形,这些预测被用作下一层的输入。卷积层通常有很多核。

在右下角有四个不同的卷积核处理同一个 2 的 8 × 8 图像。每个核产生一个 6 × 6 预测矩阵。使用四个 3 × 3 核的卷积层的结果是四个 6 × 6 预测矩阵。你可以将这些矩阵逐元素相加(逐元素求和池化),逐元素取平均值(平均池化),或者计算逐元素的最大值(最大池化)。

最后一个版本证明是最受欢迎的:对于每个位置,查看每个核的输出,找到最大值,并将其复制到页面右上角的最终 6 × 6 矩阵中。这个最终矩阵(只有这个矩阵)然后被前向传播到下一层。

在这些图中需要注意几个方面。首先,右下角的核只在前向传播 1,前提是它专注于一条水平线段。左下角的核只在前向传播 1,前提是它专注于一个向上向右的对角线。最后,右下角的核没有识别出它被训练去预测的任何模式。

重要的是要认识到,这项技术允许每个核学习特定的模式,然后在图像的某个地方搜索该模式的存在。一个小的权重集可以训练一个更大的训练示例集,因为尽管数据集没有变化,每个微型核在多个数据段上多次前向传播,从而改变了权重与训练数据点的比率。这对网络有强大的影响,极大地减少了它对训练数据的过度拟合能力,并增加了它的一般化能力。

图片

NumPy 中的简单实现

只需想想微型线性层,你就已经知道你需要知道的内容了

让我们从前向传播开始。这个方法展示了如何在 NumPy 中从一批图像中选择一个子区域。请注意,它为整个批次选择了相同的子区域:

def get_image_section(layer,row_from, row_to, col_from, col_to):
    sub_section = layer[:,row_from:row_to,col_from:col_to]
    return subsection.reshape(-1,1,row_to-row_from, col_to-col_from)

现在,让我们看看这个方法是如何使用的。因为它选择了一批输入图像的一个子集,所以你需要多次调用它(在图像的每个位置上)。这样的for循环看起来可能像这样:

layer_0 = images[batch_start:batch_end]
layer_0 = layer_0.reshape(layer_0.shape[0],28,28)
layer_0.shape

sects = list()
for row_start in range(layer_0.shape[1]-kernel_rows):
    for col_start in range(layer_0.shape[2] - kernel_cols):
        sect = get_image_section(layer_0,
                                 row_start,
                                 row_start+kernel_rows,
                                 col_start,
                                 col_start+kernel_cols)
        sects.append(sect)

expanded_input = np.concatenate(sects,axis=1)
es = expanded_input.shape
flattened_input = expanded_input.reshape(es[0]*es[1],-1)

在这段代码中,layer_0是一个形状为 28 × 28 的图像批次。for循环遍历图像中的每个(kernel_rows × kernel_cols)子区域,并将它们放入一个名为sects的列表中。然后,这个部分列表以独特的方式连接和重塑。

(暂时)假设每个单独的子区域都是它自己的图像。因此,如果你有一个包含 8 张图像的批次,并且每张图像有 100 个子区域,那么你就可以假装这是一个包含 800 张更小图像的批次。通过一个只有一个输出神经元的线性层前向传播它们,就相当于预测每个批次中每个子区域的线性层(暂停并确保你理解了这一点)。

如果你使用具有n个输出神经元的线性层进行前向传播,它将生成与在每个输入位置的图像中预测n个线性层(核)相同的输出。你这样做是因为这样做可以使代码更简单、更快:

kernels = np.random.random((kernel_rows*kernel_cols,num_kernels))
               ...
kernel_output = flattened_input.dot(kernels)

以下列表显示了整个 NumPy 实现:

import numpy as np, sys
np.random.seed(1)

from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

images, labels = (x_train[0:1000].reshape(1000,28*28) / 255,
                  y_train[0:1000])

one_hot_labels = np.zeros((len(labels),10))
for i,l in enumerate(labels):
    one_hot_labels[i][l] = 1
labels = one_hot_labels

test_images = x_test.reshape(len(x_test),28*28) / 255
test_labels = np.zeros((len(y_test),10))
for i,l in enumerate(y_test):
    test_labels[i][l] = 1

def tanh(x):
    return np.tanh(x)

def tanh2deriv(output):
    return 1 - (output ** 2)

def softmax(x):
    temp = np.exp(x)
    return temp / np.sum(temp, axis=1, keepdims=True)

alpha, iterations = (2, 300)
pixels_per_image, num_labels = (784, 10)
batch_size = 128

input_rows = 28
input_cols = 28

kernel_rows = 3
kernel_cols = 3
num_kernels = 16

hidden_size = ((input_rows - kernel_rows) *
               (input_cols - kernel_cols)) * num_kernels

kernels = 0.02*np.random.random((kernel_rows*kernel_cols,
                                 num_kernels))-0.01

weights_1_2 = 0.2*np.random.random((hidden_size,
                                    num_labels)) - 0.1

def get_image_section(layer,row_from, row_to, col_from, col_to):
    section = layer[:,row_from:row_to,col_from:col_to]
    return section.reshape(-1,1,row_to-row_from, col_to-col_from)

for j in range(iterations):
    correct_cnt = 0
    for i in range(int(len(images) / batch_size)):
        batch_start, batch_end=((i * batch_size),((i+1)*batch_size))
        layer_0 = images[batch_start:batch_end]
        layer_0 = layer_0.reshape(layer_0.shape[0],28,28)
        layer_0.shape

        sects = list()
        for row_start in range(layer_0.shape[1]-kernel_rows):
            for col_start in range(layer_0.shape[2] - kernel_cols):
                sect = get_image_section(layer_0,
                                         row_start,
                                         row_start+kernel_rows,
                                         col_start,
                                         col_start+kernel_cols)
                sects.append(sect)

        expanded_input = np.concatenate(sects,axis=1)
        es = expanded_input.shape
        flattened_input = expanded_input.reshape(es[0]*es[1],-1)

        kernel_output = flattened_input.dot(kernels)
        layer_1 = tanh(kernel_output.reshape(es[0],-1))
        dropout_mask = np.random.randint(2,size=layer_1.shape)
        layer_1 *= dropout_mask * 2
        layer_2 = softmax(np.dot(layer_1,weights_1_2))

        for k in range(batch_size):
            labelset = labels[batch_start+k:batch_start+k+1]
            _inc = int(np.argmax(layer_2[k:k+1]) ==
                               np.argmax(labelset))
            correct_cnt += _inc

        layer_2_delta = (labels[batch_start:batch_end]-layer_2)\
                        / (batch_size * layer_2.shape[0])
        layer_1_delta = layer_2_delta.dot(weights_1_2.T) * \
                        tanh2deriv(layer_1)
        layer_1_delta *= dropout_mask
        weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
        l1d_reshape = layer_1_delta.reshape(kernel_output.shape)
        k_update = flattened_input.T.dot(l1d_reshape)
        kernels -= alpha * k_update

    test_correct_cnt = 0

    for i in range(len(test_images)):

        layer_0 = test_images[i:i+1]
        layer_0 = layer_0.reshape(layer_0.shape[0],28,28)
        layer_0.shape

        sects = list()
        for row_start in range(layer_0.shape[1]-kernel_rows):
            for col_start in range(layer_0.shape[2] - kernel_cols):
                sect = get_image_section(layer_0,
                                         row_start,
                                         row_start+kernel_rows,
                                         col_start,
                                         col_start+kernel_cols)
                sects.append(sect)

        expanded_input = np.concatenate(sects,axis=1)
        es = expanded_input.shape
        flattened_input = expanded_input.reshape(es[0]*es[1],-1)

        kernel_output = flattened_input.dot(kernels)
        layer_1 = tanh(kernel_output.reshape(es[0],-1))
        layer_2 = np.dot(layer_1,weights_1_2)

        test_correct_cnt += int(np.argmax(layer_2) ==
                                np.argmax(test_labels[i:i+1]))
    if(j % 1 == 0):
        sys.stdout.write("\n"+ \
         "I:" + str(j) + \
         " Test-Acc:"+str(test_correct_cnt/float(len(test_images)))+\
         " Train-Acc:" + str(correct_cnt/float(len(images))))

                   I:0 Test-Acc:0.0288 Train-Acc:0.055
                   I:1 Test-Acc:0.0273 Train-Acc:0.037
                   I:2 Test-Acc:0.028 Train-Acc:0.037
                   I:3 Test-Acc:0.0292 Train-Acc:0.04
                   I:4 Test-Acc:0.0339 Train-Acc:0.046
                   I:5 Test-Acc:0.0478 Train-Acc:0.068
                   I:6 Test-Acc:0.076 Train-Acc:0.083
                   I:7 Test-Acc:0.1316 Train-Acc:0.096
                   I:8 Test-Acc:0.2137 Train-Acc:0.127

                                  ....

                 I:297 Test-Acc:0.8774 Train-Acc:0.816
                 I:298 Test-Acc:0.8774 Train-Acc:0.804
                 I:299 Test-Acc:0.8774 Train-Acc:0.814

如你所见,将第九章中的网络的第一层替换为卷积层,可以进一步减少几个百分点的错误。卷积层的输出(kernel_output)本身也是一系列二维图像(每个输入位置中每个核的输出)。

大多数卷积层的应用都是将多层堆叠在一起,使得每个卷积层将前一层作为输入图像处理。(你可以自由地将这作为一个个人项目来做;这将进一步提高准确性。)

堆叠卷积层是允许非常深的神经网络(以及由此产生的“深度学习”这一术语的普及)的主要发展之一。不能过分强调这一发明是该领域的里程碑时刻;没有它,我们可能仍然处于写作时的上一个 AI 冬天。

摘要

重复使用权重是深度学习中最重要的一项创新。

卷积神经网络是一种比你所意识到的更一般的发展。重用权重以提高准确性的概念非常重要,并且有直观的基础。考虑一下你为了检测图像中是否有猫需要理解什么。首先,你需要理解颜色,然后是线条和边缘,角落和小的形状,最终是这些低级特征的组合,这些特征对应于猫。假设神经网络也需要学习这些低级特征(如线条和边缘),检测线条和边缘的智能就体现在权重中。

但如果你使用不同的权重来分析图像的不同部分,每个权重的部分都必须独立地学习线条是什么。为什么?好吧,如果一组权重观察图像的一部分学会了线条是什么,就没有理由认为另一部分的权重会以某种方式拥有使用该信息的能力:它位于网络的另一部分。

卷积是利用学习的一个特性。偶尔,,你需要多次使用相同的思想或智能;如果是这样,你应该尝试在这些位置使用相同的权重。这带我们到了本书中最重要的思想之一。如果你不学习其他任何东西,请学习这一点:

结构技巧

当神经网络需要在多个地方使用相同的思想时,应努力在这两个地方使用相同的权重。这将通过提供更多样本供学习,使这些权重变得更加智能,从而提高泛化能力。

过去五年(有些在之前)中深度学习领域最大的发展(其中一些在之前)都是这一想法的迭代。卷积、循环神经网络(RNNs)、词嵌入以及最近发布的胶囊网络都可以通过这个角度来理解。当你知道一个网络将在多个地方需要相同的思想时,强迫它在那些地方使用相同的权重。我完全预期更多的深度学习发现将继续基于这一想法,因为发现新的、更高层次的抽象思想,神经网络可以在其架构的各个部分重复使用,是具有挑战性的。

第十一章。理解语言的网络:王 - 男 + 女 == ?

本章内容

  • 自然语言处理(NLP)

  • 监督 NLP

  • 捕获输入数据中的词相关性

  • 嵌入层的介绍

  • 神经架构

  • 比较词嵌入

  • 填空

  • 意义来源于损失

  • 词类比

“人是一个缓慢、粗心大意但聪明的人;计算机是快速、准确但愚蠢的。”

约翰·费菲尔,在《财富》杂志,1961 年

理解语言意味着什么?

人们关于语言会做出哪些预测?

到目前为止,我们一直在使用神经网络来建模图像数据。但神经网络可以用来理解更广泛的数据集。探索新的数据集也让我们对神经网络的一般知识有了很多了解,因为不同的数据集通常根据数据中隐藏的挑战来证明不同的神经网络训练风格。

图片

我们将从这个章节开始,探索一个与深度学习重叠的古老领域:自然语言处理(NLP)。这个领域专门致力于人类语言的自动化理解(之前没有使用深度学习)。我们将讨论深度学习对这个领域的处理方法的基本原理。

自然语言处理(NLP)

NLP 被划分为一系列任务或挑战

可能最快了解自然语言处理(NLP)的方法之一是考虑 NLP 社区试图解决的许多挑战中的一些。以下是 NLP 中常见的几种分类问题类型:

  • 使用文档的字符来预测单词的开始和结束位置

  • 使用文档的单词来预测句子开始和结束的位置

  • 使用句子中的单词来预测每个单词的词性

  • 使用句子中的单词来预测短语开始和结束的位置

  • 使用句子中的单词来预测命名实体(人、地点、事物)引用的开始和结束位置

  • 使用文档中的句子来预测哪些代词指的是同一个人/地点/事物

  • 使用句子中的单词来预测句子的情感

一般而言,NLP 任务试图做以下三件事情之一:为文本区域(如词性标注、情感分类或命名实体识别)贴标签;将两个或更多文本区域(如指代消解,试图回答两个对现实世界事物的提及是否实际上指的是同一个现实世界事物,其中现实世界事物通常是人、地点或其他命名实体)链接起来;或者根据上下文尝试填补缺失的信息(缺失的单词)。

可能也明显地看出机器学习和 NLP 是如何紧密相连的。直到最近,大多数最先进的 NLP 算法都是高级、概率性、非参数模型(不是深度学习)。但最近两个主要神经网络算法的发展和普及已经席卷了 NLP 领域:神经词嵌入和循环神经网络(RNNs)。

在本章中,我们将构建一个词嵌入算法,并展示它如何提高自然语言处理算法的准确性。在下一章中,我们将创建一个循环神经网络,并展示它在预测序列时的有效性。

还值得一提的是,自然语言处理(可能使用深度学习)在人工智能进步中扮演的关键角色。人工智能寻求创造能够像人类一样(甚至超越人类)思考和与世界互动的机器。自然语言处理在这个努力中扮演着非常特殊的角色,因为语言是人类意识逻辑和沟通的基础。因此,机器使用和理解语言的方法构成了机器中类似人类逻辑的基础:思想的基础。

监督式自然语言处理

输入单词,输出预测

你可能还记得第二章中的以下图示。监督学习就是将“你所知道的”转化为“你想要知道的”。到目前为止,“你所知道的”总是以某种方式由数字组成。但自然语言处理使用文本作为输入。你该如何处理它?

图片

因为神经网络只将输入数字映射到输出数字,所以第一步是将文本转换为数值形式。就像我们转换街灯数据集一样,我们需要将现实世界的数据(在这种情况下,文本)转换为神经网络可以消费的矩阵。实际上,我们如何做这一点非常重要!

图片

我们应该如何将文本转换为数字?回答这个问题需要对问题进行一些思考。记住,神经网络在其输入层和输出层之间寻找相关性。因此,我们希望以使输入和输出之间的相关性对网络来说最明显的方式将文本转换为数字。这将使训练更快,泛化能力更好。

为了知道哪种输入格式能让网络最明显地感知输入/输出相关性,我们需要了解输入/输出数据集的样子。为了探讨这个话题,让我们接受挑战,进行主题分类

IMDB 电影评论数据集

你可以预测人们发布的评论是正面还是负面

IMDB 电影评论数据集是一组评论->评分对,通常看起来像以下这样(这是一个模仿,并非来自 IMDB):

“这部电影太糟糕了!剧情枯燥,表演不可信,我还把爆米花洒在了衬衫上。”

评分:1(星星)

整个数据集大约由 50,000 个这样的对组成,其中输入评论通常是几句话,输出评分在 1 到 5 星之间。人们认为这是一个情感数据集,因为星星表示电影评论的整体情感。但很明显,这个情感数据集可能与其他情感数据集(如产品评论或医院患者评论)有很大不同。

你想要训练一个神经网络,它可以使用输入文本来准确预测输出分数。为了实现这一点,你必须首先决定如何将输入和输出数据集转换为矩阵。有趣的是,输出数据集是一个数字,这可能使得它更容易开始。你将调整星级范围在 0 到 1 之间,而不是 1 到 5,这样你就可以使用二进制的softmax。这就是你需要对输出所做的所有事情。我将在下一页上展示一个例子。

然而,输入数据却有些棘手。首先,让我们考虑原始数据。它是一系列字符。这带来了一些问题:不仅输入数据是文本而不是数字,而且它是可变长度的文本。到目前为止,神经网络总是需要一个固定大小的输入。你需要克服这个问题。

因此,原始输入将不起作用。接下来要问的问题是,“这些数据中的哪些将与输出有相关性?”表示这个属性可能效果很好。首先,我不期望任何字符(在字符列表中)与情感有任何相关性。你需要以不同的方式思考。

那么,关于单词呢?这个数据集中的一些单词会有一些相关性。我敢打赌糟糕不可信与评分有显著的负相关性。这里的负相关性是指,随着它们在任何输入数据点(任何评论)中的频率增加,评分往往会下降。

也许这个属性更普遍!也许单词本身(即使在没有上下文的情况下)会与情感有显著的相关性。让我们进一步探讨这个问题。

在输入数据中捕捉单词相关性

词袋模型:给定一个评论的词汇,预测情感

如果你观察到 IMDB 评论的词汇与其评分之间的相关性,那么你可以进行下一步:创建一个表示电影评论词汇的输入矩阵。

在这种情况下通常的做法是创建一个矩阵,其中每一行(向量)对应于每一部电影评论,每一列表示评论是否包含词汇表中的特定单词。为了创建评论的向量,你计算评论的词汇表,然后在对应的列中为该评论放置 1,其他所有地方放置 0。这些向量有多大?嗯,如果有 2000 个单词,并且你需要在每个向量中为每个单词留出空间,那么每个向量将有 2000 个维度。

这种存储形式被称为独热编码,是编码二进制数据(在可能的输入数据点词汇表中,输入数据点的二进制存在或不存在)最常见的形式。如果词汇表只有四个单词,独热编码可能看起来像这样:

import numpy as np

onehots = {}
onehots['cat'] = np.array([1,0,0,0])
onehots['the'] = np.array([0,1,0,0])
onehots['dog'] = np.array([0,0,1,0])
onehots['sat'] = np.array([0,0,0,1])

sentence = ['the','cat','sat']
x = word2hot[sentence[0]] + \
    word2hot[sentence[1]] + \
    word2hot[sentence[2]]

print("Sent Encoding:" + str(x))

正如你所见,我们为词汇表中的每个术语创建一个向量,这允许你使用简单的向量加法来创建表示总词汇子集的向量(例如,与句子中的单词相对应的子集)。

*Output:*        Sent Encoding:[1 1 0 1]

注意,当你为几个术语(如“the cat sat”)创建嵌入时,如果单词重复出现多次,你有多种选择。如果短语是“cat cat cat”,你可以选择将“cat”的向量相加三次(结果为[3,0,0,0]),或者只取唯一的“cat”一次(结果为[1,0,0,0])。后者通常对语言来说效果更好。

预测电影评论

使用编码策略和之前的网络,你可以预测情感

使用我们刚才确定的战略,你可以为情感数据集中的每个单词构建一个向量,并使用之前的两层网络来预测情感。我会展示代码,但我强烈建议你尝试从记忆中完成这个任务。打开一个新的 Jupyter 笔记本,加载数据集,构建你的独热向量,然后构建一个神经网络来预测每篇电影评论的评分(正面或负面)。

我会这样进行预处理步骤:

import sys

f = open('reviews.txt')
raw_reviews = f.readlines()
f.close()

f = open('labels.txt')
raw_labels = f.readlines()
f.close()

tokens = list(map(lambda x:set(x.split(" ")),raw_reviews))

vocab = set()
for sent in tokens:
    for word in sent:
        if(len(word)>0):
            vocab.add(word)
vocab = list(vocab)

word2index = {}
for i,word in enumerate(vocab):
    word2index[word]=i

input_dataset = list()
for sent in tokens:
    sent_indices = list()
    for word in sent:
        try:
            sent_indices.append(word2index[word])
        except:
            ""
    input_dataset.append(list(set(sent_indices)))

target_dataset = list()
for label in raw_labels:
    if label == 'positive\n':
        target_dataset.append(1)
    else:
        target_dataset.append(0)

嵌入层的简介

这还有一个使网络更快的小技巧

右边是之前神经网络的图,你现在将使用它来预测情感。但在那之前,我想描述一下层名称。第一层是数据集(layer_0)。接下来是所谓的线性层weights_0_1)。接下来是一个relu层(layer_1),另一个线性层(weights_1_2),然后是输出,也就是预测层。实际上,你可以通过用嵌入层替换第一个线性层(weights_0_1)来稍微缩短到layer_1的路径。

使用 1s 和 0s 的向量在数学上等同于对矩阵的几行求和。因此,选择weights_0_1的相关行并求和要比进行大规模的向量-矩阵乘法更高效。因为情感词汇量大约有 70,000 个单词,大部分的向量-矩阵乘法都是在输入向量中的 0 与矩阵的不同行相乘后再求和。选择矩阵中对应每个单词的行并求和要高效得多。

使用选择行并执行求和(或平均)的过程,意味着将第一线性层(weights_0_1)作为嵌入层。从结构上看,它们是相同的(layer_1使用两种方法进行前向传播时都是一样的)。唯一的区别是求和少量行要快得多。

在运行之前的代码后,运行此代码

import numpy as np
np.random.seed(1)

def sigmoid(x):
    return 1/(1 + np.exp(-x))

alpha, iterations = (0.01, 2)
hidden_size = 100

weights_0_1 = 0.2*np.random.random((len(vocab),hidden_size)) - 0.1
weights_1_2 = 0.2*np.random.random((hidden_size,1)) - 0.1

correct,total = (0,0)
for iter in range(iterations):

    for i in range(len(input_dataset)-1000):               *1*

        x,y = (input_dataset[i],target_dataset[i])
        layer_1 = sigmoid(np.sum(weights_0_1[x],axis=0))   *2*
        layer_2 = sigmoid(np.dot(layer_1,weights_1_2))     *3*
        layer_2_delta = layer_2 - y                        *4*
        layer_1_delta = layer_2_delta.dot(weights_1_2.T)   *5*
        weights_0_1[x] -= layer_1_delta * alpha
        weights_1_2 -= np.outer(layer_1,layer_2_delta) * alpha

        if(np.abs(layer_2_delta) < 0.5):
            correct += 1
        total += 1
        if(i % 10 == 9):
            progress = str(i/float(len(input_dataset)))
            sys.stdout.write('\rIter:'+str(iter)\
                             +' Progress:'+progress[2:4]\
                             +'.'+progress[4:6]\
                             +'% Training Accuracy:'\
                             + str(correct/float(total)) + '%')
    print()
correct,total = (0,0)
for i in range(len(input_dataset)-1000,len(input_dataset)):

       x = input_dataset[i]
       y = target_dataset[i]

       layer_1 = sigmoid(np.sum(weights_0_1[x],axis=0))
       layer_2 = sigmoid(np.dot(layer_1,weights_1_2))

    if(np.abs(layer_2 - y) < 0.5):
        correct += 1
    total += 1
print("Test Accuracy:" + str(correct / float(total)))
  • 1 在前 24,000 条评论上进行训练

  • 2 嵌入 + sigmoid

  • 3 线性 + softmax

  • 4 将预测与真实值进行比较

  • 5 反向传播

解释输出

神经网络在过程中学到了什么?

这是电影评论神经网络的结果。从一个角度来看,这与我们之前讨论过的相同的相关性总结:

Iter:0 Progress:95.99% Training Accuracy:0.832%
Iter:1 Progress:95.99% Training Accuracy:0.8663333333333333%
Test Accuracy:0.849

神经网络正在寻找输入数据点和输出数据点之间的相关性。但这些数据点具有我们熟悉的特征(尤其是语言的那些特征)。此外,考虑由相关性总结检测到的语言模式,以及更重要的是,哪些模式不会被检测到,这一点极为有益。毕竟,仅仅因为网络能够在输入和输出数据集之间找到相关性,并不意味着它理解了语言中的每一个有用模式。

图片

此外,理解网络(在其当前配置下)能够学习的内容与它为了正确理解语言所需知道的内容之间的区别,是一条极其富有成效的思考路线。这正是处于最前沿的研究人员所考虑的,也是我们在这里将要考虑的。

电影评论网络学习了哪些关于语言的知识?让我们首先考虑呈现给网络的内容。如右上角的图所示,你将每篇评论的词汇作为输入,并要求网络预测两个标签中的一个(正面负面)。鉴于相关性总结表明网络将在输入和输出数据集之间寻找相关性,至少你可以期望网络识别出具有正或负相关性的单词(单独来看)。

这自然地导致了相关性总结。你呈现一个词的存在或不存在。因此,相关性总结将找到这种存在/不存在与两个标签中的每一个之间的直接相关性。但这并不是全部的故事。

图片

神经架构

架构的选择是如何影响网络学习的内容的?

我们刚刚讨论了神经网络学习的第一种、最简单类型的信息:输入和目标数据集之间的直接相关性。这一观察结果在很大程度上是神经网络智能的起点。(如果一个网络无法在输入和输出数据之间发现直接相关性,那么可能出了问题。)更复杂架构的发展基于寻找比直接相关性更复杂模式的需要,而这个网络也不例外。

识别直接相关性的最小架构是一个两层网络,其中网络有一个直接从输入层连接到输出层的单权重矩阵。但我们的网络有一个隐藏层。这引发了一个问题,这个隐藏层有什么作用?

基本上,隐藏层是将前一层的数据点分组为 n 组(其中 n 是隐藏层中神经元的数量)。每个隐藏神经元接收一个数据点并回答问题:“这个数据点是否在我的组中?”随着隐藏层的不断学习,它会寻找其输入的有用分组。什么是有用的分组?

如果一个输入数据点分组能够做两件事,那么它是有用的。首先,分组必须对预测输出标签有用。如果它对输出预测没有用,相关性总结永远不会引导网络找到该组。这是一个非常有价值的认识。神经网络研究的大部分内容都是关于找到训练数据(或为网络制造的其他信号,以便它能够人为地预测)以找到对任务有用的分组(例如预测电影评论星级)。我们将在稍后讨论这一点。

其次,如果分组是数据中你关心的实际现象,那么它是有用的。不良的分组只是记住数据。好的分组能够捕捉到有用的语言现象。

例如,在预测电影评论是正面还是负面时,理解“糟糕”和“不糟糕”之间的差异是一种强大的分组。如果有一个神经元在看到“糟糕”时关闭,在看到“不糟糕”时打开,这将是一个强大的分组,供下一层使用以做出最终预测。但由于神经网络输入是评论的词汇,“它很棒,不糟糕”与“它很糟糕,不棒”创造了完全相同的layer_1值。因此,网络非常不可能创建一个理解否定意义的隐藏神经元。

基于某种语言模式测试层是否相同或不同,是了解架构是否可能使用相关性总结找到该模式的一个很好的第一步。如果你可以构建两个具有相同隐藏层的例子,一个有你感兴趣的模式,另一个没有,那么网络不太可能找到该模式。

正如你刚刚学到的,隐藏层在本质上将前一层的数据分组。在细粒度层面,每个神经元将数据点分类为属于或不属于其组。在更高层面,如果两个数据点(电影评论)属于许多相同的组,则它们是相似的。最后,如果连接到各种隐藏神经元的权重(每个单词的组亲和力度量)相似,则两个输入(单词)是相似的。有了这些知识,在前一个神经网络中,你应该在连接到隐藏神经元的单词权重中观察到什么?

你应该在连接单词和隐藏神经元的权重中看到什么?

这里有一个提示:具有相似预测能力的单词应该属于相似的组(隐藏神经元配置)。这对连接每个单词到每个隐藏神经元的权重意味着什么?

这里是答案。与相似标签(正面或负面)相关的单词将会有类似的权重将它们连接到各种隐藏神经元。这是因为神经网络学会将它们归入相似的隐藏神经元,以便最终层(weights_1_2)可以做出正确的正面或负面预测。

您可以通过选择一个特别积极或消极的词语,并搜索具有最相似权值的其他词语来观察这一现象。换句话说,您可以取每个词语,并查看哪些其他词语与每个隐藏神经元(每个组)具有最相似的权值连接。

图片

“好”的三个粗体权值形成了“好”的嵌入。它们反映了“好”这个术语是每个组(隐藏神经元)的成员程度。具有相似预测能力的词语具有相似的词嵌入(权值)。

属于相似组的词语将具有相似的正负标签预测能力。因此,属于相似组且具有相似权值(权重)的词语也将具有相似的含义。从抽象的角度来看,在神经网络中,一个神经元与同一层中其他神经元的含义相似,当且仅当它与下一层和/或前一层的连接权重相似。

比较词嵌入

如何可视化权重相似性?

对于每个输入词语,您可以通过选择weights_0_1中对应的行来选择从该词语到各个隐藏神经元的权值列表。行中的每个条目代表从该行词语到每个隐藏神经元的权值。因此,为了确定哪些词语与目标术语最相似,您需要比较每个词语的向量(矩阵的行)与目标术语的向量。选择的比较方法是称为欧几里得距离,如下面的代码所示:

from collections import Counter
import math

def similar(target='beautiful'):
    target_index = word2index[target]
    scores = Counter()
    for word,index in word2index.items():
        raw_difference = weights_0_1[index] - (weights_0_1[target_index])
        squared_difference = raw_difference * raw_difference
        scores[word] = -math.sqrt(sum(squared_difference))

     return scores.most_common(10)

这使得您可以根据网络轻松查询最相似的词语(神经元):

print(similar('beautiful'))           print(similar('terrible'))

[('beautiful', -0.0),                 [('terrible', -0.0),
 ('atmosphere', -0.70542101298),       ('dull', -0.760788602671491),
 ('heart', -0.7339429768542354),       ('lacks', -0.76706470275372),
 ('tight', -0.7470388145765346),       ('boring', -0.7682894961694),
 ('fascinating', -0.7549291974),       ('disappointing', -0.768657),
 ('expecting', -0.759886970744),       ('annoying', -0.78786389931),
 ('beautifully', -0.7603669338),       ('poor', -0.825784172378292),
 ('awesome', -0.76647368382398),       ('horrible', -0.83154121717),
 ('masterpiece', -0.7708280057),       ('laughable', -0.8340279599),
 ('outstanding', -0.7740642167)]       ('badly', -0.84165373783678)]

如您所预期的那样,每个词语最相似的术语是它自己,其次是具有与目标术语相似有用性的词语。再次,正如您所预期的那样,因为网络只有两个标签(正面负面),输入术语根据它们倾向于预测的标签分组。

这是一种标准的关联总结现象。它试图根据预测的标签在网络上创建相似的表现(layer_1值),以便能够预测正确的标签。在这种情况下,副作用是输入到layer_1的权重根据输出标签分组。

关联总结现象的关键启示是对这一现象的直观理解。它始终试图说服隐藏层基于应该预测的标签保持相似。

神经元的含义是什么?

意义完全基于预测的目标标签

注意,不同词语的含义并没有完全反映您可能如何将它们分组。与“美丽”最相似的术语是“气氛”。这是一个宝贵的教训。为了预测电影评论是正面还是负面,这些词语具有几乎相同的意义。但在现实世界中,它们的含义却截然不同(例如,一个是形容词,另一个是名词)。

print(similar('beautiful'))           print(similar('terrible'))

[('beautiful', -0.0),                 [('terrible', -0.0),
 ('atmosphere', -0.70542101298),       ('dull', -0.760788602671491),
 ('heart', -0.7339429768542354),       ('lacks', -0.76706470275372),
 ('tight', -0.7470388145765346),       ('boring', -0.7682894961694),
 ('fascinating', -0.7549291974),       ('disappointing', -0.768657),
 ('expecting', -0.759886970744),       ('annoying', -0.78786389931),
 ('beautifully', -0.7603669338),       ('poor', -0.825784172378292),
 ('awesome', -0.76647368382398),       ('horrible', -0.83154121717),
 ('masterpiece', -0.7708280057),       ('laughable', -0.8340279599),
 ('outstanding', -0.7740642167)]       ('badly', -0.84165373783678)]

这一认识非常重要。网络中(神经元)的含义是基于目标标签定义的。神经网络中的所有内容都是基于试图正确预测的相关性总结进行语境化的。因此,尽管你和我对这些单词非常了解,但神经网络对任务之外的所有信息一无所知。

你如何让网络学习关于神经元(在这种情况下,是单词神经元)的更细微的信息?好吧,如果你给它输入和目标数据,这些数据需要更细微的语言理解,它就会有理由学习各种术语的更细微的解释。

你应该使用神经网络来预测什么,以便它为单词神经元学习更有趣的权重值?你将用于学习单词神经元更有趣的权重值的学习任务是一个美化的填空任务。为什么使用这个?首先,有几乎无限的训练数据(互联网),这意味着神经网络有几乎无限的信号来学习关于单词的更细微的信息。此外,能够准确地填充空白至少需要一些关于现实世界的上下文概念。

例如,在以下示例中,空白处更可能是被“anvil”(砧)还是“wool”(羊毛)正确填充?让我们看看神经网络能否解决这个问题。

填空

通过拥有更丰富的学习信号来学习单词的更丰富含义

这个例子几乎与上一个例子使用完全相同的神经网络,只有一些修改。首先,你不会预测针对电影评论的单个标签,而是将每个(五个单词)短语中的每个单词(焦点词)移除,并尝试训练一个网络来根据剩余的短语确定被移除的单词的身份。其次,你将使用一种称为负采样的技巧来使网络训练更快。

考虑到为了预测哪个术语缺失,你需要为每个可能的单词提供一个标签。这将需要数千个标签,这将导致网络训练缓慢。为了克服这一点,让我们在每次前向传播步骤中随机忽略每个标签的大部分(即,假装它们不存在)。虽然这看起来可能是一种粗略的近似,但在实践中这是一种效果很好的技术。以下是本例的预处理代码:

import sys,random,math
from collections import Counter
import numpy as np

np.random.seed(1)
random.seed(1)
f = open('reviews.txt')
raw_reviews = f.readlines()
f.close()

tokens = list(map(lambda x:(x.split(" ")),raw_reviews))
wordcnt = Counter()
for sent in tokens:
    for word in sent:
        wordcnt[word] -= 1
vocab = list(set(map(lambda x:x[0],wordcnt.most_common())))

word2index = {}
for i,word in enumerate(vocab):
    word2index[word]=i

concatenated = list()
input_dataset = list()
for sent in tokens:
    sent_indices = list()
    for word in sent:
        try:
             sent_indices.append(word2index[word])
             concatenated.append(word2index[word])
        except:
            ""
    input_dataset.append(sent_indices)
concatenated = np.array(concatenated)

random.shuffle(input_dataset)

alpha, iterations = (0.05, 2)
hidden_size,window,negative = (50,2,5)

weights_0_1 = (np.random.rand(len(vocab),hidden_size) - 0.5) * 0.2
weights_1_2 = np.random.rand(len(vocab),hidden_size)*0

layer_2_target = np.zeros(negative+1)
layer_2_target[0] = 1

def similar(target='beautiful'):
  target_index = word2index[target]

  scores = Counter()
  for word,index in word2index.items():
    raw_difference = weights_0_1[index] - (weights_0_1[target_index])
    squared_difference = raw_difference * raw_difference
    scores[word] = -math.sqrt(sum(squared_difference))
  return scores.most_common(10)

def sigmoid(x):
    return 1/(1 + np.exp(-x))

for rev_i,review in enumerate(input_dataset * iterations):
  for target_i in range(len(review)):

    target_samples = [review[target_i]]+list(concatenated\                   *1*
      [(np.random.rand(negative)*len(concatenated)).astype('int').tolist()]) *1*

       left_context = review[max(0,target_i-window):target_i]
       right_context = review[target_i+1:min(len(review),target_i+window)]

       layer_1 = np.mean(weights_0_1[left_context+right_context],axis=0)
       layer_2 = sigmoid(layer_1.dot(weights_1_2[target_samples].T))
       layer_2_delta = layer_2 - layer_2_target
       layer_1_delta = layer_2_delta.dot(weights_1_2[target_samples])

       weights_0_1[left_context+right_context] -= layer_1_delta * alpha
       weights_1_2[target_samples] -= np.outer(layer_2_delta,layer_1)*alpha

  if(rev_i % 250 == 0):
    sys.stdout.write('\rProgress:'+str(rev_i/float(len(input_dataset)
        *iterations)) + "   " + str(similar('terrible')))
  sys.stdout.write('\rProgress:'+str(rev_i/float(len(input_dataset)
        *iterations)))
print(similar('terrible'))

  Progress:0.99998 [('terrible', -0.0), ('horrible', -2.846300248788519),
  ('brilliant', -3.039932544396419), ('pathetic', -3.4868595532695967),
  ('superb', -3.6092947961276645), ('phenomenal', -3.660172529098085),
  ('masterful', -3.6856112636664564), ('marvelous', -3.9306620801551664),
  • 1 只预测随机子集,因为预测每个词汇都非常昂贵

意义来源于损失

使用这个新的神经网络,你可以主观地看到单词嵌入的聚类方式有所不同。以前单词是根据预测“积极”或“消极”标签的可能性进行聚类的,而现在它们是根据在同一短语中出现的可能性进行聚类的(有时不考虑情感)。

Predicting POS/NEG Fill in the blank

|

print(similar('terrible'))

[('terrible', -0.0),
 ('dull', -0.760788602671491),
 ('lacks', -0.76706470275372),
 ('boring', -0.7682894961694),
 ('disappointing', -0.768657),
 ('annoying', -0.78786389931),
 ('poor', -0.825784172378292),
 ('horrible', -0.83154121717),
 ('laughable', -0.8340279599),
 ('badly', -0.84165373783678)]

print(similar('beautiful'))

[('beautiful', -0.0),
 ('atmosphere', -0.70542101298),
 ('heart', -0.7339429768542354),
 ('tight', -0.7470388145765346),
 ('fascinating', -0.7549291974),
 ('expecting', -0.759886970744),
 ('beautifully', -0.7603669338),
 ('awesome', -0.76647368382398),
 ('masterpiece', -0.7708280057),
 ('outstanding', -0.7740642167)]

|

print(similar('terrible'))

[('terrible', -0.0),
 ('horrible', -2.79600898781),
 ('brilliant', -3.3336178881),
 ('pathetic', -3.49393193646),
 ('phenomenal', -3.773268963),
 ('masterful', -3.8376122586),
 ('superb', -3.9043150978490),
 ('bad', -3.9141673639585237),
 ('marvelous', -4.0470804427),
 ('dire', -4.178749691835959)]

print(similar('beautiful'))

[('beautiful', -0.0),
 ('lovely', -3.0145597243116),
 ('creepy', -3.1975363066322),
 ('fantastic', -3.2551041418),
 ('glamorous', -3.3050812101),
 ('spooky', -3.4881261617587),
 ('cute', -3.592955888181448),
 ('nightmarish', -3.60063813),
 ('heartwarming', -3.6348147),
 ('phenomenal', -3.645669007)]

|

关键的收获是,尽管网络在具有非常相似架构(三层,交叉熵,sigmoid非线性)的相同数据集上进行了训练,但你可以通过改变你告诉网络预测的内容来影响网络在其权重中学习的内容。尽管它正在查看相同的统计信息,但你可以根据你选择的输入和目标值来定位它学习的内容。暂时,让我们称这个过程为选择你希望网络学习的内容为智能目标化

控制输入/目标值并不是执行智能目标化的唯一方法。您还可以调整网络如何衡量错误,它所具有的层的大小和类型,以及要应用的正则化类型。在深度学习研究中,所有这些技术都属于构建所谓的损失函数的范畴。

神经网络实际上并不是学习数据;它们最小化损失函数

在第四章中,你学习了学习是关于调整神经网络中的每个权重,将错误降低到 0。在本节中,我将从不同角度解释相同的现象,选择错误,以便神经网络学习我们感兴趣的模式。你还记得这些教训吗?

学习的黄金法则

调整每个权重到正确的方向和正确的量,以便将错误减少到 0。

秘密

对于任何输入goal_pred,定义了错误权重之间的精确关系,这是通过结合预测错误公式找到的。

error = ((0.5 * weight) - 0.8) ** 2

也许你还记得这个公式来自单权重神经网络。在那个网络中,你可以通过首先正向传播(0.5 * 权重)然后与目标(0.8)进行比较来评估错误。我鼓励你不要从两个不同步骤(正向传播,然后错误评估)的角度来思考,而应该将整个公式(包括正向传播)视为错误值的评估。这个上下文将揭示不同词嵌入聚类背后的真正原因。尽管网络和数据集相似,但错误函数在本质上不同,导致每个网络内部的不同词聚类。

预测 POS/NEG 填空

|

print(similar('terrible'))

[('terrible', -0.0),
 ('dull', -0.760788602671491),
 ('lacks', -0.76706470275372),
 ('boring', -0.7682894961694),
 ('disappointing', -0.768657),
 ('annoying', -0.78786389931),
 ('poor', -0.825784172378292),
 ('horrible', -0.83154121717),
 ('laughable', -0.8340279599),
 ('badly', -0.84165373783678)]

|

print(similar('terrible'))

[('terrible', -0.0),
 ('horrible', -2.79600898781),
 ('brilliant', -3.3336178881),
 ('pathetic', -3.49393193646),
 ('phenomenal', -3.773268963),
 ('masterful', -3.8376122586),
 ('superb', -3.9043150978490),
 ('bad', -3.9141673639585237),
 ('marvelous', -4.0470804427),
 ('dire', -4.178749691835959)]

|

损失函数的选择决定了神经网络的认知

一个 误差函数 的更正式的术语是 损失函数目标函数(这三个短语可以互换使用)。将学习视为最小化损失函数(这包括前向传播)的过程,可以提供一个更广泛的视角来理解神经网络是如何学习的。两个神经网络可能具有相同的起始权重,在相同的数据集上进行训练,但最终学习到非常不同的模式,因为您选择了不同的损失函数。在两个电影评论神经网络的情况下,损失函数之所以不同,是因为您选择了两个不同的目标值(正面或负面与填空)。

不同的架构、层、正则化技术、数据集和非线性并不是真的那么不同。这些都是您可以用来构建损失函数的方式。如果网络没有正确学习,解决方案通常可以来自这些可能的任何一种。

例如,如果一个网络过度拟合,您可以通过选择更简单的非线性、较小的层大小、较浅的架构、更大的数据集或更激进的正则化技术来增强损失函数。所有这些选择将对损失函数产生根本上的相似影响,并对网络的行为产生相似的影响。它们相互作用,随着时间的推移,您将学会如何改变一个影响另一个的性能;但就目前而言,重要的启示是学习是关于构建损失函数然后最小化它。

无论何时您想让神经网络学习一个模式,您需要知道的一切都会包含在损失函数中。当您只有一个权重时,这使得损失函数变得简单,正如您所回忆的那样:

|

error = ((0.5 * weight) - 0.8) ** 2

|

但随着您将大量复杂的层连接起来,损失函数将变得更加复杂(这是可以的)。但请记住,如果出现问题,解决方案就在损失函数中,这包括前向预测和原始误差评估(如均方误差或交叉熵)。

国王 – 男人 + 女人 ~= 女王

单词类比是之前构建的网络的一个有趣的结果

在结束这一章之前,让我们讨论一下,在撰写本文时,仍然是神经网络词嵌入(如我们刚刚创建的词向量)最著名的属性之一。填空的任务创建了具有有趣现象的词嵌入,这种现象被称为 单词类比,您可以对不同单词的向量执行基本的代数运算。

例如,如果你在一个足够大的语料库上训练前面的网络,你将能够从king的向量中减去man的向量,加上woman的向量,然后搜索最相似的向量(除了查询中的那些)。结果证明,最相似的向量通常是“queen”。在电影评论上训练的填空网络中甚至有类似的现象。

          def analogy(positive=['terrible','good'],negative=['bad']):

              norms = np.sum(weights_0_1 * weights_0_1,axis=1)
              norms.resize(norms.shape[0],1)

              normed_weights = weights_0_1 * norms

              query_vect = np.zeros(len(weights_0_1[0]))
              for word in positive:
                  query_vect += normed_weights[word2index[word]]
              for word in negative:
                  query_vect -= normed_weights[word2index[word]]

              scores = Counter()
              for word,index in word2index.items():
                  raw_difference = weights_0_1[index] - query_vect
                  squared_difference = raw_difference * raw_difference
                  scores[word] = -math.sqrt(sum(squared_difference))

              return scores.most_common(10)[1:]

     *terrible – bad + good ~=*                 *elizabeth – she + he ~=*

analogy(['terrible','good'],['bad'])     analogy(['elizabeth','he'],['she'])

[('superb', -223.3926217861),            [('christopher', -192.7003),
 ('terrific', -223.690648739),            ('it', -193.3250398279812),
 ('decent', -223.7045545791),             ('him', -193.459063887477),
 ('fine', -223.9233021831882),            ('this', -193.59240614759),
 ('worth', -224.03031703075),             ('william', -193.63049856),
 ('perfect', -224.125194533),             ('mr', -193.6426152274126),
 ('brilliant', -224.2138041),             ('bruce', -193.6689279548),
 ('nice', -224.244182032763),             ('fred', -193.69940566948),
 ('great', -224.29115420564)]             ('there', -193.7189421836)]

单词类比

数据中现有特性的线性压缩

当这个特性首次被发现时,它引起了人们的极大兴奋,因为人们推测了许多这种技术的可能应用。这本身就是一个惊人的特性,它确实在生成各种类型的单词嵌入方面创造了一个真正的 cottage industry。但自从那时起,单词类比特性本身并没有增长多少,而目前大部分的语言研究工作都集中在循环架构上(我们将在第十二章中讨论)。

话虽如此,由于选择损失函数的结果,对单词嵌入中发生的事情有一个良好的直观理解是非常宝贵的。你已经了解到损失函数的选择可以影响单词的分组方式,但这个单词类比现象是另一回事。是什么新的损失函数导致了这种现象的发生?

如果你考虑一个单词嵌入有两个维度,那么也许更容易想象这些单词类比是如何工作的。

king  = [0.6 , 0.1]
man   = [0.5 , 0.0]
woman = [0.0 , 0.8]
queen = [0.1 , 1.0]

king - man = [0.1 , 0.1]
queen - woman  = [0.1 , 0.2]

图片

“king”(国王)/“man”(男人)与“queen”(王后)/“woman”(女人)之间的相对有用性是相似的。为什么? “king”和“man”之间的差异留下了一个royalty(皇室)向量。有一组与男性、女性相关的单词,然后还有另一组在皇室方向的分组。

这可以追溯到选择的损失函数。当单词“king”出现在一个短语中时,它会改变其他单词以某种方式出现的概率。它增加了与“man”相关的单词的概率以及与皇室相关的单词的概率。短语中出现的“queen”增加了与“woman”相关的单词的概率以及与皇室(作为一个群体)相关的概率。因此,由于单词对输出概率有这种类似维恩图的影响,它们最终订阅了类似的组合分组。

简而言之,“king”订阅了隐藏层的男性维度和皇室维度,而“queen”订阅了隐藏层的女性维度和皇室维度。从“king”的向量中减去一些男性维度的近似值并添加女性维度,可以得到接近“queen”的结果。最重要的收获是,这更多地关于语言的特性,而不是深度学习。这些共现统计的任何线性压缩都会表现出类似的行为。

摘要

你已经学到了很多关于神经词嵌入以及损失对学习的影响的知识。

在本章中,我们探讨了使用神经网络研究语言的基本原理。我们从自然语言处理的主要问题概述开始,然后探讨了神经网络如何使用词嵌入在词级别模拟语言。你还学习了损失函数的选择如何改变词嵌入捕获的性质。我们最后讨论了在这个领域可能最神奇的一种神经现象:词类比。

与其他章节一样,我鼓励您从头开始构建本章的示例。尽管这个章节看起来似乎是独立的,但关于损失函数创建和调整的教训是无价的,并且在你未来章节中处理越来越复杂的策略时将极其重要。祝你好运!

第十二章:像莎士比亚一样写作的神经网络:用于可变长度数据的循环层

本章内容

  • 任意长度挑战

  • 平均词向量的惊人力量

  • 词袋向量局限性

  • 使用单位向量求和词嵌入

  • 学习过渡矩阵

  • 学习创建有用的句子向量

  • Python 中的正向传播

  • 具有任意长度的正向传播和反向传播

  • 具有任意长度的权重更新

“循环神经网络有一种神奇的力量。”

安德烈·卡帕蒂,“循环神经网络的不合理有效性”,mng.bz/VPW

任意长度挑战

让我们用神经网络来模拟任意长度的数据序列!

本章和第十一章相互交织,我鼓励你在深入研究这一章之前,确保你已经掌握了第十一章中的概念和技术。第十一章中,你学习了关于自然语言处理(NLP)的内容。这包括如何修改损失函数来学习神经网络权重中的特定信息模式。你还培养了对词嵌入的理解,以及它如何与其他词嵌入表示相似度的细微差别。在本章中,我们将通过创建能够传达可变长度短语和句子意义的嵌入来扩展这种对嵌入传达单个词语意义的直觉。

让我们首先考虑这个挑战。如果你想要创建一个包含整个符号序列的向量,就像词嵌入存储关于一个词的信息一样,你会如何实现?我们将从最简单的方法开始。从理论上讲,如果你连接或堆叠词嵌入,你将得到一种类型的向量,它包含整个符号序列。

图片

但这种方法仍有不足之处,因为不同的句子会有不同长度的向量。这使得比较两个向量变得困难,因为其中一个向量会突出出来。考虑以下第二句话:

图片

理论上,这两个句子应该非常相似,比较它们的向量应该显示出高度的相似性。但是因为“the cat sat”是一个较短的向量,你必须选择“the cat sat still”向量中的哪一部分进行比较。如果你从左边对齐,向量看起来将完全相同(忽略“the cat sat still”实际上是一个不同的句子这一事实)。但是如果你从右边对齐,那么向量看起来将非常不同,尽管四分之三的单词是相同的,并且顺序相同。尽管这种朴素的方法显示出一些希望,但在以有用方式(可以与其他向量进行比较的方式)表示句子意义方面还远远不够理想。

比较真的重要吗?

为什么你应该关心你是否可以比较两个句子向量?

比较两个向量的行为很有用,因为它可以近似地反映出神经网络所看到的内容。即使你不能直接读取两个向量,你也能判断它们是相似还是不同(使用第十一章中的函数 chapter 11)。如果生成句子向量的方法没有反映出你观察到的两个句子之间的相似性,那么网络在识别两个句子相似时也会遇到困难。它所需要处理的只有向量!

当我们继续迭代并评估计算句子向量的各种方法时,我想让你记住我们为什么要这样做。我们试图从神经网络的视角出发。我们问,“相关性总结是否会找到与这个句子向量类似的句子和期望标签之间的相关性,或者两个几乎相同的句子会产生截然不同的向量,使得句子向量和相应的标签之间的相关性非常小?”我们希望创建对预测句子中的事物有用的句子向量,这至少意味着相似的句子需要创建相似的向量。

之前创建句子向量的方法(连接)存在问题,因为它们对齐的方式相当任意,所以让我们探索下一个最简单的方法。如果你取句子中每个单词的向量并取平均值会怎样?嗯,一开始,你不必担心对齐问题,因为每个句子向量长度相同!

图片

此外,“the cat sat”和“the cat sat still”这两个句子将会有相似的句子向量,因为进入它们的单词是相似的。更好的是,“a dog walked”可能和“the cat sat”相似,即使没有单词重叠,因为使用的单词也是相似的。

实际上,平均词嵌入是一种出奇有效的创建词嵌入的方法。它并不完美(正如你将看到的),但它很好地捕捉了你可能感知到的词语之间复杂关系的各个方面。在继续之前,我认为从第十一章中提取词嵌入并尝试平均策略将非常有益。

平均词向量的惊人力量

它是神经预测中强大而常用的工具

在上一节中,我提出了创建表示一系列词语意义的向量的第二种方法。这种方法取句子中对应词语的向量的平均值,直观地,我们期望这些新的平均句子向量以几种期望的方式表现。

在本节中,让我们使用上一章生成的嵌入来玩句子向量。将第十一章中的代码提取出来,像之前一样在 IMDB 语料库上训练嵌入,然后让我们实验平均句子嵌入。

在右侧是之前比较词嵌入时执行的同一次归一化。但这次,让我们将所有词嵌入预先归一化到一个称为normed_weights的矩阵中。然后,创建一个名为make_sent_vect的函数,并使用它通过平均方法将每个评论(单词列表)转换为嵌入。这存储在矩阵reviews2vectors中。

import numpy as np
norms = np.sum(weights_0_1 * weights_0_1,axis=1)
norms.resize(norms.shape[0],1)
normed_weights = weights_0_1 * norms

def make_sent_vect(words):
  indices = list(map(lambda x:word2index[x],\
        filter(lambda x:x in word2index,words)))
  return np.mean(normed_weights[indices],axis=0)

reviews2vectors = list()
for review in tokens:                           *1*
  reviews2vectors.append(make_sent_vect(review))
reviews2vectors = np.array(reviews2vectors)

def most_similar_reviews(review):
  v = make_sent_vect(review)
  scores = Counter()
  for i,val in enumerate(reviews2vectors.dot(v)):
    scores[i] = val
  most_similar = list()

  for idx,score in scores.most_common(3):
    most_similar.append(raw_reviews[idx][0:40])
  return most_similar
most_similar_reviews(['boring','awful'])

     ['I am amazed at how boring this film',
      'This is truly one of the worst dep',
      'It just seemed to go on and on and.]
  • 1 分词评论

在此之后,你将创建一个函数,通过在输入评论的向量与语料库中每个其他评论的向量之间执行点积,来查询给定输入评论的最相似评论。这种点积相似度指标与我们之前在第四章中简要讨论的相同,当时你正在学习使用多个输入进行预测。

可能令人惊讶的是,当你查询两个词“无聊”和“糟糕”之间的平均向量最相似的评论时,你收到了三个非常负面的评论。似乎在这些向量中存在有趣的统计信息,使得负面和正面的嵌入聚集在一起。

这些嵌入中信息是如何存储的?

当你平均词嵌入时,平均形状保持不变

考虑这里发生的事情需要一点抽象思维。我建议你花一段时间消化这类信息,因为它可能与你习惯的教训不同。暂时,我想让你考虑一个词向量可以像这样被可视化为一条波浪线

图片

不要将向量视为数字列表,而要将其视为一条有高点和低点的线,这些高点低点对应于向量中不同位置的高值和低值。如果你从语料库中选择了几个词,它们可能看起来像这样:

考虑各种单词之间的相似性。注意,每个向量的对应形状是唯一的。但“糟糕”和“无聊”在形状上具有一定的相似性。“美丽”和“奇妙”也与它们的形状相似,但与其它单词不同。如果我们对这些小波浪线进行聚类,具有相似意义的单词会聚在一起。更重要的是,这些波浪线的一部分本身就有真正的意义。

图片

例如,对于负面词汇,从左侧大约 40%的位置有一个向下然后向上的尖峰。如果我要继续绘制与单词对应的线条,这个尖峰将继续保持独特。那个尖峰并没有什么神奇之处意味着“负面”,如果我重新训练网络,它可能会出现在其他地方。这个尖峰仅表明负面,因为所有负面词汇都有这个特征!

因此,在训练过程中,这些形状被塑造成不同的曲线在不同位置传达意义(如第十一章所述)。当你对一个句子中的单词取平均曲线时,句子的最主导意义是真实的,而任何特定单词产生的噪声则被平均掉。

神经网络如何使用嵌入?

神经网络检测与目标标签相关的曲线

你已经了解到一种将词嵌入视为具有独特性质(曲线)的波浪线的新方法。你还了解到,这些曲线是在训练过程中逐步发展以实现目标目标的。在某种意义上相似意义的单词通常会共享曲线上的一个独特弯曲:权重中的高低模式组合。在本节中,我们将考虑相关性总结过程如何将这些曲线作为输入进行处理。对于一层来说,将这些曲线作为输入意味着什么呢?

实际上,神经网络消费嵌入的方式就像它在本书早期章节中消费街灯数据集一样。它寻找隐藏层中各种凹凸和曲线与它试图预测的目标标签之间的相关性。这就是为什么具有特定相似方面的单词会共享相似的凹凸和曲线。在训练过程中某个时刻,神经网络开始发展不同单词形状之间的独特特征,以便将其区分开来,并将它们分组(给予它们相似的凹凸/曲线),以帮助做出准确的预测。但这又是总结第十一章末尾教训的另一种方式。我们希望进一步探讨。

在本章中,我们将探讨将这些嵌入求和成一个句子嵌入的含义。这种求和向量对哪些类型的分类会有用?我们已经确定,对句子中所有单词嵌入取平均会得到一个具有句子中所有单词特征平均值的向量。如果有很多积极词汇,最终的嵌入将看起来有些积极(其他单词的噪声通常相互抵消)。但请注意,这种方法有点模糊:给定足够多的单词,这些不同的波浪线都应该平均在一起,通常变成一条直线。

这引出了这种方法的第一大弱点:当试图将任意长度的信息序列(一个句子)存储到固定长度的向量中时,如果你试图存储太多,最终句子向量(作为众多单词向量的平均值)将平均成一个直线(接近 0 的向量)。

简而言之,这个过程存储句子信息的方式并不优雅。如果你试图将太多单词存储到单个向量中,最终你几乎什么都没有存储。话虽如此,一个句子通常不会有很多单词;如果一个句子有重复的模式,这些句子向量可能是有用的,因为句子向量将保留被求和的单词向量中最占主导地位的图案(例如,前一部分中的负峰值)。

词袋向量的局限性

当你对单词嵌入取平均时,顺序变得无关紧要

平均嵌入的最大问题是它们没有顺序的概念。例如,考虑两个句子“Yankees defeat Red Sox”和“Red Sox defeat Yankees”。使用平均方法为这两个句子生成句子向量将得到相同的向量,但这两个句子传达的信息正好相反!此外,这种方法忽略了语法和句法,所以“Red Sox Yankees defeat Sox”也会得到相同的句子嵌入。

将单词嵌入求和或平均以形成短语或句子嵌入的方法在经典上被称为“词袋”方法,因为这与把一堆单词扔进一个袋子类似,顺序没有被保留。关键限制是你可以取任何句子,打乱所有单词的顺序,并生成一个句子向量,无论你如何打乱单词,向量都会相同(因为加法是结合的:a + b == b + a)。

本章的真正主题是以一种顺序确实重要的方式生成句子向量。我们希望创建的向量是,打乱它们的顺序会改变结果向量。更重要的是,顺序重要性的方式(也称为顺序改变向量的方式)应该被学习。这样,神经网络对顺序的表示就可以围绕尝试解决语言任务来构建,并且通过扩展,希望捕捉到语言中顺序的本质。我在这里使用语言作为例子,但你可以将这些陈述推广到任何序列。语言只是一个特别具有挑战性但普遍为人所知的领域。

生成序列(如句子)向量的最著名和最成功的方法之一是循环神经网络(RNN)。为了向您展示它是如何工作的,我们将首先提出一种新的、看似浪费的方法来使用所谓的单位矩阵进行平均词嵌入。单位矩阵只是一个任意大的正方形矩阵(行数等于列数),其中 0 填充,从左上角到右下角有 1。

这三个矩阵都是单位矩阵,它们有一个目的:与任何向量进行向量-矩阵乘法都会返回原始向量。如果我将向量 [3,5] 乘以顶部的单位矩阵,结果将是 [3,5]

  [1,0]
  [0,1]

 [1,0,0]
 [0,1,0]
 [0,0,1]

[1,0,0,0]
[0,1,0,0]
[0,0,1,0]
[0,0,0,1]

使用单位向量求和词嵌入

让我们使用不同的方法实现相同的逻辑

你可能会认为单位矩阵没有用。一个将向量输入并输出相同向量的矩阵有什么用?在这种情况下,我们将将其用作教学工具,展示如何设置一种更复杂的方法来求和词嵌入,以便神经网络在生成最终的句子嵌入时可以考虑到顺序。让我们探索另一种求和嵌入的方法。

图片

这是将多个词嵌入相加形成句子嵌入(除以单词数量得到平均句子嵌入)的标准技术。右侧的示例在每次求和之间增加了一步:通过单位矩阵进行向量-矩阵乘法。

“红色”的向量乘以单位矩阵,然后将输出与“索克斯”的向量相加,接着将“索克斯”的向量通过单位矩阵进行向量-矩阵乘法并加到“击败”的向量上,以此类推,直到整个句子。注意,因为通过单位矩阵进行向量-矩阵乘法返回的是相同的向量,所以右侧的过程与左上角的过程产生完全相同的句子嵌入

图片

是的,这确实是浪费计算,但这种情况即将改变。这里要考虑的主要问题是,如果使用的矩阵不是单位矩阵,那么改变单词的顺序将改变生成的嵌入。让我们用 Python 来看看这个例子。

完全不改变任何东西的矩阵

让我们用 Python 使用单位矩阵创建句子嵌入

在本节中,我们将演示如何在 Python 中玩转单位矩阵,并最终实现上一节中提到的新句子向量技术(证明它产生相同的句子嵌入)。

在右侧,我们首先初始化长度为 3 的四个向量(abcd),以及一个三行三列的单位矩阵(单位矩阵总是方阵)。请注意,单位矩阵具有从左上角到右下角的对角线上的 1(顺便说一下,这在线性代数中被称为对角线)。任何对角线上有 1 而其他地方都是 0 的方阵都是单位矩阵。

图片

我们然后继续对每个向量与单位矩阵进行向量矩阵乘法(使用 NumPy 的点函数)。如您所见,此过程的输出是一个与输入向量相同的新向量。

由于单位矩阵与向量的矩阵乘法返回相同的向量,将此过程纳入句子嵌入应该看起来很简单,确实如此:

this = np.array([2,4,6])
movie = np.array([10,10,10])
rocks = np.array([1,1,1])

print(this + movie + rocks)
print((this.dot(identity) + movie).dot(identity) + rocks)
[13 15 17]
[ 13\.  15\.  17.]

两种创建句子嵌入的方法都会生成相同的向量。这仅仅是因为单位矩阵是一种非常特殊的矩阵。但如果我们不使用单位矩阵会怎样呢?如果我们使用不同的矩阵会怎样呢?实际上,单位矩阵是唯一保证返回与它进行向量矩阵乘法相同的向量的矩阵。没有其他矩阵有这样的保证。

学习转换矩阵

如果允许单位矩阵改变以最小化损失会怎样?

在我们开始之前,让我们记住目标:生成根据句子意义聚类的句子嵌入,这样给定一个句子,我们可以使用向量找到具有相似意义的句子。更具体地说,这些句子嵌入应该关注单词的顺序。

之前,我们尝试过将词嵌入相加。但这意味着“红袜队击败洋基队”与句子“洋基队击败红袜队”具有相同的向量,尽管这两个句子具有相反的意义。相反,我们希望形成句子嵌入,使得这两个句子生成不同的嵌入(但仍以有意义的方式进行聚类)。理论上是这样的,如果我们使用单位矩阵创建句子嵌入的方式,但使用任何除单位矩阵之外的矩阵,句子嵌入将根据顺序的不同而不同。

现在显然的问题是:用哪个矩阵代替单位矩阵。有无限多的选择。但在深度学习中,这类问题的标准答案是,“你将像学习神经网络中的任何其他矩阵一样学习这个矩阵!”好吧,所以你将只学习这个矩阵。怎么学?

每当你想要训练一个神经网络学习某样东西时,你总是需要给它一个学习任务。在这种情况下,这个任务应该要求它通过学习有用的词向量和有用的单位矩阵修改来生成有趣的句子嵌入。你应该使用什么任务?

当你想要生成有用的词嵌入(填空)时,目标相似。让我们尝试完成一个非常类似的任务:训练一个神经网络,使其能够接受一系列单词并尝试预测下一个单词。

学习创建有用的句子向量

创建句子向量,进行预测,并通过其部分修改句子向量

在这个下一个实验中,我不想让你像以前那样思考网络。相反,考虑创建一个句子嵌入,使用它来预测下一个单词,然后修改形成句子嵌入的相关部分,以使这个预测更准确。因为你正在预测下一个单词,所以句子嵌入将由你迄今为止看到的句子部分组成。神经网络将类似于图中的样子。

它由两个步骤组成:创建句子嵌入,然后使用该嵌入来预测下一个单词。这个网络的输入是文本“Red Sox defeat”,要预测的单词是“Yankees”。

我在词向量之间的框中写下了单位矩阵。这个矩阵最初将只是一个单位矩阵。在训练过程中,你将反向传播梯度到这些矩阵中,并更新它们以帮助网络做出更好的预测(就像网络中其余的权重一样)。

这样,网络将学会如何整合比词嵌入总和更多的信息。通过允许(最初是单位矩阵的)矩阵发生变化(并成为不是单位矩阵),你让神经网络学会如何创建嵌入,其中单词呈现的顺序会改变句子嵌入。但这种变化不是任意的。网络将学会以对预测下一个单词的任务有用的方式整合单词的顺序。

你还将转换矩阵(最初是单位矩阵的矩阵)约束为都是同一个矩阵。换句话说,从“Red”到“Sox”的矩阵将被重新用于从“Sox”到“defeat”的转换。网络在一个转换中学到的任何逻辑都将被用于下一个转换,并且只有对每个预测步骤有用的逻辑才允许在网络中学习。

Python 中的正向传播

让我们看看如何执行简单的正向传播

现在你已经对想要构建的概念有了概念上的理解,让我们来看看 Python 中的玩具版本。首先,让我们设置权重(我使用了一个包含九个单词的有限词汇量):

import numpy as np

def softmax(x_):
    x = np.atleast_2d(x_)
    temp = np.exp(x)
    return temp / np.sum(temp, axis=1, keepdims=True)

word_vects = {}
word_vects['yankees'] = np.array([[0.,0.,0.]])   *1*
word_vects['bears'] = np.array([[0.,0.,0.]])     *1*
word_vects['braves'] = np.array([[0.,0.,0.]])    *1*
word_vects['red'] = np.array([[0.,0.,0.]])       *1*
word_vects['sox'] = np.array([[0.,0.,0.]])       *1*
word_vects['lose'] = np.array([[0.,0.,0.]])      *1*
word_vects['defeat'] = np.array([[0.,0.,0.]])    *1*
word_vects['beat'] = np.array([[0.,0.,0.]])      *1*
word_vects['tie'] = np.array([[0.,0.,0.]])       *1*

sent2output = np.random.rand(3,len(word_vects))  *2*

identity = np.eye(3)                             *3*
  • 1 词嵌入

  • 2 句子嵌入以输出分类权重

  • 3 转换权重

这段代码创建了三组权重。它创建了一个包含词嵌入、单位矩阵(转换矩阵)和分类层的 Python 字典。这个分类层sent2output是一个权重矩阵,用于根据长度为 3 的句子向量预测下一个单词。有了这些工具,正向传播变得非常简单。这是如何使用句子“red sox defeat” -> “yankees”进行正向传播的:

layer_0 = word_vects['red']                             *1*
layer_1 = layer_0.dot(identity) + word_vects['sox']     *1*
layer_2 = layer_1.dot(identity) + word_vects['defeat']  *1*

pred = softmax(layer_2.dot(sent2output))                *2*
print(pred)                                             *2*
  • 1 创建句子嵌入

  • 2 在整个词汇表上预测

[[ 0.11111111  0.11111111  0.11111111  0.11111111  0.11111111  0.11111111
   0.11111111  0.11111111  0.11111111]]

你如何进行反向传播?

这可能看起来有点复杂,但它们是你已经学过的相同步骤

你刚刚看到了如何进行这个网络的正向预测。一开始,可能不清楚如何进行反向传播。但它很简单。也许你会看到:

layer_0 = word_vects['red']                             *1*
layer_1 = layer_0.dot(identity) + word_vects['sox']     *2*
layer_2 = layer_1.dot(identity) + word_vects['defeat']

pred = softmax(layer_2.dot(sent2output))                *3*
print(pred)                                             *3*
  • 1 正常神经网络 (第一章–第五章)

  • 2 一些奇怪的附加部分

  • 3 再次是正常神经网络 (第九章的内容)

根据前面的章节,你应该对计算损失和反向传播直到得到layer_2的梯度,即layer_2_delta感到舒适。在这个时候,你可能想知道,“我应该向哪个方向反向传播?”梯度可以通过通过identity矩阵乘法向后传播回到layer_1,或者它们可以进入word_vects['defeat']

当你在正向传播过程中将两个向量相加时,你将相同的梯度反向传播到加法运算的两边。当你生成layer_2_delta时,你将反向传播两次:一次通过单位矩阵创建layer_1_delta,然后再次传播到word_vects['defeat']

y = np.array([1,0,0,0,0,0,0,0,0])                   *1*

pred_delta = pred - y
layer_2_delta = pred_delta.dot(sent2output.T)
defeat_delta = layer_2_delta * 1                    *2*
layer_1_delta = layer_2_delta.dot(identity.T)
sox_delta = layer_1_delta * 1                       *3*
layer_0_delta = layer_1_delta.dot(identity.T)
alpha = 0.01
word_vects['red'] -= layer_0_delta * alpha
word_vects['sox'] -= sox_delta * alpha
word_vects['defeat'] -= defeat_delta * alpha
identity -= np.outer(layer_0,layer_1_delta) * alpha
identity -= np.outer(layer_1,layer_2_delta) * alpha
sent2output -= np.outer(layer_2,pred_delta) * alpha
  • 1 针对的是“yankees”的单热向量

  • 2 可以忽略第十一章中的“1”

  • 3 再次,可以忽略“1”

让我们开始训练它!

你已经拥有了所有工具;让我们在玩具语料库上训练网络

为了让你对正在发生的事情有一个直观的了解,让我们首先在一个称为 Babi 数据集的玩具任务上训练新的网络。这个数据集是一个合成的问答语料库,用于教机器如何回答关于环境的简单问题。你现在还没有用它来进行问答(还没有),但任务的简单性将帮助你更好地看到学习单位矩阵带来的影响。首先,下载 Babi 数据集。以下是 bash 命令:

wget http://www.thespermwhale.com/jaseweston/babi/tasks_1-20_v1-1.tar.gz
tar -xvf tasks_1-20_v1-1.tar.gz

使用一些简单的 Python,你可以打开并清理一个小数据集来训练网络:

import sys,random,math
from collections import Counter
import numpy as np

f = open('tasksv11/en/qa1_single-supporting-fact_train.txt','r')
raw = f.readlines()
f.close()

tokens = list()
for line in raw[0:1000]:
    tokens.append(line.lower().replace("\n","").split(" ")[1:])

print(tokens[0:3])

[['Mary', 'moved', 'to', 'the', 'bathroom'],
 ['John', 'went', 'to', 'the', 'hallway'],
 ['Where', 'is', 'Mary', 'bathroom'],

如你所见,这个数据集包含各种简单的陈述和问题(已移除标点符号)。每个问题后面都跟着正确的答案。在问答(QA)的上下文中,神经网络按顺序读取陈述并基于最近读取的陈述中的信息回答问题(要么正确要么错误)。

目前,你将训练网络尝试在给定一个或多个起始单词的情况下完成每个句子。在这个过程中,你会看到允许循环矩阵(之前是单位矩阵)学习的重要性。

设置环境

在你能够创建矩阵之前,你需要了解你有多少个参数

与词嵌入神经网络一样,你首先需要创建一些有用的计数、列表和实用函数,以便在预测、比较、学习过程中使用。这些实用函数和对象在此处显示,应该看起来很熟悉:

vocab = set()                     def words2indices(sentence):
for sent in tokens:                   idx = list()
    for word in sent:                 for word in sentence:
        vocab.add(word)                   idx.append(word2index[word])
                                      return idx
vocab = list(vocab)

word2index = {}                   def softmax(x):
for i,word in enumerate(vocab):       e_x = np.exp(x - np.max(x))
    word2index[word]=i                return e_x / e_x.sum(axis=0)

在左侧,你创建了一个简单的词汇表单词列表以及一个查找字典,允许你在单词的文本和其索引之间来回转换。你将使用词汇表列表中的索引来选择嵌入和预测矩阵的哪一行和哪一列对应于哪个单词。在右侧是一个将单词列表转换为索引列表的实用函数,以及用于softmax的函数,你将使用它来预测下一个单词。

以下代码初始化了随机种子(以获得一致的结果),然后将嵌入大小设置为 10。你创建了一个词嵌入矩阵、循环嵌入矩阵以及一个初始的start嵌入。这是表示空短语的嵌入,这对于网络模拟句子倾向于如何开始至关重要。最后,还有一个decoder权重矩阵(就像嵌入一样)和一个one_hot实用矩阵:

np.random.seed(1)
embed_size = 10

embed = (np.random.rand(len(vocab),embed_size) - 0.5) * 0.1    *1*

recurrent = np.eye(embed_size)                                 *2*

start = np.zeros(embed_size)                                   *3*

decoder = (np.random.rand(embed_size, len(vocab)) - 0.5) * 0.1 *4*

one_hot = np.eye(len(vocab))                                   *5*
  • 1 Word embeddings

  • 2 Embedding -> embedding (initially the identity matrix)

  • 3 Sentence embedding for an empty sentence

  • 4 Embedding -> output weights

  • 5 One-hot lookups (for the loss function)

前向传播任意长度

你将使用前面描述的相同逻辑进行前向传播

以下代码包含前向传播和预测下一个单词的逻辑。请注意,尽管构建可能感觉不熟悉,但它遵循与之前相同的程序,在求和嵌入的同时使用单位矩阵。在这里,单位矩阵被替换为一个名为recurrent的矩阵,它被初始化为全 0(并且将通过训练来学习)。

此外,你不仅预测最后一个单词,而且在每个时间步长都基于前一个单词生成的嵌入进行预测(layer['pred'])。这比每次想要预测新词时都从短语的开头进行新的前向传播更有效。

def predict(sent):

    layers = list()
    layer = {}
    layer['hidden'] = start
    layers.append(layer)

    loss = 0

    preds = list()                                                   *1*
    for target_i in range(len(sent)):

        layer = {}

        layer['pred'] = softmax(layers[-1]['hidden'].dot(decoder))   *2*

        loss += -np.log(layer['pred'][sent[target_i]])

        layer['hidden'] = layers[-1]['hidden'].dot(recurrent) +\     *3*
                                              embed[sent[target_i]]
        layers.append(layer)
    return layers, loss
  • 1 Forward propagates

  • 2 Tries to predict the next term

  • 3 Generates the next hidden state

与你过去学到的内容相比,这段代码并没有什么特别之处,但有一个特定的部分我想确保你在我们继续前进之前熟悉。名为 layers 的列表是一种新的正向传播方式。

注意,如果 sent 的长度较大,你最终会进行更多的正向传播。因此,你不能再像以前那样使用静态层变量。这次,你需要根据所需数量不断向列表中添加新层。确保你对这个列表的每一部分都感到舒适,因为如果在正向传播过程中你不熟悉它,那么在反向传播和权重更新步骤中了解情况将会非常困难。

随机长度反向传播

你将使用前面描述的相同逻辑进行反向传播

正如“红袜队击败洋基队”示例中所述,让我们实现任意长度序列的反向传播,假设你有权访问上一节中函数返回的前向传播对象。最重要的对象是 layers 列表,它包含两个向量(layer['state']layer['previous->hidden'])。

为了进行反向传播,你需要将输出梯度添加到每个列表中一个新的对象,称为 layer['state_delta'],它将代表该层的梯度。这对应于“红袜队击败洋基队”示例中的 sox_deltalayer_0_deltadefeat_delta 等变量。你正在以这种方式构建相同的逻辑,使其能够消费正向传播逻辑中的变长序列。

for iter in range(30000):                                                  *1*
    alpha = 0.001
    sent = words2indices(tokens[iter%len(tokens)][1:])
    layers,loss = predict(sent)

      for layer_idx in reversed(range(len(layers))):                       *2*
          layer = layers[layer_idx]
          target = sent[layer_idx-1]
             if(layer_idx > 0):                                            *3*
                 layer['output_delta'] = layer['pred'] - one_hot[target]
                 new_hidden_delta = layer['output_delta']\
                                                 .dot(decoder.transpose())

                 if(layer_idx == len(layers)-1):                           *4*
                     layer['hidden_delta'] = new_hidden_delta
                 else:
                     layer['hidden_delta'] = new_hidden_delta + \
                     layers[layer_idx+1]['hidden_delta']\
                                             .dot(recurrent.transpose())
             else: # if the first layer
                 layer['hidden_delta'] = layers[layer_idx+1]['hidden_delta']\
                                                 .dot(recurrent.transpose())
  • 1 正向

  • 2 反向传播

  • 3 如果不是第一层

  • 4 如果是最后一层,不要从后面拉取,因为它不存在

在进入下一节之前,确保你能阅读这段代码并向朋友(或者至少是自己)解释它。这段代码中没有新的概念,但它的构建可能一开始会让你觉得有些陌生。花点时间将这段代码中写的内容与“红袜队击败洋基队”示例中的每一行联系起来,你应该为下一节和更新使用反向传播得到的梯度权重做好准备。

随机长度权重更新

你将使用前面描述的相同逻辑来更新权重

与正向和反向传播逻辑一样,这个权重更新逻辑并不新颖。但我在解释了它之后才提出它,这样你可以专注于工程复杂性,希望你已经(可能)已经理解了理论复杂性。

for iter in range(30000):                                              *1*
    alpha = 0.001
    sent = words2indices(tokens[iter%len(tokens)][1:])

    layers,loss = predict(sent)

    for layer_idx in reversed(range(len(layers))):                     *2*
        layer = layers[layer_idx]
        target = sent[layer_idx-1]

        if(layer_idx > 0):
            layer['output_delta'] = layer['pred'] - one_hot[target]
            new_hidden_delta = layer['output_delta']\
                                            .dot(decoder.transpose())

            if(layer_idx == len(layers)-1):                            *3*
                layer['hidden_delta'] = new_hidden_delta
            else:
                layer['hidden_delta'] = new_hidden_delta + \
                layers[layer_idx+1]['hidden_delta']\
                                        .dot(recurrent.transpose())
        else:
            layer['hidden_delta'] = layers[layer_idx+1]['hidden_delta']\
                                            .dot(recurrent.transpose())

    start -= layers[0]['hidden_delta'] * alpha / float(len(sent))      *4*
    for layer_idx,layer in enumerate(layers[1:]):

        decoder -= np.outer(layers[layer_idx]['hidden'],\
                        layer['output_delta']) * alpha / float(len(sent))

        embed_idx = sent[layer_idx]
        embed[embed_idx] -= layers[layer_idx]['hidden_delta'] * \
                                                       alpha / float(len(sent))
        recurrent -= np.outer(layers[layer_idx]['hidden'],\
                        layer['hidden_delta']) * alpha / float(len(sent))

    if(iter % 1000 == 0):
        print("Perplexity:" + str(np.exp(loss/len(sent))))
  • 1 正向

  • 2 反向传播

  • 3 如果是最后一层,不要从后面拉取,因为它不存在

  • 4 更新权重

执行和输出分析

你将使用前面描述的相同逻辑来更新权重

现在是真相大白的时候:当你运行它时会发生什么?嗯,当我运行这段代码时,我得到了一个被称为 困惑度 的指标的相对稳定的下降趋势。技术上讲,困惑度是通过一个对数函数传递的正确标签(单词)的概率,取反,然后指数化(e^x)。

但从理论上讲,它代表了两个概率分布之间的差异。在这种情况下,完美的概率分布将是 100%的概率分配给正确的术语,而其他地方都是 0%。

当两个概率分布不匹配时,困惑度会很高,而当它们匹配时,困惑度会很低(接近 1)。因此,像所有与随机梯度下降一起使用的损失函数一样,困惑度的下降是一个好现象!这意味着网络正在学习预测与数据匹配的概率。

                        Perplexity:82.09227500075585
                        Perplexity:81.87615610433569
                        Perplexity:81.53705034457951
                                    ....
                        Perplexity:4.132556753967558
                        Perplexity:4.071667181580819
                        Perplexity:4.0167814473718435

但这几乎不能告诉你权重中发生了什么。困惑度多年来一直受到一些批评(尤其是在语言建模社区中),因为它被过度用作一个指标。让我们更仔细地看看预测:

sent_index = 4

l,_ = predict(words2indices(tokens[sent_index]))

print(tokens[sent_index])

for i,each_layer in enumerate(l[1:-1]):
    input = tokens[sent_index][i]
    true = tokens[sent_index][i+1]
    pred = vocab[each_layer['pred'].argmax()]
    print("Prev Input:" + input + (' ' * (12 - len(input))) +\
          "True:" + true + (" " * (15 - len(true))) + "Pred:" + pred)

这段代码接受一个句子,并预测模型认为最可能的单词。这很有用,因为它可以让你对模型所具有的特征有一个感觉。它做对了哪些事情?它犯了哪些错误?你将在下一节中看到。

查看预测可以帮助你了解正在发生的事情

你可以查看神经网络在训练过程中学习时的输出预测,这不仅可以帮助你了解它所选择的模式类型,还可以了解它学习这些模式的顺序。经过 100 个训练步骤后,输出看起来是这样的:

['sandra', 'moved', 'to', 'the', 'garden.']
Prev Input:sandra      True:moved          Pred:is
Prev Input:moved       True:to             Pred:kitchen
Prev Input:to          True:the            Pred:bedroom
Prev Input:the         True:garden.        Pred:office

神经网络往往是从随机开始的。在这种情况下,神经网络可能只偏向于它在第一次随机状态中开始的任何单词。让我们继续训练:

['sandra', 'moved', 'to', 'the', 'garden.']
Prev Input:sandra      True:moved          Pred:the
Prev Input:moved       True:to             Pred:the
Prev Input:to          True:the            Pred:the
Prev Input:the         True:garden.        Pred:the

经过 10,000 个训练步骤后,神经网络挑选出最常见的单词(“the”)并在每个时间步预测它。这是循环神经网络中一个非常常见的错误。在高度倾斜的数据集中学习更精细的细节需要大量的训练。

['sandra', 'moved', 'to', 'the', 'garden.']
Prev Input:sandra      True:moved          Pred:is
Prev Input:moved       True:to             Pred:to
Prev Input:to          True:the            Pred:the
Prev Input:the         True:garden.        Pred:bedroom.

这些错误真的很有趣。在只看到单词“sandra”之后,网络预测了“is”,虽然并不完全等同于“moved”,但也不是一个糟糕的猜测。它选择了错误的动词。接下来,注意“to”和“the”这两个词是正确的,这并不令人惊讶,因为这些是数据集中的一些更常见的单词,并且据推测,网络已经被训练了许多次来预测在动词“moved”之后的短语“to the”。最后的错误也很引人注目,将“bedroom”误认为是单词“garden”。

重要的是要注意,几乎没有任何方法可以让这个神经网络完美地学习这个任务。毕竟,如果我只给你单词“sandra moved to the”,你能告诉我正确的下一个单词吗?需要更多的上下文来解决这个任务,但在我看来,这个任务无法解决,这实际上为分析它失败的方式提供了教育意义。

摘要

循环神经网络预测任意长度的序列

在本章中,你学习了如何为任意长度的序列创建向量表示。最后一个练习训练了一个线性循环神经网络,根据之前的一串术语预测下一个术语。为此,它需要学习如何创建能够将可变长度的术语字符串准确表示为固定大小向量的嵌入。

这最后一句话应该引发一个问题:神经网络如何将可变数量的信息拟合到固定大小的盒子中?事实是,句子向量并没有编码句子中的所有内容。循环神经网络的游戏规则不仅仅是这些向量记得什么,还包括它们忘记了什么。在预测下一个单词的情况下,大多数 RNN 学习到只有最后几个单词真正必要,^([*])并且它们学会了忘记(即在它们的向量中不形成独特模式)历史中更早的单词。

^*

例如,参见 Michał Daniluk 等人撰写的“Frustratingly Short Attention Spans in Neural Language Modeling”(在 2017 年 ICLR 会议上发表的论文),arxiv.org/abs/1702.04521

但请注意,这些表示的生成过程中没有非线性。你认为这会带来什么样的局限性?在下一章中,我们将使用非线性性和门来形成一个称为长短期记忆网络(LSTM)的神经网络,来探讨这个问题以及其他问题。但首先,确保你能坐下来(从记忆中)编写一个能够收敛的工作线性 RNN。这些网络的动力和控制流可能有点令人畏惧,复杂性即将大幅增加。在继续之前,熟悉一下本章所学的内容。

就这样,让我们深入探讨 LSTMs!

第十三章:介绍自动优化:让我们构建一个深度学习框架

本章

  • 什么是深度学习框架?

  • 张量简介

  • 自动求导简介

  • 加法反向传播是如何工作的?

  • 如何学习一个框架

  • 非线性层

  • 嵌入层

  • 交叉熵层

  • 循环层

“无论我们基于碳还是硅,在本质上都没有区别;我们都应该得到适当的尊重。”

亚瑟·C·克拉克,2010 年:《2001 太空漫游》(1982 年)

什么是深度学习框架?

好的工具可以减少错误,加快开发速度,并提高运行时性能

如果你长期关注深度学习,你可能已经遇到了一些主要的框架,例如 PyTorch、TensorFlow、Theano(最近已弃用)、Keras、Lasagne 或 DyNet。在过去几年中,框架的发展非常迅速,尽管所有框架都是免费的开源软件,但每个框架周围都有一股竞争和团结的气氛。

到目前为止,我一直在避免讨论框架的话题,因为首先,了解这些框架底层的工作原理对于你来说极其重要,这可以通过自己实现算法(从 NumPy 的零开始)来实现。但现在我们将过渡到使用框架,因为接下来你将要训练的网络——长短期记忆网络(LSTMs)——非常复杂,描述它们实现的 NumPy 代码难以阅读、使用或调试(梯度无处不在)。

深度学习框架的创建正是为了减轻这种代码复杂性。特别是如果你希望在 GPU 上训练神经网络(提供 10-100 倍的训练速度),深度学习框架可以显著减少代码复杂性(减少错误并提高开发速度),同时提高运行时性能。出于这些原因,它们在研究社区中几乎被普遍使用,对深度学习框架的深入了解将成为你成为深度学习用户或研究人员的旅程中必不可少的。

但我们不会跳入你听说过的任何深度学习框架,因为这会阻碍你了解复杂模型(如 LSTMs)底层的工作原理。相反,你将根据框架发展的最新趋势构建一个轻量级的深度学习框架。这样,你将毫无疑问地了解框架在用于复杂架构时的作用。此外,自己构建一个小框架应该会为使用实际的深度学习框架提供一个平稳的过渡,因为你已经熟悉了 API 及其底层的功能。我发现这项练习很有益,我在构建自己的框架中学到的教训在尝试调试麻烦的模型时特别有用。

框架是如何简化你的代码的?抽象地说,它消除了多次重复编写代码的需要。具体来说,深度学习框架最有益的部分是其对自动反向传播和自动优化的支持。这些功能让你只需指定模型的正向传播代码,框架会自动处理反向传播和权重更新。大多数框架甚至通过提供高级接口来简化常见的层和损失函数,使正向传播代码更容易编写。

张量简介

张量是向量和矩阵的抽象形式

到目前为止,我们一直在使用向量和矩阵作为深度学习的基本数据结构。回想一下,矩阵是一系列向量的列表,而向量是一系列标量(单个数字)的列表。张量是这种嵌套数字列表形式的抽象版本。向量是一维张量。矩阵是二维张量,更高维度的称为n维张量。因此,一个新的深度学习框架的开始是构建这种基本类型,我们将称之为Tensor

import numpy as np

class Tensor (object):

     def __init__(self, data):
         self.data = np.array(data)

     def __add__(self, other):
         return Tensor(self.data + other.data)

     def __repr__(self):
         return str(self.data.__repr__())

     def __str__(self):
         return str(self.data.__str__())

x = Tensor([1,2,3,4,5])
print(x)

    [1 2 3 4 5]

y = x + x
print(y)

    [ 2   4   6   8 10]

这是这种基本数据结构的第一个版本。请注意,它将所有数值信息存储在 NumPy 数组(self.data)中,并且支持一个张量操作(加法)。添加更多操作相对简单:在张量类上创建更多具有适当功能的功能。

自动梯度计算(autograd)简介

以前,你手动执行了反向传播。让我们让它自动化吧!

在第四章中,你学习了关于导数的内容。从那时起,你一直在为每个训练的神经网络手动计算导数。回想一下,这是通过在神经网络中向后移动来完成的:首先计算网络的输出处的梯度,然后使用该结果来计算下一个组件的导数,依此类推,直到架构中的所有权重都有正确的梯度。这种计算梯度的逻辑也可以添加到张量对象中。让我向你展示我的意思。新的代码以粗体显示:

import numpy as np

class Tensor (object):

     def __init__(self, data, creators=None, creation_op=None):
         self.data = np.array(data)
         self.creation_op = creation_op
         self.creators = creators
         self.grad = None

     def backward(self, grad):
         self.grad = grad

         if(self.creation_op == "add"):
             self.creators[0].backward(grad)
             self.creators[1].backward(grad)

     def __add__(self, other):
         return Tensor(self.data + other.data,
                       creators=[self,other],
                       creation_op="add")

     def __repr__(self):
         return str(self.data.__repr__())

     def __str__(self):
         return str(self.data.__str__())

x = Tensor([1,2,3,4,5])
y = Tensor([2,2,2,2,2])

z = x + y
z.backward(Tensor(np.array([1,1,1,1,1])))

此方法引入了两个新概念。首先,每个张量都获得两个新属性。creators是一个列表,包含用于创建当前张量的任何张量(默认为None)。因此,当两个张量xy相加时,z有两个creators,即xycreation_op是一个相关功能,它存储创建过程中使用的creators指令。因此,执行z = x + y创建了一个包含三个节点(xyz)和两个边(z -> xz -> y)的计算图。每个边都标记为creation_op add。此图允许你递归地反向传播梯度。

图片

在这个实现中引入的第一个新概念是,每当进行数学运算时,自动创建这个图。如果你对z进行了进一步的操作,图将继续与指向z的任何结果新变量相关联。

Tensor的这个版本中引入的第二个新概念是使用这个图来计算梯度。当你调用z.backward()时,它会根据应用于创建zadd)的函数,发送xy的正确梯度。查看图,你将一个梯度向量(np.array([1,1,1,1,1]))放在z上,然后它们被应用到它们的父节点上。正如你在第四章中学到的,通过加法进行反向传播意味着在反向传播时也要应用加法。在这种情况下,因为只有一个梯度要加到xy上,所以你将z上的梯度复制到xy上:

print(x.grad)
print(y.grad)
print(z.creators)
print(z.creation_op)

[1 1 1 1 1]
[1 1 1 1 1]
[array([1, 2, 3, 4, 5]), array([2, 2, 2, 2, 2])]
add

这种形式的 autograd 最优雅的部分可能是它还可以递归地工作,因为每个向量都会在其所有self.creators上调用.backward()

|

a = Tensor([1,2,3,4,5])
b = Tensor([2,2,2,2,2])
c = Tensor([5,4,3,2,1])
d = Tensor([-1,-2,-3,-4,-5])
e = a + b
f = c + d
g = e + f
g.backward(Tensor(np.array([1,1,1,1,1])))
print(a.grad)

| 输出

[1 1 1 1 1]

|

一个快速的检查点

Tensor 中的所有内容都是已经学到的知识的另一种形式

在继续之前,我想首先承认,即使考虑梯度在图形结构上流动可能感觉有点牵强或费劲,但与你已经使用过的内容相比,这并不是什么新鲜事。在关于 RNN 的上一章中,你在一个方向上进行了正向传播,然后在(虚拟图)激活上进行了反向传播。

你并没有在图形数据结构中明确编码节点和边。相反,你有一个层的列表(字典),并手动编码了正向和反向传播操作的正确顺序。现在你正在构建一个很好的接口,这样你就不需要写那么多代码了。这个接口让你可以递归地进行反向传播,而无需手动编写复杂的反向传播代码。

本章主要涉及一些理论性的内容。它主要讲述的是学习深度神经网络时常用的工程实践。特别是,在正向传播过程中构建的这种图结构被称为动态计算图,因为它是在正向传播过程中即时构建的。这种类型的 autograd 存在于较新的深度学习框架中,如 DyNet 和 PyTorch。而像 Theano 和 TensorFlow 这样的旧框架则有一个所谓的静态计算图,它是在正向传播开始之前就定义好的。

通常,动态计算图更容易编写/实验,而静态计算图由于底层的一些复杂逻辑,运行速度更快。但请注意,动态和静态框架最近正朝着中间发展,允许动态图编译为静态图(以获得更快的运行时间)或允许静态图动态构建(以获得更易实验)。从长远来看,你很可能会两者都拥有。主要区别在于正向传播是在图构建期间发生还是在图已经定义之后发生。在这本书中,我们将坚持使用动态的。

本章的主要目的是帮助您为现实世界中的深度学习做好准备,在那里您将花费 10%(或更少)的时间思考新的想法,90%的时间用于弄清楚如何让深度学习框架协同工作。有时调试这些框架可能极其困难,因为大多数错误不会引发错误并打印出堆栈跟踪。大多数错误隐藏在代码中,导致网络无法按预期训练(即使它看起来似乎在训练)。

所有这些只是为了真正深入本章。当你在凌晨 2 点追逐一个优化错误,这个错误阻止你获得那个美味的最新分数时,你会很高兴你做了这件事。

被多次使用的张量

基本 autograd 有一个相当讨厌的错误。让我们把它压扁!

当前版本的Tensor只支持将反向传播到变量一次。但有时,在正向传播过程中,你会多次使用同一个张量(神经网络的权重),因此图的不同部分将反向传播梯度到同一个张量。但当前代码在反向传播到被多次使用的变量时(是多个子张量的父张量)会计算错误的梯度。我的意思如下:

a = Tensor([1,2,3,4,5])
b = Tensor([2,2,2,2,2])
c = Tensor([5,4,3,2,1])

d = a + b
e = b + c
f = d + e
f.backward(Tensor(np.array([1,1,1,1,1])))

print(b.grad.data == np.array([2,2,2,2,2]))

array([False, False, False, False, False])

在这个例子中,b变量在创建f的过程中被使用了两次。因此,其梯度应该是两个导数的总和:[2,2,2,2,2]。这里展示了由这一系列操作创建的结果图。注意现在有两个指针指向b:因此,它应该是来自ed的梯度的总和。

但当前Tensor的实现只是用前一个导数覆盖每个导数。首先,d应用其梯度,然后它被e的梯度覆盖。我们需要改变梯度写入的方式。

将 autograd 升级以支持多用途张量

添加一个新函数,并更新三个旧函数

Tensor对象的这次更新增加了两个新功能。首先,梯度可以累积,以便当变量被多次使用时,它从所有子张量接收梯度:

import numpy as np
class Tensor (object):

    def __init__(self,data,
                 autograd=False,
                 creators=None,
                 creation_op=None,
                 id=None):

        self.data = np.array(data)
        self.creators = creators
        self.creation_op = creation_op
        self.grad = None
        self.autograd = autograd
        self.children = {}
        if(id is None):
            id = np.random.randint(0,100000)
        self.id = id

        if(creators is not None):
            for c in creators:
                if(self.id not in c.children):                        *1*
                    c.children[self.id] = 1
                else:
                    c.children[self.id] += 1

    def all_children_grads_accounted_for(self):                       *2*
        for id,cnt in self.children.items():
            if(cnt != 0):
                return False
        return True

    def backward(self,grad=None, grad_origin=None):
        if(self.autograd):
            if(grad_origin is not None):
                if(self.children[grad_origin.id] == 0):               *3*
                    raise Exception("cannot backprop more than once")
                else:
                    self.children[grad_origin.id] -= 1

            if(self.grad is None):
                self.grad = grad                                      *4*
            else:
                self.grad += grad

            if(self.creators is not None and
               (self.all_children_grads_accounted_for() or
                grad_origin is None)):

                if(self.creation_op == "add"):
                    self.creators[0].backward(self.grad, self)
                    self.creators[1].backward(self.grad, self)        *5*

    def __add__(self, other):
        if(self.autograd and other.autograd):
            return Tensor(self.data + other.data,
                          autograd=True,
                          creators=[self,other],
                          creation_op="add")
        return Tensor(self.data + other.data)

    def __repr__(self):
        return str(self.data.__repr__())

    def __str__(self):
        return str(self.data.__str__())

a = Tensor([1,2,3,4,5], autograd=True)
b = Tensor([2,2,2,2,2], autograd=True)
c = Tensor([5,4,3,2,1], autograd=True)

d = a + b
e = b + c
f = d + e

f.backward(Tensor(np.array([1,1,1,1,1])))

print(b.grad.data == np.array([2,2,2,2,2]))
  • 1 跟踪张量有多少个子张量

  • 2 检查张量是否从每个子张量接收到了正确数量的梯度

  • 3 检查是否可以反向传播,或者你是否正在等待梯度,如果是,则递减计数器

  • 4 从多个子节点累积梯度

  • 5 开始实际的反向传播

[ True  True  True  True  True]

此外,你创建了一个self.children计数器,在反向传播期间计算每个子节点接收到的梯度数量。这样,你也可以防止变量意外地从同一个子节点两次进行反向传播(这会抛出异常)。

第二个新增功能是一个具有相当冗长名称的新函数all_children_grads_accounted_for()。这个函数的目的是计算一个张量是否从其图中的所有子节点接收到了梯度。通常,每当在图中的中间变量上调用.backward()时,它会立即对其父节点调用.backward()。但由于一些变量从多个父节点接收梯度值,每个变量需要等待直到它具有局部的最终梯度后,再调用.backward()对其父节点进行反向传播。

如前所述,从深度学习理论的角度来看,这些概念并不新颖;这些都是深度学习框架试图面对的工程挑战。更重要的是,它们是你在标准框架中调试神经网络时将面临的那种挑战。在继续之前,花点时间玩一下这段代码,熟悉它。尝试删除不同的部分,看看它以各种方式崩溃。尝试两次调用.backprop()

加法反向传播是如何工作的?

让我们研究这个抽象,以了解如何添加对更多函数的支持

到目前为止,这个框架已经达到了一个令人兴奋的地方!你现在可以通过将函数添加到Tensor类并添加其导数到.backward()方法来支持任意操作。对于加法,有以下方法:

def __add__(self, other):
    if(self.autograd and other.autograd):
        return Tensor(self.data + other.data,
                      autograd=True,
                      creators=[self,other],
                      creation_op="add")
    return Tensor(self.data + other.data)

对于通过加法函数的反向传播,以下是.backward()方法中的以下梯度传播:

           if(self.creation_op == "add"):
             self.creators[0].backward(self.grad, self)
             self.creators[1].backward(self.grad, self)

注意,在这个类中其他地方并没有处理加法。通用的反向传播逻辑被抽象化,所以所有必要的加法定义都包含在这两个地方。进一步注意,反向传播逻辑在每次加法中都会调用.backward()两次,一次针对每个参与加法的变量。因此,反向传播逻辑的默认设置是始终将反向传播应用于图中的每个变量。但有时,如果变量关闭了自动微分(self.autograd == False),则会跳过反向传播。这个检查是在.backward()方法中进行的:

尽管加法的反向传播逻辑将梯度反向传播到所有对其有贡献的变量,但除非将该变量的 .autograd 设置为 True,否则反向传播不会运行(对于 self.creators[0]self.creators[1] 分别)。注意,在 __add__() 的第一行中,创建的张量(稍后是 running.backward())的 self.autograd 仅当 self.autograd == other.autograd == True 时才为 True

添加对否定的支持

让我们修改支持加法以支持否定

现在加法功能已经正常工作,你应该能够复制并粘贴加法代码,进行一些修改,并为否定添加自动微分支持。让我们试试。__add__ 函数的修改内容以粗体显示:

def __neg__(self):
    if(self.autograd):
        return Tensor(self.data * -1,
                      autograd=True,
                      creators=[self],
                      creation_op="neg")
    return Tensor(self.data * -1)

几乎所有内容都是相同的。你不接受任何参数,所以“other”参数已在多个地方被移除。让我们看看你应该添加到 .backward() 中的反向传播逻辑。__add__ 函数反向传播逻辑的修改内容以粗体显示:

             if(self.creation_op == "neg"):
               self.creators[0].backward(self.grad.__neg__())

因为 __neg__ 函数只有一个创建者,所以你只需要调用一次 .backward()。如果你想知道如何知道正确的梯度进行反向传播,请回顾第 4、5 和 6 章。你现在可以测试新的代码了:

a = Tensor([1,2,3,4,5], autograd=True)
b = Tensor([2,2,2,2,2], autograd=True)
c = Tensor([5,4,3,2,1], autograd=True)

d = a + (-b)
e = (-b) + c
f = d + e

f.backward(Tensor(np.array([1,1,1,1,1])))

print(b.grad.data == np.array([-2,-2,-2,-2,-2]))

[ True  True  True  True  True]

当你使用 -b 而不是 b 进行前向传播时,反向传播的梯度也会翻转符号。此外,你不需要对一般的反向传播系统做任何修改来使其工作。你可以根据需要创建新的函数。让我们添加一些吧!

添加对其他函数的支持

减法、乘法、求和、扩展、转置和矩阵乘法

使用你为加法和否定学到的相同思想,让我们添加几个其他函数的前向和反向传播逻辑:

def __sub__(self, other):
     if(self.autograd and other.autograd):
         return Tensor(self.data - other.data,
                       autograd=True,
                       creators=[self,other],
                       creation_op="sub")
     return Tensor(self.data - other.data)

 def __mul__(self, other):
     if(self.autograd and other.autograd):
         return Tensor(self.data * other.data,
                       autograd=True,
                       creators=[self,other],
                       creation_op="mul")
     return Tensor(self.data * other.data)

 def sum(self, dim):
     if(self.autograd):
         return Tensor(self.data.sum(dim),
                        autograd=True,
                        creators=[self],
                        creation_op="sum_"+str(dim))
     return Tensor(self.data.sum(dim))

 def expand(self, dim,copies):

     trans_cmd = list(range(0,len(self.data.shape)))
     trans_cmd.insert(dim,len(self.data.shape))
     new_shape = list(self.data.shape) + [copies]
     new_data = self.data.repeat(copies).reshape(new_shape)
     new_data = new_data.transpose(trans_cmd)

     if(self.autograd):
         return Tensor(new_data,
                       autograd=True,
                       creators=[self],
                       creation_op="expand_"+str(dim))
     return Tensor(new_data)

 def transpose(self):
     if(self.autograd):
         return Tensor(self.data.transpose(),
                       autograd=True,
                       creators=[self],
                       creation_op="transpose")
     return Tensor(self.data.transpose())

 def mm(self, x):
     if(self.autograd):
         return Tensor(self.data.dot(x.data),
                       autograd=True,
                       creators=[self,x],
                       creation_op="mm")
     return Tensor(self.data.dot(x.data))

我们之前讨论了所有这些函数的导数,尽管 sumexpand 可能看起来有些陌生,因为它们有新的名称。sum 在张量的一个维度上执行加法;换句话说,假设你有一个名为 x 的 2 × 3 矩阵:

            x = Tensor(np.array([[1,2,3],
                                [4,5,6]]))

.sum(dim) 函数在一个维度上求和。x.sum(0) 将得到一个 1 × 3 矩阵(长度为 3 的向量),而 x.sum(1) 将得到一个 2 × 1 矩阵(长度为 2 的向量):

你使用 expand 来通过 .sum() 进行反向传播。这是一个复制数据沿一个维度的函数。给定相同的矩阵 x,沿第一个维度复制会得到两个张量副本:

为了明确起见, whereas .sum() 移除一个维度(2 × 3 -> 只剩 2 或 3),expand 添加一个维度。2 × 3 的矩阵变成了 4 × 2 × 3。您可以将其视为一个包含四个张量的列表,每个张量都是 2 × 3。但如果您扩展到最后一个维度,它将沿着最后一个维度复制,因此原始张量中的每个条目都变成了一个条目的列表:

因此,当您对一个在该维度有四个条目的张量执行 .sum(dim=1) 时,您需要在反向传播时对梯度执行 .expand(dim=1, copies=4)

您现在可以将相应的反向传播逻辑添加到 .backward() 方法中:

         if(self.creation_op == "sub"):
              new = Tensor(self.grad.data)
              self.creators[0].backward(new, self)
              new = Tensor(self.grad.__neg__().data)
              self.creators[1].backward(, self)

          if(self.creation_op == "mul"):
              new = self.grad * self.creators[1]
              self.creators[0].backward(new , self)
              new = self.grad * self.creators[0]
              self.creators[1].backward(new, self)

          if(self.creation_op == "mm"):
              act = self.creators[0]                    *1*
              weights = self.creators[1]                *2*
              new = self.grad.mm(weights.transpose())
              act.backward(new)
              new = self.grad.transpose().mm(act).transpose()
              weights.backward(new)

          if(self.creation_op == "transpose"):
              self.creators[0].backward(self.grad.transpose())

          if("sum" in self.creation_op):
              dim = int(self.creation_op.split("_")[1])
              ds = self.creators[0].data.shape[dim]
              self.creators[0].backward(self.grad.expand(dim,ds))

          if("expand" in self.creation_op):
              dim = int(self.creation_op.split("_")[1])
              self.creators[0].backward(self.grad.sum(dim))
  • 1 通常是一个激活

  • 2 通常是一个权重矩阵

如果您不确定这个功能,最好的做法是回顾一下您在第六章中是如何进行反向传播的。那一章有展示反向传播每个步骤的图表,其中一部分我在这里又展示了一遍。

梯度从网络的末端开始。然后您通过调用与用于将激活向前传递到网络中的函数相对应的函数,将错误信号 反向传递到网络中。如果最后一个操作是矩阵乘法(并且确实是),您通过在转置矩阵上执行矩阵乘法(点积)来进行反向传播。

在以下图像中,这发生在 layer_1_delta=layer_2_delta.dot (weights_1_2.T) 这一行。在之前的代码中,它发生在 if(self.creation_op == "mm")(加粗显示)。您正在执行与之前(按前向传播的相反顺序)完全相同的操作,但代码组织得更好。

使用 autograd 训练神经网络

您不再需要编写反向传播逻辑!

这可能看起来需要相当多的工程努力,但很快就会得到回报。现在,当您训练神经网络时,您不必编写任何反向传播逻辑!作为一个玩具示例,这里有一个用于手动反向传播的神经网络:

import numpy
np.random.seed(0)

data = np.array([[0,0],[0,1],[1,0],[1,1]])
target = np.array([[0],[1],[0],[1]])

weights_0_1 = np.random.rand(2,3)
weights_1_2 = np.random.rand(3,1)

for i in range(10):

    layer_1 = data.dot(weights_0_1)                     *1*
    layer_2 = layer_1.dot(weights_1_2)

    diff = (layer_2 - target)                           *2*
    sqdiff = (diff * diff)
    loss = sqdiff.sum(0)                                *3*
    layer_1_grad = diff.dot(weights_1_2.transpose())    *4*
    weight_1_2_update = layer_1.transpose().dot(diff)
    weight_0_1_update = data.transpose().dot(layer_1_grad)

    weights_1_2 -= weight_1_2_update * 0.1
    weights_0_1 -= weight_0_1_update * 0.1
    print(loss[0])
  • 1 预测

  • 2 比较

  • 3 均方误差损失

  • 4 学习;这是反向传播的部分。

0.4520108746468352
0.33267400101121475
0.25307308516725036
0.1969566997160743
0.15559900212801492
0.12410658864910949
0.09958132129923322
0.08019781265417164
0.06473333002675746
0.05232281719234398

您必须以这种方式进行前向传播,使得 layer_1layer_2diff 作为变量存在,因为您稍后需要它们。然后您必须将每个梯度反向传播到相应的权重矩阵,并适当地更新权重。

import numpy
np.random.seed(0)

data = Tensor(np.array([[0,0],[0,1],[1,0],[1,1]]), autograd=True)
target = Tensor(np.array([[0],[1],[0],[1]]), autograd=True)

w = list()
w.append(Tensor(np.random.rand(2,3), autograd=True))
w.append(Tensor(np.random.rand(3,1), autograd=True))

for i in range(10):

     pred = data.mm(w[0]).mm(w[1])                    *1*

     loss = ((pred - target)*(pred - target)).sum(0)  *2*

     loss.backward(Tensor(np.ones_like(loss.data)))   *3*

     for w_ in w:
         w_.data -= w_.grad.data * 0.1
         w_.grad.data *= 0

     print(loss)
  • 1 预测

  • 2 比较

  • 3 学习

但有了新潮的 autograd 系统,代码要简单得多。您不需要保留任何临时变量(因为动态图会跟踪它们),也不需要实现任何反向传播逻辑(因为 .backward() 方法处理这个)。这不仅更方便,而且您在反向传播代码中犯愚蠢错误的可能性更小,从而降低了出错的可能性!

[0.58128304]
[0.48988149]
[0.41375111]
[0.34489412]
[0.28210124]
[0.2254484]
[0.17538853]
[0.1324231]
[0.09682769]
[0.06849361]

在继续之前,我想指出这个新实现中的一个风格问题。注意,我把所有参数都放在了一个列表中,这样我就可以在执行权重更新时遍历它们。这是对下一个功能功能的一点暗示。当你有一个自动微分系统时,随机梯度下降的实现变得非常简单(它只是最后的那个for循环)。让我们试着把它也做成一个类。

添加自动优化

让我们创建一个随机梯度下降优化器

从字面上看,创建一个名为随机梯度下降优化器的东西可能听起来很困难,但实际上只是从上一个例子中复制粘贴,加上一点老式的面向对象编程:

class SGD(object):

    def __init__(self, parameters, alpha=0.1):
        self.parameters = parameters
        self.alpha = alpha

    def zero(self):
        for p in self.parameters:
            p.grad.data *= 0

    def step(self, zero=True):

        for p in self.parameters:

            p.data -= p.grad.data * self.alpha

            if(zero):
               p.grad.data *= 0

之前的神经网络进一步简化如下,与之前完全相同的结果:

import numpy
np.random.seed(0)

data = Tensor(np.array([[0,0],[0,1],[1,0],[1,1]]), autograd=True)
target = Tensor(np.array([[0],[1],[0],[1]]), autograd=True)

w = list()
w.append(Tensor(np.random.rand(2,3), autograd=True))
w.append(Tensor(np.random.rand(3,1), autograd=True))

optim = SGD(parameters=w, alpha=0.1)

for i in range(10):

    pred = data.mm(w[0]).mm(w[1])                       *1*

    loss = ((pred - target)*(pred - target)).sum(0)     *2*

    loss.backward(Tensor(np.ones_like(loss.data)))      *3*
    optim.step()
  • 1 预测

  • 2 比较

  • 3 学习

添加对层类型的支持

你可能熟悉 Keras 或 PyTorch 中的层类型

到目前为止,你已经完成了新深度学习框架中最复杂的部分。接下来的工作主要是向张量添加新函数,创建方便的高阶类和函数。在几乎所有框架中,最常见的一种抽象是层抽象。它是一组常用的正向传播技术,封装在一个简单的 API 中,并带有某种.forward()方法来调用它们。以下是一个简单线性层的示例:

class Layer(object):

    def __init__(self):
        self.parameters = list()

    def get_parameters(self):
        return self.parameters

class Linear(Layer):

    def __init__(self, n_inputs, n_outputs):
        super().__init__()
        W = np.random.randn(n_inputs, n_outputs)*np.sqrt(2.0/(n_inputs))
        self.weight = Tensor(W, autograd=True)
        self.bias = Tensor(np.zeros(n_outputs), autograd=True)

        self.parameters.append(self.weight)
        self.parameters.append(self.bias)

    def forward(self, input):
        return input.mm(self.weight)+self.bias.expand(0,len(input.data))

这里没有什么特别新的。权重被组织到一个类中(并且我添加了偏置权重,因为这是一个真正的线性层)。你可以一次性初始化这个层,使得权重和偏置都使用正确的尺寸,并且始终使用正确的正向传播逻辑。

还要注意,我创建了一个抽象类Layer,它有一个单一的 getter。这允许更复杂的层类型(例如包含其他层的层)。你只需要重写get_parameters()来控制稍后传递给优化器(如上一节中创建的SGD类)的张量。

包含层的层

层也可以包含其他层

最受欢迎的层是顺序层,它正向传播一个层的列表,其中每个层将其输出馈送到下一个层的输入:

class Sequential(Layer):

    def __init__(self, layers=list()):
        super().__init__()

        self.layers = layers

    def add(self, layer):
        self.layers.append(layer)

    def forward(self, input):
        for layer in self.layers:
            input = layer.forward(input)
        return input

    def get_parameters(self):
        params = list()
        for l in self.layers:
            params += l.get_parameters()
        return params

data = Tensor(np.array([[0,0],[0,1],[1,0],[1,1]]), autograd=True)
target = Tensor(np.array([[0],[1],[0],[1]]), autograd=True)

model = Sequential([Linear(2,3), Linear(3,1)])

optim = SGD(parameters=model.get_parameters(), alpha=0.05)

for i in range(10):

    pred = model.forward(data)                         *1*

    loss = ((pred - target)*(pred - target)).sum(0)    *2*

    loss.backward(Tensor(np.ones_like(loss.data)))     *3*
    optim.step()
    print(loss)
  • 1 预测

  • 2 比较

  • 3 学习

损失函数层

一些层没有权重

你也可以创建函数层,这些函数作用于输入。这类层中最受欢迎的版本可能是损失函数层,例如均方误差:

class MSELoss(Layer):

    def __init__(self):
        super().__init__()

    def forward(self, pred, target):
        return ((pred - target)*(pred - target)).sum(0)

import numpy
np.random.seed(0)

data = Tensor(np.array([[0,0],[0,1],[1,0],[1,1]]), autograd=True)
target = Tensor(np.array([[0],[1],[0],[1]]), autograd=True)

model = Sequential([Linear(2,3), Linear(3,1)])
criterion = MSELoss()

optim = SGD(parameters=model.get_parameters(), alpha=0.05)

for i in range(10):

    pred = model.forward(data)                        *1*

    loss = criterion.forward(pred, target)            *2*

    loss.backward(Tensor(np.ones_like(loss.data)))    *3*
    optim.step()
    print(loss)
  • 1 预测

  • 2 比较

  • 3 学习

                     [2.33428272]
                     [0.06743796]
                         ...
                     [0.01153118]
                     [0.00889602]

如果您能原谅重复,再次强调,这里没有什么特别新的。在底层,最后几个代码示例都执行了完全相同的计算。只是自动微分正在执行所有的反向传播,正向传播步骤被封装在漂亮的类中,以确保功能按正确的顺序执行。

如何学习一个框架

过于简化地说,框架是自动微分加上一系列预构建的层和优化器

您已经能够(相当快速地)使用底层自动微分系统编写各种新的层类型,这使得组合任意功能层变得相当容易。说实话,这是现代框架的主要功能,消除了为正向和反向传播手动编写每个数学运算的需要。使用框架大大增加了您从想法到实验的速度,并将减少您代码中的错误数量。

将框架视为仅是一个与大量层和优化器耦合的自动微分系统,这有助于您学习它们。我预计您将能够快速地从本章转向几乎任何框架,尽管与在此处构建的 API 最相似的框架是 PyTorch。无论如何,为了您的参考,花点时间浏览一下几个大型框架中的层和优化器列表:

学习新框架的一般工作流程是找到最简单的代码示例,对其进行调整,了解自动微分系统的 API,然后逐步修改代码示例,直到达到您关心的任何实验。

def backward(self,grad=None, grad_origin=None):
    if(self.autograd):

        if(grad is None):
            grad = Tensor(np.ones_like(self.data))

在我们继续之前,我正在向 Tensor.backward() 添加一个方便的函数,这样您在第一次调用 .backward() 时就不必传入 1 的梯度。严格来说,这不是必需的——但它很方便。

非线性层

让我们在 Tensor 中添加非线性函数,然后创建一些层类型

对于下一章,您将需要 .sigmoid().tanh()。让我们将它们添加到 Tensor 类中。您很久以前就学过了这两个函数的导数,所以这应该很容易:

def sigmoid(self):
    if(self.autograd):
        return Tensor(1 / (1 + np.exp(-self.data)),
                      autograd=True,
                      creators=[self],
                      creation_op="sigmoid")
    return Tensor(1 / (1 + np.exp(-self.data)))

def tanh(self):
    if(self.autograd):
        return Tensor(np.tanh(self.data),
                      autograd=True,
                      creators=[self],
                      creation_op="tanh")
    return Tensor(np.tanh(self.data))

以下代码展示了添加到 Tensor.backward() 方法的反向传播逻辑:

if(self.creation_op == "sigmoid"):
    ones = Tensor(np.ones_like(self.grad.data))
    self.creators[0].backward(self.grad * (self * (ones - self)))

if(self.creation_op == "tanh"):
    ones = Tensor(np.ones_like(self.grad.data))
    self.creators[0].backward(self.grad * (ones - (self * self)))

希望这感觉相当常规。看看您能否创建更多的非线性函数:尝试 HardTanhrelu

class Tanh(Layer):                          class Sigmoid(Layer):
    def __init__(self):                         def __init__(self):
        super().__init__()                          super().__init__()

     def forward(self, input):                  def forward(self, input):
         return input.tanh()                        return input.sigmoid()

让我们尝试新的非线性函数。新添加的内容以粗体显示:

import numpy
np.random.seed(0)

data = Tensor(np.array([[0,0],[0,1],[1,0],[1,1]]), autograd=True)
target = Tensor(np.array([[0],[1],[0],[1]]), autograd=True)

model = Sequential([Linear(2,3), Tanh(), Linear(3,1), Sigmoid()])
criterion = MSELoss()

optim = SGD(parameters=model.get_parameters(), alpha=1)

for i in range(10):

    pred = model.forward(data)                         *1*

    loss = criterion.forward(pred, target)             *2*

    loss.backward(Tensor(np.ones_like(loss.data)))     *3*
    optim.step()
    print(loss)
  • 1 预测

  • 2 比较

  • 3 学习

[1.06372865]
[0.75148144]
[0.57384259]
[0.39574294]
[0.2482279]
[0.15515294]
[0.10423398]
[0.07571169]
[0.05837623]
[0.04700013]

如您所见,您可以将新的 Tanh()Sigmoid() 层直接放入 Sequential() 的输入参数中,神经网络会确切地知道如何使用它们。简单!

在上一章中,你学习了循环神经网络。特别是,你训练了一个模型来预测下一个单词,给定前几个单词。在我们完成本章之前,我希望你能将那段代码翻译成新的框架。为此,你需要三种新的层类型:一个学习词嵌入的嵌入层,一个可以学习建模输入序列的 RNN 层,以及一个可以预测标签概率分布的 softmax 层。

嵌入层

嵌入层将索引转换为激活

在第十一章中,你学习了关于词嵌入的内容,这些嵌入是将向量映射到单词,你可以将其前向传播到神经网络中。因此,如果你有一个 200 个单词的词汇表,你也将有 200 个嵌入。这为创建嵌入层提供了初始规范。首先,初始化一个(正确长度的)单词嵌入列表(正确的大小):

class Embedding(Layer):

    def __init__(self, vocab_size, dim):
        super().__init__()

        self.vocab_size = vocab_size
        self.dim = dim

        weight = np.random.rand(vocab_size, dim) - 0.5) / dim         *1*
  • 1 这种初始化风格是来自 word2vec 的惯例。

到目前为止,一切顺利。矩阵为词汇表中的每个单词都有一个行(向量)。现在,你将如何进行前向传播呢?好吧,前向传播始终从问题“输入将如何编码?”开始。在词嵌入的情况下,显然你不能传递单词本身,因为单词不会告诉你应该用self.weight中的哪一行进行前向传播。相反,如你从第十一章中可能记得的,你前向传播索引。幸运的是,NumPy 支持这个操作:

图片

注意,当你将整数矩阵传递给 NumPy 矩阵时,它返回相同的矩阵,但每个整数都被替换为指定的行。因此,一个索引的二维矩阵变成了一个三维的嵌入矩阵(行)。这太完美了!

将索引添加到 autograd

在构建嵌入层之前,autograd 需要支持索引

为了支持新的嵌入策略(该策略假设单词作为索引矩阵进行前向传播),你在上一节中玩弄的索引必须由 autograd 支持。这是一个相当简单的想法。你需要确保在反向传播过程中,梯度被放置在与前向传播中索引到的相同行中。这要求你保留传递的任何索引,以便在反向传播期间使用简单的for循环将每个梯度放置在适当的位置:

def index_select(self, indices):

    if(self.autograd):
        new = Tensor(self.data[indices.data],
                     autograd=True,
                     creators=[self],
                     creation_op="index_select")
        new.index_select_indices = indices
        return new
    return Tensor(self.data[indices.data])

首先,使用你在上一节中学到的 NumPy 技巧来选择正确的行:

             if(self.creation_op == "index_select"):
                 new_grad = np.zeros_like(self.creators[0].data)
                 indices_ = self.index_select_indices.data.flatten()
                 grad_ = grad.data.reshape(len(indices_), -1)
                 for i in range(len(indices_)):
                     new_grad[indices_[i]] += grad_[i]
                 self.creators[0].backward(Tensor(new_grad))

然后,在 backprop() 期间,初始化一个正确大小的新梯度(原始矩阵的大小,该矩阵正在被索引)。其次,展平索引,以便你可以遍历它们。第三,将 grad_ 压缩成一个简单的行列表。(微妙之处在于 indices_ 中的索引列表和 grad_ 中的向量列表将按对应顺序排列。)然后,遍历每个索引,将其添加到你正在创建的新梯度的正确行中,并将其反向传播到 self.creators[0]。正如你所看到的,grad_[i] 正确地更新了每一行(在这种情况下,添加一个由 1 组成的向量),并按照索引被使用的次数进行更新。索引 2 和 3 更新了两次(加粗):

嵌入层(重访)

现在你可以使用新的 .index_select() 方法完成正向传播

对于正向传播,调用 .index_select(),autograd 将处理其余部分:

class Embedding(Layer):

    def __init__(self, vocab_size, dim):
        super().__init__()

        self.vocab_size = vocab_size
        self.dim = dim

        weight = np.random.rand(vocab_size, dim) - 0.5) / dim        *1*
        self.weight = Tensor((weight, autograd=True)

        self.parameters.append(self.weight)

    def forward(self, input):
        return self.weight.index_select(input)
  • 1 这种初始化风格是来自 word2vec 的约定。
data = Tensor(np.array([1,2,1,2]), autograd=True)
target = Tensor(np.array([[0],[1],[0],[1]]), autograd=True)

embed = Embedding(5,3)
model = Sequential([embed, Tanh(), Linear(3,1), Sigmoid()])
criterion = MSELoss()

optim = SGD(parameters=model.get_parameters(), alpha=0.5)

for i in range(10):

    pred = model.forward(data)                        *1*

    loss = criterion.forward(pred, target)            *2*

    loss.backward(Tensor(np.ones_like(loss.data)))    *3*
    optim.step()
    print(loss)
  • 1 预测

  • 2 比较

  • 3 学习

[0.98874126]
[0.6658868]
[0.45639889]
    ...
[0.08731868]
[0.07387834]

在这个神经网络中,你学习将输入索引 1 和 2 与预测 0 和 1 相关联。在理论上,索引 1 和 2 可以对应于单词(或某种其他输入对象),在最终的例子中,它们将这样做。这个例子是为了展示嵌入的工作。

交叉熵层

让我们向 autograd 添加交叉熵并创建一个层

希望到这一点,你已经开始对如何创建新的层类型感到舒适。交叉熵是一个相当标准的层,你在本书中已经多次见过。因为我们已经介绍了如何创建几个新的层类型,所以我会在这里留下代码供你参考。在复制此代码之前,尝试自己完成它。

    def cross_entropy(self, target_indices):

         temp = np.exp(self.data)
         softmax_output = temp / np.sum(temp,
                                        axis=len(self.data.shape)-1,
                                        keepdims=True)

         t = target_indices.data.flatten()
         p = softmax_output.reshape(len(t),-1)
         target_dist = np.eye(p.shape[1])[t]
         loss = -(np.log(p) * (target_dist)).sum(1).mean()

         if(self.autograd):
             out = Tensor(loss,
                          autograd=True,
                          creators=[self],
                          creation_op="cross_entropy")
             out.softmax_output = softmax_output
             out.target_dist = target_dist
             return out

         return Tensor(loss)

                 if(self.creation_op == "cross_entropy"):
                     dx = self.softmax_output - self.target_dist
                     self.creators[0].backward(Tensor(dx))

class CrossEntropyLoss(object):

       def __init__(self):
           super().__init__()

       def forward(self, input, target):
           return input.cross_entropy(target)

import numpy
np.random.seed(0)

# data indices
data = Tensor(np.array([1,2,1,2]), autograd=True)

# target indices
target = Tensor(np.array([0,1,0,1]), autograd=True)

model = Sequential([Embedding(3,3), Tanh(), Linear(3,4)])
criterion = CrossEntropyLoss()

optim = SGD(parameters=model.get_parameters(), alpha=0.1)

for i in range(10):

    pred = model.forward(data)                        *1*

    loss = criterion.forward(pred, target)            *2*

    loss.backward(Tensor(np.ones_like(loss.data)))    *3*
    optim.step()
    print(loss)
  • 1 预测

  • 2 比较

  • 3 学习

1.3885032434928422
0.9558181509266037
0.6823083585795604
0.5095259967493119
0.39574491472895856
0.31752527285348264
0.2617222861964216
0.22061283923954234
0.18946427334830068
0.16527389263866668

使用在几个先前神经网络中使用的相同交叉熵逻辑,你现在有一个新的损失函数。这个损失函数的一个显著特点是与其他不同:最终的 softmax 和损失的计算都在损失类内部完成。这在深度神经网络中是一个非常常见的约定。几乎每个框架都会这样做。当你想要完成一个网络并使用交叉熵进行训练时,你可以在正向传播步骤中省略 softmax,并调用一个交叉熵类,该类将自动将 softmax 作为损失函数的一部分执行。

这些之所以如此一致地组合在一起,是因为性能。在交叉熵函数中一起计算 softmax 和负对数似然比梯度的速度比在两个不同的模块中分别进行正向传播和反向传播要快得多。这与梯度数学中的捷径有关。

循环神经网络层

通过组合多个层,你可以学习时间序列

作为本章的最后一个练习,让我们再创建一个由多个较小的层类型组成的层。这个层的目的是学习你在上一章结束时完成的任务。这个层是循环层。你将使用三个线性层来构建它,.forward()方法将接受前一个隐藏状态和当前训练数据的输入:

class RNNCell(Layer):

    def __init__(self, n_inputs,n_hidden,n_output,activation='sigmoid'):
        super().__init__()

        self.n_inputs = n_inputs
        self.n_hidden = n_hidden
        self.n_output = n_output

        if(activation == 'sigmoid'):
            self.activation = Sigmoid()
        elif(activation == 'tanh'):
            self.activation == Tanh()
        else:
            raise Exception("Non-linearity not found")

        self.w_ih = Linear(n_inputs, n_hidden)
        self.w_hh = Linear(n_hidden, n_hidden)
        self.w_ho = Linear(n_hidden, n_output)

        self.parameters += self.w_ih.get_parameters()
        self.parameters += self.w_hh.get_parameters()
        self.parameters += self.w_ho.get_parameters()

    def forward(self, input, hidden):
        from_prev_hidden = self.w_hh.forward(hidden)
        combined = self.w_ih.forward(input) + from_prev_hidden
        new_hidden = self.activation.forward(combined)
        output = self.w_ho.forward(new_hidden)
        return output, new_hidden

    def init_hidden(self, batch_size=1):
        return Tensor(np.zeros((batch_size,self.n_hidden)),autograd=True)

本章不涉及重新介绍 RNNs,但指出一些应该已经熟悉的组成部分是值得的。RNNs 有一个状态向量,它在时间步长之间传递。在这种情况下,它是变量hidden,它既是forward函数的输入参数也是输出变量。RNNs 还有几个不同的权重矩阵:一个将输入向量映射到隐藏向量(处理输入数据),一个将隐藏向量映射到隐藏向量(根据前一个更新每个隐藏向量),以及可选的隐藏到输出层,该层学习根据隐藏向量进行预测。这个 RNNCell 实现包括了这三个。self.w_ih层是输入到隐藏层,self.w_hh是隐藏到隐藏层,self.w_ho是隐藏到输出层。注意每个的维度。self.w_ih的输入大小和self.w_ho的输出大小都是词汇表的大小。所有其他维度都是基于n_hidden参数可配置的。

最后,一个activation输入参数定义了在每个时间步长应用于隐藏向量的非线性函数。我添加了两种可能性(SigmoidTanh),但有很多选项可供选择。让我们训练一个网络:

import sys,random,math
from collections import Counter
import numpy as np

f = open('tasksv11/en/qa1_single-supporting-fact_train.txt','r')
raw = f.readlines()
f.close()

tokens = list()
for line in raw[0:1000]:
    tokens.append(line.lower().replace("\n","").split(" ")[1:])

new_tokens = list()
for line in tokens:
    new_tokens.append(['-'] * (6 - len(line)) + line)
tokens = new_tokens

vocab = set()
for sent in tokens:
    for word in sent:
        vocab.add(word)

vocab = list(vocab)

word2index = {}
for i,word in enumerate(vocab):
    word2index[word]=i

def words2indices(sentence):
    idx = list()
    for word in sentence:
        idx.append(word2index[word])
    return idx

indices = list()
for line in tokens:
    idx = list()
    for w in line:
        idx.append(word2index[w])
    indices.append(idx)

data = np.array(indices)

你可以学习适应你在上一章中完成的任务

现在,你可以使用嵌入输入初始化循环层,并训练一个网络来解决上一章相同的任务。请注意,尽管代码要简单得多,但由于这个小框架,这个网络稍微复杂一些(它有一个额外的层)。

embed = Embedding(vocab_size=len(vocab),dim=16)
model = RNNCell(n_inputs=16, n_hidden=16, n_output=len(vocab))

criterion = CrossEntropyLoss()
params = model.get_parameters() + embed.get_parameters()
optim = SGD(parameters=params, alpha=0.05)

首先,定义输入嵌入,然后定义循环单元。(注意,当循环层仅实现单个递归时,通常将其命名为cell。如果你创建了一个可以配置任意数量单元格的层,它将被称为 RNN,n_layers将是一个输入参数。)

for iter in range(1000):
    batch_size = 100
    total_loss = 0

    hidden = model.init_hidden(batch_size=batch_size)

    for t in range(5):
        input = Tensor(data[0:batch_size,t], autograd=True)
        rnn_input = embed.forward(input=input)
        output, hidden = model.forward(input=rnn_input, hidden=hidden)

    target = Tensor(data[0:batch_size,t+1], autograd=True)
    loss = criterion.forward(output, target)
    loss.backward()
    optim.step()
    total_loss += loss.data
    if(iter % 200 == 0):
        p_correct = (target.data == np.argmax(output.data,axis=1)).mean()
        print_loss = total_loss / (len(data)/batch_size)
        print("Loss:",print_loss,"% Correct:",p_correct)
Loss: 0.47631100976371393 % Correct: 0.01
Loss: 0.17189538896184856 % Correct: 0.28
Loss: 0.1460940222788725 % Correct: 0.37
Loss: 0.13845863915406884 % Correct: 0.37
Loss: 0.135574472565278 % Correct: 0.37
batch_size = 1
hidden = model.init_hidden(batch_size=batch_size)
for t in range(5):
    input = Tensor(data[0:batch_size,t], autograd=True)
    rnn_input = embed.forward(input=input)
    output, hidden = model.forward(input=rnn_input, hidden=hidden)

target = Tensor(data[0:batch_size,t+1], autograd=True)
loss = criterion.forward(output, target)

ctx = ""
for idx in data[0:batch_size][0][0:-1]:
    ctx += vocab[idx] + " "
print("Context:",ctx)
print("Pred:", vocab[output.data.argmax()])
Context: - mary moved to the
Pred: office.

如你所见,神经网络学会了以大约 37%的准确率预测训练数据集的前 100 个示例(对于这个玩具任务来说几乎是完美的)。它预测了玛丽可能移动的方向,就像在第十二章的结尾一样。

摘要

框架是前向和反向逻辑的高效、方便的抽象

我希望这一章的练习让你体会到了框架的便利性。它们可以使你的代码更易读,编写更快,执行更快(通过内置优化),并且错误更少。更重要的是,这一章将为你使用和扩展行业标准框架如 PyTorch 和 TensorFlow 做好准备。无论是调试现有的层类型还是原型设计你自己的,你在本章学到的技能将是你在本书中获得的最重要的技能之一,因为它们将之前章节中关于深度学习的抽象知识与你未来将用于实现模型的真实工具的设计联系起来。

与这里构建的框架最相似的是 PyTorch,我强烈建议你在完成这本书后深入探索它。它很可能是你感觉最熟悉的框架。

第十四章。学习像莎士比亚一样写作:长短期记忆

本章

  • 字符语言模型

  • 截断反向传播

  • 梯度消失和梯度爆炸

  • RNN 反向传播的一个玩具示例

  • 长短期记忆(LSTM)单元

“主啊,这些凡人多么愚蠢!”

莎士比亚 仲夏夜之梦

字符语言模型

让我们用 RNN 解决一个更具挑战性的任务

在第十二章和第十三章的结尾,你训练了简单的循环神经网络(RNN),这些网络学习了一个简单的序列预测问题。但你是在一个玩具数据集上训练的,这个数据集是通过规则合成的短语。

在本章中,你将尝试在一个更具挑战性的数据集上进行语言建模:莎士比亚的作品。而且,与上一章中学习根据前面的单词预测下一个单词不同,该模型将训练在字符上。它需要学习根据观察到的先前字符预测下一个字符。这就是我的意思:

import sys,random,math
from collections import Counter
import numpy as np
import sys

np.random.seed(0)

f = open('shakespear.txt','r')
raw = f.read()                  *1*
f.close()

vocab = list(set(raw))
word2index = {}
for i,word in enumerate(vocab):
    word2index[word]=i
indices = np.array(list(map(lambda x:word2index[x], raw)))

与第十二章和第十三章的词汇表由数据集中的单词组成不同,现在词汇表由数据集中的字符组成。因此,数据集也被转换为一个索引列表,这些索引对应于字符而不是单词。之上是indices NumPy 数组:

embed = Embedding(vocab_size=len(vocab),dim=512)
model = RNNCell(n_inputs=512, n_hidden=512, n_output=len(vocab))

criterion = CrossEntropyLoss()
optim = SGD(parameters=model.get_parameters() + embed.get_parameters(),
            alpha=0.05)

这段代码看起来都很熟悉。它初始化嵌入维度为 8,RNN 隐藏状态的大小为 512。输出权重初始化为 0(这不是规则,但我觉得这样效果更好)。最后,你初始化交叉熵损失和随机梯度下降优化器。

截断反向传播的需要

通过 100,000 个字符进行反向传播是不可行的

阅读 RNN 代码的更具挑战性的方面之一是为输入数据而进行的批处理逻辑。之前的(更简单的)神经网络有一个这样的内部for循环(粗体部分):

for iter in range(1000):
    batch_size = 100
    total_loss = 0

    hidden = model.init_hidden(batch_size=batch_size)

    for t in range(5):
        input = Tensor(data[0:batch_size,t], autograd=True)
        rnn_input = embed.forward(input=input)
        output, hidden = model.forward(input=rnn_input, hidden=hidden)

    target = Tensor(data[0:batch_size,t+1], autograd=True)
    loss = criterion.forward(output, target)
    loss.backward()
    optim.step()
    total_loss += loss.data
    if(iter % 200 == 0):
        p_correct = (target.data == np.argmax(output.data,axis=1)).mean()
        print_loss = total_loss / (len(data)/batch_size)
        print("Loss:",print_loss,"% Correct:",p_correct)

你可能会问,“为什么迭代到 5?”实际上,之前的语料库没有超过六个单词的例子。它读取了五个单词,然后尝试预测第六个。

更重要的是反向传播步骤。考虑当你对一个简单的前馈网络进行 MNIST 数字分类时:梯度总是反向传播到整个网络,对吧?它们一直反向传播,直到达到输入数据。这允许网络调整每个权重,试图学习如何根据整个输入示例正确预测。

这里的循环例子也没有不同。你通过五个输入示例进行前向传播,然后,当你稍后调用loss.backward()时,它将梯度反向传播回网络到输入数据点。你可以这样做,因为你一次没有输入那么多数据点。但是莎士比亚数据集有 10 万个字符!这对于每个预测进行反向传播来说太多了。你怎么办?

你不需要!你向后传播一个固定数量的步骤到过去,然后停止。这被称为截断反向传播,并且是行业标准。你向后传播的长度成为另一个可调参数(就像批量大小或 alpha)。

截断反向传播

从技术上讲,它削弱了神经网络的最高理论极限

使用截断反向传播的缺点是它缩短了神经网络可以学习记住事物的距离。基本上,在比如说五个时间步长之后切断梯度,意味着神经网络无法学习记住过去超过五个时间步长的事件。

严格来说,情况比这更复杂。在 RNN 的隐藏层中,从过去超过五个时间步长可能会意外地保留一些残留信息,但神经网络不能使用梯度来特别请求模型从六个时间步长之前保留信息以帮助当前预测。因此,在实践中,神经网络不会学习基于过去超过五个时间步长的输入信号进行预测(如果截断设置为五个时间步长)。在实践中,对于语言建模,截断变量被称为bptt,它通常设置在 16 到 64 之间:

batch_size = 32
bptt = 16
n_batches = int((indices.shape[0] / (batch_size)))

截断反向传播的另一个缺点是它使得小批量逻辑变得稍微复杂一些。要使用截断反向传播,你假装你有一个大数据集,而不是大小为bptt的一堆小数据集。你需要相应地分组数据集:

trimmed_indices = indices[:n_batches*batch_size]
batched_indices = trimmed_indices.reshape(batch_size, n_batches)
batched_indices = batched_indices.transpose()

input_batched_indices = batched_indices[0:-1]
target_batched_indices = batched_indices[1:]

n_bptt = int(((n_batches-1) / bptt))
input_batches = input_batched_indices[:n_bptt*bptt]
input_batches = input_batches.reshape(n_bptt,bptt,batch_size)
target_batches = target_batched_indices[:n_bptt*bptt]
target_batches = target_batches.reshape(n_bptt, bptt, batch_size)

这里有很多事情在进行中。最上面一行使得数据集成为batch_sizen_batches之间的一个偶数倍。这样做是为了当你将其分组为张量时,它是平方的(或者你也可以用 0 填充数据集以使其成为平方)。第二行和第三行重新塑形数据集,使得每一列是初始indices数组的一个部分。我将展示这部分,就像batch_size被设置为 8(为了可读性):

|

print(raw[0:5])
print(indices[0:5])

|

|

'That,'
array([ 9, 14, 2, 10, 57])

|

这些是莎士比亚数据集中的前五个字符。它们拼写出字符串“That,”。接下来是batched_indices中变换后的前五行的输出:

|

print(batched_indices[0:5])

|

|

array([[ 9, 43, 21, 10, 10, 23, 57, 46],
       [14, 44, 39, 21, 43, 14, 1, 10],
       [ 2, 41, 39, 54, 37, 21, 26, 57],
       [10, 39, 57, 48, 21, 54, 38, 43],
       [57, 39, 43, 1, 10, 21, 21, 33]])

|

我已经用粗体突出显示了第一列。看看短语“That,”的索引是否在左边的第一列?这是一个标准构造。有八个列的原因是batch_size是 8。这个张量随后被用来构建一个更小的数据集列表,每个数据集的长度为bptt

你可以在这里看到输入和目标是如何构建的。注意,目标索引是输入索引偏移一行(因此网络预测下一个字符)。再次注意,在这个打印输出中batch_size设置为 8,这使得阅读更容易,但实际上你将其设置为 32。

print(input_batches[0][0:5])

print(target_batches[0][0:5])

array([[ 9, 43, 21, 10, 10, 23, 57, 46],
       [14, 44, 39, 21, 43, 14, 1, 10],
       [ 2, 41, 39, 54, 37, 21, 26, 57],
       [10, 39, 57, 48, 21, 54, 38, 43],
       [57, 39, 43, 1, 10, 21, 21, 33]])

array([[14, 44, 39, 21, 43, 14, 1, 10],
       [ 2, 41, 39, 54, 37, 21, 26, 57],
       [10, 39, 57, 48, 21, 54, 38, 43],
       [57, 39, 43, 1, 10, 21, 21, 33],
       [43, 43, 41, 60, 52, 12, 54, 1]])

如果这对你来说现在还不明白,也不要担心。这和深度学习理论关系不大;这只是设置 RNN 时一个特别复杂的部分,你有时会遇到。我想花几页纸来解释它。

让我们看看如何使用截断反向传播进行迭代

以下代码展示了截断反向传播的实际应用。注意,它看起来与第十三章中的迭代逻辑非常相似。唯一的真正区别是,你会在每个步骤生成一个batch_loss;并且每进行一次bptt步骤后,你都会进行反向传播并更新权重。然后你继续像什么都没发生一样读取数据集(甚至使用之前的相同隐藏状态,它仅在每轮中重置):

def train(iterations=100):
    for iter in range(iterations):
        total_loss = 0
        n_loss = 0

        hidden = model.init_hidden(batch_size=batch_size)
        for batch_i in range(len(input_batches)):

            hidden = Tensor(hidden.data, autograd=True)
            loss = None
            losses = list()
            for t in range(bptt):
                input = Tensor(input_batches[batch_i][t], autograd=True)
                rnn_input = embed.forward(input=input)
                output, hidden = model.forward(input=rnn_input,
                                               hidden=hidden)
               target = Tensor(target_batches[batch_i][t], autograd=True)
                batch_loss = criterion.forward(output, target)
                losses.append(batch_loss)
                if(t == 0):
                    loss = batch_loss
                else:
                    loss = loss + batch_loss
            for loss in losses:
                ""
            loss.backward()
            optim.step()
            total_loss += loss.data
            log = "\r Iter:" + str(iter)
            log += " - Batch "+str(batch_i+1)+"/"+str(len(input_batches))
            log += " - Loss:" + str(np.exp(total_loss / (batch_i+1)))
            if(batch_i == 0):
                log += " - " + generate_sample(70,'\n').replace("\n"," ")
            if(batch_i % 10 == 0 or batch_i-1 == len(input_batches)):
                sys.stdout.write(log)
        optim.alpha *= 0.99
        print()
train()

 Iter:0 - Batch 191/195 - Loss:148.00388828554404
 Iter:1 - Batch 191/195 - Loss:20.588816924127116 mhnethet tttttt t t t
                                     ....
 Iter:99 - Batch 61/195 - Loss:1.0533843281265225 I af the mands your

输出样本的一个示例

通过从模型的预测中采样,你可以写出莎士比亚的作品!

以下代码使用训练逻辑的子集来使用模型进行预测。你将预测存储在一个字符串中,并将字符串版本作为输出返回给函数。生成的样本看起来非常像莎士比亚的作品,甚至包括对话角色:

def generate_sample(n=30, init_char=' '):
    s = ""
    hidden = model.init_hidden(batch_size=1)
    input = Tensor(np.array([word2index[init_char]]))
    for i in range(n):
        rnn_input = embed.forward(input)
        output, hidden = model.forward(input=rnn_input, hidden=hidden)
        output.data *= 10                                              *1*
        temp_dist = output.softmax()
        temp_dist /= temp_dist.sum()

        m = (temp_dist > np.random.rand()).argmax()                    *2*
        c = vocab[m]
        input = Tensor(np.array([m]))
        s += c
    return s
print(generate_sample(n=2000, init_char='\n'))
  • 1 样本采样的温度;越高 = 越贪婪

  • 2 预测样本

I war ded abdons would.

CHENRO:
Why, speed no virth to her,
Plirt, goth Plish love,
Befion
 hath if be fe woulds is feally your hir, the confectife to the nightion
As rent Ron my hath iom
the worse, my goth Plish love,
Befion
Ass untrucerty of my fernight this we namn?

ANG, makes:
That's bond confect fe comes not commonour would be forch the conflill
As poing from your jus eep of m look o perves, the worse, my goth
Thould be good lorges ever word

DESS:
Where exbinder: if not conflill, the confectife to the nightion
As co move, sir, this we namn?

ANG VINE PAET:
There was courter hower how, my goth Plish lo res
Toures
ever wo formall, have abon, with a good lorges ever word.

消失和爆炸梯度

简单的 RNN 会受到消失和爆炸梯度的影响

你可能还记得当你第一次组合 RNN 时的这个图像。想法是能够以某种方式组合单词嵌入,使得顺序很重要。你是通过学习一个矩阵来做到这一点的,该矩阵将每个嵌入转换到下一个时间步。然后,前向传播变成了两步过程:从第一个单词嵌入(以下示例中的“Red”嵌入)开始,乘以权重矩阵,并加上下一个嵌入(“Sox”)。然后你将得到的向量乘以相同的权重矩阵,并加入下一个单词,重复这个过程,直到读取整个单词序列。

但正如你所知,隐藏状态生成过程中添加了一个额外的非线性项。因此,前向传播变成了一个三步过程:将前一个隐藏状态通过权重矩阵进行矩阵乘法,加入下一个单词的嵌入,并应用非线性函数。

注意,这种非线性在网络稳定性中起着重要作用。无论单词序列有多长,隐藏状态(理论上可能随时间增长而增长)都被迫保持在非线性函数的值之间(在 sigmoid 的情况下是 0 到 1)。但是,反向传播发生的方式与正向传播略有不同,不具有这种良好的性质。反向传播往往会引起极端大或极端小的值。大值可能导致发散(许多非数字 [NaN]),而极端小的值则使网络无法学习。让我们更仔细地看看 RNN 反向传播。

RNN 反向传播的玩具示例

为了亲身体验梯度消失/爆炸,让我们合成一个示例

以下代码展示了 sigmoidrelu 激活的反向传播循环。注意,对于 sigmoid/relu,梯度分别变得非常小/大。在反向传播过程中,由于矩阵乘法的结果,它们变得很大,而由于 sigmoid 激活在尾部具有非常平坦的导数(许多非线性函数的常见情况),它们又变得很小。

(sigmoid,relu)=(lambda x:1/(1+np.exp(-x)), lambda x:(x>0).astype(float)*x)
weights = np.array([[1,4],[4,1]])
activation = sigmoid(np.array([1,0.01]))

print("Sigmoid Activations")
activations = list()
for iter in range(10):
    activation = sigmoid(activation.dot(weights))
    activations.append(activation)
    print(activation)

print("\nSigmoid Gradients")
gradient = np.ones_like(activation)
for activation in reversed(activations):                     *1*
    gradient = (activation * (1 - activation) * gradient)
    gradient = gradient.dot(weights.transpose())
    print(gradient)

print("Activations")
activations = list()
for iter in range(10):
    activation = relu(activation.dot(weights))               *2*
    activations.append(activation)
    print(activation)
print("\nGradients")
gradient = np.ones_like(activation)
for activation in reversed(activations):
    gradient = ((activation > 0) * gradient).dot(weights.transpose())
    print(gradient)
  • 1 sigmoid 的导数在激活值非常接近 0 或 1(尾部)时会导致非常小的梯度。

  • 2 矩阵乘法会导致梯度爆炸,而非线性函数(如 sigmoid)无法将其压缩。

Sigmoid Activations                  Relu Activations
[0.93940638 0.96852968]              [23.71814585 23.98025559]
[0.9919462 0.99121735]               [119.63916823 118.852839 ]
[0.99301385 0.99302901]              [595.05052421 597.40951192]
        ...                                     ...
[0.99307291 0.99307291]              [46583049.71437107 46577890.60826711]

Sigmoid Gradients                    Relu Gradients
[0.03439552 0.03439552]              [5\. 5.]
[0.00118305 0.00118305]              [25\. 25.]
[4.06916726e-05 4.06916726e-05]      [125\. 125.]
             ...                                ...
[1.45938177e-14 2.16938983e-14]      [9765625\. 9765625.]

长短期记忆(LSTM)单元

LSTM 是对抗梯度消失/爆炸的行业标准模型

前一节解释了梯度消失/爆炸是如何由 RNN 中隐藏状态的更新方式引起的。问题是矩阵乘法和非线性函数的组合用于形成下一个隐藏状态。LSTM 提供的解决方案出人意料地简单。

门控复制技巧

LSTM 通过复制前一个隐藏状态并在必要时添加或删除信息来创建下一个隐藏状态。LSTM 用于添加和删除信息的机制被称为

def forward(self, input, hidden):
    from_prev_hidden = self.w_hh.forward(hidden)
    combined = self.w_ih.forward(input) + from_prev_hidden
    new_hidden = self.activation.forward(combined)
    output = self.w_ho.forward(new_hidden)
    return output, new_hidden

之前的代码是 RNN 单元的正向传播逻辑。以下是 LSTM 单元的新正向传播逻辑。LSTM 有两个隐藏状态向量:h(隐藏)和 cell

你需要关注的是 cell。注意它是如何更新的。每个新的单元是前一个单元加上 u,通过 if 加权。f 是“忘记”门。如果它取值为 0,则新单元将擦除之前看到的内容。如果 i 为 1,它将完全添加 u 的值以创建新单元。o 是一个输出门,它控制输出预测可以查看多少单元状态。例如,如果 o 全为零,则 self.w_ho.forward(h) 行将忽略单元状态进行预测。

def forward(self, input, hidden):

    prev_hidden, prev_cell = (hidden[0], hidden[1])

    f = (self.xf.forward(input) + self.hf.forward(prev_hidden)).sigmoid()
    i = (self.xi.forward(input) + self.hi.forward(prev_hidden)).sigmoid()
    o = (self.xo.forward(input) + self.ho.forward(prev_hidden)).sigmoid()
    u = (self.xc.forward(input) + self.hc.forward(prev_hidden)).tanh()
    cell = (f * prev_cell) + (i * u)
    h = o * cell.tanh()
    output = self.w_ho.forward(h)
    return output, (h, cell)

关于 LSTM 门的直觉

LSTM 门与从内存中读取/写入的语义相似

所以,这就是全部了!有三个门控器——fio——和一个细胞更新向量u;分别想象这些是忘记、输入、输出和更新。它们共同工作以确保要存储或操作在c中的任何信息都可以这样做,而无需要求每次更新c时都应用任何矩阵乘法或非线性。换句话说,您正在避免永远调用nonlinearity(c)c.dot(weights)

这使得 LSTM 能够在时间序列中存储信息而不用担心梯度消失或梯度爆炸。每一步都是一个复制(假设f不为零)加上一个更新(假设i不为零)。隐藏值h随后是用于预测的细胞的一个掩码版本。

注意,三个门控器都是用相同的方式形成的。它们有自己的权重矩阵,但每个门控器都基于输入和前一个隐藏状态,通过一个sigmoid函数进行条件化。正是这个sigmoid非线性使得它们作为门控器非常有用,因为它在 0 和 1 之间饱和:

f = (self.xf.forward(input) + self.hf.forward(prev_hidden)).sigmoid()
i = (self.xi.forward(input) + self.hi.forward(prev_hidden)).sigmoid()
o = (self.xo.forward(input) + self.ho.forward(prev_hidden)).sigmoid()

最后一个可能的批评是关于h的。显然,它仍然容易受到梯度消失和梯度爆炸的影响,因为它基本上被用来和原始 RNN 一样。首先,因为h向量总是使用一个组合的向量创建,这些向量被tanhsigmoid压缩,所以梯度爆炸实际上并不是一个问题——只有梯度消失。但最终这没问题,因为h依赖于c,它可以携带长距离信息:梯度消失无法学习携带的那种信息。因此,所有长距离信息都是通过c传输的,而h只是c的一个局部解释,对于在下一个时间步进行输出预测和构建门控激活很有用。简而言之,c可以学习在长距离上传输信息,所以即使h不能,这也没有关系。

长短期记忆层

您可以使用自动微分系统来实现一个 LSTM

class LSTMCell(Layer):

    def __init__(self, n_inputs, n_hidden, n_output):
        super().__init__()

        self.n_inputs = n_inputs
        self.n_hidden = n_hidden
        self.n_output = n_output
        self.xf = Linear(n_inputs, n_hidden)
        self.xi = Linear(n_inputs, n_hidden)
        self.xo = Linear(n_inputs, n_hidden)
        self.xc = Linear(n_inputs, n_hidden)
        self.hf = Linear(n_hidden, n_hidden, bias=False)
        self.hi = Linear(n_hidden, n_hidden, bias=False)
        self.ho = Linear(n_hidden, n_hidden, bias=False)
        self.hc = Linear(n_hidden, n_hidden, bias=False)

        self.w_ho = Linear(n_hidden, n_output, bias=False)

        self.parameters += self.xf.get_parameters()
        self.parameters += self.xi.get_parameters()
        self.parameters += self.xo.get_parameters()
        self.parameters += self.xc.get_parameters()
        self.parameters += self.hf.get_parameters()
        self.parameters += self.hi.get_parameters()
        self.parameters += self.ho.get_parameters()
        self.parameters += self.hc.get_parameters()

        self.parameters += self.w_ho.get_parameters()

    def forward(self, input, hidden):

        prev_hidden = hidden[0]
        prev_cell = hidden[1]

        f=(self.xf.forward(input)+self.hf.forward(prev_hidden)).sigmoid()
        i=(self.xi.forward(input)+self.hi.forward(prev_hidden)).sigmoid()
        o=(self.xo.forward(input)+self.ho.forward(prev_hidden)).sigmoid()
        g = (self.xc.forward(input) +self.hc.forward(prev_hidden)).tanh()
        c = (f * prev_cell) + (i * g)
        h = o * c.tanh()

        output = self.w_ho.forward(h)
        return output, (h, c)

    def init_hidden(self, batch_size=1):
        h = Tensor(np.zeros((batch_size, self.n_hidden)), autograd=True)
        c = Tensor(np.zeros((batch_size, self.n_hidden)), autograd=True)
        h.data[:,0] += 1
        c.data[:,0] += 1
   return (h, c)

升级字符语言模型

让我们用新的 LSTM 单元替换原始 RNN

在本章的早期,您训练了一个字符语言模型来预测莎士比亚。现在让我们训练一个基于 LSTM 的模型来做同样的事情。幸运的是,上一章的框架使得这变得很容易实现(书中的完整代码可在www.manning.com/books/grokking-deep-learning;或在 GitHub 上github.com/iamtrask/grokking-deep-learning找到)。以下是新的设置代码。所有从原始 RNN 代码的编辑都在粗体中。注意,您设置神经网络的方式几乎没有变化:

import sys,random,math
from collections import Counter
import numpy as np
import sys

np.random.seed(0)

f = open('shakespear.txt','r')
raw = f.read()
f.close()

vocab = list(set(raw))
word2index = {}
for i,word in enumerate(vocab):
    word2index[word]=i
indices = np.array(list(map(lambda x:word2index[x], raw)))

embed = Embedding(vocab_size=len(vocab),dim=512)
model = LSTMCell(n_inputs=512, n_hidden=512, n_output=len(vocab))
model.w_ho.weight.data *= 0                                         *1*

criterion = CrossEntropyLoss()
optim = SGD(parameters=model.get_parameters() + embed.get_parameters(),
            alpha=0.05)

batch_size = 16
bptt = 25
n_batches = int((indices.shape[0] / (batch_size)))

trimmed_indices = indices[:n_batches*batch_size]
batched_indices = trimmed_indices.reshape(batch_size, n_batches)
batched_indices = batched_indices.transpose()

input_batched_indices = batched_indices[0:-1]
target_batched_indices = batched_indices[1:]

n_bptt = int(((n_batches-1) / bptt))
input_batches = input_batched_indices[:n_bptt*bptt]
input_batches = input_batches.reshape(n_bptt,bptt,batch_size)
target_batches = target_batched_indices[:n_bptt*bptt]
target_batches = target_batches.reshape(n_bptt, bptt, batch_size)
min_loss = 1000
  • 1 这似乎有助于训练。

训练 LSTM 字符语言模型

训练逻辑也没有发生太多变化

从标准的 RNN 逻辑中,你唯一需要做的真正改变是截断反向传播逻辑,因为每个时间步长有两个隐藏向量而不是一个。但这只是一个相对较小的修复(粗体)。我还增加了一些使训练更简单的功能(alpha 随时间缓慢减少,并且有更多的日志记录):

for iter in range(iterations):
    total_loss, n_loss = (0, 0)
     hidden = model.init_hidden(batch_size=batch_size)
     batches_to_train = len(input_batches)

     for batch_i in range(batches_to_train):

         hidden = (Tensor(hidden[0].data, autograd=True),
                   Tensor(hidden[1].data, autograd=True))
          losses = list()

          for t in range(bptt):
              input = Tensor(input_batches[batch_i][t], autograd=True)
              rnn_input = embed.forward(input=input)
             output, hidden = model.forward(input=rnn_input, hidden=hidden)

              target = Tensor(target_batches[batch_i][t], autograd=True)
              batch_loss = criterion.forward(output, target)

              if(t == 0):
                  losses.append(batch_loss)
              else:
                  losses.append(batch_loss + losses[-1])
          loss = losses[-1]

          loss.backward()
          optim.step()

         total_loss += loss.data / bptt
         epoch_loss = np.exp(total_loss / (batch_i+1))
         if(epoch_loss < min_loss):
             min_loss = epoch_loss
             print()
         log = "\r Iter:" + str(iter)
         log += " - Alpha:" + str(optim.alpha)[0:5]
         log += " - Batch "+str(batch_i+1)+"/"+str(len(input_batches))
         log += " - Min Loss:" + str(min_loss)[0:5]
         log += " - Loss:" + str(epoch_loss)
         if(batch_i == 0):
             s = generate_sample(n=70, init_char='T').replace("\n"," ")
             log += " - " + s
         sys.stdout.write(log)
     optim.alpha *= 0.99

调整 LSTM 字符语言模型

我花费了大约两天的时间调整这个模型,并且它是在夜间训练完成的

这里是此模型的某些训练输出。请注意,训练这个模型花费了非常长的时间(有很多参数)。我还不得不多次训练它,以找到这个任务的良好调整(学习率、批量大小等),并且最终的模型是在夜间(8 小时)训练完成的。一般来说,你训练的时间越长,你的结果就会越好。

I:0 - Alpha:0.05 - Batch 1/249 - Min Loss:62.00 - Loss:62.00 - eeeeeeeeee
                                   ...
I:7 - Alpha:0.04 - Batch 140/249 - Min Loss:10.5 - Loss:10.7 - heres, and
                                   ...
I:91 - Alpha:0.016 - Batch 176/249 - Min Loss:9.900 - Loss:11.9757225699
def generate_sample(n=30, init_char=' '):
    s = ""
    hidden = model.init_hidden(batch_size=1)
    input = Tensor(np.array([word2index[init_char]]))
    for i in range(n):
        rnn_input = embed.forward(input)
        output, hidden = model.forward(input=rnn_input, hidden=hidden)
        output.data *= 15
        temp_dist = output.softmax()
        temp_dist /= temp_dist.sum()

        m = output.data.argmax()           *1*
        c = vocab[m]
        input = Tensor(np.array([m]))
        s += c
    return s
print(generate_sample(n=500, init_char='\n'))
  • 1 取最大预测
Intestay thee.

SIR:
It thou my thar the sentastar the see the see:
Imentary take the subloud I
Stall my thentaring fook the senternight pead me, the gakentlenternot
they day them.

KENNOR:
I stay the see talk :
Non the seady!

Sustar thou shour in the suble the see the senternow the antently the see
the seaventlace peake,
I sentlentony my thent:
I the sentastar thamy this not thame.

摘要

LSTM 是极其强大的模型

LSTM 学习生成莎士比亚语言的分布不容小觑。语言是一个极其复杂的统计分布,学习起来非常困难,而 LSTM 能够做得如此出色(在撰写本文时,它们是广泛领先的最佳方法)仍然让我(以及其他一些人)感到困惑。这个模型的小型变体要么是,要么最近一直是各种任务中的最佳状态,并且与词嵌入和卷积层一起,无疑将是我们长期以来的首选工具。

第十五章. 对未见数据进行的深度学习:介绍联邦学习

本章内容

  • 深度学习中的隐私问题

  • 联邦学习

  • 学习检测垃圾邮件

  • 窃取联邦学习

  • 安全聚合

  • 同态加密

  • 同态加密联邦学习

“朋友不会互相监视;真正的友谊也关乎隐私。”

斯蒂芬·金,《海特斯堡之心》(1999 年)

深度学习中的隐私问题

深度学习(及其工具)通常意味着你可以访问你的训练数据

如你所敏锐地意识到的,深度学习作为机器学习的一个子领域,全部都是关于从数据中学习。但通常,被学习的数据极其个人化。最有意义的模型与人类生活中最个人化的信息互动,并告诉我们一些可能难以通过其他方式了解的事情。换句话说,深度学习模型可以研究成千上万人的生活,帮助你更好地理解自己。

深度学习的主要自然资源是训练数据(无论是合成的还是自然的)。没有它,深度学习就无法学习;而且因为最有价值的使用案例通常与最个人化的数据集互动,深度学习往往是公司寻求聚合数据的原因。他们需要它来解决特定的使用案例。

但在 2017 年,谷歌发布了一篇非常激动人心的论文和博客文章,对这次对话产生了重大影响。谷歌提出,我们不需要集中一个数据集来在上面训练模型。公司提出了这个问题:如果我们不能将所有数据带到一处,我们能否将模型带到数据那里?这是一个新的、令人兴奋的机器学习子领域,称为联邦学习,这正是本章的主题。

如果不是将训练数据集带到一处来训练模型,而是能够将模型带到数据生成的任何地方,会怎么样呢?

这种简单的逆转极其重要。首先,这意味着为了参与深度学习供应链,人们实际上不必将他们的数据发送给任何人。在医疗保健、个人管理和其他敏感领域,有价值的模型可以在不要求任何人透露个人信息的情况下进行训练。理论上,人们可以保留对自己个人数据唯一副本的控制权(至少在深度学习方面)。

这种技术将对企业竞争和创业中的深度学习竞争格局产生巨大影响。以前不会(或不能,由于法律原因)共享客户数据的大型企业可能仍然可以从这些数据中获得收入。在有些领域,数据的敏感性和监管约束一直是进步的阻力。医疗保健就是一个例子,数据集通常被严格锁定,使得研究变得困难。

联邦学习

你不必访问数据集才能从中学习

联邦学习的前提是许多数据集包含对解决问题有用的信息(例如,在 MRI 中识别癌症),但很难以足够的数量访问这些相关的数据集来训练一个足够强大的深度学习模型。主要担忧是,尽管数据集有足够的信息来训练深度学习模型,但它还包含了一些(可能)与学习任务无关的信息,如果泄露可能会对某人造成潜在伤害。

联邦学习是指模型进入一个安全的环境,学习如何解决问题,而不需要数据移动到任何地方。让我们来看一个例子。

import numpy as np
from collections import Counter
import random
import sys
import codecs
np.random.seed(12345)
with codecs.open('spam.txt',"r",encoding='utf-8',errors='ignore') as f: *1*
    raw = f.readlines()

vocab, spam, ham = (set(["<unk>"]), list(), list())
for row in raw:
    spam.append(set(row[:-2].split(" ")))
    for word in spam[-1]:
        vocab.add(word)

with codecs.open(`ham.txt',"r",encoding='utf-8',errors='ignore') as f:
    raw = f.readlines()

for row in raw:
    ham.append(set(row[:-2].split(" ")))
    for word in ham[-1]:
        vocab.add(word)

vocab, w2i = (list(vocab), {})
for i,w in enumerate(vocab):
    w2i[w] = i

def to_indices(input, l=500):
    indices = list()
    for line in input:
        if(len(line) < l):
            line = list(line) + ["<unk>"] * (l - len(line))
            idxs = list()
            for word in line:
                idxs.append(w2i[word])
            indices.append(idxs)
    return indices

学习检测垃圾邮件

假设你想要在人们的电子邮件上训练一个模型来检测垃圾邮件

我们将要讨论的使用案例是电子邮件分类。第一个模型将在一个公开可用的数据集上训练,这个数据集被称为 Enron 数据集,它是一批来自著名 Enron 诉讼案(现在是一个行业标准的电子邮件分析语料库)的大量电子邮件。有趣的事实:我曾经认识一个人,他专业地阅读/注释了这个数据集,人们互相发送了各种各样的疯狂东西(其中很多非常私人)。但由于它在法庭案件中公开发布,现在可以免费使用。

上一节和这一节的代码只是预处理。输入数据文件(ham.txt 和 spam.txt)可在本书的网站上找到,www.manning.com/books/grokking-deep-learning;以及 GitHub 上github.com/iamtrask/Grokking-Deep-Learning。你预处理它以准备好将其前向传播到在第十三章中创建的嵌入类。和之前一样,这个语料库中的所有单词都被转换成了索引列表。你还通过截断电子邮件或用 <unk> 标记填充它,使所有电子邮件正好 500 个单词长。这样做使得最终数据集是方形的。

spam_idx = to_indices(spam)
ham_idx = to_indices(ham)

train_spam_idx = spam_idx[0:-1000]
train_ham_idx = ham_idx[0:-1000]

test_spam_idx = spam_idx[-1000:]
test_ham_idx = ham_idx[-1000:]

train_data = list()
train_target = list()

test_data = list()
test_target = list()

for i in range(max(len(train_spam_idx),len(train_ham_idx))):
    train_data.append(train_spam_idx[i%len(train_spam_idx)])
    train_target.append([1])

    train_data.append(train_ham_idx[i%len(train_ham_idx)])
    train_target.append([0])

for i in range(max(len(test_spam_idx),len(test_ham_idx))):
    test_data.append(test_spam_idx[i%len(test_spam_idx)])
    test_target.append([1])

    test_data.append(test_ham_idx[i%len(test_ham_idx)])
    test_target.append([0])

def train(model, input_data, target_data, batch_size=500, iterations=5):
    n_batches = int(len(input_data) / batch_size)
    for iter in range(iterations):
        iter_loss = 0
        for b_i in range(n_batches):

            # padding token should stay at 0
            model.weight.data[w2i['<unk>']] *= 0
            input = Tensor(input_data[b_i*bs:(b_i+1)*bs], autograd=True)
           target = Tensor(target_data[b_i*bs:(b_i+1)*bs], autograd=True)

            pred = model.forward(input).sum(1).sigmoid()
            loss = criterion.forward(pred,target)
            loss.backward()
            optim.step()

            iter_loss += loss.data[0] / bs

            sys.stdout.write("\r\tLoss:" + str(iter_loss / (b_i+1)))
        print()
    return model

def test(model, test_input, test_output):

    model.weight.data[w2i['<unk>']] *= 0

    input = Tensor(test_input, autograd=True)
    target = Tensor(test_output, autograd=True)

    pred = model.forward(input).sum(1).sigmoid()
    return ((pred.data > 0.5) == target.data).mean()

有这些不错的 train()test() 函数,你可以使用以下几行代码初始化一个神经网络并对其进行训练。仅经过三次迭代,网络就可以在测试数据集上以 99.45%的准确率进行分类(测试数据集是平衡的,所以这相当不错):

|

model = Embedding(vocab_size=len(vocab), dim=1)
model.weight.data *= 0
criterion = MSELoss()
optim = SGD(parameters=model.get_parameters(), alpha=0.01)

for i in range(3):
    model = train(model, train_data, train_target, iterations=1)
    print("% Correct on Test Set: " + \
          str(test(model, test_data, test_target)*100))

|

|

       Loss:0.037140416860871446
% Correct on Test Set: 98.65
       Loss:0.011258669226059114
% Correct on Test Set: 99.15
       Loss:0.008068268387986223
% Correct on Test Set: 99.45

|

让我们将其变为联邦学习

之前的例子是普通的深度学习。让我们保护隐私

在上一节中,你得到了电子邮件的例子。现在,让我们把所有电子邮件放在一个地方。这是老式的方法(这在世界上仍然非常普遍)。让我们首先模拟一个联邦学习环境,它包含多个不同的电子邮件集合:

bob = (train_data[0:1000], train_target[0:1000])
alice = (train_data[1000:2000], train_target[1000:2000])
sue = (train_data[2000:], train_target[2000:])

足够简单。现在你可以像以前一样进行相同的训练,但同时在每个人的电子邮件数据库中进行。每次迭代后,你将平均鲍勃、爱丽丝和苏的模型值并评估。请注意,一些联邦学习的聚合方法在每个批次(或批次集合)之后进行;我保持简单:

for i in range(3):
    print("Starting Training Round...")
    print("\tStep 1: send the model to Bob")
    bob_model = train(copy.deepcopy(model), bob[0], bob[1], iterations=1)

    print("\n\tStep 2: send the model to Alice")
    alice_model = train(copy.deepcopy(model),
                        alice[0], alice[1], iterations=1)

    print("\n\tStep 3: Send the model to Sue")
    sue_model = train(copy.deepcopy(model), sue[0], sue[1], iterations=1)

    print("\n\tAverage Everyone's New Models")
    model.weight.data = (bob_model.weight.data + \
                         alice_model.weight.data + \
                         sue_model.weight.data)/3

    print("\t% Correct on Test Set: " + \
          str(test(model, test_data, test_target)*100))

    print("\nRepeat!!\n")

下一个部分将展示结果。模型的学习效果几乎与之前相同,从理论上讲,你没有访问到训练数据——或者你有吗?毕竟,每个人都在以某种方式改变模型,对吧?你真的不能发现任何关于他们的数据集的信息吗?

Starting Training Round...
  Step 1: send the model to Bob
  Loss:0.21908166249699718

           ......

   Step 3: Send the model to Sue
 Loss:0.015368461608470256

 Average Everyone's New Models
 % Correct on Test Set: 98.8

窃取联邦学习

让我们用一个玩具示例来看看如何仍然学习训练数据集

联邦学习面临两大挑战,这两个挑战在训练数据集中每个人只有少量训练示例时最为严重。这些挑战是性能和隐私。实际上,如果某人只有少量训练示例(或者他们发送给你的模型改进只使用了少量示例:一个训练批次),你仍然可以学到很多关于数据的信息。假设有 10,000 人(每人有一些数据),你将花费大部分时间在来回发送模型,而不是在训练(尤其是如果模型非常大时)。

但我们跑题了。让我们看看当用户在单个批次上执行权重更新时,你能学到什么:

import copy

bobs_email = ["my", "computer", "password", "is", "pizza"]

bob_input = np.array([[w2i[x] for x in bobs_email]])
bob_target = np.array([[0]])

model = Embedding(vocab_size=len(vocab), dim=1)
model.weight.data *= 0

bobs_model = train(copy.deepcopy(model),
                   bob_input, bob_target, iterations=1, batch_size=1)

鲍勃将使用他收件箱中的一封电子邮件来创建对模型的更新。但鲍勃把他自己的密码保存在一封发给自己的电子邮件中,说:“我的电脑密码是披萨。”愚蠢的鲍勃。通过查看哪些权重发生了变化,你可以推断出鲍勃电子邮件的词汇(并推断其含义):

for i, v in enumerate(bobs_model.weight.data - model.weight.data):
    if(v != 0):
    print(vocab[i])
is
pizza
computer
password
my

就这样,你学会了鲍勃的超秘密密码(也许还有他最喜欢的食物)。怎么办?如果从权重更新中很容易看出训练数据,你该如何使用联邦学习?

安全聚合

在任何人看到之前,让我们平均来自成千上万人的权重更新

解决方案是永远不要让鲍勃像那样公开地发布梯度。如果人们不应该看到它,鲍勃如何贡献他的梯度?社会科学使用一种有趣的技巧,称为随机响应

它是这样的。假设你正在进行一项调查,你想询问 100 个人他们是否犯过严重的罪行。当然,即使你承诺不会告诉任何人,他们也会回答“没有”。相反,你让他们抛两次硬币(在你看不见的地方),并告诉他们如果第一次抛硬币是正面,他们应该诚实地回答;如果是反面,他们应该根据第二次抛硬币的结果回答“是”或“否”。

在这种情况下,你实际上从未要求人们告诉你他们是否犯了罪。真正的答案隐藏在第一次和第二次抛硬币的随机噪声中。如果 60%的人说“是”,你可以通过简单的数学计算确定,大约 70%的受访者犯了严重的罪行(上下几个百分点)。这个想法是,随机噪声使得你了解到关于个人的任何信息可能来自噪声而不是他们自己。

通过可辩驳的否认来保护隐私

特定答案来自随机噪声而不是个人的概率水平,通过提供可辩驳的否认来保护他们的隐私。这构成了安全聚合的基础,以及更广泛地,差分隐私的大部分内容。

你只看到整体的汇总统计数据。(你永远不会直接看到任何人的答案;你只看到答案对或更大的分组。)因此,在添加噪声之前,你可以聚合更多的人,你就不需要添加太多的噪声来隐藏他们(并且结果会更加准确)。

在联邦学习的背景下,你可以(如果你愿意)添加大量的噪声,但这会损害训练。相反,首先将所有参与者的梯度求和,这样没有人能看到除了自己的梯度以外的任何人的梯度。这类问题被称为 安全聚合,为了做到这一点,你还需要一个额外的(非常酷)工具:同态加密

同态加密

你可以对加密值执行算术运算

研究中最激动人心的前沿之一是人工智能(包括深度学习)与密码学的交叉领域。在这个激动人心的交叉点中,有一个非常酷的技术叫做同态加密。简单来说,同态加密允许你在不解密的情况下对加密值进行计算。

尤其是我们对在这些值上执行加法感兴趣。详细解释其工作原理需要一本整本书,但我会用几个定义来展示它是如何工作的。首先,一个 公钥 允许你加密数字。一个 私钥 允许你解密加密的数字。加密的值称为 密文,未加密的值称为 明文

让我们通过使用 phe 库的例子来看看同态加密。(要安装库,请运行 pip install phe 或从 GitHub github.com/n1analytics/python-paillier 下载):

import phe

public_key, private_key = phe.generate_paillier_keypair(n_length=1024)

x = public_key.encrypt(5)         *1*

y = public_key.encrypt(3)         *2*

z = x + y                         *3*

z_ = private_key.decrypt(z)       *4*
print("The Answer: " + str(z_))
  • 1 加密数字 5

  • 2 加密数字 3

  • 3 将两个加密值相加

  • 4 解密结果

The Answer: 8

这段代码在加密状态下将两个数字(5 和 3)相加。非常巧妙,不是吗?还有一种技术与同态加密有点类似:安全多方计算。你可以在“密码学与机器学习”博客(mortendahl.github.io)上了解它。

现在,让我们回到安全聚合的问题。鉴于你新获得的知识,你可以将你看不见的数字相加,答案就变得显而易见了。初始化模型的个人将一个public_key发送给鲍勃、爱丽丝和苏,这样他们就可以分别加密他们的权重更新。然后,鲍勃、爱丽丝和苏(他们没有私钥)直接相互沟通,并将所有梯度累积成一个单一、最终的更新,发送回模型所有者,该所有者使用private_key对其进行解密。

同态加密联邦学习

让我们使用同态加密来保护正在聚合的梯度

model = Embedding(vocab_size=len(vocab), dim=1)
model.weight.data *= 0

# note that in production the n_length should be at least 1024
public_key, private_key = phe.generate_paillier_keypair(n_length=128)

def train_and_encrypt(model, input, target, pubkey):
    new_model = train(copy.deepcopy(model), input, target, iterations=1)

    encrypted_weights = list()
    for val in new_model.weight.data[:,0]:
        encrypted_weights.append(public_key.encrypt(val))
    ew = np.array(encrypted_weights).reshape(new_model.weight.data.shape)

    return ew

for i in range(3):
    print("\nStarting Training Round...")
    print("\tStep 1: send the model to Bob")
    bob_encrypted_model = train_and_encrypt(copy.deepcopy(model),
                                            bob[0], bob[1], public_key)

    print("\n\tStep 2: send the model to Alice")
    alice_encrypted_model=train_and_encrypt(copy.deepcopy(model),
                                            alice[0],alice[1],public_key)

    print("\n\tStep 3: Send the model to Sue")
    sue_encrypted_model = train_and_encrypt(copy.deepcopy(model),
                                            sue[0], sue[1], public_key)

    print("\n\tStep 4: Bob, Alice, and Sue send their")
    print("\tencrypted models to each other.")
    aggregated_model = bob_encrypted_model + \
                       alice_encrypted_model + \
                       sue_encrypted_model

    print("\n\tStep 5: only the aggregated model")
    print("\tis sent back to the model owner who")
    print("\t can decrypt it.")
    raw_values = list()
    for val in sue_encrypted_model.flatten():
        raw_values.append(private_key.decrypt(val))
    new = np.array(raw_values).reshape(model.weight.data.shape)/3
    model.weight.data = new

    print("\t% Correct on Test Set: " + \
              str(test(model, test_data, test_target)*100))

现在,你可以运行新的训练方案,它增加了一个步骤。爱丽丝、鲍勃和苏在将模型发送回你之前,将他们的同态加密模型相加,这样你就永远不会看到哪些更新来自哪个人(一种合理的否认形式)。在生产中,你还会添加一些额外的随机噪声,足以满足鲍勃、爱丽丝和苏(根据他们的个人偏好)所需的一定隐私阈值。更多内容将在未来的工作中介绍。

Starting Training Round...
  Step 1: send the model to Bob
  Loss:0.21908166249699718

  Step 2: send the model to Alice
  Loss:0.2937106899184867

             ...
             ...
             ...

  % Correct on Test Set: 99.15

摘要

联邦学习是深度学习中最激动人心的突破之一

我坚信,联邦学习将在未来几年改变深度学习的格局。它将解锁之前由于过于敏感而无法处理的新的数据集,从而为这种新出现的创业机会创造巨大的社会效益。这是加密与人工智能研究更广泛融合的一部分,在我看来,这是十年中最激动人心的融合。

阻碍这些技术在实际应用中发挥作用的因素主要是它们在现代深度学习工具包中的不可用性。转折点将是任何人都可以运行pip install...然后获得访问深度学习框架的权限,在这些框架中,隐私和安全是首要公民,并且内置了联邦学习、同态加密、差分隐私和安全多方计算等技术(而且你不需要是专家就能使用它们)。

出于这种信念,我在过去一年中作为 OpenMined 项目的一部分,与一群开源志愿者一起工作,将这些原语扩展到主要的深度学习框架中。如果你相信这些工具对未来隐私和安全的重要性,请访问我们的网站openmined.org或 GitHub 仓库(github.com/OpenMined)。即使只是给几个仓库点个赞,也请表达你的支持;如果你能加入我们,那就更好了(聊天室:slack.openmined.org)。

第十六章。从这里开始:简要指南

本章内容

  • 第 1 步:开始学习 PyTorch

  • 第 2 步:开始另一门深度学习课程

  • 第 3 步:获取一本数学深度学习教科书

  • 第 4 步:开始写博客,教授深度学习

  • 第 5 步:Twitter

  • 第 6 步:实现学术论文

  • 第 7 步:获取访问 GPU 的权限

  • 第 8 步:通过实践获得报酬

  • 第 9 步:加入开源项目

  • 第 10 步:发展你的本地社区

“无论你是否相信你能做某事,你都是对的。”

亨利·福特,汽车制造商

恭喜你!

如果你正在阅读这篇文章,你已经走过了近 300 页的深度学习内容

你做到了!这是一大堆材料。我为你感到骄傲,你也应该为自己感到骄傲。今天应该是一个庆祝的日子。到目前为止,你理解了人工智能背后的基本概念,并且应该对自己的能力感到相当自信,无论是谈论它们还是学习高级概念。

这最后一章包含了一些简短的章节,讨论了适合你的下一步行动,尤其是如果你是深度学习领域的初学者。我的一般假设是你对这个领域感兴趣,或者至少想在旁边继续探索,我希望我的总体评论能帮助你找到正确的方向(尽管它们只是非常一般的指导方针,可能或可能不直接适用于你)。

第 1 步:开始学习 PyTorch

你制作的深度学习框架最接近 PyTorch

你一直使用 NumPy 学习深度学习,这是一个基本的矩阵库。然后你构建了自己的深度学习工具包,并且你也相当多地使用了它。但从这一点开始,除非在学习新的架构,你应该使用实际的框架进行实验。这将更少出错。它将运行(非常快),你将能够继承/学习其他人的代码。

为什么你应该选择 PyTorch?有很多好的选择,但如果你来自 NumPy 背景,PyTorch 会感觉最熟悉。此外,你在第十三章(kindle_split_021.xhtml#ch13)中构建的框架与 PyTorch 的 API 非常相似。我这样做是为了特别为你准备一个实际的框架。如果你选择 PyTorch,你会感到非常自在。话虽如此,选择深度学习框架有点像加入霍格沃茨的一所房子:它们都很棒(但 PyTorch 绝对是格兰芬多)。

现在下一个问题:你应该如何学习 PyTorch?最好的方法是参加一门教授你使用该框架进行深度学习的深度学习课程。这将帮助你回忆起你已经熟悉的概念,同时向你展示每个部分在 PyTorch 中的位置。(你将在学习随机梯度下降的同时了解它在 PyTorch API 中的位置。)在撰写本文时,做这件事的最好地方可能是 Udacity 的深度学习纳米学位(尽管我有所偏见:我帮助教授了它)或 fast.ai。此外,pytorch.org/tutorialsgithub.com/pytorch/examples是金子般的学习资源。

第 2 步:开始另一门深度学习课程

我通过反复学习相同的概念来学习深度学习

虽然认为一本书或一门课程就足以满足你整个深度学习教育是件很美好的事情,但事实并非如此。即使这本书(它们并没有)涵盖了所有概念,从多个角度听到相同的概念对于你真正理解它们是至关重要的(看看我做了什么?)。在我作为开发者的成长过程中,我可能参加了大约六门不同的课程(或 YouTube 系列),除了观看大量的 YouTube 视频和阅读大量描述基本概念的博客文章。

在 YouTube 上寻找来自大型深度学习大学或 AI 实验室(斯坦福、麻省理工、牛津、蒙特利尔、纽约大学等)的在线课程。观看所有视频。完成所有练习。如果可能的话,做 fast.ai 和 Udacity 的课程。反复学习相同的概念。练习它们。熟悉它们。你希望基本原理在你的脑海中变得像第二本能一样自然。

第 3 步:找一本数学深度学习教科书

你可以从你的深度学习知识中逆向工程数学

我在大学本科的专业是应用离散数学,但我从深度学习中学到的代数、微积分和统计学比我在课堂上学到的要多得多。此外,这可能听起来令人惊讶,我是通过编写 NumPy 代码并回到它实现的数学问题来解决问题的。这就是我真正在更深的层次上学习深度学习相关数学的方法。这是一个我希望你能够铭记在心的好方法。

如果你不确定该选择哪本数学书籍,那么在撰写本文时,市场上可能最好的是 Ian Goodfellow、Yoshua Bengio 和 Aaron Courville 合著的《深度学习》(MIT Press,2016 年)。它在数学方面并不疯狂,但它是这本书(以及书前面的数学符号指南)的下一步(数学符号指南是金子般的资源)。

第 4 步:开始写博客,并教授深度学习

我所做的一切都极大地帮助了我的知识和职业生涯

我可能应该把它作为第一步,但就这样开始了。没有什么比在我的博客上教授深度学习(以及我在深度学习领域的职业生涯)更能提升我的深度学习知识了。教学迫使你尽可能简单地解释一切,而公众羞辱的恐惧将确保你做得很好。

有趣的故事:我的第一篇博客文章登上了 Hacker News,但写得非常糟糕,一个顶级 AI 实验室的主要研究员在评论中彻底摧毁了我。这伤害了我的感情和自信,但它也使我的写作更加严谨。它让我意识到,大多数时候,当我读某件事很难理解时,这不是我的错;写这篇文章的人没有花足够的时间解释我需要了解的所有小细节,以便理解完整的概念。他们没有提供相关的类比来帮助我的理解。

总之,开始写博客。尽量登上 Hacker News 或 ML Reddit 首页。从教授基本概念开始。尽量做得比任何人都要好。不用担心话题是否已经被覆盖。时至今日,我最受欢迎的博客文章是“用 11 行 Python 实现神经网络”,它教授了深度学习中教授最多的东西:一个基本的正向神经网络。但我能够以新的方式解释它,这帮助了一些人。它之所以能这样做,主要是因为我以帮助我理解的方式写了这篇文章。这就是关键。以你想要学习的方式教授事物。

不要只是总结深度学习概念!总结很无聊,没有人想读它们。写教程。你写的每一篇博客文章都应该包括一个学习做某事的神经网络——读者可以下载并运行的东西。你的博客应该逐行说明每个部分的作用,这样即使是五岁的孩子也能理解。这就是标准。当你为两页博客文章工作了三天后,你可能想放弃,但那不是回头的时候:那是继续前进并使其变得惊人的时候!一篇优秀的博客文章可以改变你的生活。相信我。

如果你想要申请一份工作、硕士或博士项目来从事人工智能,选择一个你想要在那个项目中与之合作的研究员,并写关于他们工作的教程。每次我这样做,都导致后来遇到了那位研究员。这样做表明你理解他们正在使用的概念,这是他们想要与你合作的前提条件。这比冷邮件要好得多,因为,假设它出现在 Reddit、Hacker News 或某个其他场合,其他人会先把它发给他们。有时他们甚至会主动联系你。

第 5 步:推特

大多数人工智能对话都发生在推特上

我在 Twitter 上遇到的世界各地的研究人员比其他任何方式都要多,我几乎读过的每一篇论文都是因为我关注了那些发推文的人。你想要了解最新的变化;更重要的是,你想要成为对话的一部分。我开始是找到一些我尊敬的人工智能研究人员,关注他们,然后关注他们关注的人。这让我开始有了动态,极大地帮助了我。(只是不要让它变成一种上瘾!)

第 6 步:实现学术论文

Twitter + 你的博客 = 学术论文教程

观察你的 Twitter 动态,直到你遇到一篇听起来既有趣又不需要大量 GPU 的论文。为它写一篇教程。你将不得不阅读这篇论文,解读数学,并经历原始研究人员也必须经历的调整过程。如果你对抽象研究感兴趣,这无疑是一项最好的练习。我在国际机器学习会议(ICML)上发表的第一篇论文就是从我阅读这篇论文并随后逆向工程 word2vec 中的代码开始的。最终,当你阅读时,你会想,“等等!我觉得我可以让它变得更好!”就这样:你成为了一名研究人员。

第 7 步:获取访问 GPU(或多个)的权限

你能更快地进行实验,你就能更快地学习

没有人不知道 GPU 可以提供 10 到 100 倍更快的训练时间,但它的含义是你可以以 100 倍的速度迭代你自己的(好与坏)想法。这对学习深度学习来说是无价之宝。我在职业生涯中犯的一个错误是等得太久才开始使用 GPU。不要像我一样:从 NVIDIA 购买一个,或者使用你可以在 Google Colab 笔记本中访问的免费的 K80。NVIDIA 偶尔也允许学生在某些 AI 竞赛中免费使用他们的 GPU,但你必须小心。

第 8 步:获得报酬来练习

你有更多的时间进行深度学习,你就能更快地学习

我的职业生涯中的另一个转折点是当我得到一份让我能够探索深度学习工具和研究的工作。成为一名数据科学家、数据工程师或研究工程师,或者作为统计顾问自由职业。关键是,你想要找到一种方式,在工作时间内获得报酬的同时继续学习。这些工作确实存在;只是需要一些努力去找到它们。

你的博客对于获得这类工作至关重要。无论你想要什么样的工作,至少写两篇博客文章来展示你能够胜任他们想要招聘的任何工作。这就是完美的简历(比数学学位还要好)。理想的候选人已经证明他们能够胜任这项工作。

第 9 步:加入开源项目

在人工智能领域,最快建立人脉和职业发展的方式是成为开源项目中的核心开发者

找到一个你喜欢的深度学习框架,并开始实施项目。很快,你就会与顶级实验室的研究人员互动(他们将会阅读/批准你的拉取请求)。我知道很多人通过这种方法找到了令人惊叹的工作(看似从天而降)。

话虽如此,你必须投入时间。没有人会牵着你的手。阅读代码。交朋友。从添加单元测试和解释代码的文档开始,然后修复 bug,最终开始着手更大的项目。这需要时间,但这是对你未来的投资。如果你不确定,可以选择像 PyTorch、TensorFlow 或 Keras 这样的主要深度学习框架,或者你可以来 OpenMined 与我一起工作(我认为这是周围最酷的开源项目)。我们非常欢迎新手。

第 10 步:发展你的本地社区

我真正学习深度学习是因为我喜欢和那些

我在 Bongo Java 学习了深度学习,当时我坐在我的最好朋友旁边,他们也对这个领域感兴趣。当遇到难以修复的 bug(我花了两天时间才找到一个句号)或难以掌握的概念时,我之所以能坚持下去,很大一部分原因是我花时间与我所爱的人在一起。不要低估这一点。如果你在一个你喜欢的地方,与你喜欢的人在一起,你将会工作更长,进步更快。这不是火箭科学,但你必须是有意的。谁知道呢?你可能在这个过程中还会有点乐趣!

posted @ 2025-11-23 09:26  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报