Daniel-Bourke-PyTorch-深度学习笔记-全-

Daniel Bourke PyTorch 深度学习笔记(全)

1:欢迎来到PyTorch世界 🚀

在本节课中,我们将要学习什么是PyTorch,为什么它在当今的机器学习和人工智能领域如此重要,以及本课程将如何帮助你从零开始掌握这一强大的工具。


概述:什么是PyTorch?🤔

机器学习与数据科学已是当今世界最热门的领域,现在是时候更进一步了。欢迎来到学习PyTorch并成为深度学习大师的最佳殿堂。

如果你还不了解PyTorch,这里为你介绍:PyTorch是一个用Python代码编写的机器学习框架。它使你能够构建最先进的深度学习算法,例如神经网络,这些算法为当今许多人工智能应用提供动力。

这听起来很厉害,因为PyTorch目前正炙手可热。我不仅仅是指它的标志名称,它正被特斯拉等领先科技公司用于构建其自动驾驶汽车的计算机视觉系统,被Meta用于为其内容时间线提供内容筛选和理解系统,被苹果用于创建计算增强摄影,甚至被用于自动化除草拖拉机。

此外,许多最新的机器学习研究都是使用PyTorch代码完成和发表的。因此,懂得如何编写和阅读PyTorch代码意味着你将处于该领域的前沿。


学习方法:在实践中学习 💻

你知道最好的学习方式就是动手实践,对吗?那么,请将本课程视为一个PyTorch动力构建器。我们将以学徒制的方式,肩并肩、逐行地一起编写PyTorch代码,帮助你学习作为数据科学家或机器学习工程师每天都会用到的PyTorch技能。

我们将从最基础的知识开始,所以即使你是机器学习的新手,也能快速跟上进度。


课程路线图:我们将探索什么?🗺️

上一节我们介绍了PyTorch的基本概念,本节中我们来看看本课程将涵盖的核心内容。

我们将探索更高级的领域,包括:

  • PyTorch工作流程
  • PyTorch神经网络分类
  • 计算机视觉
  • 自定义数据集
  • 实验跟踪
  • 模型部署
  • 以及我个人最喜欢的:迁移学习——一种强大的技术,可以将一个机器学习模型在另一个问题上学习到的知识应用到你的问题上。

在此过程中,你将围绕一个名为“FoodVision”的总体项目构建三个里程碑式的项目。FoodVision是一个用于对食物图像进行分类的神经网络计算机视觉模型。

以下是这些里程碑项目将带来的好处:

  • 帮助你练习使用PyTorch,同时涵盖重要的机器学习概念。
  • 创建一个你可以展示给雇主的作品集,并告诉他们:“这就是我所做的。”

课程目标与讲师介绍 🎯

在本课程结束时,你将拥有丰富的PyTorch实践经验,并能自如地参与公共PyTorch项目、构建你自己的PyTorch项目,并在奇妙的机器学习世界中加速你的职业生涯。

差点忘了自我介绍,我是Daniel Bourke,你的PyTorch讲师。我是一名自学成才的机器学习工程师,曾在澳大利亚发展最快的人工智能公司之一工作。现在,我向全球成千上万的学生教授最新、最受欢迎的机器学习技能。

在本课程中,我们将一起学习掌握PyTorch。我希望你能够像机器学习工程师一样思考和处理机器学习问题。这样,当需要你作为一名机器学习工程师应用新技能时,你已经有了大量的实践。


总结:你准备好了吗?✨

本节课中我们一起学习了PyTorch的定义、其行业重要性以及本课程的结构和目标。

因为这就是我们的目标,对吗?如果你想学习机器学习职业所需的技能,如果你想学习被特斯拉和苹果等公司使用的最流行的机器学习框架,如果你喜欢通过实践来学习,如果你想掌握PyTorch,那么你来对地方了。

我们课程里见。让我们开始编码吧!

2:欢迎与深度学习简介 🚀

在本节课中,我们将学习深度学习的基本概念,了解它与机器学习和传统编程的区别,并为后续的实践操作打下基础。

概述

我是Daniel,欢迎来到PyTorch深度学习课程。本课程将重点介绍如何使用PyTorch进行深度学习。我们将从核心概念入手,但更侧重于动手实践和编写代码,而非仅仅停留在理论定义上。

什么是机器学习? 🤔

机器学习是将各种形式的数据(如图像、文本、数字表格、视频、音频文件)转化为计算机可以处理的数字,并从中寻找规律的过程。计算机通过机器学习算法或深度学习算法来完成这一任务,这些算法本质上是由代码和数学构成的。

本课程以代码为核心。我们将编写大量代码来实现各种功能,这些代码背后会触发相应的数学运算来发现数据中的规律。如果你想深入了解代码背后的数学原理,我会提供额外的学习资源链接。

机器学习 vs. 深度学习

为了更清晰地理解,我们来分解一下这些概念。人工智能是一个广泛的领域,机器学习是它的一个子集,而深度学习又是机器学习的一个分支。

我们将在本课程中使用PyTorch,专注于编写深度学习代码。当然,PyTorch也可用于许多其他机器学习任务。实际上,我经常交替使用“机器学习”和“深度学习”这两个术语。虽然机器学习范畴更广,深度学习更具特定性,但本课程的重点不在于精确定义,而在于理解它们如何运作。

传统编程与机器学习范式

如果你熟悉机器学习的基础知识,可能了解这个范式,但我们还是简要回顾一下。

在传统编程中,我们通过编写明确的规则来处理输入并产生输出。例如,要编写一个能复现祖母著名烤鸡菜肴的程序,我们需要输入食材(如蔬菜和鸡肉),并编写一系列明确的规则(如切菜、给鸡肉调味、预热烤箱、烤制30分钟、加入蔬菜)。这些输入与规则相结合,最终产出烤鸡这道菜。

相比之下,典型的机器学习算法接收一些输入和期望的输出,然后自行找出连接输入与输出的规则(即规律)。在传统编程中,我们需要手动编写所有规则;而理想的机器学习算法会自动构建从输入到理想输出之间的桥梁。

在机器学习领域,这通常被称为监督学习,因为你会有一些带有对应输出的输入(也称为特征和标签)。机器学习算法的任务就是找出输入(特征)与输出(标签)之间的关系。

例如,要编写一个机器学习算法来学习祖母的烤鸡食谱,我们需要收集大量食材(输入)和对应的成品照片(输出),然后让算法尝试找出从食材到成品的转换方法。

以上就是传统编程与机器学习在定义上的主要区别。我们将在整个课程中动手编写这类算法。

为什么使用机器学习或深度学习?

在进入下一个视频之前,请思考一个问题:与传统编程相比,你为什么会想使用机器学习算法?

回想一下我们刚刚看到的传统编程与机器学习的范式区别。如果需要手动编写所有规则,是否会变得非常繁琐?请思考这个问题,我们将在下一个视频中探讨答案。

总结

本节课我们一起学习了深度学习的基本定位,了解了机器学习是将数据转化为数字并寻找其规律的过程,并明确了深度学习是机器学习的一个子集。我们还对比了传统编程(手动定义规则)机器学习(算法从数据中学习规则) 的核心范式差异,为后续的实践课程做好了准备。

3:003_01_003 加入我们的在线课堂!🎓

在本节课中,我们将探讨如何通过加入在线学习社区来提升课程完成率与学习效果。我们将介绍一个包含三个步骤的行动计划,帮助你建立学习责任感,并融入一个积极互助的学习环境。


大多数学习者难以独自完成在线课程。这是因为在家学习的舒适环境缺少了一个关键要素:责任感。停止去实体学校上学比停止观看在线课程要困难得多。

然而,有一种方法可以确保你完成课程,无论课程变得多难。我们教授的正是这些因难以掌握而极具价值的技能。完成课程意味着掌握这些技能,否则便是浪费你的投入。

为了显著提高你的成功几率,你需要采取一个可能让你略感不适但极其有效的行动:加入我们的在线课堂

在下一讲中,我将提供一个仅供ZTM学员使用的私密链接。在那里,你需要完成以下三件事:

以下是三个关键步骤:

  1. 自我介绍:在社区中介绍自己。你可以自由选择分享多少个人信息。同时,设定一个目标:“我将在 [此处填写日期] 前完成X课程”,并将这个日期记入你的日历。
  2. 寻找学习伙伴:前往“责任伙伴”频道,寻找一位目前正在同时开始学习ZTM课程的人(课程不必相同)。你们可以结成多个伙伴。目标是确保彼此都能完成课程和设定的目标。
  3. 参与社区交流:在通用聊天室中畅所欲言。此外,还有针对特定课程的频道供你讨论。置身于志同道合的学习者中,能帮助你保持动力,找到班级的归属感。

在社区中,我们还将定期举办由我本人、其他ZTM讲师和学员参与的线上聚会。我建议你在学习过程中保持Discord服务器开启。

这不是推销,而是学习的基本事实:在群体中学习、被其他学习者环绕、拥有责任感,你更有可能成功

最后,在整个学习过程中,当你遇到问题或挑战时,请不要放弃。回到我们的Discord服务器提问并寻求帮助。但更重要的是,当你开始掌握技能并成长时,请回到这里帮助其他学员。教授和帮助他人是巩固所学知识的最佳方法之一

如果你能围绕学习建立这样的例行程序——打开Discord、开始听课、让自己置身于拥有相似目标的人群中——你将拥有完成课程所需的责任感和支持系统。我向你保证。

还有一个有趣的环节:当你完成课程后,你可以在“校友”频道发布你的结业证书,成为校友。社区中表现突出的成员甚至有机会成为明星导师。

这一切听起来或许有些傻,你可能会说自己不需要任何人。但请相信我们多年的经验:那些完成课程、找到工作并在职业生涯中取得成功的学习者,往往都遵循了这一模式。有时,成功需要你去做一些不那么舒适的事情


本节课中,我们一起学习了如何通过加入在线学习社区来克服独自学习的挑战。我们介绍了通过自我介绍、寻找学习伙伴和积极参与讨论来建立责任感和支持系统的具体方法。记住,在群体中学习能显著提高你的成功率。我们下一讲再见,也期待在社区中见到你!

4:ZTM社区资源指南 🎁

在本节课中,我们将了解作为ZTM学员可以获得的额外免费资源和福利。这些资源旨在帮助你更好地学习、实践、建立联系并紧跟技术潮流。

上一节我们介绍了课程的基本结构,本节中我们来看看ZTM为学员提供的丰富社区与学习资源。

社区与活动

作为ZTM学员,你将加入一个庞大的技术爱好者和学习者社区。社区提供了多种互动和练习机会。

以下是社区提供的主要活动与资源:

  • 编程挑战:你可以在官网的“社区”板块找到编程挑战,用于练习和巩固技能。
  • 开源项目:ZTM学员可以参与为社区成员开放的开源项目。
  • Discourse论坛:我们拥有Discourse服务器,供学员交流讨论。
  • 线上校园活动:这是一个虚拟直播课堂,你可以在这里结识其他学员并进行交流。
  • 年度活动:我们全年会举办诸如“Advent of Code”或“Hacktoberfest”等活动。

学习辅助材料

除了社区活动,ZTM还提供了系统的学习辅助工具,帮助你更高效地掌握知识。

以下是主要的学习辅助材料:

  • 速查表:这些速查表完全免费,无需注册即可使用。你可以打开与你所学课程相关的速查表(例如JavaScript速查表)进行对照学习或记笔记。我们每月都会新增一份速查表,你可以根据兴趣随时查看。
  • 技术博客:我们的博客每周会发布2-3篇博文,内容涵盖广泛,包括面向初学者的教程、行业深度文章、求职技巧以及不同讲师的观点分享等。博客设有不同标签,如“初学者入门”、“职业进阶”等,方便你按需阅读。

行业动态与职业发展

为了帮助你从海量信息中高效获取行业精华,并规划职业路径,ZTM提供了以下专项资源。

以下是相关的资讯与规划工具:

  • 月度行业简报:每月,我和其他ZTM讲师会撰写行业月度回顾,总结当月重要动态、推荐最佳阅读资源并提炼核心要点,将信息消化为一篇博文。目前我们提供Web开发者月度简报Python月度简报机器学习月度简报区块链开发者月度简报,未来还会增加更多领域。
  • 职业路径指南:如果你在寻找特定的职业方向,这个工具会为你提供建议,包括学习路径、求职时机以及各职业所需的技能水平。该网站同样免费,无需注册。

社交与额外渠道

最后,ZTM还建立了社交平台和视频渠道,以拓展你的职业网络并获取更多免费学习内容。

以下是两个重要的社交与学习渠道:

  1. LinkedIn群组:我们为所有学员建立了LinkedIn群组。如果你想完善LinkedIn档案、与其他学员互荐技能、获取职业建议,这里是一个绝佳的起点。群组内经常分享职位空缺、职业技巧,并互相进行技能认可。
  2. YouTube频道:我们最近启动了名为“0 to Mastery”的YouTube频道,每周发布3-4个免费视频,涵盖各种主题。你可以随时查看,未来一年还会有更多内容上线。

本节课中我们一起学习了作为ZTM学员所能享有的全部额外资源,包括活跃的社区、实用的学习工具、前沿的行业资讯以及拓展职业网络的渠道。请善用这些资源,祝你在学习之旅中一切顺利,再次欢迎加入ZTM!

5:为何使用机器学习或深度学习?🤔

在本节课中,我们将探讨为何要选择使用机器学习或深度学习,而不是传统的编程方法。我们将通过具体例子来理解其适用场景,并介绍一条重要的指导原则。


欢迎回来。在上一节视频中,我们简要介绍了传统编程与机器学习之间的区别。

同样,我不想在定义上花费太多时间。我更希望你能在实践中看到这些概念。

我留给你一个问题:为何要使用机器学习或深度学习?

让我们思考一个充分的理由。为什么不呢?我的意思是,如果我们每次都必须编写所有那些手写规则来复现艾丽西娅祖母的烤鸡菜肴,那将相当繁琐,对吧?


让我们就此划清界限。为什么不呢?一个更好的理由,正如我们刚才所说,是针对复杂问题。你能想出所有规则吗?

让我们想象一下,我们正试图制造一辆自动驾驶汽车。如果你学过开车,你可能花了大约20小时或100小时。但现在我给你一个任务:写下关于驾驶的每一条规则。

  • 你如何从自家车道倒车出来?
  • 你如何左转并驶入街道?
  • 你如何进行倒车入库?
  • 你如何在十字路口停车?
  • 你如何知道去某处该开多快?

我们刚刚列出了半打规则。但你很可能还能想出更多,甚至可能达到数千条。

因此,对于像驾驶这样的复杂问题,你能想出所有规则吗?很可能不能。

这就是机器学习和深度学习发挥作用的地方。

这是一条我很喜欢分享的、来自我某个YouTube视频下的精彩评论。

这是我的2020年机器学习路线图。这条评论来自Yahuwi(我可能读错了)。Yahuwi说:“我认为你可以将机器学习(顺便说一下,我将在课程中频繁使用ML这个缩写,ML即机器学习)用于几乎任何事情,只要你能将其转化为数字。” 这正是我们之前所说的:机器学习是将事物转化为计算机可读的数字,然后编程让其寻找模式,只不过在机器学习算法中,通常是我们编写算法,然后由算法(而不是我们)来发现模式。

因此,从字面上看,它可以是任何事物。宇宙中的任何输入或输出。这正是机器学习非常酷的一点。

但是,仅仅因为它能用于任何事情,你就应该总是使用它吗?

接下来,我想向你介绍谷歌的机器学习第一法则。


如果你能构建一个简单的基于规则的系统,例如我们之前提到的、将食材映射到西西里祖母烤鸡菜肴的那五个步骤规则,如果你能仅用五个步骤就做到这一点,并且每次都能奏效,那么你很可能应该那么做。

所以,如果你能构建一个不需要机器学习的简单规则系统,那就去做。

当然,也许问题并不那么简单,但也许你仍然可以编写一些规则来解决你正在处理的问题。

这是一位睿智的软件工程师提出的,我之前也暗示过这一点。这是谷歌机器学习手册的第一条法则。我强烈建议你通读它,但我们不会在本视频中详细讨论。你可以去搜索一下,或者链接会在你获取资源的地方提供。

请记住这一点:尽管机器学习非常强大、有趣且令人兴奋,但这并不意味着你应该总是使用它。我知道在深度学习与机器学习课程的开头说这个有点特别,但我只是希望你记住:简单的基于规则的系统仍然很好。机器学习并非解决一切问题的万能药。

现在,让我们看看深度学习擅长什么。但我要留一个悬念作为家庭作业,因为我们将在下一个视频中探讨这个问题。

下课。

6:机器学习首要法则与深度学习适用场景 🎯

在本节课中,我们将学习机器学习的首要法则,并探讨深度学习适合与不适合的应用场景。理解这些原则有助于我们在实际项目中做出更明智的技术选择。

概述

上一节我们介绍了机器学习的基本概念。本节中,我们将深入了解谷歌提出的机器学习首要法则,并分析深度学习技术最适合解决哪些类型的问题,以及哪些情况下应避免使用它。

机器学习首要法则

谷歌提出的首要法则是:如果你不需要使用机器学习,就不要使用它

这意味着,在考虑采用机器学习或深度学习解决方案之前,应首先评估传统方法是否足够。只有当传统方法无法有效解决问题时,才应考虑引入更复杂的学习算法。

深度学习的适用场景

以下是深度学习技术通常能发挥优势的几种情况:

规则列表冗长的问题
当传统方法失效时,深度学习可能提供帮助。传统方法通常涉及:输入数据 -> 编写一系列规则处理数据 -> 得到已知输出。然而,对于规则极其复杂的问题,例如驾驶汽车的规则(可能涉及成百上千甚至数百万条规则),编写和维护这样的规则列表将非常困难。这正是当前自动驾驶领域广泛采用机器学习和深度学习作为前沿解决方案的原因。

环境持续变化的问题
深度学习的一个优势在于其持续学习的能力。模型可以适应并学习新的场景。例如,如果你更新了模型训练所用的数据,模型能够调整以适应未来不同类型的数据。这类似于驾驶汽车:你可能非常熟悉自己的社区,但当你前往一个从未去过的地方时,虽然可以依赖已知的驾驶基础,但仍需适应新的速度限制、停车位置等具体情况。

拥有大型数据集的问题
深度学习在当今科技世界中蓬勃发展,很大程度上得益于海量数据的存在。例如,Food-101数据集包含了101种不同食物的图像。试想一下,如果要构建一个能识别101种不同食物的应用程序,编写区分它们的规则列表将会极其冗长。仅以香蕉为例,你不仅需要编码香蕉的外观特征,还需要编码所有“非香蕉”事物的特征。对于这类问题,让模型从数据中自动学习模式比手动编写规则更为高效。

综上所述,深度学习擅长处理:规则列表冗长的问题、环境持续变化的问题,以及从海量数据集中发现洞察的问题

深度学习的不适用场景

需要注意的是,深度学习并非万能钥匙。以下情况通常不适合采用深度学习(这里使用“通常”一词,因为具体问题需要具体分析,且技术也在不断发展):

需要可解释性的场景
深度学习模型学习到的模式,通常体现为大量称为“权重”和“偏置”的数字(我们后续会详细讨论)。这些模式对人类而言通常是难以解释的。有些模型拥有数百万、数千万甚至数万亿个参数(参数即模型从数据中学到的数字模式)。理解一个涉及数百万个数字的列表是极其困难的。

传统方法是更好选择的场景
这再次呼应了机器学习的首要法则。如果你能用简单的基于规则的系统实现目标,那么可能根本不需要机器学习或深度学习。

错误不可接受的场景
深度学习模型的输出并不总是完全可预测的。它们是概率性的,意味着模型进行预测时,实际上是在做一个概率性的“赌注”。而基于规则的系统,其输出每次都是确定可知的。因此,如果应用场景无法承受由概率性错误带来的风险,那么可能应该回归使用简单的规则系统。

数据量不足的场景
深度学习模型通常需要相当大量的数据才能产生优异的结果。当然,这里也有例外,存在一些技术可以在数据量不大时也能取得良好效果,我们后续会看到。

需要强调的是,以上列举的是“通常”情况。例如,你可以研究“深度学习可解释性”领域来寻找提高模型透明度的技术;也可以查阅具体案例来比较机器学习与深度学习的适用性;通过大量测试,也能在一定程度上确保模型的可靠性和可重复性。技术是不断发展的,保持开放的心态很重要。

总结与过渡

本节课中,我们一起学习了机器学习的首要法则,并探讨了深度学习技术适用与不适用的主要场景。关键在于根据具体问题的特性(如规则复杂性、环境变化性、数据规模、可解释性要求和容错率)来权衡技术选型。

下一节,我们将进一步对比机器学习与深度学习,并依据所有处理的数据类型,来了解不同的机器学习问题领域。我们将在下个视频中详细介绍这些丰富多彩的内容。

7:机器学习与深度学习对比 🤖🧠

在本节课中,我们将要学习机器学习与深度学习之间的核心区别。我们将探讨它们各自适用的数据类型、常用算法,并理解为何在某些场景下选择一种方法优于另一种。

上一节我们介绍了深度学习的适用场景与不适用场景。本节中我们来看看机器学习与深度学习之间的具体对比。

结构化数据与传统机器学习 📊

传统机器学习算法通常适用于结构化数据。结构化数据指以行和列组织的数据,例如表格数据。

以下是处理结构化数据的常用算法:

  • 随机森林
  • 梯度提升机(例如 XGBoost
  • 朴素贝叶斯
  • K-近邻算法
  • 支持向量机(SVM)

在这些算法中,XGBoost 是处理结构化数据时备受青睐的算法,常见于数据科学竞赛和生产环境。

非结构化数据与深度学习 🌌

深度学习则通常更擅长处理非结构化数据。非结构化数据没有固定的行列结构,形式多样。

常见的非结构化数据类型包括:

  • 自然语言文本(如社交媒体帖子、维基百科文章)
  • 图像数据
  • 音频文件

处理这类数据,通常会使用某种神经网络

深度学习常用算法 🧬

深度学习之所以称为“深度”,是因为其模型可以包含许多层。常见的神经网络类型包括:

  • 全连接神经网络
  • 卷积神经网络(CNN)
  • 循环神经网络(RNN)
  • 变换器(Transformer)

神经网络的魅力在于,其架构几乎可以针对不同问题无限变化和组合。

选择之道:艺术与科学的结合 🎨🔬

需要明确的是,机器学习与深度学习的应用边界并非绝对。根据具体问题的表征方式,上述许多算法可以交叉使用。选择何种方法,部分是科学,部分是艺术。

本节课中我们一起学习了机器学习与深度学习的核心区别:传统机器学习算法(如XGBoost)通常更适用于结构化数据,而深度学习神经网络则更擅长处理非结构化数据。同时,我们也了解到在实际应用中,根据问题灵活选择最佳方法至关重要。

8:神经网络结构解析 🧠

在本节课中,我们将要学习神经网络的基本结构。我们将从定义神经网络开始,逐步解析其组成部分,并了解数据如何在其中流动。通过本课程,你将掌握神经网络的核心概念及其工作原理。


欢迎回来。在上一个视频中,我留下了一个悬念问题:什么是神经网络?我建议你去搜索这个问题。但你可能已经搜索过了。现在,我们一起搜索一下。

如果我输入“什么是神经网络”,我已经搜索过了。什么是神经网络?解释神经网络。神经网络定义。网上有数百种类似的定义。五分钟了解神经网络。3Blue1Brown。我强烈推荐该频道关于神经网络的系列视频,这将在课外资源中提及。StatQuest 也非常棒。

这里有数百种不同的定义。你可以阅读其中10个、5个或3个,然后形成自己的定义。但为了本课程,我将这样定义神经网络。

我们有一些数据,可能是食物图像、推文或自然语言,也可能是语音。这些是非结构化数据的输入示例,因为它们不是行和列。这些是我们拥有的输入数据。

那么,我们如何在神经网络中使用它们呢?在数据可用于神经网络之前,需要将其转换为数字。人类喜欢看拉面和意大利面的图片。我们知道那是拉面,那是意大利面,看过一两次后就能识别。我们喜欢阅读好的推文,喜欢听美妙的音乐或听朋友在音频文件中说话。

然而,在计算机理解这些输入内容之前,需要将其转换为数字。这就是我所说的数值编码或表示。这个数值编码用方括号表示它是矩阵或张量的一部分,我们将在本课程中深入实践。

我们有了输入,将其转换为数字,然后将其传递给神经网络。这是神经网络的图形表示。然而,神经网络的图形表示可能会相当复杂。但它们都代表相同的基本原理。

例如,在这个图形中,我们有一个输入层,然后有多个隐藏层。你可以根据需要设计这些层。然后我们有一个输出层。因此,我们的输入将以某种数据形式进入。隐藏层将对输入执行数学运算,即对数字进行操作。然后我们将得到输出。

3Blue1Brown 的“从零开始的神经网络”视频非常棒,强烈推荐你观看。但回到这里。

我们有了输入,将其转换为数字,并将其输入神经网络。这通常是输入层、隐藏层。你可以根据需要设置任意多个不同的层。每个小点称为一个节点。这里有很多信息,但我们将通过实践来了解其外观。

然后我们得到某种输出。你应该使用哪种神经网络?你可以为问题选择适当的神经网络,这可能涉及手动设计每个步骤,或者你可以找到在类似问题上有效的神经网络。例如,对于图像,你可能使用卷积神经网络(CNN);对于自然语言,你可能使用Transformer;对于语音,你可能也使用Transformer。但基本上,它们都遵循输入、操作、输出的相同原则。

因此,神经网络将自行学习一种表示。我们不会定义它学习什么。它将以某种方式操纵这些模式。当我说学习表示时,我也将其称为学习数据中的模式。很多人称之为特征。特征可能是单词“do”通常跟在“how”之后,跨越多种语言。特征几乎可以是任何东西。同样,我们不定义这个,神经网络自行学习这些表示、特征,也称为权重。

然后我们从那里去哪里?我们有一些数字,数值编码将数据转换为数字。我们的神经网络学习了一种它认为最能代表数据模式的表示。然后它输出这些表示输出,我们可以使用它们。你经常会听到这些被称为特征、权重矩阵、权重张量,学习表示也是另一个常见术语。这些东西有很多不同的术语。

然后,它将输出。我们可以将这些输出转换为人类可理解的输出。例如,如果我们看这些,这些可能是表示或模式。神经网络单独可以有数百万个数字,这里只有9个。想象一下,如果这些是数百万个不同的数字,我几乎无法理解这里的九个数字。因此,我们需要一种方法将这些转换为人类可理解的术语。

对于这个例子,我们可能有一些输入数据,即食物图像,然后我们希望神经网络学习拉面图像和意大利面图像之间的表示,最终我们将获取它学习的模式,并将其转换为它认为这是拉面图像还是意大利面图像。对于这条推文,它是自然灾害推文还是非自然灾害推文。

因此,我们的神经网络已经,我们已经编写代码将其转换为数字,通过神经网络传递。神经网络学习了一些模式。然后我们理想地希望它将这条推文表示为非灾害。然后我们可以编写代码执行这里的每个步骤。对于语音输入也是如此,将其转换为你可能对智能扬声器说的话,但我不说,因为我的许多设备可能会启动。

现在,让我们介绍神经网络的解剖结构。我们已经暗示过这一点,但这是神经网络解剖101。同样,这个东西实际上是高度可定制的。我们稍后将在PyTorch代码中看到它,但数据进入输入层,在这种情况下,单元/神经元/节点的数量是2。

隐藏层,你可以有多个。我在这里加了一个“S”,因为你可以有一个隐藏层,而深度学习中的“深度”来自于拥有许多层。这里只显示了四层。你可能有很多层,例如ResNet-152有152个不同的层。所以,你可以有很多隐藏层。这里我们只画了一个。在这种情况下,有三个隐藏单元/神经元。然后我们有一个输出层。因此,输出学习表示或预测概率,取决于我们如何设置。我们稍后会看到这些是什么。在这种情况下,它有一个隐藏单元。所以是2个输入,3个隐藏,1个输出。你可以自定义这些的数量,自定义有多少层,自定义输入什么,自定义输出什么。

现在,如果我们讨论整体架构,这是描述所有层的组合。所以当你听到神经网络架构时,它指的是输入层、隐藏层(可能不止一个)和输出层。这是整体架构的术语。

我说模式是一个任意术语,你可能听到嵌入、权重、特征表示、特征向量,都指类似的东西。所以,我们如何将数据转换为某种数值形式?构建一个神经网络来找出模式,输出我们想要的期望输出。

现在,更技术性地说,每一层通常是线性和非线性函数的组合。线性函数是直线,非线性函数是非直线。如果我让你用无限直线和非直线画任何你想要的东西,你可以使用直线或曲线。你能画出什么样的模式?在基本层面上,这基本上就是神经网络所做的。它使用直线和非直线的组合来绘制数据中的模式。我们稍后会看到这是什么样子。

在下一个视频中,让我们简要介绍不同类型的学习。我们已经研究了神经网络是什么,整体算法,但神经网络学习也有不同的范式。我们下个视频见。

9:不同类型的学习范式 🧠

在本节课中,我们将要学习机器学习与深度学习中的几种核心学习范式。理解这些范式是构建有效模型的基础。

上一节我们简要介绍了神经网络的基本结构,本节中我们来看看不同类型的学习范式。

概述:主要学习范式

机器学习主要包含以下几种学习范式:

  • 监督学习
  • 无监督学习与自监督学习
  • 迁移学习
  • 强化学习(作为延伸了解)

1. 监督学习 📊

监督学习是指你同时拥有数据标签

以下是监督学习的两个例子:

  • 烹饪案例:在之前构建神经网络学习西西里祖母烤鸡食谱规则的例子中,数据是生食材(蔬菜、鸡肉),标签是这些食材最终应该呈现的理想状态。
  • 图像分类案例:在区分猫狗照片的任务中,你拥有1000张猫的照片和1000张狗的照片,并且你知道每张照片对应的类别。你将照片(数据)和类别(标签)一同提供给机器学习算法进行学习。

因此,监督学习的核心公式可表示为:算法学习从 数据标签 的映射关系

2. 无监督学习与自监督学习 🧩

无监督学习与自监督学习是指你只有数据本身,而没有标签

以下是这种范式的一个例子:

  • 同样在猫狗照片的例子中,如果你只有一大堆照片,但没有“猫”或“狗”的标签,这就是无监督/自监督学习的场景。

在这种范式下,算法旨在学习数据内在的表征。所谓“表征”,指的是数据中的模式、特征、权重等,它们通常以数字形式表示。

一个自监督学习算法可以找出狗和猫图像之间的基本模式,但它最初并不知道这些模式分别对应“狗”和“猫”的概念。之后,你可以查看它学到的模式,并手动进行解释:“这种模式看起来是狗,那种模式看起来是猫”。

简而言之,无监督和自监督学习仅基于数据本身进行学习

3. 迁移学习 🔄

迁移学习是深度学习中一个非常重要的范式。

迁移学习是指将一个模型从数据中学到的模式,迁移到另一个模型中

以下是一个迁移学习的例子:

  • 假设我们要构建一个区分猫狗照片的监督学习模型。我们可以从一个已经在大量图像上学习过通用模式的模型(例如,一个预训练的图像识别模型)开始,将这些基础模式迁移到我们自己的模型中。这样,我们的模型就获得了一个“先发优势”,可以更快、更好地学习特定任务。

迁移学习是一种非常强大的技术。在本课程中,我们将主要编写代码来专注于监督学习迁移学习,因为它们是机器学习和深度学习中最常见的两种范式。当然,我们所编写的代码风格也可以适应其他学习范式。

4. 强化学习(延伸阅读)🎮

还有一种未被归入上述类别、自成一体的范式,那就是强化学习

以下是强化学习的基本框架:

  • 强化学习涉及一个环境和一个在该环境中执行动作智能体。智能体根据其动作从环境获得奖励观察结果。

例如,如果你想训练你的狗在室外小便,你会在它在室外小便时给予奖励,而在它弄脏沙发时不给予奖励(或给予惩罚)。这就是强化学习的思路。

强化学习与其他范式有所不同。建议你在课后自行深入研究一下不同的学习范式。

总结与挑战 💡

本节课我们一起学习了机器学习的几种核心范式:需要数据与标签的监督学习、仅从数据中寻找模式的无监督/自监督学习、以及复用已有知识的迁移学习。我们还简要了解了基于奖励机制的强化学习

在进入下一视频之前,给你一个挑战:请自行搜索“深度学习目前有哪些实际应用?”,并列出一些你自己的发现和想法。试试看吧,我们下个视频见!

10:深度学习的应用领域 🚀

在本节课中,我们将探讨深度学习在实际生活中的各种应用。我们将了解这项技术如何影响我们的日常生活,并理解其背后的核心概念。

上一节我们介绍了深度学习的基础,本节中我们来看看深度学习具体能用来做什么。

概述

深度学习是机器学习的一个分支,它能够处理大量数据并从中学习复杂的模式。只要能将问题转化为数字并找到其中的规律,深度学习几乎可以应用于任何领域。

深度学习的核心原则

记住机器学习的第一原则:如果不需要,就不要使用它。但如果使用,它几乎可以应用于任何事物。

一个重要的观点是:只要你能将某物编码成数字,你就有可能构建一个机器学习算法来发现这些数字中的模式。它能否成功?这既是科学,也是艺术。

具体应用案例

以下是深度学习在日常生活中的一些常见应用,这些也是我亲身接触的例子。

  • 推荐系统:例如YouTube的推荐页面,它由深度学习驱动,能够根据你的观看历史推荐视频。
  • 机器翻译:近十年的翻译质量显著提升,这同样得益于深度学习技术。
  • 语音识别:当你使用语音助手或点击翻译软件的发声按钮时,背后是深度学习的语音识别技术。
  • 计算机视觉:这项技术可用于目标检测,例如在监控摄像头中自动识别并框出特定物体(如车辆)。
  • 自然语言处理:用于分析非结构化文本,例如你邮箱中高效的垃圾邮件过滤器,它通过学习邮件内容模式来区分正常邮件和垃圾邮件。

问题类型分类

根据输入和输出的形式,我们可以将上述应用归类。

  • 序列到序列:输入一个序列,输出另一个序列。例如,语音识别(音频波序列到文本序列)和机器翻译(一种语言序列到另一种语言序列)。
  • 分类与回归
    • 分类:预测某个事物属于哪个类别。例如,垃圾邮件检测(“垃圾邮件”或“非垃圾邮件”)。
    • 回归:预测一个连续数值。例如,在目标检测中预测边界框的坐标位置(像素值)。

总结

本节课我们一起学习了深度学习的广泛应用,从推荐系统、翻译、语音识别到计算机视觉和自然语言处理。我们还了解了这些应用背后对应的问题类型:序列到序列、分类和回归。现在我们已经为课程打下了基础,接下来,让我们开始正式探讨PyTorch。

11:PyTorch简介与优势 🚀

在本节课中,我们将要学习PyTorch的基础知识,了解它是什么以及为什么它在深度学习领域如此受欢迎。


什么是PyTorch?🤔

首先,我们来介绍PyTorch的基础知识。你可能会问,PyTorch是什么?当然,我们可以直接访问互联网,查看PyTorch的官方网站。

这是PyTorch的主页。本课程并不能替代主页上的所有内容。这个网站应该是你学习PyTorch的权威参考。你可以从这里开始。

你可以在网站上看到一个庞大的生态系统。这里有在本地计算机上设置PyTorch的方法、各种资源、文档、GitHub仓库、搜索功能、博客等一切内容。在学习本课程并编写PyTorch代码的过程中,这个网站应该是你访问最多的地方。你可以来这里阅读资料、查阅信息、查看示例。

但为了本课程的目的,让我们来分解一下PyTorch。

PyTorch的核心定义

PyTorch是最流行的研究型深度学习框架。它允许你用Python编写快速的深度学习代码。如果你了解Python,就会知道它是一种非常用户友好的编程语言。PyTorch使我们能够用Python编写最先进的深度学习代码,并利用GPU进行加速。

它让你能够访问TorchHub上的许多预构建深度学习模型。TorchHub是一个网站,如果你还记得,我说过迁移学习是一种利用其他深度学习模型来增强我们自己模型的方法,TorchHub就是实现这一点的资源库。Torchvision模型也是如此,我们将在整个课程中学习它。

PyTorch为机器学习的整个流程提供了一个生态系统:从数据预处理开始,将数据转换为张量。例如,如果你有一些图像,如何将它们表示为数字?然后,你可以构建模型(如神经网络)来对这些数据进行建模。最后,你甚至可以将模型部署到你的应用程序或云中。

云部署的具体方式取决于你使用的云平台,但通常都会运行某种PyTorch模型。

PyTorch最初由Facebook(现在已更名为Meta)内部设计和使用。但它现在是开源的,并被特斯拉、微软和OpenAI等公司使用。

PyTorch的流行度 📈

当我说PyTorch是最流行的深度学习研究框架时,不要只听我的一面之词。让我们看看Papers with Code网站的趋势。如果你不确定Papers with Code是什么,它是一个追踪最新、最优秀的机器学习论文及其是否附带代码的网站。

这里还有其他一些深度学习框架:PyTorch、TensorFlow、JAX、MXNet、PaddlePaddle,以及原始的Torch。PyTorch是用Python编写的Torch的进化版本。

如果我们查看数据(截至2021年12月),PyTorch以58%的占比遥遥领先,是用于编写最先进机器学习算法代码的最受欢迎的研究型机器学习框架。


Papers with Code是一个很棒的网站,涵盖了语义分割、图像分类、目标检测、图像生成、计算机视觉、自然语言处理、医学等领域的论文。我建议你自己探索一下,这是我保持对该领域了解最喜欢的资源之一。

正如你所见,在该网站追踪的65,000篇附带代码的论文中,有58%是用PyTorch实现的。这非常酷,而这正是我们要学习的内容。

为什么选择PyTorch?💡

除了我们刚刚谈到的原因(它是研究人员的首选),还有更多理由。



如果一篇机器学习论文发表了优秀的研究成果,通常会附带代码,你可以访问并使用这些代码进行自己的应用或研究。

再次强调为什么选择PyTorch?这是François Chollet(另一个流行深度学习框架Keras的作者)的一条推文。他提到,借助像Colab这样的工具(我们稍后会看到Colab是什么),加上Keras、TensorFlow(我在这里补充了)和PyTorch,现在几乎任何人都可以在一天内,无需初始投资,解决那些在2014年需要一个工程师团队工作一个季度并花费2万美元硬件才能解决的问题。这突显了深度学习和机器学习工具领域已经变得多么强大。

Colab、Keras和TensorFlow都非常出色,现在PyTorch也加入了这个行列。如果你想了解更多,可以在Twitter上关注François Chollet,他是机器学习领域非常杰出的声音。





如果你需要更多选择PyTorch的理由,看看这些。看看所有使用PyTorch的地方,它无处不在。

特斯拉的人工智能总监Andrej Karpathy在这里。特斯拉正在使用PyTorch为其自动驾驶的计算机视觉模型提供支持。

例如,像这样检测场景中情况的汽车。当然,还会有其他用于路径规划的代码。

OpenAI是世界上最大的人工智能研究机构之一(“开放”在于他们发布了许多研究方法)。他们在2020年1月的一篇博客文章中提到,OpenAI现已全面标准化使用PyTorch。

还有一个名为“Incredible PyTorch”的代码仓库,收集了大量基于PyTorch构建的不同项目。PyTorch的美妙之处在于你可以在其基础上进行构建。

PyTorch也被用于农业等领域。例如,农业机器人使用PyTorch进行目标检测,以识别哪些杂草应该喷洒肥料。

回到这里,PyTorch在Facebook(也是Meta AI)构建人工智能和机器学习的未来中扮演着核心角色。他们在内部将所有机器学习应用都基于PyTorch。

微软在PyTorch生态中也扮演着重要角色。PyTorch绝对无处不在。


PyTorch与GPU加速 ⚡

如果这些还不足以成为你使用PyTorch的理由,那么也许你选错了课程。你已经看到了足够多的使用PyTorch的理由,我再给你一个:它能帮助你在GPU上加速运行你的机器学习代码。

我们之前简要提到过。但什么是GPU(或TPU,因为这是近年来较新的芯片)?

GPU是图形处理单元,最初为电子游戏设计,本质上非常擅长快速处理数字计算。如果你设计或玩过电子游戏,就会知道如今的图形渲染非常复杂,需要大量的数值计算。

PyTorch的美妙之处在于,它使你能够通过一个名为CUDA的接口来利用GPU。CUDA是一个并行计算平台和应用程序编程接口(API),它允许软件使用特定类型的图形处理单元进行通用计算。这正是我们想要的。

因此,PyTorch利用CUDA使你能在NVIDIA GPU上运行机器学习代码。当然,也有在TPU(张量处理单元)上运行PyTorch代码的能力。

但在实践中,GPU要常见得多,所以我们将重点学习如何在GPU上运行PyTorch代码。

这些芯片之所以被称为张量处理单元,是因为机器学习和深度学习大量处理张量。


总结与预告 📚

本节课中我们一起学习了PyTorch是什么,了解了它作为最流行的研究型深度学习框架的地位,以及它被特斯拉、OpenAI、Meta和微软等顶级公司广泛使用的原因。我们还探讨了PyTorch能够利用GPU(通过CUDA)加速计算的核心优势。

在下一节视频中,我们将回答一个问题:什么是张量?但在从我这里得到答案之前,我建议你先自己研究一下这个问题。打开谷歌或你喜欢的搜索引擎,输入“what is a tensor”,看看你能找到什么。我们下个视频见。

12:张量概念解析 🧮

在本节课中,我们将要学习深度学习的核心基础概念——张量。我们将探讨张量的定义、它在神经网络中的作用,以及为什么张量是 PyTorch 等深度学习框架的基本构建块。


上一节我们介绍了神经网络的基本流程,本节中我们来看看这个流程中的核心数据载体——张量。

张量几乎可以是任何数字的表示形式。在深度学习中,我们通常将输入数据(如图像、文本、音频)进行数值编码,转换成张量。这个张量随后被输入到神经网络中,神经网络对其执行数学运算,并输出另一个张量。最后,我们将输出张量转换回人类可以理解的形式。

以下是张量在深度学习流程中的核心作用:

  • 输入:原始数据(如图像)被数值化,形成输入张量。
  • 处理:神经网络对输入张量执行一系列数学运算。
  • 输出:网络产生一个输出张量,其中包含了学习到的模式或结果。

为了更清晰地说明,让我们聚焦于一个具体的例子:构建一个图像分类模型。

假设我们想区分一张图片是拉面还是意大利面。流程如下:

  1. 输入是图像。
  2. 我们将图像转换为数字,这些数字以张量的形式表示。
  3. 我们将这个数字张量(或大批量的张量)输入神经网络。
  4. 神经网络对这些张量执行数学运算。
  5. 网络输出一个张量。
  6. 我们将这个输出张量转换为人类可以理解的分类标签(例如,“拉面”或“意大利面”)。

这个原理是普适的。无论你处理的是10张图片还是10亿张图片,核心步骤都是:将数据编码为张量形式的数值表示,用神经网络处理这些张量,再将输出的张量解码。

PyTorch 中的基本构建块是 torch.Tensor。我们很快就会在代码中亲手使用它。在 PyTorch 中,许多底层的数学运算都被自动处理,我们主要通过编写代码来定义和执行对张量的操作。


到目前为止,我们已经涵盖了许多基础知识:什么是机器学习、深度学习、神经网络,以及为什么使用 PyTorch 和 PyTorch 是什么。现在,我们明确了深度学习的根本构建块是张量。

在下一节中,我们将更具体地了解在本模块的代码实践中将要涵盖的内容。

本节课中我们一起学习了张量的核心概念。我们了解到张量是数据的数值化表示,是神经网络输入、处理和输出的基本单位。理解张量是理解深度学习工作流程的第一步。

13:PyTorch课程内容概览 🧠

在本节课中,我们将具体介绍基础模块中将要涵盖的代码内容。

上一节我们提到了“张量”这个概念,并鼓励大家自行搜索。本节中我们来看看课程的具体学习路径和核心工作流程。

主动学习与搜索的重要性

在上一节,我建议大家去搜索“什么是张量”。这正是我日常作为机器学习工程师的工作方式:编写代码,遇到不明确的概念时,立即使用搜索引擎查找答案,例如搜索“PyTorch 什么是张量”或遇到的错误信息。

我特意演示这个过程,不仅是为了告诉你遇到问题可以搜索,更是为了鼓励你养成这个习惯。在整个课程中,你会多次看到我这样做。记住,主动搜索和解决问题是机器学习工程师的核心技能之一。

📚 课程学习路径参考

以下是根据埃隆·马斯克一条关于学习路径的推文启发的比喻,它概括了我们将要经历的学习过程:

  • 大学课程:提供扎实的机器学习(ML)与深度学习(DL)理论基础。
  • 在线课程:例如本课程,帮助你开始实践并扩展知识。
  • YouTube视频:提供直观、动态的学习体验。
  • 技术文章:例如本课程配套的在线书 learnpytorch.io,所有课程材料都以在线书籍的形式提供,便于深入参考。
  • 社区与梗图:从社区文化中汲取灵感,最终达到熟练运用的境界。

我们的课程将融合以上多种形式,提供全面的学习体验。

🎯 本模块核心学习目标

现在,让我们具体看看在这个基础模块中,我们将要学习哪些PyTorch核心内容:

  1. PyTorch基础与张量操作:我们将重点学习张量(tensor)及其操作。记住,神经网络的核心就是输入张量,对张量执行操作,并生成输出张量
  2. 数据预处理:学习如何将原始数据(如图片)转换为数值编码,即张量
  3. 构建与使用预训练模型:具体来说是构建神经网络。我们将编写代码,让模型学习我们预处理好的数据中的模式。
  4. 模型训练与预测:让模型拟合数据,并利用学习到的模式进行预测。这正是深度学习的意义所在:利用过去的模式预测未来
  5. 模型评估:学习如何评估模型预测结果的好坏。
  6. 模型的保存与加载:例如,将训练好的模型从开发环境导出到应用程序中。
  7. 使用模型进行自定义预测:学习如何使用训练好的模型对我们自己的数据进行预测,这非常有趣。

机器学习兼具科学性与艺术性。它有点像精确的化学实验,也像随性的烹饪——加点这个,试试那个,看看效果如何。我喜欢把它看作一档“机器学习烹饪秀”。欢迎来到“与Daniel一起烹饪PyTorch”!

🔄 PyTorch核心工作流程

最后,我们介绍一个贯穿本课程的核心PyTorch工作流程。以下是主要步骤:

  1. 准备数据:将数据转换为模型可处理的形式。
  2. 选择或构建模型:根据问题选择合适的预训练模型或构建新模型。
    • 2.1 选择损失函数与优化器:我们很快就会详细讲解它们。
    • 2.2 构建训练循环:这是模型学习的核心引擎。
  3. 模型训练与预测:让模型拟合数据并进行预测。例如,在图像分类任务中,让网络识别图片中是拉面还是意大利面。
  4. 模型评估:评估模型的预测性能,判断其是否有效。
  5. 通过实验改进:机器学习具有很强的实验性,需要不断尝试和调整。
  6. 保存与重载训练好的模型:便于后续使用或部署。

这些步骤大致按顺序排列,但在实际项目中可能会根据情况交叉进行。目前按数字顺序理解即可。

总结

本节课中我们一起学习了PyTorch基础模块的具体学习目标,理解了主动搜索的重要性,并熟悉了贯穿课程的核心PyTorch工作流程。记住,学习过程是科学和艺术的结合。下一节课,我将介绍学习本课程的一些非常重要的方法和心态。我们下节课见。

14:正确与错误的学习方法 🧠➡️🚀

在本节课中,我们将学习如何高效地学习这门PyTorch深度学习课程。我们将探讨一系列最佳实践,帮助你从编写代码中获得最大收益,并避免常见的思维陷阱。


概述:如何学习本课程

学习机器学习和编写机器学习代码是两件不同的事情。本课程的重点是编写PyTorch代码,而非深入理论。因此,我们的学习方法将围绕实践展开。

上一节我们介绍了课程的整体结构,本节中我们来看看具体的学习策略。


如何学习本课程:六个核心方法

以下是六个帮助你高效学习本课程的核心方法。

1. 边看边写代码

首要步骤是跟着写代码。本课程专注于纯粹的代码编写,我会提供额外的资源链接,帮助你理解代码背后的原理。我的教学理念是:如果我们能一起写代码、看代码如何运行,这将激发你的好奇心去探索幕后的机制。

我们的第一个座右铭是:如有疑问,就运行代码。写出来,运行它,看看会发生什么。

2. 探索与实验

带着科学家和厨师的心态来学习。像科学家一样严谨地尝试,也像厨师一样为了乐趣而尝试。实验、实验、再实验

3. 可视化不理解的内容

这一点再怎么强调都不为过。我们目前有三个座右铭:如有疑问就运行代码、不断实验,以及可视化、可视化、再可视化

因为机器学习和深度学习处理大量数据和数字。我发现,如果我能以非纯数字页面的形式将数字可视化,我往往能更好地理解它。我将链接一些优秀的额外资源,它们能将我们编写的代码转化为出色的可视化图表。

4. 提出问题,包括“愚蠢”的问题

实际上,没有所谓的“愚蠢”问题。每个人只是处于学习旅程的不同阶段。事实上,如果你有一个“愚蠢”的问题,很可能很多人也有同样的问题。

所以,请务必提问。我稍后会链接一个你可以提问的资源。请不仅向社区提问,也向谷歌、互联网或任何你能想到的地方提问,甚至向你自己提问。对代码提出问题,并通过编写代码来找出答案。

5. 完成练习

我为每个模块都创建了一些很棒的练习。在课程书籍版本的所有章节底部,都会有练习和课外拓展内容。

我强烈建议你不要仅仅跟着课程和我一起写代码。请务必尝试完成练习,因为这将拓展你的知识。我们将一起进行大量编写代码的实践,而练习将给你机会应用所学知识。当然,课外拓展内容也为你提供了深入学习的机会。

6. 分享你的成果

我无法充分强调,通过Github、不同代码资源或与社区分享我的工作,对我的学习帮助有多大。

所以,如果你学到了关于PyTorch的很酷的东西,我很想看到它。请通过Discord聊天或Github等方式链接给我。分享你的成果不仅是一种学习方式(因为当你分享或写作时,你也在思考如何让他人理解),也是帮助他人学习的好方法。


如何避免错误的学习方法

上面我们介绍了如何学习本课程,现在,让我们看看如何避免错误的学习方法。

我希望你避免过度思考这个过程。想象这是你的大脑,这是你大脑“着火”的样子。要避免让你的大脑“着火”,那不是一个好状态。我们正在使用PyTorch(火炬),所以可能会很“热”(这里玩了一下torch这个词的双关)。但要避免你的大脑“着火”。

同时,避免说“我学不会XXX”。我曾多次对自己说过这句话,然后我进行了练习。结果证明,我实际上可以学会那些东西。

让我们在上面画一条红线。哦,一条更粗的红线。好了,一条又粗又好的红线。我们把它放在那里。现在写着“避免”和划掉可能不太合理。但不要说“我学不会”,并防止你的大脑“着火”。


总结

本节课中我们一起学习了高效学习PyTorch课程的六个核心方法:边看边写代码、积极探索实验、将内容可视化、勇敢提出问题、认真完成练习以及主动分享成果。同时,我们也探讨了需要避免的思维陷阱,如过度思考和自我设限。记住,实践和好奇是学习深度学习的最佳伙伴。

在下一节课中,我们将在开始编码之前,介绍本课程可用的资源。

15:📚 重要课程资源

在本节课中,我们将了解学习本课程所需的核心资源。掌握这些资源的位置和使用方法,将帮助你更高效地学习和解决问题。

在深入学习本课程之前,有一些基础的资源需要你了解。这些资源对我们后续的学习至关重要。

本课程主要涉及以下三部分资源:

首先是GitHub代码仓库。你可以点击这个链接访问。我在浏览器中收藏了这个页面,建议你在学习过程中也这样做。这个仓库的地址是 Mr. Deepbus/my_github_slash_pytorch_deep_learning。在录制本视频时,它仍在不断完善中。但当你学习时,它的核心内容不会有太大变化,只是会添加更多材料。仓库中会有一个“材料大纲”部分,说明课程涵盖的内容。在录制时,有些部分标注为“即将推出”。当你观看时,这些部分很可能已经完成。练习和课外拓展的链接也会放在这里。基本上,课程所需的一切材料都会在这个GitHub仓库中。


接下来,在同一个GitHub仓库(Mr. D Burk slash pytorch deep learning)中,如果你点击“Discussions”标签页。

这里将是课程的问答区。链接就是上面这个。如果你有问题,可以点击“New discussion”,然后选择“Q&A”类别。接着,你可以输入视频标题,例如“pytorch fundamentals”,然后描述你的问题。或者你也可以直接输入遇到的错误,例如“张量的N维是什么?”。在描述中,你可以这样写:“你好,我在视频XYZ(请填入具体视频名称)遇到了问题”,这样我或者其他同学就能帮助你。在代码部分,你可以用三个反引号包裹代码块,并标注语言为python,例如:

import torch
torch.rand(2, 3) # 这将创建一个张量

我们稍后会看到这个。然后提交你的问题。这种格式化的代码非常有助于我们理解问题所在。提问的基本框架是:说明视频、描述问题、附上相关代码和错误信息。之后点击“Start discussion”。我或者课程社区的其他成员会在这里提供帮助。这样做的好处是所有内容都集中在一处,你还可以进行搜索。目前这里还没有内容,因为课程刚刚开始。但随着学习的深入,这里的内容会越来越多。如果你认为代码有需要改进的地方,也可以在这里提交一个新的“Issue”。你可以阅读相关说明了解更多。我已经提交了一些关于需要录制视频、创建材料的Issue。如果你觉得有可以改进的地方,请提交Issue。如果你对课程有疑问,请发起一个讨论。




回到我们的要点,我们还有一项资源。以上就是课程材料,它们都存放在GitHub上。课程问答在GitHub仓库的“Discussions”标签页,此外还有课程的在线电子书。

这本书堪称艺术品,非常精美。它通过一些代码自动将GitHub上的所有材料转换而成。如果我们进入代码部分,点击“Note 00”,有时如果你在GitHub上使用过Jupyter笔记本,可能会加载一会儿。这里的所有材料都会自动转换成这本书。


这本书的优点是它有清晰的标题,易于阅读,完全在线,包含所有图片,并且你可以在这里搜索内容,例如“PyTorch training steps”。

“在PyTorch中创建训练循环”。非常棒,我们稍后会看到这个。以上就是你需要了解的三大核心资源,它们专门针对本课程:GitHub上的课程材料、课程问答区、以及课程在线电子书。电子书的网址是 learnpytorch.io,这是一个简单易记的URL,所有材料都会在那里。

最后,针对PyTorch本身,还有两个通用资源:PyTorch官方网站和PyTorch论坛。如果你有与课程无关但关于PyTorch本身的问题,我强烈建议你访问PyTorch论坛,网址是 discuss.pytorch.org,链接已提供。以及PyTorch官网 pytorch.org。这将是你在PyTorch领域的大本营。这里有完整的文档。需要说明的是,本课程并不能替代熟悉PyTorch官方文档的过程。实际上,本课程正是基于所有PyTorch文档构建的,只是以略有不同的方式组织。因此,官网这里有大量关于PyTorch的优质资源。这是你的根据地。在课程中,你会经常看到我引用这些内容。

请记住这些资源:GitHub上的课程材料、课程讨论区、learnpytorch.io。这些是针对本课程的。而针对PyTorch通用学习(不限于本课程),则有PyTorch官网和PyTorch论坛。

综上所述,我们已经介绍了很多内容。但是,猜猜现在该做什么了?是时候写一些代码了。我们下一个视频再见。


本节课总结

在本节课中,我们一起学习了支撑本PyTorch深度学习课程的核心资源体系。我们明确了三大课程专属资源:GitHub代码仓库(存放所有材料与代码)、课程问答讨论区(在GitHub的Discussions标签页)以及课程在线电子书learnpytorch.io)。此外,我们还介绍了两个PyTorch通用学习宝库:PyTorch官方网站pytorch.org,含官方文档)和PyTorch论坛discuss.pytorch.org)。熟悉并善用这些资源,将为你的学习之路提供坚实的支持。

16:PyTorch 代码环境配置 🛠️

在本节课中,我们将学习如何配置 PyTorch 的代码环境。我们将重点介绍一个名为 Google Colab 的在线工具,它能让初学者轻松开始编写和运行 PyTorch 代码,而无需处理复杂的本地安装过程。

理论过渡到实践

上一节我们从理论角度介绍了 PyTorch 的基础知识。本节中,我们来看看如何开始实际编写代码。

我将介绍一个在本课程中会一直使用的主要工具:Google Colab。

我建议跟随本课程学习的方式之一,就是边看边动手编码。

介绍 Google Colab

以下是使用 Google Colab 的步骤:

  1. 打开浏览器,访问 colab.research.google.com
  2. 页面加载后,你将看到 Google Colab 的界面。

如果你想全面了解 Google Colab 的功能,可以浏览其官方概述文档。但本质上,Google Colab 允许我们创建新的笔记本,这正是我们练习编写 PyTorch 代码的方式。

参考学习文档 learnpytorch.io,你会发现这些资料实际上就是以在线书籍格式呈现的 Colab 笔记本。这些是课程的基础材料。每个新模块我们都会开启一个新的笔记本。

创建你的第一个笔记本

我将在这里放大界面。

第一个模块的笔记本编号为 00,因为 Python 代码通常从 00 开始。我将这个笔记本命名为 “PyTorch Fundamentals”。为了区分,我给我的笔记本加上 “video” 标签,表明这是视频教程中使用的笔记本。

点击 “连接” 后,我们将获得一个编写 Python 代码的空间。

例如,我们可以输入:

print("Hello, I'm excited to learn PyTorch.")

然后按下 Shift + Enter 来运行代码。

Google Colab 的优势

Google Colab 的一个显著优势是它提供了免费的硬件加速器。我个人使用的是专业版,每月约 10 美元,价格可能因地区而异。我使用专业版是因为我经常使用 Colab。然而,你完全不需要付费版本也能完成本课程。Google Colab 提供免费版本,足以满足本课程的需求。

以下是设置硬件加速器的步骤:

  1. 点击顶部菜单栏的 “运行时”。
  2. 选择 “更改运行时类型”。
  3. 在 “硬件加速器” 下拉菜单中,选择 “GPU”。
  4. 点击 “保存”。

现在,我们编写的代码(如果以特定方式编写)将在 GPU 上运行。稍后我们会看到,对于深度学习任务,在 GPU 上运行的代码在计算时间上要快得多。

我们可以运行 !nvidia-smi 命令来确认是否可以访问 GPU。在我的例子中,我获得了一个 Tesla P100 GPU。付费用户通常会获得更好的 GPU,而免费用户也能获得 GPU,只是速度可能不如付费版本提供的。请记住这一点。

笔记本的结构与资源

Colab 笔记本主要由代码单元格和文本单元格构成。

我可以按 Ctrl + M M 将一个单元格从代码模式转换为文本模式,然后按 Shift + Enter 确认。这样我们就有了一个文本单元格。如果需要新的代码单元格,可以点击 “+ 代码” 按钮。

为了结束本视频,我们将导入 PyTorch 并检查版本。

以下是需要运行的代码:

import torch
print(torch.__version__)

Google Colab 的另一个优点是它预装了 PyTorch 以及许多其他常用的 Python 数据科学包。

例如,我们也可以导入:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

对于本课程来说,Google Colab 无疑是入门的最简单方式。

本地运行选项

你也可以选择在本地机器上运行代码。如果你想这么做,可以参考 PyTorch 官方文档中关于本地设置的章节。但如果你想尽快开始,我强烈推荐使用 Google Colab。事实上,整个课程都可以通过 Google Colab 完成。

让我们完成这个视频,确保 PyTorch 已准备就绪。

运行代码后,输出显示我们拥有 PyTorch 1.10.0 版本。如果你的版本号远高于此(例如,几年后你观看此视频时 PyTorch 已更新到 2.11),本笔记本中的部分代码可能无法工作。但 1.10.0 版本对于我们即将进行的学习已经足够。

输出中的 cu111 代表 CUDA 11.1 版本。CUDA 是 NVIDIA 的开发工具包,它使我们能够在 NVIDIA GPU 上运行 PyTorch 代码,而我们在 Google Colab 中正好可以访问这些 GPU。

截至录制本视频时,最新的 PyTorch 版本是 1.10.2。要完成本课程,你至少需要 PyTorch 1.10 和 CUDA 11.3 工具包。

总结

本节课中,我们一起学习了如何配置 PyTorch 的编码环境。我们重点介绍了使用 Google Colab 这一在线平台,它免去了复杂的本地安装步骤,并提供了免费的 GPU 加速资源,是初学者入门深度学习的绝佳选择。现在,我们的环境已经设置完毕,准备就绪。在下一节课中,我们将开始编写一些 PyTorch 代码。

17:PyTorch 张量入门 🧮

在本节课中,我们将要学习PyTorch的核心数据结构——张量。我们将从最基础的标量开始,逐步了解向量、矩阵以及更高维度的张量,并学习如何创建它们以及查看其属性。


环境准备与课程学习建议

我们已经完成了环境设置,可以访问PyTorch。这里运行着一个Google Colab实例。

我们拥有一个GPU,因为我们之前通过“运行时”->“更改运行时类型”->“硬件加速器”进行了设置。你并不一定在整个笔记本中都需要GPU,但我想向你展示如何获取GPU访问权限,因为我们后续会用到它。

关于如何学习本课程,我建议采用分屏模式。例如,你可以在屏幕左侧播放我正在讲解和编写代码的视频,在屏幕右侧打开你自己的Colab笔记本窗口。你可以新建一个笔记本,随意命名,然后跟随视频编写相同的代码。如果你遇到问题,可以参考提供的参考笔记本,也可以在此提问。


张量简介

首先,我们来了解PyTorch中的张量。张量是深度学习和数据科学中的基本构建模块。

你可能已经看过“什么是张量?”的视频。在本课程中,张量是一种表示数据的方式,特别是多维的数值数据。这些数值数据可以代表其他事物。


创建张量

以下是创建不同类型张量的方法。

标量

标量是张量中最简单的形式。在PyTorch中,我们使用 torch.tensor() 来创建张量。

scalar = torch.tensor(7)

执行 scalar 会返回 tensor(7),并显示其数据类型为张量。

要了解 torch.tensor 的详细信息,可以查阅其官方文档。这是PyTorch中最常用的类之一,几乎所有PyTorch功能都基于 torch 模块。

现在,让我们看看标量的一些属性。标量没有维度,它只是一个单一的数字。

scalar.ndim  # 返回 0

如果我们想从张量类型中提取出这个数字,可以使用 .item() 方法。

scalar.item()  # 返回 7,一个普通的Python整数

向量

接下来是向量。向量通常具有大小和方向,但在我们的上下文中,向量是包含多个数字的一维张量。

vector = torch.tensor([7, 7])

向量与标量的区别在于,向量通常包含多个数字。让我们检查它的维度。

vector.ndim  # 返回 1

这可能会让人困惑,因为向量包含两个数字,但维度却是1。理解维度的一个方法是数方括号的对数。这里有一对方括号 [],所以维度是1。

我们也可以查看向量的形状。

vector.shape  # 返回 torch.Size([2])

shape 属性返回 torch.Size([2]),表示这个向量有2个元素。维度指的是方括号的对数,而形状指的是每个维度上的元素数量。

矩阵

现在,让我们升级到矩阵。矩阵是一个二维张量。

matrix = torch.tensor([[7, 8],
                       [9, 10]])

矩阵有两个维度的方括号对。让我们检查它的属性。

matrix.ndim   # 返回 2
matrix.shape  # 返回 torch.Size([2, 2])

ndim 为2,因为有两对方括号。shape[2, 2],表示这是一个2行2列的矩阵,总共有4个元素。

我们可以通过索引来访问矩阵中的元素。

matrix[0]  # 返回第一行: tensor([7, 8])
matrix[1]  # 返回第二行: tensor([9, 10])

张量

最后,我们来看一个更通用的“张量”(这里指三维或更高维的张量)。我们将创建一个三维张量。

tensor = torch.tensor([[[1, 2, 3],
                        [3, 6, 9],
                        [2, 5, 4]]])

这个张量有三对方括号。让我们检查它的维度和形状。

tensor.ndim   # 返回 3
tensor.shape  # 返回 torch.Size([1, 3, 3])

ndim 为3,对应三对方括号。shape[1, 3, 3],可以这样理解:

  • 最外层的 1 对应第一维(dim 0),表示有1个“块”。
  • 中间的 3 对应第二维(dim 1),表示这个块有3行。
  • 最内层的 3 对应第三维(dim 2),表示每行有3列。

为了更直观地理解,我们可以通过索引来查看:

tensor[0]  # 返回第一个(也是唯一一个)三维“切片”,形状为 (3, 3)

在实际应用中,你很少需要手动创建包含数百万数字的张量,PyTorch会在幕后处理这些。然而,理解这些基本构建块对于构建深度学习模型至关重要。


练习建议

为了巩固理解,我建议你进行以下练习:尝试创建你自己的张量,使用任意数量的方括号和数字组合。然后,像我们上面做的那样,与张量进行交互:检查它的 .ndim.shape 属性,并尝试使用索引来访问不同维度的元素。


总结

本节课中,我们一起学习了PyTorch张量的基础知识。我们从最简单的标量开始,逐步深入到向量、矩阵和更高维度的张量。我们学习了如何使用 torch.tensor() 创建它们,以及如何使用 .ndim.shape 属性来理解它们的结构。记住,张量是PyTorch中表示数据的核心方式,掌握它们是进行深度学习的第一步。在下一课中,我们将继续探索张量的更多操作。

18:创建随机张量 🔢

在本节课中,我们将学习如何在 PyTorch 中创建随机张量。随机张量是神经网络学习过程中的基础,因为它们通常作为模型的初始参数。

回顾与引入

上一节我们介绍了深度学习数据表示的基本构建块——张量(torch.Tensor)。我们探讨了标量、向量、矩阵和张量的概念,并了解了它们在维度上的区别。

本节中,我们来看看如何创建随机张量,并理解它们在神经网络中的重要性。

为什么需要随机张量?

随机张量在 PyTorch 中非常重要,因为许多神经网络的学习方式是从充满随机数的张量开始,然后根据数据调整这些随机数,以更好地表示数据。

以下是神经网络学习的核心循环,可以用伪代码表示:

# 神经网络学习核心循环
随机数 = 初始化随机张量()
for 数据 in 数据集:
    预测 = 模型(随机数, 数据)
    误差 = 计算误差(预测, 真实值)
    随机数 = 更新参数(随机数, 误差)

这个过程——从随机数开始,观察数据,更新随机数——是神经网络的核心。

创建随机张量

在 PyTorch 中,我们可以使用 torch.rand() 函数轻松创建随机张量。这个函数允许我们指定张量的大小或形状。

以下是创建随机张量的基本语法:

随机张量 = torch.rand(大小)

其中“大小”是一个定义张量形状的元组。

随机张量示例

让我们通过几个例子来理解如何创建不同形状的随机张量。

示例 1:基本随机张量

首先,我们创建一个 3x4 的二维随机张量:

随机张量 = torch.rand(3, 4)

这个张量有 3 行和 4 列,总共 12 个元素,每个元素都是 0 到 1 之间的随机数。

示例 2:不同维度的张量

我们可以创建任意维度的随机张量。例如,创建一个三维张量:

三维张量 = torch.rand(1, 10, 10)

这个张量有 1 个“深度”维度,每个深度有 10x10 的矩阵。总元素数为 1 × 10 × 10 = 100

示例 3:更大规模的张量

创建更大规模的随机张量同样简单:

大规模张量 = torch.rand(10, 10, 10)

这个张量有 10 × 10 × 10 = 1000 个元素。PyTorch 能够高效处理包含数十万甚至数百万元素的张量。

创建图像形状的随机张量

在深度学习中,我们经常需要处理图像数据。图像通常表示为具有特定形状的张量。

以下是图像张量的常见表示方式:

# 图像张量形状:颜色通道 × 高度 × 宽度
图像张量形状 = (颜色通道数, 高度, 宽度)

对于彩色图像,颜色通道通常是红、绿、蓝(RGB),所以颜色通道数为 3。

让我们创建一个模拟图像张量的随机张量:

随机图像张量 = torch.rand(3, 224, 224)  # 3个颜色通道,224像素高度,224像素宽度

这个张量模拟了一个 224x224 像素的彩色图像。在实际应用中,图像数据会被转换为这种张量格式进行处理。

需要注意的是,有时颜色通道的位置可能不同(例如高度×宽度×颜色通道),但我们可以通过代码轻松调整这些维度。

关键要点

以下是本节课的核心要点:

  1. 随机张量的重要性:神经网络通常从随机初始化的参数开始学习。
  2. 创建方法:使用 torch.rand(大小) 可以创建指定形状的随机张量。
  3. 灵活性:可以创建任意形状和维度的随机张量。
  4. 图像表示:图像通常被表示为形状为(颜色通道,高度,宽度)的张量。

练习挑战

现在轮到你了!尝试创建你自己的随机张量:

  1. 创建一个形状为 (5, 10, 10) 的随机张量
  2. 检查它的维度和总元素数
  3. 尝试创建其他形状的随机张量,观察它们的变化

总结

本节课中我们一起学习了如何在 PyTorch 中创建随机张量。我们了解了随机张量在神经网络初始化中的重要性,掌握了使用 torch.rand() 创建不同形状张量的方法,并探讨了图像数据在张量中的表示方式。

记住,几乎任何类型的数据都可以表示为张量,而随机张量是许多深度学习模型的起点。在下一节课中,我们将继续探索 PyTorch 张量的其他特性和操作。

19:创建全零与全一张量 🧮

在本节课中,我们将学习如何在PyTorch中创建元素全为0或全为1的张量。这些张量在深度学习中常用于初始化、创建掩码或执行特定的数学运算。

上一节我们介绍了如何创建随机张量,本节中我们来看看如何创建具有特定值的张量。

创建全零张量

全零张量是指所有元素值都为0的张量。在深度学习中,它常被用作掩码,以屏蔽掉张量中的某些部分。

以下是创建全零张量的方法:

zeros = torch.zeros(size=(3, 3))

代码解释torch.zeros() 函数接受一个 size 参数,用于指定张量的形状。上述代码创建了一个形状为 3x3 的全零张量。

全零张量的一个关键应用是作为掩码。例如,当你将一个张量与全零张量相乘时,结果张量的所有元素都会变为0。这在需要忽略模型中某些特定数据时非常有用。

创建全一张量

全一张量是指所有元素值都为1的张量。其创建方式与全零张量类似。

以下是创建全一张量的方法:

ones = torch.ones(size=(3, 4))

代码解释torch.ones() 函数同样接受 size 参数。上述代码创建了一个形状为 3x4 的全一张量。

在创建这些张量时,还有一个重要的参数是 dtype,它代表数据类型。默认情况下,PyTorch创建的张量数据类型是 torch.float32,即32位浮点数。除非你显式指定其他类型,否则所有通过PyTorch方法创建的张量默认都是此类型。

实践练习

为了巩固理解,建议你尝试以下练习:

  • 创建一个任意形状的全零张量。
  • 创建一个任意形状的全一张量。

通过动手实践,你可以更直观地感受这些张量的特性和用途。

本节课中我们一起学习了如何使用 torch.zeros()torch.ones() 函数来创建全零和全一张量,并了解了它们在深度学习中的基本应用,例如作为掩码。虽然随机张量更为常见,但全零和全一张量也是你会在实际项目中遇到的重要工具。下一节,我们将学习如何创建具有特定数值范围的张量。

20:创建范围张量与相似张量 📊

在本节课中,我们将学习如何使用PyTorch创建具有特定数值范围的张量,以及如何创建与现有张量形状相同的新张量。这是构建和初始化神经网络层时非常实用的技能。

上一节我们介绍了如何创建全零和全一张量。本节中,我们来看看如何创建数值序列张量以及复制张量的形状。

创建范围张量

torch.range() 函数在过去用于创建数值序列张量,但在新版本的PyTorch中已被弃用。现在,我们应该使用 torch.arange() 函数。

以下是 torch.arange() 的基本用法:

# 创建一个从0到9的张量(默认起始值为0)
torch.arange(10)
# 输出:tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 创建一个从1到10的张量
torch.arange(start=1, end=11)
# 输出:tensor([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

# 创建一个从0到1000,步长为77的张量
torch.arange(start=0, end=1000, step=77)
# 输出:tensor([0, 77, 154, 231, 308, 385, 462, 539, 616, 693, 770, 847, 924])

公式torch.arange(start, end, step) 生成一个从 start 开始,到 end-1 结束,以 step 为步长的一维张量。

创建相似张量

有时,我们希望创建一个与现有张量形状相同但内容不同的新张量(例如全零)。PyTorch提供了 torch.zeros_like()torch.ones_like() 等方法来实现。

以下是创建相似张量的步骤:

  1. 首先,创建一个示例张量。
  2. 然后,使用 _like 方法创建形状相同的新张量。
# 1. 创建一个示例张量
example_tensor = torch.arange(1, 11)  # 形状为 [10]
print(example_tensor.shape)  # 输出:torch.Size([10])

# 2. 创建一个形状与 example_tensor 相同的全零张量
zeros_like_tensor = torch.zeros_like(input=example_tensor)
print(zeros_like_tensor)
# 输出:tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

核心概念torch.zeros_like(input) 返回一个与 input 张量形状和数据类型完全相同,但所有元素都为零的新张量。

实践练习

为了巩固理解,请尝试完成以下练习:

  • 使用 torch.arange() 创建一个从50到100(包含100)、步长为5的张量。
  • 使用 torch.ones_like() 创建一个与你刚创建的范围张量形状相同的全一张量。

如果在练习中遇到代码长时间运行或无响应的情况,可以尝试在Google Colab中点击 Runtime -> Restart runtime 来重启计算内核,这通常能解决临时性问题。

总结

本节课中我们一起学习了两个创建张量的重要方法:

  1. torch.arange():用于创建具有特定数值范围的序列张量。
  2. torch.zeros_like() / torch.ones_like():用于创建与给定张量形状相同的全零或全一张量。

记住,torch.range() 已是弃用函数,在新代码中应使用 torch.arange()。掌握这些张量创建方法,将为后续定义模型参数和数据处理打下坚实基础。

21:张量数据类型详解 🔢

在本节课中,我们将要学习PyTorch中一个非常核心的概念:张量的数据类型。理解数据类型对于避免常见的深度学习编程错误至关重要。


概述:张量数据类型的重要性

欢迎回来。现在,让我们深入探讨一个非常重要的主题:张量的数据类型。

我们之前已经简要地提到过这一点。现在,让我们创建一个张量来开始。

float_32_tensor = torch.tensor([3, 6, 9], dtype=None)

我们创建了一个名为float_32_tensor的张量,并传入数字[3, 6, 9]。我们将dtype参数设置为None,看看会发生什么。结果是float32张量。即使我们指定为None,默认的数据类型也是float32。这是因为PyTorch的默认数据类型是float32

如果我们想将其更改为其他类型呢?我们可以这样写:

float_16_tensor = torch.tensor([3, 6, 9], dtype=torch.float16)

现在,我们得到了一个float16张量。创建张量时还有另一个非常重要的参数,那就是device。我们稍后会看到它是什么。最后还有一个同样重要的参数是requires_grad,我们可以将其设置为FalseTrue。目前我们将其设为False

因此,创建张量时最重要的三个参数是:

  • dtype:张量的数据类型,例如float32float16
  • device:张量所在的设备(CPU或GPU)。
  • requires_grad:是否跟踪张量操作的梯度。

再次强调,你并不总是需要在创建张量时输入这些参数,因为PyTorch会在幕后为你完成很多张量创建工作。


可用的数据类型

如果你想了解PyTorch张量有哪些可用的数据类型,我们可以查看torch.tensor的文档。在文档顶部,除非文档有变化,否则首先出现的就是“数据类型”。数据类型在创建张量时是如此重要,以至于它是文档中首先出现的内容。

我们有以下类型:32位浮点数、64位浮点数、16位浮点数、32位复数等。你最常与之交互的很可能是32位浮点数和16位浮点数。

那么,这些数字到底意味着什么呢?它们与计算中的精度有关。


理解计算精度

在计算机科学中,数值量的精度是衡量该量被表达细节程度的一个指标。这通常以比特(bits)来衡量,有时也以十进制数字来衡量。它与数学中的精度相关,后者描述了用于表达一个值的数字位数。

对我们来说,精度是数值量的一个度量,衡量了该量被表达的细节程度。

我不会深入探讨计算机科学的背景以及计算机如何表示数字。你从这里获得的重要启示是:单精度浮点数通常称为float32,这意味着一个数字在计算机内存中包含32个比特。同样,float16则使用16个比特。

这意味着:

  • float32单精度
  • float16半精度

默认是float32,这意味着它将在计算机内存中占用一定的空间。你可能会想,为什么我要使用默认值以外的类型呢?原因在于,如果你愿意牺牲一些数字表示的细节(即用16比特而非32比特来表示),那么占用内存更少的数字可以计算得更快。这就是32位和16位之间的主要区别。如果你需要更高的精度,则可以使用64位。

请记住这一点:单精度是32位,半精度是16位。这些数字代表了一个数字在内存中存储的细节量。


深度学习中三个常见错误

关于张量数据类型的内容很多。我在这里花了很多时间,因为我想强调一个要点。

注意:张量数据类型是你在使用PyTorch和深度学习时会遇到的三大错误之一。

这三大错误是:

  1. 张量的数据类型不正确
  2. 张量的形状不正确(我们之前已经看到过一些形状的例子)。
  3. 张量不在正确的设备上

在本例中,如果我们有一个float16张量,并尝试与一个float32张量进行计算,就可能会遇到一些错误。这就是张量数据类型不正确的情况。因此,了解这里的dtype参数非常重要。

而张量形状不正确的问题,在我们讲到矩阵乘法时会看到。如果一个张量是某种形状,另一个张量是另一种形状,并且这些形状不匹配,我们就会遇到形状错误。

这完美地过渡到了device参数。device默认为None,即CPU。这就是我们使用Google Colab的原因,因为它使我们能够访问GPU。正如我之前所说,GPU使我们能够进行更快的计算。我们可以将其更改为cuda。我们稍后会看到如何编写设备无关的代码。但是,如果你尝试在两个不在同一设备上的张量之间进行操作(例如,一个张量在GPU上用于快速计算,另一个在CPU上),PyTorch会报错。

最后,requires_grad参数表示你是否希望PyTorch在张量进行某些数值计算时跟踪其梯度。我们还没有介绍梯度是什么。

这些内容可能有点密集,但既然我们在讨论数据类型,我认为有必要将这些重要参数一并提出。确实,如果只讨论数据类型而不讨论形状或设备问题,那将是不完整的。


如何转换张量数据类型

现在,我们有一个float32张量。我们如何改变这个张量的数据类型呢?让我们创建一个float16张量。

我们看到可以显式地写入float16,或者我们可以这样做:

float_16_tensor = float_32_tensor.type(torch.float16)
# 或者使用 .half()
float_16_tensor = float_32_tensor.half()

这两种方式是相同的。让我们检查一下float_16_tensor。很好,我们已经将float32张量转换成了float16。这是解决你遇到的“张量数据类型不正确”问题的方法之一。

关于计算精度,如果你想了解更多,我在这里提供一个链接。这全是关于计算机如何存储数字的。


实践与总结

本节课中,我们一起学习了PyTorch张量的数据类型、计算精度的概念,以及深度学习中常见的三大错误(数据类型、形状和设备)。我们还学习了如何使用.type().half()方法来转换张量的数据类型。

现在,请尝试创建一些张量,查阅torch.tensor的文档,了解更多关于dtypedevicerequires_grad的信息。创建一些不同数据类型的张量,随意尝试,看看是否能遇到一些错误。例如,尝试将float16张量与float32张量相乘,看看会发生什么。

我们下个视频再见。

22:获取张量属性 📊

在本节课中,我们将学习如何从 PyTorch 张量中获取关键信息,包括其形状、数据类型和所在设备。掌握这些属性对于诊断和解决深度学习模型开发中的常见错误至关重要。

回顾与引入

上一节我们介绍了张量的数据类型,以及创建张量时的一些常见参数。我们留了一个挑战:创建不同数据类型的张量,并观察一个 float16 张量与一个 float32 张量相乘会发生什么。结果是,操作成功了。

然而,这引出了 PyTorch 和深度学习中的一个常见注意事项:有时即使张量数据类型不同,某些操作也不会报错。但在训练大型神经网络时,你可能会在其他操作中遇到数据类型问题。关键在于要意识到,当张量数据类型不匹配时,某些操作确实会引发错误。

让我们尝试另一个类型,例如 int32

int_32_tensor = torch.tensor([3, 6, 9], dtype=torch.int32)

然后尝试将其与一个浮点数张量相乘,看看会发生什么。

float_32_tensor * int_32_tensor

操作同样成功了。即使尝试 int64long 类型,许多操作也能正常进行。这表明 PyTorch 在某些情况下比我们想象的更健壮。但请记住,在训练模型时,我们仍可能因张量数据类型不正确而遇到错误。如果 PyTorch 抛出数据类型错误,我们现在至少知道如何更改或设置数据类型。

获取张量信息

基于我们将要面对的神经网络和深度学习中的三大常见错误,我们需要从张量中获取三类核心信息。

以下是需要检查的三个关键方面:

  • 形状:张量的维度结构。
  • 数据类型:张量中元素的类型(如 float32, int64)。
  • 设备:张量所在的硬件(CPU 或 GPU)。

让我们具体看看如何获取这些信息。

要获取张量的数据类型,可以使用 .dtype 属性。

tensor.dtype

要获取张量的形状,可以使用 .shape 属性。

tensor.shape

要获取张量所在的设备,可以使用 .device 属性。

tensor.device

实践操作

现在,让我们创建一个张量并实践获取这些属性。

import torch

# 创建一个随机张量
some_tensor = torch.rand(3, 4)
print(some_tensor)

接下来,我们获取并打印该张量的详细信息。

# 获取并打印张量的数据类型、形状和设备
print(f"Datatype of tensor: {some_tensor.dtype}")
print(f"Shape of tensor: {some_tensor.shape}")
print(f"Device tensor is on: {some_tensor.device}")

运行上述代码,你会看到类似以下输出:

  • 数据类型是 torch.float32,因为这是我们未指定时的默认类型。
  • 形状是 (3, 4),这与我们创建时传入的参数一致。
  • 设备是 cpu,这也是默认设备,除非我们显式指定将其放在 GPU 上。

注意.shape 是一个属性,而 .size() 是一个方法,两者通常返回相同的结果。你可以根据习惯使用,但需注意语法上的区别(tensor.shape 对比 tensor.size())。

总结与挑战

本节课我们一起学习了如何获取 PyTorch 张量的三个核心属性:形状数据类型设备。这些是诊断“张量形状错误”、“张量数据类型错误”和“张量设备错误”的基础。

现在,请尝试以下挑战来巩固所学:

  1. 创建一个随机张量,但将其数据类型设置为 float16 而非默认的 float32
  2. (额外挑战)研究如何将 PyTorch 张量移动到 GPU 设备上(我们将在后续课程详细讨论)。

尝试完成这些练习,我们下节课再见!

23:张量操作(张量运算)🧮

在本节课中,我们将要学习 PyTorch 中张量的基本数学运算。这些运算是构建神经网络的基础,因为神经网络本质上就是一系列数学函数的组合。我们将涵盖加法、减法、乘法、除法和矩阵乘法。

上一节我们介绍了张量的属性,如数据类型、形状和设备。本节中我们来看看如何对张量进行数学操作,以改变其数值。

张量运算简介

神经网络由许多数学函数组成,PyTorch 会在幕后为我们运行这些代码。为了在数据集中发现模式,神经网络会以某种方式组合这些函数。它从一个充满随机数的张量开始,执行加法、减法、乘法、除法或矩阵乘法的某种组合,来调整这些数字以表示数据集。这就是神经网络的学习方式。

以下是几种核心的张量运算:

  • 加法
  • 减法
  • 乘法(包括两种类型)
  • 除法
  • 矩阵乘法

前四种是大家可能熟悉的常规运算。矩阵乘法是这里唯一不同的运算,我们稍后会详细查看。

基本运算示例

首先,我们需要创建一个张量。

import torch
tensor = torch.tensor([1, 2, 3])

加法

要给张量加上一个值,我们可以使用 Python 中的加法运算符 +

# 张量加 10
tensor + 10
# 输出:tensor([11, 12, 13])

# 张量加 100
tensor + 100
# 输出:tensor([101, 102, 103])

乘法

类似地,我们可以使用乘法运算符 * 来乘以一个值。

# 张量乘以 10
tensor * 10
# 输出:tensor([10, 20, 30])

请注意,上述操作不会改变原始张量 tensor 的值。如果想永久改变它,需要重新赋值。

# 重新赋值以永久改变张量
tensor = tensor * 10
print(tensor)
# 输出:tensor([10, 20, 30])

减法

减法运算使用减号 -

# 张量减去 10
tensor - 10
# 输出:tensor([ 0, 10, 20])

PyTorch 内置函数

除了使用运算符,PyTorch 也提供了内置函数来执行相同的操作。例如:

  • torch.mul() 用于乘法。
  • torch.add() 用于加法。
# 使用 torch.mul 进行乘法
torch.mul(tensor, 10)
# 输出:tensor([100, 200, 300])

# 使用 torch.add 进行加法
torch.add(tensor, 10)
# 输出:tensor([20, 30, 40])

通常,直接使用 Python 运算符(+, -, *, /)代码更易读。但在某些特定情况下,可能会看到使用 PyTorch 的内置函数。

关于矩阵乘法

在深度学习中,你会经常听到两种乘法:逐元素乘法矩阵乘法。我们目前演示的都是逐元素运算。

矩阵乘法是一个更重要的概念,我们将在下一个视频中深入探讨。作为一个小挑战,建议你先搜索并了解“什么是矩阵乘法”。维基百科和“Math is Fun”等网站都有很好的指南。思考一下如何在 PyTorch 中实现它,即使你现在还不确定具体方法。

本节课中我们一起学习了 PyTorch 张量的基本数学运算,包括加法、减法、乘法和除法,并了解了它们与神经网络构建的关系。我们还提到了矩阵乘法的重要性,为下一节的学习做好了准备。

24:矩阵乘法(第一部分)📚

在本节课中,我们将要学习深度学习中两种主要的乘法运算:逐元素乘法和矩阵乘法。我们将重点探讨矩阵乘法的核心概念、它与逐元素乘法的区别,并通过PyTorch代码进行实践。


矩阵乘法 🧮

上一节我们介绍了一些基础的张量运算,如加法、减法和逐元素乘法。本节中,我们来看看深度学习中另一种至关重要的运算:矩阵乘法。

在深度学习中,主要有两种执行乘法的方式:

  1. 逐元素乘法
  2. 矩阵乘法

矩阵乘法(也称为点积)可能是你在神经网络内部最常遇到的张量运算。


逐元素乘法与矩阵乘法的区别 🔍

为了理解矩阵乘法,我们首先需要明确它与逐元素乘法的区别。

逐元素乘法意味着将两个张量中对应位置的元素相乘。例如,对于一个矩阵 [2, 0, 1, -9] 乘以标量 2,结果是 [8, 0, 2, -18]

而矩阵乘法(点积)则涉及更复杂的计算规则。例如,计算矩阵 [[1, 2, 3], [4, 5, 6]][[7, 8], [9, 10], [11, 12]] 的乘积时,结果矩阵的第一个元素 58 是这样得到的:
1*7 + 2*9 + 3*11 = 58

以下是两种乘法在PyTorch中的实现:

import torch

# 创建一个张量
tensor = torch.tensor([1, 2, 3])

# 1. 逐元素乘法
elementwise_result = tensor * tensor
print(f"逐元素乘法结果: {elementwise_result}")
# 输出: tensor([1, 4, 9])

# 2. 矩阵乘法(点积)
# PyTorch使用 torch.matmul() 进行矩阵乘法
matrix_result = torch.matmul(tensor, tensor)
print(f"矩阵乘法结果: {matrix_result}")
# 输出: tensor(14)

为什么矩阵乘法的结果是 14 而不是 [1, 4, 9] 呢?让我们手动计算一下:
1*1 + 2*2 + 3*3 = 14
可以看到,矩阵乘法在相乘之后还进行了求和操作,这是它与逐元素乘法的核心区别。


性能对比:手动循环 vs PyTorch优化函数 ⚡

我们可以尝试用for循环手动实现矩阵乘法,并与PyTorch内置的优化函数进行性能对比。

import time

# 手动实现矩阵乘法(点积)
value = 0
start_time = time.time()
for i in range(len(tensor)):
    value += tensor[i] * tensor[i]
manual_time = time.time() - start_time
print(f"手动循环结果: {value}, 耗时: {manual_time:.6f} 秒")

# 使用PyTorch的torch.matmul()
start_time = time.time()
torch_result = torch.matmul(tensor, tensor)
torch_time = time.time() - start_time
print(f"PyTorch matmul结果: {torch_result}, 耗时: {torch_time:.6f} 秒")

即使对于只有三个元素的张量,PyTorch的向量化实现(torch.matmul)通常也比手写循环快得多。向量化是一种编程范式,它利用底层优化来一次性处理整个数组,而不是逐个元素循环,这在处理大型张量(如百万级元素)时能带来巨大的速度提升。


总结 📝

本节课中我们一起学习了:

  • 逐元素乘法矩阵乘法(点积) 的核心区别在于,矩阵乘法在元素相乘后还会进行求和。
  • 矩阵乘法是神经网络中的基础且关键的操作。
  • 在PyTorch中,应优先使用 torch.matmul() 等优化函数,而不是手动编写循环,以获得最佳性能。
  • 向量化计算能显著提升运算效率,尤其是在处理大规模数据时。

在下一节中,我们将探讨适用于更大规模矩阵乘法的规则。

25:矩阵乘法(第二部分):两大核心规则 🔢

在本节课中,我们将要学习矩阵乘法的两大核心规则。上一节我们介绍了矩阵乘法的基本概念和PyTorch的实现方式,本节中我们来看看执行矩阵乘法时必须满足的两个关键条件,否则会导致深度学习中最常见的错误之一。

概述

矩阵乘法是神经网络中最常见的运算之一。虽然我们尚未深入探讨其应用,但理解其规则至关重要。PyTorch提供了高效的矩阵乘法实现,通常比自己手动编写的代码更快、更简洁。然而,如果不遵守特定规则,就会遇到形状错误。

核心规则

执行矩阵乘法时,必须满足以下两大核心规则,否则程序将报错。

规则一:内部维度必须匹配

第一个规则是,进行乘法运算的两个矩阵,其内部维度必须相同。

以下是内部维度的具体含义:

  • 假设我们有两个张量,形状分别为 (3, 2)(3, 2)。这里的内部维度指的是第一个张量的第二维(2)和第二个张量的第一维(3)。由于 2 不等于 3,它们不匹配,因此无法相乘。
  • 如果两个张量的形状是 (2, 3)(3, 2),那么内部维度(第一个张量的第二维 3 和第二个张量的第一维 3)是匹配的,因此可以相乘。

在PyTorch中,如果内部维度不匹配,你会遇到类似下面的错误:

RuntimeError: mat1 and mat2 shapes cannot be multiplied (axb and cxd)

这表示矩阵1和矩阵2的形状无法相乘,因为它违反了规则一。

规则二:结果矩阵的形状由外部维度决定

第二个规则是,矩阵乘法结果矩阵的形状,由两个原始矩阵的外部维度决定。

让我们通过例子来理解:

  • 以形状为 (2, 3)(3, 2) 的两个矩阵为例。内部维度 3 匹配。结果矩阵的形状将是第一个矩阵的第一维(2)和第二个矩阵的第二维(2),即 (2, 2)
  • 如果我们将顺序调换,用形状为 (3, 2) 的矩阵乘以形状为 (2, 3) 的矩阵。内部维度 2 匹配。结果矩阵的形状将是第一个矩阵的第一维(3)和第二个矩阵的第二维(3),即 (3, 3)

这个规则是普适的。只要内部维度匹配,你可以使用任意数字,结果矩阵的形状总是由外部维度决定。

实践与挑战

为了加深理解,我推荐你访问一个非常实用的网站:matrixmultiplication.xyz

在进入下一节之前,你的挑战是:

  • 访问该网站。
  • 随意输入一些数字作为矩阵的维度(例如2,10,5,6等)。
  • 观察当你点击“乘”时会发生什么。
  • 特别注意内部维度匹配与不匹配时的情况,以及结果矩阵的形状如何由外部维度决定。

在下一个视频中,我们将用PyTorch代码复现类似的操作,并更具体地探讨形状错误。

总结

本节课中我们一起学习了矩阵乘法的两大核心规则:

  1. 内部维度必须匹配:这是矩阵乘法能够进行的前提。
  2. 结果形状由外部维度决定:这决定了输出张量的形状。

理解并牢记这两条规则,是避免深度学习中最常见的形状错误的关键。下一节,我们将应用这些规则解决更具体的问题。

26:矩阵乘法(第三部分)与张量形状错误处理 🔧

在本节课中,我们将深入学习矩阵乘法,并探讨如何处理深度学习中最常见的错误之一:张量形状错误。我们将通过实际代码演示,理解矩阵乘法的核心规则,并学习如何使用转置操作来调整张量形状以满足这些规则。

概述 📋

上一节我们介绍了矩阵乘法的两条核心规则,并看到了当这些规则(特别是内维必须匹配)不满足时会发生错误。本节中,我们将通过创建具体的张量,实际触发这些形状错误,并学习如何使用转置操作来解决它们。

创建用于矩阵乘法的张量

首先,我们创建两个张量,用于后续的矩阵乘法操作。

import torch

# 创建张量A,形状为(3, 2)
tensor_A = torch.tensor([[1, 2],
                         [3, 4],
                         [5, 6]])

# 创建张量B,形状为(3, 2)
tensor_B = torch.tensor([[7, 8],
                         [9, 10],
                         [11, 12]])

尝试矩阵乘法并触发错误

现在,我们尝试对这两个张量进行矩阵乘法。

# 尝试矩阵乘法
# torch.mm 是 torch.matmul 的别名,用于更简洁的代码
try:
    output = torch.mm(tensor_A, tensor_B)
except Exception as e:
    print(f"错误信息: {e}")

运行上述代码会触发错误:mat1 and mat2 shapes cannot be multiplied (3x2 and 3x2)。这是因为张量A的形状是(3, 2),张量B的形状也是(3, 2),它们的内维(2和3)不匹配,违反了矩阵乘法的第一条核心规则。

检查张量形状

让我们检查两个张量的形状,以确认问题所在。

print(f"张量A的形状: {tensor_A.shape}")
print(f"张量B的形状: {tensor_B.shape}")

输出将显示两个张量的形状都是torch.Size([3, 2])

使用转置操作调整形状

为了解决形状不匹配的问题,我们可以使用转置操作来调整其中一个张量的形状。转置操作会交换张量的维度。

# 对张量B进行转置
tensor_B_transposed = tensor_B.T
print(f"转置后的张量B:\n{tensor_B_transposed}")
print(f"转置后张量B的形状: {tensor_B_transposed.shape}")

转置操作将张量B的形状从(3, 2)变为(2, 3)。现在,张量A的形状是(3, 2),转置后的张量B形状是(2, 3),它们的内维(2和2)匹配了。

成功进行矩阵乘法

现在,我们可以成功地对张量A和转置后的张量B进行矩阵乘法。

# 进行矩阵乘法
output = torch.matmul(tensor_A, tensor_B_transposed)
print(f"矩阵乘法结果:\n{output}")
print(f"输出张量的形状: {output.shape}")

输出张量的形状将是(3, 3),这符合矩阵乘法的第二条规则:输出矩阵的形状由外维决定。

完整代码示例与规则回顾

以下是完整的代码示例,结合了形状检查和规则验证。

import torch

# 1. 创建张量
tensor_A = torch.tensor([[1, 2],
                         [3, 4],
                         [5, 6]])
tensor_B = torch.tensor([[7, 8],
                         [9, 10],
                         [11, 12]])

# 2. 打印原始形状
print("原始形状:")
print(f"tensor_A.shape: {tensor_A.shape}")
print(f"tensor_B.shape: {tensor_B.shape}")

# 3. 转置张量B并打印新形状
tensor_B_transposed = tensor_B.T
print(f"\n转置后张量B的形状: {tensor_B_transposed.shape}")

# 4. 验证规则:内维必须匹配
# tensor_A.shape = (3, 2)
# tensor_B_transposed.shape = (2, 3)
# 内维: 2 和 2 -> 匹配 ✓
print("\n规则验证: 内维匹配检查")
print(f"tensor_A 的第二个维度: {tensor_A.shape[1]}")
print(f"tensor_B_transposed 的第一个维度: {tensor_B_transposed.shape[0]}")
print("内维匹配" if tensor_A.shape[1] == tensor_B_transposed.shape[0] else "内维不匹配")

# 5. 执行矩阵乘法
output = torch.matmul(tensor_A, tensor_B_transposed)
print(f"\n矩阵乘法输出:\n{output}")
print(f"输出形状: {output.shape}")

# 6. 验证规则:输出形状由外维决定
# 外维: 3 和 3 -> 输出形状应为 (3, 3)
print("\n规则验证: 输出形状检查")
print(f"预期输出形状: ({tensor_A.shape[0]}, {tensor_B_transposed.shape[1]})")
print(f"实际输出形状: {output.shape}")
print("输出形状符合预期" if output.shape == (tensor_A.shape[0], tensor_B_transposed.shape[1]) else "输出形状不符合预期")

可视化理解

为了更直观地理解矩阵乘法,我们可以使用在线工具(如 matrixmultiplication.xyz)来可视化这个过程。

以下是手动计算第一个输出元素的过程,对应于在线工具的可视化步骤:

  1. 输出张量位置 [0,0] 的元素计算:
    • 取张量A的第一行 [1, 2]
    • 取转置后张量B的第一列 [7, 10]
    • 计算点积:1*7 + 2*10 = 7 + 20 = 27

这个结果与我们代码输出中的第一个元素 27 一致。

练习与挑战

为了巩固理解,请尝试以下练习:

以下是你可以进行的操作练习:

  1. 尝试转置张量A而不是张量B,然后进行矩阵乘法。
  2. 创建不同形状的张量,故意制造形状错误,观察错误信息。
  3. 使用 torch.mmtorch.matmul,确认它们的功能相同。
  4. 在在线矩阵乘法可视化工具中重现本节的例子。

总结 🎯

本节课中我们一起学习了如何处理矩阵乘法中的张量形状错误。我们回顾了矩阵乘法的核心规则,特别是内维必须匹配这一关键条件。通过使用转置操作,我们能够调整张量形状以满足矩阵乘法的要求。记住,在深度学习中,矩阵乘法是最常见的操作之一,因此熟练掌握形状处理和错误调试至关重要。在下一节课中,我们将继续探索PyTorch中更多的张量操作。

27:张量聚合:极值、均值与求和 📊

在本节课中,我们将学习张量聚合操作。这些操作包括寻找张量中的最小值、最大值、平均值和总和。张量聚合是数据分析和神经网络中的基础步骤,它能将大量数据浓缩为少数几个关键数值。

上一节我们介绍了矩阵乘法,本节中我们来看看如何对张量进行聚合操作。

张量聚合简介

张量聚合是指从通常包含大量数值的张量中,提取出少量汇总数值的过程。例如,从一个包含9个元素的张量中找出最小值,就是将9个元素聚合为1个元素。

创建示例张量

首先,我们创建一个张量用于演示。

import torch
x = torch.arange(start=0, end=100, step=10)

寻找最小值与最大值

以下是寻找张量最小值和最大值的方法。

# 使用 torch 函数
min_value_torch = torch.min(x)
max_value_torch = torch.max(x)

# 使用张量方法
min_value_method = x.min()
max_value_method = x.max()

两种方式功能相同,你可以根据个人编码风格选择使用。

计算平均值

接下来,我们尝试计算张量的平均值。

# 尝试计算平均值
try:
    mean_value = torch.mean(x)
except Exception as e:
    print(f"错误信息: {e}")

执行上述代码会遇到一个常见错误:Mean input data type should be either floating point or complex dtypes. Got Long instead.

这是因为 torch.mean() 函数要求输入张量的数据类型为浮点数(如 float32)或复数类型,而我们创建的 xtorch.int64(Long)类型。

注意torch.mean() 函数需要一个 float32 类型的张量。

为了解决这个问题,我们需要在计算前转换张量的数据类型。

# 将张量转换为 float32 类型后再计算平均值
mean_value = torch.mean(x.type(torch.float32))
# 或者使用张量方法
mean_value_alt = x.type(torch.float32).mean()

计算总和

最后,我们来计算张量的总和。

# 使用 torch 函数
sum_value_torch = torch.sum(x)
# 使用张量方法
sum_value_method = x.sum()

同样,两种方式都是有效的。

本节总结

本节课中我们一起学习了张量的基本聚合操作:

  1. 使用 torch.min().min() 方法寻找最小值
  2. 使用 torch.max().max() 方法寻找最大值
  3. 使用 torch.mean().mean() 方法计算平均值,但需注意输入张量必须是浮点类型。
  4. 使用 torch.sum().sum() 方法计算总和

我们再次遇到了PyTorch中两个主要错误类型之一:数据类型错误(另一个是形状错误)。处理数据时,时刻留意张量的 dtype 属性至关重要。

在下一节视频中,我们将探讨如何寻找极值所在的位置(索引),即 argminargmax 操作。你可以提前尝试使用 torch.argmin()torch.argmax() 方法来解决这个问题。

28:定位张量极值位置 📚

在本节课中,我们将学习如何在PyTorch张量中定位最小值和最大值的位置。我们将介绍torch.argmin()torch.argmax()函数,并理解它们在深度学习中的实际应用。

回顾与引入

上一节我们学习了张量聚合操作,掌握了如何寻找张量的最小值、最大值、平均值和总和。同时,我们也遇到了PyTorch和深度学习中的一个常见问题:数据类型错误。我们通过将张量转换为正确的数据类型(例如,torch.mean()需要torch.float32类型)解决了这个问题。

本节中,我们将探讨如何不仅找到极值,还能找到这些极值在张量中的具体位置。

定位最小值的位置

以下是使用torch.argmin()函数定位最小值索引的步骤:

  1. 理解argmin的含义argmin代表“参数最小值”,它返回的是张量中最小值所在位置的索引,而不是最小值本身。
  2. 查看示例张量:假设我们有一个张量x
    import torch
    x = torch.arange(1, 11)  # 创建张量 [1, 2, 3, ..., 10]
    
  3. 应用argmin函数:对张量x调用torch.argmin()
    min_index = torch.argmin(x)
    print(min_index)  # 输出: tensor(0)
    
  4. 验证结果:索引0对应张量x的第一个元素,其值为1,这确实是该张量的最小值。
    print(x[min_index])  # 输出: tensor(1)
    

核心概念torch.argmin()返回的是目标张量中最小值首次出现处的索引位置

定位最大值的位置

定位最大值位置的逻辑与最小值类似,我们使用torch.argmax()函数。

以下是具体操作:

  1. 理解argmax的含义argmax代表“参数最大值”,它返回张量中最大值所在位置的索引
  2. 对同一张量应用argmax:继续使用上面的张量x
    max_index = torch.argmax(x)
    print(max_index)  # 输出: tensor(9)
    
  3. 验证结果:索引9对应张量x的第十个元素(因为索引从0开始),其值为10,即该张量的最大值。
    print(x[max_index])  # 输出: tensor(10)
    

核心概念torch.argmax()返回的是目标张量中最大值首次出现处的索引位置

为何需要定位极值位置?

你可能会问,既然torch.min()torch.max()可以直接给出极值,为什么还需要argminargmax呢?

关键在于应用场景的不同:

  • 当你只关心最小值或最大值是多少时,使用min()/max()
  • 当你关心的是极值出现在哪个位置时,使用argmin()/argmax()

这在深度学习中尤为重要。例如,在后续学习分类模型和Softmax激活函数时,模型的输出通常是一个概率分布。torch.argmax()可以快速告诉我们哪个类别的预测概率最高,从而确定模型的预测结果。记住这个函数,它在未来会非常有用。

总结

本节课中我们一起学习了PyTorch中两个重要的函数:

  1. torch.argmin():用于定位张量中最小值所在的索引位置。
  2. torch.argmax():用于定位张量中最大值所在的索引位置。

我们理解了它们与torch.min()/torch.max()的区别,并初步了解了argmax在后续深度学习任务(如分类)中的关键作用。掌握如何定位数据的位置,是进行更复杂张量操作和模型理解的基础。

下节课我们将继续探索PyTorch的其他功能。

29:张量重塑、视图与堆叠 📚

在本节课中,我们将学习PyTorch中几种关键的张量操作:重塑(reshape)、视图(view)、堆叠(stack)、压缩(squeeze)和反压缩(unsqueeze)。这些操作对于调整张量的形状和维度至关重要,是解决机器学习中常见形状不匹配问题的核心工具。


恢复工作环境 🔄

上一节我们介绍了张量的基础操作。本节中我们来看看如何在实际编码中恢复工作状态。在使用Google Colab等在线环境时,如果断开连接一段时间,运行时状态可能会被重置。为了恢复之前的工作,我们可以执行“全部运行”操作,重新执行所有代码单元格。

# 示例:重新运行所有单元格以恢复状态
# 在Colab中,选择“运行时” -> “全部运行”

张量重塑(Reshape)与视图(View) 🔧

重塑操作可以改变张量的形状,但新形状必须与原始张量的元素总数兼容。视图操作则返回一个与原始张量共享内存的新张量视图,形状可以不同。

以下是重塑操作的关键点:

  • 兼容性:新形状的维度乘积必须等于原始张量的元素总数。例如,一个包含9个元素的张量可以重塑为(1, 9)(3, 3),但不能重塑为(2, 5)
  • 代码示例
    import torch
    x = torch.arange(1, 10)  # 创建张量,值为1到9
    print(f"原始张量 x: {x}")
    print(f"原始形状 x.shape: {x.shape}")
    
    # 重塑为 (1, 9)
    x_reshaped = x.reshape(1, 9)
    print(f"重塑后 x_reshaped: {x_reshaped}")
    print(f"重塑后形状: {x_reshaped.shape}")
    
    # 尝试不兼容的形状会引发错误
    # x.reshape(2, 5)  # 这将导致错误
    

视图操作与重塑类似,但存在一个关键区别:

  • 内存共享:视图返回的张量与原始张量共享同一块内存。修改视图会直接影响原始张量。
  • 代码示例
    z = x.view(1, 9)  # 创建x的一个视图
    print(f"视图 z: {z}")
    z[0, 0] = 5       # 修改视图的第一个元素
    print(f"修改后视图 z: {z}")
    print(f"原始张量 x 也随之改变: {x}")  # x的第一个元素也变成了5
    

张量堆叠(Stack) 📦

堆叠操作可以将多个张量沿着一个新的维度组合起来。torch.stack函数允许我们指定组合的维度。

以下是堆叠操作的核心概念:

  • 默认维度torch.stack默认在维度0(最外层)进行堆叠。
  • 维度参数:通过dim参数可以改变堆叠的维度,从而改变输出张量的结构。
  • 代码示例
    # 创建多个相同形状的张量
    x = torch.arange(1, 10)
    # 在维度0堆叠(垂直堆叠)
    stacked_0 = torch.stack([x, x, x, x], dim=0)
    print(f"dim=0 堆叠结果形状: {stacked_0.shape}")
    print(f"dim=0 堆叠结果:\n{stacked_0}")
    
    # 在维度1堆叠(水平堆叠)
    stacked_1 = torch.stack([x, x, x, x], dim=1)
    print(f"\ndim=1 堆叠结果形状: {stacked_1.shape}")
    print(f"dim=1 堆叠结果:\n{stacked_1}")
    

此外,PyTorch还提供了便捷函数:

  • torch.vstack:垂直堆叠(类似于dim=0)。
  • torch.hstack:水平堆叠(类似于dim=1)。

张量压缩(Squeeze)与反压缩(Unsqueeze) 🧽

压缩操作可以移除张量中所有大小为1的维度,而反压缩操作则可以在指定位置添加一个大小为1的维度。这些操作对于调整张量以符合特定函数或模型的输入要求非常有用。

以下是操作的简要说明:

  • torch.squeeze():移除所有大小为1的维度。
    # 假设有一个形状为 (1, 3, 1, 5) 的张量
    # 使用 squeeze() 后,形状变为 (3, 5)
    
  • torch.unsqueeze(dim):在指定的dim位置添加一个维度。
    # 假设有一个形状为 (3, 4) 的张量
    # 使用 unsqueeze(dim=0) 后,形状变为 (1, 3, 4)
    # 使用 unsqueeze(dim=1) 后,形状变为 (3, 1, 4)
    

建议您查阅官方文档并尝试使用这些函数,以加深理解。


张量置换(Permute) 🔀

置换操作可以返回一个张量视图,其维度顺序被重新排列。这相当于同时进行多次转置操作。

  • 功能:按照指定的新顺序重新排列张量的维度。
  • 代码示例
    # 假设有一个形状为 (2, 3, 4) 的张量
    original_tensor = torch.randn(2, 3, 4)
    # 将维度顺序从 (0, 1, 2) 改为 (2, 0, 1)
    permuted_tensor = original_tensor.permute(2, 0, 1)
    print(f"原始形状: {original_tensor.shape}")
    print(f"置换后形状: {permuted_tensor.shape}")  # 变为 (4, 2, 3)
    

总结 📝

本节课中我们一起学习了PyTorch中几种核心的张量形状操作:

  1. 重塑(reshape)与视图(view:用于改变张量形状,视图共享内存。
  2. 堆叠(stack:用于沿新维度组合多个张量。
  3. 压缩(squeeze)与反压缩(unsqueeze:用于移除或添加大小为1的维度。
  4. 置换(permute:用于重新排列张量的维度顺序。

掌握这些操作是构建和调试深度学习模型的基础,能有效解决张量形状不匹配的问题。建议您多加练习,并在实际项目中灵活运用。

30:张量压缩、解压缩与置换 🧮

在本节课中,我们将学习PyTorch中三个重要的张量操作:squeezeunsqueezepermute。这些操作能帮助我们改变张量的维度,以适应不同的模型输入要求或数据处理流程。

张量压缩(Squeeze) 📦

上一节我们介绍了张量的基础操作,本节中我们来看看如何压缩张量。torch.squeeze()方法用于移除张量中所有大小为1的维度。

公式y = torch.squeeze(x)

以下是具体操作步骤:

  1. 创建一个包含单一维度的张量
  2. 检查原始张量的形状
  3. 应用squeeze方法
  4. 观察压缩后的张量形状
import torch

# 创建示例张量
x_reshaped = torch.tensor([[[1, 2, 3, 4, 5, 6, 7, 8, 9]]])
print("原始张量形状:", x_reshaped.shape)  # torch.Size([1, 1, 9])

# 压缩张量
x_squeezed = x_reshaped.squeeze()
print("压缩后形状:", x_squeezed.shape)    # torch.Size([9])

张量解压缩(Unsqueeze) 📈

现在我们已经学会了压缩张量,接下来看看如何反向操作。torch.unsqueeze()方法用于在指定维度添加一个大小为1的维度。

公式y = torch.unsqueeze(x, dim=d)

以下是具体操作步骤:

  1. 从压缩后的张量开始
  2. 在指定维度添加新维度
  3. 观察维度变化
# 从压缩张量开始
print("压缩张量形状:", x_squeezed.shape)  # torch.Size([9])

# 在第0维度添加维度
x_unsqueezed_0 = x_squeezed.unsqueeze(dim=0)
print("在第0维度解压缩后形状:", x_unsqueezed_0.shape)  # torch.Size([1, 9])

# 在第1维度添加维度
x_unsqueezed_1 = x_squeezed.unsqueeze(dim=1)
print("在第1维度解压缩后形状:", x_unsqueezed_1.shape)  # torch.Size([9, 1])

张量维度置换(Permute) 🔄

理解了维度的压缩与解压缩后,我们来看看如何重新排列张量的维度顺序。torch.permute()方法用于按照指定顺序重新排列张量的维度。

公式y = x.permute(dim_order)

以下是具体操作步骤:

  1. 创建一个模拟图像数据的张量
  2. 使用permute重新排列维度顺序
  3. 验证维度置换是否正确
# 创建模拟图像张量 (高度, 宽度, 颜色通道)
x_original = torch.rand(size=(224, 224, 3))  # 高度=224, 宽度=224, 颜色通道=3
print("原始形状 (高度, 宽度, 颜色通道):", x_original.shape)

# 将颜色通道维度移到最前面
x_permuted = x_original.permute(2, 0, 1)  # 新顺序: 颜色通道, 高度, 宽度
print("置换后形状 (颜色通道, 高度, 宽度):", x_permuted.shape)

重要提示permute创建的是原始张量的视图(view),这意味着它们共享相同的内存。修改其中一个张量的值会影响另一个。

# 验证内存共享
x_original[0, 0, 0] = 999
print("修改后原始张量值:", x_original[0, 0, 0])
print("置换张量对应值:", x_permuted[0, 0, 0])  # 应该也是999

总结 📝

本节课中我们一起学习了PyTorch中三个关键的张量维度操作:

  1. squeeze:移除所有大小为1的维度,简化张量结构
  2. unsqueeze:在指定位置添加大小为1的维度,扩展张量结构
  3. permute:重新排列维度顺序,常用于调整数据格式以适应模型输入

这些操作在深度学习数据处理中非常实用,特别是在准备图像、文本或其他多维数据时。通过实践这些方法,你将能更灵活地操控张量维度,为构建和训练神经网络模型打下坚实基础。

31:张量数据索引 🔢

在本节课中,我们将要学习如何在PyTorch张量中使用索引技术来选择和提取数据。索引是处理多维数据时的核心操作,掌握它对于后续的深度学习模型构建至关重要。

上一节我们介绍了张量的挤压、解压和维度重排操作,这些操作帮助我们解决张量形状和维度不匹配的问题。本节中我们来看看如何使用索引从张量中精确地选取我们需要的数值。

创建示例张量

首先,我们创建一个简单的三维张量作为练习对象。

import torch

# 创建一个形状为 (1, 3, 3) 的三维张量
x = torch.arange(1, 10).reshape(1, 3, 3)
print(x)
print(x.shape)

代码执行后,我们会得到一个包含数字1到9的张量,其形状为 torch.Size([1, 3, 3])。这表示它有一个外层(维度0),包含一个中间层(维度1),而每个中间层又包含三个内层元素(维度2)。

基础索引操作

PyTorch的索引语法与NumPy非常相似。索引从0开始,通过方括号 [] 指定每个维度的位置。

以下是索引的基本规则:

  • x[0]:选取维度0的第一个(也是唯一一个)元素,返回一个形状为 (3, 3) 的张量。
  • x[0, 0]:先选取维度0的第一个元素,再从其结果中选取维度1的第一个元素,返回一个形状为 (3,) 的一维张量,即 [1, 2, 3]
  • x[0, 0, 0]:依次在三个维度上选取第一个元素,返回标量值 1

我们可以通过改变索引值来获取不同的元素。例如,x[0, 1, 1] 会返回数字 5

使用冒号进行切片

除了指定具体索引,我们还可以使用冒号 : 来选取某个维度的所有元素。

以下是使用冒号索引的几种常见方式:

  • 获取所有外层数据,但只取内层的第一个元素x[:, :, 0]
    这行代码会返回一个形状为 (1, 3) 的张量,其值为 [[1, 4, 7]]。它选取了所有维度0和维度1的数据,但只取每个最内层列表的第一个元素。

  • 获取特定位置的外层和中间层数据,以及所有内层数据x[0, 0, :]
    这等价于 x[0, 0],会返回 [1, 2, 3]。它明确指定了要获取维度2的所有元素。

  • 更复杂的组合x[0, :, 1]
    这行代码选取了维度0的第一个元素,维度1的所有元素,以及维度2的索引为1的元素。结果会返回 [2, 5, 8]

实践挑战

为了巩固理解,请尝试对上面创建的张量 x 进行索引操作,完成以下两个挑战:

  1. 挑战一:通过索引获取数字 9
  2. 挑战二:通过一次索引操作获取数字 3, 6, 9

你可以尝试不同的索引组合,例如 x[0, 2, 2]x[0, :, 2],看看它们分别返回什么结果。


本节课中我们一起学习了PyTorch张量的数据索引。我们了解了如何使用数字进行精确索引,以及如何使用冒号 : 对某个维度进行全切片。这些操作是数据预处理和模型调试中提取特定信息的强大工具。请务必通过实践挑战来加深印象,我们下节课再见!

32:PyTorch张量与NumPy 🔄

在本节课中,我们将学习如何在PyTorch张量和NumPy数组之间进行转换。这是深度学习工作流中一个非常常见的操作,因为数据通常以NumPy数组的形式开始,而PyTorch模型则需要张量作为输入。


回顾与挑战

上一节我们介绍了张量的索引操作。我留下了一个挑战:如何从张量 x 中索引出数字 9 以及序列 [3, 6, 9]

以下是我的解决方案。请注意,解决这类问题有多种方法。

因为 x 的形状是 (1, 3, 3)。要选择 9,我们需要:

  • 0:选择第一个外层括号内的所有元素。
  • 2:选择底部这个元素。
  • 最后的 2:选择这个底部元素的第二个维度。

要选择 [3, 6, 9],我们需要:

  • 第一个维度的所有元素。
  • 第零个维度的所有元素。
  • 第一个维度的所有元素。
  • 然后选择 2,即 [3, 6, 9] 这个集合。

我建议你通过练习来掌握索引:创建一个任意形状的张量,然后尝试编写不同的索引语句来选择你指定的数字。


PyTorch张量与NumPy

现在,让我们进入本节课的核心内容:PyTorch张量与NumPy。

NumPy是一个极其流行的科学计算Python库。实际上,安装PyTorch时通常需要NumPy。由于这种紧密关系,PyTorch内置了与NumPy交互的功能。

在实际工作中,你的数据可能最初以NumPy数组(ndarray)的形式存在。但如果你想利用PyTorch的深度学习能力,就需要将其转换为PyTorch张量。

PyTorch为此提供了 torch.from_numpy() 方法,它接收一个NumPy数组并将其转换为PyTorch张量。

反之,如果你有一个PyTorch张量,并希望使用某些NumPy方法,你可以使用 .numpy() 方法将其转换回NumPy数组。


NumPy数组转PyTorch张量

让我们通过代码来实践。首先,我们导入必要的库并创建一个NumPy数组。

import torch
import numpy as np

# 创建一个NumPy数组
array = np.arange(1.0, 9.0)
print(f"NumPy数组: {array}")
print(f"数组数据类型: {array.dtype}")

# 将NumPy数组转换为PyTorch张量
tensor = torch.from_numpy(array)
print(f"PyTorch张量: {tensor}")
print(f"张量数据类型: {tensor.dtype}")

运行上述代码,你会发现张量的数据类型是 torch.float64。这是因为NumPy的默认数据类型是 float64,而 torch.from_numpy() 方法会保留原始数组的数据类型。

需要注意的是,PyTorch的默认张量数据类型是 torch.float32。如果你在后续计算中遇到数据类型不匹配的问题,可能需要在转换后手动调整数据类型。

# 将张量数据类型转换为float32
tensor = tensor.type(torch.float32)
print(f"转换后的张量数据类型: {tensor.dtype}")

重要提示:通过 torch.from_numpy() 创建的张量是原始数组数据的一个新副本。修改原始NumPy数组不会影响已创建的张量。

# 修改原始NumPy数组
array = array + 1
print(f"修改后的数组: {array}")
print(f"张量(未改变): {tensor}")

PyTorch张量转NumPy数组

接下来,我们看看如何将PyTorch张量转换回NumPy数组。

# 创建一个PyTorch张量
tensor = torch.ones(7)
print(f"PyTorch张量: {tensor}")
print(f"张量数据类型: {tensor.dtype}")

# 将张量转换为NumPy数组
numpy_tensor = tensor.numpy()
print(f"NumPy数组: {numpy_tensor}")
print(f"数组数据类型: {numpy_tensor.dtype}")

转换后的NumPy数组会继承原始张量的数据类型。由于我们创建的张量默认是 float32,所以转换后的数组也是 float32

同样,这种转换也会创建数据的副本。修改原始张量不会影响已转换的NumPy数组。

# 修改原始张量
tensor = tensor + 1
print(f"修改后的张量: {tensor}")
print(f"NumPy数组(未改变): {numpy_tensor}")

总结

在本节课中,我们一起学习了PyTorch张量与NumPy数组之间的相互转换:

  1. 从NumPy到PyTorch:使用 torch.from_numpy(array)。转换后的张量会保留NumPy数组的数据类型(默认为float64),请注意这可能与PyTorch的默认float32不同。
  2. 从PyTorch到NumPy:在张量上调用 .numpy() 方法。转换后的数组会继承张量的数据类型。
  3. 内存独立性:这两种转换都会创建数据的新副本。修改原始对象(无论是数组还是张量)不会影响已转换的对象。

掌握这两种数据结构之间的转换对于构建深度学习流水线至关重要,因为它允许你灵活地利用NumPy进行数据预处理,再使用PyTorch进行模型训练。

在下一节课中,我们将探讨深度学习中的一个重要概念:可复现性

33:PyTorch可复现性(消除随机性)🎯

在本节课中,我们将要学习PyTorch中一个非常重要的概念——可复现性。我们将探讨如何控制神经网络中的随机性,确保实验的结果可以被他人精确地复现。

概述

神经网络的学习过程始于随机数。简而言之,其学习流程是:从随机数开始 -> 执行张量运算 -> 更新随机数,使其更好地表示数据 -> 不断重复此过程。然而,在进行可复现的实验时,我们有时希望减少这种随机性。

随机性的挑战

到目前为止,每当我们创建一个随机张量,例如使用 torch.rand(3, 3),每次运行代码都会得到一组全新的随机数。这意味着,如果你将笔记本分享给朋友,他们运行代码时得到的结果很可能与你不同。

解决方案:随机种子

为了减少神经网络和PyTorch中的随机性,我们引入了随机种子的概念。其核心作用是“调味”随机性。由于计算机本质上是确定性的,它们会重复执行相同的步骤,因此我们使用的“随机”实际上是伪随机生成随机。随机种子正是用来控制这种伪随机性的。

实践:无种子的随机张量

让我们先看看没有设置随机种子时的情况。以下是创建两个随机张量并比较的代码:

import torch

# 创建两个随机张量
random_tensor_A = torch.rand(3, 4)
random_tensor_B = torch.rand(3, 4)

print(“随机张量 A:”, random_tensor_A)
print(“随机张量 B:”, random_tensor_B)
print(“A 和 B 是否相等:”, random_tensor_A.eq(random_tensor_B).any())

运行上述代码,你会得到两个充满随机值的张量。虽然理论上存在数值相等的可能性,但概率极低。这意味着每次运行,你和他人的结果都可能不同。

上一节我们看到了随机性带来的不确定性,本节中我们来看看如何使用随机种子来创造“可复现的随机”。

实践:使用随机种子

设置随机种子可以让我们获得可复现的“随机”结果。以下是具体步骤:

import torch

# 设置随机种子为42(这是一个常用值,代表“宇宙的答案”)
torch.manual_seed(42)

# 创建随机张量C
random_tensor_C = torch.rand(3, 4)

# 重要:在调用下一个随机方法前,需要重新设置种子
torch.manual_seed(42)

# 创建随机张量D
random_tensor_D = torch.rand(3, 4)

print(“随机张量 C:”, random_tensor_C)
print(“随机张量 D:”, random_tensor_D)
print(“C 和 D 是否相等:”, random_tensor_C.eq(random_tensor_D).any())

现在,这两个张量虽然看起来仍然是随机的,但其数值是确定且可复现的。任何使用相同随机种子(42)运行这段代码的人,都将得到与你完全相同的“随机”张量C和D。

关键点:在笔记本环境中,torch.manual_seed() 通常只对其后紧接着的一个代码块有效。如果你需要连续创建多个随机张量,必须在每次调用随机方法前重新设置种子。另一种常见做法是在一个代码单元格的开头设置一次种子,然后执行该单元格内的所有代码。

核心概念与资源

为了深入理解可复现性,以下是你应该了解的核心概念和推荐资源:

  • 伪随机数生成器:计算机通过确定性算法生成的、看似随机的数字序列。随机种子是这个算法的起始输入值。
  • PyTorch可复现性文档:这是关于此主题的权威指南。即使你现在不能完全理解其中的所有代码,也请务必知道这份资源的存在。
  • 随机种子(通用概念):随机种子并非PyTorch独有,它在NumPy等众多科学计算库中同样适用,是一个通用的计算机科学概念。

总结

本节课中我们一起学习了PyTorch的可复现性。我们了解到神经网络学习始于随机性,但通过设置 torch.manual_seed(),我们可以控制这种随机性,确保实验过程与结果能够被精确地复现,这对于分享研究、调试代码和确保实验一致性至关重要。记住,可复现性是机器学习和深度学习研究中一个非常重要的主题。

34:PyTorch GPU 访问方式详解 🚀

在本节课中,我们将学习如何在 PyTorch 中访问和使用 GPU 来加速计算。我们将介绍获取 GPU 的不同方式,如何检查 PyTorch 是否能访问 GPU,以及如何编写“设备无关”的代码,以确保你的代码能在有 GPU 时使用 GPU,没有时则自动回退到 CPU。

概述:为何使用 GPU?

GPU 能够显著加速数值计算。这得益于 NVIDIA 的 CUDA 编程接口、相应的硬件以及 PyTorch 在后台的协同工作。

上一节我们介绍了张量的基础操作,本节中我们来看看如何利用 GPU 来加速这些计算。

1. 获取 GPU 的几种方式

有多种方式可以获取用于深度学习的 GPU 计算资源。以下是三种主要途径:

  • 使用 Google Colab 获取免费 GPU:这是最简单且免费的方式。Colab 提供免费的 GPU 运行时,Colab Pro 和 Pro+ 则提供更快的 GPU 和更长的运行时间。本课程完全可以在免费版本上完成。
  • 使用自己的 GPU:这需要自行购买硬件并进行一些设置。你可以参考 Tim Dettmers 的博客文章来了解如何选择适合深度学习的 GPU。
  • 使用云计算服务:例如 Google Cloud Platform (GCP)、Amazon Web Services (AWS) 或 Microsoft Azure。这些服务允许你在云端租用配备 GPU 的计算机。

对于初学者,建议从 Google Colab 开始。我的个人工作流是:在 Colab 中进行小规模实验和学习,如果需要运行大型实验,则使用自己的深度学习主机或云计算服务。

2. 在 Google Colab 中设置 GPU

在 Google Colab 中启用 GPU 非常简单。以下是具体步骤:

  1. 在笔记本中,点击菜单栏的 “运行时”
  2. 选择 “更改运行时类型”
  3. “硬件加速器” 下拉菜单中,选择 “GPU”
  4. 点击 “保存”。这会重启运行时并连接到一个配备 GPU 的 Google 计算实例。

保存后,你可以运行 !nvidia-smi 命令来查看分配的 GPU 信息(例如 Tesla P100 或 K80)。

3. 检查 PyTorch 的 GPU 访问权限

连接 GPU 后,需要确认 PyTorch 能否识别并使用它。Google Colab 的另一个优势是它已为我们配置好了 PyTorch 与 NVIDIA GPU 之间的连接。

我们可以使用以下代码进行检查:

import torch
torch.cuda.is_available()

如果返回 True,则表明 PyTorch 可以访问 GPU。cuda 是 NVIDIA 的编程接口,它使得我们能够利用 GPU 进行数值计算。

4. 设置设备无关的代码

这是一个重要的 PyTorch 概念。由于你的代码可能在不同环境(有时有 GPU,有时没有)下运行,最佳实践是编写“设备无关”的代码。这意味着代码会自动在有 GPU 时使用 GPU,否则使用 CPU。

我们可以通过设置一个 device 变量来实现这一点:

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

这段代码的逻辑是:如果 torch.cuda.is_available() 为真(即 GPU 可用),则将设备设置为 "cuda";否则,设置为 "cpu"。在后续的代码中,我们可以将张量和模型移动到 device 变量所指定的设备上。

你还可以通过 torch.cuda.device_count() 来查看可用的 GPU 数量。这对于需要在多个 GPU 上分配大型模型或数据集的场景非常有用。

PyTorch 官方文档的“最佳实践”部分也推荐设置设备参数来编写设备无关的代码。其核心思想是:让代码能够灵活地在 GPU 或 CPU 上运行。

总结

本节课中我们一起学习了 PyTorch 中 GPU 访问的核心知识。我们了解了获取 GPU 资源的三种主要途径,掌握了在 Google Colab 中启用 GPU 的方法,学会了使用 torch.cuda.is_available() 检查 GPU 访问权限,并理解了编写“设备无关”代码 (device = "cuda" if torch.cuda.is_available() else "cpu") 的重要性及其实现方式。

在下一节课中,我们将具体学习如何将 PyTorch 张量和模型对象移动到指定的设备(GPU 或 CPU)上进行计算。

35:设备无关代码与 GPU 张量迁移 🚀

在本节课中,我们将学习如何编写设备无关的代码,以及如何将张量和模型在 CPU 与 GPU 之间进行迁移。掌握这些知识对于高效利用硬件资源、加速深度学习模型训练至关重要。

概述

上一节我们探讨了获取 GPU 资源的不同方式。现在,我们将具体学习如何利用 GPU 进行计算。核心在于将张量和模型放置到 GPU 上,因为 GPU 能显著加速数值计算,特别是我们即将频繁进行的张量运算。更快的计算意味着我们能更快地在数据中发现模式,进行更多实验,从而为手头的问题找到最佳模型。

将张量移至 GPU

首先,我们创建一个默认在 CPU 上的张量。

import torch

# 创建一个在 CPU 上的张量
tensor = torch.tensor([1, 2, 3], device=‘cpu’)
print(tensor)

即使不指定 device 参数,张量默认也创建在 CPU 上。为了利用 GPU 加速,我们需要将其移至目标设备。

以下是移动张量到 GPU 的步骤:

  1. 检查 GPU 可用性:我们之前已经设置了 device 变量,它根据环境自动指向 ‘cuda’(GPU)或 ‘cpu’。
  2. 使用 .to() 方法:这是 PyTorch 中移动张量(和模型)的核心方法。
# 将张量移动到目标设备(GPU 如果可用)
tensor_on_gpu = tensor.to(device)
print(tensor_on_gpu)

执行后,输出会显示张量现在位于 cuda:0 设备上。这里的 0 是 GPU 的索引号。当我们使用单个 GPU 时,索引始终为 0。这种设备无关代码的优点是,无论实际有没有 GPU,这段代码都不会报错,它会自动适配可用设备。

将张量移回 CPU

有时我们需要将张量移回 CPU,例如当需要使用 NumPy 库时,因为 NumPy 无法直接处理 GPU 上的张量。

如果我们尝试直接转换 GPU 上的张量为 NumPy 数组,会遇到错误。设备不匹配是 PyTorch 深度学习中常见的三大错误之一(另外两个是形状错误和数据类型错误)。

# 错误示例:尝试直接转换 GPU 张量为 NumPy
# numpy_array = tensor_on_gpu.numpy() # 这会引发错误

要解决这个问题,我们必须先将张量复制回主机内存(CPU)。

以下是修复步骤:

  1. 使用 .cpu() 方法:将 GPU 张量移回 CPU。
  2. 再进行转换:然后即可安全地转换为 NumPy 数组。
# 正确步骤:先移回 CPU,再转换为 NumPy
tensor_back_on_cpu = tensor_on_gpu.cpu()
numpy_array = tensor_back_on_cpu.numpy()
print(numpy_array)

需要注意的是,调用 .cpu() 方法会返回一个在 CPU 上的新张量副本,原始的 GPU 张量保持不变。

总结

本节课我们一起学习了 PyTorch 中设备无关编程的核心操作。我们了解了为何要将计算移至 GPU 以获得速度提升,掌握了使用 .to(device) 方法将张量和模型(后续课程会涉及)移至目标设备,并学会了在需要时使用 .cpu() 方法将张量安全移回 CPU 以进行后续处理(如使用 NumPy)。这些是使用 PyTorch 进行 GPU 加速计算的基础。虽然还有多 GPU 等高级主题,但掌握这些基础已足够我们开始构建和训练模型。

练习建议

为了巩固所学,建议你进行以下练习:

  • 在 Google Colab 中获取 GPU 访问权限,验证其可用性。
  • 编写设备无关的代码,创建一些示例张量,并尝试将它们设置到不同的设备上。
  • 故意制造一些错误,例如尝试对 GPU 上的张量进行 NumPy 运算,观察错误信息,然后练习如何正确地将这些张量移回 CPU 并成功转换。

通过动手实践,你将能更牢固地掌握这些关键概念。

36:PyTorch基础练习与拓展 📚

在本节课中,我们将回顾PyTorch基础部分的学习内容,并介绍如何通过练习和拓展资料来巩固所学知识。课程结尾会提供练习模板和解决方案的获取方式。


回顾与鼓励

上一节我们介绍了PyTorch的基础知识,现在你应该为自己感到自豪。我们已经学习了许多PyTorch的核心概念,这些将成为后续课程的基础模块。

在进入下一部分之前,我鼓励你通过练习和拓展课程来实践所学内容。

练习内容介绍

以下是基于已学内容设置的一些练习:

  1. 阅读文档:我们经常参考PyTorch官方文档,熟悉文档对学习至关重要。
  2. 创建随机张量:创建一个形状为 (7, 7) 的随机张量。
  3. 矩阵乘法:对上述张量与另一个随机张量执行矩阵乘法。

这些练习都基于我们已覆盖的知识点。我鼓励你参考相关笔记或视频中的代码来完成。

如何完成练习

你可以通过以下方式完成练习:

  1. 在Colab或本地新建一个笔记本。
  2. 导入PyTorch库:import torch
  3. 根据练习要求编写代码。

例如,创建随机张量的代码如下:

import torch
random_tensor = torch.rand(7, 7)

部分练习较为简单,部分会随着课程深入而变得更复杂。

练习模板与解决方案

如果你需要练习模板,可以访问课程GitHub仓库。在“extras/exercises”文件夹中,我提供了每个模块的练习模板。

例如,“PyTorch基础练习”模板中已经列出了所有练习的标题。你可以将模板链接复制到Google Colab中打开并开始练习。

每个课程模块的结尾都包含练习和拓展资料。练习以代码为主,拓展资料通常为阅读材料。

如果你在练习中遇到困难,可以先尝试独立解决,再参考已编写的代码。如果需要查看示例解决方案,可以访问“extras/solutions”文件夹。

但请记住,我鼓励你先尝试自己完成练习,至少尝试后再查看解决方案。

拓展学习建议

为了进一步巩固知识,建议你:

  1. 花一小时阅读PyTorch官方教程,特别是“快速入门”和“张量”部分。
  2. 观看视频《什么是张量?》,深入了解张量如何表示数据。

完成PyTorch基础部分是一个巨大的成就。我们下一节再见!


本节课中我们一起学习了如何通过练习和拓展资料来巩固PyTorch基础知识,并掌握了获取练习模板和解决方案的方法。

37:PyTorch工作流程与求助渠道 📚

在本节课中,我们将学习PyTorch深度学习的基本工作流程,并了解在学习过程中遇到问题时可以寻求帮助的渠道。


上一节我们介绍了PyTorch的基础知识,本节中我们来看看构建一个深度学习项目的典型步骤。

PyTorch工作流程是一个通用框架。当你深入深度学习领域时,会发现有多种方法可以实现目标,但以下是一个大致的核心流程:

以下是PyTorch工作流程的主要步骤:

  1. 准备数据:获取并整理数据。
  2. 数据张量化:将数据转换为张量(tensor),因为张量几乎可以表示任何类型的数据。
  3. 选择或构建模型:选择一个预训练模型或从头开始构建自己的模型。
  4. 选择损失函数和优化器:定义模型如何衡量错误(损失函数)以及如何更新自身以减少错误(优化器)。
  5. 构建训练循环:编写代码来迭代训练数据,让模型学习。
  6. 模型拟合:通过训练循环,使模型适应我们的数据以进行预测。
  7. 模型评估:学习如何评估训练好的模型的性能。
  8. 实验与改进:通过实验调整参数,尝试改进模型。
  9. 保存与加载模型:保存训练好的模型,以便从笔记本中导出并在其他地方使用。

了解了工作流程后,学习过程中难免会遇到问题。以下是遇到困难时可以采取的求助步骤。

以下是获取帮助的有效途径:

  1. 动手实践:最好的学习方式是跟随代码一起编写和运行。首要原则是:如有疑问,就运行代码。亲自尝试、犯错、再尝试,直到成功。
  2. 查阅文档字符串:使用 Shift + Command + Space(Mac)或 Ctrl(Windows/Colab)查看函数文档,了解其用法。
  3. 搜索问题:如果仍然卡住,尝试搜索问题。你可能会找到诸如 Stack OverflowPyTorch官方文档 等资源,这些将是整个课程中最重要的参考依据。
  4. 再次尝试:不要轻易放弃,多次尝试是学习的一部分。
  5. 提问:如果以上方法都无法解决问题,可以在课程专属的 GitHub讨论区github.com/mrdbourke/pytorch-deep-learning/discussions)提问。提问时,请注明视频编号和正在运行的代码,以便他人参考并提供帮助。

此外,本课程还提供了配套的书面材料作为参考。

不要忘记,本课程有对应的书籍版本(learnpytorch.io)。视频内容正是基于这些章节制作的。这些材料可以作为你自主学习的参考资料,但我们的课程重点将放在一起编写代码上。


说到编写代码,让我们开始实践吧。下一节我们将在Google Colab中相见,一起动手编码。


本节课中我们一起学习了PyTorch深度学习项目的基本工作流程,涵盖了从数据准备到模型保存的完整步骤。同时,我们也明确了在学习过程中遇到困难时,可以通过动手实践、查阅文档、搜索资源和社区提问等多种渠道来解决问题。现在,我们已经为接下来的实战编码做好了准备。

38:环境配置与内容规划 🚀

在本节课中,我们将学习如何设置PyTorch开发环境,并规划一个完整的端到端深度学习项目流程。我们将从零开始,在Google Colab中创建一个新的笔记本,并导入必要的库,为后续的实战编码做好准备。


概述 📋

我们将遵循一个标准的机器学习工作流程,涵盖从数据准备到模型部署的核心步骤。具体来说,我们将学习:

  1. 数据准备与加载。
  2. 使用PyTorch构建机器学习或深度学习模型。
  3. 将模型拟合到数据上,即训练模型。
  4. 使用训练好的模型进行预测并评估其性能。
  5. 保存和加载训练好的模型。
  6. 将所有步骤整合在一起。

本节课的目标是搭建好开发环境,并清晰地理解我们将要执行的每一步。


创建新笔记本与标题

首先,我们访问 colab.research.google.com 并创建一个新的笔记本。

我将这个笔记本命名为 01_pytorch_workflow,并注明它源自本视频课程。这样,在课程资源中,你可以清楚地知道这个笔记本的来源。

在课程资源中,我们提供了原始的参考笔记本,其中包含丰富的图片和文本注释。视频将主要聚焦于代码实现,而你可以将原始笔记本作为参考。

因此,我将在笔记本开头链接这两个资源:

  • 原始参考笔记本
  • 书籍版本的笔记本(内容相同,格式不同)

最后,我还会添加一个链接,指向课程讨论页面,方便提问。


课程核心内容规划

上一节我们设置了笔记本的基本信息,本节中我们来看看本课程将要涵盖的具体内容。

我们将遵循一个PyTorch端到端工作流程。与花费大量时间讲解幻灯片不同,我更倾向于直接一起编写代码,并在需要时解释相关概念。这更贴近实际使用PyTorch进行开发的方式。

以下是本课程将要覆盖的六个核心步骤:

  1. 数据准备与加载:学习如何获取和格式化数据以供模型使用。
  2. 构建模型:学习如何使用PyTorch的构建块创建机器学习或深度学习模型。
  3. 训练模型:学习如何将模型拟合到数据上。“拟合”是机器学习中“训练”的另一种说法。
  4. 评估模型:学习如何使用训练好的模型进行预测并评估其性能。进行预测也常被称为“推理”。
  5. 保存与加载模型:学习如何保存训练好的模型,以便后续重新加载和使用。
  6. 整合所有步骤:将以上所有步骤组合成一个完整的工作流程。

这个流程与我们稍后会深入学习的完整PyTorch工作流程略有不同。完整的流程还包括“通过实验改进模型”等环节,我们将在后续课程中重点探讨。


导入必要的依赖库

在开始编码之前,我们需要导入必要的Python库。以下是本课程所需的依赖项:

import torch
from torch import nn
import matplotlib.pyplot as plt

# 检查PyTorch版本
print(f“PyTorch版本:{torch.__version__}”)
  • torch:导入PyTorch主库。
  • torch.nnnn模块包含了构建神经网络的所有基础构建块。在PyTorch文档中,你可以看到nn模块提供了用于构建计算图(对于神经网络,即网络层结构)的各种组件。作为数据科学家和机器学习工程师,我们的任务就是组合这些构建块来创建各种神经网络。
  • matplotlib.pyplot:用于数据可视化。我们的信条是:可视化、可视化、再可视化。
  • torch.__version__:检查当前PyTorch版本。确保版本至少为1.10(如果版本更高,代码通常也能正常运行)。输出中的“CUDA”表示是否支持GPU加速,目前我们的运行时尚未启用GPU,后续可能会进行设置。

总结 🎯

本节课中,我们一起学习了如何为PyTorch深度学习项目设置开发环境。我们创建了一个新的Colab笔记本,规划了从数据准备到模型保存的完整端到端工作流程,并导入了所有必要的库(torchtorch.nnmatplotlib)。

现在,我们的“视频笔记本”已经准备就绪,拥有了清晰的路线图和所需的工具。在下一节课中,我们将正式进入第一个实战环节:数据准备与加载

39:使用线性回归公式创建简单数据集 📊

在本节课中,我们将学习PyTorch深度学习工作流程的第一步:数据的准备与加载。我们将通过创建一个简单的线性回归数据集来演示这一过程。


概述:机器学习的两部分游戏 🎮

机器学习,包括深度学习,本质上可以看作一个由两部分组成的游戏。

  1. 将数据转换为数值表示:无论数据是Excel表格、图像、视频、音频、DNA序列还是文本,第一步都是将其转换为数值形式(通常是张量)。
  2. 构建模型以学习数值表示中的模式:然后,我们构建一个模型(如神经网络)来学习这些数值数据中的模式、特征或权重。

模型的最终目标是输出学到的表示,并基于此进行预测或分类,例如判断一张图片是拉面还是意大利面,或者一条推文是否为垃圾信息。

上一节我们介绍了机器学习的基本框架,本节中我们来看看如何动手创建我们的第一个数据集。


创建已知参数的线性回归数据 📈

为了清晰地展示上述过程,我们将从一个已知的、简单的数据集开始。我们将使用线性回归公式来生成一条直线数据。

线性回归的标准公式通常表示为:
y = a + bX
其中:

  • X 是自变量(解释变量)。
  • y 是因变量。
  • b 是直线的斜率(在深度学习中常称为权重)。
  • a 是截距(在深度学习中常称为偏置)。

我们将手动设定权重和偏置的值,然后根据公式生成对应的 Xy 数据。我们的目标是让后续构建的模型能够从 Xy 的对应关系中,学习并逼近我们预先设定的这些参数值。

以下是创建数据的具体步骤:

import torch

# 1. 设定已知参数(模型最终要学习的目标)
weight = 0.7  # 斜率 b
bias = 0.3    # 截距 a

# 2. 创建输入数据 X:生成从0到1,步长为0.02的一系列数值
start = 0
end = 1
step = 0.02
X = torch.arange(start, end, step).unsqueeze(dim=1) # unsqueeze添加一个维度,便于后续计算

# 3. 根据线性公式计算输出数据 y
y = weight * X + bias

# 4. 查看生成的数据
print(f"前10个X值:\n{X[:10]}")
print(f"\n前10个y值:\n{y[:10]}")
print(f"\nX的长度: {len(X)}")
print(f"y的长度: {len(y)}")

代码解析:

  • torch.arange(start, end, step) 创建了一个一维张量。
  • .unsqueeze(dim=1) 的作用是在索引为1的维度上增加一个维度。例如,将形状 [50] 变为 [50, 1]。这通常是为了让数据的形状符合模型对输入格式的要求(例如,每个样本是一个特征)。
  • 我们完全知晓 weightbias 的值(0.7和0.3),但后续的模型将不知道这些,它需要从 Xy 中自己发现这个关系。

运行上述代码,我们将得到50对 (X, y) 数据点。模型的任务就是观察这些 X 值,并学习预测出对应的 y 值,进而推断出背后 y = 0.7*X + 0.3 的关系。


总结与下节预告 🎯

本节课中我们一起学习了机器学习工作流的基础,并亲手创建了一个用于线性回归的简单数据集。我们明确了两个核心步骤:将数据数值化,以及构建模型学习模式。同时,我们通过代码实现了:

  • 定义了模型的权重偏置参数。
  • 生成了自变量 X 和因变量 y 的张量数据。

目前,我们的数据还只是“页面上的数字”,不够直观。在下一节课中,我们将遵循数据探索者的格言:“可视化你的数据”,通过绘图来更直观地观察我们刚刚创建的 Xy 之间的关系。

40:划分训练集与测试集 📊

在本节课中,我们将学习机器学习中一个至关重要的概念:如何将数据划分为训练集和测试集。我们将通过一个简单的线性回归数据示例,来实践这一划分过程,并理解其背后的核心思想。


概述

上一节我们使用线性回归公式和一些已知参数创建了一些数据。本节中,我们将介绍机器学习中最核心的概念之一:将数据划分为训练集和测试集。这是评估模型泛化能力的关键步骤。


机器学习中的核心概念

在机器学习中,数据通常被划分为三个主要部分:训练集、验证集和测试集。这类似于大学的学习过程:

  • 训练集:相当于课程的全部学习材料。模型从这里学习数据中的模式。
  • 验证集:相当于期中考试或模拟考试。用于在训练过程中调整和评估模型,看其是否从训练材料中学到了东西。
  • 测试集:相当于期末考试。用于最终评估模型在从未见过的数据上的表现,检验其泛化能力。

泛化的定义是:机器学习或深度学习模型在未见过的数据上表现良好的能力。我们的最终目标正是构建一个能在训练数据上学习模式,并能在部署后对新数据做出有效预测的模型。


数据划分的常见比例

以下是数据划分的典型比例:

  • 训练集:通常占数据的 60% 到 80%
  • 验证集(如果使用):通常占数据的 10% 到 20%
  • 测试集:通常占数据的 10% 到 20%

训练集和测试集是必须的,而验证集则经常使用,但并非总是必需。对于我们的简单数据集,我们将只创建训练集和测试集。


实践:划分我们的数据

我们之前创建了包含50个样本的数据(X 和 y)。我们将采用一个非常常见的比例:80% 用于训练,20% 用于测试

首先,我们计算训练集的大小:

train_split = int(0.8 * len(X))  # 计算80%分界点的索引

接下来,我们使用索引来划分特征(X)和标签(y):

# 划分训练集(前80%的数据)
X_train, y_train = X[:train_split], y[:train_split]

# 划分测试集(后20%的数据)
X_test, y_test = X[train_split:], y[train_split:]

现在,让我们检查划分后的数据大小:

len(X_train), len(y_train), len(X_test), len(y_test)

结果应该是 (40, 40, 10, 10)。这意味着我们有40个训练样本(特征和标签)和10个测试样本。


重要说明

  • 我们这里使用的划分方法非常简单直接。在实际项目中,更常用的是像 Scikit-learn 的 train_test_split 这样的函数,它可以为划分过程引入随机性,使结果更可靠。
  • 创建恰当的训练集和测试集是机器学习中最大的挑战之一,因此理解这个概念至关重要。

总结

本节课中,我们一起学习了机器学习数据划分的核心概念。我们了解了训练集、验证集和测试集的作用,并亲自动手将一个简单的数据集按 80/20 的比例划分成了训练集和测试集。记住,测试集的目的是评估模型的泛化能力,即模型处理新数据的能力。

在下一节,我们将探索如何将这些“纸上的数字”可视化,以便更直观地理解我们的数据和模型。

41:构建数据可视化函数 📊

在本节课中,我们将学习如何构建一个数据可视化函数,以便更直观地理解我们的训练数据和测试数据。通过可视化,我们可以清晰地看到数据之间的关系,为后续构建机器学习模型打下基础。


概述

上一节我们将数据分割成了训练集和测试集。本节中,我们将构建一个名为 plot_predictions 的函数,用于可视化这些数据。这个函数将帮助我们直观地观察数据点,并理解模型预测的目标。


构建可视化函数

为了探索和理解数据,一个有效的方法是将其可视化。因此,我们创建一个函数来绘制数据点。

以下是 plot_predictions 函数的定义:

def plot_predictions(train_data=X_train,
                     train_labels=Y_train,
                     test_data=X_test,
                     test_labels=Y_test,
                     predictions=None):
    """
    绘制训练数据、测试数据,并比较预测结果。
    """
    plt.figure(figsize=(10, 7))

    # 绘制训练数据,用蓝色表示
    plt.scatter(train_data, train_labels, c="b", s=4, label="Training data")

    # 绘制测试数据,用绿色表示
    plt.scatter(test_data, test_labels, c="g", s=4, label="Testing data")

    # 如果存在预测结果,则用红色绘制
    if predictions is not None:
        plt.scatter(test_data, predictions, c="r", s=4, label="Predictions")

    # 显示图例
    plt.legend(prop={"size": 14})

函数参数说明

该函数接受以下参数:

  • train_data:训练数据的特征值。
  • train_labels:训练数据的标签值。
  • test_data:测试数据的特征值。
  • test_labels:测试数据的标签值。
  • predictions:模型的预测结果,初始设置为 None

可视化结果分析

运行该函数后,我们得到一张散点图。图中蓝色点代表训练数据,绿色点代表测试数据。

我们构建机器学习模型的目标是:让模型学习蓝色数据点(训练数据)中 XY 之间的关系。然后,当我们向模型输入绿色数据点的 X 值(测试数据特征)时,模型能够预测出对应的 Y 值。

一个完美的模型,其预测结果(红色点)应该与绿色点(测试数据的真实标签)完全重合。这背后的数据关系,实际上是由我们之前定义的线性公式生成的:Y = weight * X + bias,也就是大家熟知的线性方程 y = mx + c


总结

本节课我们一起学习了如何构建一个数据可视化函数。通过这个函数,我们能够直观地看到训练集和测试集的分布情况,并理解了模型预测的最终目标——让预测值尽可能接近测试集的真实值。在下一节中,我们将开始构建一个能够学习这种数据关系的模型。

42:创建首个 PyTorch 线性回归模型 🧠

在本节课中,我们将学习如何从零开始构建一个 PyTorch 线性回归模型。我们将创建一个能够学习数据中潜在模式的模型,并使用它进行预测。


回顾与准备

上一节我们介绍了如何可视化数据,并遵循了“可视化、可视化、再可视化”的数据探索者格言。我们了解了训练数据和测试数据,目标是构建一个模型来学习训练数据中的模式(主要是这里的上升趋势),以便预测测试数据。

现在,让我们开始构建我们的第一个 PyTorch 模型。


构建线性回归模型类

我们将直接进入代码部分。首先,创建一个线性回归模型类。如果你不熟悉 Python 类,没关系,我会在编写代码时解释每一步。但如果你想深入了解,我推荐阅读 Real Python 关于 Python 类和面向对象编程(OOP)的教程。

以下是创建模型类的步骤:

  1. 导入必要的模块:我们使用 torch.nn 模块,它是构建 PyTorch 模型的基础。
  2. 定义模型类:创建一个继承自 nn.Module 的类。在 PyTorch 中,几乎所有模型都继承自这个基类,它提供了构建模型所需的大量内置功能。
  3. 初始化参数:在类的构造函数 __init__ 中,我们初始化模型的参数(权重和偏置)。这些参数开始时是随机值。
  4. 定义前向传播方法:在 forward 方法中,我们定义模型如何根据输入数据计算输出。

以下是具体的代码实现:

import torch
from torch import nn

# 创建线性回归模型类
class LinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        # 初始化权重参数,使用随机值并启用梯度计算
        self.weights = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float32))
        # 初始化偏置参数,使用随机值并启用梯度计算
        self.bias = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float32))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # 定义前向计算:y = weights * x + bias
        return self.weights * x + self.bias

模型工作原理详解

现在,让我们详细解释一下上面代码的每个部分及其背后的原理。

1. 继承 nn.Module

nn.Module 是 PyTorch 中所有神经网络模块的基类。通过继承它,我们的模型自动获得了 PyTorch 框架提供的许多功能,例如参数管理、梯度计算等。

2. 参数初始化

我们使用 nn.Parameter 来定义模型的参数(weightsbias)。nn.Parametertorch.Tensor 的一个子类,具有一个特殊属性:当它被赋值给一个模块属性时,会自动被添加到模块的参数列表中,便于后续的优化器访问和更新。

  • torch.randn(1):生成一个来自标准正态分布的随机数作为参数的初始值。
  • requires_grad=True:这是关键设置。它告诉 PyTorch 需要计算这个张量(参数)的梯度。梯度是优化算法(如梯度下降)用来更新参数、使模型表现更好的关键信息。
  • dtype=torch.float32:将数据类型设置为 PyTorch 默认且高效使用的 32 位浮点数。

3. 前向传播方法

forward 方法定义了数据如何通过模型。对于线性回归,这就是线性公式:
output = weights * input + bias
这正是我们创建原始数据时使用的公式。模型的目标是学习出接近真实数据生成过程(即我们预设的 weightbias)的权重和偏置值。


模型的学习目标

为了更清晰地理解模型在做什么,让我们回顾一下机器学习的基本流程:

  1. 起点:模型从随机初始化的权重和偏置开始。
  2. 观察数据:模型查看训练数据(输入 X_train 和标签 y_train)。
  3. 调整参数:通过算法(梯度下降和反向传播),模型不断调整其随机的权重和偏置值。
  4. 目标:使调整后的参数值尽可能接近(甚至完美匹配)我们用来生成数据的真实参数值。

这就是机器学习的核心:从随机开始,通过查看数据,让模型自动找到数据背后的规律

PyTorch 的伟大之处在于,它已经为我们实现了复杂的优化算法(如梯度下降和反向传播)。我们只需要编写高级代码来定义模型结构和数据流,PyTorch 会自动处理底层的梯度计算和参数更新。


总结

本节课中,我们一起学习了:

  • 如何创建一个继承自 nn.Module 的 PyTorch 模型类。
  • 如何使用 nn.Parameter 定义并初始化模型的可学习参数(权重和偏置)。
  • 如何在 forward 方法中实现线性回归的前向计算逻辑。
  • 理解了模型 requires_grad=True 参数的重要性,它启用了自动梯度计算,这是模型能够学习的基础。
  • 明确了模型的学习目标:从随机参数出发,通过观察训练数据,逐步调整参数以拟合数据中的真实模式。

在下一节中,我们将更深入地探讨 PyTorch 的核心模块,并开始训练这个模型,观察它如何从随机状态学习到有意义的参数。

43:解析线性回归模型运行机制 🧠

在本节课中,我们将深入解析线性回归模型的运行机制,理解PyTorch如何通过面向对象编程构建模型,并探索模型参数、梯度跟踪以及前向传播的核心概念。


概述

上一节我们介绍了如何创建一个继承自nn.Module的PyTorch模型,并讨论了面向对象编程在PyTorch中的广泛应用。本节中,我们将进一步解析模型的内部机制,包括参数初始化、梯度跟踪以及前向传播的实现。


模型构建基础

在PyTorch中构建模型时,我们通常会继承nn.Module类。这个类包含了神经网络的所有基础构建块。以下是我们上一节创建的线性回归模型代码:

import torch
from torch import nn

class LinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float32))
        self.bias = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float32))
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.weight * x + self.bias

参数初始化与梯度跟踪

在模型构造函数中,我们初始化了模型参数。对于简单的数据集,我们可以显式定义权重和偏置参数。但在更复杂的场景中,如处理图像数据,我们可能不会手动定义这些参数,而是使用nn模块中的层来自动处理。

以下是参数初始化的关键点:

  • 随机初始值:模型开始时使用随机值作为权重和偏置,例如通过torch.randn()生成。
  • 梯度跟踪:通过设置requires_grad=True,PyTorch会自动跟踪这些参数的梯度,以便在反向传播中使用。
  • 数据类型:我们明确指定了数据类型为torch.float32,虽然PyTorch默认会处理这些设置,但显式定义有助于保持代码的清晰性。


前向传播方法

任何继承自nn.Module的子类都必须重写forward方法。这个方法定义了模型在每次前向传播中执行的计算。在我们的线性回归模型中,forward方法实现了线性回归函数:

公式y = weight * x + bias

当我们将数据传入模型时,forward方法会自动被调用,执行上述计算。


核心概念总结

以下是本节课的核心概念总结:

  • 模型继承:PyTorch模型通过继承nn.Module类来构建。
  • 参数初始化:在构造函数中初始化模型所需的参数或层。
  • 梯度跟踪:设置requires_grad=True以启用自动梯度计算。
  • 前向传播:重写forward方法定义模型的计算逻辑。


下一步计划

在接下来的课程中,我们将进一步探索PyTorch模型构建的要点,包括检查模型内容、使用模型进行预测等。通过动手实践,你将更深入地理解模型的运行机制。


总结

本节课中,我们一起学习了线性回归模型的内部运行机制,包括参数初始化、梯度跟踪和前向传播的实现。理解这些基础概念是掌握PyTorch深度学习的关键步骤。在下一节课中,我们将继续深入,动手实践模型的使用和预测。

44:核心PyTorch模型构建类详解 🧱

在本节课中,我们将详细解析构建PyTorch模型时最核心的几个类。理解这些基础组件是掌握PyTorch的关键。

在上一节视频中,我们逐步创建了第一个PyTorch模型。虽然看起来涉及不少内容,但主要的收获是:几乎所有的PyTorch模型都继承自 nn.Module。如果你要继承 nn.Module,就必须重写 forward 方法,以定义模型内部的计算过程。

后续,当我们的模型开始学习时(即通过梯度下降和反向传播,将其权重和偏置值从随机值更新为更符合数据的值),我们将编写代码来触发这些过程。目前,我们只是定义了一个包含前向计算过程的模型。

说到模型,让我们来看看几个PyTorch模型构建的核心要素。本节视频不会编写太多代码,内容相对简短,旨在介绍你将在PyTorch中交互的一些主要类,其中一些我们已经见过了。

以下是几个核心的PyTorch构建模块:

  • torch.nn:包含构建计算图(即神经网络)的所有基础模块。
  • torch.nn.Parameter:定义模型应该尝试学习的参数。通常,PyTorch的 torch.nn 层会为我们设置这些参数。
  • torch.nn.Module:所有神经网络模块的基类。如果你继承它,就必须重写 forward 方法,正如我们之前所做的那样。
  • torch.optim:优化器所在之处。它们将协助进行梯度下降。优化器包含了算法,能够将模型的随机参数优化为能更好表示我们数据的值。
  • torch.utils.data:我们尚未详细讨论,但后续会用到。当你拥有更复杂的数据集时,DatasetDataLoader 会非常有帮助。它们能协助我们加载数据。

关于 nn.Moduleforward 方法,需要明确的是:所有 nn.Module 的子类都要求你重写 forward。这个方法定义了前向计算的过程。在我们的例子中,如果向线性回归模型传递一些数据,forward 方法会接收这些数据并执行我们定义的计算。随着模型变得越来越大、越来越复杂,这个前向计算过程可以根据你的需求变得简单或复杂。

PyTorch是一个相当庞大的库。本节课的额外学习建议是花5到10分钟浏览PyTorch速查表,阅读其中的内容。你不需要立刻全部理解,我们将在后续通过编写代码来逐渐熟悉它们。

本节课我们一起学习了PyTorch中构建神经网络模型的核心类,包括 torch.nntorch.nn.Moduletorch.optimtorch.utils.data。理解这些组件是有效使用PyTorch的基础。

在下一节视频中,我们将实际创建一个线性回归模型的实例,看看会发生什么。

45:探究模型内部结构 🔍

在本节课中,我们将深入探究我们构建的第一个PyTorch模型的内部结构。我们将学习如何查看模型的参数,理解这些参数是如何初始化的,并思考模型如何通过学习数据来调整这些参数。


上一节我们介绍了PyTorch模型构建的基本要素。本节中,我们来看看如何检查我们刚刚构建的线性回归模型内部有什么。

首先,为了确保结果的可复现性,我们需要设置一个随机种子。这是因为模型的参数通常使用随机值进行初始化。

torch.manual_seed(42)

接下来,我们创建模型的一个实例。

model_0 = LinearRegressionModel()

现在,让我们看看模型内部有什么。我们可以使用 .parameters() 方法来查看模型的参数。

list(model_0.parameters())

这会返回一个生成器,将其转换为列表后,我们可以看到模型包含两个参数:权重(weight)和偏置(bias)。这些参数目前是随机初始化的张量,并且 requires_grad=True,这意味着PyTorch会跟踪这些张量的所有操作,以便后续进行梯度计算和反向传播。

为了更好地理解参数的结构,我们可以查看模型的状态字典。

model_0.state_dict()

这会返回一个字典,清晰地展示了参数 weightbias 的名称及其当前的随机值。

以下是理解模型参数的关键点:

  • 模型的参数(权重和偏置)在创建时被随机初始化。
  • 深度学习的核心过程就是:从随机值开始,通过观察训练数据并利用梯度下降和反向传播算法,不断调整这些参数,使它们尽可能接近能够准确描述数据关系的理想值。
  • 在我们的简单例子中,我们预先知道理想参数(weight=0.7, bias=0.3)。但在大多数实际场景中,我们并不知道理想值是什么,模型的目标就是通过训练去发现它们。

本节课中我们一起学习了如何检查PyTorch模型的内部参数,理解了模型参数随机初始化的意义,并建立了深度学习模型通过调整参数来拟合数据的基本概念。在下一节,我们将使用这个尚未训练的、参数随机的模型进行预测,并观察其预测效果。

46:使用推理模式进行随机模型预测 🧠

在本节课中,我们将学习如何使用PyTorch的推理模式(torch.inference_mode())来评估一个随机初始化模型的预测能力。我们将看到,由于模型参数是随机初始化的,其初始预测结果会非常不准确。


回顾与过渡

上一节我们探讨了第一个PyTorch模型的内部结构。我们发现,由于我们使用torch.randn()创建模型参数,这些参数初始值是随机的。深度学习的核心前提正是从随机数开始,然后根据数据逐步优化这些数字,使其变得不那么随机,更接近理想值。

在开始优化这些参数之前,本节我们来看看它们当前的预测能力如何。


使用推理模式进行预测

为了检查模型的预测能力,我们需要看看它在测试数据X_test上的表现如何。机器学习模型的另一个前提是:接收一些特征作为输入,并输出接近真实标签的预测值。

当我们把数据传入模型时,模型会执行其forward方法。以下是进行预测的步骤:

  1. 使用torch.inference_mode()上下文管理器。
  2. 将测试数据X_test传入模型。
  3. 获取预测结果y_preds

以下是核心代码:

with torch.inference_mode():
    y_preds = model_0(X_test)

代码解释

  • torch.inference_mode():一个上下文管理器,用于在推理(预测)时关闭梯度跟踪,以提高效率。
  • model_0(X_test):这实际上会调用我们定义的forward方法,将X_test作为输入x传入。

注意:在PyTorch代码中,forward方法的输入参数通常命名为x,代表输入数据。


处理常见错误

在执行上述代码时,你可能会遇到一个常见的错误:NotImplementedError。这通常是由于代码缩进问题导致的。

解决方法
确保forward方法的定义与类定义正确对齐。在Google Colab等环境中,有时需要手动调整缩进。请检查并确保forward方法体相对于class定义有正确的缩进级别。


可视化预测结果

现在,让我们将模型的预测结果y_preds与真实的测试标签y_test进行比较并可视化。

plot_predictions(predictions=y_preds)

结果分析
由于模型参数是随机初始化的,其预测结果(红色点)与真实测试数据(绿色点)相差甚远。一个理想的模型,其预测点(红色)应该与真实数据点(绿色)基本重合。

目前,我们的模型做出的基本上是随机预测。


理解推理模式

我们使用了torch.inference_mode(),它与直接调用模型(y_preds = model_0(X_test))有什么区别?

主要区别在于梯度跟踪

  • 直接调用:输出张量会附带一个.grad_fn属性,这意味着PyTorch仍在跟踪计算图,为梯度下降和反向传播做准备。
  • 使用inference_mode():输出张量没有.grad_fn属性。它关闭了梯度跟踪。

为什么在推理时关闭梯度跟踪?

  1. 提升速度:在推理(预测)阶段,我们不需要计算或更新梯度。关闭梯度跟踪可以减少PyTorch在后台需要记录的数据量,从而加快预测速度,尤其是在处理大型数据集时。
  2. 减少内存占用:不保存训练时所需的中间数据,可以节省内存。

历史用法
torch.inference_mode()出现之前,常用的是torch.no_grad()。两者功能相似,但inference_mode()在性能上更有优势,是当前推荐用于推理的方法。

# 旧方法(仍可使用,但不推荐为首选)
with torch.no_grad():
    y_preds_old = model_0(X_test)

想了解更多关于推理模式的细节,可以查阅PyTorch官方文档或相关技术博客。


本节总结

在本节课中,我们一起学习了以下内容:

  1. 使用推理模式:我们学会了使用torch.inference_mode()上下文管理器来进行模型预测。
  2. 评估初始模型:我们验证了使用随机参数初始化的模型,其预测能力几乎等同于随机猜测。
  3. 理解推理优化:我们探讨了在推理时关闭梯度跟踪(requires_grad=False)的重要性,它能提升预测速度并节省内存。
  4. 可视化对比:通过绘图,我们直观地看到了模型当前糟糕的预测结果(红色点)与理想目标(绿色点)之间的巨大差距。

目前,我们的模型预测结果远不理想。在接下来的课程中,我们将编写PyTorch训练代码,通过查看训练数据来优化模型参数,目标是让这些红色预测点逐渐向绿色的真实数据点靠拢。

47:模型训练直觉(必备要素)🧠

在本节课中,我们将学习训练一个机器学习模型所需的核心要素。我们将探讨模型如何从随机参数开始,通过特定的机制逐步改进其预测能力,最终逼近理想状态。

上一节我们介绍了如何构建一个简单的线性回归模型,并观察到其初始预测效果不佳。本节中,我们来看看如何通过训练来改进模型。

训练的核心思想

训练的根本目的是让模型从一组未知的(通常是随机的)参数,逐步调整到一组已知的、理想的参数。换句话说,是让模型对数据的表征从较差的状态改进到更好的状态。

以我们的模型为例,其预测的红点与真实的绿点之间存在明显偏差,这表明当前的表征是较差的。

衡量预测误差:损失函数

为了量化模型预测的“错误”程度,我们需要一个衡量标准。以下是衡量模型预测错误程度的方法:

  • 损失函数:一个用于衡量模型预测值与理想输出值之间差异的函数。差异越小(损失值越低),模型性能越好。

在PyTorch中,损失函数有时也被称为代价函数准则。其核心作用是提供一个可量化的指标,指导模型的优化方向。

调整模型参数:优化器

仅仅知道预测有多“错”还不够,我们需要一个机制来修正它。以下是调整模型参数以改进预测的方法:

  • 优化器:优化器的作用是考虑模型的损失值,并据此调整模型的参数(在我们的例子中,即权重 weight 和偏置 bias),以降低损失函数的值。

我们可以通过 model.state_dict() 查看模型的当前参数。优化器将根据损失值,智能地更新这些参数。

训练与评估流程

在PyTorch中,完整的模型训练通常包含两个循环过程:

  1. 训练循环:在此循环中,模型在训练数据上进行前向传播计算预测值,计算损失,通过反向传播计算梯度,最后优化器利用梯度更新模型参数。
  2. 测试循环:在此循环中,模型在未见过的测试数据上进行前向传播以评估其泛化性能,此阶段不更新模型参数。

这些核心概念——损失函数和优化器——是通用的。无论模型只有两个参数(如我们的线性模型)还是有数百万个参数(如复杂的计算机视觉模型),其训练的基本原理都是相通的。

本节课中我们一起学习了模型训练的三大必备直觉要素:训练的目标是改进参数使用损失函数量化误差以及利用优化器调整参数。在接下来的课程中,我们将深入探讨如何为我们的问题选择合适的损失函数和优化器,并开始编写训练代码。

48:配置优化器与损失函数 🧠⚙️

在本节课中,我们将学习如何为PyTorch模型配置两个核心组件:损失函数优化器。它们是训练循环的心脏,负责衡量模型预测的好坏,并指导模型参数进行调整以提升性能。

上一节我们介绍了模型参数的概念,本节中我们来看看如何通过损失函数和优化器来优化这些参数。

损失函数:衡量模型的“错误”程度

损失函数(Loss Function)的核心作用是量化模型预测值与真实值之间的差距。我们的训练目标就是最小化这个差距。

在PyTorch中,损失函数位于 torch.nn 模块下。针对不同类型的问题(如回归、分类),需要选择不同的损失函数。

以下是PyTorch中一些常见的损失函数:

  • L1Loss / MAE:用于回归问题,计算平均绝对误差。
  • MSELoss:用于回归问题,计算均方误差。
  • BCELoss:用于二分类问题,计算二元交叉熵。
  • CrossEntropyLoss:用于多分类问题,计算交叉熵。

对于我们的线性回归示例,我们将使用 L1Loss,它等同于平均绝对误差(MAE)。其计算公式可以表示为:

MAE = mean( | y_pred - y_true | )

在代码中,我们可以这样设置损失函数:

import torch.nn as nn

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/daniel-bourke/img/167c45768dc22cba7c867c4bcb50003c_6.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/daniel-bourke/img/167c45768dc22cba7c867c4bcb50003c_7.png)

# 设置损失函数为平均绝对误差 (L1Loss)
loss_fn = nn.L1Loss()

优化器:指导模型参数更新

优化器(Optimizer)的作用是根据损失函数计算出的“错误”程度,来调整模型的参数(如权重和偏置),目标是找到一组能使损失值最小的参数。

优化器位于 torch.optim 模块中。最常用的是随机梯度下降(SGD)及其改进版本Adam

以下是设置优化器时需要考虑的关键点:

  • 参数(params):需要优化器来调整的模型参数,通常通过 model.parameters() 获取。
  • 学习率(lr):这是最重要的超参数之一。它决定了优化器每次更新参数的步长大小。
    • 学习率太小:收敛速度慢。
    • 学习率太大:可能导致无法收敛或震荡。

在代码中,我们可以这样设置优化器:

import torch.optim as optim

# 设置优化器为随机梯度下降 (SGD)
# 需要传入要优化的模型参数和学习率
optimizer = optim.SGD(params=model.parameters(), lr=0.01)

损失函数与优化器如何协同工作

损失函数和优化器在训练过程中紧密配合:

  1. 前向传播:模型根据当前参数做出预测。
  2. 计算损失:损失函数计算预测值与真实值之间的误差。
  3. 反向传播:计算损失相对于每个模型参数的梯度(即,每个参数对总误差的“贡献度”)。
  4. 参数更新:优化器利用这些梯度信息,按照学习率指定的步长,更新模型参数以降低损失。

这个过程会循环往复,直到模型性能达到满意水平或训练轮次结束。

总结

本节课中我们一起学习了:

  1. 损失函数的作用是衡量模型预测的错误程度,PyTorch在 torch.nn 中提供了多种选择。
  2. 优化器的作用是根据损失函数的反馈来更新模型参数,PyTorch在 torch.optim 中提供了多种算法。
  3. 对于回归问题,常用 L1Loss(MAE)MSELoss 作为损失函数。
  4. SGD(随机梯度下降) 是一种基础且常用的优化器。
  5. 学习率(lr) 是优化器的一个关键超参数,控制着参数更新的步长。

现在,我们已经准备好了损失函数和优化器。下一节,我们将把这些组件整合起来,构建一个完整的训练循环,亲眼见证模型如何从随机参数开始学习。

49:PyTorch训练流程步骤与原理 🚀

在本节课中,我们将要学习PyTorch模型训练的核心流程,包括前向传播、损失计算、反向传播和参数优化。我们将一步步构建一个完整的训练循环,并理解其背后的数学原理。


概述

上一节我们设置了损失函数和优化器。本节中,我们将把这些组件整合起来,构建一个完整的训练循环。这个循环将反复调整模型参数,以最小化预测误差。


训练循环的核心步骤

一个典型的PyTorch训练循环包含以下关键步骤。以下是每个步骤的简要说明:

  1. 前向传播:数据通过模型的forward方法,生成预测。
  2. 计算损失:将模型的预测结果与真实标签进行比较,计算误差。
  3. 反向传播:计算损失相对于每个模型参数的梯度。
  4. 优化器步骤:优化器根据计算出的梯度更新模型参数。

步骤详解与代码实现

1. 设置训练轮次

首先,我们需要定义训练轮次(epoch)。一个epoch代表模型完整遍历一次训练数据。

epochs = 1  # 这是一个超参数,由我们设定

2. 设置模型为训练模式

PyTorch模型有两种主要模式:训练模式和评估模式。在训练开始前,我们需要将模型设置为训练模式。

model_0.train()  # 设置模型为训练模式

训练模式会确保模型的所有参数都启用梯度追踪(requires_grad=True),这是反向传播计算梯度所必需的。

3. 构建训练循环

现在,我们进入核心的训练循环。以下是循环内每一步的代码实现:

for epoch in range(epochs):
    # 1. 前向传播 (Forward Pass)
    y_pred = model_0(X_train)

    # 2. 计算损失 (Calculate Loss)
    loss = loss_fn(y_pred, y_train)

    # 3. 优化器梯度清零 (Zero Grad)
    optimizer.zero_grad()

    # 4. 反向传播 (Loss Backward / Backpropagation)
    loss.backward()

    # 5. 优化器步骤 (Optimizer Step / Gradient Descent)
    optimizer.step()

核心概念解析

前向传播与反向传播

  • 前向传播:数据从输入层流向输出层,模型根据当前参数进行计算并做出预测。
  • 反向传播:这是一个算法,用于计算损失函数相对于每个模型参数的梯度。你可以将其理解为找出每个参数对总误差“贡献”了多少,以及应该朝哪个方向调整才能减少误差。

梯度与梯度下降

  • 梯度:在数学上,梯度是一个向量,指向函数值增长最快的方向。在机器学习中,我们关注的是损失函数的梯度。
  • 梯度下降:优化器(如SGD)使用的算法。其核心思想是沿着损失函数梯度的反方向更新参数,因为这是使损失函数值下降最快的方向。

直观比喻:想象你站在一座山上(损失值很高),目标是走到山谷底部(损失值为0)。梯度告诉你山坡最陡的方向(上升最快)。梯度下降则让你朝相反的方向(下降最快)迈出一步。学习率(lr)决定了这一步迈多大。

公式表示(参数更新)
新参数 = 旧参数 - 学习率 * 梯度


优化器与损失函数的选择

上一节我们介绍了优化器和损失函数的基本角色。这里补充一些选择经验:

  • 回归问题(如本课预测房价):常使用L1损失torch.nn.L1Loss)或MSE损失,配合SGD优化器torch.optim.SGD)。
  • 分类问题(如图片是猫还是狗):常使用交叉熵损失(如torch.nn.BCELoss),配合Adam优化器torch.optim.Adam)可能效果更好。

具体选择需要根据问题和经验进行调整。


总结

本节课中我们一起学习了构建PyTorch训练循环的完整流程。我们了解了前向传播如何做出预测,损失函数如何衡量误差,反向传播如何计算梯度,以及优化器如何利用梯度下降算法更新模型参数以降低损失。

记住这个核心循环:前向传播 -> 计算损失 -> 梯度清零 -> 反向传播 -> 优化器更新。在接下来的课程中,我们将实际运行这个循环,观察模型是如何从随机预测一步步学习到数据规律的。

50:编写PyTorch训练循环代码 🧠

在本节课中,我们将学习如何编写一个完整的PyTorch训练循环。我们将一步步地实现前向传播、损失计算、梯度清零、反向传播和优化器更新这五个核心步骤。


概述

训练循环是模型学习的核心。它通过重复执行一系列步骤,使模型能够从数据中学习并改进其预测能力。我们将从零开始构建一个训练循环,并理解每个步骤的作用。

上一节我们讨论了训练循环背后的重要概念,本节中我们来看看如何用代码实现它。


训练循环的五个核心步骤

以下是构建一个PyTorch训练循环需要执行的五个主要步骤:

  1. 前向传播:将训练数据通过模型的forward方法,生成预测值。
  2. 计算损失:使用损失函数计算模型预测值与真实标签之间的误差。
  3. 梯度清零:将优化器中累积的梯度归零,为新一轮的反向传播做准备。
  4. 反向传播:计算损失相对于模型每个参数的梯度。
  5. 优化器更新:根据计算出的梯度,使用优化算法(如梯度下降)更新模型参数。


代码实现

现在,让我们用代码实现上述步骤。假设我们已经定义了一个名为model_0的模型、一个损失函数loss_fn和一个优化器optimizer

# 1. 前向传播:模型根据训练数据X_train做出预测
y_pred = model_0(X_train)

# 2. 计算损失:比较预测值(y_pred)和真实标签(y_train)
loss = loss_fn(y_pred, y_train)

# 3. 梯度清零:防止梯度在循环中累积
optimizer.zero_grad()

# 4. 反向传播:计算损失关于模型参数的梯度
loss.backward()

# 5. 优化器更新:根据梯度调整模型参数
optimizer.step()

步骤详解与过渡

我们已经写下了训练循环的代码框架。接下来,让我们更深入地理解每个步骤,特别是容易混淆的梯度清零步骤。

1. 前向传播

前向传播是模型进行预测的过程。代码y_pred = model_0(X_train)调用了模型的forward方法,输入训练数据X_train,输出预测结果y_pred

2. 计算损失

损失函数量化了模型预测的“错误”程度。我们使用平均绝对误差(MAE,在PyTorch中为L1Loss)来计算y_predy_train之间的差距。其公式为:
loss = mean(|y_pred - y_train|)

3. 梯度清零 (optimizer.zero_grad())

这个步骤至关重要。在默认情况下,PyTorch优化器计算的梯度会在每次.backward()调用时进行累积。这意味着如果不手动清零,第二次循环的梯度会加到第一次的梯度上,以此类推。
这通常不是我们想要的行为,因为每次迭代我们都希望基于当前批次的损失来独立地更新参数。因此,在每次反向传播之前,我们需要调用optimizer.zero_grad()将梯度重置为零。

4. 反向传播 (loss.backward())

反向传播是深度学习的引擎。loss.backward()这个调用会从最终的损失值开始,利用链式法则,自动计算损失函数相对于模型中每一个可训练参数的梯度。这些梯度指明了参数应该如何调整才能减小损失。

5. 优化器更新 (optimizer.step())

优化器(如SGD、Adam)根据上一步计算出的梯度来更新模型参数。例如,在随机梯度下降中,更新规则是:
parameter = parameter - learning_rate * parameter.grad
这就是“梯度下降”发生的地方,模型由此向更优的方向迈出一步。


实践与总结

本节课中我们一起学习了如何构建一个PyTorch训练循环。我们实现了五个核心步骤:前向传播、计算损失、梯度清零、反向传播和优化器更新。理解这个循环是掌握模型训练的关键。

为了巩固知识,建议你尝试自己动手编写这个循环,并观察模型的输出。在下一节课中,我们将为这个训练循环添加评估步骤,并观察模型在整个训练过程中的表现变化。

记住这个训练循环的口诀,它能帮助你回忆步骤顺序:“前向传播,计算损失,梯度清零,反向传播,优化更新”。


额外练习

  1. 在不看笔记的情况下,独立重写上面的训练循环代码。
  2. 尝试搜索并观看关于“反向传播”和“梯度下降”原理的讲解视频,加深对这两个核心概念的理解。

51:逐步解析训练循环步骤 🚀

在本节课中,我们将详细解析PyTorch训练循环的每一步。我们将回顾之前视频中介绍的内容,并深入理解每个步骤背后的原理,为后续的测试环节做好准备。

概述 📋

训练循环是深度学习模型学习的核心过程。它涉及将数据多次(通过多个“轮次”)传递给模型,并基于模型的预测误差调整其内部参数。我们将逐步拆解这个过程,确保你理解每个环节的作用。

训练循环步骤详解 🔄

上一节我们介绍了训练循环的整体概念,本节中我们来看看每个具体步骤。

1. 设置模型为训练模式

在开始训练循环之前,我们需要将模型设置为训练模式。这是通过调用 model.train() 实现的。

model.train()

此操作会设置模型参数的一系列后台配置,特别是启用梯度跟踪,这是后续优化步骤所必需的。PyTorch为我们自动处理了许多此类设置。

2. 前向传播

在训练循环中,我们对训练数据执行前向传播。这是模型在训练数据上学习模式的关键步骤。

y_pred = model(X_train)

前向传播会调用我们在模型类中定义的 forward 方法。因为我们的线性回归模型是 nn.Module 的子类,所以需要自定义前向传播逻辑。从技术上讲,前向传播意味着数据从网络输入层流向输出层。

3. 计算损失值

接下来,我们计算损失值,以衡量模型预测的错误程度。

loss = loss_fn(y_pred, y_train)

损失值取决于你使用的损失函数、模型的输出类型以及真实标签。这一步的核心是将模型在训练数据上的预测结果与理想值(即训练标签)进行比较。

4. 优化器梯度归零

在每次训练步骤开始时,我们需要将优化器中的梯度归零。

optimizer.zero_grad()

这样做的原因是,优化器计算的梯度会随时间累积。在每个训练步骤(或轮次)中,我们希望梯度从零开始重新计算,以确保当前步骤的更新只基于当前批次的误差。梯度累积的具体原因涉及PyTorch内部的计算优化。

5. 损失反向传播

这是训练循环中的关键步骤,我们通过调用 loss.backward() 来执行反向传播。

loss.backward()

反向传播会计算模型中每个设置了 requires_grad=True 的参数的梯度。我们之前设置 requires_grad=True 正是为了让PyTorch能够跟踪这些参数的梯度。

为了理解梯度,我们可以将其想象成一个损失函数曲线(例如均方误差MSE或平均绝对误差MAE)。我们的目标是找到这个曲线的最低点(即损失最小)。梯度表示了曲线在某一点的斜率或陡峭程度。

以下是反向传播与梯度下降的核心概念:

  • 梯度:一个多变量函数的导数,表示函数在该点的变化率和方向。
  • 梯度下降:通过计算梯度,我们沿着梯度的相反方向更新参数,以逐步逼近损失函数的最低点。
  • 学习率:一个超参数,定义了优化器在每次更新参数时的步长大小。步长太大会越过最低点,步长太小则收敛过慢。

6. 优化器执行参数更新

最后一步是让优化器根据计算出的梯度更新模型参数。

optimizer.step()

优化器执行一步“下降”,尝试调整参数以使损失值降低。它使用我们之前设置的学习率来决定这一步的幅度。

步骤总结与顺序 📝

以下是训练循环中六个核心步骤的总结,以及它们的关键顺序:

  1. 设置模式model.train()
  2. 前向传播:计算预测 y_pred = model(X)
  3. 计算损失loss = loss_fn(y_pred, y_true)
  4. 梯度归零optimizer.zero_grad()
  5. 反向传播loss.backward()
  6. 参数更新optimizer.step()

顺序至关重要:必须先进行前向传播才能计算损失;必须在反向传播之后,优化器才能根据新梯度执行更新。一个常见的记忆口诀是:“前向传播,计算损失,优化器归零,损失反向传播,优化器更新”。

核心概念回顾 🎯

本节课中我们一起学习了训练循环的完整步骤:

  • 训练循环:模型通过多个轮次(epoch)在数据上学习的过程。
  • 前向传播:数据通过网络得到预测结果。
  • 损失函数:量化模型预测与真实值之间的差异。
  • 反向传播:计算损失函数相对于每个模型参数的梯度。
  • 梯度下降:利用梯度信息,沿反方向更新参数以最小化损失。
  • 优化器与学习率:优化器执行参数更新,学习率控制更新的步长。

理解这些步骤是构建和训练有效神经网络模型的基础。在接下来的课程中,我们将一步步执行这个循环,并观察模型的参数是如何随着每个步骤而发生变化的。

52:逐轮次运行训练循环与效果观察 🧠

在本节课中,我们将深入探讨PyTorch训练循环的核心机制。我们将手动执行多个训练轮次,直观地观察模型参数如何更新、损失值如何下降,以及模型的预测能力如何逐步提升。


概述

上一节我们介绍了训练循环的基本结构。本节中,我们将手动逐轮次运行这个循环,并观察每一步中模型参数、损失值和预测结果的变化。这是理解模型如何“学习”的关键一步。

训练循环的核心步骤回顾

训练循环的核心步骤可以概括为以下公式和代码:

核心公式: 参数_新 = 参数_旧 - 学习率 * 梯度

核心代码流程:

# 1. 前向传播计算预测值
y_pred = model(X_train)

# 2. 计算损失(预测值与真实值的差距)
loss = loss_fn(y_pred, y_train)

# 3. 将优化器的梯度归零
optimizer.zero_grad()

# 4. 反向传播计算梯度
loss.backward()

# 5. 根据梯度更新参数(梯度下降)
optimizer.step()

初始化模型与优化器

首先,我们重新实例化模型,以确保从随机的初始参数开始。我们同时设置损失函数和优化器。

以下是关键设置:

  • 模型: LinearRegressionModel
  • 损失函数: L1Loss (即MAE)
  • 优化器: SGD,学习率设为0.01

初始的模型参数(权重和偏置)是随机值,因此最初的预测会非常不准确。

手动执行训练轮次并观察

现在,让我们手动执行多个训练轮次,并打印出每一步的变化。

以下是每个训练轮次后我们需要观察的内容列表:

  • 损失值: 衡量模型预测的错误程度,值越低越好。
  • 模型参数: 即权重和偏置,观察它们是否向真实值(本例中已知)靠近。
  • 预测结果: 通过绘图直观对比预测值(红点)与真实值(绿点)的接近程度。

执行几轮后,我们观察到:

  1. 损失值从初始的较高值开始持续下降。
  2. 模型的权重和偏置参数逐步调整,向真实值逼近。
  3. 预测结果的红点逐渐向代表真实值的绿线靠拢。

这个过程展示了梯度下降在起作用:优化器根据损失函数计算出的梯度,反复调整参数,以最小化损失。

梯度下降可视化 🎯

为了更直观地理解,我们可以将梯度下降想象成下山的过程。

我们从一个随机的初始权重(山腰某点)开始。通过计算损失函数的梯度(最陡的下山方向),优化器引导参数朝着损失最低的山谷(最优解)移动。每一步的步长由学习率控制。

挑战与下一步

我们仅手动运行了约10个轮次,模型预测已有明显改善。

给你的挑战:
修改代码,让训练循环自动运行100个轮次。观察损失值能降到多低,并可视化最终的预测结果,看看红点是否几乎与绿线重合。

通过这个练习,你将巩固对训练循环和梯度下降的理解。


总结

本节课中我们一起学习了如何逐轮次运行训练循环。我们看到了在反向传播和梯度下降的驱动下,模型的损失值如何降低,参数如何优化,预测能力如何逐步提升。这是训练任何深度学习模型的核心过程。在下一课中,我们将引入测试循环,学习如何评估训练好的模型在未见数据上的表现。

53:编写测试循环代码与逐步解析 🧪

在本节课中,我们将学习如何为PyTorch模型编写测试循环代码,并详细解析其每一步的作用。上一节我们介绍了训练循环,本节中我们来看看如何评估模型在未见过的数据上的表现。

概述

测试循环用于评估训练好的模型在测试数据集上的性能。与训练循环不同,测试循环不涉及模型参数的更新,其主要目的是验证模型从训练数据中学到的模式是否能泛化到新数据上。

测试循环代码解析

以下是测试循环的核心步骤及其作用。

1. 设置模型为评估模式

在测试开始前,需要将模型切换到评估模式。

model.eval()

此方法会关闭模型中某些在评估或测试时不需要的设置,例如Dropout层和BatchNorm层。这些层在训练和评估时的行为不同。

2. 启用推理模式

接下来,我们使用torch.inference_mode()上下文管理器。

with torch.inference_mode():
    # 测试代码

此模式会关闭梯度追踪以及其他一些后台操作,从而加速推理代码的执行。在旧版PyTorch代码中,你可能会看到torch.no_grad(),它实现类似功能,但inference_mode是更优、更快的方式。

3. 执行前向传播

在推理模式下,我们对测试数据进行前向传播以获取预测结果。

test_preds = model(X_test)

这里,X_test代表测试特征数据。模型利用从训练数据中学到的参数来对这些新数据进行预测。

4. 计算测试损失

使用与训练时相同的损失函数,计算模型预测与测试数据真实标签之间的差异。

test_loss = loss_fn(test_preds, y_test)

这个test_loss值衡量了模型在未见过的测试数据上的表现。理想情况下,我们希望测试损失尽可能低,这表明模型具有良好的泛化能力。

5. 打印输出结果

为了监控测试过程,我们可以定期(例如每10个训练周期)打印出损失和模型参数等信息。

if epoch % 10 == 0:
    print(f"Epoch: {epoch} | Test loss: {test_loss:.5f}")

核心概念回顾

以下是测试循环中涉及的核心概念总结:

  • model.eval():切换模型至评估模式,关闭Dropout等训练专用层。
  • torch.inference_mode():关闭梯度追踪,优化推理速度。
  • 前向传播:模型根据输入数据计算输出,公式为 y_pred = model(X)
  • 测试损失:衡量模型预测与测试集真实值的差异,公式为 loss = loss_fn(y_pred, y_test)

总结

本节课中我们一起学习了如何构建PyTorch模型的测试循环。我们了解到,测试循环的核心在于评估而非学习,因此需要关闭梯度计算和模型中的特定训练层。通过定期计算测试损失,我们可以监控模型在未知数据上的泛化能力,这是判断模型是否过拟合或欠拟合的关键。在下一节课中,我们将探讨如何进一步优化模型,使其预测结果更加准确。

54:逐步解析测试循环流程 📊

在本节课中,我们将深入解析 PyTorch 深度学习中的测试循环流程。我们将回顾如何训练模型、评估其性能,并可视化训练过程中的关键指标,例如损失曲线。


概述

上一节我们训练了一个模型来预测红点和绿点,并取得了不错的效果。本节中,我们将详细拆解测试循环的每一步,学习如何跟踪模型性能,并可视化训练与测试损失的变化。


回顾训练过程

我们之前训练模型 100 个周期,预测结果显著改善。为了进一步优化,我们可以继续训练模型。模型会从上次训练结束时的参数开始,尝试进一步改进。

核心代码:训练循环

for epoch in range(num_epochs):
    model.train()
    y_pred = model(X_train)
    loss = loss_fn(y_pred, y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

这是深度学习的核心步骤:训练和评估模型。我们已成功应用这些步骤来预测数据点。


延长训练周期

为了将红点与绿点对齐,我们可以将训练周期增加到 200 个。重新运行训练代码,模型将从已优化的参数开始,进一步减少损失。

训练后,损失值从约 0.02 降至 0.008,测试损失从 0.0056 降至 0.005,提升了近 10 倍。模型参数接近理想值(权重 ~0.7,偏差 ~0.3)。

理想参数公式:
[
y = 0.7x + 0.3
]

模型预测结果非常接近目标,虽然仍有微小误差,但已足够展示训练效果。


测试循环详解

现在,让我们用图文并茂的方式解析测试循环。测试循环用于评估模型在未见数据上的性能。

以下是测试循环的关键步骤:

  1. 创建空列表存储指标:用于跟踪模型进度,比较不同实验的结果。
  2. 设置模型为评估模式:关闭训练特有的功能(如 Dropout 和 Batch Normalization)。
  3. 启用 Torch 推理模式:禁用梯度跟踪,提升推理性能。
  4. 将测试数据传入模型:调用模型的 forward 方法进行预测。
  5. 计算测试损失:衡量模型在测试集上的预测误差。
  6. 打印并记录指标:监控训练过程,记录周期数、训练损失和测试损失。

测试循环核心代码:

model.eval()
with torch.inference_mode():
    test_pred = model(X_test)
    test_loss = loss_fn(test_pred, y_test)

可视化损失曲线

跟踪损失值有助于直观理解模型的学习过程。我们绘制了训练损失和测试损失随周期变化的曲线。

绘图代码:

import matplotlib.pyplot as plt
plt.plot(epoch_count, loss_values, label="Train Loss")
plt.plot(epoch_count, test_loss_values, label="Test Loss")
plt.legend()
plt.show()

理想情况下,损失曲线应随时间下降。如果训练损失和测试损失同步下降且接近,说明模型收敛良好。这是我们追求的“完美下降曲线”。


总结

本节课我们一起学习了测试循环的完整流程。我们回顾了如何通过延长训练周期来优化模型,详细解析了测试循环的每一步,并学会了通过绘制损失曲线来监控模型性能。

记住核心循环的步骤:设置模型模式、进行前向传播、计算损失、记录指标。这些步骤是构建和评估所有深度学习模型的基础。


附:PyTorch 优化循环口诀

for epoch in range(num_epochs):
    model.train()
    # 前向传播
    # 计算损失
    # 反向传播
    # 优化器步进
    model.eval()
    with torch.inference_mode():
        # 测试前向传播
        # 计算测试损失
    # 打印状态

建议您亲自运行代码,尝试调整参数,并查阅额外资料加深理解。我们下节课再见!

55:保存与加载模型 🚀

在本节课中,我们将学习如何保存和加载训练好的 PyTorch 模型。这是将你的工作成果持久化、与他人分享或在未来项目中复用的关键一步。

概述

在上一节中,我们训练了一个模型并评估了其预测效果。然而,如果我们的开发环境(如 Google Colab)断开连接,所有代码和训练好的模型都会丢失。本节将介绍三种核心方法来解决这个问题:torch.save()torch.load()torch.nn.Module.load_state_dict()

保存模型的三种核心方法

以下是 PyTorch 中用于保存和加载模型的三个主要方法:

  1. torch.save()

    • 此方法允许你将 PyTorch 对象以 Python 的 pickle 格式保存到磁盘。序列化(serializing)即保存的过程。
  2. torch.load()

    • 此方法用于加载一个已保存的 PyTorch 对象。反序列化(deserializing)即加载的过程。
  3. torch.nn.Module.load_state_dict()

    • 此方法至关重要,它允许你加载一个模型的已保存状态字典(state dictionary)。

理解状态字典(State Dict)

在深入代码之前,我们需要理解什么是“状态字典”。PyTorch 的一个优点是,它将模型所有可学习的参数(如权重和偏置)存储在一个简单的 Python 字典对象中,这就是状态字典。

你可以通过以下代码查看模型的状态字典:

print(model_0.state_dict())

对于复杂模型,这个字典可能包含数百万个参数,但基本原理不变:它是一个映射了模型每一层到其参数张量的字典。

代码实现:保存模型

现在,让我们通过代码来实际保存我们的模型。我们将遵循推荐的做法:保存模型的状态字典,而非整个模型对象。

首先,我们需要导入必要的库并创建用于保存模型的目录。

import torch
from pathlib import Path

# 1. 创建模型保存目录
MODEL_PATH = Path("models")
MODEL_PATH.mkdir(parents=True, exist_ok=True)

# 2. 创建模型保存路径
MODEL_NAME = "01_pytorch_workflow_model_0.pth"
MODEL_SAVE_PATH = MODEL_PATH / MODEL_NAME

# 3. 保存模型的状态字典
print(f"正在保存模型到: {MODEL_SAVE_PATH}")
torch.save(obj=model_0.state_dict(), f=MODEL_SAVE_PATH)

执行以上代码后,你的模型状态字典就会被保存到指定的 .pth 文件中。你可以刷新文件浏览器来确认文件已生成。

挑战与总结

本节课我们一起学习了保存 PyTorch 模型的基础知识。我们介绍了三种核心方法,并重点实现了通过保存状态字典来持久化模型。

你的挑战是: 在下一节课开始前,请尝试阅读 PyTorch 官方文档中关于“保存和加载整个模型”的部分,并思考与仅保存状态字典相比,其优缺点各是什么。

在下一节中,我们将学习如何使用 torch.load() 将保存的模型加载回来,并验证其功能是否完好。

56:加载PyTorch模型代码实现 🚀

在本节课中,我们将学习如何加载一个已保存的PyTorch模型。我们将使用之前保存的模型状态字典,创建一个新的模型实例,并将保存的参数加载进去,最后验证加载的模型是否与原始模型等效。


概述

上一节我们介绍了如何保存PyTorch模型的状态字典。本节中,我们来看看如何加载这个已保存的状态字典,并将其应用到新的模型实例中。

加载模型状态字典

为了加载已保存的模型状态字典,我们需要遵循以下步骤:

  1. 创建一个与原始模型结构相同的新模型实例。
  2. 使用 torch.load() 方法加载保存的状态字典文件。
  3. 使用模型的 load_state_dict() 方法将加载的状态字典应用到新模型实例上。

以下是具体实现代码:

# 1. 创建一个新的模型实例
loaded_model_0 = LinearRegressionModel()

# 2. 加载保存的状态字典
loaded_model_0.load_state_dict(torch.load(f=MODEL_SAVE_PATH))

运行上述代码后,如果状态字典的键与模型参数匹配成功,控制台会输出相应的成功信息。

验证加载的模型

加载模型后,我们需要验证其是否与原始模型具有相同的预测能力。为此,我们可以将两个模型都设置为评估模式,并对相同的测试数据进行预测,然后比较预测结果。

以下是验证步骤:

  1. 将原始模型和加载的模型都设置为评估模式。
  2. 在推理模式下,使用测试数据 X_test 进行预测。
  3. 比较两个模型的预测结果 y_preds 是否相等。
# 将模型设置为评估模式
model_0.eval()
loaded_model_0.eval()

# 在推理模式下进行预测
with torch.inference_mode():
    y_preds = model_0(X_test)
    loaded_model_preds = loaded_model_0(X_test)

# 比较预测结果
print(y_preds == loaded_model_preds)

如果一切正确,比较结果应全部为 True,表明两个模型的预测完全一致。

总结

本节课中我们一起学习了PyTorch模型加载的核心流程。我们首先创建了一个新的模型实例,然后使用 torch.load()load_state_dict() 方法加载了之前保存的模型状态字典。最后,我们通过比较预测结果验证了加载的模型与原始模型是等效的。掌握模型的保存与加载是部署和复用模型的关键步骤。

57:配置设备无关代码实践环境 🚀

在本节课中,我们将学习如何配置设备无关的代码实践环境。我们将回顾之前学过的PyTorch工作流程,并重点介绍如何编写能够在GPU和CPU上无缝运行的代码。

概述

在之前的视频中,我们已经学习了PyTorch工作流程的多个步骤,包括数据处理、模型构建、训练和评估等。本节课,我们将把这些步骤整合起来,并引入设备无关代码的概念,以确保我们的代码能够在不同的硬件环境中高效运行。

回顾PyTorch工作流程

上一节我们介绍了PyTorch的基本工作流程,本节中我们来看看如何将这些步骤整合到一个完整的实践中。

首先,我们回顾一下已经学过的步骤:

  1. 数据准备:将数据转换为张量。
  2. 模型构建:使用PyTorch的nn模块构建模型。
  3. 损失函数和优化器:选择合适的损失函数和优化器。
  4. 训练循环:编写训练和测试循环。
  5. 模型评估:通过可视化方法评估模型性能。
  6. 模型保存与加载:保存训练好的模型以便后续使用。

配置设备无关代码

现在,我们将重点介绍如何配置设备无关的代码环境。设备无关代码意味着无论是否有GPU可用,我们的代码都能正常运行。如果有GPU,代码将利用GPU进行加速计算;如果没有GPU,代码将默认使用CPU。

以下是配置设备无关代码的步骤:

  1. 导入必要的库。
  2. 检查GPU是否可用。
  3. 设置设备变量,根据可用性选择GPU或CPU。

代码实现

首先,我们导入所需的库:

import torch
from torch import nn
import matplotlib.pyplot as plt

接下来,我们检查PyTorch的版本,以确保代码兼容性:

print(torch.__version__)

然后,我们配置设备无关代码:

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

这段代码会检查CUDA(NVIDIA的GPU编程框架)是否可用。如果可用,设备设置为cuda(即GPU),否则设置为cpu

在Google Colab中使用GPU

如果你使用Google Colab,可以通过以下步骤启用GPU:

  1. 点击菜单栏中的“运行时”。
  2. 选择“更改运行时类型”。
  3. 在“硬件加速器”下拉菜单中选择“GPU”。
  4. 保存设置并重新启动运行时。

重启后,运行所有代码单元格,设备变量将自动设置为cuda(如果GPU可用)。

总结

本节课中,我们一起学习了如何配置设备无关的代码实践环境。我们回顾了PyTorch的工作流程,并重点介绍了如何编写能够在GPU和CPU上无缝运行的代码。通过配置设备无关代码,我们可以确保模型在不同硬件环境中都能高效运行,为后续的深度学习实践打下坚实基础。

在接下来的课程中,我们将使用这个设备无关的环境,重新构建并训练模型,进一步巩固所学知识。

58:全流程整合(第一部分):数据准备 📊

在本节课中,我们将学习如何为线性回归模型准备一个简单的数据集。我们将从零开始创建数据,并将其拆分为训练集和测试集,为后续构建和训练模型打下基础。


创建数据

上一节我们设置了设备无关的代码。本节中,我们来看看如何创建用于模型训练的数据。

我们将使用线性回归公式来生成数据。线性回归的基本公式是:

y = weight * x + bias

这个公式你可能也见过 y = mx + cy = mx + b 等形式。虽然变量名不同,但它们描述的是同一个数学关系。

以下是生成数据的代码:

# 定义权重和偏置
weight = 0.7
bias = 0.3

# 创建特征值 x,范围从 0 到 1,步长为 0.02
start = 0
end = 1
step = 0.02
X = torch.arange(start, end, step).unsqueeze(dim=1)

# 根据公式生成标签 y
y = weight * X + bias

在这段代码中,weightbias 是我们希望模型最终能够学习到的真实参数。我们创建了一个从 0 到 1 的等间隔向量作为特征 X,并通过公式计算出对应的标签 y。使用 .unsqueeze(dim=1) 是为了将一维张量转换为二维张量,以避免后续出现维度不匹配的问题。


拆分数据

数据创建好后,我们需要将其拆分为训练集和测试集。这是机器学习中的一个标准步骤,用于评估模型在未见过的数据上的表现。

以下是拆分数据的步骤:

首先,确定训练集的比例。我们使用 80% 的数据进行训练,剩下的 20% 用于测试。这个比例可以根据数据量和具体问题进行调整。

# 设置训练集比例
train_split = int(0.8 * len(X))

# 拆分数据
X_train, y_train = X[:train_split], y[:train_split]
X_test, y_test = X[train_split:], y[train_split:]

拆分后,我们可以检查一下数据集的尺寸。按照 80/20 的比例,训练样本大约有 40 个,测试样本大约有 10 个。在实际项目中,数据量会大得多,但拆分比例通常是相似的。


可视化数据

数据准备完成后,将其可视化是一个好习惯。这能帮助我们直观地理解数据的分布和关系。

以下是绘制数据的说明:

我们可以使用一个预先定义好的绘图函数 plot_predictions 来可视化我们的训练数据(蓝色点)和测试数据(绿色点)。

plot_predictions(train_data=X_train,
                 train_labels=y_train,
                 test_data=X_test,
                 test_labels=y_test)

如果运行代码时出现错误,提示找不到 plot_predictions 函数,那是因为该函数定义在之前的单元格中。你需要确保已经运行过包含该函数定义的单元格,或者将函数定义代码复制到当前笔记本中再运行。

绘制出的图表将显示一系列蓝色点(训练数据)和绿色点(测试数据)。我们的目标是构建一个模型,能够根据蓝色点学习规律,并准确预测绿色点的位置。


本节总结

本节课中我们一起学习了机器学习流程的第一步:数据准备。

我们使用线性回归公式手动创建了一个合成数据集,并将其按 80/20 的比例拆分成了训练集和测试集。最后,我们通过可视化确认了数据的线性关系。

现在,我们已经拥有了结构清晰的训练数据和测试数据。在下一节中,我们将进入流程的下一步:构建一个 PyTorch 线性模型来尝试拟合这些数据。你可以先尝试自己构建一个模型,我们下节课再见。

59:全流程整合(第二部分):模型构建 🧱

在本节课中,我们将学习如何构建一个PyTorch线性模型。我们将使用torch.nn模块中的预构建层来简化模型创建过程,并理解其背后的工作原理。


上一节我们创建了一些模拟的线性数据。本节中,我们来看看如何构建一个适合该问题的模型。

我们将构建一个PyTorch线性模型。虽然你可以按照之前“构建模型”章节的步骤手动完成,但这里我们将介绍一种更高效的方法:利用torch.nn模块的强大功能。

我们将通过子类化nn.Module来创建一个线性模型。许多PyTorch模型都采用这种方式。

以下是创建模型的代码:

class LinearRegressionModelV2(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear_layer = nn.Linear(in_features=1, out_features=1)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.linear_layer(x)

让我们逐步解析这段代码:

  • nn.Linear:我们使用这个层来构建线性回归模型,因为它实现了线性回归公式 y = weight * x + bias
  • in_features=1, out_features=1:这表示模型接受一个特征(x)作为输入,并输出一个特征(y)。这取决于我们的数据形状(X_trainY_train 都是单值对应)。
  • forward 方法:我们定义了模型的前向计算,即简单地将输入 x 传递给线性层。

nn.Linear 层在内部为我们创建并管理了权重(weight)和偏置(bias)参数。这是PyTorch的一个核心概念:你通常初始化的是包含参数的层,而不是直接初始化参数本身。

这个线性层也被称为线性变换、仿射层、全连接层或密集层(在TensorFlow中)。它们都实现了相同的基础数学公式。

现在让我们看看这个模型的实际效果:

torch.manual_seed(42) # 设置随机种子以保证可复现性
model_1 = LinearRegressionModelV2()
print(model_1.state_dict())

运行代码后,你会看到模型的状态字典中包含了由nn.Linear层自动初始化的weightbias参数。它们的初始值可能与我们手动初始化时略有不同,这是因为PyTorch内部使用了不同的随机初始化方式。

通过使用nn.Linear,我们有效地将之前手动初始化参数并编写前向计算公式的步骤,替换为使用一个预构建的层。这种模式是构建大多数PyTorch深度学习模型的标准方式。

torch.nn模块提供了许多预构建的层,例如卷积层、池化层、归一化层、循环层、Transformer层等。对于深度学习中的常见数学变换,PyTorch通常都有现成的实现。


本节课中我们一起学习了如何使用torch.nn.Linear层来构建一个线性回归模型。我们理解了通过子类化nn.Module并利用预构建层来简化模型创建过程,这是PyTorch中的标准做法。模型已经构建完成,下一步将是训练这个模型。

60:全流程整合(第三部分)模型训练 🚀

在本节课中,我们将学习如何将之前准备好的数据和模型结合起来,编写一个完整的PyTorch模型训练流程。我们将设置损失函数和优化器,并构建一个训练循环来优化我们的线性回归模型。


模型与设备设置

上一节我们使用 torch.nn.Linear 层构建了一个简单的PyTorch线性模型。现在,在编写训练代码之前,我们需要确保模型运行在正确的设备上。

为了编写设备无关的代码,我们首先检查并设置目标设备。如果GPU可用,我们将使用GPU(CUDA),否则将默认使用CPU。

# 检查当前设备
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

# 将模型发送到目标设备
model_1.to(device)

# 检查模型参数所在的设备
print(next(model_1.parameters()).device)

执行上述代码后,模型的参数将被转移到目标设备(例如GPU)的内存中,从而加速后续的计算过程。


设置损失函数与优化器

现在,让我们进入训练的核心环节。根据PyTorch工作流程,在准备好数据和模型后,我们需要选择损失函数和优化器。

  • 损失函数:用于衡量模型预测值与真实值之间的误差。
  • 优化器:根据损失函数的梯度来调整模型的参数,以减小误差。

对于我们的回归问题,我们选择L1损失函数(也称为平均绝对误差,MAE)。对于优化器,我们选择随机梯度下降(SGD),并为其设置一个学习率。学习率决定了优化器每次更新参数时的步长大小。

# 设置损失函数
loss_fn = torch.nn.L1Loss()

# 设置优化器,并传入需要优化的模型参数
optimizer = torch.optim.SGD(params=model_1.parameters(), lr=0.01)

学习率的选择很重要:步长太大可能导致模型不稳定,步长太小则可能导致学习速度过慢。


构建训练与测试循环

接下来,我们将编写一个完整的训练循环。这个循环将重复多个轮次(epochs),在每个轮次中执行以下步骤:

以下是训练循环中的关键步骤:

  1. 前向传播:将训练数据输入模型,计算预测值。
  2. 计算损失:使用损失函数比较预测值与真实值。
  3. 梯度清零:将优化器中累积的梯度归零,为新一轮计算做准备。
  4. 反向传播:计算损失相对于每个模型参数的梯度。
  5. 优化器步进:优化器根据计算出的梯度更新模型参数。

同时,我们也会在每个训练轮次中评估模型在测试集上的表现。

torch.manual_seed(42) # 设置随机种子以获得可重现的结果
epochs = 200

# 将数据也转移到目标设备,确保所有张量都在同一设备上
X_train = X_train.to(device)
y_train = y_train.to(device)
X_test = X_test.to(device)
y_test = y_test.to(device)

for epoch in range(epochs):
    ### 训练步骤 ###
    model_1.train() # 将模型设置为训练模式
    # 1. 前向传播
    y_pred = model_1(X_train)
    # 2. 计算损失
    loss = loss_fn(y_pred, y_train)
    # 3. 梯度清零
    optimizer.zero_grad()
    # 4. 反向传播
    loss.backward()
    # 5. 优化器步进
    optimizer.step()

    ### 测试步骤 ###
    model_1.eval() # 将模型设置为评估模式
    with torch.inference_mode(): # 关闭梯度跟踪等以提升推理效率
        test_pred = model_1(X_test)
        test_loss = loss_fn(test_pred, y_test)

    # 每10个轮次打印一次损失
    if epoch % 10 == 0:
        print(f"Epoch: {epoch} | Train loss: {loss:.4f} | Test loss: {test_loss:.4f}")

运行上述代码,你将看到训练损失和测试损失随着轮次增加而逐渐下降,这表明模型正在从数据中学习。


模型评估与预测

训练完成后,我们可以检查模型学习到的参数,并与我们生成数据时使用的真实参数(权重 weight=0.7,偏置 bias=0.3)进行比较。

# 查看模型学习到的最终参数
print(model_1.state_dict())

你会发现模型学习到的权重和偏置值非常接近真实值(例如 weight ≈ 0.6968, bias ≈ 0.3025)。这证明了我们的训练流程是有效的。在实际问题中,我们虽然不知道真实的参数,但模型会通过训练不断逼近能最好描述数据关系的参数。

最后,你可以使用之前定义的绘图函数,将模型的预测结果(红色点)与原始测试数据(绿色点)绘制在一起,直观地查看模型的拟合效果。


总结

本节课中我们一起学习了PyTorch模型训练的全流程。我们首先将模型和数据设置为设备无关,然后选择了合适的损失函数和优化器,接着构建了一个包含前向传播、损失计算、反向传播和参数更新的训练循环,并同时在循环中评估模型性能。最终,我们验证了模型能够有效地学习数据中的线性关系。恭喜你快速完成了一个完整模型的训练!

61:全流程整合(第四部分):训练模型预测 🧠➡️📈

在本节课中,我们将学习如何评估训练好的神经网络模型,并利用它对新数据进行预测。我们将重点关注如何将模型切换到评估模式、进行预测,并可视化预测结果以评估模型性能。


在上一节视频中,我们完成了一项非常激动人心的工作:训练了一个完整的神经网络。虽然之前理解这些步骤花费了我们大约一个小时,但我们在一个视频中就完成了编码。现在,让我们回顾一下一个训练周期(epoch)内的核心优化循环步骤。

以下是PyTorch非官方的优化循环“歌曲”所描述的步骤:

  1. 执行前向传播(forward pass)。
  2. 计算损失(calculate the loss)。
  3. 优化器梯度归零(optimizer.zero_grad())。
  4. 执行反向传播(loss.backward())。
  5. 优化器更新参数(optimizer.step())。

在测试或推理阶段,我们使用torch.inference_mode(),执行前向传播并计算损失,然后打印结果。这个循环会为每一个epoch重复执行。

我们之前还编写了设备无关的代码,确保计算在与模型相同的设备(CPU或GPU)上进行,因为模型本身也使用了设备无关的代码。


上一节我们介绍了模型的训练过程,本节中我们来看看如何评估模型并做出预测。我们已经观察了训练损失和测试损失,知道模型的损失在下降。但这在实际预测中意味着什么?这才是我们最关心的。我们也查看了模型参数,它们已经非常接近理想参数。

在上节课结束时,我给大家布置了一个挑战:进行预测并绘制结果图。希望你已经尝试过了。现在,让我们一起看看具体怎么做。

首先,将模型切换到评估模式。这是因为每当进行预测或推理时,我们都希望模型处于评估模式;而训练时,则希望模型处于训练模式。

接着,在测试数据上进行预测。我们使用训练数据训练模型,而在模型从未见过的测试数据上评估其性能(除了预测时)。

使用torch.inference_mode(),在进行推理或预测时启用推理模式。我们将预测结果赋值给y_preds

让我们看看y_preds是什么样子。很好,我们得到了一个张量,并且它显示仍在CUDA设备上。这是因为之前我们将model1和测试数据都设置到了目标设备上,因此预测结果也在CUDA设备上。

现在,引入我们的plot_predictions函数来可视化模型预测。数据探索者的座右铭是:可视化、可视化、再可视化。

我们遇到了一个错误:TypeError: can‘t convert cuda device type tensor to numpy。这是因为plot_predictions函数使用matplotlib库,而matplotlib是基于NumPy和CPU工作的,但我们的预测张量在GPU(CUDA)上。错误信息提示我们:使用tensor.cpu()先将张量复制到主机内存。

对预测张量调用.cpu()方法后,再绘图。看!结果非常棒。红色圆点代表的预测值几乎完全覆盖在绿色圆点代表的测试数据之上。这非常令人兴奋。你的具体数值可能不完全相同,这完全没问题,但趋势应该非常相似。多亏了反向传播和梯度下降的力量,我们模型的随机参数已经更新到尽可能接近理想参数,现在的预测结果看起来相当不错。

但我们还没有结束。如果现在笔记本断开连接,训练好的模型就会丢失,这显然不理想。


因此,在下一部分(P62),我们将进入第26.5节:保存和加载训练好的模型。

这里我也给你一个挑战:参考之前关于“在PyTorch中保存模型”和“加载PyTorch模型”的代码,尝试保存model1的状态字典(state_dict),然后再将其加载回来,并得到类似的可视化结果。去试试吧,我们下个视频见。


本节课中我们一起学习了如何将训练好的模型切换到评估模式、在测试集上进行预测,并通过可视化来直观评估模型的性能。我们还解决了因数据设备(CPU/GPU)不一致导致的绘图错误,为模型的持久化保存做好了准备。

62:模型保存与加载 🚀

在本节课中,我们将学习PyTorch工作流程的最后一步:如何保存和加载训练好的模型。保存模型至关重要,它能让我们保留训练成果,并在未来无需重新训练即可使用模型进行预测。

上一节我们成功训练了一个线性回归模型并获得了不错的预测结果。本节中,我们将把训练好的模型保存到文件中,然后再将其加载回来,验证加载后的模型与原始模型功能一致。

创建模型保存目录

首先,我们需要导入必要的库并创建一个用于保存模型的目录。

from pathlib import Path

# 1. 创建模型保存目录
MODEL_PATH = Path("models")
MODEL_PATH.mkdir(parents=True, exist_ok=True)

以下是代码步骤解析:

  • 我们导入 Path 库来处理文件路径。
  • 定义模型保存的目录路径为 "models"
  • 使用 .mkdir(parents=True, exist_ok=True) 创建目录。exist_ok=True 确保即使目录已存在也不会报错。

定义模型保存路径

接下来,我们为模型文件定义一个具体的保存路径和文件名。

# 2. 定义模型保存路径
MODEL_NAME = "01_pytorch_workflow_model_1.pth"
MODEL_SAVE_PATH = MODEL_PATH / MODEL_NAME

以下是关键点说明:

  • PyTorch模型通常保存为 .pt.pth 扩展名的文件。
  • 我们使用 Path 对象的 / 操作符来组合目录路径和文件名,形成完整的保存路径 MODEL_SAVE_PATH

保存模型状态字典

在PyTorch中,推荐保存模型的 state_dict,而不是整个模型对象。state_dict 是一个Python字典,包含了模型的所有可学习参数(如权重和偏置)。

# 3. 保存模型的状态字典
print(f"正在保存模型到: {MODEL_SAVE_PATH}")
torch.save(obj=model_1.state_dict(), f=MODEL_SAVE_PATH)

以下是具体操作:

  • model_1.state_dict() 获取我们训练好的 model_1 的参数。
  • torch.save() 函数将这个状态字典保存到指定的文件路径 MODEL_SAVE_PATH
  • 保存后,你可以在 models 文件夹下找到名为 01_pytorch_workflow_model_1.pth 的文件。

加载已保存的模型

现在,我们来加载刚才保存的模型。这需要创建一个新的模型实例,然后将保存的参数加载进去。

# 4. 加载模型
# 首先,创建一个模型的新实例(其结构必须与保存的模型相同)
loaded_model_1 = LinearRegressionModelV2()

# 然后,将保存的状态字典加载到这个新实例中
loaded_model_1.load_state_dict(torch.load(f=MODEL_SAVE_PATH))

# 将加载的模型放到目标设备上(例如GPU),以保持设备一致性
loaded_model_1.to(device)

以下是加载步骤解析:

  • LinearRegressionModelV2() 实例化了一个全新的、未经训练的模型。
  • torch.load(MODEL_SAVE_PATH) 从磁盘加载保存的状态字典。
  • loaded_model_1.load_state_dict(...) 将加载的参数赋予新模型。
  • .to(device) 确保模型在正确的设备(CPU/GPU)上运行。

验证加载的模型

最后,我们必须验证加载的模型是否与原始模型完全一致。我们通过比较它们的预测结果来进行验证。

# 5. 评估加载的模型,确保其功能与保存前一致
loaded_model_1.eval()
with torch.inference_mode():
    loaded_model_1_preds = loaded_model_1(X_test)

# 检查加载模型的预测结果是否与原始模型的预测结果相同
print(torch.allclose(y_preds, loaded_model_1_preds))
# 输出应为:tensor(True)

以下是验证过程:

  • 将模型设置为评估模式 (loaded_model_1.eval())。
  • 在推理模式下 (torch.inference_mode()) 对测试数据 X_test 进行预测。
  • 使用 torch.allclose() 比较原始模型预测结果 y_preds 和加载模型预测结果 loaded_model_1_preds。如果输出为 True,则证明两个模型的预测在数值上几乎完全相同,加载成功。

本节课中我们一起学习了PyTorch模型保存与加载的完整流程。我们了解了保存模型的 state_dict 是推荐做法,掌握了从创建保存目录、定义路径、保存模型到加载并验证模型的全套操作。这是将机器学习模型投入实际应用的关键一步,确保你的训练成果得以持久化和复用。恭喜你完成了整个PyTorch基础工作流程的学习!

63:应对冒名顶替综合征 🧠

在本节课中,我们将暂时离开技术内容,探讨一个在学习过程中普遍存在的心理现象——冒名顶替综合征。我们将了解它的本质,并学习如何将其转化为学习的动力。


什么是冒名顶替综合征?

冒名顶替综合征是一种认为自己能力不足、不配获得成功,并担心被他人视为“骗子”的心理状态。在学习新技能,尤其是像深度学习这样复杂的领域时,这种感觉尤为常见。

上一节我们介绍了神经网络的基础,本节中我们来看看在学习旅程中可能遇到的心理挑战。

你可能会看着行业专家,心想:“我永远也达不到那种水平。”这种感觉是学习过程中的一个正常症状。它仅仅意味着你还没有到达那个阶段,而掌握任何有价值的技能都需要时间和努力。

公式:冒名顶替综合征 ≈ 学习过程中的正常症状


将挑战转化为动力

记住你第一天上学或第一天上班时的感受。那是一种陌生的、充满不确定性的体验。但随着时间的推移,通过重复和实践,你会逐渐变得舒适,积累经验,那种冒名顶替的感觉也会慢慢消退。

存在冒名顶替综合征的感觉其实是件好事。它意味着你正在做一些有价值、有挑战性的事情。不要将其视为负面情绪,而应将其视为一种鼓励,证明你正在学习,正在突破自己的极限。


一个重要的学习练习:教学相长

为了巩固所学知识,我建议我们进行一个简短的练习。暂停观看课程,然后前往我们的 Discord 社区。

以下是利用社区进行学习的具体步骤:

  1. 访问 Discord 服务器:如果你还没加入,现在就去。
  2. 找到相关频道:进入例如“编程与课程”板块下的“Python”频道。
  3. 帮助他人:浏览其他学员提出的问题,尝试运用你目前所学的知识去解答。

即使你对自己的知识还没有100%的把握,尝试向他人解释你刚刚学会的内容,这个过程本身对你的学习就极为有益。如果你从不尝试教学、解释或展示所学,你将永远停留在初学者的阶段。

核心概念教 = 学 的最有效方式之一。

在我们的社区里, motto 是保持友善,不评判他人。我们都是初学者,都在学习的过程中。请勇敢地回答问题,要记住,除了尤达大师,没有人是完美的。


总结

本节课我们一起学习了如何认识和应对冒名顶替综合征。我们了解到它是学习过程中的自然现象,可以通过实践和时间的积累来克服。更重要的是,我们介绍了一个强大的学习工具——通过帮助他人来巩固自己的知识。记住,感到不足恰恰说明你走在了正确的、有挑战性的道路上。继续前进,我们下节课再见!

64:PyTorch工作流练习与拓展 📚

在本节课中,我们将回顾PyTorch工作流的核心步骤,并介绍如何通过练习和拓展资源来巩固所学知识。我们将重点了解课程提供的练习模板、解决方案以及额外的学习材料。


课程概述

上一节我们完成了PyTorch工作流的构建,包括保存和加载训练好的模型。本节我们将进入练习与拓展环节,介绍如何利用课程提供的资源进行实践和深入学习。

以下是本节内容的要点:

  1. 练习与拓展资源介绍:了解在哪里可以找到练习和拓展材料。
  2. 练习内容概览:介绍基于第01节代码设计的练习题。
  3. 练习模板与解决方案:说明如何获取练习模板和参考答案。
  4. 核心工作流回顾:总结PyTorch工作流的所有关键步骤。

练习与拓展资源介绍

所有练习和拓展材料都包含在课程的书本版本中。访问地址是 learnpytorch.io。在第01节“PyTorch工作流基础”部分,你可以找到相关材料。

在每小节的末尾,目录中会列出“练习”和“拓展课程”部分。我在整个01系列视频中提到过许多概念,例如梯度下降和反向传播。因此,那里提供了大量资源供深入学习。

拓展资源包括:

  • PyTorch官方关于加载和保存模型的文档。
  • PyTorch速查表。
  • Jeremy Howard撰写的一篇帮助深入理解torch.nn的文章。
  • 一个非官方的PyTorch优化之歌,内容很有趣。

练习内容概览

这里的练习全部基于我们在第01节中编写的代码。练习中没有任何我们未曾涵盖的内容。如果有,我会在练习本身中添加说明。

一个示例练习是:使用线性回归公式创建一个直线数据集,然后通过子类化nn.Module来构建一个模型。


练习模板与解决方案

对于这些练习,有一个练习笔记本模板。该模板链接在课程材料中,同时也位于PyTorch深度学习的GitHub仓库中。

进入仓库后,导航至 extras/exercises 目录,你会找到所有按章节编号的模板。例如,pytorch_workflow_exercises.ipynb 对应本章练习。

如果你想完成这些练习,可以点击此笔记本并在Google Colab中打开。你可以将其保存到自己的Google Drive中并开始编写代码。笔记本中包含了你应该完成的任务说明。当然,你也可以参考文本版本的练习说明。

如果你想查看一些解决方案示例,请注意,我强烈建议你先自己尝试完成练习。你可以使用我们在这里提供的书本(其中包含所有视频代码),也可以使用我提供的众多笔记本来帮助你完成练习。

完成尝试后,你可以回到 extras 文件夹,在那里可以找到 solutions(解决方案)。例如,extras/solutions/01_pytorch_workflow_exercises_solutions.ipynb 就是第01节的解决方案示例。所有额外的资源和解决方案也包含在课程的书本版本中。


核心工作流回顾

至此,第01节“PyTorch工作流”就结束了。我们基本上走完了PyTorch工作流的所有步骤:

  1. 准备数据,并将其转换为张量。
  2. 构建或选择模型。
  3. 选择损失函数和优化器。
  4. 构建训练循环。
  5. 将模型拟合到数据。
  6. 进行预测。
  7. 评估模型。
  8. 通过实验进行改进(例如训练更多轮次)。
  9. 保存并重新加载训练好的模型。

课程总结

本节课中,我们一起学习了如何利用课程提供的练习和拓展资源来巩固PyTorch工作流知识。我们介绍了练习模板的获取方式、练习内容的核心思想,并回顾了完整的PyTorch模型开发流程。实践是掌握深度学习的关键,请务必尝试完成提供的练习。我们下个章节再见。

65:PyTorch机器学习分类导论 🧠

在本节课中,我们将学习机器学习中的核心问题之一:分类。我们将探讨什么是分类问题,它与回归问题的区别,以及如何使用PyTorch构建神经网络来解决分类任务。

概述

分类是预测一个事物属于哪个类别的问题,而回归是预测一个数值。掌握这两个问题,就掌握了机器学习中最主要的两种任务。本节课将重点介绍分类问题的基础知识、不同类型以及我们将要构建的模型架构。

如何获取帮助

在学习过程中,如果遇到困难,可以遵循以下步骤:

  • 首先,尽量跟随代码操作,亲自运行和编写代码,这是最重要的学习方式。
  • 如果遇到问题,可以按 Shift + Command + Space(Mac)或 Ctrl + Space(Windows)查看函数的文档字符串。
  • 如果仍然无法解决,将错误信息复制到搜索引擎中查找答案,通常会找到Stack Overflow或PyTorch官方文档等资源。
  • 最后,如果问题依然存在,可以在课程GitHub仓库的讨论区提问,注明视频编号和时间戳,以便获得帮助。

什么是分类问题

分类是机器学习的主要问题之一。你可能每天都在与机器学习驱动的分类问题打交道。

以下是分类问题的几个例子:

  • 垃圾邮件过滤:判断一封电子邮件是垃圾邮件还是非垃圾邮件。这属于二元分类,因为结果只有两种可能(是或否,通常用0和1表示)。
  • 图像识别:判断一张照片是寿司、牛排还是披萨。这属于多类别分类,因为类别数量超过两个(例如,ImageNet数据集包含1000个类别)。
  • 文章标签:为一篇维基百科文章自动分配多个相关标签(如“深度学习”、“人工神经网络”)。这属于多标签分类,因为一个样本可以同时拥有多个标签。

二元分类与多类别分类

让我们更深入地理解二元分类和多类别分类的区别。

  • 二元分类:模型在两种可能性中做出选择。例如,训练一个模型来区分照片中是狗还是猫。
  • 多类别分类:模型在多种可能性中做出选择。例如,在区分狗和猫的基础上,再加入鸡的照片,模型就需要在狗、猫、鸡三个类别中进行判断。

本节内容安排

上一节我们介绍了回归问题,本节中我们来看看分类问题的构建方法。我们将按照以下步骤进行:

  • 学习神经网络分类模型的架构。
  • 了解分类模型的输入和输出形状(特征和标签)。
  • 创建用于查看、拟合和预测的自定义数据。
  • 为神经网络分类创建一个模型(与回归模型略有不同)。
  • 为分类模型设置损失函数和优化器。
  • 重建训练循环和评估(测试)循环。
  • 学习如何保存和加载模型。
  • 利用线性(直线)的威力。
  • 探索不同的分类评估方法。

我们将通过编写大量代码来实践这些概念,就像厨师烹饪菜肴一样,一步步构建出我们的分类模型。

在下一节开始编码之前,我们将进一步探讨分类模型的输入和输出具体是什么样子。

总结

本节课我们一起学习了机器学习分类问题的基本概念。我们了解了分类与回归的区别,认识了二元分类、多类别分类和多标签分类,并概述了使用PyTorch构建分类模型的完整流程。下一节,我们将深入探讨分类模型的输入与输出。

66:分类问题的输入与输出形状示例 🍕📊

在本节课中,我们将学习分类问题中数据的输入与输出形状。我们将以构建一个名为“Food Vision”的食物图像分类器为例,具体探讨如何将图像转换为数值表示(张量),以及模型如何输出预测概率。


上一节我们简要介绍了什么是分类问题。本节中,我们来看看一个具体分类任务(如食物图像分类)的输入和输出数据具体是什么样子。

假设我们有一些精美的食物照片,并试图构建一个“Food Vision”系统来识别照片中的食物。这个过程可以分解为三个部分:输入机器学习算法输出

对于输入,我们需要以某种数值形式表示这些图像。一种常见的方法是将每张图片调整为224像素宽、224像素高的正方形。图像在计算机中通常由宽度、高度和颜色通道三个维度表示。颜色通道通常指红、绿、蓝(RGB)三色,每个像素点都由这三个通道的数值组合成最终颜色。

因此,一张图片的数值编码(即像素值)可以表示为一个三维张量。我们将这个数值张量输入到机器学习算法中。算法可以是现成的,我们也可以使用PyTorch为特定问题构建一个。

对于输出,在“Food Vision”例子中,我们希望模型能告诉我们图片中是寿司、牛排还是披萨。机器学习模型的输出通常不是绝对的类别,而是预测概率,即一个介于0和1之间的数值,表示模型对每个类别的置信度。例如,模型可能输出 [0.01, 0.05, 0.94],分别对应寿司、牛排和披萨的概率,其中披萨的概率最高。

我们的目标是将这些数值概率转换回人类可读的标签(如“披萨”)。这个过程需要模型通过观察大量样本进行训练,并不断调整算法和数据以提高预测准确性。


现在,让我们从张量形状的角度来具体分析。我们以构建“Food Vision”为例。

输入是一张图像,我们将其调整为宽224、高224。经过数值编码后,它成为机器学习算法的输入。算法输出预测概率,数值越接近1,表示模型对该类别的预测信心越高。

以下是输入和输出张量的典型形状:

输入张量形状
输入图像被表示为一个四维张量,其形状通常为:
[batch_size, color_channels, height, width]

  • batch_size:模型一次处理的图像数量。设为 None 表示该维度可变,训练时代码会自动填充。32 也是一个非常常见且高效的批次大小。
  • color_channels:颜色通道数。对于RGB图像,该值为 3
  • height:图像高度,例如 224
  • width:图像宽度,例如 224

因此,一个具体的输入张量形状可能是 [32, 3, 224, 224],代表一次处理32张224x224的RGB图像。

注意:张量中维度(颜色通道、宽、高)的顺序可能存在不同约定。在PyTorch中,默认顺序是 [color_channels, height, width],但代码可以调整这个顺序。

输出张量形状
输出是一个二维张量,其形状为:
[batch_size, num_classes]

  • batch_size:与输入对应的批次大小。
  • num_classes:分类问题的类别总数。在我们的例子中有3类食物,所以 num_classes3

因此,对于批次大小为32的输入,对应的输出形状为 [32, 3],即模型为32张图片中的每一张都输出了3个类别的预测概率。

提示:输入和输出的具体形状会根据你处理的问题而变化。例如,对于猫狗二分类,输出形状可能是 [batch_size, 2][batch_size, 1]。但核心原则不变:将数据编码为数值表示作为输入,输出则是基于类别的预测概率。


本节课中,我们一起学习了分类问题中输入与输出数据的形状。我们了解到图像输入通常被表示为 [batch_size, color_channels, height, width] 形状的张量,而模型输出则是 [batch_size, num_classes] 形状的预测概率张量。理解这些形状是使用PyTorch构建和调试模型的重要基础。

在下一节视频中,我们将在动手编写代码之前,先讨论分类模型的整体架构。

67:分类神经网络典型架构概览 🧠

在本节课中,我们将要学习分类神经网络的典型架构。我们将了解其核心组成部分,包括输入层、隐藏层、输出层以及相关的激活函数和损失函数。

欢迎回来。在上一个视频中,我们看了一些分类模型的输入和输出示例。主要结论是,分类模型(尤其是神经网络)的输入需要是某种数值表示形式,而输出通常是某种预测概率。

那么,让我们来讨论分类模型的典型架构。请注意,这目前只是页面上的文字,但我们后续会构建不少这样的模型。

我们这里有一些超参数,包括二分类和多分类。这两种问题类型在我们处理的问题上有一些相似之处,但也存在一些差异。顺便提一下,所有这些内容都来自本课程的书籍版本,其中包含了“什么是分类问题”和“分类神经网络的架构”部分。因此,所有这些文本都可以在 learnptorge.io 的第 2 节中找到。


让我们回到正题。

输入层形状

输入层形状通常由 in_features 参数决定,如上图所示。它等于特征的数量。

例如,如果我们想预测某人是否患有心脏病,我们可能有五个输入特征,比如年龄(一个数字,例如 28)、性别(男性)、身高(180 厘米,或者更接近 177 厘米)、体重(取决于饮食,大约 75 公斤)和吸烟状况(0 或 1)。我们希望使用数值表示,所以性别可以是 0 代表男性,1 代表女性;身高和体重就是数字本身。这些数字可以更大或更小,非常灵活。这是一个超参数,因为我们需要为每个特征决定其数值。

在我们的图像预测问题中,我们可以设置 in_features = 3,代表颜色通道的数量。

隐藏层

接下来是隐藏层,即图中的蓝色圆圈。每个隐藏层都是一个 nn.Linear 层,通常与 nn.ReLU 等激活函数结合使用。在 PyTorch 中,你会看到 nn.something 这样的语法来表示一个层。PyTorch 的 torch.nn 模块中有许多不同类型的层。

基本上,这里面的所有内容都是神经网络中的一层。如果我们看一个神经网络的结构图:

回想一下,所有这些层都是某种数学运算:输入层、隐藏层。你可以拥有任意数量的隐藏层。例如,ResNet 架构中,有些模型有 50 层。看看这个,这只是 34 层的版本。实际上还有 ResNet-152,它有 152 层。我们目前还没到那个阶段,但正在积累工具以达到那个水平。

让我们回到这里。每个隐藏层的神经元数量由 out_features 参数决定,即图中的绿色方块。回到我们的神经网络示意图,这些就是神经元,每个小圆圈代表一个神经元,包含一些参数。如果我们有 100 个神经元,那会是什么样子?我们会有一个相当大的图形。这就是为什么我喜欢用代码教学,因为你可以根据需要灵活定制。在幕后,PyTorch 会为我们创建这 100 个小圆圈,每个圆圈内部都包含某种数学运算。

输出层形状

接下来是输出层形状,即我们有多少个输出特征。对于二分类问题,输出特征数量是 1(一个类别或另一个)。对于多分类问题,你可能有三个输出特征,每个类别一个。例如,如果你正在构建一个食物、人物或狗的图片分类模型,输出特征就是三个。

隐藏层激活函数

隐藏层激活函数,我们还没有详细讨论过。ReLU(修正线性单元)是其中之一,但 PyTorch 还有许多其他非线性激活函数。我们稍后会看到这些。我在这里只是埋下伏笔。我们已经见过线性线是什么样子,但我想让你想象一下非线性线是什么样子。这对于我们的分类问题来说将是一个强大的工具。


输出层激活函数

我们这里没有列出输出激活函数,但稍后也会看到。对于二分类,通常是 Sigmoid;对于多分类,通常是 Softmax。目前这些只是名称,我们还没有具体学习。我喜欢在实际遇到时再讲解,但这只是我们将要涵盖内容的一个概览。

损失函数

损失函数的作用是什么?它衡量我们模型的预测与理想预测之间的差异程度。

以下是常见的损失函数选择:

  • 对于二分类,我们可能使用 PyTorch 中的 BCELoss(二元交叉熵损失)。
  • 对于多分类,我们可能使用 CrossEntropyLoss(交叉熵损失)。

优化器

优化器,例如 SGD(随机梯度下降),我们之前已经见过。另一个常见选项是 Adam 优化器。当然,torch.optim 包中还有更多选项。

这是一个多分类问题的示例网络。我们还没有实际见过 nn.Sequential,但你可以想象,Sequential 代表按顺序执行这些步骤。多分类之所以有三个输出特征,是因为它要区分多个类别。例如,对于食物、人物或狗的分类,输出特征就是三个。

回到我们的食物视觉问题,输入可能是寿司、牛排或披萨的图片。因此,我们会有三个输出特征,每个图像类别对应一个预测概率。我们有三个类别:寿司、牛排和披萨。

我认为我们已经讨论了足够多的内容,也看了足够多的幻灯片文字。在下一个视频中,让我们开始编写代码吧。我们 Google Colab 见。


总结

本节课中,我们一起学习了分类神经网络的典型架构。我们介绍了输入层形状、隐藏层、输出层形状、激活函数(如 ReLUSigmoidSoftmax)、损失函数(如二元交叉熵和交叉熵)以及优化器(如 SGDAdam)。这些是构建有效分类模型的基础组件。在接下来的课程中,我们将通过实际编码来应用这些概念。

68:使用 PyTorch 进行神经网络分类 🧠

在本节课中,我们将学习如何使用 PyTorch 构建一个神经网络来解决分类问题。我们将从零开始,创建一个玩具数据集,探索数据,并理解分类问题的基本输入和输出结构。


概述

分类问题是预测某个事物属于哪一类别的问题,例如判断一封邮件是否为垃圾邮件,或识别一张图片是猫还是狗。本节课,我们将专注于一个简单的二元分类问题,并使用 PyTorch 构建模型来学习数据中的模式。


第一步:创建数据

所有机器学习问题都始于数据。我们无法让算法学习不存在的数据模式。因此,我们的第一步是准备数据。在本例中,我们将使用 scikit-learn 库创建一个自定义的玩具数据集。

以下是创建数据集的代码:

from sklearn.datasets import make_circles

# 设置样本数量
n_samples = 1000

# 生成圆形数据集
X, y = make_circles(n_samples=n_samples,
                    noise=0.03,
                    random_state=42)

我们导入了 scikit-learn 库中的 make_circles 函数来生成数据。我们创建了 1000 个样本,并添加了一点噪声以增加随机性。random_state 参数用于确保结果的可重复性。


第二步:探索数据

在构建模型之前,我们需要了解数据的基本情况。让我们先查看数据的前几个样本。

以下是查看数据样本的代码:

# 查看前5个特征样本
print("First 5 samples of X:")
print(X[:5])

# 查看前5个标签样本
print("\nFirst 5 samples of y:")
print(y[:5])

输出显示,每个样本有两个特征(X1 和 X2)和一个标签(0 或 1)。这是一个二元分类问题,因为标签只有两种可能的值。


第三步:可视化数据

数字表格难以直观展示数据模式。因此,我们使用图形来可视化数据,以便更好地理解其结构。

以下是可视化数据的代码:

import matplotlib.pyplot as plt

# 创建散点图
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.RdYlBu)
plt.title("Toy Dataset: Circles")
plt.xlabel("Feature X1")
plt.ylabel("Feature X2")
plt.show()

可视化结果显示,数据由两个同心圆组成,红色点代表一类,蓝色点代表另一类。我们的目标是构建一个模型,能够根据特征(X1, X2)预测样本属于红色类还是蓝色类。


第四步:理解问题结构

现在我们已经有了数据,接下来需要明确问题的输入和输出形状,并考虑如何将数据划分为训练集和测试集。

输入形状是特征的数量,输出形状是类别的数量。对于我们的数据集:

  • 输入形状:每个样本有 2 个特征。
  • 输出形状:有 2 个类别(0 和 1)。

在下一节中,我们将讨论如何划分数据,并开始构建神经网络模型。


总结

本节课中,我们一起学习了如何创建和探索一个用于分类的玩具数据集。我们使用 scikit-learn 生成了数据,通过可视化的方式理解了数据的结构,并明确了分类问题的输入和输出形状。在接下来的课程中,我们将基于这些数据构建一个神经网络模型。

69:数据张量化与训练测试集划分 🧠📊

在本节课中,我们将学习如何将数据转换为PyTorch张量,并将其划分为训练集和测试集。这是构建神经网络模型前至关重要的数据准备步骤。


1.1 检查输入与输出形状 🔍

上一节我们创建了用于分类的模拟数据。本节中,我们首先需要检查数据的输入和输出形状。在机器学习中,张量的输入和输出形状至关重要,因为形状不匹配是导致错误的常见原因。

以下是检查数据形状的代码:

print(f"X的形状: {X.shape}")
print(f"y的形状: {y.shape}")

运行上述代码,我们可以看到X的形状为(1000, 2),表示有1000个样本,每个样本有2个特征。而y的形状为(1000,),表示有1000个对应的标签(标量值)。

为了更清晰地理解,我们可以查看第一个样本的特征和标签:

X_sample = X[0]
y_sample = y[0]
print(f"第一个X样本的值: {X_sample}")
print(f"第一个y样本的值: {y_sample}")
print(f"一个X样本的形状: {X_sample.shape}")
print(f"一个y样本的形状: {y_sample.shape}")

输出显示,一个X样本包含两个特征值,而一个y样本是一个单独的标签数字。这明确了我们的任务:使用两个特征(X)来预测一个标签(y)


1.2 将数据转换为张量并划分训练/测试集 ⚙️➗

现在我们已经了解了数据形状,接下来需要将NumPy数组转换为PyTorch张量,并创建训练集和测试集。这个流程适用于几乎所有数据集。

首先,确保导入PyTorch并检查版本:

import torch
print(torch.__version__)

接着,将数据转换为PyTorch张量。默认情况下,NumPy数组是float64类型,而PyTorch的默认类型是float32,因此我们需要进行类型转换以避免后续错误。

# 将特征X和标签y转换为PyTorch张量,并指定数据类型为torch.float32
X = torch.from_numpy(X).type(torch.float)
y = torch.from_numpy(y).type(torch.float)

# 查看转换后的前几个值
print(X[:5])
print(y[:5])
# 检查数据类型
print(f"X的数据类型: {X.dtype}")
print(f"y的数据类型: {y.dtype}")

数据成功转换为张量后,下一步是将其划分为训练集和测试集。我们将使用Scikit-learn库中的train_test_split函数来实现随机划分。

以下是划分数据的步骤:

  1. sklearn.model_selection导入train_test_split
  2. 指定测试集的比例(例如20%)。
  3. 设置random_state参数以确保每次划分结果一致,便于复现。
from sklearn.model_selection import train_test_split

# 将数据划分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X,
                                                    y,
                                                    test_size=0.2, # 20%的数据作为测试集
                                                    random_state=42) # 设置随机种子以保证结果可复现

划分完成后,我们可以检查各数据集的样本数量:

print(f"训练特征样本数: {len(X_train)}")
print(f"测试特征样本数: {len(X_test)}")
print(f"训练标签样本数: {len(y_train)}")
print(f"测试标签样本数: {len(y_test)}")

根据输出,1000个总样本被划分为800个训练样本和200个测试样本,这与我们设定的20%测试比例相符。


总结 📝

本节课中我们一起学习了数据准备流程中的两个核心步骤:

  1. 数据张量化:将NumPy数组转换为PyTorch张量,并统一数据类型(float32)。
  2. 数据集划分:使用train_test_split函数将数据随机划分为训练集和测试集,并设置random_state以确保结果可复现。

现在,我们的数据已经以张量的形式准备好,并被合理地划分为用于模型训练和评估的两部分。在下一节课中,我们将利用这些数据开始构建我们的神经网络模型。

70:建模步骤规划与设备无关代码配置 🚀

概述

在本节课中,我们将开始构建一个用于分类红蓝圆点的PyTorch模型。我们将首先规划建模的核心步骤,并重点学习如何编写“设备无关”的代码,以确保我们的模型能在CPU或GPU上无缝运行。

从数据到模型

上一节我们完成了数据的准备,将数据分割为训练集和测试集。我们采用了80-20的比例,因此大约有800个样本用于训练,200个样本用于测试。训练集的目的是让模型学习数据中的模式,即代表红点和蓝点的模式。测试集则用于评估这些学习到的模式。

在开始构建模型之前,我需要重新连接我的笔记本环境。我们可以通过运行之前的所有单元格来实现,这不会花费太长时间,因为我们尚未进行任何大型计算。

现在,我们进入第二部分:构建模型。虽然步骤不少,但都是我们之前接触过的内容。我们将逐步分解。

建模步骤规划

我们的目标是构建一个模型来分类我们的蓝点和红点。为此,我们需要处理张量数据。

以下是构建模型的主要步骤:

  1. 设置设备无关代码:养成编写此代码的习惯,以确保我们的代码能在加速器(如GPU)上运行。
  2. 构建模型:通过子类化 nn.Module 来构造我们的模型。正如我们在上一节所见,所有PyTorch模型都应继承自 nn.Module
  3. 定义损失函数和优化器:选择适合我们问题的损失函数和优化算法。
  4. 创建训练和测试循环:这部分内容可能会在下一节重点展开,本节课我们专注于模型构建。

所有这些步骤都遵循一个通用的机器学习工作流程:选择或构建一个适合你问题的模型,选择损失函数和优化器,然后构建训练循环。

实施设备无关代码

让我们开始第一步。首先导入必要的库。

import torch
from torch import nn

接下来,我们设置设备无关代码。这意味着代码会自动检测是否有可用的GPU(CUDA),并优先使用它进行计算;如果没有,则回退到CPU。

# 设置设备
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

运行这段代码后,如果当前环境没有设置GPU,device 变量将会是 "cpu"

为了演示GPU的设置,我们可以更改运行环境。在运行时菜单中选择“更改运行时类型”,然后选择GPU并保存。这会重启运行时并重新连接。

重启后,我们需要重新运行之前定义数据(如 X_train)的单元格。我们可以选择“运行所有”或“运行之前”的单元格。

一旦环境重新连接并运行了设备设置代码,device 变量现在应该显示为 "cuda",表明我们正在使用GPU。这样,我们就成功配置了设备无关的代码。

总结

本节课我们一起学习了构建PyTorch模型的初步规划,并重点实践了如何编写设备无关的代码。我们通过 torch.cuda.is_available() 来检测GPU可用性,并据此设置计算设备,这是确保代码在不同硬件环境下可移植性的重要一步。

在下一节中,我们将基于这个设备设置,正式开始构建我们的分类模型。

71:编写处理分类数据的小型神经网络 🧠

在本节课中,我们将学习如何构建一个能够处理分类数据的小型神经网络。我们将通过子类化 nn.Module 来创建模型,定义其前向传播过程,并将其部署到目标设备上。


概述

上一节我们设置了设备无关的代码,这为后续将模型和数据发送到目标设备(如GPU)奠定了基础。本节中,我们将具体构建一个用于分类的神经网络模型。

构建模型

现在我们已经设置了设备无关的代码,接下来让我们创建一个模型。我们将把这个过程分解为几个子步骤。

以下是构建模型的具体步骤:

  1. 子类化 nn.Module
  2. 创建两个能够处理数据形状的 nn.Linear 层。
  3. 定义一个 forward 方法,以描述模型的前向计算过程。
  4. 实例化我们的模型类,并将其发送到目标设备。

1. 子类化 nn.Module

在 PyTorch 中,几乎所有模型都是通过子类化 nn.Module 来创建的,因为它为我们处理了许多幕后的重要工作。

2. 创建线性层

我们需要创建两个 nn.Linear 层,它们必须能够处理我们数据的形状。

首先,查看我们的训练数据 X_train 的形状。我们有 800 个训练样本,每个样本包含 2 个特征。因此,输入特征的数量是 2。

第一个线性层(layer1)的 in_features 应为 2。out_features 我们暂时设为 5。这个数字是任意的,可以理解为隐藏单元的数量。增加隐藏特征的数量,模型就有更多机会学习数据中的模式。

第二个线性层(layer2)的 in_features 必须与第一个层的 out_features 匹配,即 5。而它的 out_features 应设为 1,以匹配我们的目标 y 的形状(一个标量值)。

nn.Linear 层执行的函数可以用以下公式表示:
output = input * weight^T + bias

3. 定义前向传播方法

forward 方法定义了模型的前向计算或前向传播过程。它接收输入数据 x,并规定数据如何流经各层。

在我们的模型中,数据 x 首先进入 layer1,然后 layer1 的输出进入 layer2,最后 layer2 产生最终输出。

4. 实例化模型并发送到设备

创建模型类的一个实例,并利用之前设置的设备无关代码,将其参数发送到目标设备(如 GPU)上,以加速计算。

代码实现

让我们将上述步骤转化为代码。我们将模型命名为 CircleModelV1,因为我们的目标是分离红色和蓝色的圆圈数据点。

import torch
from torch import nn

# 假设 device 已根据可用性设置为 “cuda” 或 “cpu”
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

class CircleModelV1(nn.Module):
    def __init__(self):
        super().__init__()
        # 创建两个线性层
        self.layer1 = nn.Linear(in_features=2, out_features=5)  # 从2个特征扩展到5个
        self.layer2 = nn.Linear(in_features=5, out_features=1)  # 从5个特征映射到1个输出

    def forward(self, x):
        # 定义前向传播:x -> layer1 -> layer2
        return self.layer2(self.layer1(x))

# 实例化模型并发送到目标设备
model_0 = CircleModelV1().to(device)

# 检查模型参数所在的设备
print(next(model_0.parameters()).device)  # 应输出 ‘cuda:0’ 或 ‘cpu’

总结

本节课中,我们一起学习了如何构建一个用于分类任务的小型神经网络。我们通过子类化 nn.Module 创建了模型类,定义了其层结构和前向传播逻辑,并最终将模型部署到了合适的计算设备上。这个过程是构建更复杂深度学习模型的基础。在下一节中,我们将对这个模型进行可视化,以更直观地理解其内部的数据流动。

72:神经网络可视化 🧠

在本节课中,我们将学习如何将上一节创建的神经网络代码转化为直观的视觉表示。我们将通过一个在线工具来可视化网络结构,并理解其内部数据流动过程。


上一节我们介绍了如何用代码构建一个多层神经网络。本节中我们来看看如何将这个抽象的代码结构可视化,以便更好地理解其工作原理。

我们创建的第一个多层神经网络目前只是页面上的代码。然而,这确实是使用 PyTorch 构建机器学习模型的主要形式:创建若干层(可简单可复杂),然后在某种形式的前向传播计算中使用这些层。

为了让理解更直观,我们可以借助 TensorFlow Playground 这个工具。TensorFlow 是另一个类似于 PyTorch 的深度学习框架。这个工具允许你编写类似的代码来构建神经网络,将模型拟合到某种数据中以发现模式,然后将这些机器学习模型用于你的应用程序。

以下是访问地址:

playground.tensorflow.org

这是一个可以在浏览器中训练的神经网络,非常酷。我们这里有一个数据集,它类似于我们正在处理的数据。例如,我们的“圆圈1”数据集,可以说它足够接近——它是圆形的,这正是我们想要的。

如果我们增加这里的神经元数量,现在有 5 个神经元。这里有两个特征 X1 和 X2。这让你想起了什么?这里有很多我们尚未涉及的内容,但暂时不必担心。我们只关注这里的神经网络结构。

我们有一些特征作为输入,还有 5 个隐藏单元。这正是我们刚刚构建的模型所发生的情况:我们传入 x1 和 x2 的值。回到我们的数据集,这些就是 x1 和 x2。我们将它们传入,所以有两个输入特征,然后将它们传递到一个隐藏层——一个包含 5 个神经元的单隐藏层。

我们刚刚构建了什么?查看我们的模型代码:

# 一层:输入特征2,输出特征5
self.layer_1 = nn.Linear(in_features=2, out_features=5)
# 二层:输入特征5,输出特征1
self.layer_2 = nn.Linear(in_features=5, out_features=1)

这与我们在 Playground 中构建的模型完全相同。

现在,让我们暂时将激活函数切换回线性,因为我们目前坚持使用线性。我们稍后会研究不同形式的激活函数。也许我们已经将学习率设置为 0.01,这里有训练轮数(epochs),任务是分类。我们将尝试让这个神经网络拟合这些数据。让我们看看会发生什么。

测试损失大约在中间位置,0.5,即大约 50% 的损失。如果我们只有两个类别,损失为 50% 意味着什么?完美损失是 0,最差损失是 1。我们只是将 1 除以 2 得到 50%。但我们只有两个类别。这意味着如果我们的模型只是随机猜测,它将获得大约 0.5 的损失,因为你可以随机猜测任何数据点属于蓝色还是橙色(在本例中)。

在二分类问题中,如果每个类别(本例中的蓝点和橙点)的样本数量相同,随机猜测的正确率大约为 50%,就像抛硬币一样。抛硬币 100 次,你大约会得到 50/50 的结果,可能略有不同,但从长远来看大致如此。

我们刚刚拟合了 3000 个轮次,损失仍然没有任何改善。我想知道我们的神经网络是否也会出现这种情况。

为了以不同的方式绘制这个网络,我将使用一个名为 Figjam 的小工具,它只是一个我们可以在上面放置形状的白板,并且基于浏览器。所以这不会很花哨,它将是一个简单的图表。

假设这是我们的输入(用绿色表示,因为我最喜欢的颜色是绿色)。然后我们将有一些不同颜色的点。这里可以是一个蓝点(点 1),那里是另一个点(点 2)。我们正在这里构建一个神经网络,这正是我们刚刚构建的。让我们把这个标记为输入 X1,这样更有意义。然后复制这个作为 X2。接着我们会有某种形式的输出(用橙色表示)。

你可以想象这里的连接点:它们会连接起来。我们的输入将经过所有这些点。虽然画起来可能有点复杂,但这没关系。这就是我们所做的:这里有两个输入特征。如果我们想继续画这些连接,我们可以。所有这些输入特征都将经过我们刚刚绘制的所有隐藏单元(同样的箭头画了两次,这没关系)。但这就是前向传播计算中发生的情况。

当我们用代码实现时,可能会有点令人困惑。为什么?从这里看,我们似乎只有一个输入层、一个蓝色的单隐藏层和一个输出层。但实际上,这是完全相同的形状。你明白了要点:所有这些都连接到输出。不过,我们稍后会从计算角度看到这一点。

无论你处理什么数据集,你都必须构建某种形式的输入层。如果你有 10 个特征,这里可能有 10 个输入;如果你有 4 个特征,就有 4 个。然后,如果你想调整这些,你可以增加隐藏单元的数量或某一层的输出特征数量。需要记住的是,传入的层必须具有与这里传出的内容相似的形状。

在我们的案例中,我们只有一个输出,所以输出在这里。

这是一个视觉版本。我们使用了 TensorFlow Playground。你可以用它来探索和调整。例如,你可能想要 5 个隐藏层,每层 5 个神经元。这是一种有趣的探索方式。

这里有一个挑战:访问 playground.tensorflow.org,复制这个网络,看看它是否能拟合这类数据。你认为它会成功吗?我们将在接下来的几个视频中找出答案。

在下一个视频中,我将向你展示另一种创建我们刚刚构建的网络的方法,使用的代码甚至比之前更少。我们下个视频见。


本节课中我们一起学习了如何将神经网络代码可视化。我们利用 TensorFlow Playground 工具直观地看到了网络的结构(输入层、隐藏层、输出层)以及数据流动的方向。我们还理解了在二分类问题中 50% 损失的含义,并动手尝试了调整网络参数。可视化是理解复杂模型的有力工具,能帮助我们更好地设计和调试神经网络。

73:使用nn.Sequential重构模型与探索内部机制 🔍

在本节课中,我们将学习如何使用PyTorch的nn.Sequential模块来重构之前创建的神经网络模型,并深入探索模型内部的参数结构。我们将比较nn.Sequential与子类化nn.Module两种构建模型方式的异同,并理解模型初始化时随机参数的含义。


概述

上一节我们通过子类化nn.Module创建了一个名为CircleModelV0的简单神经网络。该模型包含两个线性层,用于处理我们的二分类圆形数据。本节中,我们将看看如何使用更简洁的nn.Sequential方法来构建相同的模型,并借此机会探索模型内部的权重和偏置参数。

使用nn.Sequential重构模型

nn.Sequential提供了一种按顺序堆叠层的简单方式来构建模型。对于结构简单的网络,这比子类化nn.Module更加便捷。

以下是使用nn.Sequential重构我们之前模型的代码:

model_0 = nn.Sequential(
    nn.Linear(in_features=2, out_features=5),
    nn.Linear(in_features=5, out_features=1)
).to(device)

这段代码创建了一个与我们之前CircleModelV0功能完全相同的模型。第一层将2个输入特征映射到5个特征,第二层再将这5个特征映射到1个输出特征。

两种构建方式的对比

现在,让我们对比一下两种构建模型的方式。

  • nn.Sequential:优点是代码简洁,适用于按顺序执行各层的简单网络。它自动处理了前向传播的逻辑。
  • 子类化nn.Module:优点是灵活性高。当网络需要更复杂的前向传播逻辑(例如跳跃连接、条件分支)时,必须使用这种方法。我们从子类化开始学习,是为了理解模型构建的基础原理。

实际上,我们也可以在子类化nn.Module的内部使用nn.Sequential来组织层,结合两者的优点:

class CircleModelV0(nn.Module):
    def __init__(self):
        super().__init__()
        self.two_linear_layers = nn.Sequential(
            nn.Linear(in_features=2, out_features=5),
            nn.Linear(in_features=5, out_features=1)
        )

    def forward(self, x):
        return self.two_linear_layers(x)

这体现了PyTorch的灵活性:构建模型有多种途径,你可以根据复杂度和清晰度选择最适合的一种。

探索模型内部参数

构建好模型后,我们可以查看其内部的权重和偏置参数。这些参数在模型初始化时被随机设置,并将在后续的训练过程中通过优化器进行调整。

使用以下代码可以查看模型的状态字典:

print(model_0.state_dict())

输出将显示类似以下的内容:

OrderedDict([('0.weight', tensor([[...]])),
             ('0.bias', tensor([...])),
             ('1.weight', tensor([[...]])),
             ('1.bias', tensor([...]))])

以下是关于这些参数的说明:

  • 0.weight:对应第一层(索引0)的权重矩阵。其形状为(5, 2),因为该层有5个神经元,每个神经元接收2个输入。2 * 5 = 10,所以共有10个权重值。
  • 0.bias:对应第一层的偏置向量。其形状为(5,),每个神经元有一个偏置。
  • 1.weight1.bias:对应第二层的权重(形状(1, 5))和偏置(形状(1,))。

这些随机初始化的参数是模型训练的起点。试想一下,如果我们的网络有50层,每层128个神经元,手动管理所有这些参数将是不可行的。PyTorch在幕后为我们自动创建并管理了这些参数,这正是其强大之处。

使用未训练模型进行预测

现在,让我们用这个尚未训练的模型在测试数据上进行预测,以观察其初始性能。

我们需要确保数据和模型在同一个设备上(例如CPU或GPU),并使用推理模式来禁用梯度计算以提升效率。

model_0.eval()
with torch.inference_mode():
    untrained_preds = model_0(X_test)

查看前10个预测值和对应的真实标签:

print(f"First 10 predictions:\n{torch.round(untrained_preds[:10])}")
print(f"First 10 test labels:\n{y_test[:10]}")

你可能会发现,预测值(经过四舍五入后可能全是0或1)与真实标签y_test(值为0或1)完全不匹配,甚至不在同一个范围内(原始预测值是浮点数)。这是因为模型的参数是随机的,它还没有从数据中学到任何规律。在二分类问题中,一个随机模型大约有50%的准确率,就像抛硬币一样。

这个练习的关键在于:模型的预测需要与标签的格式(数据类型和形状)一致。我们将在后续课程中通过损失函数、优化器和训练循环来调整模型参数,使其预测逐渐逼近真实标签。


总结

本节课中我们一起学习了:

  1. 使用 nn.Sequential 快速构建顺序神经网络模型。
  2. 对比了 nn.Sequential子类化nn.Module 两种方法的适用场景与优劣。
  3. 探索了模型内部的 权重(weight)偏置(bias) 参数,理解了它们如何根据网络结构自动初始化。
  4. 使用未训练的模型进行预测,观察到随机参数导致预测结果不准确,这引出了对模型训练的需求。

下一节,我们将为模型选择合适的损失函数和优化器,并构建一个完整的训练循环,开始让我们的模型从数据中学习。

74:分类网络的损失函数、优化器与评估函数 🧠

在本节课中,我们将学习如何为分类神经网络选择合适的损失函数和优化器,并创建一个评估模型性能的准确率函数。上一节我们构建了一个简单的线性分类模型,本节我们将为其配备必要的训练组件。

设置损失函数与优化器

我们已经构建了模型。现在需要为其选择损失函数和优化器。损失函数衡量模型预测的错误程度,而优化器则根据损失来更新模型的参数。

损失函数和优化器的选择取决于具体问题。例如:

  • 对于回归问题(预测数值),常用平均绝对误差均方误差
  • 对于分类问题(预测类别),常用二元交叉熵交叉熵

我们当前处理的是二元分类问题,因此将使用二元交叉熵损失。

以下是PyTorch中一些常见的损失函数和优化器示例:

问题类型 损失函数 (PyTorch) 优化器 (PyTorch)
回归 nn.L1Loss(), nn.MSELoss() torch.optim.SGD, torch.optim.Adam
二元分类 nn.BCEWithLogitsLoss() torch.optim.SGD, torch.optim.Adam
多类分类 nn.CrossEntropyLoss() torch.optim.SGD, torch.optim.Adam

实现损失函数:BCEWithLogitsLoss

我们将使用 torch.nn.BCEWithLogitsLoss()。这个损失函数将Sigmoid激活函数和二元交叉熵损失结合在一个类中,比单独使用Sigmoid层再计算BCE损失在数值上更稳定。

# 设置损失函数
loss_fn = torch.nn.BCEWithLogitsLoss()

实现优化器:随机梯度下降

对于优化器,我们选择经典的随机梯度下降。我们需要告诉优化器要更新哪些参数(即模型的参数),并设置一个学习率。

# 设置优化器
optimizer = torch.optim.SGD(params=model.parameters(), lr=0.1)

创建评估函数:准确率

对于分类问题,准确率是一个直观的评估指标。它表示模型预测正确的样本占总样本的比例。

准确率的计算公式可以表示为:
准确率 = (正确预测的样本数 / 总样本数) * 100%

以下是使用PyTorch实现准确率计算的函数:

def accuracy_fn(y_true, y_pred):
    """计算预测准确率(百分比)"""
    correct = torch.eq(y_true, y_pred).sum().item()
    acc = (correct / len(y_pred)) * 100
    return acc

这个函数的工作原理是:

  1. 使用 torch.eq 比较真实标签 y_true 和预测标签 y_pred,得到一个布尔值张量。
  2. 对布尔值张量求和(True 计为1,False 计为0),得到正确预测的总数。
  3. 将正确预测数除以总样本数,再乘以100,得到百分比形式的准确率。

总结

本节课中,我们一起学习了为分类模型配置核心训练组件:

  1. 损失函数:我们选择了 BCEWithLogitsLoss,它专为二元分类设计,并内置了Sigmoid激活函数。
  2. 优化器:我们使用了随机梯度下降优化器,它将根据损失自动调整模型参数。
  3. 评估函数:我们创建了 accuracy_fn 函数,用于在训练过程中监控模型的预测准确率。

现在,我们已经拥有了模型、损失函数、优化器和评估指标。在接下来的课程中,我们将把这些组件整合到一个训练循环中,开始训练我们的分类模型。

75:从模型逻辑值到预测概率与标签 📊➡️🎯

在本节课中,我们将学习如何将模型的原始输出(逻辑值)转换为预测概率,并最终得到预测标签。这是评估分类模型性能的关键步骤。

上一节我们讨论了分类模型的不同损失函数选项。本节中,我们来看看如何构建一个训练循环,并理解模型输出转换的具体过程。

概述:模型输出的转换流程

为了评估我们的模型,我们需要将模型的原始输出转换为可以与真实标签进行比较的格式。这个过程分为三个步骤:

  1. 逻辑值:模型的原始输出。
  2. 预测概率:通过激活函数(如Sigmoid)将逻辑值转换为概率值。
  3. 预测标签:对概率值进行四舍五入或取最大值,得到最终的类别标签。

以下是实现这一流程的具体步骤。

1. 理解逻辑值

在机器学习和深度学习中,模型的原始输出被称为逻辑值。它尚未经过任何激活函数的处理。

# 获取模型在测试数据上的原始输出(逻辑值)
with torch.inference_mode():
    y_logits = model_0(X_test.to(device))

逻辑值是模型线性层计算的结果。对于一个线性层,其背后的数学公式是:
y = x * W^T + b
其中 W 是权重,b 是偏置项。

2. 转换为预测概率

为了将逻辑值转化为易于解释的概率(通常在0到1之间),我们需要使用激活函数。

  • 对于二元分类问题,我们使用 Sigmoid 激活函数。
  • 对于多类分类问题,我们使用 Softmax 激活函数。
# 使用Sigmoid函数将逻辑值转换为预测概率
y_pred_probs = torch.sigmoid(y_logits)

现在,y_pred_probs 中的每个值都代表了模型认为该样本属于类别1(例如“红点”)的概率。

3. 转换为预测标签

得到预测概率后,我们需要一个决策边界来将其转换为具体的类别标签。

对于二元分类,通常的规则是:

  • 如果预测概率 ≥ 0.5,则预测标签为 1
  • 如果预测概率 < 0.5,则预测标签为 0

我们可以使用 torch.round() 函数自动实现这个四舍五入的过程。

# 将预测概率四舍五入,得到预测标签
y_pred_labels = torch.round(y_pred_probs)

你也可以将步骤2和3合并为一行代码:

y_pred_labels = torch.round(torch.sigmoid(model_0(X_test.to(device))))

为了验证我们分步计算的结果与合并计算的结果一致,可以进行如下检查:

# 检查两种方式得到的预测标签是否相同
print(torch.eq(y_pred_labels.squeeze(), y_pred_labels_combined.squeeze()))

这里使用 .squeeze() 是为了移除张量中多余的维度,确保形状一致以便比较。

总结与下一节预告

本节课中我们一起学习了模型输出的完整转换流程:从逻辑值预测概率,再到预测标签。理解这个过程对于正确计算损失和评估模型准确率至关重要。

目前,由于模型参数是随机初始化的,我们的预测结果还很差。但这为我们搭建训练循环做好了准备。下一节,我们将把这些步骤整合到完整的PyTorch训练与测试循环中,遵循“前向传播 -> 计算损失 -> 梯度清零 -> 反向传播 -> 优化器更新”的步骤,开始真正地训练我们的模型。

76:编写分类模型的训练、测试与优化循环 🚀

在本节课中,我们将学习如何为一个分类问题编写完整的训练、测试和优化循环。我们将从模型的前向传播开始,计算损失和准确率,然后通过反向传播和优化器步骤来更新模型参数。


概述

上一节我们介绍了如何将模型的原始输出(logits)通过激活函数(如sigmoid)转换为预测概率和标签。本节中,我们将把这些概念整合起来,编写一个完整的训练和测试循环,以训练我们的第一个分类模型。


设置环境与数据

首先,我们需要确保代码的可重复性,并将数据移动到正确的设备上(例如GPU)。

import torch

# 设置随机种子以确保结果可重复
torch.manual_seed(42)
# 如果使用CUDA设备,也设置CUDA的随机种子
torch.cuda.manual_seed(42)

# 设置训练轮数
epochs = 100

# 将数据移动到目标设备(例如GPU)
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device)

构建训练与评估循环

以下是构建训练和评估循环的核心步骤。我们将逐步实现每个部分。

1. 前向传播与预测

在训练循环中,我们首先进行前向传播,获取模型的原始输出(logits),然后将其转换为预测标签。

# 将模型设置为训练模式
model_0.train()

# 前向传播:获取logits(模型的原始输出)
y_logits = model_0(X_train).squeeze()

# 将logits转换为预测标签
# 1. 使用sigmoid将logits转换为预测概率
# 2. 使用round将概率四舍五入为0或1的标签
y_pred = torch.round(torch.sigmoid(y_logits))

2. 计算损失与准确率

接下来,我们计算损失和准确率。这里我们使用BCEWithLogitsLoss损失函数,它直接接受logits作为输入。

# 计算损失
loss = loss_fn(y_logits, y_train)  # loss_fn是BCEWithLogitsLoss

# 计算准确率
acc = accuracy_fn(y_true=y_train, y_pred=y_pred)

3. 优化器步骤

优化器步骤包括梯度清零、反向传播和参数更新。

# 梯度清零
optimizer.zero_grad()

# 反向传播:计算梯度
loss.backward()

# 优化器步骤:更新参数
optimizer.step()

4. 测试循环

在测试循环中,我们将模型设置为评估模式,并进行前向传播以计算测试损失和准确率。

# 将模型设置为评估模式
model_0.eval()

with torch.inference_mode():
    # 前向传播:获取测试logits
    test_logits = model_0(X_test).squeeze()
    # 将logits转换为预测标签
    test_pred = torch.round(torch.sigmoid(test_logits))

    # 计算测试损失和准确率
    test_loss = loss_fn(test_logits, y_test)
    test_acc = accuracy_fn(y_true=y_test, y_pred=test_pred)

5. 打印结果

最后,我们打印每个epoch的训练和测试结果,以便监控模型的训练进度。

if epoch % 10 == 0:
    print(f"Epoch: {epoch} | Loss: {loss:.5f} | Acc: {acc:.2f}% | Test Loss: {test_loss:.5f} | Test Acc: {test_acc:.2f}%")

完整代码示例

以下是完整的训练和测试循环代码:

# 训练循环
for epoch in range(epochs):
    # 训练步骤
    model_0.train()
    y_logits = model_0(X_train).squeeze()
    y_pred = torch.round(torch.sigmoid(y_logits))
    loss = loss_fn(y_logits, y_train)
    acc = accuracy_fn(y_true=y_train, y_pred=y_pred)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    # 测试步骤
    model_0.eval()
    with torch.inference_mode():
        test_logits = model_0(X_test).squeeze()
        test_pred = torch.round(torch.sigmoid(test_logits))
        test_loss = loss_fn(test_logits, y_test)
        test_acc = accuracy_fn(y_true=y_test, y_pred=test_pred)

    # 打印结果
    if epoch % 10 == 0:
        print(f"Epoch: {epoch} | Loss: {loss:.5f} | Acc: {acc:.2f}% | Test Loss: {test_loss:.5f} | Test Acc: {test_acc:.2f}%")

总结

本节课中,我们一起学习了如何为一个分类模型编写完整的训练、测试和优化循环。我们从设置环境和数据开始,逐步实现了前向传播、损失计算、反向传播和参数更新等关键步骤。通过监控训练和测试的损失与准确率,我们可以评估模型的性能并优化其表现。在接下来的课程中,我们将进一步探讨如何改进模型和训练过程。

77:下载可视化模型预测的辅助函数 📥

在本节课中,我们将学习如何下载并使用一个名为 plot_decision_boundary 的辅助函数,以可视化我们模型的预测结果。这将帮助我们直观地理解模型为何表现不佳,并指导我们进行下一步的改进。


概述

在上节课中,我们编写了大量代码来训练一个二分类模型,但模型的准确率始终在 50% 左右徘徊,损失值也没有明显下降。这表明我们的模型并没有真正学习到数据中的模式。为了深入探究原因,我们需要将模型的预测结果可视化。


回顾上节课内容

上一节我们介绍了训练循环的完整代码,并讨论了二元交叉熵损失函数(BCELoss)和带 Logits 的二元交叉熵损失函数(BCEWithLogitsLoss)之间的区别。我们了解到,BCELoss 期望输入是经过 Sigmoid 函数处理后的预测概率,而 BCEWithLogitsLoss 则可以直接处理模型输出的原始 Logits。

我们的自定义准确率函数需要将预测概率转换为预测标签,以便与真实标签进行比较。然而,尽管代码结构正确,模型的表现却不尽如人意。


分析当前问题

运行训练循环后,我们发现损失值下降不明显,准确率也始终在 50% 左右。考虑到我们的数据集是平衡的(每个类别各有 500 个样本),这个结果意味着模型的表现和随机猜测差不多。

以下是数据分布的分析:

# 检查数据集中每个类别的样本数量
circles.label.value_counts()

结果显示,我们确实有 500 个类别 0 的样本和 500 个类别 1 的样本。因此,模型需要超越简单的猜测才能取得更好的性能。


引入可视化工具

为了直观地理解模型为何失败,我们将引入一个名为 plot_decision_boundary 的辅助函数。这个函数能够绘制出模型在特征空间中所做的决策边界,让我们清楚地看到模型是如何尝试分割数据的。

这个函数来源于 Made With ML 课程,由 Goku Mohandas 创建,是一个极佳的机器学习学习资源。我们稍作修改以适配本课程。


下载辅助函数

我们将编写代码,从 GitHub 仓库下载 helper_functions.py 文件。如果文件已存在,则跳过下载。

以下是下载代码:

import requests
import pathlib

# 检查 helper_functions.py 是否已存在
if pathlib.Path("helper_functions.py").is_file():
    print("helper_functions.py 已存在,跳过下载。")
else:
    print("正在下载 helper_functions.py...")
    # 获取文件的原始内容
    request = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/helper_functions.py")
    # 将内容写入文件
    with open("helper_functions.py", "wb") as f:
        f.write(request.content)
    print("下载完成!")

运行这段代码后,helper_functions.py 文件将被下载到当前工作目录中。


导入并使用函数

下载完成后,我们可以从该文件中导入所需的函数。

from helper_functions import plot_predictions, plot_decision_boundary

现在,我们可以使用 plot_decision_boundary 函数来可视化模型在训练集和测试集上的决策边界。

以下是可视化代码:

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))

# 绘制训练集的决策边界
plt.subplot(1, 2, 1)
plt.title("训练集")
plot_decision_boundary(model_0, X_train, y_train)

# 绘制测试集的决策边界
plt.subplot(1, 2, 2)
plt.title("测试集")
plot_decision_boundary(model_0, X_test, y_test)

plt.show()

运行这段代码将生成两个子图,分别展示模型在训练数据和测试数据上的决策边界。


解读可视化结果

可视化结果清晰地显示,我们的模型试图用一条直线来分割数据。然而,我们的数据是环形分布的,一条直线无法有效地将两个类别分开。这就是为什么模型的准确率始终在 50% 左右,损失值也无法下降的根本原因。

我们的模型仅由线性层构成,而线性层只能学习线性关系。要解决非线性问题(如环形数据),我们需要在模型中引入非线性激活函数。


挑战与思考

在进入下一节课之前,你可以尝试以下挑战:

  • 将模型训练更长时间(例如 1000 个周期),观察准确率和损失值是否有改善。
  • 思考为什么仅增加训练周期可能无法解决根本问题。

总结

本节课中我们一起学习了如何下载并使用 plot_decision_boundary 辅助函数来可视化模型的决策边界。通过可视化,我们发现了模型表现不佳的根本原因:它试图用线性决策边界来分割非线性数据。这为我们下一步改进模型指明了方向——我们需要引入非线性能力。

在下一节课中,我们将探讨如何通过添加非线性激活函数来增强模型的表达能力,使其能够学习更复杂的模式。

78:模型改进方案探讨 🚀

在本节课中,我们将探讨如何改进一个表现不佳的PyTorch模型。我们将从模型本身的角度出发,分析多种提升性能的策略,并理解其背后的原理。

概述

上一节我们构建了一个训练循环,并评估了模型在红蓝点分类任务上的表现。然而,我们的线性模型仅能绘制直线,可能无法有效分离非线性分布的数据。本节我们将系统地讨论模型改进的几种核心方法。

模型改进的核心策略

以下是几种从模型角度出发的改进方案。

1. 增加更多层

为模型提供更多学习数据模式的机会。当前模型(Model 0)仅有两层(输入层和输出层)。增加层数意味着增加模型参数的数量,使其能够学习更复杂的数据表示。

代码示例: 从2层结构扩展到包含多个隐藏层的深度网络。

2. 增加隐藏单元数量

隐藏单元是层内的神经元。当前模型中每层有5个隐藏单元。我们可以增加这个数量,例如从5个增加到10个或更多。这同样增加了模型的可学习参数,增强了其表示能力。

公式/概念: 更多参数 ≈ 更强的数据表示潜力(但需警惕过拟合)。

3. 延长训练时间

每个训练周期(epoch)是模型完整查看一次数据的机会。也许100个周期不足以让模型充分学习。我们可以尝试训练更长时间,例如1000个周期。

4. 更改激活函数

我们目前使用Sigmoid激活函数,这适用于二元分类问题。然而,在网络层之间还可以使用其他激活函数(例如ReLU),这可能会引入非线性,帮助模型学习更复杂的决策边界。

提示: nn.ReLU() 是一个常用的激活函数。

5. 调整学习率

学习率决定了优化器每次更新参数的步长。

  • 学习率过小:模型学习速度极慢。
  • 学习率过大:参数更新步伐太大,可能导致梯度爆炸(exploding gradient)或模型不稳定。

6. 更改优化器

我们目前使用随机梯度下降(SGD)。Adam是另一种在许多问题上表现良好的流行优化器,值得尝试。

7. 更改损失函数

对于当前的二元分类任务,Sigmoid配合二元交叉熵损失(BCEWithLogitsLoss)是标准且有效的选择,因此暂时可以保持不变。

改进方案实践思路

现在,让我们将上述策略与我们当前的实验流程结合起来看。

我们已完成数据准备、模型构建、训练循环和初步评估。现在处于“第5步:通过实验进行改进”。我们暂不需要使用TensorBoard等高级工具,而是先进行高层面的实验。

如果我们想构建一个更大的模型,可以综合运用多种策略:

  • 增加层数:例如,构建一个包含6个线性层的模型。
  • 增加隐藏单元:将各层的特征数从100逐步增加到128、256等。深度学习中对齐层间输入输出特征数至关重要。
  • 添加激活函数:在部分线性层之间插入nn.ReLU()
  • 更换优化器:从SGD改为Adam。
  • 调整学习率:如果原学习率过高,可以将其除以10。
  • 延长训练:将训练周期从10个增加到100个。

总结

本节课我们一起学习了改进PyTorch模型的多种核心策略。关键在于理解模型容量(层数、隐藏单元数)、训练过程(周期数、学习率、优化器)以及模型结构(激活函数)对最终性能的影响。由于我们的简单线性模型无法很好地分离非线性数据,在接下来的实践中,我鼓励你首先尝试增加层数、增加隐藏单元以及延长训练时间。我们将在下一课中编写代码来具体实现这些改进步骤。

79:创建含更多层与隐藏单元的增强模型 🚀

在本节课中,我们将学习如何通过调整模型的超参数来提升其性能。具体来说,我们将创建一个新的模型,增加其隐藏层数和每层的神经元(隐藏单元)数量,并延长训练时间,以期获得更好的预测结果。


概述:从模型角度提升性能

上一节我们讨论了从模型角度提升性能的几个选项。这些选项直接作用于模型本身,而非数据。在机器学习和深度学习中,这类我们可以手动调整的数值被称为超参数

超参数参数不同:

  • 参数是模型内部自动学习的数值,例如权重(weights)和偏置(biases)。
  • 超参数是我们作为工程师或科学家可以调整的设定,例如层数、隐藏单元数、训练轮次(epochs)、激活函数、学习率和损失函数。

本节课,我们将通过改变几个关键超参数来构建一个增强模型。


构建增强模型:CircleModelV1

我们将创建一个名为 CircleModelV1 的新模型。虽然可以使用 nn.Sequential 来构建,但为了练习以及为将来构建更复杂的模型做准备,我们将采用子类化 nn.Module 的方式。

我们计划进行三项主要改进:

  1. 增加隐藏单元数量(从5个增加到10个)。
  2. 增加网络层数(从2层增加到3层)。
  3. 增加训练轮次(从100轮增加到1000轮)。

科学实验提示:在实际的机器学习实验中,最好一次只改变一个变量并跟踪结果,这被称为实验追踪。这样我们才能明确知道是哪个改动带来了性能提升或下降。本节课为了演示,我们将同时进行多项改动。

以下是模型的定义代码:

import torch
from torch import nn

class CircleModelV1(nn.Module):
    def __init__(self):
        super().__init__()
        # 第一层:输入特征2个,输出10个隐藏单元
        self.layer_1 = nn.Linear(in_features=2, out_features=10)
        # 第二层:输入10个,输出10个隐藏单元
        self.layer_2 = nn.Linear(in_features=10, out_features=10)
        # 第三层:输入10个,输出1个(最终预测)
        self.layer_3 = nn.Linear(in_features=10, out_features=1)

    def forward(self, x):
        # 将数据依次通过所有层
        # 这种写法有助于PyTorch在后台进行可能的运算优化
        return self.layer_3(self.layer_2(self.layer_1(x)))

代码解析

  • in_featuresout_features 必须前后匹配。第一层的 in_features=2 对应我们的输入数据 X 有两个特征。
  • 最后一层的 out_features=1 对应我们的目标标签 y 是一个单一数值(二分类问题通常输出一个概率值)。
  • forward 方法中,我们采用了嵌套调用的方式 layer_3(layer_2(layer_1(x))),这等价于分步计算,但有时能利用PyTorch的图优化带来速度提升。

现在,让我们创建这个模型的实例并将其发送到可用设备上,以确保代码的设备无关性。

# 创建模型实例
model_1 = CircleModelV1()

# 将模型发送到目标设备(例如GPU,如果可用)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_1.to(device)

# 查看模型结构
print(model_1)

下一步:训练与评估循环

我们的增强模型已经构建完成。接下来,要训练这个模型,我们需要完成以下步骤:

以下是需要完成的准备工作列表:

  1. 创建损失函数:我们将使用与之前模型类似的损失函数,例如适用于二分类任务的 nn.BCEWithLogitsLoss
  2. 创建优化器:例如 torch.optim.SGDtorch.optim.Adam,用于更新模型的参数。
  3. 编写训练与评估循环:我们需要编写代码来让模型在数据上训练1000个轮次,并在每个轮次后评估其性能。

我建议你先尝试自己完成这些步骤。在接下来的视频中,我们将一起实现完整的训练和评估流程,看看增加层数、隐藏单元和训练时间是否能带来更好的结果。


总结

本节课中我们一起学习了:

  • 区分了模型的参数超参数
  • 从模型角度出发,通过调整超参数(如层数、隐藏单元数、训练轮次)来尝试提升性能。
  • 动手创建了一个增强模型 CircleModelV1,它拥有三层网络和更多的隐藏单元。
  • 了解了在科研中应遵循一次只改变一个变量的原则以进行有效的实验追踪。

在下一节,我们将为这个新模型配置损失函数和优化器,并开始训练它,观察其性能变化。

80:编写训练测试代码验证模型性能提升 🚀

在本节课中,我们将学习如何编写完整的训练和测试循环代码,以验证我们新构建的模型(CircleModelV1)在数据集上的性能表现。我们将设置损失函数、优化器,并观察模型是否能够从数据中学习到有效的模式。


模型与数据准备回顾

上一节我们通过子类化 nn.Module 创建了 CircleModelV1。这是对 CircleModelV0 的升级,我们增加了隐藏单元数量(从5个到10个),并额外添加了一个隐藏层。我们已经准备好了一个模型实例。

在流程图中,我们目前处于“构建模型”阶段。我们的数据没有改变,现在需要选择损失函数和优化器。

选择损失函数与优化器

我们继续使用与之前相同的损失函数和优化器,以保持实验条件的一致性。

以下是损失函数和优化器的设置代码:

loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(params=model_1.parameters(), lr=0.1)

关于模型复杂度的说明:增加隐藏单元和层数,本质上是为模型提供了更多可调整的参数(数值)。观察 model_1.state_dict() 并与 model_0 对比,你会发现参数数量显著增加。这为模型创造了更多机会来学习和拟合目标数据集中的模式。这是其背后的理论依据。

设置训练环境

为了保证结果的可复现性,我们设置随机种子。同时,我们将训练周期(epochs)增加到1000次,这是我们对模型进行的第三项改进(前两项是增加隐藏单元和层数)。更长的训练时间能让模型有更多机会观察数据并优化其内部模式。

我们还需要将数据移动到目标设备(CPU或GPU),以编写设备无关的代码。

torch.manual_seed(42)
torch.cuda.manual_seed(42)

epochs = 1000

# 将数据移动到目标设备
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device), y_test.to(device)

构建训练循环

现在,我们进入核心部分——编写训练和测试循环。虽然我们可以将很多步骤函数化,但在打基础阶段,亲手编写每一步有助于加深理解。

以下是训练循环的步骤概述:

  1. 将模型设置为训练模式。
  2. 前向传播,计算原始输出(logits)。
  3. 将 logits 通过 sigmoid 函数转换为预测概率,再四舍五入得到预测标签。
  4. 计算损失和准确率。
  5. 优化器梯度归零。
  6. 反向传播,计算梯度。
  7. 优化器执行一步,更新模型参数。

测试循环的步骤类似,但需要将模型设置为评估模式,并启用 torch.inference_mode() 以节省内存和计算资源。

以下是完整的训练测试循环代码:

for epoch in range(epochs):
    ### 训练阶段
    model_1.train()
    # 1. 前向传播
    y_logits = model_1(X_train).squeeze()
    y_pred = torch.round(torch.sigmoid(y_logits))
    # 2. 计算损失和准确率
    loss = loss_fn(y_logits, y_train)
    acc = accuracy_fn(y_true=y_train, y_pred=y_pred)
    # 3. 优化器梯度归零
    optimizer.zero_grad()
    # 4. 反向传播
    loss.backward()
    # 5. 优化器更新参数
    optimizer.step()

    ### 测试阶段
    model_1.eval()
    with torch.inference_mode():
        # 1. 前向传播
        test_logits = model_1(X_test).squeeze()
        test_pred = torch.round(torch.sigmoid(test_logits))
        # 2. 计算测试损失和准确率
        test_loss = loss_fn(test_logits, y_test)
        test_acc = accuracy_fn(y_true=y_test, y_pred=test_pred)

    ### 打印进度
    if epoch % 100 == 0:
        print(f"Epoch: {epoch} | Loss: {loss:.5f}, Acc: {acc:.2f}% | Test Loss: {test_loss:.5f}, Test Acc: {test_acc:.2f}%")

运行模型与结果分析

我们运行了拥有三层和每层10个隐藏单元的新模型。然而,结果令人意外:即使训练了1000个周期,模型的准确率仍然徘徊在50%左右,就像随机猜测一样。

为了直观地理解模型的行为,我们绘制了其决策边界。图像显示,模型仍然试图用一条直线来分割我们的圆形数据。这揭示了问题的核心:我们的数据是非线性的,但模型目前仅由线性层堆叠而成,缺乏处理非线性关系的能力。

模型能力验证挑战

当前模型似乎在我们的圆形数据集上无法学习。为了验证这个模型结构本身是否具备学习能力,我向你提出一个挑战:

尝试使用我们在“PyTorch工作流基础”章节中创建的线性回归数据集,用这个 CircleModelV1 去拟合它。如果模型能在简单的线性数据上学习,那就证明模型结构是有效的,问题在于它当前无法捕捉我们数据的非线性特征。这将是下一节我们要解决的关键问题。


本节课中,我们一起编写了完整的训练与测试循环代码,并尝试用更复杂的模型(CircleModelV1)来拟合非线性数据。我们发现,仅仅增加模型的宽度和深度,而不引入非线性,无法解决当前问题。这为我们下一节引入激活函数来赋予模型非线性能力做好了铺垫。

81:创建直线数据集检验模型学习能力 📈

在本节课中,我们将学习一种重要的模型调试方法:通过创建一个简单的、已知可解的问题(如拟合一条直线)来检验我们的模型是否具备基本的学习能力。我们将沿用之前学过的 PyTorch 工作流程,但应用到一个新的、更简单的数据集上。

概述

在之前的视频中,我们尝试构建一个模型来区分蓝点和红点,但之前的努力都失败了。为了排查问题,我们将创建一个更小、更简单的问题——让模型学习拟合一条直线。这样做的目的是验证我们的模型架构或训练过程是否存在根本性错误。如果模型连一条直线都无法学习,那么它肯定无法解决更复杂的分类问题。

创建直线数据集

首先,我们需要创建一个简单的线性回归数据集。这个数据集基于一个我们熟知的公式。

以下是创建数据集的步骤:

  1. 设置参数:我们定义直线的权重和偏置。

    weight = 0.7
    bias = 0.3
    
  2. 生成特征数据:创建从 0 到 1 的 100 个数据点作为输入 X

    X_regression = torch.arange(0, 1, 0.01).unsqueeze(dim=1)
    
  3. 生成标签数据:使用线性回归公式 y = weight * X + bias 计算对应的标签 y

    y_regression = weight * X_regression + bias
    

现在,让我们检查一下数据,确保它符合预期。在机器学习中,数据探索至关重要,可视化是理解数据的有效手段。

划分训练集和测试集

拥有数据集后,如果尚未划分,我们需要将其分为训练集和测试集。模型将在训练集上学习模式,并期望能泛化到测试集上。

以下是划分数据集的步骤:

  1. 确定分割点:我们使用 80% 的数据作为训练集。

    train_split = int(0.8 * len(X_regression))
    
  2. 创建训练集:根据分割点索引出训练特征和标签。

    X_train_regression, y_train_regression = X_regression[:train_split], y_regression[:train_split]
    
  3. 创建测试集:剩余的数据作为测试集。

    X_test_regression, y_test_regression = X_regression[train_split:], y_regression[train_split:]
    

检查长度,确认我们得到了 80 个训练样本和 20 个测试样本。

可视化数据

为了直观地检查数据,我们将使用之前章节(0.1节)中创建的 plot_predictions 辅助函数。这个函数保存在 helper_functions.py 文件中,可以避免我们重复编写绘图代码。

现在,我们传入新创建的数据来绘制图表:

plot_predictions(train_data=X_train_regression,
                 train_labels=y_train_regression,
                 test_data=X_test_regression,
                 test_labels=y_test_regression)

图表将显示我们的训练数据和测试数据。目前我们还没有任何预测结果,这只是对数据的初步观察。

思考模型适配性

现在,关键问题来了:我们之前为分类问题构建的 Model 1 能否拟合这个新的回归数据集?

我们需要思考:

  • 对于这个数据集,我们是否需要调整模型的输入特征数量?
  • 我们是否需要改变模型的输出特征数量?

我们将在下一节课中寻找答案。

总结

本节课中,我们一起学习了一种核心的调试策略:通过解决一个简单问题来排查复杂模型中的故障。我们创建了一个直线数据集,并将其划分为训练集和测试集,最后对数据进行了可视化。下一步,我们将尝试用现有模型来拟合这条直线,以检验其基本的学习能力。

82:构建拟合直线数据的模型 📈

在本节课中,我们将学习如何构建一个PyTorch模型来拟合直线数据。我们将通过一个简单的回归问题,回顾模型构建、训练和评估的基本流程,为后续解决更复杂的问题打下基础。


概述

上一节我们创建了一个直线数据集。本节中,我们将调整之前为分类任务构建的模型,使其能够拟合这个回归数据集。我们将重点关注输入输出形状的匹配、损失函数的选择以及训练循环的搭建。


调整模型以适应回归数据

我们之前构建的model1是为二分类任务设计的,它接受两个输入特征。然而,我们的直线回归数据只有一个输入特征。因此,我们需要创建一个新模型,其输入层应匹配单个特征。

以下是构建model2的代码,它使用nn.Sequential以更简洁的方式定义网络结构:

model2 = nn.Sequential(
    nn.Linear(in_features=1, out_features=10),
    nn.Linear(in_features=10, out_features=1)
)

核心概念nn.Linear层执行线性变换,其公式为 y = xA^T + b。第一层将1维输入映射到10维隐藏空间,第二层再将10维特征映射回1维输出。


定义损失函数和优化器

对于回归问题,我们使用L1损失函数(平均绝对误差)。优化器则继续使用随机梯度下降法。

以下是定义损失函数和优化器的代码:

loss_fn = nn.L1Loss()
optimizer = torch.optim.SGD(params=model2.parameters(), lr=0.1)

核心概念:学习率lr是优化器在每一步更新模型参数时的乘数,控制着参数调整的幅度。


准备训练循环

在开始训练前,我们必须确保模型和数据位于相同的计算设备上(例如CPU或GPU)。以下是准备步骤:

# 将模型和数据发送到目标设备
model2.to(device)
X_train_regression = X_train_regression.to(device)
y_train_regression = y_train_regression.to(device)

# 设置训练轮数
epochs = 1000

重要提示:如果模型和数据不在同一设备上,代码将会报错。


实现训练与测试循环

现在,我们将实现完整的训练循环,并在每100轮后打印损失值以监控训练过程。

以下是训练循环的步骤:

  1. 前向传播:计算模型预测。
  2. 计算损失:比较预测值与真实标签。
  3. 反向传播:计算梯度。
  4. 优化器步骤:更新模型参数。
  5. 评估:在测试集上计算损失。
for epoch in range(epochs):
    # 训练步骤
    model2.train()
    y_pred = model2(X_train_regression)
    loss = loss_fn(y_pred, y_train_regression)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    # 测试步骤
    model2.eval()
    with torch.inference_mode():
        test_pred = model2(X_test_regression)
        test_loss = loss_fn(test_pred, y_test_regression)

    # 每100轮打印一次信息
    if epoch % 100 == 0:
        print(f"Epoch: {epoch} | Train loss: {loss:.5f} | Test loss: {test_loss:.5f}")

运行上述代码后,如果训练损失和测试损失都在下降,则表明模型正在从数据中学习。


总结

本节课中,我们一起学习了如何构建一个用于回归任务的PyTorch模型。我们调整了模型的输入维度以匹配数据,选择了合适的回归损失函数,并实现了完整的训练与评估循环。通过观察损失值的下降,我们确认了模型能够学习并拟合直线数据。在下一课中,我们将使用绘图函数来直观验证模型的预测效果。

83:评估直线数据上的模型预测 📈

在本节课中,我们将学习如何评估一个在直线数据上训练的PyTorch模型,并通过可视化预测结果来确认模型的学习能力。


概述

上一节我们成功训练了一个模型,使其能够学习直线数据。本节我们将通过可视化模型的预测结果,来确认模型确实具备学习能力,并探讨模型在更复杂数据上可能遇到的限制。

模型评估与预测可视化

我们之前构建的模型(Model 2)在直线数据上表现出学习能力。为了确认这一点,我们需要将模型的预测结果可视化。

以下是进行预测和可视化的步骤:

  1. 将模型设置为评估模式:这会关闭训练时特有的层,如Dropout。

    model_2.eval()
    
  2. 进行预测(推理):使用torch.inference_mode()上下文管理器进行高效预测。

    with torch.inference_mode():
        y_preds = model_2(X_test_regression)
    
  3. 可视化结果:使用我们之前定义的plot_predictions函数来绘制训练数据、测试数据和模型预测。

解决设备不匹配错误

在可视化过程中,我们可能会遇到一个常见错误:TypeError: can‘t convert cuda device type tensor to numpy

错误原因:我们的模型和数据可能在GPU上,但Matplotlib(用于绘图)依赖的NumPy库只能在CPU上运行。

解决方案:在将张量传递给绘图函数之前,使用.cpu()方法将其转移到CPU上。

# 对传递给绘图函数的张量调用 .cpu()
plot_predictions(train_data=X_train_regression.cpu(),
                 train_labels=y_train_regression.cpu(),
                 test_data=X_test_regression.cpu(),
                 test_labels=y_test_regression.cpu(),
                 predictions=y_preds.cpu())

结果分析

成功绘图后,我们可以看到代表模型预测的红点非常接近代表真实测试数据的绿点。这直观地证实了我们的模型(Model 2)具备学习直线关系的能力。

这个结果引出了一个关键问题:既然模型能学会直线数据,为什么之前它在圆形分类数据上表现不佳?

核心线索在于模型的构成:我们的模型目前只由线性层(nn.Linear)组成。线性函数本质上描述的是直线关系,其公式为:
y = wx + b

然而,我们的圆形分类数据并非由简单的直线构成,它包含了非线性模式。因此,一个仅由线性函数堆叠的模型,其表达能力有限,无法拟合这类复杂数据。

过渡到非线性

这揭示了我们在后续课程中要解决的核心问题:如何让模型学习非线性模式。

提示是:我们需要在模型中引入非线性激活函数。你已经在之前的代码中见过其中一个(例如 nn.ReLU)。在PyTorch的torch.nn文档中,你可以找到“Non-linear activations”部分,其中列出了各种可用的函数。


总结

本节课我们一起学习了如何评估模型在直线数据上的预测性能。我们通过可视化确认了模型具备学习能力,并发现了其局限性:纯线性模型无法处理非线性数据。这为我们下一节引入非线性激活函数来增强模型表达能力奠定了基础。

84:引入分类模型缺失环节:非线性 🧩

在本节课中,我们将要学习为什么我们之前构建的线性模型无法完美拟合非线性数据,并引入深度学习中一个至关重要的概念:非线性。我们将通过直观的例子和代码,理解线性与非线性函数的区别,以及为什么非线性是构建强大神经网络模型的关键。


线性与非线性函数

上一节我们介绍了线性模型在分类任务中的潜力,但同时也发现了它的局限性。本节中我们来看看线性与非线性函数的根本区别。

线性函数意味着直线。在数学上,一个简单的线性函数可以表示为:

公式: y = m*x + c

其中 m 是斜率,c 是截距。其图像是一条直线。

而非线性函数则意味着曲线。例如,二次函数 y = x² 的图像是一条抛物线。

在机器学习中,我们构建的神经网络本质上就是线性函数非线性函数的组合。我们之前只赋予了模型使用直线(线性函数)的能力,但我们的数据(红色和蓝色的圆圈)是弯曲的。因此,模型需要非线性能力来拟合这种数据。


为什么需要非线性?

为了更直观地理解,让我们思考一个更复杂的任务:构建一个披萨识别模型。

以下是构建图像识别模型时需要考虑的因素:

  • 你能只用直线来描绘一个披萨的形状吗?显然不能。
  • 一个计算机视觉模型不仅寻找直线,也寻找曲线。这就是机器学习的魅力所在。
  • 想象一下,如果尝试为识别100种不同食物编写规则算法,程序会变得极其庞大和复杂。
  • 相反,我们赋予深度学习模型使用线性非线性函数的能力。模型通过堆叠这些层,自动学习数据中应该使用哪些“线条”(无论是直线还是曲线)来识别模式,无论是披萨、寿司还是其他任何食物。

所以,核心问题是:如果给你无限(在机器学习中,实际上是大量)的直线和曲线,你能描绘出多么复杂的模式?答案是:非常复杂。正是这种组合能力,赋予了神经网络拟合曲线、分离不同类别(如我们的圆圈数据),甚至驱动自动驾驶汽车视觉系统的潜力。


实践:准备非线性数据

现在,让我们动手准备数据,并开始构建一个包含非线性的模型。

以下是重新创建并可视化我们的红蓝圆圈数据集的步骤:

# 1. 导入必要的库
import torch
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

# 2. 创建非线性数据集
n_samples = 1000
X, y = make_circles(n_samples=n_samples, noise=0.03, random_state=42)

# 3. 可视化数据
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.RdYlBu)
plt.show()

运行这段代码,你将看到熟悉的红蓝圆圈图案。


将数据转换为张量并分割

接下来,我们需要将数据转换为PyTorch张量,并分割为训练集和测试集。

# 4. 将数据转换为PyTorch张量
X = torch.from_numpy(X).type(torch.float)
y = torch.from_numpy(y).type(torch.float)

# 5. 分割数据为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 6. 查看前5个样本
print(X_train[:5])
print(y_train[:5])

我们使用 torch.float 类型是因为NumPy的默认数据类型与PyTorch的自动求导(autograd)系统以及GPU加速兼容。train_test_split 函数帮助我们随机分割数据,其中20%用于测试。


预告:构建非线性模型

现在,我们拥有了非线性数据集,并准备好了训练和测试张量。激动人心的部分即将开始——构建一个包含非线性层的模型。

在进入下一节之前,你可以尝试一个小挑战:查阅PyTorch官方文档(torch.nn模块),看看能否找到我们之前已经使用过的线性层,并搜索“非线性激活函数”,看看有哪些选择。这将帮助你更好地理解我们接下来要添加的“缺失环节”。


本节课中我们一起学习了线性与非线性函数的区别,理解了为什么非线性是处理像圆圈这类复杂数据所必需的,并完成了为构建非线性模型准备数据的所有步骤。在下一课,我们将正式为模型添加非线性激活函数,解锁其真正的学习潜力。

85:构建首个非线性神经网络 🧠

在本节课中,我们将学习如何构建一个包含非线性激活函数的神经网络。我们将探讨线性与非线性之间的区别,并动手编写代码,为我们的圆形数据分类模型引入非线性能力。

概述:线性与非线性

上一节我们介绍了线性模型,它只能拟合直线。本节中我们来看看非线性模型。

线性意味着直线。相反,非线性意味着非直线。

我们之前构建的线性模型可以拟合呈直线分布的数据。然而,当我们处理非线性数据时,我们需要非线性函数的能力。例如,我们当前处理的是圆形数据。

请思考一个问题:如果你拥有无限数量的直线(即线性线)和非直线(即非线性线),你能画出什么?

引入非线性激活函数

现在,让我们开始编写一个包含非线性激活函数的分类模型代码。

我们将从 torch.nn 模块导入非线性函数。torch.nn 模块包含了许多神经网络层,其中就有一类称为“非线性激活函数”。

以下是 torch.nn 中一些常见的非线性激活函数:

  • nn.Sigmoid:Sigmoid 激活函数,对输入 X 执行特定数学运算。
  • nn.ReLU:另一个常见函数,我们在研究分类网络架构时见过它。

构建非线性模型

让我们开始编写代码,构建一个带有非线性激活函数的模型。

from torch import nn

class CircleModelV2(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer_1 = nn.Linear(in_features=2, out_features=10)
        self.layer_2 = nn.Linear(in_features=10, out_features=10)
        self.layer_3 = nn.Linear(in_features=10, out_features=1)
        self.relu = nn.ReLU()

我们创建了一个名为 CircleModelV2 的类。它包含三个线性层和一个 ReLU 激活函数。

ReLU 代表“修正线性单元”。它的工作原理很简单:它接收一个输入,如果输入是负数,则将其变为 0;如果输入是正数,则保持不变。这个函数的图形不是一条直线,因此它是一种非线性激活函数。

接下来,我们需要实现前向传播方法。

    def forward(self, x):
        # 数据流向:layer_1 -> relu -> layer_2 -> relu -> layer_3
        return self.layer_3(self.relu(self.layer_2(self.relu(self.layer_1(x)))))

在前向传播中,我们的数据依次通过以下步骤:

  1. 输入 layer_1,执行线性运算。
  2. layer_1 的输出传递给 relu 函数。relu 会将 layer_1 的所有负输出变为 0,并保留正输出。
  3. 将结果传递给 layer_2,再次执行线性运算。
  4. 再次通过 relu 函数。
  5. 最后,将结果传递给 layer_3(输出层)。我们没有在最后使用 relu,因为稍后我们会将模型的原始输出(logits)传递给 sigmoid 函数。

现在,让我们实例化我们的模型。

model_3 = CircleModelV2().to(device)

挑战与总结

本节课中我们一起学习了非线性激活函数的概念,并构建了我们的第一个非线性神经网络模型 CircleModelV2

我向你提出两个挑战:

  1. 尝试编写训练代码,看看这个模型是否能在我们的圆形数据上有效工作。
  2. 访问 TensorFlow Playground,尝试重建我们这里的神经网络。你可以设置两个隐藏层(每层5个神经元,我们的模型是每层10个),将学习率设置为0.1,使用随机梯度下降,并将激活函数从“Linear”改为“ReLU”,然后运行看看会发生什么。

在接下来的视频中,我们将继续完善训练代码并评估模型的性能。

86:编写首个非线性模型训练测试代码 🚀

在本节课中,我们将学习如何构建并训练一个包含非线性激活函数的PyTorch模型。我们将使用ReLU激活函数,并观察它如何帮助模型学习非线性数据中的模式。


概述

上一节我们介绍了线性模型在处理非线性数据时的局限性。本节中,我们来看看如何通过引入非线性激活函数(如ReLU)来增强模型的能力,使其能够学习更复杂的模式。

非线性激活函数:ReLU

ReLU是一个流行且有效的非线性激活函数。神经网络的核心在于结合线性与非线性函数,从而有能力发现数据中的模式。对于我们的数据集(蓝点和红点),虽然规模小,但同样的原理适用于更大的数据集和模型。

ReLU函数的数学定义是:
output = max(0, input)

这意味着,对于负的输入,输出为0;对于正的输入,输出保持不变。这个函数本身没有需要优化的参数,这使得它计算高效。

学习率的影响

在训练开始前,我们先直观感受一下学习率(Learning Rate)对训练过程的影响。学习率控制着模型参数更新的步长。

以下是不同学习率下训练损失的下降趋势:

  • 较高的学习率(如0.1):损失值迅速下降,模型很快收敛。
  • 较低的学习率(如0.001):损失值下降缓慢,需要更多训练轮次(epochs)才能达到相同效果。

选择合适的学习率是训练成功的关键之一。

构建模型、损失函数与优化器

现在,让我们开始编写代码。我们将构建一个包含两个隐藏层和非线性激活函数的模型。

首先,我们设置损失函数和优化器。由于我们处理的是二分类问题(蓝点或红点),因此使用二元交叉熵损失(Binary Cross Entropy Loss)。

import torch
import torch.nn as nn

# 定义模型
class NonLinearModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer_1 = nn.Linear(in_features=2, out_features=5)
        self.layer_2 = nn.Linear(in_features=5, out_features=5)
        self.layer_3 = nn.Linear(in_features=5, out_features=1)
        self.relu = nn.ReLU() # 引入非线性激活函数

    def forward(self, x):
        x = self.relu(self.layer_1(x))
        x = self.relu(self.layer_2(x))
        x = self.layer_3(x)
        return x

# 实例化模型
model_3 = NonLinearModel()

# 定义损失函数和优化器
loss_fn = nn.BCEWithLogitsLoss() # 适用于logits输入的二元交叉熵
optimizer = torch.optim.SGD(params=model_3.parameters(), lr=0.1)

训练循环

接下来,我们编写训练循环代码。这个过程包括前向传播、计算损失、反向传播和优化器更新参数。

以下是训练步骤的详细说明:

  1. 前向传播:将训练数据输入模型,得到原始输出(logits)。
  2. 计算损失与准确率:使用损失函数计算预测值与真实标签之间的误差,并计算当前准确率。
  3. 反向传播:调用 loss.backward(),PyTorch会自动计算所有参数的梯度。
  4. 优化器步骤:调用 optimizer.step(),优化器根据梯度更新模型参数。
  5. 梯度清零:在每个epoch开始前,调用 optimizer.zero_grad() 清空上一轮的梯度,防止累积。
  6. 测试集评估:在推理模式下,使用测试集评估模型性能,计算测试损失和准确率。
torch.manual_seed(42)
epochs = 1000

# 将数据转移到正确的设备(如GPU)
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device), y_test.to(device)

for epoch in range(epochs):
    ### 训练阶段 ###
    model_3.train()
    # 1. 前向传播
    train_logits = model_3(X_train).squeeze()
    train_pred = torch.round(torch.sigmoid(train_logits))

    # 2. 计算损失与准确率
    train_loss = loss_fn(train_logits, y_train)
    train_acc = accuracy_fn(y_true=y_train, y_pred=train_pred)

    # 3. 优化器梯度清零
    optimizer.zero_grad()

    # 4. 反向传播
    train_loss.backward()

    # 5. 优化器更新参数
    optimizer.step()

    ### 测试阶段 ###
    model_3.eval()
    with torch.inference_mode():
        # 1. 前向传播
        test_logits = model_3(X_test).squeeze()
        test_pred = torch.round(torch.sigmoid(test_logits))

        # 2. 计算损失与准确率
        test_loss = loss_fn(test_logits, y_test)
        test_acc = accuracy_fn(y_true=y_test, y_pred=test_pred)

    # 打印训练信息
    if epoch % 100 == 0:
        print(f"Epoch: {epoch} | Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% | Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}%")

运行代码后,我们观察到模型的准确率有所提升,损失值下降。这证明了非线性激活函数的威力。仅仅添加了ReLU层,就赋予了模型结合直线(线性)与曲线(非线性)的能力,使其有可能学会区分这两个圆圈。

总结

本节课中我们一起学习了如何构建和训练一个非线性PyTorch模型。我们引入了ReLU激活函数,并理解了它如何帮助模型学习非线性数据。我们还实践了完整的训练循环,包括设置设备无关代码、定义损失函数与优化器,以及执行前向传播、反向传播和参数更新。在下一节课中,我们将可视化模型的决策边界,看看它究竟学到了什么。

87:首个非线性模型的预测与评估 📊

在本节课中,我们将学习如何对我们刚刚训练好的首个非线性模型进行预测和评估。我们将通过可视化手段来直观地理解模型的性能,并将其与之前的纯线性模型进行比较。


模型评估与可视化

在上一节中,我们训练了第一个结合了线性函数(直线)和非线性函数(非直线)能力的模型。经过1000个训练周期后,模型的结果看起来比纯猜测(准确率为50%,因为我们有500个红点和500个蓝点,类别完全平衡)要好一些。

现在,我们需要评估我们的模型,因为目前它只是纸面上的数字。我们喜欢通过可视化来评估事物。

评估一个用非线性激活函数训练的模型时,我们还需要理解一个核心观点:神经网络本质上就是线性函数和非线性函数的大型组合,旨在数据中寻找规律。

基于此,让我们用我们最近训练的模型——模型3——来进行一些预测。

我们将把它设置为评估模式,并开启推理模式。

# 将模型设置为评估模式
model_3.eval()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/daniel-bourke/img/f491fec182993468d2f3c1697b1189ca_4.png)

# 开启推理模式进行预测
with torch.inference_mode():
    y_preds = torch.round(torch.sigmoid(model_3(X_test))).squeeze()

我们在这里使用了 .squeeze() 方法,因为在之前的视频中我们遇到了一些形状错误。这实际上是一个很好的经历,因为它让我们现场解决了一个深度学习中最常见的问题之一。

让我们查看一下预测结果 y_preds,同时也查看一下真实标签 y_test

print(y_preds[:10])
print(y_test[:10])

记住,在评估预测时,我们希望预测结果的格式与原始标签的格式相同,这样才能进行公平的比较。从格式上看,这两者看起来是相同的:它们都在CUDA上,并且都是浮点数类型。我们可以看到模型在这个样本上预测错了,而其他样本看起来预测得不错。

如果我们将结果可视化,可能会看得更清楚。


绘制决策边界

你可能已经尝试过这个挑战了:绘制决策边界。让我们现在来实现它。

import matplotlib.pyplot as plt

# 设置图形大小
plt.figure(figsize=(12, 6))

# 一个子图:训练数据上的决策边界
plt.subplot(1, 2, 1)
plt.title("Train Data Decision Boundary")
plot_decision_boundary(model_3, X_train, y_train)

# 二个子图:测试数据上的决策边界
plt.subplot(1, 2, 2)
plt.title("Test Data Decision Boundary")
plot_decision_boundary(model_3, X_test, y_test)

plt.show()

plot_decision_boundary 函数会在幕后为我们创建漂亮的图形,对输入特征 X 进行预测,然后与真实值 Y 进行比较。

让我们看看结果如何。


结果分析与挑战

看!这是我们的第一个非线性模型。它并不完美,但肯定比我们之前拥有的模型要好得多。

  • 模型1(无非线性):其决策边界是一条直线,无法很好地分割非线性数据。
  • 模型3(有非线性):其决策边界是曲线,能够更好地拟合数据的分布。

你看到非线性(或者说,直线与非直线结合)的力量了吗?

不过,我觉得我们可以做得比这更好。目前的模型在测试数据上达到了大约79%的准确率。

你的挑战是:你能改进模型3,使其在测试数据上的准确率超过80%吗?

我相信你可以做到。

以下是一些改进模型的思路,你可以尝试:

  • 增加层数:在模型中添加更多的隐藏层。
  • 增加隐藏单元:增加每一层神经元的数量。
  • 延长训练时间:增加训练的周期数(epochs)。
  • 调整激活函数:如果你添加了更多层,确保在它们之上也使用ReLU等非线性激活函数。
  • 降低学习率:目前我们使用的学习率是0.1,尝试调低它看看效果。

尝试一下吧!这将是你额外的练习挑战。在下一节中,我们将动手编写代码来复现这些非线性激活函数。


总结

在本节课中,我们一起学习了如何评估非线性模型:

  1. 我们使用模型进行预测,并将其设置为评估和推理模式。
  2. 我们通过可视化决策边界,直观地比较了非线性模型与纯线性模型的性能差异。
  3. 我们分析了结果,并提出了改进模型性能的挑战,包括调整模型结构、超参数和训练过程。

记住,模型评估和迭代优化是机器学习工作流中至关重要的环节。

88:使用纯 PyTorch 复现非线性激活函数 🔧

在本节课中,我们将学习如何仅使用 PyTorch 的基础张量操作,手动复现神经网络中常用的非线性激活函数。我们将重点关注 ReLU 和 Sigmoid 函数,并理解它们如何在神经网络中发挥作用。

概述

上一节我们介绍了如何通过添加更多层、调整隐藏单元数量和学习率来改进模型性能。本节中,我们来看看构成神经网络的核心“工具”——线性与非线性函数。我们将通过手动编写代码来复现这些非线性激活函数,从而更深入地理解其内部机制。

复现线性激活函数

一切计算都始于张量。首先,我们创建一个基础张量作为输入。

import torch
import matplotlib.pyplot as plt

# 创建一个从-10到10的张量,数据类型为32位浮点数
A = torch.arange(start=-10, end=10, step=1, dtype=torch.float32)

现在,让我们可视化这个数据。它是一个从-10到10的直线,代表一个线性关系。

plt.plot(A)
plt.show()

复现 ReLU 激活函数

ReLU(Rectified Linear Unit)函数是神经网络中最常用的激活函数之一。它的定义很简单:对于输入 x,输出是 max(0, x)。这意味着所有负数值被置为0,而正数值保持不变。

我们可以直接调用 PyTorch 内置的 torch.relu 函数来查看效果。

plt.plot(torch.relu(A))
plt.show()

但我们的目标是手动复现它。以下是自定义 ReLU 函数的实现:

def custom_relu(x: torch.Tensor) -> torch.Tensor:
    """手动实现 ReLU 激活函数。"""
    return torch.maximum(torch.tensor(0), x)

让我们测试并绘制我们自定义的 ReLU 函数,可以看到它与 PyTorch 内置函数输出一致。

plt.plot(custom_relu(A))
plt.show()

复现 Sigmoid 激活函数

另一个常见的非线性激活函数是 Sigmoid。它将输入值压缩到 0 和 1 之间,其数学公式为:

σ(x) = 1 / (1 + e^(-x))

首先,我们看看 PyTorch 内置 torch.sigmoid 函数的效果。

plt.plot(torch.sigmoid(A))
plt.show()

现在,让我们根据公式手动实现它:

def custom_sigmoid(x: torch.Tensor) -> torch.Tensor:
    """手动实现 Sigmoid 激活函数。"""
    return 1 / (1 + torch.exp(-x))

绘制我们自定义的 Sigmoid 函数,验证其输出与内置函数相同。

plt.plot(custom_sigmoid(A))
plt.show()

神经网络的工作原理

通过以上练习,我们揭示了神经网络幕后的核心操作。一个神经网络本质上就是一系列线性层(进行线性变换)和非线性激活函数(如 ReLU 或 Sigmoid)的堆叠。模型通过组合这些“工具”,自动在数据中寻找最佳模式,以最小化损失函数并提高准确率。

虽然我们可以手动构建所有组件,但使用 PyTorch 提供的高层 API(如 nn.Linearnn.ReLU)有诸多好处:它们经过充分测试、计算高效、能自动利用 GPU,并且让构建模型像搭积木一样简单直观。

总结

本节课中,我们一起学习了如何从零开始复现两个关键的非线性激活函数:ReLU 和 Sigmoid。我们通过编写自定义函数并可视化结果,深入理解了它们如何将线性输入转化为非线性输出,这是神经网络能够学习复杂模式的基础。在接下来的课程中,我们将把目前为二分类问题建立的工作流程,应用到更具挑战性的多分类问题中。

89:构建多分类数据集 🎯

在本节课中,我们将学习如何为多分类问题创建和准备数据集。我们将从二分类问题过渡到多分类问题,并了解两者之间的关键区别。

概述

在之前的课程中,我们利用非线性函数的力量,学习了神经网络如何结合线性和非线性函数来发现数据中的模式。我们成功分离了红点和蓝点。现在,我们将向前推进,从多分类问题的角度来回顾我们所做的一切。

多分类问题与二分类问题的主要区别在于,前者涉及两个以上的类别。例如,二分类可能是猫 vs 狗,而多分类可能是猫 vs 狗 vs 鸡。我们将看到,在处理多分类问题时,我们的模型架构和损失函数会有所不同。

创建多分类数据集

以下是创建多分类数据集的步骤。

首先,我们需要导入必要的依赖库。

import torch
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split

接下来,我们设置数据创建的超参数。通常,我们会用大写字母来表示这些可调整的设置。

# 设置数据创建的超参数
NUM_CLASSES = 4
NUM_FEATURES = 2
RANDOM_SEED = 42

现在,让我们使用 make_blobs 函数来创建数据。这个函数可以生成我们需要的“斑点”状数据。

# 创建多分类数据
X_blob, y_blob = make_blobs(n_samples=1000,
                             n_features=NUM_FEATURES,
                             centers=NUM_CLASSES,
                             cluster_std=1.5,
                             random_state=RANDOM_SEED)

由于我们使用的是 Scikit-learn,而 PyTorch 使用张量,所以需要将数据转换为张量格式。

# 将数据转换为张量
X_blob = torch.from_numpy(X_blob).type(torch.float)
y_blob = torch.from_numpy(y_blob).type(torch.float)

然后,我们将数据集分割为训练集和测试集。

# 分割为训练集和测试集
X_blob_train, X_blob_test, y_blob_train, y_blob_test = train_test_split(X_blob,
                                                                        y_blob,
                                                                        test_size=0.2,
                                                                        random_state=RANDOM_SEED)

最后,让我们将数据可视化,以便更好地理解我们创建的数据集。

# 可视化数据
plt.figure(figsize=(10, 7))
plt.scatter(X_blob[:, 0], X_blob[:, 1], c=y_blob, cmap=plt.cm.RdYlBu)
plt.show()

总结

在本节课中,我们一起学习了如何为多分类问题构建数据集。我们导入了必要的库,设置了超参数,使用 make_blobs 函数生成了具有四个类别的数据,将数据转换为 PyTorch 张量,并将其分割为训练集和测试集。最后,我们通过可视化来观察数据的分布情况。

现在我们已经准备好了数据,在下一节中,我们将构建我们的第一个多分类模型。

90:创建PyTorch多分类模型 🧠

概述

在本节课中,我们将学习如何构建一个用于多分类任务的PyTorch模型。我们将整合之前学过的所有知识,但这次将应用于多分类数据,而非二分类数据。

回顾与准备

上一节我们使用Scikit-Learn的make_blobs函数创建了多分类数据集。现在,我们将着手构建一个能够拟合此数据的模型。

在开始编码之前,我们需要思考一个重要问题:这个数据集是否需要非线性激活函数?换句话说,我们能否用纯粹的直线来分隔这些数据点,还是需要一些非直线的边界?如果你不确定也没关系,我们将在构建模型的过程中探索这个问题。

多分类模型的关键概念

对于多分类模型,我们需要定义几个核心部分:

  • 输入层形状 (in_features):这取决于我们的数据有多少个特征。
  • 隐藏层:我们可以自由设置隐藏层的数量和每层的神经元数量。为了保持简单,我们将使用一个隐藏层。
  • 输出层形状 (out_features):我们需要为每个类别设置一个输出神经元。我们的数据有4个类别,因此需要4个输出特征。
  • 输出激活函数:我们将使用Softmax函数,这是我们尚未接触过的新概念。
  • 损失函数:我们将使用交叉熵损失,而不是二分类中使用的二元交叉熵损失。
  • 优化器:与二分类相同,常见的选择是SGD(随机梯度下降)或Adam。

构建多分类模型

现在,让我们开始构建我们的第一个多分类模型。

首先,我们将养成编写设备无关代码的习惯,这样我们的模型可以在CPU或GPU上运行。

# 设置设备
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

接下来,我们定义模型类。我们将使用nn.Sequential来按顺序堆叠层,这是一种更简洁的构建模型的方式。

import torch
from torch import nn

class BlobModel(nn.Module):
    """
    初始化一个多分类模型。

    参数:
        input_features (int): 模型的输入特征数量。
        output_features (int): 模型的输出特征数量(即类别数)。
        hidden_units (int): 隐藏层的神经元数量,默认为8。
    """
    def __init__(self, input_features, output_features, hidden_units=8):
        super().__init__()
        # 使用 nn.Sequential 按顺序堆叠层
        self.linear_layer_stack = nn.Sequential(
            nn.Linear(in_features=input_features, out_features=hidden_units),
            nn.ReLU(), # 添加非线性激活函数
            nn.Linear(in_features=hidden_units, out_features=hidden_units),
            nn.ReLU(), # 添加非线性激活函数
            nn.Linear(in_features=hidden_units, out_features=output_features),
        )

    def forward(self, x):
        # 数据将按顺序通过 linear_layer_stack 中的每一层
        return self.linear_layer_stack(x)

请注意,我们在隐藏层之间添加了nn.ReLU()作为非线性激活函数。你可以尝试在后续练习中,对比使用和不使用非线性激活函数时模型的性能差异。

现在,让我们根据数据形状来实例化模型。我们的训练数据X_train的形状显示有2个输入特征,而Y_train的独特值显示我们有4个类别。

# 实例化模型并发送到目标设备
model_4 = BlobModel(input_features=2, output_features=4, hidden_units=8).to(device)
print(model_4)

至此,我们已经创建了一个与数据形状匹配的多分类模型。接下来,我们需要定义损失函数、优化器并编写训练循环,这些内容我们将在后续课程中完成。

总结

本节课中,我们一起学习了如何构建一个PyTorch多分类模型。我们回顾了模型的关键组成部分,包括输入层、隐藏层、输出层以及Softmax激活函数和交叉熵损失函数。我们使用nn.Sequential模块创建了一个包含非线性激活函数的模型,并确保了模型结构与我们的数据形状相匹配。在下一节课中,我们将为这个模型添加训练循环,使其能够从数据中学习。

91:配置多分类模型的损失函数与优化器 🧠

在本节课中,我们将学习如何为多分类模型配置损失函数与优化器。这是构建和训练神经网络的关键步骤。

上一节我们通过子类化 nn.Module 创建了一个多分类模型。本节中,我们来看看如何为该模型选择合适的损失函数和优化器。

选择多分类损失函数

在PyTorch中,损失函数位于 torch.nn 模块中。对于回归问题,我们可能使用L1损失或MSE损失。对于分类问题,情况则不同。

对于多分类问题,我们将使用交叉熵损失。对于二分类问题,通常使用二元交叉熵损失。交叉熵损失适用于训练具有C个类别的分类问题。

以下是创建交叉熵损失函数的代码:

loss_fn = nn.CrossEntropyLoss()

CrossEntropyLoss 函数有一个可选的 weight 参数,它是一个一维张量,用于为每个类别分配权重。这在处理类别样本数量不平衡的训练集时特别有用。例如,如果你的数据集中某些类别的样本远多于其他类别,可以使用此参数进行调整。目前我们的数据集是平衡的,因此暂时不需要使用它。

创建优化器

接下来,我们需要创建一个优化器。优化器负责根据损失函数的梯度来更新模型的参数。

torch.optim 模块中有多种优化器。其中两种最常见且在许多问题上表现良好的优化器是SGD(随机梯度下降)和Adam。

以下是使用SGD优化器的代码:

optimizer = torch.optim.SGD(params=model_4.parameters(), lr=0.1)

在这段代码中:

  • params:指定优化器需要优化的参数,这里是我们模型 model_4 的所有参数。
  • lr:代表学习率,它是一个超参数。你可以尝试改变这个值,观察它对训练过程的影响。

下一步:检查模型输出与构建训练循环

现在,我们已经为多分类问题准备好了损失函数和优化器。

在进入下一节构建训练循环之前,建议你先尝试将测试数据 X_blob_test 传入模型,观察模型的原始输出。思考一下,模型的原始输出被称为什么?

本节课中我们一起学习了如何为多分类模型配置交叉熵损失函数和SGD优化器,并了解了学习率作为超参数的作用。下一节,我们将开始构建训练循环来训练我们的模型。

92:多分类模型预测流程详解 🎯

在本节课中,我们将学习如何将多分类模型的原始输出(逻辑值)转换为预测概率,并最终得到预测标签。这是评估和测试模型的关键步骤。

上一节我们为多分类模型创建了损失函数和优化器。本节中我们来看看如何利用模型进行前向传播并解读其输出。

模型输出与设备问题

首先,我们尝试对最新创建的模型(Model 4)进行一次前向传播。然而,运行代码时遇到了一个运行时错误,提示“期望所有张量位于同一设备上”。

这是因为我们的模型参数位于CUDA设备(GPU)上,而我们的输入数据X仍然在CPU上。我们可以通过以下代码检查设备和数据的所在位置:

# 检查模型参数所在的设备
next(model_4.parameters()).device
# 检查输入数据所在的设备
X.device

为了解决这个问题,我们需要确保模型和数据位于同一设备上。这是一个常见的错误,在后续操作中需要特别注意。

从逻辑值到预测概率

在进行前向传播并得到模型的原始输出(称为逻辑值或logits)后,我们需要将其转换为预测概率。

对于多分类问题,我们使用 Softmax 激活函数来完成这个转换。这与二分类问题中使用的Sigmoid函数不同。以下是转换的核心公式:

Softmax公式
[
\text{Softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}}
]

在PyTorch中,我们可以直接使用torch.softmax()函数。关键是指定要计算Softmax的维度(通常是第一个维度,即每个样本的各个类别分数)。

以下是转换步骤的代码:

# 1. 进行前向传播得到逻辑值(使用推理模式是良好习惯)
with torch.inference_mode():
    y_logits = model_4(X)

# 2. 使用Softmax将逻辑值转换为预测概率
y_pred_probs = torch.softmax(y_logits, dim=1)

经过Softmax函数处理后,每个样本的各个类别概率值之和为1,且所有值均为非负数。这符合概率的定义。

从预测概率到预测标签

得到预测概率后,我们需要确定模型认为每个样本最可能属于哪个类别。这通过获取每个样本概率向量中最大值的索引来实现。

在PyTorch中,我们使用torch.argmax()函数。该函数返回指定维度上最大值的索引。

以下是获取预测标签的代码:

# 3. 获取预测标签(即概率最大的类别索引)
y_preds = torch.argmax(y_pred_probs, dim=1)

现在,y_preds的形状与我们的测试标签y_test相同,可以直接进行比较。不过,由于模型尚未训练,此时的预测基本上是随机的,所以预测标签与真实标签匹配度很低。

核心流程总结

综上所述,评估和测试多分类模型的完整流程可以概括为以下三步:

  1. 逻辑值:模型的原始输出。
  2. 预测概率:使用Softmax函数将逻辑值转换为概率分布。
  3. 预测标签:对预测概率取argmax,得到最终的类别预测。

过渡到下一阶段

现在我们已经掌握了将模型输出转换为可解释预测的方法。接下来,我们就可以着手构建模型的训练循环和测试循环了。

这非常令人兴奋!在下一节课中,我们将一起构建多分类PyTorch模型的第一个训练和测试循环。给你一个小提示:它与我们之前构建过的训练测试循环非常相似。你可以先尝试自己动手,如果遇到困难,我们将在下个视频中共同完成。


本节课中我们一起学习了多分类模型预测的核心流程:如何将模型的原始逻辑值输出,通过Softmax函数转换为预测概率,再通过argmax操作得到最终的预测标签。这是连接模型内部计算与外部评估的关键桥梁,为后续训练模型打下了坚实基础。

93:多分类模型训练与实时代码调试 🚀

在本节课中,我们将学习如何为多分类PyTorch模型构建一个完整的训练和测试循环。我们将从模型的原始输出(logits)开始,逐步将其转换为预测概率和预测标签,并在此过程中解决常见的代码错误。


从Logits到预测标签

上一节我们介绍了多分类模型的基础。本节中我们来看看如何将模型的原始输出转换为可理解的预测结果。

模型的原始输出称为logits。对于多分类问题,如果模型处理4个类别,它将输出4个值。处理10个类别则输出10个值,但转换步骤的原理相同。

以下是转换过程的核心步骤:

  1. 使用Softmax函数将logits转换为预测概率

    prediction_probs = torch.softmax(logits, dim=1)
    

    此操作确保所有类别的概率之和为1。

  2. 通过Argmax函数获取预测标签

    prediction_labels = prediction_probs.argmax(dim=1)
    

    此操作返回预测概率中最大值所在的索引,该索引即对应预测的类别标签。

对于一个样本,如果其logits经过Softmax后得到的概率向量中,索引1处的值最大,那么该样本的预测标签就是1。


构建训练与测试循环

现在,我们将构建一个训练循环来改进模型。以下是训练循环的主要步骤:

我们将设置手动随机种子以确保结果的可复现性,并定义训练轮数(epochs)。接着,将数据移动到目标设备(CPU或GPU),这是设备无关代码的一部分。

以下是训练循环的核心代码结构:

# 设置随机种子和训练轮数
torch.manual_seed(42)
epochs = 100

# 将数据移动到设备
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device), y_test.to(device)

# 训练循环
for epoch in range(epochs):
    model.train()
    # 前向传播
    train_logits = model(X_train)
    train_pred_probs = torch.softmax(train_logits, dim=1)
    train_pred_labels = train_pred_probs.argmax(dim=1)

    # 计算损失和准确率
    loss = loss_fn(train_logits, y_train)
    acc = accuracy_fn(y_true=y_train, y_pred=train_pred_labels)

    # 反向传播与优化
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

在测试阶段,我们需要关闭梯度跟踪以加速推理过程:

    model.eval()
    with torch.inference_mode():
        test_logits = model(X_test)
        test_pred_probs = torch.softmax(test_logits, dim=1)
        test_pred_labels = test_pred_probs.argmax(dim=1)

        test_loss = loss_fn(test_logits, y_test)
        test_acc = accuracy_fn(y_true=y_test, y_pred=test_pred_labels)

最后,我们可以定期打印训练和测试的指标,以观察模型的学习进展。


常见错误与调试

在编写和运行上述代码时,初学者常会遇到两类主要错误:数据类型错误张量形状错误。以下是排查这些错误的思路。

1. 数据类型错误
错误信息可能类似 RuntimeError: ... not implemented for 'Float'。这通常意味着传递给损失函数的张量(如标签 y_train)数据类型不正确。对于分类任务,标签应为整数类型(torch.long),而非浮点数(torch.float)。解决方法是确保标签张量的数据类型正确:

y_train = y_train.type(torch.LongTensor).to(device)

2. 张量形状错误
错误信息可能类似 ValueError: ... size A to match target size B。这表示模型的输出张量与目标标签张量的形状不匹配。常见原因是在测试循环中错误地使用了训练数据,或者前向传播的输出维度与损失函数期望的维度不一致。解决方法是仔细检查每一步输入输出张量的 .shape 属性,确保它们对齐。

调试是深度学习开发中不可或缺的一部分。遇到错误时,耐心地检查数据流、数据类型和张量形状是解决问题的关键。


模型评估与可视化

训练完成后,我们需要评估模型的性能。一个直观的方法是使用我们之前定义的 plot_decision_boundary 函数来可视化模型在测试数据上的分类边界。

这个过程与我们在二分类任务中所做的类似,但这次我们使用的是处理多分类问题的模型和数据集。通过可视化,我们可以清晰地看到模型如何划分不同的类别区域。


本节课中我们一起学习了如何为多分类PyTorch模型构建训练和测试循环,掌握了将logits转换为预测结果的完整流程,并实践了调试数据类型与张量形状错误的常用方法。最后,我们了解到可视化是评估模型性能的强大工具。

94:多分类模型预测与评估 📊

在本节课中,我们将学习如何使用 PyTorch 多分类模型进行预测,并评估其性能。我们将涵盖从原始模型输出到最终预测标签的转换过程,并通过可视化决策边界来直观理解模型的表现。此外,我们还将探讨线性与非线性模型在处理不同类型数据时的差异。

回顾与问题解决

上一节我们介绍了多分类模型的训练与测试步骤,并解决了机器学习中常见的两种问题:数据类型问题形状问题。通过实验、查阅文档和反复调试,我们成功克服了这些挑战。现在,我们的多分类模型已经能够学习:损失在下降,准确率在上升。

进行预测

本节中,我们来看看如何用训练好的模型进行预测。以下是进行预测的步骤:

首先,将模型设置为评估模式,并开启推理上下文管理器,因为我们希望进行推断而非训练。

model.eval()
with torch.inference_mode():
    y_logits = model(X_blob_test)

y_logits 是模型的原始输出。查看前10个预测的原始 logits:

y_logits[:10]

这些是未经处理的数值。接下来,我们需要将这些 logits 转换为预测概率。对于多分类模型,我们使用 softmax 函数,并沿第一个维度操作:

y_pred_probs = torch.softmax(y_logits, dim=1)
y_pred_probs[:10]

现在,我们得到了每个样本属于各个类别的概率。概率值最接近 1 的类别,即为模型的预测类别。

获取预测标签

为了将预测概率转换为具体的类别标签,我们取每个样本中概率最大的索引:

y_preds = torch.argmax(y_pred_probs, dim=1)
y_preds[:10]

现在,我们可以将预测标签 y_preds 与真实标签 y_blob_test 进行比较,评估模型的准确性。

可视化决策边界

为了更直观地评估模型,我们可以绘制决策边界。以下是可视化步骤:

plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.title("Train")
plot_decision_boundary(model, X_blob_train, y_blob_train)
plt.subplot(1, 2, 2)
plt.title("Test")
plot_decision_boundary(model, X_blob_test, y_blob_test)
plt.show()

通过可视化,我们可以看到模型是否成功地将不同类别的数据分开。如果数据是线性可分的,即使不使用非线性激活函数,模型也可能表现良好。

线性与非线性模型

我们之前提出的问题是:能否在不使用非线性函数的情况下分离多分类数据?为了验证这一点,我们可以从模型中移除非线性激活函数(如 ReLU),然后重新训练和评估。

实验表明,对于线性可分的数据,即使没有非线性函数,模型也能有效工作,决策边界会呈现为直线。然而,实际应用中许多数据(如之前涉及的圆形数据)需要结合线性和非线性函数才能有效分离。

PyTorch 使得在模型中添加非线性函数变得非常简单。即使数据可能不需要非线性,包含它们也能让模型在必要时利用这些函数,从而构建更强大的模型。

模型评估的重要性

评估模型与训练模型同等重要。在接下来的课程中,我们将介绍更多分类评估指标,以全面衡量模型性能。

本节课中我们一起学习了如何使用 PyTorch 多分类模型进行预测和评估。我们掌握了从原始 logits 到预测概率,再到最终类别标签的转换过程,并通过可视化决策边界直观理解了模型的表现。同时,我们探讨了线性与非线性模型在处理不同数据时的适用性,为后续更复杂的模型评估奠定了基础。

95:其他分类指标探讨 📊

在本节课中,我们将学习除了准确率之外,其他用于评估分类模型性能的重要指标。我们将了解它们的定义、适用场景,并学习如何在PyTorch中实现它们。


概述

上一节我们通过可视化方法评估了我们的多分类模型,并发现它在处理线性可分数据时表现近乎完美。然而,准确率并非评估分类模型的唯一标准,尤其是在处理类别不平衡的数据集时。

本节中,我们将探讨几种关键的分类评估指标,包括精确率、召回率、F1分数和混淆矩阵。我们将了解它们的计算公式、使用场景,并学习如何利用torchmetrics库等工具进行计算。


主要分类评估指标

以下是几种核心的分类模型评估方法。

1. 准确率

准确率是评估分类模型最直观的指标,它表示模型预测正确的样本数占总样本数的比例。其公式为:

准确率 = (TP + TN) / (TP + TN + FP + FN)

其中:

  • TP:真正例
  • TN:真反例
  • FP:假正例
  • FN:假反例

在代码中,我们之前已经实现了一个自定义的准确率计算函数。准确率是分类问题的默认评估指标,但不适用于类别不平衡的数据集。例如,如果一个类别有1000个样本,而另一个类别只有10个样本,仅依赖准确率可能会产生误导。

2. 精确率与召回率

对于不平衡数据集,精确率和召回率是更合适的评估指标。

  • 精确率:衡量模型预测为正例的样本中,有多少是真正的正例。其公式为:

    精确率 = TP / (TP + FP)

    高精确率意味着模型产生的假正例较少。如果你的应用场景中假正例代价高昂(例如垃圾邮件误判为正常邮件),则应追求高精确率。

  • 召回率:衡量所有真正的正例中,有多少被模型正确地预测出来。其公式为:

    召回率 = TP / (TP + FN)

    高召回率意味着模型漏报的正例(假反例)较少。如果你的应用场景中假反例代价高昂(例如疾病检测中漏诊),则应追求高召回率。

需要注意的是,精确率和召回率之间存在权衡关系。通常,提高精确率会降低召回率,反之亦然。你可以通过Will Koehrsen的文章《Beyond Accuracy: Precision and Recall》深入了解这两个指标。

在代码实现上,你可以使用torchmetrics库或scikit-learn库中的相关函数:

# 使用 torchmetrics
from torchmetrics import Precision, Recall
# 使用 scikit-learn
from sklearn.metrics import precision_score, recall_score

3. F1分数

F1分数是精确率和召回率的调和平均数,它试图在两者之间找到一个平衡点。其公式为:

F1分数 = 2 * (精确率 * 召回率) / (精确率 + 召回率)

当你想用一个单一的指标来综合反映模型的精确率和召回率表现时,F1分数是一个很好的选择。

4. 混淆矩阵与分类报告

  • 混淆矩阵:一个N x N的表格(N为类别数),用于详细展示模型预测结果与真实标签的对比情况。它能直观地显示模型在哪些类别上容易混淆。
  • 分类报告scikit-learn中的classification_report函数可以生成一个报告,汇总了精确率、召回率、F1分数和支持度等所有上述指标。

在PyTorch中使用Torchmetrics

torchmetrics是一个专为PyTorch设计的评估指标库,其API设计与PyTorch风格一致。

首先,你需要安装该库(在Google Colab等环境中可能需要):

!pip install torchmetrics

然后,你可以像下面这样使用它来计算准确率:

from torchmetrics import Accuracy

# 初始化指标计算器
metric = Accuracy()

# 确保预测值和目标值在同一设备上(CPU或GPU)
y_preds = y_preds.to(device)
y_test = y_test.to(device)

# 计算准确率
acc = metric(y_preds, y_test)
print(f"Accuracy using torchmetrics: {acc:.1%}")

重要提示:使用torchmetrics时,必须确保输入的张量(预测值和目标值)位于相同的计算设备上,否则会报错。这要求你编写设备无关的代码。

torchmetrics库提供了丰富的评估指标,你可以探索其torchmetrics.classification模块来发现更多用于分类任务的指标。


总结

本节课我们一起学习了多种分类模型的评估指标:

  1. 准确率适用于类别平衡的数据集,计算简单直观。
  2. 精确率关注预测为正例的准确性,适用于减少假正例的场景。
  3. 召回率关注找出所有正例的能力,适用于减少假反例的场景。
  4. F1分数是精确率和召回率的综合指标。
  5. 混淆矩阵分类报告提供了模型性能的详细视图。

记住,没有“最好”的指标,选择取决于你的具体问题和数据特点。机器学习实践者的座右铭是:实验、实验、再实验。鼓励你课后探索torchmetrics库,并尝试在你自己的模型上计算这些指标。

在下一节中,我们将通过一些练习来巩固你所学的知识。

96:PyTorch 分类练习与拓展 📚

在本节课中,我们将回顾分类模型的评估指标,并重点介绍如何通过实践练习来巩固所学知识。我们将提供练习指引和拓展资源,帮助你独立应用和深化对 PyTorch 分类模型的理解。

回顾与练习介绍 🔄

上一节我们介绍了几种额外的分类评估指标,从更高层面概述了评估分类模型的更多方法。同时,我也链接了一些额外的课程资源供你深入探索。

我们已经一起编写了大量的代码。现在,是时候让你自己练习这些内容了。为此,我准备了一些练习。

练习资源位置 📂

那么,练习在哪里呢?请记住,在 Learn PyTorch 这本书中,每一章都有一个专门的练习部分。看看我们已经涵盖了多少内容,我们在这个模块中已经学习了相当多的知识。

在每个章节的底部,都有一个“练习”部分。所有练习都专注于实践前面章节中的代码。本节共有 7 个练习,需要编写大量代码。当然,还有额外的拓展挑战。

这些挑战贯穿了整个第 02 节的内容。我会在这里链接到练习和拓展部分。当然,你也可以直接在 Learn PyTorch 书中找到它们。

以下是练习和拓展资源的指引。

获取练习模板与解决方案 💻

如果你想获得练习代码的模板,可以访问 PyTorch Deep Learning 代码仓库。在 extras 文件夹中,有 exercisessolutions 子文件夹,顾名思义,分别存放练习骨架代码和解决方案。

例如,在练习文件夹中,有 02_pytorch_classification_exercises.ipynb 文件,这提供了一些骨架代码。解决方案文件夹中则是对应的答案。

这仅是一种形式的解决方案。我建议你先尝试自己完成练习,再看答案。练习内容包括设置设备无关代码、创建数据集、将数据转换为 DataFrame 等我们本节学过的内容。

请尝试独立完成。如果遇到困难,可以参考我们一起编写的笔记本文档,当然也可以查阅官方文档。将查阅解决方案笔记本作为最后的手段。

总结与下一步 🎯

恭喜你完成第 02 节“PyTorch 分类”的学习!如果你还在坚持,请继续跟随我进入下一个章节。我们将继续深入探讨使用 PyTorch 进行深度学习的更多内容。

97:计算机视觉问题定义与内容规划 📚

在本节课中,我们将要学习计算机视觉的基本概念,了解其涵盖的广泛问题类型,并规划后续使用 PyTorch 进行计算机视觉学习的路线图。

概述

计算机视觉是深度学习中一个非常受欢迎的主题。在深入学习具体材料之前,我们需要明确一个重要问题:在学习过程中遇到困难时,可以从哪些渠道获取帮助。

如何获取帮助 🆘

以下是当你遇到代码问题时可以遵循的步骤:

  1. 首先,尽最大努力跟随代码实践。 我们将编写大量 PyTorch 计算机视觉代码。请记住我们的座右铭:如有疑问,运行代码。查看代码的输入和输出,并亲自尝试。
  2. 如果需要了解所用函数的功能,可以查阅文档字符串。 在 Google Colab 中,可以按 Shift + Command + Space(Windows 系统可能是 Control 键)来查看。
  3. 如果仍然卡住,可以搜索你正在运行的代码。 你可能会找到 Stack Overflow 或 PyTorch 官方文档。我们已经在 PyTorch 文档上花费了不少时间,并且在接下来的第 3 节模块中会大量引用它。
  4. 如果完成了以上四步,下一步是再试一次。 如有疑问,运行代码。
  5. 当然,如果仍然无法解决,你可以在 PyTorch 深度学习仓库的讨论区提问。 你可以新建一个讨论,标题注明“Section 03”,然后描述你的问题。在正文中,请尽可能格式化你的代码,将其放在反引号中,这样在 GitHub 讨论区显示时会更容易阅读。你可以选择类别,例如“Q&A”,然后点击“开始讨论”。这样你的问题就会被发布,并且可以被搜索到,我们也能帮助你。

学习资源 📖

除了讨论区,我们还有以下重要资源:

  • PyTorch 深度学习代码仓库: 所有需要的链接都会在相应位置提供。
  • 第 3 节笔记本: 本节我们将要编写的所有代码都包含在名为“PyTorch Computer Vision”的笔记本中。这个笔记本注释详尽,包含大量文本和图片。如果你在视频学习中对任何代码感到困惑,可以随时查阅这个笔记本作为参考。
  • 课程书籍版本: 网站 learnpytorch.io 上有本课程的书籍版本。其中“Section 03: PyTorch Computer Vision”以书籍格式涵盖了我们将要学习的所有内容,包括所有链接和额外资源。

什么是计算机视觉问题?👁️

现在,让我们来探讨计算机视觉的核心问题。计算机视觉的范围非常广泛,一个简单的定义是:凡是你能看见的东西,几乎都可以被表述为某种计算机视觉问题。

以下是几个具体的例子:

  • 二元分类问题: 例如,判断一张图片是牛排还是披萨。我们的机器学习模型会接收图像的像素,并自行学习构成牛排和披萨的不同模式。我们不会明确告诉模型要学习什么,它会从不同的图像示例中自行学习这些模式。
  • 多元分类问题: 例如,判断一张图片是寿司、牛排还是披萨。这涉及到三个类别,但也可以扩展到 100 个类别,例如 nutrify.app 这个应用就能对多达 100 种不同的食物进行分类。其原理是首先判断图像是否为食物,如果是,再进一步分类是哪种食物。
  • 目标检测问题: 回答“我们要找的东西在哪里?”例如,在监控录像中寻找特定类型的车辆。这涉及到在图像中定位特定物体。
  • 图像分割问题: 识别并分割图像中的不同部分。例如,苹果公司在 iPhone 和 iPad 上使用的技术,可以将图像中的人物、皮肤、头发、天空等部分分割开来,然后对每个部分进行不同的增强处理,这被称为计算摄影学。

计算机视觉的行业应用 🚗

许多顶尖公司都在积极应用并分享他们的计算机视觉技术:

  • 特斯拉自动驾驶: 特斯拉的每辆自动驾驶汽车都配备了 8 个摄像头,为其全自动驾驶(FSD)软件提供数据。它们使用计算机视觉来理解图像内容,并将车辆周围的环境表示转换为三维向量空间——即一长串数字。这是因为计算机理解数字远比理解图像更容易。有趣的是,特斯拉也使用 PyTorch 来训练其驱动自动驾驶软件的机器学习模型。
  • 苹果机器学习研究: 苹果公司拥有“Apple Machine Learning Research”博客,分享其在机器学习领域的众多成果,包括先进的图像分割技术。

如果你想探索计算机视觉的更多应用,可以搜索“computer vision applications”,你会发现大量资源,例如“2022 年 27 个最流行的计算机视觉应用”。

本课程内容规划 🗺️

上一节我们介绍了计算机视觉的广阔天地和获取帮助的途径。本节中,我们来看看在 PyTorch 部分,我们将具体学习哪些内容。

我们将使用 PyTorch 代码,以“部分艺术、部分科学”的方式,像烹饪一样编写大量代码。以下是我们的学习路线图:

  1. 获取视觉数据集: 使用 torchvision 库来处理计算机视觉问题,并利用其中现有的数据集进行实验。
  2. 理解卷积神经网络架构: 学习 CNN 的基本原理及其在 PyTorch 中的实现。
  3. 解决端到端的多元图像分类问题: 处理涉及多个类别(可能是 3 个,也可能是 100 个)的图像分类任务。
  4. 使用 CNN 进行建模: 在 PyTorch 中创建卷积神经网络。
  5. 选择损失函数和优化器: 为我们的问题选择合适的损失函数和优化算法。
  6. 训练模型: 实际训练我们构建的模型。
  7. 评估模型: 评估训练好的模型的性能。

总结

本节课中,我们一起学习了计算机视觉的广泛定义和多种问题类型,从简单的图像分类到复杂的自动驾驶感知。我们明确了遇到困难时的求助路径,并规划了接下来使用 PyTorch 学习计算机视觉和卷积神经网络的具体步骤。在下一节视频中,我们将深入探讨计算机视觉问题的输入和输出。

98:计算机视觉的输入与输出形状 🖼️

在本节课中,我们将学习计算机视觉问题的典型输入与输出形状。我们将探讨如何将图像表示为数字,以及机器学习模型如何处理这些数据以进行预测。

上一节我们介绍了计算机视觉的广泛概念。本节中,我们来看看一个典型计算机视觉问题的输入和输出具体是什么。

输入:将图像表示为数字

计算机视觉的核心是将图像转换为机器可以理解的数字形式。大多数数字图像使用 RGB(红、绿、蓝)色彩模式。

  • 每个像素由三个数值表示:红色值、绿色值和蓝色值。
  • 这些数值共同决定了像素的颜色。例如,一个偏橙色的像素可能红色值较高,绿色和蓝色值较低。

因此,一张图像在计算机中被表示为一个张量,其形状通常包含三个维度:高度宽度颜色通道数

对于一个常见的图像尺寸,其输入形状可以表示为:
[batch_size, height, width, color_channels]

[batch_size, color_channels, height, width]

例如,一个批次大小为 32、尺寸为 224x224 的彩色图像,其形状可能是 [32, 224, 224, 3][32, 3, 224, 224]。其中 3 代表 RGB 三个颜色通道。

请注意:PyTorch 默认使用“通道优先”格式,即 [batch_size, color_channels, height, width]。而许多其他库使用“通道最后”格式。确保张量形状顺序正确至关重要,否则会导致错误。

输出:模型的预测

模型的输出形状取决于你要解决的具体问题。以食物图像分类为例:

  • 目标:让模型识别图像中是寿司、牛排还是披萨。
  • 输出:模型为每个类别输出一个预测概率。因此,输出形状是 [batch_size, num_classes]。对于我们的三分类问题,就是 [batch_size, 3]

模型会输出类似这样的预测概率:[0.01, 0.81, 0.18],分别对应寿司、牛排、披萨的概率。最高概率的类别即为模型的预测结果。

以下是核心流程的总结:

  1. 数值化编码:将图像转换为形状为 [batch_size, channels, height, width] 的张量。
  2. 模型处理:将张量输入机器学习模型(如卷积神经网络 CNN)。
  3. 产生输出:模型输出形状为 [batch_size, num_classes] 的预测。

工作流程回顾

我们将遵循以下 PyTorch 工作流程来实现计算机视觉任务:

以下是实现上述过程的关键步骤:

  1. 准备数据:使用 torchvision.transformstorch.utils.data.DataLoader 将图像转换为张量。
  2. 构建模型:使用 torch.nn 模块构建或选择预训练模型(如来自 torchvision.models)。
  3. 训练模型:定义损失函数和优化器,并训练模型。
  4. 评估模型:使用 torchmetrics 或自定义函数评估模型性能。
  5. 实验改进:通过调整模型、数据或超参数来提升效果。
  6. 保存模型:将训练好的模型保存下来以供后续使用。

另一个例子:灰度图像分类

这个模式同样适用于其他问题。例如,对 28x28 的灰度时尚单品图像进行分类:

  • 输入形状:由于是灰度图像,颜色通道数为 1。例如 [batch_size, 1, 28, 28]
  • 输出形状:如果有 10 种服装类别,则输出形状为 [batch_size, 10]

无论问题如何变化,其核心模式不变:将数据数值化,输入模型,并确保输出形状符合你的需求。

本节课中我们一起学习了计算机视觉中输入和输出张量的典型形状。我们了解了图像如何被表示为数值张量,以及模型的输出如何对应具体的预测任务。记住确保输入输出形状匹配是构建有效模型的关键。下一节,我们将深入探讨完成这些任务的强大工具——卷积神经网络。

99:卷积神经网络概念解析 🧠

在本节课中,我们将学习卷积神经网络(CNN)的基本概念。我们将探讨CNN的典型架构、各层的作用,以及如何通过代码来理解和构建这些网络。


欢迎回来。在上一节视频中,我们看到了计算机视觉输入和输出形状的示例,并暗示了卷积神经网络(CNN)是擅长识别图像模式的深度学习模型。上一节视频结束时,我们提出了一个问题:什么是卷积神经网络?以及在哪里可以了解它?

以下是了解卷积神经网络的一种方式。网络上有很多相关资源,例如“卷积神经网络全面指南”。哪个资源最好?其实并不重要,最适合你的就是最好的。例如,Code Basics有一个很好的视频,我也看过。还有“卷积神经网络的简单解释”。你可以自行研究这些资源。如果你想通过图像学习,也有很多图像资料可供参考。

我更喜欢通过编写代码来学习,因为本课程以代码为先。作为机器学习工程师,我99%的时间都在编写代码,因此我们将重点放在代码上。

无论如何,这是CNN的典型架构。换句话说,卷积神经网络。如果在本课程中听到CNN,我指的不是新闻网站,而是卷积神经网络架构。以下是我们将要构建的PyTorch代码示例。


CNN典型架构 🏗️

我们有一些超参数和层类型。首先是一个输入层,它接收输入通道和输入形状。在机器学习和深度学习中,对齐模型的输入和输出形状非常重要,无论你使用什么模型或解决什么问题。

接下来是卷积层。卷积层在图像或张量上执行卷积窗口操作,以发现模式。让我们看看具体操作。实际上,我们可以查看torch.nn.Conv2d的文档。

卷积层的输出公式为:

输出 = 偏置 + 权重张量在卷积通道上的和 × 输入

如果你想深入了解这里的具体操作,可以自行研究。我们将编写代码来使用torch.nn.Conv2d,并调整所有参数以适应我们的问题。

这里需要注意的是,偏置项和权重矩阵是卷积层的关键组成部分。偏置通常是一个向量,权重是一个矩阵,它们对输入数据进行操作。我们稍后会在代码中看到这些内容。请记住,神经网络中的每一层都对输入数据执行某种操作,这些操作逐层进行,最终希望将数据转换为有用的输出。


隐藏层与非线性激活 ⚡

接下来是隐藏层或非线性激活层。为什么使用非线性激活?因为如果数据是非线性的(不是直线),我们需要直线和非直线的帮助来建模和绘制模式。

然后通常有一个池化层。请注意,我在这里使用“典型”一词是有原因的,因为这些架构一直在变化。这只是CNN的一个典型示例,是最基础的CNN架构。随着时间的推移,你将学会构建更复杂的模型,甚至直接使用它们,正如我们将在课程的迁移学习部分看到的那样。


输出层 📤

最后是输出层。你注意到这里的趋势了吗?我们有一个输入层,多个隐藏层对数据执行数学操作,然后是一个输出层或线性层,将输出转换为理想的形状。因此,我们在这里有一个输出形状。

在过程中,我们输入一些图像,数据经过所有这些层(因为我们使用了nn.Sequential),希望forward方法返回的X处于可用状态,我们可以将其转换为类别名称,并以某种方式集成到计算机视觉应用中。


注意事项 ⚠️

请注意,构建卷积神经网络的方式几乎无限多,本幻灯片仅展示其中一种。请记住这一点。但练习这类内容的最佳方式不是盯着页面看,而是编写代码。如果有疑问,就编写代码来验证。让我们开始编写代码吧,我们将在Google Colab中见。


总结 📝

在本节课中,我们一起学习了卷积神经网络的基本概念。我们探讨了CNN的典型架构,包括输入层、卷积层、非线性激活层、池化层和输出层。我们还强调了通过编写代码来理解和构建CNN的重要性。在下一节中,我们将通过实际代码来进一步巩固这些概念。

100:PyTorch 计算机视觉基础库导入与讨论 🖥️🔍

在本节课中,我们将学习PyTorch中用于计算机视觉任务的核心库,并了解如何导入它们以开始构建视觉模型。我们将从理论讨论转向实际编码。

概述

上一节我们讨论了计算机视觉问题和卷积神经网络的基础知识。本节中,我们将开始编写代码,并熟悉PyTorch生态系统中处理计算机视觉问题的关键库。

计算机视觉库介绍

以下是PyTorch中处理计算机视觉问题的主要库,了解它们对后续学习至关重要。

  • torchvision:这是PyTorch计算机视觉的基础领域库。
  • torchvision.datasets:包含获取数据集和数据加载的函数。
  • torchvision.models:提供预训练的计算机视觉模型。预训练模型是指在现有视觉数据上已经训练好、拥有可学习权重(训练好的模式)的模型,你可以将其用于自己的问题。
  • torchvision.transforms:提供用于处理视觉数据(即图像)的函数,使其适合机器学习模型使用。transforms帮助我们将图像数据转换为数字,以便模型能够处理。
  • torch.utils.data.Dataset:这不是视觉专用的,而是整个PyTorch的基础类。如果你想使用自己的自定义数据创建数据集,就需要用到这个基类。
  • torch.utils.data.DataLoader:这同样需要了解,因为你几乎在任何PyTorch问题中都会用到某种形式的数据集或数据加载器。它会围绕数据集创建一个Python可迭代对象。

导入核心库

现在,让我们导入一些核心库,以便开始使用它们。

import torch
import torch.nn as nn
import torchvision
from torchvision import datasets
from torchvision import transforms
import matplotlib.pyplot as plt

检查版本

导入库后,最好检查一下当前使用的版本,以确保与教程代码兼容。

print(torch.__version__)
print(torchvision.__version__)

运行上述代码后,你可能会看到类似 torch 1.10torchvision 0.11 的输出。请确保你使用的版本至少不低于这些版本,它们是完成本部分内容所需的最低版本。

关于 transforms 的说明

transforms 模块对于准备图像数据至关重要。例如,transforms.ToTensor() 是一个常用的转换函数,它的作用是将PIL图像或NumPy数组转换为张量(Tensor)。这正是我们后续需要做的:将图像转换为张量,以便模型能够使用。

总结

本节课我们一起学习了PyTorch中用于计算机视觉的核心库。我们介绍了 torchvision 及其子模块(datasetsmodelstransforms)的作用,并完成了这些库的导入和版本检查。现在,我们已经为处理计算机视觉数据做好了基础准备。

在下一节视频中,我们将开始学习如何获取一个数据集。

101:获取并检查计算机视觉数据集 🖼️

在本节课中,我们将学习如何使用 PyTorch 的 torchvision 库获取一个计算机视觉数据集,并检查其输入和输出数据的形状。这是构建任何机器学习模型的第一步。


概述

上一节我们介绍了 PyTorch 中用于计算机视觉的核心库 torchvision,以及用于处理数据的基础类 torch.utils.data.Datasettorch.utils.data.DataLoader。本节中,我们将实际操作,下载一个名为 Fashion MNIST 的经典数据集,并检查其基本属性。

获取数据集

大多数机器学习项目都从获取数据集开始。我们将使用 torchvision.datasets 模块来下载 Fashion MNIST 数据集。这个数据集是原始 MNIST 数据集的一个变体,包含了10类服装的灰度图像,比手写数字识别更具挑战性。

以下是获取数据集的步骤:

  1. 导入必要的库:我们需要 torchvision 来访问数据集和转换功能。
  2. 设置训练数据:使用 torchvision.datasets.FashionMNIST 类下载训练集。
  3. 设置测试数据:使用同一个类下载测试集,但指定参数 train=False
  4. 应用数据转换:使用 torchvision.transforms.ToTensor() 将图像数据转换为 PyTorch 张量。这是必需的,因为神经网络模型需要张量格式的输入。

以下是实现这些步骤的代码:

import torch
from torchvision import datasets
from torchvision.transforms import ToTensor

# 设置训练数据
train_data = datasets.FashionMNIST(
    root="data", # 数据下载路径
    train=True, # 下载训练集
    download=True, # 如果本地没有则下载
    transform=ToTensor(), # 将图像转换为张量
    target_transform=None # 不对标签进行转换
)

# 设置测试数据
test_data = datasets.FashionMNIST(
    root="data",
    train=False, # 下载测试集
    download=True,
    transform=ToTensor(),
    target_transform=None
)

运行这段代码后,数据集会自动下载到当前目录下的 data/ 文件夹中。

检查数据集属性

成功下载数据集后,我们需要了解它的基本情况。这包括数据集的样本数量、单个样本的形状以及标签的含义。

以下是检查数据集的关键步骤:

  1. 检查数据量:查看训练集和测试集分别有多少个样本。
  2. 查看第一个样本:通过索引获取第一个训练样本,它包含图像张量和对应的标签。
  3. 检查图像形状:理解图像张量的维度含义(通道、高度、宽度)。
  4. 查看类别名称:了解每个数字标签对应的具体服装类别。

让我们通过代码来探索这些属性:

# 1. 检查数据量
print(f"训练样本数: {len(train_data)}")
print(f"测试样本数: {len(test_data)}")

# 2. 查看第一个训练样本
image, label = train_data[0]
print(f"第一个样本的图像张量:\n {image}")
print(f"第一个样本的标签: {label}")

# 3. 检查图像形状
# 使用 ToTensor() 转换后,图像形状为 [颜色通道, 高度, 宽度]
print(f"图像形状: {image.shape} -> [颜色通道数, 图像高度, 图像宽度]")

# 4. 查看类别名称
class_names = train_data.classes
print(f"数据集类别: {class_names}")
print(f"标签 {label} 对应的类别是: {class_names[label]}")

运行上述代码,你可能会看到类似以下的输出:

训练样本数: 60000
测试样本数: 10000
图像形状: torch.Size([1, 28, 28]) -> [颜色通道数, 图像高度, 图像宽度]
标签 9 对应的类别是: Ankle boot

核心概念解释

  • 图像形状 [1, 28, 28]:这表示图像是 28 像素高、28 像素宽的灰度图。第一个维度 1 代表颜色通道数。对于灰度图像,通道数为1;对于彩色(RGB)图像,通道数为3。
  • 标签 9:这是一个整数,代表目标类别。通过 train_data.classes 列表,我们可以知道 9 对应的是“短靴”(Ankle boot)。

总结

本节课中我们一起学习了如何获取和初步探索一个计算机视觉数据集。我们使用 torchvision.datasets 轻松下载了 Fashion MNIST 数据集,并使用 ToTensor() 转换将图像数据预处理为模型可用的张量格式。最关键的是,我们检查了数据的形状,了解到输入图像是形状为 [1, 28, 28] 的张量,输出标签是0到9之间的整数。理解输入和输出的形状是构建有效模型的基础。

在下一节,我们将把这些数字可视化,真正“看到”我们正在处理的数据图像。

102:随机样本数据可视化 📊

在本节课中,我们将学习如何可视化FashionMNIST数据集中的图像样本。我们将检查数据的输入输出形状,并使用Matplotlib库来绘制和查看随机的图像样本,以更好地理解我们正在处理的数据。


数据形状回顾 🔍

上一节我们介绍了FashionMNIST数据集,并检查了其输入输出形状。本节中我们来看看如何将这些数值表示转换为可视化的图像。

FashionMNIST数据集包含10类服装的灰度图像,每张图像的尺寸是28x28像素。在PyTorch中,张量的默认格式是颜色通道优先(NCHW),这意味着一个批次的图像张量形状为 [batch_size, 1, 28, 28]

公式:输入张量形状 = [N, C, H, W]

  • N:批次大小
  • C:颜色通道数(灰度图为1)
  • H:图像高度(28)
  • W:图像宽度(28)

可视化单个图像 🖼️

理解数据的第一步是查看它。我们将使用Matplotlib来绘制图像。但需要注意的是,Matplotlib期望的图像数据格式通常是高度和宽度(HW),或者颜色通道在最后(HWC),而我们的数据是颜色通道优先(CHW)。

以下是绘制训练集中第一张图像的步骤:

import matplotlib.pyplot as plt

# 获取第一个样本(图像和标签)
image, label = train_data[0]

# 打印图像形状以了解数据结构
print(f"Image shape: {image.shape}") # 输出: torch.Size([1, 28, 28])

# 绘制图像
plt.imshow(image.squeeze(), cmap="gray")
plt.title(class_names[label])
plt.axis(False)
plt.show()

代码解释

  • image.squeeze():移除张量中大小为1的维度(即颜色通道维度),将形状从 [1, 28, 28] 变为 [28, 28],以符合Matplotlib的输入要求。
  • cmap="gray":将颜色映射设置为灰度,以正确显示灰度图像。
  • plt.axis(False):关闭坐标轴,让图像显示更清晰。

运行上述代码后,我们将看到一张像素化的“踝靴”图像及其对应的标签(数字9)。


可视化随机图像样本 🎲

只看一张图像不足以了解数据全貌。更好的做法是随机查看一批图像样本。这有助于我们发现数据中的模式、潜在问题(如类别间相似度高)或异常值。

以下是创建4x4网格,展示16张随机训练图像的步骤:

# 设置随机种子以确保结果可复现
torch.manual_seed(42)

# 设置子图网格的行数和列数
rows = 4
cols = 4

# 创建画布
fig = plt.figure(figsize=(9, 9))

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/daniel-bourke/img/253d733f7e7247d864f4b308d98d6e20_7.png)

# 循环创建16个子图
for i in range(1, rows * cols + 1):
    # 生成一个随机索引
    random_idx = torch.randint(0, len(train_data), size=[1]).item()

    # 根据随机索引获取图像和标签
    img, label = train_data[random_idx]

    # 添加子图
    fig.add_subplot(rows, cols, i)

    # 在子图中绘制图像
    plt.imshow(img.squeeze(), cmap="gray")
    plt.title(class_names[label])
    plt.axis(False)

plt.show()

代码解释

  • torch.manual_seed(42):固定随机数生成器的种子,确保每次运行代码时生成的随机索引序列相同,便于结果复现和分享。
  • torch.randint(0, len(train_data), size=[1]):生成一个在 [0, 训练集长度) 范围内的随机整数索引。
  • fig.add_subplot(rows, cols, i):在 rows x cols 的网格中,在第 i 个位置创建一个子图。

通过这个可视化,我们可以快速浏览数据集中不同类型的服装,例如T恤、裤子、连衣裙、外套等。你可能会注意到一些类别(如“衬衫”和“套头衫”)在低分辨率下看起来非常相似,这可能是我们后续模型训练中需要关注的一个挑战点。


总结 📝

本节课中我们一起学习了如何可视化FashionMNIST数据集。

  1. 回顾了数据形状:理解了PyTorch中图像张量的NCHW格式与Matplotlib库期望的格式差异。
  2. 绘制了单个图像:学会了使用 .squeeze() 方法调整张量形状,并使用Matplotlib的 imshow 函数配合灰度色彩映射来显示图像。
  3. 批量查看随机样本:通过设置随机种子和循环,创建了一个图像网格来同时查看多个随机数据样本,这是探索性数据分析(EDA)中理解数据分布和潜在问题的关键一步。

通过可视化,我们与数据建立了更直观的联系,并发现了可能影响模型性能的细节(如类别相似性)。在下一节中,我们将开始准备数据,以便将其加载到神经网络模型中进行训练。

103:DataLoader 概览:理解小批次数据 📊

在本节课中,我们将要学习如何将 PyTorch 数据集转换为 DataLoader,并深入理解使用小批次数据进行训练的核心概念和优势。

上一节我们介绍了如何加载和可视化 FashionMNIST 数据集。本节中我们来看看如何为模型训练准备数据,特别是通过 DataLoader 将数据组织成小批次。

数据建模的思考

我们拥有 60000 张服装图像,目标是构建一个计算机视觉模型将其分类为 10 个不同的类别。

在可视化了一些样本之后,我们需要思考一个问题:这些图像是否可以用纯粹的线性线(直线)来建模?还是说我们需要一个具有非线性的模型?

这个问题可以留待后续测试。现在,让我们开始进一步准备数据,即创建 DataLoader。

从数据集到 DataLoader

目前,我们的数据是 PyTorch 数据集的形式。

train_data
test_data

它们都是 FashionMNIST 数据集,并已应用了相同的转换(转换为张量)。我们的目标是将数据集转换为 DataLoader。

DataLoader 将我们的数据集转换为一个 Python 可迭代对象。更具体地说,它可以将数据分成批次或小批次。

为什么要使用小批次?

以下是使用小批次的两个主要原因:

  1. 计算效率更高:你的计算硬件(如 RAM、GPU 内存)可能无法一次性在内存中存储和处理全部 60000 张图像。通过将其分解为每次处理 32 张图像(批次大小为 32),可以显著降低内存需求。32 是一个常见的起始批次大小,你可以根据问题调整这个数字。

  2. 为神经网络提供更多梯度更新机会:在每个训练周期(epoch)中,如果一次性查看所有 60000 张图像,神经网络在整个数据集上每个周期只能更新一次权重。而如果我们每次查看 32 张图像,神经网络每处理 32 张图像(得益于优化器)就可以更新一次其内部状态(权重)。当我们编写训练循环时,这一点会变得更加清晰。

如果你想深入了解其背后的理论,强烈建议查阅 Andrew Ng 关于“小批次梯度下降”的讲座。

DataLoader 的可视化目标

我们的目标是创建批次大小为 32 的 DataLoader,应用于全部 60000 张训练图像和 10000 张测试图像。

以下是我们要实现的代码结构概览:

from torch.utils.data import DataLoader

train_dataloader = DataLoader(dataset=train_data,
                              batch_size=32,
                              shuffle=True)

test_dataloader = DataLoader(dataset=test_data,
                             batch_size=32,
                             shuffle=False) # 测试集通常不 shuffle

对于训练数据,我们设置 shuffle=True。这是因为如果数据集本身存在顺序(例如所有裤子图像排在一起),我们不希望神经网络记住数据的顺序,而只希望它学习不同类别之间的模式。打乱数据可以混合样本。

设置 batch_size=32 意味着每个批次包含 32 个样本。我们将拥有 总样本数 / 批次大小 个批次,例如训练集大约有 60000 / 32 ≈ 1875 个批次。

总结

本节课中我们一起学习了 DataLoader 的核心概念。我们了解到,将数据集转换为 DataLoader 并组织成小批次,主要出于计算效率和增加模型权重更新频率两方面的考虑。在下一节,我们将动手编写代码,为 FashionMNIST 数据集创建 DataLoader。

104:将数据集转换为DataLoader 📚

在本节课中,我们将学习如何将PyTorch数据集转换为DataLoader。DataLoader是PyTorch中一个非常重要的工具,它允许我们以小批量(mini-batch) 的形式高效地加载和处理数据。这对于训练深度学习模型至关重要,因为它能提高计算效率,并为模型提供更多更新梯度的机会。


概述

上一节我们介绍了小批量的概念。本节中,我们来看看如何通过代码实现这一过程。我们将使用PyTorch的DataLoader类,将FashionMNIST数据集转换为可迭代的数据加载器,以便在后续的模型训练中使用。


为什么要使用DataLoader?

使用DataLoader主要有两个原因:

  1. 计算效率更高:GPU的内存有限,无法一次性加载整个数据集(例如60000张图像)。通过小批量加载,我们可以更有效地利用硬件资源。
  2. 提供更多梯度更新机会:在每个训练周期(epoch)中,模型可以基于多个小批量数据多次更新其权重,这通常能带来更好的训练效果。

代码实现

以下是创建DataLoader的具体步骤。

1. 导入必要的库

首先,我们需要从torch.utils.data中导入DataLoader

from torch.utils.data import DataLoader

2. 设置超参数

接下来,我们定义批量大小(batch size) 这个超参数。批量大小是你可以自行设定的值,32是一个常用的起始值。

BATCH_SIZE = 32

3. 创建训练和测试DataLoader

我们将为训练集和测试集分别创建DataLoader。

# 创建训练DataLoader
train_dataloader = DataLoader(dataset=train_data, # 训练数据集
                              batch_size=BATCH_SIZE, # 批量大小
                              shuffle=True) # 打乱数据顺序

# 创建测试DataLoader
test_dataloader = DataLoader(dataset=test_data, # 测试数据集
                             batch_size=BATCH_SIZE,
                             shuffle=False) # 测试集通常不打乱

参数说明

  • dataset:要加载的数据集。
  • batch_size:每个批次包含的样本数。
  • shuffle:是否在每个训练周期开始时打乱数据顺序。训练集需要打乱以防止模型学习到数据顺序;测试集通常不打乱,以便于评估和结果复现。

检查DataLoader

创建好DataLoader后,让我们检查一下它的属性。

查看DataLoader长度

DataLoader的长度代表数据被分成了多少个批次。

print(f"训练DataLoader长度(批次数量): {len(train_dataloader)}")
print(f"测试DataLoader长度(批次数量): {len(test_dataloader)}")

输出结果类似于:

训练DataLoader长度(批次数量): 1875
测试DataLoader长度(批次数量): 313

这意味着训练数据被分成了1875个批次,每个批次包含32个样本(60000 / 32 ≈ 1875)。

查看一个批次的数据

我们可以从DataLoader中获取一个批次,并查看其形状。

# 将DataLoader转换为迭代器并获取第一个批次
train_features_batch, train_labels_batch = next(iter(train_dataloader))

# 查看特征(图像)和标签的形状
print(f"特征批次形状: {train_features_batch.shape} -> [batch_size, color_channels, height, width]")
print(f"标签批次形状: {train_labels_batch.shape}")

输出结果类似于:

特征批次形状: torch.Size([32, 1, 28, 28]) -> [batch_size, color_channels, height, width]
标签批次形状: torch.Size([32])

这确认了我们有一个包含32张图像的批次,每张图像是1个颜色通道、28像素高、28像素宽。同时有32个对应的标签。


可视化批次中的样本

为了更直观地理解数据,我们可以从批次中随机选取并显示一张图像及其标签。

以下是可视化单个样本的步骤:

  1. 设置随机种子以确保结果可复现。
  2. 从批次中随机选择一个索引。
  3. 获取该索引对应的图像和标签。
  4. 使用Matplotlib显示图像。
import torch
import matplotlib.pyplot as plt

# 设置随机种子
torch.manual_seed(42)

# 随机选择一个索引
random_idx = torch.randint(0, len(train_features_batch), size=[1]).item()

# 获取图像和标签
img, label = train_features_batch[random_idx], train_labels_batch[random_idx]

# 显示图像
plt.imshow(img.squeeze(), cmap="gray")
plt.title(f"Label: {label} | Class: {class_names[label]}")
plt.axis(False)
plt.show()

# 打印图像和标签信息
print(f"图像大小: {img.shape}")
print(f"标签: {label}")

运行这段代码,你将看到从当前批次中随机选取的一张FashionMNIST图像。


总结

本节课中我们一起学习了如何将PyTorch数据集转换为DataLoader。我们了解了使用DataLoader的重要性,掌握了创建和配置DataLoader的方法,并学会了如何检查和可视化DataLoader中的数据批次。

现在,我们的数据已经准备好以小批量的形式输入到神经网络模型中。在下一节课中,我们将开始构建我们的第一个模型——一个基线模型(Model 0),并着手进行训练。

105:构建基线模型 🧠

在本节课中,我们将学习如何构建一个用于图像分类的基线神经网络模型。我们将从准备数据开始,逐步创建一个简单的模型,并理解其核心组件。

概述

上一节我们将FashionMNIST数据集加载并转换成了DataLoader,以便进行批量处理。本节中,我们将开始构建我们的第一个模型——一个简单的基线模型。基线模型是一个起点,后续我们将通过实验对其进行改进。

构建基线模型的重要性

在开始一系列机器学习实验时,最佳实践是从一个基线模型开始。基线模型是一个简单的模型,你将在后续的模型或实验中尝试改进它。换句话说,从简单开始,必要时再增加复杂性。神经网络非常强大,容易在数据集上“表现过好”,即过拟合。因此,我们先构建一个简单的基线模型,然后整个目标就是按照工作流程进行实验,通过实验来改进。

引入Flatten层

在构建模型之前,我们先介绍一个新的层:Flatten层。Flatten层的作用是将输入张量的连续维度展平。为了更好地理解,我们通过代码来演示。

首先,我们创建一个Flatten层:

flatten = nn.Flatten()

然后,我们取一个训练样本进行测试:

x = train_features_batch[0]
print(f"Shape before flattening: {x.shape}")

样本的初始形状是 [1, 28, 28],代表1个颜色通道,高度28像素,宽度28像素。接下来,我们将这个样本通过Flatten层:

output = flatten(x)
print(f"Shape after flattening: {output.shape}")
print(output)

经过Flatten层后,形状变成了 [1, 784]。这是因为 28 * 28 = 784,每个像素的值被展平成了一个长向量。这个过程类似于将多维数据压缩成一个单一的向量空间,这是许多深度学习模型处理图像数据的第一步。

创建基线模型类

现在,我们来构建我们的基线模型。这个模型将继承自 nn.Module,并包含一个Flatten层和两个线性层。

以下是模型的定义:

import torch
from torch import nn

class FashionMNISTModelV0(nn.Module):
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
        super().__init__()
        self.layer_stack = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=input_shape, out_features=hidden_units),
            nn.Linear(in_features=hidden_units, out_features=output_shape)
        )
    
    def forward(self, x):
        return self.layer_stack(x)

在这个模型中:

  1. nn.Flatten() 层将输入的图像数据展平。
  2. 第一个 nn.Linear 层将展平后的向量映射到隐藏层。
  3. 第二个 nn.Linear 层将隐藏层的输出映射到最终的类别数(10类)。

实例化模型

接下来,我们实例化这个模型。我们需要指定输入形状、隐藏单元数和输出形状。

torch.manual_seed(42)

model_0 = FashionMNISTModelV0(
    input_shape=784,  # 28*28
    hidden_units=10,
    output_shape=len(class_names)  # 10
).to("cpu")

我们使用一个随机生成的张量进行前向传播测试,以确保模型能正常工作:

dummy_x = torch.rand([1, 1, 28, 28])
model_0(dummy_x)

输出应该是一个形状为 [1, 10] 的张量,代表10个类别的原始分数(logits)。

理解模型结构

我们的基线模型结构简单,没有使用非线性激活函数。这意味着它只能学习数据中的线性关系。对于像FashionMNIST这样的复杂图像数据,这可能不够。我们将在后续课程中通过添加非线性激活函数和更多层来改进模型。

总结

本节课中,我们一起学习了如何构建一个用于图像分类的基线神经网络模型。我们从介绍Flatten层开始,理解了其将多维图像数据展平为向量的作用。然后,我们创建了一个简单的模型类,包含Flatten层和两个线性层。最后,我们实例化了模型并进行了前向传播测试,确保其结构正确。

在下一节课中,我们将开始训练这个模型,并学习如何评估其性能。

106:为模型0创建损失函数与优化器 🧠

在本节课中,我们将学习如何为上一节创建的基准模型(Model 0)设置损失函数和优化器。这是训练神经网络的关键步骤,它们将指导模型如何从数据中学习。

上一节我们介绍了如何构建一个用于处理28x28灰度服装图像分类的基准模型,并验证了其输入输出形状的正确性。本节中我们来看看如何为这个模型配备“学习指南”——损失函数和优化器。

设置损失函数、优化器与评估指标

我们的模型目前包含随机初始化的权重和偏置参数。深度学习的核心就是通过优化器,根据损失函数的反馈,不断更新这些随机值,使其能更好地表示数据中的特征。这里的“特征”可以是图像中的任何模式,例如手提袋的圆形手柄或边缘轮廓,模型将自动学习这些重要特征。

以下是设置步骤:

  1. 损失函数:由于我们处理的是多类别分类问题(10种服装),因此选择 nn.CrossEntropyLoss
  2. 优化器:我们选择随机梯度下降(SGD)作为入门优化器,即 torch.optim.SGD
  3. 评估指标:对于分类问题,准确率(Accuracy)是一个直观的评估指标。

导入辅助函数

在大型项目中,将常用功能(如计算准确率的函数)编写在独立的Python脚本中是常见做法。这避免了代码重复。我们将从课程GitHub仓库导入一个包含 accuracy_fn 的辅助函数文件。

# 下载 helper_functions.py 文件(如果尚未存在)
import requests
from pathlib import Path

if Path("helper_functions.py").is_file():
    print("helper_functions.py 已存在,跳过下载。")
else:
    print("正在下载 helper_functions.py...")
    request = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/helper_functions.py")
    with open("helper_functions.py", "wb") as f:
        f.write(request.content)

# 从 helper_functions.py 中导入准确率计算函数
from helper_functions import accuracy_fn

代码实现

现在,让我们用代码具体实现损失函数和优化器的设置。

# 导入必要的库
import torch
from torch import nn

# 设置损失函数
loss_fn = nn.CrossEntropyLoss()

# 设置优化器(随机梯度下降)
# 参数:要优化的模型参数,学习率
optimizer = torch.optim.SGD(params=model_0.parameters(),
                            lr=0.1)

代码解释

  • loss_fn:交叉熵损失函数,用于衡量模型预测与真实标签之间的差异。
  • optimizer:SGD优化器。params=model_0.parameters() 告诉优化器需要更新的是模型0的所有参数(权重和偏置)。lr=0.1 是学习率,它控制着每次参数更新的步长。我们为这个相对简单的数据集设置了一个较高的初始值。

总结

本节课中我们一起学习了为PyTorch模型配置训练核心组件的流程:

  1. 我们为多分类任务选择了 nn.CrossEntropyLoss 作为损失函数。
  2. 我们选择了 torch.optim.SGD 作为优化器,并为其指定了要优化的模型参数以及学习率。
  3. 我们介绍了通过导入外部Python脚本(helper_functions.py)来复用代码(如 accuracy_fn)的实践方法,这是管理大型项目的良好习惯。

现在,我们的模型(Model 0)已经配备了损失函数和优化器,万事俱备,只欠训练。在下一节课中,我们将编写训练循环,并创建一个计时函数来跟踪实验过程。

107:创建模型代码计时函数 ⏱️

在本节课中,我们将学习如何创建一个函数来计时我们的机器学习实验。计时对于评估模型训练效率、比较不同设备(如CPU与GPU)的性能至关重要。

上一节我们导入了辅助函数文件并引入了准确率计算函数。本节中,我们将编写一个计时函数,用于量化模型训练所花费的时间。

概述:为什么需要计时?

机器学习本质上是实验性的。在实验中,我们通常需要追踪两个核心指标:

  1. 模型性能,例如损失值和准确率。
  2. 模型运行速度

理想情况下,我们希望模型性能高且运行速度快。然而,这两者之间常常存在权衡。例如,增加神经网络层数或隐藏单元数可能提升性能,但也会因计算量增加而降低运行速度。运行速度对于在互联网、专用GPU或移动设备上部署模型尤为重要。

既然我们已经通过损失函数和准确率函数追踪了模型性能,现在就来编写代码检查其运行速度。请注意,我们目前有意将模型保持在CPU上,以便后续与GPU的运行速度进行比较。

创建计时函数

我们将使用Python的time模块来创建计时函数。

首先,导入所需的计时器:

from time import perf_counter as timer

perf_counter(这里别名为timer)提供了高精度的计时功能。你可以查阅Python官方文档了解更多计时函数细节。其基本原理是:记录代码开始执行的精确时间(开始时间),再记录代码结束的精确时间(结束时间),两者之差即为代码运行时长。

以下是计时函数的定义:

def print_train_time(start: float,
                     end: float,
                     device: torch.device = None):
    """
    打印并返回开始时间与结束时间之间的差值。
    """
    total_time = end - start
    print(f"Train time on {device}: {total_time:.3f} seconds")
    return total_time

函数解析

  • 参数
    • start:实验开始时间。
    • end:实验结束时间。
    • device:模型运行的设备(如cpucuda),默认为None。这有助于我们比较模型在不同设备上的运行速度。
  • 功能:计算总时间差,并以保留三位小数的格式打印出在特定设备上的训练时间,最后返回总时间值。

如何使用计时函数

你可以通过以下方式使用这个函数:

# 记录开始时间
start_time = timer()

# 此处放置你的模型训练代码
# ... (你的训练循环代码) ...

# 记录结束时间
end_time = timer()

# 计算并打印训练时间
total_train_time = print_train_time(start=start_time,
                                    end=end_time,
                                    device="cpu")

在上面的示例中,由于开始和结束时间之间几乎没有执行任何代码,所以打印出的时间会是一个非常小的数字(例如 3.304e-05 秒)。当你将实际的模型训练代码放入中间时,这个函数就能准确测量出训练过程所花费的时间。

总结与展望

本节课中,我们一起学习了如何创建一个实用的模型实验计时函数。我们了解了追踪模型运行速度的重要性,并实现了使用perf_counter进行高精度计时的代码。

至此,我们已经为构建完整的训练流程准备好了所有关键组件:

  • 损失函数 (nn.CrossEntropyLoss)
  • 优化器 (torch.optim.SGD)
  • 评估指标 (accuracy_fn)
  • 计时函数 (print_train_time)
  • 模型 (FashionMNISTModelV2)
  • 数据 (train_dataloader, test_dataloader)

在接下来的视频中,我们将利用所有这些组件,训练我们的第一个基线计算机视觉模型。敬请期待!

108:编写批次数据训练测试循环 🚀

在本节课中,我们将学习如何编写一个完整的训练和测试循环,使用批次数据来训练我们的第一个PyTorch模型。我们将使用DataLoader来处理数据批次,并理解优化器如何基于每个批次(而非每个周期)来更新模型参数。


环境准备与数据加载

上一节我们介绍了如何使用DataLoader将数据划分为批次。现在,我们将在这些批次数据上构建训练循环。

首先,确保我们已准备好数据和模型。以下代码初始化了数据加载器和模型。

# 假设 train_dataloader 和 test_dataloader 已定义
# 假设 model_0 是一个简单的神经网络模型
# 假设 loss_fn 和 optimizer 已定义

构建训练循环

本节中,我们将逐步构建一个包含训练和测试步骤的循环。核心步骤包括遍历周期、遍历训练批次、执行训练步骤、计算损失,然后对测试数据执行相同操作。

以下是构建循环的主要步骤:

  1. 遍历周期。
  2. 遍历训练批次并执行训练步骤。
  3. 计算每个批次的训练损失。
  4. 遍历测试批次并执行测试步骤。
  5. 计算每个批次的测试损失和准确率。
  6. 打印训练过程并计时。

导入进度条工具

为了可视化训练进度,我们将使用tqdm库。

from tqdm.auto import tqdm

设置计时器与周期数

我们设置一个较小的周期数以进行快速实验。

from timeit import default_timer as timer
train_time_start_on_cpu = timer()
epochs = 3  # 使用较小的周期数以便快速实验

编写训练与测试循环

现在,我们开始编写核心循环。循环结构如下:

for epoch in tqdm(range(epochs)):
    print(f"Epoch: {epoch}\n-------")
    # 训练阶段
    train_loss = 0
    for batch, (X, y) in enumerate(train_dataloader):
        model_0.train()
        # 1. 前向传播
        y_pred = model_0(X)
        # 2. 计算损失
        loss = loss_fn(y_pred, y)
        train_loss += loss
        # 3. 优化器梯度清零
        optimizer.zero_grad()
        # 4. 反向传播
        loss.backward()
        # 5. 优化器步进(关键:每个批次更新一次参数)
        optimizer.step()

        # 每400个批次打印一次进度
        if batch % 400 == 0:
            print(f"Looked at {batch * len(X)}/{len(train_dataloader.dataset)} samples.")

    # 计算平均训练损失(每个周期)
    train_loss /= len(train_dataloader)

    # 测试阶段
    test_loss, test_acc = 0, 0
    model_0.eval()
    with torch.inference_mode():
        for X_test, y_test in test_dataloader:
            # 1. 前向传播
            test_pred = model_0(X_test)
            # 2. 计算损失和准确率
            test_loss += loss_fn(test_pred, y_test)
            test_acc += accuracy_fn(y_true=y_test, y_pred=test_pred.argmax(dim=1))

        # 计算平均测试损失和准确率(每个周期)
        test_loss /= len(test_dataloader)
        test_acc /= len(test_dataloader)

    # 打印结果
    print(f"\nTrain loss: {train_loss:.4f} | Test loss: {test_loss:.4f}, Test acc: {test_acc:.4f}")

核心概念:优化器步进 optimizer.step() 位于批次循环内部,这意味着模型参数在每个批次后都会更新,而不是等待整个周期结束。这使学习过程更高效、更稳定。

计算总训练时间

循环结束后,我们计算并打印总训练时间。

train_time_end_on_cpu = timer()
total_train_time_model_0 = train_time_end_on_cpu - train_time_start_on_cpu
print(f"Total training time: {total_train_time_model_0:.2f} seconds on {next(model_0.parameters()).device}.")

总结

本节课中,我们一起学习了如何构建一个完整的PyTorch训练测试循环。关键点包括:

  • 使用 DataLoader 按批次处理数据。
  • 在循环中分别处理训练和测试阶段。
  • 理解并实现了优化器在每个批次(而非每个周期)更新模型参数的核心机制。
  • 使用 tqdm 添加进度条,并使用计时器跟踪训练时间。

这个循环是我们进行深度学习实验的基础模板。在接下来的课程中,我们将对此进行封装和优化,以便更高效地进行模型开发。

109:编写模型评估函数 🧪

在本节课中,我们将学习如何编写一个通用的模型评估函数。这个函数将帮助我们评估不同模型在测试数据集上的性能,并以字典形式返回结果,便于后续比较。


概述

上一节我们构建并训练了一个基线模型。本节中,我们将编写一个可复用的函数来评估模型在测试数据上的损失和准确率。通过函数化评估过程,我们可以轻松地比较多个模型的性能。


修复代码缩进错误

在开始编写评估函数之前,我们需要先修复上一节代码中的一个常见错误:缩进问题。

有时,即使代码逻辑正确,一个错误的空格或制表符也可能导致整个程序无法运行。这是我们刚才遇到的典型缩进错误。

# 示例:修复前的错误缩进
def train_model():
    for epoch in range(epochs):
    for batch in dataloader:  # 这里缺少缩进
        # ... 训练代码

通过逐行检查并调整缩进,我们确保所有代码块都正确对齐。修复后,模型训练成功运行,并输出了进度条和结果。

我们的基线模型在测试集上取得了约 83.5% 的准确率和约 0.47 的损失,训练时间约为 22秒。请注意,由于机器学习的随机性以及硬件差异(例如使用Google Colab的CPU),你的具体数值可能略有不同,但应处于相近范围。


编写模型评估函数

现在,我们进入核心部分:编写一个通用的模型评估函数。以下是该函数的详细步骤。

我们将创建一个名为 eval_model 的函数。它接收一个模型、一个数据加载器、一个损失函数和一个评估指标函数(如准确率计算函数)作为参数,并返回一个包含模型名称、平均损失和平均准确率的字典。

def eval_model(model: torch.nn.Module,
               data_loader: torch.utils.data.DataLoader,
               loss_fn: torch.nn.Module,
               accuracy_fn):
    """
    评估给定模型在指定数据加载器上的性能。

    参数:
        model: 要评估的PyTorch模型。
        data_loader: 用于评估的数据加载器。
        loss_fn: 用于计算损失的函数。
        accuracy_fn: 用于计算准确率的函数。

    返回:
        包含模型名称、平均损失和平均准确率的字典。
    """
    loss, acc = 0, 0
    model.eval()

    # 使用推理模式上下文管理器,禁用梯度计算以提升效率
    with torch.inference_mode():
        for X, y in data_loader:
            # 1. 前向传播,获取预测值
            y_pred = model(X)

            # 2. 累加批次损失
            loss += loss_fn(y_pred, y)

            # 3. 累加批次准确率
            # 注意:模型原始输出是logits,我们使用argmax获取预测类别
            acc += accuracy_fn(y_true=y,
                               y_pred=y_pred.argmax(dim=1))

        # 4. 计算整个数据集上的平均损失和准确率
        loss /= len(data_loader)
        acc /= len(data_loader)

    return {"model_name": model.__class__.__name__,  # 获取模型的类名
            "model_loss": loss.item(),
            "model_acc": acc}

关键点说明:

  • model.__class__.__name__ 用于动态获取模型的类名,这有助于在评估多个模型时进行区分。
  • torch.inference_mode() 上下文管理器在评估时关闭梯度计算,节省内存并加快计算速度。
  • 损失和准确率在批次循环中累加,最后除以批次总数得到平均值。

使用评估函数

函数编写完成后,我们可以用它来评估之前训练的基线模型(model_0)。

以下是调用评估函数的代码:

# 计算 model_0 在测试数据集上的结果
model_0_results = eval_model(model=model_0,
                             data_loader=test_dataloader,
                             loss_fn=loss_fn,
                             accuracy_fn=accuracy_fn)
model_0_results

运行上述代码,我们将得到一个类似这样的字典:
{'model_name': 'FashionMNISTModelV0', 'model_loss': 0.4766, 'model_acc': 83.42}

这个结果与我们训练循环结束时打印的测试结果一致。现在,我们有了一个结构化的方式来存储模型性能。


当前工作流程总结

至此,我们已经完成了机器学习项目工作流程中的多个关键步骤:

  1. 数据准备:下载并加载了FashionMNIST数据集。
  2. 选择/构建模型:创建了第一个全连接神经网络 (model_0)。
  3. 选择损失函数和优化器:使用了交叉熵损失和SGD优化器。
  4. 构建训练循环:编写了代码来迭代数据并更新模型参数。
  5. 拟合模型:在训练数据上训练了模型。
  6. 评估模型:使用我们刚编写的函数在测试数据上评估了模型性能。

下一步:通过实验改进模型

我们当前的基线模型在CPU上训练,取得了不错的结果。然而,深度学习通常可以利用GPU来显著加速训练过程。

下一节的挑战是:建立设备无关的代码。这意味着我们的代码应该能够自动检测并使用可用的GPU(如果存在),否则回退到CPU。这将为后续在GPU上构建和训练更复杂的模型(例如model_1,甚至是卷积神经网络)做好准备。

在下一个视频开始前,请尝试在Google Colab中设置你的运行时使用GPU(“运行时” -> “更改运行时类型” -> “硬件加速器”选择GPU),并准备好重新运行之前的单元格以恢复所有数据和函数。


本节课总结

在本节课中,我们一起学习了:

  1. 如何修复常见的代码缩进错误。
  2. 如何编写一个通用、可复用的模型评估函数 (eval_model)。
  3. 如何使用该函数评估模型并获取结构化的结果字典。
  4. 回顾了截至目前完成的完整机器学习工作流程。
  5. 为下一阶段——利用GPU加速训练并改进模型——做好了准备。

通过将评估过程函数化,我们为系统性地比较不同模型的性能奠定了坚实的基础。

110:配置GPU与设备无关代码 🚀

在本节课中,我们将学习如何配置GPU并编写设备无关的代码,确保我们的PyTorch模型能够在不同硬件上运行。


上一节我们介绍了如何构建和训练一个基线模型。本节中,我们来看看如何利用GPU加速计算,并编写能够自动适应不同硬件环境的代码。

检查GPU可用性

首先,我们需要检查当前环境中是否有可用的GPU。在PyTorch中,我们可以使用以下代码来检查:

import torch
torch.cuda.is_available()

如果返回True,则表示有可用的GPU;如果返回False,则只能使用CPU。

在Google Colab中激活GPU

如果你在Google Colab中运行代码,需要手动激活GPU。以下是激活步骤:

  1. 点击菜单栏中的“运行时”。
  2. 选择“更改运行时类型”。
  3. 在“硬件加速器”下拉菜单中选择“GPU”。
  4. 点击“保存”。

保存后,Colab笔记本会重启并连接到GPU后端。你可以通过运行!nvidia-smi命令来确认GPU的详细信息。

编写设备无关的代码

设备无关的代码意味着无论你的系统使用CPU还是GPU,PyTorch都能自动利用相应的硬件。这是通过设置一个device变量来实现的。

以下是设置设备无关代码的标准方法:

import torch

# 设置设备
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

这段代码会检查CUDA(即NVIDIA GPU支持)是否可用。如果可用,device变量将被设置为"cuda";否则,设置为"cpu"

将模型和数据移动到设备上

定义了device之后,我们需要确保模型和输入数据都位于同一个设备上。这通常通过调用.to(device)方法来实现。

以下是关键步骤:

  1. 将模型移动到设备
    model = YourModelClass().to(device)
    
  2. 将数据移动到设备
    # 对于单个张量
    X = X.to(device)
    y = y.to(device)
    
    # 在训练循环中
    for batch, (X, y) in enumerate(train_dataloader):
        X, y = X.to(device), y.to(device)
        # ... 训练步骤 ...
    

实践建议

以下是编写稳健的设备无关代码时的一些建议:

  • 在笔记本开头设置:通常,最好在导入库之后立即设置device变量。
  • 处理运行时重启:在Colab中切换硬件后,需要重新运行之前的所有单元格,以确保所有变量和导入都正确初始化。
  • 从小规模开始:对于大型数据集和复杂模型,在GPU上运行可能需要更多时间和内存。建议从小规模实验开始,必要时再扩大规模。

本节课中我们一起学习了如何检查GPU可用性、在Google Colab中激活GPU,以及编写能够自动适应CPU和GPU的设备无关代码。这是进行高效深度学习实验的重要一步。

接下来,我们将利用GPU的强大算力,构建一个包含非线性激活函数的更复杂模型,看看它能否在FashionMNIST数据集上取得更好的表现。

111:模型1 - 创建含非线性函数的模型 🚀

在本节课中,我们将学习如何构建一个包含非线性激活函数的神经网络模型,以探索其对模型性能的潜在提升。

概述

上一节我们介绍了设备无关代码的编写。本节中,我们来看看如何通过构建一个包含非线性层的模型来改进我们的基线模型。我们将创建一个名为 FashionMNISTModelV1 的新模型,它在原有的线性层之间插入了ReLU非线性激活函数。

非线性函数的作用

在之前的教程中,我们学习了非线性的重要性。非线性函数帮助神经网络建模非线性数据。例如,仅用直线(线性)难以完美拟合一个圆形数据分布。通过结合线性和非线性函数,神经网络能够以适当的方式建模几乎任何类型的数据。

构建非线性模型

现在,让我们创建一个新的模型类。这个模型的结构与之前的 Model 0 类似,但会在线性层之间穿插ReLU层。

ReLU(Rectified Linear Unit)是一个非线性激活函数,其公式为:

ReLU(x) = max(0, x)

这意味着,如果输入值是负数,ReLU会将其变为0;如果输入值是正数,则保持不变。

以下是创建新模型的代码:

class FashionMNISTModelV1(nn.Module):
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
        super().__init__()
        self.layer_stack = nn.Sequential(
            nn.Flatten(), # 将输入展平为单个向量
            nn.Linear(in_features=input_shape, out_features=hidden_units),
            nn.ReLU(), # 添加非线性激活函数
            nn.Linear(in_features=hidden_units, out_features=output_shape),
            nn.ReLU() # 在末尾再添加一个非线性层
        )
    
    def forward(self, x: torch.Tensor):
        return self.layer_stack(x)

这个模型与之前网络的主要区别在于添加了两个ReLU函数。第一个线性层的输出特征数需要与第二个线性层的输入特征数匹配,而ReLU层不会改变数据的形状。

实例化模型并部署到设备

接下来,我们需要实例化这个模型。为了确保结果的可复现性,我们先设置一个随机种子。然后,我们将模型部署到之前设置的设备(GPU,如果可用)上。

以下是实例化模型的步骤:

  1. 设置随机种子。
  2. 定义输入形状(28*28=784)、隐藏单元数(例如10)和输出形状(类别数量)。
  3. 创建模型实例并将其发送到目标设备。
torch.manual_seed(42)
model_1 = FashionMNISTModelV1(
    input_shape=784,
    hidden_units=10,
    output_shape=len(class_names)
).to(device)

现在,我们可以检查模型的参数所在的设备,确认它已部署在GPU上。

后续步骤

我们已经成功创建了第二个模型。回顾我们的工作流程,在构建模型之后,我们需要为其设置损失函数和优化器。

对于模型1,我们可以使用与模型0完全相同的损失函数和优化器。这是一个很好的练习机会,请你尝试自己为 model_1 创建损失函数和优化器。

在下一节视频中,我们将一起完成这一步,并开始训练和评估这个新的非线性模型。

总结

本节课中我们一起学习了如何构建一个包含非线性激活函数(ReLU)的神经网络模型。我们创建了 FashionMNISTModelV1 类,理解了非线性层在模型中的作用,并将模型实例部署到了GPU设备上。接下来,我们将为这个新模型配置训练组件并观察其性能。

112:配置损失函数与优化器 🧠

在本节课中,我们将学习如何为我们的神经网络模型配置损失函数和优化器。这是训练模型的关键步骤,它们共同决定了模型如何从错误中学习并改进。

上一节我们创建了第二个模型(FashionMNIST Model V1),它与V0的主要区别在于引入了非线性层。现在,我们需要为这个新模型设置训练所需的组件。

设置损失函数与优化器

我们已经为Model 0完成过类似步骤,因此这里将快速回顾核心概念。损失函数用于衡量模型的预测错误程度,而优化器则负责根据损失值更新模型的参数以减少错误

以下是具体步骤:

  1. 首先,从之前下载的辅助函数脚本中导入评估准确率的函数。
  2. 由于我们处理的是多分类问题,因此选择 torch.nn.CrossEntropyLoss 作为损失函数。
  3. 选择 torch.optim.SGD(随机梯度下降)作为优化器,并传入模型参数和学习率。

核心代码如下:

# 导入准确率计算函数
from helper_functions import accuracy_fn

# 设置损失函数
loss_fn = torch.nn.CrossEntropyLoss()

# 设置优化器
optimizer = torch.optim.SGD(params=model_1.parameters(),
                            lr=0.1)

关于实验设计的建议

进行多次模型实验时,一个重要的原则是:每次实验最好只调整一两个变量(例如,这里我们保持学习率与之前模型一致)。这种方法能帮助你清晰地分辨出究竟是哪些改动真正影响了模型的性能,是提升还是降低了效果。

回顾与预告

本节课我们一起学习了如何为PyTorch模型配置损失函数 (nn.CrossEntropyLoss) 和优化器 (optim.SGD)。损失函数量化了模型的错误,而优化器则利用这个信息来调整模型参数。

我们已经多次重复了创建训练循环的步骤。在之前的视频中,我们甚至将训练过程封装成了一个函数。因此,在下一节课中,我们将更进一步,创建两个可复用的函数:train_step(训练步骤)和 test_step(测试步骤)。这将使我们的代码更加模块化和整洁。我建议你先思考一下这两个函数需要接收哪些参数,我们下节课将一起编写它们。

我们下节课见。

113:将训练与评估循环封装为函数 🔄

在本节课中,我们将学习如何将之前编写的训练和测试循环代码封装成可复用的函数。这是编写高效、整洁且不易出错代码的最佳实践。


概述

我们已经准备好了损失函数和优化器。下一步是创建训练和评估循环。在本节中,我们将把之前编写的训练和测试循环代码封装成函数。这样做不仅能遵循最佳实践,还能减少因重复编写相似代码而可能引入的错误。

函数化训练与评估循环

上一节我们介绍了训练循环的基本步骤。本节中,我们来看看如何将这些步骤封装成一个名为 train_step 的函数。

我们将创建一个函数来执行训练步骤,并计划创建另一个函数来执行测试步骤。这样,在每次迭代中,我们只需调用 train_step 和 test_step 即可。

创建训练步骤函数

以下是创建 train_step 函数所需的参数和步骤:

  • 模型:一个 torch.nn.Module 实例。
  • 数据加载器:一个 torch.utils.data.DataLoader 实例。
  • 损失函数:用于计算模型预测与真实值之间差异的函数。
  • 优化器:一个 torch.optim.Optimizer 实例,用于更新模型参数。
  • 准确率函数:用于评估模型性能的可选函数。
  • 设备:指定计算设备(如CPU或GPU),使代码与设备无关。

函数的核心逻辑如下:

  1. 将模型设置为训练模式:model.train()
  2. 遍历数据加载器中的每个批次。
  3. 将数据移动到目标设备:X.to(device), y.to(device)
  4. 执行前向传播:y_pred = model(X)
  5. 计算损失:loss = loss_fn(y_pred, y)
  6. 计算准确率(如果提供了准确率函数)。
  7. 将优化器的梯度归零:optimizer.zero_grad()
  8. 执行反向传播:loss.backward()
  9. 更新模型参数:optimizer.step()
  10. 累积整个数据集的平均损失和准确率。

以下是 train_step 函数的代码实现:

def train_step(model: torch.nn.Module,
               data_loader: torch.utils.data.DataLoader,
               loss_fn: torch.nn.Module,
               optimizer: torch.optim.Optimizer,
               accuracy_fn,
               device: torch.device = device):
    """
    使用模型在数据加载器上执行训练步骤。
    """
    train_loss, train_acc = 0, 0

    # 将模型设置为训练模式
    model.train()

    for batch, (X, y) in enumerate(data_loader):
        # 将数据移动到目标设备
        X, y = X.to(device), y.to(device)

        # 1. 前向传播
        y_pred = model(X)

        # 2. 计算损失
        loss = loss_fn(y_pred, y)
        train_loss += loss

        # 3. 计算准确率
        train_acc += accuracy_fn(y_true=y,
                                 y_pred=y_pred.argmax(dim=1))

        # 4. 优化器梯度归零
        optimizer.zero_grad()

        # 5. 反向传播
        loss.backward()

        # 6. 更新参数
        optimizer.step()

    # 计算平均损失和准确率
    train_loss /= len(data_loader)
    train_acc /= len(data_loader)

    print(f"Train loss: {train_loss:.5f} | Train accuracy: {train_acc:.2f}%")

创建测试步骤函数

现在,我们来看看如何将测试循环也封装成函数。其逻辑与训练步骤类似,但无需执行反向传播和优化器更新。

以下是创建 test_step 函数的基本步骤:

  1. 将模型设置为评估模式:model.eval()
  2. 使用 torch.inference_mode() 上下文管理器来禁用梯度计算,以提升效率。
  3. 遍历数据加载器中的每个批次。
  4. 执行前向传播并计算损失和准确率。
  5. 累积整个数据集的平均损失和准确率。

你的挑战:请参考上面 train_step 函数的格式,尝试将之前编写的测试循环代码封装成一个名为 test_step 的函数。我们将在下个视频中一起完成它。


总结

本节课中我们一起学习了如何将训练循环封装成一个可复用的 train_step 函数。我们明确了函数所需的参数,并逐步实现了前向传播、损失计算、准确率评估、反向传播和参数更新的完整流程。我们还提出了将测试循环封装为 test_step 函数的挑战。通过函数化这些循环,我们的代码将变得更加模块化、易于维护和复用。

114:将测试循环封装为函数 🧪

概述

在本节课中,我们将学习如何将PyTorch中的测试循环封装成一个可复用的函数。上一节我们介绍了如何封装训练循环,本节中我们来看看如何对测试循环进行同样的操作,以提高代码的模块化和可读性。

测试循环函数设计

测试循环函数的目标是评估模型在测试数据集上的性能。与训练循环不同,测试循环不涉及参数优化(即不需要优化器),其主要任务是计算损失和准确率。

以下是测试循环函数需要接收的核心参数:

  • 模型:待评估的PyTorch模型。
  • 数据加载器:用于提供测试数据的DataLoader
  • 损失函数:用于计算模型预测与真实标签之间差异的函数。
  • 准确率函数:用于计算模型预测准确率的自定义函数。
  • 设备:指定计算设备(如CPU或GPU),默认为目标设备。

函数的基本框架如下:

def test_step(model: torch.nn.Module,
              data_loader: torch.utils.data.DataLoader,
              loss_fn: torch.nn.Module,
              accuracy_fn,
              device: torch.device = device):
    """在模型上执行测试循环步骤,遍历数据加载器。"""
    # 函数实现将放在这里

实现测试循环函数

现在,让我们一步步实现这个函数。

首先,初始化测试损失和测试准确率,并将模型设置为评估模式。评估模式会关闭如Dropout等特定于训练层的功能。

    test_loss, test_acc = 0, 0
    model.eval()  # 将模型设置为评估模式

接下来,我们使用torch.inference_mode()上下文管理器。在推理模式下,PyTorch会禁用梯度计算,从而节省内存并加速预测过程。

    with torch.inference_mode():  # 开启推理模式上下文管理器
        for X, y in data_loader:
            # 将数据发送到目标设备
            X, y = X.to(device), y.to(device)
            # 1. 前向传播
            test_pred = model(X)
            # 2. 计算损失和准确率(按批次累加)
            test_loss += loss_fn(test_pred, y)
            test_acc += accuracy_fn(y_true=y,
                                    y_pred=test_pred.argmax(dim=1))

在循环结束后,我们需要计算整个测试集上的平均损失和准确率。注意,调整度量的代码仍需保持在inference_mode上下文管理器内部。

        # 调整度量指标(计算整个测试集的平均值)
        test_loss /= len(data_loader)
        test_acc /= len(data_loader)

最后,打印测试结果。

        # 打印结果
        print(f"Test loss: {test_loss:.5f} | Test accuracy: {test_acc:.2f}%\n")

整合训练与测试函数

通过封装训练和测试循环,我们的主训练代码变得非常简洁。我们可以这样使用它们:

epochs = 3
for epoch in tqdm(range(epochs)):
    print(f"Epoch: {epoch}\n---------")
    train_step(model=model1,
               data_loader=train_dataloader,
               loss_fn=loss_fn,
               optimizer=optimizer,
               accuracy_fn=accuracy_fn,
               device=device)
    test_step(model=model1,
              data_loader=test_dataloader,
              loss_fn=loss_fn,
              accuracy_fn=accuracy_fn,
              device=device)

总结

本节课中我们一起学习了如何将PyTorch的测试循环封装成一个独立的函数。我们明确了函数所需的参数,实现了包括设置评估模式、使用推理模式、遍历数据、计算度量指标以及输出结果在内的完整步骤。将训练和测试逻辑模块化,能使代码更清晰、更易于维护和复用。在下一节,我们将利用这两个封装好的函数来训练我们的模型。

115:使用训练与测试函数评估模型 🚀

在本节课中,我们将学习如何将之前编写的训练步骤函数和测试步骤函数组合起来,创建一个完整的模型训练与评估循环。我们将使用GPU进行实验,并比较其与CPU在训练时间上的差异。


概述

上一节我们介绍了如何编写独立的训练步骤和测试步骤函数。本节中,我们将把这些函数整合到一个优化循环中,对模型进行多轮训练与评估。我们还将测量模型在GPU上的训练时间,并与之前CPU上的结果进行对比。

实验设置

首先,我们设置随机种子以确保实验的可重复性,并导入计时工具。

import torch
from timeit import default_timer as timer

torch.manual_seed(42)

接下来,我们初始化计时器,并设置训练轮数为3,以保持实验条件的一致性。

train_time_start_on_gpu = timer()
epochs = 3

创建训练与评估循环

现在,我们将使用 train_steptest_step 函数来构建主循环。

以下是构建循环所需的步骤:

  1. 遍历每一个训练轮次。
  2. 在每个轮次中,调用 train_step 函数进行模型训练。
  3. 随后,调用 test_step 函数评估模型在测试集上的性能。
  4. 打印每个轮次的训练损失、训练准确率和测试准确率。
from tqdm.auto import tqdm

for epoch in tqdm(range(epochs)):
    print(f"Epoch: {epoch}\n---------")
    # 执行训练步骤
    train_step(model=model_1,
               data_loader=train_dataloader,
               loss_fn=loss_fn,
               optimizer=optimizer,
               accuracy_fn=accuracy_fn,
               device=device)
    # 执行测试步骤
    test_step(model=model_1,
              data_loader=test_dataloader,
              loss_fn=loss_fn,
              accuracy_fn=accuracy_fn,
              device=device)

循环结束后,我们停止计时器,并计算总训练时间。

train_time_end_on_gpu = timer()
total_train_time_model_1 = print_train_time(start=train_time_start_on_gpu,
                                            end=train_time_end_on_gpu,
                                            device=device)

结果分析与讨论

运行上述代码后,我们得到了模型在GPU上的训练结果和时间。将其与之前模型在CPU上的基线结果进行对比,是评估改进的关键。

对比时,需要关注两个核心指标:

  • 模型性能:例如测试准确率。
  • 训练效率:即总训练时间。

有时你可能会发现一个有趣的现象:在某些情况下,模型在CPU上的训练速度可能反而比GPU更快。

这通常主要由以下两个原因造成:

  1. 数据转移开销:将数据和模型在CPU和GPU内存之间来回复制所产生的耗时,可能超过了GPU本身的计算优势。
  2. 硬件差异:你所使用的CPU在计算能力上可能强于你所使用的GPU。这种情况较为少见,但对于一些计算量不大的小型模型和数据集,确实可能发生。

提示:GPU在深度学习中的速度优势,在处理大型模型海量数据集计算密集型网络层时最为明显。

总结

本节课中,我们一起学习了如何将模块化的训练和测试函数组合成一个完整的训练循环。我们实践了在GPU上进行模型训练,并分析了影响训练速度的潜在因素,特别是数据转移开销对GPU加速效果的影响。

理解这些概念有助于你在未来实践中,根据任务规模和硬件条件,更有效地利用计算资源。在接下来的课程中,我们将继续探索如何改进模型架构以获得更好的性能。

116:获取模型1结果字典 📊

在本节课中,我们将学习如何为训练好的模型1创建一个结果字典,以便后续与其他模型进行性能对比。同时,我们还将解决一个常见的PyTorch错误:设备不匹配问题。


上一节我们介绍了模型1的训练过程,本节中我们来看看如何评估模型1的性能并存储结果。

为了后续能够方便地比较所有模型的性能,我们需要将模型1在测试集上的评估结果存储在一个字典中。这个字典将包含损失值和准确率。

以下是创建模型1结果字典的步骤:

  1. 调用评估函数 eval_model
  2. 传入模型1、测试数据加载器、损失函数和准确率计算函数。
  3. 将结果存储在 model_1_results 变量中。
# 获取模型1结果字典
model_1_results = eval_model(model=model_1,
                             data_loader=test_dataloader,
                             loss_fn=loss_fn,
                             accuracy_fn=accuracy_fn)

然而,运行上述代码时,我们遇到了一个错误:RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda and cpu。这个错误表明我们的数据和模型位于不同的设备上(例如,一个在GPU,一个在CPU)。

在深度学习中,有三个常见的错误来源:数据与模型的形状不匹配、设备不匹配以及数据类型不匹配。我们当前遇到的就是设备不匹配问题。

为了解决这个问题,我们需要确保在评估函数 eval_model 中,输入数据与模型位于同一设备上。因此,我们需要修改 eval_model 函数,使其具备设备无关性。

以下是修改后的 eval_model 函数代码:

def eval_model(model: torch.nn.Module,
               data_loader: torch.utils.data.DataLoader,
               loss_fn: torch.nn.Module,
               accuracy_fn,
               device: torch.device = device): # 添加设备参数
    """返回一个包含模型在给定数据加载器上性能结果的字典。"""
    loss, acc = 0, 0
    model.eval()
    with torch.inference_mode():
        for X, y in data_loader:
            # 确保数据与模型在同一设备上
            X, y = X.to(device), y.to(device)
            # 前向传播
            y_pred = model(X)
            # 累加损失和准确率
            loss += loss_fn(y_pred, y)
            acc += accuracy_fn(y_true=y, y_pred=y_pred.argmax(dim=1))

        # 计算平均损失和准确率
        loss /= len(data_loader)
        acc /= len(data_loader)

    return {"model_name": model.__class__.__name__,
            "model_loss": loss.item(),
            "model_acc": acc}

修改完成后,我们再次运行获取结果字典的代码。这次它应该能成功运行,并输出模型1在测试集上的损失和准确率。

model_1_results 与之前得到的 model_0_results(基线模型结果)进行比较,我们可能会发现基线模型的表现仍然领先。这没关系,在接下来的课程中,我们将引入更强大的模型——卷积神经网络(CNN),来提升性能。

请注意,如果你的结果数字与我的不完全相同,只要相差不大就无需担心,这是机器学习和深度学习固有的随机性所致。如果结果差异巨大,请检查代码单元格是否都正确运行。


本节课中我们一起学习了如何为模型创建评估结果字典,并解决了因数据和模型设备不匹配导致的运行时错误。我们通过修改评估函数,使其能够自动将数据发送到模型所在的设备,从而编写了设备无关的代码,这是PyTorch最佳实践之一。

117:卷积神经网络高层概览 🧠

在本节课中,我们将学习卷积神经网络(CNN)的基本概念和架构。我们将了解CNN如何专门用于处理图像等视觉数据,并探索其核心组件的工作原理。

上一节我们介绍了第一个模型实验,它未能超越基线。本节中我们来看看第二个模型:卷积神经网络。

什么是卷积神经网络?

卷积神经网络是一种专门用于处理具有网格状拓扑结构数据(如图像)的深度学习模型。其核心能力在于自动从视觉数据中寻找模式

CNN的典型架构包含以下层类型:

  • 输入层:接收图像数据(通常表示为张量)。
  • 隐藏层:通常包含卷积层、激活函数层(如ReLU)和池化层。
  • 输出层:通常是线性层,输出分类结果。

CNN工作流程可视化

以下是CNN处理图像的简化流程:

  1. 输入:一张图像(例如,一张比萨的图片)。
  2. 预处理:将图像转换为RGB通道的张量。
  3. 特征提取:通过一系列卷积层ReLU激活层池化层的组合传递数据。这些层协同工作以识别图像中的特征。
  4. 分类输出:最后通过线性输出层,为每个可能的类别生成一个值(例如,判断图像属于“比萨”或“汽车”)。

关于深度学习模型,一个重要概念是:层的确切顺序并非一成不变。研究人员每天都在探索新的层组合方式。更重要的总体原则是:如何将输入有效地转化为理想的输出。

构建更深的网络

“深度学习”中的“深度”来源于向模型中添加更多层。理论上,模型层数越多,其发现数据中模式的机会就越大

每一层都对输入数据执行一系列数学运算,后续层接收前一层的输出作为输入。虽然存在像残差连接这样的高级架构,但本课程我们专注于构建第一个基础的CNN。

探索CNN内部机制

为了更好地理解CNN,强烈推荐使用CNN Explainer网站进行交互式学习。这个工具可以直观展示图像如何逐层通过CNN。

以下是该网站演示的核心过程:

  • 输入分解:彩色图像被分解为红、绿、蓝三个通道。
  • 卷积操作卷积核(或过滤器)在图像上滑动,执行卷积运算,以检测局部特征(如边缘、纹理)。
  • 特征激活:每个隐藏单元会学习数据的不同特征。深度学习的魅力(也是挑战)在于,模型自行决定学习什么特征最有效
  • 尺寸变化:通常,经过卷积或池化层后,特征图的尺寸会发生变化。
  • 最终输出:网络末端输出每个类别的得分,得分最高的类别即为预测结果。

核心概念总结

本节课中我们一起学习了:

  1. 卷积神经网络是处理视觉数据的强大工具。
  2. CNN的基本架构包括输入层卷积层激活层池化层输出层
  3. 网络的“深度”通过添加更多层来实现,以增强模式识别能力。
  4. 卷积操作是CNN的核心,其数学形式可简化为:输出特征图 = 卷积(输入图像, 卷积核) + 偏置
  5. 理解CNN内部机制的最佳方式是进行交互式可视化探索。

在下一节视频中,我们将开始编写PyTorch代码,亲手构建一个用于计算机视觉任务的卷积神经网络。

118:编写首个PyTorch卷积神经网络 🧠

在本节课中,我们将要学习如何构建一个卷积神经网络。我们将复现CNN Explainer网站上的“Tiny VGG”架构,并逐步理解其每一层的含义和作用。


概述

上一节我们介绍了CNN Explainer网站,这是一个可视化理解卷积神经网络的优秀工具。本节中,我们来看看如何用代码实现一个真实的卷积神经网络模型。

我们将构建一个名为“Tiny VGG”的模型,它由多个卷积块组成,每个块包含卷积层、激活函数和池化层。这是深度学习实践中常见的模式:找到一个在特定问题上表现良好的架构,然后用代码复现它,以解决我们自己的问题。


构建模型类

首先,我们像往常一样,通过子类化 nn.Module 来创建我们的模型。

class FashionMNISTModelV2(nn.Module):
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
        super().__init__()

在初始化方法中,我们接收三个参数:输入形状(图像通道数)、隐藏单元数和输出形状(类别数)。


定义第一个卷积块

卷积神经网络通常由多个“块”组成。一个块是多个层的组合,而整个架构则由多个块堆叠而成。这就像搭乐高积木一样。

以下是第一个卷积块的构建代码:

        self.conv_block_1 = nn.Sequential(
            nn.Conv2d(in_channels=input_shape,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2)
        )

我们使用了 nn.Sequential 容器来顺序组织这些层。这个块复现了CNN Explainer网站上第一个蓝色块的结构:卷积层 -> ReLU激活 -> 卷积层 -> ReLU激活 -> 最大池化层。

nn.Conv2d 是我们使用的第一个卷积层。其中的 in_channelsout_channelskernel_sizestridepadding 都是我们可以自己设置的超参数2D 表示我们处理的是二维图像数据(高度和宽度)。

nn.MaxPool2d 是最大池化层,其 kernel_size=2 表示使用一个2x2的窗口。该层会取窗口内所有值的最大值作为输出,这有助于压缩数据并提取最显著的特征。


定义第二个卷积块

第二个卷积块的结构与第一个完全相同,只是它接收第一个块的输出作为输入。

        self.conv_block_2 = nn.Sequential(
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2)
        )

通过堆叠相同的块,我们可以构建更深的网络,从而学习更复杂的特征。


定义分类器层

前面的卷积块是特征提取器,它们学习最能代表我们数据的模式。最后,我们需要一个分类器层,将这些提取出的特征映射到我们的目标类别上。

分类器层通常包含一个展平层和一个或多个全连接层。

        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=hidden_units*?, # 需要计算
                      out_features=output_shape)
        )

这里有一个小问题:我们需要知道展平后的特征向量的长度(即 in_features 的值)。这个值取决于输入图像经过所有卷积和池化层后的尺寸。我们将在后续视频中介绍一个技巧来计算它。


定义前向传播

定义了所有组件后,我们需要在 forward 方法中定义数据如何流经这些组件。

    def forward(self, x):
        x = self.conv_block_1(x)
        print(x.shape) # 打印形状以跟踪变化
        x = self.conv_block_2(x)
        print(x.shape)
        x = self.classifier(x)
        return x

我们让输入 x 依次通过第一个卷积块、第二个卷积块,最后通过分类器层。打印形状有助于我们理解数据在每一层后的变化。


实例化模型

现在,我们可以实例化我们的第一个卷积神经网络了。

torch.manual_seed(42)
model_2 = FashionMNISTModelV2(input_shape=1,      # 灰度图像,1个通道
                              hidden_units=10,    # 与Tiny VGG一致
                              output_shape=len(class_names)).to(device)

对于FashionMNIST数据集,输入形状是1,因为它是灰度图像。如果是彩色图像(RGB),则输入形状应为3。隐藏单元数设置为10,与Tiny VGG架构保持一致。输出形状是数据集中类别的数量。


总结

本节课中我们一起学习了如何从零开始构建一个卷积神经网络。我们复现了Tiny VGG架构,它由两个相同的卷积块和一个分类器层组成。每个卷积块包含卷积层、ReLU激活函数和最大池化层。

我们编写了大量代码,这是目前最大的神经网络,你应该为此感到自豪。深度学习中的常见做法是:找到一个在某个问题上有效的架构,然后用代码复现它,并尝试解决你自己的问题。

你的课后任务是花至少20分钟深入研究CNN Explainer网站,特别是“理解超参数”部分,了解 paddingkernel_sizestride 的具体含义。这将使接下来的课程内容更容易理解。

在下一个视频中,我们将逐步分解这个网络,并解决 in_features 的计算问题。

119:模型2 - 逐步解析Conv2D层 🧠

在本节课中,我们将深入探讨PyTorch中的nn.Conv2D层。我们将通过代码实践,理解卷积神经网络(CNN)中这一核心组件的工作原理,并学习如何调整其参数以改变模型的行为。

概述

上一节我们共同编写了第一个PyTorch卷积神经网络,复现了CNN Explainer网站上的Tiny VGG架构。本节中,我们来看看构成该网络的两个新层之一:nn.Conv2D层。我们将通过创建虚拟数据并手动调整参数,来直观地理解卷积操作。

7.1 逐步解析 nn.Conv2D

要了解nn.Conv2D的内部运作,最直接的方法是查阅官方文档。其核心是一个数学运算,可以概括为以下公式:

输出 = 偏置项 + Σ (权重 × 输入)

这个公式表明,输出是通过权重张量和偏置值对输入数据进行某种变换得到的。权重矩阵和偏置值以某种方式操纵我们的输入,从而产生输出。

我们不会过度纠结于数学细节,而是通过代码来复现CNN Explainer网站上显示的第一个卷积层。使用虚拟输入进行测试是理解层行为的绝佳方式。

创建虚拟数据

首先,我们需要创建与CNN Explainer示例尺寸匹配的虚拟数据。在PyTorch中,默认使用“通道优先”的数据格式。

import torch

# 设置随机种子以保证结果可复现
torch.manual_seed(42)

# 创建一个批次的图像数据:批次大小=32, 通道数=3, 高度=64, 宽度=64
image_batch = torch.randn(size=(32, 3, 64, 64))

# 从批次中取出第一张单张图像
single_image = image_batch[0]

# 检查数据形状
print(f"图像批次形状: {image_batch.shape}")
print(f"单张图像形状: {single_image.shape}")
print(f"单张图像数据:\n{single_image}")

运行以上代码,你会得到形状为(32, 3, 64, 64)的图像批次和形状为(3, 64, 64)的单张图像。这些数据目前只是随机数,我们的模型(由随机初始化的参数构成)目标就是学习如何调整这些随机数,以最佳地表示真实数据。

创建并测试单个Conv2D层

现在,让我们创建一个单独的nn.Conv2D层,并将我们的虚拟数据传递给它。

以下是创建卷积层时需要理解的关键参数:

  • in_channels: 输入数据的通道数,必须与图像通道数匹配(本例中为3)。
  • out_channels: 输出通道数,类似于全连接层中的隐藏单元数,决定了该层将学习多少种不同的特征(或滤波器)。
  • kernel_size: 卷积核(或滤波器)的大小。例如,3 等价于 (3, 3),表示一个3x3的滑动窗口。
  • stride: 卷积核在输入上滑动时的步长。默认值为1,表示每次移动一个像素。
  • padding: 在输入图像边缘添加的像素填充数。默认值为0,表示不添加填充。

让我们用与CNN Explainer示例相同的参数创建一个层:

import torch.nn as nn

# 创建一个Conv2D层
conv_layer = nn.Conv2d(in_channels=3,  # 输入通道数,匹配图像
                       out_channels=10, # 输出通道数,学习10种特征
                       kernel_size=3,   # 3x3的卷积核
                       stride=1,        # 步长为1
                       padding=0)       # 无填充

print(conv_layer)

理解卷积参数

为了直观理解这些参数,可以参考CNN Explainer网站的可视化:

  • 卷积核(Kernel/Filter): 这是卷积层中可学习的权重矩阵,在输入图像上滑动。在CNN Explainer中,它是一个3x3的红色方块,其内部的数字就是权重。卷积层的目标就是学习能使输出最有用的权重值。
  • 步长(Stride): 控制卷积核每次滑动的像素数。步长为1时,卷积核逐个像素移动;步长为2时,则每次跳过两个像素,这会导致输出尺寸缩小。
  • 填充(Padding): 在输入图像的边缘添加额外的像素(通常值为0)。这确保了卷积核能够处理图像边缘的信息,有时也用于控制输出尺寸。

在深度学习中,一个常见的起步策略是复制现有成功架构中使用的参数值,然后根据自己任务的表现进行调整。

将数据通过卷积层

现在,尝试将我们的单张图像通过卷积层:

# 尝试传递数据
# conv_output = conv_layer(single_image) # 这行会引发错误!

直接传递single_image会引发一个常见的形状错误:nn.Conv2d期望一个四维张量 [batch_size, channels, height, width],但我们只提供了一个三维张量 [channels, height, width]

解决方法是为我们的单张图像添加一个批次维度:

# 在维度0(最前面)添加一个批次维度
single_image_with_batch = single_image.unsqueeze(dim=0)
print(f"添加批次维度后的形状: {single_image_with_batch.shape}")

# 现在可以正确通过卷积层了
conv_output = conv_layer(single_image_with_batch)
print(f"卷积层输出形状: {conv_output.shape}")
print(f"卷积层输出数据:\n{conv_output}")

成功!输出是一个形状为 [1, 10, 62, 62] 的张量。我们来解读一下:

  • 1: 批次大小(我们只输入了一张图)。
  • 10: 输出通道数,与我们设置的out_channels一致。
  • 62, 62: 输出特征图的高度和宽度。由于我们使用了3x3卷积核且无填充,每边各减少了1个像素(公式:输出尺寸 = (输入尺寸 - 核尺寸 + 2*填充) / 步长 + 1)。

探索参数变化的影响

理解参数如何影响输出的最佳方式是动手实验。以下是你可以尝试的练习:

  • 增大kernel_size: 例如设为5,观察输出尺寸如何变得更小。
  • 改变stride: 例如设为2,观察输出尺寸如何近似减半。
  • 添加padding: 例如设为1,观察输出尺寸如何恢复为与输入相近的64x64。

你可以通过修改conv_layer的初始化参数并重新运行代码来观察这些变化。例如:

# 尝试不同的参数
conv_layer_2 = nn.Conv2d(in_channels=3, out_channels=10, kernel_size=5, stride=2, padding=1)
output_2 = conv_layer_2(single_image_with_batch)
print(f"新参数下的输出形状: {output_2.shape}")

总结

本节课中,我们一起学习了PyTorch中nn.Conv2D层的核心机制。我们通过创建虚拟数据,逐步解析了卷积层的各个参数(in_channels, out_channels, kernel_size, stride, padding)的含义和作用,并亲眼见证了它们如何改变输入数据的形状。关键在于,卷积层通过其可学习的权重(卷积核)在输入数据上滑动执行计算,目的是提取对任务有用的特征。虽然我们目前使用的是随机数据,但同样的过程将应用于真实图像,模型中的随机权重将通过训练过程逐渐调整以识别有意义的模式。请务必动手尝试改变参数,这是巩固理解的最有效方式。

120:逐步解析MaxPool2D层 🧠

在本节课中,我们将深入探讨PyTorch中的MaxPool2D层。我们将通过编写测试代码,观察输入和输出的变化,来理解最大池化层如何工作。我们将从理论概念出发,结合实践代码,帮助你直观地掌握这一重要操作。


概述

最大池化层是卷积神经网络中的关键组件,它通过提取局部区域的最大值来降低特征图的空间尺寸,从而压缩信息并保留最重要的特征。本节我们将通过代码示例,逐步解析MaxPool2D层的工作原理。


1. 理解MaxPool2D层

上一节我们介绍了卷积层,本节中我们来看看最大池化层。MaxPool2D层的主要作用是对输入数据进行下采样,减少计算量并提取主要特征。

在PyTorch文档中,MaxPool2D层的输入输出关系可以描述为:

  • 输入尺寸:(N, C, H_in, W_in)
  • 输出尺寸:(N, C, H_out, W_out)

其中:

  • N:批次大小
  • C:通道数
  • H_inW_in:输入的高度和宽度
  • H_outW_out:输出的高度和宽度

输出值的计算公式为:

output = max(input[i, j, k: k + kernel_size, l: l + kernel_size])

2. 创建测试图像

为了理解最大池化层的工作方式,我们首先创建一个测试图像张量。这个张量的形状模拟了单个图像在CNN中的尺寸。

以下是创建测试图像的代码:

import torch

# 设置随机种子以确保结果可重复
torch.manual_seed(42)

# 创建随机张量,形状为(3, 64, 64),模拟RGB图像
test_image = torch.randn(size=(3, 64, 64))
print("原始图像形状:", test_image.shape)

3. 添加批次维度

在将图像传递给卷积层之前,我们需要添加一个批次维度。这是因为PyTorch的卷积层期望输入为四维张量:(batch_size, channels, height, width)

以下是添加批次维度的代码:

# 在第0维度添加批次维度
test_image_unsqueezed = test_image.unsqueeze(dim=0)
print("添加批次维度后的形状:", test_image_unsqueezed.shape)

4. 创建卷积层和最大池化层

接下来,我们创建一个卷积层和一个最大池化层。我们将使用kernel_size=2的最大池化层,这意味着池化窗口为2x2。

以下是创建层的代码:

import torch.nn as nn

# 创建卷积层
conv_layer = nn.Conv2d(in_channels=3, out_channels=10, kernel_size=3, padding=1)

# 创建最大池化层
maxpool_layer = nn.MaxPool2d(kernel_size=2)

5. 逐步传递数据

现在,我们将测试图像依次通过卷积层和最大池化层,并观察每一步的形状变化。以下是传递数据的代码:

# 通过卷积层
test_image_through_conv = conv_layer(test_image_unsqueezed)
print("卷积层输出形状:", test_image_through_conv.shape)

# 通过最大池化层
test_image_through_conv_and_maxpool = maxpool_layer(test_image_through_conv)
print("最大池化层输出形状:", test_image_through_conv_and_maxpool.shape)

6. 可视化最大池化操作

为了更直观地理解最大池化操作,我们创建一个更小的张量,并手动计算最大值。以下是可视化代码:

# 创建一个小型随机张量
random_tensor = torch.randn(size=(1, 1, 2, 2))
print("随机张量:\n", random_tensor)
print("随机张量形状:", random_tensor.shape)

# 通过最大池化层
maxpool_tensor = maxpool_layer(random_tensor)
print("最大池化张量:\n", maxpool_tensor)
print("最大池化张量形状:", maxpool_tensor.shape)

7. 最大池化的作用

最大池化层通过提取局部区域的最大值来压缩特征图。例如,一个2x2的池化窗口会将四个值压缩为一个最大值。这种操作有助于:

  • 减少计算量
  • 提取主要特征
  • 增强模型的平移不变性

在CNN中,卷积层学习特征,激活函数引入非线性,而最大池化层进一步压缩这些特征,形成更紧凑的表示。


8. 扩展实验

你可以尝试调整最大池化层的kernel_size参数,观察输出形状的变化。例如,将kernel_size设置为4,输出形状将进一步缩小。

# 创建kernel_size=4的最大池化层
maxpool_layer_4 = nn.MaxPool2d(kernel_size=4)
test_image_through_maxpool_4 = maxpool_layer_4(test_image_through_conv)
print("kernel_size=4的输出形状:", test_image_through_maxpool_4.shape)

总结

本节课中,我们一起学习了MaxPool2D层的工作原理。我们通过创建测试图像、添加批次维度、逐步传递数据以及可视化操作,深入理解了最大池化层如何压缩特征图并提取主要特征。最大池化层是卷积神经网络中的重要组件,它帮助模型减少计算量并保留关键信息。

在下一节课中,我们将尝试使用Tiny VGG网络,并观察数据通过卷积块时的形状变化。

121:模型2 - 层输入输出形状计算技巧 🧮

在本节课中,我们将学习如何计算和调试卷积神经网络中各层的输入输出形状。我们将通过向模型传递虚拟数据,并打印各层输出的形状,来确保模型结构正确,并解决常见的形状不匹配问题。


在之前的几节视频中,我们一直在复现CNN Explainer网站上的Tiny VGG架构。这实际上非常令人兴奋,因为几年前这可能需要数月的工作,而我们仅用几行PyTorch代码就分解并重建了它。这充分展示了PyTorch的强大以及深度学习领域的巨大进步。

然而,我们的工作尚未完成。让我们回顾一下我们已经完成的部分:我们创建了输入层、Conv2D层、ReLU激活层、池化层,最后是输出层。现在,让我们看看当实际数据通过整个模型时会发生什么。这是一个常见的实践:复现一个模型,然后用你自己的数据测试它。

我们将从使用一些虚拟数据开始,以确保我们的模型正常工作,然后再传递真实数据。


使用虚拟数据测试模型

为了测试模型,我们首先创建一个与真实数据形状相同的随机张量。我们的FashionMNIST图像形状是 (1, 28, 28)

以下是创建随机张量的代码:

random_image_tensor = torch.randn(size=(1, 28, 28))

接下来,我们需要将这个张量传递给模型。但在此之前,我们必须处理两个常见问题:形状不匹配设备不匹配

  1. 添加批次维度:PyTorch模型通常期望输入具有批次维度,即形状为 (batch_size, channels, height, width)。我们的随机张量只有三个维度,因此需要使用 unsqueeze(0) 添加一个批次维度。
  2. 确保设备一致:如果模型在GPU上,输入数据也必须在GPU上。我们之前已经设置了设备无关的代码,因此需要将张量移动到目标设备。

以下是准备并传递张量的代码:

# 添加批次维度并移动到正确设备
random_image_tensor = random_image_tensor.unsqueeze(0).to(device)
# 进行前向传播
output = model2(random_image_tensor)

打印各层形状以进行调试

当我们首次尝试传递数据时,可能会遇到形状错误。我的调试技巧是:在模型的 forward 方法中,打印每个主要模块(如卷积块)输出张量的形状。

例如,在我们的 model2 中,我们可以在 forward 方法中添加打印语句:

def forward(self, x):
    x = self.conv_block_1(x)
    print(f"Output shape of conv_block_1: {x.shape}")
    x = self.conv_block_2(x)
    print(f"Output shape of conv_block_2: {x.shape}")
    x = self.classifier(x)
    print(f"Output shape of classifier: {x.shape}")
    return x

通过运行这个修改后的模型,我们可以看到:

  • conv_block_1 的输出形状可能是 (1, 10, 14, 14)
  • conv_block_2 的输出形状可能是 (1, 10, 7, 7)

这些打印出来的形状信息至关重要。


解决分类器层的输入形状问题

问题通常出现在最后的分类器层(全连接层)。全连接层需要知道输入特征的数量。在我们的例子中,conv_block_2 的输出在展平后,应该作为分类器的输入。

从打印信息得知,conv_block_2 的输出形状是 (1, 10, 7, 7)。将其展平后,特征数量为:
10 * 7 * 7 = 490

因此,我们分类器中第一个线性层(nn.Linear)的 in_features 参数应设置为 490。矩阵乘法的规则是内部维度必须匹配,这个计算确保了这一点。

我们可以手动计算,但更高效的方法是让代码通过打印形状来告诉我们这个值。这就是“如有疑问,用代码解决”的策略。

在修正了 in_features 参数后,再次运行模型。现在,分类器的输出形状应该是 (1, 10),这正好对应我们数据集的10个类别。太棒了!我们刚刚成功计算并验证了模型中每一层的输入和输出形状。


总结

本节课我们一起学习了如何计算和调试CNN模型的层间形状。核心技巧是:

  1. 使用与真实数据同形状的虚拟张量测试模型。
  2. forward 方法中打印关键节点的张量形状。
  3. 根据打印出的形状信息,修正后续层(尤其是全连接层)的输入参数。
  4. 始终注意处理批次维度、设备一致性和数据类型这三个深度学习中的常见问题。

通过这种方法,我们可以确保数据能顺畅地流经整个网络,为下一步使用真实的训练和测试数据训练我们的第一个卷积神经网络做好准备。

122:配置损失函数与优化器 🧠

在本节课中,我们将学习如何为我们的第一个卷积神经网络(CNN)配置训练所需的关键组件:损失函数和优化器。这是将模型从静态架构转变为能够从数据中学习的关键一步。

上一节我们构建并分析了CNN模型的结构,本节中我们来看看如何为其设置训练过程。

工作流程回顾 📋

首先,让我们回顾一下当前的PyTorch工作流程。我们已经完成了数据准备和模型构建。现在,我们正处于工作流程的下一步:选择损失函数和优化器。

我们的目标是创建一个预测模型,它可以接收服装的灰度图像作为输入,并输出预测的类别。

配置损失函数与优化器 ⚙️

以下是配置训练组件的具体步骤。

首先,我们需要设置损失函数、评估指标和优化器。我们将从辅助函数中导入准确率计算函数。

# 导入准确率计算函数
from helper_functions import accuracy_fn

# 设置损失函数
loss_fn = nn.CrossEntropyLoss()

# 设置优化器
optimizer = torch.optim.SGD(params=model_2.parameters(),
                            lr=0.1)

我们选择 nn.CrossEntropyLoss 作为损失函数,因为这是一个多类别分类问题。优化器我们沿用之前使用过的随机梯度下降(SGD),并传入 model_2 的参数,学习率设置为 0.1。

优化目标 🎯

现在,我们的优化目标是 model_2.state_dict() 中的所有参数。这些参数包括模型各层中的权重和偏置,它们目前是随机初始化的。训练过程就是通过优化器调整这些参数,使模型能够在 FashionMNIST 数据集上做出更好的预测。

下一步:构建训练循环 🔄

在下一节视频中,我们将进入工作流程的最后一步:构建训练循环。幸运的是,我们可以复用之前编写的 train_steptest_step 函数来训练我们的 Model 2。你可以先尝试自己完成,我们将在下一节课一起实现。

本节课中我们一起学习了如何为CNN模型配置损失函数和优化器,这是启动模型训练前的必要准备。下一节,我们将把这些组件组合起来,开始真正的模型训练过程。

123:训练首个CNN并评估结果 🚀

在本节课中,我们将要学习如何训练我们的第一个卷积神经网络模型,并使用之前创建的训练和测试函数来评估其性能。我们将设置随机种子以确保实验的可重复性,并测量训练时间以比较不同模型的效率。


模型二:使用训练和测试函数

上一节我们介绍了如何构建一个卷积神经网络。本节中我们来看看如何使用之前创建的训练和测试函数来训练和评估这个模型。

我们首先设置随机种子,以确保实验的可重复性。

torch.manual_seed(42)
torch.cuda.manual_seed(42)

接下来,我们测量训练时间,因为除了评估指标外,训练时间也是模型比较的一个重要方面。

from timeit import default_timer as timer
train_time_start_model2 = timer()

以下是训练循环的步骤:

  1. 我们使用 tqdm 来可视化训练进度。
  2. 设置训练周期为3个,以保持实验简短。
  3. 调用 train_step 函数进行训练。
  4. 调用 test_step 函数进行测试。
epochs = 3
for epoch in tqdm(range(epochs)):
    print(f"Epoch: {epoch}\n---------")
    train_step(model=model_2,
               data_loader=train_dataloader,
               loss_fn=loss_fn,
               optimizer=optimizer,
               accuracy_fn=accuracy_fn,
               device=device)
    test_step(model=model_2,
              data_loader=test_dataloader,
              loss_fn=loss_fn,
              accuracy_fn=accuracy_fn,
              device=device)

训练结束后,我们计算总训练时间。

train_time_end_model2 = timer()
total_train_time_model2 = print_train_time(start=train_time_start_model2,
                                           end=train_time_end_model2,
                                           device=device)

现在,我们准备训练我们的第一个卷积神经网络。运行代码,观察训练过程。


评估模型性能

训练完成后,我们需要评估模型的性能。我们使用之前创建的 eval_model 函数来计算模型在测试数据集上的结果。

model_2_results = eval_model(model=model_2,
                             data_loader=test_dataloader,
                             loss_fn=loss_fn,
                             accuracy_fn=accuracy_fn,
                             device=device)

让我们查看模型二的结果。

print(model_2_results)

模型达到了约88.5%的测试准确率。这比我们之前的基线模型有显著提升。同时,我们记录了训练时间,以便后续与其他模型进行比较。


总结

本节课中我们一起学习了如何训练和评估我们的第一个卷积神经网络。我们使用了封装好的训练和测试函数,设置了随机种子以确保可重复性,并测量了训练时间。最终,我们的CNN模型在测试集上取得了不错的准确率,为后续的模型比较奠定了基础。

在下一讲中,我们将开始比较所有已训练模型的结果。

124:模型实验效果对比 📊

在本节课中,我们将学习如何对比不同机器学习模型的实验结果。我们将通过创建数据表格和可视化图表,来比较之前训练的多个模型的准确率、损失和训练时间,从而评估哪个模型表现最佳。


概述

在上一节中,我们训练了第一个卷积神经网络,并且从初步结果看,它似乎比基线模型有所提升。本节中,我们将通过系统化的对比来确认这一点。我们将使用 Pandas 创建数据表格,并绘制图表,直观地展示各个模型的性能差异。


创建对比数据表格

首先,我们需要导入 Pandas 库,并将之前保存的三个模型结果字典转换为数据表格进行对比。

import pandas as pd

compare_results = pd.DataFrame([model_0_results,
                                model_1_results,
                                model_2_results])

以下是三个模型的简要回顾:

  • 模型 0 (基线模型):仅包含两个线性层,准确率为 83.4%,损失为 0.47。
  • 模型 1:在 GPU 上训练并引入了非线性激活函数,但结果比基线模型更差。
  • 模型 2:采用了来自 CNN Explainer 网站的 Tiny VGG 架构,训练了我们的第一个卷积神经网络,取得了目前最好的结果。

考虑训练时间

在模型对比中,训练时间是一个重要的考量因素。有时,一个模型虽然准确率略低,但训练和推理速度快得多,这在某些应用场景下是可接受的。这被称为 性能与速度的权衡

我们将训练时间也加入到对比表格中:

compare_results["training_time"] = [total_train_time_model_0,
                                    total_train_time_model_1,
                                    total_train_time_model_2]

请注意,训练时间高度依赖于你使用的计算硬件(如 CPU、GPU 型号)。因此,如果你的训练时间与示例中显示的不同,只要性能指标相近,就无需担心。如果性能指标差异巨大,则应检查代码设置,例如随机种子是否正确。


可视化模型结果

数字表格虽然精确,但图表能提供更直观的对比。我们将绘制一个条形图来展示各模型的准确率。

以下是绘制水平条形图的代码:

compare_results.set_index("model_name")["test_acc"].plot(kind="barh")
plt.xlabel("Accuracy (%)")
plt.title("Model Accuracy Comparison on FashionMNIST")

通过这张图表,你可以清晰地看到哪个模型准确率最高。例如,你可以向他人展示:“在 FashionMNIST 数据集上,我们的卷积神经网络模型(模型 2)表现最佳,它是在 GPU 上训练的,架构参考了 CNN Explainer 网站。”


总结

本节课中,我们一起学习了如何系统化地对比机器学习实验的结果。我们使用 Pandas 整合了不同模型的准确率、损失和训练时间,并通过可视化图表直观地展示了性能差异。记住,在评估模型时,不仅要看最终准确率,还要考虑训练时间等实际因素。


下一步:进行可视化预测

目前我们看到的只是页面上的数字。既然我们的模型是基于计算机视觉数据训练的,那么其核心目的之一就是能够对图像进行预测并可视化结果。

在下一节课中,我们将使用表现最佳的模型——FashionMNIST Model V2,对测试集中的随机样本进行预测,并将预测结果以标题的形式标注在图像上。建议你先尝试自己实现这个功能,我们下节课再一起完成。

125:使用最优模型对随机测试样本预测 📊

在本节课中,我们将学习如何使用训练好的最优模型对随机选取的测试样本进行预测,并将预测结果可视化。我们将编写一个预测函数,处理数据,并将模型的原始输出转换为可读的预测标签。


模型性能回顾与硬件影响 ⚙️

上一节我们比较了不同模型的性能。我们尝试了三个实验:一个基础线性模型、一个带非线性激活函数的线性模型,以及一个卷积神经网络(Fashion MNIST模型V2)。从准确率角度看,卷积神经网络表现最佳,但其训练时间也最长。

需要强调的是,训练时间会因运行硬件不同而变化。我们在上一节讨论过这一点。在完成上一节后,我中断了工作,重新运行了之前编写的所有代码单元。你会发现,本节的训练时间与上一节的记录值有所不同。虽然不确定Google Colab后台具体使用的硬件,但请记住这一点。至少现在我们知道如何跟踪不同的变量,例如模型的训练时长和性能指标。


可视化预测:数据探索者的信条 👁️

是时候进行可视化了。让我们创建一个新标题:“制作与评估”。这是训练完机器学习模型后我最喜欢的步骤之一。

我们将遵循数据探索者的信条:可视化、可视化、再可视化。让我们编写一个名为 make_predictions 的函数。

def make_predictions(model: torch.nn.Module,
                     data: list,
                     device: torch.device = device):
    pred_probs = []
    model.eval()
    with torch.inference_mode():
        for sample in data:
            sample = torch.unsqueeze(sample, dim=0).to(device)
            pred_logit = model(sample)
            pred_prob = torch.softmax(pred_logit.squeeze(), dim=0)
            pred_probs.append(pred_prob.cpu())
    return torch.stack(pred_probs)

以下是该函数的关键步骤:

  1. 初始化列表:创建一个空列表 pred_probs 来存储预测概率。
  2. 模型评估模式:将模型设置为评估模式(model.eval()),因为进行预测时应使用此模式。
  3. 推理模式:启用 torch.inference_mode() 上下文管理器,因为预测(prediction)是推理(inference)的同义词。
  4. 遍历数据:对 data 列表中的每个样本进行循环处理。
  5. 准备样本:对单个图像样本使用 unsqueeze(0) 添加批次维度,并将其移动到目标设备。
  6. 前向传播:将样本输入模型,获得原始逻辑值(logits)。
  7. 计算概率:使用 torch.softmax() 函数将逻辑值转换为预测概率,并移除多余的维度。
  8. 转移数据:将预测概率移至CPU,因为Matplotlib库无法直接处理GPU数据。
  9. 堆叠结果:使用 torch.stack() 将列表中的所有预测概率合并为一个张量。

这只是一种实现方式,预测和可视化的方法有很多种,此处仅作示例。


准备随机测试样本 🎲

现在让我们在实战中尝试这个函数。首先导入 random 模块并设置随机种子以确保结果可复现。

import random
random.seed(42)

接下来,创建两个空列表,一个用于存储测试样本,另一个用于存储对应的真实标签。这样,在模型做出预测后,我们可以将其与真实标签进行比较。

test_samples = []
test_labels = []

我们将从整个测试数据集(而非测试数据加载器)中随机抽取9个样本。选择9是因为后续我们将创建一个3x3的绘图网格。

for sample, label in random.sample(list(test_data), k=9):
    test_samples.append(sample)
    test_labels.append(label)

让我们查看第一个样本的形状和图像。

print(f"First sample shape: {test_samples[0].shape}")
plt.imshow(test_samples[0].squeeze(), cmap="gray")
plt.title(class_names[test_labels[0]])
plt.show()

输出显示,第一个样本是一个形状为 [1, 28, 28] 的张量,其对应的标签是“凉鞋”。


使用最优模型进行预测 🔮

现在,使用我们之前创建的 make_predictions 函数,传入训练好的最优模型(model_2)和随机测试样本列表(test_samples),进行预测。

pred_probs = make_predictions(model=model_2,
                              data=test_samples)

查看前两个预测概率:

print(pred_probs[:2])

输出是每个样本对应10个类别的概率张量。为了与整数形式的真实标签(test_labels)进行公平比较,我们需要将这些概率转换为预测标签。


将预测概率转换为预测标签 🏷️

我们可以使用 argmax() 函数,获取每个样本预测概率中最大值所在的索引,该索引即为预测的类别标签。

pred_classes = pred_probs.argmax(dim=1)
print(pred_classes)

现在,pred_classes 中的预测标签与 test_labels 中的真实标签格式一致,便于后续比较。


总结与下节预告 📝

本节课我们一起学习了如何使用训练好的PyTorch模型对随机测试样本进行预测。我们编写了一个通用的预测函数,处理了数据准备和设备转移,并将模型的原始输出转换成了具体的类别标签。

在下一节中,我们将编写代码创建一个Matplotlib绘图函数,将九个随机样本、它们的真实标签以及模型的预测标签一起可视化显示出来,从而直观地评估模型的性能。如果你希望样本完全随机,可以注释掉设置随机种子的代码行。我将其设为42是为了确保你我运行代码时能抽取到相同的样本,便于结果对比。

让我们拭目以待下一节的可视化结果!

126:可视化最优模型的随机测试样本预测与评估 📊

在本节课中,我们将学习如何可视化我们训练好的模型在随机测试样本上的预测结果,并与真实标签进行比较。通过直观的图表,我们可以更深入地评估模型的性能,并发现潜在的问题。


上一节我们介绍了如何对测试数据进行预测并计算准确率。本节中,我们来看看如何将这些预测结果以图像的形式直观地展示出来。

我们将创建一个Matplotlib图表,用于绘制随机选择的测试样本图像,并在图像上方显示模型的预测标签和真实标签。为了快速判断预测是否正确,我们将根据预测结果的正误来改变标题文字的颜色。

以下是实现这一可视化功能的核心代码步骤:

  1. 创建图表:设置一个3x3的图表布局,用于展示9个随机样本。

    import matplotlib.pyplot as plt
    fig = plt.figure(figsize=(9, 9))
    rows, cols = 3, 3
    
  2. 遍历样本并绘制:对每个随机选择的测试样本,创建一个子图。

    for i, sample in enumerate(test_samples):
        # 创建子图
        plt.subplot(rows, cols, i+1)
        # 绘制图像,移除批次维度并设置为灰度图
        plt.imshow(sample.squeeze(), cmap="gray")
        plt.axis(False)
    
  3. 获取预测与真实标签:将模型输出的数字类别转换为可读的文本标签。

        # 获取预测标签
        pred_label = class_names[pred_classes[i]]
        # 获取真实标签
        truth_label = class_names[test_labels[i]]
    

  1. 设置动态颜色的标题:根据预测是否正确,将标题文字设置为绿色或红色。
        title_text = f"Pred: {pred_label} | Truth: {truth_label}"
        if pred_label == truth_label:
            plt.title(title_text, fontsize=10, color="green")  # 正确为绿色
        else:
            plt.title(title_text, fontsize=10, color="red")    # 错误为红色
    

运行上述代码后,我们得到了一个包含9个样本的图表。从结果来看,我们的模型表现相当出色,所有预测都是正确的。例如,“预测:凉鞋,真实:凉鞋”、“预测:裤子,真实:裤子”。


然而,为了更全面地评估模型,我们需要查看它在更多样本上的表现,特别是它犯错误的情况。因此,我们可以多次运行随机采样和可视化的代码。

在后续的随机抽样中,模型开始出现一些错误预测。例如,它将一件“外套”预测为“连衣裙”,或将一件“衬衫”预测为“T恤/上衣”。这些错误揭示了模型可能存在的混淆点,也提示我们数据集中某些类别的标签定义本身可能存在重叠或模糊之处(例如“T恤”和“衬衫”的区别)。

这种可视化方法的价值在于,它不仅能验证模型在数字指标(如准确率)上的表现,还能让我们直观地看到模型在哪些具体情况下会出错,从而为进一步的模型改进或数据清洗提供方向。


本节课中我们一起学习了如何可视化PyTorch模型在测试集上的预测结果。我们编写代码绘制了随机测试样本的图像,并对比了模型的预测标签与真实标签,通过颜色高亮显示了预测的正误。这个过程是探索性数据分析的重要一环,它能帮助我们更直观、更深入地理解模型的性能和行为,发现数字指标背后隐藏的细节问题。

在下一节课中,我们将引入另一种强大的评估工具——混淆矩阵,来系统性地分析模型在所有类别上的错误模式。

127:预测与混淆矩阵绘制库导入 📊

在本节课中,我们将学习如何评估机器学习模型,特别是通过可视化预测结果和使用混淆矩阵来深入理解模型的性能。我们将从对整个测试数据集进行预测开始,然后安装必要的库来创建和绘制混淆矩阵。


模型预测可视化

上一节我们介绍了如何对随机样本进行预测。本节中,我们来看看如何对整个测试数据集进行预测,以获得更全面的模型性能视图。

我们观察到,模型有时会对某些类别(例如T恤/上衣和衬衫)感到困惑。这种对模型预测的洞察也可能提示我们某些数据标签或许可以改进。

以下是进行批量预测的步骤:

  1. 将模型设置为评估模式。
  2. 使用 torch.inference_mode() 上下文管理器来禁用梯度计算,提高效率。
  3. 遍历测试数据加载器,对每个批次进行前向传播。
  4. 将模型的原始输出(logits)转换为预测概率,再转换为预测标签。
  5. 将所有批次的预测结果收集到一个张量中。
import torch
from tqdm.auto import tqdm

# 1. 创建空列表以存储预测
test_preds = []
# 2. 将模型设置为评估模式
model_2.eval()

# 3. 在推理模式下进行预测
with torch.inference_mode():
    for X, y in tqdm(test_dataloader, desc="Making predictions"):
        # 将数据发送到目标设备
        X, y = X.to(device), y.to(device)
        # 4. 前向传播
        y_logit = model_2(X)
        # 将logits转换为预测概率,再转换为标签
        y_pred = torch.softmax(y_logit, dim=1).argmax(dim=1)
        # 5. 将预测移到CPU并添加到列表
        test_preds.append(y_pred.cpu())

# 将预测列表连接成一个张量
test_preds_tensor = torch.cat(test_preds)

运行此代码后,我们将得到一个包含所有10000个测试样本预测结果的张量。


安装评估库

为了创建混淆矩阵,我们需要使用两个外部库:torchmetricsmlxtendtorchmetrics 提供了丰富的机器学习评估指标,而 mlxtend 包含了许多实用的辅助函数,例如绘制混淆矩阵。

由于 Google Colab 默认可能没有安装或没有所需版本的这些库,我们需要先进行检查和安装。

以下是安装和验证库的步骤:

# 尝试导入所需的库,如果失败则安装
try:
    import torchmetrics, mlxtend
    # 检查 mlxtend 版本是否 >= 0.19.0
    assert int(mlxtend.__version__.split('.')[1]) >= 19, "mlxtend version should be 0.19.0 or higher"
    print(f"mlxtend version: {mlxtend.__version__}")
except:
    # 安装 torchmetrics 并升级 mlxtend
    !pip install -q torchmetrics
    !pip install -q --upgrade mlxtend
    import torchmetrics, mlxtend
    print(f"mlxtend version: {mlxtend.__version__}")

注意:在 Google Colab 中运行安装命令后,可能需要重启运行时才能使新安装的库生效。如果遇到错误,请手动重启内核并重新运行必要的单元格。

安装完成后,我们就可以利用 torchmetrics 中的 ConfusionMatrix 类和 mlxtend.plotting 中的 plot_confusion_matrix 函数了。


总结

本节课中我们一起学习了模型评估的重要步骤。首先,我们编写了代码对整个测试数据集进行批量预测,这比随机抽样更能代表模型的整体性能。接着,我们介绍了如何安装和验证 torchmetricsmlxtend 这两个强大的库,为下一节课实际创建和绘制混淆矩阵做好了准备。

混淆矩阵是一种非常直观的工具,它能清晰地展示模型在各个类别上的预测情况,帮助我们识别模型容易混淆的类别,从而为进一步优化模型或数据集提供方向。

128:使用混淆矩阵评估最优模型预测 📊

在本节课中,我们将学习如何使用混淆矩阵来评估和可视化我们训练好的PyTorch模型在测试集上的预测性能。混淆矩阵是分类任务中一个非常强大的评估工具。


在上一节中,我们编写了代码来导入绘制混淆矩阵所需的一些额外库。同时,我们也对整个测试数据集进行了预测,得到了一个包含10,000个预测结果的张量。本节中,我们将使用混淆矩阵来对比这些预测结果与测试集中的真实标签。

以下是实现此目标的三个主要步骤:

  1. 准备必要的库。
  2. 创建混淆矩阵实例并计算。
  3. 绘制混淆矩阵。

我们已经完成了第一步,并为第二、三步安装了 torchmetricsmlxtend 库。现在,让我们继续后续步骤。

步骤二:创建混淆矩阵实例

首先,我们需要从已安装的库中导入必要的类。

# 导入混淆矩阵类和绘图函数
from torchmetrics import ConfusionMatrix
from mlxtend.plotting import plot_confusion_matrix

接下来,我们设置混淆矩阵实例,并将模型的预测结果与真实目标标签进行比较。这是模型评估的核心。

# 实例化混淆矩阵,参数为类别数量
confmat = ConfusionMatrix(num_classes=len(class_names))

# 计算混淆矩阵张量
confmat_tensor = confmat(preds=y_pred_tensor,
                         target=test_data.targets)

在这段代码中:

  • class_names 是我们数据集中所有类别的列表。
  • y_pred_tensor 是上一节中模型对整个测试集做出的预测张量。
  • test_data.targets 是测试数据集的真实标签。

步骤三:绘制混淆矩阵

现在,我们已经得到了一个包含数值的混淆矩阵张量。为了更直观地分析,我们使用 mlxtend 库将其绘制成图。

# 设置图形尺寸
fig, ax = plt.subplots(figsize=(10, 7))

# 绘制混淆矩阵
plot_confusion_matrix(conf_mat=confmat_tensor.numpy(), # matplotlib 需要 NumPy 数组
                      class_names=class_names,
                      figsize=(10, 7))

运行代码后,我们将看到一个可视化的混淆矩阵。理想情况下,一个完美的模型其混淆矩阵只有对角线(从左上到右下)有高亮值,其他位置都应为零,这表示预测标签与真实标签完全一致。

分析混淆矩阵

观察我们得到的混淆矩阵,可以看到一条非常明显的高亮对角线,这说明模型整体表现良好。然而,我们也可以发现一些预测错误:

例如,模型有时会将“衬衫”(Shirt)预测为“T恤/上衣”(T-shirt/top),反之亦然。这反映了我们在之前可视化单个预测时看到的情况。

另一个例子是,模型有时会将“运动鞋”(Sneaker)预测为“踝靴”(Ankle boot),混淆了两种不同的鞋类。

通过混淆矩阵,我们可以直观地检查模型所犯的错误是否在视觉上是合理的。例如,某些服装类别(如套头衫和外套)在图像上可能本身就非常相似,导致模型容易混淆。这提示我们,或许可以审视数据标签的区分度,或者考虑对模型进行进一步优化。


本节课总结

在本节课中,我们一起学习了如何使用混淆矩阵来深度评估分类模型。我们首先导入了 torchmetricsmlxtend 库,然后创建了混淆矩阵实例来计算预测与真实标签的对比结果,最后将其绘制成直观的图表进行分析。混淆矩阵是理解和改进分类模型性能不可或缺的工具。

我们已经完成了相当多的模型评估工作。根据我们的工作流程,接下来是时候保存并加载我们训练好的最佳模型了。让我们在下一节课中继续。

129:保存与加载最优性能模型 💾

在本节课中,我们将学习如何保存训练好的PyTorch模型,并在之后重新加载它进行验证和使用。这是将模型部署到实际应用中的关键一步。

概述

上一节我们利用torch.metrics创建了漂亮的混淆矩阵来评估模型性能。本节中,我们将学习如何保存表现最佳的模型,以便在其他地方使用,并验证保存和加载过程是否正确。

创建模型保存路径

首先,我们需要为模型文件创建一个保存目录。以下是具体步骤:

  • pathlib导入Path模块,用于处理文件路径。
  • 创建一个名为models的目录路径。
  • 使用.mkdir()方法创建目录,设置parents=Trueexist_ok=True参数确保目录存在且不会因重复创建而报错。
from pathlib import Path

# 创建模型保存目录
MODEL_PATH = Path("models")
MODEL_PATH.mkdir(parents=True, exist_ok=True)

# 定义模型保存的文件名和完整路径
MODEL_NAME = "03_pytorch_computer_vision_model_2.pth"
MODEL_SAVE_PATH = MODEL_PATH / MODEL_NAME

保存模型状态字典

我们保存的是模型的state_dict(),它包含了模型从数据中学到的所有参数(如权重和偏置)。这些参数最初是随机初始化的,经过训练后已更新为能代表训练图像的特征。

以下是保存模型的代码:

print(f"Saving model to: {MODEL_SAVE_PATH}")
torch.save(obj=model_2.state_dict(), f=MODEL_SAVE_PATH)

运行此代码后,模型文件将被保存到指定的models目录中。

加载已保存的模型

由于我们只保存了模型的state_dict,要重新加载它,需要先创建一个与原始模型结构完全相同的新实例。

以下是加载模型的步骤:

  1. 设置随机种子,确保新模型实例的初始随机状态一致。
  2. 使用与原始模型相同的类(FashionMNISTModelV2)和参数(输入形状、隐藏单元数、输出形状)实例化一个新模型。
  3. 使用torch.load()加载保存的state_dict文件。
  4. 将加载的state_dict载入到新模型实例中。
  5. 将模型发送到目标设备(如GPU)。
# 设置随机种子
torch.manual_seed(42)

# 实例化一个结构相同的新模型
loaded_model_2 = FashionMNISTModelV2(input_shape=1,
                                     hidden_units=10,
                                     output_shape=len(class_names))

# 加载保存的状态字典
loaded_model_2.load_state_dict(torch.load(f=MODEL_SAVE_PATH))

# 将模型发送到设备
loaded_model_2.to(device)

评估加载的模型

评估加载的模型与评估训练后的模型同样重要。这能确保模型被正确保存,在部署前验证其性能。

我们将使用之前定义的eval_model函数,在相同的测试数据集上评估加载的模型。

# 评估加载的模型
loaded_model_2_results = eval_model(model=loaded_model_2,
                                    data_loader=test_dataloader,
                                    loss_fn=loss_fn,
                                    accuracy_fn=accuracy_fn)

loaded_model_2_results

加载模型的评估结果(如测试损失和准确率)应该与原始模型model_2的结果非常接近。

验证结果一致性

我们可以通过编程方式检查两个模型的结果是否足够接近,而不仅仅是肉眼观察。

使用torch.isclose()函数可以比较两个张量在指定容差范围内是否接近。

# 检查原始模型与加载模型的损失值是否接近
torch.isclose(torch.tensor(model_2_results["model_loss"]),
              torch.tensor(loaded_model_2_results["model_loss"]),
              atol=1e-08) # 绝对容差

如果返回True,说明结果在容差范围内一致。如果结果差异较大(例如超过小数点后两三位),则应检查代码,确保模型保存正确且随机种子设置无误。可以调整atol(绝对容差)参数来定义“足够接近”的标准。

总结

本节课中,我们一起学习了PyTorch模型保存与加载的完整流程。我们首先创建了模型保存路径,然后保存了模型的状态字典。接着,我们实例化了一个结构相同的新模型并加载了保存的参数。最后,我们评估了加载的模型,并通过比较结果验证了保存和加载过程的正确性。这是将训练好的模型投入实际应用的关键步骤。

130:PyTorch 计算机视觉内容回顾与练习拓展 📚

在本节课中,我们将回顾在 PyTorch 计算机视觉部分共同完成的所有工作,并为你提供一系列练习和拓展资源,以巩固所学知识。


内容回顾

上一节我们完成了卷积神经网络的构建与评估。现在,让我们系统地回顾整个计算机视觉学习路径。

我们共同编写了大量的 PyTorch 计算机视觉代码。

我们从最顶部开始。我们查看了参考笔记本和在线书籍。

我们查看了 PyTorch 中的计算机视觉库,最主要的是 TorchVision。

然后我们获取了一个数据集,即 Fashion MNIST 数据集。实际上还有更多数据集可供查看。

我鼓励你尝试 torchvision.datasets 中的其他数据集,并使用我们在此完成的所有步骤在另一个数据集上进行尝试。

我们准备了数据加载器,将数据转换为批次。

我们构建了一个基线模型,这是机器学习中的重要一步。基线模型通常相对简单,它将作为一个基准,你可以通过各种实验尝试改进它。

我们随后使用模型零进行了预测,并对其进行了评估。

我们为预测计时,以查看在 GPU 上运行模型是否更快。我们了解到,对于相对较小的数据集,由于数据从 CPU 复制到 GPU 的开销,GPU 不一定会加速代码。

我们尝试了一个带有非线性的模型,发现它并没有真正改进我们的基线模型。

但随后我们引入了“重型武器”——一个复制了 CNN Explainer 网站的卷积神经网络。我们在这里花费了大量时间。

我鼓励你作为课外学习,反复回顾这部分内容。我自己在制作本节视频和代码材料时也经常参考它。

因此,请务必返回并查看 CNN Explainer 网站,以了解更多关于 CNN 幕后工作的信息。

我们使用纯 PyTorch 编写了一个卷积神经网络,这非常棒。

我们比较了不同实验的模型结果。

我们发现我们的卷积神经网络表现最佳,尽管训练时间稍长一些。我们还了解到,训练时间值肯定会因你使用的硬件而异,这是需要记住的一点。

我们使用最佳模型进行了评估并做出了随机预测。这是可视化模型预测的重要步骤,因为你可以获得评估指标,但直到你开始实际可视化正在发生的事情,你才能真正理解模型的想法。

我们使用两个不同的库(TorchMetrics 和 MLxtend)看到了混淆矩阵,这是评估分类模型的绝佳方式。

我们看到了如何将性能最佳的模型保存并加载到文件中,并确保保存模型的结果与我们在笔记本中训练的模型没有太大差异。


练习与实践

现在是时候了,我希望你练习所学的内容。这实际上非常令人兴奋,因为你已经完成了一个端到端的计算机视觉问题。

我准备了一些练习。如果你访问 LearnPyTorch.io 网站的第 03 节并向下滚动,你可以阅读所有这些内容。这些是我们刚刚用纯代码涵盖的所有材料。

这个笔记本中有很多有助于理解内容的注释。

以下是练习内容。所有练习都侧重于练习上述部分的代码。

我们有两个资源,我还整理了一些课外学习材料。

如果你想深入了解卷积神经网络幕后的工作原理,因为我们重点讲了很多代码,我强烈推荐 MIT 的深度学习计算机视觉入门讲座。

你可以花 10 分钟浏览 PyTorch Vision 库 TorchVision 中的不同选项。

在 TorchVision 模型库中查找最常见的卷积神经网络。

对于大量预训练的 PyTorch 计算机视觉模型,如果你更深入地研究计算机视觉,你可能会遇到 Torch Image Models 库,也称为 timm。但我会将其作为课外学习内容。

我将再次链接这个练习部分,它在 learnpytorch.io 的练习部分。

这里还有一个资源:一个练习模板笔记本。

以下是计算机视觉目前在工业中应用的三个领域。

这是在 PyTorch 深度学习仓库的 extras/exercises 中的第 3 号练习。我在这里为你提供了一些模板代码,供你填写这些不同的部分。

其中一些与代码相关,一些只是基于文本的,但它们都应该能够通过参考我们在这个笔记本中所学的内容来完成。

还有一个资源,如果我们回到 PyTorch 深度学习仓库,这里可能会在你看到时更新。

你总是可以通过访问“计算机视觉” -> “练习与课外学习”找到练习和课外学习材料。或者,如果我们进入 extras 文件夹,然后进入 solutions,我现在也开始为每个解决方案添加视频讲解。

这是我亲自完成每个练习并编写代码的过程。你会看到这些是未经编辑的视频,它们是一次长的直播录像。

我已经为 02、03 和 04 节录制了一些,当你观看此视频时,这里会有更多内容。

但如果你想了解我如何找出练习的解决方案,你可以观看这些视频并自己学习。但首先,我强烈建议你先自己尝试练习。

如果你遇到困难,可以参考这里的笔记本,参考 PyTorch 文档。最后,你可以查看我编写的可能解决方案。

所以,这里是第 3 号计算机视觉练习解决方案。


总结

恭喜你完成了 PyTorch 计算机视觉部分的学习。

我们共同回顾了从数据准备、模型构建、训练、评估到可视化的完整流程。通过练习和课外资源,你可以进一步巩固这些技能,并探索更广阔的计算机视觉世界。

我们将在下一节见面,我们将学习 PyTorch 自定义数据集。但先不剧透,我们很快再见。

131:自定义数据集概念与内容规划 🗂️

在本节课中,我们将学习如何使用 PyTorch 处理自定义数据集。我们将了解如何将你自己的数据(例如图像)加载到 PyTorch 中,以便构建和训练深度学习模型。

概述

到目前为止,我们已经使用过多个 PyTorch 内置的数据集(如 Fashion MNIST)来构建神经网络。然而,你可能会想:“我拥有自己的数据集,或者正在处理自己的问题,能否用 PyTorch 构建模型来预测这个数据集?”答案是肯定的。但你需要完成一些预处理步骤,以使你的数据集与 PyTorch 兼容。这正是我们本节课程要涵盖的内容。

获取帮助的途径

在学习过程中,遇到问题是很正常的。以下是几个获取帮助的有效途径:

  • 跟随代码:尽可能跟随课程代码一起编写。
  • 尝试运行:如果遇到疑问,尝试运行代码。记住我们的格言:“如果存疑,运行代码”。
  • 查阅文档:在 Google Colab 中,你可以按 Shift + Command + Space(Windows 系统可能是 Control)来查看函数文档字符串。
  • 搜索资源:如果仍然困惑,可以搜索相关资源。你可能会经常用到 Stack Overflow 或优秀的 PyTorch 官方文档
  • 回顾代码:当然,你也可以回头检查自己的代码。
  • 发起讨论:如果以上方法都无法解决问题,你可以在课程的 GitHub 讨论页面提问。你可以选择类别(如“Q&A”),注明视频编号,描述问题并附上相关代码。

本课程的所有资源都可以在 learnpytorch.io 找到。我们目前处于第 4 节。该网站提供了本节所有材料的在线书籍版本。此外,GitHub 仓库中也提供了相同的 Jupyter Notebook(例如 pytorch_custom_datasets.ipynb),如果你遇到困难,可以将其作为参考。

什么是自定义数据集?

我们已经使用 PyTorch 在多个数据集上构建了不少深度学习神经网络。但当你拥有自己的数据时,就需要创建一个自定义数据集。这涉及到一系列预处理步骤,以使你的数据能够被 PyTorch 的模型使用。

PyTorch 领域库简介

为了高效处理不同类型的数据,PyTorch 提供了多个领域特定的库。了解这些库有助于你根据数据类型选择合适的工具。

  • torchvision:用于计算机视觉任务,例如图像分类(如区分披萨、牛排或寿司)。
  • torchtext:用于自然语言处理任务,例如情感分析(判断评论是正面还是负面)。
  • torchaudio:用于音频处理任务,例如歌曲分类。
  • torchrec:用于推荐系统任务。

每个领域库都包含一个 datasets 模块,它既提供了加载预构建数据集(如 Fashion MNIST)的功能,也提供了加载你自己数据集的函数。例如,对于视觉数据,你应该查看 torchvision.datasets;对于文本数据,则查看 torchtext.datasets

此外,还有一个处于测试阶段的 torchdata 库,它包含了许多用于加载数据的高级辅助函数,未来会不断更新以支持更多数据源。

本节目标:构建 Food Vision Mini 🍕🥩🍣

我们将通过一个具体项目来学习如何处理自定义数据。我们的目标是构建一个名为 Food Vision Mini 的图像分类模型。

我们将从 Food 101 数据集中加载披萨、寿司和牛排的图片,构建一个能够对这些食物图像进行分类的模型。这类似于一个食物识别应用(例如 nutrifai.app)背后的核心模型。

具体来说,我们需要编写代码来加载这些食物图像,从而创建我们自己的自定义数据集,用于训练 Food Vision Mini 模型。

遵循 PyTorch 工作流

我们将遵循之前多次使用的 PyTorch 标准工作流来构建这个项目:

  1. 获取数据:学习如何加载自定义数据集,而非 PyTorch 内置数据集。
  2. 构建模型:构建一个适用于我们自定义数据的模型。
  3. 训练模型:完成训练所需的所有步骤,包括选择损失函数和优化器,编写训练循环。
  4. 评估模型:评估模型的性能。
  5. 实验改进:通过实验(例如数据增强)来提升模型效果。
  6. 保存与加载:保存训练好的模型并重新加载。
  7. 进行预测:使用训练好的模型对我们自己的自定义数据(即不在训练集或测试集中的新数据)进行预测,这是模型部署中非常关键的一步。

本节涵盖内容

以下是本节课程将详细讲解的要点:

  • 获取 PyTorch 自定义数据集:学习数据加载的基础。
  • 熟悉数据:准备和可视化数据,做到“与数据融为一体”。
  • 数据转换:学习如何对数据进行转换,使其适用于模型训练。
  • 加载自定义数据:使用 PyTorch 内置函数和我们自己编写的自定义函数来加载数据。
  • 构建计算机视觉模型:构建 Food Vision Mini 模型,实现对披萨、牛排和寿司图像的多类别分类。
  • 对比实验:比较使用数据增强和不使用数据增强的模型性能。
  • 自定义预测:学习如何对全新的自定义数据进行预测。

我们将通过编写大量代码来实践这些概念。现在,让我们进入 Google Colab,开始编码吧!

总结

在本节课中,我们一起学习了自定义数据集的核心概念。我们了解了在 PyTorch 中处理自己数据的重要性,介绍了 PyTorch 的不同领域库(如 torchvision, torchtext),并明确了本节课程的目标:构建一个 Food Vision Mini 图像分类模型。我们还回顾了将遵循的 PyTorch 工作流以及本节要涵盖的具体知识点。接下来,我们将动手实践,学习如何将自定义图像数据加载到 PyTorch 中。

132:自定义数据集 🍲

在本节课中,我们将学习如何将你自己的数据导入PyTorch,以便构建模型来解决你感兴趣的问题。我们将重点介绍如何创建自定义数据集,这是处理非标准数据的关键步骤。


上一节我们介绍了课程概述,本节中我们来看看如何设置开发环境并导入必要的库。

导入PyTorch与配置设备无关代码 ⚙️

我们将从导入PyTorch核心库开始,并设置设备无关的代码。这是一种最佳实践,它能让我们的代码自动在GPU(如果可用)或CPU上运行,从而提升灵活性和性能。

首先,我们导入torchnn模块。

import torch
from torch import nn

接下来,我们检查当前使用的PyTorch版本。本课程要求PyTorch版本为1.10.0或更高。

print(torch.__version__)

然后,我们配置设备无关的代码。其核心思想是:如果系统有可用的CUDA(GPU),则使用它;否则,回退到CPU。

以下是实现此功能的代码公式:

device = "cuda" if torch.cuda.is_available() else "cpu"

在Google Colab等环境中,默认运行时可能不使用GPU。因此,我们需要手动启用GPU加速。

操作步骤如下:

  1. 点击顶部菜单栏的 Runtime(运行时)。
  2. 选择 Change runtime type(更改运行时类型)。
  3. Hardware accelerator(硬件加速器)下拉菜单中,选择 GPU
  4. 点击 Save(保存)。

完成设置后,我们可以验证GPU是否可用及其型号。

print(f"Using device: {device}")
if device == "cuda":
    print(f"GPU型号: {torch.cuda.get_device_name(0)}")


本节课中我们一起学习了如何导入PyTorch并设置设备无关的代码。这是构建任何PyTorch项目的标准起点,它确保了我们的模型和数据能够利用最佳的可用硬件资源进行计算。在下一节,我们将获取一些数据,正式开始构建我们的自定义数据集。

133:下载披萨牛排寿司图像自定义数据集 🍕🥩🍣

在本节课中,我们将学习如何下载一个自定义的图像数据集,为后续构建计算机视觉模型做准备。我们将使用一个包含披萨、牛排和寿司三类食物图像的子集,该子集源自Food 101数据集。

概述

上一节我们介绍了自定义数据集的概念。本节中,我们来看看如何实际获取数据。我们将编写代码从GitHub下载一个压缩文件,其中包含我们需要的三类食物图像,并将其解压到本地目录中。

获取数据

正如上一视频所述,没有数据就无法处理自定义数据集。因此,我们需要获取一些数据。我们计划构建一个名为“Food Vision Mini”的模型,所以需要获取食物图像。

TorchVision数据集包含许多内置数据集,其中之一就是Food 101数据集。Food 101数据集包含101个不同食物类别,共有101,000张图像,是一个相当大的数据集。每个类别提供250张经过人工审核的测试图像,以及750张训练图像。

为了便于练习,我创建了该数据集的一个较小子集。我建议你在处理自己的问题时也采用相同策略:从小规模开始,必要时再扩大。我将类别数量减少到三个,图像数量减少到原数据集的10%。你可以任意缩减规模,但我认为三个类别和10%的数据量足以开始。如果模型运行良好,你可以自行扩大规模。

我想展示一下用于创建此数据集的笔记本。作为课外学习,你可以查看这个笔记本。在extras/04_custom_data_creation中,记录了我如何创建这个数据子集。我以自定义图像数据集或图像分类的风格创建了它。我们有一个顶级文件夹pizza_steak_sushi,其中包含带有披萨、牛排和寿司图像的训练目录和测试目录。你可以查看该笔记本了解其创建过程。

现在,我们将编写一些代码来获取这个数据集。我创建的较小版本位于PyTorch深度学习代码库的data目录下,文件名为pizza_steak_sushi.zip

编写下载代码

我们的数据是Food 101数据集的子集。Food 101数据集始于101个不同的食物类别。我们当然可以为101个类别构建计算机视觉模型,但我们将从小规模开始。我们的数据集始于三类食物,且仅包含10%的图像。每个类别大约有75张训练图像和25张测试图像。

这样做是因为在开始机器学习项目时,先在小规模上尝试,必要时再扩大规模,这一点很重要。其核心目的是加速实验迭代速度。如果一开始就尝试在100,000张图像上训练,模型每次训练可能需要半小时。在开始时,我们希望提高实验速率。

以下是下载数据的步骤:

首先,导入必要的库。我们将使用requests从GitHub请求数据,使用zipfile处理压缩文件,并使用pathlib处理文件路径。

import requests
import zipfile
from pathlib import Path

接下来,设置数据文件夹的路径。我通常喜欢创建一个名为data的文件夹来存储项目中的所有数据。

data_path = Path("data")
image_path = data_path / "pizza_steak_sushi"

然后,检查图像文件夹是否存在。如果不存在,则创建它并下载数据。

if image_path.is_dir():
    print(f"{image_path} 目录已存在,跳过下载。")
else:
    print(f"{image_path} 不存在,正在创建...")
    image_path.mkdir(parents=True, exist_ok=True)

现在,下载披萨、牛排和寿司数据。我们将使用requests.get从GitHub获取压缩文件,并将其写入本地。

with open(data_path / "pizza_steak_sushi.zip", "wb") as f:
    print("正在下载披萨、牛排、寿司数据...")
    request = requests.get("https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip")
    f.write(request.content)

下载完成后,我们需要解压这个ZIP文件。

with zipfile.ZipFile(data_path / "pizza_steak_sushi.zip", "r") as zip_ref:
    print("正在解压披萨、牛排、寿司数据...")
    zip_ref.extractall(image_path)

请注意,从GitHub下载文件时,必须使用原始文件链接(URL中包含raw),而不是blob链接,否则会导致下载错误。

运行上述代码后,数据将被下载并解压到data/pizza_steak_sushi目录中。该目录下应包含traintest子文件夹,每个子文件夹中又包含pizzasteaksushi三个类别的图像。

总结

本节课中我们一起学习了如何下载自定义图像数据集。我们编写了代码从GitHub获取一个包含披萨、牛排和寿司图像的压缩文件,并将其解压到本地目录。这个过程的核心步骤——设置路径、检查目录、下载文件和解压——是处理自定义数据时的通用模式。无论你的数据存储在何处,编写代码将其加载到PyTorch中的思路都是相似的。在下一节中,我们将探索刚刚下载的数据。

134:数据探索(第一部分):数据格式解析 📊

在本节课中,我们将学习如何探索和分析已下载的自定义数据集。我们将通过编写代码来遍历目录结构,理解数据的组织格式,并了解标准图像分类数据集的常见布局。


在上一节中,我们编写了代码来下载一个自定义数据集。现在,让我们开始探索这些数据。

与数据融为一体

数据准备和探索是机器学习项目中至关重要的一步。正如一句名言所说:“如果我有八个小时来构建一个机器学习模型,我会花前六个小时准备我的数据集。” 这正是我们现在要做的。

既然我们已经下载了一些数据,让我们开始探索它。我们将编写一些代码来遍历每个目录。

以下是遍历目录并返回其内容的辅助函数:

import os

def walk_through_dir(dir_path):
    """
    遍历目录路径,返回其内容。
    """
    for dirpath, dirnames, filenames in os.walk(dir_path):
        print(f"在 `{dirpath}` 中有 {len(dirnames)} 个目录和 {len(filenames)} 个图像。")

现在,让我们使用这个函数来探索我们的数据目录:

image_path = "data/pizza_steak_sushi"
walk_through_dir(image_path)

运行上述代码后,我们将看到类似以下的输出:

在 `data/pizza_steak_sushi` 中有 2 个目录和 0 个图像。
在 `data/pizza_steak_sushi/test` 中有 3 个目录和 0 个图像。
在 `data/pizza_steak_sushi/test/steak` 中有 0 个目录和 19 个图像。
...
在 `data/pizza_steak_sushi/train` 中有 3 个目录和 0 个图像。
在 `data/pizza_steak_sushi/train/steak` 中有 0 个目录和 75 个图像。
...

这个输出告诉我们,数据被组织在 traintest 文件夹中,每个文件夹内又包含 pizzasteaksushi 三个子文件夹。每个子文件夹中存放着对应类别的图像。

标准图像分类数据格式

我们数据集的这种组织方式并非偶然。它是一种广泛使用的标准图像分类数据格式。

以下是这种格式的典型结构:

data/
├── train/
│   ├── class_1/
│   │   ├── image_001.jpg
│   │   └── image_002.jpg
│   └── class_2/
│       ├── image_001.jpg
│       └── image_002.jpg
└── test/
    ├── class_1/
    │   ├── image_003.jpg
    │   └── image_004.jpg
    └── class_2/
        ├── image_003.jpg
        └── image_004.jpg

在我们的案例中,class_1class_2 等被替换为了 pizzasteaksushi。这种格式的优势在于,图像的标签(即类别)直接由其所在的文件夹名称决定。这使得数据加载过程变得非常直观和高效。

许多深度学习框架(如 PyTorch 的 torchvision)都内置了能够自动处理这种格式数据集的加载器。例如,torchvision.datasets.ImageFolder 就是为此设计的。

为了后续处理,我们可以定义训练和测试数据的路径:

train_dir = "data/pizza_steak_sushi/train"
test_dir = "data/pizza_steak_sushi/test"

可视化数据

对于计算机视觉问题,除了查看文件结构,另一个重要的探索步骤是可视化图像本身。我们之前可以通过点击文件来查看,但让我们用代码来实现它,这样更便于自动化处理。

我们将在下一节中详细学习如何用代码加载和显示图像。


本节课中,我们一起学习了如何探索数据集的目录结构,并理解了标准图像分类数据格式的组织方式。我们编写了遍历目录的代码,并明确了训练和测试数据的路径。在下一节,我们将进一步学习如何可视化这些图像数据。

135:数据探索(第二部分)随机图像可视化 🖼️

概述

在本节课中,我们将继续探索数据,学习如何从数据集中随机选择并可视化图像。我们将使用Python的random模块和PIL(Python Imaging Library)库来实现这一目标,并了解标准图像分类数据集的目录结构。


标准图像分类数据结构

上一节我们介绍了数据的基本情况,本节中我们来看看如何通过代码探索具体的图像样本。

标准的图像分类数据集通常按以下结构组织:

data/
├── train/
│   ├── class_1/
│   │   ├── image1.jpg
│   │   └── image2.jpg
│   └── class_2/
│       ├── image1.jpg
│       └── image2.jpg
└── test/
    ├── class_1/
    └── class_2/

例如,如果你想创建一个猫狗分类数据集,可以这样组织:

  • train/dog/ - 存放所有训练用的狗图片
  • train/cat/ - 存放所有训练用的猫图片
  • test/dog/ - 存放所有测试用的狗图片
  • `test/cat/ - 存放所有测试用的猫图片

我们的核心目标是将这些文件中的数据转换为PyTorch能够处理的张量(tensor)。但在转换之前,我们需要先熟悉数据本身。


随机可视化图像步骤

为了更深入地了解数据,我们将编写代码随机选择并显示一张图像。以下是实现此功能的主要步骤:

  1. 获取所有图像路径:使用pathlib库收集数据集中所有图像的路径。
  2. 随机选择图像路径:使用Python的random.choice()函数从路径列表中随机选取一个。
  3. 提取图像类别名称:从图像路径中提取其所在目录的名称,该名称即为图像的类别标签(例如“pizza”)。
  4. 打开图像:使用PIL(或称为Pillow)库打开选中的图像文件。
  5. 显示图像与元数据:打印图像的路径、类别以及尺寸(高度和宽度)等信息,并显示图像本身。

代码实现详解

现在,让我们按照上述步骤编写代码。首先导入必要的库。

import random
from pathlib import Path
from PIL import Image

# 设置随机种子以确保结果可复现
random.seed(42)

1. 获取所有图像路径

我们使用pathlib.Path对象的glob方法来匹配并收集所有符合特定模式的文件路径。模式*/ *.jpg表示:匹配任意一级子目录下的任意名称的.jpg文件。

# 定义数据路径(请根据你的实际路径修改)
data_path = Path("data/pizza_steak_sushi")

# 使用glob获取所有图像路径
image_path_list = list(data_path.glob("*/*/*.jpg"))
print(f"找到 {len(image_path_list)} 张图像。")

2. 随机选择图像路径

利用random.choice函数从路径列表中随机选取一个。

# 随机选择一个图像路径
random_image_path = random.choice(image_path_list)
print(f"随机选择的图像路径: {random_image_path}")

3. 提取图像类别名称

图像所在的父目录名称就是其类别。我们可以使用Path对象的.parent属性获取父目录,再用.stem属性获取目录名(不含后缀)。

# 从路径中提取类别名称
image_class = random_image_path.parent.stem
print(f"图像类别: {image_class}")

4. 打开图像并获取元数据

使用PIL.Image.open()打开图像,然后可以访问其尺寸等属性。

# 使用PIL打开图像
img = Image.open(random_image_path)

# 打印图像元数据
print(f"图像高度: {img.height}")
print(f"图像宽度: {img.width}")

# 显示图像
img.show()

运行示例

运行上述代码,你可能会看到类似以下的输出,并且会弹出一个图像窗口:

找到 225 张图像。
随机选择的图像路径: data/pizza_steak_sushi/test/sushi/592799.jpg
图像类别: sushi
图像高度: 512
图像宽度: 512

每次运行(在固定随机种子的情况下)都会看到同一张寿司图片。如果注释掉random.seed(42)这一行,则每次都会随机显示不同的图片,例如披萨或牛排。

通过多次随机可视化图像,你可以对数据集的多样性、图像质量以及可能存在的标签错误有一个直观的感受。这是构建可靠机器学习模型前的重要一步。


挑战任务

在下一节课之前,尝试完成一个小挑战:

使用Matplotlib库来可视化随机图像。

你的任务是编写一段类似的代码,但最终使用matplotlib.pyplot(通常导入为plt)来显示图像,而不是使用PIL.show()方法。这将是下一节课我们将要一起完成的内容。


总结

本节课中我们一起学习了:

  1. 回顾了标准图像分类数据集的目录结构。
  2. 掌握了使用pathlibglob模式获取所有文件路径的方法。
  3. 实践了利用random模块进行随机抽样以探索数据。
  4. 学会了使用PIL库打开图像、读取基本元数据并显示。
  5. 理解了在建模前随机检查数据样本的重要性。

通过手动或编码方式浏览数据,能帮助你建立对数据的直觉,并为后续将数据转换为张量并输入模型做好准备。

136:数据探索(第三部分):Matplotlib图像可视化 📊

在本节课中,我们将学习如何使用Matplotlib库来可视化图像数据。我们将把PIL库打开的图像转换为NumPy数组,并使用Matplotlib进行显示,同时关注图像的形状信息,为后续转换为PyTorch张量做准备。


上一节我们介绍了使用PIL库查看图像。本节中,我们来看看如何使用Matplotlib进行图像可视化。

Matplotlib是数据科学中最基础的库之一,掌握其图像绘制方法非常重要。为了使用Matplotlib,我们需要先将图像转换为NumPy数组。

以下是使用Matplotlib绘制图像的步骤:

  1. 导入必要的库:我们需要导入Matplotlib和NumPy。
  2. 将图像转换为数组:使用NumPy的asarray方法。
  3. 绘制图像:使用Matplotlib的imshow函数。
  4. 添加信息:为图像添加标题,显示其类别和形状。

让我们通过代码来实现这个过程。

import matplotlib.pyplot as plt
import numpy as np

# 假设 `image` 是一个已经用PIL打开的Image对象
image_as_array = np.asarray(image)

plt.figure(figsize=(10, 7))
plt.imshow(image_as_array)
plt.title(f"{image_class} | Shape: {image_as_array.shape}")
plt.axis('off')
plt.show()

运行上述代码后,我们可以看到图像被成功显示。标题中包含了图像的类别和形状。例如,一个图像的形状可能是 (512, 306, 3)

这个形状信息非常关键。它表示图像的高度是512像素,宽度是306像素,并且有3个颜色通道(红、绿、蓝)。这种格式被称为 “通道在后”(channels-last),即形状为 (高度, 宽度, 通道数)

然而,PyTorch的默认张量格式是 “通道在前”(channels-first),即 (通道数, 高度, 宽度)。了解数据形状与框架要求之间的差异,是避免后续机器学习模型出现形状不匹配错误的重要一步。


现在,让我们将这个方法应用到另一张图像上,以巩固理解。

遵循相同的步骤,我们可以可视化数据集中不同的图像。例如,另一张披萨图像的形状可能是 (512, 512, 3)。通过随机查看多个样本,我们能更好地理解数据集的多样性,例如注意到图像的高度和宽度可能各不相同,但颜色通道数通常都是3。

这种探索方式不仅适用于图像,也适用于其他类型的数据,如查看文本样本或聆听音频片段。


本节课中,我们一起学习了使用Matplotlib可视化图像数据。我们掌握了将PIL图像转换为NumPy数组并用Matplotlib显示的方法,并重点理解了图像形状 (高度, 宽度, 通道数) 的含义及其与PyTorch默认格式 (通道数, 高度, 宽度) 的区别。

现在我们已经对数据有了直观的认识。在下一节视频中,我们将开始编写代码,把文件夹中的所有图像都转换为PyTorch张量,为构建自定义数据集做准备。

137:数据转换(第一部分)🎨

概述

在本节课中,我们将学习如何将图像数据转换为PyTorch能够处理的张量格式。我们将使用torchvision.transforms模块来创建数据转换流程,为后续构建自定义数据集和数据加载器做准备。


数据转换的必要性

上一节我们介绍了如何将图像转换为NumPy数组。本节中我们来看看如何将自定义数据集中的图像转换为PyTorch张量。

在使用PyTorch处理图像数据之前,我们需要完成两个关键步骤:

  1. 将目标数据转换为张量格式。
  2. 将其转换为torch.utils.data.Dataset

回忆之前的课程,我们使用Dataset来容纳所有张量格式的数据,然后将其转换为torch.utils.data.DataLoader。数据加载器会创建我们数据集的迭代器或批处理版本。

简而言之,我们将使用数据集数据加载器

在PyTorch文档中,无论是torchvisiontorchaudio还是torchtext,创建数据集的方法都类似。它们通常提供一个transform参数,允许我们在加载数据时对数据样本应用转换。


使用torchvision.transforms转换数据

为了更好地理解,我们将通过实例来学习。首先,重新导入我们将要使用的主要库。

from torch.utils.data import DataLoader
from torchvision import datasets, transforms

接下来,创建一个图像转换流程。主要目标是将JPEG图像转换为张量表示。

以下是创建组合转换的方法:

data_transform = transforms.Compose([
    transforms.Resize((64, 64)),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.ToTensor()
])

以下是每个转换步骤的说明:

  1. 调整大小 (Resize((64, 64))): 将图像尺寸统一调整为64x64像素。这有助于后续使用特定的计算机视觉模型(如之前章节提到的Tiny VGG架构)。
  2. 随机水平翻转 (RandomHorizontalFlip(p=0.5)): 这是一种数据增强技术,以50%的概率随机水平翻转图像,可以人工增加数据集的多样性。
  3. 转换为张量 (ToTensor()): 这是核心转换。它将PIL图像或NumPy数组(数值范围0-255)转换为一个形状为(颜色通道, 高度, 宽度)、数值范围在0到1之间的torch.FloatTensor

现在,我们可以将单张图像通过这个转换流程:

transformed_image = data_transform(original_pil_image)
print(transformed_image.shape)  # 输出: torch.Size([3, 64, 64])
print(transformed_image.dtype)  # 输出: torch.float32

可以看到,我们成功地将单张图像转换为了一个形状为[3, 64, 64]torch.float32张量。通过修改Resize的参数,我们可以轻松地将图像调整为任何模型所需的输入尺寸,例如常见的224x224。


总结

本节课我们一起学习了数据转换的基础知识。我们介绍了使用torchvision.transforms.Compose创建转换流水线的方法,并实现了调整图像大小、数据增强以及最终转换为PyTorch张量的关键步骤。现在,我们已经掌握了将单张图像转换为模型可读格式的方法。

在下一节中,我们将编写可视化代码,对比原始图像与转换后图像的效果,并进一步学习如何将转换流程应用到整个数据文件夹中的所有图像上。

138:数据转换(第二部分)可视化转换后的图像 📊

在本节课中,我们将学习如何可视化经过数据转换(transforms)处理后的图像。我们将编写一个函数,用于随机选择图像,应用我们之前定义的转换流程,并并排对比原始图像与转换后的图像,以直观理解转换的效果。


概述

上一节我们介绍了如何使用 torchvision.transforms 创建数据转换流程。本节中,我们来看看如何将这些转换应用到图像上并进行可视化,这是数据探索过程中的关键一步。

探索 torchvision.transforms 文档

如果你想查找关于 torchvision.transforms 的更多文档,可以访问其官方文档页面。该模块提供了大量常见的图像转换和增强方法。

以下是 torchvision.transforms 的核心作用:

  • 功能:提供一系列常见的图像变换。
  • 组合方式:可以使用 Compose 将它们链式组合在一起,这正是我们之前所做的。

文档中包含了许多不同的转换,包括一些数据增强(Data Augmentation)方法。如果你想直观地查看这些转换的效果,建议查阅“转换图示”部分。不过,让我们先编写代码来可视化我们自己的转换流程。

编写可视化函数

我们将创建一个函数,用于随机选择图像路径,加载图像,应用转换,并绘制原始图像与转换后图像的对比图。

以下是该函数的核心步骤:

  1. 设置随机种子:为了结果可复现,可以设置随机种子。
  2. 随机采样图像路径:从所有图像路径列表中随机选择指定数量的路径。
  3. 循环处理每个图像
    • 使用 PIL 库打开原始图像。
    • 应用转换流程得到转换后的图像张量。
    • 使用 Matplotlib 创建子图,左侧显示原始图像,右侧显示转换后图像。
    • 注意调整张量的维度顺序以适配 Matplotlib 的显示要求。

以下是实现该功能的代码:

import random
from pathlib import Path
import matplotlib.pyplot as plt
from PIL import Image
import torch

def plot_transformed_images(image_paths: list,
                            transform,
                            n: int = 3,
                            seed: int = None):
    """
    从图像路径列表中随机选择图像,加载并转换它们,然后绘制原始图像与转换后版本的对比图。

    参数:
        image_paths (list): 图像文件路径的列表。
        transform (torchvision.transforms): 要应用的 PyTorch 图像转换流程。
        n (int): 要绘制的图像数量。
        seed (int): 随机种子,用于结果可复现。
    """
    # 设置随机种子
    if seed:
        random.seed(seed)

    # 随机选择图像路径
    random_image_paths = random.sample(image_paths, k=n)

    # 遍历每个选中的图像路径
    for image_path in random_image_paths:
        # 使用 PIL 打开原始图像
        with Image.open(image_path) as f:
            # 应用转换。注意:transform 会将图像转换为 PyTorch 张量。
            transformed_image = transform(f).permute(1, 2, 0) # 注意维度重排

            # 创建对比图
            fig, ax = plt.subplots(1, 2)
            # 绘制原始图像
            ax[0].imshow(f)
            ax[0].set_title(f"Original\nSize: {f.size}")
            ax[0].axis("off")

            # 绘制转换后的图像
            ax[1].imshow(transformed_image) # transformed_image 现在是 (H, W, C)
            ax[1].set_title(f"Transformed\nShape: {transformed_image.shape}")
            ax[1].axis("off")

            # 设置整个图的标题为图像类别
            fig.suptitle(f"Class: {Path(image_path).parent.stem}", fontsize=16)

    plt.show()

代码关键点说明

  • transform(f):将 PIL 图像 f 送入我们定义的转换流程(如 data_transform),输出是一个 PyTorch 张量。
  • .permute(1, 2, 0):这是至关重要的步骤。torchvision.transforms 通常输出形状为 (C, H, W)(通道在前)的张量,但 Matplotlib 的 imshow() 函数期望输入形状为 (H, W, C)(通道在后)。.permute(1, 2, 0) 将维度顺序从 (0, 1, 2) 调整为 (1, 2, 0),即把通道维度移到最后。

运行可视化函数

现在,让我们使用之前创建的 image_path_listdata_transform 来运行这个函数。

# 假设 image_path_list 和 data_transform 已定义
plot_transformed_images(image_paths=image_path_list,
                        transform=data_transform,
                        n=3,
                        seed=42)

运行上述代码后,你将看到类似下图的输出:

分析可视化结果

观察生成的对比图,我们可以得出以下结论:

  1. 图像尺寸变化:原始图像尺寸较大(例如 512x512),经过 Resize((64, 64)) 转换后,图像变为 64x64 像素。这会导致图像看起来更像素化,因为信息量减少了。

    • 为何这样做?:较小的图像意味着数据量更少,模型训练和推理速度会更快。但这也可能因为信息丢失而影响模型性能。图像尺寸是一个可以调整的超参数。
  2. 数据增强效果:由于转换流程中包含了 RandomHorizontalFlip(p=0.5),部分图像(如示例中的牛排和寿司)被水平翻转了。这是一种数据增强技术,可以人为增加训练数据的多样性,帮助模型学习更通用的特征,而不是记忆特定的图像方向。

  3. 张量格式:转换后的图像已经是 PyTorch 张量格式(torch.Tensor),这是模型直接需要的输入格式。

更多转换与数据增强

torchvision.transforms 提供了丰富的转换选项,远不止我们使用的 ResizeRandomHorizontalFlip

以下是你可以探索的其他转换类型:

  • 裁剪CenterCrop, RandomCrop, FiveCrop
  • 颜色变换ColorJitter, Grayscale, RandomAdjustSharpness
  • 几何变换RandomRotation, RandomAffine
  • 模糊与噪声GaussianBlur

建议查阅 torchvision.transforms 的官方文档和“转换图示”,以直观了解每种转换的效果。这将是你本节课的延伸学习内容。

本节总结

本节课中我们一起学习了如何可视化经过 torchvision.transforms 处理后的图像。

我们主要完成了以下工作:

  1. 回顾了 torchvision.transforms 的作用和文档资源。
  2. 编写了 plot_transformed_images 函数,用于并排对比原始图像与转换后图像。
  3. 理解了转换中关键的张量维度重排操作(.permute),以适配 Matplotlib 的显示。
  4. 通过可视化结果,分析了 ResizeRandomHorizontalFlip 转换的具体效果及其对模型训练的意义。
  5. 简要介绍了其他可用的图像转换与数据增强方法。

现在,我们已经确认了数据转换流程能按预期工作。在下一节中,我们将使用这个 data_transform,借助 torchvision.datasets.ImageFolder 来一次性加载整个数据集,为模型训练做好准备。


核心概念公式/代码回顾

  • 转换流程定义:data_transform = transforms.Compose([...])
  • 维度重排(通道后置):transformed_image.permute(1, 2, 0)
  • 可视化函数调用:plot_transformed_images(image_paths, transform, n=3, seed=42)

139:使用 ImageFolder 加载图像并转换为张量 📸➡️🔢

概述

在本节课中,我们将学习如何使用 PyTorch 的 torchvision.datasets.ImageFolder 类,将存储在标准图像分类格式文件夹中的图像数据加载并转换为模型所需的张量格式。我们将结合之前学到的数据变换(transforms)知识,创建一个可直接用于训练的数据集。


上一节我们介绍了如何使用 transforms 对图像进行预处理和转换为张量。本节中,我们来看看如何利用 PyTorch 内置的工具,自动加载整个文件夹的图像并应用这些变换。

观察这个可视化结果。我们有一些原始图像和一些经过变换的图像。经过变换的图像的优势在于,它们已经是张量格式,这正是我们的模型所需要的。这是我们逐步努力的目标。我们有了数据集,现在也有了将其转换为模型就绪张量的方法。

我们再可视化一些图像。这里将关闭随机种子,以便查看更多随机图像。

效果不错。我们看到牛排图像因为调整尺寸到 64x64 而像素化了,这张图片也进行了水平翻转,披萨图像也做了同样处理。我们再完成最后一张图像的展示。

很好,这就是 transforms 的核心:将图像转换为张量,并且可以根据需要操作这些图像。

现在,我们进入第四部分,这是第一种数据加载方式。

选项一:使用 ImageFolder 加载图像数据

torchvision.datasets 模块包含了许多内置函数来帮助加载数据。回想一下,PyTorch 视觉领域的每个主要库都有自己的数据模块。在本例中,我们将使用 ImageFolder 类,它可以帮助我们加载符合通用图像分类格式的数据。这是一个预构建的数据集函数,就像有预构建的数据集一样,我们可以使用预构建的数据集加载函数。

提示:选项二(后续课程的剧透)是我们将创建自己的自定义数据集加载器,这会在后面的视频中看到。

现在,让我们看看如何使用 ImageFolder 将我们所有的自定义图像加载为张量。这正是 transforms 将发挥作用的地方。

以下是使用 ImageFolder 加载图像分类数据的步骤:

首先,从 torchvision 导入 datasets 模块,因为 ImageFolder 类位于其中。

from torchvision import datasets

接着,为训练目录创建数据集。我们将传入根目录路径(我们的 train_dir)和一个 transform 参数,该参数将等于我们之前定义的 data_transform

train_data = datasets.ImageFolder(root=train_dir,
                                  transform=data_transform,
                                  target_transform=None)

transform 参数用于对图像数据进行变换,而 target_transform 参数用于对标签(或目标)进行变换。在我们的案例中,不需要对标签进行变换,因为标签将由图像所在的目标目录推断出来。披萨图像所在的目录将使它们获得“pizza”标签,因为我们的数据集是标准图像分类格式。

现在,对测试数据执行相同的操作。

test_data = datasets.ImageFolder(root=test_dir,
                                 transform=data_transform)

在幕后,我们所有的图像都将通过这些变换步骤,这正是它们被转换为数据集时的样子。

让我们打印出我们的数据集,看看它们的具体信息。

print(train_data)
print(test_data)

我们将得到一个 PyTorch 数据集对象(一个 ImageFolder 实例),其中包含数据点数量、根目录位置以及应用的变换信息。

使用 PyTorch 预构建数据加载器(或数据集加载器)的好处之一是它附带了许多有用的属性。

以下是我们可以从 ImageFolder 实例获取的一些关键信息:

  • 获取类别名称列表train_data.classes 将返回一个包含所有类别名称(如 ‘pizza‘, ‘steak‘, ‘sushi‘)的列表。这在后续绘图或预测时用于标注图像非常有用。
  • 获取类别到索引的映射字典train_data.class_to_idx 返回一个将字符串类别名称映射到其整数索引的字典(例如 {‘pizza‘: 0, ‘steak‘: 1, ‘sushi‘: 2})。如果需要对标签本身进行某种形式的转换,可以在此处传入 target_transform 参数。
  • 检查数据集的长度:使用 len(train_data)len(test_data) 可以获取训练集和测试集的样本数量。
  • 探索其他属性:你还可以查看 .samples(所有图像路径和标签)、.targets(所有标签列表)等属性。

现在我们已经完成了数据加载。让我们延续一直以来的做法,从训练数据集中可视化一个样本及其标签。


总结

本节课中,我们一起学习了如何使用 ImageFolder 将图像加载为张量。由于我们的数据已经是标准图像分类格式,因此可以利用 torchvision.datasets 中的一个预构建函数来简化流程。在下一视频中,我们将进行更多的可视化操作。

140:可视化训练集加载图像 🖼️

在本节课中,我们将学习如何从已转换为张量的训练数据集中加载和可视化单个图像样本。我们将检查图像的形状、数据类型和标签,并使用Matplotlib将其正确显示出来。


从数据集中获取样本

上一节我们使用ImageFolder和转换管道将图像数据转换成了张量。现在,我们来看看如何从这个数据集中提取样本。

我们可以通过索引train_data数据集来获取单个图像及其标签。

# 获取训练数据集的第一个样本
image, label = train_data[0]

执行上述代码后,image变量将包含一个图像张量,label变量则包含其对应的数字标签(例如,披萨类别的标签可能是0)。


检查样本信息

在可视化之前,了解数据的格式至关重要。以下是检查图像和标签信息的方法。

# 打印图像张量及其信息
print(f"Image tensor:\n{image}")
print(f"Image shape: {image.shape}")
print(f"Image datatype: {image.dtype}")

# 打印标签及其信息
print(f"Label: {label}")
print(f"Label datatype: {type(label)}")

输出结果可能如下:

  • 图像形状torch.Size([3, 64, 64]),表示3个颜色通道,高度和宽度均为64像素。
  • 图像数据类型torch.float32,这是PyTorch的默认浮点类型。
  • 标签数据类型int,整数类型。

了解这些信息有助于在后续建模过程中调试常见的形状、设备或数据类型不匹配错误。


将数字标签转换回文本

我们的标签目前是数字格式。为了便于理解,我们可以使用之前创建的class_names列表将其转换回文本标签。

# 将数字标签转换为人类可读的文本
label_str = class_names[label]
print(f"Label as string: {label_str}")

例如,如果label是0,label_str将是“pizza”。


可视化图像样本

Matplotlib库期望图像的维度顺序为(高度,宽度,颜色通道)。然而,PyTorch图像张量的默认顺序是(颜色通道,高度,宽度)。因此,我们需要使用.permute()方法调整维度顺序。

以下是调整维度并绘制图像的完整步骤:

import matplotlib.pyplot as plt

# 1. 调整图像维度顺序以适应Matplotlib
# 将形状从 [C, H, W] 调整为 [H, W, C]
image_permuted = image.permute(1, 2, 0)

# 打印形状变化以确认
print(f"Original image shape: {image.shape}") # [color_channels, height, width]
print(f"Permuted image shape: {image_permuted.shape}") # [height, width, color_channels]

# 2. 绘制图像
plt.figure(figsize=(10, 7))
plt.imshow(image_permuted)
plt.axis(False) # 关闭坐标轴
plt.title(class_names[label], fontsize=14)
plt.show()

执行代码后,你将看到一张(可能有些像素化的)披萨图片。图像像素化是因为我们将原始图像(例如512x512)调整为了64x64的大小。


练习与探索建议

为了加深理解,我鼓励你尝试以下操作:

  • 随机查看不同样本:修改索引(如train_data[10])来查看数据集中的其他图像。
  • 调整转换管道:尝试修改transforms.Compose中的参数,例如将Resize调整为(128, 128),观察图像质量的变化。
  • 探索更多变换:查阅torchvision.transforms文档,尝试添加其他变换(如色彩抖动、旋转),看看它们如何影响图像。

下节预告

本节课我们一起学习了如何从自定义数据集中加载、检查并可视化图像样本。我们确认了数据已处于适合输入PyTorch模型的张量格式。

目前,我们拥有的是数据集(Dataset)。在下一节课中,我们将把这些数据集转换为数据加载器(DataLoader)。数据加载器能够批量加载数据,并支持随机打乱,这对于高效训练模型至关重要。我建议你先尝试自己创建train_data_loadertest_data_loader,我们将在下个视频中一起完成这个步骤。

141:将图像数据集转换为PyTorch DataLoader 🚀

在本节课中,我们将学习如何将已加载的图像数据集转换为PyTorch的DataLoader。DataLoader是训练深度学习模型的关键组件,它能高效地批量处理数据,并帮助我们更好地管理内存。


概述

上一节我们介绍了如何使用torchvision.datasets.ImageFolder将图像数据加载为PyTorch数据集。本节中,我们来看看如何将这些数据集转换为DataLoader,以便在模型训练时进行批处理和迭代。


DataLoader的作用

DataLoader能帮助我们将数据集转换为可迭代对象。我们可以自定义批次大小,让模型每次处理一批图像。

核心概念batch_size参数决定了模型每次看到的图像数量。这对于处理大型数据集至关重要,因为一次性加载所有数据可能导致内存不足。

例如,如果我们的GPU只有16GB内存,尝试一次性加载10万张图像并进行计算,很可能会耗尽内存。通过使用DataLoader,我们可以让模型每次只处理32张图像,从而更有效地利用硬件资源。


创建DataLoader

以下是创建训练和测试DataLoader的步骤。

首先,导入必要的模块并设置批次大小:

import torch
from torch.utils.data import DataLoader

# 设置批次大小
BATCH_SIZE = 32

接下来,创建训练DataLoader。我们将设置shuffle=True来打乱训练数据,防止模型学习到数据中的顺序。

# 创建训练DataLoader
train_dataloader = DataLoader(dataset=train_data,
                              batch_size=BATCH_SIZE,
                              num_workers=1,
                              shuffle=True)

然后,创建测试DataLoader。对于测试数据,我们通常不进行打乱,以便评估结果的一致性。

# 创建测试DataLoader
test_dataloader = DataLoader(dataset=test_data,
                             batch_size=BATCH_SIZE,
                             num_workers=1,
                             shuffle=False)

参数说明

  • num_workers:用于加载数据的CPU核心数。通常,数值越高,数据加载越快。可以使用os.cpu_count()获取可用的CPU核心数。
  • shuffle:是否打乱数据顺序。训练数据需要打乱,测试数据则保持固定顺序。

检查DataLoader

创建DataLoader后,我们可以检查其长度和数据结构。

# 检查DataLoader长度
print(f"训练DataLoader长度: {len(train_dataloader)}")
print(f"测试DataLoader长度: {len(test_dataloader)}")

BATCH_SIZE=1时,DataLoader的长度与原始数据集相同。如果增大批次大小,长度会相应减少。


可视化DataLoader中的数据

我们可以从DataLoader中提取一批数据,并查看其形状。

# 从训练DataLoader中获取一批数据
for images, labels in train_dataloader:
    print(f"图像形状: {images.shape}")  # 形状为 [batch_size, color_channels, height, width]
    print(f"标签形状: {labels.shape}")  # 形状为 [batch_size]
    break

输出示例

  • BATCH_SIZE=1时,图像形状为[1, 3, 64, 64],标签形状为[1]
  • BATCH_SIZE=32时,图像形状为[32, 3, 64, 64],标签形状为[32]

总结

本节课中,我们一起学习了如何将PyTorch数据集转换为DataLoader。我们了解了DataLoader的作用、如何设置批次大小和num_workers参数,以及如何检查和处理DataLoader中的数据。

通过使用DataLoader,我们可以高效地批量处理图像数据,为后续的模型训练做好准备。下一节,我们将探讨如何在不使用torchvision.datasets.ImageFolder的情况下,自定义数据加载流程。


我们下节课再见!👋

142:自定义数据集类高层概览 🧠

在本节课中,我们将学习如何为PyTorch创建自定义的数据集类。我们将从零开始构建一个类,用于从文件目录中加载图像数据,并将其转换为PyTorch可用的张量格式。通过这个过程,你将理解PyTorch数据加载的核心机制,并掌握处理非标准数据集的技能。

课程概述

在之前的课程中,我们学习了如何使用PyTorch内置的ImageFolder类来加载图像数据。然而,在实际项目中,你可能会遇到没有现成数据加载函数可用的情况。因此,掌握如何创建自定义数据集类是一项非常重要的技能。

自定义数据集类的目标

我们的目标是创建一个自定义类,它需要实现以下核心功能:

  1. 从文件路径加载图像数据。
  2. 能够从数据集中获取类别名称列表。
  3. 能够将类别名称映射为字典(例如,{‘pizza’: 0, ‘steak’: 1, ‘sushi’: 2})。

最终,这个类应该能够与PyTorch的DataLoader类无缝协作,就像我们之前使用ImageFolder一样。

创建自定义数据集的优缺点

在开始编码之前,让我们先分析一下创建自定义数据集的利弊。

以下是创建自定义数据集的主要优点:

  • 灵活性高:你可以为几乎任何格式的数据创建数据集,只要编写相应的加载代码。
  • 不受限制:你不必局限于PyTorch官方库中预置的数据集函数。

当然,自定义数据集也存在一些缺点:

  • 需要更多测试:即使代码能运行,也不代表它能完美工作。你需要进行充分的测试,以确保数据加载方式符合预期,并且模型能够正常训练。
  • 代码量更大:自定义实现通常意味着需要编写更多代码,这可能会引入更多潜在的错误或性能问题。

通常,被纳入PyTorch标准库或领域库(如TorchVision)的功能都经过了大量测试和验证,具有很高的稳健性。而我们自己编写的代码则需要从头开始建立这种稳健性。尽管如此,理解如何构建自定义数据集仍然至关重要。

准备工作:导入必要的库

为了构建我们的自定义数据集类,我们需要导入一些Python和PyTorch模块。

import os
from pathlib import Path
import torch
from PIL import Image
from torch.utils.data import Dataset
from torchvision import transforms
from typing import Tuple, Dict, List

代码解释

  • ospathlib.Path:用于处理文件系统和路径操作。
  • torch:PyTorch核心库。
  • PIL.Image:用于打开和操作图像文件。
  • torch.utils.data.Dataset:所有PyTorch数据集的基类。我们的自定义类将继承(子类化)这个类。
  • torchvision.transforms:用于将图像数据转换为张量并进行其他预处理。
  • typing:用于为函数和类添加类型提示,使代码更清晰。

核心概念:继承Dataset基类

在PyTorch中,所有自定义数据集通常都是通过继承(子类化)torch.utils.data.Dataset这个抽象基类来实现的。

这个基类要求子类必须重写(override)两个关键方法:

  1. __getitem__(self, index):根据索引index返回一个数据样本(例如,一个图像张量和其对应的标签)。
  2. __len__(self):返回数据集的总样本数。

在接下来的课程中,我们将详细实现这些方法。本节课我们先搭建好整体的框架。

构建辅助函数

在创建完整的自定义数据集类之前,我们先构建一个辅助函数。这个函数的目标是模仿ImageFolder的一个功能:给定一个包含分类子文件夹的目录路径,它能自动提取出类别名称。

具体来说,这个函数需要:

  • 接收一个目标目录路径(例如,data/pizza_steak_sushi)。
  • 遍历该目录下的子文件夹,每个子文件夹名代表一个类别(例如,pizza, steak, sushi)。
  • 返回一个包含所有类别名称的列表。
  • (可选)返回一个将类别名称映射到数字索引的字典。

这个功能将为我们后续构建数据集类打下基础。

本节总结

本节课我们一起学习了创建PyTorch自定义数据集类的高层概览。我们明确了自定义数据集的目标和需要实现的功能,分析了其优缺点,并准备好了编码环境。最重要的是,我们了解到所有自定义数据集都应继承自torch.utils.data.Dataset基类,并需要实现__getitem____len__方法。

在下一节课中,我们将开始动手编码,首先实现那个能从目录中提取类别信息的辅助函数。

143:创建获取类名的辅助函数 📂

在本节课中,我们将学习如何编写一个辅助函数,用于从目录结构中自动获取图像分类任务的类别名称。这个函数将是我们后续构建自定义数据集类的基础。

在上一节视频中,我们讨论了创建自定义数据集的激动人心的概念,并列出了一些我们想要实现的功能。我们了解到,许多自定义数据集类都继承自 torch.utils.data.Dataset,这也是我们稍后要做的。本节中,我们将专注于编写一个辅助函数来重现 torchvision.datasets.ImageFolder 自动获取类名的功能。

5.1 创建辅助函数

我们的目标是创建一个函数,给定一个标准图像分类格式的目录,它能返回类别名称列表和一个将类别名映射到整数的字典。

以下是实现此功能的主要步骤:

  1. 获取类别名称:使用 os.scandir() 遍历目标目录。
  2. 错误处理:如果未找到任何类别,则引发错误以提示目录结构可能存在问题。
  3. 创建映射字典:将类别名称列表转换为字典,以便计算机可以使用数字(而非字符串)作为标签。

让我们开始编写代码。首先,我们导入必要的模块。

import os
from typing import Tuple, List, Dict

接下来,我们定义函数 find_classes。它接收一个目录路径字符串,并返回一个包含类别列表和映射字典的元组。

def find_classes(directory: str) -> Tuple[List[str], Dict[str, int]]:
    """
    在目标目录中查找类别文件夹名称。

    参数:
        directory (str): 目标目录路径。

    返回:
        Tuple[List[str], Dict[str, int]]: (类别列表, 类别名到索引的字典)。
    """

现在,我们来实现上述步骤。

步骤一:扫描目录获取类别名

我们使用 os.scandir() 来遍历目录,并筛选出其中的文件夹条目。

    # 1. 通过扫描目标目录获取类别名称
    classes = sorted(entry.name for entry in os.scandir(directory) if entry.is_dir())

步骤二:添加错误检查

如果 classes 列表为空,说明在指定目录中未找到任何类别文件夹,此时我们抛出一个 FileNotFoundError

    # 2. 如果未找到类别名则引发错误
    if not classes:
        raise FileNotFoundError(f"在目录 ‘{directory}’ 中找不到任何类别。请检查文件结构。")

步骤三:创建类别名到索引的映射字典

计算机处理数字标签比字符串标签更高效。我们使用 enumerate() 函数为每个类别名分配一个唯一的整数索引。

    # 3. 创建索引标签的字典
    class_to_idx = {cls_name: i for i, cls_name in enumerate(classes)}

最后,函数返回这两个结果。

    return classes, class_to_idx

让我们测试一下这个函数。假设我们的数据目录结构如下:

data/train/
    pizza/
    steak/
    sushi/

我们可以这样调用函数:

# 示例:使用训练目录
target_dir = "data/train"
class_names, class_dict = find_classes(target_dir)

print(f"类别名称: {class_names}")
print(f"类别映射字典: {class_dict}")

运行上述代码,你将得到类似以下的输出:

类别名称: [‘pizza‘, ‘steak‘, ‘sushi‘]
类别映射字典: {‘pizza‘: 0, ‘steak‘: 1, ‘sushi‘: 2}

至此,我们已经成功复制了 torchvision.datasets.ImageFolder 自动获取类别信息的核心功能。这个辅助函数 find_classes 将成为我们下一节构建自定义 Dataset 类的重要组件。

总结

本节课中,我们一起学习了如何创建一个辅助函数 find_classes。该函数能够自动扫描指定目录,提取符合标准图像分类格式的类别文件夹名称,并将其转换为列表和映射字典。我们实现了三个关键步骤:目录遍历、错误处理以及创建标签映射。在下一节课中,我们将利用这个函数,通过子类化 torch.utils.data.Dataset 来完整地复现一个自定义的图像数据集类。

144:从零编写PyTorch自定义数据集类 📁

在本节课中,我们将学习如何通过继承 torch.utils.data.Dataset 类,从零开始构建一个自定义的PyTorch数据集类。我们将复现 torchvision.datasets.ImageFolder 的核心功能,并理解其内部工作机制。

概述

上一节我们编写了一个名为 find_classes 的辅助函数,它接收一个目标目录并返回一个类名列表以及一个将类名映射到整数的字典。本节中,我们来看看如何利用这个函数,创建一个完整的自定义数据集类。

创建自定义数据集的步骤

要创建一个自定义数据集,我们需要遵循几个关键步骤。以下是构建过程的核心要点:

  1. 继承基类:我们的自定义类需要继承自 torch.utils.data.Dataset
  2. 初始化方法:在 __init__ 方法中,我们需要接收目标目录和可选的图像变换函数。
  3. 定义属性:创建必要的属性,例如图像路径列表、变换函数、类名列表和类名到索引的映射字典。
  4. 创建图像加载函数:编写一个函数,用于根据索引加载并返回图像。
  5. 重写 __len__ 方法:此方法应返回数据集中样本的总数。
  6. 重写 __getitem__ 方法:这是核心方法,它接收一个索引,并返回对应的(图像,标签)元组。

代码实现详解

现在,让我们一步步将这些概念转化为代码。

1. 导入与类定义

首先,我们需要导入必要的模块并定义我们的自定义数据集类。

import torch
from torch.utils.data import Dataset
import pathlib
from PIL import Image

class ImageFolderCustom(Dataset):
    # 后续代码将写在这里

2. 初始化方法 __init__

__init__ 方法中,我们设置数据集的基本参数和属性。

    def __init__(self, targ_dir: str, transform=None):
        # 获取所有图像文件的路径
        self.paths = list(pathlib.Path(targ_dir).glob("*/*.jpg"))
        # 设置图像变换函数
        self.transform = transform
        # 获取类名列表和类名到索引的映射字典
        self.classes, self.class_to_idx = find_classes(targ_dir)

3. 图像加载函数 load_image

这个辅助函数负责打开指定索引的图像。

    def load_image(self, index: int) -> Image.Image:
        """根据索引打开图像并返回。"""
        image_path = self.paths[index]
        return Image.open(image_path)

4. 重写 __len__ 方法

此方法返回数据集中样本的数量。

    def __len__(self) -> int:
        """返回数据集中样本的总数。"""
        return len(self.paths)

5. 重写 __getitem__ 方法

这是数据集类的核心,它定义了如何通过索引获取一个数据样本。

    def __getitem__(self, index: int) -> tuple[torch.Tensor, int]:
        """返回一个数据样本(图像,标签)。"""
        # 1. 加载图像
        img = self.load_image(index)
        # 2. 从文件路径中提取类名
        # 假设路径格式为:data_folder/class_name/image.jpg
        class_name = self.paths[index].parent.name
        # 3. 将类名转换为索引(标签)
        class_idx = self.class_to_idx[class_name]

        # 4. 如果提供了变换函数,则应用变换
        if self.transform:
            return self.transform(img), class_idx
        # 否则,返回原始图像和标签
        else:
            return img, class_idx

总结

本节课中我们一起学习了如何从零构建一个PyTorch自定义数据集类。我们通过继承 torch.utils.data.Dataset 并重写 __len____getitem__ 方法,成功复现了 ImageFolder 的基本功能。这个自定义类 ImageFolderCustom 现在可以像官方数据集一样,与 DataLoader 配合使用,用于训练深度学习模型。掌握创建自定义数据集的能力,是处理非标准或特定领域数据的关键步骤。在下一节,我们将测试这个自定义类,确保它能正确工作。

145:对比自定义数据集类与原始ImageFolder类 📊

在本节课中,我们将学习如何创建一个自定义的数据集类,并验证其功能是否与PyTorch内置的ImageFolder类一致。我们将通过设置数据转换、实例化自定义类并进行一系列对比测试来完成这一目标。


设置数据转换

上一节我们介绍了自定义数据集类的基本结构。本节中,我们来看看如何为数据准备转换流程,以便将图像转换为PyTorch可处理的张量格式。

我们需要创建两个转换管道:一个用于训练数据(包含数据增强),另一个用于测试数据(仅进行基础转换)。

import torchvision.transforms as transforms

train_transforms = transforms.Compose([
    transforms.Resize(size=(64, 64)),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor()
])

test_transforms = transforms.Compose([
    transforms.Resize(size=(64, 64)),
    transforms.ToTensor()
])

训练转换流程将图像尺寸调整为64x64像素,并随机进行水平翻转以增强数据多样性。测试转换流程仅调整尺寸并转换为张量,不对测试数据进行数据增强操作。


实例化自定义数据集类

现在,我们将使用自定义的ImageFolderCustom类来加载我们的训练和测试数据。

以下是实例化过程的代码:

train_data_custom = ImageFolderCustom(tdir=train_dir,
                                      transform=train_transforms)

test_data_custom = ImageFolderCustom(tdir=test_dir,
                                     transform=test_transforms)

我们分别传入了训练和测试数据的目录路径,以及对应的转换流程。


验证自定义类的功能

接下来,我们将从多个维度对比自定义数据集类与原始ImageFolder类的输出,以确保功能一致。

以下是需要验证的几个关键点:

  1. 数据集长度:检查两个类加载的数据样本数量是否相同。
  2. 类别属性:检查classesclass_to_idx属性是否一致。
  3. 数据样本:尝试获取单个样本,确认其格式(图像张量和标签)是否正确。

让我们执行这些检查:

# 1. 检查数据集长度
print(len(train_data_custom) == len(train_data))
print(len(test_data_custom) == len(test_data))

# 2. 检查类别属性
print(train_data_custom.classes == train_data.classes)
print(train_data_custom.class_to_idx == train_data.class_to_idx)

# 3. 检查单个样本
sample_image, sample_label = train_data_custom[0]
print(f"Image shape: {sample_image.shape}, Label: {sample_label}")

如果所有比较结果都为True,并且样本格式正确,则证明我们的自定义类成功复制了原始ImageFolder类的核心功能。


核心概念与总结

本节课中我们一起学习了如何构建并验证一个自定义的PyTorch数据集类。关键在于继承torch.utils.data.Dataset基类,并重写__len____getitem__这两个核心方法。

核心公式可以概括为:
自定义数据集类 = Dataset基类 + __len__方法 + __getitem__方法

通过这种方式,无论你的数据以何种格式存储,都可以创建一个与之交互的数据加载器,从而无缝地集成到PyTorch的训练流程中。

在下一讲中,我们将创建一个可视化函数,从我们自定义的数据集中随机显示一些图像,以更直观地验证数据加载的正确性。

146:编写自定义数据集随机图像可视化函数 📊

在本节课中,我们将学习如何编写一个辅助函数,用于从自定义数据集中随机选取并可视化图像。通过可视化,我们可以直观地检查数据加载和预处理是否正确。


概述

上一节我们介绍了如何创建自定义数据集。本节中,我们来看看如何编写一个函数来可视化数据集中的随机图像,以验证数据加载过程是否正常。

创建可视化函数

以下是创建 display_random_images 函数的步骤。

步骤一:定义函数参数

首先,我们需要定义一个函数,它接收一个数据集、类别名称列表以及要显示的图像数量等参数。

def display_random_images(dataset: torch.utils.data.Dataset,
                          classes: list[str] = None,
                          n: int = 10,
                          display_shape: bool = True,
                          seed: int = None):

步骤二:限制显示图像数量

为了防止显示过多图像导致布局混乱,我们将最大显示数量限制为10张。如果传入的 n 大于10,则进行调整。

    if n > 10:
        n = 10
        display_shape = False
        print("For display purposes, n shouldn't be larger than 10, setting to 10 and removing shape display.")

步骤三:设置随机种子

为了确保结果可复现,我们可以设置一个随机种子。

    if seed:
        random.seed(seed)

步骤四:获取随机样本索引

接下来,我们需要从数据集中随机抽取 n 个样本的索引。

    random_samples_idx = random.sample(range(len(dataset)), k=n)

步骤五:设置绘图区域

在绘制图像之前,我们需要设置 Matplotlib 的图形和轴。

    plt.figure(figsize=(16, 8))

步骤六:遍历并绘制图像

现在,我们遍历随机选取的索引,获取对应的图像和标签,并将它们绘制出来。

    for i, target_sample in enumerate(random_samples_idx):
        target_image, target_label = dataset[target_sample][0], dataset[target_sample][1]

步骤七:调整张量维度以匹配 Matplotlib

PyTorch 图像的默认维度顺序是 (C, H, W)(通道、高度、宽度),但 Matplotlib 期望的顺序是 (H, W, C)。因此,我们需要使用 permute 方法调整维度。

        target_image_adjusted = target_image.permute(1, 2, 0)  # 将形状从 (C, H, W) 改为 (H, W, C)

步骤八:创建子图并设置标题

为每个图像创建一个子图,并根据是否提供类别名称来设置标题。

        plt.subplot(1, n, i+1)
        plt.imshow(target_image_adjusted)
        plt.axis(False)
        if classes:
            title = f"Class: {classes[target_label]}"
            if display_shape:
                title = title + f"\nshape: {target_image_adjusted.shape}"
            plt.title(title)

使用函数可视化数据

我们已经定义了函数,现在可以将其应用于不同的数据集进行可视化。

可视化 ImageFolder 创建的数据集

首先,让我们使用 torchvision.datasets.ImageFolder 创建的数据集进行测试。

display_random_images(dataset=train_data,
                      classes=class_names,
                      n=5,
                      seed=42)

可视化自定义数据集

接下来,使用我们之前创建的自定义数据集进行可视化。

display_random_images(dataset=train_data_custom,
                      classes=class_names,
                      n=10,
                      seed=42)

运行上述代码后,你将看到从数据集中随机选取的图像,每张图像都标有其对应的类别。如果设置了 display_shape=True,还会显示图像的形状。


总结

本节课中我们一起学习了如何编写一个用于可视化数据集中随机图像的辅助函数。我们涵盖了从定义函数参数、限制显示数量、设置随机种子,到调整张量维度以匹配 Matplotlib 要求的全过程。通过这个函数,我们可以直观地检查数据加载和预处理的效果,为后续的模型训练打下坚实基础。

在下一节中,我们将探讨如何将自定义数据集转换为 DataLoader,以便更高效地进行批量训练。

147:将自定义数据集转换为DataLoader 🚀

在本节课中,我们将学习如何将自定义数据集转换为PyTorch的DataLoader,以便能够以批量的形式高效地加载数据,供模型训练使用。


上一节我们介绍了如何创建自定义数据集类,本节中我们来看看如何将其转换为DataLoader。

首先,我们需要导入必要的模块。

from torch.utils.data import DataLoader

接下来,我们将创建训练数据的DataLoader。

BATCH_SIZE = 32

train_dataloader_custom = DataLoader(
    dataset=train_data_custom,
    batch_size=BATCH_SIZE,
    num_workers=0,
    shuffle=True
)

以下是DataLoader关键参数的说明:

  • dataset:传入我们之前创建的自定义数据集实例。
  • batch_size:设置每个批次包含的样本数量,这里设为32。
  • num_workers:设置用于加载数据的子进程数量。默认值为0。更高的数值通常能加快数据加载速度,但需要根据你的硬件配置进行调整。
  • shuffle:在训练时,通常需要打乱数据顺序,以避免模型学习到数据顺序带来的偏差。

同样地,我们为测试数据创建DataLoader。

test_dataloader_custom = DataLoader(
    dataset=test_data_custom,
    batch_size=BATCH_SIZE,
    num_workers=0,
    shuffle=False
)

创建完成后,我们可以检查一下DataLoader是否工作正常。

img_custom, label_custom = next(iter(train_dataloader_custom))
print(f"Image batch shape: {img_custom.shape}")
print(f"Label batch shape: {label_custom.shape}")

代码执行后,输出应为 [32, 3, 64, 64]。这表示我们成功加载了一个批次,包含32张图像,每张图像有3个颜色通道,尺寸为64x64像素。这个尺寸与我们之前定义的图像变换(transforms)是一致的。

如果需要调整批次大小,只需修改 BATCH_SIZE 参数即可。一个好的实践是将批次大小设置为8的倍数,这有助于计算优化。


本节课中我们一起学习了将自定义数据集转换为DataLoader的完整流程。我们了解到,虽然PyTorch的领域库(如TorchVision)提供了许多现成的数据加载功能,但当我们需要处理特殊格式的数据时,可以通过子类化 torch.utils.data.Dataset 来创建自定义数据集类,并依然能方便地使用 DataLoader 进行批量加载。这种代码通常具有可复用性,可以封装成工具函数以供后续项目使用。

在下一节,我们将探讨数据增强(Data Augmentation)技术,了解如何通过对训练图像进行变换来人工增加数据集的多样性,从而提升模型的泛化能力。

148:探索Torchvision最先进的数据增强技术 🚀

在本节课中,我们将学习数据增强的概念,并探索如何使用PyTorch的Torchvision库,特别是其最先进的TrivialAugment技术,来人为地增加训练数据的多样性,从而提升模型的泛化能力。


数据加载与转换回顾

在之前的课程中,我们创建了函数和类来加载自定义数据。加载自定义数据最关键的一步是数据转换,尤其是将目标数据转换为张量。

我们也初步了解了Torchvision的transforms模块,发现它提供了多种转换数据的方式。其中一种重要的方式就是数据增强

如果我们查看transforms的图示,可以看到许多不同的方法:Resize可以改变图像尺寸,CenterCropFiveCrop可以进行裁剪,Grayscale可以转换色彩,还有各种随机变换,如RandomRotationRandomAffineRandomCrop等。实际上,我鼓励你亲自查看所有可用的选项。

但请注意,这里还有AutoAugmentRandomAugment。这正是我之前提到的数据增强。你注意到原始图像是如何以不同方式被增强的吗?图像被人为地改变:轻微旋转、变暗或变亮、向上平移,或者颜色发生改变。这个过程就是数据增强。


什么是数据增强? 🤔

那么,如何了解数据增强呢?你可以搜索相关资料。维基百科的定义是:在数据分析中,数据增强是通过添加已有数据的略微修改副本,或从现有数据中新创建的合成数据来增加数据量的技术。

因此,我们可以这样定义:
数据增强是通过人为方式为训练数据增加多样性的过程。

对于图像数据,这可能意味着对训练图像应用各种图像变换。我们在Torchvision的transforms包中看到了许多这样的变换。现在,让我们特别关注一种类型的数据增强:TrivialAugment

为了说明这一点,我准备了一张幻灯片。它展示了从不同视角观察同一张图像。我们这样做,正如我所说,是为了人为地增加数据集的多样性。

想象我们的原始图像在左侧。如果我们想旋转它,可以应用旋转变换。如果想在垂直和水平轴上平移,可以应用平移变换。如果想放大图像,可以应用缩放变换。变换的种类很多,例如裁剪、替换、剪切等,这张幻灯片只演示了少数几种。


为什么要使用数据增强? 🎯

我想重点介绍另一种数据增强类型,它被用来将PyTorch Torchvision图像模型训练到最先进的水平。

让我们看看这种用于将PyTorch视觉模型训练到最先进水平的特定数据增强类型。

你可能不确定我们为什么要这样做。我们希望通过增加训练数据的多样性,使图像对模型来说更难学习,或者让模型有机会从不同视角观察同一图像。这样,当你在实践中使用图像分类模型时,它就已经见过许多不同角度的同类图像。

希望这能让模型学习到可泛化到不同角度的模式。因此,这种做法有望得到一个对未见数据更具泛化能力的模型


探索最先进的训练方法

如果我们访问Torchvision的“最先进”部分,会发现PyTorch团队最近的一篇博客文章:《如何使用Torchvision的最新原语训练最先进的模型》。这正是我们想做的。“最先进”意味着业内最佳,通常缩写为SOTA,你会经常看到这个缩写。

Torchvision是我们处理视觉数据所使用的包,它包含一系列“原语”,换句话说,就是能帮助我们训练出高性能模型的函数。

在这篇博客文章中,如果我们向下滚动,会看到一些改进。例如,原始的ResNet-50模型(一种常见的计算机视觉架构)的准确率。通过添加所有使用的改进,准确率从76%左右提升到了近81%,提高了近5个百分点,这非常不错。

我们将要关注的是TrivialAugment。博客中提到了许多不同的改进项,如学习率优化、延长训练时间、随机擦除图像数据、标签平滑(可作为交叉熵损失函数的参数添加)、MixUp和CutMix、权重衰减调整、FixRes缓解、指数移动平均(EMA)、推理尺寸调整等。这里有一整套不同的“配方”项目。

但我们将重点分解其中一个:TrivialAugment


动手实现TrivialAugment 🛠️

让我们在自己的数据上看看它的效果。

首先,我们导入必要的模块并创建一个训练转换管道:

from torchvision import transforms

# 创建训练转换管道
train_transform = transforms.Compose([
    transforms.Resize(size=(224, 224)), # 将图像大小调整为常见的224x224
    transforms.TrivialAugmentWide(num_magnitude_bins=31), # 应用TrivialAugment,强度设为最大31
    transforms.ToTensor() # 最后转换为张量
])

我们刚刚就实现了TrivialAugment。这多棒啊!它来自PyTorch Torchvision的transforms库。TrivialAugmentWide被用来训练PyTorch Torchvision模型库中最新的最先进视觉模型。

如果你想了解TrivialAugment,可以搜索相关论文阅读。它巧妙地利用了随机性的力量。不过,我更倾向于先在我们的数据上尝试并可视化它。


测试我们的增强流程

接下来,我们测试这个增强流程。首先获取所有图像路径(虽然之前做过,但为了重申,我们再做一次):

# 获取图像路径列表
image_path_list = list(image_path.glob("*/*/*.jpg"))

# 使用我们之前创建的函数绘制随机增强后的图像
plot_transformed_images(
    image_paths=image_path_list,
    transform=train_transform, # 使用包含TrivialAugment的转换
    n=3, # 绘制3张图像
    seed=None
)

观察结果:第一张是“披萨”类图像。TrivialAugment调整了它的大小,颜色可能被以某种方式操作了。第二张图像看起来被调整了大小,但变化不大。第三张图像的颜色似乎被以某种形式操纵了。

TrivialAugment的工作原理是:它从所有其他增强类型中随机选择,并以一定的强度级别(我们设置为0到31)应用它们。所以,这些图像中的每一张都被随机选择和随机强度地应用了某种变换。

再看另一组三张图像:这张看起来被切掉了一点;那张颜色又以某种方式改变了;另一张变暗了。

你看到我们是如何人为地为训练数据集增加多样性的吗?我们不再让所有图像都保持单一视角,而是添加了许多不同的角度,并告诉模型:“即使图像被处理过,你也必须尝试学习这些模式。”

再试一次:看那张,它被处理得相当多了,对吧?但它仍然是一张“牛排”图像。我们正试图让模型即使在被处理过后,仍然能识别出这是一张牛排图像。

这会有效吗?可能有效,也可能无效。但这就是实验的本质。我鼓励你像我们刚才做的那样,进入transforms文档,将这里的TrivialAugmentWide换成你能找到的另一种增强类型,看看它会对我们的随机图像产生什么效果。

我之所以重点介绍TrivialAugment,是因为PyTorch团队在他们最近的博客文章中,将其作为训练最先进视觉模型的“配方”的一部分。

说到训练模型,让我们继续前进,构建我们的第一个模型。


总结 📝

本节课中,我们一起学习了:

  1. 数据增强是通过人为应用变换(如旋转、平移、缩放、颜色调整)来增加训练数据多样性的过程。
  2. 数据增强的目的是提高模型对未见数据的泛化能力
  3. Torchvision的transforms模块提供了丰富的内置数据增强方法。
  4. 我们重点探索了TrivialAugment,这是一种被用于训练最先进PyTorch视觉模型的增强技术,它随机选择和应用多种变换。
  5. 我们通过代码transforms.TrivialAugmentWide(num_magnitude_bins=31)实现了它,并可视化了对图像的影响。

记住,选择哪种数据增强方式是一个需要根据具体问题和实验来回答的问题。从社区已验证有效的方法(如TrivialAugment)开始尝试是一个很好的策略。

149:构建基线模型(第一部分):数据加载与转换 🚀

概述

在本节课中,我们将学习如何为自定义数据集加载和转换数据,这是构建计算机视觉模型的第一步。我们将创建一个简单的数据转换流程,并使用PyTorch的ImageFolderDataLoader类来准备数据,为后续构建和训练基线模型打下基础。


回顾与过渡

上一节我们介绍了PyTorch团队如何使用当时最先进的“TrivialAugment Wide”数据增强技术来训练其最新的计算机视觉模型。我们看到,借助Torchvision的transforms模块,可以轻松应用这种增强技术。

为了更清楚地展示数据增强的效果,我们再来看一个例子。虽然增强后的图像看起来变化不大,但我们可以看到这张图片被移动了,留下了一些黑色空间;另一张图片则被轻微旋转,同样产生了黑色空间。


构建基线模型

现在,是时候在我们自己的自定义数据集上构建第一个计算机视觉模型了。让我们开始吧。我们将这个模型称为Model 0

我们将复用之前在计算机视觉部分介绍过的TinyVGG架构。我们的第一个实验是构建一个基线模型,这正是Model 0的目标。

我们将不使用数据增强来构建它。这意味着,我们将不使用上面提到的、PyTorch团队用来训练其最先进模型的TrivialAugment。我们先从训练一个没有数据增强的计算机视觉模型开始。这样,在后续实验中,我们可以尝试使用数据增强,以观察它是否有效。

以下是TinyVGG架构的简要说明。我们不会在此深入探讨,你只需要知道:模型的输入尺寸是64x64x3,它会经过多个不同的层(如卷积层、ReLU层、最大池化层),最后是一个适合我们类别数量的输出层。

在我们的案例中,数据集有3个不同的类别:披萨、牛排和寿司。现在,让我们根据CNN Explainer网站上的图示来复现TinyVGG架构。这是一个很好的练习。

当然,在训练模型之前,我们必须做什么呢?让我们进入7.1节:创建数据转换与加载


为Model 0加载数据

我们将为Model 0加载数据。虽然我们可以使用已经加载好的变量,但为了练习,我们将重新创建它们。

首先,创建一个简单的数据转换流程。为Model 0加载数据的核心前提是:从data文件夹下的pizza_steak_sushi目录中,分别读取traintest文件夹内的图像,并将它们转换为张量。

我们已经做过几次了。其中一种方法是创建一个transform

simple_transform = transforms.Compose([
    transforms.Resize((64, 64)),
    transforms.ToTensor()
])

我们使用transforms.Resize将图像大小调整为TinyVGG架构所需的64x64。然后,使用transforms.ToTensor将图像转换为张量,张量中的值将被归一化到0到1之间。

现在,我们来加载数据。如果你想暂停视频自己尝试,我鼓励你试试选项1:使用ImageFolder类加载图像数据,然后将该数据集转换为DataLoader,以便与PyTorch模型一起使用。

如果你准备好了,我们就一起操作。

首先,导入必要的模块:

from torchvision import datasets

然后,创建训练数据集。我称之为“simple”,因为我们首先使用的是没有数据增强的简单转换:

train_data_simple = datasets.ImageFolder(root=train_dir,
                                         transform=simple_transform)

接着,创建测试数据集:

test_data_simple = datasets.ImageFolder(root=test_dir,
                                        transform=simple_transform)

我们对训练数据和测试数据应用相同的转换。

下一步是将这些数据集转换为数据加载器。首先导入相关模块:

import os
from torch.utils.data import DataLoader

然后设置批大小和工作进程数:

BATCH_SIZE = 32
NUM_WORKERS = os.cpu_count()

现在,创建数据加载器:

train_dataloader_simple = DataLoader(dataset=train_data_simple,
                                     batch_size=BATCH_SIZE,
                                     shuffle=True,
                                     num_workers=NUM_WORKERS)

test_dataloader_simple = DataLoader(dataset=test_data_simple,
                                    batch_size=BATCH_SIZE,
                                    shuffle=False,
                                    num_workers=NUM_WORKERS)

我希望你已经尝试过了。但你是否看到,如果数据格式正确,我们可以多快地加载数据?虽然我们花了很多时间通过多个视频和大量代码来逐步讲解这些步骤,但实际设置数据加载就是这么迅速:创建一个简单的转换,同时加载并转换数据,然后像这样将数据集转换为数据加载器。

现在,我们已经准备好将这些数据加载器与模型一起使用了。


下一步:构建模型

说到模型,我们将在下一个视频中构建TinyVGG架构。事实上,我们已经在之前的3号笔记本中做过这件事。如果你想参考我们当时构建的模型(即那里的Model 2),我鼓励你回顾那个部分并自己尝试一下。否则,我们将在下一个视频中一起构建TinyVGG架构。


总结

本节课中,我们一起学习了为自定义计算机视觉项目准备数据的关键步骤:

  1. 我们回顾了数据增强技术。
  2. 我们定义了构建基线模型(Model 0)的目标,并决定初始阶段不使用数据增强。
  3. 我们创建了一个简单的数据转换流程,将图像调整大小并转换为张量。
  4. 我们使用torchvision.datasets.ImageFolder类加载了训练和测试数据集。
  5. 我们使用torch.utils.data.DataLoader类将数据集批量化,为模型训练做好了准备。

现在,数据已经就绪,我们将在下一节课中构建TinyVGG模型架构。

150:从零构建TinyVGG模型 🧠

在本节课中,我们将学习如何从零开始复现一个名为TinyVGG的卷积神经网络架构。我们将遵循一个常见的机器学习实践:找到一个适用于类似问题的模型,然后复制并尝试将其应用于我们自己的问题。


概述

上一节我们开始为自定义数据建模做准备。本节我们将一起快速构建TinyVGG模型。这个架构源自CNN Explainer网站,我们在之前的笔记本中已经介绍过。现在,让我们看看如何用代码实现它。

7.2 创建TinyVGG模型类

我们将创建一个继承自nn.Module的模型类。虽然我们之前在第三部分已经编写过类似的模型,但有一个重要的区别:之前的模型处理的是黑白图像,而我们现在要处理的是彩色图像。这意味着输入通道数将从1变为3。此外,在分类器层,我们可能需要一些技巧来确定正确的输入形状。

以下是构建模型的步骤:

  1. 定义模型类:创建一个名为TinyVGG的类,继承自nn.Module
  2. 初始化参数:接收输入形状、隐藏单元数和输出形状作为参数。
  3. 构建卷积块:按照TinyVGG架构,构建两个卷积块,每个块包含卷积层、ReLU激活层和最大池化层。
  4. 构建分类器层:添加一个展平层和一个线性层,将卷积特征映射到最终的类别输出。
  5. 实现前向传播:定义数据如何通过网络层流动。

让我们开始编写代码。

import torch
from torch import nn

class TinyVGG(nn.Module):
    """
    模型架构复制自CNN Explainer网站的Tiny VGG。
    """
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
        super().__init__()
        # 第一个卷积块
        self.conv_block_1 = nn.Sequential(
            nn.Conv2d(in_channels=input_shape,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,
                         stride=2)
        )
        # 第二个卷积块
        self.conv_block_2 = nn.Sequential(
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,
                         stride=2)
        )
        # 分类器层
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=hidden_units, # 注意:这里需要修正
                      out_features=output_shape)
        )

    def forward(self, x):
        # 打印形状以进行调试
        x = self.conv_block_1(x)
        print(f"卷积块1后形状: {x.shape}")
        x = self.conv_block_2(x)
        print(f"卷积块2后形状: {x.shape}")
        x = self.classifier(x)
        print(f"分类器后形状: {x.shape}")
        return x

注意:在分类器的线性层中,我们暂时将in_features设置为hidden_units。实际上,这个值应该是经过两个卷积块和展平层后的特征数量。我们稍后会通过程序化的方式来确定正确的值。

关于前向传播的优化技巧

在上面的forward方法中,我们逐步将数据传递到每一层。然而,有一种更高效的方式可以重写整个前向传播过程,利用操作符融合技术。

操作符融合可以将多个计算步骤合并为一个,减少数据在GPU内存和计算单元之间的传输次数,从而加速计算。虽然这个主题超出了本课程的范围,但了解其基本思想是有益的。

我们可以将前向传播重写为:

def forward(self, x):
    return self.classifier(self.conv_block_2(self.conv_block_1(x)))

这种写法在幕后更高效。如果你对此感兴趣,可以阅读Horace He的博客文章《Making Deep Learning Go Brrr From First Principles》,其中详细讨论了如何优化深度学习计算。

实例化模型并检查

现在,让我们创建一个模型实例,看看它是否能够正常工作。

# 实例化模型
model_0 = TinyVGG(input_shape=3,      # 彩色图像的通道数
                  hidden_units=10,    # 与原始TinyVGG相同的隐藏单元数
                  output_shape=len(class_names)) # 输出类别数(例如披萨、牛排、寿司为3)

# 将模型发送到GPU(如果可用)
device = "cuda" if torch.cuda.is_available() else "cpu"
model_0.to(device)

请注意,将模型移动到GPU内存可能需要几秒钟时间。当你构建大型神经网络时,使用操作符融合等技术可以帮助加速计算。

下一步:验证模型

我们已经构建了模型架构,但它可能存在错误。如何发现呢?一个我最喜欢的调试方法是:向模型传递一些虚拟数据

在下一节中,我们将向模型传递虚拟数据,检查前向传播是否正确实现,并验证每一层的输入和输出形状。


总结

本节课我们一起学习了如何从零开始构建TinyVGG卷积神经网络模型。我们:

  1. 创建了一个继承自nn.Module的模型类。
  2. 定义了两个卷积块和一个分类器层。
  3. 实现了前向传播方法,并了解了操作符融合的优化概念。
  4. 实例化了模型并将其移动到目标设备。

记住,构建模型时,复制一个在类似问题上有效的架构是一个很好的起点,但你可以自由调整超参数(如隐藏单元数、卷积核大小等)来适应你的具体任务。

151:构建基线模型(第三部分):前向传播测试模型形状 🧪

概述

在本节课中,我们将学习如何对刚刚构建的神经网络模型进行一次“虚拟”前向传播,以验证模型结构是否正确,特别是输入和输出的张量形状是否符合预期。这是模型构建过程中一个关键的调试步骤。


前向传播测试

上一节我们成功复现了CNN解释器网站上的微型VGG架构。现在,我们需要确保数据能够正确地在模型中流动。

获取单批次数据

首先,我们需要从数据加载器中获取一个批次的图像和标签数据,以便进行测试。

以下是具体步骤:

  1. 从训练数据加载器中获取一个批次的图像和标签。
  2. 打印出该批次图像和标签的张量形状,以了解其结构。
# 获取一个批次的图像和标签
image_batch, label_batch = next(iter(train_dataloader))

# 检查形状
print(f"图像批次形状: {image_batch.shape}")
print(f"标签批次形状: {label_batch.shape}")

执行前向传播

获取数据后,我们可以尝试将其输入到模型中,进行一次前向传播。

# 尝试对图像批次进行前向传播
model_0(image_batch)

执行上述代码时,你很可能会遇到两个常见错误。

错误排查与解决

第一个错误通常是设备不匹配。我们的模型可能位于GPU(cuda设备)上,而数据批次位于CPU上。解决方案是将数据移动到与模型相同的设备上。

# 将数据移动到目标设备
image_batch = image_batch.to(device)
model_0(image_batch)

解决设备问题后,第二个常见错误是形状不匹配。错误信息可能类似于:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x2560 and 10x3)

这里的256010是关键。10是我们分类器层中定义的隐藏单元数。错误表明,在进入线性层进行矩阵乘法时,中间的两个维度必须相等。

调试技巧:查看错误中第一个形状的中间数字(例如2560)。这个数字通常来自卷积层输出的扁平化(Flatten)结果。你需要将这个数字与分类器层第一个线性层的输入特征数对齐。

在我们的模型中,这个数字是卷积块2的输出经过扁平化后的尺寸。计算公式为:
隐藏单元数 * 特征图高度 * 特征图宽度

通过计算 10 * 16 * 16 = 2560,我们确认了这一点。因此,需要将模型分类器层中第一个线性层的输入特征数修改为2560

# 在模型定义中,修改分类器层
self.classifier = nn.Sequential(
    nn.Flatten(),
    nn.Linear(in_features=10*16*16, out_features=10), # 修改 in_features
    nn.Linear(in_features=10, out_features=output_shape)
)

修改后重新运行模型和前向传播,形状错误应该得到解决。

与参考架构对齐

为了完全复现CNN解释器中的模型,我们可能需要调整卷积层的padding(填充)参数。原模型可能未使用填充,这会导致特征图尺寸进一步缩小。

例如,将卷积层的padding1改为0后,特征图尺寸会从16*16变为13*13。此时,分类器层第一个线性层的输入特征数就需要相应地更新为 10 * 13 * 13 = 1690

# 调整卷积层参数(无填充)
self.conv_block_1 = nn.Sequential(
    nn.Conv2d(in_channels=3, out_channels=10, kernel_size=3, stride=1, padding=0), # padding=0
    nn.ReLU(),
    nn.Conv2d(in_channels=10, out_channels=10, kernel_size=3, stride=1, padding=0), # padding=0
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=2, stride=2)
)

# 更新分类器层输入特征数
nn.Linear(in_features=10*13*13, out_features=10)

经过这些调整,我们不仅确保了数据流的通畅,也精确地复现了目标架构。


模型可视化工具介绍

成功进行一次前向传播后,我们验证了模型的基本结构。在进入下一阶段之前,我想介绍一个非常实用的工具:torchinfo

torchinfo 可以打印出模型的详细摘要,包括每一层的输入/输出形状、参数数量等信息,这比手动调试形状要直观和高效得多。

在下一节课中,我们将一起学习如何安装并使用 torchinfo 来生成我们模型model_0的摘要报告,类似于下图所示的清晰结构视图。

(此处可想象一张torchinfo输出摘要的图片)

我建议你可以先尝试在Google Colab中安装torchinfo (!pip install torchinfo),并看看能否为我们的模型生成一份摘要。下节课我们将一起完成这个步骤。


总结

本节课中,我们一起学习了如何通过执行一次前向传播来测试新建的PyTorch模型。我们经历了获取测试数据、执行前向传播、排查并解决设备不匹配和形状不匹配错误的过程。特别是,我们掌握了通过错误信息和计算来调试并修正线性层输入特征数的关键技巧。最后,我们还了解了一个强大的模型可视化工具torchinfo,为后续的模型分析和优化打下基础。

152:使用Torchinfo获取模型摘要 📊

概述

在本节课中,我们将学习如何使用torchinfo库来获取PyTorch模型的详细摘要信息。通过这个工具,我们可以清晰地看到数据在模型中流动时每一层的输入输出形状变化,以及模型的总参数量等信息。


回顾与引入

上一节我们通过手动执行前向传播来检查模型,确认了我们的forward方法工作正常,且数据在模型中流动时没有出现形状错误。

本节中,我们来看看如何以更程序化的方式获取这些信息,这就是torchinfo库的作用。

安装与导入Torchinfo

首先,我们需要安装torchinfo库。在Google Colab等环境中,它可能不是默认安装的。

以下是安装和导入的代码:

# 尝试导入torchinfo,如果失败则安装
try:
    import torchinfo
except:
    !pip install torchinfo
    import torchinfo

# 从torchinfo导入summary函数
from torchinfo import summary

使用Torchinfo获取模型摘要

在导入torchinfo后,我们可以使用summary函数来生成模型的摘要。我们需要为函数提供模型实例和一个示例输入尺寸。

以下是生成摘要的代码示例:

# 假设我们的模型实例名为 model_0
# 输入尺寸为 (batch_size, color_channels, height, width)
# 这里我们使用一个批次大小为1的示例图像
summary(model=model_0,
        input_size=(1, 3, 64, 64))

注意:输入尺寸必须与模型设计时预期的尺寸匹配,否则torchinfo在内部执行前向传播时会报错。

解读模型摘要输出

运行上述代码后,torchinfo会输出一个结构清晰的摘要。以下是对摘要中关键部分的解读:

  • 模型结构:摘要会列出模型的整体结构,例如我们的TinyVGG模型由多个Sequential块组成。
  • 层信息:每个Sequential块内部会详细列出每一层(如卷积层Conv2d、激活层ReLU、池化层MaxPool2d)及其参数。
  • 形状变化:摘要的核心部分是展示数据流经每一层时,输入和输出张量的形状变化。这能帮助我们直观地验证数据形状是否正确转换。
  • 参数量统计:摘要底部会提供总参数量。参数指的是模型中可学习的权重和偏置项,它们最初是随机数,深度学习的核心目标就是调整这些参数以更好地表示数据。我们的模型约有8000多个参数,这属于非常小的模型。
  • 模型大小估计:摘要还会给出模型在内存中占用空间的估计值。我们的模型小于1MB。随着模型层数和参数量的增加,其大小也会显著增长,这在部署到存储受限的设备时需要特别注意。

总结

本节课我们一起学习了如何使用torchinfo库来高效地获取和分析PyTorch模型的摘要信息。这个工具能帮助我们:

  1. 可视化模型结构。
  2. 跟踪数据在模型中的形状变化。
  3. 统计模型的总参数量。
  4. 估计模型的文件大小。

它是一个非常有用的调试和模型理解工具。记得在使用时传入正确的示例输入尺寸。

在下一节中,我们将开始为我们的TinyVGG模型创建训练和测试函数,正式进入模型训练阶段。

153:创建训练与测试循环函数 🚀

在本节课中,我们将学习如何将训练和测试过程封装成可复用的函数。我们将创建一个通用的训练步骤函数和一个测试步骤函数,它们可以应用于几乎任何模型和数据加载器。通过这种方式,我们可以简化代码,提高效率,并专注于模型架构和实验。


概述 📋

在之前的章节中,我们手动编写了训练和测试循环。现在,我们将把这些步骤抽象成函数,使代码更加模块化和可重用。我们将创建两个核心函数:train_step 用于训练模型,test_step 用于评估模型。这些函数设计为通用,可以适应不同的模型和数据加载器。


创建训练步骤函数 🏋️‍♂️

上一节我们介绍了训练循环的基本概念,本节中我们来看看如何将其封装成一个函数。训练步骤函数将负责执行单个训练周期内的所有操作,包括前向传播、损失计算、反向传播和参数更新。

以下是训练步骤函数 train_step 的实现步骤:

  1. 设置模型为训练模式model.train()
  2. 初始化评估指标:将训练损失和准确率归零
  3. 遍历数据加载器:对每个批次的数据执行以下操作
  4. 将数据发送到目标设备X, y = X.to(device), y.to(device)
  5. 执行前向传播y_pred = model(X)
  6. 计算损失loss = loss_fn(y_pred, y)
  7. 优化器梯度归零optimizer.zero_grad()
  8. 执行反向传播loss.backward()
  9. 更新模型参数optimizer.step()
  10. 计算并累积批次准确率
  11. 循环结束后,计算平均损失和准确率

以下是 train_step 函数的代码实现:

def train_step(model: torch.nn.Module,
               data_loader: torch.utils.data.DataLoader,
               loss_fn: torch.nn.Module,
               optimizer: torch.optim.Optimizer,
               device: torch.device = device):
    """
    对模型执行一个训练周期。
    """
    # 将模型设置为训练模式
    model.train()
    
    # 初始化训练损失和准确率
    train_loss, train_acc = 0, 0
    
    # 遍历数据加载器中的每个批次
    for batch, (X, y) in enumerate(data_loader):
        # 将数据发送到目标设备
        X, y = X.to(device), y.to(device)
        
        # 1. 前向传播
        y_pred = model(X)
        
        # 2. 计算损失
        loss = loss_fn(y_pred, y)
        train_loss += loss.item()
        
        # 3. 优化器梯度归零
        optimizer.zero_grad()
        
        # 4. 反向传播
        loss.backward()
        
        # 5. 更新参数
        optimizer.step()
        
        # 计算准确率并累积
        y_pred_class = torch.argmax(torch.softmax(y_pred, dim=1), dim=1)
        train_acc += (y_pred_class == y).sum().item() / len(y_pred)
    
    # 计算整个周期的平均损失和准确率
    train_loss /= len(data_loader)
    train_acc /= len(data_loader)
    
    return train_loss, train_acc

创建测试步骤函数 🧪

现在我们已经有了训练函数,接下来看看如何创建测试步骤函数。测试步骤函数用于评估模型在未见数据上的性能,它不涉及参数更新,只进行前向传播和指标计算。

以下是测试步骤函数 test_step 的实现步骤:

  1. 设置模型为评估模式model.eval()
  2. 初始化评估指标:将测试损失和准确率归零
  3. 在推理模式下遍历数据加载器:使用 torch.inference_mode()
  4. 将数据发送到目标设备
  5. 执行前向传播
  6. 计算损失并累积
  7. 计算准确率并累积
  8. 循环结束后,计算平均损失和准确率

以下是 test_step 函数的代码实现:

def test_step(model: torch.nn.Module,
              data_loader: torch.utils.data.DataLoader,
              loss_fn: torch.nn.Module,
              device: torch.device = device):
    """
    对模型执行一个测试周期。
    """
    # 将模型设置为评估模式
    model.eval()
    
    # 初始化测试损失和准确率
    test_loss, test_acc = 0, 0
    
    # 在推理模式下进行,不跟踪梯度
    with torch.inference_mode():
        # 遍历数据加载器中的每个批次
        for batch, (X, y) in enumerate(data_loader):
            # 将数据发送到目标设备
            X, y = X.to(device), y.to(device)
            
            # 1. 前向传播
            test_pred_logits = model(X)
            
            # 2. 计算损失
            loss = loss_fn(test_pred_logits, y)
            test_loss += loss.item()
            
            # 计算准确率并累积
            # 注意:可以直接对logits使用argmax,softmax不是必须的,但为了完整性可以加上
            test_pred_labels = torch.argmax(torch.softmax(test_pred_logits, dim=1), dim=1)
            test_acc += (test_pred_labels == y).sum().item() / len(test_pred_labels)
    
    # 计算整个周期的平均损失和准确率
    test_loss /= len(data_loader)
    test_acc /= len(data_loader)
    
    return test_loss, test_acc

总结 🎯

本节课中我们一起学习了如何创建通用的训练和测试循环函数。我们实现了 train_step 函数来封装训练过程,包括前向传播、损失计算、反向传播和优化器更新。同时,我们实现了 test_step 函数来封装评估过程,用于计算模型在测试集上的性能指标。

通过将这些步骤函数化,我们的代码变得更加清晰、模块化且易于重用。在接下来的课程中,我们将学习如何将这些函数组合成一个完整的训练循环,以便轻松地训练和评估多个模型。

154:创建模型训练与评估函数 🚀

在本节课中,我们将学习如何将训练步骤和测试步骤的功能整合到一个统一的训练函数中。通过创建这个函数,我们可以简化模型训练和评估的流程,提高代码的复用性。


概述

在上一节中,我们分别创建了 train_steptest_step 函数。本节中,我们将把这两个函数整合到一个名为 train 的函数中。这个函数将负责模型的整个训练和评估过程,并记录每个周期的损失和准确率。


创建训练函数

首先,我们需要导入必要的库,并定义 train 函数。这个函数将接收模型、训练数据加载器、测试数据加载器、优化器、损失函数、训练周期数和设备作为参数。

import torch
from tqdm.auto import tqdm

def train(model: torch.nn.Module,
          train_dataloader: torch.utils.data.DataLoader,
          test_dataloader: torch.utils.data.DataLoader,
          optimizer: torch.optim.Optimizer,
          loss_fn: torch.nn.Module = torch.nn.CrossEntropyLoss(),
          epochs: int = 5,
          device: torch.device = device):

初始化结果字典

为了跟踪模型在每个训练周期的表现,我们将创建一个结果字典。这个字典将包含训练损失、训练准确率、测试损失和测试准确率的列表。

以下是初始化结果字典的代码:

    results = {
        "train_loss": [],
        "train_acc": [],
        "test_loss": [],
        "test_acc": []
    }

训练循环

接下来,我们将进入训练循环。在每个周期中,我们将调用 train_step 函数进行训练,然后调用 test_step 函数进行评估。同时,我们会记录每个周期的结果并打印出来。

以下是训练循环的代码:

    for epoch in tqdm(range(epochs)):
        # 训练步骤
        train_loss, train_acc = train_step(model=model,
                                           dataloader=train_dataloader,
                                           loss_fn=loss_fn,
                                           optimizer=optimizer,
                                           device=device)
        
        # 测试步骤
        test_loss, test_acc = test_step(model=model,
                                        dataloader=test_dataloader,
                                        loss_fn=loss_fn,
                                        device=device)
        
        # 打印结果
        print(f"Epoch: {epoch} | "
              f"Train Loss: {train_loss:.4f} | "
              f"Train Acc: {train_acc:.4f} | "
              f"Test Loss: {test_loss:.4f} | "
              f"Test Acc: {test_acc:.4f}")
        
        # 更新结果字典
        results["train_loss"].append(train_loss)
        results["train_acc"].append(train_acc)
        results["test_loss"].append(test_loss)
        results["test_acc"].append(test_acc)
    
    return results

函数整合

通过以上步骤,我们已经成功创建了一个完整的训练函数。这个函数整合了训练和评估的流程,并能够记录每个周期的表现。现在,我们可以使用这个函数来训练和评估我们的模型。


总结

本节课中,我们一起学习了如何将训练步骤和测试步骤整合到一个统一的训练函数中。通过创建这个函数,我们简化了模型训练和评估的流程,提高了代码的复用性。在下一节课中,我们将使用这个函数来训练我们的第一个模型,并进一步优化其性能。

155:使用训练函数评估模型 0 🚀

在本节课中,我们将学习如何使用之前构建的训练循环函数来训练和评估我们的基准模型(Model 0)。我们将实例化模型、选择损失函数和优化器,并观察模型在自定义数据集上的初步表现。


设置环境与模型

上一节我们介绍了训练循环函数。本节中,我们来看看如何将这些函数应用于我们的基准模型。

首先,我们需要设置随机种子以确保结果的可复现性。请注意,在实际应用中,我们可能不会总是固定随机种子,因为理想情况下,模型性能应不依赖于特定的随机种子。但出于教学目的,我们在此固定它。

import torch
torch.manual_seed(42)
torch.cuda.manual_seed(42)

接下来,我们定义训练轮数,并创建 TinyVGG 模型的实例。该模型将处理我们的彩色图像数据集。

# 定义训练轮数
NUM_EPOCHS = 5

# 创建模型实例
model_0 = TinyVGG(
    input_shape=3,      # 彩色图像的通道数
    hidden_units=10,    # 隐藏单元数,与 CNN Explainer 网站保持一致
    output_shape=len(train_data.classes)  # 输出类别数
).to(device)

定义损失函数与优化器

模型创建好后,我们需要定义损失函数和优化器。对于多类别分类任务,交叉熵损失是合适的选择。我们将使用 Adam 优化器,并采用其默认学习率。

以下是核心组件的定义:

# 定义损失函数
loss_fn = nn.CrossEntropyLoss()

# 定义优化器
optimizer = torch.optim.Adam(params=model_0.parameters(),
                             lr=0.001)  # Adam 的默认学习率

训练模型并计时

现在,我们可以使用之前编写的 train 函数来训练模型。同时,我们将记录训练所花费的时间。

from timeit import default_timer as timer

# 开始计时
start_time = timer()

# 训练模型
model_0_results = train(model=model_0,
                        train_dataloader=train_dataloader_simple,
                        test_dataloader=test_dataloader_simple,
                        optimizer=optimizer,
                        loss_fn=loss_fn,
                        epochs=NUM_EPOCHS)

# 结束计时并打印耗时
end_time = timer()
print(f"总训练时间:{end_time - start_time:.3f} 秒")

运行代码后,模型开始训练。在我们的例子中,模型训练速度很快。训练完成后,我们得到了在训练集上约 40% 和在测试集上约 50% 的准确率。

分析结果与改进思路

我们的模型在三个类别的数据集上取得了约 50% 的测试准确率。考虑到随机猜测的基线准确率约为 33%,模型的表现略有提升,但仍有很大改进空间。

以下是几种可以尝试的模型改进方法:

  • 增加更多层:例如,在当前的 TinyVGG 架构中再添加一个卷积块。
  • 增加隐藏单元数:尝试将隐藏单元数从 10 增加到 20 或更多。
  • 延长训练时间:将训练轮数从 5 增加到 10、20 或更多。
  • 更改激活函数:ReLU 可能不是我们特定用例的最佳选择。
  • 调整学习率:尝试不同于 Adam 默认值(0.001)的学习率。
  • 更改损失函数:虽然对于多分类任务,交叉熵损失通常效果很好,但也可以进行尝试。

前三种方法(增加层、增加隐藏单元、延长训练时间)可以快速进行实验,是很好的起点。


总结

本节课中,我们一起学习了如何将训练循环函数应用于基准模型 Model 0。我们完成了环境设置、模型实例化、损失函数与优化器的定义,并成功训练了模型。初步结果显示模型性能优于随机猜测,但仍有提升潜力。我们讨论了几种可行的改进策略。在下一节课中,我们将绘制损失曲线,以更直观地分析模型的训练过程。

156:绘制模型0的损失曲线 📉

在本节课中,我们将学习如何绘制模型的损失曲线。损失曲线是评估模型训练过程的重要工具,它能直观地展示模型性能随时间(如训练轮次)的变化趋势。

上一节我们训练了第一个卷积神经网络,但发现其性能有待提升。本节中我们来看看如何通过绘制损失曲线来可视化模型的训练过程。

什么是损失曲线?

损失曲线是一种追踪模型随时间进展的方式。它通常以训练轮次(或批次)为横轴,以损失值(或准确率)为纵轴。理想的趋势是损失值随时间下降,而准确率随时间上升。

绘制损失曲线的步骤

以下是绘制损失曲线的具体步骤。我们将编写一个函数,接收包含训练和测试结果的字典,并生成相应的图表。

首先,我们需要从结果字典中提取数据。

def plot_loss_curves(results):
    """
    绘制结果字典的训练曲线。
    """
    # 获取损失值
    train_loss = results["train_loss"]
    test_loss = results["test_loss"]

    # 获取准确率值
    train_accuracy = results["train_acc"]
    test_accuracy = results["test_acc"]

    # 确定训练轮次数量
    epochs = range(len(results["train_loss"]))

接下来,我们设置图表布局并绘制损失曲线。

    import matplotlib.pyplot as plt

    plt.figure(figsize=(15, 7))

    # 绘制损失子图
    plt.subplot(1, 2, 1)
    plt.plot(epochs, train_loss, label="Train Loss")
    plt.plot(epochs, test_loss, label="Test Loss")
    plt.title("Loss")
    plt.xlabel("Epochs")
    plt.legend()

然后,我们在同一图表中绘制准确率曲线。

    # 绘制准确率子图
    plt.subplot(1, 2, 2)
    plt.plot(epochs, train_accuracy, label="Train Accuracy")
    plt.plot(epochs, test_accuracy, label="Test Accuracy")
    plt.title("Accuracy")
    plt.xlabel("Epochs")
    plt.legend()

    plt.show()

解读损失曲线

调用函数并传入模型0的结果字典。

plot_loss_curves(model_0_results)

生成的图表显示,我们的模型损失有下降趋势,准确率有上升趋势。这是一个积极的信号。虽然当前模型的绝对性能不高,但趋势表明,如果增加训练轮次,模型性能可能会继续提升。

理想的损失曲线应从左上方向右下方下降。准确率曲线则应从左下方向右上方上升。在后续课程中,我们将探讨不同类型的损失曲线,例如理想曲线、欠拟合曲线和过拟合曲线。

本节课中我们一起学习了如何绘制并初步解读模型的损失曲线。损失曲线是诊断模型训练状态的关键可视化工具。理解其趋势有助于我们判断模型是否在有效学习,并为后续的模型改进提供方向。

157:过拟合与欠拟合的平衡与应对策略 📊

在本节课中,我们将要学习机器学习中的两个核心挑战:过拟合与欠拟合。我们将探讨如何通过观察损失曲线来诊断模型状态,并学习一系列应对策略,以帮助模型达到“刚刚好”的理想状态。


损失曲线:模型性能的“仪表盘” 📉

上一节我们介绍了如何评估模型性能。本节中我们来看看损失曲线,它是评估模型随时间(例如训练时长)表现的一种重要方式。

损失曲线是调试模型最有帮助的工具之一。理想的趋势是,损失值应随时间下降,而像准确率这样的评估指标则应随时间上升。

以下是三种主要的损失曲线形态,但实际中会遇到更多变化:

  • 欠拟合:模型在训练集和测试集上的损失都可能更低。
  • 过拟合:模型在训练集上的损失远低于测试集。
  • 刚刚好:训练损失和测试损失以相近的速率下降。

理解过拟合与欠拟合 🤔

欠拟合

欠拟合是指模型的损失可以更低,即模型未能充分学习数据中的模式。在我们的例子中,模型看起来是欠拟合的,可能需要训练更长时间(例如10或20个周期)来观察损失是否继续下降。

过拟合

过拟合是欠拟合的反面,是机器学习的两大难题之一。过拟合发生时,训练损失低于测试损失。

这意味着模型过于擅长学习训练数据,导致训练集上的损失下降(这通常是好事),但这种学习并未体现在测试集上。模型本质上是在记忆训练数据中无法良好泛化到测试集的特定模式。

一个类比是:如果你为期末考试只死记硬背了课程材料(训练集),当遇到没见过的题目(测试集)时,你将无法灵活运用所学知识。

“刚刚好”的理想状态

理想情况下,我们希望训练损失和测试损失尽可能同步下降。通常训练损失会略低于测试损失,因为模型接触过训练数据而从未见过测试数据。我们的目标是达到一个平衡点,即模型既不过于简单(欠拟合),也不过于复杂(过拟合)。


应对策略:如何解决过拟合与欠拟合 ⚙️

以下是处理过拟合和欠拟合的一些常用方法。

减少过拟合的策略

我们希望模型在训练集和测试集上表现同样出色。以下是减少过拟合的方法:

  • 获取更多数据:扩大训练集,让模型接触更多样本,理论上能学习到更通用的模式。
  • 使用数据增强:通过对训练数据进行变换(如旋转、裁剪),增加数据多样性,使模型学习更鲁棒的特征。
  • 获取更好的数据:提升数据质量,有助于模型学习更通用的模式。
  • 使用迁移学习:利用在大型数据集(如ImageNet)上预训练好的模型,将其学到的模式调整应用于你自己的问题。
  • 简化模型:减少模型复杂度,例如减少网络层数或每层的隐藏单元数。这迫使模型用更有限的资源学习更通用的模式。
  • 使用学习率衰减:随着训练进行,逐渐降低学习率。初始时可以用较大的学习率快速下降,接近收敛时则用小步长精细调整,防止“错过”最优解。
    • 类比:就像在沙发缝里找硬币,开始时可以大步寻找,快找到时则需小步调整,以防硬币掉得更深。
    • 代码示例:PyTorch中可使用学习率调度器。
      from torch.optim.lr_scheduler import StepLR
      scheduler = StepLR(optimizer, step_size=30, gamma=0.1) # 每30个epoch,学习率乘以0.1
      
  • 使用早停法:在测试误差开始上升之前停止训练,并保存验证集上表现最好的模型权重。

减少欠拟合的策略

欠拟合是指模型未能很好地拟合数据。以下是减少欠拟合的方法:

  • 增加模型复杂度:为模型添加更多层或隐藏单元,增强其学习能力。
  • 调整学习率:学习率可能初始设置不当,需要调整。
  • 延长训练时间:增加训练周期数,让模型有更多机会学习数据中的模式。
  • 使用迁移学习:同样有助于解决欠拟合。
  • 减少正则化:正则化是为了防止过拟合,但如果正则化过强,可能会抑制模型学习能力,导致欠拟合。

核心:寻找平衡 ⚖️

机器学习很大程度上是在欠拟合和过拟合之间寻找平衡的艺术。

  • 如果过于努力减少欠拟合(如让模型过于复杂或训练过久),可能会导致过拟合。
  • 如果过于努力防止过拟合(如过度简化模型或使用强正则化),可能会导致欠拟合。

我们的目标是让模型达到“刚刚好”的拟合状态。这将是整个机器学习实践中持续面临的挑战和核心研究领域。


总结与思考 💡

本节课中我们一起学习了:

  1. 损失曲线是评估模型性能随时间变化的重要工具。
  2. 欠拟合意味着模型能力不足,损失还有下降空间。
  3. 过拟合意味着模型过于复杂,记忆了训练集的特有噪声,导致泛化能力差。
  4. 掌握了一系列应对过拟合(如数据增强、简化模型、早停)和欠拟合(如增加复杂度、延长训练)的策略。
  5. 机器学习的核心之一是找到欠拟合与过拟合之间的最佳平衡点

根据上一节的损失曲线,我们的模型似乎存在欠拟合。请思考:为了提升该模型从训练数据中学习模式的能力,你会采取什么措施?是训练更长时间、添加更多层,还是增加隐藏单元数?

在下一个视频中,我们将开始构建另一个模型,并尝试使用数据增强等方法。

158:为模型1创建增强训练数据集与DataLoader 📊

在本节课中,我们将学习如何通过数据增强技术来创建训练数据集和DataLoader,以应对模型可能出现的过拟合问题。我们将使用PyTorch的transforms模块来实现图像增强,并构建适用于训练和测试的数据加载器。


上一节我们介绍了损失曲线的概念,以及如何通过它判断模型是欠拟合还是过拟合。本节中,我们来看看如何通过数据增强来改善模型性能。

数据增强是处理过拟合的一种方法。它通过对训练图像进行随机变换(如旋转、翻转等),在不收集新数据的情况下,人工增加训练数据集的多样性。这有助于模型学习更通用的模式,而不是仅仅记住训练数据。

以下是实现数据增强的步骤:

首先,我们需要创建一个包含数据增强的转换流程。我们将使用PyTorch的transforms模块,并采用TrivialAugmentWide这种增强策略。

from torchvision import transforms

# 创建训练数据转换流程(包含数据增强)
train_transform_trivial = transforms.Compose([
    transforms.Resize(size=(64, 64)),
    transforms.TrivialAugmentWide(num_magnitude_bins=31),
    transforms.ToTensor()
])

# 创建测试数据转换流程(不包含数据增强)
test_transform_simple = transforms.Compose([
    transforms.Resize(size=(64, 64)),
    transforms.ToTensor()
])

接下来,我们使用这些转换流程来加载数据集。我们将使用ImageFolder类从指定目录加载图像,并应用相应的转换。

from torchvision import datasets

# 创建训练数据集(应用数据增强)
train_data_augmented = datasets.ImageFolder(
    root=train_dir,
    transform=train_transform_trivial
)

# 创建测试数据集(不应用数据增强)
test_data_simple = datasets.ImageFolder(
    root=test_dir,
    transform=test_transform_simple
)

现在,我们将数据集转换为DataLoader,以便在训练过程中批量加载数据。我们将设置批量大小、工作线程数,并打乱训练数据。

import os
from torch.utils.data import DataLoader

# 设置批量大小和工作线程数
BATCH_SIZE = 32
NUM_WORKERS = os.cpu_count()

# 创建训练数据加载器(包含数据增强)
train_dataloader_augmented = DataLoader(
    dataset=train_data_augmented,
    batch_size=BATCH_SIZE,
    shuffle=True,
    num_workers=NUM_WORKERS
)

# 创建测试数据加载器(不包含数据增强)
test_dataloader_simple = DataLoader(
    dataset=test_data_simple,
    batch_size=BATCH_SIZE,
    shuffle=False,
    num_workers=NUM_WORKERS
)

通过以上步骤,我们成功创建了包含数据增强的训练数据集和DataLoader,以及简单的测试数据集和DataLoader。在下一节中,我们将使用这些数据加载器来构建和训练模型1。


本节课中我们一起学习了如何通过数据增强技术创建训练数据集和DataLoader。我们介绍了数据增强的概念、实现步骤,并完成了数据加载器的构建。在下一讲中,我们将使用这些数据加载器来训练模型1。

159:构建与训练模型1 🏗️

在本节课中,我们将学习如何构建并训练一个使用了数据增强的模型。我们将使用与之前相同的模型架构,但这次会在训练数据上应用数据增强技术,以观察其性能与未使用数据增强的基线模型有何不同。


概述

上一节我们准备好了数据集、数据加载器以及数据增强策略。本节中,我们将利用这些组件来构建并训练我们的第一个模型(Model 1)。这个模型将使用数据增强后的训练数据,我们将通过对比实验来评估数据增强的效果。


构建模型

首先,我们需要创建模型并将其发送到目标设备(如GPU)。由于我们之前已经定义了一个模型类,现在可以方便地复用。

以下是创建模型的步骤:

import torch

# 设置随机种子以确保结果可复现
torch.manual_seed(42)

# 获取数据增强后训练集的类别数量
num_classes = len(train_data_augmented.classes)

# 创建模型实例并发送到设备
model_1 = TinyVGG(input_shape=3,
                  hidden_units=10,
                  output_shape=num_classes).to(device)

# 查看模型结构
print(model_1)

设置训练参数

在开始训练之前,我们需要定义损失函数、优化器以及训练轮数等参数。

以下是需要设置的参数:

  • 损失函数:我们使用交叉熵损失,在PyTorch中常被称为 criterion
  • 优化器:我们选择Adam优化器,并设置学习率为0.001。
  • 训练轮数:暂时设置为5个epoch。
from torch import nn
from torch.optim import Adam
from timeit import default_timer as timer

# 设置随机种子
torch.manual_seed(42)

# 定义训练轮数
num_epochs = 5

# 定义损失函数
loss_fn = nn.CrossEntropyLoss()

# 定义优化器
optimizer = Adam(params=model_1.parameters(),
                 lr=0.001)

# 开始计时
start_time = timer()

训练模型

现在,我们可以调用之前编写好的训练函数来训练我们的模型。

以下是训练模型的代码:

# 训练模型
model_1_results = train(model=model_1,
                        train_dataloader=train_dataloader_augmented,
                        test_dataloader=test_dataloader_simple,
                        optimizer=optimizer,
                        loss_fn=loss_fn,
                        epochs=num_epochs,
                        device=device)

# 结束计时并打印训练时间
end_time = timer()
print(f"Model 1 训练时间: {end_time - start_time:.3f} 秒")

运行上述代码后,模型开始训练。在本例中,训练耗时约7秒。需要注意的是,训练时间会根据所使用的GPU性能而有所不同。


初步分析与过渡

训练完成后,我们初步观察到,使用了数据增强的Model 1在训练集和测试集上的准确率,似乎不如之前未使用数据增强的基线模型(Model 0)。

这可能是因为我们的基线模型尚未出现过拟合现象。数据增强的主要作用是防止模型过拟合。既然模型还没有过拟合,强行引入数据增强可能不会带来性能提升,甚至可能干扰模型学习有效的特征。

尽管如此,这仍然是一次有价值的实验。它告诉我们,并非所有高级技术都适用于当前阶段的问题。机器学习工作流通常从简单模型开始,仅在必要时引入复杂性。

在下一节中,我们将通过绘制Model 1的损失曲线来更深入地分析其训练过程。你可以尝试自己调用 plot_loss_curves 函数,我们将在下一讲一起查看结果。


总结

本节课中,我们一起学习了如何构建并训练一个应用了数据增强的深度学习模型。我们完成了设置模型、定义损失函数与优化器、以及启动训练流程的完整步骤。通过对比实验,我们初步探讨了数据增强在模型未过拟合时可能起到的作用,并强调了从简单基线开始实验的重要性。

160:绘制模型1损失曲线 📈

在本节课中,我们将学习如何通过绘制损失曲线来评估模型1的性能。损失曲线是衡量模型在训练过程中表现随时间变化的重要工具,它能直观地展示模型是否存在欠拟合或过拟合问题。


评估模型性能:损失曲线

上一节我们介绍了使用数据增强技术训练模型1,但定量分析显示改进有限。本节中,我们来看看如何通过可视化手段进一步评估模型。

损失曲线能帮助你评估模型随时间变化的性能,并直观地展示模型是否欠拟合或过拟合。

以下是绘制模型1结果损失曲线的步骤:

# 使用之前创建的函数绘制损失曲线
plot_loss_curves(results=model_1_results)

观察生成的曲线,我们发现测试损失呈现上升趋势。这并非理想方向,因为损失衡量的是模型的错误程度,理想情况下损失应随时间下降。同时,准确率曲线也显得波动不定。

一个可行的实验是延长模型0和模型1的训练周期,观察损失曲线是否会趋于平缓。


分析模型状态:欠拟合与过拟合

现在,我们来分析模型当前的状态:它是在欠拟合、过拟合,还是两者兼有?

对于损失曲线(此处特指损失值,而非准确率),理想的方向是随时间下降。目前,我们的模型似乎存在欠拟合,因为损失值仍有下降空间。同时,模型也表现出过拟合的迹象,因为测试损失远高于训练损失,这表明模型未能很好地泛化到新数据。


改进模型的策略

回顾Learnpytorch.io书籍第4节,理想的损失曲线应呈现平滑下降趋势。针对过拟合问题,我们可以考虑以下方法:

  • 获取更多数据
  • 简化模型结构
  • 使用迁移学习(后续课程将涉及)

针对欠拟合问题,可以尝试以下策略:

  • 增加网络层数,例如添加另一个卷积块
  • 增加每层的隐藏单元数量(例如从10个增加到64个)
  • 延长训练周期(例如训练20个周期)

建议你参考这些策略进行实验,尝试让损失曲线更接近理想形状。


后续内容预告

在下一视频中,我们将继续推进。我们将并排比较两个模型的结果。目前我们已经完成了两项实验,并知道它们都有改进空间。比较所有实验结果的良好方式是将模型结果并排对比,这正是我们下一节课要做的内容。


本节课中,我们一起学习了如何绘制并解读损失曲线,以评估模型是否存在欠拟合或过拟合,并探讨了针对这些问题的潜在改进策略。

161:对比所有模型损失曲线 📊

在本节课中,我们将学习如何对比不同模型的训练结果。我们已经分别评估了各个模型的损失曲线,现在我们将把它们放在一起进行比较,以便更直观地看出哪个模型表现更好。

概述

在评估了各个建模实验的独立表现后,对比它们之间的结果至关重要。这有助于我们识别哪些调整有效,哪些无效,从而指导后续的模型优化。

有多种方法可以实现模型结果的对比。

以下是几种常见的方法:

  1. 硬编码方式:就像我们之前所做的那样,编写函数和辅助函数,然后手动绘制图表。这是我们本节课将采用的方法。
  2. 使用工具:例如 PyTorch + TensorBoard。TensorBoard 是一个用于跟踪实验的绝佳工具,我们将在课程后面的部分看到它。
  3. 权重与偏置:这是我最喜欢的工具之一,它是一个用于实验的平台。你可以轻松设置它来跟踪不同的模型超参数。例如,你可以导入 wandb,开始一个新的运行,并记录学习率等所有信息。
  4. MLflow:这是另一个我喜欢的工具,它提供机器学习跟踪、项目、模型注册等功能。

虽然本课程主要关注纯 PyTorch,但了解这些工具是有益的,因为你最终会遇到它们。目前,我们将坚持使用硬编码方式,以最简单的方法开始。如果需要,我们以后可以随时添加其他工具。

创建模型结果对比图表

现在,让我们为每个模型的结果创建一个数据框。我们可以这样做,因为我们的模型结果是以字典形式存储的。

通过硬编码方式实现这一点相当繁琐。想象一下,如果我们有10个甚至只是5个模型,就需要编写大量代码来处理所有的字典。而上面提到的工具可以帮助你自动跟踪一切。

我们已经有了一个数据框,显示了 Model0 随时间变化的结果。我们可以看到训练损失开始下降,测试损失也开始下降,训练和测试数据集的准确率开始上升。这些正是我们期望看到的趋势。

一个可以尝试的实验是:将这个 Model0 训练更长时间,看看它是否会改进。但目前,我们只对比较结果感兴趣。

接下来,我们设置一个图表。我们希望在同一个图表上绘制 Model 0Model 1 的结果。我们需要四个子图:训练损失、训练准确率、测试损失和测试准确率。每个子图上都有两条线,分别代表模型0和模型1。

无论我们有多少个不同的实验或想要比较的指标,这种模式都是相似的。你通常希望将它们全部绘制在一起以便可视化。这正是像权重与偏置、TensorBoard 和 MLflow 这样的工具可以帮助你完成的工作。

让我们开始设置图表。我将使用 Matplotlib 库,创建一个较大的图形,因为我们有四个子图,每个子图对应一个我们想要在不同模型之间比较的指标。

首先,获取训练周期数。epochs 将是 model_0_df 的长度范围。

现在,创建训练损失的图表。我们想要比较模型0和模型1的训练损失。

我们注意到 Model 0 的趋势是正确的。Model 1 在某个周期(比如第2个周期)损失飙升,但随后又开始下降。如果我们继续训练这些模型,可能会发现训练数据集上的损失总体呈下降趋势,这正是我们想要的。

接下来,绘制测试损失。我们注意到 Model 1 在这个阶段可能过拟合了,所以数据增强可能不是对我们模型的最佳改变。

请记住,即使你对模型进行了更改(例如防止过拟合或欠拟合),也不能保证这种改变总是能让模型的评估指标朝着正确的方向发展。理想情况下,损失应该随着时间的推移从左上方移动到右下方。目前看来,在损失方面,Model 0 暂时领先。

现在,让我们绘制训练和测试的准确率。在准确率方面,似乎我们的两个模型都朝着正确的方向发展。我们希望准确率从底部向左上方移动。

但是,测试准确率……哦,抱歉。我搞错了,这不是训练准确率。你发现了吗?训练准确率,我们正朝着正确的方向前进,但看起来 Model 1 仍然过拟合。因此,我们在训练数据上得到的结果并没有完全迁移到测试数据集上。而我们真正希望模型表现出色的地方,是在它从未见过的测试数据上。

训练数据集上的指标是好的,但理想情况下,我们希望模型在测试数据集上表现良好。因此,每当你进行一系列建模实验时,记住这一点总是好的。不仅要单独评估它们,还要相互比较。这样,你就可以回顾你的实验,看看哪些有效,哪些无效。

如果你问我对于这两个模型会怎么做,我可能会将它们训练更长时间,并可能在每一层添加更多的隐藏单元,看看结果会如何变化。

总结

在本节课中,我们一起学习了如何通过硬编码方式对比不同深度学习模型的训练结果。我们创建了包含训练损失、测试损失、训练准确率和测试准确率的综合图表,直观地比较了基线模型与加入数据增强的模型之间的性能差异。我们发现,简单的更改(如数据增强)并不总是能带来直接的性能提升,有时甚至可能导致过拟合。关键是要系统地跟踪和对比实验,以指导后续的优化方向。

在下一节课中,我们将学习如何使用训练好的模型对我们自己的自定义食物图像进行预测。我们将尝试上传一张不在训练集和测试集中的图片,让我们的 PyTorch 模型对其进行分类。

162:自定义数据预测(第一部分):下载图像 📸

在本节课中,我们将学习如何为自定义图像进行预测。我们将从下载一张不在训练集或测试集中的图像开始,为后续的预测步骤做准备。

上一节我们比较了不同的建模实验。本节中,我们来看看深度学习中最令人兴奋的部分之一:对自定义图像进行预测。虽然我们已经用自定义数据训练了模型,但如何对训练集和测试集之外的样本(在我们的案例中是图像)进行预测呢?

假设你正在构建一个食物识别应用(例如 Nurify),其功能是拍摄食物照片并识别它。你希望使用计算机视觉技术,将食物图像转化为类似二维码的信息。以下是其工作流程:如果我们上传一张我爸爸为美味披萨竖起大拇指的照片,Nurify 会将其预测为“披萨”,然后提供营养信息和耗时等数据。

我们可以使用训练好的 PyTorch 模型复制类似的过程。尽管由于模型准确率和损失等指标尚有提升空间,预测结果可能不会非常出色,但让我们先了解一下整个工作流程。我们要做的第一件事是获取一张自定义图像。

我们可以在 Google Colab 中点击上传按钮并选择图像,以交互方式导入。但为了可重复性,我将像之前一样,通过编写代码以编程方式完成。因此,在本视频中,我们将编写代码来下载一张自定义图像。我将使用 requests 库来实现。

就像所有优秀的烹饪节目一样,我已经为大家准备了一张自定义图像。其路径为 custom_image_path。请注意,你可以将此流程应用于你自己的披萨、牛排或寿司图像。如果你想在其他自定义数据集上训练自己的模型,工作流程也将非常相似。

我将从 GitHub 下载一张名为 04-pizza-dad.jpeg 的照片。这张照片位于课程 GitHub 仓库中。如果图像尚未存在于我们的 Colab 实例中,我们将编写代码来下载它。如果你想上传单张图像,可以点击上传按钮,但请注意,与我们的其他数据一样,如果 Colab 断开连接,上传的文件将会消失。这就是我喜欢编写代码的原因,这样我们就不必每次都重新上传。

以下是下载图像的代码逻辑:如果 custom_image_path 对应的文件不存在,我们将打开一个请求,从 GitHub 的原始文件链接下载图像。下载图像或从 GitHub 下载文件时,通常需要使用原始文件链接。

以下是实现此功能的代码:

import requests

# 自定义图像路径
custom_image_path = data_path / "04-pizza-dad.jpeg"

# 如果图像不存在,则下载
if not custom_image_path.is_file():
    # 原始图像 URL
    image_url = "https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/images/04-pizza-dad.jpeg"
    
    # 打开文件以二进制写入模式
    with open(custom_image_path, "wb") as f:
        # 从 URL 下载图像内容
        request = requests.get(image_url)
        # 将内容写入文件
        f.write(request.content)
    print(f"[INFO] 下载数据: {custom_image_path}")
else:
    print(f"[INFO] {custom_image_path} 已存在,跳过下载。")

运行此代码后,图像将被下载并保存到我们的 data 文件夹中。现在,我们可以在 Google Colab 中查看这张图像。图像中有一个大披萨,我们的目标是在后续视频中让模型正确预测它为“披萨”。

在接下来的几节中,我们将编写代码,对此自定义图像执行与处理自定义数据集相同的流程:将其转换为张量,然后将其输入我们的模型进行预测。

本节课中,我们一起学习了如何以编程方式从 GitHub 下载自定义图像,为后续的预测步骤做好准备。下一节,我们将学习如何将这张图像转换为适合模型输入的张量格式。

163:自定义数据预测(第二部分)—— 加载自定义图像 🖼️

在本节课中,我们将学习如何使用 PyTorch 加载自定义图像,并准备将其输入到我们之前训练好的深度学习模型中进行预测。我们将重点关注如何将图像转换为模型所需的张量格式。


概述

上一节我们介绍了自定义数据预测的基本概念。本节中,我们来看看如何将一张自定义图像(例如一张披萨照片)加载到 PyTorch 中,并将其转换为适合我们模型输入的张量格式。

加载自定义图像

我们构建深度学习模型最令人兴奋的部分之一,就是使用自定义数据进行预测。在本例中,我们将使用一张自定义图像——一张我父亲吃披萨的照片。由于我们的模型是在披萨、牛排和寿司图像上训练的,理想情况下,模型应该预测这张图像为“披萨”。

我们需要将自定义图像转换为张量形式。

图像格式要求

我们必须确保自定义图像的格式与模型训练时使用的数据格式一致。具体来说,数据应为张量形式,数据类型为 torch.float32,形状为 [64, 64, 3]。因此,我们可能需要调整图像的形状,并确保它在正确的计算设备上。

使用 Torchvision 加载图像

用于加载数据的包取决于你所在的领域。对于图像数据,我们使用 torchvision

以下是加载图像的关键步骤:

  1. 查阅文档torchvision 库提供了读取和写入图像的函数。我们需要使用 torchvision.io.read_image 函数来读取图像。
  2. 读取图像:该函数将 JPEG 或 PNG 图像读取为一个三维的 RGB 或灰度张量。输出张量的值类型为 uint8

让我们看看如何实际操作。

import torchvision

# 读取自定义图像,返回 uint8 格式的张量
custom_image_uint8 = torchvision.io.read_image(str(custom_image_path))

注意read_image 函数要求路径是字符串类型。如果我们的路径是 Path 对象,需要先将其转换为字符串。

读取后,我们可以查看图像的张量表示、形状和数据类型。

print(f"Custom image tensor:\n{custom_image_uint8}")
print(f"Custom image shape: {custom_image_uint8.shape}")
print(f"Custom image datatype: {custom_image_uint8.dtype}")

执行上述代码后,我们会发现:

  • 形状:原始图像的形状可能很大(例如 [3, 3024, 4032]),远大于模型训练时使用的 64x64 图像。这意味着图像包含的信息比模型训练时见过的更多,因此我们必须调整其大小。
  • 数据类型:张量数据类型为 torch.uint8,而我们的模型期望 torch.float32 类型。这可能在后续步骤中引发错误。

下一步任务

在继续之前,你可以尝试以下练习:

  • 使用 torchvision.transforms 将图像张量的大小调整为 64x64
  • 将张量的数据类型从 torch.uint8 转换为 torch.float32

完成这些预处理步骤后,我们就可以在下一部分尝试使用这个图像进行预测了。


总结

本节课中,我们一起学习了如何使用 torchvision.io.read_image 函数加载自定义图像,并分析了其原始的形状数据类型。我们明确了为了匹配模型输入要求,需要进行的两个关键操作:调整图像大小转换数据类型。下一节,我们将完成这些预处理步骤,并最终使用模型进行预测。

164:自定义数据预测(第三部分):图像格式标准化 🖼️

在本节课中,我们将学习如何对自定义图像进行预测。我们将重点关注在将图像输入模型之前,如何确保其格式与模型训练时使用的数据格式完全一致。这是实际应用深度学习模型的关键步骤。


11.2 使用训练好的PyTorch模型对自定义图像进行预测

上一节我们加载了自己的自定义图像,并将其转换为张量。现在,我们来看看如何用它进行预测。

我们的模型性能可能尚未达到理想水平,但了解如何对自定义数据进行端到端的预测非常重要,因为这是最有趣的部分。

现在,我们尝试对图像进行预测。我想强调不同数据类型、形状和设备的重要性。在深度学习中,这是三个最常见的错误来源。

让我们看看如果尝试用 uint8 格式的数据进行预测会发生什么。

with torch.inference_mode():
    prediction = model_1(custom_image)

我们遇到了一个运行时错误。错误信息提示输入类型是 uint8。这是我们讨论过的第一个错误:需要确保自定义数据的数据类型与模型训练时使用的数据类型相同。我们的模型期望的是 torch.float32 张量,而我们提供的是 uint8 图像张量。


修复数据类型

为了解决这个问题,我们需要加载自定义图像并将其转换为 torch.float32 类型。

custom_image = torchvision.io.read_image(image_path).type(torch.float32)

现在,我们的图像是 torch.float32 类型了。让我们再次尝试预测。

with torch.inference_mode():
    prediction = model_1(custom_image.to(device))

我们又遇到了问题。这次是因为我们的图像形状太大了。


调整图像数值范围

让我们检查一下图像的形状和数值范围。

print(custom_image.shape)
print(custom_image.min(), custom_image.max())

我们发现图像尺寸非常大(例如4032x3024),并且像素值范围是0到255,而不是我们模型训练时使用的0到1。

为了将像素值缩放到0到1之间,我们可以将其除以255,因为255是标准RGB图像格式的最大值。

custom_image = custom_image / 255.
print(custom_image.min(), custom_image.max()) # 现在值在0到1之间了

虽然数值范围正确了,但图像尺寸仍然太大。


调整图像形状

我们的模型是在64x64像素的图像上训练的。因此,我们需要将自定义图像调整为相同的大小。

以下是创建转换管道来调整图像大小的方法:

custom_image_transform = transforms.Compose([
    transforms.Resize(size=(64, 64))
])

现在,让我们应用这个转换并查看结果。

custom_image_transformed = custom_image_transform(custom_image)
print(f"原始形状: {custom_image.shape}")
print(f"转换后形状: {custom_image_transformed.shape}")

图像从几千像素缩小到了64x64像素。这会导致图像质量下降,变得像素化。请记住,提高模型性能的一个潜在方法是使用更大的训练图像(例如224x224),但为了与当前教程保持一致,我们暂时使用64x64。


添加批次维度并确保设备一致

即使调整了大小,预测仍然可能失败。常见的错误包括缺少批次维度和设备不匹配。

我们的模型期望的输入形状是 [batch_size, color_channels, height, width]。对于单张图像,我们需要添加一个批次维度(大小为1)。

# 添加批次维度
custom_image_transformed_with_batch = custom_image_transformed.unsqueeze(dim=0)
print(custom_image_transformed_with_batch.shape) # 应为 [1, 3, 64, 64]

同时,我们必须确保图像张量与模型在同一个设备上(例如GPU)。

custom_image_transformed_with_batch = custom_image_transformed_with_batch.to(device)

最终预测

现在,所有条件都已满足,我们可以进行预测了。

with torch.inference_mode():
    custom_prediction = model_1(custom_image_transformed_with_batch)

print(custom_prediction)

成功了!我们得到了模型对于“披萨”、“牛排”、“寿司”三个类别的原始输出(logits)。


关键步骤总结

本节课中,我们一起学习了如何对自定义图像进行预测。以下是必须牢记的关键步骤,这些步骤几乎适用于所有自定义数据:

  1. 加载图像并将其转换为张量。
  2. 确保图像数据类型与模型一致(通常是 torch.float32)。
  3. 确保图像形状与模型训练数据一致(例如 [color_channels, height, width],并添加批次维度 [1, color_channels, height, width])。
  4. 确保图像与模型在同一个设备上(CPU或GPU)。

对自定义数据进行预测是可行的,但前提是必须将数据格式化为与模型训练时相似的格式。请务必处理好PyTorch深度学习中三个最常见的错误:错误的数据类型、错误的形状和错误的设备。无论处理的是图像、音频还是文本,这三个问题都会一直伴随你。

目前,我们用了大约10个代码单元来完成一次预测。在下一节课中,我们将把这些步骤封装成一个函数,使其更加简洁和可重用。


我们下节课再见。

165:自定义数据预测(第四部分)🎯

概述

在本节课中,我们将学习如何将模型的原始输出(logits)转换为具体的预测标签。我们将通过应用Softmax函数将logits转换为概率,然后使用argmax获取预测类别,最终构建一个能够处理自定义图像并输出预测结果的流程。


从模型原始输出到预测概率

上一节我们成功获取了模型对自定义图像的原始输出(logits)。本节中,我们来看看如何将这些logits转换为更容易理解的预测概率。

以下是转换步骤:

  1. 使用torch.softmax()函数。
  2. 在第一个维度(dim=1)上应用Softmax,该维度对应批量中的不同样本。
custom_image_pred_probs = torch.softmax(custom_image_pred, dim=1)

执行上述代码后,custom_image_pred_probs将包含模型为每个类别分配的概率值。由于我们的模型在训练集上表现并非完美,这些概率值可能会比较分散地分布在各个类别上,而不是高度集中在正确的目标类别上。


从预测概率到预测标签

得到预测概率后,下一步是确定模型最终预测的类别标签。

以下是转换步骤:

  1. 使用torch.argmax()函数。
  2. 在第一个维度(dim=1)上寻找概率值最大的索引,该索引对应类别名称列表中的位置。
custom_image_pred_label = torch.argmax(custom_image_pred_probs, dim=1)

现在,custom_image_pred_label是一个包含预测类别索引的张量。为了将其转换为可读的类别名称,我们需要使用之前定义的class_names列表进行索引。请注意,在索引前通常需要将张量移至CPU。

class_names[custom_image_pred_label.cpu()]

尽管当前模型的预测概率分布较散,但上述流程已成功地将一张自定义图像(例如披萨图片)通过预处理、模型推理、后处理,最终输出了一个预测标签(例如“pizza”)。


整合流程:构建预测函数

我们已经编写了多行代码来完成从图像路径到预测结果的整个流程。为了使代码更简洁、可复用,理想的做法是将这些步骤封装成一个函数。

我们的目标是构建一个函数,其理想效果是:传入一个图像文件路径,函数能够自动加载图像、进行预处理、使用模型预测、并最终绘制图像并将预测结果作为标题显示。

以下是函数需要完成的核心步骤:

  1. 使用torchvision.io.read_image导入图像。
  2. 按照之前的流程对图像进行格式化处理(调整数据类型、变换形状等)。
  3. 将图像数据移至正确的设备(如GPU)。
  4. 使用训练好的模型进行预测。
  5. 将模型的logits输出转换为预测概率和标签。
  6. 绘制图像,并将预测的类别名称设置为图表标题。

我建议你将此作为一个小挑战,尝试根据以上步骤整合我们之前编写的所有代码,构建出这个预测函数。在下一节视频中,我们将一起完成这个函数的构建。


总结

本节课中我们一起学习了模型预测流程的后处理部分。我们掌握了如何利用Softmax函数将模型的原始logits输出转换为预测概率,并通过argmax操作得到具体的预测标签索引,进而映射为人类可读的类别名称。最后,我们提出了将整个自定义图像预测流程封装成函数的任务,为下一节实现一个完整的端到端预测工具做好准备。

166:自定义数据预测(第五部分):全流程整合 🧩

在本节课中,我们将学习如何将之前所有关于自定义数据预测的步骤整合到一个简洁、可复用的函数中。这个函数将能够加载图像、进行预处理、使用训练好的模型进行预测,并最终可视化结果。


上一节我们介绍了如何对单张自定义图像进行预测。本节中,我们来看看如何将这些步骤封装成一个函数,以便于重复使用。

函数设计思路

我们的目标是创建一个名为 pred_and_plot_image 的函数。这个函数需要接收以下参数:

  • 一个训练好的 PyTorch 模型。
  • 目标图像的路径。
  • 可选的类别名称列表,用于将预测索引转换为可读标签。
  • 可选的图像变换,以确保输入与模型训练时一致。
  • 目标计算设备(如 CPU 或 GPU)。

以下是该函数的核心步骤:

  1. 加载图像:使用 torchvision.io.read_image 加载图像。
  2. 数据转换:将图像数据类型转换为 torch.float32,并将像素值从 [0, 255] 缩放到 [0, 1] 范围。
  3. 应用变换:如果提供了变换参数,则对图像应用该变换。
  4. 准备模型:确保模型位于正确的计算设备上。
  5. 模型预测:为图像添加一个批次维度,将其移至正确的设备,然后让模型进行推理。
  6. 处理输出:将模型的原始输出(logits)通过 softmax 函数转换为预测概率,并使用 argmax 获取预测标签。
  7. 可视化结果:绘制图像,并在标题中显示预测的类别名称和对应的概率。

代码实现

以下是 pred_and_plot_image 函数的完整实现代码:

import torch
import torchvision
import matplotlib.pyplot as plt

def pred_and_plot_image(model: torch.nn.Module,
                        image_path: str,
                        class_names: list[str] = None,
                        transform=None,
                        device: torch.device = None):
    """
    使用训练好的模型对目标图像进行预测并绘制结果。

    参数:
        model: 训练好的 PyTorch 模型。
        image_path: 目标图像的文件路径。
        class_names: 可选的类别名称列表。
        transform: 可选的图像变换。
        device: 目标计算设备(如 "cuda" 或 "cpu")。
    """

    # 1. 加载图像
    target_image = torchvision.io.read_image(str(image_path)).type(torch.float32)

    # 2. 缩放像素值到 [0, 1] 范围
    target_image = target_image / 255.

    # 3. 应用变换(如果提供)
    if transform:
        target_image = transform(target_image)

    # 4. 确保模型在目标设备上
    model.to(device)

    # 5. 将模型设置为评估模式并进行预测
    model.eval()
    with torch.inference_mode():
        # 添加批次维度 [batch_size, color_channels, height, width]
        target_image = target_image.unsqueeze(dim=0)

        # 将图像移至目标设备
        target_image = target_image.to(device)

        # 进行预测
        target_image_pred = model(target_image)

    # 6. 将 logits 转换为预测概率和标签
    target_image_pred_probs = torch.softmax(target_image_pred, dim=1)
    target_image_pred_label = torch.argmax(target_image_pred_probs, dim=1)

    # 7. 绘制图像和预测结果
    # 移除批次维度并调整维度顺序为 [height, width, color_channels] 以供 Matplotlib 显示
    plt.imshow(target_image.squeeze().permute(1, 2, 0).cpu())

    # 设置标题
    if class_names:
        title = f"Pred: {class_names[target_image_pred_label.cpu()]} | Prob: {target_image_pred_probs.max().cpu():.3f}"
    else:
        title = f"Pred: {target_image_pred_label.cpu()} | Prob: {target_image_pred_probs.max().cpu():.3f}"

    plt.title(title)
    plt.axis(False)

使用函数进行预测

现在,我们可以使用这个函数来预测我们自己的图像。首先,确保你有所需的组件:模型、图像路径、类别列表和变换。

# 定义图像变换(例如,调整大小以匹配模型输入)
custom_image_transform = torchvision.transforms.Compose([
    torchvision.transforms.Resize((64, 64)),
])

# 调用预测函数
pred_and_plot_image(model=model_1,
                    image_path="path/to/your/custom_pizza_image.jpg",
                    class_names=["pizza", "steak", "sushi"],
                    transform=custom_image_transform,
                    device=device)

运行上述代码后,你将看到图像以及模型对其的预测结果和置信度。


总结与思考

本节课中我们一起学习了如何将自定义图像预测的完整流程封装成一个函数。我们涵盖了从加载图像、预处理、模型推理到结果可视化的所有关键步骤。

这个函数 pred_and_plot_image 是一个强大的工具,它允许你轻松地对任何新图像测试你的模型。通过可视化,你可以直观地评估模型的性能,即使定量指标(如准确率)可能不高。正如我们在示例中看到的,一个在测试集上表现“较差”的模型,有时在特定自定义图像上却能做出正确的预测。

核心要点

  • 代码复用:将常用流程函数化是提高效率和组织代码的好习惯。
  • 数据一致性:确保预测时的数据预处理(如缩放、变换)与模型训练时完全一致。
  • 设备管理:注意将数据和模型放在相同的计算设备(CPU/GPU)上。
  • 可视化验证:始终通过可视化来定性评估模型的预测,这能提供比单纯数字更丰富的见解。

尝试用你自己的图像和不同的模型来使用这个函数吧!观察并分析结果,这是深入理解模型行为的最佳方式。

167:自定义数据集与预测实践 📚

在本节课中,我们将总结自定义数据集的构建方法,并学习如何对自定义图像进行预测。我们将回顾关键概念,并介绍后续的练习资源。


课程总结 🎯

上一节我们实现了对自定义图像的预测。虽然图像较为模糊,且模型在定量评估上表现一般,但定性预测结果尚可。当然,模型性能仍有多种改进方式。

核心要点在于:我们必须进行一系列预处理,确保自定义图像的格式与模型期望的格式一致。这在实际应用中非常常见,例如在图像分类服务中,上传的图像会经过类似预处理流程。


自定义数据预测的三个关键点 ✅

以下是进行自定义数据预测时必须确保的三点,无论处理图像、文本还是音频数据都适用:

  1. 确保数据为正确的数据类型
    在我们的案例中,数据应为 torch.float32

  2. 确保数据与模型位于同一设备
    我们需要将自定义图像移至GPU,因为模型也在GPU上。

  3. 确保数据具有正确的形状
    原始图像形状为 [64, 64, 3](实际应为 [3, 64, 64],即通道优先)。我们需添加批次维度并根据需要调整维度顺序。最终形状为:[batch_size, color_channels, height, width]

具体的数据形状、设备和数据类型取决于你的具体问题和所用框架。


PyTorch核心要点与资源 🔧

PyTorch提供了丰富的内置函数来处理各类数据:

  • torchvision:用于计算机视觉(我们本节课实践的内容)。
  • torchaudio:用于音频处理。
  • torchtext:用于文本处理。
  • torchdata(测试版):另一种数据加载方式,未来值得关注。

如果内置数据加载功能无法满足需求,你可以通过继承 torch.utils.data.Dataset 类来编写自定义数据集类,我们在本课的“选项2”中已经实践过。

机器学习中一个核心挑战是平衡过拟合欠拟合。这需要仔细调整模型与数据,相关研究也大量集中于此。

进行自定义数据预测时,务必警惕三个常见错误:错误的数据类型错误的数据形状错误的数据设备。我们在实践中已经亲身体验了如何为训练好的模型准备自定义图像。


练习与拓展学习 🚀

你的练习时间到了!以下是提供的资源:

  • 练习模板:包含问题、代码框架和注释的Jupyter笔记本。
  • 示例解答:我提供的一种参考实现,但并非唯一或最佳方案。
  • 视频讲解:我在YouTube上直播编写这些解答的过程,包括遇到的错误和调试思路。

我强烈建议你先独立尝试完成练习模板,利用课程笔记、已编写代码和官方文档。如果遇到困难,再参考示例解答或视频。

所有练习和拓展资料都位于课程仓库的“extras/exercises”目录中。


总结 📝

本节课我们一起学习了PyTorch自定义数据集的构建、关键的数据预处理步骤(类型、形状、设备),并对自定义图像进行了预测。我们还回顾了PyTorch的核心数据模块和过拟合/欠拟合的概念。现在,是时候通过实践来巩固这些知识了。

下一节再见!👋

168:模块化编程概念与内容规划 🧩

在本节课中,我们将学习如何将Jupyter Notebook中编写的实验性PyTorch代码,转化为可重用、结构化的Python脚本。我们将探讨模块化编程的核心概念,并规划如何将之前课程中构建的“Food Vision Mini”项目代码进行模块化重构。


概述

在之前的课程中,我们编写了许多有用的代码,例如数据加载、模型构建和训练循环。然而,这些代码通常都存在于单个笔记本文件中。模块化编程的目标是将这些代码组织成独立的、可重用的Python脚本。这样做的好处是,我们可以在不同的项目中轻松复用这些代码,并且可以通过命令行参数灵活地控制训练过程。

上一节我们构建了“Food Vision Mini”模型来识别披萨、牛排和寿司图像。本节中,我们来看看如何将构建该模型的所有代码模块化。

获取帮助的途径

以下是遇到问题时可以遵循的步骤:

  1. 运行代码。如果不确定,就运行代码。
  2. 所有代码资源都可以在Github上找到。如果遇到困难,可以参考这些资源。
  3. 自己动手尝试。编写代码,运行它,观察结果。
  4. 按Shift+Command+空格键可以阅读文档字符串。
  5. 如果仍然卡住,建议搜索相关问题。你会找到Stack Overflow或非常有用的PyTorch文档等资源。
  6. 再次尝试。记住我们的座右铭:如果不确定,就运行代码。
  7. 如果问题依旧,你可以在本课程的PyTorch深度学习讨论页上提问。

什么是“模块化”?

“模块化”的核心思想是代码复用。你可能在笔记本中编写了一些优秀的代码,并想知道是否能在其他地方重用它们。事实上,我们已经在前面的许多笔记本中编写了大量优秀代码。其中很多代码我们已经练习编写了多次。我故意这样做,因为练习代码的最佳方式之一就是编写更多代码。但现在,我们已经练习了编写训练循环、数据加载等代码,我们可能希望重用它们。因此,“模块化”的全部前提就是,你可以重用你那些有用的代码。这就是我们本节要涵盖的内容:将之前笔记本中最有用的代码复用到Python脚本中。

让我们更具体一些。在上一节中,我们构建了“Food Vision Mini”项目。我们加载了一些数据,编写了一些PyTorch代码来加载牛排、寿司和披萨的图像数据。我们构建并训练了一个模型来识别不同的食物。然后我们用模型做了一些预测,尽管预测结果不如本图示显示的那么好,它确实弄错了一些,但这没关系。本节我们要关注的是,将我们用于加载数据、构建模型、训练模型的所有代码转化为一系列Python脚本。

目标目录结构

在本节结束时,我们将得到一个类似这样的目录结构:

  • 一个顶级目录,在本节后命名为 going_modular
  • 一个同样名为 going_modular 的子目录。
  • 在该 going_modular 子目录内,我们将有一系列Python脚本:
    • data_setup.py:用于准备数据。
    • engine.py:用于训练和测试模型。
    • model_builder.py:用于构建我们的PyTorch模型。
    • train.py:主要的训练脚本。
    • utils.py:工具函数。
  • 我们最终还会得到一个 models 目录,用于存放我们训练好的不同PyTorch模型。

我们的数据将采用过去几节中使用过的相同格式,即标准的图像分类格式(披萨、牛排和寿司图像)。但需要注意的是,你可以根据你正在处理的问题,以几乎任何形式自定义这种结构。然而,你通常会在实际项目中发现的PyTorch代码就是类似这样的结构。

核心目标:一行命令训练模型

我们真正想做的是能够用一行代码训练一个PyTorch模型。并且,通过这行代码,最好能传入不同的超参数,例如训练哪个模型、批量大小应该是多少、应该使用什么学习率、应该训练多长时间。

这是一个命令行命令。我们可以运行 python train.py。这是我们的目标脚本。我们想训练某个模型。我们想使用某个批量大小。我们想使用某个学习率。我们想训练若干个周期。这里的这些标志称为参数标志。

以下是一个例子。如果我们想训练上一节中的TinyVGG模型,我们可以这样写:

python train.py --model tiny_vgg --batch_size 32 --learning_rate 0.001 --num_epochs 10

当然,你可能会猜,我们能否在这里添加更多超参数,例如隐藏单元数或层数等。答案是肯定的。这真的可以扩展到20个不同的超参数、30个、10个,这并不重要,它是无限可定制的。

所以,这行代码本质上是在说:使用批量大小32、学习率0.001训练TinyVGG模型10个周期。

实际项目中的模块化实践

正如我之前提到的,这通常就是你在实际项目中找到PyTorch代码的方式。当然,你也会在不同的笔记本中找到它们,但Detectron2文档(Meta/Facebook的使用PyTorch的计算机视觉库)就是一个例子。在那里,你会看到 train_net.py 脚本,它用于在多个GPU上训练神经网络。如果你想用单个GPU训练,只需更改相应的参数标志即可。

TorchVision(PyTorch官方库的一部分)也是如此。这是他们进行目标检测的方式。如果你访问PyTorch/vision/references/detection,你会看到一堆Python脚本:train.pyutils.pyengine.py

PyTorch博客文章中训练最先进计算机视觉模型的部分也使用了类似的方式,他们使用 torchrun 来运行PyTorch脚本。

工作流程:从笔记本到脚本

我想与你分享我的工作流程。这不一定是最佳或唯一的工作流程,它只是一个例子。这是将有用的笔记本代码转化为Python脚本的众多选项之一。

  1. 我通常从Jupyter/Google Colab笔记本开始,因为它们非常适合实验和可视化。
  2. 如果我在一些代码单元格中编写了一些有用的笔记本代码,我会将最有用的代码移动到Python脚本中。

例如,这里可能有一个名为 data_setup.py 的脚本,它包含设置数据的代码:

import os
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

在这个脚本中,我们可能会返回一个训练数据加载器、一个测试数据加载器和类别名称。我们将在接下来的编码部分中学习如何创建这样的文件。

本节的两个部分:单元格模式与脚本模式

本节有两个部分或两个笔记本需要注意。一个是单元格模式,另一个是脚本模式。

  1. 第1部分:单元格模式笔记本。这是笔记本 05_pytorch_going_modular_part_1_cell_mode.ipynb。这与普通的Jupyter笔记本和Google Colab笔记本没有什么不同。我们将从上到下运行它。它只是在单元格中执行Python代码,就像我们在课程其余部分一直在做的那样。
  2. 第2部分:脚本模式笔记本。这是笔记本 05_pytorch_going_modular_part_2_script_mode.ipynb。它将使用Jupyter魔术命令将我们有用的代码转化为Python脚本。

魔术命令是以双百分号 %% 开头的命令。我们将使用 %%writefile 这个命令。例如:

%%writefile going_modular/model_builder.py

这会将此代码单元格中的所有内容写入到 going_modular 目录下名为 model_builder.py 的脚本中。我们稍后会看到这个实际操作。

因此,本节分为两部分。第1部分是一个单元格模式的笔记本。第2部分是脚本模式,它将把单元格模式中的代码转化为Python脚本。

模块化代码结构

我们将遵循我们的工作流程。我们现在已经看过几次了,所以可以快速浏览一下。但我想让你注意的是,这里的每个小部分,因为它们各自具有一定的功能。它们确实相互交互,但都可以被个体化为不同的Python脚本。这就是我们将在本节中看到的内容。

因此,每个部分都可以转化为一个Python脚本:一个用于准备数据,一个用于构建模型,一个用于训练模型(拟合是训练模型的一部分),一个用于评估,一个用于保存模型,等等。

本节涵盖内容

最后,我们广泛地概述一下本节将涵盖的内容:

  • 转换数据以供模型使用(我们已经做了一些)。
  • 使用预构建函数加载自定义数据。
  • 构建“Food Vision Mini”模型来分类披萨、牛排和寿司图像(这是我们关注的重点)。
  • 将上述所有有用的笔记本代码转化为Python脚本。
  • 了解如何从命令行训练PyTorch模型。

虽然这是一个机器学习“烹饪”节目,我们将编写大量代码,但话虽如此,让我们开始编码吧!


总结

本节课中,我们一起学习了模块化编程的核心概念与本节的内容规划。我们明确了模块化的目标是实现代码复用,并规划了如何将“Food Vision Mini”项目的代码重构为一系列独立的Python脚本,最终实现通过单行命令灵活训练模型的目标。接下来,我们将进入实践环节,开始编写这些模块化的脚本。

169:模块化笔记本(第一部分)🚀

在本节课中,我们将学习如何将Jupyter笔记本中的代码转换为可重用的Python脚本文件。这是将实验性代码转化为生产级代码的重要一步。

概述

上一节我们学习了如何构建自定义数据集。本节我们将开始模块化之旅,将笔记本中零散的代码单元格,组织成结构清晰、可复用的Python模块。

什么是模块化?

模块化是指将代码按功能拆分成独立的文件或模块。这样做的好处是提高代码的可读性、可维护性和复用性。在PyTorch项目中,常见的做法是将数据准备、模型定义、训练引擎和工具函数分别放在不同的脚本中。

以下是本部分将使用的两个笔记本:

  • 第一部分(Cell模式):以常规的、从上到下运行的笔记本形式,包含了上一节(04节)所有代码的浓缩版本。
  • 第二部分(Script模式):在第一部分的基础上进行重构,展示了我们最终要达成的模块化目标。

启动Cell模式笔记本

我们首先从第一部分(Cell模式)开始。这个笔记本已经包含了我们所需的所有功能代码。

为了在Google Colab中运行,我们需要复制一份到自己的云端硬盘。

# 在Colab中,通过点击“复制到云端硬盘”按钮,即可创建个人副本

确保运行时使用了GPU加速。

# 在Colab中,选择:运行时 -> 更改运行时类型 -> 选择GPU

代码结构解析

现在,让我们运行并解析这个Cell模式笔记本中的代码。整个流程分为以下几个步骤:

以下是笔记本中按顺序执行的核心步骤:

  1. 获取数据:下载披萨、牛排、寿司的图像数据集,并设置训练和测试目录。
  2. 创建数据加载器:基于数据集创建训练和测试数据加载器(DataLoader)。
  3. 构建模型:实例化一个TinyVGG模型。
  4. 设置设备无关代码:确保模型能在CPU或GPU上运行。
  5. 定义训练与测试步骤:创建train_step()test_step()函数,并为其添加Google风格的文档字符串(Docstring),以说明函数功能。
  6. 组合训练循环:编写train()函数,整合训练和测试步骤,完成模型的训练过程。
  7. 保存模型:创建save_model()函数,将训练好的模型保存到指定路径。
  8. 执行训练与评估:运行代码,训练模型,输出每个epoch的损失和准确率,最后保存训练好的模型。

运行结束后,你会在工作区看到生成的data/目录和models/目录,其中models/里保存了训练好的模型文件。

从Cell到Script的转变目标

目前,所有代码都运行在一个笔记本文件中。我们的目标是将其模块化,最终得到一组Python脚本文件。

我们期望的最终文件结构如下:

  • data_setup.py: 包含数据下载和预处理逻辑。
  • model_builder.py: 包含模型定义(如TinyVGG)。
  • engine.py: 包含训练(train_step)、测试(test_step)和主训练循环(train)函数。
  • train.py: 主执行脚本,整合以上模块,启动训练流程。
  • utils.py: 包含辅助函数,如保存模型(save_model)等。

总结

本节课我们一起运行了一个完整的、端到端的PyTorch训练流程笔记本(Cell模式)。我们回顾了从数据准备、模型构建、训练到保存的每一步,并理解了将所有代码写在一个笔记本中的形式。

下一节,我们将开始动手重构,将这些有用的代码单元格提取出来,逐步转换成上述可复用的Python脚本文件,迈出模块化的第一步。

170:下载数据集 🗂️

在本节课中,我们将学习如何下载并准备一个用于图像分类的数据集。我们将复用之前课程中编写的代码,并将其整理成清晰的步骤,为后续将其转换为可复用的Python脚本打下基础。


概述

上一节我们介绍了“模块化”项目的整体结构。本节中,我们来看看如何获取数据。我们将运行一个代码单元,下载披萨、牛排和寿司的图像数据集,并将其解压到标准格式的目录中。

以下是获取数据的具体步骤。

  1. 导入所需库:首先导入处理压缩文件所需的Python模块。
  2. 设置数据路径:定义存储数据的目标文件夹路径。
  3. 下载数据:如果目标文件夹不存在,则从网络下载数据集压缩包。
  4. 解压数据:将下载的压缩文件解压到指定目录。
  5. 清理文件:删除已解压的原始压缩文件,以节省空间。

详细步骤

1. 导入所需库

我们使用 zipfile 模块来处理压缩文件。

import zipfile

2. 设置数据路径

定义数据将要存储的根目录。

data_path = “./data”

3. 下载并准备数据

检查数据目录是否存在。如果不存在,则执行下载和解压操作。

import os
from pathlib import Path
import requests

# 设置数据路径
data_path = Path(“data/“)
image_path = data_path / “pizza_steak_sushi”

# 如果图像文件夹不存在,则下载数据
if image_path.is_dir():
    print(f“{image_path} 目录已存在,跳过下载。“)
else:
    print(f“{image_path} 不存在,正在创建并下载数据…“)
    image_path.mkdir(parents=True, exist_ok=True)

    # 下载数据集压缩包
    url = “https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip“
    filename = image_path / “pizza_steak_sushi.zip“
    print(“正在下载数据…“)
    with open(filename, “wb“) as f:
        request = requests.get(url)
        f.write(request.content)

    # 解压文件
    print(“正在解压数据…“)
    with zipfile.ZipFile(filename, “r“) as zip_ref:
        zip_ref.extractall(image_path)

    # 删除压缩文件
    print(“清理压缩文件…“)
    os.remove(filename)

4. 设置训练和测试路径

数据解压后,会按照标准图像分类格式组织,即 train/test/ 目录下各有以类别命名的子文件夹。

# 设置训练和测试目录路径
train_dir = image_path / “train“
test_dir = image_path / “test“

train_dir, test_dir

总结

本节课中我们一起学习了如何下载和准备一个图像分类数据集。我们编写代码来自动完成从检查目录、下载压缩包到解压和清理的整个过程。数据集被组织成 traintest 文件夹,每个文件夹内包含以类别(披萨、牛排、寿司)命名的子文件夹,这是一种非常常见的格式。

下一节,我们将以此为基础,开始创建数据集和数据加载器,并学习如何将这部分代码模块化,转换成独立的Python脚本。

171:编写首个数据配置 Python 脚本大纲 📝

在本节课中,我们将学习如何将 Jupyter Notebook 中用于创建数据集和数据加载器的代码,转化为一个可复用的 Python 脚本。我们将使用 Jupyter 的魔法命令来高效地完成这一过程。


从 Notebook 到脚本的转变

上一节我们介绍了如何使用 Torchvision 的数据集和转换来加载数据。本节中,我们来看看如何将这些代码模块化,以便于重用。

我们当前的目标是将以下功能封装成独立的 Python 脚本:

  • 使用 ImageFolder 加载标准格式的图像分类数据。
  • 应用数据转换。
  • 将数据集转换为数据加载器。

使用 Jupyter 魔法命令

为了将代码单元的内容保存为 .py 文件,我们将使用 Jupyter 的一个内置魔法命令:%%writefile。这是一个单元格魔法,意味着它会将整个单元格的内容写入指定的文件。

以下是使用该命令的语法:

%%writefile 文件路径/文件名.py

构建 data_setup.py 脚本

现在,让我们开始编写我们的第一个脚本。我们将创建一个名为 data_setup.py 的文件,并将其保存在 going_modular 目录下。

首先,我们写入一个文档字符串,说明文件的功能:

"""
功能:为图像分类数据创建 PyTorch 数据加载器。
"""

接着,导入必要的库:

import os
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

然后,我们定义核心函数 create_dataloaders。我们将按照 Google Python 风格指南为其编写详细的文档字符串,说明参数和返回值。

def create_dataloaders(
    train_dir: str,
    test_dir: str,
    transform: transforms.Compose,
    batch_size: int,
    num_workers: int = os.cpu_count()
):
    """
    从训练和测试目录创建训练和测试数据加载器。

    参数:
        train_dir: 训练数据目录的路径。
        test_dir: 测试数据目录的路径。
        transform: 要对训练和测试数据执行的转换组合。
        batch_size: 每个数据加载器中每批的样本数量。
        num_workers: 每个数据加载器的工作进程数(整数)。

    返回:
        一个包含以下内容的元组:
            train_dataloader: 训练数据加载器。
            test_dataloader: 测试数据加载器。
            class_names: 目标类别的列表。

    示例用法:
        train_dataloader, test_dataloader, class_names = create_dataloaders(
            train_dir="path/to/train_data",
            test_dir="path/to/test_data",
            transform=some_transform,
            batch_size=32,
            num_workers=4
        )
    """
    # 函数内部代码将在这里实现
    pass

你的挑战 🎯

在下一个视频开始前,你的任务是完成 create_dataloaders 函数内部的代码。你需要整合我们之前在 notebook 中编写的逻辑:

  1. 使用 datasets.ImageFolder 和传入的 transform 创建训练和测试数据集。
  2. 使用 DataLoader 将数据集转换为数据加载器,并利用 batch_sizenum_workers 参数。
  3. 从训练数据集中获取类别名称列表。
  4. 最后,函数应返回训练数据加载器、测试数据加载器和类别名称。

尝试基于之前章节的代码来完成这个函数。我们将在下一节课中一起实现它。


总结

本节课中我们一起学习了如何开始模块化我们的 PyTorch 代码。我们介绍了使用 %%writefile 魔法命令将 Jupyter 单元格保存为 Python 脚本的方法,并构建了 data_setup.py 脚本的基本框架,包括导入语句、函数定义和详细的文档字符串。通过完成这个脚本,我们将拥有一个可重用的工具来为图像分类任务快速设置数据管道。

172:创建PyTorch DataLoader生成脚本 🚀

在本节课中,我们将学习如何将Jupyter Notebook中用于创建数据加载器的代码,重构并保存为一个独立的、可复用的Python脚本。我们将重点关注torch.utils.data.DataLoader的创建过程,并介绍一个能提升GPU数据加载效率的重要参数。


概述

在上一节中,我们为第一个Python脚本dataset.py编写了框架,定义了一个用于创建数据加载器的函数。本节我们将一起填充该函数的逻辑,完成脚本的编写,并学习如何使用它来高效地加载数据。

创建数据加载器函数

我们的目标是创建一个名为create_dataloaders的函数。它接收训练和测试目录路径、数据变换、批次大小和工作进程数作为参数,并返回对应的数据加载器以及类别名称。

以下是该函数的核心逻辑步骤:

  1. 使用ImageFolder创建数据集
    我们使用PyTorch内置的torchvision.datasets.ImageFolder类,它能够自动根据文件夹结构创建数据集。每个子文件夹名将被视为一个类别标签。

    train_data = datasets.ImageFolder(root=train_dir, transform=transform)
    test_data = datasets.ImageFolder(root=test_dir, transform=transform)
    
  2. 获取类别名称
    从训练数据集中提取类别名称列表,这在我们后续构建和评估模型时非常有用。

    class_names = train_data.classes
    
  3. 创建DataLoader
    这是将数据集转换为可迭代数据加载器的关键步骤。DataLoader负责按批次加载数据,并支持打乱顺序、多进程加载等特性。

    train_dataloader = DataLoader(dataset=train_data,
                                  batch_size=batch_size,
                                  shuffle=True,
                                  num_workers=num_workers,
                                  pin_memory=True)
    

理解关键参数:pin_memory=True

在创建DataLoader时,我们设置了一个参数pin_memory=True。这个参数对于使用GPU进行训练时提升速度至关重要。

  • 作用:当pin_memory=True时,数据加载器会将数据预先固定在(pin)主机(CPU)内存的特定区域。这使得后续将数据从CPU内存复制到GPU内存的过程更快,因为避免了额外的内存分配和传输开销。
  • 公式/原理:可以理解为 数据传输时间 ∝ 1 / 内存固定优化。通过固定内存,减少了数据在CPU端准备时的延迟,从而加速了 CPU -> GPU 的数据流。
  • 适用场景:当使用GPU训练且数据量较大时,启用此选项通常能获得明显的性能提升。对于小型数据集,效果可能不明显,但养成使用它的习惯是有益的。

脚本的使用与验证

函数编写完成后,我们将其保存为going_modular/data_setup.py。现在,我们可以在任何地方通过导入这个模块来快速创建数据加载器,而无需重复编写代码。

from going_modular import data_setup

train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(
    train_dir=train_dir,
    test_dir=test_dir,
    transform=data_transform,
    batch_size=32
)

运行上述代码,我们成功获得了数据加载器,并打印出了类别名称:['pizza', 'steak', 'sushi']。这证明我们的脚本工作正常。

总结

本节课中我们一起学习了如何将数据准备代码模块化。我们完成了以下工作:

  1. 实现了create_dataloaders函数,它封装了使用ImageFolder创建数据集和DataLoader的过程。
  2. 深入了解了pin_memory=True参数的作用,它是优化GPU训练数据流水线的一个关键技巧。
  3. 成功创建并验证了我们的第一个模块化脚本data_setup.py,实现了代码的复用。

通过将常用功能脚本化,我们的工作流程变得更加清晰和高效。在下一节中,我们将尝试把定义模型的代码也转化为一个独立的脚本。

173:将模型构建代码转为Python脚本 🚀

在本节课中,我们将学习如何将Jupyter Notebook中的PyTorch模型构建代码转换为独立的Python脚本。这样做的好处是,我们可以将有用的代码模块化,以便在其他项目中重复使用,从而提高开发效率。


概述

上一节我们创建了用于数据设置的Python脚本。本节中,我们来看看如何将模型构建代码也转换为Python脚本。我们将以之前使用过的TinyVGG模型为例,演示如何将其封装成一个可导入的脚本文件。


将模型代码转换为脚本

以下是我们在Notebook中创建的TinyVGG模型代码。我们的目标是将这段代码保存到一个独立的Python文件中。

import torch
from torch import nn

class TinyVGG(nn.Module):
    """
    根据CNN Explainer网站复现的TinyVGG模型架构。
    参数:
        input_shape: 输入图像的通道数。
        hidden_units: 隐藏层的通道数。
        output_shape: 输出类别的数量。
    """
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int) -> None:
        super().__init__()
        self.conv_block_1 = nn.Sequential(
            nn.Conv2d(in_channels=input_shape,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,
                         stride=2)
        )
        self.conv_block_2 = nn.Sequential(
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,
                         stride=2)
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=hidden_units*13*13,
                      out_features=output_shape)
        )

    def forward(self, x: torch.Tensor):
        x = self.conv_block_1(x)
        x = self.conv_block_2(x)
        x = self.classifier(x)
        return x

为了将其转换为脚本,我们使用Jupyter的魔术命令将代码写入文件。

%%writefile going_modular/model_builder.py
import torch
from torch import nn

class TinyVGG(nn.Module):
    """
    根据CNN Explainer网站复现的TinyVGG模型架构。
    参数:
        input_shape: 输入图像的通道数。
        hidden_units: 隐藏层的通道数。
        output_shape: 输出类别的数量。
    """
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int) -> None:
        super().__init__()
        self.conv_block_1 = nn.Sequential(
            nn.Conv2d(in_channels=input_shape,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,
                         stride=2)
        )
        self.conv_block_2 = nn.Sequential(
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units,
                      out_channels=hidden_units,
                      kernel_size=3,
                      stride=1,
                      padding=0),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,
                         stride=2)
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=hidden_units*13*13,
                      out_features=output_shape)
        )

    def forward(self, x: torch.Tensor):
        x = self.conv_block_1(x)
        x = self.conv_block_2(x)
        x = self.classifier(x)
        return x

执行上述代码后,会在going_modular目录下创建model_builder.py文件,其中包含了我们的模型类。


从脚本导入并使用模型

创建脚本后,我们可以在Notebook或其他Python文件中导入并使用它。以下是导入和实例化模型的步骤。

首先,导入必要的库和我们的模型构建脚本。

import torch
from going_modular import model_builder

# 设置设备无关代码
device = "cuda" if torch.cuda.is_available() else "cpu"

# 设置随机种子以确保可重复性
torch.manual_seed(42)

# 从脚本实例化模型
model_1 = model_builder.TinyVGG(input_shape=3,
                                hidden_units=10,
                                output_shape=len(class_names)).to(device)

# 查看模型结构
print(model_1)

接下来,我们可以进行一个前向传播测试,以确保模型工作正常。

# 进行一次虚拟前向传播以验证模型
# 获取一个批次的图像
images, labels = next(iter(train_dataloader))

# 获取单张图像并调整形状以匹配模型输入
single_image = images[0].unsqueeze(dim=0).to(device)

# 进行前向传播
model_1.eval()
with torch.inference_mode():
    pred = model_1(single_image)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/daniel-bourke/img/4993a5005eee8f00d1f2691c1fe178b8_2.png)

print(pred)

如果输出显示预测的张量形状正确,则说明模型从脚本导入并工作正常。


模块化工作流的优势

将代码转换为Python脚本的主要优势在于可重用性和组织性。以下是这种工作流的核心步骤:

  1. 在Notebook中实验:首先在Jupyter Notebook中编写和测试代码。
  2. 封装为函数或类:将工作正常的代码组织成函数或类。
  3. 保存为脚本:使用魔术命令或手动方式将代码保存到.py文件中。
  4. 导入和使用:在其他项目或脚本中导入这些模块,避免重复编写代码。

这种模式特别适合机器学习工程,因为它允许快速原型开发,同时保持生产代码的整洁和可维护性。


总结

本节课中我们一起学习了如何将Notebook中的PyTorch模型代码转换为独立的Python脚本。我们创建了model_builder.py文件来存放TinyVGG模型类,并演示了如何导入和使用它。这种模块化的方法能显著提升代码的复用性和项目的组织性。

在下一课中,我们将把训练循环相关的函数(如train_steptest_steptrain)也封装到名为engine.py的脚本中,进一步推进我们的模块化进程。

174:将模型训练代码转为 Python 脚本 🚀

在本节课中,我们将学习如何将之前编写的模型训练和测试循环代码,封装成一个独立的 Python 脚本。这样做可以避免重复编写代码,提高项目的模块化和可重用性。


概述

在前两节课程中,我们分别创建了 data_setup.pymodel_builder.py 脚本。现在,我们将进入下一步:将训练、测试步骤以及整合它们的训练函数,打包成名为 engine.py 的脚本。

4.1 将训练函数转为脚本

上一节我们介绍了如何构建模型,本节中我们来看看如何将训练逻辑模块化。

我们将创建一个名为 engine.py 的脚本。这个脚本将包含我们之前在 Jupyter Notebook(notebook 04)中编写的 train_steptest_steptrain 函数。为了提升代码的可读性和可维护性,我们为每个函数添加了 Google 风格的文档字符串,并使用了 Python 类型注解作为提示。

以下是创建 engine.py 脚本的步骤:

  1. 导入必要的库:脚本中的函数依赖于一些外部模块,我们必须导入它们。
  2. 复制函数代码:将已编写并测试好的函数代码复制到脚本中。
  3. 保存脚本:使用 Jupyter 的魔法命令将代码块写入 engine.py 文件。

让我们看看具体的实现。

首先,我们需要在脚本顶部进行必要的导入。这些导入确保了我们的函数能够正常运行。

from typing import Dict, List, Tuple
import torch
from tqdm.auto import tqdm

接下来,我们将三个核心函数复制到脚本中。以下是每个函数的简要说明:

  • train_step 函数:负责模型的一个训练周期(前向传播、计算损失、反向传播、优化器更新)。
  • test_step 函数:负责模型的一个测试/评估周期(前向传播、计算损失和准确率)。
  • train 函数:整合上述两个步骤,循环指定的周期数,并输出训练和测试结果。

以下是 engine.py 脚本的完整内容示例:

from typing import Dict, List, Tuple
import torch
from tqdm.auto import tqdm

def train_step(model: torch.nn.Module,
               dataloader: torch.utils.data.DataLoader,
               loss_fn: torch.nn.Module,
               optimizer: torch.optim.Optimizer,
               device: torch.device) -> Tuple[float, float]:
    """
    对模型进行一个训练周期。

    参数:
        model: 要训练的PyTorch模型。
        dataloader: 用于训练数据的DataLoader。
        loss_fn: 损失函数。
        optimizer: 优化器。
        device: 目标设备(如 "cuda" 或 "cpu")。

    返回:
        一个包含训练损失和训练准确率的元组。
    """
    model.train()
    train_loss, train_acc = 0, 0

    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)
        y_pred = model(X)
        loss = loss_fn(y_pred, y)
        train_loss += loss.item()
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        y_pred_class = torch.argmax(torch.softmax(y_pred, dim=1), dim=1)
        train_acc += (y_pred_class == y).sum().item()/len(y_pred)

    train_loss = train_loss / len(dataloader)
    train_acc = train_acc / len(dataloader)
    return train_loss, train_acc

def test_step(model: torch.nn.Module,
              dataloader: torch.utils.data.DataLoader,
              loss_fn: torch.nn.Module,
              device: torch.device) -> Tuple[float, float]:
    """
    对模型进行一个测试周期。

    参数:
        model: 要测试的PyTorch模型。
        dataloader: 用于测试数据的DataLoader。
        loss_fn: 损失函数。
        device: 目标设备(如 "cuda" 或 "cpu")。

    返回:
        一个包含测试损失和测试准确率的元组。
    """
    model.eval()
    test_loss, test_acc = 0, 0
    with torch.inference_mode():
        for batch, (X, y) in enumerate(dataloader):
            X, y = X.to(device), y.to(device)
            test_pred_logits = model(X)
            loss = loss_fn(test_pred_logits, y)
            test_loss += loss.item()
            test_pred_labels = test_pred_logits.argmax(dim=1)
            test_acc += ((test_pred_labels == y).sum().item()/len(test_pred_labels))

    test_loss = test_loss / len(dataloader)
    test_acc = test_acc / len(dataloader)
    return test_loss, test_acc

def train(model: torch.nn.Module,
          train_dataloader: torch.utils.data.DataLoader,
          test_dataloader: torch.utils.data.DataLoader,
          optimizer: torch.optim.Optimizer,
          loss_fn: torch.nn.Module,
          epochs: int,
          device: torch.device) -> Dict[str, List]:
    """
    训练和测试一个PyTorch模型。

    参数:
        model: 要训练和测试的PyTorch模型。
        train_dataloader: 用于训练数据的DataLoader。
        test_dataloader: 用于测试数据的DataLoader。
        optimizer: 优化器。
        loss_fn: 损失函数。
        epochs: 训练周期数。
        device: 目标设备(如 "cuda" 或 "cpu")。

    返回:
        一个包含训练和测试损失及准确率的字典。
    """
    results = {"train_loss": [],
               "train_acc": [],
               "test_loss": [],
               "test_acc": []
    }

    for epoch in tqdm(range(epochs)):
        train_loss, train_acc = train_step(model=model,
                                          dataloader=train_dataloader,
                                          loss_fn=loss_fn,
                                          optimizer=optimizer,
                                          device=device)
        test_loss, test_acc = test_step(model=model,
          dataloader=test_dataloader,
          loss_fn=loss_fn,
          device=device)

        print(
          f"Epoch: {epoch+1} | "
          f"train_loss: {train_loss:.4f} | "
          f"train_acc: {train_acc:.4f} | "
          f"test_loss: {test_loss:.4f} | "
          f"test_acc: {test_acc:.4f}"
        )

        results["train_loss"].append(train_loss)
        results["train_acc"].append(train_acc)
        results["test_loss"].append(test_loss)
        results["test_acc"].append(test_acc)

    return results

创建好脚本文件后,我们可以在其他 Python 文件或 Notebook 中导入并使用它:

from going_modular import engine

# 之后就可以调用 engine.train() 等函数,传入必要的参数
# results = engine.train(model, train_dataloader, ...)

总结

本节课中我们一起学习了如何将模型训练代码模块化。我们创建了 engine.py 脚本,它封装了训练步骤、测试步骤和完整的训练循环。通过这种方式,我们避免了代码重复,使项目结构更加清晰,并且可以轻松地在不同项目中复用这些训练逻辑。

在下一节课程中,我们将尝试创建一个用于保存模型的工具函数,并将其放入 utils.py 脚本中。

175:将模型保存工具函数转为Python脚本 📂

在本节课中,我们将学习如何将之前编写的模型保存功能,整理并保存为一个独立的Python脚本文件。这有助于代码的模块化和重用。


概述

上一节我们创建了 engine.py 脚本,它包含了我们主要的训练和测试逻辑。本节中,我们来看看如何将其中“保存模型”这个实用功能,提取出来并保存到一个名为 utils.py 的独立脚本中。这样做可以让我们的代码结构更清晰,更易于维护。

创建 utils.py 文件

utils.py 在Python项目中通常用于存放各种工具函数。目前我们只有一个“保存模型”的函数,但随着项目增长,这里可以存放更多辅助功能。

以下是创建 utils.py 文件的步骤:

  1. 首先,我们需要导入必要的库。
  2. 然后,将我们之前编写的 save_model 函数复制进去。
  3. 最后,保存文件。

让我们开始操作。

编写 utils.py 脚本

我们需要确保脚本中导入了所有必需的模块。对于 save_model 函数,我们需要导入 torch

以下是 utils.py 文件的内容:

"""
一个包含用于PyTorch模型训练的各种工具函数的文件。
"""

import torch
from pathlib import Path

def save_model(model: torch.nn.Module,
               target_dir: str,
               model_name: str):
    """
    将PyTorch模型保存到目标目录。

    参数:
        model: 要保存的目标模型。
        target_dir: 模型要保存到的目录路径。
        model_name: 保存模型时使用的文件名。应包含“.pth”或“.pt”作为文件扩展名。

    示例用法:
        save_model(model=model_0,
                   target_dir="models",
                   model_name="05_going_modular_tingvgg_model.pth")
    """
    # 创建目标目录
    target_dir_path = Path(target_dir)
    target_dir_path.mkdir(parents=True,
                        exist_ok=True)

    # 创建模型保存路径
    assert model_name.endswith(".pth") or model_name.endswith(".pt"), "model_name应以'.pth'或'.pt'结尾"
    model_save_path = target_dir_path / model_name

    # 保存模型状态字典
    print(f"[INFO] 正在将模型保存到: {model_save_path}")
    torch.save(obj=model.state_dict(),
             f=model_save_path)

代码解析

现在,让我们简要解析一下这段代码:

  • 导入模块:我们导入了 torch 用于模型操作,导入了 pathlib 中的 Path 用于处理文件路径。
  • 函数定义save_model 函数接受三个参数:要保存的模型、目标目录和模型文件名。
  • 目录创建:使用 Path.mkdir() 确保目标目录存在。
  • 路径验证:使用 assert 语句确保文件名以正确的扩展名结尾。
  • 模型保存:使用 torch.save() 将模型的状态字典保存到指定路径。

从实践到模块化

你可能会问,为什么不从一开始就编写这些脚本文件?虽然完全可以那样做,但花费时间一步步编写代码有它的价值。通过先在笔记本中探索、实验和可视化,我们能够深入理解每一行代码的作用。这样,当我们最终将其整理成脚本时,我们清楚地知道里面发生了什么,而不仅仅是复制粘贴。

PyTorch官方团队在其代码库中也采用了类似的方式。他们拥有 train.pyutils.py 等脚本,将功能模块化。我们正在实践的是一个简化版本,但核心原则是相同的:将有用的代码保存到Python文件中以便重用。

下一步挑战

目前,我们的 utils.py 中只有一个工具函数。随着项目复杂度的增加,你可以将更多辅助函数添加进来,例如加载模型的函数、设置随机种子的函数等。

现在,我们已经有了 engine.py(训练/测试逻辑)和 utils.py(工具函数)。一个自然的下一步是创建一个 train.py 脚本,它将整合所有功能:加载数据、初始化模型、进行训练和评估,并最终调用 utils.py 中的函数来保存训练好的模型。

理想情况下,我们最终可以通过在终端运行一条命令,例如 python going_modular/train.py,来完成整个模型的训练流程。


总结

本节课中我们一起学习了如何将模型保存功能模块化。我们创建了一个 utils.py 文件,并将 save_model 函数放入其中,这使我们的代码更加整洁和可复用。记住,模块化是构建可维护和可扩展项目的重要一步。在下一讲中,我们将尝试创建最终的 train.py 脚本来整合所有训练流程。

176:创建单行代码训练脚本 🚀

在本节课中,我们将学习如何将之前编写的多个Python脚本整合起来,创建一个名为 train.py 的训练脚本。这个脚本将利用模块化的代码,实现用一行命令训练一个PyTorch图像分类模型的目标。


概述

到目前为止,我们已经创建了四个Python脚本:data_setup.pyengine.pymodel_builder.pyutils.py。本节的目标是整合所有这些代码,创建一个简洁高效的 train.py 脚本,实现模型的训练、评估和保存。


6.1 训练、评估并保存模型(脚本模式)

上一节我们介绍了各个功能模块的创建,本节中我们来看看如何将它们组合成一个完整的训练流程。

我们将创建一个名为 train.py 的文件,它位于 going_modular 目录下。这个脚本将调用我们之前编写的所有模块化代码。

以下是 train.py 脚本的完整内容:

import os
import torch
from torch import nn
import torchvision
from torchvision import transforms
import data_setup
import engine
import model_builder
import utils
from timeit import default_timer as timer

# 设置超参数
NUM_EPOCHS = 5
BATCH_SIZE = 32
HIDDEN_UNITS = 10
LEARNING_RATE = 0.001

# 设置数据目录
train_dir = "data/pizza_steak_sushi/train"
test_dir = "data/pizza_steak_sushi/test"

# 设置设备无关代码
device = "cuda" if torch.cuda.is_available() else "cpu"

# 创建数据变换
data_transform = transforms.Compose([
    transforms.Resize((64, 64)),
    transforms.ToTensor()
])

# 创建数据加载器并获取类别名称
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(
    train_dir=train_dir,
    test_dir=test_dir,
    transform=data_transform,
    batch_size=BATCH_SIZE
)

# 创建模型
model = model_builder.TinyVGG(
    input_shape=3,
    hidden_units=HIDDEN_UNITS,
    output_shape=len(class_names)
).to(device)

# 设置损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
                             lr=LEARNING_RATE)

# 开始计时
start_time = timer()

# 训练模型
engine.train(model=model,
             train_dataloader=train_dataloader,
             test_dataloader=test_dataloader,
             loss_fn=loss_fn,
             optimizer=optimizer,
             epochs=NUM_EPOCHS,
             device=device)

# 结束计时
end_time = timer()
print(f"[INFO] 总训练时间:{end_time-start_time:.3f} 秒")

# 保存模型到文件
utils.save_model(model=model,
                 target_dir="models",
                 model_name="05_going_modular_script_mode_tiny_vgg_model.pth")

脚本详解

以下是 train.py 脚本中各个部分的功能说明:

  1. 导入模块:我们导入了所有必需的PyTorch模块以及我们自定义的脚本(data_setupenginemodel_builderutils)。
  2. 设置超参数:定义了训练周期数、批次大小、隐藏单元数和学习率。
  3. 设置数据目录:指定了训练和测试数据的路径。
  4. 设备无关代码:自动检测并使用可用的GPU或CPU。
  5. 数据变换:创建了将图像调整大小并转换为张量的变换。
  6. 数据加载器:调用 data_setup.create_dataloaders 函数来创建训练和测试数据加载器,并获取类别名称。
  7. 创建模型:使用 model_builder.TinyVGG 类实例化模型,并将其移动到目标设备。
  8. 损失函数与优化器:为多分类任务设置交叉熵损失函数和Adam优化器。
  9. 计时与训练:开始计时,调用 engine.train 函数进行模型训练,然后结束计时。
  10. 保存模型:最后,使用 utils.save_model 函数将训练好的模型保存到指定目录。

运行训练脚本

要运行这个脚本,你只需要在终端中进入 going_modular 目录,并执行以下命令:

python train.py

脚本将自动开始训练过程,输出每个周期的损失和准确率,并在完成后将模型保存到 models 目录中。


最终项目结构

完成本课后,你的项目目录结构应如下所示:

going_modular/
├── data/
│   └── pizza_steak_sushi/
│       ├── train/
│       └── test/
├── models/
│   └── 05_going_modular_script_mode_tiny_vgg_model.pth
├── data_setup.py
├── engine.py
├── model_builder.py
├── utils.py
└── train.py

这个结构清晰地将数据、代码和模型输出分开,是构建可维护机器学习项目的良好实践。


总结

本节课中我们一起学习了如何将模块化的PyTorch代码整合到一个 train.py 训练脚本中。我们实现了:

  • 设置超参数和数据路径。
  • 利用设备无关代码。
  • 调用自定义模块创建数据加载器和模型。
  • 设置训练循环并计时。
  • 保存训练好的模型。

通过这种方式,我们成功地将多行笔记本代码浓缩成了一个可通过单行命令执行的、高效且可复用的训练脚本。这是迈向专业机器学习工程师的重要一步,它使得代码更易于管理、分享和部署。

你可以尝试将本课的 train.py 脚本中的超参数(如学习率、周期数)改为通过命令行参数传入,这将使脚本更加灵活和强大。

177:模块化总结与练习拓展 🚀

在本节课中,我们将回顾模块化代码的创建过程,并探讨如何通过练习进一步拓展技能。我们将从笔记本代码转向Python脚本,实现一行代码训练模型的目标。

回顾模块化进程

上一节我们创建了train.py脚本,该脚本整合了之前所有章节的代码,实现了用单行命令训练PyTorch模型的功能。

我们完成了从笔记本代码到Python脚本的转换。具体来说,我们将第一部分“模块化入门”中的笔记本代码转换为了脚本模式。

实际工作流程示例

以下是我的典型工作流程:

  1. 使用Google Colab探索不同选项并测试代码可行性
  2. 确认代码有效后,将其转换为Python脚本

在实际应用中,PyTorch代码通常以脚本形式存在,因此熟悉脚本模式非常重要。

待探索内容

目前还有一些未覆盖的内容,例如使用参数标志进行不同超参数训练。这引出了我们的第一个练习。

练习任务

以下是三个拓展练习,帮助你巩固模块化技能:

练习一:数据获取脚本化
get_data.py(第1节)中的代码转换为独立脚本。提示:可参考现有脚本的创建方法。

练习二:超参数命令行传递
使用Python的argparse模块,为train.py添加自定义超参数传递功能。推荐查阅Python官方文档和Real Python的“如何使用argparse构建命令行界面”教程。

练习三:预测脚本创建
基于第4节的自定义图像预测代码,创建名为predict.py的脚本,实现通过文件路径加载保存模型并进行预测的功能。

学习资源

如需练习模板,可在PyTorch深度学习仓库的extras/exercises/05_pytorch_going_modular_exercise_template.ipynb中找到。示例解答位于extras/solutions/目录,我还在近期章节中录制了练习的实时解答视频。

延伸阅读建议

若想深入了解本节内容,强烈推荐查阅补充资料:

  • Python项目结构:学习如何合理组织模块化项目中的文件
  • PyTorch代码风格指南:Igor Susmelj编写的指南,为本课程代码风格提供了重要参考
  • 高级训练脚本示例:研究PyTorch官方仓库中的train.py脚本,了解如何训练最先进的图像分类模型

课程总结

本节课我们一起学习了PyTorch代码模块化的完整流程。我们创建了整合性训练脚本,探讨了实际工作模式,并提供了三个拓展练习来深化理解。虽然当前模型规模较小,但通过模块化代码,我们已经为构建更复杂的训练流程奠定了基础。

下一节我们将进入更令人兴奋的内容:如果单行代码训练模型已经让你感到兴奋,那么如何提升模型性能将是我们的新挑战。敬请期待!

posted @ 2026-03-26 01:40  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报