DLAI-PyTorch-深度学习笔记-全-
DLAI PyTorch 深度学习笔记(全)
001:欢迎与课程概述
在本节课中,我们将要学习PyTorch深度学习专业证书系列的第一门课程——“PyTorch基础”。我们将了解PyTorch在AI领域的重要性,以及本课程如何通过清晰、结构化的路径,帮助你从零开始掌握这一强大工具。
课程概述
PyTorch已成为AI领域应用最广泛的框架之一,是研究人员依赖的工具,工程师构建应用的基石,也是全球学生和专业人士渴望学习的技能。事实上,近期生成式AI革命的很大一部分正是由PyTorch驱动的。
然而,学习PyTorch的路径并不总是清晰。网络上教程众多,对于初学者而言,很难分辨哪些内容相关且高质量。为此,我们精心设计了这条清晰、结构化的学习路径。你将从基础开始,逐步构建更高级的架构。
与专家对话:为什么选择PyTorch?
我们很高兴向您介绍劳伦斯·莫罗尼,他的课程和书籍已经帮助了数百万人入门AI和深度学习。劳伦斯多年来广泛使用PyTorch,并见证了它如何改变我们构建模型的方式。
劳伦斯,是什么让你对教授PyTorch感到兴奋?
我认为,当我最初深入研究PyTorch时,最打动我的一点是:回想过去,我们曾合作制作过一些TensorFlow专项课程。其中最受欢迎的课程之一是关于Keras函数式API的。
Keras函数式API改变了我对模型设计的一切认知,它让你不再局限于设计线性模型。你可以开始构思更复杂的模型结构,例如连体网络和跳跃连接等。
当我开始研究PyTorch时,我发现PyTorch原生就支持这种设计方式。你通过代码设计网络和前向传播过程,这种方式非常简单,并且是默认体验,而非附加功能。这让我感到非常兴奋,因为我可以更简单地开始构建更先进的模型。
这正是我喜欢这门入门课程的原因:我们将立即开始构建。 我们将看到第一个PyTorch模型,编写代码并运行它。我们将观察它如何从数据中学习。到课程结束时,你将实际拥有一个能够识别图像中物体的图像分类器。
过去Keras做得好的一点,现在PyTorch也做到了,那就是让你能够组合构建模块,从而更轻松地组装成更复杂的架构。我对此感到非常兴奋,我们如何能通过你提到的这种基于模块的方法,帮助学习者从零开始,一路进阶到最先进的架构。
我知道很多人使用预训练模型就能完成很多工作。 但在某个阶段之后,我的许多团队会进行非常高级的操作,例如微调模型、微调生成式AI模型,或者在视觉AI模型上实现非常高的性能。因此,我认为在当今AI工作蓬勃发展的世界里,这确实是一项重要的技能。
我还想帮助你理解,即使你刚刚开始学习之旅, 从代码开始,从理解基础开始,并使用像PyTorch这样强大的框架,这真的能为你在这个世界取得成功奠定基础。
关于PyTorch,另一个让我感到愉快的特点是它的易用性实际上让学习变得更有趣。 当你下载开源的PyTorch包,并相对轻松地从Hugging Face获取Transformers库并进行微调时,这个过程本身就充满了乐趣。因此,除了找到工作(这当然非常重要)之外,我发现这种高效也让工作变得更加愉快。
我完全同意。 我认为这种易用性不仅体现在构建模型上,也体现在当出现问题时,你能够更快地发现、修复和调试问题,从而让你能更快速地取得成功。
通过这个专业证书课程,从第一门基础课程开始,最终完成整个专业证书,我相信它将让学习者为就业做好更充分的准备。
希望你喜欢学习PyTorch。让我们进入下一个视频,正式开始学习!

本节课中,我们一起学习了PyTorch深度学习课程的概述,了解了PyTorch在AI领域的关键地位以及本课程的结构化学习路径。我们还聆听了专家对PyTorch易用性和强大功能的见解,为后续的实际编码学习做好了准备。
002:为什么选择PyTorch 🚀
在本节课中,我们将学习为什么PyTorch成为了深度学习领域的主流框架。我们将通过对比传统编程、早期深度学习工具与PyTorch的差异,来理解其核心优势。
概述
深度学习工具的发展经历了从复杂到简单的过程。早期框架为了处理海量计算,设计得十分复杂,而PyTorch的出现改变了这一局面。它的核心原则是:深度学习应该像编写普通Python代码一样简单。
传统编程与机器学习的差异

上一节我们概述了PyTorch的核心理念,本节中我们来看看传统编程与机器学习的根本区别。

在传统编程中,开发者需要编写明确的规则来将输入转化为输出。例如,构建一个推荐系统:
- 传统方法:编写大量
if-else规则。if customer_buys("camera"): recommend("lens") - 问题:规则难以覆盖所有例外情况(例如,商品是礼物,或用户已拥有多个镜头)。
机器学习则采用了不同的范式。你无需编写规则,而是向系统提供输入和输出的示例,系统会自动学习其中的映射关系。
深度学习进一步利用神经网络来实现这一点。例如:
- 提供客户历史记录,神经网络能学习人们的购买趋势。
- 展示猫的图片,神经网络能学习“猫”的特征。
早期深度学习框架的挑战
了解了机器学习的基本思想后,我们来看看实现它的技术挑战,这导致了早期工具的复杂性。
神经网络需要从海量示例中学习,这涉及巨大的计算量。每一张图片、每一次购买记录都需要在整个网络中进行数百万次计算。早期框架为了高效管理这些计算,采用了静态计算图。


你可以将静态计算图想象成一个工厂流水线:
以下是使用静态计算图时面临的主要问题:
- 缺乏灵活性:流水线必须预先完整定义并编译。一旦运行,无法中途修改或插入新步骤。
- 难以调试:必须构建完整的流水线后才能测试。错误信息指向框架内部,而非你的代码。
- 非Pythonic:无法使用普通的Python
if语句或for循环,必须使用框架特殊的、复杂的操作符。 - 输入限制:流水线通常只能处理固定尺寸的输入。
这种复杂性使得即使是最简单的操作也变得十分繁琐。例如,仅仅想查看两个数相加的结果,输出可能是一串难以理解的符号,而不是直观的数值。
PyTorch的解决方案:动态性与Python优先
面对早期工具的种种不便,PyTorch应运而生,其核心原则是:让深度学习感觉像编写普通的Python代码。
PyTorch引入了动态计算图。你只需编写干净、地道的Python代码,PyTorch会在后台自动构建和管理计算图。
以下是PyTorch带来的关键优势:
- 直观的代码:代码看起来和运行起来都像普通Python。例如,加法操作简洁明了:
import torch result = torch.tensor(1) + torch.tensor(2) print(result) # 输出: tensor(3) - 完全的灵活性:你可以随时使用
if语句、for循环,随时修改网络结构,一切都按预期工作。 - 友好的调试:当程序出错时,错误信息直接指向你的代码行。你可以像调试普通Python程序一样,随时中断程序、检查变量。
- 强大的社区:PyTorch拥有一个庞大的社区,研究人员分享代码,常见问题都有解决方案,并且框架在不断改进。
正是这种Python式的简洁、强大的工具链和活跃的社区的完美结合,使PyTorch成为了从学生到顶尖AI研究者的首选工具。
总结

本节课中我们一起学习了:
- 传统编程与机器学习在解决问题范式上的根本区别。
- 早期深度学习框架因采用静态计算图而导致的复杂性和不灵活性。
- PyTorch如何通过动态计算图和Python优先的设计哲学,解决了这些问题,使得深度学习开发变得直观、灵活且高效。
- PyTorch凭借其易用性和强大的社区生态,已成为深度学习领域的首选框架。

现在,你已经了解了为什么选择PyTorch,并准备好开始用它构建你的第一个模型了。让我们开始吧!
003:神经网络的构建模块 🧱
在本节课中,我们将要学习神经网络的基本构建单元——神经元。我们将从一个简单的实际问题出发,理解单个神经元如何工作,以及它如何通过数学运算来学习和预测。这为理解更复杂的神经网络奠定了基础。
从实际问题出发
假设你在一家本地快递公司工作,公司承诺在30分钟内送达订单。上个月你有三次延误,经理很不满意。现在,一个新订单来了,需要配送7英里。你需要判断是否能在30分钟内完成。
神经网络擅长解决这类问题。我们将使用可能的最简单神经网络——仅包含一个神经元——来应对这个挑战。
单个神经元:一个线性方程
复杂的网络本质上是由许多执行相同基本操作的神经元组成的。因此,理解一个神经元的工作原理就为理解所有神经网络打下了基础。


虽然灵感来源于生物学,但此处的神经元只是数学单元。它们是具有可调参数的工具,这些参数会调整以匹配数据中的模式。
这听起来很直观。让我们看一些历史配送数据:5英里耗时22.2分钟,6英里耗时25.6分钟。现在,假设客户需要配送7英里。我们将数据绘制出来。
注意这些点如何遵循一条直线。对这些数据一个好的预测模型就是一条直线。如果你知道这条直线的方程,就可以预测新值。
关键点在于:单个神经元就是一个带有两个参数(权重和偏置)的线性方程。 其数学形式如下:
y = w * x + b
这就是直线的方程。因此,神经元需要做的就是为 w(权重)和 b(偏置)找到正确的值,以创建一条最贴合所有数据的直线。而寻找这些最佳值的过程,就是机器学习中的“学习”。
神经元如何学习最佳直线?

那么,神经元如何找出哪条直线是最佳的呢?让我们从一个粗略的猜测开始。
假设我们初始猜测权重 w=1,偏置 b=10。那么,预测5英里的配送时间为:1 * 5 + 10 = 15 分钟。但实际数据显示5英里耗时22.2分钟,我们的预测误差超过了7分钟。看起来需要更陡的斜率。
让我们尝试 w=3.4,b=5。预测5英里的时间为:3.4 * 5 + 5 = 22 分钟。现在几乎完全吻合,只差0.2分钟,拟合效果好得多。
就像我刚才查看误差并认为需要提高权重和偏置、改变斜率一样,神经网络也做类似的决策,但它使用数学而非直觉。
学习过程:从随机到优化
神经网络从随机的权重和偏置值开始。在PyTorch中,这些被称为模型的参数。


接着,它会查看每个预测值与实际数据相差多远。点偏离越远,总误差就越大。然后,网络将使用微积分来找出调整权重和偏置的方向。本质上,它在问:“如果我稍微增加权重,误差是上升还是下降?”

一旦弄清楚了这些,它就会朝正确的方向迈出一小步,测量新误差,再次微调,并不断重复这个过程。这个过程可能进行数百甚至数千次,但最终它会找到接近最佳可能值的参数。
在PyTorch中,你只需几行代码就能看到这一切发生。
扩展到多个输入
你可能会想,当你开始连接成千上万个甚至数百万个神经元时,数学会变得无比复杂吗?这里有一个令人惊讶的事实:它仍然是线性的。每个神经元仍然在进行相同的基本计算。
我们的配送问题神经元只有一个输入:距离。但如果你想考虑多个因素呢?比如距离、一天中的时间和天气。
一个具有三个输入的神经元只是扩展了相同的模式。它仍然只是一个线性方程,只是有更多的项相加。每个输入都有其独特的权重,它们全部相加,再加上一个偏置值。
y = w1*x1 + w2*x2 + w3*x3 + b
从神经元到网络
神经网络就是神经元连接到神经元。一个层只是一组接收相同输入的神经元。当你将一层的输出连接到下一层的输入时,你就构建了一个网络。
- 输入层:接收原始数据(如距离、时间、天气)。
- 隐藏层:中间的那些层,因为你从不直接设置或查看它们的值。
- 输出层:给出你的预测(如配送时间)。
在PyTorch中,将这些层堆叠在一起非常容易。但目前,让我们继续专注于单个神经元。
总结与展望
本节课中,我们一起学习了神经网络的核心构建模块——神经元。我们了解到:

- 单个神经元本质上是一个线性方程
y = w*x + b。 - 神经网络的“学习”过程就是通过计算误差,并利用微积分不断调整权重
w和偏置b这两个参数,以找到最佳拟合直线的过程。 - 多输入神经元只是线性方程的扩展,每个输入对应一个权重。
- 神经网络通过将这样的神经元分层连接而成。

在下一个视频中,你将看到完整的流程,了解如何从原始数据到部署模型。这是每个PyTorch项目遵循的系统过程。之后,你将构建你的第一个神经网络,并在这个具体的配送问题上训练它。只需几行PyTorch代码,就能让所有这些概念变得生动起来。让我们继续前进。
004:机器学习流程 🚀
在本节课中,我们将要学习构建机器学习模型的完整流程。这个流程是一个系统化的框架,无论你是在预测配送时间,还是未来构建图像分类器,都将遵循相同的六个核心阶段。
上一节我们介绍了神经网络如何从历史数据中学习简单的线性模式。本节中,我们来看看将这个学习过程付诸实践所需的完整步骤。
阶段一:数据获取 📥
一切从数据开始。在训练任何模型之前,你需要收集原始信息并将其组织起来,以便PyTorch能够高效地处理每个数据点。
对于配送时间预测器,数据将来自公司的配送记录。但这里会遇到一个常见难题:数据通常是混乱的。例如,早期记录可能将配送时间写为“22分钟”这样的自由文本,而新记录则使用“22.0”这样的数值。此外,数据中总会出现诸如缺失值、负的配送时间,或者记录显示你以每小时200英里的速度骑行等异常情况。

这种混乱的数据是常态而非例外。在我们的第一个实验中,为了简化,你将从一个已准备好的、干净的配送数据集开始。但理解这些步骤有助于你认识到真实项目需要投入什么,随着课程的深入,你将亲自应对更多此类挑战。
阶段二:数据准备 🧹
获取数据只是第一步。无论你处理的是配送记录、自定义图像还是电子邮件文本,下一个挑战是相同的:你必须清理、转换并组织数据,使其成为模型可以学习的形式。
对于配送数据,这可能意味着处理我们之前提到的错误,例如删除不可能的配送时间或重复条目。它也可能意味着处理时间戳未记录时的缺失值,或者通过将“橡树街123号”这样的地址转换为“8.2英里”这样的距离来构建新特征。
在真实项目中,这个阶段通常花费最多的时间和编写最多的代码,这很正常。大多数模型失败不是因为数学错误,而是因为数据混乱。


阶段三:模型构建 🏗️
现在你的数据已经清理完毕并准备就绪,是时候设计将要从中学习的模型了。
无论你是预测配送时间、分类图像还是分析文本,这一步都是为你的问题选择正确的架构。“架构”指的是结构:你将使用多少个神经元?它们如何连接?模型需要什么类型的层?
对于你的配送预测器,架构再简单不过了:1个神经元,用于学习距离和配送时间之间的关系。在PyTorch中,定义该模型的架构只需一行代码。
model = nn.Linear(1, 1) # 一个简单的线性模型
PyTorch的美妙之处在于,即使是这个简单模型,也将遵循与你在本课程后期将要处理的更复杂模型相同的模式。然而,一个好的模型设计只是一个蓝图,接下来你还需要实际教会它做出好的预测。

阶段四:模型训练 🏋️
这是你的模型开始学习的地方。你将输入示例,例如“配送距离8.2英里,耗时22分钟”或“这个距离12.5英里,耗时31分钟”,模型将逐渐开始找出其中的模式。
在训练阶段,你将学习如何配置使这个过程运作的关键部分:如何衡量预测误差?如何引导模型改进?如何通过训练设置控制其学习速度?然后,你将运行训练循环,由PyTorch完成繁重的计算工作,你的模型则从数据中学习。
但即使训练成功完成,你的工作也尚未结束。现在迎来了真正的考验:你的模型能在新的、未见过的数据上做出好的预测吗?
阶段五:评估与调试 📊

要知道你是否拥有一个真正的好模型,你需要观察它在新数据(即它未训练过的示例)上的表现如何。为此,你将使用一个测试集——这是在训练期间预留的一部分配送数据。
例如,你的模型可能预测15英里的配送需要28分钟,但实际上花了32分钟。你的预测有多频繁是接近的?又有多频繁是相差甚远的?随着课程的推进,你将学习如何在出现问题时检测更深层次的问题并调试模型。但目前,关键问题很简单:你的模型是否足够好,值得你信任?
阶段六:模型部署 🚀
这是最后一个阶段:将你的模型部署到现实世界中供人们使用。但让我们暂时先将这个阶段从流程中移除,当时机成熟时我们再回来讨论它。
现在你已经看到了全貌,准备好深入实践了。在下一个视频中,你将看到即使是我们简单的配送模型,在PyTorch代码中是如何遵循这些确切阶段的。然后在实验环节,你将亲自运行它,并判断是否应该接下那个配送订单。




本节课中我们一起学习了构建机器学习模型的六个核心阶段:数据获取、数据准备、模型构建、模型训练、评估与调试,以及最终的模型部署。理解这个系统化的流程是成功实施任何深度学习项目的基础。
005:构建一个简单的神经网络 🧠
在本节课中,我们将学习如何将神经网络的理论概念映射到实际的PyTorch代码中。我们将从导入必要的库开始,逐步构建一个用于预测配送时间的简单神经网络,并理解其训练过程。
概述
你已经了解了机器学习的工作流程和神经网络的学习原理。现在,让我们看看这一切如何转化为实际的PyTorch代码。我们将从导入库开始,然后准备数据、构建模型、定义训练工具,最后通过训练循环让模型学习。
导入必要的库
首先,我们需要导入PyTorch的核心功能模块。这些导入语句将为我们提供构建和训练神经网络所需的核心工具。
import torch
import torch.nn as nn
import torch.optim as optim
torch 提供了PyTorch的核心功能。torch.nn 包含了构建神经网络的组件。torch.optim 提供了训练这些网络的优化器工具。


