当前位置:   article > 正文

FastAI 之书(面向程序员的 FastAI)(二)_facct原则

facct原则

原文:www.bookstack.cn/read/th-fastai-book

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:数据伦理

原文:www.bookstack.cn/read/th-fastai-book/9bc6d15b4440b85d.md

译者:飞龙

协议:CC BY-NC-SA 4.0

正如我们在第一章和第二章中讨论的,有时机器学习模型可能出错。它们可能有错误。它们可能被呈现出以前没有见过的数据,并以我们意料之外的方式行事。或者它们可能完全按设计工作,但被用于我们非常希望它们永远不要被用于的事情。

因为深度学习是如此强大的工具,可以用于很多事情,所以我们特别需要考虑我们选择的后果。哲学上对伦理的研究是对对错的研究,包括我们如何定义这些术语,识别对错行为,以及理解行为和后果之间的联系。数据伦理领域已经存在很长时间,许多学者都专注于这个领域。它被用来帮助定义许多司法管辖区的政策;它被用在大大小小的公司中,考虑如何最好地确保产品开发对社会的良好结果;它被研究人员用来确保他们正在做的工作被用于好的目的,而不是坏的目的。

因此,作为一个深度学习从业者,你很可能在某个时候会面临需要考虑数据伦理的情况。那么数据伦理是什么?它是伦理学的一个子领域,所以让我们从那里开始。

杰里米说

在大学里,伦理哲学是我的主要研究领域(如果我完成了论文,而不是辍学加入现实世界,它本来会是我的论文题目)。根据我花在研究伦理学上的年份,我可以告诉你这个:没有人真正同意什么是对什么是错,它们是否存在,如何识别它们,哪些人是好人哪些人是坏人,或者几乎任何其他事情。所以不要对理论抱太大期望!我们将在这里专注于例子和思考的起点,而不是理论。

在回答问题“什么是伦理?” 应用伦理马库拉中心说,这个术语指的是以下内容:

  • 有根据的对人类应该做什么的正确和错误的标准

  • 研究和发展自己的伦理标准

没有正确答案的清单。没有应该和不应该做的清单。伦理是复杂的,依赖于背景。它涉及许多利益相关者的观点。伦理是一个你必须发展和实践的能力。在本章中,我们的目标是提供一些路标,帮助你在这个旅程中前进。

发现伦理问题最好是作为一个协作团队的一部分来做。这是你真正可以融入不同观点的唯一方式。不同人的背景将帮助他们看到你可能没有注意到的事情。与团队合作对于许多“锻炼肌肉”的活动都是有帮助的,包括这个。

这一章当然不是本书中唯一讨论数据伦理的部分,但是有一个地方专注于它一段时间是很好的。为了定位,也许最容易看一些例子。所以,我们挑选了三个我们认为有效地说明了一些关键主题的例子。

数据伦理的关键例子

我们将从三个具体的例子开始,这些例子说明了技术中三个常见的伦理问题(我们将在本章后面更深入地研究这些问题):

救济程序

阿肯色州有缺陷的医疗保健算法让患者陷入困境。

反馈循环

YouTube 的推荐系统帮助引发了阴谋论繁荣。

偏见

当在谷歌上搜索传统的非裔美国人名字时,会显示犯罪背景调查的广告。

事实上,在本章中我们介绍的每个概念,我们都会提供至少一个具体的例子。对于每一个例子,想想在这种情况下你可以做什么,以及可能会有什么样的障碍阻止你完成。你会如何处理它们?你会注意什么?

错误和救济:用于医疗福利的错误算法

《The Verge》调查了在美国半数以上州使用的软件,以确定人们接受多少医疗保健,并在文章《当算法削减您的医疗保健时会发生什么》中记录了其发现。在阿肯色州实施算法后,数百人(许多患有严重残疾的人)的医疗保健被大幅削减。

例如,Tammy Dobbs 是一名患有脑瘫的女性,需要助手帮助她起床、上厕所、拿食物等,她的帮助时间突然减少了 20 小时每周。她无法得到任何解释为什么她的医疗保健被削减。最终,一场法庭案件揭示了算法的软件实施中存在错误,对患有糖尿病或脑瘫的人造成了负面影响。然而,Dobbs 和许多其他依赖这些医疗福利的人生活在恐惧中,担心他们的福利可能再次突然而莫名其妙地被削减。

反馈循环:YouTube 的推荐系统

当您的模型控制您获得的下一轮数据时,反馈循环可能会发生。返回的数据很快就会被软件本身破坏。

例如,YouTube 有 19 亿用户,他们每天观看超过 10 亿小时的 YouTube 视频。其推荐算法(由谷歌构建)旨在优化观看时间,负责约 70%的观看内容。但出现了问题:它导致了失控的反馈循环,导致《纽约时报》在 2019 年 2 月发表了标题为《YouTube 引发了阴谋论繁荣。能够控制吗?》的文章。表面上,推荐系统正在预测人们会喜欢什么内容,但它们也在很大程度上决定了人们甚至看到什么内容。

偏见:拉塔尼亚·斯威尼“被捕”

拉塔尼亚·斯威尼博士是哈佛大学的教授,也是该大学数据隐私实验室的主任。在论文《在线广告投放中的歧视》中,她描述了她发现谷歌搜索她的名字会出现“拉塔尼亚·斯威尼,被捕了?”的广告,尽管她是唯一已知的拉塔尼亚·斯威尼,从未被捕。然而,当她搜索其他名字,如“Kirsten Lindquist”时,她得到了更中立的广告,尽管 Kirsten Lindquist 已经被捕了三次。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1。谷歌搜索显示关于拉塔尼亚·斯威尼(不存在的)被捕记录的广告

作为一名计算机科学家,她系统地研究了这个问题,并查看了 2000 多个名字。她发现了一个明显的模式:历史上黑人的名字会收到暗示这个人有犯罪记录的广告,而传统上的白人名字则会有更中立的广告。

这是偏见的一个例子。它可能对人们的生活产生重大影响,例如,如果一个求职者被谷歌搜索,可能会出现他们有犯罪记录的情况,而实际上并非如此。

这为什么重要?

考虑这些问题的一个非常自然的反应是:“那又怎样?这和我有什么关系?我是一名数据科学家,不是政治家。我不是公司的高级执行官之一,他们决定我们要做什么。我只是尽力构建我能构建的最具预测性的模型。”

这些是非常合理的问题。但我们将试图说服您,答案是每个训练模型的人都绝对需要考虑他们的模型将如何被使用,并考虑如何最好地确保它们被尽可能积极地使用。有一些你可以做的事情。如果你不这样做,事情可能会变得相当糟糕。

当技术人员以任何代价专注于技术时,发生的一个特别可怕的例子是 IBM 与纳粹德国的故事。2001 年,一名瑞士法官裁定认为“推断 IBM 的技术援助促进了纳粹在犯下反人类罪行时的任务,这些行为还涉及 IBM 机器进行的会计和分类,并在集中营中使用。”

你看,IBM 向纳粹提供了数据制表产品,以追踪大规模灭绝犹太人和其他群体。这是公司高层的决定,向希特勒及其领导团队推销。公司总裁托马斯·沃森亲自批准了 1939 年发布特殊的 IBM 字母排序机,以帮助组织波兰犹太人的驱逐。在图 3-2 中,阿道夫·希特勒(最左)与 IBM 首席执行官汤姆·沃森(左二)会面,希特勒在 1937 年授予沃森特别的“对帝国的服务”奖章。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-2. IBM 首席执行官汤姆·沃森与阿道夫·希特勒会面

但这并不是个案 - 该组织的涉入是广泛的。IBM 及其子公司在集中营现场提供定期培训和维护:打印卡片,配置机器,并在它们经常出现故障时进行维修。IBM 在其打孔卡系统上设置了每个人被杀害的方式,他们被分配到的组别以及跟踪他们通过庞大的大屠杀系统所需的后勤信息的分类。IBM 在集中营中对犹太人的代码是 8:约有 600 万人被杀害。对于罗姆人的代码是 12(纳粹将他们标记为“不合群者”,在“吉普赛营”中有超过 30 万人被杀害)。一般处决被编码为 4,毒气室中的死亡被编码为 6。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-3. IBM 在集中营中使用的打孔卡

当然,参与其中的项目经理、工程师和技术人员只是过着普通的生活。照顾家人,周日去教堂,尽力做好自己的工作。服从命令。市场营销人员只是尽力实现他们的业务发展目标。正如《IBM 与大屠杀》(Dialog Press)的作者埃德温·布莱克所观察到的:“对于盲目的技术官僚来说,手段比目的更重要。犹太人民的毁灭变得更不重要,因为 IBM 技术成就的振奋性只会因在面包排长队的时候赚取的奇幻利润而更加突出。”

退一步思考一下:如果你发现自己是一个最终伤害社会的系统的一部分,你会有什么感受?你会愿意了解吗?你如何帮助确保这种情况不会发生?我们在这里描述了最极端的情况,但今天观察到与人工智能和机器学习相关的许多负面社会后果,其中一些我们将在本章中描述。

这也不仅仅是道德负担。有时,技术人员会直接为他们的行为付出代价。例如,作为大众汽车丑闻的结果而被监禁的第一个人并不是监督该项目的经理,也不是公司的执行主管。而是其中一名工程师詹姆斯·梁,他只是听从命令。

当然,情况并非全是坏的 - 如果你参与的项目最终对一个人产生了巨大的积极影响,这会让你感到非常棒!

好的,希望我们已经说服您应该关心这个问题。但是您应该怎么做呢?作为数据科学家,我们自然倾向于通过优化某些指标来改进我们的模型。但是优化这个指标可能不会导致更好的结果。即使它确实有助于创造更好的结果,几乎肯定不会是唯一重要的事情。考虑一下从研究人员或从业者开发模型或算法到使用这项工作做出决策之间发生的步骤流程。如果我们希望获得我们想要的结果,整个流程必须被视为一个整体。

通常,从一端到另一端有一条非常长的链。如果您是一名研究人员,甚至可能不知道您的研究是否会被用于任何事情,或者如果您参与数据收集,那就更早了。但是没有人比您更适合告知所有参与这一链的人您的工作的能力、约束和细节。虽然没有“灵丹妙药”可以确保您的工作被正确使用,但通过参与这个过程,并提出正确的问题,您至少可以确保正确的问题正在被考虑。

有时,对于被要求做一项工作的正确回应就是说“不”。然而,我们经常听到的回应是:“如果我不做,别人会做。”但请考虑:如果您被选中做这项工作,那么您是他们找到的最合适的人——所以如果您不做,最合适的人就不会参与该项目。如果他们询问的前五个人也都说不,那就更好了!

将机器学习与产品设计整合

假设您做这项工作的原因是希望它被用于某些目的。否则,您只是在浪费时间。因此,让我们假设您的工作最终会有所作为。现在,当您收集数据并开发模型时,您会做出许多决定。您将以什么级别的聚合存储数据?应该使用什么损失函数?应该使用什么验证和训练集?您应该专注于实现的简单性、推理的速度还是模型的准确性?您的模型如何处理域外数据项?它可以进行微调,还是必须随时间从头开始重新训练?

这些不仅仅是算法问题。它们是数据产品设计问题。但是产品经理、高管、法官、记者、医生——最终会开发和使用您的模型的系统的人——将无法理解您所做的决定,更不用说改变它们了。

例如,两项研究发现亚马逊的面部识别软件产生了不准确种族偏见的结果。亚马逊声称研究人员应该更改默认参数,但没有解释这将如何改变有偏见的结果。此外,事实证明,亚马逊并没有指导使用其软件的警察部门这样做。可以想象,开发这些算法的研究人员和为警察提供指导的亚马逊文档人员之间存在很大的距离。