准备数据
在真实项目中,数据获取和准备通常是独立的步骤。首先收集原始数据,然后进行清理和格式化。但在此示例中,我们合并了这些阶段,直接使用已清理并准备好的数据。我们从Python列表创建数据。
# 输入特征:配送距离
inputs = torch.tensor([[5.0], [15.0], [25.0], [35.0]], dtype=torch.float32)
# 目标值:实际配送时间
targets = torch.tensor([[20.0], [40.0], [60.0], [80.0]], dtype=torch.float32)
这些数据不仅仅是列表。张量(Tensor) 是针对神经网络所需数学运算进行优化的数据结构。你现在可以将它们视为以模型能理解的方式存储和组织数据的容器。
现在,我们来看看数据的结构。每个数字都被包裹在自己的括号中。这些外层括号代表整个数据集的一个子集,即一个批次(Batch)。每个内层括号是批次中的一个样本。由于我们有四条配送记录,你会看到四个样本,每个都包裹在自己的括号中。因为每次配送只有一个特征(距离),所以每个样本只包含一个值。
但请记住,之前我们讨论过具有多个输入(如距离、时间、天气)的数据。那时,这些括号就变得非常重要,因为每个样本可能包含多个值,每个输入特征对应一个。因此,无论是一个还是数百个特征,这些括号都会告诉PyTorch一个样本在哪里结束,下一个样本在哪里开始。
最后,注意 dtype=torch.float32 这部分。它告诉PyTorch存储什么类型的数字。在本例中,是32位浮点数,非常适合我们的配送时间这类十进制数值。
构建神经网络模型
现在让我们创建你的神经网络。还记得早期深度学习框架中那些僵化的流水线吗?Sequential 是PyTorch的简化版本。它是一个容器,按顺序将数据传递各层,但让你可以轻松更换组件,而无需进行混乱的重新布线。
model = nn.Sequential(
nn.Linear(1, 1)
)
这里你只使用一层,这就是你的单个神经元。从技术上讲,它是一个线性层(Linear Layer)。第一个参数 1 表示它接受一个输入(即我们的距离)。第二个参数 1 表示它只产生一个输出(即我们预测的配送时间)。它会对输入应用一个线性变换,这就是线性层实际所做的全部。
PyTorch使这一切变得简单。像这样的层已内置在框架中,你只需一行代码即可创建。这个神经元将学习最佳的权重(Weight) 和偏置(Bias),将我们的距离映射到配送时间,就像找到一条穿过所有数据点的最佳拟合线。
定义训练工具
你的模型需要两种工具来进行实际学习。第一个是损失函数(Loss Function),我们使用均方误差损失(Mean Squared Error Loss)。它将衡量你的预测有多错误或多正确。例如,如果实际用时60分钟,你预测45分钟,这个误差就比预测58分钟要大。
loss_function = nn.MSELoss()
第二个工具是优化器(Optimizer)。我们使用 SGD(随机梯度下降,Stochastic Gradient Descent)。这个算法负责找出调整权重和偏置的方向,以减少误差。
optimizer = optim.SGD(model.parameters(), lr=0.01)
model.parameters() 是你访问PyTorch中那些值(在本例中就是单个神经元的权重和偏置)的方式。lr 是学习率(Learning Rate)。较小的值意味着你以较小的幅度调整权重和偏置参数,较大的值则意味着进行较大的调整。两种极端情况都可能带来各自的挑战。
如果你对损失函数或优化背后的数学原理感到好奇,你将在模块2中获得更深入的介绍。你也可以在课程资源中找到更多内容,或者探索深度学习专项课程进行全面的深入学习。
训练循环
这是实际发生学习的训练循环。还记得我手动调整权重和偏置以更接近正确直线的过程吗?这段代码正在自动地做同样的事情,而且是数百次。事实上,它将进行500次。每次完整遍历训练数据称为一个周期(Epoch)。在每个周期中,模型将:1. 进行预测;2. 测量这些预测的偏差有多大;3. 调整其内部参数以改进这些预测。
以下是每一行代码的作用:
epochs = 500
for epoch in range(epochs):
# 1. 清除上一轮的梯度
optimizer.zero_grad()
# 2. 进行预测
outputs = model(inputs)
# 3. 计算损失
loss = loss_function(outputs, targets)
# 4. 反向传播计算梯度
loss.backward()
# 5. 更新参数
optimizer.step()
首先,看 optimizer.zero_grad()。你将在课程后面更多地了解这个方法。但现在只需知道,它会清除上一轮训练中计算的所有值。没有它,PyTorch会在多轮中累积调整,这会扰乱你的整个学习过程。
outputs = model(inputs) 这行代码告诉模型使用距离作为输入进行预测。
loss = loss_function(outputs, targets) 将预测值与真实时间进行比较,并计算它们有多错误。
loss.backward() 计算出如何调整权重和偏置以减少该误差。就像之前我说“我需要一个更陡的斜率来预测配送时间”一样,现在它通过背后的微积分完成了。这个过程的技术术语称为反向传播(Backpropagation)。
optimizer.step() 将执行所有这些调整。
进行预测
现在模型已经训练好了,让我们尝试进行预测。
with torch.no_grad():
prediction = model(torch.tensor([[30.0]], dtype=torch.float32))
print(prediction)
with torch.no_grad(): 这行代码告诉PyTorch你不再进行训练,只是进行推理(Inference)。训练在底层需要额外的工作,但对于推理,我们可以跳过所有这些,更高效地运行。
如果模型学会了它应该学的东西,它应该会给你一个好的答案。但你需要在实验课中亲自尝试才能看到结果。
总结


在本节课中,我们一起学习了如何用PyTorch构建一个简单的神经网络。我们从导入库开始,理解了张量作为数据容器的作用。然后,我们使用 nn.Sequential 和 nn.Linear 构建了一个单层神经网络模型。接着,我们定义了均方误差损失函数和随机梯度下降优化器来训练模型。最后,我们剖析了训练循环的每一步:清除梯度、前向传播、计算损失、反向传播和更新参数,并学会了如何在训练完成后使用 torch.no_grad() 上下文管理器进行高效的预测。这为你动手实践,看看你的配送工作是否仍然安全,打下了坚实的基础。
006:激活函数 🧠
在本节课中,我们将要学习神经网络中的一个核心概念:激活函数。我们将了解为什么简单的线性模型无法处理复杂数据,以及激活函数如何为模型引入非线性能力,使其能够学习曲线和更复杂的模式。
从线性到非线性
上一节我们介绍了线性模型在处理简单关系时的局限性。本节中我们来看看当数据关系变得复杂时会发生什么。
你的快递公司从自行车服务扩展到全市范围,开始使用汽车进行长途配送。但随着距离增加,配送时间不再遵循直线关系。这是因为最初的几英里会遇到密集的城市交通,而更远的距离汽车会驶上高速公路,速度更快。这种关系是曲线形的。

解决方案不是简单地增加神经元数量。即使添加多个神经元并将它们组合成一个隐藏层,如果所有操作都是线性的(仅进行加权和加偏置),最终输出仍然是一个线性方程,只能表示直线。
激活函数的作用
为了学习曲线而不仅仅是直线,模型需要比线性变换更多的东西。它需要非线性激活函数。这些函数为模型增加了恰到好处的复杂性,帮助其学习更有趣的模式。在PyTorch中,我们通常将它们逐元素地应用于层中的每个神经元。
神经元首先计算其输入的线性变换:
Z = W * X + b
但随后,我们不直接输出这个原始数值,而是将其传递给一个激活函数,例如:
A = activation_function(Z)
正是这个微小的改变——添加非线性变换——使得你的模型能够学习丰富得多的模式。
认识ReLU激活函数
现在,让我介绍一个非常重要的激活函数:ReLU。它极其简单,却出奇地强大。
以下是ReLU的作用:
- 如果输入为负数,输出0。
- 如果输入为正数,则直接输出该数,不做改变。
在PyTorch中,只需一行代码即可将ReLU添加到你的单神经元模型中。我们使用Sequential层类型,它像一个流水线,让数据按顺序流经所有层。
model = nn.Sequential(
nn.Linear(1, 1), # 线性层
nn.ReLU() # ReLU激活函数
)
数据首先通过线性层,然后通过ReLU进行变换。
ReLU如何帮助建模曲线?
让我们看看将ReLU添加到单个神经元时会发生什么。没有ReLU时,神经元输出W * distance + b,是一条直线。有了ReLU后:
- 当
W * distance + b为负时,输出为0,表现为一条水平线。 - 当其为正时,输出正常跟随直线。
这样,你就得到了一个拐点,行为在此处改变,从而跳出了直线的世界。
这个拐点发生的位置至关重要。当ReLU应用于神经元的输出时,拐点出现在Wx + b = 0处,这实际上给出了x = -b / W。在此点,神经元停止输出0并开始对输入做出响应。
组合多个神经元
城市交通数据的模式不止弯曲一次,它会随着交通状况在不同距离上逐渐变化。如果一个神经元给我们一个拐点,那么多个神经元可能会给我们多个拐点。
每个神经元都有自己的权重和偏置,这意味着每个神经元在不同的距离上被激活。想象三个神经元都接收相同的距离输入:
- 神经元1 在大约3英里处激活(城市交通开始)。
- 神经元2 在大约8英里处激活(进入高速公路)。
- 神经元3 在大约15英里处激活(全速行驶在高速公路上)。
以下是它们的输出组合在一起时发生的情况:
model = nn.Sequential(
nn.Linear(1, 3), # 第一层:1个输入特征,3个神经元
nn.ReLU(), # ReLU激活函数
nn.Linear(3, 1) # 第二层:将3个值组合成1个最终预测
)
我们只定义了两个线性层。在PyTorch中,你只编码计算层。ReLU是一个转换值的函数,在此计数中不算作一个单独的层。
- 第一个线性层是你的神经元组。
1代表一个输入特征(距离),3代表三个神经元。该层输出三个值。 - 每个输出都经过ReLU变换。
- 第二个线性层接收这三个变换后的值,并用一个神经元将它们组合起来,产生一个最终预测(配送时间)。
如果你想使曲线更平滑,只需增加第一层中的神经元数量。只要有足够的神经元,每个神经元学习在何处激活以及贡献的强度,你就可以近似任何平滑曲线。你复杂的城市交通模式终于可以被建模了。
其他激活函数
我们一直在使用ReLU,因为它是现代深度学习的“主力军”。它快速、有效且被广泛使用。但它并不是唯一的激活函数。
以下是其他常见的激活函数:
- Sigmoid:将输出压缩到0到1的范围内,非常适合用于表示概率。
- Tanh:将值映射到-1到+1的范围内,这对许多任务非常有用。
当然,还有很多其他的激活函数。如果你想深入了解,可以参考PyTorch官方文档等优秀资源。但说实话,对于大多数情况,ReLU就是你所需要的全部。

总结
本节课中我们一起学习了激活函数的核心作用。我们了解到,单纯的线性组合无法让神经网络学习复杂模式,而激活函数(如ReLU)引入了非线性,使得网络能够拟合曲线关系。通过组合多个使用激活函数的神经元,神经网络可以近似复杂的现实世界数据模式,例如城市中随距离变化的配送时间。现在,是时候回到实验中去,为你之前的线性模型添加ReLU,最终捕获那些复杂的交通模式了。让我们把这些知识付诸实践吧。
007:张量基础教程 🧮
在本节课中,我们将学习PyTorch张量的核心概念与操作。张量是PyTorch的基础数据结构,理解其形状、数据类型、创建、重塑和索引方法是构建和调试深度学习模型的关键。
张量形状:理解数据的组织方式 📐
上一节我们介绍了张量的重要性,本节中我们来看看如何理解张量的形状。张量的形状描述了数据的组织维度,是避免常见错误的第一步。
当你打印 distances.shape 时,会得到类似 torch.Size([6, 1]) 的输出。这明确告诉你数据是如何组织的:有6个样本(即批次大小),每个样本有1个特征(即每次配送的距离)。形状不匹配是PyTorch中最常见的错误之一,通常与设备和数据类型问题并列。
我们来看一个形状匹配并能正常工作的例子。这里使用你之前训练过的同一个单神经元模型。
# 传入形状为 [6, 1] 的距离张量
model(distances) # 正常运行
它为什么能工作?因为每个样本恰好有一个特征,符合模型的预期。那么那个 6 呢?为什么批次大小不会导致问题?这是因为模型期望第一个维度是批次大小,即模型一次处理的样本数量。
可以将其想象成一叠纸:模型以相同的方式读取每一页,无论这叠纸是6页还是600页。第一个维度是“有多少”,其余维度描述“每个样本是什么样子”。
现在,尝试传入多个特征(如距离、小时、天气)。它会失败,因为模型只为一个输入特征而构建。当你遇到形状不匹配时,PyTorch会告诉你哪里错了,但不会告诉你如何修复。一旦你看到两个形状,通常修复方法就会很明显。

数据类型:确保数值的精确表示 🔢

理解了形状之后,让我们谈谈数据类型。创建张量时,PyTorch会使用默认类型。
如果你想输入整数,会得到 torch.int64。如果包含小数点,则会得到 torch.float32。如果你想明确指定,可以使用 dtype 参数。
# 明确指定数据类型为 float32
torch.tensor([1, 2, 3], dtype=torch.float32)
这可以保证得到 float32,即使你忘记了小数点。或者,你可以使用 .float() 方法将任何张量转换为 float32。
如果混合类型会怎样?过去,当你尝试混合数据类型时,PyTorch会抛出错误。现在不再如此。PyTorch现在会通过类型提升自动处理混合类型。例如,如果你将一个整数张量和一个浮点数张量相加,PyTorch会自动进行转换。
但对于神经网络而言,torch.float32 是最佳选择。它在现代硬件上速度快、精度足够,并且是标准配置。
创建张量:从列表到NumPy数组 🏗️
现在你理解了数据类型,让我们看看创建张量的不同方法。最简单的方法是使用Python列表。
# 从Python列表创建张量
torch.tensor([1, 2, 3, 4, 5])
如果你来自NumPy,PyTorch张量的行为方式几乎完全相同。你可以像这样转换一个NumPy数组:
import numpy as np
numpy_array = np.array([1, 2, 3])
torch_tensor = torch.from_numpy(numpy_array)
但请记住,这会共享内存。如果你改变其中一个,另一个也会改变。
如果你需要快速生成测试数据,可以尝试这些内置模式:
以下是创建特殊张量的方法:
torch.zeros((2, 3)):创建形状为 (2, 3) 的全零张量。torch.ones((2, 3)):创建形状为 (2, 3) 的全一张量。torch.rand((2, 3)):创建形状为 (2, 3) 的随机张量(值在0到1之间)。
重塑张量:匹配模型的输入要求 🔄
一旦有了张量,你经常需要重塑它们以匹配模型的期望。记住,PyTorch模型期望输入具有批次维度,就像我们之前看到的 [6, 1]。第一个数字 6 告诉模型它一次获得多少个样本。
当然,最常见的形状错误之一就是忘记添加那个批次维度。假设你想预测单个订单的配送时间,例如25英里。
single_distance = torch.tensor([25.0]) # 形状为 [1]
这是一个标量,但你的模型期望的形状是 [批次大小, 特征数],最小是 [1, 1]。你可以使用 unsqueeze 来添加维度。
# 添加一个批次维度
batch_ready = single_distance.unsqueeze(0) # 形状变为 [1, 1]
现在它就可以输入模型了。因此,在使用 unsqueeze 之前,一定要检查形状。
如果你需要朝另一个方向操作(即移除维度),可以尝试使用 squeeze()。squeeze() 会移除所有大小为1的维度,非常适合在批处理操作后进行清理。
在调试时,务必经常打印 tensor.shape。这些工具是你防御形状错误的第一道防线。
索引与切片:提取你需要的数据 ✂️
有时你需要从张量中获取特定的值,例如检查预测结果或抓取几个样本。索引和切片可以用于此目的,它们的工作方式就像Python列表一样。
predictions = model(some_input) # 假设 predictions 形状为 [10, 1]
# 获取第一个预测
first_pred = predictions[0] # 形状为 [1]
# 获取前三个预测
first_three_preds = predictions[:3] # 形状为 [3, 1]
但请注意,即使是一个索引值,它仍然是一个张量。所以,如果你想要实际的Python数值,请使用 .item() 将其转换为浮点数。
python_value = first_pred.item() # 得到一个浮点数
但要小心:.item() 只对恰好有一个元素的张量有效。在一个更大的张量上调用它,你会得到一个错误。
到目前为止,我们看的都是每个样本一个特征的情况。对于多个特征,你可以跨两个维度进行索引,如下所示:
# 假设 data 形状为 [batch, features]
first_sample_all_features = data[0, :] # 第一个样本的所有特征
all_samples_first_feature = data[:, 0] # 所有样本的第一个特征
specific_value = data[2, 1] # 第三个样本的第二个特征
总结 📝
本节课中,我们一起学习了PyTorch张量的核心技能。我们探讨了如何理解和检查张量的形状,这是避免常见错误的基础。我们了解了不同的数据类型,特别是 float32 在深度学习中的重要性。我们学习了从列表、NumPy数组或使用内置函数创建张量的多种方法。我们还掌握了如何通过重塑(如使用 unsqueeze 和 squeeze)使张量形状与模型输入要求匹配。最后,我们学习了如何使用索引和切片来提取张量中的特定数据,并使用 .item() 获取Python标量值。


内容很多,但别担心,你不需要记住所有东西。通过实践,这些模式会成为你的第二本能。即使是专家也会经常查阅文档。在接下来的视频中,你将学习PyTorch如何处理逐元素运算和广播,这些是构建模型和转换数据的强大工具。在即将到来的实验课中,你将使用刚刚学到的工具来调试真实的张量错误。
008:张量运算与广播
在本节课中,我们将要学习PyTorch中张量的核心数学运算,特别是元素级运算和强大的“广播”机制。理解这些概念是高效使用PyTorch进行深度学习的基础。
概述
上一节我们介绍了如何创建、重塑和调试张量。本节中,我们来看看PyTorch如何对它们进行计算。我们将从简单的单神经元模型计算开始,逐步深入到处理更复杂数据形状的广播机制。
从基础运算开始
让我们从一个单神经元模型的计算开始:权重 * 距离 + 偏差。
当你需要处理多个距离时,计算过程如下所示。PyTorch的张量数学以元素级方式工作。这意味着每个元素都是独立进行运算的。
你的计算将相同的权重应用于每个距离,然后将相同的偏差加到每个结果上。数学表达式看起来就像普通的Python代码,但PyTorch会高效地一次性对所有元素执行这些操作。
公式:output = weight * distances + bias
这对于标量(单个值)以及具有相同形状的张量都适用。
引入广播机制
但是,如果你有更复杂的数据呢?想象有三个配送任务,每个任务有三个特征:距离、小时和天气。现在你想应用调整因子:距离乘以1.1,时间不变,坏天气乘以5倍惩罚。
创建一个每行重复[1.1, 1.0, 5.0]的张量是可行的,因为张量形状相同,但这很冗余。如果能只指定一次这些调整值就好了。
这就是广播的用武之地。我们知道形状相同的张量会进行逐元素运算。但还记得一个标量如何更新我们距离张量中的每个值吗?当你将一个标量加到一个张量上时,PyTorch会自动将该单个值扩展以匹配每个元素。这就是权重和偏差可以一次性应用于所有距离的方式。这种自动扩展就是广播在起作用。
广播的工作原理
那么,如果我们有一个形状为(1, 1)的张量(例如 [[5]]),你认为它能与我们的形状为(1, 3)的张量(例如 [[1, 2, 3]])一起工作吗?
- 第一个张量形状是
(1, 3):1行,3个值。 - 第二个张量形状是
(1, 1):1行,1个值。
通常,PyTorch要求运算的维度精确匹配,不同的形状会导致错误。但神奇之处在于:当一个维度是1,而另一个维度更大时,PyTorch会自动通过重复值来扩展较小的维度。
所以,我们大小为(1, 1)的张量[[5]]会变成[[5, 5, 5]],以匹配(1, 3)的形状。现在它们就可以相加了。
代码示例:
import torch
A = torch.tensor([[1, 2, 3]]) # shape (1, 3)
B = torch.tensor([[5]]) # shape (1, 1)
C = A + B # B被广播为[[5, 5, 5]],然后与A相加
print(C) # 输出: tensor([[6, 7, 8]])
更复杂的广播示例
让我们看一个展示广播真正威力的更复杂例子。当你组合一个(1, 3)的张量和一个(3, 1)的张量时会发生什么?
PyTorch会查看每个维度:
- 第一个维度:1 对比 3。1扩展为3。
- 第二个维度:3 对比 1。1扩展为3。
因此,两者都变成了(3, 3)的形状。
代码示例:
A = torch.tensor([[1, 2, 3]]) # shape (1, 3)
B = torch.tensor([[10], [20], [30]]) # shape (3, 1)
C = A + B
# A被广播为 [[1,2,3], [1,2,3], [1,2,3]]
# B被广播为 [[10,10,10], [20,20,20], [30,30,30]]
print(C)
# 输出: tensor([[11, 12, 13],
# [21, 22, 23],
# [31, 32, 33]])
广播的实际应用
那么你实际上会如何使用它呢?回到配送调整的例子,你不需要将[1.1, 1.0, 5.0]重复3次,你可以直接这样写:
代码示例:
features = torch.tensor([[10.0, 9, 0.2], # 配送1: 距离, 小时, 天气
[20.0, 10, 0.8], # 配送2
[15.0, 11, 0.5]])# 配送3
adjustments = torch.tensor([1.1, 1.0, 5.0]) # 调整因子
adjusted_features = features * adjustments # 广播发生在这里!
print(adjusted_features)
无需循环,无需手动重复。PyTorch通过广播处理所有事情。

这种模式无处不在:跨批次调整多个特征、组合数据的不同维度、高效应用变换等等。一旦你学会寻找它,你会在深度学习的各个地方看到广播的机会。

模块一结束与总结
恭喜你!在过去的两个视频中,你已经涵盖了基本的张量操作。提供的实验包含更多你可以探索的示例,因为就像任何工具一样,张量需要通过练习才能变得直观。
最重要的是,我们已经完成了模块一的学习。我们首先探索了PyTorch的独特之处,现在你已经掌握了基础:你完成了机器学习流程,训练了你的第一个神经网络,掌握了张量操作,打下了坚实的基础。
接下来是张量实验课,你将在那里练习这些概念,随后是一个评分作业来测试你的技能。然后在模块二中,你将处理分类问题,并更深入地研究神经网络是如何真正学习的。你已经掌握了PyTorch基础,是时候将它们付诸实践了。

本节课中我们一起学习了:
- PyTorch中元素级张量运算的基本原理。
- 广播机制:当张量形状不完全匹配但兼容时,PyTorch自动扩展维度为1的张量以执行运算。
- 广播的实际应用场景,它能极大简化代码,避免不必要的重复数据。
- 通过理解这些核心运算,为后续构建更复杂的神经网络模型奠定了坚实基础。
009:解码秘密信息 🕵️♂️
概述
在本节课中,我们将学习如何使用PyTorch处理图像数据,并构建一个多层神经网络来解码一封难以辨认的手写信件。我们将从更简单的MNIST手写数字数据集开始,掌握处理图像数据的核心流程和PyTorch特定技巧。

从模块1到图像世界
在模块1中,我们使用PyTorch构建了第一个模型,并熟悉了深度学习的基础知识。这为初学者打下了坚实的基础,也为有经验的学员提供了一些PyTorch特有的模式。
现在,让我们来处理一些更有趣的挑战。首先,我需要你们帮助解决一个问题。

我的朋友Andrew给我寄了这封信,但他的字迹和我的一样难以辨认。我一个字都看不懂。这确实很独特。于是我有了一个想法:能否训练一个神经网络来替我阅读这封信?
我拿了这封信,并将其分割成独立的图像,就像这样。然后我尝试构建一个模型来对每个字符进行分类。一旦我能对每个字母进行分类,就应该能够重建整个信息。
但当我运行我的模型时,得到了这样的结果。这并不完全正确。
所以我需要你们的帮助来翻译这封信,但你们需要一些新工具,因为你们将要处理图像数据。这意味着数据量将远超模块1中的规模。这里的每张图像都包含数百甚至数千个像素值。
事实上,在本课程的剩余部分,我们将专注于图像处理。在我们转向更高级的主题之前,这将给你们时间真正掌握机器学习流程。你们将更详细地探索损失函数和优化器,并学习一些PyTorch特有的技术,例如如何管理GPU等设备。

到本模块结束时,你们将构建一个多层神经网络,希望能解码Andrew的信件。但在你们处理这些手写体之前,你们将从一些更简单的内容开始练习。



从MNIST手写数字开始




这些是手写数字。你们可以看到不同书写风格之间存在很大差异。数字可能写得工整、潦草、倾斜等等。虽然你们可能很容易解读这些数字,但如何让神经网络做到同样的事情呢?
你们可能认出来了,这些例子来自一个名为MNIST的数据集。是的,你们可能对此有点不以为然。MNIST确实被大量使用,我理解。但请听我说,从这里开始有一个很好的理由,尤其是当你们刚刚开始PyTorch之旅时。
MNIST是一个非常好的起点,因为它简单,并且其结构使得训练神经网络变得容易。所有图像都是28x28像素的灰度图,整齐居中,并标有0到9的10个数字之一。使用MNIST,你们可以在几分钟内训练出一个有效的模型,而不是几小时。这将帮助你们在学习更复杂的挑战之前掌握核心思想。
学习路径与目标
在接下来的两个视频中,你们将重新审视机器学习流程,并学习PyTorch处理图像的方法。
第一个视频侧重于数据。真实世界的数据集庞大且杂乱,你们需要更好的工具来有效地加载和管理它们。

然后,在下一个视频中,你们将超越顺序模型,构建更具灵活性的模型,同时更仔细地观察训练的实际工作原理。

这些视频将为你们使用MNIST作为垫脚石构建第一个图像分类器奠定基础,并最终迈向构建所谓的卷积神经网络。
所以,让我们深入探索,看看有什么新内容。

总结

本节课中我们一起学习了从处理简单数据到处理复杂图像数据的过渡。我们明确了本模块的目标:构建一个能够解码手写信件的多层神经网络。我们选择了MNIST手写数字数据集作为起点,因为它结构简单、易于训练,是掌握图像分类核心流程的理想选择。接下来,我们将深入学习PyTorch中处理图像数据的工具和方法。
010:机器学习流程概览与PyTorch数据处理(第一部分)
在本节课中,我们将学习机器学习流程中的数据处理环节,并了解PyTorch如何通过其核心工具高效地处理数据,即使面对海量数据集。
概述:数据处理的重要性
在上一模块中,我们了解了机器学习的基本步骤。现在,为了深入理解Andrew的课程内容,我们需要掌握PyTorch的数据处理工具。在接触复杂的图像数据之前,让我们从一个更熟悉的例子开始:模块一中提到的配送数据。处理数百万条配送记录的工具,同样可以处理数百万张图像。一旦你理解了简单数据背后的模式,处理图像数据就会变得清晰很多。
从简单数据到海量数据
假设模块一中的配送公司业务增长了。现在,你需要分析的不是10条配送记录,而是10万条,甚至更多。
此时,如果你尝试一次性加载所有10万条记录,可能会使用类似下面的代码:
# 示例:一次性加载所有数据
data = load_all_records()
加载这些数据时,每一条记录都需要占用你电脑的内存。对于10万条记录,你的电脑或许还能应付。但如果是数百万条记录呢?或者,如果数据中还包含了天气数据、交通模式、司机信息等各种附加信息呢?在这种规模下,你的电脑可能会迅速耗尽内存并崩溃。

这正是为什么我们需要分批加载数据。

PyTorch的数据处理三剑客
一个实用的解决方案是分批处理数据,即将完整数据集分割成更小、更易管理的批次。但分批只是整个流程的一部分。在数据准备好用于训练之前,你需要对其进行处理、格式化,并以模型能够理解的方式提供给它。这就是PyTorch三个核心数据工具发挥作用的地方:Transforms、Dataset 和 DataLoader。它们各自负责流程中的一个关键部分,并且设计得能够很好地协同工作。
1. 数据转换(Transforms)
为了对数据应用转换,你通常会编写如下代码:
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean, std)
])
Transforms是在每个数据点被加载时对其执行的操作,目的是为模型准备数据。Compose 意味着按顺序执行以下操作。
以下是两种最常见的转换:
ToTensor():将你的数据转换为PyTorch张量,并将其数值缩放到0到1之间。Normalize(mean, std):进一步调整这些数值,使其以零为中心,并使用标准差进行缩放。
神经网络对输入数据相当“挑剔”。当所有输入都是较小的数字(理想情况下以零为中心)时,它们的训练效果会好得多。这两个转换正是为此而设计的。目前,你只需知道它们能帮助你的模型更有效地训练。在下一个模块中,你将了解更多关于它们以及其他转换技术的知识。
2. 数据集(Dataset)
接下来,你需要将数据包装在一个 Dataset 对象中。这个对象在被请求时才会从磁盘获取每个样本,而不会一次性预加载所有数据。这是处理海量数据集的秘诀之一。
dataset = SomeBuiltInDataset(..., transform=transform, train=True, download=True)
它负责处理诸如数据在磁盘上的位置、如何加载等问题。参数 train=True 让你可以在训练集和测试集之间进行选择。如果你对此还不熟悉,在讨论模型评估时你会看到更多相关内容。参数 download=True 会在数据不存在时自动下载数据。
你可以简单地通过索引来从数据集中检索一个样本:
sample = dataset[0]
在模块三中,你将学习如何使用 Dataset 类来构建自定义数据集。但现在,我们将坚持使用PyTorch中提供的预构建数据集。
3. 数据加载器(DataLoader)
定义好数据集后,你将使用 DataLoader 来以批次的形式提供数据。
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
它是整个流程的一部分,通过每次从数据集中请求一个批次,使得在海量数据集上进行训练成为可能。batch_size 告诉加载器每次提供多少个样本。你还可以打乱数据顺序,这有助于模型在训练过程中更有效地学习。
完整的数据处理流程
现在,让我们看看完整的数据处理流程是如何运作的。
# 1. 定义数据转换
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(...)])
# 2. 创建数据集
dataset = SomeBuiltInDataset(..., transform=transform, train=True, download=True)
# 3. 创建数据加载器
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
# 4. 在训练循环中使用
for batch in dataloader:
# 将批次数据送入模型进行训练
...
至此,你的数据就准备就绪,可以开始训练了。这种模式能够高效地处理那些原本会压垮你电脑内存的数据集。无论你处理的是成千上万的配送记录,还是数百万张图像,其核心原则保持不变:只在需要时,加载你需要的数据。
总结与展望
本节课中,我们一起学习了PyTorch数据处理流程的核心部分。我们了解了为什么需要分批加载数据,并认识了PyTorch的三个关键工具:Transforms(用于数据预处理)、Dataset(用于封装和按需加载数据)以及DataLoader(用于提供批次数据并支持打乱顺序)。这个流程是高效训练模型的基础。
但是,将数据送入模型仅仅是开始。在下一个视频中,你将处理机器学习流程的其余部分,然后了解损失函数和优化器是如何更新模型的。我们下节课见。


011:模型)🚀
概述
在本节中,我们将学习如何使用PyTorch构建、训练和评估深度学习模型。我们将从模型定义的基础开始,逐步深入到训练循环的细节,并了解如何正确评估模型性能。
模型构建:从Sequential到Module
上一节我们介绍了如何使用数据转换和数据加载器高效管理数据。本节中我们来看看模型构建的核心部分。
在模块1中,你已经见过Sequential的用法。它非常便捷,但PyTorch中更常见且灵活的模式是使用nn.Module来创建自定义模型。以下代码实现的功能与Sequential版本完全相同,但提供了更多控制权。
import torch.nn as nn

class CustomModel(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 128)
self.layer2 = nn.Linear(128, 10)
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
return x
每个PyTorch模块类都包含两个核心部分:
__init__:定义模型中包含的层。这类似于准备你的工具。forward:描述数据如何通过这些层流动。这里定义了数据在模型中的具体路径。
这种模式与Sequential安排数据流的方式类似,但以不同的风格编写。你会在PyTorch代码中频繁看到这种模式。
运行模型:调用与内部机制
你已经定义了forward方法,但如何实际运行它呢?你可能会想到model.forward()。但请回想一下,Sequential本身就是nn.Module的子类,它也有一个forward方法。然而,你运行那个模型时,从未直接调用过forward。对于自定义模型也是如此。
当你调用模型对象并传入数据时(例如 output = model(input_data)),PyTorch所做的不仅仅是运行你的forward方法。它还会进行一些内部检查、跟踪必要的数学运算,并为后续的模型更新做好准备。这就是为什么你总是通过调用模型对象并传入输入数据来运行模型,而不是直接调用model.forward(input_data)。PyTorch会为你处理forward调用,并将其包装在必要的簿记工作中。
关于代码中的 super().__init__() 这一行,它真的必要吗?你可能会认为这只是Python的样板代码,跳过它也许不会出问题。但当你创建模型时,PyTorch需要建立一个系统来跟踪其所有可学习参数(即训练过程中需要更新的权重和偏置)。调用 super().__init__() 实际上就是为你创建了这个跟踪系统。没有它,PyTorch将无处注册你的层。
训练循环:标准顺序至关重要
现在让我们来看看训练部分。你在模块1中已经见过这个训练循环。以下是大多数PyTorch训练循环的核心标准序列:
for epoch in range(num_epochs):
for batch in dataloader:
# 1. 前向传播计算预测值
predictions = model(batch_features)
# 2. 计算损失
loss = loss_fn(predictions, batch_labels)
# 3. 梯度清零
optimizer.zero_grad()
# 4. 反向传播计算梯度
loss.backward()
# 5. 根据梯度更新参数
optimizer.step()
这个标准顺序至关重要。zero_grad() 清除所有旧的计算结果,backward() 计算出改进方向(梯度),step() 根据梯度更新你的模型。