缺乏紧密整合导致社会、警察和亚马逊出现严重问题。结果表明,其系统错误地将 28 名国会议员与犯罪照片匹配!(而与犯罪照片错误匹配的国会议员是有色人种,如图 3-4 所示。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-4. 亚马逊软件将国会议员与犯罪照片匹配

数据科学家需要成为跨学科团队的一部分。研究人员需要与最终使用他们研究成果的人密切合作。更好的是,领域专家们自己可以学到足够的知识,以便能够自己训练和调试一些模型——希望你们中的一些人正在阅读这本书!

现代职场是一个非常专业化的地方。每个人都倾向于有明确定义的工作要做。特别是在大公司,很难知道所有的细节。有时公司甚至会故意模糊正在进行的整体项目目标,如果他们知道员工不会喜欢答案的话。有时通过尽可能地将部分隔离来实现这一点。

换句话说,我们并不是说这些都很容易。这很难。真的很难。我们都必须尽力而为。我们经常看到那些参与这些项目更高层次背景的人,试图发展跨学科能力和团队的人,成为他们组织中最重要和最受奖励的成员之一。这是一种工作,往往受到高级主管的高度赞赏,即使有时被中层管理人员认为相当不舒服。

数据伦理学主题

数据伦理学是一个广阔的领域,我们无法涵盖所有内容。相反,我们将选择一些我们认为特别相关的主题:

  • 追索和问责制的需求

  • 反馈循环

  • 偏见

  • 虚假信息

让我们依次看看每一个。

追索和问责制

在一个复杂的系统中,很容易没有任何一个人感到对结果负责。虽然这是可以理解的,但这并不会带来好的结果。在早期的阿肯色州医疗保健系统的例子中,一个错误导致患有脑瘫的人失去了所需护理的访问权限,算法的创建者责怪政府官员,政府官员责怪那些实施软件的人。纽约大学教授丹娜·博伊德描述了这种现象:“官僚主义经常被用来转移或逃避责任……今天的算法系统正在扩展官僚主义。”

追索如此必要的另一个原因是数据经常包含错误。审计和纠错机制至关重要。加利福尼亚执法官员维护的一个涉嫌帮派成员的数据库发现充满了错误,包括 42 名不到 1 岁的婴儿被添加到数据库中(其中 28 名被标记为“承认是帮派成员”)。在这种情况下,没有流程来纠正错误或在添加后删除人员。另一个例子是美国信用报告系统:2012 年联邦贸易委员会(FTC)对信用报告进行的大规模研究发现,26%的消费者的档案中至少有一个错误,5%的错误可能是灾难性的。

然而,纠正这类错误的过程非常缓慢和不透明。当公共广播记者鲍比·艾伦发现自己被错误列为有枪支罪时,他花了“十几个电话,一个县法院书记的手工操作和六周的时间来解决问题。而且这还是在我作为一名记者联系了公司的传播部门之后。”

作为机器学习从业者,我们并不总是认为理解我们的算法最终如何在实践中实施是我们的责任。但我们需要。

反馈循环

我们在第一章中解释了算法如何与环境互动以创建反馈循环,做出预测以加强在现实世界中采取的行动,从而导致更加明显朝着同一方向的预测。举个例子,让我们再次考虑 YouTube 的推荐系统。几年前,谷歌团队谈到他们如何引入了强化学习(与深度学习密切相关,但你的损失函数代表了潜在长时间后行动发生的结果)来改进 YouTube 的推荐系统。他们描述了如何使用一个算法,使推荐以优化观看时间为目标。

然而,人类往往被争议性内容所吸引。这意味着关于阴谋论之类的视频开始越来越多地被推荐给用户。此外,事实证明,对阴谋论感兴趣的人也是那些经常观看在线视频的人!因此,他们开始越来越多地被吸引到 YouTube。越来越多的阴谋论者在 YouTube 上观看视频导致算法推荐越来越多的阴谋论和其他极端内容,这导致更多的极端分子在 YouTube 上观看视频,更多的人在 YouTube 上形成极端观点,进而导致算法推荐更多的极端内容。系统失控了。

这种现象并不局限于这种特定类型的内容。2019 年 6 月,《纽约时报》发表了一篇关于 YouTube 推荐系统的文章,标题为“在 YouTube 的数字游乐场,对恋童癖者敞开大门”。文章以这个令人不安的故事开头:

当 Christiane C.的 10 岁女儿和一个朋友上传了一个在后院游泳池玩耍的视频时,她并没有在意……几天后……视频的观看次数已经达到了数千次。不久之后,观看次数已经增加到 40 万……“我再次看到视频,看到观看次数,我感到害怕,”Christiane 说。她有理由感到害怕。研究人员发现,YouTube 的自动推荐系统……开始向观看其他预备期、部分穿着少儿视频的用户展示这个视频。

单独看,每个视频可能是完全无辜的,比如一个孩子制作的家庭影片。任何暴露的画面都是短暂的,看起来是偶然的。但是,当它们被组合在一起时,它们共享的特征变得明显。

YouTube 的推荐算法开始为恋童癖者策划播放列表,挑选出偶然包含预备期、部分穿着少儿的无辜家庭视频。

谷歌没有计划创建一个将家庭视频变成儿童色情片的系统。那么发生了什么?

这里的问题之一是指标在推动一个财政重要系统中的核心性。当一个算法有一个要优化的指标时,正如你所看到的,它会尽其所能来优化这个数字。这往往会导致各种边缘情况,与系统互动的人类会寻找、发现并利用这些边缘情况和反馈循环以谋取利益。

有迹象表明,这正是发生在 YouTube 的推荐系统中的情况。卫报发表了一篇题为“一位前 YouTube 内部人员是如何调查其秘密算法的”的文章,讲述了前 YouTube 工程师 Guillaume Chaslot 创建了一个网站来跟踪这些问题。Chaslot 在罗伯特·穆勒“关于 2016 年总统选举中俄罗斯干预调查”的发布后发布了图表,如图 3-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-5. 穆勒报告的报道

俄罗斯今日电视台对穆勒报告的报道在推荐频道中是一个极端的离群值。这表明俄罗斯今日电视台,一个俄罗斯国有媒体机构,成功地操纵了 YouTube 的推荐算法。不幸的是,这种系统缺乏透明度,使我们很难揭示我们正在讨论的问题。

本书的一位审阅者 Aurélien Géron,曾在 2013 年至 2016 年间领导 YouTube 的视频分类团队(远在这里讨论的事件之前)。他指出,涉及人类的反馈循环不仅是一个问题。也可能存在没有人类参与的反馈循环!他向我们讲述了 YouTube 的一个例子:

对视频的主题进行分类的一个重要信号是视频的来源频道。例如,上传到烹饪频道的视频很可能是烹饪视频。但我们如何知道一个频道的主题是什么?嗯…部分是通过查看它包含的视频的主题!你看到循环了吗?例如,许多视频有描述,指示拍摄视频所使用的相机。因此,一些视频可能被分类为“摄影”视频。如果一个频道有这样一个错误分类的视频,它可能被分类为“摄影”频道,使得未来在该频道上的视频更有可能被错误分类为“摄影”。这甚至可能导致失控的病毒般的分类!打破这种反馈循环的一种方法是对有和没有频道信号的视频进行分类。然后在对频道进行分类时,只能使用没有频道信号获得的类别。这样,反馈循环就被打破了。

有人和组织试图解决这些问题的积极例子。Meetup 的首席机器学习工程师 Evan Estola 讨论了男性对科技见面会表现出比女性更感兴趣的例子。因此,考虑性别可能会导致 Meetup 的算法向女性推荐更少的科技见面会,结果导致更少的女性了解并参加科技见面会,这可能导致算法向女性推荐更少的科技见面会,如此循环反馈。因此,Evan 和他的团队做出了道德决定,让他们的推荐算法不会创建这样的反馈循环,明确不在模型的那部分使用性别。看到一家公司不仅仅是盲目地优化指标,而是考虑其影响是令人鼓舞的。根据 Evan 的说法,“你需要决定在算法中不使用哪个特征…最优算法也许不是最适合投入生产的算法。”

尽管 Meetup 选择避免这种结果,但 Facebook 提供了一个允许失控的反馈循环肆虐的例子。与 YouTube 类似,它倾向于通过向用户介绍更多阴谋论来激化用户。正如虚构信息传播研究员 Renee DiResta 所写的那样:

一旦人们加入一个阴谋论倾向的[Facebook]群组,他们就会被算法路由到其他大量群组。加入反疫苗群组,你的建议将包括反转基因、化学尾迹观察、地平论者(是的,真的)和“自然治愈癌症”群组。推荐引擎不是将用户拉出兔子洞,而是将他们推得更深。

非常重要的是要记住这种行为可能会发生,并在看到自己项目中出现第一个迹象时,要么预见到一个反馈循环,要么采取积极行动来打破它。另一件要记住的事情是偏见,正如我们在上一章中简要讨论的那样,它可能与反馈循环以非常麻烦的方式相互作用。

偏见

在线讨论偏见往往会变得非常混乱。 “偏见”一词有很多不同的含义。统计学家经常认为,当数据伦理学家谈论偏见时,他们在谈论统计学术语“偏见”,但他们并没有。他们当然也没有在谈论出现在模型参数中的权重和偏见中的偏见!

他们所谈论的是社会科学概念中的偏见。在“理解机器学习意外后果的框架”中,麻省理工学院的 Harini Suresh 和 John Guttag 描述了机器学习中的六种偏见类型,总结在图 3-6 中。

显示机器学习中偏见可能出现的所有来源的图表

图 3-6。机器学习中的偏见可能来自多个来源(由 Harini Suresh 和 John V. Guttag 提供)

我们将讨论其中四种偏见类型,这些是我们在自己的工作中发现最有帮助的(有关其他类型的详细信息,请参阅论文)。

历史偏见

历史偏见源于人们的偏见,过程的偏见,以及社会的偏见。苏雷什和古塔格说:“历史偏见是数据生成过程的第一步存在的基本结构性问题,即使进行了完美的抽样和特征选择,它也可能存在。”

例如,以下是美国历史上种族偏见的几个例子,来自芝加哥大学 Sendhil Mullainathan 的《纽约时报》文章“种族偏见,即使我们有良好意图”

  • 当医生看到相同的档案时,他们更不可能向黑人患者推荐心脏导管化(一种有益的程序)。

  • 在讨价还价购买二手车时,黑人被要求支付的初始价格高出 700 美元,并获得了远低于预期的让步。

  • 在 Craigslist 上回应带有黑人姓名的公寓出租广告比带有白人姓名的回应要少。

  • 一个全白人陪审团比一个黑人被告有 16 个百分点更有可能定罪,但当陪审团有一个黑人成员时,他们以相同的比率定罪。

在美国用于判决和保释决定的 COMPAS 算法是一个重要算法的例子,当ProPublica进行测试时,实际上显示出明显的种族偏见(图 3-7)。

表格显示,即使重新犯罪,COMPAS 算法更有可能给白人保释

图 3-7。COMPAS 算法的结果

任何涉及人类的数据集都可能存在这种偏见:医疗数据、销售数据、住房数据、政治数据等等。由于潜在偏见是如此普遍,数据集中的偏见也非常普遍。甚至在计算机视觉中也会出现种族偏见,正如 Twitter 上一位 Google 照片用户分享的自动分类照片的例子所示,见图 3-8。

Google 照片使用黑人用户和她朋友的照片标记为大猩猩的屏幕截图

图 3-8。其中一个标签是非常错误的…

是的,这正是你认为的:Google 照片将一位黑人用户的照片与她的朋友一起分类为“大猩猩”!这种算法错误引起了媒体的广泛关注。一位公司女发言人表示:“我们对此感到震惊和真诚地抱歉。自动图像标记仍然存在许多问题,我们正在研究如何防止将来发生这类错误。”

不幸的是,当输入数据存在问题时,修复机器学习系统中的问题是困难的。谷歌的第一次尝试并没有激发信心,正如卫报的报道所建议的那样(图 3-9)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-9。谷歌对问题的第一次回应

这些问题当然不仅限于谷歌。麻省理工学院的研究人员研究了最受欢迎的在线计算机视觉 API,以了解它们的准确性。但他们并不只是计算一个准确性数字,而是查看了四个组的准确性,如图 3-10 所示。

表格显示各种面部识别系统在较深肤色和女性上表现更差

图 3-10。各种面部识别系统的性别和种族错误率

例如,IBM 的系统对较深肤色的女性有 34.7%的错误率,而对较浅肤色的男性只有 0.3%的错误率——错误率高出 100 多倍!一些人对这些实验的反应是错误的,他们声称差异仅仅是因为较深的皮肤更难被计算机识别。然而,事实是,由于这一结果带来的负面宣传,所有相关公司都大幅改进了他们对较深肤色的模型,以至于一年后,它们几乎和对较浅肤色的一样好。因此,这表明开发人员未能利用包含足够多较深肤色面孔的数据集,或者未能用较深肤色的面孔测试他们的产品。

麻省理工学院的一位研究人员 Joy Buolamwini 警告说:“我们已经进入了自信过度但准备不足的自动化时代。如果我们未能制定道德和包容性的人工智能,我们将冒着在机器中立的幌子下失去民权和性别平等所取得的成就的风险。”

问题的一部分似乎是流行数据集的构成存在系统性不平衡,用于训练模型。Shreya Shankar 等人的论文“没有代表性就没有分类:评估发展中国家开放数据集中的地理多样性问题”的摘要中指出,“我们分析了两个大型公开可用的图像数据集,以评估地理多样性,并发现这些数据集似乎存在明显的美洲中心和欧洲中心的代表性偏见。此外,我们分析了在这些数据集上训练的分类器,以评估这些训练分布的影响,并发现在不同地区的图像上表现出强烈的相对性能差异。”图 3-11 展示了论文中的一个图表,展示了当时(以及本书撰写时仍然如此)两个最重要的图像数据集的地理构成。

图表显示流行训练数据集中绝大多数图像来自美国或西欧

图 3-11。流行训练集中的图像来源

绝大多数图像来自美国和其他西方国家,导致在 ImageNet 上训练的模型在其他国家和文化的场景中表现更差。例如,研究发现这样的模型在识别低收入国家的家庭物品(如肥皂、香料、沙发或床)时表现更差。图 3-12 展示了 Facebook AI Research 的 Terrance DeVries 等人的论文“目标识别对每个人都有效吗?”中的一幅图像,说明了这一点。

图表显示一个目标检测算法在西方产品上表现更好

图 3-12。目标检测的实际应用

在这个例子中,我们可以看到低收入肥皂的例子离准确还有很长的路要走,每个商业图像识别服务都预测“食物”是最可能的答案!

接下来我们将讨论,绝大多数人工智能研究人员和开发人员都是年轻的白人男性。我们看到的大多数项目都是使用产品开发团队的朋友和家人进行用户测试。鉴于此,我们刚刚讨论的问题不应该令人惊讶。

类似的历史偏见也存在于用作自然语言处理模型数据的文本中。这会在许多下游机器学习任务中出现。例如,据广泛报道,直到去年,Google 翻译在将土耳其中性代词“o”翻译成英语时显示了系统性偏见:当应用于通常与男性相关联的工作时,它使用“he”,而当应用于通常与女性相关联的工作时,它使用“she”(图 3-13)。

显示语言模型训练中数据集中性别偏见在翻译中的体现的图表

图 3-13。文本数据集中的性别偏见

我们也在在线广告中看到这种偏见。例如,2019 年穆罕默德·阿里等人的一项研究发现,即使放置广告的人没有故意歧视,Facebook 也会根据种族和性别向非常不同的受众展示广告。展示了同样文本但图片分别是白人家庭或黑人家庭的房屋广告被展示给了种族不同的受众。

测量偏见

在《“机器学习是否自动化了道德风险和错误”》一文中,Sendhil Mullainathan 和 Ziad Obermeyer 研究了一个模型,试图回答这个问题:使用历史电子健康记录(EHR)数据,哪些因素最能预测中风?这是该模型的前几个预测因素:

  • 先前的中风

  • 心血管疾病

  • 意外伤害

  • 良性乳腺肿块

  • 结肠镜检查

  • 鼻窦炎

然而,只有前两个与中风有关!根据我们迄今所学,你可能已经猜到原因。我们实际上并没有测量中风,中风是由于脑部某个区域由于血液供应中断而被剥夺氧气而发生的。我们测量的是谁有症状,去看医生,接受了适当的检查,并且被诊断出中风。实际上患中风不仅与这个完整列表相关联,还与那些会去看医生的人相关联(这受到谁能获得医疗保健、能否负担得起自付款、是否经历种族或性别歧视等影响)!如果你在发生意外伤害时可能会去看医生,那么在中风时你也可能会去看医生。

这是测量偏见的一个例子。当我们的模型因为测量错误、以错误方式测量或不恰当地将该测量纳入模型时,就会发生这种偏见。

聚合偏见

聚合偏见发生在模型未以包含所有适当因素的方式聚合数据,或者模型未包含必要的交互项、非线性等情况下。这在医疗环境中尤其常见。例如,糖尿病的治疗通常基于简单的单变量统计和涉及小组异质人群的研究。结果分析通常未考虑不同种族或性别。然而,事实证明糖尿病患者在不同种族之间有不同的并发症,HbA1c 水平(用于诊断和监测糖尿病的广泛指标)在不同种族和性别之间以复杂方式不同。这可能导致人们被误诊或错误治疗,因为医疗决策基于不包含这些重要变量和交互作用的模型。

表征偏见

Maria De-Arteaga 等人的论文“Bias in Bios: A Case Study of Semantic Representation Bias in a High-Stakes Setting”的摘要指出,职业中存在性别不平衡(例如,女性更有可能成为护士,男性更有可能成为牧师),并表示“性别之间的真正阳性率差异与职业中现有的性别不平衡相关,这可能会加剧这些不平衡。”

换句话说,研究人员注意到,预测职业的模型不仅反映了潜在人口中的实际性别不平衡,而且放大了它!这种表征偏差是相当常见的,特别是对于简单模型。当存在明显、容易看到的基本关系时,简单模型通常会假定这种关系始终存在。正如论文中的图 3-14 所示,对于女性比例较高的职业,模型往往会高估该职业的普遍性。

显示模型预测如何过度放大现有偏见的图表

图 3-14。预测职业中的模型误差与该职业中女性比例的关系

例如,在训练数据集中,14.6%的外科医生是女性,然而在模型预测中,真正阳性中只有 11.6%是女性。因此,模型放大了训练集中存在的偏见。

既然我们已经看到这些偏见存在,我们可以采取什么措施来减轻它们呢?

解决不同类型的偏见

不同类型的偏见需要不同的缓解方法。虽然收集更多样化的数据集可以解决表征偏见,但这对历史偏见或测量偏见无济于事。所有数据集都包含偏见。没有完全无偏的数据集。该领域的许多研究人员一直在提出一系列建议,以便更好地记录决策、背景和有关特定数据集创建方式的细节,以及为什么在什么情况下使用它,以及其局限性。这样,使用特定数据集的人不会被其偏见和局限性所困扰。

我们经常听到这样的问题,“人类有偏见,那么算法偏见真的重要吗?”这个问题经常被提出,肯定有一些让提问者认为有道理的理由,但对我们来说似乎并不太合乎逻辑!独立于这是否合乎逻辑,重要的是要意识到算法(特别是机器学习算法!)和人类是不同的。考虑一下关于机器学习算法的这些观点:

机器学习可以创建反馈循环

少量偏见可能会因为反馈循环而迅速呈指数增长。

机器学习可能会放大偏见

人类偏见可能导致更多的机器学习偏见。

算法和人类的使用方式不同

在实践中,人类决策者和算法决策者并不是以插拔方式互换使用的。这些例子列在下一页的清单中。

技术就是力量

随之而来的是责任。

正如阿肯色州医疗保健的例子所示,机器学习通常在实践中实施并不是因为它能带来更好的结果,而是因为它更便宜和更高效。凯西·奥尼尔在她的书《数学毁灭的武器》(Crown)中描述了一个模式,即特权人士由人处理,而穷人由算法处理。这只是算法与人类决策者使用方式的许多方式之一。其他方式包括以下内容:

  • 人们更有可能认为算法是客观或无误差的(即使他们有人类覆盖的选项)。

  • 算法更有可能在没有上诉程序的情况下实施。

  • 算法通常以规模使用。

  • 算法系统成本低廉。

即使在没有偏见的情况下,算法(尤其是深度学习,因为它是一种如此有效和可扩展的算法)也可能导致负面社会问题,比如当用于虚假信息时。

虚假信息

虚假信息的历史可以追溯到数百甚至数千年前。它不一定是让某人相信错误的事情,而是经常用来播撒不和谐和不确定性,并让人们放弃寻求真相。收到矛盾的说法可能会导致人们认为他们永远无法知道该信任谁或什么。

有些人认为虚假信息主要是关于错误信息或假新闻,但实际上,虚假信息经常包含真相的种子,或者是脱离上下文的半真相。拉迪斯拉夫·比特曼是苏联的一名情报官员,后来叛逃到美国,并在 20 世纪 70 年代和 80 年代写了一些关于苏联宣传行动中虚假信息角色的书籍。在《克格勃和苏联虚假信息》(Pergamon)中,他写道“大多数活动都是精心设计的事实、半真相、夸大和故意谎言的混合物。”

在美国,近年来,FBI 详细描述了与 2016 年选举中的俄罗斯有关的大规模虚假信息活动。了解在这次活动中使用的虚假信息非常有教育意义。例如,FBI 发现俄罗斯的虚假信息活动经常组织两个独立的假“草根”抗议活动,一个支持某一方面,另一个支持另一方面,并让他们同时抗议!休斯顿纪事报报道了其中一个奇怪事件(图 3-15):

一个自称为“德克萨斯之心”的团体在社交媒体上组织了一场抗议活动,他们声称这是反对“德克萨斯伊斯兰化”的。在特拉维斯街的一边,我发现大约有 10 名抗议者。在另一边,我发现大约有 50 名反对抗议者。但我找不到集会的组织者。没有“德克萨斯之心”。我觉得这很奇怪,并在文章中提到:一个团体在自己的活动中缺席是什么样的团体?现在我知道为什么了。显然,集会的组织者当时在俄罗斯的圣彼得堡。“德克萨斯之心”是特别检察官罗伯特·穆勒最近指控试图干预美国总统选举的俄罗斯人中引用的一个互联网喷子团体。

德克萨斯之心组织的活动截图

图 3-15。由德克萨斯之心组织的活动

虚假信息通常涉及协调的不真实行为活动。例如,欺诈账户可能试图让人们认为许多人持有特定观点。虽然大多数人喜欢认为自己是独立思考的,但实际上我们进化为受到内部群体的影响,并与外部群体对立。在线讨论可能会影响我们的观点,或改变我们认为可接受观点的范围。人类是社会动物,作为社会动物,我们受周围人的影响极大。越来越多的极端化发生在在线环境中;因此影响来自虚拟空间中的在线论坛和社交网络中的人们。

通过自动生成的文本进行虚假信息传播是一个特别重要的问题,这是由于深度学习提供的大大增强的能力。当我们深入研究创建语言模型时,我们会深入讨论这个问题第十章。

一种提出的方法是开发某种形式的数字签名,以无缝方式实施它,并创建我们应该信任仅经过验证的内容的规范。艾伦人工智能研究所的负责人奥伦·艾齐奥尼在一篇题为“我们将如何防止基于人工智能的伪造?”的文章中写道:“人工智能正准备使高保真伪造变得廉价和自动化,可能会对民主、安全和社会造成灾难性后果。人工智能伪造的幽灵意味着我们需要采取行动,使数字签名成为验证数字内容的手段。”

虽然我们无法讨论深度学习和算法带来的所有伦理问题,但希望这个简短的介绍可以成为您的有用起点。现在我们将继续讨论如何识别伦理问题以及如何处理它们。

识别和解决伦理问题

错误是难免的。了解并处理错误需要成为包括机器学习在内的任何系统设计的一部分(还有许多其他系统)。数据伦理中提出的问题通常是复杂且跨学科的,但至关重要的是我们努力解决这些问题。

那么我们能做什么?这是一个重要的话题,但以下是一些解决伦理问题的步骤:

  • 分析你正在进行的项目。

  • 在您的公司实施流程以发现和解决伦理风险。

  • 支持良好的政策。

  • 增加多样性。

让我们逐步进行,从分析你正在进行的项目开始。

分析你正在进行的项目

在考虑工作的伦理影响时很容易忽略重要问题。一个极大的帮助是简单地提出正确的问题。Rachel Thomas 建议在数据项目的开发过程中考虑以下问题:

  • 我们甚至应该这样做吗?

  • 数据中存在什么偏见?

  • 代码和数据可以进行审计吗?

  • 不同子群体的错误率是多少?

  • 基于简单规则的替代方案的准确性如何?

  • 有哪些处理申诉或错误的流程?

  • 构建它的团队有多少多样性?

这些问题可能有助于您识别未解决的问题,以及更容易理解和控制的可能替代方案。除了提出正确的问题外,考虑实施的实践和流程也很重要。

在这个阶段需要考虑的一件事是你正在收集和存储的数据。数据往往最终被用于不同于最初意图的目的。例如,IBM 在大屠杀之前就开始向纳粹德国出售产品,包括帮助纳粹德国进行的 1933 年人口普查,这次普查有效地识别出了比之前在德国被认可的犹太人更多。同样,美国人口普查数据被用来拘留二战期间的日裔美国人(他们是美国公民)。重要的是要认识到收集的数据和图像如何在以后被武器化。哥伦比亚大学教授蒂姆·吴写道:“你必须假设 Facebook 或 Android 保存的任何个人数据都是世界各国政府将试图获取或盗贼将试图窃取的数据。”

实施流程

马库拉中心发布了工程/设计实践的伦理工具包,其中包括在您的公司实施的具体实践,包括定期安排的扫描,以主动搜索伦理风险(类似于网络安全渗透测试),扩大伦理圈,包括各种利益相关者的观点,并考虑可怕的人(坏人如何滥用、窃取、误解、黑客、破坏或武器化您正在构建的东西?)。

即使您没有多样化的团队,您仍然可以尝试主动包括更广泛群体的观点,考虑这些问题(由马库拉中心提供):

  • 我们是否只是假设了谁/哪些团体和个人的利益、愿望、技能、经验和价值观,而没有实际咨询?

  • 谁将直接受到我们产品影响的所有利益相关者?他们的利益是如何得到保护的?我们如何知道他们的真正利益是什么——我们有没有询问过?

  • 哪些团体和个人将受到重大影响而间接受到影响?

  • 谁可能会使用这个产品,而我们没有预料到会使用它,或者出于我们最初没有打算的目的?

伦理镜头

马库拉中心的另一个有用资源是其技术和工程实践中的概念框架。这考虑了不同基础伦理镜头如何帮助识别具体问题,并列出以下方法和关键问题:

权利的观点

哪个选项最尊重所有利益相关者的权利?

正义的观点

哪个选项平等或成比例地对待人们?

功利主义的观点

哪个选项将产生最多的好处并造成最少的伤害?

共同利益的观点

哪个选项最好地服务于整个社区,而不仅仅是一些成员?

美德的观点

哪个选项会让我表现得像我想成为的那种人?

马库拉的建议包括更深入地探讨这些观点,包括通过后果的视角来审视一个项目:

  • 谁将直接受到这个项目的影响?谁将间接受到影响?

  • 总体上,这些影响可能会产生更多的好处还是伤害,以及什么类型的好处和伤害?

  • 我们是否考虑了所有相关类型的伤害/好处(心理、政治、环境、道德、认知、情感、制度、文化)?

  • 未来的后代可能会受到这个项目的影响吗?

  • 这个项目可能会对社会中最弱势的人造成的伤害风险是否不成比例?好处是否会不成比例地给予富裕者?

  • 我们是否充分考虑了“双重使用”和意外的下游影响?

另一种视角是义务论的视角,它侧重于的基本概念:

  • 我们必须尊重他人的哪些权利和对他人的义务

  • 这个项目可能会如何影响每个利益相关者的尊严和自主权?

  • 信任和正义的考虑对这个设计/项目有何影响?

  • 这个项目是否涉及与他人的冲突道德责任,或者与利益相关者的冲突权利?我们如何能够优先考虑这些?

帮助提出完整和周到的答案的最佳方法之一是确保提出问题的人是多样化的。

多样性的力量

根据Element AI 的一项研究,目前不到 12%的人工智能研究人员是女性。在种族和年龄方面的统计数据同样令人堪忧。当团队中的每个人背景相似时,他们很可能在道德风险方面有相似的盲点。哈佛商业评论(HBR)发表了许多研究,显示了多样化团队的许多好处,包括以下内容:

多样性可以导致问题更早地被识别,并考虑更广泛的解决方案。例如,Tracy Chou 是 Quora 的一名早期工程师。她描述了自己的经历,描述了她在内部为添加一个功能而进行倡导,该功能可以允许封锁恶意用户和其他不良行为者。Chou 回忆道,“我渴望参与这个功能的开发,因为我在网站上感到被挑衅和虐待(性别可能是一个原因)…但如果我没有那种个人视角,Quora 团队可能不会那么早地将构建封锁按钮作为优先事项。”骚扰经常会导致边缘群体的人离开在线平台,因此这种功能对于维护 Quora 社区的健康至关重要。

一个关键的方面要理解的是,女性离开科技行业的速度是男性的两倍以上。根据哈佛商业评论的数据,41%的从事科技行业的女性离开,而男性只有 17%。对 200 多本书籍、白皮书和文章的分析发现,她们离开的原因是“她们受到不公平对待;薪酬较低,不如男同事那样容易获得快速晋升,无法晋升。”

研究已经证实了一些使女性在职场中更难晋升的因素。女性在绩效评估中收到更多模糊的反馈和个性批评,而男性收到与业务结果相关的可操作建议(更有用)。女性经常被排除在更具创造性和创新性的角色之外,并且没有获得有助于晋升的高能见度的“拓展”任务。一项研究发现,即使阅读相同的脚本,男性的声音被认为比女性的声音更具有说服力、基于事实和逻辑。

统计数据显示,接受指导有助于男性晋升,但对女性没有帮助。背后的原因是,当女性接受指导时,这是关于她们应该如何改变和获得更多自我认识的建议。当男性接受指导时,这是对他们权威的公开认可。猜猜哪个对于晋升更有用?

只要合格的女性继续退出科技行业,教更多女孩编程并不能解决困扰该领域的多样性问题。多样性倡议往往主要关注白人女性,尽管有色人种女性面临许多额外障碍。在对从事 STEM 研究的 60 名有色人种女性进行的采访中,100%的人表示曾经遭受过歧视。

技术领域的招聘过程特别混乱。一项表明这种功能障碍的研究来自 Triplebyte,这是一家帮助将软件工程师安置到公司的公司,作为这一过程的一部分进行了标准化的技术面试。该公司拥有一个引人入胜的数据集:300 多名工程师在考试中的表现结果,以及这些工程师在各种公司的面试过程中的表现结果。Triplebyte 的研究中的第一个发现是,“每家公司寻找的程序员类型往往与公司的需求或业务无关。相反,它们反映了公司文化和创始人的背景。”

这对于试图进入深度学习领域的人来说是一个挑战,因为大多数公司的深度学习团队今天都是由学者创立的。这些团队往往寻找“像他们一样”的人——也就是说,能够解决复杂数学问题并理解密集行话的人。他们并不总是知道如何发现那些真正擅长使用深度学习解决实际问题的人。

这为那些愿意超越地位和门第,专注于结果的公司提供了一个巨大的机会!

公平、问责和透明度

计算机科学家的专业协会 ACM 举办了一个名为“公平性、问责制和透明度会议”的数据伦理会议(ACM FAccT),以前使用的缩写是 FAT,现在使用不那么有争议的 FAccT。微软也有一个专注于 AI 中的公平性、问责制、透明度和伦理的团队(FATE)。在本节中,我们将使用缩写 FAccT 来指代公平性、问责制和透明度的概念。

FAccT 是一些人用来考虑伦理问题的一种视角。一个有用的资源是 Solon Barocas 等人的免费在线书籍《公平性与机器学习:限制与机会》,该书“提供了一个将公平性视为中心问题而不是事后想法的机器学习视角”。然而,它也警告说,“它故意范围狭窄……机器学习伦理的狭窄框架可能会诱使技术人员和企业专注于技术干预,而回避有关权力和问责制的更深层次问题。我们警告不要陷入这种诱惑。”与提供 FAccT 伦理方法概述的重点不同(最好在像那样的书籍中完成),我们的重点将放在这种狭窄框架的局限性上。

考虑伦理视角是否完整的一个好方法是尝试提出一个例子,其中视角和我们自己的伦理直觉给出不同的结果。Os Keyes 等人在他们的论文中以图形方式探讨了这一点。该论文的摘要如下:

算法系统的伦理含义在人机交互和对技术设计、开发和政策感兴趣的更广泛社区中已经被广泛讨论。在本文中,我们探讨了一个著名的伦理框架——公平性、问责制和透明度——在一个旨在解决食品安全和人口老龄化等各种社会问题的算法中的应用。通过使用各种标准化的算法审计和评估形式,我们大大增加了算法对 FAT 框架的遵从,从而实现了更具伦理和善意的系统。我们讨论了这如何可以作为其他研究人员或从业者的指南,帮助他们确保在工作中的算法系统产生更好的伦理结果。

在本文中,相当有争议的提议(“将老年人变成高营养浆料”)和结果(“大大增加算法对 FAT 框架的遵从,从而实现更具伦理和善意的系统”)是相互矛盾的……至少可以这么说!

在哲学中,尤其是伦理哲学中,这是最有效的工具之一:首先,提出一个过程、定义、一组问题等,旨在解决问题。然后尝试提出一个例子,其中明显的解决方案导致一个没有人会认为可接受的提议。这可以进一步完善解决方案。

到目前为止,我们关注的是您和您的组织可以做的事情。但有时个人或组织的行动是不够的。有时政府也需要考虑政策影响。

政策的作用

我们经常与那些渴望技术或设计修复成为解决我们所讨论问题的全部解决方案的人交谈;例如,对数据进行去偏见的技术方法,或者制定技术不那么容易上瘾的设计指南。虽然这些措施可能有用,但它们不足以解决导致我们目前状态的根本问题。例如,只要创造上瘾的技术是有利可图的,公司将继续这样做,无论这是否会导致推广阴谋论并污染我们的信息生态系统。虽然个别设计师可能会尝试调整产品设计,但在基础利润激励措施改变之前,我们不会看到实质性的变化。

监管的有效性

要看看是什么导致公司采取具体行动,考虑 Facebook 的以下两个行为示例。2018 年,联合国调查发现 Facebook 在缅甸罗兴亚人持续种族灭绝中发挥了“决定性作用”,联合国秘书长安东尼奥·古特雷斯将罗兴亚人描述为“世界上最受歧视的人之一,如果不是最受歧视的人”。自 2013 年以来,当地活动人士一直在警告 Facebook 高管,称他们的平台被用来传播仇恨言论和煽动暴力。2015 年,他们被警告说,Facebook 可能在缅甸扮演与卢旺达种族灭绝期间广播电台扮演的相同角色(那里有一百万人被杀)。然而,到 2015 年底,Facebook 只雇用了四名会说缅甸语的承包商。正如一位知情人士所说,“这不是事后诸葛亮。这个问题的规模很大,而且已经显而易见。”扎克伯格在国会听证会上承诺雇佣“几十人”来解决缅甸的种族灭绝问题(2018 年,数年后种族灭绝已经开始,包括 2017 年 8 月之后至少摧毁了北拉钦邦至少 288 个村庄)。

这与 Facebook 迅速在德国雇佣了 1,200 人以避免根据德国新法律反对仇恨言论面临高达 5000 万欧元的昂贵罚款形成鲜明对比。显然,在这种情况下,Facebook 更多地是对财务处罚的威胁做出反应,而不是对一个种族少数群体的系统性破坏。

一篇关于隐私问题的文章中,马切伊·塞格洛夫斯基与环境运动进行了类比:

这一监管项目在第一世界取得了如此成功,以至于我们可能忘记了之前的生活是什么样子。今天在雅加达和德里杀死成千上万人的浓烟曾经是伦敦的象征。俄亥俄州的奎哈霍加河曾经经常起火。在一个特别可怕的意外后果的例子中,添加到汽油中的四乙基铅导致全球暴力犯罪率上升了五十年。这些伤害都不能通过告诉人们用钱包投票,或者仔细审查他们给予业务的每家公司的环境政策,或者停止使用相关技术来解决。这需要跨越司法辖区的协调和有时高度技术化的监管来解决。在一些情况下,比如禁止商用制冷剂导致臭氧层消耗,这种监管需要全球共识。我们已经到了需要在隐私法中进行类似转变的时候。

权利和政策

清洁空气和清洁饮用水是几乎不可能通过个人市场决策来保护的公共物品,而是需要协调的监管行动。同样,许多技术误用的意外后果造成的伤害涉及公共物品,比如污染的信息环境或恶化的环境隐私。隐私往往被框定为个人权利,然而广泛监视会产生社会影响(即使有一些个人可以选择退出也是如此)。

我们在科技领域看到的许多问题都是人权问题,比如一个带有偏见的算法建议黑人被告应该获得更长的监禁,特定的工作广告只显示给年轻人,或者警察使用面部识别来识别抗议者。解决人权问题的适当场所通常是法律。

我们需要监管和法律变革,以及个人的道德行为。个人行为的改变无法解决不一致的利润激励、外部性(即企业在向更广泛社会转嫁成本和危害的同时获得巨额利润)或系统性失败。然而,法律永远不可能涵盖所有边缘案例,重要的是个人软件开发人员和数据科学家能够在实践中做出道德决策。

汽车:历史先例

我们面临的问题是复杂的,没有简单的解决方案。这可能令人沮丧,但我们在考虑历史上人们已经解决的其他重大挑战时找到了希望。一个例子是增加汽车安全的运动,被提及为“数据集数据表”一书中的案例研究,作者是 Timnit Gebru 等人,以及设计播客99% Invisible。早期汽车没有安全带,仪表盘上有金属旋钮,在事故中可能刺入人们的头颅,常规平板玻璃窗以危险的方式破碎,非可折叠转向柱刺穿驾驶员。然而,汽车公司甚至不愿讨论安全作为他们可以帮助解决的问题,普遍的看法是汽车就是它们的样子,是使用它们的人造成了问题。

消费者安全活动家和倡导者经过几十年的努力,改变了国家对汽车公司可能需要通过监管来解决一些责任的讨论。可折叠转向柱发明后,由于没有财务激励,几年内并未实施。主要汽车公司通用汽车公司雇佣了私家侦探,试图挖掘消费者安全倡导者拉尔夫·纳德的黑材料。安全带、碰撞测试假人和可折叠转向柱的要求是重大胜利。直到 2011 年,汽车公司才被要求开始使用代表普通女性的碰撞测试假人,而不仅仅是代表普通男性的身体;在此之前,女性在相同冲击下的车祸中受伤的可能性比男性高 40%。这是偏见、政策和技术产生重要后果的生动例证。

结论

从二进制逻辑的背景出发,伦理学中缺乏明确答案可能一开始会令人沮丧。然而,我们的工作如何影响世界,包括意外后果和工作被不良行为者武器化的影响,是我们可以(也应该!)考虑的最重要问题之一。尽管没有简单的答案,但有明确的陷阱要避免和实践要遵循,以朝着更具道德行为迈进。

许多人(包括我们!)正在寻找更令人满意、扎实的答案,以解决技术带来的有害影响。然而,考虑到我们面临的问题的复杂性、广泛性和跨学科性质,没有简单的解决方案。Julia Angwin,ProPublica 前资深记者,专注于算法偏见和监视问题(也是 2016 年调查 COMPAS 累犯算法的调查人员之一,该算法帮助引发了 FAccT 领域),在 2019 年的一次采访中表示:

我坚信,要解决问题,必须先诊断问题,而我们仍处于诊断阶段。如果您考虑到世纪之交和工业化,我们经历了,我不知道,30 年的童工、无限工作时间、糟糕的工作条件,需要大量记者揭发和倡导来诊断问题并对其有所了解,然后通过积极行动来改变法律。我觉得我们正处于数据信息的第二次工业化…我认为我的角色是尽可能清楚地表明问题的不利方面,并准确诊断问题,以便能够解决。这是艰苦的工作,需要更多的人来做。

令人欣慰的是,Angwin 认为我们在很大程度上仍处于诊断阶段:如果您对这些问题的理解感到不完整,那是正常和自然的。目前还没有“治疗”方法,但我们继续努力更好地理解和解决我们面临的问题是至关重要的。

我们这本书的一位审阅者 Fred Monroe 曾在对冲基金交易领域工作。他在阅读本章后告诉我们,这里讨论的许多问题(数据分布与模型训练不同、部署和扩展后反馈循环对模型的影响等)也是构建盈利交易模型的关键问题。考虑到社会后果所需做的事情将与考虑组织、市场和客户后果所需做的事情有很多重叠,因此认真思考伦理问题也可以帮助您认真思考如何使您的数据产品更普遍地成功!

问卷

  1. 伦理是否提供了“正确答案”清单?

  2. 在考虑伦理问题时,与不同背景的人合作如何有助于解决问题?

  3. IBM 在纳粹德国的角色是什么?为什么公司会参与其中?为什么工人会参与其中?

  4. 第一个在大众柴油丑闻中被监禁的人的角色是什么?

  5. 加利福尼亚执法官员维护的涉嫌黑帮成员数据库存在什么问题?

  6. 为什么 YouTube 的推荐算法会向恋童癖者推荐部分裸露儿童的视频,尽管谷歌的员工没有编程这个功能?

  7. 指标的中心性存在哪些问题?

  8. 为什么 Meetup.com 在其技术见面会的推荐系统中没有包括性别?

  9. 根据 Suresh 和 Guttag,机器学习中有哪六种偏见类型?

  10. 在美国历史上,有哪两个种族偏见的例子?

  11. ImageNet 中的大多数图像来自哪里?

  12. 在论文“机器学习是否自动化道德风险和错误?”中,为什么鼻窦炎被发现与中风有关?

  13. 代表性偏见是什么?

  14. 在决策方面,机器和人有何不同?

  15. 虚假信息和“假新闻”是一回事吗?

  16. 通过自动生成的文本传播虚假信息为什么是一个特别重要的问题?

  17. 马库拉中心描述的五种伦理视角是什么?

  18. 政策在解决数据伦理问题方面是否是一个合适的工具?

进一步研究

  1. 阅读文章“当算法削减您的医疗保健”(链接)。未来如何避免类似问题?

  2. 研究更多关于 YouTube 推荐系统及其社会影响的信息。你认为推荐系统是否必须始终具有带有负面结果的反馈循环?谷歌可以采取什么方法来避免这种情况?政府呢?

  3. 阅读论文“在线广告投放中的歧视”。你认为谷歌应该对 Sweeney 博士发生的事情负责吗?什么是一个合适的回应?

  4. 跨学科团队如何帮助避免负面后果?

  5. 阅读论文“机器学习是否自动化了道德风险和错误?” 你认为应该采取什么行动来处理这篇论文中指出的问题?

  6. 阅读文章“我们将如何防止基于 AI 的伪造?” 你认为 Etzioni 提出的方法能行得通吗?为什么?

  7. 完成部分“分析你正在进行的项目”。

  8. 考虑一下你的团队是否可以更多元化。如果可以,有哪些方法可能会有所帮助?

实践中的深度学习:总结!

恭喜!你已经完成了书的第一部分。在这一部分中,我们试图向你展示深度学习可以做什么,以及你如何使用它来创建真实的应用和产品。在这一点上,如果你花一些时间尝试你所学到的东西,你将从这本书中获得更多。也许你一直在学习的过程中已经在做这些事情了,如果是这样,太棒了!如果没有,也没关系——现在是开始自己尝试实验的好时机。

如果你还没有去过书的网站,现在就去吧。非常重要的是你要设置好自己来运行这些笔记本。成为一个有效的深度学习从业者就是要不断练习,所以你需要训练模型。所以,如果你还没有开始运行这些笔记本,请现在就去运行!并查看网站上的任何重要更新或通知;深度学习变化迅速,我们无法改变这本书中印刷的文字,所以你需要查看网站以确保你拥有最新的信息。

确保你已经完成了以下步骤:

  1. 连接到书网站上推荐的 GPU Jupyter 服务器之一。

  2. 自己运行第一个笔记本。

  3. 上传你在第一个笔记本中找到的图像;然后尝试一些不同类型的图像,看看会发生什么。

  4. 运行第二个笔记本,根据你提出的图像搜索查询收集你自己的数据集。

  5. 思考一下如何利用深度学习来帮助你自己的项目,包括你可以使用什么类型的数据,可能会遇到什么问题,以及你如何在实践中可能会减轻这些问题。

在书的下一部分,你将了解深度学习是如何以及为什么起作用的,而不仅仅是看到你如何在实践中使用它。了解如何以及为什么对从业者和研究人员都很重要,因为在这个相当新的领域中,几乎每个项目都需要一定程度的定制和调试。你对深度学习的基础理解越深入,你的模型就会越好。这些基础对于高管、产品经理等人来说不那么重要(尽管仍然有用,所以请继续阅读!),但对于任何正在训练和部署模型的人来说都是至关重要的。

第二部分:理解 fastai 的应用

第四章:底层:训练数字分类器

原文:www.bookstack.cn/read/th-fastai-book/026b6e039c998ba1.md

译者:飞龙

协议:CC BY-NC-SA 4.0

在第二章中看到训练各种模型的样子后,现在让我们深入了解并看看究竟发生了什么。我们将使用计算机视觉来介绍深度学习的基本工具和概念。

确切地说,我们将讨论数组和张量的作用以及广播的作用,这是一种使用它们表达性地的强大技术。我们将解释随机梯度下降(SGD),这是通过自动更新权重学习的机制。我们将讨论基本分类任务的损失函数的选择,以及小批量的作用。我们还将描述基本神经网络正在执行的数学。最后,我们将把所有这些部分组合起来。

在未来的章节中,我们还将深入研究其他应用,并看看这些概念和工具如何泛化。但本章是关于奠定基础的。坦率地说,这也使得这是最困难的章节之一,因为这些概念彼此相互依赖。就像一个拱门,所有的石头都需要放在正确的位置才能支撑结构。也像一个拱门,一旦发生这种情况,它就是一个强大的结构,可以支撑其他事物。但是需要一些耐心来组装。

让我们开始吧。第一步是考虑图像在计算机中是如何表示的。

像素:计算机视觉的基础

要理解计算机视觉模型中发生的事情,我们首先必须了解计算机如何处理图像。我们将使用计算机视觉中最著名的数据集之一 MNIST 进行实验。MNIST 包含由国家标准与技术研究所收集的手写数字图像,并由 Yann Lecun 及其同事整理成一个机器学习数据集。Lecun 在 1998 年使用 MNIST 在 LeNet-5 中,这是第一个演示实用手写数字序列识别的计算机系统。这是人工智能历史上最重要的突破之一。

对于这个初始教程,我们只是尝试创建一个模型,可以将任何图像分类为 3 或 7。所以让我们下载一个包含这些数字图像的 MNIST 样本:

path = untar_data(URLs.MNIST_SAMPLE)
  • 1

我们可以使用ls来查看此目录中的内容,这是 fastai 添加的一个方法。这个方法返回一个特殊的 fastai 类L的对象,它具有 Python 内置list的所有功能,还有更多功能。其中一个方便的功能是,在打印时,它会显示项目的计数,然后列出项目本身(如果项目超过 10 个,它只显示前几个):

path.ls()
  • 1
(#9) [Path('cleaned.csv'),Path('item_list.txt'),Path('trained_model.pkl'),Path('
 > models'),Path('valid'),Path('labels.csv'),Path('export.pkl'),Path('history.cs
 > v'),Path('train')]
  • 1
  • 2
  • 3

MNIST 数据集遵循机器学习数据集的常见布局:训练集和验证(和/或测试)集分开存放。让我们看看训练集中的内容:

(path/'train').ls()
  • 1
(#2) [Path('train/7'),Path('train/3')]
  • 1

有一个包含 3 的文件夹,和一个包含 7 的文件夹。在机器学习术语中,我们说“3”和“7”是这个数据集中的标签(或目标)。让我们看看其中一个文件夹中的内容(使用sorted确保我们都得到相同的文件顺序):

threes = (path/'train'/'3').ls().sorted()
sevens = (path/'train'/'7').ls().sorted()
threes
  • 1
  • 2
  • 3
(#6131) [Path('train/3/10.png'),Path('train/3/10000.png'),Path('train/3/10011.pn
 > g'),Path('train/3/10031.png'),Path('train/3/10034.png'),Path('train/3/10042.p
 > ng'),Path('train/3/10052.png'),Path('train/3/1007.png'),Path('train/3/10074.p
 > ng'),Path('train/3/10091.png')...]
  • 1
  • 2
  • 3
  • 4

正如我们所预期的那样,它充满了图像文件。让我们现在看一个。这是一个手写数字 3 的图像,来自著名的手写数字 MNIST 数据集:

im3_path = threes[1]
im3 = Image.open(im3_path)
im3
  • 1
  • 2
  • 3

在这里,我们使用Python Imaging Library(PIL)中的Image类,这是最广泛使用的 Python 包,用于打开、操作和查看图像。Jupyter 知道 PIL 图像,所以它会自动为我们显示图像。

在计算机中,一切都以数字表示。要查看构成这幅图像的数字,我们必须将其转换为NumPy 数组PyTorch 张量。例如,这是转换为 NumPy 数组后图像的一部分的样子:

array(im3)[4:10,4:10]
  • 1
array([[  0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,  29],
       [  0,   0,   0,  48, 166, 224],
       [  0,  93, 244, 249, 253, 187],
       [  0, 107, 253, 253, 230,  48],
       [  0,   3,  20,  20,  15,   0]], dtype=uint8)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4:10表示我们请求从索引 4(包括)到 10(不包括)的行,列也是一样。NumPy 从上到下,从左到右索引,因此此部分位于图像的左上角附近。这里是一个 PyTorch 张量:

tensor(im3)[4:10,4:10]
  • 1
tensor([[  0,   0,   0,   0,   0,   0],
        [  0,   0,   0,   0,   0,  29],
        [  0,   0,   0,  48, 166, 224],
        [  0,  93, 244, 249, 253, 187],
        [  0, 107, 253, 253, 230,  48],
        [  0,   3,  20,  20,  15,   0]], dtype=torch.uint8)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们可以切片数组,只选择包含数字顶部部分的部分,然后使用 Pandas DataFrame 使用渐变对值进行着色,这清楚地显示了图像是如何由像素值创建的:

im3_t = tensor(im3)
df = pd.DataFrame(im3_t[4:15,4:22])
df.style.set_properties(**{'font-size':'6pt'}).background_gradient('Greys')
  • 1
  • 2
  • 3

你可以看到,背景白色像素存储为数字 0,黑色为数字 255,灰色在两者之间。整个图像横向包含 28 个像素,纵向包含 28 个像素,总共 768 个像素。(这比你从手机相机得到的图像要小得多,手机相机有数百万像素,但对于我们的初始学习和实验来说,这是一个方便的大小。我们将很快构建更大的全彩图像。)

所以,现在你已经看到了计算机对图像的看法,让我们回顾一下我们的目标:创建一个能够识别 3 和 7 的模型。你会如何让计算机做到这一点呢?

停下来思考!

在继续阅读之前,花点时间考虑一下计算机可能如何识别这两个数字。它可能能够看到什么样的特征?它可能如何识别这些特征?它如何将它们结合起来?学习最好的方式是尝试自己解决问题,而不仅仅是阅读别人的答案;所以离开这本书几分钟,拿一张纸和笔,写下一些想法。

第一次尝试:像素相似度

所以,这是一个第一个想法:我们可以找到每个 3 的像素的平均值,然后对 7 做同样的操作。这将给我们两组平均值,定义了我们可能称之为“理想”3 和 7。然后,为了将图像分类为一个数字或另一个数字,我们看看这两个理想数字中图像与哪个更相似。这肯定似乎比没有好,所以这将成为一个很好的基线。

术语:基线

一个简单的模型,你有信心应该表现得相当不错。它应该简单实现和易于测试,这样你就可以测试每个改进的想法,并确保它们始终优于基线。如果没有以合理的基线开始,很难知道你的超级花哨的模型是否好用。创建基线的一个好方法是做我们在这里做的事情:考虑一个简单、易于实现的模型。另一个好方法是四处寻找解决类似问题的其他人,并在你的数据集上下载并运行他们的代码。最好两者都尝试一下!

我们简单模型的第一步是获取我们两组像素值的平均值。在这个过程中,我们将学习很多有趣的 Python 数值编程技巧!

让我们创建一个包含所有 3 的张量堆叠在一起。我们已经知道如何创建包含单个图像的张量。要创建一个包含目录中所有图像的张量,我们将首先使用 Python 列表推导来创建一个单个图像张量的普通列表。

我们将使用 Jupyter 在途中做一些小的检查——在这种情况下,确保返回的项目数量看起来合理:

seven_tensors = [tensor(Image.open(o)) for o in sevens]
three_tensors = [tensor(Image.open(o)) for o in threes]
len(three_tensors),len(seven_tensors)
  • 1
  • 2
  • 3
(6131, 6265)
  • 1

列表推导

列表和字典推导是 Python 的一个很棒的特性。许多 Python 程序员每天都在使用它们,包括本书的作者们——它们是“Python 的成语”。但是来自其他语言的程序员可能以前从未见过它们。许多很棒的教程只需一次网络搜索,所以我们现在不会花很长时间讨论它们。这里有一个快速的解释和示例,让您开始。列表推导看起来像这样:new_list = [f(o) for o in a_list if o>0]。这将返回a_list中大于 0 的每个元素,在将其传递给函数f之后。这里有三个部分:您正在迭代的集合(a_list),一个可选的过滤器(if o>0),以及对每个元素执行的操作(f(o))。不仅写起来更短,而且比用循环创建相同列表的替代方法更快。

我们还将检查其中一张图像是否正常。由于我们现在有张量(Jupyter 默认会将其打印为值),而不是 PIL 图像(Jupyter 默认会显示图像),我们需要使用 fastai 的show_image函数来显示它:

show_image(three_tensors[1]);
  • 1

对于每个像素位置,我们想要计算该像素的强度在所有图像上的平均值。为了做到这一点,我们首先将此列表中的所有图像组合成一个三维张量。描述这样的张量最常见的方式是称之为rank-3 张量。我们经常需要将集合中的单个张量堆叠成一个张量。不出所料,PyTorch 带有一个名为stack的函数,我们可以用它来实现这个目的。

PyTorch 中的一些操作,如取平均值,需要我们将整数类型转换为浮点类型。由于我们稍后会需要这个,我们现在也将我们的堆叠张量转换为float。在 PyTorch 中进行转换就像写下您希望转换为的类型名称,并将其视为方法一样简单。

通常,当图像是浮点数时,像素值应该在 0 到 1 之间,所以我们也会在这里除以 255:

stacked_sevens = torch.stack(seven_tensors).float()/255
stacked_threes = torch.stack(three_tensors).float()/255
stacked_threes.shape
  • 1
  • 2
  • 3
torch.Size([6131, 28, 28])
  • 1

张量最重要的属性也许是其形状。这告诉您每个轴的长度。在这种情况下,我们可以看到我们有 6,131 张图像,每张图像大小为 28×28 像素。关于这个张量没有特别的地方表明第一个轴是图像的数量,第二个是高度,第三个是宽度——张量的语义完全取决于我们以及我们如何构建它。就 PyTorch 而言,它只是内存中的一堆数字。

张量形状的长度是其秩:

len(stacked_threes.shape)
  • 1
3
  • 1

对于您来说,将张量术语的这些部分记忆并加以实践非常重要:是张量中轴或维度的数量;形状是张量每个轴的大小。

Alexis 说

要小心,因为术语“维度”有时以两种方式使用。考虑我们生活在“三维空间”中,其中物理位置可以用长度为 3 的向量v描述。但根据 PyTorch,属性v.ndim(看起来确实像v的“维度数量”)等于一,而不是三!为什么?因为v是一个向量,它是一个秩为一的张量,这意味着它只有一个(即使该轴的长度为三)。换句话说,有时维度用于描述轴的大小(“空间是三维的”),而其他时候用于描述秩或轴的数量(“矩阵有两个维度”)。当感到困惑时,我发现将所有陈述转换为秩、轴和长度这些明确的术语是有帮助的。

我们也可以直接使用ndim来获取张量的秩:

stacked_threes.ndim
  • 1
3
  • 1

最后,我们可以计算理想的 3 是什么样子的。我们通过沿着我们堆叠的 rank-3 张量的维度 0 取平均值来计算所有图像张量的平均值。这是索引所有图像的维度。

换句话说,对于每个像素位置,这将计算所有图像中该像素的平均值。结果将是每个像素位置的一个值,或者一个单独的图像。这就是它:

mean3 = stacked_threes.mean(0)
show_image(mean3);
  • 1
  • 2

根据这个数据集,这是理想的数字 3!(您可能不喜欢,但这就是顶级数字 3 表现的样子。)您可以看到在所有图像都认为应该是暗的地方非常暗,但在图像不一致的地方变得模糊。

让我们对 7 做同样的事情,但一次将所有步骤放在一起以节省时间:

mean7 = stacked_sevens.mean(0)
show_image(mean7);
  • 1
  • 2

现在让我们选择一个任意的 3,并测量它与我们的“理想数字”的距离

停下来思考一下!

您如何计算特定图像与我们的每个理想数字之间的相似程度?在继续前进之前,请记得远离这本书,记录一些想法!研究表明,通过解决问题、实验和尝试新想法,您参与学习过程时,召回和理解会显著提高。

这是一个示例 3:

a_3 = stacked_threes[1]
show_image(a_3);
  • 1
  • 2

我们如何确定它与我们理想的 3 之间的距离?我们不能简单地将此图像的像素之间的差异相加,并与理想数字进行比较。一些差异将是正的,而另一些将是负的,这些差异将相互抵消,导致一种情况,即在某些地方太暗而在其他地方太亮的图像可能被显示为与理想的总差异为零。那将是误导性的!

为了避免这种情况,数据科学家在这种情况下使用两种主要方法来测量距离:

  • 取差值的绝对值的平均值(绝对值是将负值替换为正值的函数)。这被称为平均绝对差L1 范数

  • 取差值的平方的平均值(使所有值变为正数),然后取平方根(撤销平方)。这被称为均方根误差(RMSE)或L2 范数

忘记数学是可以的

在这本书中,我们通常假设您已经完成了高中数学,并且至少记得一些内容 - 但每个人都会忘记一些东西!这完全取决于您在此期间有理由练习的内容。也许您已经忘记了平方根是什么,或者它们究竟是如何工作的。没问题!每当您遇到本书中没有完全解释的数学概念时,不要只是继续前进;相反,停下来查一下。确保您理解基本概念,它是如何工作的,以及为什么我们可能会使用它。刷新您理解的最佳地方之一是 Khan Academy。例如,Khan Academy 有一个很棒的平方根介绍

现在让我们尝试这两种方法:

dist_3_abs = (a_3 - mean3).abs().mean()
dist_3_sqr = ((a_3 - mean3)**2).mean().sqrt()
dist_3_abs,dist_3_sqr
  • 1
  • 2
  • 3
(tensor(0.1114), tensor(0.2021))
  • 1
dist_7_abs = (a_3 - mean7).abs().mean()
dist_7_sqr = ((a_3 - mean7)**2).mean().sqrt()
dist_7_abs,dist_7_sqr
  • 1
  • 2
  • 3
(tensor(0.1586), tensor(0.3021))
  • 1

在这两种情况下,我们的 3 与“理想”的 3 之间的距离小于与理想的 7 之间的距离,因此在这种情况下,我们简单的模型将给出正确的预测。

PyTorch 已经提供了这两种作为损失函数。您会在torch.nn.functional中找到这些,PyTorch 团队建议将其导入为F(并且默认情况下以这个名称在 fastai 中可用):

F.l1_loss(a_3.float(),mean7), F.mse_loss(a_3,mean7).sqrt()
  • 1
(tensor(0.1586), tensor(0.3021))
  • 1

在这里,MSE代表均方误差l1是标准数学术语平均绝对值的缩写(在数学中称为L1 范数)。

Sylvain 说

直观地,L1 范数和均方误差(MSE)之间的区别在于,后者会比前者更严厉地惩罚更大的错误(并对小错误更宽容)。

杰里米说

当我第一次遇到这个 L1 的东西时,我查了一下看它到底是什么意思。我在谷歌上发现它是使用“绝对值”作为“向量范数”,所以我查了“向量范数”并开始阅读:“给定一个实数或复数域 F 上的向量空间 V,V 上的范数是一个非负值的任意函数 p: V → [0,+∞),具有以下属性:对于所有的 a ∈ F 和所有的 u, v ∈ V,p(u + v) ≤ p(u) + p(v)…”然后我停止阅读。“唉,我永远也理解不了数学!”我想,这已经是第一千次了。从那时起,我学到了每当实践中出现这些复杂的数学术语时,我可以用一点点代码来替换它们!比如,L1 损失 只等于 (a-b).abs().mean(),其中 ab 是张量。我猜数学家们只是和我想法不同…我会确保在本书中,每当出现一些数学术语时,我会给你相应的代码片段,并用通俗的语言解释发生了什么。

我们刚刚在 PyTorch 张量上完成了各种数学运算。如果你之前在 PyTorch 中进行过数值编程,你可能会发现这些与 NumPy 数组相似。让我们来看看这两个重要的数据结构。

NumPy 数组和 PyTorch 张量

NumPy 是 Python 中用于科学和数值编程最广泛使用的库。它提供了类似的功能和类似的 API,与 PyTorch 提供的功能相似;然而,它不支持使用 GPU 或计算梯度,这两者对于深度学习都是至关重要的。因此,在本书中,我们通常会在可能的情况下使用 PyTorch 张量而不是 NumPy 数组。

(请注意,fastai 在 NumPy 和 PyTorch 中添加了一些功能,使它们更加相似。如果本书中的任何代码在您的计算机上无法运行,可能是因为您忘记在笔记本的开头包含类似这样的一行代码:from fastai.vision.all import *。)

但是数组和张量是什么,为什么你应该关心呢?

Python 相对于许多语言来说速度较慢。在 Python、NumPy 或 PyTorch 中快速的任何东西,很可能是另一种语言(特别是 C)编写(并优化)的编译对象的包装器。事实上,NumPy 数组和 PyTorch 张量可以比纯 Python 快几千倍完成计算

NumPy 数组是一个多维数据表,所有项都是相同类型的。由于可以是任何类型,它们甚至可以是数组的数组,内部数组可能是不同大小的 - 这被称为 不规则数组。通过“多维数据表”,我们指的是,例如,一个列表(一维)、一个表或矩阵(二维)、一个表的表或立方体(三维),等等。如果所有项都是简单类型,如整数或浮点数,NumPy 将它们存储为紧凑的 C 数据结构在内存中。这就是 NumPy 的优势所在。NumPy 有各种运算符和方法,可以在这些紧凑结构上以优化的 C 速度运行计算,因为它们是用优化的 C 编写的。

PyTorch 张量几乎与 NumPy 数组相同,但有一个额外的限制,可以解锁额外的功能。它与 NumPy 数组相同,也是一个多维数据表,所有项都是相同类型的。然而,限制是张量不能使用任何旧类型 - 它必须对所有组件使用单一基本数值类型。因此,张量不像真正的数组数组那样灵活。例如,PyTorch 张量不能是不规则的。它始终是一个形状规则的多维矩形结构。

NumPy 在这些结构上支持的绝大多数方法和运算符在 PyTorch 上也支持,但 PyTorch 张量具有额外的功能。一个主要功能是这些结构可以存在于 GPU 上,这样它们的计算将被优化为 GPU,并且可以运行得更快(给定大量值进行处理)。此外,PyTorch 可以自动计算这些操作的导数,包括操作的组合。正如你将看到的,没有这种能力,实际上是不可能进行深度学习的。

Sylvain 说

如果你不知道 C 是什么,不用担心:你根本不需要它。简而言之,它是一种低级语言(低级意味着更类似于计算机内部使用的语言),与 Python 相比非常快。为了在 Python 中利用其速度,尽量避免编写循环,用直接作用于数组或张量的命令替换它们。

也许对于 Python 程序员来说,学习如何有效地使用数组/张量 API 是最重要的新编码技能。我们将在本书的后面展示更多技巧,但现在这里是你需要知道的关键事项的摘要。

要创建一个数组或张量,将列表(或列表的列表,或列表的列表的列表等)传递给arraytensor

data = [[1,2,3],[4,5,6]]
arr = array (data)
tns = tensor(data)
  • 1
  • 2
  • 3
arr  # numpy
  • 1
array([[1, 2, 3],
       [4, 5, 6]])
  • 1
  • 2
tns  # pytorch
  • 1
tensor([[1, 2, 3],
        [4, 5, 6]])
  • 1
  • 2

以下所有操作都是在张量上展示的,但 NumPy 数组的语法和结果是相同的。

你可以选择一行(请注意,与 Python 中的列表一样,张量是从 0 开始索引的,所以 1 指的是第二行/列):

tns[1]
  • 1
tensor([4, 5, 6])
  • 1

或者通过使用:来指示所有第一个轴(我们有时将张量/数组的维度称为)选择一列。

tns[:,1]
  • 1
tensor([2, 5])
  • 1

你可以结合 Python 切片语法([*start*:*end*],其中*end*被排除)来选择一行或一列的一部分:

tns[1,1:3]
  • 1
tensor([5, 6])
  • 1

你可以使用标准运算符,如+-*/

tns+1
  • 1
tensor([[2, 3, 4],
        [5, 6, 7]])
  • 1
  • 2

张量有一个类型:

tns.type()
  • 1
'torch.LongTensor'
  • 1

并且会根据需要自动更改该类型;例如,从intfloat

tns*1.5
  • 1
tensor([[1.5000, 3.0000, 4.5000],
        [6.0000, 7.5000, 9.0000]])
  • 1
  • 2

那么,我们的基准模型好吗?为了量化这一点,我们必须定义一个度量。

使用广播计算度量

回想一下度量是基于我们模型的预测和数据集中正确标签计算出来的一个数字,以告诉我们我们的模型有多好。例如,我们可以使用我们在上一节中看到的两个函数之一,均方误差或平均绝对误差,并计算整个数据集上它们的平均值。然而,这两个数字对大多数人来说并不是很容易理解;实际上,我们通常使用准确度作为分类模型的度量。

正如我们讨论过的,我们想要在验证集上计算我们的度量。这样我们就不会无意中过拟合——也就是说,训练一个模型只在我们的训练数据上表现良好。这对于我们在这里作为第一次尝试使用的像素相似度模型来说并不是真正的风险,因为它没有经过训练的组件,但我们仍然会使用一个验证集来遵循正常的实践,并为我们稍后的第二次尝试做好准备。

为了获得一个验证集,我们需要完全从训练数据中删除一些数据,这样模型根本就看不到它。事实证明,MNIST 数据集的创建者已经为我们做了这个。你还记得valid这个整个独立的目录吗?这个目录就是为此而设立的!

所以,让我们从那个目录中为我们的 3 和 7 创建张量。这些是我们将用来计算度量的张量,用来衡量我们第一次尝试模型的质量,这个度量衡量了与理想图像的距离:

valid_3_tens = torch.stack([tensor(Image.open(o))
                            for o in (path/'valid'/'3').ls()])
valid_3_tens = valid_3_tens.float()/255
valid_7_tens = torch.stack([tensor(Image.open(o))
                            for o in (path/'valid'/'7').ls()])
valid_7_tens = valid_7_tens.float()/255
valid_3_tens.shape,valid_7_tens.shape
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
(torch.Size([1010, 28, 28]), torch.Size([1028, 28, 28]))
  • 1

在进行操作时检查形状是一个好习惯。在这里我们看到两个张量,一个代表了 1,010 张大小为 28×28 的 3 的验证集,另一个代表了 1,028 张大小为 28×28 的 7 的验证集。

我们最终想要编写一个函数is_3,它将决定任意图像是 3 还是 7。它将通过确定任意图像更接近我们的两个“理想数字”中的哪一个来实现这一点。为此,我们需要定义距离的概念——即,计算两个图像之间距离的函数。

我们可以编写一个简单的函数,使用与我们在上一节中编写的表达式非常相似的表达式来计算平均绝对误差:

def mnist_distance(a,b): return (a-b).abs().mean((-1,-2))
mnist_distance(a_3, mean3)
  • 1
  • 2
tensor(0.1114)
  • 1

这是我们先前为这两个图像之间的距离计算的相同值,理想数字 3 mean_3和任意样本 3 a_3,它们都是形状为[28,28]的单个图像张量。

但是要计算整体准确度的指标,我们需要计算验证集中每张图像到理想数字 3 的距离。我们如何进行这种计算?我们可以编写一个循环,遍历验证集张量valid_3_tens中堆叠的所有单图像张量,其形状为[1010,28,28],表示 1,010 张图像。但是有一种更好的方法。

当我们使用相同的距离函数,设计用于比较两个单个图像,但将表示 3 的验证集张量valid_3_tens作为参数传入时,会发生一些有趣的事情:

valid_3_dist = mnist_distance(valid_3_tens, mean3)
valid_3_dist, valid_3_dist.shape
  • 1
  • 2
(tensor([0.1050, 0.1526, 0.1186,  ..., 0.1122, 0.1170, 0.1086]),
 torch.Size([1010]))
  • 1
  • 2

它没有抱怨形状不匹配,而是为每个单个图像返回了一个距离(即,长度为 1,010 的秩-1 张量)。这是如何发生的?

再看看我们的函数mnist_distance,您会看到我们在那里有减法(a-b)。魔术技巧在于 PyTorch 在尝试在不同秩的两个张量之间执行简单的减法操作时,将使用广播:它将自动扩展秩较小的张量,使其大小与秩较大的张量相同。广播是一种重要的功能,使张量代码更容易编写。

在广播后,使两个参数张量具有相同的秩后,PyTorch 对于秩相同的两个张量应用其通常的逻辑:它对两个张量的每个对应元素执行操作,并返回张量结果。例如:

tensor([1,2,3]) + tensor([1,1,1])
  • 1
tensor([2, 3, 4])
  • 1

因此,在这种情况下,PyTorch 将mean3视为一个表示单个图像的秩-2 张量,就好像它是 1,010 个相同图像的副本,然后从我们的验证集中的每个 3 中减去每个副本。您期望这个张量的形状是什么?在查看这里的答案之前,请尝试自己想出来:

(valid_3_tens-mean3).shape
  • 1
torch.Size([1010, 28, 28])
  • 1

我们正在计算我们的理想数字 3 与验证集中的每个 1,010 个 3 之间的差异,对于每个 28×28 图像,结果形状为[1010,28,28]

有关广播实现的一些重要要点,使其不仅对于表达性有价值,而且对于性能也有价值:

  • PyTorch 实际上并没有将mean3复制 1,010 次。它假装它是一个具有该形状的张量,但不分配任何额外内存。

  • 它在 C 中完成整个计算(或者,如果您使用 GPU,则在 CUDA 中,相当于 GPU 上的 C),比纯 Python 快数万倍(在 GPU 上甚至快数百万倍!)。

这适用于 PyTorch 中所有广播和逐元素操作和函数。这是您要了解的最重要的技术,以创建高效的 PyTorch 代码。

接下来在mnist_distance中我们看到abs。现在您可能能猜到将其应用于张量时会发生什么。它将方法应用于张量中的每个单独元素,并返回结果的张量(即,它逐元素应用方法)。因此,在这种情况下,我们将得到 1,010 个绝对值。

最后,我们的函数调用mean((-1,-2))。元组(-1,-2)表示一系列轴。在 Python 中,-1指的是最后一个元素,-2指的是倒数第二个元素。因此,在这种情况下,这告诉 PyTorch 我们要对张量的最后两个轴的值进行平均。最后两个轴是图像的水平和垂直维度。在对最后两个轴进行平均后,我们只剩下第一个张量轴,它索引我们的图像,这就是为什么我们的最终大小是(1010)。换句话说,对于每个图像,我们对该图像中所有像素的强度进行了平均。

在本书中,我们将学习更多关于广播的知识,特别是在第十七章中,并且也会经常进行实践。

我们可以使用mnist_distance来确定一幅图像是否为 3,方法是使用以下逻辑:如果问题中的数字与理想的 3 之间的距离小于到理想的 7 的距离,则它是一个 3。这个函数将自动进行广播,并逐个应用,就像所有 PyTorch 函数和运算符一样:

def is_3(x): return mnist_distance(x,mean3) < mnist_distance(x,mean7)
  • 1

让我们在我们的示例案例上测试一下:

is_3(a_3), is_3(a_3).float()
  • 1
(tensor(True), tensor(1.))
  • 1

请注意,当我们将布尔响应转换为浮点数时,True会得到1.0False会得到0.0

由于广播,我们还可以在所有 3 的完整验证集上进行测试:

is_3(valid_3_tens)
  • 1
tensor([True, True, True,  ..., True, True, True])
  • 1

现在我们可以计算每个 3 和 7 的准确率,方法是对所有 3 的函数取平均值,对所有 7 的函数取其倒数的平均值:

accuracy_3s =      is_3(valid_3_tens).float() .mean()
accuracy_7s = (1 - is_3(valid_7_tens).float()).mean()

accuracy_3s,accuracy_7s,(accuracy_3s+accuracy_7s)/2
  • 1
  • 2
  • 3
  • 4
(tensor(0.9168), tensor(0.9854), tensor(0.9511))
  • 1

这看起来是一个相当不错的开始!我们在 3 和 7 上都获得了超过 90%的准确率,我们已经看到了如何使用广播方便地定义度量。但让我们诚实一点:3 和 7 是非常不同的数字。到目前为止,我们只对 10 个可能的数字中的 2 个进行分类。所以我们需要做得更好!

为了做得更好,也许现在是时候尝试一个真正学习的系统了,一个可以自动修改自身以提高性能的系统。换句话说,现在是时候谈论训练过程和 SGD 了。

随机梯度下降

你还记得 Arthur Samuel 在第一章中描述机器学习的方式吗?

假设我们安排一些自动手段来测试任何当前权重分配的有效性,以实际性能为基础,并提供一种机制来改变权重分配以最大化性能。我们不需要详细了解这种程序的细节,就可以看到它可以完全自动化,并且可以看到一个这样编程的机器会从中学习。

正如我们讨论过的,这是让我们拥有一个可以变得越来越好的模型的关键,可以学习。但我们的像素相似性方法实际上并没有做到这一点。我们没有任何权重分配,也没有任何根据测试权重分配的有效性来改进的方法。换句话说,我们无法通过修改一组参数来改进我们的像素相似性方法。为了充分利用深度学习的力量,我们首先必须按照 Samuel 描述的方式来表示我们的任务。

与其尝试找到图像与“理想图像”之间的相似性,我们可以查看每个单独的像素,并为每个像素提出一组权重,使得最高的权重与最有可能为特定类别的黑色像素相关联。例如,向右下方的像素不太可能被激活为 7,因此它们对于 7 的权重应该很低,但它们很可能被激活为 8,因此它们对于 8 的权重应该很高。这可以表示为一个函数和每个可能类别的一组权重值,例如,成为数字 8 的概率:

def pr_eight(x,w) = (x*w).sum()
  • 1

在这里,我们假设X是图像,表示为一个向量—换句话说,所有行都堆叠在一起形成一个长长的单行。我们假设权重是一个向量W。如果我们有了这个函数,我们只需要一种方法来更新权重,使它们变得更好一点。通过这种方法,我们可以重复这个步骤多次,使权重变得越来越好,直到我们能够使它们尽可能好。

我们希望找到导致我们的函数对于那些是 8 的图像结果高,对于那些不是的图像结果低的向量W的特定值。搜索最佳向量W是搜索最佳函数以识别 8 的一种方式。(因为我们还没有使用深度神经网络,我们受到我们的函数能力的限制,我们将在本章后面解决这个约束。)

更具体地说,以下是将这个函数转化为机器学习分类器所需的步骤:

  1. 初始化权重。

  2. 对于每个图像,使用这些权重来预测它是 3 还是 7。

  3. 基于这些预测,计算模型有多好(它的损失)。

  4. 计算梯度,它衡量了每个权重的变化如何改变损失。

  5. 根据这个计算,改变(即,改变)所有权重。

  6. 回到步骤 2 并重复这个过程。

  7. 迭代直到你决定停止训练过程(例如,因为模型已经足够好或者你不想再等待了)。

这七个步骤,如图 4-1 所示,是所有深度学习模型训练的关键。深度学习完全依赖于这些步骤,这是非常令人惊讶和反直觉的。令人惊奇的是,这个过程可以解决如此复杂的问题。但是,正如你将看到的,它确实可以!

显示梯度下降步骤的图表

图 4-1. 梯度下降过程

每个步骤都有许多方法,我们将在本书的其余部分学习它们。这些细节对于深度学习从业者来说非常重要,但事实证明,对于每个步骤的一般方法都遵循一些基本原则。以下是一些建议:

初始化

我们将参数初始化为随机值。这可能听起来令人惊讶。我们当然可以做其他选择,比如将它们初始化为该类别激活该像素的百分比—但由于我们已经知道我们有一种方法来改进这些权重,结果证明只是从随机权重开始就可以完全正常运行。

损失

这就是 Samuel 所说的根据实际表现测试任何当前权重分配的有效性。我们需要一个函数,如果模型的表现好,它将返回一个小的数字(标准方法是将小的损失视为好的,大的损失视为坏的,尽管这只是一种约定)。

步骤

一个简单的方法来判断一个权重是否应该增加一点或减少一点就是尝试一下:增加一点权重,看看损失是增加还是减少。一旦找到正确的方向,你可以再多改变一点或少改变一点,直到找到一个效果好的量。然而,这很慢!正如我们将看到的,微积分的魔力使我们能够直接找出每个权重应该朝哪个方向改变,大概改变多少,而不必尝试所有这些小的改变。这样做的方法是通过计算梯度。这只是一种性能优化;我们也可以通过使用更慢的手动过程得到完全相同的结果。

停止

一旦我们决定要为模型训练多少个周期(之前的列表中给出了一些建议),我们就会应用这个决定。对于我们的数字分类器,我们会继续训练,直到模型的准确率开始变差,或者我们用完时间为止。

在将这些步骤应用于我们的图像分类问题之前,让我们在一个更简单的情况下看看它们是什么样子。首先我们将定义一个非常简单的函数,二次函数—假设这是我们的损失函数,x是函数的权重参数:

def f(x): return x**2
  • 1

这是该函数的图表:

plot_function(f, 'x', 'x**2')
  • 1

我们之前描述的步骤序列从选择参数的随机值开始,并计算损失的值:

plot_function(f, 'x', 'x**2')
plt.scatter(-1.5, f(-1.5), color='red');
  • 1
  • 2

现在我们来看看如果我们稍微增加或减少参数会发生什么—调整。这只是特定点的斜率:

显示在某一点的斜率的平方函数的图表

我们可以稍微改变我们的权重朝着斜坡的方向,计算我们的损失和调整,然后再重复几次。最终,我们将到达曲线上的最低点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个基本思想最早可以追溯到艾萨克·牛顿,他指出我们可以以这种方式优化任意函数。无论我们的函数变得多么复杂,梯度下降的这种基本方法不会有太大变化。我们在本书后面看到的唯一微小变化是一些方便的方法,可以让我们更快地找到更好的步骤。

计算梯度

唯一的魔法步骤是计算梯度的部分。正如我们提到的,我们使用微积分作为性能优化;它让我们更快地计算当我们调整参数时我们的损失会上升还是下降。换句话说,梯度将告诉我们我们需要改变每个权重多少才能使我们的模型更好。

您可能还记得高中微积分课上的导数告诉您函数参数的变化会如何改变其结果。如果不记得,不用担心;我们很多人高中毕业后就忘了微积分!但在继续之前,您需要对导数有一些直观的理解,所以如果您对此一头雾水,可以前往 Khan Academy 完成基本导数课程。您不必自己计算导数;您只需要知道导数是什么。

导数的关键点在于:对于任何函数,比如我们在前一节中看到的二次函数,我们可以计算它的导数。导数是另一个函数。它计算的是变化,而不是值。例如,在值为 3 时,二次函数的导数告诉我们函数在值为 3 时的变化速度。更具体地说,您可能还记得梯度被定义为上升/水平移动;也就是说,函数值的变化除以参数值的变化。当我们知道我们的函数将如何变化时,我们就知道我们需要做什么来使它变小。这是机器学习的关键:有一种方法来改变函数的参数使其变小。微积分为我们提供了一个计算的捷径,即导数,它让我们直接计算我们函数的梯度。

一个重要的事情要注意的是我们的函数有很多需要调整的权重,所以当我们计算导数时,我们不会得到一个数字,而是很多个—每个权重都有一个梯度。但在这里没有数学上的技巧;您可以计算相对于一个权重的导数,将其他所有权重视为常数,然后对每个其他权重重复这个过程。这就是计算所有梯度的方法,对于每个权重。

刚才我们提到您不必自己计算任何梯度。这怎么可能?令人惊讶的是,PyTorch 能够自动计算几乎任何函数的导数!而且,它计算得非常快。大多数情况下,它至少与您手动创建的任何导数函数一样快。让我们看一个例子。

首先,让我们选择一个张量数值,我们想要梯度:

xt = tensor(3.).requires_grad_()
  • 1

注意特殊方法requires_grad_?这是我们告诉 PyTorch 我们想要计算梯度的神奇咒语。这实质上是给变量打上标记,这样 PyTorch 就会记住如何计算您要求的其他直接计算的梯度。

Alexis 说

如果您来自数学或物理学,这个 API 可能会让您困惑。在这些背景下,函数的“梯度”只是另一个函数(即,它的导数),因此您可能期望与梯度相关的 API 提供给您一个新函数。但在深度学习中,“梯度”通常意味着函数的导数在特定参数值处的。PyTorch API 也将重点放在参数上,而不是您实际计算梯度的函数。起初可能感觉有些反常,但这只是一个不同的视角。

现在我们用这个值计算我们的函数。注意 PyTorch 打印的不仅是计算的值,还有一个提示,它有一个梯度函数将在需要时用来计算我们的梯度:

yt = f(xt)
yt
  • 1
  • 2
tensor(9., grad_fn=<PowBackward0>)
  • 1

最后,我们告诉 PyTorch 为我们计算梯度:

yt.backward()
  • 1

这里的backward指的是反向传播,这是计算每一层导数的过程的名称。我们将在第十七章中看到这是如何精确完成的,当我们从头开始计算深度神经网络的梯度时。这被称为网络的反向传播,与前向传播相对,前者是计算激活的地方。如果backward只是被称为calculate_grad,生活可能会更容易,但深度学习的人确实喜欢在任何地方添加行话!

我们现在可以通过检查我们张量的grad属性来查看梯度:

xt.grad
  • 1
tensor(6.)
  • 1

如果您记得高中微积分规则,x**2的导数是2*x,我们有x=3,所以梯度应该是2*3=6,这就是 PyTorch 为我们计算的结果!

现在我们将重复前面的步骤,但使用一个向量参数来计算我们的函数:

xt = tensor([3.,4.,10.]).requires_grad_()
xt
  • 1
  • 2
tensor([ 3.,  4., 10.], requires_grad=True)
  • 1

并且我们将sum添加到我们的函数中,以便它可以接受一个向量(即,一个秩为 1 的张量)并返回一个标量(即,一个秩为 0 的张量):

def f(x): return (x**2).sum()

yt = f(xt)
yt
  • 1
  • 2
  • 3
  • 4
tensor(125., grad_fn=<SumBackward0>)
  • 1

我们的梯度是2*xt,正如我们所期望的!

yt.backward()
xt.grad
  • 1
  • 2
tensor([ 6.,  8., 20.])
  • 1

梯度告诉我们函数的斜率;它们并不告诉我们要调整参数多远。但它们确实给了我们一些想法:如果斜率非常大,那可能意味着我们需要更多的调整,而如果斜率非常小,那可能意味着我们接近最优值。

使用学习率进行步进

根据梯度值来决定如何改变我们的参数是深度学习过程中的一个重要部分。几乎所有方法都从一个基本思想开始,即将梯度乘以一些小数字,称为学习率(LR)。学习率通常是 0.001 到 0.1 之间的数字,尽管它可以是任何值。通常人们通过尝试几个学习率来选择一个,并找出哪个在训练后产生最佳模型的结果(我们将在本书后面展示一个更好的方法,称为学习率查找器)。一旦选择了学习率,您可以使用这个简单函数调整参数:

w -= w.grad * lr
  • 1

这被称为调整您的参数,使用优化步骤

如果您选择的学习率太低,可能意味着需要执行很多步骤。图 4-2 说明了这一点。

梯度下降示例,学习率过低

图 4-2。学习率过低的梯度下降

但选择一个学习率太高的学习率更糟糕——它可能导致损失变得更糟,正如我们在图 4-3 中看到的!

学习率过高的梯度下降示例

图 4-3. 学习率过高的梯度下降

如果学习率太高,它也可能会“弹跳”而不是发散;图 4-4 显示了这样做需要许多步骤才能成功训练。

带有弹跳学习率的梯度下降示例

图 4-4. 带有弹跳学习率的梯度下降

现在让我们在一个端到端的示例中应用所有这些。

一个端到端的 SGD 示例

我们已经看到如何使用梯度来最小化我们的损失。现在是时候看一个 SGD 示例,并看看如何找到最小值来训练模型以更好地拟合数据。

让我们从一个简单的合成示例模型开始。想象一下,您正在测量过山车通过顶峰时的速度。它会开始快速,然后随着上坡而变慢;在顶部最慢,然后在下坡时再次加速。您想建立一个关于速度随时间变化的模型。如果您每秒手动测量速度 20 秒,它可能看起来像这样:

time = torch.arange(0,20).float(); time
  • 1
tensor([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12., 13.,
 > 14., 15., 16., 17., 18., 19.])
  • 1
  • 2
speed = torch.randn(20)*3 + 0.75*(time-9.5)**2 + 1
plt.scatter(time,speed);
  • 1
  • 2

我们添加了一些随机噪声,因为手动测量不够精确。这意味着很难回答问题:过山车的速度是多少?使用 SGD,我们可以尝试找到一个与我们的观察相匹配的函数。我们无法考虑每种可能的函数,所以让我们猜测它将是二次的;即,一个形式为a*(time**2)+(b*time)+c的函数。

我们希望清楚地区分函数的输入(我们测量过山车速度的时间)和其参数(定义我们正在尝试的二次函数的值)。因此,让我们将参数收集在一个参数中,从而在函数的签名中分离输入t和参数params

def f(t, params):
    a,b,c = params
    return a*(t**2) + (b*t) + c
  • 1
  • 2
  • 3

换句话说,我们已经将找到最佳拟合数据的最佳函数的问题限制为找到最佳二次函数。这极大地简化了问题,因为每个二次函数都由三个参数abc完全定义。因此,要找到最佳二次函数,我们只需要找到最佳的abc的值。

如果我们可以解决二次函数的三个参数的问题,我们就能够对其他具有更多参数的更复杂函数应用相同的方法——比如神经网络。让我们先找到f的参数,然后我们将回来对 MNIST 数据集使用神经网络做同样的事情。

首先,我们需要定义“最佳”是什么意思。我们通过选择一个损失函数来精确定义这一点,该函数将根据预测和目标返回一个值,其中函数的较低值对应于“更好”的预测。对于连续数据,通常使用均方误差

def mse(preds, targets): return ((preds-targets)**2).mean()
  • 1

现在,让我们按照我们的七步流程进行工作。

第一步:初始化参数

首先,我们将参数初始化为随机值,并告诉 PyTorch 我们要使用requires_grad_跟踪它们的梯度:

params = torch.randn(3).requires_grad_()
  • 1

第二步:计算预测

接下来,我们计算预测:

preds = f(time, params)
  • 1

让我们创建一个小函数来查看我们的预测与目标的接近程度,并看一看:

def show_preds(preds, ax=None):
    if ax is None: ax=plt.subplots()[1]
    ax.scatter(time, speed)
    ax.scatter(time, to_np(preds), color='red')
    ax.set_ylim(-300,100)
  • 1
  • 2
  • 3
  • 4
  • 5
show_preds(preds)
  • 1

这看起来并不接近——我们的随机参数表明过山车最终会倒退,因为我们有负速度!

第三步:计算损失

我们计算损失如下:

loss = mse(preds, speed)
loss
  • 1
  • 2
tensor(25823.8086, grad_fn=<MeanBackward0>)
  • 1

我们的目标现在是改进这一点。为了做到这一点,我们需要知道梯度。

第四步:计算梯度

下一步是计算梯度,或者近似参数需要如何改变:

loss.backward()
params.grad
  • 1
  • 2
tensor([-53195.8594,  -3419.7146,   -253.8908])
  • 1
params.grad * 1e-5
  • 1
tensor([-0.5320, -0.0342, -0.0025])
  • 1

我们可以利用这些梯度来改进我们的参数。我们需要选择一个学习率(我们将在下一章中讨论如何在实践中做到这一点;现在,我们将使用 1e-5 或 0.00001):

params
  • 1
tensor([-0.7658, -0.7506,  1.3525], requires_grad=True)
  • 1

第 5 步:调整权重

现在我们需要根据刚刚计算的梯度更新参数:

lr = 1e-5
params.data -= lr * params.grad.data
params.grad = None
  • 1
  • 2
  • 3

Alexis 说

理解这一点取决于记住最近的历史。为了计算梯度,我们在loss上调用backward。但是这个loss本身是通过mse计算的,而mse又以preds作为输入,preds是使用f计算的,fparams作为输入,params是我们最初调用required_grads_的对象,这是最初的调用,现在允许我们在loss上调用backward。这一系列函数调用代表了函数的数学组合,使得 PyTorch 能够在幕后使用微积分的链式法则来计算这些梯度。

让我们看看损失是否有所改善:

preds = f(time,params)
mse(preds, speed)
  • 1
  • 2
tensor(5435.5366, grad_fn=<MeanBackward0>)
  • 1

再看一下图表:

show_preds(preds)
  • 1

我们需要重复这个过程几次,所以我们将创建一个应用一步的函数:

def apply_step(params, prn=True):
    preds = f(time, params)
    loss = mse(preds, speed)
    loss.backward()
    params.data -= lr * params.grad.data
    params.grad = None
    if prn: print(loss.item())
    return preds
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

第 6 步:重复这个过程

现在我们进行迭代。通过循环和进行许多改进,我们希望达到一个好的结果:

for i in range(10): apply_step(params)
  • 1
5435.53662109375
1577.4495849609375
847.3780517578125
709.22265625
683.0757446289062
678.12451171875
677.1839599609375
677.0025024414062
676.96435546875
676.9537353515625
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

损失正在下降,正如我们所希望的!但仅仅看这些损失数字掩盖了一个事实,即每次迭代代表尝试一个完全不同的二次函数,以找到最佳可能的二次函数。如果我们不打印出损失函数,而是在每一步绘制函数,我们可以看到形状是如何接近我们的数据的最佳可能的二次函数:

_,axs = plt.subplots(1,4,figsize=(12,3))
for ax in axs: show_preds(apply_step(params, False), ax)
plt.tight_layout()
  • 1
  • 2
  • 3

第 7 步:停止

我们刚刚决定在任意选择的 10 个 epochs 后停止。在实践中,我们会观察训练和验证损失以及我们的指标,以决定何时停止,正如我们所讨论的那样。

总结梯度下降

现在您已经看到每个步骤中发生的事情,让我们再次看一下我们的梯度下降过程的图形表示(图 4-5)并进行一个快速回顾。

显示梯度下降步骤的图表

图 4-5. 梯度下降过程

在开始时,我们模型的权重可以是随机的(从头开始训练)或来自预训练模型(迁移学习)。在第一种情况下,我们从输入得到的输出与我们想要的完全无关,即使在第二种情况下,预训练模型也可能不太擅长我们所针对的特定任务。因此,模型需要学习更好的权重。

我们首先将模型给出的输出与我们的目标进行比较(我们有标记数据,所以我们知道模型应该给出什么结果),使用一个损失函数,它返回一个数字,我们希望通过改进我们的权重使其尽可能低。为了做到这一点,我们从训练集中取出一些数据项(如图像)并将它们馈送给我们的模型。我们使用我们的损失函数比较相应的目标,我们得到的分数告诉我们我们的预测有多么错误。然后我们稍微改变权重使其稍微更好。

为了找出如何改变权重使损失稍微变好,我们使用微积分来计算梯度。(实际上,我们让 PyTorch 为我们做这个!)让我们考虑一个类比。想象一下你在山上迷路了,你的车停在最低点。为了找到回去的路,你可能会朝着随机方向走,但那可能不会有太大帮助。由于你知道你的车在最低点,你最好是往下走。通过始终朝着最陡峭的下坡方向迈出一步,你最终应该到达目的地。我们使用梯度的大小(即坡度的陡峭程度)来告诉我们应该迈多大一步;具体来说,我们将梯度乘以我们选择的一个称为学习率的数字来决定步长。然后我们迭代直到达到最低点,那将是我们的停车场;然后我们可以停止

我们刚刚看到的所有内容都可以直接转换到 MNIST 数据集,除了损失函数。现在让我们看看如何定义一个好的训练目标。

MNIST 损失函数

我们已经有了我们的x—也就是我们的自变量,图像本身。我们将它们全部连接成一个单一的张量,并且还将它们从矩阵列表(一个秩为 3 的张量)转换为向量列表(一个秩为 2 的张量)。我们可以使用view来做到这一点,view是一个 PyTorch 方法,可以改变张量的形状而不改变其内容。-1view的一个特殊参数,意思是“使这个轴尽可能大以适应所有数据”:

train_x = torch.cat([stacked_threes, stacked_sevens]).view(-1, 28*28)
  • 1

我们需要为每张图片标记。我们将使用1表示 3,0表示 7:

train_y = tensor([1]*len(threes) + [0]*len(sevens)).unsqueeze(1)
train_x.shape,train_y.shape
  • 1
  • 2
(torch.Size([12396, 784]), torch.Size([12396, 1]))
  • 1

在 PyTorch 中,当索引时,Dataset需要返回一个(x,y)元组。Python 提供了一个zip函数,当与list结合使用时,可以简单地实现这个功能:

dset = list(zip(train_x,train_y))
x,y = dset[0]
x.shape,y
  • 1
  • 2
  • 3
(torch.Size([784]), tensor([1]))
  • 1
valid_x = torch.cat([valid_3_tens, valid_7_tens]).view(-1, 28*28)
valid_y = tensor([1]*len(valid_3_tens) + [0]*len(valid_7_tens)).unsqueeze(1)
valid_dset = list(zip(valid_x,valid_y))
  • 1
  • 2
  • 3

现在我们需要为每个像素(最初是随机的)分配一个权重(这是我们七步过程中的初始化步骤):

def init_params(size, std=1.0): return (torch.randn(size)*std).requires_grad_()
  • 1
weights = init_params((28*28,1))
  • 1

函数weights*pixels不够灵活—当像素等于 0 时,它总是等于 0(即其截距为 0)。你可能还记得高中数学中线的公式是y=w*x+b;我们仍然需要b。我们也会将其初始化为一个随机数:

bias = init_params(1)
  • 1

在神经网络中,方程y=w*x+b中的w被称为权重b被称为偏置。权重和偏置一起构成参数

术语:参数

模型的权重偏置。权重是方程w*x+b中的w,偏置是该方程中的b

现在我们可以为一张图片计算一个预测:

(train_x[0]*weights.T).sum() + bias
  • 1
tensor([20.2336], grad_fn=<AddBackward0>)
  • 1

虽然我们可以使用 Python 的for循环来计算每张图片的预测,但那将非常慢。因为 Python 循环不在 GPU 上运行,而且因为 Python 在一般情况下循环速度较慢,我们需要尽可能多地使用高级函数来表示模型中的计算。

在这种情况下,有一个非常方便的数学运算可以为矩阵的每一行计算w*x—它被称为矩阵乘法。图 4-6 展示了矩阵乘法的样子。

矩阵乘法

图 4-6. 矩阵乘法

这幅图展示了两个矩阵AB相乘。结果的每个项目,我们称之为AB,包含了A的对应行的每个项目与B的对应列的每个项目相乘后相加。例如,第 1 行第 2 列(带有红色边框的黄色点)计算为。如果您需要复习矩阵乘法,我们建议您查看 Khan Academy 的“矩阵乘法简介”,因为这是深度学习中最重要的数学运算。

在 Python 中,矩阵乘法用@运算符表示。让我们试一试:

def linear1(xb): return xb@weights + bias
preds = linear1(train_x)
preds
  • 1
  • 2
  • 3
tensor([[20.2336],
        [17.0644],
        [15.2384],
        ...,
        [18.3804],
        [23.8567],
        [28.6816]], grad_fn=<AddBackward0>)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

第一个元素与我们之前计算的相同,正如我们所期望的。这个方程batch @ weights + bias是任何神经网络的两个基本方程之一(另一个是激活函数,我们马上会看到)。

让我们检查我们的准确性。为了确定输出代表 3 还是 7,我们只需检查它是否大于 0,因此我们可以计算每个项目的准确性(使用广播,因此没有循环!)如下:

corrects = (preds>0.0).float() == train_y
corrects
  • 1
  • 2
tensor([[ True],
        [ True],
        [ True],
        ...,
        [False],
        [False],
        [False]])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
corrects.float().mean().item()
  • 1
0.4912068545818329
  • 1

现在让我们看看一个权重的微小变化对准确性的影响是什么:

weights[0] *= 1.0001
  • 1
preds = linear1(train_x)
((preds>0.0).float() == train_y).float().mean().item()
  • 1
  • 2
0.4912068545818329
  • 1

正如我们所看到的,我们需要梯度来通过 SGD 改进我们的模型,为了计算梯度,我们需要一个损失函数,它代表了我们的模型有多好。这是因为梯度是损失函数如何随着对权重的微小调整而变化的度量。

因此,我们需要选择一个损失函数。显而易见的方法是使用准确性作为我们的度量标准,也作为我们的损失函数。在这种情况下,我们将为每个图像计算我们的预测,收集这些值以计算总体准确性,然后计算每个权重相对于总体准确性的梯度。

不幸的是,我们在这里有一个重要的技术问题。函数的梯度是其斜率,或者是其陡峭程度,可以定义为上升与下降——也就是说,函数值上升或下降的幅度,除以我们改变输入的幅度。我们可以用数学方式写成:

(y_new – y_old) / (x_new – x_old)
  • 1

x_new非常类似于x_old时,这给出了梯度的良好近似,这意味着它们的差异非常小。但是,只有当预测从 3 变为 7,或者反之时,准确性才会发生变化。问题在于,从x_oldx_new的权重的微小变化不太可能导致任何预测发生变化,因此(y_new - y_old)几乎总是为 0。换句话说,梯度几乎在任何地方都为 0。

权重值的微小变化通常不会改变准确性。这意味着使用准确性作为损失函数是没有用的——如果我们这样做,大多数时候我们的梯度将为 0,模型将无法从该数字中学习。

Sylvain 说

在数学术语中,准确性是一个几乎在任何地方都是常数的函数(除了阈值 0.5),因此它的导数几乎在任何地方都是零(在阈值处为无穷大)。这将导致梯度为 0 或无穷大,这对于更新模型是没有用的。

相反,我们需要一个损失函数,当我们的权重导致稍微更好的预测时,给出稍微更好的损失。那么,“稍微更好的预测”具体是什么样呢?在这种情况下,这意味着如果正确答案是 3,则分数稍高,或者如果正确答案是 7,则分数稍低。

现在让我们编写这样一个函数。它是什么形式?

损失函数接收的不是图像本身,而是模型的预测。因此,让我们做一个参数prds,值在 0 和 1 之间,其中每个值是图像是 3 的预测。它是一个矢量(即,一个秩-1 张量),索引在图像上。

损失函数的目的是衡量预测值与真实值之间的差异,即目标(又称标签)。因此,让我们再做一个参数trgts,其值为 0 或 1,告诉图像实际上是 3 还是不是 3。它也是一个矢量(即,另一个秩-1 张量),索引在图像上。

例如,假设我们有三幅图像,我们知道其中一幅是 3,一幅是 7,一幅是 3。假设我们的模型以高置信度(0.9)预测第一幅是 3,以轻微置信度(0.4)预测第二幅是 7,以公平置信度(0.2),但是错误地预测最后一幅是 7。这意味着我们的损失函数将接收这些值作为其输入:

trgts  = tensor([1,0,1])
prds   = tensor([0.9, 0.4, 0.2])
  • 1
  • 2

这是一个测量predictionstargets之间距离的损失函数的第一次尝试:

def mnist_loss(predictions, targets):
    return torch.where(targets==1, 1-predictions, predictions).mean()
  • 1
  • 2

我们正在使用一个新函数,torch.where(a,b,c)。这与运行列表推导[b[i] if a[i] else c[i] for i in range(len(a))]相同,只是它在张量上运行,以 C/CUDA 速度运行。简单来说,这个函数将衡量每个预测离 1 有多远,如果应该是 1 的话,以及它离 0 有多远,如果应该是 0 的话,然后它将取所有这些距离的平均值。

阅读文档

学习 PyTorch 这样的函数很重要,因为在 Python 中循环张量的速度是 Python 速度,而不是 C/CUDA 速度!现在尝试运行help(torch.where)来阅读此函数的文档,或者更好的是,在 PyTorch 文档站点上查找。

让我们在我们的prdstrgts上尝试一下:

torch.where(trgts==1, 1-prds, prds)
  • 1
tensor([0.1000, 0.4000, 0.8000])
  • 1

您可以看到,当预测更准确时,当准确预测更自信时(绝对值更高),以及当不准确预测更不自信时,此函数返回较低的数字。在 PyTorch 中,我们始终假设损失函数的较低值更好。由于我们需要一个标量作为最终损失,mnist_loss取前一个张量的平均值:

mnist_loss(prds,trgts)
  • 1
tensor(0.4333)
  • 1

例如,如果我们将对一个“错误”目标的预测从0.2更改为0.8,损失将减少,表明这是一个更好的预测:

mnist_loss(tensor([0.9, 0.4, 0.8]),trgts)
  • 1
tensor(0.2333)
  • 1

mnist_loss当前定义的一个问题是它假设预测总是在 0 和 1 之间。因此,我们需要确保这实际上是这种情况!恰好有一个函数可以做到这一点,让我们来看看。

Sigmoid

sigmoid函数总是输出一个介于 0 和 1 之间的数字。它的定义如下:

def sigmoid(x): return 1/(1+torch.exp(-x))
  • 1

PyTorch 为我们定义了一个加速版本,所以我们不需要自己的。这是深度学习中一个重要的函数,因为我们经常希望确保数值在 0 和 1 之间。它看起来是这样的:

plot_function(torch.sigmoid, title='Sigmoid', min=-4, max=4)
  • 1

正如您所看到的,它接受任何输入值,正数或负数,并将其压缩为 0 和 1 之间的输出值。它还是一个只上升的平滑曲线,这使得 SGD 更容易找到有意义的梯度。

让我们更新mnist_loss,首先对输入应用sigmoid

def mnist_loss(predictions, targets):
    predictions = predictions.sigmoid()
    return torch.where(targets==1, 1-predictions, predictions).mean()
  • 1
  • 2
  • 3

现在我们可以确信我们的损失函数将起作用,即使预测不在 0 和 1 之间。唯一需要的是更高的预测对应更高的置信度。

定义了一个损失函数,现在是一个好时机回顾为什么这样做。毕竟,我们已经有了一个度量标准,即整体准确率。那么为什么我们定义了一个损失?

关键区别在于指标用于驱动人类理解,而损失用于驱动自动学习。为了驱动自动学习,损失必须是一个具有有意义导数的函数。它不能有大的平坦部分和大的跳跃,而必须是相当平滑的。这就是为什么我们设计了一个损失函数,可以对置信水平的小变化做出响应。这个要求意味着有时它实际上并不完全反映我们试图实现的目标,而是我们真正目标和一个可以使用其梯度进行优化的函数之间的妥协。损失函数是针对数据集中的每个项目计算的,然后在时代结束时,所有损失值都被平均,整体均值被报告为时代。

另一方面,指标是我们关心的数字。这些是在每个时代结束时打印的值,告诉我们我们的模型表现如何。重要的是,我们学会关注这些指标,而不是损失,来评估模型的性能。

SGD 和小批次

现在我们有了一个适合驱动 SGD 的损失函数,我们可以考虑学习过程的下一阶段涉及的一些细节,即根据梯度改变或更新权重。这被称为优化步骤

要进行优化步骤,我们需要计算一个或多个数据项的损失。我们应该使用多少?我们可以为整个数据集计算并取平均值,或者可以为单个数据项计算。但这两种方法都不理想。为整个数据集计算将需要很长时间。为单个数据项计算将不会使用太多信息,因此会导致不精确和不稳定的梯度。您将费力更新权重,但只考虑这将如何改善模型在该单个数据项上的性能。

因此,我们做出妥协:我们一次计算几个数据项的平均损失。这被称为小批次。小批次中的数据项数量称为批次大小。较大的批次大小意味着您将从损失函数中获得更准确和稳定的数据集梯度估计,但这将需要更长时间,并且您将在每个时代处理较少的小批次。选择一个好的批次大小是您作为深度学习从业者需要做出的决定之一,以便快速准确地训练您的模型。我们将在本书中讨论如何做出这个选择。

使用小批次而不是在单个数据项上计算梯度的另一个很好的理由是,实际上,我们几乎总是在加速器上进行训练,例如 GPU。这些加速器只有在一次有很多工作要做时才能表现良好,因此如果我们可以给它们很多数据项来处理,这将是有帮助的。使用小批次是实现这一目标的最佳方法之一。但是,如果您一次给它们太多数据来处理,它们会耗尽内存——让 GPU 保持愉快也是棘手的!

正如您在第二章中关于数据增强的讨论中所看到的,如果我们在训练过程中可以改变一些东西,我们会获得更好的泛化能力。我们可以改变的一个简单而有效的事情是将哪些数据项放入每个小批次。我们通常不是简单地按顺序枚举我们的数据集,而是在每个时代之前随机洗牌,然后创建小批次。PyTorch 和 fastai 提供了一个类,可以为您执行洗牌和小批次整理,称为DataLoader

DataLoader可以将任何 Python 集合转换为一个迭代器,用于生成多个批次,就像这样:

coll = range(15)
dl = DataLoader(coll, batch_size=5, shuffle=True)
list(dl)
  • 1
  • 2
  • 3
[tensor([ 3, 12,  8, 10,  2]),
 tensor([ 9,  4,  7, 14,  5]),
 tensor([ 1, 13,  0,  6, 11])]
  • 1
  • 2
  • 3

对于训练模型,我们不只是想要任何 Python 集合,而是一个包含独立和相关变量(模型的输入和目标)的集合。包含独立和相关变量元组的集合在 PyTorch 中被称为Dataset。这是一个极其简单的Dataset的示例:

ds = L(enumerate(string.ascii_lowercase))
ds
  • 1
  • 2
(#26) [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd'),(4, 'e'),(5, 'f'),(6, 'g'),(7,
 > 'h'),(8, 'i'),(9, 'j')...]
  • 1
  • 2

当我们将Dataset传递给DataLoader时,我们将得到许多批次,它们本身是表示独立和相关变量批次的张量元组:

dl = DataLoader(ds, batch_size=6, shuffle=True)
list(dl)
  • 1
  • 2
[(tensor([17, 18, 10, 22,  8, 14]), ('r', 's', 'k', 'w', 'i', 'o')),
 (tensor([20, 15,  9, 13, 21, 12]), ('u', 'p', 'j', 'n', 'v', 'm')),
 (tensor([ 7, 25,  6,  5, 11, 23]), ('h', 'z', 'g', 'f', 'l', 'x')),
 (tensor([ 1,  3,  0, 24, 19, 16]), ('b', 'd', 'a', 'y', 't', 'q')),
 (tensor([2, 4]), ('c', 'e'))]
  • 1
  • 2
  • 3
  • 4
  • 5

我们现在准备为使用 SGD 的模型编写我们的第一个训练循环!

把所有东西放在一起

是时候实现我们在图 4-1 中看到的过程了。在代码中,我们的过程将为每个时期实现类似于这样的东西:

for x,y in dl:
    pred = model(x)
    loss = loss_func(pred, y)
    loss.backward()
    parameters -= parameters.grad * lr
  • 1
  • 2
  • 3
  • 4
  • 5

首先,让我们重新初始化我们的参数:

weights = init_params((28*28,1))
bias = init_params(1)
  • 1
  • 2

DataLoader可以从Dataset创建:

dl = DataLoader(dset, batch_size=256)
xb,yb = first(dl)
xb.shape,yb.shape
  • 1
  • 2
  • 3
(torch.Size([256, 784]), torch.Size([256, 1]))
  • 1

我们将对验证集执行相同的操作:

valid_dl = DataLoader(valid_dset, batch_size=256)
  • 1

让我们创建一个大小为 4 的小批量进行测试:

batch = train_x[:4]
batch.shape
  • 1
  • 2
torch.Size([4, 784])
  • 1
preds = linear1(batch)
preds
  • 1
  • 2
tensor([[-11.1002],
        [  5.9263],
        [  9.9627],
        [ -8.1484]], grad_fn=<AddBackward0>)
  • 1
  • 2
  • 3
  • 4
loss = mnist_loss(preds, train_y[:4])
loss
  • 1
  • 2
tensor(0.5006, grad_fn=<MeanBackward0>)
  • 1

现在我们可以计算梯度了:

loss.backward()
weights.grad.shape,weights.grad.mean(),bias.grad
  • 1
  • 2
(torch.Size([784, 1]), tensor(-0.0001), tensor([-0.0008]))
  • 1

让我们把所有这些放在一个函数中:

def calc_grad(xb, yb, model):
    preds = model(xb)
    loss = mnist_loss(preds, yb)
    loss.backward()
  • 1
  • 2
  • 3
  • 4

并测试它:

calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad
  • 1
  • 2
(tensor(-0.0002), tensor([-0.0015]))
  • 1

但是看看如果我们调用两次会发生什么:

calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad
  • 1
  • 2
(tensor(-0.0003), tensor([-0.0023]))
  • 1

梯度已经改变了!这是因为loss.backward 添加loss的梯度到当前存储的任何梯度中。因此,我们首先必须将当前梯度设置为 0:

weights.grad.zero_()
bias.grad.zero_();
  • 1
  • 2

原地操作

PyTorch 中以下划线结尾的方法会原地修改它们的对象。例如,bias.zero_会将张量bias的所有元素设置为 0。

我们唯一剩下的步骤是根据梯度和学习率更新权重和偏差。当我们这样做时,我们必须告诉 PyTorch 不要对这一步骤进行梯度计算,否则当我们尝试在下一个批次计算导数时会变得混乱!如果我们将张量的data属性赋值,PyTorch 将不会对该步骤进行梯度计算。这是我们用于一个时期的基本训练循环:

def train_epoch(model, lr, params):
    for xb,yb in dl:
        calc_grad(xb, yb, model)
        for p in params:
            p.data -= p.grad*lr
            p.grad.zero_()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们还想通过查看验证集的准确性来检查我们的表现。要决定输出是否代表 3 或 7,我们只需检查它是否大于 0。因此,我们可以计算每个项目的准确性(使用广播,所以没有循环!)如下:

(preds>0.0).float() == train_y[:4]
  • 1
tensor([[False],
        [ True],
        [ True],
        [False]])
  • 1
  • 2
  • 3
  • 4

这给了我们计算验证准确性的这个函数:

def batch_accuracy(xb, yb):
    preds = xb.sigmoid()
    correct = (preds>0.5) == yb
    return correct.float().mean()
  • 1
  • 2
  • 3
  • 4

我们可以检查它是否有效:

batch_accuracy(linear1(batch), train_y[:4])
  • 1
tensor(0.5000)
  • 1

然后把批次放在一起:

def validate_epoch(model):
    accs = [batch_accuracy(model(xb), yb) for xb,yb in valid_dl]
    return round(torch.stack(accs).mean().item(), 4)
  • 1
  • 2
  • 3
validate_epoch(linear1)
  • 1
0.5219
  • 1

这是我们的起点。让我们训练一个时期,看看准确性是否提高:

lr = 1.
params = weights,bias
train_epoch(linear1, lr, params)
validate_epoch(linear1)
  • 1
  • 2
  • 3
  • 4
0.6883
  • 1

然后再做几次:

for i in range(20):
    train_epoch(linear1, lr, params)
    print(validate_epoch(linear1), end=' ')
  • 1
  • 2
  • 3
0.8314 0.9017 0.9227 0.9349 0.9438 0.9501 0.9535 0.9564 0.9594 0.9618 0.9613
 > 0.9638 0.9643 0.9652 0.9662 0.9677 0.9687 0.9691 0.9691 0.9696
  • 1
  • 2

看起来不错!我们的准确性已经接近“像素相似性”方法的准确性,我们已经创建了一个通用的基础可以构建。我们的下一步将是创建一个将处理 SGD 步骤的对象。在 PyTorch 中,它被称为优化器

创建一个优化器

因为这是一个如此通用的基础,PyTorch 提供了一些有用的类来使实现更容易。我们可以做的第一件事是用 PyTorch 的nn.Linear模块替换我们的linear函数。模块是从 PyTorch nn.Module类继承的类的对象。这个类的对象的行为与标准 Python 函数完全相同,您可以使用括号调用它们,它们将返回模型的激活。

nn.Linear做的事情与我们的init_paramslinear一样。它包含了权重偏差在一个单独的类中。这是我们如何复制上一节中的模型:

linear_model = nn.Linear(28*28,1)
  • 1

每个 PyTorch 模块都知道它有哪些可以训练的参数;它们可以通过parameters方法获得:

w,b = linear_model.parameters()
w.shape,b.shape
  • 1
  • 2
(torch.Size([1, 784]), torch.Size([1]))
  • 1

我们可以使用这些信息创建一个优化器:

class BasicOptim:
    def __init__(self,params,lr): self.params,self.lr = list(params),lr

    def step(self, *args, **kwargs):
        for p in self.params: p.data -= p.grad.data * self.lr

    def zero_grad(self, *args, **kwargs):
        for p in self.params: p.grad = None
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们可以通过传入模型的参数来创建优化器:

opt = BasicOptim(linear_model.parameters(), lr)
  • 1

我们的训练循环现在可以简化:

def train_epoch(model):
    for xb,yb in dl:
        calc_grad(xb, yb, model)
        opt.step()
        opt.zero_grad()
  • 1
  • 2
  • 3
  • 4
  • 5

我们的验证函数不需要任何更改:

validate_epoch(linear_model)
  • 1
0.4157
  • 1

让我们把我们的小训练循环放在一个函数中,让事情变得更简单:

def train_model(model, epochs):
    for i in range(epochs):
        train_epoch(model)
        print(validate_epoch(model), end=' ')
  • 1
  • 2
  • 3
  • 4

结果与上一节相同:

train_model(linear_model, 20)
  • 1
0.4932 0.8618 0.8203 0.9102 0.9331 0.9468 0.9555 0.9629 0.9658 0.9673 0.9687
 > 0.9707 0.9726 0.9751 0.9761 0.9761 0.9775 0.978 0.9785 0.9785
  • 1
  • 2

fastai 提供了SGD类,默认情况下与我们的BasicOptim做相同的事情:

linear_model = nn.Linear(28*28,1)
opt = SGD(linear_model.parameters(), lr)
train_model(linear_model, 20)
  • 1
  • 2
  • 3
0.4932 0.852 0.8335 0.9116 0.9326 0.9473 0.9555 0.9624 0.9648 0.9668 0.9692
 > 0.9712 0.9731 0.9746 0.9761 0.9765 0.9775 0.978 0.9785 0.9785
  • 1
  • 2

fastai 还提供了Learner.fit,我们可以使用它来代替train_model。要创建一个Learner,我们首先需要创建一个DataLoaders,通过传入我们的训练和验证DataLoader

dls = DataLoaders(dl, valid_dl)
  • 1

要创建一个Learner而不使用应用程序(如cnn_learner),我们需要传入本章中创建的所有元素:DataLoaders,模型,优化函数(将传递参数),损失函数,以及可选的任何要打印的指标:

learn = Learner(dls, nn.Linear(28*28,1), opt_func=SGD,
                loss_func=mnist_loss, metrics=batch_accuracy)
  • 1
  • 2

现在我们可以调用fit

learn.fit(10, lr=lr)
  • 1
epochtrain_lossvalid_lossbatch_accuracytime
00.6368570.5035490.49558400:00
10.5457250.1702810.86604500:00
20.1992230.1848930.83120700:00
30.0865800.1078360.91118700:00
40.0451850.0784810.93277700:00
50.0291080.0627920.94651600:00
60.0225600.0530170.95534800:00
70.0196870.0465000.96221800:00
80.0182520.0419290.96516200:00
90.0174020.0385730.96761500:00

正如您所看到的,PyTorch 和 fastai 类并没有什么神奇之处。它们只是方便的预打包部件,使您的生活变得更轻松!(它们还提供了许多我们将在未来章节中使用的额外功能。)

有了这些类,我们现在可以用神经网络替换我们的线性模型。

添加非线性

到目前为止,我们已经有了一个优化函数的一般过程,并且我们已经在一个无聊的函数上尝试了它:一个简单的线性分类器。线性分类器在能做什么方面受到限制。为了使其更复杂一些(并且能够处理更多任务),我们需要在两个线性分类器之间添加一些非线性(即与 ax+b 不同的东西)——这就是给我们神经网络的东西。

这是一个基本神经网络的完整定义:

def simple_net(xb):
    res = xb@w1 + b1
    res = res.max(tensor(0.0))
    res = res@w2 + b2
    return res
  • 1
  • 2
  • 3
  • 4
  • 5

就是这样!在simple_net中,我们只有两个线性分类器,它们之间有一个max函数。

在这里,w1w2是权重张量,b1b2是偏置张量;也就是说,这些参数最初是随机初始化的,就像我们在上一节中所做的一样:

w1 = init_params((28*28,30))
b1 = init_params(30)
w2 = init_params((30,1))
b2 = init_params(1)
  • 1
  • 2
  • 3
  • 4

关键点是w1有 30 个输出激活(这意味着w2必须有 30 个输入激活,以便匹配)。这意味着第一层可以构建 30 个不同的特征,每个特征代表不同的像素混合。您可以将30更改为任何您喜欢的数字,以使模型更复杂或更简单。

那个小函数res.max(tensor(0.0))被称为修正线性单元,也被称为ReLU。我们认为我们都可以同意修正线性单元听起来相当花哨和复杂…但实际上,它不过是res.max(tensor(0.0))——换句话说,用零替换每个负数。这个微小的函数在 PyTorch 中也可以作为F.relu使用:

plot_function(F.relu)
  • 1

Jeremy 说

深度学习中有大量行话,包括修正线性单元等术语。绝大多数这些行话并不比我们在这个例子中看到的一行代码更复杂。事实是,学术界为了发表论文,他们需要让论文听起来尽可能令人印象深刻和复杂。他们通过引入行话来实现这一点。不幸的是,这导致该领域变得比应该更加令人生畏和难以进入。您确实需要学习这些行话,因为否则论文和教程对您来说将毫无意义。但这并不意味着您必须觉得这些行话令人生畏。只需记住,当您遇到以前未见过的单词或短语时,它几乎肯定是指一个非常简单的概念。

基本思想是通过使用更多的线性层,我们的模型可以进行更多的计算,从而模拟更复杂的函数。但是,直接将一个线性布局放在另一个线性布局之后是没有意义的,因为当我们将事物相乘然后多次相加时,可以用不同的事物相乘然后只相加一次来替代!也就是说,一系列任意数量的线性层可以被替换为具有不同参数集的单个线性层。

但是,如果我们在它们之间放置一个非线性函数,比如max,这就不再成立了。现在每个线性层都有点解耦,可以做自己有用的工作。max函数特别有趣,因为它作为一个简单的if语句运行。

Sylvain 说

数学上,我们说两个线性函数的组合是另一个线性函数。因此,我们可以堆叠任意多个线性分类器在一起,而它们之间没有非线性函数,这将与一个线性分类器相同。

令人惊讶的是,可以数学证明这个小函数可以解决任何可计算问题,只要你能找到w1w2的正确参数,并且使这些矩阵足够大。对于任何任意波动的函数,我们可以将其近似为一堆连接在一起的线条;为了使其更接近波动函数,我们只需使用更短的线条。这被称为通用逼近定理。我们这里的三行代码被称为。第一和第三行被称为线性层,第二行代码被称为非线性激活函数

就像在前一节中一样,我们可以利用 PyTorch 简化这段代码:

simple_net = nn.Sequential(
    nn.Linear(28*28,30),
    nn.ReLU(),
    nn.Linear(30,1)
)
  • 1
  • 2
  • 3
  • 4
  • 5

nn.Sequential创建一个模块,依次调用列出的每个层或函数。

nn.ReLU是一个 PyTorch 模块,与F.relu函数完全相同。大多数可以出现在模型中的函数也有相同的模块形式。通常,只需将F替换为nn并更改大小写。在使用nn.Sequential时,PyTorch 要求我们使用模块版本。由于模块是类,我们必须实例化它们,这就是为什么在这个例子中看到nn.ReLU

因为nn.Sequential是一个模块,我们可以获取它的参数,它将返回它包含的所有模块的所有参数的列表。让我们试一试!由于这是一个更深层的模型,我们将使用更低的学习率和更多的周期:

learn = Learner(dls, simple_net, opt_func=SGD,
                loss_func=mnist_loss, metrics=batch_accuracy)
  • 1
  • 2
learn.fit(40, 0.1)
  • 1

我们这里不展示 40 行输出,以节省空间;训练过程记录在learn.recorder中,输出表存储在values属性中,因此我们可以绘制训练过程中的准确性:

plt.plot(L(learn.recorder.values).itemgot(2));
  • 1

我们可以查看最终的准确性:

learn.recorder.values[-1][2]
  • 1
0.982826292514801
  • 1

在这一点上,我们有一些非常神奇的东西:

  • 给定正确的参数集,可以解决任何问题到任何精度的函数(神经网络)

  • 找到任何函数的最佳参数集的方法(随机梯度下降)

这就是为什么深度学习可以做出如此奇妙的事情。相信这些简单技术的组合确实可以解决任何问题是我们发现许多学生必须迈出的最大步骤之一。这似乎太好了,以至于难以置信——事情肯定应该比这更困难和复杂吧?我们的建议是:试一试!我们刚刚在 MNIST 数据集上尝试了一下,你已经看到了结果。由于我们自己从头开始做所有事情(除了计算梯度),所以你知道背后没有隐藏任何特殊的魔法。

更深入地探讨

我们不必止步于只有两个线性层。我们可以添加任意数量的线性层,只要在每对线性层之间添加一个非线性。然而,正如您将了解的那样,模型变得越深,实际中优化参数就越困难。在本书的后面,您将学习一些简单但非常有效的训练更深层模型的技巧。

我们已经知道,一个带有两个线性层的单个非线性足以逼近任何函数。那么为什么要使用更深的模型呢?原因是性能。通过更深的模型(具有更多层),我们不需要使用太多参数;事实证明,我们可以使用更小的矩阵,更多的层,获得比使用更大的矩阵和少量层获得更好的结果。

这意味着我们可以更快地训练模型,并且它将占用更少的内存。在 1990 年代,研究人员如此专注于通用逼近定理,以至于很少有人尝试超过一个非线性。这种理论但不实际的基础阻碍了该领域多年。然而,一些研究人员确实尝试了深度模型,并最终能够证明这些模型在实践中表现得更好。最终,出现了理论结果,解释了为什么会发生这种情况。今天,几乎不可能找到任何人只使用一个非线性的神经网络。

当我们使用与我们在第一章中看到的相同方法训练一个 18 层模型时会发生什么:

dls = ImageDataLoaders.from_folder(path)
learn = cnn_learner(dls, resnet18, pretrained=False,
                    loss_func=F.cross_entropy, metrics=accuracy)
learn.fit_one_cycle(1, 0.1)
  • 1
  • 2
  • 3
  • 4
时代训练损失验证损失准确性时间
00.0820890.0095780.99705600:11

近乎 100%的准确性!这与我们简单的神经网络相比有很大的差异。但是在本书的剩余部分中,您将学习到一些小技巧,可以让您自己从头开始获得如此出色的结果。您已经了解了关键的基础知识。 (当然,即使您知道所有技巧,您几乎总是希望使用 PyTorch 和 fastai 提供的预构建类,因为它们可以帮助您省去自己考虑所有细节的麻烦。)

术语回顾

恭喜:您现在知道如何从头开始创建和训练深度神经网络了!我们经历了很多步骤才达到这一点,但您可能会惊讶于它实际上是多么简单。

既然我们已经到了这一点,现在是一个很好的机会来定义和回顾一些术语和关键概念。

神经网络包含很多数字,但它们只有两种类型:计算的数字和这些数字计算出的参数。这给我们学习最重要的两个术语:

激活

计算的数字(线性和非线性层)

参数

随机初始化并优化的数字(即定义模型的数字)

在本书中,我们经常谈论激活和参数。请记住它们具有特定的含义。它们是数字。它们不是抽象概念,而是实际存在于您的模型中的具体数字。成为一名优秀的深度学习从业者的一部分是习惯于查看您的激活和参数,并绘制它们以及测试它们是否正确运行的想法。

我们的激活和参数都包含在 张量 中。这些只是正规形状的数组—例如,一个矩阵。矩阵有行和列;我们称这些为 维度。张量的维度数是它的 等级。有一些特殊的张量:

  • 等级-0:标量

  • 等级-1:向量

  • 等级-2:矩阵

神经网络包含多个层。每一层都是线性非线性的。我们通常在神经网络中交替使用这两种类型的层。有时人们将线性层及其后续的非线性一起称为一个单独的层。是的,这很令人困惑。有时非线性被称为激活函数

表 4-1 总结了与 SGD 相关的关键概念。

表 4-1. 深度学习词汇表

术语意义
ReLU对负数返回 0 且不改变正数的函数。
小批量一小组输入和标签,聚集在两个数组中。在这个批次上更新梯度下降步骤(而不是整个 epoch)。
前向传播将模型应用于某些输入并计算预测。
损失代表我们的模型表现如何(好或坏)的值。
梯度损失相对于模型某个参数的导数。
反向传播计算损失相对于所有模型参数的梯度。
梯度下降沿着梯度相反方向迈出一步,使模型参数稍微变得更好。
学习率当应用 SGD 更新模型参数时我们所采取的步骤的大小。

选择你的冒险 提醒

在你兴奋地想要窥探内部机制时,你选择跳过第 2 和第三章节了吗?好吧,这里提醒你现在回到第二章,因为你很快就会需要了解那些内容!

问卷调查

  1. 灰度图像在计算机上是如何表示的?彩色图像呢?

  2. MNIST_SAMPLE数据集中的文件和文件夹是如何结构化的?为什么?

  3. 解释“像素相似性”方法如何工作以对数字进行分类。

  4. 什么是列表推导?现在创建一个从列表中选择奇数并将其加倍的列表推导。

  5. 什么是秩-3 张量?

  6. 张量秩和形状之间有什么区别?如何从形状中获取秩?

  7. RMSE 和 L1 范数是什么?

  8. 如何才能比 Python 循环快几千倍地一次性对数千个数字进行计算?

  9. 创建一个包含从 1 到 9 的数字的 3×3 张量或数组。将其加倍。选择右下角的四个数字。

  10. 广播是什么?

  11. 度量通常是使用训练集还是验证集计算的?为什么?

  12. SGD 是什么?

  13. 为什么 SGD 使用小批量?

  14. SGD 在机器学习中有哪七个步骤?

  15. 我们如何初始化模型中的权重?

  16. 什么是损失?

  17. 为什么我们不能总是使用高学习率?

  18. 什么是梯度?

  19. 你需要知道如何自己计算梯度吗?

  20. 为什么我们不能将准确率作为损失函数使用?

  21. 绘制 Sigmoid 函数。它的形状有什么特别之处?

  22. 损失函数和度量之间有什么区别?

  23. 使用学习率计算新权重的函数是什么?

  24. DataLoader类是做什么的?

  25. 编写伪代码,显示每个 epoch 中 SGD 所采取的基本步骤。

  26. 创建一个函数,如果传递两个参数[1,2,3,4]'abcd',则返回[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]。该输出数据结构有什么特别之处?

  27. PyTorch 中的view是做什么的?

  28. 神经网络中的偏差参数是什么?我们为什么需要它们?

  29. Python 中的@运算符是做什么的?

  30. backward方法是做什么的?

  31. 为什么我们必须将梯度清零?

  32. 我们需要向Learner传递什么信息?

  33. 展示训练循环的基本步骤的 Python 或伪代码。

  34. ReLU 是什么?为值从-2+2绘制一个图。

  35. 什么是激活函数?

  36. F.relunn.ReLU之间有什么区别?

  37. 通用逼近定理表明,任何函数都可以使用一个非线性逼近得到所需的精度。那么为什么我们通常使用更多的非线性函数?

进一步研究

  1. 从头开始创建自己的Learner实现,基于本章展示的训练循环。

  2. 使用完整的 MNIST 数据集完成本章的所有步骤(不仅仅是 3 和 7)。这是一个重要的项目,需要花费相当多的时间来完成!您需要进行一些研究,以找出如何克服在途中遇到的障碍。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/662508
推荐阅读
相关标签
  

闽ICP备14008679号