如果你不遵循这个标准顺序,你的训练可能会静默地失败。PyTorch不会抛出错误,但模型无法正常学习。例如:
- 如果你交换了
backward()和step()的顺序,代码可以正常运行,但模型将基于上一个批次的计算结果来更新自己,而不是当前批次。 - 如果你把
zero_grad()放在backward()之后,那么你就丢弃了backward()所做的所有工作。 - 如果你把
zero_grad()放在循环之外,那么梯度计算会不断累积。每个批次的梯度都会加到前一个批次上,导致模型做出本应是微调的巨大调整。
接下来的两个视频将更深入地探讨这些主题。现在,只需记住这个模式很重要,每次训练模型时几乎都希望保持相同的顺序。
模型评估:验证学习成果
你一直在训练模型,但如何知道它是否真的在学习呢?这就是评估的目的:在模型从未见过且未在其上训练过的新数据上测试模型。
这里有两点至关重要:
model.eval():小心,这个方法不是评估你的模型(正如一些人错误认为的那样)。它只是将模型设置为评估模式。你需要这样做,因为它在计算上更高效,并且模型中的某些层在训练和评估期间的行为方式不同。torch.no_grad():这将禁用PyTorch在训练期间进行的额外跟踪。如果你不关闭它,PyTorch即使在不需要时也会继续存储大量细节,这会浪费内存,甚至在验证期间可能导致程序崩溃。
评估最重要的部分是观察模型在新数据上的表现。在训练集上测试就像给学生同一份试卷考两次,学生可能只是记住了答案。你希望看到的是他们能否应用所学知识。
由于本模块主要探讨图像分类,你可以轻松地使用准确率来衡量性能。准确率的计算很直接:统计模型分类正确的次数,然后除以总尝试次数。
准确率公式:
准确率 = (正确预测的数量) / (总预测数量)
例如,如果你的模型在10000个数字中正确预测了9500个,那么准确率就是95%。
最后,如果你打算让模型返回训练状态,别忘了使用 model.train() 切换回训练模式。
总结

本节课中我们一起回顾了完整的机器学习流程:数据加载、模型构建、训练和评估。然而,对于训练部分,我们仅仅触及了表面。在接下来的两个视频中,我们将更深入地了解损失函数和优化器内部发生的事情。
012:损失函数详解 📉
在本节课中,我们将要学习深度学习训练过程中的核心环节之一——损失函数。我们将深入理解其作用、常见类型以及如何根据任务选择合适的损失函数。
概述
在训练神经网络时,你会反复看到三行核心代码。它们写起来简单,但背后执行了复杂的数学运算。PyTorch会为你处理这些复杂计算。本节课,我们将聚焦于这三步中的第一步:测量损失,即评估模型预测的错误程度。
损失函数的作用
每一行代码在训练序列中按顺序协同工作:测量 -> 诊断 -> 更新。
首先,你需要测量预测的错误程度,这就是损失。它是一个汇总了所有错误的单一数值。随后,backward() 函数会诊断问题,分析模型中每个权重对误差的贡献。最后,optimizer.step() 根据这些诊断分数来更新权重,问题越大的权重会得到越大的修正。

本节课,我们专注于第一步:测量损失。
常见的损失函数
你已经见过一种损失函数,称为均方误差,它在预测送达时间这类任务中效果很好。你还将认识一种新的损失函数——交叉熵损失,它用于分类问题。我们将分解这些损失函数的工作原理,并学习如何为你的任务选择正确的损失函数。
每个损失函数都执行相同的基本工作:将模型的预测与真实答案进行比较,然后给出一个数值。数值越高,说明错误越大。
均方误差
回想快递公司的例子,模型根据距离预测送达时间。假设模型做了两个预测:第一个预测是6分钟,但实际时间是4分钟;第二个预测是3分钟,但实际时间是5分钟。
为了计算误差,你用预测值减去目标值。这就像测量猜测与现实之间的距离。那么模型整体表现如何呢?这里的损失是所有预测的平均误差。可以把它看作模型的成绩单:分数越低,说明你的预测平均上越接近现实。
但是,如果你直接平均原始误差(本例中是+2和-2,除以2得到0),看起来像是完美表现,尽管两个预测实际上都是错的。这就是为什么我们要使用均方误差:对差值进行平方可以消除负号。现在,两个错误都被计算在内,并且不会相互抵消。这就是“均方误差”名称的由来。
平方还有另一个好处:它让更大的错误影响更大。误差10分钟远比误差1分钟严重得多。
因此,MSE损失在两方面有帮助:
- 确保所有错误都被计入。
- 对较大错误的惩罚比对较小错误的惩罚更重。
公式:
MSE = (1/n) * Σ (预测值 - 真实值)^2
当你预测连续值时,如距离、温度或价格,均方误差是完美的选择。
交叉熵损失
在本模块中,你要处理分类问题(例如,识别这是什么数字)。这里你需要一个不同的损失函数,我们将使用一种称为交叉熵损失的函数。
使用交叉熵损失时,你的模型不只是选择一个答案。它会为所有可能的答案或类别输出一个置信度分数。
以MNIST数据集为例,你的模型必须在0到9的数字之间做出选择。因此,对于每张图像,它输出10个数字,即每个数字的概率。例如,模型可能70%确信它是3,8%确信它是2,等等。所有这些分数加起来应为100%。
核心思想是:交叉熵损失会惩罚过于自信的错误答案。如果你的模型说它有95%的把握认为这是数字7,但实际上它是3,这就是一个很大的错误,损失会非常高。但如果它只有55%的把握认为这是7,虽然仍然是错的,但没那么自信,因此损失会小一些。
你希望模型对正确答案有信心,对错误答案不确定。交叉熵损失正是为了塑造这种行为。
代码(在PyTorch中):
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(model_output, target_labels)
重要注意事项


以下是选择损失函数时的重要警告:
- 不要混淆你的损失函数。对分类任务使用MSE损失可能有效,但效果会很差,训练会变慢且可能不稳定。
- 如果你对回归任务使用交叉熵损失,它很可能会出错,因为它期望的是概率分布,而不是连续值。
- 同样,不要直接比较不同损失函数的原始数值。例如,MSE是0.08,而交叉熵是2.3,哪个更好?这就像比较两个完全不同的东西,它们尺度不同。只需确保在训练过程中,你选择的那个损失函数的数值在下降即可。
目标是最小化损失。
如何选择损失函数
那么,何时使用哪种损失函数呢?
- 如果你预测的是一个数字,如温度、价格或距离,使用MSE。
- 如果你预测的是一个类别,如数字、动物或单词,使用交叉熵损失。
当然,还有很多其他损失函数,每种都适用于特定情况。如果你感兴趣,可以查看下方资源进一步探索。但目前,了解MSE和交叉熵损失已经可以覆盖深度学习中的大部分任务了。
总结
本节课我们一起学习了损失函数的核心概念。我们了解到损失函数是衡量模型预测错误程度的工具,并重点介绍了两种最常用的损失函数:用于回归任务的均方误差和用于分类任务的交叉熵损失。关键在于根据你的任务类型(预测数值还是类别)来正确选择损失函数。

这就是我们“测量、诊断、更新”序列中的“测量”部分。在下一个视频中,我们将探索PyTorch如何诊断问题并进行权重更新。
013:优化器与梯度 🧠
在本节课中,我们将要学习训练循环中的两个核心步骤:梯度计算与参数优化。我们将了解损失函数如何评估模型预测的准确性,以及如何利用这些信息来改进模型。
上一节我们介绍了损失函数如何衡量预测的对错,这是每个训练循环的第一步。本节中我们来看看接下来的两个关键步骤:反向传播和优化器。
损失评估与原因分析
在通过损失函数衡量了预测的错误程度后,下一步是找出造成这些损失的原因,这正是反向传播的作用。
回想一下神经网络的工作原理。每个神经元接收输入,将每个输入乘以一个权重,加上偏置,然后通过激活函数传递结果。即使在一个小型神经网络中,也可能涉及成千上万个权重和偏置。
以一个简单网络为例:
- 它有 784 个输入。
- 128 个隐藏层神经元。
- 10 个输出类别。
这个网络拥有超过 100,000 个权重和偏置参数。反向传播就像一个侦探,它会检查每一个权重和偏置,并询问:“你对最终的损失贡献了多少?”
理解梯度
这些诊断分数被称为梯度。梯度不仅告诉你哪些参数导致了误差,还告诉你贡献了多少以及方向如何。
- 正值意味着增加该权重会使损失变得更糟。
- 负值意味着增加该权重本可以帮助减少损失。
- 大值表示该参数影响很大。
- 小值表示该参数几乎无关紧要。
这里有一个常见的误解:人们常以为反向传播会直接更新权重。实际上,反向传播只计算梯度,即找出每个权重对总损失的贡献程度。实际的更新操作稍后会在调用 optimizer.step() 时发生。
梯度下降的直观理解
你的目标是最小化损失。可以想象成站在山坡上,试图到达山谷的底部。梯度告诉你当前位置的坡度,即哪边是上坡,哪边是下坡。为了到达谷底,你需要朝下坡方向走,也就是朝着损失更低的方向前进。这就是为什么我们称之为梯度下降。
最简单的实现就是我们一直在使用的随机梯度下降。其策略很直接:
- 如果一个权重的梯度为负,就增加它。
- 如果一个权重的梯度为正,就减少它。
- 梯度大,就做大的调整;梯度小,就做小的调整。
但优化器并非直接减去梯度,它会先用学习率对梯度进行缩放。例如,如果你的梯度是 0.5,学习率是 0.01,那么实际的更新量将是 0.5 * 0.01 = 0.005。
学习率的选择至关重要:
- 过小的学习率:更新步伐极小,需要极长时间才能到达谷底。
- 合适的学习率:稳步前进,高效到达谷底。
- 过大的学习率:更新步伐巨大,可能会在最小值附近来回震荡,甚至无法收敛。
更智能的优化器:Adam
SGD 效果不错,但还有更智能的优化器,它们能为每个权重进行自适应的调整,Adam 就是其中之一。它就像一个助手,知道哪些权重需要大幅调整,哪些只需要微调。Adam 因其可靠、灵活且通常比其它方法更快,已成为一个流行的首选优化器。
但请注意:不要将 SGD 的学习率直接复制给 Adam 使用,因为你的损失可能会爆炸。Adam 的学习率调优方式完全不同。
PyTorch 中还有其他优化器,如 RMSprop、Adagrad 等。但对于大多数项目,SGD 和 Adam 已经足够。
梯度清零的重要性
理解了梯度是每个参数的诊断分数后,zero_grad() 的作用就变得清晰了。每次调用 backward() 时,PyTorch 会将新的梯度累加到已有的梯度上。如果你不调用 zero_grad(),你就不只是在诊断当前批次的参数,而是在累积每一个批次的诊断结果,梯度会不断错误地累加,导致训练崩溃。
因此,你需要在每个训练循环开始时调用 optimizer.zero_grad()。
你可能会问,为什么 PyTorch 默认要累积梯度呢?这种行为对于高级用例非常有用,例如梯度累积或某些自定义的训练计划。但对于包括本课程在内的绝大多数项目,你每次都需要清除这些梯度。
完整的训练循环
现在,你可以理解完整的训练循环了:
- 损失函数:衡量模型预测的对错。
- 反向传播:诊断每个参数对误差的贡献(计算梯度)。
- 优化器:利用这些诊断分数(梯度)来更新权重。
本节课中我们一起学习了:
- 反向传播如何像侦探一样,通过计算梯度来诊断每个模型参数对预测错误的贡献。
- 优化器(如 SGD 和 Adam)如何利用梯度信息,遵循梯度下降的原则来更新模型权重,以最小化损失。
- 学习率在优化过程中的关键作用,以及为什么需要在每个训练步骤前调用
optimizer.zero_grad()来清除累积的梯度。


在开始构建你的第一个分类器之前,还有一个 PyTorch 特有的重要主题需要讨论:设备管理,即如何让代码在 GPU 上运行以实现快速训练。我们下节课见。
014:设备管理 🖥️➡️🎮
在本节课中,我们将要学习PyTorch中一个至关重要的概念:设备管理。理解如何控制数据和计算所在的设备(CPU或GPU),是避免常见错误、高效运行深度学习模型的基础。
概述
当你开始在PyTorch中更多地使用张量和模型时,有一个重要概念需要尽早理解:每个张量和每个模型都存在于一个设备上。这个设备可能是你的CPU,也可能是GPU或其他可用的加速器。关键在于,PyTorch不会自动为你移动数据。如果你的张量和模型不在同一个设备上,你的代码可能无法运行,甚至可能崩溃。
上一节我们介绍了张量和模型的基础操作,本节中我们来看看如何管理它们所在的硬件设备,以确保计算顺利进行。
设备是什么?
每台计算机都有一个CPU。这是PyTorch默认使用的设备,除非你另有指定。CPU是为通用计算设计的,按顺序执行操作。
此外,一些系统还拥有像GPU(图形处理器)这样的加速器。GPU可以极大地加速张量运算,尤其是在模型训练期间。事实上,在GPU等加速器上进行训练,其速度可能比仅使用CPU快10到15倍。因此,如果你的系统有GPU,你几乎总是希望使用它。
如何选择设备?
首先,你需要检查你的系统是否有可用的加速器。PyTorch提供了一个简单的方法:
import torch
# 检查是否有可用的CUDA(GPU)设备
torch.cuda.is_available()

如果上述代码返回True,则PyTorch可以使用GPU来加速计算。
以下是选择设备的一个常见模式:
# 设置设备:如果有GPU则使用CUDA,否则使用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
这是一个安全的默认设置,你在PyTorch代码中会经常看到。这里的"cuda"关键字指的是配备了CUDA工具包的NVIDIA GPU。虽然还有其他选项(如Apple Silicon的"mps"),但CUDA是最常用的,也是本课程实验将使用的。
如何将模型和数据移动到设备上?
一旦定义了你的设备,下一步就是将模型和数据移动到该设备上。
首先,移动你的模型:
model = YourModelClass() # 实例化你的模型
model.to(device) # 将模型的所有参数移动到选定的设备上
这行代码将模型的参数放置到所选设备上。
然后,在你的训练循环中,移动每一批数据:
for batch in dataloader:
data, targets = batch
# 将数据和目标标签都移动到设备上
data, targets = data.to(device), targets.to(device)
# ... 后续训练步骤

如何检查设备位置?

如果你不确定某个对象在哪个设备上,可以随时检查。
对于张量,这很简单:
print(data.device) # 输出例如:device(type='cuda', index=0)
对于模型,情况略有不同。模型本身不在设备上,但它的参数在。因此,你可以检查其中一个参数来了解所有参数的位置:
# 检查模型第一个参数所在的设备
print(next(model.parameters()).device)
如果你遇到设备错误,还应该仔细检查你的目标标签(targets)和模型的输出是否也在同一个设备上。
一个常见的陷阱
当你使用.to()方法时,需要注意:它不会原地(in-place)更改张量,而是会创建一个新的张量。因此,如果你想真正移动张量,需要重新赋值。
# 错误做法:这不会改变原始tensor
tensor.to(device)
# 后续使用 `tensor` 时,它仍在原来的设备上
# 正确做法:重新赋值
tensor = tensor.to(device)
任何时候使用.to(),请确保将结果赋值给你实际会使用的变量。
完整的训练循环示例
以下是一个包含正确设备管理的完整训练循环示例:
# 1. 预先选择设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 2. 实例化模型并一次性移动到设备
model = YourModelClass().to(device)
# 3. 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
# 4. 训练循环
for epoch in range(num_epochs):
for data, targets in train_dataloader:
# 将每一批数据移动到设备
data, targets = data.to(device), targets.to(device)
# 前向传播
outputs = model(data)
loss = criterion(outputs, targets)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
关键步骤是:预先选择设备、一次性移动模型、然后在每个批次中移动数据。这个模式是你用PyTorch编写的每一个训练脚本的基础。
注意GPU内存限制
即使所有东西都在正确的设备上,还有一点需要注意:GPU内存是有限的。如果你的模型和批次大小占用的内存超过了GPU的可用内存,你会看到类似“CUDA out of memory”的错误。
这就是为什么批次大小(Batch Size)很重要:
- 批次太小:训练速度慢。
- 批次太大:可能超出GPU内存导致崩溃。
对于许多系统,32到64之间的批次大小是一个不错的起点,但这取决于你的硬件和模型架构。如果你看到内存错误,首先尝试降低你的批次大小,这是最常见的解决方法。
总结
本节课中我们一起学习了PyTorch中的设备管理。我们了解到:
- 张量和模型需要位于相同的设备(CPU或GPU)上才能进行计算。
- 可以使用
torch.cuda.is_available()检查GPU是否可用,并使用torch.device()设置设备。 - 使用
.to(device)方法将模型和数据移动到目标设备,并注意需要重新赋值。 - 可以通过
.device属性检查张量或模型参数所在的设备。 - 需要注意GPU的内存限制,合理设置批次大小。

早期就正确处理设备管理,可以帮助你避免PyTorch中最令人沮丧的一类错误。当出现问题时,你也将确切地知道该检查什么。

现在你已经了解了如何管理设备。结合目前所学的所有知识,你已经准备好进行整合了。在下一个视频中,你将看到如何在PyTorch中训练你的第一个图像分类器,让整个流程生动起来。
015:图像分类(第一部分)—— 数据准备与模型构建
在本节课中,我们将学习如何将之前学到的知识整合起来,使用PyTorch构建你的第一个图像分类器。我们将使用MNIST手写数字数据集,完成从数据加载、预处理到构建神经网络模型的完整流程。
到目前为止,你已经学习了如何加载数据、管理设备、计算损失和更新权重。现在,让我们将这些知识结合起来,用PyTorch构建你的第一个图像分类器。
你将要处理的是MNIST数据集,也就是之前见过的手写数字。这个数据集包含60000张训练图像和10000张测试图像。每张图像都是28x28像素的灰度图。在完成作业中的Andrew信件任务之前,这是一个完美的热身练习。
数据管道构建
让我们开始编写代码并构建模型。我们将从数据管道开始。
首先,你需要导入torchvision。这是PyTorch的计算机视觉库,内置了像MNIST这样的流行数据集,以及图像处理工具。
以下是构建数据管道的步骤:
-
定义数据转换:我们使用
transforms.Compose来组合一系列预处理步骤。ToTensor():将图像转换为张量,并将像素值从0-255缩放到0-1的范围。Normalize(mean, std):使用数据集的均值和标准差进行归一化,使数据以0为中心。对于MNIST,均值为0.1307,标准差为0.3081。使用相同的值归一化所有图像,可以使数据更一致,有助于模型更快地学习。
-
加载数据集:
- 训练集:
root='./data'指定文件在计算机上的存储位置。train=True表示加载60000张训练图像。download=True表示如果MNIST数据集不存在,则自动下载。transform参数将我们定义的预处理步骤自动应用到每张图像上。 - 测试集:与训练集类似,但使用
train=False来获取10000张测试图像。我们使用相同的转换、存储位置等。torchvision会为你处理所有的下载和组织工作。
- 训练集:
-
创建数据加载器:
- 训练加载器:设置
batch_size=64,意味着每批包含64张图像。shuffle=True表示在每个训练周期,模型都会以不同的随机顺序看到图像。 - 测试加载器:设置
batch_size=1000。这些批次更大,因为我们不需要计算梯度,只是进行测试。注意,测试数据不进行打乱。
- 训练加载器:设置
现在,思考一下为什么训练数据需要打乱而测试数据不需要。数据通常按类别组织。如果不打乱,模型可能会连续看到6000个“0”之后才看到“1”。它可能会学习到错误的模式(例如,前面的批次都是0,后面的批次都是9),而不是真正学习每个数字的特征。打乱可以混合所有数据,确保每个批次都有多样性。但对于测试,模型已经完成学习,我们只是检查它是否能正确识别数字,因此顺序无关紧要。
构建神经网络模型
现在,是时候创建一个神经网络了。你将超越简单的Sequential模型,构建一个自定义架构。
我们来逐步解析这个模型:
-
创建模型类:你创建一个继承自
nn.Module的类。这为你提供了PyTorch所有的神经网络功能。 -
定义层:在类的
__init__方法中定义网络层。nn.Flatten():这是一个新层。它的作用如下:MNIST图像以特定形状的张量形式到达。当PyTorch加载单个MNIST图像时,它给出一个形状为[1, 28, 28]的张量(1个通道,28像素高,28像素宽)。当使用批次训练时(例如batch_size=64),数据形状变为[64, 1, 28, 28]。线性层期望的是平坦的向量(即每个图像是一长串数字,而不是二维网格)。Flatten层的作用就是将每个28x28的图像重塑为一行784个值(因为28 * 28 = 784)。这样,批次数据就从[64, 1, 28, 28]变成了[64, 784]。没有Flatten层,图像数据到达线性层时会发生形状不匹配错误。nn.Linear(784, 128):将这784个像素值转换为128个隐藏特征。nn.ReLU():我们的激活函数,保留正值,将负值置零。nn.Linear(128, 10):将128个特征转换为10个输出。10个输出对应10个数字类别(0到9)。
-
定义前向传播:在
forward方法中定义数据的流动。接收输入,将其展平,然后通过定义的层,最后返回输出。
以下是模型定义的代码示例:
import torch
import torch.nn as nn
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 128),
nn.ReLU(),
nn.Linear(128, 10),
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
现在,你已经准备好了一切:一个加载和预处理MNIST图像的数据管道,以及一个可以处理这些图像的神经网络。
但是,目前这个模型还无法区分0和9。在下一部分,你将看到如何通过训练让这个模型“活”起来。你将学习如何设置优化器、定义训练循环,并观察你的模型识别数字的准确率如何逐步提高。

总结

本节课中,我们一起学习了图像分类任务的第一部分。我们构建了处理MNIST数据集的数据管道,包括使用torchvision进行数据加载、转换(归一化)以及创建数据加载器。接着,我们构建了一个自定义的神经网络模型,它包含Flatten层以适配图像数据,以及线性层和ReLU激活函数来学习特征。现在,我们已经拥有了数据和模型架构,为下一部分的模型训练做好了准备。
016:图像分类模型训练与评估
概述
在本节课中,我们将学习如何使用PyTorch训练和评估一个图像分类模型。我们将从设置训练环境开始,逐步讲解训练循环、损失函数、优化器的使用,以及如何评估模型在未见数据上的性能。
设备选择与模型准备
上一节我们介绍了如何构建数据管道和神经网络架构。本节中,我们来看看如何为训练做好准备。
首先,需要选择运行设备。如果CUDA可用,我们将使用GPU进行加速,否则PyTorch将自动回退到CPU。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
接着,创建模型并将其移动到选定的设备上。请记住,模型和数据必须位于同一设备上,否则训练过程中会出现错误。

model = YourNeuralNetwork()
model.to(device)
定义损失函数与优化器
我们将使用交叉熵损失函数,它专为多分类任务设计,非常适合从0到9中选择一个数字。
loss_fn = nn.CrossEntropyLoss()
优化器选择Adam,并设置学习率为0.001。Adam优化器能够在训练过程中自适应地调整学习率,在梯度噪声较大时进行较大调整,在训练稳定后进行较小修正。
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
构建训练循环
现在,让我们创建一个函数来训练模型一个完整的周期(epoch)。该函数接收五个输入:模型、数据加载器、损失函数、优化器和运行设备。
以下是训练函数的核心步骤:
- 将模型设置为训练模式。
- 初始化跟踪变量,用于累计损失和统计预测正确的样本数。
- 遍历数据加载器中的所有批次(batch)。
- 对每个批次执行前向传播、计算损失、反向传播和参数更新。
def train_one_epoch(model, dataloader, loss_fn, optimizer, device):
model.train()
running_loss = 0.0
correct = 0
total = 0
for batch_idx, (data, target) in enumerate(dataloader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
# 每100个批次打印一次进度
if batch_idx % 100 == 0:
print(f'Batch {batch_idx}, Loss: {loss.item():.4f}, Acc: {100.*correct/total:.2f}%')
观察训练过程,损失值会从较高水平(如0.64)下降,而准确率则会从较低水平(如81%)上升。仅通过一次完整的数据集遍历,模型性能就能得到显著提升。
构建评估函数
训练只是故事的一半,我们还需要在未见过的数据上测试模型的性能。
评估模式与训练模式有两个关键区别:
- 使用
model.eval()将模型切换到评估模式。 - 使用
torch.no_grad()上下文管理器,在评估期间不计算梯度,以节省内存并加速计算。
评估过程相对直接:运行每个批次通过模型,统计预测正确的样本数量,最后返回准确率百分比。
def evaluate(model, dataloader, device):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for data, target in dataloader:
data, target = data.to(device), target.to(device)
output = model(data)
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
accuracy = 100. * correct / total
return accuracy
执行完整训练与评估
现在,我们有了训练函数和评估函数,可以将它们组合起来进行完整的模型训练。
我们将模型训练10个周期。这不仅仅是简单的重复,每个周期模型都会优化其对不同数字(例如区分2和7)特征的理解。
在每个训练周期之后,我们都在测试集上进行评估,以观察模型在未见数据上的表现。这能告诉我们模型是在学习可泛化的模式,还是仅仅在记忆训练数据。
num_epochs = 10
for epoch in range(num_epochs):
print(f'Epoch {epoch+1}/{num_epochs}')
train_one_epoch(model, train_loader, loss_fn, optimizer, device)
test_acc = evaluate(model, test_loader, device)
print(f'Test Accuracy after Epoch {epoch+1}: {test_acc:.2f}%\n')
经过10个周期的训练,你会看到损失变得非常小,而准确率则很高。当测试集上的准确率停止提升时,通常意味着模型在当前设置下已经完成了学习,可能不需要完整的10个周期。

总结
本节课中,我们一起学习了如何使用PyTorch训练和评估一个图像分类模型。我们涵盖了从设备准备、定义损失函数和优化器,到编写训练循环和评估函数的关键步骤。现在,你已经准备好训练你的第一个PyTorch图像分类器了。
017:数据管道简介 🌸
在本模块中,我们将学习如何为深度学习模型构建高效、可靠的数据管道。我们将从一个真实世界的挑战——为植物园构建花卉识别应用——入手,探讨处理非标准、混乱数据集的完整流程。
在前两个模块中,你已经掌握了PyTorch的基础知识,并构建了第一个图像分类器。你学会了加载数据、构建神经网络、训练模型并获得预测结果。这个基础是后续一切工作的基石。现在,我们将进入更有趣的部分。
到目前为止,你处理的是像MNIST这样干净、组织良好的数据集。但实践中,数据集通常更加混乱且充满挑战。这正是你接下来要面对的问题。
项目背景与挑战
一个植物园希望为其游客构建一个花卉识别应用,而你被请来帮忙。在此之前,已有三个不同的团队尝试过。他们都使用了完全相同的神经网络架构、相同的层和参数,但得到了截然不同的结果。






为什么?因为真正的挑战不仅在于模型,更在于数据。无论你的模型多么复杂,如果你不能正确地访问和准备数据,从一开始就注定失败。糟糕的数据处理意味着糟糕的结果,这正是那些团队失败的原因。
现在你接手了这个项目,我们需要确保你不会犯同样的错误。因为即使是世界上最好的模型,也无法拯救一个破碎的数据管道。
数据集初探
植物园提供了牛津102花卉数据集用于训练。当你下载并解压后,会得到一个装满图像的文件夹。打开文件夹,你会看到一堆通用命名的JPEG文件,例如 image_0001.jpg。打开文件,可以看到里面的图像是各种花卉。
在许多数据集中,图像会被整齐地分类到以其类别命名的文件夹中。但这里并非如此。相反,标签被单独存储在一个 .mat 文件中,这是一种来自Matlab的压缩二进制格式。


你需要弄清楚如何从这个可能不熟悉的文件类型中提取标签。这就是你对数据的初步了解。请记住,那些失败尝试之间的唯一区别,就是每个团队处理这些数据的方式。这决定了一个成功应用和一个令人沮丧的失败之间的所有差异。


数据处理流程与潜在问题
那么,处理数据究竟意味着什么?当你处理任何数据集时,都有一个自然的流程,并且每一步都可能出现问题。
以下是数据处理的关键步骤:
- 访问文件并匹配图像与标签:对于牛津花卉数据集,这已经具有挑战性。图像使用通用名称,而标签则隐藏在
.mat文件中。这对初学者并不友好。 - 将图像转换为正确格式:包括正确的尺寸、数据类型和结构,以便你的模型能够从中学习。
- 高效加载数据:需要以批次(batch)的形式加载所有内容,而不是一次加载一张图像,否则训练速度会变得非常缓慢。
问题可能出现在以上任何一步。




而且这些问题并不总是显而易见的。

本模块学习路径



在本模块中,我们将这些问题归纳为三类:访问问题、质量问题和效率问题。之前那些团队出错的原因,可能就出在其中任何一个环节。
我们将系统地使用牛津花卉数据集,逐一解决这些领域的问题。你的主要工具将是PyTorch的 Dataset 和 DataLoader 类。你之前见过它们,但现在我们将深入探讨它们真正能为你做什么。
到本模块结束时,你将掌握构建植物园应用所需的数据管道技能,并能应对未来遇到的任何项目挑战。

总结

本节课我们一起学习了数据管道在深度学习项目中的核心重要性。我们通过一个真实案例看到,即使模型相同,不同的数据处理方式也会导致截然不同的结果。我们初步了解了牛津102花卉数据集的非标准结构,并明确了数据处理流程中的三个主要挑战:访问、质量和效率。在接下来的课程中,我们将使用PyTorch工具逐一攻克这些难题。
018:数据访问 📂
在本节课中,我们将要学习如何为PyTorch构建一个自定义的数据集类。我们将以牛津花卉数据集为例,解决数据文件与标签分离、格式不统一等常见问题,并实现一个能够系统化访问数据的Dataset类。
概述
上一节我们介绍了植物园花卉识别应用因数据处理不当而失败。本节中,我们来看看如何解决第一个主要问题:数据访问。如果无法可靠地加载数据,其他一切都无从谈起。我们将了解混乱数据(如牛津花卉数据集)的问题所在,并学习如何使用PyTorch的Dataset类来解决这些问题。
数据集的挑战
首先,我们下载并查看数据集的结构。下载函数会获取图像文件和标签文件。




我们得到了超过8000张图像,文件名是通用的,例如 image_00001.jpeg。这些文件没有按花卉类型组织,只是一个扁平的数字文件列表。

标签则单独存储在一个 .mat 文件中,这是一种Matlab使用的二进制格式。它将每张图像映射到一个花卉类别。
因此,图像和标签存储在不同的位置,且格式也不同。我们需要一种清晰、系统化的方式来连接它们。
构建自定义Dataset类
在之前的模块中,我们使用过像MNIST这样的预构建数据集。它们已经知道如何找到图像并匹配正确的标签。牛津花卉数据集没有这样的预包装,因此我们需要通过构建一个自定义的Dataset类来告诉PyTorch如何处理它。
一个PyTorch自定义Dataset类需要回答三个问题:
__init__:如何设置?数据在哪里?__len__:有多少个样本?__getitem__:当请求一个样本(例如图像42)时,应该返回什么?
以下是实现这三个方法的关键步骤。


1. __init__ 方法:数据设置
这是设置方法。在这里,我们将告诉自定义数据集在哪里找到所有内容,并加载后续需要的信息。
我们需要处理文件路径,使用SciPy读取Matlab文件,并修正标签索引。关键点在于,我们现在可以访问所有图像路径及其对应的标签。
让我们看看标签数组里有什么。我们有8189个标签,每个图像一个。每个标签是一个从1到102的数字,代表一种花卉类型。看起来前10张图像都被标记为类型1,数据集似乎是按类别组织的。
但是,标签是从1开始的。这是一个问题。我们有102种花卉类型,但PyTorch期望从0开始索引的标签。因此,我们需要将1-102的范围改为0-101。我们通过从每个标签中减去1来修正它。
另外,请注意我们在__init__中没有做什么:我们没有加载任何图像。我们只是设置了稍后查找它们所需的信息。这被称为惰性加载,它至关重要。一次性加载所有8189张图像会占用数GB内存。相反,我们只存储路径和标签,实际的图像加载将在__getitem__中稍后进行。
2. __len__ 方法:样本数量
这个方法很简单:返回数据集中的样本总数。由于每个图像对应一个标签,标签数组的长度就告诉PyTorch预期有多少个样本。PyTorch在训练期间使用这个长度来正确迭代你的数据。
3. __getitem__ 方法:获取样本
这是最重要的方法。其核心思想是:PyTorch给你一个索引,你的工作是返回该索引对应的数据。
对于这个数据集,可能意味着:PyTorch说“给我第42个样本”,你返回第42张花卉图像及其标签(即它代表的花卉类型)。
以下是关键操作:
f”image_{index:05d}.jpg”:将索引格式化为5位数字(用零填充)以匹配文件名(例如image_00042.jpg)。- 使用PIL(Python图像库)的
Image.open()加载图像。 - 从标签数组中获取对应的标签。
这样,当PyTorch请求索引42时,它会得到图像42及其标签。这就是你定义的契约:你定义每个样本的样子,PyTorch将处理何时以及如何频繁地调用此方法。
测试与调试


现在让我们确保我们的数据集确实能工作。哦,我们遇到了一个错误。让我们检查文件夹:图像被命名为 image_00001, image_00002, image_00003 等,编号从1开始,而不是0。所以当我们请求索引0时,我们试图加载 image_00000.jpg,但这个文件不存在。
因此,我们需要在 __getitem__ 方法中修正这个“差一错误”。修正后,再次尝试。
很好!这就是为什么你总是要尽早测试你的数据集。像这样的简单差一错误可能会在以后破坏一切。如果现在一切正常,你的数据就成功地将图像与标签连接起来了。
总结
本节课中我们一起学习了如何为PyTorch构建自定义Dataset类。我们解决了数据与标签分离、标签索引不从零开始以及文件名索引不匹配等实际问题。通过实现 __init__、__len__ 和 __getitem__ 这三个核心方法,并采用惰性加载策略,我们成功构建了一个能够系统化访问超过8000张图像和标签,且不会导致内存崩溃的数据集。


但访问数据只是第一步。在下一节中,你将处理数据质量问题,例如当你的图像具有各种不同的形状、大小和格式时该怎么办。我将向你展示如何构建一个转换管道,为训练准备好数据。
019:数据转换流水线 🛠️
在本节课中,我们将学习如何使用PyTorch的转换流水线来处理原始图像数据,解决尺寸不一、格式错误等问题,使其能够被深度学习模型使用。
概述
上一节我们构建了一个数据集类来加载牛津花卉数据集。然而,访问数据只是第一步。原始图像通常存在各种问题,无法直接用于PyTorch模型。本节将介绍如何使用PyTorch的转换流水线来处理这些问题,包括调整尺寸、格式转换和归一化。
识别数据问题


当我们尝试直接使用数据时,可能会遇到错误。这通常意味着图像具有不同的高度和宽度。PyTorch无法将不同尺寸的图像堆叠成一个批次,因为图像批次需要遵循固定的格式:[batch_size, channels, height, width]。如果维度不匹配,PyTorch会抛出错误。
但尺寸问题可能并非唯一的问题。让我们深入检查一下数据。
这些图像尺寸各不相同,这可能是由于多年来使用不同相机拍摄所致,存在差异是合理的。然而,还存在另一个问题:数据类型。你得到的是PIL(Pillow)图像,但PyTorch模型期望的是张量。因此,我们面临两个不同的问题:一是尺寸不同,二是格式错误。
这正是PyTorch转换系统发挥作用的地方,它能以清晰、一致的方式处理预处理步骤。
解决尺寸不匹配问题


PyTorch提供了可以链接在一起使用的转换操作来解决这个问题,例如 Resize。
Resize 可以将高度和宽度设置为224。但如果你的图像是矩形,会发生什么?它会扭曲图像,拉伸或挤压以适应。更好的方法是使用 Resize 并只指定一个值,这将按比例缩放较短边,同时保持宽高比。然后,你可以使用 CenterCrop 从中间提取一个正方形区域。
注意命名:如果你将你的转换变量命名为 transforms(带s),你可能会覆盖PyTorch的 transforms 模块,导致一些令人困惑的错误。
以下是构建转换流水线时的一个快速调试技巧:单独测试每个步骤,以便清楚地看到发生了什么。像这样逐步检查是调试转换操作的关键,你可以精确地看到数据如何变化,从而及早发现问题。
转换图像格式


接下来,你需要将这些PIL图像转换为张量。你之前见过 ToTensor,但还记得它具体做了什么吗?让我们更仔细地检查一下 ToTensor 的实际功能。
首先,我们的PIL图像尺寸是224x224像素。当你对此图像调用 ToTensor 时,它首先会将图像转换为张量,并具有一些有趣的属性。打印其形状,你会发现它重新排列了维度,为红、绿、蓝三个颜色通道添加了一个通道层。
像素值通常在0到255之间。ToTensor 会缩放这些值,将所有值除以255,使值域落在0到1之间。从张量中的这些样本值可以看出,它们确实都在0到1之间。这是相同像素数据,但处于不同的尺度。
这种缩放有助于网络学习。首先,如果所有特征都在同一尺度上,会很有帮助。试想一下,年龄增加10年是巨大的变化,但身高增加10毫米则不是。然而,模型只会看到两个“10”。通过将所有值缩放到0到1的范围,10%的变化始终是10%的变化,无论单位如何。此外,神经网络会进行大量乘法运算,像255这样的大数字很容易导致数值爆炸。因此,缩放也有助于保持数学运算的稳定性。
添加归一化转换


现在,让我们再添加一个转换:归一化。我们之前见过归一化,但让我们更仔细地看看。
虽然我们的强度值现在落在0到1之间,但如果你的图像大多是明亮的花朵,值会接近1;如果大多是暗色背景,值会聚集在0附近。这留下了大量未使用的空间,将重要的细节挤压到一个狭窄的范围内,使得模型更难发现细微的差异。
使用均值和标准差进行归一化可以更均匀地分布这些值。这里给定的值是预先确定的,在这种情况下效果很好。现在,你的强度值具有了正确的形状、尺度和分布,使网络更容易学习重要的特征。
理解转换顺序
在构建转换流水线时,了解每个转换期望的数据类型会很有帮助。在运行 ToTensor 之前,你处理的是常规图像。在这个例子中,ToTensor 就像一座桥梁。一旦你跨过它,你就进入了张量的领域。有些转换只在一侧有意义。


过去,像 Resize 或 CenterCrop 这样的图像转换只对图像有效。因此,如果你在转换为张量后尝试使用它们,会遇到错误。如今,torchvision 更加灵活。许多图像转换现在可以在我们桥梁的两侧工作,但情况并非总是如此。例如,Normalize 只对张量有效,如果你尝试在图像上运行它,会出错。
因此,请注意你处于 ToTensor 桥梁的哪一侧,以便以正确的方式使用每个转换,避免流水线中的细微错误。
应用完整的转换流水线
现在,让我们用完整的转换流水线更新我们的数据。现在是关键时刻:我们最终能创建之前导致崩溃的批次吗?
完美!我们现在批次中有四张图像。每张图像都有三个颜色通道,并且尺寸都是224x224。数值已正确归一化。你的牛津花卉数据现在应该已准备好进行训练了。
这里有一个快速提示:在构建转换流水线时,你可以从数据中提取一张图像,这将应用转换,你可以检查这张图像以确保一切看起来都正确。如果出现问题,你可以从数据中检索原始图像,然后通过一次应用一个转换来调试问题。这个方法为我节省了无数小时的调试时间。
总结
本节课中,我们一起学习了如何解决阻碍数据集与PyTorch模型协同工作的质量问题。你现在可以一致地调整图像尺寸,将其转换为正确的格式,然后对数值进行归一化以获得最佳训练效果。你的数据现在已经格式正确。


但工作尚未完成。在下一个视频中,我们将构建完整的数据流水线,分割数据,高效地进行批处理,并避免一些常见的性能陷阱。这些步骤对于让你的模型顺利训练以及在未来处理更大规模数据时进行扩展至关重要。
020:DataLoader与数据分割
在本节课中,我们将学习如何将数据集分割为训练集、验证集和测试集,以及如何使用PyTorch的DataLoader来高效地批量加载数据。这是构建可靠机器学习模型的关键步骤。
数据分割的必要性
上一节我们介绍了如何使用transforms来清洗和预处理数据。本节中,我们来看看如何组织这些数据以进行有效的模型训练和评估。
如果更多的数据是好事,为什么我们不直接用全部的花卉图像来训练模型呢?关键在于,你需要知道你的模型在从未见过的新照片上表现如何。这就是为什么需要分割数据。
通常,数据集被分为三个部分:训练集、验证集和测试集。每个部分都有不同的用途。训练集是模型学习知识的来源,它在训练过程中会反复看到这些图像。验证集帮助你在训练过程中检查模型性能,以便调整超参数。测试集是最终的检查,仅在训练完成后使用一次。

在PyTorch中实现数据分割

对于Oxford Flowers数据集,你可以将总共8189张图像大致按以下比例分割:5700张用于训练,1200张用于验证,其余用于测试。以下是PyTorch中的实现方法:
# 假设 data 是你的完整数据集
train_size = int(0.7 * len(data))
val_size = int(0.15 * len(data))
test_size = len(data) - train_size - val_size
train_data, val_data, test_data = random_split(data, [train_size, val_size, test_size])
关键函数是random_split。它会随机将图像分配到每个集合中,从而避免所有雏菊都在一个集合而所有玫瑰在另一个集合的情况。这确保了每个分割都包含102种花卉类型的良好混合。需要明确的是,原始数据集并未被修改,你只是创建了同一数据的三个独立视图。
现在,你拥有了用于训练、验证和测试的三个独立数据集。
使用DataLoader进行批量加载
接下来,你将使用DataLoader来高效地从每个集合中加载批次数据。这在训练期间对性能尤其重要。
还记得批量大小(batch size)等于32吗?这意味着你一次会得到32个样本,而不是一个。有趣的是,遍历DataLoader时,每次迭代会得到一个批次。
以下是检查第一个批次的代码模式:
# 创建DataLoader
train_dataloader = DataLoader(dataset=train_data, batch_size=32, shuffle=True)
# 获取一个批次进行检查
train_features_batch, train_labels_batch = next(iter(train_dataloader))
每个批次会给你两样东西:一个图像批次和一个标签批次。例如,这意味着你有32张图像,每张图像有3个颜色通道,尺寸为224x224。标签张量则给出了该批次中每张图像对应的一个标签。
为什么需要打乱(Shuffle)数据?
我们需要为训练数据的DataLoader添加一个关键参数:shuffle=True。这有两个主要原因:
- 防止顺序偏差:如果你的数据是有序的(例如先全是雏菊,然后是玫瑰,再是向日葵),模型可能会学习将位置与花卉类型关联起来,而不是学习实际的特征。这对于真实世界的预测没有帮助。
- 防止灾难性遗忘:如果模型在许多批次中只看到雏菊,然后只看到玫瑰,它实际上可能会忘记之前学到的关于雏菊的知识。打乱数据会让每个批次都混合不同的花卉类型,帮助模型记住它所学到的一切。
那么,为什么验证集和测试集的shuffle要设置为False呢?这很简单,因为模型不是从这些集合中学习,而只是被评估,所以不需要打乱,之前提到的问题也不会出现。
需要澄清的一点是:shuffle只影响DataLoader提供批次的方式,你的原始数据保持不变。
批次(Batch)与周期(Epoch)的数学关系
让我们理清一个关于批次和周期的常见困惑点。以下是数学计算:
假设你的训练集有5732张图像,批量大小为32。
- 完整批次数量:
5732 // 32 = 179 - 剩余图像数量:
5732 % 32 = 4
你不能有0.125个批次,所以你会得到179个完整的32张图像的批次,以及1个包含4张图像的部分批次,总共180个批次。
一个周期(epoch)意味着你遍历所有180个批次一次,看到数据集中的每张图像恰好一次。
因此,当你训练10个周期时,你就是在遍历所有180个批次10次。由于shuffle=True,你的模型总共会看到每张图像10次,但每个周期的顺序都不同。
常见错误与最佳实践
最后,我们来看看两个可能导致严重问题的常见错误。
错误1:每次调用__getitem__时都从磁盘加载整个文件
# 错误做法:每次获取单个样本都重新加载整个CSV
def __getitem__(self, index):
data = pd.read_csv(‘path/to/data.csv‘) # 每次调用都加载!
# ... 处理数据
return sample
如果你的数据集有5732个条目,这意味着每次检索一个样本时,整个CSV文件(所有条目)都会被重新加载,即每个周期加载5732次。如果训练10个周期,那就是超过57000次对同一文件的完整加载,会造成巨大且不必要的性能下降。修复方法很简单:在__init__中加载一次。
错误2:CUDA内存不足(OOM)错误
如果你遇到CUDA内存不足错误,首先要尝试的是减少批量大小。可以从32甚至16开始,然后逐步增加,以适应可用的内存。
总结


本节课中,我们一起学习了构建PyTorch数据管道的核心步骤。我们了解了将数据分割为训练集、验证集和测试集的重要性,并掌握了使用random_split实现分割的方法。接着,我们深入探讨了DataLoader的作用,它如何通过批量加载数据来提升训练效率,以及为什么需要在训练时打乱数据而在评估时不需要。我们还明确了批次与周期的概念及其数学关系。最后,我们指出了两个常见的编程错误及其解决方案,帮助你构建更健壮、高效的数据加载流程。
现在,你已经为植物园应用构建了一个完整的数据管道,从杂乱的图像文件一直到高效的数据加载。在下一个视频中,我们将快速了解如何调试和加固这个数据管道。
021:构建健壮的数据管道 🛡️
在本节课中,我们将学习如何将一个基础的牛津花卉数据集管道,升级为一个能够应对现实世界挑战的健壮系统。我们将涵盖数据增强、错误处理、管道验证和性能监控等关键技术。
概述:为何需要健壮的管道?
你已经构建了一个可以运行的牛津花卉数据集管道。但这个管道有多健壮呢?如果数据包含损坏的图像怎么办?如果模型需要在不同或较差的光照条件下识别花朵呢?你如何确保在开始长达数小时的训练之前,你的管道是正常工作的?本节视频将教你相关技术,使你的管道更可靠,模型更鲁棒。
数据增强:让模型适应真实世界 🌈
上一节我们介绍了基础的图像加载和预处理。本节中,我们来看看如何通过数据增强来提升模型的泛化能力。


到目前为止,你的模型只见过原始的牛津花卉图像,这些图像都是在相似条件下拍摄的。为了让模型能在更多样化的场景中识别花朵,你可以使用数据增强来生成这些图像的新版本。


为什么这会有帮助呢?如果你的模型只见过在明亮阳光下完美居中的玫瑰,那么当有人给它看一张偏离中心且在阴影中的图像时,它可能会失败。通过从不同角度、不同光照条件以及细微变化中展示同一朵花,你是在教模型关注本质特征(如形状和颜色),而不是图像中花朵的位置等表面细节。
一种方法是将翻转、旋转或亮度调整后的副本保存为新文件。但PyTorch有一个更聪明的解决方案:即时增强。PyTorch不会存储额外的文件,而是在每次加载图像时应用随机变换。例如,在第1个训练周期,第42朵花可能被水平翻转;在第2个周期,它可能被轻微旋转并调暗。这为你的模型提供了无穷无尽的训练数据变体,而无需占用额外的存储空间。
为了实现这一点,我们需要为训练和验证定义不同的变换。
以下是训练变换的代码示例,它包含随机变化:
from torchvision import transforms
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomRotation(degrees=15), # 随机旋转
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 随机颜色抖动
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])


你可以看到,在验证阶段,我们移除了这些增强变换。为什么要在验证时跳过增强呢?这是因为你希望在一致的数据上评估模型。如果验证图像也随机变化,你将无法判断性能变化是真实的模型改进,还是仅仅因为不同的输入造成的。
错误处理:防止单点故障导致崩溃 🚨
现在你已经添加了增强功能以使模型更鲁棒,接下来让我们也使你的管道更健壮。
这里有一个令人沮丧的场景:你的训练一直运行顺利,然后两小时后崩溃了——一张损坏的图像导致一切停止。或者,一张图像在技术上有效,但尺寸太小,以至于破坏了你的变换。这些问题比你想象的更常见,尤其是在现实世界的数据集中。牛津花卉数据集相对干净,但如果文件只是部分下载并损坏了呢?或者有人不小心保存了一张损坏的图像?
让我们构建一个能优雅处理这些问题的数据集。
第一步很简单:不直接崩溃,而是跟踪错误。接下来是重要部分:使 __getitem__ 方法对坏数据具有弹性。它开始时和往常一样,但现在我们可以添加一些安全检查。
以下是增强错误处理的自定义数据集 __getitem__ 方法示例:
import torch
from PIL import Image
import warnings
class RobustFlowerDataset(torch.utils.data.Dataset):
def __init__(self, file_paths, labels, transform=None):
self.file_paths = file_paths
self.labels = labels
self.transform = transform
self.error_log = [] # 用于记录错误
def __getitem__(self, idx):
try:
# 1. 尝试加载图像
img_path = self.file_paths[idx]
image = Image.open(img_path).convert('RGB') # 确保转换为RGB
# 2. 安全检查:跳过过小的图像
if image.size[0] < 32 or image.size[1] < 32:
warnings.warn(f"图像 {img_path} 尺寸过小 ({image.size}),跳过。")
self.error_log.append(f"尺寸过小: {img_path}")
# 递归调用以跳过此图像(注意:需确保不会无限递归)
return self.__getitem__((idx + 1) % len(self.file_paths))
# 3. 应用变换
if self.transform:
image = self.transform(image)
label = self.labels[idx]
return image, label
except Exception as e:
# 4. 记录错误并优雅地跳过
error_msg = f"索引 {idx}, 路径 {self.file_paths[idx]}: {str(e)}"
warnings.warn(error_msg)
self.error_log.append(error_msg)
# 跳过坏数据,返回下一个样本(同样需注意递归边界)
return self.__getitem__((idx + 1) % len(self.file_paths))
def __len__(self):
return len(self.file_paths)
def get_errors(self):
"""训练后查看问题"""
return self.error_log
验证步骤确认文件未损坏。尺寸检查跳过可能破坏变换的过小图像。convert('RGB') 修复可能混入的灰度图像。但关键是:当仍然出现问题时怎么办?我们不直接崩溃,而是精确记录出错内容,打印警告,然后优雅地跳转到下一张图像。对 __getitem__ 的递归调用将保持管道运行,即使文件损坏。最后,我们将添加一个方法,用于在训练后审查问题。
现在,在训练时,你可以预期哪些图像有问题,然后决定是修复它们还是将其排除。有了错误处理,你的管道不会因坏数据而崩溃,并且你将清楚地记录出错情况。
验证增强效果:眼见为实 👀
但还有一个可能损害模型性能的微妙问题:过度激进的增强。我见过一些管道,其增强效果如此极端,以至于花朵变成了无法识别的色块。如果你的模型无法分辨它看到的是什么,它就无法学习特征,也就无法学会分类。
那么,如何知道你的增强是否合理呢?一个简单的方法就是查看它。我们可以构建一个快速的可视化工具。每次调用 data[idx] 时,随机增强都会创建一个新的变体。我们可以利用这一点来显示同一朵花的八个不同版本。
但有一个注意事项:图像是经过标准化的,因此在显示之前我们需要撤销这个操作。以下代码反转了标准化,以便你能看到真实的颜色,而不是扭曲的数值。
import matplotlib.pyplot as plt
import numpy as np
def visualize_augmentations(dataset, idx, num_samples=8):
"""可视化对同一张图像应用的不同增强效果"""
fig, axes = plt.subplots(1, num_samples, figsize=(15, 5))
original_image, label = dataset[idx] # 获取原始(已增强)图像和标签
# 注意:dataset[idx] 每次调用可能因随机增强而不同。
# 为了展示同一原始图像的不同增强,我们需要一个自定义循环。
# 更简单的方法是:从数据集中获取原始图像路径,然后多次应用变换。
# 这里假设我们有一个方法能获取原始PIL图像。
# 以下是一个概念性示例:
# 假设 `get_raw_pil_image(idx)` 返回未变换的PIL图像
# raw_img = dataset.get_raw_pil_image(idx)
# for i in range(num_samples):
# augmented_img = dataset.transform(raw_img) # 每次应用随机变换
# # 反标准化显示
# img_np = augmented_img.numpy().transpose((1, 2, 0))
# mean = np.array([0.485, 0.456, 0.406])
# std = np.array([0.229, 0.224, 0.225])
# img_np = std * img_np + mean
# img_np = np.clip(img_np, 0, 1)
# axes[i].imshow(img_np)
# axes[i].axis('off')
# 由于实现细节取决于数据集结构,此处提供核心反标准化代码:
# 对一个已变换的张量进行反标准化:
def denormalize(tensor):
"""将标准化后的张量转换回可显示的图像格式"""
mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
return tensor * std + mean
# 示例:显示数据集中的多个样本(它们可能已经是不同增强版本)
for i in range(num_samples):
# 注意:直接调用dataset[idx]每次可能得到不同的增强,因为索引可能被随机化。
# 更稳定的做法是创建一个使用固定随机种子的临时变换来可视化。
pass
plt.suptitle(f'数据增强效果示例 (标签: {label})')
plt.show()
# 提示:在实际操作中,你可能需要修改数据集类,使其能返回原始PIL图像或应用特定变换。
你可以测试你的增强效果,以下是要观察的内容:
- 良好状态:如果花朵在每个版本中都清晰可辨,那么你的增强是合适的。
- 增强无效:如果它们看起来完全一样,那么你的增强可能没有起作用。
- 过度增强:如果它们看起来像抽象艺术,那可能太激进了。
- 标准化问题:如果图像全是黑色或有奇怪的颜色,可能是标准化出了问题。


监控管道:洞察训练过程 📊
你已经优雅地处理了错误,并验证了增强效果看起来合理。但这里有一个关键问题:你如何知道你的管道在训练期间实际是如何工作的?你可能因为洗牌错误而有一些从未被访问到的图像,或者某些图像可能被加载的次数远多于其他图像。没有监控,你永远不会知道。
让我们添加一些轻量级的跟踪,以揭示底层发生了什么。这将扩展你的数据集,以跟踪哪些图像被加载、每张图像被访问的频率以及每次加载所需的时间。然后,我们将添加一个简单的方法来审查所有这些统计数据,并在每个训练周期后调用它。
这种监控可以揭示:
- 洗牌错误:某些图像从未被访问。
- 性能问题:加载时间过慢。
- 数据不平衡:某些图像被访问过于频繁。
最好现在就发现这些问题,而不是在训练数天后才发现。
以下是向数据集添加统计跟踪的示例:
import time


class MonitoredFlowerDataset(RobustFlowerDataset):
def __init__(self, file_paths, labels, transform=None):
super().__init__(file_paths, labels, transform)
self.access_count = [0] * len(file_paths)
self.load_times = [0.0] * len(file_paths)
def __getitem__(self, idx):
start_time = time.time()
try:
result = super().__getitem__(idx) # 调用父类方法,包含错误处理
end_time = time.time()
# 记录成功访问(注意:如果父类因错误跳过,可能记录的是跳转后的索引)
# 更精确的记录需要在父类中修改或使用其他方法。
self.access_count[idx] += 1
self.load_times[idx] += (end_time - start_time)
return result
except RecursionError:
# 防止因连续坏数据导致无限递归
raise RuntimeError("数据集中存在过多连续坏数据,请检查。")
except Exception as e:
end_time = time.time()
# 即使出错,也记录尝试时间(可选)
# self.load_times[idx] += (end_time - start_time)
raise e # 或者按照父类逻辑处理
def get_stats(self):
"""获取数据加载统计信息"""
total_accesses = sum(self.access_count)
avg_load_time = sum(self.load_times) / total_accesses if total_accesses > 0 else 0
never_accessed = [i for i, count in enumerate(self.access_count) if count == 0]
often_accessed = [(i, count) for i, count in enumerate(self.access_count) if count > 5] # 假设阈值是5
stats = {
'total_samples': len(self.file_paths),
'total_accesses': total_accesses,
'avg_load_time_per_sample': avg_load_time,
'never_accessed_indices': never_accessed,
'often_accessed_samples': often_accessed,
'error_log': self.error_log
}
return stats


# 在每个epoch后调用 dataset.get_stats() 并打印或记录结果。
总结:从基础到生产就绪 ✅
本节课中,我们一起学习了如何将你的牛津花卉数据集管道从一个基础加载器转变为一个生产就绪的系统。我们主要掌握了以下四个核心技巧:
- 应用数据增强:通过即时随机变换(如翻转、旋转、颜色抖动)增加训练数据的多样性,提升模型在真实世界中的鲁棒性。关键是为训练和验证集使用不同的变换策略。
- 实现错误处理:在数据集的
__getitem__方法中添加异常捕获和容错逻辑(如检查文件完整性、图像尺寸),防止单个损坏样本导致整个训练过程崩溃,并记录错误以供后续分析。 - 可视化验证增强:通过将标准化后的图像反变换并显示,直观检查数据增强的效果是否合理(花朵应保持可识别,既不是完全不变,也不是变成无法辨认的抽象图案)。
- 添加轻量级监控:在数据集中嵌入统计跟踪功能,记录每个样本的访问次数和加载耗时。这有助于早期发现数据洗牌错误、加载瓶颈或样本访问不平衡等问题。
如果你想看到所有这些部分的实际运作,请查看本模块的实验课,在那里你将构建并探索完整的管道,甚至在训练开始之前就将你的数据置于现实世界的测试中。


你已经准备好了数据,现在是时候用它来构建一些了不起的东西了。恭喜你掌握了PyTorch的数据管道构建技巧,我们下个模块再见。
022:滤波器、模式与特征图 🦋
在本节课中,我们将要学习卷积神经网络的基础知识。我们将了解为什么在处理图像数据时,卷积层比普通的线性层更有效,并深入探讨卷积层如何通过滤波器来“观察”图像中的边缘、纹理和模式。
概述
上一节我们介绍了如何处理植物园应用的数据。现在,我们需要扩展应用功能,使其能够对昆虫和小动物进行分类。这要求我们的模型能够识别更高级的特征,例如边缘、纹理和模式。为此,我们需要引入卷积神经网络。
线性层的局限性
你之前使用线性层构建了分类器。但线性层存在一个问题:它将每个像素视为独立的。当模型观察一朵花或一只蝴蝶的图像时,它看到的是成千上万个独立的数字,无法理解相邻像素可以共同构成如翅膀、眼斑等特征。
卷积神经网络简介
卷积神经网络是计算机视觉的基石。其灵感来源于生物学。在20世纪60年代,神经科学家发现视觉皮层中的某些神经元只在看到特定模式时才会响应。CNN通过使用滤波器来筛选图像中的特征并从中学习,从而模仿了这一机制。
卷积的工作原理
让我们通过一张自然照片的灰度图来了解其工作原理。这是一张我们数据集的灰度特写。放大后,你可以看到单个像素。现在,我们聚焦于一个值为61的像素。将这个像素及其周围的3x3网格视为它的邻居。
想象一个滤波器,它是一个独立的3x3数字网格。你将这个滤波器滑过图像。在每个位置上,将滤波器值与下方的像素值相乘,然后将所有这些乘积相加并取平均值。得到的平均值将替换中心像素的值,从而改变其颜色。当你将滤波器滑过整个图像,并为每个像素基于其邻居计算新值时,这个过程就称为卷积。
滤波器的功能
你可能会问,为什么要这样做?通过为滤波器中的权重赋予不同的值,你实际上可以突出显示不同类型的模式。
让我们看一个例子。你能猜出这个滤波器的作用吗?你可以将其中心值视为从零或黑色的基线开始。但如果你观察左右两侧,相似的相邻像素值实际上会相互抵消。然而,如果存在强烈的对比,比如左侧非常暗而右侧非常亮,这就会为该滤波器产生一个强输出。这正是图像中垂直边缘的体现。滤波器捕捉到这种对比度,并突出显示垂直边缘出现的位置。
那么,这个滤波器可能做什么呢?它与前一个非常相似,但检测的是水平边缘。这就是它产生的结果。
滤波器在分类中的重要性
这对于分类任务为何重要?当你识别一只蝴蝶时,你不会分析每个像素。你会注意到翅膀形状、翅脉图案、那些独特的橙色和黑色部分。你的大脑利用这些特征来识别这是一只帝王蝶。对于我们的模型,原理是相同的。滤波器有助于突出那些能区分帝王蝶和燕尾蝶、或蝴蝶和甲虫的模式。
CNN的关键优势
有趣的部分来了。你可以手动设计这些滤波器。但你如何知道哪些滤波器对蝴蝶、花朵或甲虫最有效?或者,如果模型能够学习哪些滤波器效果最好,并调整它们以找到识别每个类别的特定模式呢?这就是卷积神经网络的关键能力。它们将找出哪些视觉特征对于你手头的特定任务最为重要。
在PyTorch中创建卷积层
现在,让我们深入了解如何在PyTorch中使用nn.Module创建卷积层。你之前使用过nn.Linear等层来构建网络。卷积层的工作方式完全相同,它只是你添加到模型架构中的另一种类型的层。在PyTorch中,你可以使用nn.Conv2D来定义一个卷积层。这个名字意味着它是一个二维卷积,就像你在二维图像中使用的那样。
以下是每个设置参数的逐步说明:
- in_channels:输入图像的颜色通道数。对于我们的自然照片,通常是三个通道:红色、绿色和蓝色。
- out_channels:决定你的卷积层将使用多少个滤波器。每个滤波器学习检测不同的特征。可能一个会找到翅膀边缘,另一个找到斑点眼纹,还有一个检测绒毛纹理。通常你会使用多个滤波器来捕捉图像中所有不同的特征。
- kernel_size:设置每个滤波器的大小。3x3的滤波器很常见,因为它检查每个像素及其直接邻居,非常适合检测局部模式,如翅膀鳞片或花瓣纹理。
- stride:控制滤波器在扫描图像时每一步移动的距离。步幅为1意味着它仔细检查每个像素。增加步幅会使滤波器跳过一些像素,速度更快,但可能会错过像触角节段这样的精细细节。
- padding:这个参数非常有趣,它与内核大小有关。思考一下:你如何将像素定位在图像的边缘?想象一下,你放大到图像的这个角落,使得边界现在位于图像之外。现在拿一个滤波器并将其滑入角落。但如果你想将滤波器中心对准角落的像素会发生什么?你的滤波器现在有邻居值落在了图像之外。填充值为1会简单地添加这些值,默认情况下它们通常被设置为零。这与内核大小相关,因为如果我的内核大小为5,那么你将需要填充大小为2才能将中心对准角落像素。
总结
本节课我们一起学习了卷积神经网络的基础。我们探讨了线性层在处理图像时的局限性,并介绍了卷积层如何通过滤波器来检测图像中的局部模式,如边缘和纹理。我们了解了卷积操作的基本步骤,以及滤波器如何突出对分类任务至关重要的特征。最后,我们介绍了在PyTorch中定义卷积层nn.Conv2D的关键参数及其作用。


下一节视频中,你将探索完整卷积神经网络的架构,并理解卷积层、池化层和全连接层如何协同工作,然后我们将在PyTorch中实现它。
023:完整CNN架构解析 🏗️
在本节课中,我们将学习如何将卷积层、激活函数、池化层和全连接层组合成一个完整的卷积神经网络架构。我们将详细解析每一部分的作用及其在图像分类任务中的协作方式。
概述
在上一节中,我们探讨了卷积层的工作原理以及CNN如何学习有用的滤波器来从图像中提取特征和模式。本节中,我们将把这些部分组合成一个完整的卷积神经网络架构。
网络结构定义
该网络继承自 nn.Module,与之前构建的神经网络类似。在 __init__ 函数中,我们定义网络的结构。它从两个卷积层开始,以一个用于图像分类的全连接层结束。
“全连接”意味着输入中的每个神经元都与输出中的每个神经元相连,它只是线性层的另一个名称。


第一卷积层详解
在第一卷积层中,我们从一个具有一个通道的图像开始。这是一个灰度图像,每个像素只有一个亮度值。模型将尝试学习32个不同的滤波器。
每个滤波器都是一个3x3的数字网格,因此每个滤波器有9个权重。这些滤波器在图像上滑动,并以填充为1的方式响应不同的模式。
# 示例:定义一个卷积层
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
该层的输出可能看起来像32张新图像,但它们实际上只是数值数组,显示了每个滤波器对图像不同部分的响应强度。这些通常被称为特征图或激活图,它们映射了每个特征在输入中出现的位置。
激活函数与池化层

接下来是ReLU激活函数。它将结果特征图中的任何负数设置为0,这有助于模型学习更复杂的模式。
然后,数据被输入到名为 MaxPool2d 的层中。池化是卷积神经网络中的一种常用技术,用于减小特征图的大小。它本质上是在应用滤波器后丢弃像素,压缩数据,同时保留最重要的部分,且不应影响结果。
以下是其工作原理:
假设最大池化的核大小为2。在左侧,您有滤波器输出的特征图。由于池化核大小为2,您选择一个2x2的值区域,然后只保留该组中的最大值(例如192),并丢弃其余部分。这就是为什么它被称为最大池化。
# 示例:定义一个最大池化层
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
如果您对特征图中的每个2x2值组重复此过程,最终将得到一个更小的输出,仅保留每个2x2区域的最大值。
这里的逻辑是,您的滤波器已经从原始图像中提取了重要特征。因此,通过应用池化,您正在压缩每个滤波后的图像,仅保留最重要的信息。结果,通过网络传递的数据量减少,下一层看到的图像大小仅为原始大小的四分之一。

这一点很重要,因为在第一个卷积层之后,您现在有32个不同的特征图流入第二层。对于大图像,这很快就会产生大量数据。池化减少了这种信息量,使您的神经网络更加高效,而不会丢失有价值的细节,并且对小变化更具鲁棒性。
全连接层与分类
在卷积层和池化层突出显示并压缩了图像中的关键特征之后,现在是时候使用全连接层对结果进行分类了。这个全连接层将所有特征组合成最终的预测。
在这个例子中,您可以看到线性层有 64 * 7 * 7 个输入。这里的64应该相对直观。例如,图像被调整为28x28像素。您需要弄清楚这一点。
请记住最大池化层是如何工作的:它从2x2的块中获取图像。因此,一个28x28的图像将在每个轴上被分成两半,得到14x14的图像。第二个最大池化层将做同样的事情,将14x14减半,得到7x7。然后,这些7x7的特征图将被输入到最终的线性层。
换句话说,每个最大池化层将其输入的大小减半。
前向传播流程
然后是 forward 方法,用于定义数据流,这相当直接。您将数据传递到每个卷积层,然后回想一下,线性层期望一个单行的值。因此,您需要将张量展平为一个向量,并将其传递到最后一层以产生预测。
这就是您完整的CNN流程:从原始像素到学习到的特征,再到分类。
总结
到目前为止,您已经看到了CNN如何处理图像:从那些生成特征图的学习滤波器开始,然后通过池化来减小大小并提高鲁棒性,最后通过全连接层进行分类。现在您可以理解其核心理论了。

在下一视频中,我将逐步引导您使用PyTorch构建一个卷积神经网络的代码。
024:训练用于图像分类的CNN 🖼️
在本节课中,我们将学习如何构建并训练一个完整的卷积神经网络(CNN),用于一个扩展的自然图像分类任务。我们将使用一个包含32x32像素彩色图像的小型多样化数据集。
概述
上一节我们介绍了卷积层的工作原理。本节中,我们将构建一个完整的CNN架构,并探讨训练过程中的关键概念,如Dropout和权重衰减。
网络架构定义
以下是完整的CNN架构。它与之前介绍的类似,但有两个主要区别:输入图像现在是彩色的,并且网络包含三个卷积块,而不是两个。
请重点关注每个块的输入和输出通道数。



- 第一个卷积层接收3个输入通道(分别对应红、绿、蓝三色),并产生32个输出通道。这意味着它将学习32个不同的滤波器。你可以理解为模型看到了图像的三个版本(红、绿、蓝),而32个滤波器中的每一个都会从这些输入中提取不同的模式。
- 第二层接收这32个通道,并产生64个。
- 第三层从64个通道扩展到128个。
因此,随着网络加深,模型学习的滤波器数量也在增加。
每个卷积块后面都跟着一个最大池化层,它将图像的高度和宽度减半。因此,当图像通过所有三个块后,其空间尺寸从32x32缩小到了仅4x4。
在forward方法中,特征图被展平,然后传入一个包含512个神经元的全连接层。该层像之前一样使用ReLU激活函数,帮助模型将特征组合成更抽象的表示。
引入Dropout层
接下来是一个新概念:Dropout层。在训练期间,该层会随机使大约50%的神经元失活。
这听起来可能违反直觉。我们为什么要关闭网络的一部分呢?让我们看一个例子。

想象一个训练来区分狗和狼的模型。一张哈士奇的照片被错误地分类为狼。原因并非哈士奇长得像狼,而是因为模型在训练集中过度关注了背景中的雪——一些狼的图片有雪景,这足以让模型开始学习“雪意味着狼”。
这种现象被称为“共适应”。一些神经元变成了专门的雪检测器,而其他神经元开始依赖它们。模型变得懒惰,依赖捷径,而不是学习像身体形状或面部结构这样鲁棒的特征。
Dropout通过在训练期间随机关闭神经元来打破这些捷径。Dropout使得过度依赖任何一种模式都变得有风险。如果雪检测器神经元被关闭,模型就必须寻找其他真正重要的线索。
在实践中,Dropout率通常在0.2到0.5之间,并且通常放置在激活函数之后、最终分类层之前。
需要明确的是,如果你的数据中大多数狼的图片都有雪而狗的图片没有,那这是一个数据问题,而不是过拟合。模型只是学习了它看到的唯一模式。但在典型的现实世界数据中,模式并非如此清晰。Dropout通过鼓励模型学习泛化性强的特征,而不是那些偶然相关的特征,来提供帮助。
输出层与训练设置


最后,是第二个全连接层,它输出15个值,对应数据集中每个类别一个。



定义好模型后,还需要设置优化器和损失函数。在本例中,我们使用交叉熵损失和Adam优化器。
在分级作业中,你还会看到对Adam优化器应用了权重衰减。与Dropout类似,它是一种正则化技术,有助于提高泛化能力,但工作原理不同。
权重衰减不是关闭神经元,而是阻止网络使用非常大的权重。为什么要这样做?因为大的权重可能表明模型正在记忆训练数据中的特定模式,而不是学习能够泛化的特征。权重衰减对大权重施加了一个小的惩罚,促使模型趋向于更简单、更鲁棒的解决方案。
数据流形状变化
当你进入实验环节时,你将看到数据在流经CNN时形状是如何变化的。
你在每个形状中看到的第一个数字是批量大小。因此,如果你一次训练一张图像,这个数字就是1。
每张图像从3个通道(红、绿、蓝)开始,每个通道是32x32的数值。


随着图像通过每个卷积层和池化层,空间尺寸会缩小,而通道数量会增加。
在最后一层,模型输出15个值,每个类别一个,代表预测的类别概率。
训练结果
当我仅训练这个模型10个周期时,结果看起来相当不错。
你可以在左侧的图表中看到,训练损失稳步下降。在右侧,模型在未见过的数据上的准确率随着时间的推移而提高。
当你查看一些样本预测时,模型的准确性令人惊讶。即使对于这些小型、低分辨率的图像(它们在这里看起来可能有点像素化,但这只是因为我们将它们从原始的32x32尺寸放大了)。
总结
本节课中,我们一起学习了如何构建一个用于彩色图像分类的CNN,理解了Dropout如何通过随机失活神经元来防止过拟合和共适应,并了解了权重衰减作为另一种正则化技术的作用。我们还跟踪了数据在网络中的形状变化,并观察了一个简单CNN在小型数据集上的有效训练过程。


现在轮到你了,请前往实验笔记本,详细探索这一切是如何运作的。
025:动态计算图 🧠
在本节课中,我们将要学习PyTorch框架的核心特性之一:动态计算图。我们将了解它与传统静态计算图的区别,以及这种动态性如何赋予我们构建更灵活、更强大模型的自由。
概述
欢迎回来。你刚刚为植物园应用训练了第一个卷积神经网络。但让我们更仔细地观察一下。它的结构是完全线性的。数据直接流过。它包含三个卷积、ReLU和池化层块,然后被展平并通过几个全连接层,最终输出预测。这是一个坚实的起点。但到目前为止,它只是一堆层的堆叠。它看起来有些重复,并且没有真正展示出PyTorch的特殊之处。
在PyTorch中,你并不被锁定在那种僵化的模型结构中。它通过一种称为动态计算图的机制来实现这一点,该图在你的模型逐步运行时构建而成。


正如你在模块1中听到的,旧框架的工作方式不同。你必须在任何数据通过之前,提前定义好你的计算图。这实际上意味着什么?什么是计算图?是什么让PyTorch的动态方法如此强大?让我们来仔细看看。



从顺序模型到计算图
让我们从一个使用 nn.Sequential 定义的模型开始。该模型按顺序将你的数据通过每一层,正如你所期望的那样。每一层都转换输入并将其传递下去。它简单、可读,对于简单的架构是有效的。
但这里有一个问题:nn.Sequential 将你锁定在一个固定的结构中。它不支持循环、条件判断或其他类型的分支逻辑。实际上,这种代码与你在使用静态图的旧框架中构建模型的方式并没有太大不同。
为了理解原因,让我们更深入地看看计算图到底是什么。
什么是计算图?


当你编写这个顺序块时,你正在定义一个特定的数学方程。Conv2D 可能指定了成千上万的乘法和加法运算。ReLU 将任何负值归零。当它们链式连接时,你得到一个巨大的方程。现在,想象一下把这个方程一步一步地详细展开:乘以这个,加上那个,将结果归零,再乘以,加上偏置,如此反复无数次。这个逐步分解的过程,就是一个计算图。你应用的每一个操作都被记录为该图的一部分。
为什么需要这个图?在训练期间,所有深度学习框架都需要使用微积分中的链式法则沿着图反向传播。这就是它如何计算出如何调整每一个参数的方法。而旧框架处理这一点的方式与PyTorch现在的方式不同。
静态图 vs. 动态图
在旧框架中,你必须在任何程序运行之前,预先定义整个图:每一个操作,每一个连接。一旦定义,结构就被锁定了。当你在PyTorch中使用 nn.Sequential 时,你面临着类似的限制。一切都按固定顺序运行,你不能打印中间值来调试,没有条件判断,没有循环。每个输入都遵循相同的路径,没有例外。这就是为什么 nn.Sequential 感觉有点像静态图编程:你又回到了定义那个没有灵活性的固定方程。
你可能会想,为什么会有人想以这种方式工作?事实证明,当框架提前知道完整的计算图时,它可以为了速度和内存效率而优化它。但代价是你放弃了灵活性,因为你描述的是一个固定的方程,没有空间在运行时调整事物或探索意想不到的想法。而这正是PyTorch旨在改变的地方。


PyTorch的动态图优势
让我们看一个例子。想象你正在为你的植物园应用构建一个CNN,但现在你希望它处理花朵和处理蝴蝶的方式不同。在PyTorch中,你可以直接在 forward 方法中编写这种逻辑。在使用静态图的旧框架中,这种控制非常、非常困难。是的,它们允许你有条件判断,但只能通过预先构建所有可能的路径来实现。
PyTorch采取了不同的方法。那个 if 语句不仅仅控制逻辑,它还塑造了图本身。每次 forward 运行时,PyTorch都会精确记录发生了什么:每一次乘法、加法、层和分支。结果是一个根据你的数据实际路径即时构建的定制计算图。该图用于反向传播,以便参数得到更新,然后图被丢弃。下次 forward 运行时,PyTorch从头开始,构建一个全新的图,为那次运行量身定制,即使它遵循完全不同的路径。
这就是核心区别。静态框架让你像编译器一样思考,但PyTorch让你像Python程序员一样思考:编写逻辑、分支、即时适应。
这种灵活性来自于使用 nn.Module 而不是 nn.Sequential。__init__ 定义我们的模型,而 forward 允许我们定义动态图的流程。
动态图的实际应用与权衡
现在,这些动态图确实会带来一些小的性能权衡。但对于研究人员和开发者来说,这种灵活性意味着更快的迭代、更容易的调试和更具表现力的模型。这不仅仅是一个好主意,它解决了你实际会遇到的问题。
以下是动态图解决的一些实际问题:
- 可变长度输入:静态图通常要求所有输入具有相同的形状。但如果你处理句子,有些句子有3个词,有些很长的句子有50个词怎么办?在PyTorch中,这可以直接工作。
- 轻松调试:如果你需要在计算过程中调试某些东西,在静态框架中,这意味着切换到特殊的调试模式,不允许中断流程。在PyTorch中,这只是Python。如果需要调查问题,只需添加一个
print语句。 - 自适应模型:你甚至可以构建适应输入的模型,例如对简单情况运行更简单的模型,对复杂情况运行更复杂的模型。你的模型会对其自身的计算变得“智能”。
这些并非边缘情况,这就是现代AI在PyTorch中的工作方式。所有这些都是通过编写Python代码实现的。


总结
本节课中,我们一起学习了PyTorch动态计算图的核心概念。我们了解到:
- 计算图是模型运算的逐步记录,用于反向传播。
- 静态图需要预先定义所有操作,结构固定,优化好但缺乏灵活性。
- 动态图在运行时即时构建,允许使用Python原生的条件、循环和分支逻辑,提供了极大的灵活性。
- 使用
nn.Module和自定义forward方法是实现动态图的关键。 - 动态图虽然可能带来轻微性能开销,但它为快速实验、调试和构建复杂、自适应模型打开了大门。


这种灵活性非常适合实验,但随着模型的增长,你将需要更好的方式来组织你的代码。这将是我们接下来要探索的内容。
026:模块化架构 🧱
在本节课中,我们将要学习如何利用PyTorch的模块化特性来构建更专业、更易于维护的神经网络代码。我们将探讨如何避免代码冗余,以及如何结合使用nn.Sequential和自定义模块来创建清晰、可扩展的模型架构。
动态图与代码冗余
上一节我们介绍了PyTorch的动态计算图及其带来的灵活性。然而,这种灵活性并未解决构建卷积神经网络(CNN)时出现的代码重复问题。
观察一个典型CNN的__init__方法,其中可能定义了9个独立的层。在forward方法中,又需要按照完全相同的顺序手动调用这9个操作。这种在__init__和forward方法之间的重复是冗余的。
这种设计将模型架构的定义(__init__)与计算流程的定义(forward)分离开来,符合关注点分离的原则。__init__中定义的层和可学习参数将在整个训练过程中持续存在,而forward方法则定义了动态的计算图流程。但对于一个简单的、顺序执行的模型,这种重复并无必要。



引入 nn.Sequential 简化代码
当模型结构是固定的线性流程时,我们可以使用nn.Sequential来简化代码。nn.Sequential可以将按顺序运行的层组合在一起。
代码示例:使用 nn.Sequential
# 冗余的写法
self.conv1 = nn.Conv2d(...)
self.relu1 = nn.ReLU(...)
self.pool1 = nn.MaxPool2d(...)
# ... 更多层定义
def forward(self, x):
x = self.relu1(self.conv1(x))
x = self.pool1(x)
# ... 更多手动调用
# 使用 nn.Sequential 的简洁写法
self.features = nn.Sequential(
nn.Conv2d(...),
nn.ReLU(...),
nn.MaxPool2d(...),
# ... 更多层
)
def forward(self, x):
x = self.features(x) # 只需一行调用


使用nn.Sequential后,forward方法变得非常简洁。当需要修改模型,例如添加第四个卷积块时,只需在__init__中更新Sequential即可,无需再修改forward方法,从而避免了命名冲突和遗漏调用层等错误。
模块化设计:创建自定义块
虽然nn.Sequential很方便,但它强制一个固定的线性路径,不支持条件分支、循环或动态行为。好消息是,我们不必在灵活性和简洁性之间二选一。
当模型中存在重复的模式时,例如“卷积 -> 激活函数 -> 池化”多次出现,我们可以将这些层分组,创建自定义的模块块。
代码示例:创建自定义卷积块
class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
def forward(self, x):
return self.block(x)
现在,主CNN模型可以变得非常清晰:
代码示例:使用自定义块构建模型
class MyCNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
ConvBlock(3, 64), # 块 1
ConvBlock(64, 128), # 块 2
ConvBlock(128, 256) # 块 3
)
self.classifier = nn.Linear(256, 10)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x
这种模块化设计带来了巨大优势:
- 易于修改:要添加第四个块,只需在
features序列中添加一个新的ConvBlock。 - 易于维护:如果想为所有块添加批归一化(Batch Normalization),只需在
ConvBlock类中修改一次,所有实例都会自动更新。 - 可扩展:你甚至可以嵌套模块,构建清晰、可扩展的架构。
工作流程建议
以下是构建模型的一个实用工作流程:
- 先明确,后重构:在构建新模型时,即使代码重复,也先明确写出所有层。这有助于调试和理解模型的具体行为。
- 识别模式:当模型正常工作后,寻找重复的模式或序列。
- 进行重构:将重复的序列用
nn.Sequential封装,或将可复用的组件提取成自定义模块(如ConvBlock)。


这就像先打草稿,再逐步打磨。你并不是因为不懂而写冗余代码,而是为了更有效地理解和调试。
总结
本节课中我们一起学习了如何构建模块化的PyTorch模型。我们从手动定义每一层,演进到使用nn.Sequential简化顺序结构,最终学会了创建自定义模块来封装重复模式,从而构建出像专业人士一样清晰、模块化的模型。


通过分离架构定义与计算流程,并利用模块化设计,我们能够编写出更易维护、更易扩展且更少错误的代码。在下一节中,我们将探索如何检查和分析这些模型,学习如何查看Sequential块内部的结构、可视化模型架构,并确切了解模型正在做什么。
027:模型检查与调试 🔍
在本节课中,我们将学习如何检查PyTorch模型的内部结构、统计参数量,并利用这些技巧来调试常见的错误,例如形状不匹配问题。
你已经学会了如何通过坚实的数据管道和性能提升工具来逐层训练卷积神经网络。现在,是时候掌握最后一项技能:检查模型的内部。在本视频中,你将探索用于检查模型结构、统计参数数量以及理解各层如何连接的工具。你还会看到这些工具如何帮助你解决实际问题,比如那些恼人的形状不匹配错误。
如何查看模型内部结构? 🏗️
让我们从一个简单的问题开始:如何查看模型内部?
你的第一反应可能是直接打印它。你会得到类似这样的输出:
print(model)
每一行显示了你为层指定的名称(如 conv1 或 fc2)、层的类型(如 Conv2D、MaxPool2D 或 Dropout)以及关键设置(如输入输出尺寸或卷积核大小)。这完全反映了你在模型中定义的内容,就像PyTorch向你展示你的蓝图,非常适合发现结构错误。

但请注意缺少了什么信息:每一层有多少参数?张量的形状是什么?那些 Sequential 块里面到底有什么?

统计模型参数数量 📊
要统计参数数量,你可能会尝试:
model.parameters()
但得到的不是一个列表,而是一个生成器。你能想到PyTorch在这里使用生成器的原因吗?因为它高效。它不会一次性将所有内容加载到内存中,而是在需要时一次提供一个参数。
要实际看到参数,你需要进行迭代,你会得到这样的形状:
for param in model.parameters():
print(param.shape)
每个形状代表一组参数,通常是来自各层的权重和偏置。
但如果你想要参数的总数,以下是标准方法:
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数量: {total_params}")
.numel() 方法给出每个张量中的元素数量。将它们全部加起来,你就得到了总参数量。
定位参数位置 🔍
很好,但它们具体在哪里呢?要找出答案,你需要逐个查看每一层,这就是 .named_parameters() 的用武之地。
for name, param in model.named_parameters():
print(f"{name}: {param.shape}")
这可以逐层精确地显示每组权重和偏置的位置。
为了理解这些形状,让我们以 fc1.weight 为例。它连接了2048个输入到512个输出,因此你得到一个形状为 (512, 2048) 的权重矩阵。每一行保存一个输出神经元的权重,每个输入对应一个权重。如果你期望它是按输入-输出的顺序,这可能会感觉是反的,但这个形状反映了其目的:每个输出都结合了所有输入的信息。因此,PyTorch围绕输出来组织权重。至于偏置,那就是 (512,),每个输出神经元一个值。
检查嵌套模块 🔬
但如果你的模型包含嵌套块,比如 Sequential 或自定义模块,如何查看内部呢?
PyTorch为你提供了两个方便的方法:.children() 和 .modules()。
让我们从 .children() 开始。它只显示顶层组件,比如卷积层或全连接层。如果一个块包含其他层(如 Sequential 或自定义模块),你将看不到内部内容。
要深入查看,请尝试 .modules()。区别是什么?把你的模型想象成一个文件夹结构。.children() 只显示顶层文件夹,而 .modules() 显示内部的所有内容,包括嵌套在 Sequential 或其他自定义块内的层。这在处理模块化架构并希望检查每一层时尤其有用。
调试常见错误 🐛
现在,你已经对模型有了全面的了解。但当出现问题时会发生什么?让我们看一个常见的PyTorch错误:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (a x b and c x d)
这通常意味着线性层的输入与层期望的不匹配。
在实际项目中,大多数人首先检查的是层本身的形状。如果错误发生在 fc1,你可以像这样打印它的权重形状:
print(model.fc1.weight.shape)
这会给你最快的答案。
但如果你不确定是哪一层导致了问题,或者想一次检查多个层,你可以直接使用 .named_parameters()。
例如,fc1 期望1024个输入,但你的模型传入了2048。为什么会不匹配?为了追踪模型中的形状变化,可以尝试在 forward 传递过程中打印形状。
def forward(self, x):
print(f"输入形状: {x.shape}")
x = self.features(x)
print(f"特征块输出形状: {x.shape}")
x = torch.flatten(x, 1)
print(f"展平后形状: {x.shape}")
x = self.fc1(x)
return x
现在你可以看到特征块产生的形状,以及展平操作是否按预期工作。通过结合模型检查(fc1 期望什么)和形状追踪(它实际得到了什么),你可以快速定位问题出在哪里。



这就是检查和调试如何协同工作。

总结 📝
本节课中,我们一起学习了如何深入检查PyTorch模型。我们掌握了查看模型结构蓝图、统计总参数量以及定位特定层参数的方法。我们还探讨了 .children() 与 .modules() 的区别,用于检查嵌套模块。最后,我们应用这些检查技巧来调试形状不匹配等常见错误,通过在 forward 函数中追踪张量形状来定位问题根源。
至此,你已完成了本课程的学习。从预测送达时间的单个神经元到卷积神经网络,你已取得了长足的进步。你在PyTorch中打下了坚实的基础,包括创建数据管道、构建和训练模型、评估性能以及检查底层运行情况。

在下一课程中,你将开始使用PyTorch生态系统更高级的工具进行构建。你将使用 torchvision 处理视觉数据,探索文本模型,优化超参数,并构建更快、更灵活、更接近真实世界机器学习的训练流程。这正是坚实基础转化为强大能力的地方。期待在那里见到你!


浙公网安备 33010602011771号