威斯康星-STAT453-生成式人工智能笔记-全-
威斯康星 STAT453 生成式人工智能笔记(全)
深度学习课程介绍 P1:L1.0 🧠
在本节课中,我们将学习本深度学习课程的整体安排与核心目标,并对机器学习的基本概念进行简要回顾。
课程概述与安排 📅
上一节我们进行了简单的开场,本节中我们来看看本课程的具体组织方式。本课程将完全专注于深度学习与深度神经网络。课程采用虚拟异步形式进行,但会通过Zoom安排实时的在线答疑时间。所有课程资料、论坛和交流都将通过Canvas平台组织,以便于查找和使用。
以下是课程的核心沟通与学习方式:
- 异步学习:主要课程内容将通过视频和资料发布,学员可自行安排时间学习。
- 实时互动:我们将定期安排Zoom在线答疑,解答学习过程中遇到的问题。
- 课程平台:Canvas将作为中心枢纽,集成所有讲座视频、作业、项目说明和在线讨论论坛。
为了更清晰地展示概念,我将切换到iPad视图进行讲解和标注。
机器学习简要回顾 🤖
在深入深度学习之前,我们有必要对机器学习建立一个基本的认识。本课程虽不要求先修机器学习,但了解其核心框架有助于后续学习。机器学习主要关注如何让计算机从数据中学习规律,而深度学习是其中一个专注于使用深层神经网络的强大子领域。
本课程与传统的机器学习课程(如涉及决策树、随机森林等方法)没有重叠,将完全专注于深度学习。两者唯一的共同点是都涉及监督学习的基本思想,即利用带有标签的数据训练模型。
一个典型的监督机器学习工作流程如下:
- 数据准备:收集并预处理带标签的数据集。
- 模型选择:选择一个算法或模型结构。
- 训练:使用训练数据让模型学习输入与输出之间的关系。这通常通过优化一个损失函数来实现,例如均方误差:
Loss = (1/N) * Σ (y_true - y_pred)^2。 - 评估:使用未见过的测试数据评估模型的性能。
- 预测:使用训练好的模型对新数据进行预测。
课程实践环节 💻
上一节我们介绍了理论框架,本节中我们来看看本课程的实践部分。本课程将结合概念讲解与实践编程,以帮助大家为学术或工业界的深度学习项目做好准备。
实践环节的核心是完成一个课程项目,并将使用PyTorch这一主流的深度学习框架进行编码。PyTorch提供了动态计算图等特性,使得构建和调试神经网络变得更加直观。
本节课中我们一起学习了本深度学习课程的组织结构、机器学习的基本概念以及课程包含的实践环节。我们明确了课程完全专注于深度学习,并将通过理论结合PyTorch实践的方式,帮助大家掌握这一领域的核心知识与技能。
机器学习符号与专有名词教程 📚
在本节课中,我们将学习机器学习中常用的符号表示和核心专有名词。这些术语和符号是理解后续课程内容的基础,也是机器学习与深度学习社区的通用语言。
监督学习回顾 🔍
上一节我们介绍了机器学习的基本概念,本节中我们来看看监督学习的符号表示。监督学习的目标是学习一个函数,该函数能够将输入特征 X 映射到输出目标 Y。
- X 代表特征(Features),也称为观测值(Observations)。它通常是一个向量,例如一朵花的萼片长度、萼片宽度、花瓣长度和花瓣宽度,或者一张图像的像素值。
- Y 代表目标(Targets),即我们希望模型预测的值。
在书写时,我们使用特定符号来区分不同类型的数据:
- 标量(Scalar):使用普通字体,如
x。 - 向量(Vector):使用粗体或带箭头的字体,如 x 或 \(\vec{x}\)。
- 矩阵(Matrix):使用大写粗体或带下划线的字体,如 X 或 \(\underline{X}\)。
结构化数据与非结构化数据 🗂️
接下来,我们区分两种主要的数据类型。
结构化数据通常指以表格形式组织的数据,例如数据库(SQL)、Excel 电子表格或 CSV 文件。其特点是数据有清晰的行和列。
非结构化数据通常指原始数据,例如图像、音频信号或文本句子。在深度学习兴起之前,处理这类数据通常需要大量的特征工程,即手动从原始数据中提取有意义的特征。
以下是特征工程的一个例子:
假设我们有一张人脸图像,任务是进行人脸识别。传统方法可能先提取面部的关键点(如 67 个特征点),用这些简化后的特征向量代替原始的成千上万个像素值,从而降低问题的复杂度。
# 示例:使用库提取面部关键点(特征工程)
# 此代码仅为示意,并非实际可运行代码
landmarks = extract_face_landmarks(image)
# landmarks 现在是一个包含67个特征点的向量,而非所有像素
训练集与符号表示 📊
现在,我们来看看如何用数学符号表示一个机器学习数据集。
一个训练集通常记为 D,它由许多训练样本组成。每个训练样本是一对数据:特征向量和对应的目标标签。
- n 代表训练集中的样本数量。
- m 代表每个特征向量的维度(即特征的数量)。
- 我们使用上标括号表示样本索引,例如 \(\mathbf{x}^{(i)}\) 代表第
i个训练样本的特征向量。 - 我们使用下标表示特征索引,例如 \(x^{(i)}_j\) 代表第
i个样本的第j个特征值。
因此,整个训练集可以视为一个设计矩阵 X,其形状通常为 n × m(n 行,m 列),每一行是一个样本的特征向量。
数据来源于某个未知的数据生成分布,存在一个未知函数 \(f\) 将特征映射到真实标签。机器学习模型的目标就是学习一个假设函数 \(h\)(或称模型),来近似这个未知函数 \(f\)。模型的预测输出记为 \(\hat{y}\)。
分类与回归任务 🎯
根据输出类型的不同,监督学习主要分为两类任务:
在分类任务中,模型将 M 维特征向量映射到 K 个离散的类别标签之一。例如,鸢尾花数据集(Iris Dataset)就是将花的四个特征映射到三个品种之一。
模型函数表示为:\(h: \mathbb{R}^m \rightarrow \{c_1, c_2, ..., c_k\}\)
在回归任务中,模型将 M 维特征向量映射到一个连续的实数值。
模型函数表示为:\(h: \mathbb{R}^m \rightarrow \mathbb{R}\)
图像数据的处理 🖼️
图像是非结构化数据,但我们可以将其转换为结构化形式以供某些模型使用。例如,一张 28x28 的灰度图像可以按行展开,拼接成一个长度为 784 的像素值向量。
原始图像(矩阵): shape = (28, 28)
展开后的特征向量: shape = (784,)
然而,在深度学习中,更常见的做法是直接以张量形式处理图像。例如,在卷积神经网络中,一批图像通常被表示为四维张量,其维度为 (批量大小 N, 通道数 C, 高度 H, 宽度 W)。对于黑白图像,通道数 C 为 1。
核心术语对照表 📖
最后,我们整理一份机器学习常用术语的同义词表,以帮助大家理解不同语境下的表述。
以下是模型训练相关术语:
- 训练模型 = 拟合模型 = 参数化模型 = 从数据中学习
以下是单个数据点相关术语:
- 训练样本 = 训练记录 = 训练实例 = 训练样例(为避免歧义,推荐使用“训练样本”指代单个数据点)
以下是输入数据相关术语:
- 特征 = 观测值 = 预测变量 = 自变量 = 输入 = 属性 = 协变量(在机器学习中,“特征”最常用)
以下是真实值相关术语:
- 目标 = 真实值 = 输出 = 响应变量 = 因变量(分类任务中常称为“类别标签”或“标签”)
以下是模型输出相关术语:
- 输出 = 预测值(这是模型的产物,应与“目标”区分开)
总结 ✨


本节课中我们一起学习了机器学习的基础符号和核心专有名词。我们明确了监督学习中特征 X 与目标 Y 的表示方法,区分了结构化与非结构化数据,介绍了训练集、设计矩阵的符号体系,并梳理了分类、回归任务以及图像数据处理的基本概念。最后,我们汇总了常见的术语同义词,为后续课程的学习打下了坚实的语言基础。

P100:L13.3- 卷积神经网络基础 🧠
在本节课中,我们将要学习卷积神经网络(CNN)的基本概念。这是一种专门用于处理图像等具有空间结构数据的神经网络,其核心思想是捕捉数据的局部相关性。
概述:卷积神经网络的基本假设

上一节我们介绍了多层感知机(MLP)等网络结构。本节中我们来看看卷积神经网络。
卷积神经网络基于一个核心假设:局部性。这意味着数据中相邻的元素(如图像中的像素)是相互关联的,而非完全独立的。例如,在一张人脸图片中,构成眼睛的所有像素点之间存在紧密的联系。CNN正是为了捕捉这种局部依赖关系而设计的。


早期CNN架构:LeNet-5
为了更好地理解CNN,让我们来看一个经典的早期架构——LeNet-5。这个网络由Yann LeCun等人在1989年提出,至今其核心思想仍然适用。
该网络最初用于手写数字识别,输入图像尺寸为16x16(或论文中提到的32x32)。其核心操作是分析图像中的局部“补丁”(patch),并将这些补丁的信息传递到下一层。

以下是LeNet-5架构的另一个更清晰的图示,它展示了网络如何处理32x32的输入图像:


CNN的核心工作流程

现在,我们来详细拆解CNN的工作流程。整个过程可以概括为:卷积 -> 池化(下采样) -> 展平 -> 全连接分类。
- 卷积操作:网络使用称为“卷积核”或“滤波器”的小窗口(例如5x5)在输入图像上滑动。在每个位置,卷积核与图像局部区域进行点积运算,生成一个数值,最终形成一张“特征图”。一个卷积层通常包含多个不同的卷积核,从而生成多张特征图(例如第一层的6张)。
- 公式示意:
特征图[i, j] = sum(图像局部区域 * 卷积核权重) + 偏置
- 公式示意:
- 池化操作:在卷积之后,通常会进行“池化”(也称为“下采样”),例如最大池化。其目的是降低特征图的空间尺寸(例如从28x28降到14x14),增强特征的鲁棒性并减少计算量。
- 重复堆叠:上述“卷积+池化”的过程会重复多次。随着网络加深,特征图的通道数(即特征数量)通常会增加(例如从6个到16个),而空间尺寸会减小。这允许网络从低级特征(如边缘、纹理)逐步组合出高级特征(如眼睛、鼻子)。
- 展平与分类:经过若干次卷积和池化后,得到的三维特征张量会被展平成一个一维向量。这个向量随后被送入一个或多个全连接层(即传统的多层感知机部分),最终通过输出层(如Softmax)得到分类结果。


CNN的三大核心特性

理解了工作流程后,我们总结一下CNN区别于全连接网络的三个关键特性:
- 稀疏连接:在CNN中,特征图上的一个神经元只与输入图像的一个小局部区域(感受野)相连。这与全连接网络中每个神经元都与上一层所有神经元相连形成鲜明对比。
- 参数共享:同一个卷积核会被滑动应用于整张输入图像的不同位置。这意味着我们使用同一组参数(权重)来检测图像中任何位置可能出现的特定模式(如垂直边缘),极大地减少了模型参数数量。
- 层级特征提取:通过堆叠多个卷积层,CNN能够自动学习从低级到高级的层次化特征表示。浅层网络学习简单特征(如边缘、角点),深层网络则将这些简单特征组合成更复杂的结构(如物体部件)。

总结与后续
本节课中我们一起学习了卷积神经网络的基础知识。我们了解了CNN基于局部性假设,通过卷积、池化等操作自动提取图像特征,并利用稀疏连接、参数共享和层级结构来高效处理图像数据。

如果本节课的内容让你感到有些抽象,请不要担心。在接下来的课程中,我们将深入探讨卷积核的具体计算方式、步长、填充等细节,并通过代码实践来巩固理解。建议你先继续学习下一节内容,许多疑问将会得到解答。


P101:L13.4- 卷积滤波器和权重共享 👁️

概述
在本节课中,我们将详细探讨卷积神经网络中的核心概念:卷积滤波器和权重共享原理。我们将一步步拆解滤波器如何应用于输入图像以生成特征图,并解释这种设计为何高效且有效。
卷积滤波器与权重共享详解
上一节我们简要提到了卷积层的基本思想,本节中我们来看看其核心机制——卷积滤波器和权重共享——是如何具体运作的。
卷积网络中有一个被称为特征检测器的组件,我们也称之为滤波器或卷积核。我个人更喜欢“特征检测器”这个术语,因为它更直观地描述了其功能:我们将其在输入数据上滑动,以生成一张特征图。

如图所示,中心部分展示了一个手写数字“5”的图像。我们有一个3x3的特征检测器(卷积核),将其应用于图像上以计算特征图。当我们将这个卷积核覆盖到图像的某个区域时,该区域被称为感受野。感受野就是与卷积核重叠的那部分图像像素。
特征图中的每个值是如何计算出来的呢?其计算过程非常简单,就是计算加权和。
我们可以将卷积核视为一个权重矩阵。假设卷积核的权重为 W1, W2, ..., W9,而感受野中的像素值为 X1, X2, ..., X9。那么,输出特征值就是这两组值对应相乘再求和的结果。
公式表示:
输出特征值 = Σ (Wi * Xi),其中 i 从 1 到 9。

权重共享的优势

那么,为什么要采用权重共享呢?这主要有两大优势:

- 参数效率高:如果为图像中的每个区域都使用一个独立的特征检测器,计算成本将非常高昂,模型参数会急剧增加,使其更接近一个全连接网络,从而变得非常“昂贵”。
- 特征普适性:使用同一个滤波器滑动扫描整张图像的理念在于,如果某个特征检测器学会了识别一种特定模式(例如边缘),那么这个检测器在图像的不同部位都是有用的。没有必要为识别同一种边缘而在不同位置开发不同的检测器。
滑动卷积操作过程
为了更清晰地展示如何应用滤波器,让我们一步步观察滑动过程。


我们从左上角开始,将3x3的卷积核与第一个感受野对齐,计算得到特征图的第一个值。然后,我们将卷积核向右移动一个像素(步长为1),计算下一个值。如此重复,从左到右填充完第一行。




当第一行处理完毕后,我们将卷积核向下移动一个像素,回到最左侧,开始处理第二行。重复此过程,直到遍历完整张图像,最终生成完整的特征图。

生成特征图后,我们可以对其应用下采样(也称为池化),或者再叠加另一个卷积层,进一步提取更高级的特征。
使用多个滤波器
上面介绍的是使用单个滤波器生成一张特征图的情况。但一个卷积层通常包含多个不同的滤波器,用于提取多种类型的特征。
以下是使用多个特征检测器的示意图:

其原理非常简单:我们独立地应用多个不同的滤波器。如图所示,除了绿色的滤波器,我们还可以同时应用一个蓝色的滤波器和一个橙色的滤波器。每个滤波器都有自己的权重矩阵,它们以相同的方式在输入图像上滑动,但各自生成不同的特征图。

这样,通过使用三个不同的滤波器,我们就可以得到三张不同的特征图,每张图专注于捕捉输入中某种特定的模式。

卷积输入输出尺寸计算
在应用卷积时,输入和输出的尺寸会发生变化。这里有一个公式可以计算输出特征图的大小。
公式:
输出尺寸 = (输入尺寸 - 卷积核尺寸 + 2 * 填充) / 步长 + 1
假设我们有一个32x32的输入图像,使用一个5x5的卷积核,步长为1,并且没有填充(填充=0)。那么输出特征图的宽度计算如下:
(32 - 5) / 1 + 1 = 28
因此,输出是一个28x28的特征图。尺寸缩小的原因在于,当卷积核滑动到图像边缘时,为了不超出边界,我们无法进行完整的重叠计算。
- 步长:指每次滑动卷积核时移动的像素数。步长为1是最常见的情况。
- 填充:指在输入图像边缘周围添加额外的像素(通常是0),以控制输出尺寸。这将在后续课程中详细讨论。
卷积层的参数量
卷积层以其参数效率高而著称。让我们通过一个代码片段来理解其参数量。
代码示例:
# 假设输入是MNIST图像,尺寸为 1x28x28 (通道x高x宽)
conv_layer = nn.Conv2d(in_channels=1, out_channels=8, kernel_size=5, stride=1)
在这个例子中:
in_channels=1:输入是灰度图,只有一个通道。out_channels=8:我们希望得到8个不同的特征图。kernel_size=5:使用5x5的卷积核。stride=1:步长为1。

这个卷积层的可训练参数量计算如下:
- 权重参数:每个5x5的卷积核有25个权重。我们需要8个这样的核来处理1个输入通道,所以权重总数为
8 * 1 * 5 * 5 = 200。 - 偏置参数:每个输出特征图有一个偏置值,共8个。
- 总参数量:
200 (权重) + 8 (偏置) = 208。
相比之下,一个连接784个输入像素到10个神经元的全连接层,仅权重就需要 784 * 10 = 7840 个参数。可见,卷积层在参数使用上要高效得多。
关于平移、旋转与尺度不变性的说明
卷积网络具有一定程度的平移、旋转和尺度不变性,但这并非完全不变。

- 不变性的来源:由于使用相同的滤波器扫描整个图像,无论目标出现在图像的哪个位置,相同的特征检测器都能对其做出响应。
- 局限性:然而,响应的位置信息仍然保留在特征图中。目标在图像中的不同位置,会导致其对应特征在特征图中出现在不同位置。
- 深层网络的改善:通过堆叠多个卷积层,网络可以逐渐将信息汇总到更小的特征区域中,从而在更高层次上获得更强的位置不变性。
此外,最大池化层也有助于提升这种局部不变性。
最大池化层简介
传统上,最大池化层常用于卷积层之间,用于降低特征图的尺寸,并提供一定的平移不变性。

最大池化操作非常简单:它在一个小窗口(例如2x2或3x3)内滑动,并只保留该窗口内的最大值。例如,使用一个3x3的池化窗口,步长为3,它会取每个3x3区域中的最大值作为输出。
- 作用:它不在乎最大值在窗口内的具体位置,只关心其是否存在。这使其对特征的小幅平移不那么敏感。
- 参数量:标准的池化层(如最大池化、平均池化)没有可训练的参数。
- 现代架构:值得注意的是,并非所有现代卷积网络都使用池化层。一些架构通过使用步长大于1的卷积来直接实现下采样。

总结

本节课中我们一起学习了卷积神经网络的两个基石:
- 卷积滤波器:作为特征检测器,通过在输入上滑动来生成特征图,其核心计算是局部区域的加权和。
- 权重共享:同一个滤波器在整个输入上重复使用,这极大地减少了模型参数,并使得网络能够检测图像中任何位置出现的相同特征。

我们还了解了如何计算卷积后的输出尺寸、估算卷积层的参数量,并简要探讨了卷积网络所具备的有限平移不变性以及池化层的作用。在接下来的课程中,我们将深入更多细节,并动手构建一个完整的卷积网络。
P107:L13.9.1 - PyTorch 中的 LeNet-5 实现教程 🧠
在本节课中,我们将学习如何使用 PyTorch 实现经典的 LeNet-5 卷积神经网络,并将其应用于 MNIST 手写数字分类任务。我们将从网络结构解析开始,逐步深入到代码的每一部分,并最终完成模型的训练与评估。

概述
LeNet-5 是一个经典的卷积神经网络架构,最初设计用于手写字符识别。本节课我们将使用 PyTorch 框架,按照原始论文的结构,构建一个 LeNet-5 模型来对 MNIST 数据集中的手写数字进行分类。我们将涵盖数据预处理、模型定义、训练循环以及结果可视化等完整流程。
LeNet-5 网络结构回顾
首先,我们来回顾一下 LeNet-5 的网络结构。下图展示了其经典设计:

原始的 LeNet-5 输入是 32x32 的单通道图像。我们的 MNIST 数据集图像是 28x28,因此需要进行尺寸调整。

网络结构遵循一个清晰的模式:
- 卷积层:通常用于增加通道数,同时保持特征图的高和宽大致不变(若使用填充)。
- 池化层(下采样):通常用于将特征图的高和宽减半,同时保持通道数不变。


在 LeNet-5 中,这个模式重复两次,形成特征提取器,之后连接全连接层作为分类器。


代码实现详解


上一节我们回顾了网络结构,本节中我们来看看具体的代码实现。我将逐步解释每个部分。

1. 环境准备与数据加载
首先,我们导入必要的库并设置环境。以下代码完成了随机种子设置、数据转换定义和数据加载器创建。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms



# 设置随机种子以保证CPU上的可重复性(GPU上某些算法可能非确定性)
torch.manual_seed(1)

# 定义数据转换
# 1. 将28x28的MNIST图像调整为32x32以匹配LeNet-5输入
# 2. 转换为张量
# 3. 标准化到[-1, 1]范围
transform = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])

# 加载MNIST数据集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1000, shuffle=False)
2. 定义 LeNet-5 模型


接下来,我们定义 LeNet-5 模型类。为了使模型更通用(例如能处理彩色图像),我们添加了 grayscale 和 num_classes 参数。
以下是模型定义的核心部分:


class LeNet5(nn.Module):
def __init__(self, grayscale=True, num_classes=10):
super(LeNet5, self).__init__()
self.grayscale = grayscale
self.num_classes = num_classes
# 特征提取器 (Feature Extractor)
in_channels = 1 if grayscale else 3
self.features = nn.Sequential(
# 第一个卷积块: Conv -> ReLU -> Pool
nn.Conv2d(in_channels, 6, kernel_size=5), # 公式: 输出尺寸 = (输入尺寸 - 核尺寸 + 1)
nn.ReLU(),
nn.AvgPool2d(kernel_size=2, stride=2), # 公式: 输出尺寸 = 输入尺寸 / 步长
# 第二个卷积块: Conv -> ReLU -> Pool
nn.Conv2d(6, 16, kernel_size=5),
nn.ReLU(),
nn.AvgPool2d(kernel_size=2, stride=2),
)
# 分类器 (Classifier) - 相当于一个多层感知机
self.classifier = nn.Sequential(
nn.Linear(16 * 5 * 5, 120), # 全连接层,输入维度需根据特征图尺寸计算
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
nn.Linear(84, num_classes)
# 注意:这里没有Softmax,因为CrossEntropyLoss内部包含了它
)
def forward(self, x):
# 通过特征提取器
x = self.features(x)
# 将特征图展平为一维向量,以输入全连接层
# 输入x的形状: (batch_size, channels, height, width)
# 展平后形状: (batch_size, channels * height * width)
x = torch.flatten(x, 1)
# 通过分类器
x = self.classifier(x)
return x

关键点解释:
nn.Sequential用于按顺序组合网络层,使代码更清晰。- 第一个全连接层
nn.Linear(16 * 5 * 5, 120)的输入维度16 * 5 * 5需要根据卷积和池化后的特征图尺寸计算得出。可以通过打印x在flatten前的形状来验证。 - 在
forward方法中,torch.flatten(x, 1)将除批次维度外的所有维度展平。
3. 模型初始化与设备设置
定义好模型后,我们需要实例化它,并将其移动到可用的计算设备(GPU或CPU)上。
# 实例化模型
model = LeNet5(grayscale=True, num_classes=10)


# 检查是否有可用的GPU,并设置设备
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
model.to(device)
print(f‘Using device: {device}’)
4. 定义损失函数与优化器

为了训练模型,我们需要定义损失函数来衡量预测误差,以及优化器来更新模型参数。

criterion = nn.CrossEntropyLoss() # 交叉熵损失函数,适用于多分类
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 带动量的随机梯度下降



# 可选:学习率调度器,用于在训练过程中动态调整学习率
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)


5. 训练模型


现在,我们进入核心的训练循环。以下是训练函数的关键步骤:



def train_model(model, train_loader, criterion, optimizer, scheduler=None, num_epochs=10):
model.train() # 将模型设置为训练模式
for epoch in range(num_epochs):
running_loss = 0.0
correct = 0
total = 0
for batch_idx, (inputs, labels) in enumerate(train_loader):
# 将数据移动到指定设备
inputs, labels = inputs.to(device), labels.to(device)
# 前向传播
outputs = model(inputs)
loss = criterion(outputs, labels)
# 反向传播与优化
optimizer.zero_grad() # 清空过往梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新模型参数
# 统计信息
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
# 每个epoch结束后打印统计信息
epoch_loss = running_loss / len(train_loader)
epoch_acc = 100. * correct / total
print(f‘Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss:.4f}, Acc: {epoch_acc:.2f}%’)
# 更新学习率(如果使用了调度器)
if scheduler:
scheduler.step()

# 开始训练
train_model(model, train_loader, criterion, optimizer, scheduler, num_epochs=10)


训练过程中,你会看到损失逐渐下降,准确率逐渐上升。


6. 模型评估与结果展示


训练完成后,我们需要在测试集上评估模型的性能,并可视化一些预测结果。


以下是评估和展示的函数:


def evaluate_model(model, test_loader):
model.eval() # 将模型设置为评估模式
correct = 0
total = 0
with torch.no_grad(): # 评估时不计算梯度,节省内存和计算
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
test_acc = 100. * correct / total
print(f‘Test Accuracy: {test_acc:.2f}%’)
return test_acc


# 评估模型
test_accuracy = evaluate_model(model, test_loader)


# 可视化一些预测结果
import matplotlib.pyplot as plt
def show_examples(model, test_loader, num_examples=10):
model.eval()
dataiter = iter(test_loader)
images, labels = next(dataiter)
images, labels = images.to(device), labels.to(device)
with torch.no_grad():
outputs = model(images[:num_examples])
_, preds = outputs.max(1)
fig, axes = plt.subplots(1, num_examples, figsize=(15, 3))
for idx in range(num_examples):
ax = axes[idx]
# 将图像从[-1,1]反标准化回[0,1]以便显示
img = images[idx].cpu().squeeze().numpy() * 0.5 + 0.5
ax.imshow(img, cmap='gray')
ax.set_title(f‘P:{preds[idx].item()} T:{labels[idx].item()}’)
ax.axis('off')
plt.show()

show_examples(model, test_loader)



运行后,你将看到模型在测试集上的准确率,以及一些样例图片及其预测(P)和真实(T)标签。


总结


本节课中,我们一起学习了如何使用 PyTorch 实现并训练 LeNet-5 卷积神经网络。我们完成了以下关键步骤:


- 回顾了 LeNet-5 的网络结构,理解了卷积层增加通道、池化层缩减尺寸的设计模式。
- 进行了数据预处理,将 MNIST 图像调整尺寸并标准化。
- 使用
nn.Module和nn.Sequential定义了 LeNet-5 模型,清晰地分离了特征提取器和分类器。 - 设置了训练环境,包括损失函数(交叉熵)、优化器(带动量的SGD)和学习率调度器。
- 编写了完整的训练循环,包含了前向传播、损失计算、反向传播和参数更新。
- 评估了模型性能并可视化预测结果,验证了模型的有效性。


通过这个实践,你不仅掌握了实现一个经典 CNN 的方法,也熟悉了 PyTorch 训练神经网络的标准流程。在接下来的课程中,我们将探索更复杂的网络结构,如 AlexNet。
PyTorch 教程 P108:L13.9.2 - 在 PyTorch 中保存和加载模型 📚
在本节课中,我们将要学习如何在 PyTorch 中保存和加载训练好的模型。这对于中断后继续训练、模型部署或分享模型至关重要。
上一节我们介绍了模型训练的基本流程,本节中我们来看看如何将训练成果持久化保存。


准备工作与模型训练 🛠️
首先,我们需要设置环境并训练一个简单的模型。以下代码展示了如何导入必要的库并添加自定义辅助文件的路径。
import sys
sys.path.insert(0, ‘..‘) # 返回上一级目录以找到辅助文件
接着,我们导入所有必要的模块并初始化模型、数据加载器等组件。这个过程与之前的训练流程完全一致。

# 假设 helper 模块包含了模型定义、数据准备等函数
from helper import *
model, train_loader, val_loader, optimizer, scheduler = initialize_training()


为了演示,我们只训练模型两个周期。

num_epochs = 2
train_model(model, train_loader, val_loader, optimizer, scheduler, num_epochs)


训练完成后,我们可以使用一个辅助函数来可视化一些预测结果,以检查模型表现。


show_examples(model, val_loader)

此外,我们还可以计算混淆矩阵来更详细地分析模型的错误类型。混淆矩阵显示了预测标签与真实标签的对比情况。

以下是混淆矩阵能帮助我们理解的信息:
- 理想情况下,最高的数值应出现在矩阵的对角线上。
- 通过观察非对角线的高值,我们可以发现模型最容易混淆哪些类别(例如,将数字9误判为5,或将2误判为7)。
保存模型状态 💾
模型训练完成后,下一步就是保存其状态,以便后续使用。在 PyTorch 中,这主要通过保存模型的参数(即状态字典)来实现。

核心的保存操作涉及以下三个部分:
# 保存模型参数
torch.save(model.state_dict(), ‘model.pt‘)
# 保存优化器状态(例如 SGD 中的动量)
torch.save(optimizer.state_dict(), ‘optimizer.pt‘)
# 保存学习率调度器状态
torch.save(scheduler.state_dict(), ‘scheduler.pt‘)

model.state_dict():是一个 Python 字典对象,包含了模型的所有可学习参数(权重和偏置)。- 文件扩展名
.pt或.pth是 PyTorch 的常见约定,但你可以使用任何名称。 - 保存优化器和调度器状态是可选的,但如果你想从中断处精确恢复训练,则必须保存它们。
执行上述代码后,你会在指定文件夹中找到这些 .pt 文件。
加载模型并继续训练 🔄
现在,假设我们想要加载已保存的模型并继续训练,或者在新环境中使用它。首先,我们需要在一个新的脚本或笔记本中重新初始化模型结构。


加载模型的过程分为几个关键步骤:
- 重新初始化模型:必须创建与保存时结构完全相同的模型类实例。
- 加载参数:将保存的状态字典加载到初始化好的模型中。
- 加载优化器和调度器(可选):如果打算继续训练,也需要加载它们的状态。


以下是实现代码:

# 1. 重新初始化模型结构
model = MyModelClass() # 使用与保存时相同的模型类
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30)

# 2. 加载保存的状态
model.load_state_dict(torch.load(‘model.pt‘))
optimizer.load_state_dict(torch.load(‘optimizer.pt‘))
scheduler.load_state_dict(torch.load(‘scheduler.pt‘))
# 3. 将模型设置为评估或训练模式
model.eval() # 用于推理/测试
# 或者 model.train() # 用于继续训练



重要提示:必须在加载 state_dict 之前实例化模型、优化器和调度器,因为 load_state_dict 方法是将参数填充到一个已存在的对象中,而不是创建对象本身。


完成加载后,你就可以继续训练模型了。

# 继续训练更多周期
train_model(model, train_loader, val_loader, optimizer, scheduler, num_epochs=10)


本节课中我们一起学习了 PyTorch 中保存和加载模型的核心方法。我们了解了如何保存模型的 state_dict 以及优化器和调度器的状态,并掌握了如何重新初始化模型结构并加载这些状态以继续训练或进行预测。下一节,我们将把注意力转向经典的 AlexNet 模型。

🧠 P109:L13.9.3 - PyTorch 中的 AlexNet

在本节课中,我们将学习如何在 PyTorch 中实现 AlexNet 架构,并将其应用于 CIFAR-10 数据集。我们将了解 AlexNet 的基本结构、针对小尺寸图像的调整方法、数据预处理技巧,并观察训练过程中的一些有趣现象。



📊 AlexNet 架构回顾



上一节我们介绍了 LeNet,本节中我们来看看更复杂的 AlexNet。AlexNet 是一个经典的卷积神经网络架构。

其输入图像尺寸为 224 x 224 x 3。经过第一层卷积后,通道数变为 96,随后是 256 通道。原论文中此处应为 384 通道,图中可能是个笔误,接着又是 384 通道,然后是 256 通道。最后是三个巨大的全连接层:4096 -> 4096 -> 1000(对应 ImageNet 的 1000 个类别)。

在本实现中,我们将第一层的通道数从 96 缩减到了 64,但总体上保持了相同的架构。请注意,我们并非在 ImageNet 上训练,而是在尺寸更小的 CIFAR-10 图像上训练。因此,我们对网络进行了一些小调整,以适应更小的输入图像尺寸。


🔧 针对 CIFAR-10 的调整


由于 CIFAR-10 原始图像尺寸为 32x32,直接输入 AlexNet 会导致特征图尺寸过早归零,网络无法工作。因此,我们需要对输入图像进行预处理。





以下是数据预处理的关键步骤:


- 图像放大:首先将 CIFAR-10 图像从 32x32 放大到 70x70。
- 随机裁剪:在训练时,从 70x70 的图像中随机裁剪出一个 64x64 的区域。这有助于增强模型对微小扰动的鲁棒性,防止过拟合。
- 中心裁剪:在验证和测试时,我们使用中心裁剪,而不是随机裁剪。因为评估模型在新数据上的性能时,不应引入随机性。
- 归一化:使用
transforms.Normalize将像素值归一化到 [-1, 1] 的范围内。对于 RGB 三通道图像,每个通道的均值和标准差都设为(0.5, 0.5, 0.5)。





# 训练数据转换
train_transform = transforms.Compose([
transforms.Resize(70),
transforms.RandomCrop(64),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 测试数据转换
test_transform = transforms.Compose([
transforms.Resize(70),
transforms.CenterCrop(64),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])


经过处理后,输入网络的批次数据维度为:[256, 3, 64, 64],对应 [批次大小, 通道数, 高, 宽]。CIFAR-10 共有 10 个类别。


🏗️ PyTorch 实现 AlexNet



现在,我们来看看如何在 PyTorch 中定义 AlexNet 模型。整个网络可以分为特征提取器和分类器两部分。



特征提取器由多个卷积块组成。每个卷积块通常包含卷积层、激活函数和池化层。在 AlexNet 中,前两个卷积块后跟有最大池化层。



import torch.nn as nn


class AlexNet(nn.Module):
def __init__(self, num_classes=10):
super(AlexNet, self).__init__()
# 特征提取器
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
# 自适应平均池化
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
# 分类器
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1) # 展平特征图
x = self.classifier(x)
return x



核心概念解析:
- 自适应平均池化 (
nn.AdaptiveAvgPool2d):无论输入特征图的尺寸如何,该层都会将其池化到指定的输出尺寸(这里是 6x6)。这使得网络能够接受比训练时尺寸更大的输入图像,增加了灵活性。但如果输入图像过小,导致特征图尺寸小于 6x6,此操作会失败。 - Dropout:原论文可能未使用 Dropout。本实现中添加了 Dropout 层以缓解严重的过拟合问题。
- 展平操作:在将特征图输入全连接层之前,需要使用
torch.flatten将其从多维张量转换为一维向量。


🚀 模型训练与观察



我们使用随机梯度下降(SGD)优化器配合动量(Momentum)和学习率调度器来训练模型。在 GPU 上训练 200 个周期大约需要 3 小时。


在训练过程中,我们观察到一个有趣的现象:双下降现象。损失率先下降,然后上升,接着再次下降。验证准确率也呈现出先提升、后下降、再提升的趋势。这体现了模型复杂度与训练周期之间的复杂关系。

尽管如此,模型仍然表现出明显的过拟合,训练准确率远高于验证准确率,差距约 20%。为了进一步减少过拟合,可以考虑以下方法:





- 增加 Dropout 的比例。
- 添加更多的数据增强手段,如色彩抖动、随机旋转等。


📈 结果可视化与分析





训练完成后,我们可以可视化一些预测结果。在显示图像前,需要将归一化的像素值反归一化回原始范围。

def unnormalize(img, mean, std):
for t, m, s in zip(img, mean, std):
t.mul_(s).add_(m)
return img


通过可视化,我们发现模型在区分“猫”和“狗”这两个类别时错误率较高,这说明了该分类任务本身的挑战性。混淆矩阵也进一步揭示了模型容易混淆的类别对,例如“船”和“飞机”,“青蛙”和“猫”。




💡 总结与扩展




本节课中我们一起学习了如何在 PyTorch 中实现并训练 AlexNet 网络。我们回顾了 AlexNet 的架构,针对 CIFAR-10 数据集调整了输入预处理,实现了网络模型,并观察了训练过程中的双下降和过拟合现象。



AlexNet 和之前学习的 LeNet 都属于基础的卷积神经网络架构。为了获得更好的性能,可以尝试更现代的架构,如使用批归一化、Leaky ReLU 等技巧的网络。实验表明,一个加入了批归一化和 Leaky ReLU 的 CNN 能在更短的训练时间(30分钟)内取得相近甚至略好的准确率,但过拟合问题依然存在。



在下一讲中,我们将更深入地探讨其他更强大的神经网络架构。
课程 P11:L1.6 - 实践工具与环境介绍 🛠️
在本节课中,我们将介绍本课程将使用的编程语言、核心库以及开发工具。我们将重点了解Python科学计算生态系统,并解释为何选择PyTorch作为主要的深度学习框架。
Python科学计算生态系统
上一节我们概述了课程的整体结构,本节中我们来看看实践环节将使用的具体工具。正如你所猜测的,我们将主要使用Python作为编程语言。
以下是Jake VanderPlas在2015年绘制的图表,它清晰地展示了Python科学计算生态系统的不同层次:

- 基础层:Python语言本身,是所有上层库的基础。
- 核心计算库:例如
IPython交互式计算环境、NumPy线性代数库。 - 科学计算扩展:
SciPy在NumPy基础上提供了更多科学计算算法。 - 数据操作与可视化:
pandas是数据处理库,Matplotlib是可视化库。 - 交互式笔记本:
Jupyter生态系统提供了Jupyter Notebook。 - 机器学习库:
scikit-learn是传统机器学习库,MLxtend是其功能扩展库。
需要说明的是,scikit-learn和MLxtend是我们在另一门课程(统计451)中使用过的库,但本课程几乎不会使用它们。本课程将专注于深度学习,而非传统机器学习,因此两门课程的重叠度极低。
本课程核心工具
在了解了整个生态系统后,我们聚焦到本课程将主要使用的工具。
在本课程中,我们将主要使用 Python 和 PyTorch。PyTorch是一个构建在Python之上的深度学习库。我们当然也会少量使用NumPy进行基础计算(特别是在下节课用于平缓入门),并在需要时使用Matplotlib进行可视化。但课程的核心将真正聚焦于PyTorch。
关于Jupyter Notebook
我个人非常喜欢Jupyter Notebook,并经常将其用于数据分析。然而,根据我的经验,在处理深度学习代码时,我最近开始更倾向于使用常规的Python脚本文件。
这主要有几个原因:深度学习代码有时较为冗长且运行耗时,有时需要在不同计算机上通过终端运行。在某些方面,脚本文件处理起来更方便。此外,在脚本编辑器或集成开发环境(IDE)中有更好的代码提示和高亮功能,调试代码也更容易。我稍后会再对此进行说明。
为何选择PyTorch?
接下来,我们解释一下选择PyTorch作为教学框架的原因。
我个人的使用历史如下:大约在2013-2014年,我开始使用名为Theano的库进行深度学习。2015年,谷歌发布了TensorFlow,我也开始像其他人一样使用它,并在过去几年中大量使用。
然而在2017年,PyTorch问世,它让许多事情变得方便得多。我个人感觉这个库更有条理,它支持动态计算图,并且更接近NumPy的风格。如今,PyTorch和TensorFlow的功能已经非常相似,都支持静态和动态图。但在2017-2019年间,从个人角度看,PyTorch比TensorFlow友好得多。
深度学习社区也非常青睐PyTorch。从下图的可视化数据可以看出,在2017年至2019年期间,各大顶级机器学习和深度学习会议上,PyTorch的使用量(图中实线)呈现出急剧增长,而TensorFlow的增长则相对平缓甚至有所下降。

因此,许多人最近都转向了PyTorch。当你阅读新论文并在GitHub上寻找代码时,现在最常找到的也是PyTorch实现(当然也常有TensorFlow代码)。PyTorch已经变得非常流行。
一个库的流行性很重要,因为如果你想实现更复杂的模型,会更容易找到教程、示例代码或论文对应的实现。对于本课程,我仍然认为PyTorch比TensorFlow更有条理、更易用,它更类似NumPy,学习曲线相对平缓,相信你会欣赏这一点。
开发环境建议

在介绍了核心库之后,我们来看看编写代码的环境。
正如之前提到的,对于深度学习,我如今更倾向于使用文本编辑器或IDE。原因在于,例如,如果我在这里打错字,编辑器会将其标出,并且提供了更多便捷功能。深度学习代码有时很长,我发现在文本编辑器中开发代码更为容易。
我仍然大量使用Jupyter Notebook,例如今天就在处理一篇论文的可视化工作。对于数据分析这类任务,Jupyter Notebook确实非常出色。但对于编写网络结构等较长的代码,我个人觉得文本编辑器更方便。
我推荐使用诸如 Visual Studio Code 这样的文本编辑器。它免费、快速,并支持Windows、Linux和macOS等所有主流操作系统。它对Python有很好的支持,包括调试等功能。
一个简单的PyTorch示例
为了让你对PyTorch代码有一个直观感受,这里有一个简短的示例。
我实现了一个之前提到过的LeNet-5网络。在PyTorch中,实现这个相当复杂的卷积网络实际上只需要几行代码。当然,训练模型还需要更多代码,但其实并不复杂。在本课程后续,我们将逐步详细讲解。现在你无需理解其工作原理,我只想展示代码其实并不复杂。
# 这是一个简化的LeNet-5模型结构示例
import torch.nn as nn

class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.pool = nn.AvgPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 4 * 4, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x)))
x = self.pool(torch.relu(self.conv2(x)))
x = x.view(-1, 16 * 4 * 4)
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
运行这段代码(在配备GPU的计算机上),对包含60,000张图像的训练集进行10轮(epoch)训练,耗时不到一分钟,速度非常快。训练后,模型在验证集上可以达到接近99%的准确率。

你可以看到,甚至在第二轮或第四轮训练后,准确率就已经接近99%,这令人印象深刻。图中也能看到验证集和训练集准确率之间存在差距,这暗示了过拟合现象,我们后续也会详细讨论。这只是深度学习工作方式的一个快速演示。

课程资料与总结

如果本节介绍中仍有不清楚的地方,请不要过于担心,这只是一个概述。我注意到我在此处或彼处涉及了太多细节。
我实际上为此撰写了一篇博客文章,因为我计划基于我的讲义编写一本小教科书(这是一个进行中的项目)。无论如何,我已将第一章作为博客文章上传,你可以阅读我所讲述内容的所有书面形式。
对于那些只对传统机器学习概述感兴趣的人,可以找到451课程的讲义。当然,你不必阅读它。另外,也许有帮助的是我的《Python机器学习》书籍的第一章,它可以说是这篇博客文章的较短版本,是从不同角度撰写的,是我很久以前写的东西。
本节课中我们一起学习了本课程将使用的核心编程语言Python、深度学习框架PyTorch,以及推荐的开发环境。我们了解了选择PyTorch的原因,并通过一个简单示例感受了其代码风格。

在下一讲中,我们将回顾神经网络与深度学习的简史,概述其令人兴奋之处,并介绍主要的网络架构。这将为你提供整个学期将要涵盖主题的全景概览。
📰 深度学习新闻 #9,2021年3月27日
在本节课中,我们将一起回顾2021年3月27日发布的深度学习新闻。主要内容包括一个新的自监督学习方法、深度学习在因果关系理解方面的挑战、以及一些实用的工具介绍。这些内容旨在拓宽视野,了解领域内的最新动态,但请注意,它们并非考试内容。
🎙️ 新发现的机器学习播客
上一节我们介绍了课程概述,本节中我们来看看一个有趣的播客。
本周我发现了一个很棒的播客,由知名深度学习研究员Peter Abiil主持。在第一期节目中,他采访了特斯拉的AI总监Andrej Karpathy。他们主要讨论了机器学习和深度学习在工业界的实际应用,特别是在特斯拉自动驾驶系统中的应用。

以下是该播客的一些关键要点:
- 特斯拉的自动驾驶系统主要依赖车载摄像头和卷积神经网络。
- 在工业实践中,专注于收集更多、质量更好的数据,往往比单纯地微调模型更能有效提升性能。这与学术界专注于在固定基准数据集上优化模型的做法有所不同。
🧠 新的自监督学习方法:Barlow Twins
上一节我们了解了一个关于工业实践的播客,本节中我们来看看一个新颖的自监督学习技术。
本周我发现了一个名为 Barlow Twins 的自监督学习方法。其核心思想是通过减少数据表征中的冗余信息来学习有效的特征,而无需使用人工标注的标签。
该方法的工作原理如下:
- 对同一张图像生成两个不同的增强版本(例如,改变亮度、颜色或轻微旋转)。
- 使用同一个卷积神经网络(这种结构称为孪生网络)分别处理这两个增强图像,得到两个特征向量。
- 计算这两个特征向量之间的互相关矩阵。
- 设计一个损失函数,迫使这个互相关矩阵接近单位矩阵。
核心公式/目标:
网络的目标是最小化以下损失函数,它衡量了互相关矩阵 C 与单位矩阵 I 的差异:
损失 = Σ(1 - Cᵢᵢ)² + λ * ΣΣ_{i≠j} Cᵢⱼ²
其中,第一项鼓励对角线元素(自相关)接近1,第二项惩罚非对角线元素(互相关),迫使它们接近0。λ 是一个控制惩罚权重的超参数。
代码风格描述:
# 伪代码示意
def barlow_twins_loss(z1, z2, lambda_param):
# z1, z2: 两个增强视图的特征向量,已标准化
batch_size = z1.size(0)
c = torch.mm(z1.T, z2) / batch_size # 计算互相关矩阵C
on_diag = (torch.diag(c) - 1).pow(2).sum() # 对角线接近1
off_diag = (c.flatten()[:-1].view(batch_size-1, batch_size+1)[:, 1:].pow(2).sum() # 非对角线接近0
loss = on_diag + lambda_param * off_diag
return loss
训练完成后,这些学到的特征可以用于下游任务。例如,在ImageNet数据集上,冻结特征提取器,仅训练一个简单的线性分类器(如逻辑回归),就能达到73.2%的Top-1准确率。这个方法因其简洁性和有效性而颇具吸引力。
🛡️ 仇恨言论检测与多标签分类挑战
上一节我们探讨了自监督学习,本节中我们来看看一个具有社会意义的AI应用挑战。
Facebook AI Research等机构组织了一个关于检测网络仇恨言论的研讨会和竞赛,特别关注仇恨表情包(Meme)的识别。这可以作为一个有趣的多标签分类项目。
多标签分类与我们在课程中常见的多类别分类(如MNIST手写数字识别)不同:
- 多类别分类:每个样本只属于一个类别(互斥),输出层使用Softmax函数,所有类别概率之和为1。
P(class=i | x) = exp(z_i) / Σ_j exp(z_j) - 多标签分类:一个样本可以同时属于多个类别(例如,一个表情包可能同时攻击种族和宗教)。输出层通常为每个类别使用独立的Sigmoid函数,判断其是否存在。
P(has_label=i | x) = 1 / (1 + exp(-z_i))
以下是该竞赛的一些信息:
- 任务包括预测受保护的类别(如种族、残疾、宗教等)。
- 这是一个典型的多标签分类问题,可以使用卷积神经网络等模型解决。
- 竞赛提供了参与和提交论文的机会。

⚙️ 深度学习的因果推理挑战
上一节我们讨论了具体的应用挑战,本节中我们来看看深度学习面临的一个更基础的理论挑战。
当前深度学习模型主要依赖于独立同分布的数据,并学习输入与输出之间的统计相关性,而非因果关系。一篇近期的论文探讨了推动深度学习向因果表征学习发展的挑战和方向。
理解因果关系的重要性体现在:
- 增强模型鲁棒性:使模型对对抗性攻击(如干扰交通标志的激光)或分布外数据更稳健。
- 提升数据效率:人类无需从所有角度观察一把椅子就能识别它,因为理解了“椅子”的因果概念。具备因果理解的AI可能也需要更少的训练样本。
- 促进模型迁移与复用:在强化学习中,一个学会玩《帝国时代》的智能体,如果理解了战略游戏的因果逻辑,可能更容易迁移到《星际争霸》中。

然而,实现因果学习面临巨大挑战,例如如何从数据中推断出抽象的因果变量。这仍然是未来研究的重要方向。

🛠️ 实用工具推荐
上一节我们探讨了理论挑战,本节中我们来看看一些能提升开发效率的实用工具。
以下是本周发现的一些有用工具:

1. 图像标注工具:Label Studio
这是一个开源的图像数据标注工具,特别方便进行目标检测等任务的标注,界面友好,功能强大。
2. 代码性能分析:PyTorch Profiler 与 TensorBoard
在优化代码时,找到性能瓶颈至关重要。新发布的PyTorch Profiler与TensorBoard集成,可以可视化地分析模型训练过程中各环节的时间消耗和内存使用,帮助快速定位瓶颈(例如,低效的数据遍历或计算)。

3. 实验追踪与管理:Weights & Biases
这是一个用于比较不同实验(如不同超参数配置)结果的工具。只需添加几行代码,它就能自动记录实验指标并生成可视化对比面板,非常适合模型调优和实验管理。类似的工具还有MLflow等。



📚 总结与预告

本节课中我们一起学习了以下内容:
- 一个探讨工业界AI实践的播客,强调了数据质量的重要性。
- 一种名为Barlow Twins的简洁有效的自监督学习方法。
- 仇恨言论检测竞赛中涉及的多标签分类问题。
- 当前深度学习在因果推理方面面临的挑战与未来展望。
- 三个提升工作效率的实用工具:Label Studio、PyTorch Profiler/TensorBoard和Weights & Biases。
这些内容展示了深度学习领域的活跃发展与多元应用。最后提醒,本周二有考试,祝大家好运。此外,我将在周三进行一次线上讲座,感兴趣的同学可以通过Piazza获取详细信息。
P111:L14.0- 卷积神经网络架构 🏗️
课程概述
在本节课中,我们将学习卷积神经网络的一些高级概念。在前几讲中,我们已经涵盖了深度学习的基础知识。本节课将作为卷积神经网络的收尾,探讨几个值得了解的重要主题。接下来的一讲,我们将转向用于文本分类的循环神经网络。
完成这些高级概念和循环神经网络的讲解后,我们将转换主题,讨论生成模型,包括自编码器、生成对抗网络和Transformer。这样,我们既能掌握深度学习的基础,也能了解其在生成式建模中的应用,从而对整个领域有一个全面的认识。
对于本课程而言,虽然不再有考试,但每周仍有测验和课程项目需要完成。由于基础内容已基本覆盖,后续课程将尝试控制在75分钟以内,以便大家有更多时间进行实践。
本节课内容经过精简,但所涵盖的主题对于实际应用卷积神经网络至关重要。如果你对某些主题特别感兴趣,可以进一步研读相关论文。
课程大纲
以下是本节课将要探讨的八个主题:
- 填充:介绍如何通过添加像素来控制卷积操作的输出尺寸。
- 空间Dropout与空间批归一化:将熟悉的Dropout和批归一化概念扩展到二维卷积场景。
- 经典网络架构:介绍VGG16和ResNet这两种重要且至今仍有竞争力的卷积网络架构。
- 用卷积层替代最大池化:探讨如何构建一个仅由卷积层组成的网络。
- 用卷积层替代全连接层:思考如何进一步简化网络结构。
- 迁移学习:学习如何利用在大数据集上预训练的模型来提升小数据集上的性能。
1. 填充 🧩
上一节我们回顾了卷积网络的基础。本节中,我们来看看填充这个概念。填充允许我们在输入图像的边缘添加像素,从而精确控制卷积层输出特征图的大小。
当我们应用卷积核时,输出尺寸通常会缩小。通过填充,我们可以保持或调整输出尺寸。这在构建深层网络时非常有用,可以避免特征图尺寸过快地减小。
公式:对于一个输入尺寸为 n x n、卷积核尺寸为 f x f、步长为 s、填充为 p 的情况,输出尺寸 o 的计算公式为:
o = floor( (n + 2p - f) / s ) + 1
通过调整填充值 p,我们可以实现“相同填充”,使输出尺寸与输入尺寸相同。



2. 空间Dropout与空间批归一化 📊

我们已经了解了Dropout和批归一化在标准神经网络中的作用。在卷积神经网络中,我们可以将它们扩展到二维空间。

空间Dropout:在卷积层中,我们不是随机丢弃单个神经元,而是随机丢弃整个特征图(通道)。这有助于防止特征之间的协同适应,增强模型的泛化能力。


空间批归一化:批归一化同样可以应用于卷积层。它对每个特征通道单独进行归一化,即沿着批次、高度和宽度维度计算均值和方差,然后进行缩放和平移。这有助于稳定训练过程,加快收敛速度。

代码示意(PyTorch):
# 空间Dropout2d
torch.nn.Dropout2d(p=0.5)

# 批归一化2d
torch.nn.BatchNorm2d(num_features)
3. 经典网络架构:VGG16与ResNet 🏛️
之前我们学习了LeNet-5和AlexNet。本节中,我们来看看两个更现代且影响深远的架构:VGG16和ResNet。它们虽然已有数年历史,但在许多数据集上依然表现优异。

VGG16:其核心思想是通过堆叠多个小尺寸(3x3)的卷积核来替代大尺寸卷积核,在保持相同感受野的同时,减少了参数量,并增加了网络深度。VGG16结构非常规整,全部使用3x3卷积和2x2最大池化。




ResNet:随着网络加深,模型可能会遇到梯度消失/爆炸或退化问题。ResNet引入了残差连接这一巧妙设计。它不再让网络层直接拟合目标映射,而是拟合残差映射。



公式:残差块的核心公式为:
输出 = 恒等映射(x) + 残差函数F(x)
其中 F(x) 由几层卷积组成。即使 F(x) 学习为零,输出仍等于输入 x,这保证了至少不会比浅层网络更差。

这种“短路连接”使得训练极深的网络(如ResNet-152)成为可能,并显著提升了性能。


4. 用卷积层替代最大池化 🔄


最大池化层常用于下采样,减少特征图尺寸并增加平移不变性。但我们可以思考:能否用卷积层实现类似效果?
答案是肯定的。我们可以使用步长大于1的卷积层(例如,步长为2的2x2卷积)来替代最大池化。这样,网络可以完全由卷积层构成,所有参数都是可学习的,理论上更具灵活性。


这种设计在某些架构(如全卷积网络)中被采用,使得网络能够处理任意尺寸的输入。


5. 用卷积层替代全连接层 🧠
在传统卷积网络末端,通常会连接几个全连接层来进行分类。我们进一步思考:能否也用卷积层替代它们?

可以。我们可以将最后的全连接层视为使用与输入特征图尺寸相同的卷积核进行的全局卷积操作。例如,对于一个7x7xC的特征图,使用7x7的卷积核进行卷积,每个卷积核会产生一个标量输出,这等价于一个全连接层。




这种视角的转变有助于我们理解卷积网络的本质,并构建“全卷积网络”,使其能够更灵活地处理输入。

6. 迁移学习 🚀


最后,我们讨论一个通用但极其重要的概念:迁移学习。它并非卷积网络专属,但在计算机视觉领域应用尤为成功。


迁移学习的核心思想是:先在一个大型通用数据集(如ImageNet,包含数百万张图像)上训练一个模型,学习到通用的视觉特征。然后,将这个预训练模型的权重作为起点,在我们关心的、规模较小的特定目标数据集上进行微调。

为什么有效? 底层特征(如边缘、纹理)通常是通用的。预训练模型已经学到了这些特征,我们只需让其高层特征适应新任务即可。

如何操作? 通常,我们冻结预训练模型的前几层权重,只重新训练最后几层或新添加的分类层。这大大减少了对目标数据集数据量的需求,并加快了训练速度。
对于课程项目,如果你处理图像问题,利用在ImageNet上预训练的VGG16或ResNet等模型进行微调,是一个非常有效的策略。
总结



本节课我们一起学习了卷积神经网络的多个高级主题。




我们首先学习了填充技术来控制输出尺寸。接着,探讨了空间Dropout和批归一化在卷积场景下的应用。然后,深入分析了VGG16和ResNet这两个经典架构,特别是ResNet的残差连接如何解决了深度网络的训练难题。我们还进行了两个思想实验:如何用卷积层替代最大池化和全连接层,以构建更统一的网络结构。最后,我们介绍了强大的迁移学习技术,它允许我们利用大规模预训练模型来高效解决小数据集的视觉任务。

掌握这些概念,将帮助你更深入地理解现代卷积神经网络的设计思想,并能够更有效地将其应用于实际问题中。
课程 P112:L14.1 - 卷积和填充 🧠
在本节课中,我们将要学习卷积神经网络中的填充机制。填充是一种与步长配合使用,用于控制卷积层输出尺寸的技术。我们将通过公式和示例,详细解释其工作原理和计算方法。
概述
上一节我们介绍了卷积的基本概念和步长。本节中我们来看看填充。填充允许我们在输入图像的边缘添加额外的像素(通常是零值),从而控制卷积操作后输出特征图的大小。它既可以防止输出尺寸过快缩小,也能在某些情况下保持输入与输出的尺寸相同。
填充的基本概念
填充的核心思想是在输入图像的四周添加指定数量的像素行/列。以下是填充的几个关键点:
- 目的:控制输出尺寸,使其大于或等于不填充时的尺寸。
- 常见值:添加的像素值通常设置为0。
- 对称性:填充可以是对称的(上下左右填充相同数量),也可以是非对称的。
下图展示了在一个4x4的输入上,使用3x3卷积核、步长为1时,不同填充值的效果:


输出尺寸计算公式
计算卷积层输出尺寸的通用公式如下。该公式同时考虑了输入尺寸、卷积核尺寸、步长和填充:

公式:
output_size = floor((input_size + 2*padding - kernel_size) / stride) + 1
其中:
floor()表示向下取整。input_size是输入的高度或宽度。kernel_size是卷积核的高度或宽度。stride是步长。padding是填充的像素数(假设为对称填充)。
这个公式适用于输出的高度和宽度计算。
填充效果示例分析
以下是几个具体的示例,展示了不同填充和步长设置下的输出结果:

- 无填充,步长为1:输入为4x4,使用3x3卷积核。根据公式计算,输出为2x2。卷积核在滑动时会“丢失”边缘的像素信息。
- 填充为2,步长为1:输入为5x5,使用4x4卷积核。添加两圈零值填充后,输出尺寸增大。
- 无填充,步长为2:输入为5x5,使用3x3卷积核。步长增大导致输出尺寸进一步缩小至2x2。
“Valid”与“Same”卷积
在深度学习框架中,常会遇到两种特定的填充模式术语:


- Valid 卷积:指不进行任何填充的操作。其结果是输出尺寸会小于输入尺寸。上图中的示例1和示例3即属于此类。


- Same 卷积:指通过填充,使得输出尺寸与输入尺寸完全相同的操作。这需要精心选择填充值。

如何实现“Same”卷积
为了实现“Same”卷积(输出尺寸等于输入尺寸),我们可以对公式进行推导。为了简化,我们先假设步长 stride = 1,并忽略向下取整操作。


推导过程:
我们希望 output_size = input_size。
代入公式:
input_size = (input_size + 2*padding - kernel_size) / 1 + 1
两边整理后得到:
padding = (kernel_size - 1) / 2

结论:当步长为1时,要得到“Same”卷积,所需的对称填充量 padding 应为 (kernel_size - 1) / 2。这解释了为何实践中常见的卷积核尺寸是3x3、5x5、7x7等奇数,因为这样计算出来的填充量是整数,便于对称填充。
例如,对于3x3卷积核:padding = (3-1)/2 = 1。
对于5x5卷积核:padding = (5-1)/2 = 2。

总结
本节课中我们一起学习了卷积神经网络中的填充技术。我们了解到:
- 填充通过在输入边缘添加像素(常为0)来影响输出尺寸。
- 掌握输出尺寸的通用计算公式
output_size = floor((input_size + 2*padding - kernel_size) / stride) + 1至关重要。 - “Valid”卷积代表无填充,“Same”卷积代表通过填充使输入输出尺寸一致。
- 为实现“Same”卷积,通常选择奇数尺寸的卷积核,并设置
padding = (kernel_size - 1) / 2(步长为1时)。
理解填充是构建有效CNN架构的基础,它帮助我们精细控制网络中层与层之间的空间维度变化。




下一节预告:在接下来的内容中,我们将探讨空间Dropout和空间池化操作。
🧠 课程 P113:L14.2 - 空间丢失与批量归一化
在本节课中,我们将学习如何将熟悉的概念,如 Dropout 和 BatchNorm,应用到卷积神经网络(CNN)的二维图像处理场景中。这些概念在卷积设置下分别被称为 空间丢失 和 批量归一化。虽然听起来很高级,但你会发现它们的实现非常直接,无需太多额外工作。
🔍 为什么需要空间丢失?

上一节我们介绍了多层感知机中的 Dropout。那么,为什么在卷积网络中需要一个新的版本或进行修改呢?
当然,你可以直接使用之前学过的常规 Dropout。但根据 Thompson 在下方链接论文中的观点,常规 Dropout 在图像场景中存在一个问题。
在图像处理中,我们通常使用卷积核在图像上滑动,相邻像素之间通常具有高度相关性。因此,如果在某个感受野中随机丢弃一半的像素,除了可能带来一些缩放差异外,并不会对输出产生太大影响。
我们可以这样理解:假设有一张人脸图像,其中有许多像素共同构成了一只眼睛。如果我们将这些像素中的一半屏蔽掉,眼睛这个概念本身并没有发生根本性改变。
因此,论文中的观点是:与其在特征图中随机丢弃单个像素位置,不如丢弃整个通道。在网络的后几层,这些通道通常代表了更高级、更宏观的概念,正如我们在上一节纹理分析中提到的,一个通道可能代表检测到的眼睛,另一个代表嘴巴,等等。
空间丢失的核心思想是:通过丢弃这些高级特征,即丢弃整个特征图,而不是单个像素。本质上,这是将 Dropout 应用于通道而非像素。
💻 如何在 PyTorch 中实现空间丢失?
在 PyTorch 中实现空间丢失非常简单。你只需要使用 nn.Dropout2d 来代替之前使用的 nn.Dropout1d。
以下是一个示例,展示了它的工作原理。图中每个方框代表一个通道。假设我们有一个具有三个通道的随机输入,可以看到其中两个通道被置零。这就是空间丢失的工作方式。

import torch.nn as nn
# 定义一个空间丢失层,丢弃概率为 0.5
spatial_dropout = nn.Dropout2d(p=0.5)

📊 卷积网络中的批量归一化
接下来,我们看看批量归一化。这与我们在全连接层中使用的 BatchNorm1d 类似,但在卷积层中,我们使用 BatchNorm2d。
为了快速回顾,我们之前在全连接网络中使用 BatchNorm1d 时,是针对每个特征在批次维度上计算均值和方差。假设输入维度是 N x M(N 是批次大小,M 是特征数量),那么我们会为每个特征计算一组独立的缩放参数 γ(gamma)和偏移参数 β(beta)。如果有三个特征,就有三组 γ 和 β。
现在,我们将这个概念扩展到二维情况。卷积层的输入是四维张量:[批次大小, 通道数, 高度, 宽度]。
BatchNorm2d 的计算方式是:在批次、高度和宽度这三个维度上计算均值和方差。因此,γ 和 β 的数量与通道数相对应。如果有 64 个通道,就有 64 个 γ 和 64 个 β。
💻 如何在 PyTorch 中实现批量归一化?
在 PyTorch 中实现 BatchNorm2d 同样直接。你需要在卷积层之后添加它,并指定特征数量(即输出通道数)。
import torch.nn as nn
# 定义一个卷积层,输入通道为 3,输出通道为 192
conv_layer = nn.Conv2d(in_channels=3, out_channels=192, kernel_size=3)
# 在卷积层后添加批量归一化层,参数数量需与输出通道数匹配(192)
batch_norm = nn.BatchNorm2d(num_features=192)

在上面的代码中,num_features 参数设置为 192,因为卷积层输出了 192 个通道,所以批量归一化层会有 192 个 γ 和 192 个 β 参数。



📝 本节总结
本节课中,我们一起学习了如何将 Dropout 和 BatchNorm 适配到卷积神经网络中:

- 我们了解了空间丢失的原理:它通过丢弃整个特征图通道来更有效地正则化卷积网络,避免因像素间相关性而削弱 Dropout 的效果。
- 我们学习了批量归一化在卷积中的应用:
BatchNorm2d在批次、高度和宽度维度上计算统计量,并为每个通道学习独立的 γ 和 β 参数。 - 我们掌握了在 PyTorch 中实现这两个层的简单方法:使用
nn.Dropout2d和nn.BatchNorm2d。

在下一节视频中,我们将简要回顾不同的卷积网络架构,并深入探讨 VGG16 和 ResNet 模型。
🏗️ 课程 P114:L14.3 - 架构概述
在本节课中,我们将回顾几种经典的卷积神经网络架构,并了解它们的发展历程与核心特点。通过对比不同架构的参数规模与性能,我们可以理解设计高效网络不仅依赖于层数,还需要借助各种技巧。

上一节我们回顾了AlexNet,本节中我们来看看更多后续发展的网络架构。
以下是2016年一篇论文中展示的当时几种常见架构的概览图。这张图清晰地展示了该领域快速发展的态势。

图中列出的架构并不全面,仅是当时部分常见的选择。好消息是,架构数量并未呈指数级增长。直至2021年,VGG和ResNet等架构因其优秀的基线性能,仍被广泛用作各种方法的骨干网络。
然而,如今还有一些其他流行的架构未在图中列出。需要说明的是,图中架构主要针对图像分类任务,其他任务则有另一套不同的架构体系。
以下是其他一些常见的架构示例:
- MobileNet
- Wide Residual Networks
- EfficientNet
- DenseNet
- Highway Networks
尽管如此,原图仍是一个很好的入门概览。如果你想开始尝试不同的架构,我推荐也考虑上述这些。

从该图中可以得到的一个重要启示是:网络性能并非只与参数量有关。

例如,VGG16参数量相对较大,但其性能却不如Inception网络或残差网络。这是因为后者融入了一些特殊的设计技巧。
我们将要讨论的残差网络(ResNet)就采用了残差连接。这种连接有助于构建更深的网络,同时缓解梯度消失问题。
而Inception网络则采用了独特的方式,在同一层中组合不同尺寸的卷积核。我原本计划更详细地讲解Inception,但由于时间关系,我们将跳过这部分。它的技巧包括将不同大小的卷积组合到一层中,以及在网络中间层添加辅助损失函数。这类技巧都能帮助提升架构性能。
VGG16本质上只是一个16层的卷积网络,层数已经很多。但设计网络不能只看层数,还需要各种技巧。例如,ResNet-152拥有152层。在普通的VGG架构背景下,直接堆叠152层是不可能的,因为会遇到严重的梯度消失问题。
不过最近,也有一些研究致力于开发不使用残差连接的极深网络。例如,有趣的无归一化ResNet(Normalizer-Free ResNet)甚至去除了批归一化层。这表明该领域仍在持续产生有趣的发展。
总而言之,这些仍是基础且重要的架构。
在下一个视频中,我将首先讨论VGG16架构。




本节课中我们一起回顾了从AlexNet到ResNet等经典CNN架构的发展脉络,理解了网络性能的提升不仅依赖于增加深度和参数量,更得益于残差连接、多尺度卷积融合等核心设计技巧。这些基础架构为后续更复杂的模型奠定了重要基础。
课程 P115:L14.3.1.1 - VGG16 架构概述 🏗️

在本节课中,我们将具体学习 VGG16 架构。这是一个非常简洁、直接的网络结构。
架构简介
上一节我们介绍了 AlexNet,VGG16 可以看作是它的一个延伸,但层数更多。本质上,它是一个包含 16 层的网络。

作为参考,根据上一视频展示的图表,VGG16 位于此处。可以看到它相对较大,实际上是图中第二大的网络,拥有大约 1.25 亿个参数。图中更大的网络是 VGG19,一个有 19 层的变体,但增加这三层并未显著改变性能。因此,我们重点讨论 16 层的版本。

本节视频主要展示架构的外观,下一节视频将通过代码实现它。本概述基于 2014 年的论文《Very Deep Convolutional Networks for Large-Scale Image Recognition》,虽然已有七年历史,但其结构简单,易于实现和实验,非常适合学习。
核心特点与结构
VGG16 的一个优点是结构相对直观,这使得编码实现非常简单。

其核心是大量使用 3x3 卷积核。所有卷积核大小均为 3x3,这使得结构非常简洁。卷积步长(stride)为 1,并采用“相同卷积”(same convolution)方式,通过填充(padding)使得每次卷积后输入与输出的空间尺寸(高度和宽度)保持一致。




网络使用 2x2 最大池化(max pooling)来减小特征图尺寸,上图未展示,下一张幻灯片会说明。
需要注意的一个方面是,VGG16 参数量非常大,因此运行速度也较慢。回顾图表,它看起来很大。一个自然的问题是:它只有 16 层,为什么如此庞大?相比之下,我们将在后续课程中看到的 ResNet-34 有 34 层,但参数量却小得多。VGG16 庞大的原因主要在于其通道数。
以下是网络中一个卷积层的参数计算示例:




以其中一个卷积块为例,它有 512 个输入通道和 512 个输出通道。对于一个 3x3 卷积,每个卷积核的参数数量是 3 * 3 * 512。由于有 512 个这样的卷积核,所以权重参数总量为 3 * 3 * 512 * 512。此外,每个输出通道还有一个偏置(bias)参数,因此总参数量非常庞大。这样的结构在网络中重复多次,参数量便累积起来。
全连接层部分同样巨大,例如一个 4096x4096 的全连接层就有超过 1600 万个权重参数。多个这样的层叠加,使得总参数量惊人。
架构可视化与设计理念

这里有一个来自相关网站的、更直观的架构可视化图。

卷积网络的一个基本设计理念是“挤压”出特征信息。通常遵循一个通用趋势或指导原则:随着网络加深,逐步减小特征图的高度和宽度,同时增加通道数。
以下是 VGG16 的具体流程:
- 输入图像尺寸为
224x224,具有 3 个颜色通道(RGB)。 - 经过第一次卷积后,我们得到 64 个通道的特征图。这里使用了“相同卷积”以保持空间尺寸。
- 图中红色部分代表 2x2 最大池化,它将特征图尺寸减半。
- 随后,网络继续进行多轮类似的“挤压”操作。
- 可以看到,通道数(网络的“宽度”)在增加,而特征图的高度和宽度在减小。这正是在逐步提炼和浓缩图像中的信息。
- 最后是全连接层部分。实际上,全连接层也可以用卷积层等效表示,这将在后续视频中展示,因此上图也以此方式呈现。
总结

本节课我们一起学习了 VGG16 架构的概述。我们了解到它是一个层数较深、结构规整的卷积神经网络,核心是堆叠多个使用小尺寸(3x3)卷积核和最大池化的模块。其设计遵循了逐步减少空间尺寸、增加特征通道数的经典模式。尽管参数量庞大导致计算效率较低,但其清晰的结构使其成为理解深度卷积网络基础的优秀范例。在下一节中,我们将通过代码来具体实现这个架构。

课程 P116:L14.3.1.2 - PyTorch 中的 VGG16 🏗️

在本节课中,我们将学习如何在 PyTorch 中实现和训练 VGG16 网络。我们将回顾代码结构、数据预处理、模型定义以及训练过程,并分析训练结果。


概述


我们将查看一个已实现的 VGG16 代码示例。由于完整训练耗时较长(约1.5小时),本节将主要分析代码结构和训练结果。我会分享相关代码链接。
导入与设置
首先,我们导入必要的库并设置环境。
import watermark
import torch
import torchvision
# ... 其他导入



我的辅助文件与上周用于 AlexNet 的文件相同,唯一的区别在于网络架构。

超参数设置

以下是训练中使用的主要超参数。
- 随机种子:设置为
123,用于保证结果可复现。 - 批大小:
256。 - 训练轮数:
50。 - 设备:使用 GPU 进行训练以加速计算(例如在 Google Colab 上)。




关于 GPU 计算的说明
在 GPU 上训练时,PyTorch 可能会自动选择不同的高效卷积算法(如基于快速傅里叶变换的算法)。这些算法是卷积的近似,虽然精度极高,但不同算法或多次运行间可能存在微小差异,导致结果略有不同。这是深度学习中的正常现象。





数据准备
我们使用 CIFAR-10 数据集,其原始图像尺寸为 32x32。
VGG16 原设计输入尺寸为 224x224。对于过小的输入,经过多层池化后特征图尺寸会变得极小,影响性能。因此,我们将图像上采样至 70x70。

以下是训练集的数据增强流程:
- 随机裁剪:将 70x70 的图像随机裁剪回 64x64,以减轻模型对像素位置的过拟合。
- 转换为张量。
- 标准化:使各通道像素值均值为 0,标准差为 1。
测试集仅进行中心裁剪,不进行随机裁剪。
VGG16 模型架构
接下来是模型的核心部分——VGG16 架构的实现。
我将其分为多个“块”。每个块包含若干卷积层(保持特征图尺寸不变),最后接一个最大池化层(将特征图尺寸减半)。

# 示例:一个卷积块的结构
self.block1 = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.ReLU(),
# ... 更多卷积层
nn.MaxPool2d(kernel_size=2, stride=2) # 尺寸减半
)

这些卷积块构成了特征提取器。之后是分类器部分,即全连接层(对应原论文图中的浅蓝色部分),包含线性层和 Dropout。


权重初始化采用 kaiming_uniform_ 方法,与原论文一致。


在进入全连接层之前,我们使用了自适应平均池化(nn.AdaptiveAvgPool2d((3, 3)))。该操作会将任意尺寸的输入调整到指定的 3x3 输出,从而便于我们计算展平后的特征数量,以匹配全连接层的输入维度。


在实践中,若不想计算尺寸,一个简单的方法是在前向传播中临时添加 print(x.size()) 语句来查看特征图尺寸,然后再确定全连接层的参数。




模型训练

模型针对 CIFAR-10 的 10 个类别进行初始化(原 VGG16 在 ImageNet 上针对 1000 个类别训练)。
优化器使用带动量的随机梯度下降(SGD)。我们还使用了学习率调度器,当验证集准确率不再提升时,将学习率降低为原来的十分之一。


训练脚本与之前 AlexNet 使用的相同。在训练初期观察损失和准确率的变化非常重要,如果模型没有学习迹象,可以提前停止以调整参数,节省时间。


训练结果与分析
训练完成后,我们可视化结果。


损失曲线平稳下降,在 50 轮后基本收敛。验证集准确率(橙色曲线)显示模型存在明显的过拟合。为了缓解过拟合,可以在卷积层后使用 Dropout2d(而非普通的 Dropout),这通常对卷积网络更有效。


查看部分预测样本,模型大多能正确分类。CIFAR-10 图像分辨率较低,有时人类也难以辨认。


混淆矩阵显示了一个有趣的现象:模型容易混淆“猫”和“狗”等动物类别,但很少将动物与“飞机”或“汽车”等物体混淆。这符合直觉,因为动物之间的特征差异可能小于动物与机械物体之间的差异。
关于标准化的补充


我尝试了更“精确”的标准化方法,即计算数据集中每个通道的实际均值和标准差,而非简单地使用 [0.5, 0.5, 0.5]。


# 计算得到的近似均值和标准差
mean = [0.4914, 0.4822, 0.4465]
std = [0.2470, 0.2435, 0.2616]

但实验表明,使用精确值并未带来性能提升,准确率反而略有下降(约 82%),这可能与过拟合或学习率调整的时机有关。



总结

本节课我们一起学习了 VGG16 在 PyTorch 中的完整实现流程。我们涵盖了数据预处理、模型架构的模块化构建、训练过程监控以及结果分析。关键点在于理解如何通过堆叠卷积块构建深层网络,以及使用自适应池化来衔接卷积层与全连接层。同时,我们也观察到深层网络容易过拟合,需要采用如 Dropout2d 等正则化技术。

在下一课中,我们将探讨比单纯堆叠层更有趣的网络结构——残差网络。
课程 P117:L14.3.2.1 - ResNet 概述 🧠
概述
在本节课中,我们将要学习一种名为残差网络的深度神经网络架构。我们将探讨为什么简单地增加网络层数并不总能提升性能,以及残差网络如何通过一种巧妙的“捷径连接”来解决深层网络训练中的梯度消失问题。

从VGG16到更深层的网络
在之前的课程中,我们讨论了VGG16架构。一个很自然的问题是:我们能否添加更多层,并期望性能得到进一步提升?
理论上,增加层数应该有所帮助。但在实践中,我们之前讨论过的梯度消失等问题可能会出现。在反向传播中,我们使用链式法则,它本质上是多个导数的乘积。如果其中某个导数值非常小,即使是在网络早期,它也可能抵消掉反向传播中所有后续的信号。因此,我们不能仅仅通过增加层数就期望性能变得更好,这在实验中也得到了证实。

残差网络的核心思想
为了解决上述问题,研究人员提出了残差网络。残差网络依赖于所谓的捷径连接,它本质上可以让网络学会跳过某些对性能有害的层。这是一种应用于卷积神经网络的简单技巧。
让我们来看看它的结构。
残差块的结构
假设有一个输入 X,这可以是网络中任意一层的输入。我们称之为该层的输入 A^[l]。
一个基础的残差块包含以下步骤:
- 一个权重层(例如卷积层)。
- 一个ReLU激活函数。
- 另一个权重层。
- 一个加法操作,将第3步的输出与原始的输入
A^[l]相加。 - 最后再经过一个ReLU激活函数。
用公式表示这个过程:
A^[l+2] = ReLU( F(A^[l]) + A^[l] )
其中,F(A^[l]) 代表经过两个权重层和第一个ReLU后的输出。这个加法操作就是捷径连接。

为什么叫“残差”网络?
之所以称为“残差”,是因为如果我们重新排列上述公式,可以将 F(A^[l]) 看作是学习输出与输入之间的残差。即,网络学习的是 A^[l+2] 与 A^[l] 的差值。但这更多是一个概念上的理解。
更重要的是,这种结构允许网络学习恒等映射。如果网络发现这两个权重层是有害的,它可以通过将权重和偏置学习为接近零的值,使得 F(A^[l]) ≈ 0。这样,输出就近似等于输入 A^[l],相当于跳过了这两个层。这确保了即使某些层效果不佳,信号也能通过捷径连接顺畅地传递,缓解了梯度消失问题。

残差网络架构全景
以下是残差网络架构的一个可视化概览。研究人员对比了一个34层的“普通”网络和一个34层的“残差”网络。
他们发现,没有捷径连接的普通深层网络性能较差。而添加了捷径连接的残差网络显著提升了性能。这种实验通常被称为“消融研究”,即通过移除网络的某个部分来观察其影响。
深入残差块:维度匹配问题
在基础残差块中,我们做了一个重要假设:输入 A^[l] 和经过变换后的输出 F(A^[l]) 必须具有相同的维度,因为加法操作要求两者形状一致。
这意味着在使用捷径连接时,我们需要通过适当的填充来确保卷积操作不改变特征图的空间尺寸。然而,这会导致一个问题:如果网络始终不进行下采样,那么最终的特征图尺寸会非常大,计算效率低下且不实用。
解决方案:带投影的捷径连接
在实践中,有一种特殊的残差块可以解决维度不匹配的问题。当需要下采样(即减少特征图尺寸或增加通道数)时,我们可以在捷径连接上也应用一个操作。
这个操作通常是一个 1x1 的卷积层(可能还包含批归一化)。这个 1x1 卷积可以同时改变特征图的通道数和空间尺寸,使其与主路径的输出 F(A^[l]) 维度匹配,从而能够进行相加操作。

通过这种方式,我们可以在构建深层网络的同时,灵活地控制特征图的尺寸。

总结
本节课中,我们一起学习了残差网络的核心概念。我们了解到:
- 简单地堆叠更多网络层会遇到梯度消失等训练难题。
- 残差网络通过引入捷径连接,允许信号直接从一层跳跃到后面几层。
- 其核心组件是残差块,其公式为
A^[l+2] = ReLU( F(A^[l]) + A^[l] )。 - 这种结构使网络能够轻松地学习恒等映射,从而可以选择性地跳过无用的层,稳定了深层网络的训练。
- 当输入输出维度不匹配时,可以通过在捷径连接上使用
1x1卷积进行投影来解决。

残差网络的思想非常强大,为训练成百上千层的超深度神经网络奠定了基础。在下一节课中,我们将通过代码实现来具体构建一个残差网络。
P118:L14.3.2.2 - PyTorch 中的 ResNet-34 实现 🧠





在本节课中,我们将学习如何在 PyTorch 中实现残差网络。我们将从零开始构建一个简单的残差块,理解其核心思想,然后介绍 PyTorch 官方社区提供的、更成熟的 ResNet-34 实现。通过对比,你将掌握残差网络的基本原理和实际应用方法。


概述:两种实现路径




我们将展示两个不同的实现。首先是一个我亲手编写的、较为简单的实现,用于演示上一节视频中讨论的两种残差块。然后,我们将展示 PyTorch 社区提供的、更复杂的 ResNet-34 实现。



我不会重新运行这个简单的笔记本,因为它耗时不多,且没有必要等待。我将直接展示其结果。


第一部分:简单的残差块实现
上一节我们介绍了残差网络的基本概念,本节中我们来看看如何用代码实现一个基础的残差块。这个实现旨在概念验证,因此使用了简单的 MNIST 数据集。
数据准备与模型结构

以下是导入必要库和准备数据的代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms






# 使用 MNIST 数据集,因其简单,适合作为概念验证
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)




接下来,我们实现第一种残差块,即输入与残差部分输出维度相同的情况。


残差块代码解析


我们使用 torch.nn.Module 类来实现一个残差块。该块包含两个卷积层,每个卷积层后接批归一化和 ReLU 激活。

class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).__init__()
# 第一个卷积层:in_channels -> out_channels
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True) # inplace=True 表示原地操作,节省内存
# 第二个卷积层:out_channels -> out_channels
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 捷径连接:如果输入输出维度或步长不同,需要1x1卷积调整
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = x # 保存输入作为捷径
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += self.shortcut(identity) # 加入捷径连接
out = self.relu(out) # 相加后再激活
return out




代码要点说明:
inplace=True:这是一个优化选项,意味着 ReLU 激活函数会直接修改输入张量,而不是创建新的张量副本,从而节省内存。虽然结果相同,但效率稍高。- 捷径连接:这是残差网络的核心。在
forward函数中,我们将原始输入identity与经过两个卷积层处理后的输出out相加。 - 维度匹配:如果输入和输出的通道数或空间尺寸(通过
stride改变)不同,则不能直接相加。我们通过一个包含 1x1 卷积和批归一化的shortcut序列来调整identity的维度,使其与out匹配。




构建简单网络并训练



使用上述残差块构建一个包含两个块的小网络,并添加一个全连接层作为分类器。




class SimpleResNet(nn.Module):
def __init__(self, num_classes=10):
super(SimpleResNet, self).__init__()
self.in_channels = 1 # MNIST 是单通道图像
# 第一个残差块:1 -> 4 通道
self.layer1 = self._make_layer(4, stride=1)
# 第二个残差块:4 -> 8 通道
self.layer2 = self._make_layer(8, stride=2)
self.linear = nn.Linear(8 * 7 * 7, num_classes) # 经过两次下采样后尺寸为 7x7
def _make_layer(self, out_channels, stride):
return ResidualBlock(self.in_channels, out_channels, stride)
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = x.view(x.size(0), -1) # 展平
x = self.linear(x)
return x

# 训练模型(此处省略训练循环细节)
model = SimpleResNet()
# ... 训练代码 ...
训练后,该简单模型在测试集上达到了约 92% 的准确率。这验证了残差块的基本功能。



调试技巧:在确定全连接层输入大小时,如果不确定张量经过卷积层后的尺寸,一个实用的方法是在 forward 函数中临时添加 print(x.size()) 语句来查看。




第二部分:可复用的残差块与维度变化

现在,我们聚焦于更一般化的情况,即残差块中可能改变特征图尺寸和通道数。我们将实现一个更通用的、可复用的残差块单元。




通用残差块实现



这个实现允许我们指定中间通道数(例如实现“瓶颈”结构)和步长。




class GeneralResidualBlock(nn.Module):
def __init__(self, in_channels, mid_channels, out_channels, stride=1):
super(GeneralResidualBlock, self).__init__()
# 第一个卷积层,可能改变尺寸
self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(mid_channels)
self.relu = nn.ReLU(inplace=True)
# 第二个卷积层
self.conv2 = nn.Conv2d(mid_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 捷径连接
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += self.shortcut(identity)
out = self.relu(out)
return out

使用通用块构建网络








我们可以用这个通用块轻松构建更深的网络。例如,构建一个从 1 通道到 8 通道再到 32 通道的网络,并伴随两次下采样。


class DeeperResNet(nn.Module):
def __init__(self, num_classes=10):
super(DeeperResNet, self).__init__()
# 块1: 1 -> 8 通道,步长2下采样 (28x28 -> 14x14)
self.block1 = GeneralResidualBlock(1, 4, 8, stride=2)
# 块2: 8 -> 32 通道,步长2下采样 (14x14 -> 7x7)
self.block2 = GeneralResidualBlock(8, 16, 32, stride=2)
self.linear = nn.Linear(32 * 7 * 7, num_classes)
def forward(self, x):
x = self.block1(x)
x = self.block2(x)
x = x.view(x.size(0), -1)
x = self.linear(x)
return x
这个网络的性能优于之前的简单实现。但残差网络的真正优势在于构建非常深的网络(如34层、50层)。


第三部分:PyTorch 官方的 ResNet-34 实现 🏗️



对于像 ResNet-34 这样的深层网络,从零开始实现既复杂又容易出错。在实践中,我们通常直接使用经过充分测试的官方或社区实现。



使用现有实现



PyTorch 的 torchvision.models 模块提供了 ResNet 的各种版本(如18、34、50、101层)。我们也可以参考其源码来学习。以下是如何使用和简要分析:




import torchvision.models as models
# 直接加载预训练模型
resnet34 = models.resnet34(pretrained=True)
# 或者,查看和调整其结构以适应CIFAR-10(32x32图像)
# 通常需要修改第一层卷积和最后的全连接层



在配套的笔记本中,我复制并简化了 PyTorch 官方的 ResNet 构建代码。它定义了 Bottleneck 块(用于更深的 ResNet-50/101等)和基本的 BasicBlock(用于 ResNet-18/34),并通过一个 _make_layer 函数来堆叠多个块。






核心结构要点:
- 初始层:一个卷积层接批归一化和 ReLU,以及一个最大池化层。
- 四个主要阶段:每个阶段包含若干残差块,逐步降低空间分辨率并增加通道数(例如,通道数从64增加到512)。
- 分类器:全局平均池化层后接一个全连接层。



在 CIFAR-10 上训练 ResNet-34



我们将图像大小调整为 70x70 以适应 ResNet 的输入习惯(原设计用于 ImageNet 的 224x224)。使用与 VGG16 相同的训练流程。




# 示例:修改第一层以适应 CIFAR-10(3通道,较小卷积核和步长)
resnet34.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
resnet34.maxpool = nn.Identity() # 移除初始的最大池化,因为图像已很小
num_ftrs = resnet34.fc.in_features
resnet34.fc = nn.Linear(num_ftrs, 10) # CIFAR-10 有10类
训练后,ResNet-34 在 CIFAR-10 上获得了约 48% 的准确率。虽然与 VGG16 结果相近,但请注意,即使在图像更大的情况下,其训练时间(62分钟)也短于 VGG16(90分钟),这体现了残差连接在优化深度网络时的效率。模型也存在过拟合,可以考虑添加 Dropout 等正则化方法。




分析混淆矩阵可以发现,模型主要在动物类别(如鹿、狗、猫、鸟)之间产生混淆,这是细粒度图像分类的常见挑战。


总结与实践建议

本节课中我们一起学习了残差网络在 PyTorch 中的实现。



我们从零开始构建了简单的残差块,理解了其核心公式 输出 = F(x) + x 以及如何通过 shortcut 处理维度变化。然后,我们探讨了如何构建更通用的残差块。最后,我们介绍了如何利用 PyTorch 官方实现来使用成熟的 ResNet-34 模型,并在 CIFAR-10 数据集上进行了训练评估。





给初学者的实践建议:
- 学习目的:为了理解原理,可以从头实现简单的残差块(如2-4层)。
- 实际项目:当需要 ResNet-34 或更深的网络时,强烈建议直接使用
torchvision.models中的现有实现或从论文作者的官方 GitHub 代码库进行适配。这可以节省大量时间,并避免因自己实现复杂网络而引入的错误。 - 调整网络:使用现有模型时,通常需要修改第一层(输入通道、卷积核)和最后一层(输出类别数)以适应你的数据集。


通过本教程,你应该已经掌握了残差网络的基本实现逻辑,并知道了在实战中如何高效地利用强大的现有模型。
📰 深度学习新闻 #10:2021年4月3日
在本节课中,我们将学习本周深度学习领域的两项重要进展。首先,我们将探讨一篇关于分析和缓解深度学习模型偏见的新论文及其提出的新数据集。随后,我们将介绍一种新的卷积神经网络架构——EfficientNetV2。最后,我们还会简要了解一个新的优化器和一个实验跟踪工具列表。

🔍 偏见分析与缓解方法评估
上一节我们介绍了卷积神经网络,本节中我们来看看如何评估和缓解模型中的偏见问题。先前我们讨论过,将深度学习应用于某些问题时可能存在严重的偏见问题。例如,之前视频中提到的系统,它通过深度神经网络来评估求职者,但一个主要问题是,如果背景中有书架,系统对求职者的评分会有显著差异,这显然是不应该发生的。

本周有一篇论文评估了不同缓解此类偏见的系统。已经开发了多种技术,核心问题是:这些技术到底有多有效?学习到这些不恰当的偏见会导致深度学习模型在少数群体上表现极差,这是我们希望预防或避免的。这些已开发的方法,其核心问题在于它们是否真的有效,以及有多大用处。几周前我介绍过一篇论文,其中提到这些方法只在非常刻板的公平性意义上有效。
无论如何,我认为存在多个层面的问题。首先,将模型应用于特定类型的问题是否合理或明智?其次,我们可以使用哪些方法来减少偏见?再者,我们如何评估这些方法是否真的有效?这是我们应该提出的三个不同层次的问题。

在这篇论文中,作者试图改进评估协议,并提供了一个名为“Biased MNIST”的新数据集。他们特别评估了七种不同的方法,其代码和数据集已在GitHub上开源。
以下是论文中描述偏见分析问题的概述。存在不同类型的数据集,例如问答数据集和“Setup A”数据集(我们可能在后续关于生成对抗网络或变分自编码器的课程中使用)。问答数据集包含了多种复杂的偏见,但分析起来并不容易。而“Setup A”数据集和彩色MNIST数据集(即带颜色的MNIST版本)虽然易于分析,但其中需要检测的偏见过于简单,不足以真正评估不同的偏见缓解方法。因此,他们开发了这个“Biased MNIST”数据集,声称它既易于分析,又能提供足够难度的偏见。

这个“Biased MNIST”数据集的任务是识别0到9这10个不同的数字。它包含了7种不同的偏见来源:
- 背景颜色
- 待分类目标数字的颜色
- 数字的位置(有9个网格位置)
- 干扰形状(图像中其他用于分散预测注意力的形状)
- 干扰物的颜色
- 纹理类型
- 纹理颜色

以下是数据集的一个示例。顶行是数字“1”。在这个例子中,数字“1”通常是绿色的,并且最常出现在紫色背景上,并且经常与直角三角形同时出现。但有时它会是其他颜色,例如红色。在这种情况下,绿色是多数类,而其他颜色是少数类。因此,对于每个数字,他们设定了一些在多数类中常见、在少数类中不常见的特征,例如改变颜色或背景。通过这种方式,他们试图评估网络对这些非数字本身特征的敏感程度。网络对改变干扰物颜色或形状有多敏感?网络对数字位置(是在中心还是在上部)有多敏感?他们分析了不同类型的干扰,以判断网络是专注于数字本身,还是其他如颜色等特征。

以下是该论文的一些结果。我必须承认这些结果有点难以解读,希望我的理解是正确的。论文中的表述并非完全清晰,但根据我的理解,我来为大家讲解一下。
首先,我们来看这个MMD图。他们将MMD定义为“多数-少数差异”,用于衡量多数群体和少数群体之间的差异。他们计算了多数类的准确率(例如,上一张幻灯片中绿色的数字“1”是多数类)和少数类的准确率(例如,红色的数字“1”)。他们针对我们上一张幻灯片讨论的七种偏见分别计算了MMD,每次专注于一种他们称之为“显性偏见”的偏见,而“隐性偏见”则是未被处理的那些。这里的箱线图(如果我没理解错的话)是针对不同方法绘制的。我们稍后会看这些方法,但下方列出了七种不同的方法。因此,箱线图是跨不同方法绘制的。
例如,在这里你可以看到,当将背景颜色视为显性偏见时,显性偏见的MMD中位数(平均而言)低于隐性偏见。隐性偏见是未被处理的那些。在所有情况下,显性偏见的中位数都低于隐性偏见的中位数。因此,这些方法在降低你指定的目标变量(例如干扰物形状、干扰物颜色等)的偏见方面,在某种程度上是有效的。对于数字位置,我们看到没有太大差异,这可能是因为卷积网络天生对位置具有一定的不变性,而且人们通常已经使用了数据增强(如随机裁剪)来处理输入。
但除此之外,你可以看到显性偏见和隐性偏见之间存在巨大差异。因此,这些方法在某种程度上是有效的,但另一方面,它们离零值还很远,所以效果并不是非常好。而且你可以看到,对于隐性偏见,它们仍然存在,多数类和少数类之间仍有高达30%到40%的巨大差异。
接下来,我们更详细地看看这些方法。这里我们可以看到七种方法。SD是标准方法,你可以将其视为基线模型。然后他们比较了其他7种方法。以这里的UpWt为例,他们将其显性偏见的结果圈了出来。现在,箱线图的范围(如果我没理解错的话)是针对这些不同类型的隐性偏见的。因此,箱线图的范围现在是跨隐性偏见的。你可以看到,对于一种特定的显性偏见,其MMD接近0,但其他隐性偏见的MMD仍然很高。我认为他们的分析方式是:遍历所有偏见,每次将其中一个视为显性偏见,而将剩余的六个视为隐性偏见,然后取平均值。这就是该方法在区分显性偏见和隐性偏见时的平均表现。
你可以看到,如果你指定了显性偏见,这种方法在处理这些显性偏见方面相当不错,但所有其他偏见仍然是一个大问题,实际上甚至比标准模型中的问题更大。因此,如果你使用这种方法,你可能会修复你指定的显性偏见,但会使其他所有问题变得更糟。总体而言,没有一种方法能够真正处理所有的隐性偏见。你可以看到,到处都存在大问题。也许这里这个方法,对于显性和隐性偏见,其MMD都是最低的。我认为真正棘手的部分是隐性偏见,因为它们可能潜藏在你的数据集中。对于显性偏见,即使存在,你也能意识到,也许可以告诉人们要注意这一点。但隐性偏见可能是一个更大的问题。然而,你可以看到所有这些方法对于这类问题都不是非常鲁棒。
在右侧,有另一种评估指标,他们称之为“IOSM”(相对于标准模型的改进)。它衡量的是与标准模型相比,群体准确率差异的变化。同样,你可以看到在大多数情况下,这些模型或方法都存在一些问题。但对于“Setup A”数据集,有一些方法在这四种偏见类型上的差异非常小。因此,在像“Setup A”这样的简单数据集上,有些方法处理得很好。但当他们观察这个“Biased MNIST”数据集时,这些问题仍然存在,因为你可以看到L和L以及I am(指代具体方法)的结果仍然有问题。因此,也许在开发方法时,“Biased MNIST”数据集也是值得考虑的,因为右侧的“Setup A”数据集可能过于简单了。
好吧,我认为这仍然是一个活跃的研究领域。我也不是这个领域的专家,只是觉得这篇论文和一个用于评估潜在偏见缓解方法的新数据集可能值得一看。

🏗️ 新的卷积神经网络架构:EfficientNetV2

上一节我们讨论了偏见分析,本节中我们来看看一种新的卷积神经网络架构。在课堂上,由于时间限制,我们只讨论了几种架构,但总会有更多有趣的架构出现。我之前提到过EfficientNet,这里先简要介绍一下,因为我们还没正式讨论过它。大约两年前,EfficientNet架构家族被提出,这不算新闻,我们将在下一张幻灯片讨论新闻。本质上,这是一种构建高效神经网络架构的方法。
它涉及使用复合系数来扩展卷积网络。传统上,我们有三种扩展卷积网络的方式:
- 增加更多层(深度)
- 增加层的宽度(通道数)
- 提高图像分辨率
通常,如果我们提高图像分辨率,网络性能不会很好,因此我们也必须改变层数、层宽等。EfficientNet背后的思想是分析这些缩放维度如何相互关联。他们提出了一个特定的公式来协调缩放这三个组件。他们发现,通过他们的方法,可以用更少的参数获得更好的准确率,并且在应用缩放时,网络运行速度比参考网络更快。
在左侧的图表中是一个比较。例如,这里有ResNet-152(一个大型网络)、DenseNet、Inception等。横轴是参数数量,右侧的网络参数当然更多。纵轴是在ImageNet上的性能。你可以看到,与其他参考架构相比,EfficientNet的参数数量更少,但性能更好。观察性能最好的非EfficientNet网络,可能是这个。你可以看到,他们开发了一个高效得多的架构,达到了与这个MobileNet相当的准确率。这确实是一个非常有趣的方法,即EfficientNet。

然而,这里的新闻是关于EfficientNetV2,它也在4月1日发布(有两篇4月1日的论文)。对于4月1日的论文,你总是需要小心一点,就像你可能记得我的“Car Network”可能有时不是真正的网络,可能只是个愚人节玩笑。但在这种情况下,这似乎是一篇正经的论文,而且实际上非常酷。它扩展了EfficientNet,使其性能更好。

他们引入了新的操作,例如“Fused and B Convolution”。我们不会深入探讨细节,但本质上,它涉及在训练过程中逐步增加图像尺寸。通常,当你增加图像尺寸时,性能会下降,也可能出现更多过拟合,这可能仅仅是因为现在有更多像素了。他们的做法是在训练过程中逐步提高分辨率,同时自适应地调整正则化,结合使用了Dropout和数据增强。我认为这也是一个很酷的想法。你可以看到,所有这些架构都是EfficientNetV2。它们都比这里的EfficientNetV1表现好得多,实际上是一个巨大的改进,在ImageNet上从83%提升到了87.4%,同时仍然保持高效。


🧠 其他新闻:新优化器与实验跟踪工具
既然我们最近讨论了不同的优化算法,并且也有相关的测验和考试题目,我想介绍一个新的优化器。现在有一个名为“Matt Grt”的优化器,它不代表“Matt Grt学生”,而是对EGrt和Adam的一种改进。
他们说Adam并没有完全达到成为通用深度学习优化器的目标,而Matt Grt方法正是为了解决这些问题而设计的。Adam和带动量的SGD在不同类型的问题上表现良好,但并非所有问题。在左侧图表上可能有点难看清,这里是Adam。在右侧,Adam位于这个高性能区域的上方。对于SGD加上动量,我认为它在这里和这里(指图表位置)的上方。你可以看到,有时M(可能指Matt Grt)更好,有时SGD更好。他们现在提出了一个新方法Matt Grd,它总是表现良好,总是位于上方区域。这可能是另一个值得关注的有趣事物。我还没有在实践中尝试过,但如果你感兴趣,可以试试。他们在GitHub上提供了代码,论文中应该也有链接。我还没有详细研究这篇论文,但它可能是另一个值得考虑的有趣方法。
上周我简要提到了一种用于跟踪机器学习实验的新工具,叫做Aim。出于某种原因,Aim不在这张列表上。但这里有一个不同实验跟踪工具的有趣列表。上周我还提到了MLflow和TensorBoard。这里还有更多选择。这张图表是由DagsHub的人制作的,DagsHub也在这个列表上。所以,你应该对这张图表持保留态度,但它仍然可能是一个有趣的表格,你可以在其中找到不同的实验跟踪替代方案。如果你正在做课程项目,并且希望采用更有条理的方法(相比于在Excel电子表格中跟踪模型性能或使用Matplotlib),这些工具可能对你有用。就我个人而言,我并没有广泛使用这些方法。我和一位同事正在使用MLflow,我有时也用TensorBoard,但经常两者都不用。我认为其他工具也相当不错。这真的只是个人喜好、实验规模以及你是否经常进行大量实验的问题。就像所有事情一样,关键在于找到平衡点:不要使用对你所做的事情来说过于复杂的东西,但如果你经常大规模地进行某些工作,那么在重新发明轮子、编写自己的跟踪工具之前,也许可以先看看是否已经有现成的解决方案可以解决你的问题。

📝 总结

本节课中我们一起学习了本周深度学习领域的重要动态。我们首先深入探讨了一篇关于评估深度学习模型偏见缓解方法的新论文,以及其提出的“Biased MNIST”数据集,该数据集旨在更有效地评估模型对多种偏见的敏感性。接着,我们介绍了EfficientNetV2这一新的卷积神经网络架构,它通过渐进式分辨率缩放和自适应正则化,在保持高效的同时显著提升了性能。最后,我们还简要了解了一个新的优化器Matt Grt和一个实验跟踪工具的概览列表。这些进展展示了该领域在追求更公平、更高效、更易管理的模型方面的持续努力。
📰 深度学习新闻 #1,2021 年 1 月 27 日
在本节课中,我们将一起回顾2021年1月27日这一周内,深度学习领域的一些有趣新闻和应用。这些内容涵盖了从医疗健康到社交媒体分析等多个方面,旨在帮助你了解深度学习技术的最新动态和实际应用。
🩺 深度学习助力COVID-19资源需求预测
上一节我们介绍了本课程的目标,本节中我们来看看一个将深度学习应用于医疗领域的实际案例。
Facebook AI Research与纽约大学的医学专家合作,开发了一个利用X光片预测COVID-19患者病情恶化和资源需求的系统。这个案例强调了与领域专家合作的重要性,尤其是在医疗等关键应用中。
研究人员训练了三个模型:
- 第一个模型仅基于单张X光片预测患者病情是否会恶化。
- 第二个模型基于一系列X光片(视为时间序列)完成同样的预测任务。
- 第三个模型基于单张X光片预测患者所需的氧气供应量。
他们的建模方法采用了自监督学习。首先,在一个大型非COVID胸部X光数据库上进行预训练,以利用更多数据。然后,在一个包含5000名患者的27000张COVID-19 X光片的小型数据集上进行微调。
有趣的是,在某些指标上,这些模型的表现超过了人类专家。这并非要取代医生,而是为了在医疗资源紧张时,为医生提供辅助工具,帮助减少可能的失误。
你可以在arXiv上找到完整的论文,相关预训练模型也已开源在GitHub上。

📚 探索arXiv上的COVID-19研究
上一节我们看到了一个具体的医疗应用,本节中我们来看看如何寻找更多相关研究。
arXiv是一个机器学习预印本服务器,每天都有大量新论文上传。其中包含了数千篇与COVID-19相关的研究。你可以使用搜索功能来查找特定主题,例如结合“deep learning”和“chest x-ray”等关键词。



需要注意的是,arXiv上的论文尚未经过同行评审,因此对其发现应持审慎态度。不过,这仍然是获取最新研究思路和用于课程项目参考的宝贵资源。
🧬 学习病毒进化的“语言”
除了医学影像分析,深度学习还能从其他角度助力抗疫。本节介绍一项将序列数据视为“语言”的有趣研究。
有研究者训练了一个双向LSTM模型来研究病毒的进化与免疫逃逸。LSTM是一种循环神经网络,我们将在后续课程中详细讨论。
在这项研究中,他们将病毒对应的氨基酸序列视为一种“语言”:
- 将“语法正确”类比为生物学上的序列合理性。
- 将“语义含义”类比为序列是否会引起免疫反应。
他们取得了不错的性能,AUC(ROC曲线下面积)达到了0.85。AUC是衡量模型性能的指标,0.5相当于随机猜测,1.0是完美模型,因此0.85表明这是一个相当好的预测模型。

🧠 商业前沿:个人AI与“第二大脑”
深度学习不仅在学术界蓬勃发展,在商业领域也催生了大胆的构想。本节我们转向一个更偏向商业应用的新闻。

一个名为“Human AI”的项目获得了320万美元融资,旨在构建一个基于区块链技术的个人智能平台。其目标是利用神经科学、自然语言处理和区块链技术,打造一个属于个人的、安全的“第二大脑”或数字记忆库,这与仅基于公共互联网信息的通用AI(如GPT-3)形成对比。
🐦 数据获取:Twitter向学术研究者开放全量档案
要训练好的模型,通常需要大量数据。本节介绍一个对研究者利好的消息。

Twitter宣布向学术研究人员免费开放其全量推文档案。获批的研究者每月可访问的推文量上限提升至1000万条,是以前的20倍,并且允许更精确的数据过滤。这使得基于社交媒体数据的研究,如舆情分析、危机管理等,变得更加便利。
例如,有研究就利用推文等社交媒体数据,通过开发噪声过滤机制,来更有效地定位和管理洪水、暴风雪等极端天气事件的受灾区域。

🖼️ 数据集革新:重新标注ImageNet
我们常用的大型基准数据集本身也在不断进化。本节介绍一个改进经典数据集的工程。
ImageNet是深度学习领域最著名的图像数据集之一,包含约1400万张图片,常用于评估图像分类模型。但它也存在标签错误或不完整的问题(例如一张图中有多个物体,却只标了一个标签)。
为了解决这个问题,研究者们正在对ImageNet进行重新标注,为图像添加多标签。他们利用从Instagram获取的10亿张图片训练了一个EfficientNet模型,作为“机器标注器”。该模型通过裁剪图像的不同区域并分别分类,来为ImageNet中的每张图片生成更准确、丰富的多标签。
♿ 技术向善:用AI为视障人士描述照片

最后,我们来看一个体现技术人文关怀的应用。本节介绍Facebook AI如何利用深度学习改善无障碍服务。
Facebook利用AI改进了一款智能手机应用,旨在为盲人或视障人士提供更好的照片描述服务。该系统通过音频描述图片内容。
他们在一个包含35亿张Instagram照片及对应标签的数据集上训练了一个ResNeXt模型,并结合Fast R-CNN目标检测器来描述图像的不同方面。例如,对于一张照片,系统可能会生成这样的描述:“这可能是一张一个人站在马丘比丘的照片。”



本节课中我们一起学习了2021年1月底深度学习领域的多个新闻与应用。我们看到了深度学习在医疗诊断(COVID-19预测)、基础科研(病毒序列分析)、商业构想(个人AI)、数据基础设施(Twitter数据开放、ImageNet改进)以及社会公益(辅助视障人士)等方面的广泛影响和快速发展。保持对行业动态的关注,有助于我们更好地理解技术的潜力和方向。
课程 P120:L14.4.1 - 用卷积层代替最大池化 🧠
在本节课中,我们将学习一个与卷积神经网络架构相关的主题:如何用卷积层替代传统的最大池化层。这虽然不一定会显著提升网络性能,但能帮助我们更深入地理解卷积操作的本质,并探索简化网络架构的可能性。

概述
上一节我们介绍了VGG16和残差网络等架构。本节中,我们来看看如何对经典架构进行一种简化:用带步长的卷积层替换最大池化层。这种思路源于一篇名为《Striving for Simplicity: The All Convolutional Net》的论文。
传统架构 vs. 全卷积网络
在传统的卷积神经网络中,我们通常交替使用卷积层和池化层。
以下是典型的模式:
- 一个步长为1的卷积层。
- 一个步长为2的2x2最大池化层。
- 重复此模式。
最大池化层的主要作用是将特征图的尺寸减半,同时提供一定的平移不变性。
用卷积层替代最大池化
全卷积网络的核心思想是移除最大池化层,并通过增大后续卷积层的步长来实现下采样。
具体做法如下:
- 移除最大池化层。
- 将紧随其后的卷积层的步长设置为2(即使用步长卷积)。

这样,步长为2的卷积层同样能将特征图尺寸减半。论文作者认为,这相当于一种“可学习的池化”,因为卷积层带有可训练的参数。虽然这会引入更多参数,但能使网络结构更统一(仅使用卷积操作)。
注:有观点认为最大池化并非必需。例如,Geoffrey Hinton曾表示最大池化是计算机视觉领域的一个“大错误”。不过,最大池化在实践中表现良好,本节的替代方案更多是一种帮助理解的概念性实验。
进一步简化:替换全连接层
除了池化层,我们还可以简化网络末端的全连接层。这将在下一个视频详细讨论,主要有两种方法:
-
全局平均池化:假设最后一个卷积层的通道数等于分类类别数。对每个通道的特征图进行全局平均,直接得到一个标量值,从而替代全连接层。
# 伪代码示意:全局平均池化 # 输入特征图尺寸: [batch_size, channels, height, width] # 输出尺寸: [batch_size, channels, 1, 1] -> 可展平为 [batch_size, channels] output = torch.nn.functional.adaptive_avg_pool2d(input, (1, 1)) -
使用等效的卷积操作:用1x1卷积层来模拟全连接层的功能。
传统全连接层参数量大,而上述方法能有效减少参数。不过,全连接层在多数现有架构中工作良好,并非必须移除。
总结



本节课我们一起学习了如何用步长卷积层替代最大池化层,从而构建一个“全卷积网络”。这种方法通过统一网络中的操作类型来简化架构,并提供了另一种实现下采样的视角。虽然其性能提升不一定显著,但这是一个有助于巩固对卷积网络理解的优秀思维实验。在接下来的课程中,我们将通过代码实例来具体实现这种全卷积网络。

🧠 P121:L14.4.2 - PyTorch 中的全卷积网络
在本节课中,我们将学习如何在 PyTorch 中实现一个全卷积网络。这个网络架构移除了传统的最大池化层和全连接层,仅使用卷积层、ReLU激活层和批归一化层来构建。我们将通过代码示例来理解其设计原理和实现细节。
📋 概述与背景
上一节我们讨论了全卷积网络的理论概念。本节中,我们将通过具体的 PyTorch 代码来实现它。
全卷积网络的核心思想是简化网络结构。它使用步长为2的卷积层来替代最大池化层进行下采样,并使用全局平均池化层来替代最后的全连接层进行分类。
以下是实现该网络的关键代码结构:



import torch.nn as nn

class AllConvNet(nn.Module):
def __init__(self, num_classes=10):
super(AllConvNet, self).__init__()
# 网络层定义将在这里
...
🏗️ 网络架构详解
网络由多个卷积块堆叠而成。每个块通常包含一个增加通道数的卷积层和一个保持通道数但进行空间下采样的卷积层。
卷积层设计
以下是网络中的两种主要卷积操作:
- 保持空间尺寸的卷积:使用
kernel_size=3, stride=1, padding=1。其公式可表示为:
输出尺寸 = (输入尺寸 - kernel_size + 2*padding) / stride + 1
当参数如上设置时,输出尺寸与输入尺寸相同。

- 进行下采样的卷积:使用
kernel_size=3, stride=2, padding=1。这会将特征图的高度和宽度减半,替代了最大池化的功能。
全局平均池化
在网络的末端,我们使用全局平均池化层替代全连接层。在 PyTorch 中,这可以通过 nn.AdaptiveAvgPool2d(1) 实现。该操作对每个通道的所有空间位置(高度和宽度)取平均值,最终得到一个 1x1 的特征图。
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
🔧 代码实现解析


以下是构建网络核心部分的一个示例。请注意,为了简化,当卷积核的高度和宽度相同时,我们可以用一个整数(如3)来同时指定两者。

self.features = nn.Sequential(
# 第一个卷积块:增加通道数,保持尺寸
nn.Conv2d(3, 96, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(96),
nn.ReLU(inplace=True),
# 下采样卷积:保持通道数,尺寸减半
nn.Conv2d(96, 96, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(96),
nn.ReLU(inplace=True),
# ... 后续可以重复此模式
# 最后一层卷积:输出通道数等于类别数
nn.Conv2d(256, num_classes, kernel_size=3, stride=1, padding=1),
)
在训练设置方面,我们使用与 VGG16 相同的简单配置(如优化器、学习率等)。由于模型参数更少,结构更简单,其训练时间通常比 VGG16 更短。




⏱️ 训练时间与性能
深度学习的训练通常需要较长时间,这与传统处理小型表格数据的统计学方法不同。复杂模型和大数据集可能需要数天甚至数周的训练。


对于我们的全卷积网络在 CIFAR-10 数据集上的训练:
- 训练耗时约为 41 分钟(作为对比,VGG16 约需90分钟)。
- 最终测试准确率约为 80%(VGG16 可达84-85%)。



这个结果表明,全卷积网络在牺牲少量精度的情况下,获得了更高的训练效率。

🎯 总结


本节课中,我们一起学习了全卷积网络在 PyTorch 中的实现。我们了解到如何仅用卷积层构建网络,用步长为2的卷积替代池化层,以及用全局平均池化替代全连接层。这种设计使网络更简洁,参数更少,训练更快。

下一节,我们将探讨另一种替代全连接层的方法——使用 1x1 卷积,这将是一种参数化的替代方案。
P122:L14.5 - 用卷积层替代全连接层 🧠

在本节课中,我们将学习如何用卷积层来替代神经网络中的全连接层。我们将探讨两种不同的实现方法,并通过代码示例验证它们的等价性。理解这种转换有助于我们更深入地理解卷积操作的原理,并构建完全由卷积层组成的网络。
全连接层回顾 🔍
在上一节中,我们学习了全连接层的基本结构。现在,我们来看看一个具体的例子。
左侧展示的是一个我们之前见过的全连接层。为了适应幻灯片,这个全连接层有4个输入和2个输出。
以下是计算过程:
- 我们从一个绿色的权重向量 W1 开始。输出(绿色点)是通过将权重向量与输入 X 相乘,再加上一个偏置单元(图中未显示)计算得到的。我们称这个结果为 output1。
- 第二个输出 output2 的计算方式完全相同,只是使用了黄色的权重向量 W2。
方法一:将输入视为2D图像 🖼️
上一节我们回顾了全连接层的计算。本节中,我们来看看第一种用卷积层替代它的方法。

我们可以使用卷积层完成完全相同的计算。具体操作如下:
- 我们将4个输入排列成一个2x2的图像。
- 我们使用一个卷积核。以绿色输出为例,我们使用一个具有1个输入通道、2x2核大小的卷积核,其权重就是 W1。
- 第二个卷积核(对应黄色输出)同样具有1个输入通道和2x2的核大小,其权重是 W2。
- 卷积操作本质上是对感受野内的输入进行加权求和(点积),然后加上偏置。这与全连接层的计算完全相同。
从宏观上看,这相当于使用了两个卷积核,每个核有1个输入通道,核大小为2x2。
代码验证:方法一 💻
概念可能比看起来复杂,让我们通过代码示例来验证第一种方法。
以下是全连接层的设置:
# 定义输入 (1, 2, 3, 4),并重塑为卷积网络常用的格式 (N, C, H, W)
inputs = torch.tensor([1, 2, 3, 4], dtype=torch.float32).view(1, 1, 2, 2)
# 定义全连接层
fc_layer = nn.Linear(4, 2)

# 手动设置权重和偏置以便比较
weights = torch.tensor([[1, 2, 3, 4], # W1 (绿色)
[5, 6, 7, 8]]) # W2 (黄色)
bias = torch.tensor([0.1, 0.2]) # 偏置
fc_layer.weight.data = weights
fc_layer.bias.data = bias
# 计算全连接层输出(需要将输入展平为向量)
fc_output = fc_layer(inputs.view(1, -1))
print(fc_output) # 输出结果例如:tensor([[14., 19.]])
现在,我们用卷积层实现相同的功能:
# 定义卷积层:1个输入通道,2个输出通道(两个核),核大小2x2
conv_layer = nn.Conv2d(in_channels=1, out_channels=2, kernel_size=2)
# 将全连接层的权重重塑为卷积核的形状 (out_channels, in_channels, H, W)
conv_weights = weights.view(2, 1, 2, 2)
conv_layer.weight.data = conv_weights
conv_layer.bias.data = bias

# 计算卷积层输出
conv_output = conv_layer(inputs)
print(conv_output) # 输出形状为 (1, 2, 1, 1),数值与全连接层结果相同
运行代码后,卷积层输出的数值与全连接层完全一致,只是结果的排列方式(多出了通道维度)不同。这证明了第一种替代方法的有效性。

方法二:将输入堆叠为通道 📚

除了将输入排列成2D图像,我们还有第二种替代方法。
第二种方法是将这个输入向量堆叠起来。我们可以使用两个卷积核,每个核有4个输入通道,来实现与全连接层相同的效果。


之前,我们有两个核,每个核有1个通道。现在,我们有两个核,每个核有4个通道。主要的区别在于,这里我们将输入堆叠为通道,而第一种方法是将其排列成2x2的格式。两种方法都能产生与全连接层等价的结果。

代码验证:方法二 🔬


如果你不相信,这里还有另一个例子。
以下是第二种方法的代码实现(使用4输入通道的卷积):
# 将输入视为4个通道的1x1“图像”
inputs_stacked = torch.tensor([1, 2, 3, 4], dtype=torch.float32).view(1, 4, 1, 1)
# 定义卷积层:4个输入通道,2个输出通道,核大小1x1
conv_layer_stacked = nn.Conv2d(in_channels=4, out_channels=2, kernel_size=1)

# 注意:权重需要对应新的形状。这里为了比较,我们使用变换后的相同权重。
# 实际上,一个4输入通道、1x1的卷积核等价于一个在特定位置上的全连接计算。
# 为了精确匹配原全连接层,需要精心设置权重值。
weights_stacked = ... # 根据W1, W2重新排列权重以匹配4输入通道
conv_layer_stacked.weight.data = weights_stacked
conv_layer_stacked.bias.data = bias
conv_output_stacked = conv_layer_stacked(inputs_stacked)
print(conv_output_stacked) # 输出结果同样与全连接层等价
使用相同的权重进行比较,我们再次得到了完全相同的结果。这是用卷积层替代全连接层的两种不同方式。
应用于全卷积网络 🚀
现在,让我们重新审视我们的全卷积网络,之前我们使用了自适应池化层。
例如,我们可以不使用自适应池化层或全连接层,而是应用我们的新想法,在这里也使用卷积层,让这个全卷积网络更加“纯粹”。此时,我将卷积核大小设置为8x8,因为在上一个代码笔记本中我们发现,到达这一阶段时特征图的大小是8x8(64个通道,高8宽8)。运行这个网络,虽然不会得到与自适应池化完全相同的结果(因为现在参数更多了),但它能正常运行,甚至可能给出更好的结果。
两种架构对比 ⚖️
最后,我们来直接对比一下使用全连接层实现和完全使用卷积层实现的网络。
以下是两种架构的对比:
- 左侧(传统方式):
- 一个Conv2D层(64输入通道,64输出通道)。
- 将输出展平(Flatten),将64x8x8的特征图转换为一维特征向量。
- 一个全连接层(Linear),将特征向量映射到类别数(如10类)。
- 右侧(全卷积方式):
- 同样的Conv2D层。
- 一个卷积层(Conv2d),其输入通道为64,输出通道数等于类别数(如10),核大小设置为前一层的特征图大小(8x8)。这样做的目的是得到一个1x1的输出空间维度。
理论上,右侧的实现应该能给出与左侧等价的结果。在实践中尝试时,如果发现结果略有不同,可能与随机权重初始化有关,因为每次调用层时都会创建随机权重,这使得比较变得困难。但正如我们之前所见,在精心设置权重的情况下,它们能产生完全相同的结果。

需要说明的是,几年前我出于兴趣在更严肃的网络中尝试过这种方法。通常这样做并没有好处。我当时认为,在CUDA实现中可能更容易获得更好的性能,因为可以更好地利用多GPU的并行性。但实际上,我发现最后一层使用全连接还是卷积层几乎没有任何差异。因此,这里展示的内容更像是一个玩具实验或示例,旨在说明我们有能力这样做以达到等价效果,但并非必要。你可以将此视频视为完全可选的内容,它主要有助于从技术角度理解卷积的工作原理。


总结 📝

本节课中,我们一起学习了用卷积层替代全连接层的两种方法。我们通过具体的图示和代码,验证了这两种方法在数学上的等价性。我们还探讨了如何将这种思想应用于构建更“纯粹”的全卷积网络,并对比了传统架构与全卷积架构的异同。重要的是要理解,这种替代在理论上是可行的,能帮助我们深化对网络层本质的理解,但在实际应用中,根据具体任务和框架选择全连接层或卷积层通常都是合理的。在接下来的课程中,我们将回到一个更重要的主题——迁移学习,这对于大家的课程项目将非常有用,因为它能帮助我们利用相关的数据集。
🧠 课程 P123:L14.6.1 - 迁移学习
在本节课中,我们将要学习迁移学习的概念及其在卷积神经网络中的应用。迁移学习是一种强大的技术,它允许我们利用在大型数据集上预训练的模型,来提升在较小目标数据集上的任务性能。

📚 概述
所有优秀的课程和讲座都有结束的时候。这是卷积神经网络架构系列中的最后一个主题。虽然迁移学习并非卷积网络特有的主题,但现在是讨论它的好时机,因为目前已有一些优秀的预训练计算机视觉模型可供使用。迁移学习对于课程项目尤其有用。
🔍 核心概念
迁移学习的核心思想在于,卷积神经网络通常包含两个主要部分:
- 特征提取部分:由卷积层构成。
- 分类器部分:通常由全连接层(或线性层)构成,即多层感知机。
迁移学习的理念是,卷积层构成的自动特征提取管道,可能对其他相关任务同样有用。
例如,许多同学正在进行的课程项目涉及通过胸部X光数据预测新冠肺炎。一个合理的假设是:在一个与新冠肺炎无关的大型通用胸部X光数据集上训练的网络,如果在我们特定的新冠肺炎数据上进行微调,可能对新冠肺炎检测也有帮助。
在深度学习中,我们通常有大型基准数据集,但这些数据集主要用于模型比较和基准测试。在实际应用中,我们通常拥有的数据集要小得多。因此,问题在于:我们能否利用这些海量数据来训练网络,然后在我们的较小目标数据集上对这些网络进行微调?

🛠️ 迁移学习方法
以下是迁移学习的几种常见方法:
- 冻结权重:使用在ImageNet等数据集上预训练的模型,冻结特征提取层(卷积层)的权重。这意味着在训练过程中保持这些权重固定,不进行更新,只训练最后几层(如全连接层)。
- 整体微调:在给定数据上训练整个网络,然后在较小的目标数据集上对整个网络进行微调。这是迁移学习的一种特殊情况,即不冻结任何权重。
📊 实践效果
下图展示了一项关于大规模视频分类的旧论文中的实验结果,它很好地总结了使用迁移学习时的几种选项及其性能对比:


- 从头开始训练:准确率为41%。
- 预训练后仅微调最后一层:准确率为64%。
- 预训练后微调最后三层:准确率达到最佳的65%。
- 预训练后微调所有层:准确率为62%。

从这个案例可以看出,微调三层网络取得了最佳结果。然而,这并非普遍规律。微调多少层只是另一个需要考虑的超参数,其最佳值取决于网络架构、数据集等诸多因素。
一个关键发现是:从头开始训练的性能远不如使用大型数据集预训练后再微调,即使不冻结任何层也是如此。
🎬 下节预告

在下一节视频中,我们将具体演示如何在PyTorch中实现迁移学习。我们将使用已学习的VGG16模型进行说明,因为它是最容易解释的模型之一。
具体步骤将是:
- 使用在ImageNet上预训练的VGG16模型。
- 冻结其卷积层。
- 替换其输出层。
- 在一个不同的数据集(为简化代码,我们将使用CIFAR-10数据集)上微调这些新层。
当然,这是一个通用概念,可以应用于任何数据集。
📝 可用模型与注意事项

除了VGG16,还有其他可用的预训练模型。VGG16可能不是性能最佳的模型,且计算成本较高。对于课程项目,如果想获得良好性能,Wide ResNet和Inception v3也是不错的选择,但运行速度较慢。MobileNet是一个性能不错且速度较快的网络。当然,还有残差网络(ResNet)。实际效果可能因任务而异。
进行迁移学习时,有一个重要注意事项:
你必须仔细检查预训练模型的提供者所使用的数据标准化方法。在PyTorch中,一些在ImageNet上预训练的模型是可用的。如果你想使用这些模型,必须注意使用与它们相同的数据标准化参数。对于ImageNet,他们使用了特定的均值和标准差进行归一化。通常,我们需要使用相同的参数来处理我们的新数据集,以确保数据尺度与网络预期的一致。
🎯 总结


本节课我们一起学习了迁移学习。我们了解了其核心思想:利用预训练模型的特征提取能力来加速和提升在小数据集上的学习效果。我们探讨了冻结权重和整体微调等不同方法,并通过实例看到了迁移学习相比从头训练的优势。最后,我们预告了下一节将在PyTorch中进行实践,并提醒了使用预训练模型时数据标准化的重要性。迁移学习是深度学习实践中一项极其有价值的技能。
P124:L14.6.2 - PyTorch 中的迁移学习 🚀



在本节课中,我们将要学习如何在 PyTorch 中实现迁移学习。具体来说,我们将使用一个在 ImageNet 数据集上预训练好的 VGG16 模型,并在 CIFAR-10 目标数据集上对其进行微调。


迁移学习的核心思想是,利用在大规模数据集(如 ImageNet)上训练好的模型所学习到的通用特征,来加速和提升我们在较小目标数据集上的模型训练效果。在本例中,我们将冻结 VGG16 的所有卷积层,只训练最后的全连接层,并替换输出层以适应 CIFAR-10 的 10 个类别。




1. 数据准备与标准化 📊

上一节我们介绍了迁移学习的基本概念,本节中我们来看看如何准备数据。代码的大部分内容与我们之前讨论的 VGG16 实现相同,但有两个关键行需要注意。
以下是数据标准化时使用的均值和标准差参数:
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
这些数值并非随意设定,而是来源于 ImageNet 数据集。因为我们将使用在 ImageNet 上预训练的模型,所以需要确保输入数据与模型训练时使用的数据处于相同的尺度。

有时也建议根据你自己的数据集重新计算这些参数。这是因为你的图像可能在亮度、色彩等方面与 ImageNet 存在系统性差异。你可以尝试两种方法,如果结果差异显著,则两种都值得尝试。


2. 加载预训练模型 🧠



现在,我们进入更有趣的部分——迁移学习本身。我们将从 torchvision 库中加载预训练的 VGG16 模型。

以下是加载模型的代码:
import torchvision.models as models
model = models.vgg16(pretrained=True)
torchvision 提供了多种预训练模型,如 DenseNet、MobileNet、ResNet、Inception 等。它们都在 ImageNet 上进行了预训练,你可以根据项目需求选择。VGG16 因其结构经典而被我们选用。




打印模型结构 print(model) 可以看到,它主要分为两部分:features(卷积层,用于特征提取)和 classifier(全连接层,用于分类)。


3. 冻结模型与微调特定层 🔒
我们计划冻结所有卷积层,只微调最后的全连接层。首先,我们需要冻结整个模型的所有参数。

以下是冻结模型的代码:
for param in model.parameters():
param.requires_grad = False
这行代码遍历模型的所有参数,并将 requires_grad 属性设置为 False。这意味着在反向传播时,这些参数不会被更新,但前向和反向传播仍可正常进行。

接下来,我们需要“解冻”并微调 classifier 部分中的特定层。查看模型结构可知,classifier 包含多个层(如 Linear、Dropout、ReLU)。我们只训练那些包含可学习参数的线性层。



以下是解冻并替换输出层的代码:
# 解冻最后两个全连接层(索引1和3)
model.classifier[1].requires_grad = True
model.classifier[3].requires_grad = True


# 替换最后的输出层,因为ImageNet有1000类,而CIFAR-10只有10类
model.classifier[6] = torch.nn.Linear(4096, 10)
我们解冻了索引为 1 和 3 的线性层,并将最后的输出层替换为一个新的线性层,其输出特征数从 1000 改为 10。



4. 模型训练与性能分析 📈




模型设置完成后,训练过程与之前完全相同。但使用预训练模型的一个显著优势是,模型在训练初期就具有较高的准确率,因为它已经具备从 ImageNet 学习到的特征提取能力。




然而,在最初的尝试中,当使用 CIFAR-10 原始的小尺寸图像(如 32x32)进行训练时,模型性能可能并不理想(例如仅达到 77% 的准确率)。一个可能的原因是,预训练的 VGG16 期望输入图像的最小尺寸为 224x224。虽然网络中的自适应平均池化层(AdaptiveAvgPool2d)能处理不同尺寸的输入,但卷积层本身是在特定尺度特征上进行训练的。

为了解决这个问题,我们可以将输入图像的大小调整为 224x224。虽然这会使 CIFAR-10 的图像显得像素化,但能更好地匹配预训练模型所期望的输入尺度。实验表明,调整尺寸后,模型的性能得到了显著提升(例如达到约 85% 的准确率)。



此外,由于我们冻结了大部分参数,只训练少数全连接层,训练时间也大大缩短。例如,微调模型可能只需 38 分钟,而从零开始训练一个类似的 VGG16 可能需要 90 分钟。



5. 迁移学习的策略与总结 🎯



关于微调多少层,并没有固定的答案。根据相关研究,微调最后三个全连接层通常能取得不错的效果,但这完全取决于具体任务和数据。有时只训练最后一层效果更好,有时则需要微调所有层。最佳策略需要通过实验来确定。


本节课中我们一起学习了在 PyTorch 中实现迁移学习的完整流程:
- 使用与预训练模型匹配的参数对数据进行标准化。
- 加载预训练模型并理解其结构。
- 冻结模型的大部分参数,只解冻并准备微调目标层。
- 替换模型的输出层以适应新任务的类别数。
- 进行训练,并注意输入尺寸与预训练模型的匹配问题。


迁移学习是一种强大的技术,它能让我们在数据有限的情况下,快速构建出高性能的模型。希望本教程对你有所帮助。接下来,我们将开始一个全新的主题——用于处理文本数据的循环神经网络(RNN)。
P125:L15.0- 循环神经网络简介 📚
在本节课中,我们将要学习循环神经网络。循环神经网络是一种特殊的神经网络,能够对序列数据进行建模。一个常见的应用是自然语言处理,例如将文档视为一系列单词,或将每个单词视为一系列字符或字母。今天,我们将从宏观角度了解循环神经网络的工作原理,并在课程最后展示一个文档分类的示例。
本课程名为“深度学习与生成模型导论”,通过本节课,我们将完成深度学习导论部分。相信届时你将能对深度学习及其可应用的不同问题领域有一个广泛而良好的理解,这将成为你深度学习的基础知识。之后,我们将把这些知识应用到生成模型部分,这部分内容将从下周开始。下周,我们将运用卷积层等概念,以及稍后介绍的循环神经网络来生成新数据。具体来说,我计划下周讲解自编码器和变分自编码器,它们是专门用于生成数据的网络类型,我们将使用卷积层来处理图像数据。之后,我还会讲解生成对抗网络,这是一个非常热门的话题。最后,我们将再次回到循环神经网络,探讨如何生成新的文本数据。
今天,我们的重点是循环神经网络的宏观概述,以及课程末尾提供的分类示例。大约在一到两周后,我们将重新探讨循环神经网络在文本生成中的应用。
现在,让我们正式开始今天的课程,因为有很多内容需要讨论。
以下是今天课程计划涵盖的主题。如果你对处理文本数据感兴趣,需要知道循环神经网络并非处理文本的唯一方法。因此,我将首先简要介绍其他一些处理文本数据的方法。在简要概述之后,我们将更深入地探讨循环神经网络,了解它们如何进行序列建模。





接下来,我将讨论不同类型的序列建模任务,因为除了文本分类之外还有更多应用。然后,我们将讲解随时间反向传播,这是我们在多层感知机和卷积网络背景下已经讨论过的反向传播算法的一个修改版本。



之后,我们将讨论长短期记忆网络。这是一种特殊的循环神经网络单元,有助于处理更长的序列。








接着,我们将探讨“多对一”词级循环神经网络。这只是一个比较花哨的说法,指的是我们处理一个文本序列并希望对其进行分类的场景,即从多个单词组成的序列得到一个输出标签。我们将在此使用词级循环神经网络。稍后我也会讨论字符级循环神经网络,即一次处理一个字符,而这里我们一次处理一个单词。




或者,观察一个例子,当我们有一段文本并希望对其进行分类时。即从一个序列(包含许多单词)到一个输出标签,我们将在这里使用词级循环神经网络。稍后我也会讨论字符级循环神经网络,字符级循环神经网络一次处理一个字符,而这里我们一次处理一个单词。













最后,我将展示如何在 PyTorch 中实现这些网络。





本节课中,我们一起学习了循环神经网络的基本概念、其处理序列数据的能力、不同类型的序列建模任务、随时间反向传播算法、长短期记忆网络单元的结构与作用,以及“多对一”词级循环神经网络在文本分类中的应用框架。我们还预览了后续课程中循环神经网络在文本生成等更高级任务中的角色。

📚 课程 P126:L15.1 - 处理文本数据的不同方法
在本节课中,我们将学习处理文本数据的几种主要方法。我们将从经典的词袋模型开始,然后探讨一维卷积神经网络,最后简要介绍更现代的Transformer模型。了解这些方法的优缺点,有助于你在不同场景下选择合适的技术。
📦 词袋模型
在深入探讨循环神经网络之前,我们先回顾一种经典的文本分类方法——词袋模型。本节中,我们将了解其基本原理和实现方式。
词袋模型是一种将文本转换为表格数据集的简单方法。它的缺点在于忽略了单词之间的序列关系,但作为基线模型,它在某些任务上依然有效。
以下是构建词袋模型的基本步骤:
- 构建词汇表:收集整个训练数据集中所有独特的单词,并为每个单词分配一个唯一的索引。
- 创建设计矩阵:将每个文本样本转换为一个向量。向量的长度等于词汇表的大小,每个位置的值代表对应单词在该文本中出现的次数(即词频)。
- 训练分类器:使用生成的设计矩阵(如
X_train)和对应的标签(如y_train)来训练一个传统的分类器,例如逻辑回归或多层感知机。
例如,对于句子 “the sun is shining”,在词汇表 {“and”:0, “is”:1, “one”:2, “shining”:3, “sun”:4, “sweet”:5, “the”:6, “weather”:7, “2”:8} 中,其向量表示为 [0, 1, 0, 1, 1, 0, 1, 0, 0]。这可以形式化地表示为:
设计矩阵 X 的形状为 (n_samples, vocabulary_size),其中每个元素 X[i, j] 表示词汇表中第 j 个单词在第 i 个文本样本中出现的次数。
🧠 一维卷积神经网络
上一节我们介绍了忽略序列信息的词袋模型,本节中我们来看看如何利用卷积神经网络来捕捉文本中的局部模式。
卷积神经网络不仅可用于图像,经过调整后也能处理文本数据。这里我们使用一维卷积层,让卷积核在文本序列上滑动以提取特征。

其工作原理如下:
- 文本数值化:首先将文本中的每个字符或单词转换为数字。例如,可以将字母映射到其在字母表中的位置。
- 一维卷积操作:使用一个一维卷积核在数值化后的序列上滑动。在每一步,卷积核覆盖一个局部窗口,计算窗口内数值与卷积核权重的点积,并加上偏置,通过激活函数后生成一个特征值。
- 堆叠卷积层:可以堆叠多个卷积层来提取更高级别的特征。
例如,对于单词 “the”,将其字符转换为数值 [20, 8, 5]。一个大小为3的卷积核 [w1, w2, w3] 在其上滑动,计算方式为:output = activation(w1*20 + w2*8 + w3*5 + bias)。
在代码中,这通常通过 torch.nn.Conv1d 或 tf.keras.layers.Conv1D 层来实现。
⚡ Transformer 模型

我们了解了基于局部窗口的卷积方法,现在来看看当前处理文本数据更先进的方式——Transformer模型。它完全依赖于自注意力机制,无需循环结构。
Transformer模型是当前自然语言处理领域的主流架构。它的核心是自注意力机制,能够建模序列中任意两个元素之间的关系,无论它们相距多远。
Transformer的成功与以下两个关键点密不可分:
- 海量数据需求:Transformer模型通常需要在数十亿句子的语料库上进行训练,这需要巨大的计算资源。
- 自监督学习:为了利用无标签的海量文本数据,Transformer使用自监督任务来生成训练信号。例如,BERT模型就使用了以下两种任务:
- 掩码语言建模:随机遮盖输入句子中15%的单词,然后训练模型预测被遮盖的单词。
- 下一句预测:给定两个句子,训练模型判断它们是否在原文中连续出现。
由于训练Transformer需要极大的成本,对于许多实际任务,循环神经网络仍然是更可行和高效的选择。我们将在后续课程中更详细地探讨Transformer。
📝 总结
本节课中我们一起学习了处理文本数据的三种主要方法:
- 词袋模型:一种简单快速的基线方法,将文本转换为词频向量,但忽略了单词顺序。
- 一维卷积神经网络:通过卷积核捕捉文本中的局部特征模式,适用于某些分类任务。
- Transformer模型:基于自注意力机制的强大架构,能建模长距离依赖,但需要海量数据和计算资源进行训练。


理解这些方法的适用场景和权衡,是构建有效文本处理系统的第一步。在接下来的课程中,我们将重点深入探讨循环神经网络。
课程 P127:L15.2 - 使用 RNN 进行序列建模 🧠
在本节课中,我们将学习如何修改多层感知机以捕获序列信息。具体来说,我们将探讨如何使用循环神经网络进行序列建模。
如何判断模型是否使用了序列信息
在深入探讨循环神经网络之前,我们先思考一个问题:如何判断一个模型是否已经使用了序列信息?例如,逻辑回归或多层感知机这类模型,它们是否利用了序列信息?
答案是否定的。我们可以从两个维度来理解训练数据中可能存在的序列信息:一是跨训练样本轴,二是跨特征轴。
为了说明这一点,让我们回顾一下经典的鸢尾花数据集。虽然这个例子有些简单,但它能很好地阐明问题。
在鸢尾花数据集中,我们有花萼长度、花萼宽度、花瓣长度和花瓣宽度等特征,构成一个表格数据集。我们有150个训练样本。
当你拥有这个数据集并将其分割为训练集和测试集后,你可以对测试集中的所有记录进行随机打乱。然后,当你在这个打乱后的测试集上评估模型性能时,应该得到与打乱前完全相同的结果。这表明模型并未使用跨训练样本轴的序列信息,它将数据视为独立同分布的。
另一种序列信息可能编码在特征中。例如,如果你交换数据集中两列的位置(比如交换花萼长度和花瓣宽度),然后以相同的方式分割数据集并训练模型,你应该得到与交换前完全相同的准确率。这是因为多层感知机和逻辑回归等模型将特征视为独立的,不依赖于特征的特定顺序。

然而,当我们回顾词袋模型时,这可能会成为一个问题。词袋模型有一个词汇表,但它本质上丢弃了每个训练样本中单词的顺序信息。
考虑以下两个句子:
- The movie my friend has not seen is good.
- The movie my friend has seen is not good.
这两个句子含义截然不同。第一个句子表示朋友没看过一部好电影,第二个句子表示朋友看过一部不好的电影。但如果使用基于词频的词袋模型,这两个句子会产生完全相同的特征向量,导致语义信息丢失。在这里,单词的顺序至关重要。
循环神经网络可以帮助我们捕获这种顺序信息。

序列数据的例子 📊
以下是几个序列数据的例子:
- 文本分类:这是我们本节课将重点关注的领域。在文本数据集中,时间维度体现在单词的顺序上。每个文档可以看作一个训练样本。
- 语音识别:处理一系列声音信号。
- 机器翻译:将一个序列(源语言)翻译成另一个序列(目标语言)。
- 股票市场预测:每个股票可以看作一个训练样本,时间维度是日期,特征向量可能包含价格、新闻情绪等信息。
- DNA序列建模:将DNA序列视为字符序列进行处理。

循环神经网络的结构

之前我们使用的是前馈神经网络,如多层感知机、卷积网络和逻辑回归。它们通常有一个输入特征向量 x,经过若干隐藏层,最终产生输出。
在循环神经网络的设置中,我们引入了循环边。新元素在于我们有了一个时间步 t。在时间步 t,我们得到一个特征向量,将其输入到隐藏状态,并产生一个输出。但除此之外,隐藏层不仅接收当前时间步的输入,还接收来自先前时间步(例如 t-1)的信息。

为了更清晰地展示,我们通常使用展开的视图,这也是实际代码实现的方式。下图展示了单层循环神经网络的展开状态。

让我们聚焦于中心的时间步 t。它接收时间步 t 的特征向量 x_t,这会产生隐藏状态 h_t,进而产生输出 o_t。此外,这个时间步还接收来自前一个时间步 t-1 的隐藏状态 h_{t-1} 作为输入。

可以看到,现在有两个输入:一个是来自序列的当前特征向量 x_t,另一个是前一个时间步的隐藏状态 h_{t-1}。这使得网络能够感知序列的顺序。然后,h_t 会被传递到下一个时间步 t+1,依此类推。
右侧的紧凑表示法和左侧的展开表示法是等价的,只是展示方式不同。在代码实践中,我们通常使用展开版本。
多层循环神经网络
上一节我们介绍了单层循环神经网络,当然,我们也可以将此概念扩展到多层循环神经网络。下图展示了一个具有两个隐藏层的循环神经网络。

同样的原理适用于每一层:对于每个隐藏层,我们都有循环边。展开后可以看到,某一层的某个单元既接收来自同一时间步上一层的输入,也接收来自同一层前一个时间步的输入。

再次强调,每个隐藏单元接收两个输入。如果我们聚焦于某个特定单元,它会接收来自当前层前一时间步和同一时间步上一层的输入。

总结与展望
本节课我们一起学习了循环神经网络的基本结构,了解了它如何通过引入循环连接来捕获序列中的顺序信息,从而解决了像词袋模型那样丢失单词顺序的问题。

我们看到了循环神经网络在时间步 t 的隐藏状态 h_t 是如何由当前输入 x_t 和前一时间步的隐藏状态 h_{t-1} 共同决定的,其核心思想可以概括为以下公式:
h_t = f(W_{xh} * x_t + W_{hh} * h_{t-1} + b_h)

其中 f 是激活函数,W_{xh} 和 W_{hh} 是权重矩阵,b_h 是偏置项。

在接下来的课程中,我们将探讨如何使用这种架构处理不同类型的序列建模任务,并深入讲解适用于此类模型的反向传播算法(即通过时间反向传播)是如何工作的。


课程 P128:L15.3 - 不同类型的序列建模任务 📚

在本节课中,我们将学习循环神经网络(RNN)可以应用于哪些不同类型的序列建模任务。我们将逐一介绍四种常见的任务类型,并通过具体例子帮助理解每种任务的特点。

在上一节视频中,我们了解了循环神经网络(RNN)的整体架构概览。本节中,我们来看看如何利用这种架构来处理不同类型的序列建模任务。
以下是四种常见的序列建模任务概览,我们将在接下来的内容中逐步讲解。


首先,我们从“多对一”设置开始。这种任务的特点是输入是一个序列,但输出是一个固定大小的向量或值,而非序列。
一个典型的例子是情感分析。输入可以是一段文本,输出则是一个类别标签,用于判断文本的情感是积极还是消极。
回想之前提到的电影评论例子。每条输入可以是一篇书面影评,而输出则是判断该影评对电影的评价是正面还是负面。

接下来是“一对多”类型的序列建模任务。这种任务稍微超出了标准RNN的范畴,更常与卷积神经网络(CNN)结合使用。
在这种设置中,输入数据是标准输入(例如一张图像),而输出是一个序列。图像描述生成(Image Captioning)就是一个很好的例子。

输入可能是一张图片,输出则是描述该图片的文本序列。例如,一张有人打网球的图片,其描述可能是“一个人正在打网球”这样的文本序列。

另一种任务是“多对多”类型,它实际上有两种形式。在这种通用设置中,输入和输出都是序列。
第一种是直接对应的形式。对于序列中的每个时间步,都有一个输入对应一个输出。例如,视频描述生成任务就属于此类。视频由多帧图像组成,为每一帧图像生成描述,就构成了一个多对多的直接任务。



第二种是延迟对应的形式。在这种设置中,模型需要先读取完整的输入序列,然后再生成完整的输出序列。机器翻译是典型的例子。
例如,将一句英文翻译成德文。这并不适合直接逐词翻译,因为不同语言的语法规则不同。模型需要先理解整个句子的含义,才能生成准确的翻译。


以上是对不同序列建模任务的快速概览。在下一个视频中,我们将学习随时间反向传播(Backpropagation Through Time),这是RNN学习参数的方法。

之后,我们将讨论对标准RNN设置的改进,并查看一些具体例子。这些例子将包括使用词级RNN的“多对一”分类任务,以及在本课程后期可能涉及的文本生成和语言翻译等内容。



那么,接下来让我们开始讨论随时间反向传播。



本节课总结

本节课中,我们一起学习了循环神经网络可以处理的四种主要序列建模任务:“多对一”(如情感分析)、“一对多”(如图像描述)、“多对多-直接对应”(如视频描述)以及“多对多-延迟对应”(如机器翻译)。理解这些任务类型是应用RNN解决实际问题的基础。
课程 P129:L15.4 - 时间反向传播概述 📚
在本节课中,我们将学习循环神经网络(RNN)内部的核心训练算法——时间反向传播(Backpropagation Through Time, BPTT)。我们将了解其基本工作原理,并认识它与标准反向传播的区别,特别是如何处理时间维度上的梯度计算。

循环神经网络的结构回顾 🔄
上一节我们介绍了循环神经网络的基本概念,本节中我们来看看其内部结构,特别是权重矩阵的构成。
下图展示了一个单隐藏层的循环神经网络。左侧是紧凑表示法,右侧是沿时间轴展开的版本。

以下是网络中涉及的三个主要权重矩阵:
- 权重矩阵 1 (W_xh):连接输入层到隐藏层。
- 权重矩阵 2 (W_hy):连接隐藏层到输出层。
- 权重矩阵 3 (W_hh):连接前一个隐藏状态到当前隐藏状态。
与普通的多层感知机(MLP)相比,循环神经网络新增了权重矩阵 3 (W_hh),这是处理序列依赖性的关键。在展开的视图中,可以看到这些权重矩阵在每个时间步都是共享的。
前向传播计算 🧮
了解了结构后,我们来看看前向传播时,如何计算每个时间步的隐藏状态和输出。

隐藏状态的计算
在时间步 t,隐藏状态的净输入由两部分组成:当前输入和上一个隐藏状态。
其计算公式如下:
net_h[t] = W_xh * x[t] + W_hh * h[t-1] + b_h
其中:
x[t]是时间步 t 的输入。h[t-1]是时间步 t-1 的隐藏状态。b_h是隐藏层的偏置项。
得到净输入后,我们通过一个激活函数(如 tanh、ReLU)来计算当前隐藏状态:
h[t] = activation_function(net_h[t])
输出的计算
输出层的计算方式与多层感知机完全相同:
y[t] = activation_output(W_hy * h[t] + b_y)
根据任务类型(如分类、回归),输出层的激活函数可以是 Softmax、Sigmoid 或线性函数。
损失函数与时间反向传播 ⚖️

前向传播得到了预测结果,接下来我们需要定义损失并计算梯度以更新网络。
损失函数的构成
损失函数的设计取决于具体的序列建模任务:
- 多对一任务(如文本分类):通常只使用最后一个时间步的损失。
- 多对多任务(如序列标注):通常每个时间步都有一个损失。
在通用情况下,我们可以为每个时间步计算一个损失 L[t],总损失是它们的和:
L_total = sum(L[t] for t in 1 to T)
时间反向传播的核心思想
时间反向传播是标准反向传播算法在时间序列上的扩展。其核心挑战在于计算损失对循环权重 W_hh 的梯度。
假设我们计算时间步 T 的损失 L_T 对 W_hh 的梯度。由于 W_hh 在每个时间步都被使用,梯度必须沿着时间轴反向传播回去。

梯度计算涉及一个连乘项:
∂L_T/∂W_hh ≈ sum( ∂L_T/∂h_T * ∂h_T/∂h_k * ∂h_k/∂W_hh ),其中 k 从 1 到 T。
这个公式表明,当前损失对早期权重的梯度,需要通过所有中间隐藏状态进行链式求导。
梯度消失与爆炸问题 💥
上述链式求导中的连乘操作会带来一个严重问题:梯度消失或梯度爆炸。
- 梯度消失:当连乘的梯度值大多小于 1 时,随着时间步回溯,梯度会指数级衰减到接近零,导致早期层的权重几乎无法更新。
- 梯度爆炸:当连乘的梯度值大多大于 1 时,梯度会指数级增长,变得极大,导致训练不稳定。


幸运的是,在实际应用中,我们通常使用 PyTorch、TensorFlow 等深度学习框架,它们内置的自动微分(Autograd)功能会为我们处理这些复杂的梯度计算。然而,理解其背后的原理有助于我们认识循环神经网络的局限性。

总结与展望 🎯
本节课中我们一起学习了时间反向传播的基本概念。我们回顾了循环神经网络的结构,了解了其前向传播过程,并重点探讨了 BPTT 算法如何沿时间轴传播梯度,以及由此引发的梯度消失/爆炸问题。


认识到基本循环神经网络的这一缺陷后,在下一节课中,我们将介绍一种改进的循环网络结构——长短期记忆网络(LSTM),它被专门设计来缓解梯度消失问题,从而能够学习更长期的依赖关系。
课程 P13:L2.0 - 深度学习简史 📜
在本节课中,我们将一起回顾神经网络与深度学习的发展历程。这段历史不仅有趣,更能帮助我们理解整个领域的知识脉络。我们将从最简单的概念开始,逐步看到技术如何演进,并最终了解当前的研究趋势。
1. 人工神经元:一切的起点 🧠
上一节我们介绍了课程的整体安排,本节中我们来看看深度学习的起点——人工神经元。
人工神经元是神经网络最基本的构建模块,它模拟了生物神经元接收信号并决定是否激活的过程。一个经典模型是感知机,其核心思想是对输入进行加权求和,然后通过一个阈值函数产生输出。
以下是感知机的基本工作原理:
- 输入:接收多个输入信号
x1, x2, ..., xn。 - 权重:每个输入对应一个权重
w1, w2, ..., wn,代表该输入的重要性。 - 加权和:计算所有输入与权重的乘积之和,再加上一个偏置项
b。公式为:z = w1*x1 + w2*x2 + ... + wn*xn + b。 - 激活函数:将加权和
z通过一个激活函数(如阶跃函数)得到最终输出y。例如:y = 1 if z > 0 else 0。
然而,单层感知机存在明显的局限性,它无法解决线性不可分的问题,比如异或逻辑运算。这促使了研究者们去探索更复杂的网络结构。
2. 多层神经网络:解决复杂问题 🔄
由于单层神经网络的局限性,研究者们开始探索增加网络层数。本节中我们来看看多层神经网络是如何被发明并变得有用的。
多层神经网络在输入层和输出层之间引入了“隐藏层”。这些隐藏层使得网络能够学习输入数据中更复杂、更抽象的特征表示。例如,在图像识别中,第一层隐藏层可能学习到边缘,第二层可能学习到由边缘组成的形状,更高层则可能识别出完整的物体。
从单层到多层的转变,极大地扩展了神经网络解决问题的能力。但训练多层网络在当时面临巨大挑战,尤其是如何有效地将误差从输出层反向传播到更早的层以更新权重。
3. 从多层网络到深度学习 🚀

多层神经网络的概念已经存在了数十年,而“深度学习”是近年才变得流行的术语。本节我们将探讨这一演变过程。
“深度学习”本质上是指具有多个隐藏层的神经网络。其兴起的关键在于三大要素的成熟:
- 算法突破:反向传播算法的有效应用,解决了深层网络的训练难题。
- 数据规模:互联网时代产生了海量的标注数据,为训练复杂模型提供了燃料。
- 计算硬件:图形处理器(GPU)的强大并行计算能力,使得训练深层网络在时间上变得可行。
这些因素共同作用,使得研究者能够构建和训练前所未有的深度模型,并在图像识别、语音处理等领域取得突破性进展,从而迎来了“深度学习”的浪潮。
4. 硬件与软件的发展 🛠️
随着深度学习模型的日益复杂,对计算能力和开发效率的要求也水涨船高。本节我们来看看支持深度学习发展的硬件与软件工具。
在硬件方面,GPU因其并行架构成为训练神经网络的首选。随后,更专业的张量处理单元(TPU)等AI芯片被开发出来,以进一步提升效率。
在软件方面,一系列深度学习框架应运而生,它们将常见的网络层、优化算法等封装成易于调用的模块,极大地降低了研究和工程的门槛。
以下是两个主流的深度学习框架示例:
# TensorFlow 示例
import tensorflow as tf
model = tf.keras.Sequential([tf.keras.layers.Dense(units=1, input_shape=[1])])
# PyTorch 示例
import torch
model = torch.nn.Linear(in_features=1, out_features=1)
这些工具使得研究人员和工程师能够更专注于模型设计与应用,而非底层实现细节。
5. 当前研究趋势简述 🌟
在了解了历史与基础支撑后,本节我们简要概述一下近几年的研究趋势。
深度学习领域的发展日新月异。近年来,一些方向受到了特别多的关注:
- 注意力机制与Transformer模型:该架构在自然语言处理领域取得统治性成功,并逐渐扩展到计算机视觉等领域。
- 自监督学习:旨在从大量无标签数据中自行学习有效表示,减少对数据标注的依赖。
- 生成模型:如生成对抗网络(GAN)和扩散模型,能够生成高质量的图像、音频等内容。
- 可解释性与鲁棒性:研究如何理解模型的决策过程,并提升模型对对抗性攻击的防御能力。
这些趋势代表了深度学习领域正在向更高效、更通用、更可靠的方向演进。
总结 📝

本节课中我们一起学习了深度学习的发展简史。我们从模拟神经元的基本单元——感知机开始,看到了单层网络的局限如何催生了多层神经网络。随后,我们回顾了算法、数据和算力的进步如何共同推动多层网络演进为今天的“深度学习”。我们还了解了支撑这一领域的硬件与软件工具,并简要浏览了当前最前沿的研究趋势。这段历史为我们后续深入学习各个具体主题提供了清晰的路线图。
📚 课程 P130:L15.5 - 长短期记忆 (LSTM) 🧠

在本节课中,我们将要学习一种称为长短期记忆 (LSTM) 的模型。这是一种特殊的循环神经网络 (RNN) 单元,专门用于建模序列数据中的长程依赖关系。对于处理较长的序列数据,LSTM 至关重要,它能显著提升循环神经网络的性能。
🔍 背景:梯度消失与爆炸问题
上一节我们介绍了循环神经网络的基本结构及其面临的挑战。在深度学习中,梯度消失和爆炸是训练深层网络时的常见问题。我们之前讨论过一些解决方案,例如在多层感知机中使用 ReLU 激活函数代替 Sigmoid 函数,以及使用批归一化 (Batch Normalization) 技术。
然而,即使采用了这些方法,构建非常深的网络(例如超过16层)仍然困难。在卷积神经网络中,我们引入了残差连接 (Skip Connections) 来构建超过30、50甚至100层的网络(如 ResNet)。
在循环神经网络的场景中,问题更加复杂。我们不仅要考虑网络的深度(层数),还要考虑序列的长度(时间步数)。在反向传播过程中,梯度需要跨越多个时间步进行传播,这极易导致梯度消失或爆炸。
以下是三种处理循环神经网络中梯度问题的常用技术:
- 梯度裁剪 (Gradient Clipping):这是最简单且广泛应用的技术。它为梯度设置一个最大值和最小值。例如,我们可以规定梯度绝对值不能超过2。如果梯度超过这个阈值,就将其裁剪到这个值,从而避免参数更新步长过大。
- 沿时间截断的反向传播 (Truncated Backpropagation Through Time, TBPTT):这是一种限制反向传播时间步数的方法。在处理长序列时,前向传播会使用整个序列。但在反向传播时,我们可能只对最后若干个时间步(例如最后20步)计算梯度,而不是回溯整个序列。这能有效缓解长序列带来的计算和梯度问题。
- 长短期记忆 (LSTM):这是一种更高级的方法,它通过引入一个记忆细胞 (Memory Cell) 来显式地建模长程依赖,从而从根本上缓解梯度消失问题。LSTM 架构最早在1997年的一篇有影响力的论文中提出。
除了 LSTM,还有一种流行的变体叫做门控循环单元 (GRU)。GRU 是 LSTM 的一个简化版本,参数更少。在实际应用中,LSTM 和 GRU 的性能通常相近,有时 LSTM 更好,有时 GRU 更好,没有绝对的优劣之分。目前,LSTM 仍然是使用最广泛的 RNN 记忆单元。本节课我们将重点讲解 LSTM,并在后续使用 PyTorch 实现它。
接下来的内容可能看起来比较复杂,但请不要担心。作为入门课程,我们的目标是获得一个宏观的理解。如果你希望深入掌握,可能需要花费数周时间研读论文并尝试从头实现。
🧩 LSTM 单元结构详解
LSTM 单元的核心思想是引入一个细胞状态 (Cell State),作为贯穿整个序列的“信息高速公路”,使得信息可以相对无损地传递。同时,它通过三个“门”结构来精细控制信息的流动:遗忘门、输入门和输出门。
下图展示了 LSTM 单元在一个时间步内的完整计算图。它看起来复杂,但我们将一步步拆解。

LSTM 在 RNN 中的位置
首先,理解 LSTM 单元如何嵌入到 RNN 框架中很重要。在标准的 RNN 中,每个时间步有一个隐藏状态 h_t。在 LSTM-RNN 中,我们用 LSTM 单元替换了这个简单的隐藏层计算。
对于一个多层 LSTM 网络,每一层都可以包含 LSTM 单元。下图示意了 LSTM 单元的输入和输出:
- 蓝色箭头:来自上一个时间步的隐藏状态
h_{t-1}。 - 红色/黑色箭头:来自上一个时间步的细胞状态
C_{t-1}(这是 LSTM 新增的核心)。 - 绿色箭头:输出到下一个时间步的隐藏状态
h_t。 - 粉色/紫色箭头:输出到下一层(如果存在)的隐藏状态。



核心计算流程
LSTM 的计算围绕更新两个状态进行:
- 细胞状态 (C_t):长期记忆,在时间步间传递。
- 隐藏状态 (h_t):短期记忆/输出,传递给下一个时间步和下一层。
其核心操作包括逐元素乘法 (⊙) 和加法 (+),并使用 Sigmoid (σ) 和 Tanh 作为激活函数。
🚪 三大门控机制
LSTM 通过三个门来控制信息流,每个门都是一个 Sigmoid 神经网络层,输出 0 到 1 之间的值,表示“允许通过”的比例。

1. 遗忘门 (Forget Gate)
遗忘门决定从上一个细胞状态 C_{t-1} 中丢弃哪些信息。
公式:
f_t = σ(W_f · [h_{t-1}, x_t] + b_f)

其中:
[h_{t-1}, x_t]表示将上一个隐藏状态和当前输入拼接起来。W_f和b_f是遗忘门的权重和偏置。σ是 Sigmoid 函数,输出值在[0, 1]。- 输出
0表示“完全忘记”对应的信息。 - 输出
1表示“完全保留”对应的信息。
- 输出
遗忘门的输出 f_t 会与上一个细胞状态 C_{t-1} 进行逐元素相乘,从而有选择地遗忘旧信息。

2. 输入门 (Input Gate) 与候选值
输入门决定将哪些新信息存入细胞状态。这一步分为两部分:
- 输入门 (i_t):一个 Sigmoid 层,决定哪些值需要更新。
- 候选细胞状态 (C̃_t):一个 Tanh 层,生成新的候选值向量,这些值可能被加入到细胞状态中。

公式:
i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C)
接下来,我们将遗忘门的输出(控制旧记忆)和输入门的输出(控制新记忆)结合起来,更新细胞状态。
细胞状态更新公式:
C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t
这个公式是 LSTM 的核心:
f_t ⊙ C_{t-1}:选择性遗忘旧状态。i_t ⊙ C̃_t:选择性添加新候选值。



3. 输出门 (Output Gate)

输出门基于更新后的细胞状态 C_t,决定当前时间步要输出什么到隐藏状态 h_t。
- 输出门 (o_t):一个 Sigmoid 层,决定细胞状态的哪些部分将输出。
- 隐藏状态 (h_t):将细胞状态通过 Tanh 激活(将值规范到 [-1, 1]),然后与输出门相乘。
公式:
o_t = σ(W_o · [h_{t-1}, x_t] + b_o)
h_t = o_t ⊙ tanh(C_t)

这个 h_t 会作为当前时间步的输出,并传递到下一个时间步。






📊 总结与对比
本节课我们一起学习了长短期记忆 (LSTM) 网络。我们来总结一下关键点:
- 目标:解决标准 RNN 在长序列上的梯度消失问题,建模长程依赖。
- 核心创新:引入细胞状态 (C) 作为贯穿序列的“记忆通道”,并通过门控机制精细调控信息流。
- 三大门控:
- 遗忘门:决定丢弃哪些旧信息。
f_t = σ(W_f·[h_{t-1}, x_t] + b_f) - 输入门:决定添加哪些新信息。
i_t = σ(W_i·[h_{t-1}, x_t] + b_i) - 输出门:决定基于当前记忆输出什么。
o_t = σ(W_o·[h_{t-1}, x_t] + b_o)
- 遗忘门:决定丢弃哪些旧信息。
- 状态更新:
- 细胞状态:
C_t = f_t ⊙ C_{t-1} + i_t ⊙ tanh(W_C·[h_{t-1}, x_t] + b_C) - 隐藏状态:
h_t = o_t ⊙ tanh(C_t)
- 细胞状态:
LSTM 的结构虽然复杂,但其设计使得它能够有效地学习并记住长期信息,因此在机器翻译、文本生成、语音识别等任务中取得了巨大成功。它的一个流行变体是门控循环单元 (GRU),结构更简单,参数更少,两者在实际应用中性能相当。
在接下来的课程中,我们将使用 PyTorch 实现一个基于 LSTM 的“多对一”分类器。大约两周后,我们还会重温 RNN/LSTM,将其应用于“多对多”的文本生成任务。


扩展阅读:如果你对 LSTM 及其变体 GRU 的详细比较和应用场景感兴趣,可以搜索相关论文和文章进行深入学习。本课程作为概述,旨在提供宏观理解,更多细节将在 Piazza 或 Canvas 上提供参考资料。



P131:L15.7 - PyTorch 中的 RNN 情感分类器 🎬





在本节课中,我们将学习如何在 PyTorch 中实现一个用于情感分类的循环神经网络。我们将使用一个“多对一”的 RNN 架构,将一段文本(如电影评论)作为输入,并输出一个类别标签(如正面或负面情感)。我们将从数据准备、模型构建到训练和预测,完整地走一遍流程。






1. 概述与准备工作 📋



上一节我们介绍了“多对一”RNN的基本概念。本节中,我们来看看如何用 PyTorch 和 TorchText 库具体实现一个情感分类器。




首先,我们需要设置一些通用参数并导入必要的库。


以下是核心参数设置:
VOCAB_SIZE = 20000
LEARNING_RATE = 0.001
BATCH_SIZE = 64
NUM_EPOCHS = 10
EMBEDDING_DIM = 128
HIDDEN_DIM = 256
NUM_CLASSES = 2





我们将使用 IMDB 电影评论数据集。为了简化预处理步骤,我们从指定链接下载一个已处理好的版本。


import pandas as pd
import torch
import torchtext
from torchtext.legacy import data
import spacy

# 下载并加载数据集
df = pd.read_csv('imdb_dataset.csv')
# 为保持代码一致性,重命名列
TEXT_COLUMN_NAME = 'review'
LABEL_COLUMN_NAME = 'sentiment'
df = df.rename(columns={df.columns[0]: TEXT_COLUMN_NAME, df.columns[1]: LABEL_COLUMN_NAME})









2. 使用 TorchText 处理数据 🔧



现在,我们需要将原始文本数据转换为模型可以处理的格式。我们将使用 SpaCy 进行分词,并使用 TorchText 的 legacy API 来构建数据管道。



首先,定义用于处理文本和标签的 Field。





# 加载 SpaCy 英语分词器
spacy_en = spacy.load('en_core_web_sm')
def tokenizer(text):
return [tok.text for tok in spacy_en.tokenizer(text)]




# 定义字段
TEXT = data.Field(tokenize=tokenizer, lower=True, include_lengths=False)
LABEL = data.LabelField(dtype=torch.long)



接下来,使用 TabularDataset 加载我们的 CSV 文件,并按照指定字段进行解析。


fields = [(TEXT_COLUMN_NAME, TEXT), (LABEL_COLUMN_NAME, LABEL)]
dataset = data.TabularDataset(
path='imdb_dataset.csv',
format='csv',
fields=fields,
skip_header=True
)

然后,我们将数据集分割为训练集、验证集和测试集。





# 首先分割出测试集
train_data, test_data = dataset.split(split_ratio=0.8)
# 再从训练集中分割出验证集
train_data, valid_data = train_data.split(split_ratio=0.85)
print(f'训练集: {len(train_data)}')
print(f'验证集: {len(valid_data)}')
print(f'测试集: {len(test_data)}')







3. 构建词汇表 📚


词汇表将每个单词映射到一个唯一的整数索引。我们只在训练集上构建词汇表,以防止信息从验证集和测试集泄露。





以下是构建词汇表的步骤:
# 基于训练集构建词汇表,并限制最常出现的20000个词
TEXT.build_vocab(train_data, max_size=VOCAB_SIZE)
LABEL.build_vocab(train_data)



print(f"词汇表大小: {len(TEXT.vocab)}")
print(f"类别: {LABEL.vocab.stoi}")
词汇表大小会是 VOCAB_SIZE + 2,因为包含了 <unk>(未知词)和 <pad>(填充)两个特殊标记。





4. 创建数据加载器 ⚙️



为了高效训练,我们使用 TorchText 的 BucketIterator。它会将长度相近的句子分到同一个批次中,从而减少所需的填充(padding)量,提升计算效率。






device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')







train_loader, valid_loader, test_loader = data.BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size=BATCH_SIZE,
sort_within_batch=True,
sort_key=lambda x: len(x.text),
device=device
)



让我们检查一下第一个批次的数据形状,确保一切正常。
for batch in train_loader:
text, labels = batch.review, batch.sentiment
print(f'文本张量形状: {text.shape}') # (句子长度, 批次大小)
print(f'标签张量形状: {labels.shape}') # (批次大小,)
break
注意,RNN 输入的张量形状通常是 (序列长度, 批次大小),这与卷积网络不同。






5. 构建 LSTM 模型 🧠





我们的模型将包含三个主要部分:嵌入层(Embedding Layer)、LSTM 层和全连接输出层。






以下是模型的定义:
import torch.nn as nn





class RNNClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.rnn = nn.LSTM(embed_dim, hidden_dim, batch_first=False)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text):
# text 形状: (sent_len, batch_size)
embedded = self.embedding(text) # 形状: (sent_len, batch_size, embed_dim)
output, (hidden, cell) = self.rnn(embedded)
# 我们取最后一个时间步的隐藏状态作为句子表示
# hidden 形状: (1, batch_size, hidden_dim)
hidden = hidden.squeeze(0) # 形状: (batch_size, hidden_dim)
logits = self.fc(hidden) # 形状: (batch_size, output_dim)
return logits



model = RNNClassifier(len(TEXT.vocab), EMBEDDING_DIM, HIDDEN_DIM, NUM_CLASSES).to(device)
在 forward 函数中,我们只使用了 LSTM 最后一个时间步的隐藏状态(hidden),然后通过一个全连接层得到分类逻辑值(logits)。这是一种典型的“多对一”处理方式。









6. 训练模型 🏋️





训练循环与我们之前见过的类似。我们使用交叉熵损失和 Adam 优化器。


以下是训练和评估函数:
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)



def compute_accuracy(preds, y):
_, predicted = torch.max(preds, 1)
correct = (predicted == y).float()
acc = correct.sum() / len(correct)
return acc





def train(model, iterator, optimizer, criterion):
model.train()
epoch_loss = 0
epoch_acc = 0
for batch in iterator:
text, labels = batch.review, batch.sentiment
optimizer.zero_grad()
predictions = model(text)
loss = criterion(predictions, labels)
acc = compute_accuracy(predictions, labels)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
我们可以用类似的函数来评估验证集和测试集。










7. 对新文本进行预测 🔮






模型训练好后,我们需要能够对新的、未见过的文本进行预测。




以下是一个预测函数:
def predict_sentiment(model, sentence, text_field, label_field, device):
model.eval()
# 分词并转换为索引
tokenized = [tok.text for tok in spacy_en.tokenizer(sentence.lower())]
indexed = [text_field.vocab.stoi[t] if t in text_field.vocab else text_field.vocab.stoi['<unk>'] for t in tokenized]
tensor = torch.LongTensor(indexed).unsqueeze(1).to(device) # 形状: (sent_len, 1)
with torch.no_grad():
prediction = model(tensor)
probabilities = torch.softmax(prediction, dim=1)
# 获取正面情感的概率(假设索引1对应正面)
positive_prob = probabilities[0][label_field.vocab.stoi['1']].item()
return positive_prob




# 示例
sentence = "This is such an awesome movie, I really love it! 😊"
prob = predict_sentiment(model, sentence, TEXT, LABEL, device)
print(f"正面情感概率: {prob:.2%}")








8. 更高效的方法:Packed LSTM 🚀



上面介绍的是基础的 LSTM 实现。在实践中,为了处理不同长度的序列并减少计算浪费,我们可以使用 Packed Sequence。这种方法在内部对序列进行打包,跳过填充部分的计算,能显著提升训练速度。



主要改动在于数据加载和模型前向传播:
- 在定义
TEXT字段时,设置include_lengths=True以获取序列实际长度。 - 在
BucketIterator中设置sort_within_batch=True和sort_key。 - 在模型前向传播中,使用
torch.nn.utils.rnn.pack_padded_sequence和torch.nn.utils.rnn.pad_packed_sequence。




由于实现更为复杂,这里不展开代码细节,但它是处理变长序列的标准且高效的方法。











9. 总结 📝






本节课我们一起学习了如何在 PyTorch 中实现一个基于 LSTM 的情感分类器。我们涵盖了从使用 TorchText 和 SpaCy 进行数据加载与处理,到构建包含嵌入层、LSTM 层和全连接层的模型,再到训练模型并对新文本进行预测的完整流程。




关键点总结:
- 数据处理:使用
Field定义数据处理方式,用TabularDataset加载数据,用BucketIterator创建高效的数据加载器。 - 词汇表:只在训练集上构建,并限制大小以防止过拟合。
- 模型架构:
Embedding -> LSTM -> Linear,采用“多对一”结构,使用最后一个时间步的隐藏状态进行分类。 - 训练:标准的训练循环,使用交叉熵损失和 Adam 优化器。
- 预测:需要将新文本分词并转换为词汇表索引,再输入模型。
- 高级优化:提及了使用 Packed Sequence 来处理变长序列,以提高效率。


实现 RNN 文本分类涉及较多步骤,初次接触可能会感到复杂,这是完全正常的。希望本教程能为你提供一个坚实的起点和可用的代码模板。在接下来的课程中,我们将进入生成式模型的世界。
课程 P132:L16.0 - 自编码器简介 🧠
在本节课中,我们将要学习自编码器的基本概念。自编码器是一种能够将数据编码为更紧凑表示,并尝试将其重构回原始维度的架构。虽然自编码器本身可能并非极其强大,但它是理解后续生成模型(如变分自编码器和生成对抗网络)的重要基础。此外,在学习自编码器的过程中,我们也会接触到反卷积等概念,这些知识在后续课程中同样有用。
动机与应用示例 🎯
上一节我们介绍了自编码器的基本定位,本节中我们来看看为什么自编码器是有趣且有用的。自编码器不仅是生成模型的入门,其本身也能用于多种实际应用。

以下是自编码器的一些应用示例:

- 语音去噪:研究人员使用一种称为“去噪自编码器”的模型,将含噪语音的频谱图转换为更清晰、干净的语音。
- 图像增强:在医学图像处理等领域,自编码器被用于从噪声图像中恢复出更清晰的图像。尽管会损失一些细节,但其去噪和恢复能力依然令人印象深刻。
- 隐私增强:例如,在面部识别场景中,可以使用卷积自编码器来移除图像中的性别信息,同时保留用于身份匹配的关键特征。这有助于在完成安全验证(如与犯罪数据库比对)的同时,最小化对个人敏感信息的收集。
课程主题概览 📋

以下是本节课将涵盖的五个主要主题:
- 降维简介:理解自编码器背后的降维思想。
- 全连接自编码器:介绍类似于多层感知机的、使用全连接层构建的自编码器。
- 卷积自编码器:将自编码器概念扩展到更适合处理图像的卷积结构。
- PyTorch实现:展示如何使用PyTorch实现一个卷积自编码器。
- 其他自编码器类型:简要介绍其他类型的自编码器,其中变分自编码器将在下一节课中详细讨论。

本节课中我们一起学习了自编码器的核心概念、应用场景以及课程的主要内容框架。自编码器通过编码与解码的过程学习数据的紧凑表示,是通往更复杂生成模型世界的一块重要基石。在接下来的章节中,我们将深入探讨其具体实现与变体。
课程 P133:L16.1 - 降维 🎯
在本节课中,我们将要学习降维这一机器学习中的重要概念。降维旨在减少数据集中的特征数量,是数据预处理和特征工程中的关键技术。我们将从降维的基本概念出发,介绍其两种主要方法,并重点讲解一种经典的无监督降维技术——主成分分析(PCA),为后续理解自编码器打下基础。

降维概述 📊
在深入讲解自编码器之前,让我们先简要讨论一下降维。
降维是一个广义术语,用于描述减少数据集中特征数量的过程。
降维的一个子主题是特征选择。如果你有一个包含大量特征的数据集,你可以选择这些特征的一个子集,这就是特征选择。
降维的另一个子主题是特征提取。特征选择与特征提取的区别在于,在特征提取中,我们不一定保留原始特征。通常,特征提取是原始特征的组合,例如通过线性或非线性变换得到新特征。在本视频中,我将展示一个特征提取的例子,这也是自编码器背后的核心概念。自编码器可以被视为一种特征提取算法。
无监督学习与降维目标 🤖
自编码器(至少是常规的自编码器)也属于无监督学习的范畴。你可能在本学期早期的视频中听说过无监督学习,当时我们讨论了机器学习的三大类别。
第一类是监督学习,另一类是强化学习,而这里讨论的第三类就是无监督学习。顾名思义,无监督学习就是没有标签的监督学习。我们只使用特征,而不使用标签,即忽略目标变量。
有些问题我们根本没有目标变量。有时我们可能有目标变量,但可能决定不使用它。当我们谈论无监督学习时,有不同的应用类型和目标。
以下是几种常见的无监督学习目标:

- 发现数据中的隐藏结构。
- 压缩数据,例如出于存储需求。
- 一些机器学习算法容易受到维度灾难的影响。通过减少特征数量,我们可以提升模型性能。
- 聚类,即对相似类型的对象进行分组,本质上与“检索相似对象”这一点非常相似。例如,如果你有两张人物照片,背景不同,直接逐像素比较以判断是否为同一人非常困难。但我们可以为两张图像提取一个更低维度的特征向量,然后比较这些特征向量。我们假设用于构建此特征向量的方法只关注了关键信息(如图像中的人脸),而忽略了背景。
- 特征提取的另一个目标可以是探索性数据分析,这与发现数据中的隐藏结构有关。例如,你想绘制一个高维数据集,而人类无法真正可视化超过三个维度的图。因此,你可以将高维数据集通过特征提取降至两到三维,然后进行绘制。
- 最后一个重要目标是生成新样本。我们可以尝试学习数据集的分布,然后从该分布中采样以生成新数据。这将是下一讲讨论变分自编码器时的主题。今天我将展示一个生成新数据的例子,但这里的方法并不理想,更好的方法将在下一讲的变分自编码器中介绍。
总而言之,在讨论自编码器之前,让我继续用一个降维的例子来说明。
主成分分析(PCA)示例 🔍
我想你们很多人都听说过主成分分析。我们不会在本课程中深入探讨PCA的细节,因为它不是一个深度学习主题,但这里会从宏观角度进行概述。

在PCA中,我们本质上是寻找最大方差的方向,这是第一步。如下图所示,圆圈代表数据点,你有两个特征x1和x2。图中的PC1和PC2代表你的主成分。
如果你将这里的数据集视为一个二维矩阵,这些主成分本质上代表该矩阵的特征向量。PC1是与最大特征值相关的特征向量,PC2是与第二大特征值相关的特征向量。它们给出了数据最分散的方向。我们假设数据在这个轴(PC1)上最分散,而PC2是第二分散的方向。

一旦我们找到了这些主成分(即最大方差的方向),下一步就是进行旋转。如下图所示,x1和x2是我们的原始特征空间。现在我将它向右旋转,得到了两个新特征轴:PC2和PC1。我将我的数据集对齐到这个新的特征轴上,这本质上可以看作是一个线性变换。
如果我想进行降维,我可以只考虑具有最大方差的方向,而忽略另一个。在这里,我会将我的数据集从二维降至一维。例如,我会考虑第一个特征向量(PC1),而忽略第二个(PC2)。这样做的结果就是“压缩”数据。你可以想象将所有数据点向下压缩到一维上。
下图展示了将数据仅投影到一维(PC1)上的样子。这是方差最分散的主成分。如果你对PC2做同样的事情,你会发现数据在这个方向上分散程度较小,很多点会重叠在一起。
这只是一个宏观概念。当然,从二维降到一维的例子有些简单。在实际应用中,我们可能有数百或数千个特征,需要将其降至更小的维度空间。那么,我们应该保留多少个主成分呢?这是一个需要查看主成分所保留或捕获的累积方差的话题。由于这并非深度学习的核心内容,此处不再深入。我只是想向你展示一种经典的降维技术。
事实上,自编码器做的事情与此类似。这里有一点我尚未提及:PCA有一个正交性约束。自编码器本质上做的是类似的事情,也会给出类似主成分的结果。然而,自编码器至少没有显式地施加这种正交性约束。不过,使用线性激活函数的自编码器本质上就类似于主成分分析。当然,在深度学习的背景下,我们也会讨论非线性激活函数。我们将在下一个视频中看到,自编码器实际上会比这个主成分分析更复杂一些。
以上是对一种经典降维技术的简要概述。在下一个视频中,我将实际展示自编码器的样子。
总结 📝


本节课中,我们一起学习了降维的基本概念。我们了解到降维包括特征选择和特征提取两种主要方式,并认识到自编码器属于后者的范畴。通过主成分分析(PCA)的例子,我们直观地理解了如何通过寻找数据最大方差的方向,并对其进行线性变换和投影,来实现数据从高维到低维的压缩。这为我们接下来深入学习更强大的非线性降维工具——自编码器,奠定了重要的基础。
课程 P134:L16.2 - 完全连接的自编码器 🧠

在本节课中,我们将要学习自编码器的基本概念,特别是完全连接的自编码器。我们将了解其结构、工作原理、与主成分分析的区别,以及它在实际中的应用。
自编码器简介
自编码器是一种特殊类型的神经网络,其结构类似于沙漏。你可以将其视为一个多层感知机。

上图展示了一个非常简单的完全连接自编码器。它本质上是一个多层感知机。
编码器与解码器
让我们从观察上图开始。图中被圈出的部分是所谓的编码器部分。本质上,这是一个全连接层。在 PyTorch 的语境中,你可以将其视为一个线性层。
编码器可以表示为一个全连接层,例如,具有 5 个输入和 2 个输出。
在另一侧,我们有一个解码器。解码器本质上也是一个全连接层,但方向相反,具有 2 个输入和 5 个输出。它旨在将数据恢复回原始维度。
在编码器和解码器之间,我们有一个被称为隐藏单元或嵌入空间(有时也称为潜在空间或瓶颈)的部分。输出是重构的输入。
我们的操作是将特征投影到一个更小维度的空间,然后再将其重构出来。
与主成分分析的区别
如果自编码器仅按上述方式设置,它将与上一视频中介绍的主成分分析相似,但存在关键区别。
区别在于,这里没有正交性约束。没有明确的方法来确保嵌入空间中的特征1和特征2是正交的,而这在主成分分析中是通过提取特征向量来保证的。
另一个区别是,我们使用了全连接层。但仅使用线性变换会显得能力有限。在实践中,为了学习非线性变换,我们会引入非线性激活函数。

在实践中,我们会在全连接层之后连接一个激活函数。例如,在 PyTorch 中,结构可能如下:
# 编码器部分
encoder = torch.nn.Sequential(
torch.nn.Linear(in_features=784, out_features=32),
torch.nn.LeakyReLU()
)
# 解码器部分
decoder = torch.nn.Sequential(
torch.nn.Linear(in_features=32, out_features=784),
torch.nn.Sigmoid()
)
这意味着我们有一个全连接层,接着是一个非线性激活函数,然后是另一个全连接层和另一个激活函数。中心隐藏单元的数量可以是任意的,也可以使用多个隐藏层。

如前所述,如果不使用非线性激活函数,自编码器将类似于主成分分析。但在实践中,我们使用非线性激活函数,因此它实际上比主成分分析更强大,因为它可以学习非线性变换。
学习机制与损失函数
上一节未提及的关键点是,我们拥有输入和输出(即重构的输入)的目的是为了学习这种变换。
我们计算输入和输出之间的差异,并利用它进行反向传播,从而使自编码器学会对数据进行良好的压缩。
自编码器从高维输入映射到较小的嵌入空间表示,然后再映射回高维。我们如何知道这是一个好的表示?我们如何知道它很好地代表了原始数据?
这就是为什么我们需要将其投影回来。我们将其投影回原始空间,然后可以比较输出和输入。我们希望它们相似。
如果它们相似,即输入数据能够被重构,输出与输入接近,那就意味着中心的潜在表示必须捕获了关于输入数据的重要信息。如果中心不包含任何有用信息,我们将无法通过输出重构输入。
因此,这种设置确保了自编码器确实能够在嵌入空间中保留最有用的信息。

有不同的损失函数可用于此目的,最简单的是 L2 差或均方误差。
我们会逐个比较每个输入和输出。例如,对于五个输入和五个输出,我们会逐一比较它们,求和,然后可能取平均值,即均方误差。
均方误差公式可以表示为:
MSE = (1/M) * Σ (input_i - output_i)^2
其中 M 是维度数。

这就是自编码器的基本设置。

为何使用自编码器?
既然使用主成分分析也能实现类似效果(主成分分析本质上是一种非常高效的线性变换或矩阵分解,比使用随机梯度下降训练自编码器更高效),我们为何还要使用自编码器?
原因在于,主成分分析是线性变换,而自编码器可以更强大,能够学习非线性变换。例如,在处理图像数据时,我们可以将全连接层替换为卷积层等。因此,对于更复杂的数据类型,我们有更多机会构建更好的模型。

自编码器的应用
在实践中,仅仅重构图像本身可能没有直接意义,因为如果我们已经有输入图像,为何还要对重构感兴趣?
在常规自编码器中,重构仅用于计算均方误差损失,以学习嵌入空间。
训练完成后,我们可以移除解码器部分,仅使用嵌入作为我们提取的特征。
例如,有些人可能在大型数据集上训练自编码器,生成所有嵌入,然后尝试在其上训练分类器(如使用支持向量机、K近邻、随机森林等传统机器学习方法)。当然,你也可以直接使用它,例如构建一个具有一个隐藏层的多层感知机。
自编码器的优势在于它不需要标签信息,因此可以应用于大型未标记数据。这类似于迁移学习:你可以在非常大的数据集上训练自编码器,让它学习如何提取好的特征。然后,如果你只有该数据子集的标签,你可以使用传统方法在这些嵌入上训练模型。
你还可以将潜在空间(低维空间)用于可视化目的,例如,如果是二维的,可以用于探索性数据分析的散点图。当然,它不限于二维,也可以是更高维度,用于聚类等任务。然而,必须指出,在实际应用中,对于非线性变换的降维,可能有更好的技术,例如 t-SNE 或 UMAP。
但自编码器仍然是学习大型未标记数据并将其作为传统机器学习分类器输入的良好技术。当然,自编码器还有许多更有趣的方面和应用,我们将在后续课程中介绍,例如变分自编码器。


简单实现示例
这里是一个简单自编码器的实现,应用于 MNIST 数据集。
左侧是一个手写数字“7”的图像。在 MNIST 中,它是 28x28 维的图像,我们将其重塑为 784 维的特征向量。
这个向量输入到全连接层,后接 Leaky ReLU 激活函数。该全连接层将输入图像从 784 维压缩到 32 维。
然后,我们使用另一个全连接层将 32 维的隐藏空间转换回 784 维表示,该层后接一个 Sigmoid 激活函数。使用 Sigmoid 是因为我们将输入图像像素归一化到 0 到 1 之间,Sigmoid 的输出也在 0 到 1 之间,这样我们就可以计算像素之间的均方误差并使其最小化。
上图展示了原始 MNIST 图像和底部的重构图像。可以看到重构并不完美,存在一些伪影,但总体效果很好。因此,我们非常简单的自编码器能够在这个 32 维空间中保留足够的信息来重构原始图像。32 维大约是 784 维输入的 1/20,因此我们能够将尺寸减少约 20 倍。
如果你对此代码感兴趣,可以在 GitHub 上找到。我们不会在此详细讲解代码,因为在接下来的视频中,我们将展示一个更有趣的卷积自编码器示例。

总结与下节预告
本节课中,我们一起学习了完全连接自编码器的基本概念。我们了解了其沙漏形结构、编码器和解码器的作用、通过重构输入进行学习的机制,以及它与主成分分析的区别。我们还探讨了自编码器的应用场景和一个简单的实现示例。

在下一个视频中,我将介绍卷积自编码器的一些概念,涉及转置卷积和反卷积(两者是同一事物的不同名称)。之后,我们将在 PyTorch 中实现它。



课程 P135:L16.3 - 卷积自编码器与转置卷积 🧠
在本节课中,我们将要学习卷积自编码器的基本结构,并深入探讨其核心组件——转置卷积(也称为反卷积或分数步长卷积)。我们将了解为什么需要它,以及它是如何工作的。
概述

卷积自编码器的整体结构与之前学过的全连接自编码器类似。主要区别在于,编码器和解码器不再使用全连接层,而是分别使用卷积层和转置卷积层。编码器通过卷积操作将输入图像压缩为更小的特征表示,而解码器则需要一种方法将这个小的表示“还原”为原始尺寸的图像。这就是转置卷积的作用。
什么是转置卷积?🔄
上一节我们介绍了卷积自编码器的基本概念,本节中我们来看看其解码部分的核心——转置卷积。
转置卷积、反卷积或分数步长卷积,这些术语在深度学习中通常指代同一种操作。其核心目标是实现上采样,即输入一个较小的特征图,输出一个较大的特征图。这与常规卷积的下采样效果正好相反。
在数学上,严格的反卷积被定义为卷积运算的逆运算。但在深度学习实践中,我们通常只需要一个能实现上采样的卷积操作,而不必是精确的数学逆运算。因此,“转置卷积”这个名称更为常用。

转置卷积的工作原理 ⚙️
为了更好地理解转置卷积,我们先回顾一下常规卷积。常规卷积使用一个滑动窗口(卷积核)在输入图像上移动,通过加权求和计算输出特征图的每个像素值。
转置卷积可以看作是这个过程的反向。它从一个小的输入开始,通过卷积核生成一个大的输出。以下是其工作原理的直观理解:
假设我们有一个 2x2 的输入和一个 3x3 的卷积核,步长为 2。
- 取输入的第一个像素值,将其与整个 3x3 卷积核的每个权重相乘,得到一个 3x3 的中间块。
- 根据步长(此处为 2),将输入指针移动到下一个像素,重复步骤 1,生成另一个 3x3 中间块,并将其放置在输出网格的相应位置(通常向右移动 2 格)。
- 对输入的所有行和列重复此过程,将生成的中间块在输出网格上叠加(重叠部分的值会相加)。
- 最终,这些中间块组合成一个更大的输出特征图(例如,从 2x2 输入得到 5x5 输出)。
这种“一输入像素扩展为一块输出”的视角,是理解转置卷积最直观的方式之一。

转置卷积的实现方式(仿真方法)💻


虽然上一节介绍的概念视角很直观,但在代码实现中,转置卷积通常通过一种“仿真”方式,利用常规卷积操作来完成。
这种方法的核心思想是:通过向输入特征图添加特定的填充和间距,然后对其应用一个常规卷积,从而模拟出上采样的效果。
具体步骤如下:
- 插入间距:根据步长
s,在输入特征图的每个像素之间插入(s-1)行和列的零值。 - 添加填充:在扩展后的特征图四周添加填充。填充量
p'通常与原始卷积核大小和期望的输出有关,计算公式为p' = k - p - 1,其中k是核大小,p是原始卷积的填充。 - 执行常规卷积:使用一个与原始卷积核形状相同、但权重可能经过转置的核,对处理后的特征图进行步长为 1 的常规卷积。
这种“分数步长卷积”的称呼,正是源于在输入像素间插入零值步长的操作。尽管这种实现方式在代码上更直接,但“一输入扩展为一块”的概念模型对于理解其行为更有帮助。
输出尺寸计算公式 📐
在使用 PyTorch 等框架时,我们需要知道转置卷积层的输出尺寸。给定输入尺寸,可以通过以下公式计算:
公式:
H_out = (H_in - 1) * stride - 2 * padding + kernel_size
其中:
H_in:输入特征图的高度(或宽度)。H_out:输出特征图的高度(或宽度)。stride:步长。padding:填充值。kernel_size:卷积核大小。

注意:在转置卷积中,增加填充会减小输出尺寸,这与常规卷积中增加填充会增大输出尺寸的效果相反。

以下是一个简单的代码示例,展示如何在 PyTorch 中使用转置卷积并验证输出尺寸:
import torch
import torch.nn as nn
# 定义一个转置卷积层
# 输入通道1,输出通道1,核大小3,步长2,填充1
transpose_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=3, stride=2, padding=1)
# 创建一个 2x2 的模拟输入 (batch_size=1, channels=1, height=2, width=2)
input_tensor = torch.randn(1, 1, 2, 2)

# 前向传播
output_tensor = transpose_conv(input_tensor)
print(f"输入尺寸: {input_tensor.shape}")
print(f"输出尺寸: {output_tensor.shape}")
# 根据公式计算期望的输出尺寸:
# H_out = (2 - 1)*2 - 2*1 + 3 = 3
# 因此输出应为 3x3

棋盘格效应及应对策略 🏁

转置卷积在实践中可能遇到一个称为“棋盘格效应”的问题。这是由于在上采样过程中,输出特征图中某些区域由多个输入像素的贡献重叠而成,而其他区域则贡献较少,导致不均匀的、类似棋盘格的伪影。
以下是可能导致棋盘格效应的原因:
- 当卷积核大小不能被步长整除时,重叠模式可能不均匀。
- 特别是在使用较大的步长时,重叠区域和“空洞”区域的对比会更明显。

为了缓解或避免棋盘格效应,可以考虑以下策略:
- 调整核大小与步长:选择核大小是步长的整数倍(例如,步长为2时使用2x2或4x4的核),可以减少不均匀的重叠。
- 使用替代上采样方法:一种常见且有效的替代方案是使用“最近邻上采样”或“双线性上采样”先将特征图放大到目标尺寸,然后再接一个常规卷积层进行学习。这种方法通常能产生更平滑的结果。
- 例如:
Upsample(scale_factor=2, mode=‘nearest’)+Conv2d(...)
- 例如:

在实际应用中,并非所有转置卷积都会产生明显的棋盘格效应。建议在具体任务中尝试不同方法,并根据生成图像的质量来选择最佳方案。
总结


本节课中我们一起学习了卷积自编码器的关键组成部分——转置卷积。我们首先了解了它的目的,即实现特征图的上采样。然后,我们从概念和实现两个角度探讨了其工作原理,并给出了输出尺寸的计算公式。最后,我们讨论了转置卷积可能带来的棋盘格效应及其解决方案。掌握转置卷积是理解和使用卷积自编码器、生成对抗网络以及许多图像分割模型的重要基础。
课程 P136:L16.4 - PyTorch 中的卷积自动编码器 🧠

在本节课中,我们将学习如何在 PyTorch 中实现一个卷积自动编码器。我们将从导入必要的库开始,逐步构建模型,并最终训练它来重建手写数字图像。通过这个过程,你将理解自动编码器的核心组件:编码器和解码器,以及它们如何协同工作来学习数据的压缩表示。



环境准备与数据加载
首先,我们需要导入必要的库并设置环境。我们将使用 PyTorch 来构建和训练模型,并使用 Matplotlib 来可视化结果。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
接下来,我们定义数据加载器。与分类任务不同,自动编码器通常只使用训练集,因为我们关注的是输入与重建输出之间的差异,而不是分类精度。



# 定义数据转换
transform = transforms.Compose([
transforms.ToTensor(),
])
# 加载 MNIST 训练集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)
构建卷积自动编码器模型

上一节我们介绍了数据准备,本节中我们来看看如何构建模型。我们将模型分为编码器和解码器两个独立的 Sequential 模块,这有助于后续将它们作为特征提取器和图像生成器单独使用。


以下是模型的核心结构定义:
class ConvAutoencoder(nn.Module):
def __init__(self):
super(ConvAutoencoder, self).__init__()
# 编码器
self.encoder = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, padding=1), # 输出: (32, 28, 28)
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # 输出: (64, 14, 14)
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, padding=1), # 输出: (64, 14, 14)
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, padding=1), # 输出: (64, 14, 14)
nn.ReLU(),
nn.Flatten(), # 输出: 64*14*14 = 12544
nn.Linear(12544, 2) # 压缩为2维潜在空间
)
# 解码器
self.decoder = nn.Sequential(
nn.Linear(2, 12544),
nn.Unflatten(1, (64, 14, 14)), # 重塑为 (64, 14, 14)
nn.ConvTranspose2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(),
nn.ConvTranspose2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(),
nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1), # 输出: (32, 28, 28)
nn.ReLU(),
nn.ConvTranspose2d(32, 1, kernel_size=3, padding=1), # 输出: (1, 28, 28)
nn.Sigmoid() # 将像素值压缩到 [0, 1] 范围
)
def forward(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded



核心概念解释:
- 编码器:通过一系列卷积层逐步减少空间尺寸并增加通道数,最后通过一个全连接层将数据压缩到一个低维的潜在空间向量(本例中为2维)。公式可以表示为:
z = encoder(x)。 - 解码器:接收潜在空间向量
z,通过全连接层和转置卷积层逐步恢复图像的空间尺寸和通道数,最终输出与输入同尺寸的重建图像。公式为:x_reconstructed = decoder(z)。 - Sigmoid 激活函数:确保输出像素值在 [0, 1] 范围内,与归一化的输入图像相匹配。
训练模型


模型构建完成后,我们需要定义训练循环。自动编码器的训练目标是最小化重建图像与原始图像之间的差异。


以下是训练过程的关键步骤:


- 定义损失函数和优化器:我们使用均方误差损失来衡量重建误差,并使用 Adam 优化器。
model = ConvAutoencoder() criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters())

- 训练循环:在每个批次中,我们将图像输入编码器,获取重建图像,计算损失,并反向传播更新权重。
def train_autoencoder(model, train_loader, criterion, optimizer, num_epochs): model.train() for epoch in range(num_epochs): running_loss = 0.0 for data in train_loader: img, _ = data # 自动编码器不使用标签 optimizer.zero_grad() # 前向传播 output = model(img) loss = criterion(output, img) # 比较输出与输入 # 反向传播和优化 loss.backward() optimizer.step() running_loss += loss.item() print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')


- 开始训练:我们运行训练循环20个周期。
train_autoencoder(model, train_loader, criterion, optimizer, num_epochs=20)



结果分析与可视化
训练完成后,我们可以评估模型的表现。由于我们使用了极端的2维潜在空间,重建图像会显得比较模糊,但依然能辨认出数字轮廓。
以下是几种可视化方法:



- 比较原始图像与重建图像:
结果显示,顶部为原始图像,底部为重建图像。重建图像较为模糊,例如数字“4”可能被误重建为“9”,这是因为它们在2维潜在空间中的表示区域有重叠。# 获取一个批次的测试图像 dataiter = iter(train_loader) images, _ = next(dataiter) with torch.no_grad(): reconstructed = model(images) # 绘制对比图 fig, axes = plt.subplots(2, 8, figsize=(12, 4)) for i in range(8): axes[0, i].imshow(images[i].squeeze(), cmap='gray') axes[0, i].axis('off') axes[1, i].imshow(reconstructed[i].squeeze(), cmap='gray') axes[1, i].axis('off') plt.show()

- 可视化潜在空间:我们可以将训练集中所有图像的2维编码绘制出来,并用颜色区分数字类别。
从图中可以看到,相似的数字(如所有的“0”和“1”)在潜在空间中会聚集在一起,但不同类别之间也存在大量重叠区域,这解释了为什么重建会出现混淆。def plot_latent_space(model, data_loader): model.eval() latent_vectors = [] labels = [] with torch.no_grad(): for img, label in data_loader: latent = model.encoder(img) latent_vectors.append(latent) labels.append(label) latent_vectors = torch.cat(latent_vectors, dim=0).numpy() labels = torch.cat(labels, dim=0).numpy() plt.figure(figsize=(8,6)) scatter = plt.scatter(latent_vectors[:, 0], latent_vectors[:, 1], c=labels, cmap='tab10', alpha=0.6) plt.colorbar(scatter) plt.xlabel('Latent Dimension 1') plt.ylabel('Latent Dimension 2') plt.title('2D Latent Space Visualization') plt.show() plot_latent_space(model, train_loader)

- 使用解码器生成新图像:我们可以从潜在空间中任意选取一个点,通过解码器生成对应的图像。
如果采样点位于训练数据分布之外,生成的图像可能是不合理的“幻想”数字。这引出了对更高级生成模型(如变分自动编码器)的需求。def generate_from_latent(model, latent_point): model.eval() with torch.no_grad(): # latent_point 是一个形状为 (1, 2) 的张量 generated = model.decoder(latent_point) return generated.squeeze() # 例如,从潜在空间中心附近采样 sample_point = torch.tensor([[-2.5, 2.5]]) generated_img = generate_from_latent(model, sample_point) plt.imshow(generated_img, cmap='gray') plt.axis('off') plt.show()
总结
本节课中我们一起学习了如何在 PyTorch 中实现一个卷积自动编码器。我们首先构建了将图像压缩到2维潜在空间的编码器,以及从该空间重建图像的解码器。接着,我们使用均方误差损失训练模型,以最小化重建误差。最后,我们通过可视化重建结果、潜在空间分布以及图像生成,深入理解了自动编码器的工作原理和局限性。





关键要点包括:
- 自动编码器由编码器和解码器组成,用于学习数据的有效表示。
- 使用均方误差损失来优化重建质量。
- 极低维的潜在空间会导致信息丢失和模糊的重建结果。
- 潜在空间的可视化揭示了数据点之间的相似性关系。
- 基本的自动编码器在从任意点生成逼真新数据方面能力有限,这为学习更强大的生成模型(如变分自动编码器)奠定了基础。
课程 P137:L16.5 - 其他类型的自动编码器 🧠

在本节课中,我们将学习几种自动编码器的变体,包括 Dropout 自动编码器、去噪自动编码器、稀疏自动编码器以及变分自动编码器的基本概念。这些变体通过引入不同的约束或结构,扩展了基础自动编码器的功能与应用场景。
Dropout 自动编码器

上一节我们介绍了基础自动编码器的结构,本节中我们来看看如何通过引入 Dropout 来增强其鲁棒性。
我们可以在编码器和解码器的全连接层或卷积层之间加入 Dropout 层(例如 Dropout1D 或 Dropout2D)。Dropout 会在训练过程中随机“丢弃”一部分神经元,迫使网络学习冗余的特征表示。因为网络无法依赖某些特定特征始终存在,所以必须学习更稳健的表示。
以下是其核心思想:
# 伪代码示例:在编码器中加入 Dropout
encoder_layers = [
Linear(input_dim, hidden_dim),
Dropout(p=0.5), # 随机丢弃50%的神经元
ReLU(),
# ... 更多层
]
去噪自动编码器
理解了 Dropout 的作用后,我们可以将其思想应用于构建去噪自动编码器。
在输入层之后直接添加 Dropout,相当于向输入数据中添加了噪声(例如,随机将部分像素置零)。网络的目标仍然是最小化原始干净输入与解码器输出之间的重构误差。由于编码器只能看到带噪声的输入,为了最小化重构误差,它必须学会忽略噪声,从而学习到去噪的能力。

这种方法的核心损失函数通常是输入与输出之间的均方误差(MSE):
公式: Loss = MSE(原始输入, 解码器输出(编码器(带噪声输入)))
稀疏自动编码器
除了处理噪声,我们有时还希望学习到的编码表示是稀疏的,即大部分元素为零。
这可以通过在损失函数中添加 L1 正则化项来实现。L1 正则化会惩罚编码向量中非零元素的绝对值之和,鼓励产生稀疏表示。此时的损失函数是重构损失与 L1 惩罚项的加权和。
以下是其损失函数的构成:
公式: 总损失 = 重构损失 + λ * Σ|z_i|
其中,z_i 是编码向量中的元素,λ 是控制稀疏性强度的超参数。
变分自动编码器简介
最后,我们简要介绍一个更强大的变体——变分自动编码器,我们将在下一讲中对其进行深入探讨。

变分自动编码器的核心思想是让编码器学习一个概率分布(通常是高斯分布),而不仅仅是一个固定的向量。其损失函数包含两部分:
- 重构损失:确保解码输出与输入相似。
- KL 散度项:约束学习到的潜在分布接近标准正态分布。
其损失函数可表示为:
公式: 损失 = 重构损失 + β * KL( N(μ, σ) || N(0, 1) )
其中,μ 和 σ 是编码器输出的均值和方差。我们将在后续课程中详细解释其原理与实现。
总结
本节课中我们一起学习了自动编码器的几种重要变体:
- Dropout 自动编码器通过引入随机性增强鲁棒性。
- 去噪自动编码器利用带噪声的输入学习恢复原始数据。
- 稀疏自动编码器通过 L1 正则化获得稀疏的编码表示。
- 变分自动编码器则引入了概率生成的思想,为生成模型奠定了基础。

理解这些变体有助于我们根据具体任务(如去噪、特征提取、生成数据)选择合适的自动编码器结构。下一讲我们将深入探讨变分自动编码器。
P138:L17.0- 变分自编码器简介 🧠
课程概述
在本节课中,我们将要学习一种特殊的自编码器——变分自编码器。我们将了解它如何克服传统自编码器在生成新数据方面的局限性,并成为一个强大的生成模型。
上一周我们学习了自编码器。自编码器是一种特殊的神经网络,由两部分组成:一个编码器,它接收输入并将其压缩成更低维度的表示;以及一个解码器,它接收这个低维表示并尝试重建输入。
当时我展示了一个例子:我从这个低维空间中随机抽取一个坐标,然后使用解码器来生成一张与训练数据有些相似的新图像。通过这种方式,我们可以将其视为生成新数据。
然而,当我们讨论更高维度的空间时,如果低维空间的维度超过两个,这种方法的效果就不太好了,因为存在一些挑战。我将在本次讲座中更详细地探讨这些问题。
总的来说,在本次讲座中,我想讨论一种特定类型的自编码器,称为变分自编码器。这种变分自编码器在采样新数据方面表现更好。你可以将变分自编码器视为一个生成模型,我们可以用它从分布中采样并生成新数据。
这样看来,它就像是自编码器的一个改进版本,特别适合用于创建新数据。
在深入探讨其工作原理之前,让我先概述一下本次讲座的内容,然后我们将逐步学习变分自编码器的工作原理。当然,我也会展示一些代码示例。除了简单的代码示例,我们还将观察一个包含人脸图像的数据集,并学习如何让人脸“微笑”。
好的,我们开始吧。
今日主题
以下是今天要讨论的主题。请不要担心,虽然看起来有8个主题,但这次我会尽量精简。我知道本学期剩下的时间不多了,你们都在忙于课程项目和其他课程。我理解这一点,所以我不希望在学期末把事情搞得太复杂,只想在这里给你们一个关于变分自编码器的大致概览。
我只有25张幻灯片,非常有信心能在常规的75分钟讲座时间内完成。当然,我也会提供代码示例。
通过这次讲座,我希望能让大家对变分自编码器的工作原理有一个宏观的概览。同时,如果你们感兴趣,我也会提供一些未来的参考资料,因为变分自编码器背后有很多数学基础,我们无法在本讲座中全部涵盖,但有些人可能会对此感兴趣。

好的,在下一个视频中,我们将从概述什么是变分自编码器开始。


总结

本节课中,我们一起学习了变分自编码器的基本概念及其作为生成模型的潜力。我们了解到,与传统自编码器相比,变分自编码器通过引入概率分布,能够更有效地在高维潜在空间中生成新的、合理的数据样本。在接下来的内容中,我们将深入其具体架构和训练过程。
课程 P139:L17.1 - 变分自编码器概述 🧠

在本节课中,我们将学习变分自编码器的基本概念。我们会从回顾上周学过的标准自编码器开始,然后逐步引入变分自编码器的核心思想、目标函数及其优势。
回顾:标准自编码器 🔄
上一节我们介绍了标准自编码器的基本结构。本节中,我们先来回顾一下它的工作原理。
标准自编码器是一个由两部分组成的神经网络模型:一个编码器和一个解码器。
以下是其工作流程:
- 编码器接收输入图像 X,并将其编码为一个潜在表示,我们称之为 Z。
- 解码器接收这个潜在表示 Z,并将其重构回图像,我们称之为 X'。

我们的目标是让原始输入 X 与重构输出 X' 尽可能相似。一种常用的方法是计算它们之间的均方误差。
其损失函数可以表示为:
L = || X - Decoder(Encoder(X)) ||²
或者,对于一个小批次中的 n 个样本,计算平均损失:
L = (1/n) * Σ || X_i - X'_i ||²
引入:变分自编码器 🆕
上一节我们回顾了标准自编码器。本节中,我们来看看更复杂的变分自编码器。
变分自编码器的目标函数由两项组成。整体上,我们优化的是证据下界。
其损失函数包含两个部分:
- 重构损失:与标准自编码器类似,旨在使重构图像 X' 接近原始输入 X。
- KL 散度:用于衡量潜在空间中的编码分布与一个先验分布(通常是标准多元高斯分布)之间的差异。
标准多元高斯分布具有零均值和单位方差(对于多元情况,协方差矩阵是单位矩阵)。KL 散度的目标是让编码器的输出分布接近这个标准正态分布。
对于重构损失项,通常有两种选择:
- 如果假设数据服从多元伯努利分布,可以使用二元交叉熵。
- 对于像图像这样的数据(像素值归一化到 0 到 1 之间),使用均方误差通常效果更好。虽然交叉熵也可用,但在实践中可能导致图像更模糊,因为它会偏向于像素值 0.5。
简而言之,变分自编码器有两个目标:一是生成逼真的重构图像,二是让潜在编码的分布规整化,使其类似于标准正态分布。
优势与展望 🚀
在介绍了变分自编码器的基本框架后,我们自然会问:它比标准自编码器好在哪里?
在接下来的视频中,我们将探讨如何从变分自编码器中采样,以及为什么在采样方面,变分自编码器比标准自编码器更具优势。这将是理解其价值的关键。


本节课中,我们一起学习了:
- 回顾了标准自编码器的结构与目标。
- 引入了变分自编码器,其目标函数包含重构损失和 KL 散度两项。
- 了解了 KL 散度的作用是规范潜在空间,使其分布接近标准正态分布。
- 简要讨论了重构损失函数的选择。
- 预告了变分自编码器在数据采样方面的优势,这将在后续课程中详细展开。
课程 P14:L2.1 - 人工神经元 🧠
在本节课中,我们将要学习人工神经元的基本概念,了解其如何从生物神经元中获得灵感,并演变为能够执行基本逻辑运算的数学模型。我们将探讨感知机与自适应线性神经元(Adaline)的初步概念,并理解早期神经网络的局限性。
从生物神经元到数学模型 🧬
上一节我们提到了人工神经网络的灵感来源于生物神经元。生物神经元接收输入信号,在细胞核中进行信息整合与处理,然后通过轴突终端产生输出信号。虽然人工神经网络的工作机制与生物神经元有很大不同,但核心的“输入-处理-输出”概念被保留了下来。
基于这个灵感,McCulloch和Pitts在1943年提出了一个神经元的数学模型。这个模型可以类比为一个简单的计算单元。
实现基本逻辑函数 🔢
为了阐明神经元数学模型如何与基本逻辑函数相关联,我们快速概览一下。
以下是三种基本逻辑函数的真值表:AND(与)、OR(或)和NOT(非)。
你可以将 x1 和 x2 视为输入,将 y 视为输出。
- 对于AND函数:仅当两个输入都为1时,输出才为1。
- 对于OR函数:只要有一个输入为1,输出就为1。
- 对于NOT函数:输出是输入的反转。

这里我们讨论的是二进制值,即可能的取值仅为0和1。
数学模型的实现方式 ➗


这与生物神经元类似,是使用数学方法实现AND函数的一种方式。我们有两个输入 x1 和 x2,以及一个输出。
我们将在下一讲讨论感知机模型时更详细地回顾这一点。这里只是展示,利用生物神经元的概念来实现此类函数确实是可行的。
其工作原理是:首先对输入进行求和(积分)。你可以将其视为一个分段线性函数。如果输入之和(例如 x1 + x2)大于或等于某个阈值,则返回1;否则返回0。这就是一个神经元。
这里也存在权重。请注意,在此例中我将权重设为1,因此它们实际上不起作用。通常我们会计算输入的加权和,但此处暂不讨论。由于权重为1,我们可以忽略它们。在后续课程中,我们将讨论如何通过学习来获得这些权重,即参数化模型。

你只需代入不同的值(如(0,0), (1,0), (0,1), (1,1)),查看和 x1 + x2 是否大于阈值,就会发现这实现了AND函数。
类似地,你可以通过将阈值改为0.5来实现OR函数。你也可以通过将阈值改为-0.49并将权重改为-1来实现NOT函数。其函数形式为:如果 -x1 大于等于阈值则返回1,否则返回0。

这就是数学模型如何表示AND、OR和NOT函数的一个例子。
从固定模型到学习算法 🤖
在上一张幻灯片中,我谈到了模型,但当时没有方法可以自动确定阈值。我们必须为特定函数手动编码阈值。

现在假设你有一个数据集,并且想学习一个分类器。你将需要尝试大量权重和阈值组合,以找到分类数据的好方法。
因此,Frank Rosenblatt提出了一个想法,即开发一种学习算法,能够从数据中自动找到最优的权重和阈值,以进行良好的分类。例如,如果你想分类垃圾邮件和非垃圾邮件,可以使用一个能从数据中学习以找到良好决策阈值的系统。
这大约是在McCulloch和Pitts神经元提出十年后,由Frank Rosenblatt完成的。实际上有多个感知机算法,但在实践中,当我们说“感知机算法”时,通常指一种流行的算法。我们将在下一讲更详细地讨论这个感知机算法,并用代码实现它。
感知机与Adaline的演进 ⚙️
以下是其工作原理和外观的快速概览。与上一张幻灯片类似,我们有多个输入和权重。它们在此处结合,形成加权和,这被称为净输入函数。然后有一个激活函数,这基本上就是上一张幻灯片介绍的阈值函数,之后会产生输出0或1。我们将再次更详细地讨论这一点。
在感知机之后,我们仍在讨论单层神经网络。随后出现了所谓的Adaline(自适应线性神经元),由Widrow和Hoff在1960年提出,仅三年之后。
感知机和Adaline之间的区别在于,Adaline是一个可微分的神经元模型。这稍微改变了我们学习权重的方式,因为现在我们可以使用微积分来学习权重,这更加方便。我们将在下一讲看到这个学习算法如何工作,并在未来的课程中讨论使用微积分学习权重的梯度下降法。
以下是本视频最后一个快速概览。这是之前展示的感知机。现在Adaline实际上非常相似,其主要区别在于没有线性激活函数。

在感知机中,你进行分类,得到一个输出,然后通过将此输出与真实标签进行比较来计算误差。基于此误差,你可以返回并更新权重。
对于Adaline,区别在于你在计算实际输出之前,在阈值函数之前进行此更新。这里有一个问题:下图是否让你想起了在其他统计学课程中学到的东西?如果你只看黄色高亮的部分(忽略阈值函数),它是否让你想起了什么?这可能是Piazza上的一个好问题。
早期神经网络的局限性与AI寒冬 ❄️
最后,为什么我们现在有深度学习?为什么我们没有停留在感知机和Adaline?因为显然我们现在有了可以轻松参数化的分类器。问题是它们只能学习非常简单的决策边界,即所谓的线性决策边界。
线性决策边界是将数据集分成两部分的边界。例如,在一个二维分类问题中,一个线性分类器可能会学习一条直线作为决策边界,将一侧的数据点分类为一类,另一侧的分类为另一类。
但是,如果我们有一个例如“异或”(XOR)问题,数据点的排列使得没有一个线性决策边界能够正确分类所有点。无论你如何尝试,线性分类器都无法解决这个问题。
实际上,有一本由Minsky和Papert在1969年写的书叫《Perceptrons》,它对感知机持非常负面的态度,基本上说感知机和Adaline无法解决XOR等问题,因此非常有限,可能不是AI的目标。这引发了所谓的“AI寒冬”,人们一度对神经网络研究失去了兴趣。
然而,事情并非如此黯淡,因为实际上有方法可以开发能够解决XOR问题的新网络,我将在下一个视频中讨论这一点。
总结 📝

本节课我们一起学习了人工神经元的起源与发展。我们从McCulloch和Pitts的数学模型出发,看到了它如何实现AND、OR和NOT等基本逻辑函数。接着,我们了解了Frank Rosenblatt的感知机如何引入学习算法来自动寻找权重和阈值。随后,Adaline通过引入可微模型,为使用微积分优化权重奠定了基础。最后,我们认识到这些早期单层模型的局限性——它们只能处理线性可分问题,无法解决像XOR这样的非线性问题,这直接导致了AI寒冬的到来。这为后续多层神经网络和深度学习的发展埋下了伏笔。
课程 P140:L17.2 - 从变分自动编码器采样 🎲

在本节课中,我们将要学习为什么变分自动编码器在生成新数据方面优于普通自动编码器。核心原因在于,变分自动编码器强制其潜在空间服从标准正态分布,这使得从中采样变得更容易,从而更利于数据生成。
普通自动编码器的采样问题
上一节我们介绍了普通自动编码器的基本结构。本节中我们来看看使用它进行采样时面临的挑战。
在之前的课程中,我们使用MNIST数据集,并通过编码器将其映射到一个二维潜在空间。选择二维空间主要是为了可视化,实际上潜在空间的维度可以是任意的。通常,潜在空间的维度小于输入维度,但更大的潜在空间能保留更多信息,有助于生成更好的重构图像。
在之前的代码示例中,我们曾从该二维空间中任意选取一个点(例如 (-2.5, 2)),并将其输入解码器,得到了一个手写数字图像。
然而,这个二维空间看起来连续且密集,主要是因为维度较低。如果潜在空间维度很高(例如100维),数据点之间会非常稀疏,空间将不再连续。此时,若从高维空间中任意采样一个点,解码器很可能无法生成一个看起来像有效输入数据的图像。
以下是使用普通自动编码器采样时的主要问题:

- 分布形状不规则:潜在空间的分布可能形状怪异,难以进行均衡采样。我们熟悉如何从高斯分布中采样,但对于任意的不规则分布,采样非常困难。
- 分布不居中:分布可能位于空间的任意位置,这进一步增加了采样的难度。
- 分布不连续:在低维空间中看似连续,但在高维空间中,数据点可能非常分散,导致分布不连续。
因此,普通自动编码器虽然擅长数据压缩,但在生成与训练数据相似的新数据方面表现不佳。
变分自动编码器的解决方案
现在,让我们考虑变分自动编码器。我们仍然使用MNIST数据集,但在训练变分自动编码器时,我们强制其潜在空间服从标准多元高斯分布。
这里我们以二维为例,其概率密度函数为:

其中,\(\boldsymbol{\mu}\) 是均值向量,\(\boldsymbol{\Sigma}\) 是协方差矩阵。在变分自动编码器中,我们通常假设潜在特征之间相互独立,即协方差矩阵 \(\boldsymbol{\Sigma}\) 是一个对角矩阵。更进一步,我们通常将其约束为单位矩阵 \(\mathbf{I}\),这意味着每个维度的方差为1,协方差为0。这就是标准多元高斯分布。
从分布中采样
一旦我们确定了这个分布,就可以从中进行采样。由于这是一个连续分布,我们可以预期,当我们从中采样时,解码器能够生成合理的输出,因为分布中没有“间隙”。

那么,如何从这个分布中采样呢?以下是采样一个新潜在向量 \(\mathbf{z}\) 的方法:
z = mu + sigma * epsilon
其中:
- \(\boldsymbol{\mu}\) 是均值向量。
- \(\boldsymbol{\sigma}\) 是标准差向量(由方差向量开方得到)。
- \(\boldsymbol{\epsilon}\) 是从标准正态分布 \(\mathcal{N}(0, \mathbf{I})\) 中采样的随机噪声向量。
在实现中,我们通过神经网络学习并存储均值向量 \(\boldsymbol{\mu}\) 和对数方差向量 \(\log(\boldsymbol{\sigma}^2)\)。在正向传播时,我们使用上述公式进行采样。这涉及到一个重要的技巧,即“重参数化技巧”,它允许我们通过随机节点进行反向传播。我们将在下一节详细介绍这个技巧。
总结与预告

本节课中我们一起学习了变分自动编码器在数据生成方面的优势。关键在于它通过约束潜在空间为标准正态分布,解决了普通自动编码器采样困难的问题,使得生成新数据成为可能。
下一节,我们将深入探讨实现采样所必需的重参数化技巧,之后会介绍变分自动编码器的损失函数。最后,我将通过PyTorch代码示例展示整个流程,届时所有这些概念将变得更加清晰。



请继续关注下一节关于重参数化技巧的讲解。
课程 P141:L17.3 - 对数方差技巧 📊
在本节课中,我们将要学习变分自编码器中的一个重要概念:对数方差技巧。我们将探讨为什么在训练过程中使用对数方差而非直接使用方差,以及这一技巧如何使反向传播更加稳定和高效。
回顾:变分自编码器的潜在空间
上一节我们介绍了变分自编码器的基本结构。为了确保行文流畅,我们先简要回顾一下关键点。
在变分自编码器中,编码器网络将输入数据映射到一个潜在空间。这个潜在空间的分布被设计为服从标准多元高斯分布。其概率密度函数由均值向量 μ 和协方差矩阵 Σ 定义。对于标准正态分布,我们有:

μ = 0,且 Σ = I(单位矩阵)。
这意味着每个维度都是独立的,且方差为1。
当我们从编码器得到一个数据点 x 时,编码器会输出两个向量:
- 均值向量 μ
- 方差向量 σ²

为了从这个分布中采样一个潜在向量 z,我们使用以下公式:
z = μ + σ ⊙ ε
其中 ε 是从标准正态分布 N(0, I) 中独立采样得到的随机向量,⊙ 表示逐元素乘法。
这个采样得到的 z 随后被送入解码器,以重建输入数据 x‘。我们的目标是使重建结果 x‘ 尽可能接近原始输入 x,同时确保潜在空间分布 q(z|x) 接近先验分布 p(z)(即标准正态分布)。
引入对数方差技巧
然而,在神经网络中直接使用方差向量 σ² 会带来一个问题。方差必须始终为正值,这可能会在反向传播过程中导致训练不稳定。
为了解决这个问题,我们引入了对数方差技巧。其核心思想是:让网络学习并输出对数方差向量 log_var,而不是方差向量本身。
以下是这么做的原因:
- 数值稳定性:对数函数可以将一个正数(方差)映射到整个实数域(负无穷到正无穷)。这意味着网络层的输出
log_var可以是任意实数值,训练起来更稳定。 - 计算便利:通过指数运算,我们可以轻松地从
log_var恢复出方差和标准差。
具体操作如下:
-
网络编码器部分输出两个向量:μ 和 log_var。
-
采样步骤的公式变为:
z = μ + exp(log_var / 2) ⊙ ε

这里,exp(log_var / 2) 等价于标准差 σ。因为:
log_var = log(σ²)log_var / 2 = log(σ)exp(log_var / 2) = σ
因此,这个公式与原始的 z = μ + σ ⊙ ε 是完全等价的。
网络结构图示
让我们通过一个修改后的结构图来直观理解这个过程:
输入 x → 编码器网络 → 输出 μ 和 log_var → 采样 z = μ + exp(log_var/2) * ε → 解码器网络 → 输出重建数据 x‘。
在这个流程中,μ 和 log_var 是网络通过梯度下降学习得到的参数。ε 是从外部引入的随机噪声,用于提供随机性。整个采样操作(重参数化技巧)使得梯度可以穿过随机采样层,顺利地从解码器传回编码器。

总结与下节预告

本节课中,我们一起学习了对数方差技巧。我们了解到,通过让变分自编码器的编码器输出对数方差 log_var,而不是方差本身,可以提升模型训练的数值稳定性和效率。采样过程则通过公式 z = μ + exp(log_var / 2) ⊙ ε 来实现。

下一节,我们将探讨变分自编码器的损失函数,它由两部分组成:重建损失和KL散度损失。理解损失函数后,我们将结合本节的“对数方差技巧”,进入第一个代码示例,亲眼看看这些概念是如何在PyTorch中实现的。

P142:L17.4- 变分自编码器损失函数 🧠
在本节课中,我们将学习变分自编码器(VAE)损失函数的构成与原理。我们将重点解析其两个核心部分:重构损失和KL散度项,并讨论实践中如何选择损失函数。
概述
变分自编码器的总体目标是最小化证据下界。其损失函数主要由两部分构成:数据的负对数似然期望(重构损失)和KL散度项。后者用于衡量潜在空间分布与标准多元高斯分布之间的差异。
上一节我们介绍了VAE的架构,本节中我们来看看其核心的优化目标——损失函数。
损失函数构成
变分自编码器的损失函数公式如下:
L = E[ -log p(x|z) ] + D_KL( q(z|x) || p(z) )
其中:
- E[ -log p(x|z) ] 是重构损失项。
- D_KL( q(z|x) || p(z) ) 是KL散度项,确保潜在空间分布
q(z|x)接近先验分布p(z)(标准正态分布)。


最大化数据的对数似然等价于最小化其负值,因此公式中带有负号。
重构损失的选择
在实践中,对于重构损失,常见的选择是二元交叉熵或均方误差。
以下是两种选择的对比分析:
-
二元交叉熵:当输入图像像素值被归一化到
[0, 1]范围,并且解码器输出层使用Sigmoid激活函数时,有些人会使用二元交叉熵。但需注意,图像像素是连续值而非伯努利分布的0或1离散值。使用二元交叉熵可能导致图像重建结果更模糊,且其损失计算不对称,对不同的像素值惩罚不一致。 -
均方误差:对于连续值像素(无论是
[0, 1]还是[-1, 1]范围),使用均方误差通常更合理。它直接衡量原始像素与重建像素之间的欧几里得距离,计算对称且直观。
因此,建议对连续像素值使用均方误差作为重构损失。其计算公式(对批次数据取平均)为:
MSE = (1/N) * Σ (x_i - x'_i)^2
其中 N 是批次中的样本数,求和是对所有像素进行的。



KL散度项
KL散度项用于约束编码器输出的潜在分布。我们假设先验分布 p(z) 是标准正态分布 N(0, I)。
编码器为每个输入 x 输出一个均值向量 μ 和一个对数方差向量 log(σ^2)。KL散度的具体计算公式如下:
D_KL = -0.5 * Σ (1 + log(σ^2) - μ^2 - σ^2)
其中求和是对潜在空间的所有维度进行的。

该公式的推导过程涉及一些数学变换,但核心思想很直观:最小化此项会迫使均值向量 μ 接近0,方差 σ^2 接近1。当 μ = 0 且 σ^2 = 1 时,KL散度达到最小值0,此时潜在分布 q(z|x) 就成为了标准正态分布。

损失函数推导(可选)
如果你对KL散度项的具体推导过程感兴趣,可以参考相关资料。其核心是利用了高斯分布的KL散度解析式以及对数方差的重参数化技巧。

总结
本节课中我们一起学习了变分自编码器的损失函数。
- 总损失由重构损失和KL散度损失两部分组成。
- 对于图像等连续数据,均方误差比二元交叉熵更适合作为重构损失。
- KL散度项的作用是正则化潜在空间,使其分布接近标准正态分布,这是VAE能够进行平滑插值和生成新样本的关键。

在接下来的课程中,我们将通过代码示例,将重参数化技巧、对数方差技巧以及本课介绍的损失函数结合起来,实际训练一个能够生成手写数字和人脸图像的变分自编码器。
P143:L17.5 - PyTorch 中手写数字的变分自动编码器 🧠


在本节课中,我们将要学习如何使用PyTorch为MNIST手写数字数据集构建一个变分自动编码器。我们将从最简单的卷积结构开始,逐步理解VAE的核心组件,包括编码器、重参数化技巧以及损失函数的计算。




概述与准备工作




我们将从MNIST数据集开始构建一个卷积变分自动编码器。其结构与之前课程中的普通自动编码器类似,但关键区别在于我们使用均值向量和方差对数向量,并在损失函数中加入了KL散度项。





首先,我们导入必要的库并设置一些超参数。





import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms




# 超参数设置
batch_size = 256
learning_rate = 0.0005
epochs = 50
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

接下来,我们加载MNIST数据集。由于变分自动编码器是无监督模型,我们只需要图像数据,不需要标签。
# 数据加载
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)







构建VAE模型 🏗️



上一节我们介绍了数据准备,本节中我们来看看模型的核心结构。我们的VAE模型包含一个编码器和一个解码器。



以下是模型的定义,我们使用卷积层进行编码,并使用转置卷积层进行解码。


class ConvVAE(nn.Module):
def __init__(self, latent_dim=2):
super(ConvVAE, self).__init__()
self.latent_dim = latent_dim
# 编码器
self.encoder = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1), # 输出: 14x14x32
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # 输出: 7x7x64
nn.ReLU(),
nn.Flatten(),
nn.Linear(7*7*64, 128),
nn.ReLU()
)
# 均值向量层
self.fc_mu = nn.Linear(128, latent_dim)
# 方差对数向量层
self.fc_logvar = nn.Linear(128, latent_dim)
# 解码器
self.decoder_input = nn.Linear(latent_dim, 7*7*64)
self.decoder = nn.Sequential(
nn.Unflatten(1, (64, 7, 7)),
nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1), # 输出: 14x14x32
nn.ReLU(),
nn.ConvTranspose2d(32, 1, kernel_size=3, stride=2, padding=1, output_padding=1), # 输出: 28x28x1
nn.Sigmoid() # 将输出值压缩到 [0, 1] 范围
)
def encode(self, x):
h = self.encoder(x)
mu = self.fc_mu(h)
logvar = self.fc_logvar(h)
return mu, logvar
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def decode(self, z):
h = self.decoder_input(z)
reconstruction = self.decoder(h)
return reconstruction
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparameterize(mu, logvar)
reconstruction = self.decode(z)
return reconstruction, mu, logvar, z






核心概念解释:
- 编码器 (
encode):将输入图像x映射为潜在空间的两个向量:均值mu和方差对数logvar。 - 重参数化 (
reparameterize):这是VAE的关键。我们从标准正态分布中采样噪声epsilon,然后通过公式z = mu + epsilon * exp(0.5 * logvar)得到潜在变量z。这使得梯度可以通过mu和logvar反向传播。 - 解码器 (
decode):将潜在变量z映射回原始图像空间,生成重建图像。







定义损失函数与训练循环 ⚙️
模型构建完成后,我们需要定义训练过程。VAE的损失函数由两部分组成:重建损失和KL散度。

以下是损失函数的计算和训练循环的代码。




def loss_function(recon_x, x, mu, logvar):
# 重建损失 (MSE)
reconstruction_loss = nn.functional.mse_loss(recon_x, x, reduction='sum')
# KL散度损失
kl_divergence = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
# 总损失
total_loss = reconstruction_loss + kl_divergence
return total_loss, reconstruction_loss, kl_divergence




# 初始化模型、优化器
model = ConvVAE(latent_dim=2).to(device)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)



# 训练循环
for epoch in range(epochs):
model.train()
train_loss = 0
for batch_idx, (data, _) in enumerate(train_loader):
data = data.to(device)
optimizer.zero_grad()
recon_batch, mu, logvar, _ = model(data)
loss, recon_loss, kl_loss = loss_function(recon_batch, data, mu, logvar)
loss.backward()
train_loss += loss.item()
optimizer.step()
print(f'Epoch {epoch+1}, Loss: {train_loss / len(train_loader.dataset):.4f}')





损失函数详解:
总损失公式为:
总损失 = 重建损失 + KL散度
其中:
- 重建损失 衡量解码器输出与原始输入的差异,这里使用均方误差(MSE)。
- KL散度 促使编码器产生的潜在分布
q(z|x)接近标准正态分布p(z)。其计算公式为:
KL = -0.5 * sum(1 + logvar - mu^2 - exp(logvar))







结果可视化与采样 🎨



训练完成后,我们可以查看模型的重建效果、潜在空间分布,并从中采样生成新图像。





以下是进行可视化的代码示例。





import matplotlib.pyplot as plt
import numpy as np

# 1. 查看原始图像与重建图像
model.eval()
with torch.no_grad():
data, _ = next(iter(train_loader))
data = data[:8].to(device)
recon, _, _, _ = model(data)




# 绘制对比图
fig, axes = plt.subplots(2, 8, figsize=(16, 4))
for i in range(8):
axes[0, i].imshow(data[i].cpu().squeeze(), cmap='gray')
axes[0, i].axis('off')
axes[1, i].imshow(recon[i].cpu().squeeze(), cmap='gray')
axes[1, i].axis('off')
axes[0, 0].set_ylabel('Original')
axes[1, 0].set_ylabel('Reconstructed')
plt.show()




# 2. 可视化潜在空间
def plot_latent_space(model, data_loader, device):
model.eval()
latents = []
labels = []
with torch.no_grad():
for data, target in data_loader:
data = data.to(device)
mu, logvar = model.encode(data)
z = model.reparameterize(mu, logvar)
latents.append(z.cpu())
labels.append(target)
latents = torch.cat(latents, dim=0).numpy()
labels = torch.cat(labels, dim=0).numpy()
plt.figure(figsize=(8, 6))
scatter = plt.scatter(latents[:, 0], latents[:, 1], c=labels, cmap='tab10', alpha=0.6, s=2)
plt.colorbar(scatter)
plt.xlabel('Latent Dim 1')
plt.ylabel('Latent Dim 2')
plt.title('VAE Latent Space')
plt.show()





plot_latent_space(model, train_loader, device)



# 3. 从潜在空间采样生成新图像
def sample_from_vae(model, num_samples=10, latent_dim=2):
model.eval()
with torch.no_grad():
# 从标准正态分布采样
z = torch.randn(num_samples, latent_dim).to(device)
samples = model.decode(z).cpu()
return samples

samples = sample_from_vae(model, num_samples=10)
# 绘制生成的图像
fig, axes = plt.subplots(1, 10, figsize=(15, 2))
for i in range(10):
axes[i].imshow(samples[i].squeeze(), cmap='gray')
axes[i].axis('off')
plt.suptitle('Images Sampled from VAE')
plt.show()


结果分析:
- 重建图像:底部行是模型重建的图像,虽然有些模糊且偶尔会混淆数字(如4和9),但整体上保留了数字的主要特征。
- 潜在空间:所有数据点的潜在编码
z大致以原点为中心分布,呈现出类似高斯混合的形态,这符合VAE的训练目标。 - 图像生成:通过从标准正态分布中随机采样
z并输入解码器,我们可以生成新的手写数字图像。虽然部分图像质量不高(尤其是当潜在维度仅为2时),但许多生成结果看起来是合理的。










总结





本节课中我们一起学习了如何在PyTorch中实现一个用于MNIST手写数字的变分自动编码器。






我们回顾一下核心步骤:
- 模型结构:构建了包含编码器(输出
mu和logvar)、重参数化层和解码器的卷积VAE。 - 重参数化技巧:使用公式
z = mu + epsilon * exp(0.5 * logvar)从分布中采样,使模型可训练。 - 损失函数:定义了结合重建损失(MSE)和KL散度的总损失函数,以同时优化重建能力和潜在空间的正则化。
- 训练与评估:完成了模型的训练循环,并可视化分析了重建效果、潜在空间分布以及采样生成的新图像。



由于本例中潜在空间维度设置得很小(仅为2),生成图像的质量和多样性受到限制。在实践中,增大潜在空间的维度通常能获得更清晰、更多样的生成结果。这个简单的VAE为实现更复杂的生成模型(如用于人脸生成)奠定了重要的基础。
🧠 P144:L17.6 - PyTorch 中人脸图像的变分自动编码器
在本节课中,我们将学习如何使用 PyTorch 构建一个针对人脸图像的变分自动编码器。我们将使用 CelebA 数据集,并构建一个比之前 MNIST 示例更复杂的模型,其中包含更大的潜在空间、批归一化和 Dropout 等技术。


📊 数据集介绍
上一节我们介绍了 MNIST 手写数字的变分自动编码器。本节中,我们将使用更复杂的数据集——CelebA 人脸数据集。


CelebA 是一个大规模人脸属性数据集,包含超过 20 万张名人面部图像,每张图像标注了 5 个关键点位置和 40 个二元属性。图像通常从网络爬取,多为名人照片。

以下是关于数据集的几个关键点:
- 数据集可通过 Google Drive 或百度网盘链接获取。
- 原始图像尺寸约为 200x240 像素。
- 我们将对图像进行中心裁剪,以聚焦于面部区域,去除背景。


在数据预处理阶段,我们将像素值归一化到 [0, 1] 范围。


以下是 CelebA 数据集中包含的 40 个二元属性示例,例如“微笑”、“戴眼镜”等。虽然变分自动编码器是无监督模型,不直接使用这些标签,但它们为后续分析提供了参考。


🏗️ 模型架构
现在,让我们来看看针对人脸图像设计的变分自动编码器模型架构。其核心思想与 MNIST 示例相同,但规模更大、更复杂。

主要区别如下:
- 输入:图像具有三个颜色通道(RGB)。
- 潜在表示:维度为 200,远高于 MNIST 示例的 2 维,以编码更复杂的人脸信息。
- 网络结构:更深、更宽,并引入了批归一化和 Dropout 层以提高性能和泛化能力。


模型同样由编码器和解码器两部分构成。编码器将输入图像映射为潜在空间中的均值(μ)和对数方差(log σ²),解码器则从潜在变量 z 重建图像。

核心采样公式:
z = μ + σ * ε,其中 ε ~ N(0, I)



损失函数依然是重构损失和 KL 散度的组合。


🔧 代码实现与训练
以下是模型实现和训练过程的关键步骤。

首先,我们定义数据加载器,应用中心裁剪和归一化变换。


接着,我们定义变分自动编码器类。编码器部分由多个卷积层、批归一化层和 Dropout 层组成,最终输出 200 维的 μ 和 log σ²。解码器部分则通过转置卷积或上采样操作,将潜在变量 z 重建为原始图像尺寸。

训练循环代码框架:
for epoch in range(num_epochs):
for batch_idx, (data, _) in enumerate(train_loader):
# 前向传播,计算 mu, log_var, 重建图像
recon_batch, mu, log_var = model(data)
# 计算损失(重构损失 + KL散度)
loss = loss_function(recon_batch, data, mu, log_var)
# 反向传播与优化
optimizer.zero_grad()
loss.backward()
optimizer.step()

训练过程耗时较长(在 GPU 上约 1.5 小时)。损失曲线显示模型已基本收敛。
训练完成后,我们保存模型以供后续分析使用。
📈 结果分析


训练结束后,我们来评估模型的重建与生成能力。

首先查看重建效果。下图展示了原始图像(上排)与模型重建图像(下排)的对比。重建图像虽然不如原始图像锐利,但已能较好地捕捉人脸的主要特征。


接下来,我们从标准正态分布 N(0, I) 中随机采样潜在变量 z,并通过解码器生成新的人脸图像。部分生成图像看起来合理,部分则存在瑕疵。生成质量受模型架构和训练调参的影响。
我们还对比了两种上采样方式:转置卷积 与 最近邻上采样+卷积。在本例中,两种方法的结果质量相似,未观察到明显的棋盘伪影问题。在实践中,两种方法均可使用。
🧮 潜在空间分析
由于潜在空间是 200 维的,我们无法像二维那样直接可视化散点图。因此,我们采用直方图来分析潜在空间的分布。


我们加载训练好的模型,对 10000 张测试图像进行编码,得到它们的潜在表示。然后,我们绘制潜在空间前 25 个维度的值分布直方图。
分析显示,每个潜在维度的值大致服从均值为 0、方差为 1 的标准正态分布。这表明模型成功地将数据分布约束在了我们期望的先验分布上。
🎯 总结

本节课中,我们一起学习了如何为复杂的 CelebA 人脸数据集构建和训练一个变分自动编码器。


我们了解了如何处理更大规模、更多通道的图像数据,如何在模型中引入批归一化和 Dropout 来提升性能,以及如何分析高维潜在空间的分布特性。


关键点在于,变分自动编码器的核心原理保持不变,但通过调整模型容量和引入正则化技术,可以将其有效地应用于更复杂的真实世界数据。
🎭 P145:L17.7 - PyTorch 中的 VAE 潜在空间算法
在本节课中,我们将学习如何操作变分自编码器(VAE)的潜在空间,通过简单的向量运算来修改生成图像的属性,例如让人物微笑或戴上眼镜。

📋 概述


上一节我们介绍了VAE的构建与训练。本节中,我们将探索一个有趣的应用:潜在空间算术。我们将利用在CelebA人脸数据集上训练好的VAE模型,通过计算并操作潜在空间中的向量,来修改生成图像的面部属性。


🧠 核心概念:潜在空间算术

其核心思想是,在训练好的VAE的潜在空间中,不同的方向可能对应着不同的语义特征。我们可以通过计算代表特定属性(如“微笑”)的向量,并将其添加到某张图像的潜在编码中,从而修改生成的图像。
核心公式可以表示为:
新潜在向量 = 原始潜在向量 + α * (微笑向量 - 非微笑向量)
其中,α 是一个缩放超参数,控制属性修改的强度。
🔧 实现步骤详解

以下是实现潜在空间属性操作的主要步骤。
第一步:计算属性向量
我们需要分别计算“微笑”和“非微笑”图像在潜在空间中的平均向量。
# 伪代码示意
def compute_average_faces(images, labels, feature_index, encoder):
# 初始化累加器
avg_with_feature = 0
avg_without_feature = 0
count_with = 0
count_without = 0
for batch_images, batch_labels in dataloader:
# 获取当前批次图像的潜在编码
embeddings = encoder(batch_images) if encoder else batch_images
# 根据标签筛选具有特定属性的图像
has_feature_mask = batch_labels[:, feature_index] == 1
embeddings_with_feature = embeddings[has_feature_mask]
embeddings_without_feature = embeddings[~has_feature_mask]
# 分别累加
avg_with_feature += embeddings_with_feature.sum(dim=0)
avg_without_feature += embeddings_without_feature.sum(dim=0)
count_with += len(embeddings_with_feature)
count_without += len(embeddings_without_feature)
# 计算平均值
smile_vector = avg_with_feature / count_with
no_smile_vector = avg_without_feature / count_without
return smile_vector, no_smile_vector
第二步:计算差异向量并应用

得到平均向量后,我们计算它们的差值,这个差值向量就指向了从“非微笑”到“微笑”的方向。
# 计算微笑差异向量
smile_vector, no_smile_vector = compute_average_faces(..., feature_index=31, ...) # 31对应微笑属性
difference_vector = smile_vector - no_smile_vector


# 对单张图像进行操作
original_image = ...
original_latent = encoder(original_image)

# 应用潜在空间算术
alpha = 2.0 # 控制微笑程度
new_latent = original_latent + alpha * difference_vector
# 通过解码器生成新图像
modified_image = decoder(new_latent)


第三步:可视化结果

我们将原始图像、修改后的图像以及不同α值下的效果进行对比展示。
🖼️ 效果对比:原始空间 vs 潜在空间
在深入代码前,理解为何要在潜在空间而非原始像素空间进行操作很重要。
- 原始像素空间操作:直接计算所有微笑/非微笑人像的平均像素图像,然后将差值加到目标图像上。这种方法有时有效,但通常会产生模糊、不自然的结果,因为它混合了不同图像的所有细节。
- 潜在空间操作:VAE的潜在空间是一个结构化的、连续的表示空间。在此空间进行算术运算,能更好地保持图像的身份和自然度,仅改变目标属性,通常能产生更清晰、更可控的效果。


💻 代码实践与结果

现在,让我们加载训练好的VAE模型,并实际运行上述步骤。
首先,我们计算潜在空间中的微笑向量和非微笑向量。
# 加载预训练模型和数据
model = load_pretrained_vae()
dataloader, labels = load_celeba_data()
# 计算潜在空间中的平均向量
smile_vec, no_smile_vec = compute_average_faces(
dataloader, labels, feature_index=31, encoder=model.encoder
)
diff_vec = smile_vec - no_smile_vec


接着,我们选取一张测试图像,通过调整α值来改变其微笑程度。
# 选取一张图像
test_img, _ = next(iter(dataloader))
test_img = test_img[0:1] # 取第一张


# 编码到潜在空间
z_original = model.encoder(test_img)



# 尝试不同的alpha值
alphas = [-3, -1.5, 0, 1.5, 3]
modified_images = []
for alpha in alphas:
z_new = z_original + alpha * diff_vec
img_new = model.decoder(z_new)
modified_images.append(img_new)
# 绘制结果
plot_images([test_img] + modified_images, titles=[‘Original’] + [f‘Alpha={a}’ for a in alphas])

运行上述代码后,你将看到随着α值增大,生成图像中人物的微笑变得更加明显;当α为负值时,微笑则会减弱。


同样的方法可以应用于其他属性,例如“戴眼镜”(对应特定的属性索引)。只需更改feature_index,即可尝试让人物戴上或摘下眼镜。
📝 总结




本节课中我们一起学习了VAE潜在空间算术。我们了解到,通过计算潜在空间中代表特定语义属性的方向向量,并对其进行简单的加减运算,我们可以有目的地、连续地控制生成图像的属性。这种方法比在原始像素空间操作更加强大和有效,展示了学习到的潜在表示的结构化特性。


在下一讲中,我们将探讨另一种重要的生成模型:生成对抗网络(GAN)。
课程 P146:L18.0 - 生成对抗网络简介 🎭
在本节课中,我们将要学习生成对抗网络的基本概念、原理及其实现。生成对抗网络是深度学习领域中一种非常流行的模型,主要用于生成新的数据。尽管如今已有成百上千种不同的GAN变体,但我们将从最原始的GAN模型开始,逐步深入,并最终实现一个深度卷积生成对抗网络。此外,我们还将探讨训练GAN时的一些技巧与策略。
生成对抗网络概览 🧠
上一节我们介绍了本节课的主要内容,本节中我们来看看生成对抗网络的核心思想。生成对抗网络由Ian Goodfellow等人于2014年提出,并在2016年左右开始广泛流行。该模型包含两个神经网络:一个生成器和一个判别器,它们通过对抗过程共同学习。
以下是生成对抗网络的基本工作流程:
- 生成器接收随机噪声作为输入,并尝试生成与真实数据相似的数据。
- 判别器接收真实数据和生成器生成的数据作为输入,并尝试区分它们。
- 两个网络在训练过程中相互竞争:生成器试图生成足以“欺骗”判别器的数据,而判别器则努力提升自己的鉴别能力。
目标函数与损失函数 📉
在了解了GAN的基本框架后,我们来看看其核心的优化目标。原始论文中提出的目标函数是一个极小极大博弈问题。
生成器 ( G ) 和判别器 ( D ) 的损失函数可以表示为:
[
\min_G \max_D V(D, G) = \mathbb{E}{x \sim p(x)}[\log D(x)] + \mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))]
]
其中:
- ( x ) 代表真实数据。
- ( z ) 代表输入生成器的随机噪声。
- ( D(x) ) 是判别器认为 ( x ) 是真实数据的概率。
- ( G(z) ) 是生成器根据噪声 ( z ) 生成的数据。
然而,在实际训练中,这个原始目标函数可能导致梯度消失问题。因此,一个更实用的改进是让生成器最大化 ( \log(D(G(z))) ),而不是最小化 ( \log(1 - D(G(z))) )。这为生成器在训练初期提供了更强的梯度信号。
训练全连接GAN:手写数字生成示例 ✍️
理论部分介绍完毕,现在让我们动手实践。我们将构建一个基于全连接层的基本GAN模型,用于生成MNIST手写数字。
以下是实现的关键步骤:

- 构建生成器:一个将随机噪声映射为28x28像素图像的前馈神经网络。
- 构建判别器:一个接收图像并输出其“真实性”概率的前馈神经网络。
- 交替训练:在一个训练循环中,先更新判别器,再更新生成器。
- 监控损失:观察生成器和判别器的损失变化,确保训练过程稳定。
通过这个简单的例子,你可以直观地看到GAN如何从随机噪声开始,逐渐学会生成逼真的手写数字图像。
训练GAN的技巧与策略 🛠️
正如之前提到的,训练GAN可能非常具有挑战性。两个网络需要精细地平衡,否则训练很容易失败。以下是一些被证明有效的实用技巧:
- 使用卷积层:对于图像数据,使用卷积神经网络作为生成器和判别器的骨干,这通常比全连接网络效果更好。
- 批量归一化:在生成器和判别器中使用批量归一化层,有助于稳定训练。
- 使用LeakyReLU激活函数:在判别器中用LeakyReLU替代普通的ReLU,可以缓解梯度稀疏问题。
- 标签平滑:对真实数据的标签使用略小于1的值(如0.9),可以防止判别器变得过于自信。
- 避免在生成器中使用池化层:使用步幅卷积进行上采样和下采样,而不是池化层。
- 使用Adam优化器:Adam优化器通常比SGD更适合GAN的训练。
即使遵循这些技巧,训练GAN仍然需要耐心和大量的超参数调试。
实现DCGAN:生成人脸图像 👤
掌握了基本技巧后,我们将应用它们来构建一个更强大的模型——深度卷积生成对抗网络,用于生成逼真的人脸图像。DCGAN是GAN发展史上的一个里程碑,它明确了在GAN架构中使用卷积网络的最佳实践。
以下是DCGAN架构的主要特点:

- 判别器使用带步幅的卷积层来逐步减小空间维度并增加通道数。
- 生成器使用转置卷积层将随机噪声向量“上采样”为完整图像。
- 去除全连接层,几乎全部使用卷积层。
- 在生成器和判别器中都使用批量归一化。
训练DCGAN生成高质量人脸图像需要更长时间、更多数据和更仔细的调参,但它展示了GAN在复杂数据生成任务上的巨大潜力。
总结 📚
本节课中,我们一起学习了生成对抗网络的核心概念。我们从GAN的基本思想出发,理解了生成器与判别器之间的对抗训练过程。我们分析了原始的目标函数及其在实际训练中的改进。通过手写数字生成的例子,我们实践了一个简单的全连接GAN。接着,我们探讨了训练GAN时常见的问题与一系列实用的训练技巧。最后,我们介绍了DCGAN架构,并将其应用于生成人脸图像的任务。

生成对抗网络是一个强大而活跃的研究领域,虽然训练过程可能充满挑战,但它为数据生成、图像编辑、风格迁移等任务开辟了广阔的可能性。希望本教程为你开启了探索GAN世界的大门。

课程 P147:L18.1 - GAN 背后的主要思想 🎨
在本节课中,我们将要学习生成对抗网络背后的核心思想。GAN 通过让两个神经网络相互竞争来生成新的数据。
概述
生成对抗网络之所以得名,是因为我们确实用它来生成新数据。它并非传统统计学意义上对特征和标签的联合分布或数据生成分布进行显式建模,而更侧重于生成新数据这一事实,尽管它也在隐式地学习一个分布。
传统上,GAN 主要用于生成新图像,这仍然是其最大的应用领域。当然,这个概念更广泛,也可以应用于其他类型的数据,例如文本或分子结构图。不过,对于文本数据,循环神经网络或 Transformer 等自回归模型可能更合适。
GAN 的核心机制
上一节我们介绍了 GAN 的基本目标,本节中我们来看看它是如何运作的。
GAN 在幕后隐式地学习训练集的分布,然后模仿这个分布,使我们能够生成本质上来自该分布的新数据,即创造出前所未见的新图像或新数据。这与变分自编码器的解码器部分功能相似:当我们采样一个随机噪声向量并通过解码器时,它也在生成新数据。GAN 的生成器同样可以从噪声向量生成新数据,但其工作原理有所不同。

在 GAN 的框架中,我们没有显式地对分布建模,而是设置了一个生成器和一个判别器。判别器就像一个裁判,它隐式地“迫使”生成器去建模训练集的分布。这一点在接下来的图示中会更加清晰。
与变分自编码器类似,但 GAN 是一次性生成整个输出,这不同于自回归模型或循环神经网络等逐词/逐字符生成的模型。
GAN 的结构与对抗过程
以下是 GAN 结构的一个示意图(尽管图中显示的是深度卷积 GAN,但基本概念同样适用于常规 GAN)。核心概念不在于这些卷积层或全连接层,而在于判别器和生成器这两部分。
我们有两个主要部分:
- 生成器:接收一个噪声向量作为输入,经过一些神经网络层,最终生成一张图像。这类似于变分自编码器中解码器的工作。
- 判别器:它的工作是判断一张图像是真实的还是生成的。它接收两种输入:来自训练集的真实图像和来自生成器的生成图像。

接下来,我们将分步骤详细讲解训练过程。
步骤一:训练判别器
首先,我们聚焦于训练判别器。这本质上是一个二分类任务。


步骤 1.1:用真实图像训练判别器
我们取用训练集中的真实图像,并为其赋予标签“1”(代表真实图像)。我们训练判别器,使其对于真实图像,输出概率尽可能接近 1。这相当于最大化该样本的预测概率。

步骤 1.2:用生成图像训练判别器
接着,我们使用生成器产生的图像(由随机噪声向量,例如来自标准正态分布,通过生成器得到)来训练判别器。我们为这些生成图像赋予标签“0”(代表生成图像)。我们训练判别器,使其对于生成图像,输出概率尽可能接近 0。
请注意:在此步骤中,我们只更新判别器的参数,生成器的参数保持不变。
步骤二:训练生成器



在训练生成器时,我们的目标与之前相反:我们希望生成器能“欺骗”判别器。

我们固定判别器的参数,只更新生成器的参数。此时,我们希望判别器对生成器新产生的图像做出错误判断,即输出一个高的概率值(接近1),认为它是真实图像。因此,我们训练生成器以最大化判别器将生成图像误判为真实图像的概率。
这体现了“对抗”的含义:判别器努力更好地区分真假,而生成器则努力生成更逼真的图像来欺骗判别器。
对抗循环

完成步骤二后,我们会回到步骤一,再次训练判别器以更好地识别真假图像,然后再训练生成器去欺骗更新后的判别器。如此循环往复,形成一个对抗游戏。



实际上,通过这个持续的对抗过程,判别器学会了更好地区分真实图像和生成图像,而生成器则学会了生成更逼真、更接近训练集分布的图像来迷惑判别器。如果训练足够充分,最终生成器将能够生成与训练集图像极其相似、甚至以假乱真的新图像。
正如你在先前的示例中可能看到的,许多逼真的人脸图像正是由 GAN 通过这种对抗游戏生成的。


总结



本节课中我们一起学习了生成对抗网络的核心思想。GAN 通过设置生成器和判别器这两个相互对抗的神经网络,在动态竞争中使生成器学会模仿真实数据分布,从而创造出高质量的新数据。判别器的任务是准确分类,而生成器的任务是成功欺骗,这种对抗机制是 GAN 成功的关键。在下一讲中,我们将深入探讨 GAN 损失函数的具体形式。

课程 P148:L18.2 - GAN 目标 🎯
在本节课中,我们将学习生成对抗网络(GAN)的核心目标与损失函数。我们将详细拆解GAN的优化过程,理解判别器和生成器如何通过“对抗”达到平衡,并最终生成逼真的数据。
GAN 概述与收敛问题

上一节我们介绍了GAN的基本架构。本节中,我们来看看GAN的优化目标,即损失函数的具体形式。
在深入损失函数之前,我们先思考一个重要问题:GAN何时收敛?答案可能并不直观。判别器被训练来区分真实图像和生成图像,它希望更准确地检测出生成图像。与此同时,生成器希望更好地“欺骗”判别器。两者之间存在着一种微妙的博弈关系。
如果训练得当,整个系统最终会达到一个平衡点。在博弈论中,这被称为纳什均衡。在这个平衡点上,判别器和生成器都处于一个相对满意的状态。

GAN 的损失函数:极小极大博弈
GAN的整体损失函数如下所示,它本质上是一个极小极大博弈:
公式中有两个主要部分:
- 第一部分 \(\mathbb{E}_{x \sim p_{data}(x)}[\log D(x)]\) 涉及判别器对真实图像的预测。
- 第二部分 \(\mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))]\) 涉及判别器对生成图像的预测。
其中,\(G\) 代表生成器,\(D\) 代表判别器。\(x\) 是真实图像,\(z\) 是噪声输入,\(G(z)\) 是生成的图像 \(x‘\)。\(D(\cdot)\) 的输出是一个介于0和1之间的概率值,表示输入图像为“真实”的概率。
接下来,我们将分步解析这个公式,先看判别器的部分,再看生成器的部分。
判别器的目标:最大化
现在,我们聚焦于损失函数中与判别器 \(D\) 相关的部分,即 \(\max_D\) 的部分。由于这是一个最大化问题,我们使用梯度上升法来更新判别器的参数 \(W_D\)。
在实践中,我们无法获得真实的数据分布 \(p_{data}(x)\),只能使用包含 \(N\) 个样本的训练集。因此,期望 \(\mathbb{E}\) 被替换为对训练样本的求和,形成经验损失。
以下是判别器损失相对于其参数的梯度:
为了清晰,我们分两项来理解这个梯度。
第一项:对真实图像的判别
我们首先关注公式中的第一项:\(\log D(x)\)。
判别器 \(D(x)\) 为真实图像输出一个介于0和1之间的分数,代表该图像是真实的概率。判别器的目标是最大化这个对数概率。

- 最佳情况:\(D(x) = 1\),则 \(\log(1) = 0\)。
- 最坏情况:\(D(x) = 0\),则 \(\log(0) = -\infty\)。
因此,在训练判别器时,我们希望它对所有真实图像都输出接近1的概率。
第二项:对生成图像的判别
现在,我们关注公式中的第二项:\(\log(1 - D(G(z)))\)。
这里,\(D(G(z))\) 是判别器对生成图像 \(x‘ = G(z)\) 是真实图像的预测概率。判别器的目标同样是最大化整个项。
- 最佳情况:如果判别器确信生成图像是假的,则 \(D(G(z)) = 0\)。那么 \(\log(1-0) = \log(1) = 0\)。
- 最坏情况:如果判别器错误地认为生成图像是真的,则 \(D(G(z)) = 1\)。那么 \(\log(1-1) = \log(0) = -\infty\)。
因此,在训练判别器时,我们希望它对所有生成图像都输出接近0的概率。
总结判别器的目标:在训练判别器时,我们希望它能够完美地区分真假图像,即对真实图像输出概率1,对生成图像输出概率0。
生成器的目标:最小化

完成判别器的分析后,我们现在聚焦于生成器 \(G\) 的更新。这是一个最小化问题,因此我们使用梯度下降法。
在更新生成器时,我们固定判别器的参数。生成器的损失只与公式中的第二项有关,因为第一项不包含 \(G\)。
生成器的目标是最小化以下损失:
回顾一下,\(D(G(z))\) 是判别器认为生成图像是真实的概率。
- 在判别器部分,我们希望最大化 \(\log(1 - D(G(z)))\),因此希望 \(D(G(z))\) 尽可能小(接近0)。
- 然而,在生成器部分,我们的目标变成了最小化 \(\log(1 - D(G(z)))\)。
那么,如何最小化这个项呢?
- 最坏情况(对生成器而言):如果 \(D(G(z)) = 0\),则 \(\log(1-0) = 0\)。损失值较大(因为我们要最小化,0不够小)。
- 最佳情况(对生成器而言):如果 \(D(G(z)) = 1\),则 \(\log(1-1) = \log(0) = -\infty\)。这是可能达到的最小损失值。
总结生成器的目标:生成器希望“欺骗”判别器,让判别器对生成的假图像输出高的“真实”概率(接近1)。这样,生成器的损失就会变得非常小。
GAN 的训练算法 📜

以下是原始GAN论文中提出的训练过程,称为“小批量随机梯度下降训练生成对抗网络”(实际上结合了梯度上升和下降)。
以下是算法的步骤分解:
外层循环:设定训练迭代次数或轮数。

内层交替训练:
- 判别器训练(循环k步,通常k=1):
- 从噪声分布 \(p_z(z)\) 中采样一个小批量的 \(m\) 个噪声样本 \(\{z^{(1)}, ..., z^{(m)}\}\)。
- 从真实数据分布 \(p_{data}(x)\)(即训练集)中采样一个小批量的 \(m\) 个样本 \(\{x^{(1)}, ..., x^{(m)}\}\)。
- 通过梯度上升更新判别器,以最大化其损失。
- 生成器训练:
- 从噪声分布 \(p_z(z)\) 中采样一个小批量的 \(m\) 个噪声样本 \(\{z^{(1)}, ..., z^{(m)}\}\)。
- 通过梯度下降更新生成器,以最小化其损失。
论文中建议使用带动量的随机梯度下降进行优化。在实践中,Adam优化器也常被使用,但有时人们会避免使用动量,以保持模型在对抗过程中的灵活性。
GAN 的收敛状态 ⚖️
如前所述,当GAN达到纳什均衡时,即告收敛。在博弈论中,这意味着任一玩家的策略不会因为对手的策略改变而改变。
具体到GAN:
- 生成器能够产生足以乱真的图像,其数据分布 \(p_g(x)\) 与真实数据分布 \(p_{data}(x)\) 几乎相同。
- 判别器无法区分两者,对于任何输入图像,它输出“真实”的概率都接近0.5(即随机猜测)。
此时,系统达到稳定,两者都无法再通过单独改变自身策略而获益。
GAN 训练过程图解 🖼️
原始论文中的一张图清晰地展示了GAN的训练动态过程。图中包含四个子图 (a, b, c, d),描绘了从开始到收敛的步骤。
图例说明:
- 黑色虚线:真实数据分布 \(p_{data}(x)\)。
- 绿色实线:生成器分布 \(p_g(x)\),即生成数据的分布。
- 蓝色虚线:判别器输出 \(D(x)\),即判别器认为输入 \(x\) 来自真实分布的概率。
- 水平轴 (\(x\)):数据空间(此处简化为一维)。
- 垂直轴:概率密度(对黑、绿线)或概率值(对蓝线)。
训练过程解析:
- 图 (a):训练初期。生成器分布 \(p_g\) 与真实分布 \(p_{data}\) 差异较大。判别器 \(D\) 在某些区域能很自信地做出正确判断(接近1或0)。
- 图 (b):更新判别器。在固定生成器的情况下,通过梯度上升训练判别器。判别器变得更加强大,其决策边界(蓝色曲线)变得更加尖锐和准确,能更好地区分当前生成的数据和真实数据。
- 图 (c):更新生成器。在固定判别器的情况下,通过梯度下降训练生成器。生成器学习调整其分布 \(p_g\),使其向真实分布 \(p_{data}\) 移动,以增大判别器犯错的概率(即让 \(D(G(z))\) 变大)。
- 图 (d):经过多次交替训练后达到平衡。生成器分布 \(p_g\) 完美匹配了真实分布 \(p_{data}\)。此时,判别器对于任何输入都只能输出0.5,因为它完全无法区分数据来源。
这个图示在概念上阐明了GAN的对抗训练思想,但在实际应用中,训练过程往往更加复杂和不稳定,达到完美的图(d)状态非常困难。
总结 🎓
本节课中,我们一起学习了生成对抗网络(GAN)的核心目标与训练机制。

我们首先探讨了GAN的收敛条件——纳什均衡。然后,深入剖析了GAN的损失函数,它构成了一个极小极大博弈:判别器试图最大化其区分真假的能力,而生成器试图最小化其被判别器识破的可能性。我们分别详细讲解了判别器(使用梯度上升)和生成器(使用梯度下降)的优化目标。
接着,我们介绍了GAN的标准训练算法,即交替更新判别器和生成器。最后,我们通过理论分析和图示理解了GAN的理想收敛状态,即生成器完美复现真实数据分布,而判别器失去判别能力。

在下一节课中,我们将介绍一个对原始损失函数的小修改,以便能统一使用梯度下降来训练两者,并探讨如何改善生成器训练时的梯度信号。

🧠 P149:L18.3 - 为实际使用修改 GAN 损失函数
在本节课中,我们将学习如何对生成对抗网络的损失函数进行一项关键修改,以解决生成器在训练初期可能遇到的梯度消失问题,从而提升训练效率和稳定性。
🔍 GAN 训练中的常见问题

在深入探讨解决方案之前,我们先来回顾一下传统GAN训练中可能遇到的一些典型问题。
以下是GAN训练中常见的问题列表:
- 生成器与判别器之间的持续振荡:模型可能无法收敛,而是在一个不稳定的状态中来回摆动。
- 模式崩溃:生成器可能只学会生成一种能有效欺骗判别器的特定类型数据,导致生成的样本多样性极低。
- 判别器过强:由于分类任务通常比生成数据更容易,判别器可能变得过于强大。在训练初期,如果判别器过强,生成器从损失函数中获得的梯度信号会非常微弱,导致生成器无法有效学习。
- 判别器过弱:判别器太容易被生成器欺骗,导致生成器无法学习生成高质量的逼真数据。
在实践中,判别器过强是一个更常见的问题。接下来,我们将重点讨论如何通过修改损失函数来缓解这个问题。
🛠️ 解决判别器过强的问题
上一节我们介绍了判别器过强会导致生成器梯度消失的问题。本节中我们来看看一个具体的修改方法,以增强生成器在训练初期的学习信号。
我们首先回顾原始GAN论文中生成器的目标。生成器的目标是让判别器对其生成的假图像输出接近1的概率(即判别为“真”)。其损失函数通常表示为:
原始生成器损失: L_G = log(1 - D(G(z)))
其中,D(G(z)) 是判别器对生成图像给出的“真”的概率。

我们希望最小化这个损失。当 D(G(z)) 接近0时(判别器能轻易识破假图像),损失 log(1-0) = 0,这看起来没问题。但问题在于其梯度。
让我们计算这个损失关于 D(G(z)) 的梯度(为简化,令 y_hat = D(G(z))):
梯度计算: d(L_G) / d(y_hat) = -1 / (1 - y_hat)
在训练初期,生成器能力弱,y_hat 通常接近0。此时梯度约为 -1。这个梯度信号相对较小,导致生成器学习缓慢。
为了解决这个问题,我们可以将生成器的目标从 最小化 log(1 - D(G(z))) 改为 最大化 log(D(G(z)))。
修改后的生成器损失: L_G_modified = -log(D(G(z)))

现在,当 D(G(z)) 接近0时,损失 -log(0) 趋近于无穷大,梯度 d(L_G_modified) / d(y_hat) = -1 / y_hat 会变得非常大(因为分母 y_hat 很小)。这为生成器在初期提供了更强的梯度信号,使其能够更有效地对抗强大的判别器。
🔄 统一为梯度下降框架
前面我们看到了很多关于梯度上升和下降的讨论,可能显得有些复杂。但实际上,在代码实现中,我们可以将所有优化统一到熟悉的梯度下降框架中,并使用标准的二元交叉熵损失。
在原始论文中,判别器的训练被描述为一个梯度上升问题(最大化目标函数)。然而,最大化对数似然等价于最小化负对数似然(即交叉熵损失)。因此,我们可以直接使用PyTorch中的二元交叉熵损失和常规的优化器(如SGD或Adam)来训练判别器。
对于生成器,原始目标是最小化判别器做出正确预测的概率,这同样可以转化为一个交叉熵优化问题。但正如上一节所述,直接优化原始目标会导致梯度微弱。更好的实践是:在计算生成器损失时,将假图像的标签“翻转”为真(即标签1)。
以下是具体步骤:

- 训练判别器:
- 对于真实图像,使用标签1。
- 对于生成器产生的假图像,使用标签0。
- 计算判别器对这两批图像的预测,并使用标准的二元交叉熵损失进行梯度下降优化。这鼓励判别器将真实图像判为1,假图像判为0。

- 训练生成器:
- 保持生成器产生的图像不变。
- 关键修改:将这批假图像对应的目标标签设置为1(而不是0)。
- 再次将这批图像输入判别器,计算预测值。
- 使用标签1和判别器的预测值计算二元交叉熵损失,并进行梯度下降优化。
- 这样做的效果是,生成器的训练目标变成了“让判别器将假图像错误地判定为真图像”,即欺骗判别器。同时,这种“标签翻转”的技巧在实践中能提供更优的梯度。



📝 总结


本节课中我们一起学习了如何为实际训练修改GAN的损失函数。
- 我们首先分析了GAN训练中,特别是判别器过强导致生成器梯度消失的常见问题。
- 接着,我们探讨了通过将生成器目标从
min log(1-D(G(z)))改为max log(D(G(z)))来增强初期训练梯度的理论依据。 - 最后,我们介绍了如何在代码实践中统一使用梯度下降和二元交叉熵损失,并通过翻转生成器训练时的标签这一简单技巧,来实现更稳定、更高效的GAN训练。


在接下来的视频中,我们将通过具体的代码示例来直观地展示这些步骤,你会发现其实现是非常清晰和直接的。
课程 P15:L2.2 - 多层网络 🧠
在本节课中,我们将学习多层神经网络,也称为多层感知机。我们将了解其基本架构、如何解决异或问题,以及训练这类网络的关键算法——反向传播。此外,我们还将简要探讨不同神经网络架构背后的设计思想。
多层感知机架构

现在我们来讨论如何训练多层神经网络。
右侧展示的是一个所谓的多层感知机。它有时也被称为前馈神经网络,并且是一个全连接的神经网络。这里有两个特性描述了其数据流向:数据仅向前单向流动,并且网络是全连接的。这意味着这里的每个单元都与其他单元完全连接。
请注意“感知机”这个词,尽管这类网络与原始的感知机联系并不紧密,因为这里还有另一个关键点未在图中显示:我们使用了非线性激活函数。关于这一点,我们将在后续课程中详细讨论,此处无需记忆。
这种架构使我们能够解决异或问题。即使这里只有一个输出节点,它之所以能解决异或问题,原因在于它拥有多个层以及非线性激活函数。这两个方面使其具备了解决异或问题的能力。

这里我生成了一些玩具数据,并应用了一个真实的多层感知机进行训练。我训练了两个隐藏单元数量略有不同的多层感知机,可以看到,两种情况下神经网络都为这个异或问题找到了解决方案。
在左侧,网络通过学习两条平行的二元决策边界来解决异或问题。我发现这非常有趣,它看起来非常有序且高效。网络解决异或问题的另一种方式如右侧所示。我只是稍微改变了隐藏单元的数量,可以看到解决方案截然不同。当然,使用多层神经网络解决这个问题有无限多种方案。我们将在后续课程中更详细地讨论这一点。
如果你有兴趣为自己的报告或提案绘制神经网络架构图,这里有一个由 Alex(姓氏不详)开发的非常棒的工具。它可以让你快速绘制简单的神经网络图,也包括 LeNet 风格和 AlexNet 风格的卷积网络图,这些我们也会在后续课程中涉及。
训练挑战与反向传播
关于多层神经网络,还有更多内容需要了解。

现在,通过隐藏层和非线性激活函数,我们有了解决异或问题的方法。然而,当这种网络架构被提出时,也带来了一个新问题:训练困难。最初并没有针对它的训练算法。
后来提出的一个解决方案是反向传播。它被独立提出了多次。实际上,有一篇由 Yo Schmtubba 撰写的文章讨论了谁最先发明了它。这里引用了不同的文献资源,你可能会觉得有趣。这是一篇博客文章,列出了反向传播或其相关概念的各种发明者或方式。

然而,真正使其流行并证明其能有效训练多层感知机的论文,是 Ruelhart 和 Hinton 在 1986 年发表的。这是一个独立的表述,意味着他们当时并未知晓之前的文献。正是这篇论文真正使其流行起来,因为他们确实证明了这种方法在实践中是有效的。
后来还证明,像这里展示的、具有一个隐藏层的多层感知机,如果隐藏层规模足够大,就能够近似任意函数。也就是说,如果隐藏层规模相对非常大,它可以以一定的精度近似任何函数。
当然,能够近似任意函数并不意味着容易做到。在某些情况下,训练网络仍然很困难。我们稍后将学习其他更适合特定类型问题的架构。但这仍然是一个重大突破,因为现在有了一个可以高效训练多层神经网络的算法。


剧透一下:这个算法至今仍在常用架构中使用。它仍然是我们训练神经网络最高效、最流行的算法,经受住了时间的考验。
关于反向传播的趣闻
这里有一个关于 Ruelhart 和 Hinton 反向传播算法的趣闻。我是在一本访谈录中读到 Geoff Hinton 谈论他们是如何提出这个算法的。
他说,在 1985 年底,他和 Dave Ruelhart 实际上有一个约定:他会写一篇关于反向传播的短文(这是 Dave 的想法),而 Dave 则会写一篇关于自编码器的短文(这是 Geoff 的想法)。让没有想出这个想法的人来写论文总是更好,因为他能更清楚地说明什么是重要的。于是,Geoff 写了一篇关于反向传播的短文,也就是 1986 年发表在《自然》杂志上的那篇论文。而 Dave 至今还没有写那篇关于自编码器的短文,Geoff 仍在等待。
这段内容来自一本名为《Talking Nets》的书,虽然有点旧,但如果你感兴趣,仍然很有趣。它是一本通过采访上世纪从事神经网络研究的人员来概述神经网络口述历史的书。还有另一本更有趣的书叫《Architects of Intelligence》,大概是 2018 年的,我去年读过,也非常有趣。里面有很多采访,包括 Geoff Hinton 和许多其他著名的深度学习研究者。这是一本非常有趣的读物,特别是他们让每个人都预测人工智能何时能被发明出来,但并没有明确的共识。有些人说上节课提到的通用人工智能永远不会被发明,有些人说需要 100 年等等,读起来确实很有意思。
其他学习算法与架构归纳偏置
反向传播并不是参数化神经网络(如多层感知机)的唯一算法。
还有其他类型的学习算法。这里引用了一篇由 Lily Cr、Sanenttorro、Mirrors、Akaman 和 Hinton 合著的论文,他们讨论了反向传播与大脑,基本上探讨了人类大脑的学习算法与反向传播可能有多相似。当然,我们目前还不知道答案。我们知道大脑可能并不使用我们训练神经网络时所用的反向传播,但它可能使用某种与反向传播有些相似的东西。这里我不想深入讨论神经科学以及它与人类大脑工作方式的相似性,但我觉得有趣的是,这里简要概述了我们可以用于训练神经网络的不同类型的学习算法。
例如,有赫布学习。假设你对更新这里的这个连接感兴趣,赫布学习不涉及反馈。这里的“反馈”指的是,如果你做了一个预测然后查看误差,你通常会更新网络以最小化误差。但如果没有反馈,它并不是真正在最小化误差。在赫布学习中,连接会因其被用于预测的次数越多而变得越强。这并不一定能改善误差。你可以想象一下,比如你在打网球,做了一个错误的动作,但没有人教你那是错的,而你却一遍又一遍地重复这个错误动作。如果你反复重复网球中的错误动作,却没有人教你这是错的,以后要改掉它会更难,但这并不意味着你的网球技术会变好。
另一种类型的学习是扰动学习。你对权重做一个扰动,看看是否变得更好,如果更好就保留,否则就丢弃。这种方法效率也不高,实际上非常慢。
反向传播实际上非常高效,也非常擅长最小化误差。还有另一种类型的学习算法,它与反向传播相似,但不是反向传播误差,而是涉及第二个网络。但根据那篇论文,他们也承认反向传播是目前最小化误差最高效的算法。尽管这可能不是人类大脑使用的方式,但在实践中,当我们开发计算机和预测算法时,我们关心的是低误差和良好的预测等,这就是为什么我们仍然使用反向传播,因为我们还没有找到比它更好的方法。
当然,在本课程中我们只是简要概述。当我们讨论神经网络时,我们将专注于反向传播。
这里有一张很好的图,是我上周末从一篇讨论关系归纳偏置和其他类型偏置的论文中看到的。我喜欢这张图,因为它大致勾勒出了常用的不同类型神经网络架构。
我们有多层神经网络或多层感知机,我在本视频中简要介绍过。这里我们假设数据列是独立的。因此,这是一个可以用于表格数据集的网络,我们在前一周讨论过。例如,回想一下你的鸢尾花数据集,你有最多 150 朵鸢尾花,然后有萼片长度、萼片宽度、花瓣长度和花瓣宽度,以及一个类别标签。这可以作为多层感知机的输入,而这些列的顺序并不重要。当然,一旦你训练了网络,顺序就重要了,因为它是基于这个表格训练的。但在训练之前,比如萼片长度在这里还是在那里,实际上没有区别。即使列的顺序不同,你也可以学习到相同的网络。因此,对于这些特征之间的依赖关系没有假设。
相比之下,所谓的卷积网络(我将在下一个视频中介绍)有一个假设是局部性。还有另一个假设是等变性,但我们现在关注局部性。它基本上假设存在某种结构。这通常用于图像分析,因为你可以认为相邻像素是相互关联的。例如,如果你有一张人物的图像,比如两只眼睛、鼻子、嘴巴,这些像素显然是相关的,因为它们都包含了眼睛。因此,打乱这些像素,把一个像素放到这里,就没有意义了。这样,就有一种局部性假设。因此,在关系归纳偏置方面,你可以认为,当你使用某种架构时,你对数据做出了某种假设。
还有一种叫做循环神经网络。这个假设输入是顺序相关的。因此存在顺序关系、序列关系。因此,循环神经网络对文本很有用,因为文本中的单词有特定的顺序。打乱句子中的单词是没有意义的,因为那样句子就不再具有相同的意义,就像打乱图像中的像素会失去图像之间的关系一样。
多层感知机没有这样的假设。因此,从这个意义上说,它非常强大,你可以学习很多东西。简单提一下,它可以被视为一个通用函数逼近器。然而,当然,如果你没有任何先验假设,学习也会更具挑战性。因此,利用诸如局部性或顺序性这样的先验假设实际上可以使学习更容易。这也是深度学习背后的关键思想之一。在下一个视频中,我将讨论深度学习,以及它是如何从简单的多层网络演变而来的。
最后,这里还有最后一种架构,叫做“指定式”。当然,这不是一种特定的神经网络架构。这里指的是有人手动编码事物之间的关系。我认为这不容易实现,因为你必须手动编码,这非常繁琐。因此,像这样的东西目前在实践中并不常用。实际上,在深度学习中,我们通常对图像使用卷积网络,对文本使用循环神经网络,对表格数据使用多层感知机。
总结


本节课中,我们一起学习了多层神经网络(多层感知机)的基本概念。我们了解到,通过引入隐藏层和非线性激活函数,这种网络能够解决像异或这样的非线性问题。我们探讨了训练这类网络的核心算法——反向传播,它高效且至今仍是主流方法。我们还简要了解了其他学习算法,并认识到不同的神经网络架构(如多层感知机、卷积网络、循环神经网络)背后对应着不同的数据关系假设(归纳偏置),选择合适的架构可以更有效地解决特定类型的问题。下一讲,我们将简要探讨深度学习的起源及其如何从多层网络发展而来。
P150:L18.4 - 在 PyTorch 中实现手写数字生成 GAN 🧠
在本节课中,我们将学习如何使用 PyTorch 实现一个生成对抗网络。我们将构建一个简单的、基于全连接层的 GAN,用于生成手写数字图像。通过代码实践,我们将把上一节讨论的理论概念具体化。
概述与准备工作
上一节我们介绍了 GAN 的基本原理和数学符号。本节中,我们来看看如何在 PyTorch 中具体实现一个 GAN。虽然训练 GAN 具有挑战性,但代码结构本身是清晰直接的。
首先,我们进行常规的导入和数据集准备。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt

我们使用 MNIST 数据集,并将图像像素值归一化到 [-1, 1] 的范围,这通常比 [0, 1] 范围效果更好。GAN 是无监督算法,因此我们只需要训练集图像,不需要标签。


transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)) # 将 [0,1] 映射到 [-1,1]
])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
我们可以使用 torchvision.utils.make_grid 函数快速可视化一批真实图像。
def show_images(images_tensor):
grid = torchvision.utils.make_grid(images_tensor, nrow=8, padding=2, normalize=True)
plt.imshow(grid.permute(1, 2, 0))
plt.axis('off')
plt.show()
# 获取一批数据并显示
dataiter = iter(train_loader)
images, _ = dataiter.next()
show_images(images)
构建生成器与判别器模型
现在进入核心部分:定义生成器 Generator 和判别器 Discriminator 模型。我们将使用 nn.Sequential API 来构建它们。
以下是生成器的定义。它接收一个噪声向量 z,通过一系列全连接层和激活函数,最终输出一个形状为 (C, H, W) 的“假”图像。由于输入图像被归一化到 [-1,1],我们使用 Tanh 作为输出层的激活函数,使生成图像的像素值也在同一范围内。
class Generator(nn.Module):
def __init__(self, latent_dim=100):
super(Generator, self).__init__()
self.main = nn.Sequential(
nn.Linear(latent_dim, 256),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(256, 512),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(512, 1024),
nn.LeakyReLU(0.2),
nn.Linear(1024, 28*28*1), # MNIST 图像尺寸为 28x28, 1个通道
nn.Tanh()
)
self.latent_dim = latent_dim
def forward(self, z):
# 输入 z 的形状: (batch_size, latent_dim)
img = self.main(z)
# 将输出重塑为图像形状: (batch_size, C, H, W)
img = img.view(img.size(0), 1, 28, 28)
return img
接下来是判别器的定义。判别器本质上是一个二分类器,它接收一张图像(无论是真实的还是生成的),输出一个标量 logit,表示该图像为“真”的概率。
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.main = nn.Sequential(
nn.Linear(28*28*1, 1024),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(1024, 512),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 1)
# 注意:没有 Sigmoid,因为损失函数使用 BCEWithLogitsLoss
)
def forward(self, img):
# 输入 img 的形状: (batch_size, C, H, W)
flattened = img.view(img.size(0), -1)
validity = self.main(flattened)
return validity.squeeze() # 将输出从 (batch_size, 1) 变为 (batch_size,)
初始化模型与优化器
模型定义好后,我们需要实例化它们,并为每个模型分别设置优化器。这是 GAN 训练的一个关键点:生成器和判别器有各自独立的优化器,用于在训练的不同阶段更新各自的参数。
# 初始化模型
latent_dim = 100
generator = Generator(latent_dim)
discriminator = Discriminator()
# 设置优化器
lr_g = 0.0002 # 生成器学习率
lr_d = 0.0002 # 判别器学习率
beta1 = 0.5 # Adam 优化器的超参数
optimizer_G = optim.Adam(generator.parameters(), lr=lr_g, betas=(beta1, 0.999))
optimizer_D = optim.Adam(discriminator.parameters(), lr=lr_d, betas=(beta1, 0.999))
# 定义损失函数
criterion = nn.BCEWithLogitsLoss()
请注意,optimizer_G 只包含生成器的参数,optimizer_D 只包含判别器的参数。这确保了在训练生成器时,判别器的权重被“冻结”,反之亦然。
训练循环详解
训练 GAN 的核心在于交替训练判别器和生成器。以下是训练循环中每一步的详细说明。
首先,我们定义一些标签和用于监控训练过程的固定噪声。
# 定义标签
real_label = 1.0
fake_label = 0.0
# 创建固定噪声,用于在每个epoch后生成图像以监控进度
fixed_noise = torch.randn(64, latent_dim)
以下是每个训练批次中的关键步骤:
第一步:训练判别器
目标是让判别器能正确区分真实图像和生成器产生的假图像。
- 清零判别器优化器的梯度。
- 用真实图像和标签
1计算损失。 - 用生成器产生的假图像和标签
0计算损失。这里需要使用.detach()将假图像从生成器的计算图中分离,防止梯度传播到生成器。 - 将两部分损失相加,反向传播并更新判别器的参数。
第二步:训练生成器
目标是让生成器产生的图像能够“欺骗”判别器,使其误判为真。
- 清零生成器优化器的梯度。
- 再次将假图像输入判别器,但这次使用“翻转”的标签
1(即希望判别器将假图判为真)。 - 计算损失。注意,这里不使用
.detach(),因为我们需要梯度来更新生成器。 - 反向传播并更新生成器的参数。
以下是训练循环的代码框架:
num_epochs = 100
for epoch in range(num_epochs):
for i, (real_imgs, _) in enumerate(train_loader):
batch_size = real_imgs.size(0)
# 准备真实图像的标签
real_labels = torch.full((batch_size,), real_label)
# 准备假图像的标签
fake_labels = torch.full((batch_size,), fake_label)
# 准备用于训练生成器的“翻转”假标签
flipped_fake_labels = torch.full((batch_size,), real_label)
# ---------------------
# 训练判别器 (D)
# ---------------------
optimizer_D.zero_grad()
# 计算真实图像的损失
output_real = discriminator(real_imgs)
loss_D_real = criterion(output_real, real_labels)
# 生成假图像
noise = torch.randn(batch_size, latent_dim)
fake_imgs = generator(noise)
# 计算假图像的损失,并分离计算图
output_fake = discriminator(fake_imgs.detach())
loss_D_fake = criterion(output_fake, fake_labels)
# 合并判别器损失并反向传播
loss_D = (loss_D_real + loss_D_fake) / 2
loss_D.backward()
optimizer_D.step()
# ---------------------
# 训练生成器 (G)
# ---------------------
optimizer_G.zero_grad()
# 再次将假图像输入判别器,但这次目标是让判别器输出“真”
output_fake_for_G = discriminator(fake_imgs) # 注意没有 .detach()
loss_G = criterion(output_fake_for_G, flipped_fake_labels)
loss_G.backward()
optimizer_G.step()
# 后续可以添加日志记录和可视化代码...
监控训练过程与结果可视化
训练 GAN 时,仅观察损失值往往不够直观。最有效的方法是定期查看生成器在固定噪声输入下产生的图像序列。
在训练循环中,我们可以在每个 epoch 结束后,用 fixed_noise 生成一批图像并保存。
# 在每个epoch结束后
with torch.no_grad():
fixed_fake_imgs = generator(fixed_noise).detach().cpu()
# 保存或可视化 fixed_fake_imgs...
经过 100 个 epoch 的训练后,我们可以观察生成图像的演变过程。在训练初期(epoch 0),生成器输出的是无意义的噪声。随着训练进行(epoch 5, 10, 20...),图像逐渐呈现出可辨认的数字形状。在训练后期,虽然可能仍存在一些瑕疵,但大部分生成的数字已经相当清晰合理。



总结




本节课中,我们一起学习了如何在 PyTorch 中实现一个基本的生成对抗网络。我们完成了以下工作:

- 准备数据:加载并归一化 MNIST 数据集。
- 构建模型:定义了全连接结构的生成器和判别器。
- 设置训练:为两个模型分别初始化了优化器,并解释了交替训练的策略。
- 实现训练循环:详细编码了判别器和生成器交替更新的步骤,包括梯度清零、损失计算、反向传播和参数更新。
- 监控进度:通过使用固定噪声生成图像序列,直观地展示了 GAN 的学习过程。




这个简单的 GAN 证明了通过对抗训练,神经网络能够学习并生成逼真的手写数字图像。然而,GAN 的训练过程不稳定且需要精心调参。在下一节课中,我们将探讨一些训练 GAN 的实用技巧和策略。
🎓 P151:L18.5 - 让 GAN 发挥作用的技巧和窍门




在本节课中,我们将学习一系列实用的技巧和窍门,以帮助生成对抗网络(GAN)更稳定、更有效地工作。这些技巧基于 Facebook AI 研究员 Soumith Chintala 在 GitHub 上维护的一份清单。虽然该清单已不再更新,但其中大部分建议在今天仍然非常有用。我们将逐一解析这些技巧,并结合上一节课的代码示例,看看它们是如何被应用的。




📝 概述:一份实用的 GAN 技巧清单

这份清单包含了大约 17 条建议,旨在帮助初学者和从业者解决 GAN 训练中常见的难题。接下来,我们将逐条探讨这些技巧,并理解其背后的原理。



🔧 核心技巧详解




上一节我们介绍了这份技巧清单的背景,本节中我们来看看具体的技巧内容。


1. 输入归一化
将图像输入归一化到 [-1, 1] 的范围,并在生成器的最后一层使用 tanh 激活函数。这有助于稳定训练过程。


在上一课的代码中,我们正是这样做的:
# 图像被归一化到 [-1, 1]
# 生成器最后一层使用 tanh 激活函数
output = torch.tanh(self.fc3(x))




2. 修改损失函数
在原始 GAN 论文中,生成器的损失函数是 min log(1 - D(G(z)))。但在实践中,我们通常将其改为 max log(D(G(z))),即最大化判别器对生成图像的“真实”判断概率,这通常能提供更强的梯度。




在代码中,我们通过翻转标签来实现这一点:
# 训练生成器时,将假图像的标签设为“真”(1)
g_loss = criterion(output, labels_real)
3. 使用球形潜在空间
从高斯分布(正态分布)中采样噪声向量 z,而不是从均匀分布中采样。这通常能产生更好的结果。



我们的代码使用了 torch.randn 来生成正态分布的噪声:
z = torch.randn(batch_size, latent_dim)



4. 批归一化的使用
在生成器和判别器中使用批归一化(BatchNorm)可以加速训练并提高稳定性。但需要注意,不要将真实图像和生成图像混合在同一个批次中进行批归一化计算,应分别处理。

在后续的卷积 GAN 示例中,我们使用了批归一化。



5. 避免稀疏梯度:使用 Leaky ReLU
使用 Leaky ReLU 代替普通的 ReLU 作为激活函数,可以避免“神经元死亡”问题,确保梯度能够有效流动。

在我们的判别器和生成器中,都使用了 Leaky ReLU:
nn.LeakyReLU(0.2, inplace=True)

6. 下采样与上采样
对于下采样,推荐使用平均池化(Average Pooling)。对于上采样,可以使用转置卷积(ConvTranspose)或像素重排(Pixel Shuffle)等操作。


7. 使用软标签和噪声标签
为判别器的训练标签引入一些噪声,可以使训练更稳定。
- 对于真实图像,使用
0.7到1.2之间的随机数作为标签。 - 对于生成图像,使用
0.0到0.3之间的随机数作为标签。
偶尔翻转标签(将真标签设为假,假标签设为真)也能防止判别器变得过于强大。


8. 架构选择:DCGAN
尽可能使用深度卷积 GAN(DCGAN)架构。如今,卷积层在 GAN 中已非常普遍,通常直接称为 GAN。





9. 借鉴强化学习的稳定性技巧
保存生成器和判别器的检查点(Checkpoint),以便在训练崩溃时可以回滚到之前的稳定状态。


10. 优化器选择
Adam 优化器通常效果很好。有建议称,可以为判别器使用带动量的 SGD,为生成器使用 Adam,但在实践中,两者都使用 Adam 通常也能取得良好效果。


11. 早期识别失败
- 判别器损失为 0:这是一个危险信号,意味着判别器过于强大,生成器无法学习。
- 检查梯度范数:如果梯度范数过大,可能导致训练不稳定,可以考虑使用梯度惩罚(Gradient Penalty)。
- 观察损失曲线:理想的判别器损失应该具有较低的方差并缓慢下降。如果损失剧烈波动,可能存在问题。
- 生成器损失持续下降:这可能意味着生成器在向判别器“投喂垃圾”,而判别器没有有效学习。


12. 平衡更新次数
不要机械地设定生成器和判别器的更新比例(例如,训练 k 次判别器再训练 1 次生成器)。更好的方法是根据损失值动态调整:如果某一方的损失过大,就多训练它几次直到损失降下来。


13. 利用标签信息
如果你拥有带标签的数据,可以训练一个辅助分类器(Auxiliary Classifier),让判别器同时学习区分真假和分类样本,这通常能提升生成样本的质量。这类模型被称为辅助生成对抗网络(AC-GAN)。
14. 如何评估 GAN?
评估 GAN 的性能是一个挑战。常用的指标包括:
- 初始分数(Inception Score, IS):使用在 ImageNet 上预训练的分类器来评估生成图像的多样性和清晰度。
- 弗雷歇初始距离(Fréchet Inception Distance, FID):计算真实图像和生成图像在特征空间中的分布距离,值越小越好。

15. 向输入添加噪声
在训练过程中,向判别器的输入(真实图像和生成图像)添加少量噪声,并让噪声随时间衰减,这可以作为一种正则化手段。




16. 离散变量与条件 GAN
对于条件生成任务,可以将条件信息(如类别标签)与噪声向量 z 拼接后一起输入生成器,同时输入判别器。这被称为条件 GAN(cGAN),能控制生成样本的特定属性。


17. 在训练和测试时都使用 Dropout
在生成器中,不仅在训练时使用 Dropout,在测试(生成样本)时也使用,这可能有助于提升生成样本的多样性。
🎯 总结与回顾


本节课我们一起学习了 Soumith Chintala 总结的 17 条让 GAN 更有效工作的实用技巧。我们涵盖了从输入预处理、损失函数设计、网络架构、优化器选择到训练监控和评估的各个方面。许多技巧在我们上一课的代码中已经得到体现,例如输入归一化、使用 tanh、Leaky ReLU、修改损失函数以及从高斯分布采样噪声等。




虽然这份清单已不再维护,但其核心思想对 GAN 的初学者和从业者仍有很高的参考价值。理解并尝试应用这些技巧,将帮助你更好地驾驭 GAN 的训练过程,生成更高质量的图像。在接下来的课程中,我们将把这些技巧应用到更复杂的卷积 GAN 模型中。
🎨 P152:L18.6 - 在 PyTorch 中实现生成人脸图像的 DCGAN


在本节课中,我们将学习如何使用 PyTorch 实现一个深度卷积生成对抗网络(DCGAN),用于生成人脸图像。我们将基于之前讨论的 GAN 技巧,将全连接层替换为卷积层,并处理一个真实的人脸图像数据集。


📊 概述与数据集
上一节我们介绍了 GAN 的基本原理和训练技巧。本节中,我们来看看如何将这些知识应用于一个更复杂的任务——生成人脸图像。
我们使用的数据集是 CelebA,这是一个从谷歌图片搜索中收集的名人面部图像数据集。为了简化训练过程,我们将图像裁剪并调整大小为 64x64 像素。使用较小的图像尺寸有助于模型更快地收敛,尤其是在使用常规 GAN 架构时。




以下是数据集中部分图像的示例:



🏗️ DCGAN 模型架构
与之前基于 MNIST 的全连接 GAN 相比,核心区别在于我们使用了卷积层。让我们逐步了解生成器和判别器的结构。
生成器 (Generator)
生成器的目标是将一个 100 维的噪声向量 z 上采样为一张 64x64 的 RGB 图像。其结构主要由转置卷积层(Transposed Convolutional Layers)构成。
以下是生成器的关键层序列:
- 输入:100 维噪声向量。
- 通过多个转置卷积层进行上采样。
- 每层后接批归一化(BatchNorm)和 LeakyReLU 激活函数。
- 最后一层使用
Tanh激活函数,将像素值范围约束在 [-1, 1]。


代码结构示意如下:
self.main = nn.Sequential(
# 将 100 维噪声上采样
nn.ConvTranspose2d(100, 512, 4, 1, 0, bias=False),
nn.BatchNorm2d(512),
nn.ReLU(True),
# ... 更多上采样层 ...
# 输出 64x64x3 图像
nn.ConvTranspose2d(64, 3, 4, 2, 1, bias=False),
nn.Tanh()
)

判别器 (Discriminator)
判别器是一个卷积神经网络,用于判断输入图像是真实的还是生成的。它不使用全连接层,而是通过卷积将特征图尺寸最终降至 1x1。
以下是判别器的设计思路:
- 输入:64x64x3 图像。
- 通过多个卷积层进行下采样(使用步幅为2的卷积代替池化层)。
- 每层后接批归一化和 LeakyReLU。
- 最后一层卷积将通道数变为 1,特征图尺寸变为 1x1。
- 使用
Flatten操作将其变为单个值,并通过 Sigmoid 函数输出为真实图像的概率。
其最后一层的转换可以表示为:
输出 = Sigmoid( Conv( 特征图 ) )
其中,卷积核大小设置为 4,恰好将 4x4 的特征图转换为 1x1。


⚙️ 训练过程与代码实现



训练函数的主体结构与之前的 GAN 类似,但针对图像数据进行了适配。我们使用 Adam 优化器,并设置合适的学习率。
以下是训练循环的核心步骤:
- 使用真实图像训练判别器,最大化其对真实图像的判断准确率。
- 使用生成器产生的假图像训练判别器,最大化其识别假图像的能力。
- 训练生成器,以生成能“欺骗”判别器的图像,即最小化判别器对假图像的判断准确率。

一个值得讨论的细节是噪声向量的采样。在代码中,同一批噪声既用于为判别器生成假图像,又用于训练生成器。理论上,为这两步使用不同的、新鲜的噪声批次可能是一个值得尝试的技巧,或许能带来更稳定的训练,但这需要进一步的实验验证。



📈 训练结果与分析



模型训练了约 20 个周期。观察损失曲线,理想的状况是判别器和生成器的损失动态平衡,而不是某一方迅速降至零。



以下是训练过程中的图像生成效果演变:
- 第 1 周期:生成的图像看起来不像人脸。
![]()
- 第 5-10 周期:开始出现一些人脸的结构,部分图像已具雏形。
![]()
![]()
- 第 20 周期:生成的人脸图像虽然仍可看出是生成的,但整体效果较为合理,不再显得怪异。
![]()



⚠️ 训练失败案例与模式崩溃



在实验过程中,当使用常规 ReLU 激活函数代替 LeakyReLU 时,训练出现了问题。


以下是失败案例的表现:
- 损失曲线出现巨大尖峰。
![]()
- 生成器发生模式崩溃(Mode Collapse),所有生成的图像都高度相似,失去了多样性。
![]()

这证明了之前课程中提到的使用 LeakyReLU 等技巧对于稳定 GAN 训练的重要性。
💎 总结与学习建议
本节课中,我们一起学习了如何使用 PyTorch 实现一个 DCGAN 来生成人脸图像。我们涵盖了从数据处理、模型架构(卷积生成器与判别器)到训练流程和结果分析的完整过程。
尽管代码看起来比简单的分类器复杂,但这是深度学习实践中的常态。掌握 GAN 需要时间、实践和耐心:
- 亲自动手:尝试自己实现代码,或修改现有代码,应用不同的训练技巧。
- 深入调试:通过项目实践,花时间理解代码的每一部分,遇到问题时搜索资料或寻求反馈。
- 保持耐心:熟练阅读和编写复杂的模型代码是一个渐进的过程,需要长期的积累。

通过本课程的学习,你已经掌握了 GAN 的核心概念和实现方法,为今后探索更先进的生成模型打下了坚实的基础。
📚 课程 P153:L19.0 - 用于序列到序列建模的 RNN 和转换器 🧠
在本节课中,我们将学习如何使用循环神经网络和转换器进行序列生成。课程内容为可选,与任何测验或评分无关,你可以根据自己的兴趣和时间安排学习。
学期过得比想象中快得多,我们已经来到了最后一讲。我知道大家正专注于课程项目,我也非常期待观看你们本周和下周的项目展示并阅读报告。
本节课的主题是用于序列生成的循环神经网络和转换器。我们之前介绍过用于分类的循环神经网络,现在我们将重点学习如何用它们生成新数据。接着,我们会引入注意力机制来改进循环神经网络的序列生成过程。最后,我们将抛开循环神经网络,深入探讨构成流行转换器网络基础的注意力机制本身。
📋 课程大纲
以下是今天课程涵盖的六个主要主题:
首先,我们将从循环神经网络序列生成的一般性讨论开始。在早期的课程中,我们讨论了用于分类的循环神经网络。现在我们将学习它们同样可以用于生成序列。
具体来说,我们将了解所谓的字符级循环神经网络。字符级循环神经网络是一种一次输入一个字符(例如字母表中的字母)的循环神经网络,并可以据此生成新的文本。
接着,我们将学习一个称为“注意力”的概念。这个概念最初是为循环神经网络提出的。在每个时间步,循环神经网络可以关注序列的特定部分,这对于处理长序列特别有用。
然后,我们将学习一篇名为“注意力就是你所需要的一切”的论文所提出的概念。这是关于仅使用注意力或自注意力机制,而不使用循环神经网络。研究表明,仅使用注意力机制就能获得非常好的性能,实际上并不需要循环神经网络。这催生了我们将要讨论的所谓转换器模型。
最后,我将展示一个代码示例,说明如何在 PyTorch 中使用转换器。
好的,以上就是我们的六个主要主题。让我们从下一个视频开始,首先讨论使用循环神经网络进行序列生成。
1. 🌀 使用循环神经网络进行序列生成
上一节我们概述了课程内容。本节中,我们来看看如何使用循环神经网络进行序列生成。
我们之前已经学习过循环神经网络在分类任务中的应用。现在,我们将焦点转向一个新的能力:生成全新的序列数据。这开启了诸如文本生成、音乐创作等可能性。
字符级循环神经网络
字符级循环神经网络是序列生成的一个典型例子。它的工作原理如下:
- 输入:模型一次接收一个字符作为输入(例如,字母、空格、标点)。
- 处理:循环神经网络根据当前输入和其内部隐藏状态进行处理。
- 输出:在每一步,模型会输出一个对下一个可能字符的预测(一个概率分布)。
- 生成:要生成文本,我们可以从某个起始字符(或序列)开始,将模型的预测输出作为下一个输入,如此循环往复,从而生成任意长度的字符序列。
通过在海量文本数据上训练,这种模型可以学习到语言的统计规律和风格,从而生成连贯且风格类似的新文本。


2. 👁️ 为循环神经网络引入注意力机制

在掌握了基础的序列生成后,我们面临一个新的挑战:如何处理非常长的序列?传统的循环神经网络在记忆和关联远距离信息方面存在困难。


为了解决这个问题,研究者提出了注意力机制。注意力机制允许模型在生成序列的每一个时间步,动态地“回顾”并重点关注输入序列中最相关的部分,而不是仅仅依赖最后一个隐藏状态。
其核心思想可以概括为:模型会计算一组注意力权重,这些权重决定了在生成当前输出时,应该对输入序列的每个部分赋予多少“注意力”。公式可以简化为:
注意力输出 = Σ (注意力权重_i * 值_i)
其中,值通常来自输入序列的编码表示。这使得模型能够更有效地捕捉长距离依赖关系,显著提升了在机器翻译、文本摘要等任务上的性能。



3. ⚡ 注意力就是一切:转换器模型
上一节我们看到了注意力机制如何增强循环神经网络。但一个革命性的想法是:我们是否可以完全抛弃循环结构,只依靠注意力机制来构建模型?


答案是肯定的,这就是2017年里程碑式论文《Attention Is All You Need》的核心贡献。由此诞生的模型架构被称为转换器。
转换器完全基于自注意力机制和前馈神经网络构建,摒弃了循环连接。自注意力机制允许序列中的每个位置直接与序列中所有其他位置进行交互,从而高效地捕捉全局依赖关系。
转换器通常由编码器和解码器堆叠而成,但其核心创新在于:
- 自注意力层:计算序列内部元素之间的关系。
- 多头注意力:并行运行多个自注意力机制,以从不同子空间捕捉信息。
- 位置编码:由于没有循环结构,需要显式地向输入中添加位置信息,以便模型理解序列的顺序。

这种架构不仅训练速度更快(便于并行化),而且在许多自然语言处理任务上取得了当时最好的效果。

4. 💻 在 PyTorch 中使用转换器:代码示例
理论之后,我们来实践一下。本节将展示如何在 PyTorch 中使用内置的转换器模块。
PyTorch 在 torch.nn 模块中提供了 Transformer 类,以及相关的 TransformerEncoder, TransformerDecoder 等组件,使得构建转换器模型变得非常方便。
以下是一个高度简化的示例框架,展示了关键组成部分:
import torch
import torch.nn as nn

# 定义一个简单的转换器模型
class SimpleTransformer(nn.Module):
def __init__(self, num_tokens, d_model, nhead, num_encoder_layers):
super().__init__()
self.embedding = nn.Embedding(num_tokens, d_model)
self.pos_encoder = ... # 位置编码器
encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead)
self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
self.fc_out = nn.Linear(d_model, num_tokens)
def forward(self, src):
# src: [序列长度, 批次大小]
src = self.embedding(src) * math.sqrt(d_model)
src = self.pos_encoder(src)
output = self.transformer_encoder(src)
output = self.fc_out(output)
return output # 预测每个位置的下一个token

# 实例化模型、定义损失函数和优化器
model = SimpleTransformer(num_tokens=10000, d_model=512, nhead=8, num_encoder_layers=6)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())


这段代码勾勒出了构建一个转换器模型的基本步骤:嵌入输入、添加位置信息、通过多层转换器编码器进行处理,最后通过线性层输出预测。实际应用中还需要处理掩码、批次维度等细节。



🎯 课程总结
本节课中,我们一起学习了从循环神经网络到现代转换器的序列生成技术演进。
我们首先回顾了循环神经网络如何用于生成序列,并以字符级文本生成为例。接着,为了克服循环神经网络处理长序列的瓶颈,我们引入了注意力机制,它使模型能够动态聚焦于输入的相关部分。

然后,我们探讨了革命性的转换器模型,它证明了“注意力就是一切”,完全基于自注意力机制构建,摒弃了循环结构,从而实现了更强的并行能力和对长程依赖的捕捉。最后,我们通过一个简单的 PyTorch 代码示例,了解了如何动手使用转换器模块。

希望本课程为你打开了序列建模和生成式人工智能的大门。祝你项目顺利,学习愉快!

课程 P154:L19.1 - 使用单词和字符 RNN 生成序列 📝
在本节课中,我们将学习如何使用循环神经网络(RNN)进行序列生成。我们将重点讨论用于文本生成的“多对多”RNN架构,并比较字符级RNN与单词级RNN的异同。

概述 🎯
在之前的课程中,我们讨论了用于文本分类的RNN,那是一种“多对一”的架构。本节课,我们将转向“多对多”架构,它能够接收一个输入序列并生成一个输出序列,非常适合文本生成任务。
文本生成的两种主要方法
生成文本的RNN主要有两种方法:字符级RNN和单词级RNN。两者的核心区别在于输入和输出的基本单元(token)不同。
字符级RNN
在字符级RNN中,每个输入和输出单元是一个字符。例如,对于句子“I like hiking”,输入序列是 [‘I’, ‘ ‘, ‘l’, ‘i’, ‘k’, ‘e’, ‘ ‘, …]。网络的任务是预测序列中的下一个字符。
以下是其工作原理的简化表示:
# 输入:当前字符(如 ‘I’)
# 输出:下一个字符的概率分布(如 ‘ ‘ 的概率最高)
# 训练目标:最小化预测字符与真实下一个字符之间的交叉熵损失
单词级RNN
在单词级RNN中,每个输入和输出单元是一个单词。对于同样的句子“I like hiking”,输入序列是 [‘I’, ‘like’, ‘hiking’]。网络的任务是预测序列中的下一个单词。

训练过程详解
上一节我们介绍了两种RNN的基本概念,本节中我们来看看它们是如何被训练的。
无论是字符级还是单词级RNN,其训练都遵循自监督学习的模式。我们不需要额外的人工标注标签,因为标签就是输入序列本身的下一个单元。
训练阶段
在训练时,我们向网络提供一个序列单元(字符或单词),并让它预测下一个单元。损失函数(如交叉熵损失)通过比较预测概率分布和真实的下一个单元(即“标签”)来计算。
例如,对于字符级RNN和句子“test”:
- 输入
‘t’,网络应学会输出高概率给‘e’。 - 输入
‘e’,网络应学会输出高概率给‘s’。 - 输入
‘s’,网络应学会输出高概率给‘t’。
这个过程可以用以下示意图概括:

推理/生成阶段
训练完成后,我们如何用模型生成全新的文本呢?方法与训练时不同。
- 我们提供一个随机的起始单元(如一个字符或一个特殊的起始词)。
- 模型根据当前输入,输出一个下一个单元的概率分布。
- 我们并不总是选择概率最高的单元,而是根据这个概率分布进行采样。这引入了随机性,使得生成的文本具有多样性。
- 将采样得到的单元作为下一个时间步的输入。
- 重复步骤2-4,直到生成长度满意的序列或遇到停止符。


字符级与单词级RNN的对比

了解了基本流程后,我们来系统比较一下这两种方法的优缺点。
以下是两种方法的主要特点:
- 字符级RNN
- 优点:
- 词汇表小:通常只包含字母、数字、标点等(如 ~100个字符),模型输出层维度小,计算效率高。
- 内存占用少:嵌入层和输出层的参数量远小于单词级RNN。
- 能处理任何单词:可以拼写出训练数据中未出现过的单词。
- 缺点:
- 序列更长:处理同样长度的文本,需要处理的序列步数更多。
- 更难捕捉长期依赖:由于序列更长,单词开头和结尾之间的信息更容易丢失。
- 可能生成无意义词:可能组合出不符合拼写规则的字符序列。
- 优点:

- 单词级RNN
- 优点:
- 序列更短:每个时间步包含更多语义信息。
- 更容易捕捉语义和句法:直接在单词层面进行建模。
- 不会出现拼写错误:生成的每个单元都是词典中的合法单词。
- 缺点:
- 词汇表大:通常需要上万甚至数十万的词汇量,导致嵌入层和输出层非常庞大。
- 无法处理未登录词:对于不在训练词汇表中的单词,模型无法处理。
- 输出层计算昂贵:在数万词汇上计算Softmax概率分布开销较大。
- 优点:
总结 📚
本节课中,我们一起学习了使用RNN进行序列生成的核心概念。
我们首先回顾了“多对多”RNN架构,然后详细介绍了字符级RNN和单词级RNN两种文本生成方法。我们探讨了它们的训练过程(一种自监督学习范式)和文本生成过程(基于采样的迭代生成)。最后,我们系统对比了两种方法的优缺点,为实际应用中的选择提供了依据。


在接下来的课程中,我们将深入代码层面,学习如何在PyTorch中实现一个字符级RNN来实际生成文本。
P155:L19.2.1 - 在 PyTorch 中实现字符 RNN(概念)📚
在本节课中,我们将学习如何在 PyTorch 中实现一个字符级别的循环神经网络(RNN)。我们将重点关注实现前的核心概念,特别是 PyTorch 中 LSTM 类和 LSTMCell 类的区别与用法。理解这些概念将为下一节课的实际代码实现打下坚实基础。
LSTM 类回顾 🔄




上一节我们介绍了 RNN 的基本概念,本节中我们来看看 PyTorch 中 LSTM 类的具体细节。我们之前在实现用于分类的 RNN 时使用过这个类。
LSTM 类在初始化时需要几个关键参数:
input_size:输入特征的维度。hidden_size:隐藏状态的维度。num_layers:RNN 的层数。
以下是一个初始化示例:
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=2)

在使用时,LSTM 接收两个输入:
- 输入序列
input。 - 一个包含初始隐藏状态
h0和初始细胞状态c0的元组(h0, c0)。
它的输出也是一个元组,包含:
- 所有时间步的输出
output。 - 最后一个时间步的隐藏状态
hn和细胞状态cn,即(hn, cn)。
为了更直观地理解,可以参考下图,它展示了多层 LSTM 中数据(输入、状态、输出)的流动过程:


在上图中,底部是输入序列,黄色部分代表初始状态 (h0, c0)。网络可以有多层(图中为3层)。输出包括每个时间步的输出(右侧)以及最后一个时间步的最终状态 (hn, cn)。这些最终状态可以被传递给另一个 RNN 以处理后续序列。
LSTM Cell 类 🧱
理解了完整的 LSTM 类后,我们再来看看 LSTMCell 类。LSTMCell 是构成 LSTM 的基本单元。
两者的主要区别在于:
LSTM类:一次处理整个序列(或多个时间步),并可堆叠多层。LSTMCell类:一次只处理一个时间步的输入,并且只代表单层。如果需要多层,需要手动堆叠多个LSTMCell。

下图展示了 LSTMCell 在一个时间步内的计算过程,它对应上图中红色框内的单个计算单元:


LSTMCell 接收当前时间步的输入、上一个时间步的隐藏状态和细胞状态,然后输出当前时间步的输出、以及传递给下一个时间步的新的隐藏状态和细胞状态。
对于字符级 RNN 这种需要逐字符生成和计算损失的任务,使用 LSTMCell 通常更直观和方便,因为它允许我们更精细地控制每个时间步的计算。
应用场景:多对多架构 ➡️➡️

我们实现的字符 RNN 属于“多对多”(Many-to-Many)的序列生成模型,即根据前面的字符序列预测下一个字符。

以下是另一种重要的“多对多”架构:
- 语言翻译:将一种语言的序列转换为另一种语言的序列。PyTorch 官方提供了关于此的精彩教程,如果你感兴趣,可以自行查阅学习。
由于已有优质资源,我们的课程将专注于字符级文本生成任务。
下节预告 🔜
本节课我们一起学习了 PyTorch 中 LSTM 和 LSTMCell 的核心概念与区别。下一节课,我们将运用这些知识,实际动手编写代码,使用 LSTMCell 类来构建一个能够生成新文本的字符级 RNN 模型。




课程 P156:L19.2.2 - 在 PyTorch 中实现字符 RNN 🧠
在本节课中,我们将学习如何使用 PyTorch 实现一个基于字符的循环神经网络。我们将从零开始构建一个简单的 LSTM 模型,用于学习文本序列并生成新的文本。课程将涵盖数据准备、模型构建、训练循环以及文本生成等核心步骤。
1. 概述与准备工作 📋

上一节我们介绍了 LSTM 及其单元的基本概念。本节中,我们来看看如何用代码实现一个字符级的 RNN。

首先,我们导入必要的库并设置一些超参数。
import torch
import torch.nn as nn
import torch.optim as optim
import string
import random
# 超参数
text_port_size = 200 # 文本片段长度
num_iterations = 5000 # 训练迭代次数
learning_rate = 0.001
embedding_size = 100
hidden_size = 128
我们使用 Python 的 string 库获取所有可打印字符作为字符集。
all_characters = string.printable
num_characters = len(all_characters) # 字符总数,例如 100
我们的训练数据是来自威斯康星大学网站的 COVID-19 常见问题解答文本。
with open('covid_faq.txt', 'r', encoding='utf-8') as f:
text = f.read()
print(f"文本总字符数: {len(text)}") # 例如 84000
2. 数据预处理 🔧
我们需要将文本数据转换为模型可以处理的张量格式。以下是数据处理的几个关键函数。
首先,定义一个函数来随机获取指定长度的文本片段。

def get_random_text_portion(text, portion_size):
start_idx = random.randint(0, len(text) - portion_size - 1)
return text[start_idx:start_idx + portion_size]


接着,定义一个函数将字符串转换为对应的整数索引。
def char_to_tensor(string):
tensor = torch.zeros(len(string)).long()
for c in range(len(string)):
tensor[c] = all_characters.index(string[c])
return tensor
最后,定义一个函数来获取一个训练样本,包括特征(输入序列)和标签(目标序列,即输入序列右移一位)。
def get_random_sample(text, portion_size):
# 获取随机文本片段
portion = get_random_text_portion(text, portion_size)
# 转换为整数张量
input_tensor = char_to_tensor(portion)
# 目标标签是输入右移一位
target_tensor = char_to_tensor(portion[1:] + portion[0])
return input_tensor, target_tensor


3. 构建字符 RNN 模型 🏗️

现在我们来构建核心的 RNN 模型。我们将使用 nn.LSTMCell 来手动处理序列的每个时间步,这比使用 nn.LSTM 更直观,便于理解循环过程。
模型结构如下:
- 嵌入层 (Embedding Layer):将字符索引映射为稠密向量。
- LSTM 单元 (LSTMCell):处理序列,维护隐藏状态和细胞状态。
- 全连接层 (Fully Connected Layer):将 LSTM 的输出映射回字符空间,用于预测下一个字符。
以下是模型类的定义:
class CharRNN(nn.Module):
def __init__(self, input_size, embedding_size, hidden_size, output_size):
super(CharRNN, self).__init__()
self.hidden_size = hidden_size
# 嵌入层
self.embedding = nn.Embedding(input_size, embedding_size)
# LSTM 单元
self.lstm = nn.LSTMCell(embedding_size, hidden_size)
# 输出层(全连接层)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, char, hidden_state, cell_state):
# char: (batch_size=1,)
# 1. 通过嵌入层
embedded = self.embedding(char) # 输出形状: (1, embedding_size)
# 2. 通过 LSTM 单元
hidden_state, cell_state = self.lstm(embedded, (hidden_state, cell_state))
# 3. 通过全连接层得到输出(logits)
output = self.fc(hidden_state) # 输出形状: (1, output_size)
return output, hidden_state, cell_state
def init_zero_state(self, batch_size=1):
# 初始化隐藏状态和细胞状态为零
return (torch.zeros(batch_size, self.hidden_size),
torch.zeros(batch_size, self.hidden_size))
核心概念解释:
- 前向传播流程:在每个时间步,模型接收一个字符索引、上一个时间步的隐藏状态
hidden_state和细胞状态cell_state。字符经过嵌入层变为向量,然后与状态一起输入 LSTM 单元,得到新的状态和输出。输出再经过全连接层,得到对下一个字符的预测(logits)。 - 状态初始化:序列开始时,没有先前的状态,因此我们用零向量来初始化
hidden_state和cell_state。
4. 训练循环 🏋️
模型定义好后,我们进入训练阶段。训练循环会迭代多次,每次处理一个随机文本片段。
以下是训练函数的核心步骤:
def train(model, optimizer, criterion, text, num_iterations, text_port_size):
model.train()
for i in range(num_iterations):
# 初始化损失和零状态
total_loss = 0
hidden, cell = model.init_zero_state()
# 获取随机训练样本
input_tensor, target_tensor = get_random_sample(text, text_port_size)
# 遍历序列中的每个字符(时间步)
for t in range(text_port_size):
# 准备当前输入字符和目标字符
input_char = input_tensor[t].unsqueeze(0) # 形状变为 (1,)
target_char = target_tensor[t].unsqueeze(0)
# 前向传播
output, hidden, cell = model(input_char, hidden, cell)
# 计算损失(交叉熵损失函数内部会处理 logits)
loss = criterion(output, target_char)
total_loss += loss
# 反向传播与优化
optimizer.zero_grad()
# 计算平均损失
avg_loss = total_loss / text_port_size
avg_loss.backward()
optimizer.step()
# 每隔一定迭代次数,打印损失并评估模型生成文本的能力
if i % 500 == 0:
print(f'迭代次数: {i}, 损失: {avg_loss.item():.4f}')
evaluate(model, prime_str="TH", predict_len=100, temperature=0.8)
训练流程说明:
- 每次迭代(
iteration)处理一个长度为text_port_size的文本片段。 - 在每个片段开始时,将 LSTM 的状态重置为零。
- 遍历片段中的每个字符:
- 将当前字符输入模型,得到预测输出和更新后的状态。
- 计算预测输出与真实下一个字符(目标)之间的损失。
- 遍历完整个片段后,计算平均损失,执行反向传播和参数更新。
5. 文本生成与评估 🔮


训练过程中,我们希望看到模型的学习效果。因此,我们实现一个 evaluate 函数,让模型根据给定的起始字符(prime)生成一段新文本。
文本生成的关键在于“采样”策略。我们使用 温度采样(Temperature Sampling) 来增加生成文本的多样性。
def evaluate(model, prime_str='TH', predict_len=100, temperature=0.8):
model.eval()
with torch.no_grad():
# 初始化状态
hidden, cell = model.init_zero_state()
# “预热”模型:用起始字符构建初始状态
for p in range(len(prime_str) - 1):
_, hidden, cell = model(char_to_tensor(prime_str[p]).unsqueeze(0), hidden, cell)
# 准备第一个输入字符(起始字符串的最后一个字符)
input_char = char_to_tensor(prime_str[-1]).unsqueeze(0)
predicted = prime_str
# 开始生成后续字符
for _ in range(predict_len):
# 前向传播,得到输出 logits
output, hidden, cell = model(input_char, hidden, cell)
# 应用温度采样
output_dist = output.data.view(-1).div(temperature).exp()
# 根据输出分布进行多项式采样,得到下一个字符的索引
top_char = torch.multinomial(output_dist, 1)[0]
# 将索引转换为字符并添加到生成文本中
predicted_char = all_characters[top_char]
predicted += predicted_char
# 将预测的字符作为下一个时间步的输入
input_char = char_to_tensor(predicted_char).unsqueeze(0)
print(f"生成文本: {predicted}")
model.train()
温度采样解释:
- 公式:
output_dist = exp(logits / temperature) - 作用:
temperature参数控制采样分布的尖锐程度。- 低温度(如 0.1):分布更尖锐,模型更倾向于选择概率最高的字符,生成文本更确定、更保守,但也可能更重复。
- 高温度(如 1.5):分布更平缓,选择其他字符的概率增加,生成文本更多样、更有创造性,但也可能包含更多错误或无意义内容。
- 温度=1.0:为标准 softmax。
6. 模型初始化与训练启动 🚀
最后,我们初始化模型、损失函数、优化器,并开始训练。
# 初始化模型
model = CharRNN(input_size=num_characters,
embedding_size=embedding_size,
hidden_size=hidden_size,
output_size=num_characters)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 开始训练
train(model, optimizer, criterion, text, num_iterations, text_port_size)
训练过程中,你会看到损失逐渐下降,并且每隔一定迭代次数,模型生成的文本会从最初的乱码逐渐变得包含可识别的单词和短语结构。

总结 📝

本节课中我们一起学习了如何在 PyTorch 中实现一个字符级的循环神经网络。我们完成了以下核心任务:


- 数据准备:将文本数据切割成片段,并转换为模型可处理的整数张量。
- 模型构建:使用
nn.Embedding、nn.LSTMCell和nn.Linear搭建了一个能够处理序列的 RNN 模型。 - 训练循环:实现了按时间步展开的训练过程,包括状态管理、损失计算和参数优化。
- 文本生成:实现了基于温度采样的文本生成函数,用于评估模型性能。


这个简单的模型展示了 RNN 学习序列模式的基本能力。为了获得更好的效果,你可以尝试调整超参数(如层数、隐藏层大小)、使用更大的数据集或训练更长时间。在下一节课中,我们将探讨如何为 RNN 添加注意力机制,以进一步提升其处理长序列的能力。

课程 P157:L19.3 - 带注意力机制的 RNN 🧠
在本节课中,我们将学习循环神经网络(RNN)中的一种重要增强技术——注意力机制。该机制旨在解决传统RNN在处理长序列(如长句子翻译)时,因需要将所有信息压缩到单一隐藏状态而导致性能下降的问题。

上一节我们介绍了用于语言翻译的经典编码器-解码器架构。本节中,我们来看看这种架构在处理长句子时面临的挑战。

传统编码器-解码器架构将整个输入句子编码为一个最终的隐藏状态,然后解码器仅基于此状态生成翻译。对于长句子,网络很难在单个隐藏状态中记住所有必要信息。


为了更直观地理解,请看以下翻译示例:
输入句子:“Today is a great day.”
输出翻译:“Hotaist and Goar attack.”(此为示例语言)
在某些句子中,输入与输出单词存在近似一一对应的关系。然而,更多时候情况并非如此。



请看另一个更复杂的例子:
输入句子:“If you have ever studied a foreign language, you have probably encountered a false friend at some point.”
输出翻译:(对应译文,此处展示单词对应关系)


在这个例子中:
- 两个输入单词可能合并为一个输出单词。
- 一个输出单词可能对应多个相距较远的输入单词。
- 某些输入单词在输出中没有直接对应项。



这种非对齐的特性使得简单的、逐词映射的“多对多”RNN架构难以胜任。因此,我们需要编码器-解码器架构。但随之而来的核心问题是:解码器如何仅从一个最终的隐藏状态获取翻译长句子所需的全部细节信息?

为了解决长序列信息压缩的难题,研究人员提出了注意力机制。其核心思想是:在解码器生成每一个输出单词时,允许它“回顾”整个输入序列,并动态地决定需要“关注”输入句子的哪些部分。
以下是该机制的关键优势:
- 缓解记忆负担:解码器无需将所有信息塞进一个固定长度的向量。
- 动态对齐:模型自动学习输入与输出单词之间的软对齐关系,即使顺序不一致。
- 提升长句性能:如下方的BLEU评分图所示,引入注意力机制后(橙色线),模型在处理长句子时的翻译质量下降幅度远小于传统模型(蓝色与绿色线)。



上一节我们了解了注意力机制的目的。本节中,我们来深入看看它的具体架构是如何工作的。
一个典型的带注意力机制的RNN翻译模型包含两个主要部分:
- 双向RNN编码器:用于处理输入句子。它由两个RNN组成,一个按顺序(前向)读取句子,另一个按逆序(反向)读取句子。我们将每个时间步的前向隐藏状态(例如
h_f1)和反向隐藏状态(例如h_b1)连接(concatenate)起来,得到该时间步的完整隐藏状态h1 = [h_f1, h_b1]。这样,h1就包含了关于第1个单词及其上下文的信息。# 概念性代码:获取双向隐藏状态 hidden_state = concatenate(forward_rnn(input), backward_rnn(reverse(input)))

- 带注意力机制的解码器RNN:用于生成翻译。在解码器的每一个时间步
i,它不仅仅依赖自己上一个时间步的隐藏状态s_{i-1},还会计算一个上下文向量(Context Vector)c_i。
上下文向量 c_i 的计算是注意力机制的核心:
- 首先,解码器通过一个小型神经网络(如多层感知机),评估编码器每一个隐藏状态
h_j与当前解码状态s_{i-1}的相关性,得到一个未归一化的分数e_{ij}。# 概念性公式:计算相关性分数 e_{ij} = score(s_{i-1}, h_j) - 然后,对所有
j的分数e_{ij}应用 Softmax 函数,得到归一化的注意力权重α_{ij}。所有权重之和为1。# 概念性公式:计算注意力权重 α_{ij} = softmax(e_{ij}) = exp(e_{ij}) / Σ_k(exp(e_{ik})) - 最后,上下文向量
c_i是所有编码器隐藏状态h_j的加权和,权重即为α_{ij}。# 核心公式:计算上下文向量 c_i = Σ_j (α_{ij} * h_j)
解码器在时间步 i 的最终输入,是上下文向量 c_i 与上一个输出单词嵌入(或上一个时间步的隐藏状态)的结合。然后,解码器RNN据此更新自己的隐藏状态 s_i,并预测当前输出单词 y_i。
注意力权重 α_{ij} 的可视化能让我们直观理解模型的工作方式。下图展示了一个英语到法语的翻译示例中,注意力权重的热力图。


图中,行代表输出(法语)单词,列代表输入(英语)单词。亮色(白色)表示高注意力权重(接近1),暗色(黑色)表示低注意力权重(接近0)。
我们可以观察到:
- 对角线上的亮色区域表明,许多单词存在近似一对一的翻译关系。
- 同时,也存在非对角线的亮色区域。例如,输出单词“L'”同时关注了输入单词“The”和“s”。
- 特别明显的是,输出数字“1992”几乎只专注于输入数字“1992”,这完全符合直觉。
这种可视化证明了注意力机制成功地让模型学会了在翻译时动态地、有选择地聚焦于输入句子的相关部分。

本节课中,我们一起学习了带注意力机制的RNN。我们首先探讨了传统编码器-解码器模型在处理长序列时的局限性,然后引入了注意力机制作为解决方案。我们详细剖析了该机制的架构,包括双向RNN编码器和计算上下文向量的过程,其核心公式为 c_i = Σ_j (α_{ij} * h_j)。最后,我们通过注意力权重可视化,看到了模型如何学会在输入和输出之间建立动态的软对齐。

注意力机制极大地提升了RNN在长序列任务(如机器翻译)上的性能。然而,故事并未结束。在接下来的课程中,我们将看到一项更革命性的思想:我们是否可以完全摒弃RNN结构,仅依靠注意力机制来构建模型? 这引出了Transformer架构的诞生。
课程 P158:L19.4.1 - 在没有RNN的情况下使用注意力:一种基本的自我注意力形式 🧠
在本节课中,我们将学习一种不依赖循环神经网络(RNN)的注意力机制,即自我注意力。我们将从基础概念入手,理解其核心原理,为后续学习更复杂的Transformer模型打下基础。
在上一节视频中,我们讨论了带有注意力机制的循环神经网络。这种注意力机制帮助RNN更好地处理长序列。
现在,我们将进行一个大胆的尝试:移除循环神经网络部分。我们将研究一个仅使用注意力机制、不含RNN部分的模型。😊
我们将要探讨的这种特定注意力类型也称为自我注意力。它是所谓的Transformer网络背后的基础或主要原理之一。Transformer模型是目前处理长序列和文本数据的最先进模型。

由于涉及许多小主题,为了避免制作一个过长的视频,我决定将其进一步细分为多个小节。
在本视频中,我们将先了解一个广泛的概念,然后为了教学目的,介绍一种非常基础的自我注意力形式,以理解其背后的基本原理。接着,我们会探讨原始Transformer模型中更复杂的形式。这个原始Transformer模型还有一个称为多头注意力的概念。在涵盖这些内容之后,我们将看看这些概念是如何组合成Transformer模型的。我还会介绍一些关于它的有趣见解,并讨论当今流行的一些变体。最后,我们将以在PyTorch中实现Transformer模型作为结束。


好的,这里只是对上一视频内容的回顾。我们有一个带有注意力机制的RNN。它的工作原理是:对于每个生成的词(这里我们称这个RNN为RN #1),我们有一个双向RNN。在每个时间步,这里的RNN会创建一个输出词,除了接收先前的隐藏状态外,它还接收一个上下文向量,这个向量依赖于这里的整个序列输入。我们这里有整个序列,即这里的隐藏表示,然后我们用这些注意力权重乘以它们。注意力权重是由神经网络计算出的值的归一化版本。这就是我们在上一视频中介绍的内容。关键思想是,我们以加权形式将整个序列作为输入。


现在,我们将从该模型中移除所有顺序处理部分。我们摒弃所有顺序部分,不使用任何循环、卷积或任何专门用于顺序处理输入的东西。
我们将朝着这个所谓的Transformer模型努力,它仅依赖于自我注意力机制。自我注意力机制一次性处理整个序列。这实际上也非常有利于并行化。实际上,Transformer模型的训练成本相当高,但它们能更好地利用多个GPU,因为你可以并行训练。而对于RNN,你一次生成一个东西,所以无法并行运行这些计算,因为要计算后面的部分,必须先完成前面的部分。因此,Transformer在并行化计算方面要好得多。

与“多对多”RNN类似,我们也将有一个编码器和一个解码器部分。但在这里,我们不使用RNN或LSTM,而是使用一种称为堆叠注意力层的东西。这就是我们努力的方向。这是大的图景,我们将逐步添加这些部分,一步一步地进行。

这些幻灯片的基本基础是这篇名为 《Attention Is All You Need》 的论文。这是2017年具有开创性意义的基础论文,它介绍了原始的Transformer架构,其性能在当时超越了任何其他方法。




自那时起,从2018年开始,基于Transformer的自然语言处理领域发展迅猛。你可以看到它开始时规模也相对较小,这里的Y轴(虽然很遗憾作者没有标注Y轴,但我认为在这篇文章中他们指的是以百万为单位的参数量),这个模型有83亿参数。你可以看到这些模型的规模呈巨大增长曲线,同时其受欢迎程度也大幅增长。虽然本视频中没有展示,但也有一些综述论文展示了Transformer被引用的次数以及现有模型的数量,这也是指数级增长。所以,这是一个非常非常热门的领域。当然,训练83亿个参数对于普通人来说是不可行的。如今,也有研究小组专注于开发小型Transformer模型。无论如何,这只是一个大图景,表明Transformer模型很有趣,有很多不同的变体。我们讨论的是这个基础模型《Attention Is All You Need》,如果你感兴趣,


你可以后续跟进其他一些模型。我稍后也会简要谈谈GPT-2和BERT模型,它们也是基础模型,主要概念在于它们使用了自监督学习技术,这些技术后来也被其他类型的Transformer所采用。

好的,回到自我注意力机制。在讨论Transformer中使用的自我注意力机制之前,我想先介绍一种非常基础的形式,以便循序渐进地引入这个主题。

这种非常基础的形式,我们可以将其视为一个包含三个步骤的过程:
- 推导注意力权重,它是当前输入(序列中的一个元素,可以看作句子中的一个词)与所有其他输入之间相似性或兼容性的一种形式。
- 一旦我们有了权重(我将在下一张幻灯片展示如何推导权重),我们通过softmax函数对它们进行归一化。这与我们在RNN中计算归一化注意力权重时所做的类似。
- 然后,在第三步,我们从归一化权重和相应的输入计算注意力值。
整个过程看起来与我们之前在RNN中展示的注意力机制非常相似。在RNN注意力机制中,我们也有一个加权和作为注意力值。
这里,x_j 是一个输入词。我们假设句子中有T个词。对于每个词,我们有一个注意力权重。让我们称这个词为 j,从 j=1 到 T。句子中有T个词,然后对于第 i 个词,你可以看到这是 a_{ij}。所以,i 代表第 i 个词。这是第 i 个词与第 j 个词之间关系的注意力权重。你用它来计算句子中第 i 个词的注意力值。可能信息有点密集,让我们一步一步来看,我也会展示这些注意力权重 a 是如何计算的。


在顶部,我再次展示了上一张幻灯片的内容,我们计算对应于第 i 个输入(第 i 个词)的输出。每次我在这里写“输入”,我指的是一个句子,而“第 i 个输入”就是第 i 个词。
现在,我们如何计算这些注意力权重呢?在这种简单的、非常基础的自我注意力形式中(仅用于介绍目的),我们假设通过点积来计算。在第 i 个输入词和第 j 个词之间计算点积。让我们表示为 w_i 和 w_j。然后我们对句子中的所有词(T个词)重复这个过程。我们得到 e_{i1}, e_{i2}, e_{i3}, ..., e_{iT}。当我们使用softmax函数计算归一化形式时,所有这些归一化后的值将求和为1。

这些将成为我们的注意力权重。总结前面的幻灯片,这里有一个我们刚刚讨论内容的可视化表示。
假设我们有一个输入序列。这里的输入序列,你可以把它看作一个句子。每个 x,每个向量代表一个词。我说向量,因为这是一个词嵌入。我们在RNN的背景下讨论过这个,例如,我们将单词转换为整数索引,然后从嵌入矩阵中检索嵌入。所以嵌入本质上就是每个特定词的连续值向量。
然后,在第一步中,我们计算相似性。让我们称当前输入为查询。例如,i 可以是1,即第一个词。我们对每个词都这样做,但假设我们从第一个词开始,然后执行步骤1、2、3,接着移动到第二个词,对步骤1、2、3做同样的事情。这里,第一步的输出是 a_{i=1},第二步是 a_{i=2},然后我们把它们全部堆叠起来。本质上我们会得到一个矩阵。我有点超前了,还是一次展开一件事。
我们使用点积来计算相似性。为什么用点积?这只是我们计算向量之间兼容性或相似性的一种方式。我们也可以考虑其他函数,如余弦相似性(本质上就是归一化的点积)。但为了简单起见,我们使用点积。我们在这里计算查询 x_i 与句子中每个其他词的点积。注意,这里是 x_1, x_2... 对于每一个,我们计算这个相似性,它是一个标量(单个数字)。然后我们将其通过softmax函数,使它们归一化。现在我们有了归一化的注意力分数,它们的值在0到1之间,并且求和为1。应该是从 i=1 到 T。无论如何,然后我们在这里对它们求和。我们得到注意力值,它是一个向量。因为 x_j 是我们的输入词,我们从 x_1 到 x_T 遍历所有输入。现在我们用对应的 a 对这些输入进行加权。我们做的是:加权这个输入,然后加上这个加权输入,再加上这个加权输入... 这给我们一个向量,因为我们在相加。这个向量本质上就像一个词嵌入,只是它包含了关于整个序列的信息。而原始的词嵌入只包含关于词本身的信息。所以,无论一个词在句子中的什么位置,也无论句子最初是什么样子,例如“Hello”这个词,当我们将其放入模型时,总是具有相同的嵌入。在RNN中也是如此,我们总是有相同的嵌入值,无论它在句子中是第一个词、第二个词还是最后一个词,也无论其他词是什么。但现在,相比之下,我们的输出这里,如果我们有查询词(比如第一个词“Hello”),它也是这个词的一种表示,只是它包含了“Hello”在所有其他词上下文中的信息。因为我们有这个加权步骤。所以我们现在有了一个更强大的上下文感知嵌入向量。
我们所做的本质上是:在提取信息方面,我们不再仅仅单独考虑每个词,而是现在有了能感知其上下文的词表示。这就是我称之为非常简单的、基础的自我注意力形式。当然,这不是Transformer中使用的形式,但只是为了引入这个主题。在下一个视频中,我们将看看更复杂的版本。但我认为,这个例子总结了整个概念,或者说,总结了注意力背后的主要思想之一:推导上下文。
好的,在下一个视频中,让我们来看看更复杂的版本。






总结


本节课中,我们一起学习了自我注意力的基础概念。我们从回顾带注意力的RNN开始,然后移除了RNN的顺序处理部分,引入了仅依赖注意力的模型思想。我们重点探讨了一种基础的自我注意力形式,它通过计算词向量之间的点积作为相似度,再经softmax归一化为注意力权重,最后对输入词进行加权求和,从而得到包含上下文信息的词表示。这个过程的核心在于让每个词的表示都能感知到句子中其他词的信息,为理解更复杂的Transformer模型奠定了基础。

课程 P159:L19.4.2 - 自注意力与缩放点积注意力 🧠

在本节课中,我们将学习自注意力机制的高级形式,特别是《Attention Is All You Need》论文中提出的缩放点积注意力。我们将了解如何通过引入可学习的参数矩阵,使自注意力机制能够被训练并应用于语言模型等任务。



在上一节视频中,我们讨论了自注意力的基础形式。本节中,我们将深入探讨一个更复杂的版本,即《Attention Is All You Need》论文中使用的自注意力机制。

回顾一下,基础自注意力机制的定义如下:我们有一个由词嵌入组成的输入序列。然后,我们计算一个特定输入与所有其他输入的点积。接着,使用 softmax 函数对这些点积进行归一化,最终输出是注意力加权的输入之和。对于每个输入 a_i,我们得到一个向量。如果我们有 T 个词,就会得到一个 T x T 维的注意力矩阵。
现在,我们将扩展这个基础注意力的概念,引入论文中使用的注意力机制。


首先,基础自注意力版本存在一个主要问题:它不包含任何可学习的参数。这意味着它对于学习语言模型并不十分有用,因为如果没有可学习参数,我们无法通过反向传播来更新和优化模型。为了解决这个问题,我们现在引入三个可训练的权重矩阵,它们将与输入序列的词嵌入 X 相乘。
具体来说,我们不再直接计算输入之间的点积,而是引入三个权重矩阵:W_Q(查询矩阵)、W_K(键矩阵)和 W_V(值矩阵)。通过将词嵌入向量 x_i 与这些矩阵相乘,我们分别得到查询向量 q_i、键向量 k_i 和值向量 v_i。这些权重矩阵可以通过反向传播进行更新。
以下是该自注意力机制针对特定输入(例如句子中的第二个词 x_2)的计算过程示意图。我们将 x_2 视为当前查询。通过矩阵乘法,我们为所有输入词计算其对应的查询、键和值向量。


接下来,我们计算注意力分数。与基础版本不同,这里我们计算的是当前查询向量 q_2 与所有键向量 k_i 的点积。这个过程可以形象地理解为:对于每个查询,模型学习它应该关注哪些键值对输入。
计算出的点积经过 softmax 函数归一化,得到注意力权重 a_i,这些权重之和为 1。然后,我们将每个注意力权重与其对应的值向量 v_i 相乘,并对所有结果求和,从而得到当前输入 x_2 的上下文感知嵌入表示 a_2。

这个过程会为输入序列中的每个词(x_1 到 x_T)重复执行。值得注意的是,所有这些计算都可以并行进行,因为处理每个词时并不依赖于其他词的计算结果,这是 Transformer 模型的一个优点。
为了更好地理解上一张幻灯片的内容,我们来分析一下其中各部分的维度。
- 输入词嵌入
x_i是一个1 x d_e维的向量,其中d_e是嵌入维度(在原始论文中为 512)。 - 权重矩阵
W_Q、W_K、W_V的维度是d_e x d_q、d_e x d_k和d_e x d_v。在原始 Transformer 论文中,通常设置d_q = d_k = d_v。 - 查询向量
q_i、键向量k_i和值向量v_i的维度分别是1 x d_q、1 x d_k和1 x d_v。 - 点积
q_i · k_i的结果是一个标量。 - Softmax 归一化后,我们得到标量注意力权重。
- 最终输出
a_i是1 x d_v维的向量。
之前的图示仅展示了针对第二个词的注意力计算。实际上,我们需要为输入序列中的每个词(第一个词、最后一个词等)重复此过程。虽然图示看起来是顺序的,但所有计算均可并行执行。



最终,我们会得到一个 T x d_v 维的注意力矩阵,其中每一行对应一个词的上下文感知嵌入。
我们可以使用更紧凑的矩阵表示法来总结整个过程。

将整个输入序列表示为矩阵 X,其维度为 T x d_e。通过矩阵乘法 Q = X W_Q、K = X W_K、V = X W_V,我们一次性得到所有词的查询、键和值矩阵。
注意力矩阵 A 可以通过以下公式计算:
A = softmax( (Q K^T) / sqrt(d_k) ) V
这里,Q K^T 计算了所有查询和键对之间的点积,得到一个 T x T 的矩阵。除以 sqrt(d_k) 是缩放操作,softmax 函数按行进行归一化。最后,与值矩阵 V 相乘,得到 T x d_v 维的输出矩阵。
这个缩放因子 sqrt(d_k) 的作用是防止点积的值过大。当点积值很大时,softmax 函数的梯度会变得非常小(进入饱和区),这不利于训练。通过缩放,可以使注意力权重的分布更加平缓,有利于模型学习。

自注意力机制的一个潜在缺点是,Q K^T 计算产生的 T x T 矩阵在输入序列很长时会占用大量内存(复杂度为 O(T²))。不过,其可并行计算的特性在一定程度上弥补了这一不足。
下图是《Attention Is All You Need》论文中对缩放点积注意力的可视化总结,清晰地展示了矩阵乘法、缩放、可选的掩码(在解码器中用到)、softmax 以及与值矩阵相乘的步骤。


本节课我们一起学习了自注意力机制及其更实用的变体——缩放点积注意力。我们了解了如何通过引入可学习的查询、键、值矩阵,使注意力机制能够被训练。我们还探讨了其并行计算的优势以及缩放因子的重要性。在下一节课中,我们将在此基础上学习多头注意力机制,这是构建完整 Transformer 模型的关键一步。
深度学习课程 P16:L2.3 - 深度学习的起源 🧠
在本节课中,我们将要学习深度学习的起源。我们将从多层感知机过渡到深度学习,探讨“深度学习”这一术语的由来,并了解推动其发展的关键架构与技术突破。
从多层感知机到深度学习

上一节我们介绍了多层感知机。本节中我们来看看“深度学习”这一概念是如何出现的。
“深度学习”是一个相对较新的术语,大约在10-15年前被提出。而多层感知机已经存在了至少30-40年。那么,使用多层感知机是否算作深度学习呢?这在技术上是存在争议的。
从技术上讲,“深度学习”一词最初出现时,指的是使用具有许多层的先进架构进行表示学习。因此,仅仅拥有多层结构并不是多层感知机与深度学习的主要区别。关键在于表示学习的能力,这才是深度学习的核心。
如今,在较新的论文中,人们使用多层感知机时也常称之为深度学习。为了减少混淆,现在通常将所有神经网络都自动归入深度学习的范畴。
表示学习与卷积神经网络
让我们更深入地探讨一下刚才提到的表示学习方面。这可以追溯到大约20世纪90年代,甚至在“深度学习”这个词出现之前。
卷积神经网络是建立在多层感知机之上的一个专门化版本。它引入了局部性的概念,旨在提高训练效率并提取局部特征,从而更好地捕捉特征间的依赖关系。
回想一下,多层感知机假设特征独立,适用于表格数据。而卷积网络可以利用关系归纳偏置的先验知识,专门为图像识别和图像数据设计。
以下是卷积网络的基本工作原理:

# 概念性结构:卷积部分 + 全连接部分
# 卷积部分:特征学习(卷积层、权重共享、池化)
# 全连接部分:分类器(本质上是多层感知机)
在右侧展示的是Yann LeCun等人应用于手写邮政编码识别的原始LeNet架构。我们将在后续课程中专门讲解卷积网络,这里先简要概述。

卷积网络由两部分组成:
- 卷积部分:包含卷积层,运用了权重共享的技巧,并涉及池化操作。这使得训练更加高效。你可以粗略地将其视为特征学习层,这些层能够从数据中提取抽象特征。
- 全连接部分:本质上是一个多层感知机,充当分类器。
因此,整个网络可以看作一个任务:下半部分用于特征学习,上半部分用于分类由前面层提取的特征。这使得手工设计特征变得过时。网络自己学习特征,并且学习的方式对分类器是最优的,因为整个网络是连接在一起的、共同学习的。这是神经网络在处理图像数据时非常高效的一个优势。
这同样是20世纪90年代的成果,那时“深度学习”一词尚未出现。但如今,卷积神经网络无疑是深度学习最流行的形式之一。当然,深度学习还有另一个重要分支——循环神经网络。
循环神经网络

循环神经网络也可以看作是多层感知机的一个高级版本。
它的新颖之处在于引入了循环结构。多层感知机是前馈网络,而循环神经网络可以“回顾”先前的信息。这将在本课程后续的专门讲座中详细讨论。
其核心思想是,我们使用了一种改进的反向传播算法,称为随时间反向传播,因为它包含了时间成分。这对于处理序列数据特别有用,因为这类数据中的特征之间存在顺序依赖关系。
然而,训练具有许多层的神经网络,尤其是处理长序列的循环网络时,会面临梯度消失和爆炸的挑战。针对这个问题有许多潜在的解决方案,其中至今仍常用的是长短期记忆网络。
这也是深度学习的一种流行形式。实际上,我不知道循环神经网络最初是何时发明的,大概是在20世纪80年代反向传播算法发明之后不久,作为其一项进展。
深度学习的核心定义
那么,“深度学习”最初到底指什么呢?这里引用深度学习综述(作者:Yoshua Bengio, Yann LeCun, Geoffrey Hinton)中的一句话来总结:
表示学习是一组方法,允许机器接收原始数据,并自动发现用于检测或分类所需的表示。深度学习方法是具有多个表示(或抽象)层次的表示学习方法。
这里的重点确实是深度学习所做的隐式特征提取,这是传统机器学习无法做到的(正如我们上周讨论的,传统方法依赖于手工特征与自动特征提取)。如今,深度学习基本上就等同于使用神经网络。

走向现代:硬件革命与ImageNet突破
从这些20世纪90年代发明的架构,到现代深度学习研究时代,中间不幸地经历了另一个“AI寒冬”(20世纪90年代末至21世纪初)。这可能是因为支持向量机和随机森林的流行。当时,神经网络虽然有能力,但训练成本非常高,需要巨大的计算机硬件,并且即使如此,训练时间也很长。相比之下,支持向量机和随机森林更容易训练,并且效果相当好。
然而,后来人们发现了如何利用GPU来更高效地训练神经网络。这真正帮助神经网络变得流行和可行。根据文献,GPU实现神经网络可以追溯到2004年,但真正变得流行是在2012年。
2012年,Alex Krizhevsky、Ilya Sutskever和Geoffrey Hinton在ImageNet分类挑战赛上使用了在GPU上训练的深度神经网络,其性能大幅超越了传统的计算机视觉方法。以下是关于硬件差异的简要说明:
| 特性 | CPU (如 Intel) | GPU (如 NVIDIA) |
|---|---|---|
| 核心数量 | 较少,但频率高 | 极多,但频率较低 |
| 擅长运算 | 通用计算 | 线性代数(向量/矩阵运算) |
| 关键优势 | 单核性能强 | 并行计算能力强,内存带宽高 |
GPU特别擅长并行计算点积和矩阵乘法,这使得它对深度学习非常有用。你不需要为这门课购买GPU,后续会介绍一些免费的在线资源。
关于深度学习何时真正流行起来,这里有一段有趣的引述,来自对Geoffrey Hinton的采访,谈及2012年的ImageNet竞赛:

当时,会议被传统的计算机视觉方法(如手动特征工程)主导,没有深度学习。评审们认为(神经网络)是做事情的错误方式……然而,2012年提交的AlexNet在top-5分类错误率上达到了15.4%,而第二好的方法只有26.2%。他们的方法比任何传统计算机视觉系统都好大约一倍。人们真的对深度学习的效果感到惊讶。他说,在两年内,所有人都转向了(深度学习)。对于像物体分类这样的任务,现在没人会梦想不使用神经网络。
因此,2012年确实是深度学习的突破时刻。
常用基准数据集
以下是开发卷积网络或物体分类方法时常用的基准数据集:
- MNIST:传统数据集(1998年),6万张示例,10个类别(手写数字),图像分辨率非常低。非常适合调试网络,训练速度快。
- CIFAR-10 / CIFAR-100:更高级的数据集,6万张示例,CIFAR-10有10个类别,CIFAR-100有100个类别。分辨率仍较低(32x32),但是彩色图像(3个通道)。
- ImageNet:更具挑战性的数据集,包含约1400万张全分辨率图像。其挑战性在于图像中可能有多个物体,标签有时存在歧义。因此,通常使用top-5准确率/错误率进行评估。即,检查模型预测的置信度最高的前5个标签中,是否包含图像的真实标签。
后续发展与总结
自2012年以来,当然又有了许多进展,使得深度神经网络工作得更好。一些常见且流行的技术包括:
- ReLU:修正线性单元,非常简单但非常有效的激活函数。
- 批量归一化
- Dropout
- 生成对抗网络

这些技术都将在本课程中涵盖,并且过去十年还有更多进展,它们共同推动了深度学习的发展。
在下一个视频中,我们将简要讨论硬件和软件格局在过去几年发生的变化,并简要概述当前的一些研究趋势,从而结束第二讲的系列视频。之后,我们将进入关于感知机的更详细课程。


本节课中我们一起学习了:深度学习的起源和定义,其与多层感知机的联系与区别;推动深度学习发展的两大关键架构——卷积神经网络和循环神经网络的基本思想;深度学习在2012年借助GPU和ImageNet竞赛实现突破的历史;以及常用的基准数据集和后续重要技术。理解这些起源有助于我们把握深度学习发展的脉络和核心思想。
P160:L19.4.3 - 多头注意力机制 🧠

在本节课中,我们将要学习多头注意力机制。这是Transformer模型的核心组件之一。我们将基于上一节介绍的缩放点积注意力,理解如何将其并行扩展多次,以捕捉序列中不同类型的信息。

上一节我们详细介绍了缩放点积注意力机制。本节中,我们来看看如何将其扩展为更强大的多头注意力。
核心概念:并行注意力
多头注意力的核心思想是并行运行多个缩放点积注意力机制。这类似于卷积神经网络中使用多个卷积核来提取不同特征。每个注意力“头”使用不同的权重矩阵,从而可以关注输入序列的不同方面。


以下是多头注意力的主要步骤:
- 线性投影:对于每个注意力头,使用独立的权重矩阵将输入序列分别投影为查询、键和值。
- 并行计算:每个头独立计算其缩放点积注意力。
- 拼接结果:将所有头的输出在特征维度上进行拼接。
- 最终线性变换:将拼接后的结果通过一个线性层,映射回期望的输出维度。
多头注意力结构详解

让我们结合原论文中的图示来理解这个过程。


上图展示了多头注意力的完整流程。中间的“Scaled Dot-Product Attention”模块就是我们在上一节学习的内容。这个模块被重复了H次(例如,在原论文中H=8)。
以下是流程中关键部分的说明:
- 输入:输入序列的维度为
T x D_model,其中T是序列长度,D_model是模型维度(例如512)。 - 多头拆分:对于每个头,我们使用不同的权重矩阵
W_Q^i,W_K^i,W_V^i将输入投影到较低的维度D_k,D_k,D_v。通常,D_v = D_model / H。例如,当D_model=512,H=8时,D_v = 64。 - 注意力计算:每个头独立计算注意力,得到一个
T x D_v的输出。 - 拼接:将H个头的输出(每个
T x D_v)在最后一个维度拼接,得到T x (H * D_v)的张量,即T x D_model。 - 输出线性层:拼接后的结果通过一个可学习的线性层
W_O(维度为D_model x D_model),最终输出维度仍为T x D_model。
这个过程可以用伪代码概括如下:


# 假设输入 x 的维度为 (batch_size, seq_len, d_model)
# 定义多头注意力层
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads
# 为每个头定义Q, K, V的投影矩阵
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
# 最终的输出投影矩阵
self.W_o = nn.Linear(d_model, d_model)
def forward(self, x):
batch_size, seq_len, _ = x.shape
# 1. 线性投影并分割成多头
Q = self.W_q(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
V = self.W_v(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
# 2. 对每个头应用缩放点积注意力 (scaled_dot_product_attention)
# 此处省略具体实现细节,输出 attn_output 维度为 (batch_size, num_heads, seq_len, d_k)
attn_output = scaled_dot_product_attention(Q, K, V)
# 3. 将多头输出拼接
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
# 4. 通过输出线性层
output = self.W_o(attn_output)
return output

设计动机与优势
多头注意力机制的设计主要有两个目的:
- 增强模型容量:通过使用多组权重矩阵,模型可以学习到更丰富、更复杂的表示。
- 并行关注不同信息:不同的注意力头可以学会关注序列中不同位置或不同特征模式的信息。例如,一个头可能关注局部语法结构,另一个头可能关注长距离的语义依赖。

这种设计使得模型能够更灵活、更强大地处理序列数据。

总结
本节课中我们一起学习了多头注意力机制。我们了解到,它本质上是将缩放点积注意力机制并行应用多次,每次使用不同的参数。每个头独立计算注意力,然后将所有头的输出拼接起来,最后通过一个线性变换层得到最终输出。这种结构允许模型同时从输入序列的不同子空间捕获信息,是Transformer架构取得成功的关键因素之一。

现在我们已经掌握了自注意力、缩放点积注意力和多头注意力这些核心概念。在下一节课中,我们将看到这些组件如何组合在一起,构成完整的Transformer模型。
课程 P161:L19.5.1 - Transformer 架构详解 🧠

在本节课中,我们将详细学习 Transformer 模型的核心架构。我们将从回顾缩放点积注意力机制开始,逐步拆解 Transformer 的编码器-解码器结构,并解释其中的关键组件,如多头注意力、掩码、位置编码和层归一化。

回顾:缩放点积注意力
在上一节我们介绍了多头注意力的基础,本节中我们首先回顾其核心——缩放点积注意力机制,以便更好地理解 Transformer。
缩放点积注意力机制在《Attention Is All You Need》论文中被提出,其计算过程可分为五个步骤。以下是该过程的总结:
-
构造查询、键和值:输入矩阵 X 的每一行代表一个词。我们分别使用可学习的权重矩阵 WQ**、**WK、W^V 与输入相乘,得到查询矩阵 Q、键矩阵 K 和值矩阵 V。
- 公式:Q = X W^Q, K = X W^K, V = X W^V
-
计算注意力分数:通过矩阵乘法计算查询 Q 和键 K 的相似度(点积)。结果矩阵的每个元素代表了序列中两个词之间的关联强度。
-
缩放:将上一步的结果除以键向量维度 d_k 的平方根进行缩放。这是为了防止点积值过大,导致 Softmax 函数的梯度变得过小,不利于训练。
- 公式:分数 = (Q K^T) / sqrt(d_k)
-
应用 Softmax:对缩放后的分数矩阵的每一行应用 Softmax 函数,将其转换为概率分布(注意力权重),权重之和为 1。
-
加权求和:将上一步得到的注意力权重矩阵与值矩阵 V 相乘。对于输出矩阵的每一行(对应一个词),其结果都是所有值向量的加权和,权重由该词与其他词的关联度决定。这样,每个词的输出表示都融合了整个句子的上下文信息。
多头注意力机制

上一节我们介绍了单一的注意力头,本节中我们来看看如何通过并行计算多个注意力头来增强模型能力。
多头注意力机制的核心思想是并行运行多个缩放点积注意力过程。具体步骤如下:

- 首先,使用不同的线性投影矩阵,将输入分别投影到 h 组(例如8组)查询、键和值子空间。
- 然后,在每组子空间上独立执行缩放点积注意力计算。
- 接着,将所有 h 个注意力头的输出在特征维度上进行拼接。
- 最后,将拼接后的结果通过一个可学习的线性层进行投影,得到最终输出。
这种设计允许模型在不同的表示子空间中共同关注来自不同位置的信息,从而捕获更丰富的上下文关系。
Transformer 整体架构 🏗️
了解了核心组件后,现在我们来俯瞰 Transformer 模型的完整架构。它看起来可能很复杂,但我们可以逐一拆解理解。
Transformer 模型主要分为两大部分:编码器和解码器。
- 编码器:接收输入序列(例如一个句子),并生成该序列的上下文表示。
- 解码器:接收编码器的输出以及自身已生成的部分输出序列(向右偏移一位),然后自回归地(一次一个词)预测下一个词的概率。
整个架构中有几个关键设计:
- 多头注意力层:在编码器和解码器中都使用了该层。解码器中还有一个掩码多头注意力层,用于防止模型在训练时“偷看”未来的词。
- 前馈神经网络:每个注意力层后面都接有一个全连接的前馈网络,用于进行非线性变换。
- 残差连接与层归一化:每个子层(注意力层、前馈层)的输出都会与输入进行相加(残差连接),然后进行层归一化。这有助于缓解梯度消失问题,并稳定训练过程。
- 堆叠结构:编码器和解码器都由 N 个(原论文中 N=6)完全相同的层堆叠而成,层与层之间不共享权重。
编码器和解码器的输入/输出维度始终保持一致(例如512维),这是为了确保残差连接能够顺利进行。


掩码多头注意力 🎭
上一节提到了解码器中的掩码注意力,本节中我们详细解释其作用。
在训练解码器时,我们已知完整的目标序列。但在预测第 t 个词时,模型只能依赖于第 1 到 t-1 个已生成的词,而不应“看到”第 t 个及之后的词。掩码多头注意力正是为了实现这一点。
其实现方式是在计算注意力分数(Softmax 之前)时,将未来位置(当前位置之后)的分数设置为一个极大的负数(例如 -1e9)。这样,在应用 Softmax 后,这些未来位置的注意力权重就会变为接近 0 的值,从而被有效地“屏蔽”。

例如,在翻译句子“I like planes”为德语时,当模型正在预测第二个词,它只能基于第一个词“Ich”进行计算,而“Fliege”和“gerne”这两个未来的词会被掩码掉。
解码器的最终输出会通过一个线性层映射到词汇表大小,再经过 Softmax 得到每个词的概率,通过交叉熵损失进行训练。
位置编码 📍
我们之前提到,自注意力机制本身对序列中词的位置不敏感。为了给模型注入位置信息,Transformer 引入了位置编码。

位置编码是一个与词嵌入维度相同的向量,它根据词在序列中的位置,通过正弦和余弦函数生成一组固定的值。这个位置编码向量会被直接加到对应的词嵌入向量上。
# 位置编码公式(简化示意)
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中,pos 是位置,i 是维度索引,d_model 是模型维度。
这样,不同位置就会获得独特的位置编码模式,使得模型能够区分“我喜欢飞机”和“飞机喜欢我”这种词序不同的句子。
层归一化
在 Transformer 中,每个“Add & Norm”层中的“Norm”指的就是层归一化。
层归一化与批归一化类似,但归一化的维度不同。批归一化是在批次维度上计算均值和方差,而层归一化是在特征维度(对于 Transformer,即跨注意力头或隐藏单元)上进行的。

对于某个层的输出向量 x,层归一化的计算如下:
- 公式:LayerNorm(x) = γ * (x - μ) / σ + β
其中 μ 和 σ 是该向量自身所有元素的均值和标准差,γ 和 β 是可学习的缩放和偏移参数。
层归一化有助于稳定训练过程,减少内部协变量偏移,并且对小批量大小不敏感。
注意力可视化 👀
最后,我们通过论文中的可视化图来直观理解注意力机制学到了什么。

下图展示了两个不同注意力头(Head 5 和 Head 6)在翻译句子时产生的注意力权重。颜色深浅表示注意力强度。

可以看到:
- 对于单词“law”,在 Head 5 中,它主要关注“European”和“law”本身;而在 Head 6 中,它的关注模式则完全不同。
- 这证明了多头注意力机制的有效性:不同的注意力头确实学会了关注句子中不同类型的关系和依赖,它们提供的信息是互补而非冗余的。

总结

本节课中我们一起学习了 Transformer 模型的核心架构。我们从基础的缩放点积注意力出发,逐步构建了多头注意力机制,并深入剖析了 Transformer 编码器-解码器结构中的各个关键部分,包括掩码注意力、位置编码、残差连接与层归一化。最后,我们通过注意力可视化看到了模型如何学习词与词之间的复杂关系。理解这一经典架构是掌握后续如 BERT、GPT 等现代 Transformer 变体模型的重要基础。
🧠 P162:L19.5.2.1 - 一些流行的Transformer模型:BERT、GPT与BART【概述】
在本节课中,我们将学习基于Transformer架构的几种流行模型,包括BERT、GPT和BART。我们将了解它们的基本思想、核心差异以及它们如何利用自注意力和自监督学习来取得成功。

我们已经学习了注意力机制、自注意力、多头注意力,以及它们如何共同构成Transformer架构。现在,让我们来看看基于这一架构的一些流行方法。例如,有BERT模型、不同类型的GPT模型(版本1、2和3),以及结合了BERT和GPT思想的BART模型。
这些模型本身的想法很有趣,但它们的结构并不非常复杂。它们本质上是对原始Transformer的一些小修改。由于这些模型非常流行,经常出现在新闻和讨论中,因此简要了解它们以及它们之间的区别是很有意义的。

🔄 Transformer架构回顾
首先,让我们简要回顾一下。下图展示的是《Attention Is All You Need》论文中的原始Transformer架构。我们将要讨论的BERT、GPT等方法都基于这一基本架构。当然,它们会进行一些修改,但从根本上说,BERT借鉴了编码器部分,而GPT借鉴了解码器部分。在接下来的视频中,我们将看到这些模型与这个基础模型的关系。

总的来说,我认为所有这些Transformer模型成功的关键有两点:
- 自注意力机制:用于编码长距离依赖或上下文信息。
- 自监督学习:用于利用大规模、未标记的数据集。
自监督学习有时也被称为无监督预训练。如今,“自监督学习”这个词变得更流行。我们在之前的课程中简单讨论过,其核心思想是:从一个大型未标记数据集中,根据数据自身的结构来构建标签,而不是依赖人工手动标注每个训练样本(这非常昂贵)。

例如,我们在Transformer模型中见过的一个任务是预测下一个词。这仍然是一种监督方法,因为我们使用分类损失来预测下一个词。但与常规监督学习相比,我们是从文本本身提取这个标签(即下一个词)。这就是为什么它被称为“自监督”学习——在某种意义上,是模型自己创建了标签,而不是由人类或其他过程创建的。
🚀 Transformer的训练范式
Transformer模型的训练方法通常可以分为两个步骤:
- 预训练:在大型未标记数据集上以自监督方式训练模型。
- 下游任务训练:在通常较小的、有标签的数据集上,针对特定下游任务训练模型。
下游任务可以是语言翻译、文本分类、文本摘要、问答等等。你可以为语言模型设计许多可能的下游任务。为此,你需要一个较小的、有标签的数据集进行监督学习。
例如,可以想象我们之前用过的电影评论数据集。预训练阶段可以在数千本书籍上以自监督方式进行。然后,我们可以使用仅包含10万条评论的小型电影评论数据集来训练模型。这有点类似于我们之前讨论过的迁移学习。
针对下游任务的训练,主要有两种方法:
以下是两种主要方法:
- 基于微调的方法:更新整个模型的参数。设想一下,你有一个通过自监督学习预训练好的模型(通常模型末端也有一些全连接层)。对于微调,假设你有一个分类任务(如电影评论分类),你会在模型上添加一个分类层,然后在这个有标签的数据集上训练模型,并更新所有参数。
- 基于特征的方法:这种方法与微调相关,但不同之处在于,你不更新整个预训练模型。相反,你从模型的最后几层(可能是最后一层或最后四层等,具体方法不同)提取嵌入表示。这些是上下文嵌入,它们比普通的词嵌入更好,因为它们包含了句子的上下文信息。你生成这些嵌入后,将其视为固定的特征输入,然后训练一个新的模型(可以是一个简单模型,如在BERT论文中,他们甚至使用了LSTM)来完成分类等任务。预训练模型本身保持不变。
当我们讨论BERT模型时,会进一步探讨基于特征的方法。这里只是一个概述。
📺 后续内容预告
在接下来的视频中,我将讨论这些不同类型的模型。我原本想在一个视频中涵盖所有内容,但发现材料有点多。当然,这部分是可选内容,你可以自行决定是否观看。
我将在接下来的视频中涵盖的主题包括:
- 原始的GPT模型(生成式预训练Transformer)。
- BERT模型(双向编码器表示)。
- GPT-2和GPT-3版本。

GPT模型是单向的(基于下一个词预测),而BERT是双向的,这是它们的主要区别。BART模型则结合了BERT的双向特性和GPT的单向自回归行为。
之后,我还会再谈谈Transformer的现状、计算效率等方面。在那之后,我们会看一个代码示例。关于这些流行Transformer模型(BERT, GPT, BART)的讨论,我们总共有六个视频。


总结:本节课我们一起学习了Transformer架构的几种重要衍生模型:BERT、GPT和BART。我们回顾了Transformer的核心——自注意力机制和自监督学习,并了解了模型训练的两阶段范式(预训练与下游任务微调)以及两种主要的下游任务适应方法(微调与特征提取)。这些模型通过不同的方式利用Transformer的编码器或解码器部分,在自然语言处理领域取得了巨大成功。

🧠 课程 P163:L19.5.2.2 - GPT-v1:生成式预训练Transformer
在本节课中,我们将学习生成式预训练Transformer模型,即GPT-v1。我们将了解其核心思想、两阶段训练过程、模型架构以及它在不同下游任务上的应用。

🏗️ 模型概述与核心思想
GPT模型由OpenAI开发,其共同特点是它们都是单向的。这些模型主要被训练来预测句子中的下一个词。
截至目前,共有三个版本:GPT-1于2018年发布,拥有1.1亿参数;GPT-2于2019年发布,拥有15亿参数;GPT-3于2020年发布,拥有1750亿参数。本视频将重点介绍GPT版本1。
GPT模型背后的一个主要假设是,缺乏标注数据是限制大型语言模型性能提升的主要瓶颈之一。
因此,论文中提出了一个两阶段的训练过程,他们总体上称之为半监督学习。半监督学习本质上意味着利用有标签和无标签的数据。
这里的半监督过程就是我在上一个视频中简要概述的两步过程:第一步是预训练,第二步是微调。预训练阶段他们使用一个大型的无标签数据集来训练Transformer模型,如今这也被称为自监督学习。第二步是判别式微调,他们使用一个针对你感兴趣的任务(例如电影评论分类)的较小标签数据集,这本质上是迁移学习的一种形式。
预训练本身发生在一个他们收集的数据集上,他们称之为书籍语料库数据,包含7000本未出版的书籍。他们使用的架构基于我们讨论过的原始Transformer模型的解码器架构,即“Attention is All You Need”论文中提出的那个。

🔧 模型架构与任务适配
以下是GPT架构及其用于微调的不同下游任务的可视化。让我们先从架构开始。
在左侧,你可以看到这看起来就像原始Transformer论文中的架构。这里有跳跃连接、层归一化、前馈层、掩码多头自注意力层等等。因此,在这方面,它只是原始Transformer论文中的解码器。
不同之处在于,这里有12个Transformer块,而不是“Attention is All You Need”论文中的6个。
另一个需要注意的是,输出端有两个框。一个称为文本预测,另一个是任务分类器。
同样,首先进行的是下一个词预测的预训练。预训练完成后,再针对不同的下游任务进行微调。
在微调期间,即在预训练完成后,你可以保留文本预测部分,并同时训练两者。这意味着你可以在更新模型以进行任务预测的同时,也保留用于预测下一个词的损失函数。
这里有两个不同的线性层:一个用于下一个词预测,另一个用于任务预测。他们实验了这样做是好是坏,我将在下一张幻灯片中展示结果。
现在,让我们专注于不同的任务。假设我们已经完成了下一个词预测的预训练,现在开始针对不同任务进行微调。
右侧是不同任务的可视化,展示了输入应如何格式化。
- 分类任务:他们提供一个起始标记、主要文本和一个提取标记(类似于结束标记或序列结束标记)。然后将其输入Transformer,并通过一个额外的线性输出层。这本质上就像一个分类器,最后可以使用Softmax激活和交叉熵损失进行训练。
- 蕴含任务:这类似于逻辑蕴含。提供一个前提分隔符和假设,本质上也是一个真/假分类任务。
- 相似性任务:比较两段文本是否相似或相似度如何。他们提供文本1、分隔符和文本2。为了保持对称性,可能还会将文本2放在文本1之前。将两者通过同一个Transformer,汇总嵌入表示,然后通过一个全连接层。他们可能会使用像L2距离这样的度量,来最小化相似文本之间的距离,最大化不同文本之间的距离。
- 多项选择任务:提供上下文和一个可能的答案,然后是上下文和另一个答案,依此类推。本质上这也是一个分类任务,从N个可能的答案中选择最可能是该上下文答案的那个。
主要思想仍然是两步:第一步是预训练,训练模型预测下一个词;第二步是微调,针对这些下游任务进行微调。
📊 消融实验与性能分析
他们进行了一项消融研究,探讨在微调时移除下一个词预测任务是否会使性能变得更好或更差。
在这里,他们将带有辅助语言模型的Transformer称为完整模型。辅助语言模型本质上就是这里的文本预测部分。

他们得到了相当不错的性能,计算出的分数为74.7。
然后,他们测试了没有预训练的Transformer,即仅进行微调而不进行预训练。可以看到,没有预训练的Transformer模型性能显著或大幅下降。因此,预训练确实有帮助。


接着,他们测试了没有辅助语言模型的Transformer,可以看到性能甚至更好一点。
本质上,如果你在下一个词预测上预训练模型,然后在微调时去掉这个部分,只专注于微调,可以看到性能在某些任务上甚至更好一点,但并非所有任务。

为了比较,他们还测试了常规的LSTM模型。可以看到LSTM在这里的表现比完整模型更差。

🎯 课程总结
本节课我们一起学习了GPT版本1模型。按照今天的标准,这已经是一个非常旧的模型,因为已经有了GPT版本2和3。但它是一个很好的起点。
我们了解了GPT-v1的核心思想是利用两阶段(预训练和微调)的半监督学习方法来克服标注数据不足的瓶颈。其架构基于Transformer解码器,并针对分类、蕴含、相似性、多项选择等多种下游任务设计了特定的输入格式和微调方法。实验表明,预训练阶段对最终性能提升至关重要,而在微调阶段是否保留语言模型辅助目标则需根据具体任务权衡。


在下一个视频中,在介绍其他GPT模型之前,我们将讨论BERT模型,它使用Transformer的方式略有不同。我之所以在介绍其他GPT版本之前先介绍这个,是因为在BERT论文中,他们特别将他们的模型与GPT-1进行了比较。从某种程度上说,这是按时间顺序排列的:我们有GPT版本1、BERT、GPT版本2和3。
🧠 P164:L19.5.2.3 - BERT:来自Transformer的双向编码器表示
在本节课中,我们将学习BERT模型。BERT是“Bidirectional Encoder Representations from Transformers”的缩写,由谷歌研究团队于2018年提出。它的核心创新在于其双向特性,能够同时利用上下文信息进行预训练,这使其在多项自然语言处理任务上取得了突破性进展。

上一节我们介绍了GPT-1,它是一个单向的Transformer模型,其预训练任务是预测下一个词。BERT则与之不同,它是一个双向的Transformer编码器。
BERT的主要架构与原始Transformer论文中的编码器部分基本相同,都包含多头注意力机制和前馈神经网络。关键区别在于BERT的双向预训练方式,它通过一种称为“掩码语言模型”的任务来实现。
为了理解BERT的双向预训练,我们来看一个简单的概念。它不像GPT那样从左到右顺序处理,而是随机掩盖输入句子中的一些词,然后让模型根据所有未被掩盖的词来预测这些被掩盖的词。这迫使模型同时利用左右两侧的上下文信息。
以下是BERT预训练的两个主要任务:
- 掩码语言模型:随机掩盖输入序列中15%的词汇,让模型预测这些被掩盖的词。
- 下一句预测:给定两个句子A和B,让模型判断B是否是A的下一句。这有助于模型理解句子间关系。


BERT的输入表示由三部分组成:


- 词嵌入:每个词的向量表示。
- 段落嵌入:用于区分句子A和句子B。
- 位置嵌入:表示每个词在序列中的位置。



这些嵌入相加后,形成BERT的输入。


BERT模型有不同规模的版本,例如BERT-Base包含12层Transformer编码器,而BERT-Large包含24层。模型规模越大,通常性能越好,但计算成本也越高。



在完成预训练后,BERT可以通过简单的微调,适配到各种下游任务,如文本分类、问答和命名实体识别。你只需要在预训练好的BERT模型上添加一个与任务相关的输出层,然后在特定任务的数据集上进行少量训练即可。




本节课中,我们一起学习了BERT模型。我们了解到BERT的核心是双向编码器表示,它通过掩码语言模型和下一句预测任务进行预训练,从而能够深度理解上下文。这种设计使其成为自然语言处理领域一个强大且通用的基础模型。

课程 P165:L19.5.2.4 - GPT-v2:语言模型是无监督的多任务学习者 🧠
在本节课中,我们将要学习GPT的第二代版本(GPT-2)。我们将了解它与第一代GPT的主要区别,其核心架构的改进,以及它如何通过“零样本迁移”技术,在不进行下游任务微调的情况下,直接完成多种自然语言处理任务。

现在,我们来谈谈GPT的第二代版本。

首先,我们来回顾一下之前讲过的GPT第一代版本。GPT代表“生成式预训练Transformer”,是由OpenAI开发的模型。它的一个基本特性是单向模型,训练目标是预测句子或序列中的下一个词。这与BERT模型的双向语言建模方法形成对比。

之前我们讨论的GPT-1版本是一个拥有1.1亿参数的模型,于2018年发布。现在,我们将讨论一年后发布的第二代版本。GPT-2拥有15亿参数,规模显著增大,是前代的十倍以上。我们不会深入探讨这个模型的所有细节,但如果你有兴趣,这里有一个原始论文的链接,供你在观看本视频后进一步学习。

好的,再次回顾一下GPT-1的架构。GPT-1包含一个具有12个Transformer块的解码器。训练分为两步:首先是预训练,进行下一个词的预测;然后是微调,针对下游任务(如分类、蕴含、相似性和多项选择)在带标签的数据集上进行。预训练则是在大规模无标签数据集上进行的。
我之所以回顾这些,是因为GPT-2对这个流程做了一个不小的改变。GPT-2背后的关键概念是:首先,它与GPT-1相似,也是单向的,进行下一个词的预测。


然而,与GPT-1相比,GPT-2模型规模显著更大。他们的论点是模型越大越好。他们通过实验图表表明,参数越多,性能越好。同时,更大的数据集也有助于提升模型性能。
但关键的不同点在于,GPT-2不再进行微调,而是使用了所谓的零样本迁移。这与零样本学习有些关联,但并不完全相同。这里的核心思想是,通过为模型提供一些上下文和输入,让它直接执行任务。稍后我会展示一个例子。这里需要强调的一点是,他们去掉了微调步骤。


GPT-2的架构本身总体上与GPT-1相似,当然规模更大,但仍然是基于原始的Transformer解码器。他们对层归一化和残差层做了一些小的重新排列,但这些是次要的实现细节。他们还增大了词汇表,几乎是原来的两倍。另一个细节是,他们将上下文长度增加了一倍,从512个输入词元增加到1024个,从而能捕获更多上下文信息。考虑到零样本迁移需要将任务描述与输入一起提供,我认为这也是他们增大上下文长度的原因之一。总的来说,这些改动使得模型达到了15亿参数,而原始模型已经是拥有1.1亿参数的大型模型。


接下来看看他们使用的数据集。如前所述,他们使用了更大的数据集,即所谓的“网络文本”数据集,包含数百万个网页。他们同时强调提高数据质量很重要。那么他们是如何提高质量的呢?这些网页基于Reddit帖子,他们收集了“karma”值大于等于3的帖子,认为这能链接到质量尚可的网站,而非垃圾帖子。之后,他们进行了去重和其他网站预处理、清洗等工作。最终,他们从Reddit获得了4500万个网站链接,经过预处理和清洗后,得到了800万份文档,总计约40GB的文本。



这里有一个我之前提到的零样本任务迁移的例子。与GPT-1不同,GPT-2没有针对特定任务的指令或结构调整。我之前展示的是GPT-1需要进行微调和结构调整。现在,他们不再进行任何微调。不过,我很难找到一个非常具体的例子。说实话,我不太确定具体是如何操作的。我在Hugging Face的代码库中找到了一个零样本主题分类的例子。

Hugging Face是一家专注于语言模型,特别是基于Transformer的语言模型的公司。我想我也会展示一个BERT模型的例子和代码示例,就像上一个视频那样。这里,他们有一个很好的在线界面,可以尝试零样本主题分类。你可以提供一些输入,然后提供你自己的标签。这些标签是模型之前未必见过的,因为它只训练过预测下一个词。之后,你提供用逗号分隔的可能主题。这里我提了一个随机问题:“割下来的草是什么颜色?”,并提供了可能的标签:绿色、红色、蓝色、无色、粉色、紫色。模型在这里回答正确了。


这很酷。接下来是论文中关于模型规模的一些结果。本质上,在所有四个图表中(顶部三个),Y轴是某些评估标准(数值越大越好),X轴是语言模型的参数数量。蓝线代表他们的GPT模型。可以看到,在所有情况下,曲线都是上升的,基本上说明参数越多,性能越好。但也可以看到,这些是针对不同任务的:阅读理解、翻译、摘要和问答。GPT-2并非在所有任务上都达到了其他更专门方法的性能。但这也不一定是重点。论文旨在展示,即使没有微调,这些方法也能自动学会理解文本,这实际上相当令人印象深刻。
这里还有一些其他任务的结果。在这种情况下,GPT-2表现得相当好。加粗的数字是他们的性能表现,可以看到他们在许多任务上超越了其他模型。除了一个任务(BW任务),论文中的解释是,在这个BW数据集中,句子被打乱了顺序。由于Transformer模型的一个优势就是能够考虑上下文,而打乱句子顺序破坏了这种上下文,这可能是GPT-2在该任务上表现不佳的一个可能原因。

好的,以上就是关于GPT-2的简短介绍。接下来还有一个关于GPT-3的短视频。



本节课中,我们一起学习了GPT-2的核心内容。我们了解到GPT-2是一个规模显著增大的单向语言模型,其关键创新在于摒弃了针对下游任务的微调,转而采用零样本迁移的方法。通过提供任务描述作为上下文,模型能够直接执行多种NLP任务。同时,我们也看到了更大规模的模型和更高质量的数据集对性能提升的重要性。

📚 课程 P166:L19.5.2.5 - GPT-3:语言模型是少样本学习器
在本节课中,我们将学习GPT系列模型的第三个重要版本——GPT-3。我们将了解其相较于前代模型的规模扩展、架构调整、训练数据变化,以及其核心创新:少样本学习能力。我们将重点探讨GPT-3如何在不进行微调的情况下,仅通过提供少量示例(上下文学习)就能出色地完成各种任务。
🏗️ GPT-3 模型概览

GPT-3是GPT系列的第三个主要版本。正如所有美好的事物都成三出现一样,GPT-3也带来了巨大的飞跃。

GPT-3的模型规模远超之前的版本。其参数量从GPT-2的15亿(1.5 billion)激增至1750亿(175 billion)。若想了解更详细的技术细节,可以查阅发布在arXiv上的原始论文。该论文篇幅很长,大约有40到50页。
⚙️ 架构与改进
上一节我们介绍了GPT-3的巨大规模,本节中我们来看看其具体的架构变化。


GPT-3的整体架构与GPT-2相似,但规模更大,层数更多。主要改进包括:
- 上下文长度:从1024个输入标记增加到2048个。
- 词嵌入维度:从1600大幅增加至约12800。
- 注意力机制:为了应对计算量随规模二次增长的问题,GPT-3采用了稀疏Transformer中的注意力模式,以提升计算效率。关于稀疏Transformer的更多细节,可以参考相关论文。
📊 训练数据集
了解了模型架构后,我们来看看支撑如此庞大模型训练的数据。


GPT-3使用的训练数据集规模也显著扩大。以下是其主要组成部分及权重:
- WebText2:包含约190亿个标记,是GPT-2所用WebText数据集的升级版。
- Common Crawl:一个经过过滤的版本,包含约4100亿个标记。
- 书籍数据集。
- 维基百科数据集。
这些数据集在总训练过程中被赋予了不同的混合权重。
🔄 隐式任务与上下文学习

GPT-3的核心思想在于其学习方式。它将无监督的预训练(自监督学习)称为外部循环。

研究者认为,由于训练文本中蕴含了丰富的信息,模型在进行常规语言建模训练时,会隐式地学习如何执行任务。这种能力被称为上下文学习:模型通过观察输入上下文中的模式和示例,来理解并执行新任务,例如拼写纠正等。
🎯 零样本、单样本与少样本学习
与GPT-2一样,GPT-3在预训练后不进行任务特定的微调。其创新在于扩展了任务提示的方式。

以下是三种不同的上下文学习设置:
-
零样本学习:只给模型任务描述和输入,不给示例。
- 提示示例:
翻译英文为法文:cheese =>
- 提示示例:
-
单样本学习:提供任务描述和一个输入-输出示例。
- 提示示例:
翻译英文为法文:sea otter => loutre de mer cheese =>
- 提示示例:
-
少样本学习:提供任务描述和多个输入-输出示例(通常为10-100个,以填满上下文窗口为限)。
- 提示示例:
翻译英文为法文:sea otter => loutre de mer, peppermint => menthe poivrée, plush giraffe => girafe peluche cheese =>
- 提示示例:
这与传统的监督学习(如GPT-1的微调)有本质区别。传统方法需要根据示例计算梯度并更新模型权重,而GPT-3只是将示例作为输入文本的一部分,模型需要自行推断出该执行的任务。
📈 性能表现
提供示例对模型性能有显著提升。


在下图的性能对比中,横轴是模型参数量,从少到多。可以清晰地看到:
- 单样本和少样本学习的性能始终优于零样本学习。
- 当使用庞大的GPT-3模型并结合少样本示例时,它甚至能在一些任务(如 trivia QA)上超越之前专门训练过的 state-of-the-art 模型。这非常令人印象深刻,因为GPT-3本身并未针对问答进行训练,它仅仅是通过观察几个例子就“学会”了如何回答问题。
✅ 总结
本节课中我们一起学习了GPT-3的核心内容。


GPT-3的核心贡献在于两方面:
- 将语言模型的规模推向了一个新的极致(1750亿参数)。
- 系统性地探索并证明了通过提供单样本和少样本示例(上下文学习),大语言模型可以在不进行梯度更新微调的情况下,出色地完成多种任务。



接下来,我们将介绍一个结合了BERT和GPT概念的早期但有趣的模型。


课程 P167:L19.5.2.6 - BART:结合双向与自回归 Transformer 🤖

在本节课中,我们将学习 BART 模型。BART 是一个结合了 BERT 的双向编码器思想和 GPT 的自回归解码器思想的模型,旨在同时胜任理解类任务和生成类任务。
语言模型的数量几乎是无限的,我们无法全部涵盖。但 BART 是一个有趣的模型,值得介绍。BART 的全称是 Bidirectional and AutoRegressive Transformers,它结合了 BERT 和 GPT 的核心概念。
上一节我们介绍了 BERT 和 GPT 各自的特性,本节中我们来看看 BART 如何将它们融合。


BART 模型由 Facebook AI Research 于 2019 年提出。虽然其性能可能已非最前沿,但其核心思想依然很有价值。其背后的逻辑是:BERT 的双向(自编码)特性使其擅长需要理解整个句子的下游任务,例如分类任务。然而,BERT 在生成任务(如问答、文本生成)上表现不佳,因为生成任务需要模型基于已生成的内容来预测下一个词。
另一方面,GPT 的单向自回归方法更擅长生成文本,但在需要同时考虑整个输入序列的任务(如分类)上则不如 BERT。
BART 的论点在于,它结合了两者的优点,实现了“鱼与熊掌兼得”。它本质上是一个 BERT 编码器 加上一个 GPT 解码器,并引入了一些额外的噪声变换。

上图来自原论文,阐释了这一概念。左侧是双向编码器,右侧是自回归解码器。如果你回想一下,这本质上重构了原始 Transformer 模型的结构。在原始 Transformer 的“Attention is All You Need”论文中,编码器接收整个文本输入,解码器则通过移位来预测下一个词。

当然,BART 并非完全相同,因为它额外引入了噪声变换。这使得 BART 的编码器更像一个去噪自编码器,其概念与去噪自编码器讲座中的内容非常相似:扰动输入,然后尝试重建原始输入。
以下是 BART 论文中使用的几种噪声变换示例:


- 词元掩码:类似于 BERT 的
[MASK]操作。 - 句子置换:打乱文档中句子的顺序。
- 文档旋转:随机选择一个词元,然后旋转文档使其以该词元开头。
- 词元删除:随机删除一些词元。
- 文本填充:用单个
[MASK]标记替换一段连续的文本。
模型的任务是重建回原始的、未受干扰的输入。

论文研究了不同噪声变换对性能的影响。基础模型配合不同的单一噪声变换(如仅使用词元掩码或词元删除)表现都很好。结合使用两种变换方法通常能获得最佳性能。
在预训练阶段,模型学习如何从被噪声干扰的输入中重建原始文本。接下来我们看看如何针对不同下游任务进行微调。


对于微调,BART 有两种主要设置:




- 左侧 - 分类/判别式任务:例如文本分类。编码器处理输入,解码器的最终输出用于预测一个标签。
- 右侧 - 生成式任务:例如机器翻译、文本摘要。这里需要一个小的修改:在预训练的 BART 编码器前,额外添加一个随机初始化的编码器。这个新编码器接收源语言(如英语)的词嵌入,其输出再送入预训练的 BART 编码器-解码器结构,以生成目标语言(如法语)的文本。

BART 模型表现非常出色。在判别式任务(如分类)上,它与当时强大的 BERT 变体(如 RoBERTa)性能相当,甚至在某些任务上更优。


更重要的是,在生成式任务上,如抽象问答、对话响应生成和文本摘要,BART 达到了当时的顶尖水平。这证明了它确实结合了 BERT 和 GPT 的优势。
本节课中我们一起学习了 BART 模型。BART 通过结合 BERT 的双向编码器和 GPT 的自回归解码器,并引入去噪预训练目标,成为一个能同时出色完成理解(如分类)和生成(如翻译、摘要)任务的通用架构。它体现了将不同模型优点融合以取得更全面能力的思路。


关于这些流行的 Transformer 模型,还有更多内容可以探讨。



课程 P168:L19.5.2.7 - Transformers的近期增长趋势 📈

在本节课中,我们将探讨近年来广受欢迎的大规模语言模型的发展趋势。我们将了解模型规模的增长、面临的挑战以及旨在提升效率的各种创新方法。
上一节我们介绍了Transformer模型的基础架构,本节中我们来看看该领域近期的动态与趋势。
当然,我无法涵盖所有模型,因为数量实在太多,可能达到成百上千个。但可以再提及几个有趣的模型,特别是那些处理长序列的模型。

例如,Transformer-XL。它是一个类似于GPT的仅解码器模型,同样被训练用于预测句子中的下一个词。然而,与常规的GPT模型不同,它使用了一种隐藏状态来连接不同的输入片段。回想一下GPT-1,它使用512个输入标记。而Transformer-XL可以连接多个这样的512标记片段。在某种程度上,它有点像在Transformer模型中引入了RNN的思想。这是一种处理更长输入的方法。
另一种方法是Longformer(长文档Transformer)。注意力机制的复杂度随输入规模呈二次方增长,这相当昂贵。Longformer开发了一种机制,其复杂度仅随序列长度线性增长,而非二次方。因此,它能够处理数千个标记的长文本段落,其规模与RoBERTa类似。当然,这只是众多使Transformer更高效、以处理更长输入的方法之一。
总体而言,这些语言模型的一个趋势是变得越来越大。这是一张来自Hugging Face的图表(截至2019年)。我们可以看到从ELMo、GPT(1.1亿参数)、BERT(3.4亿参数)、GPT-2(15亿参数)到GPT-3(1750亿参数)的发展。模型规模正在急剧增长。
幸运的是,也有一些方法专注于提升效率。例如,可以看到一些更小的模型,特别是DistilBERT,它是BERT的一个精简版本,让技术再次变得更容易获取。因为这些模型如此庞大,我们普通人可能永远无法训练它们,除非你能访问非常庞大的计算资源。即使你能访问数千个GPU,训练这些大规模语言模型也可能非常棘手,需要大量的调试和工程工作才能扩展到那么多GPU,这并非即插即用。

如今有一个有趣的现象。大约两个月前有一篇文章提到,GPT-3现在每天生成约45亿个单词。他们围绕GPT-3提供了商业化的API,这是一个非常流行且广泛应用于现实场景的模型。

如前所述,模型规模也带来了问题。有一篇关于训练大型自然语言处理模型成本的论文很有意思。其中给出了一些惊人的数字:对于1.1亿参数的GPT-1,估计训练成本在2.5万到5万美元之间;而像BERT或GPT-2这样的模型,训练成本则高达数十万甚至数百万美元。这相当令人印象深刻,但考虑到环境成本,也多少有些遗憾。当然,这本身也是一种研究,观察其发展方向很有趣,但我希望事情不要那么昂贵和耗电。
幸运的是,一些人也专注于开发更高效的模型。例如,Reformer方法用局部敏感哈希机制替代了点积注意力,从而实现了O(N log N)的复杂度,而不是O(N²)的内存成本。还有ALBERT,它是一个比BERT小5倍但达到相同性能的版本。这些都是不同的方法。
还有之前简要提到的Sparse Transformer,也就是前面提到的Longformer,它拥有随序列长度线性增长的注意力机制。

此外还有DistilBERT。方法多种多样,这里只是重点介绍几种。

以ALBERT模型为例,它也专注于通过训练后的剪枝来进行压缩。即先训练模型,然后进行剪枝以获得更小的模型。

好了,这就是对不同大规模语言模型的一次快速巡览。在下一个视频中,可能会有一个简短的代码示例,然后结束这门关于RNN、注意力和Transformer模型的漫长课程。



本节课中我们一起学习了大规模语言模型的近期趋势,包括模型规模的指数级增长、随之而来的计算成本与效率挑战,以及业界为应对这些挑战所提出的多种创新模型与方法(如Transformer-XL、Longformer、高效模型压缩等)。理解这些趋势有助于我们把握自然语言处理领域的发展方向。
🎬 课程 P169:L19.6 - 基于 PyTorch 的 DistilBERT 电影评论分类器
在本节课中,我们将学习如何使用 Hugging Face 的 Transformers 库,基于预训练的 DistilBERT 模型来构建一个电影评论情感分类器。我们将通过代码实践,了解如何加载预训练模型、进行下游任务的微调,并将其性能与我们之前学习的 LSTM 模型进行对比。


📚 概述

我们之前的长篇讲座涵盖了使用循环神经网络和 Transformer 进行序列建模。我们讨论了带注意力的 RNN、自注意力机制、多头注意力(这些是原始 Transformer 模型中的核心概念),以及一些流行的 Transformer 模型,如 BERT 和 GPT。
BERT 模型更擅长判别式建模任务(如分类预测),而 GPT 则更适用于文本生成。本节课,我们将使用熟悉的电影评论数据集,展示一个简单的 BERT 应用示例,并与我们在第 15 讲中训练的 LSTM 模型性能进行比较。


Hugging Face 公司开发了一个被广泛使用的开源 Transformers 库。本节课我们将利用这个库。





🛠️ 环境与数据准备


首先,我们需要安装必要的库并准备数据。



以下是需要导入的核心模块:


from transformers import DistilBertTokenizer, DistilBertForSequenceClassification
import torch
from torch.utils.data import Dataset, DataLoader



我们设置训练轮数为 3,因为模型较大,训练较慢。DistilBERT 是 BERT 的一个精简版,参数量减少 40%,速度提升 60%,同时保留了约 95% 的原始性能。



我们加载预处理好的电影评论 CSV 数据集。该数据集包含评论文本和情感标签(正面/负面),共有 50,000 条评论。




我们将数据划分为训练集(35,000条)、验证集(5,000条)和测试集(10,000条)。



🔤 文本编码与数据集构建

上一节我们准备好了数据,本节中我们来看看如何将文本转换为模型可以处理的格式。



我们使用与预训练模型配套的分词器(Tokenizer)来处理文本。分词器会将文本转换为词汇表对应的 ID,并处理填充(Padding)和截断(Truncation),使所有序列长度统一为 512。



以下是创建编码和构建数据集的步骤:




# 加载预训练分词器
tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')


# 对文本进行编码
encodings = tokenizer(list(train_texts), truncation=True, padding=True, max_length=512)



# 构建 PyTorch Dataset
class ReviewDataset(Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item['labels'] = torch.tensor(self.labels[idx])
return item
def __len__(self):
return len(self.labels)
train_dataset = ReviewDataset(train_encodings, train_labels)


然后,我们创建 DataLoader 来批量加载数据。由于模型较大,我们设置较小的批量大小(例如 16)以避免内存问题。






🤖 加载与配置模型


现在,我们来加载预训练的 DistilBERT 模型用于序列分类任务。


Transformer 模型的训练通常分为两步:首先在大规模无标签数据上进行预训练,然后在特定下游任务的小规模有标签数据上进行微调。我们直接加载 Hugging Face 提供的预训练模型,并在电影评论数据上进行微调。


以下是加载模型和优化器的代码:


# 加载预训练模型
model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased', num_labels=2)

# 使用支持权重衰减的 AdamW 优化器
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)



# 使用线性学习率调度器
from transformers import get_linear_schedule_with_warmup
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, num_training_steps=len(train_dataloader)*epochs)


我们使用 DistilBertForSequenceClassification 来加载模型。优化器选择了 AdamW,它通过解耦权重衰减来实现更好的 L2 正则化。同时,我们采用了线性学习率调度器,这些设置通常能带来更好的性能。







🏋️ 模型训练与评估


我们将开始训练模型。训练循环与我们之前构建的类似,但需要注意 BERT 模型前向传播的输入和输出格式。



以下是训练循环的核心部分:


def train_epoch(model, data_loader, optimizer, scheduler, device):
model.train()
total_loss = 0
for batch in data_loader:
# 将数据移至设备(如GPU)
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
# 前向传播
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss
logits = outputs.logits
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
scheduler.step()
total_loss += loss.item()
return total_loss / len(data_loader)



模型的前向传播会返回一个元组,包含损失值(loss)和逻辑回归值(logits)。我们直接使用返回的损失进行反向传播。在计算准确率时,我们使用 logits 来获取预测的类别标签。


训练过程比 LSTM 慢很多(约20分钟/轮),但最终在测试集上达到了约 92% 的准确率。




📊 性能对比与进阶技巧




我们将 DistilBERT 的性能与之前第15讲中的 LSTM 模型进行对比。



LSTM 模型训练速度极快(每轮不到半分钟),在15轮训练后达到了约 89% 的测试准确率。虽然比 DistilBERT 的 92% 略低,但其效率非常高。



为了进一步提升 DistilBERT 的性能,我们尝试了 Hugging Face 推荐的两个技巧:
- 使用 AdamW 优化器代替普通 Adam。
- 使用线性学习率调度器。



这些调整将测试准确率提升到了约 93%。
此外,Hugging Face 的 Transformers 库还提供了一个非常方便的 Trainer 类,它封装了训练、评估和多 GPU 支持等功能,可以进一步简化代码并可能带来微小的性能提升。







🎯 总结


本节课中,我们一起学习了如何使用 Hugging Face 的 Transformers 库来微调预训练的 DistilBERT 模型,完成电影评论情感分类任务。

我们回顾了关键步骤:
- 准备数据:加载并划分数据集。
- 文本编码:使用预训练分词器将文本转换为模型输入。
- 加载模型:加载
DistilBertForSequenceClassification进行下游任务微调。 - 训练与评估:设置优化器、调度器,编写训练循环,并评估模型性能。
- 性能对比:与高效的 LSTM 模型对比,展示了预训练 Transformer 模型的强大性能。



核心思想是利用预训练模型在大规模数据上学到的通用语言知识,通过微调使其适应特定的下游任务。Hugging Face 库提供了丰富的预训练模型和便捷的工具,是探索自然语言处理应用的强大资源。

深度学习课程 P17:L2.4 - 深度学习硬件与软件概览 🖥️🔧
在本节课中,我们将简要介绍深度学习领域的硬件与软件发展现状。我们将了解当前主流的计算硬件,并回顾深度学习框架的演进历史。
硬件概览 💻
上一节我们讨论了深度学习的基础概念,本节中我们来看看支撑这些算法运行的硬件设备。

目前,深度学习任务主要依赖于图形处理器(GPU)进行计算。英伟达(Nvidia)公司每年都会推出新的GPU型号,这些设备因其强大的并行计算能力,成为深度学习研究和应用的主流选择。

然而,硬件领域也在不断发展。例如,谷歌公司开发了张量处理单元(TPU)。TPU是一种专为特定任务优化的专用处理器,通常部署在谷歌云服务器上。虽然单个TPU在理论上可能比GPU成本更低,但其强大之处在于能够将多个TPU组合起来协同工作。
此外,ARM公司也在研发机器学习处理器。苹果新款MacBook搭载的ARM处理器在推理任务上表现出色,但在模型训练方面,GPU目前仍是行业标准。
值得一提的是,一家名为Graphcore的新公司推出了智能处理单元(IPU)。这种处理器特别擅长处理图神经网络相关的计算任务。我们可以这样理解不同处理器的专长:
- CPU:擅长处理标量(单个数值)的简单计算。
- GPU:擅长处理向量(数组)的并行计算。
- IPU:擅长处理图结构数据的计算。
总而言之,尽管有众多新兴硬件,GPU目前仍是深度学习领域最普遍使用的计算设备。
软件演进 📜
了解了硬件之后,我们再来看看深度学习软件,特别是框架的发展历程。这对于理解当前主流工具(如PyTorch)的定位非常有帮助。
以下是深度学习框架发展的一个简要时间线:
- 2015年之前:Theano是当时的主流框架。为了简化其使用,出现了像Lasagna和Keras这样的高级封装库。
- 2015年左右:谷歌发布了TensorFlow。同时,Keras扩展了功能,开始支持TensorFlow和微软的CNTK等后端。此外,还出现了Caffe(擅长部署)、Chainer和DyNet(支持动态计算图)等专用库。
- 随后几年:Theano停止开发,TensorFlow成为主流并整合了Keras。在TensorFlow 2.0中,Keras成为了其默认的高级API,并且加入了即时执行模式(Eager Mode),这使得其使用体验更接近PyTorch的动态图模式。
- PyTorch的崛起:PyTorch从Chainer等库中汲取了动态计算图的理念,因其易用性而受到研究人员广泛欢迎。后来,PyTorch通过整合Caffe2增强了生产部署能力,并增加了ONNX支持(一种模型交换标准)和图模式(Graph Mode),使其在保持研究灵活性的同时,也适合产品化部署。
- 新兴工具:谷歌还开发了JAX库,它是一个可以在GPU上运行、具有自动微分和即时编译功能的较低层库。虽然其上也有像Flax这样的深度学习封装,但PyTorch目前仍是功能最全面、最成熟的库之一。
简而言之,TensorFlow通过加入Eager Mode变得更像PyTorch,而PyTorch通过加入Graph Mode变得更像TensorFlow,两者如今在功能上已非常相似。对于初学者和研究人员而言,PyTorch因其易用性和活跃的社区,是目前非常推荐的选择。
总结 📝
本节课我们一起学习了深度学习的硬件与软件生态。
- 在硬件方面,GPU是当前进行深度学习计算的主力,同时TPU、IPU等专用处理器也在不断发展。
- 在软件方面,我们回顾了从Theano、TensorFlow到PyTorch等主流框架的演进历程,了解了它们各自的特点和融合趋势。


掌握这些背景知识,有助于我们更好地选择和使用工具,为后续的深度学习实践打下基础。在接下来的内容中,我们将开始学习神经网络的基础单元——感知机。
深度学习课程 P18:L2.5 - 深度学习的当前趋势 📈
在本节课中,我们将简要了解深度学习领域近年来的主要研究趋势。这些趋势代表了该领域最前沿的发展方向,虽然部分内容超出了本课程的核心范围,但了解它们有助于你把握深度学习的未来脉络。
上一节我们回顾了深度学习的发展简史,本节中我们来看看当前最受关注的一些研究方向。

自监督学习 🤖
自监督学习在过去几年变得非常流行。上一讲我简要解释了自监督学习的工作原理。本质上,它是关于利用未标记数据,通过创建一些人工标签,然后将其用于监督学习。这主要适用于那些没有目标任务标签的数据集。
以下是自监督学习的常见思路:
- 将图像分割成独立部分,然后预测图像的相邻部分。
- 例如,如果你有猫的脸部图像,这里有一只耳朵,你只向网络展示脸部,网络应该预测耳朵的位置(是在左上角、右上角等等)。
- 通过这种方式,你教会网络更好地理解猫等物体的结构。
对比学习 🔄
我去年看到的另一个趋势是对比学习。这是自监督学习的一种形式,其核心思想是生成同一图像的两个版本。
其基本流程如下:
- 假设你有一张房子的图像。
- 你将原始图像输入网络。
- 然后,你创建同一张房子的增强版本(例如,将其旋转180度),并将这个增强版本也输入同一个网络。
- 网络需要预测这两个输入是否属于同一个物体。
通过这种方式,你训练网络识别出这个倒置的房子确实是同一个物体。反之,如果你输入一张猫的图像,网络应该预测这是不同的物体。这样,你就在训练网络更好地识别物体。
请注意,这里你不需要类别标签。你只是通过数据增强(如旋转)来自己创建标签。因为你知道旋转后的图像仍然是同一栋房子,所以你无需人工标注就能免费获得这些标签。当然,这通常不是最终目标任务,但它对于在目标数据集上微调网络之前的预训练阶段非常有帮助,通常能使网络性能更好。

不过,这超出了本课程的范围,属于更高级或更近期的主题。我们不会在本课程中使用它。如果你感兴趣,Jeremy Howard 有一篇非常友好的入门文章,你可以从中了解更多关于自监督学习的知识。当然,你也可以在课程项目中尝试使用它。
图神经网络 🕸️
另一个近期的研究趋势是关注用于图结构数据的图神经网络。传统上,深度学习主要关注文本和图像数据。如今,如果数据是图结构(例如,我研究的小分子结构),人们也开始使用图神经网络。你可以将原子视为节点,共价键视为边,从而将其建模为图神经网络。

这是一个很大的主题,同样超出了本课程的范围。但我发现了一篇不错的图神经网络入门介绍,如果你感兴趣可以阅读。它涉及许多论文,我不想特别推荐某一篇,但这篇介绍对初学者比较友好,主要与消息传递机制相关。
大规模语言模型 📚
同样,一个近期的趋势是大规模语言模型的研究。这大约始于2018年,当时人们训练了具有9400万个参数的模型,然后不断扩展规模。你可能听说过GPT-2(我认为是2019年),现在有GPT-3,拥有1750亿个参数。语言模型变得越来越大。当然,也有一些研究致力于保持模型的小型化,例如DistilBERT,它是BERT的一个小型版本。但总体趋势是朝着更大的模型发展。
当然,这对于大多数学术研究者来说是遥不可及的。这通常是大公司才能做的事情。我记得训练这样一个模型的成本高达数十万美元甚至更多。所以,这通常只有在大公司工作才能实现。
这里有一篇有趣的博客文章,总结了2020年以自然语言处理为主的研究亮点。如果你对这些大规模语言模型感兴趣,这会是一篇不错的读物。实际上,在本课程后期讨论Transformer模型时,我们会涉及其中一些模型,因为它们都基于Transformer架构。
视觉Transformer 👁️

另一个趋势是视觉领域的Transformer。人们现在也开始将Transformer架构应用于计算机视觉。然而,这同样需要非常大的规模。例如,有论文指出,即使是像ImageNet这样包含1400万张图像的数据集,也不足以训练一个视觉Transformer。因此,这主要也是学术研究难以企及的。
这些Transformer模型可能在非常庞大的数据集(例如5亿张图像)上表现非常好。然而,对于大多数应用场景,卷积神经网络目前仍然优于它们。实际上,卷积神经网络最近也有新的架构出现。因此,卷积神经网络仍然是计算机视觉领域的主流选择,是实践中你会使用的东西。视觉Transformer目前还处于非常实验性的阶段,在常见图像任务上还远未达到与卷积网络竞争的水平。但我认为指出这个发展方向是有趣且值得的。

好了,在下一讲中,我们将更详细地讨论感知机算法,并在课堂上提供一些代码示例。上周我提到代码示例可能会稍后提供,但我认为尽早介绍它们可能真的很有用。
我有一个不计分的家庭作业练习给你。你不需要提交任何东西,这只是为你自己准备的预习材料。我已经在Canvas上发布了这个练习,但请务必看一下,作为接下来课程的准备。这是一个关于NumPy和线性代数的介绍。当然,作业或考试中不会有任何专门关于NumPy的问题,但它将真正帮助你理解PyTorch的工作原理以及我们如何实现代码,因为线性代数是其基础。

如果你对此有任何疑问,请告诉我。这只是一个关于NumPy的简要回顾。如果你已经熟悉NumPy,也可以跳过。但我建议你看一下,看看是否对你有意义。如果全是新内容,你可能需要更仔细地阅读。
那么,如果你有任何问题请告诉我。下周四的课程将是关于感知机的。

本节课中,我们一起学习了深度学习的几个当前趋势:自监督学习、对比学习、图神经网络、大规模语言模型以及视觉Transformer。了解这些趋势有助于你把握领域前沿,虽然其中部分内容超出了本课程的核心,但为未来的深入学习奠定了基础。下一讲,我们将回归课程核心,深入探讨感知机算法。
课程 P19:L3.0 - 感知器概述 👨🏫
在本节课中,我们将要学习感知器,这是人工神经元的首次实现。我们将了解其基本原理,并通过NumPy和PyTorch的简单代码示例来学习如何实现它。
课程安排与项目介绍
上一节我们介绍了课程的基本信息,本节中我们来看看本学期的具体安排和项目计划。
课程注册调整已于周三截止,这意味着我们可以开始为课程项目组建小组了。
以下是关于小组项目的具体安排:
- 助教会建立一个表格,供大家填写期望的组员偏好。
- 如果你在班上不认识任何人或没有偏好,助教会进行随机分配。
- 从下周开始,建议各组开始思考课程项目的选题。
关于项目资源,我将提供项目期望的概述、往届项目的示例,以及一个包含各种有趣数据集的资源列表,以激发大家的创意。
作业与答疑平台
接下来,我们看看本课程的练习与交流方式。
我将围绕本节课内容布置第一次作业,用于练习Python编程。我的设想是,大家可以将我在课上用NumPy展示的代码,在不使用NumPy的情况下翻译成纯Python代码。通过这种从NumPy到Python的“逆向工程”,你可以更好地理解其工作原理。具体细节我将在课后通过Canvas公布。
对于课程疑问,鼓励大家使用Piazza平台进行交流。这是一个互动社区,其他同学也可以参与解答。
以下是使用Piazza的注意事项:
- 鼓励同学之间互相帮助解答问题。
- 请勿在Piazza上发布任何作业的解决方案。
- 欢迎在Piazza上讨论作业相关的普遍性问题或不清楚的地方。
本节课学习目标
在介绍了课程安排后,我们正式进入本节课的核心内容。
通过本讲的学习,你将能够实现你的第一个小型神经网络——一个单层的感知器网络。你可以用它来进行预测,例如简单的二分类。虽然它本身并不复杂,但这是熟悉分类任务和查看代码示例的绝佳起点。
总结
本节课中我们一起学习了本学期的项目与作业安排,并明确了感知器这一人工神经元基础模型的学习目标。接下来,我们将开始深入感知器的具体实现。
🚀 P2:L1.1.1 - 课程概述第 1 部分:动机和主题
在本节课中,我们将要学习本课程的总体框架、学习目标以及涵盖的核心主题。我们将了解深度学习能实现哪些令人兴奋的应用,并预览整个学期的学习路线图。



现在,让我们开始本周准备的内容。
首先,我想通过一个简短的预告,展示你在完成本课程后能够实现的目标。
当然,这是一个漫长的旅程,未来有15周的时间。但在某个时刻,你将能够运用在本课程中学到的深度学习知识,完成一些非常酷的事情。

以下是上学期学生们完成的一些课程项目示例。
例如,在这个项目中,学生们将音频信号(例如语音文本)转换为频谱图。



然后,他们应用卷积神经网络来分类不同的文本,并从音频片段中提取语言信息。在这个案例中,实际上处理的不是语言,而是识别打响指和唱歌的声音,以区分不同的音频输入。这是上学期的一个课程项目示例。

另一个项目是使用3D卷积神经网络。这是所谓的MNIST数据集的3D版本,在本课程中你会经常看到它,至少在后面的入门讲座中,因为它是一个用于神经网络入门的简单数据集。
在这个项目中,学生们处理功能磁共振成像(fMRI)数据,例如脑部扫描等,并对不同类型的脑部扫描进行分类。这是另一个有趣的项目。
此外,学生们还使用了不同类型的生成对抗网络,这也会在本课程后期涉及。你将能够生成新数据,或混合来自不同数据源的数据。

在这个项目中,学生们将艺术绘画或照片等艺术输入,与模特肖像照片混合。输出结果基本上显示在右侧,就像一张融合了不同风格的人物肖像。这是风格迁移的一个例子。
我选择这三个项目的原因有些随意。老实说,我只是浏览了上学期的项目,挑选了那些有漂亮图示的,这样在幻灯片上看起来更美观。
当然,你可以自由选择任何你感兴趣的主题作为课程项目,我稍后会详细讨论。我不想一开始就用太多信息让你不知所措,只是想展示一些你在学期末能够完成的示例。

如果你对我的研究感兴趣,我主要从事机器学习和深度学习领域的工作。这里我整理了一个我参与项目的概述,也算是介绍一下我自己和我的兴趣所在。

例如,去年我研究了秩一致自回归网络,我们称该方法为CORAL。你可以将其视为对有序输入的分类,即当你的类别标签具有顺序时,我们想要对它们排序或预测标签的正确顺序及其相关的数值。我们开发了应用于年龄分类的网络。

我们还研究了面部隐私,我们称此方法为Privacy-Net。我们可以从输入图像中隐藏面部属性,例如年龄、性别和种族等,以保护个人隐私。
此外,我还与英伟达的人员合作,撰写了一篇关于Python、机器学习和深度学习领域最新趋势的综述文章,特别关注GPU内存。这也是我们稍后讨论本课程将使用的工具时会更多涉及的内容。
我和我的一个学生还合著了另一篇关于基于机器学习和AI的生物活性配体发现方法的综述文章。我的一个学生正在研究小分子配体的发现与合成,同样使用生成模型和生成式深度学习模型,应用于分子合成与设计领域。
我的另一个学生正在研究小样本学习。小样本学习是深度学习的一个分支,关注如何从小数据集中学习。大多数时候,人们会使用元学习或迁移学习。我们将在本课程后期更多地讨论迁移学习,但不会涵盖小样本学习。不过,我可能会邀请我的学生在学期后期做一次小型客座讲座,如果他有时间的话。Zhongji也参与了这篇论文的工作,他同时也是本学期的助教。如果你感兴趣,可以向Zhongji请教更多关于不同小样本学习方法的问题,我想他会在办公时间非常乐意与你深入探讨。
最后,我也从事一些传统机器学习方法的研究。这是一项合作,我们使用了非深度学习的传统机器学习方法,具体是最近邻方法,用于与计算生物学相关的预测。这里涉及的是G蛋白偶联受体(GPCR)的结构,这是一种非常重要的蛋白质受体,在人体内与小分子结合。实际上,大多数药物靶点都是针对GPCR的。但这里的研究更像是基础计算生物学研究,分析这些蛋白质的结构组成。
这只是关于我的一点简单介绍。你可能看出,我喜欢研究深度学习,并对计算生物学应用有浓厚兴趣。这两个基本上是我的主要研究领域,也是我非常热衷的方向。
上一节我们了解了课程可能实现的应用和讲师的研究背景,现在让我们更具体地谈谈课程本身。

对于本课程,我计划涵盖许多主题,主要是深度学习和生成对抗网络,正如课程标题所示。
我将本课程结构分为五个部分。这里是第1、2、3部分,下一页幻灯片将展示剩余的两个部分。
首先,是引言部分。这正是我们现在所处的位置。我将简要概述本课程,并介绍机器学习和深度学习。这就是我们本周要做的事情。
然后,我也想简要谈谈深度学习的历史。我认为这很有趣,因为它能帮助你理解这些概念和动机从何而来。因为“深度学习”这个术语相对较新,大约在十年前出现,但它有着悠久的历史。你可以将深度学习视为神经网络的一个时髦称呼。而神经网络已经存在了至少60到70年,一些早期出现的想法推动了后来不同思想的发展。我们将涵盖许多与神经网络相关的内容。因此,你可以将这次讲座视为一个宏观概述。我们将简要介绍历史,然后在后续介绍本课程中不同主题时,我们会逐步展开,并将其与历史联系起来,同时阐明学习它们的原因及其用处。
接着,我们将讨论一种早期的机器学习方法:单层神经网络,即感知机算法。这是一个非常传统的算法,如今已不常用,但我认为它是分类问题的一个简单入门。分类就是将事物归入不同的类别。我认为这将是一个很好的主题入门。
然后,我们将进入第二部分,关注数学与计算基础。这意味着介绍一些必要的数学知识,如线性代数。确实,在深度学习中通常使用线性代数来更简洁地表达事物。从技术上讲,我们可以在不使用线性代数的情况下进行深度学习,但那样会很难书写,并且实现起来很慢,因为我们在实践中使用的计算库依赖线性代数计算例程,这有助于我们比使用Python循环更高效地执行某些计算。因此,线性代数对深度学习非常重要。我们不需要任何高级的线性代数概念,基本上只需要简单的向量点积和矩阵乘法。但我认为仍然值得单独用一节课来讲解,因为为后续课程打好基础会使后面的学习更容易一些。
之后,我们将讨论梯度下降。这是一个微积分主题。梯度下降是训练神经网络的主要方法。
在复习了这个主题之后,我们将讨论使用PyTorch进行自动微分。你可以将自动微分视为计算机上的微积分。我们将使用一个名为PyTorch的工具,它是一个用于线性代数、自动微分以及神经网络训练或深度学习的库。它还允许我们在GPU上实现计算以提高效率。我将在第7讲中解释如何使用集群和云计算资源,但这部分会相对简短,因为本课程的主要主题当然是深度学习,计算方面是必要的,但对于这门入门课程,你不需要成为专家程序员或计算机用户。你应该熟悉计算机上的某些操作和编程方面,但我们这里不是教授机器学习工程,更多的是提供概念性概述。你可以使用我将在这节课中谈到的一些免费资源来完成课程。当然,如果你感兴趣,也可以使用更高级的资源,例如校园的HTC集群等,但这门课不作要求。
在数学与计算基础之后,我们将最终讨论神经网络。在第三部分,我将为深度学习奠定基础。我们将从逻辑回归开始,你可以将其视为单层神经网络。这基本上是对我们之前讨论的单层网络的扩展,现在它是可微分的。
以逻辑回归为起点,我们将添加额外的隐藏层,使其成为一个深度网络,也称为多层感知机。然后,我们将学习如何使用反向传播算法来训练这样的多层感知机。
第10到12讲更像是训练深度神经网络的技巧,例如避免过拟合的正则化技术、输入归一化和权重初始化。这些技巧使神经网络的训练更加稳健和快速。我们还将讨论学习率和一些高级优化算法,本质上是梯度下降的更高级版本。这些对于在实践中使神经网络良好工作非常重要。这些主题,尤其是第10和11讲,可能听起来不那么令人兴奋,但它们超级有用和重要,是使神经网络良好工作的关键。


然后,我们将进入本课程中有趣的部分,或者说是更高级的部分。在第四部分,我们将讨论用于计算机视觉和语言建模的深度学习。我们将花大量时间在卷积神经网络上,这是一个大主题。
我们还将讨论循环神经网络,它们用于语言建模。卷积神经网络更多用于图像建模,尽管你也可以使用一维卷积网络处理文本。但文本将更多地在第15讲中重点介绍。这些内容也将为我们将要讨论的深度生成模型奠定基础。
在深度生成模型方面,我们将讨论自编码器、所谓的变分自编码器,然后讨论生成对抗网络。你可能已经听说过它们,即GANs。这只是生成对抗网络的完整写法。这也是一个非常大的主题,我们将有两节课来讲解:一节介绍,另一节关于一些更高级的GANs,例如Wasserstein GAN,以及如何评估和比较不同的GANs。因为在这个部分,我们关注的是预测,而在这里的第二部分,我们关注的是生成事物,这有点不同,评估这些模型也更具挑战性,因此我们将有一节课专门讨论。
我还计划涵盖循环神经网络在生成建模方面的一些内容,例如在序列到序列的上下文中生成新文本。在第15讲中,我将首先尝试只关注预测部分,但我们也会重新讨论这个主题,用于生成新的文本数据,并进入一个更高级的主题:为RNN添加所谓的注意力机制,并在Transformer的背景下解释自注意力。这些可能是你在媒体上听说过的模型(如BERT或GPT-2、GPT-3)的基础构建模块。我们也会讨论这些。
我不想让这里显得太拥挤,但这个部分基本上是关于图像的,而最后这两个部分将是关于文本的。因此,我们将涵盖图像和文本的生成模型。


本节课中,我们一起学习了本课程的动机、涵盖的精彩应用示例以及整体的教学大纲结构。我们从课程结束时可实现的项目预览开始,了解了深度学习在音频分类、医学图像分析和艺术风格迁移等领域的应用。接着,我们简要了解了讲师的研究方向。最后,我们详细梳理了课程的五个主要部分:从引言和历史,到数学与计算基础,再到神经网络基础、计算机视觉与语言建模的深度学习应用,以及深度生成模型。这为我们接下来的学习描绘了清晰的路线图。
课程 P20:L3.1 - 关于大脑和神经元 🧠
概述
在本节课中,我们将要学习大脑和神经元模型的基本概念,为后续理解感知机学习规则打下基础。我们会从生物神经元的结构入手,介绍其计算模型,并探讨人工神经网络与生物大脑之间的异同。
大脑与神经元模型

上一节我们回顾了深度学习的历史,了解到其灵感来源于生物大脑和神经元的工作原理。
以下是一张大脑的图片,放大后可以看到其中的一些神经元。这张图实际上取自维基百科上老鼠的大脑,展示了一种特定类型的神经元——海马体锥体神经元。大脑中存在许多不同类型的神经元,这只是其中一种。

一个常见的问题是:我们的大脑是否真的在使用深度学习算法?目前我们无法完全确定地回答这个问题,因为人类对大脑的具体工作机制仍有许多未知和未解之谜。然而,我们可以相对肯定地说,可能并非如此。大脑非常复杂,深度学习虽然在物体检测等特定任务上表现良好,但人脑的能力与神经网络的能力仍有很大差异。我们尚未实现通用人工智能,这可能表明深度学习的工作方式有所不同。
回顾上一讲中提到的由Geoffrey Hinton等人发表的论文,其核心观点是大脑可能使用一种与反向传播相关的算法,但这一点也尚未完全明确。不过,大脑是否进行深度学习其实并不那么重要,因为深度学习对于我们关心的某些任务(如图像分类)已经相当高效。
这类似于飞机与鸟类的类比。人们受鸟类飞行的启发制造了第一架飞机,但精确模仿鸟类(例如让飞机扇动翅膀)并非必要,甚至可能效率更低。因此,有时将某物作为灵感来源,并在此基础上发展出不同的、有用的东西就足够了。


出于兴趣,我们查阅了维基百科上关于神经元数量的数据。人脑大约有160亿到340亿个神经元,而虎鲸则有430亿个。如果仅凭神经元数量决定智能,那么虎鲸可能比人类更聪明,但这显然不是事实。同样,回顾深度学习历史讲座中提到的大规模语言模型(如GPT-3),其参数量可能达到1700亿或800亿。如果将一个参数视为一个神经元,这些模型在计算规模上甚至超过了人脑,但它们并不具备人脑的理解能力。它们擅长记忆从互联网摄取的大量信息,而人类更擅长理解。这表明,差异不仅仅在于参数数量,还有其他尚未被发现的因素。

在本讲中,我们实际上不讨论深度神经网络,而是从简单的单层神经网络或单个神经元模型开始,即生物神经元的计算模型。
生物神经元与McCulloch-Pitts模型
本节中,我们来具体看看生物神经元的结构及其最早的数学模型。
生物神经元是一种神经细胞。它包含细胞核(处理信号)、树突(接收其他神经元输入的连接)以及轴突(传输信号)。信号从树突传入,在细胞核处进行整合处理,如果神经元被激活,信号则通过轴突末梢传递到其他神经元的树突。

最早的数学模型是我们在历史课中讨论过的McCulloch-Pitts模型。以下是其大致结构:
- 输入 (x): 模型的输入信号。
- 权重 (w): 与每个输入相关联的参数。
- 加权和 (z): 在“细胞核”处,对所有输入进行加权求和。在深度学习中,我们通常称此值为预激活值,用字母 z 表示,有时也称为净输入。
- 公式:
z = w1*x1 + w2*x2 + ... + wm*xm
- 公式:
- 阈值函数: 将加权和 z 与一个阈值进行比较,并输出一个二进制信号(0或1)。如果 z 超过阈值,则输出1(传输信号);否则输出0(不传输信号)。
以下是使用该模型实现逻辑门电路的例子:
与门 (AND Gate):
- 设置所有权重为1,阈值为1.5。
- 计算加权和
z = x1 + x2。 - 仅当
x1和x2均为1时,z=2 > 1.5,输出1;其他情况输出0。
或门 (OR Gate):
- 设置所有权重为1,阈值为0.5。
- 计算加权和
z = x1 + x2。 - 只要
x1或x2中有一个为1,z就至少为1,大于0.5,因此输出1;仅当两者均为0时输出0。
你还可以实现非门(NOT Gate)。作为一个课后练习,请尝试思考:能否找到一组权重和一个阈值,使这个简单的McCulloch-Pitts模型实现异或门(XOR Gate)的功能?你可以稍后在课程论坛上分享或验证你的想法。
过渡到感知机
以上是对神经元模型的简要介绍。接下来,我们将进入本节课的核心部分,详细讨论感知机模型及其学习规则。

总结

本节课我们一起学习了生物神经元的基本结构及其最早的数学模型——McCulloch-Pitts模型。我们了解到,虽然深度学习受大脑启发,但两者的工作机制并不完全相同。我们从简单的神经元计算模型出发,看到了如何通过加权和与阈值函数实现基本的逻辑运算,为下一讲深入学习感知机模型做好了准备。

课程 P21:L3.2- 感知器学习规则 🧠
在本节课中,我们将要学习如何训练单层神经网络,特别是感知器学习规则。我们将从感知器的基本模型开始,逐步理解其工作原理、数学表示以及如何通过算法自动学习权重,以解决分类问题。

感知器模型简介 📐
上一节我们介绍了神经网络的历史背景,本节中我们来看看感知器的具体模型。感知器是一种受生物神经元启发的计算模型,由罗森布拉特提出,它提供了一种自动寻找权重的方法,用于解决分类问题。
感知器的核心是一个数学模型,它接收多个输入,计算加权和,然后通过一个阈值函数输出分类结果(0或1)。其计算过程可以用以下公式表示:

净输入 Z:Z = Σ (w_i * x_i) + b,其中 b 是偏置单元(即负的阈值 -θ)。
输出 ŷ:ŷ = f(Z),其中 f 是阈值函数,当 Z > 0 时输出 1,否则输出 0。
在感知器中,激活函数和阈值函数是同一个函数,这是它与后续更复杂模型(如逻辑回归)的一个关键区别。

模型的数学表示与简化 ✨
为了使模型更便于训练和表示,我们通常会对公式进行一些调整。核心思想是将阈值 θ 整合进权重中,作为一个额外的偏置参数。

以下是两种常见的表示方法:
- 显式偏置表示:
Z = Σ (w_i * x_i) + b,其中b = -θ。这是现代深度学习框架中更常见的写法。 - 隐式偏置表示:通过修改输入向量,在开头增加一个恒为1的特征
x_0,并将偏置b视为权重w_0。这样,净输入可以简洁地写成向量点积形式:Z = w^T · x。
虽然第二种表示在数学上更紧凑,但在实际编程中,第一种(显式偏置)通常更高效,因为它避免了为每个输入样本复制和修改数组的操作。

感知器学习算法 🔄
现在我们已经了解了感知器如何做出预测,本节中我们来看看它如何通过学习来自动调整权重。感知器学习规则是一个迭代算法,其目标是找到一个能够完美区分两类数据的线性决策边界(前提是数据是线性可分的)。


算法的核心思想是:根据预测错误来调整权重。

以下是感知器学习算法的步骤:

- 初始化权重:将所有权重(包括偏置)初始化为0或小的随机数。
- 迭代训练:对于训练集中的每一个样本
(x_i, y_i),重复以下步骤:- A. 计算预测输出
ŷ_i。 - B. 计算预测误差
error = y_i - ŷ_i。 - C. 根据误差更新权重:
w_new = w_old + error * x_i。
- A. 计算预测输出
这个更新规则非常直观:
- 如果预测正确(
error = 0),权重保持不变。 - 如果预测为0但真实标签为1(
error = 1),则将输入向量x_i加到权重上,使决策边界向该样本移动。 - 如果预测为1但真实标签为0(
error = -1),则从权重中减去输入向量x_i。

通过在整个数据集上多次迭代(多个轮次),权重会不断调整,直到所有样本都被正确分类或达到最大迭代次数。
算法特性与局限性 ⚠️


感知器学习算法具有一个重要的理论保证:如果训练数据是线性可分的,那么该算法保证能在有限步内收敛,找到一个完美的分类超平面。
然而,它也有明显的局限性:
- 仅适用于线性可分问题:如果数据不是线性可分的(这在现实世界中很常见),感知器将无法收敛,权重会持续振荡。
- 仅支持二分类:标准的感知器只能输出0或1,无法直接处理多分类问题。
- 对学习顺序敏感:由于它一次处理一个样本,样本的呈现顺序会影响最终得到的决策边界。

这些局限性促使了后续更强大算法的发展,例如支持向量机(SVM)和可以处理非线性问题的多层神经网络。
总结 📝
本节课中我们一起学习了感知器学习规则。我们从感知器的生物灵感出发,理解了其作为单层神经网络的数学模型。我们学习了如何通过整合偏置项来简化模型表示,并深入探讨了感知器学习算法的核心步骤:初始化、预测、计算误差和更新权重。




关键要点包括:
- 感知器是用于二分类的线性模型。
- 其学习规则通过
w_new = w_old + (y - ŷ) * x来迭代调整权重。 - 该算法在数据线性可分时保证收敛,但无法处理非线性问题。


尽管感知器本身比较简单,但它是理解现代神经网络中梯度下降等更复杂学习算法的重要基石。在接下来的课程中,我们将看到如何用代码实现感知器,并学习能够克服其局限性的更高级模型。
课程 P22:L3.3 - Python 中的向量化 🚀
在本节课中,我们将要学习一个在科学计算中提升效率的重要概念——向量化。我们将了解为什么在深度学习中应避免使用循环,以及如何利用 NumPy 等库进行高效的向量化计算。

概述
在深度学习中,计算效率至关重要。虽然 Python 本身并非以速度著称,但通过使用 NumPy 和 PyTorch 等高效库,我们可以将计算任务委托给底层用 C++ 或 CUDA 编写的优化例程,从而极大地提升性能。本节课将重点介绍向量化的概念及其优势。
为何需要向量化?

上一节我们提到了深度学习对计算效率的要求。本节中我们来看看,为什么向量化是实现高效计算的关键。
深度学习的应用通常涉及大量计算。Python 语言本身速度不快,但这通常不会成为瓶颈,因为我们使用 NumPy 进行 CPU 计算,使用 PyTorch 进行 GPU 计算。这些库的底层是高效的 C++ 和 CUDA 例程。据 PyTorch 开发者估计,即使加上 Python 这一层,整体速度也仅比纯 C++ 实现慢约 10%。因此,只要“聪明地”使用 Python,我们就能获得接近原生代码的性能。
然而,如果使用不当,Python 中的某些操作(如循环)会非常慢。这就是我们需要向量化的原因。
计算净输入:三种方法对比
接下来,我们通过一个具体例子来理解向量化。我们将使用感知机模型中的净输入(加权和)计算作为示例。
净输入的公式为:
z = w0*x0 + w1*x1 + w2*x2 + ... + wm*xm
其中,w0 通常代表偏置项。
以下是三种不同的实现方式:
方法一:使用 For 循环
这是最基础的方法,通过循环逐个元素进行计算。
# 定义输入和权重(列表)
x = [1, 2, 3]
w = [0.1, 0.2, 0.3]
bias = 0.5
# 使用 for 循环计算净输入 z
z = 0
for i in range(len(x)):
z += x[i] * w[i]
z += bias
方法二:使用列表推导式/生成器表达式
这是 Python 中更简洁的写法,本质仍是循环,但有时效率稍高。
# 使用生成器表达式(无方括号)计算
z = sum(x_i * w_i for x_i, w_i in zip(x, w)) + bias
方法三:使用 NumPy 向量化
这是最高效的方法,利用 NumPy 数组和点积运算一次性完成所有计算。
import numpy as np
# 将列表转换为 NumPy 数组
x_np = np.array([1, 2, 3])
w_np = np.array([0.1, 0.2, 0.3])
bias = 0.5
# 使用向量化点积计算净输入 z
z = np.dot(x_np, w_np) + bias
# 或更明确地写成:
# z = x_np.T @ w_np + bias
# 对于一维数组,转置可省略
性能比较与分析

现在,我们来比较这三种方法的性能。对于大型向量,哪种方法最快?
使用 timeit 模块进行基准测试的结果如下(数值为近似值,用于对比):
- For 循环:约 40 毫秒
- 列表推导式:约 30 毫秒(快约 25%)
- NumPy 向量化:约 40 微秒(比循环快约 1000 倍)

由此可见,向量化实现带来了巨大的性能提升。在深度学习中,我们应尽量避免使用 for 循环,尽可能使用 NumPy 或 PyTorch 中的点积、矩阵乘法等向量化操作。
向量化为何高效?
上一节我们看到了性能的巨大差异,本节中我们来看看其背后的原理。

向量化高效的核心在于并行计算。
- 循环(串行):在
for循环中,计算按顺序逐个进行。完成第一个x[i] * w[i]后,才能开始第二个,以此类推。 - 向量化(并行):在点积运算
np.dot(x, w)中,所有配对元素的乘法(如x[0]*w[0],x[1]*w[1],x[2]*w[2])理论上可以同时进行。计算单元可以并行处理这些独立的乘法操作,最后再将结果相加。
现代 CPU 的 SIMD(单指令多数据)指令集和 GPU 的大量核心架构,都非常擅长这种并行数据操作。因此,向量化能够充分利用硬件潜力,实现高速计算。
总结
本节课中我们一起学习了 Python 中向量化的重要性与实践。我们了解到:
- 深度学习中应追求计算效率,而向量化是关键手段。
- 与传统的
for循环相比,使用 NumPy 等库的向量化操作能带来数百至数千倍的性能提升。 - 向量化的高效性源于其将串行计算转化为并行计算的能力,从而更好地利用现代硬件的特性。


掌握向量化思维,是进行高效科学计算和深度学习编程的基础。在接下来的课程中,我们将运用这一概念来实现感知机等模型。
课程 P23:L3.4 - 使用 NumPy 和 PyTorch 实现感知器 🧠
在本节课中,我们将学习如何使用 Python 的 NumPy 库和 PyTorch 框架来构建一个感知器模型。我们将从数据加载和预处理开始,逐步实现感知器的核心算法,并最终评估其性能。本教程旨在让初学者能够清晰地理解每一步的实现细节。
所有代码文件都已在 GitHub 上提供。作业将基于 NumPy 版本,要求你用纯 Python 重新实现,以确保你理解其中的原理。从 NumPy 转换到纯 Python 对于初学者来说相对更容易。


1. 环境设置与数据加载
首先,我们需要导入必要的库并加载数据集。我们将使用 Jupyter Notebook 进行演示,因为它可以逐步执行代码,便于解释。

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline




接下来,我们加载一个简单的玩具数据集。该数据集包含两个特征列和一个类别标签列(0 和 1)。

data = np.genfromtxt('perceptron_toydata.txt', delimiter='\t')
X = data[:, :2] # 特征矩阵
y = data[:, 2] # 类别标签数组


我们可以打印一些摘要信息来了解数据。
print('Class label counts:', np.bincount(y.astype(int)))
print('X.shape:', X.shape)
print('y.shape:', y.shape)



输出显示,我们有 100 个数据点,每个类别各 50 个样本,特征维度为 2。




2. 数据预处理


在训练模型之前,对数据进行适当的预处理是标准做法。这包括打乱数据顺序、划分训练集/测试集以及标准化特征。



上一节我们加载了原始数据,本节中我们来看看如何进行预处理。


打乱数据顺序




打乱数据有助于模型学习,避免因数据顺序带来的偏差。


shuffle_idx = np.arange(y.shape[0])
shuffle_rng = np.random.RandomState(123)
shuffle_rng.shuffle(shuffle_idx)
X, y = X[shuffle_idx], y[shuffle_idx]


划分训练集和测试集




我们将使用前 70 个样本作为训练集,后 30 个样本作为测试集。

X_train, X_test = X[:70], X[70:]
y_train, y_test = y[:70], y[70:]

标准化特征



标准化(或称为 Z-score 归一化)使每个特征的均值为 0,标准差为 1。这通常能加速后续优化算法的收敛。
mu, sigma = X_train.mean(axis=0), X_train.std(axis=0)
X_train = (X_train - mu) / sigma
X_test = (X_test - mu) / sigma
标准化后,我们可以快速验证训练集特征的均值和标准差。
print('Mean train:', X_train.mean(axis=0))
print('Std train:', X_train.std(axis=0))




3. 可视化数据集



在实现模型之前,先可视化处理后的数据,了解其分布。


以下是训练集数据的散点图,其中圆形代表类别 0,方形代表类别 1。
plt.scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], label='class 0', marker='o')
plt.scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], label='class 1', marker='s')
plt.xlabel('feature 1')
plt.ylabel('feature 2')
plt.legend()
plt.show()

我们的目标是训练一个模型,使其能够在特征空间中画出一条直线(决策边界)来区分这两个类别。





4. 实现感知器类

现在,我们开始实现感知器算法。我们将它封装成一个 Python 类,模仿 PyTorch 的风格,包含 forward 和 backward 方法,为后续学习更复杂的神经网络打下基础。
初始化权重和偏置


在构造函数中,我们根据输入特征的数量初始化权重向量和偏置项。这里我们将权重和偏置分开存储,这样更清晰。


class Perceptron:
def __init__(self, num_features):
self.num_features = num_features
self.weights = np.zeros(num_features)
self.bias = 0.0

公式表示如下:
- 权重向量: w = [w₁, w₂, ..., wₙ]
- 偏置: b


前向传播方法



forward 方法计算净输入 z(即线性组合),然后通过阶跃函数(阈值函数)将其转换为二元预测(0 或 1)。



def forward(self, x):
# 计算净输入 z = w·x + b
linear = np.dot(x, self.weights) + self.bias
# 应用阶跃函数作为激活函数
predictions = np.where(linear > 0.0, 1, 0)
return predictions



其中,阶跃函数定义为:
predictions = 1 if z > 0 else 0

后向传播方法

backward 方法计算预测值与真实标签之间的误差。对于感知器,这就是简单的差值。


def backward(self, x, y):
predictions = self.forward(x)
errors = y - predictions
return errors






5. 训练循环
我们将训练逻辑封装在 train 方法中。它遵循标准的感知器学习规则:遍历多个轮次(epoch),对每个训练样本进行预测,计算误差,并更新权重和偏置。



上一节我们定义了感知器的核心计算,本节中我们来看看如何用这些计算来更新模型参数。


以下是训练过程的步骤:

- 遍历训练轮次:决定模型查看整个训练集的次数。
- 遍历每个样本:在线学习,每次用一个样本更新模型。
- 前向与后向传播:获取预测并计算误差。
- 参数更新:根据误差更新权重和偏置。


更新规则如下:
- w = w + η * error * x
- b = b + η * error




其中 η 是学习率,这里我们设为 1。



def train(self, X, y, epochs, learning_rate=1.0):
for epoch in range(epochs):
for i in range(y.shape[0]):
# 获取单个样本,并调整维度以匹配权重向量
x_i = X[i].reshape(1, -1)
y_i = y[i].reshape(1,)
# 计算误差
errors = self.backward(x_i, y_i).reshape(-1)
# 更新权重和偏置
self.weights += learning_rate * errors * x_i.reshape(self.weights.shape)
self.bias += learning_rate * errors



在更新时需要注意数组维度的匹配,因此代码中做了一些 reshape 操作。





6. 模型评估与决策边界

训练完成后,我们需要评估模型在训练集和测试集上的性能,并可视化其学到的决策边界。



计算准确率
我们实现一个 evaluate 方法来计算模型在给定数据集上的分类准确率。

def evaluate(self, X, y):
predictions = self.forward(X).reshape(-1)
accuracy = np.sum(predictions == y) / y.shape[0]
return accuracy

训练与评估模型

现在,让我们实例化感知器,在训练集上训练它,然后进行评估。





ppn = Perceptron(num_features=2)
ppn.train(X_train, y_train, epochs=5)


print('Model parameters:')
print(' Weights:', ppn.weights)
print(' Bias:', ppn.bias)

train_acc = ppn.evaluate(X_train, y_train)
test_acc = ppn.evaluate(X_test, y_test)
print(f'Train accuracy: {train_acc*100:.2f}%')
print(f'Test accuracy: {test_acc*100:.2f}%')


对于线性可分的数据,感知器在训练集上应该能达到 100% 的准确率。在测试集上的准确率可能略低,这可能是过拟合的迹象。


可视化决策边界
决策边界是满足 w·x + b = 0 的点的集合。为了在二维平面上画出这条直线,我们可以固定 x₁(横轴)的范围,然后解出对应的 x₂(纵轴)的值。

由 w₁x₁ + w₂x₂ + b = 0,可得:
x₂ = (-w₁x₁ - b) / w₂



# 计算决策边界线的两个端点
x_min = -2
x_max = 2
w1, w2 = ppn.weights
b = ppn.bias



x1 = np.array([x_min, x_max])
x2 = -(w1 * x1 + b) / w2


# 绘图
plt.scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], label='class 0', marker='o')
plt.scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], label='class 1', marker='s')
plt.plot(x1, x2, color='red', linewidth=2, label='decision boundary')
plt.xlabel('feature 1')
plt.ylabel('feature 2')
plt.xlim([x_min, x_max])
plt.legend()
plt.show()


左图展示了模型在训练集上的决策边界,它完美地分开了两类数据。右图(测试集)显示,由于过拟合,模型在未见过的数据上分类效果稍差。



7. NumPy 与 PyTorch 实现对比



感知器也可以用 PyTorch 实现,其代码结构与 NumPy 版本非常相似,这体现了 PyTorch 设计上对 NumPy 的友好性。主要区别在于数据结构和一些函数调用上。
以下是核心部分的主要对比:
| 操作 | NumPy 实现 | PyTorch 实现 |
|---|---|---|
| 数组创建 | np.zeros(num_features) |
torch.zeros(num_features, dtype=torch.float32) |
| 矩阵乘法 | np.dot(x, weights) |
torch.mm(x, weights.view(1, -1).t()) 或 x @ weights |
| 条件赋值 | np.where(linear > 0.0, 1, 0) |
torch.where(linear > 0.0, torch.tensor(1), torch.tensor(0)) |
| 求和 | np.sum(predictions == y) |
(predictions == y).sum().float() |
PyTorch 版本的优势在于可以轻松利用 GPU 加速,并且为构建更复杂的神经网络提供了自动微分等强大功能。我们将在后续课程中详细探讨 PyTorch。

8. 关于感知器收敛定理


感知器算法有一个重要的理论性质:如果训练数据是线性可分的,那么感知器保证在有限次迭代内收敛到一个解(即能够完美分类所有训练样本的权重)。反之,如果数据不是线性可分的,算法将永远不会收敛,权重会持续振荡。

相关的数学证明涉及为权重的范数设定上下界。虽然理解这个证明有助于深入理解算法,但对于入门深度学习而言,记住上述结论即可。在实践中,你可以通过尝试分类非线性可分的数据来直观观察算法无法收敛的现象。



总结

本节课中我们一起学习了感知器算法的完整实现流程。
- 我们使用 NumPy 进行了数据加载、打乱、划分和标准化。
- 我们实现了一个
Perceptron类,包含forward(计算预测)、backward(计算误差)和train(更新参数)方法。 - 我们训练了模型,评估了其准确率,并可视化出决策边界,直观理解了模型的分类原理。
- 我们简要对比了 NumPy 和 PyTorch 在实现上的异同。
- 我们提到了感知器收敛定理的核心结论。



通过本教程,你应该对感知器如何工作以及如何在代码中实现它有了扎实的理解。在作业中,你将有机会通过用纯 Python 重写这个模型来进一步巩固这些概念。
课程 P24:L3.5 - 感知器背后的几何直觉 🧠
在本节课中,我们将探讨感知器学习算法背后的几何原理。我们将理解为什么简单的权重更新规则能够有效地调整决策边界,并分析感知器模型的局限性。
感知器学习算法回顾
上一节我们介绍了感知器的基本概念,本节中我们来看看其学习算法的具体步骤。
感知器学习算法如下:
- 初始化权重向量 w 为零或小的随机数。
- 对于训练集中的每个样本 x:
- 计算预测值:
ŷ = step(**w**·**x** + b),其中step是阶跃函数。 - 如果预测错误,则更新权重:
- 如果
ŷ = 0但真实标签y = 1,则 w = w + x。 - 如果
ŷ = 1但真实标签y = 0,则 w = w - x。
- 如果
- 计算预测值:
核心问题在于:为什么通过加上或减去输入向量 x 就能修正决策边界?接下来我们将从几何角度进行解释。
权重向量与决策边界的关系
首先,我们需要理解权重向量 w 与决策边界之间的几何关系。
决策边界由方程 w·x + b = 0 定义。可以证明,权重向量 w 垂直于这条决策边界。理解这一点是理解后续更新的关键。
为什么是垂直(90度角)呢?考虑点积公式:
w·x = ||w|| * ||x|| * cos(θ)
在决策边界上,点积为零(w·x + b = 0,为简化我们先忽略偏置 b)。要使点积为零,而权重和输入向量的长度通常不为零,唯一的方法是让 cos(θ) = 0。而 cos(θ) = 0 意味着角度 θ = 90°。因此,在决策边界上的点,其与权重向量的夹角为90度。
夹角与分类预测
理解了垂直关系后,我们可以用夹角来解释分类决策。
- 正确预测为类别1: 当一个属于类别1的样本被正确分类时,其输入向量 x 与权重向量 w 的夹角 θ < 90°。此时 cos(θ) > 0,使得点积 w·x > 0,从而预测为1。
- 错误预测: 如果该样本被错误预测为0,则意味着其夹角 θ > 90°。此时 cos(θ) < 0,导致点积 w·x < 0。
我们的目标就是修正这种错误,将夹角从大于90度调整为小于90度。
权重更新的几何解释
现在,我们来看感知器如何通过更新权重来修正错误。
假设我们有一个属于类别1的样本 x,但当前权重 w 导致夹角 θ > 90°,因此被错误预测为0。
更新规则是:w_new = w_old + x
从几何上看,向量加法 w_old + x 会产生一个新的向量 w_new。这个新向量更接近于输入向量 x 的方向。通过这种加法,w_new 与 x 之间的夹角会减小(变得小于90度),从而使该样本在下一次计算时能获得正的点积,被正确分类为1。
这个过程直观地“推动”决策边界,使其向误分类样本的方向移动,直到该样本被正确分类到边界正确的一侧。
对于另一种错误情况(预测为1但真实为0),更新规则 w_new = w_old - x 具有相反的效果,将权重向量推离错误样本,同样是为了减小误分类。
感知器的局限性
尽管感知器提供了清晰的几何解释和简单的学习算法,但它存在几个重大缺陷,这也解释了为什么现代实践中较少使用它。
以下是感知器的主要缺点:
- 线性分类器: 感知器只能产生线性决策边界(一条直线或一个超平面)。它无法解决非线性可分问题,例如经典的“异或”问题或数据呈同心圆分布的情况。
- 二分类限制: 单个感知器只能进行二分类。虽然可以通过“一对多”或“一对一”策略组合多个感知器来实现多类分类,但这并非其原生能力。
- 非线性可分时不收敛: 如果数据本身不是线性可分的,感知器学习算法将永远不会收敛。它会持续振荡,决策边界在不同解之间来回跳跃。
- 解不唯一: 即使对于线性可分的数据,感知器也可能找到多个有效的决策边界。最终解高度依赖于初始权重和训练样本的顺序,而算法本身无法找到“最优”或“最鲁棒”的那个解(例如最大间隔的解)。
一个有趣的历史案例
感知器的局限性在历史上一个著名的实验中暴露无遗。研究人员试图用感知器从照片中识别是否有坦克。
他们用一组有坦克的森林照片和一组无坦克的森林照片训练感知器。经过训练,感知器达到了100%的准确率。然而几小时后,他们尴尬地发现,两组照片是用不同的方式冲洗的:有坦克的照片整体更亮一些。感知器实际上学会的是区分照片的亮度,而非识别坦克。如果有一张暗色调的坦克照片,它就会分类错误。
这个案例说明了感知器(乃至许多机器学习模型)可能学习到数据中虚假的、非本质的相关性。
总结
本节课中我们一起学习了感知器背后的几何直觉。我们了解到:
- 权重向量垂直于决策边界。
- 分类决策取决于输入向量与权重向量之间的夹角。
- 权重更新(加上或减去输入向量)在几何上相当于调整权重向量的方向,以修正误分类样本的夹角。
- 尽管原理简单直观,但感知器受限于其线性本质,无法处理复杂的非线性问题,这也是更强大的模型(如神经网络)得以发展的原因。


从下周开始,我们将介绍一些必要的线性代数基础,并逐步学习使用 PyTorch 实现更复杂的机器学习方法。
📰 深度学习新闻 #2,2021年2月6日

在本节课中,我们将回顾2021年2月初深度学习与人工智能领域的一些重要新闻和研究进展。我们将探讨自动化论文评审、新型生成模型、零样本学习、编程语言趋势、机器人学习以及数据可视化技术等多个主题。
🤖 自动化AI论文评审
上一节我们介绍了本周的概述,本节中我们来看看一项关于自动化AI论文评审的研究。
一篇名为《Seu Research Explo Crazy idea of Automating AI paper reviews》的文章引起了广泛关注。论文评审是当前深度学习与机器学习社区中一个非常敏感的话题。像ICML和NeurIPS这样的顶级会议每年会收到约5000到10000篇投稿。维持高质量的评审工作极具挑战性。评审者通常需要在很短的时间内审阅3到5篇论文。面对如此庞大的投稿量,寻找足够多的合格评审者同样困难。这可能是促使研究人员探索自动化评审想法的动机。
虽然我对使用AI真正决定论文质量持保留态度,但这项研究从语言理解的角度来看非常有趣。它展示了深度学习在理解人类语言和复杂文本内容方面的进展。
以下是评估一篇优秀评审报告最常被提及的几个品质,这些也可以作为我们课程项目同行评审的参考标准:
- 决断性:评审意见应明确。
- 全面性:评审应覆盖论文的各个方面。
- 论证充分性:观点需要有依据支撑。
- 准确性:评价应准确无误。
- 友善性:评审态度应友好、建设性。
他们提出的模型基于BART。BART是一种去噪自编码器,与BERT相关,用于预训练序列到序列模型。我们将在课程第五部分详细讨论序列到序列模型。序列到序列意味着模型的输入是一个序列,输出也是一个序列,而不仅仅是单个预测值。一个简单的例子是语言翻译,将一种语言翻译成另一种语言。
在该研究中,他们首先让人类标注员为论文打上各种标签(如摘要、动机、实质内容等)。然后训练一个能够自动完成相同标注任务的模型。之后进行一些后处理,最终仍需人类评估结果。
如果您对此项目感兴趣,可以在arXiv上找到相关论文,代码也已开源在GitHub上。
🎨 DALL·E:从文本生成图像
上一节我们讨论了自动化评审,本节中我们来看看一个有趣的生成模型项目。
您可能从未想过“意大利面做的夜晚”会是什么样子。这个名为DALL·E的项目(名字融合了Wall-E和艺术家达利)是一个机器学习项目。研究人员训练了一个拥有120亿参数的语言模型GPT-3。DALL·E基于GPT-3的概念,在图像及其描述文本的数据集上进行训练。然后,用户可以通过输入文本(如“意大利面做的夜晚”)来查询模型,模型会合成融合了文本概念的全新图像。
目前,DALL·E的论文和代码尚未公开,但有一篇博客文章可供深入了解。如果您对生成模型感兴趣,这是一个非常有趣的项目。
🔍 CLIP:连接语言与图像的零样本学习
虽然DALL·E的细节尚未完全公开,但其核心组件之一——CLIP模型已经可用。
CLIP代表对比语言-图像预训练。您可以将其理解为一个图像分类器,但它采用了一种巧妙的方法。CLIP基于零样本迁移学习。零样本意味着对模型从未在训练中见过的类别进行分类。
在少样本学习中,我们设定为 N-shot K-way,其中N代表每个类别的示例数量,K代表类别数量。例如,一个5-shot 10-way问题意味着有10个不同类别,但每个类别只有5个示例。零样本学习则更为极端,意味着某个类别完全没有训练示例。
CLIP通过自然语言监督和多模态学习来解决这个问题。多模态学习意味着使用不同的数据源,这里即图像数据和文本数据。具体来说,他们使用词嵌入来预测新的图像类别。在训练时,模型同时看到图像及其描述。当遇到新类别时,模型可以利用词嵌入或描述来预测类别。
以下是一个性能对比示例:在ImageNet数据集上,一个标准的ResNet-101模型达到了76.2%的准确率。而CLIP模型在另一个数据集上训练,并未在ImageNet上微调,却同样达到了76.2%的准确率。更令人印象深刻的是,它在其他未见过的ImageNet变体数据集上也表现良好。这表明CLIP通过对比语言-图像预训练的概念,能够更好地泛化到其他类型的数据集。
这种方法潜力巨大。例如,在开发自动驾驶系统时,您可能拥有海量数据。如果想寻找“行人穿过繁忙十字路口”的特定图像或视频片段,利用CLIP模型,您可以通过输入文本描述,高效地在大型数据集中进行查询。
以下是CLIP方法的高层次概述:
- 它基于自监督学习中的对比预训练思想。
- 模型同时在文本描述和图像上训练,分别通过文本编码器和图像编码器生成嵌入向量。
- 模型学习将正确的文本描述与对应的图像关联起来。匹配的图文对应高分,不匹配的对应低分。
- 预训练完成后,可以从文本标签创建分类器。例如,将类别标签嵌入到“一张XX的照片”这样的描述中,生成文本嵌入。
- 对于一张需要分类的新图像(零样本预测),通过图像编码器得到其嵌入,然后找出与之相似度最高的文本描述嵌入,从而确定其类别。
CLIP的论文和代码均已公开。
🌐 开源大型语言模型与编程语言趋势
上一节我们介绍了CLIP模型,本节中我们关注社区动态和工具趋势。
我多次提到了GPT-3模型。遗憾的是,GPT-3尚未公开。但出现了一项旨在训练开源版本GPT-3规模模型的计划。该计划的主要目标是复制GPT-3规模的模型并向公众免费开源。他们在一个名为“The Pile”的825GB数据集(包含YouTube描述、PubMed论文等)上训练模型。这是一个拥有约2000亿参数的大型模型。训练此类模型成本极高(可能高达数百万美元),通常需要大公司支持,因此这项开源计划非常有价值。
另一则新闻是关于编程语言和工具流行度的分析。根据O‘Reilly基于其网站查询和书籍搜索的数据,Python毫无疑问位居榜首,并且其热度仍在持续增长。Scala语言的关注度则出现显著下降。这提醒我们,在追求前沿技术(如学习新语言)和高效完成工作之间需要权衡。目前看来,Python仍然是机器学习和深度学习的绝佳选择。
🤖 基于视觉演示的机器人操作学习
另一个有趣的项目来自Facebook AI Research,他们致力于教AI通过视觉演示来操纵物体。
目前训练机器人仍然非常具有挑战性。强化学习领域让机器人通过试错来学习。另一种方法是逆强化学习,即基于演示进行学习。然而,传统方法通常需要在虚拟环境中模拟期望行为。
Facebook AI Research提出了一种基于模型的逆强化学习方法,它允许机器人在物理机器人上直接通过视觉演示(如视频或系列图像)进行学习。这意味着,无需构建复杂的3D虚拟环境,只需向强化学习智能体展示人类执行任务的视频(例如抓取并移动瓶子),它就能在一定程度上学会执行类似行为。
该项目提供视频演示、博客文章和GitHub上的开源代码。
📊 t-SNE:高维数据可视化及其技巧
最后,我们来讨论一个在深度学习中非常流行的技术——t-SNE。
t-SNE是一种用于可视化高维数据集的强大技术。以MNIST手写数字数据集为例,每张图像是28x28像素,即784维。我们无法直接在散点图上可视化如此高维的数据。t-SNE可以将数据集的维度降至二维(或三维),并保留高维空间中数据点之间的关系。
t-SNE将高维的MNIST数据嵌入到二维空间后,您可以看到数字0聚集在一起,数字7聚集在一起,等等。它通过投影到低维空间,保留了这些数字之间的相对关系。
您可以将其与主成分分析(PCA)对比。PCA是一种线性变换,而t-SNE方法则更为复杂,是非线性的。t-SNE的一个缺点是必须调整其超参数才能获得良好效果,而通常我们并不知道最佳设置是什么。
本周有一篇论文指出,初始化对t-SNE和UMAP保持全局数据结构至关重要。研究人员发现,如果使用PCA的结果作为t-SNE的初始化起点,而不是随机初始化,t-SNE的性能会好得多。他们通过计算原始高维空间和降维后空间中数据点距离之间的相关性来衡量性能,发现使用PCA初始化的t-SNE能更好地保持距离关系。
UMAP是另一种非常优秀且近期越来越流行的方法,有时在保持数据结构方面甚至优于t-SNE。
在实践中,t-SNE不仅用于可视化数据集,也常用于理解深度神经网络的行为。例如,研究人员可以提取神经网络某一层的嵌入表示,然后应用t-SNE来观察该层嵌入对不同类别的分离效果。
关于使用t-SNE的一个实用技巧是:在Scikit-learn中,t-SNE的默认初始化方案是随机的。根据上述研究,在实践中建议将其改为PCA初始化。目前Scikit-learn的issue跟踪器上正在讨论是否将默认行为改为使用PCA。
📝 总结

本节课中,我们一起回顾了2021年2月初的多项深度学习进展。我们探讨了自动化论文评审的尝试、DALL·E和CLIP这类连接语言与图像的创新模型、零样本学习的潜力、编程语言的发展趋势、机器人通过视觉演示进行学习的新方法,以及t-SNE数据可视化技术的最佳实践。这些动态展示了深度学习领域快速发展的多样性和创新活力。
深度学习中的线性代数:P26课程 🧮
在本节课中,我们将学习深度学习中的线性代数基础。这些概念是理解后续复杂模型(如多层神经网络)的关键。我们将从张量开始,逐步介绍向量、矩阵、广播机制,以及神经网络中的常用符号约定。
张量在深度学习中的应用 📦
上一节我们概述了课程内容,本节中我们来看看张量的基本概念。张量是多维数组的泛化,在深度学习中用于高效表示和处理数据。
以下是张量的几个关键点:
- 标量是零维张量,表示单个数值。
- 向量是一维张量,表示一组有序数值。
- 矩阵是二维张量,表示行和列组成的数值表。
- 更高维度的张量可以表示图像、视频序列等复杂数据。
在代码中,我们可以使用PyTorch创建和操作张量。
import torch
# 创建一个标量
scalar = torch.tensor(5.0)
# 创建一个向量
vector = torch.tensor([1.0, 2.0, 3.0])
# 创建一个矩阵
matrix = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
使用PyTorch操作张量 ⚙️
了解了张量的基本形式后,本节我们来看看如何使用PyTorch这一工具来操作它们。PyTorch提供了丰富的函数来执行张量运算,这对于实现深度学习算法至关重要。
以下是使用PyTorch进行张量运算的示例:
- 可以使用
torch.add()或+运算符进行加法。 - 可以使用
torch.matmul()或@运算符进行矩阵乘法。 - 可以使用
.reshape()方法改变张量的形状。
# 张量加法
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
c = a + b # 结果为 tensor([5, 7, 9])
# 矩阵乘法
mat_a = torch.tensor([[1, 2], [3, 4]])
mat_b = torch.tensor([[5, 6], [7, 8]])
mat_c = torch.matmul(mat_a, mat_b) # 或使用 mat_a @ mat_b
向量、矩阵与广播机制 📡
掌握了基本的张量操作后,本节中我们重点探讨一个能极大简化代码的机制:广播。广播允许在不同形状的张量之间进行算术运算,而无需显式复制数据。
以下是广播机制的工作原理:
- 当操作两个张量时,PyTorch会从后向前比较它们的维度。
- 如果两个维度相等,或其中一个为1,或其中一个不存在,则它们是兼容的。
- 在兼容的情况下,尺寸为1的维度会被扩展以匹配另一个张量的对应维度。
例如,将一个形状为(3, 1)的矩阵与一个形状为(1, 3)的矩阵相加,通过广播,它们都会扩展为形状(3, 3)再进行计算。
# 广播示例
matrix = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 形状 (3, 3)
vector = torch.tensor([10, 20, 30]) # 形状 (3,)
# 向量会被广播为 (3, 3) 的矩阵,然后与 matrix 逐元素相加
result = matrix + vector
神经网络的符号约定 📝
最后,我们来了解神经网络中常见的符号约定。值得注意的是,理论教科书中的公式表示与PyTorch等计算框架的实现方式存在一些差异,这主要是出于计算效率的考虑。
以下是主要的符号约定差异:
- 在传统教科书标记中,线性变换常写作 y = Wx + b,其中输入
x是列向量。 - 在PyTorch等框架中,为了批量处理的效率,通常将输入组织为行矩阵,因此线性层实现为 Y = XW^T + b。这里
X的每一行是一个样本,W的转置是为了匹配维度。 - 这种约定在卷积神经网络和循环神经网络中处理高维张量时尤为重要。
# PyTorch 中线性层的实现方式
import torch.nn as nn
linear_layer = nn.Linear(in_features=10, out_features=5)
# 假设输入 X 的形状为 (batch_size, 10)
# 内部计算实质上是:output = X @ weight.T + bias
总结 🎯
本节课中我们一起学习了深度学习所需的线性代数基础。我们从张量的概念出发,介绍了如何使用PyTorch创建和操作张量。接着,我们深入探讨了向量、矩阵以及强大的广播机制,该机制能让我们更简洁地编写代码。最后,我们对比了神经网络理论表示与PyTorch实际实现中的符号约定差异,为后续学习更复杂的模型奠定了坚实的基础。
深度学习基础课程 P27:L4.1 - 深度学习中的张量 🧮
在本节课中,我们将要学习深度学习中的一个核心概念——张量。我们将从数学定义出发,了解其在深度学习中的具体应用,并学习如何在代码中表示和操作张量。
概述
张量是深度学习中最基本的数据结构。理解张量的概念、维度和表示方法,是掌握后续模型构建和数据处理的基础。本节将系统介绍标量、向量、矩阵与张量的关系,以及它们在深度学习框架中的具体实现。
张量的定义与等级
首先,我们来定义“张量”这个术语,并了解它在深度学习语境下的使用方式。
张量是向量、矩阵和标量等概念的泛化。例如,标量可以被视为一个零阶张量。标量的一个例子是一个实数,例如一个整数。在深度学习的上下文中,我们通常处理的是实数,尽管在类别标签中可能会使用整数。
一个标量就是一个单独的值,例如 1.23。
向量是一个一阶张量,你可以看到我们为其增加了一个维度。向量通常在线性代数中被视为包含 n 个值的数据容器。然而,在机器学习和深度学习的语境中,我们通常将向量视为 n × 1 维的。本质上,你可以将其看作一个非常“瘦”的矩阵,即一个只有一列的矩阵。这主要是为了方便某些计算。
通常,默认情况下,我将向量表示为列向量。但当我写转置时,我们有时也会使用行向量。行向量本质上就是列向量的转置,是一个 1 × n 维的向量。
矩阵是一个 n × m 维的矩阵,其中 m 是行数,n 是列数。如果 n 为 1,那么我们就只有第一列,这便是一个列向量。
以上内容主要是为张量的概念、向量、矩阵和标量及其关系建立基础的符号表示。
深度学习中的设计矩阵
在深度学习和机器学习的上下文中,我们通常为数据使用所谓的“设计矩阵”。这是一个常见的惯例。
如果我们使用字母 X,那么我们通常指的是一个设计矩阵,它是一个 n × m 维的矩阵。在之前的幻灯片中,我写的是 m × n 维。这只是在一般线性代数教材中的写法。这里的字母 m 和 n 并不重要。但在机器学习和深度学习领域,大多数人遵循一个惯例:n 表示训练样本的数量,m 表示特征的数量。这就是为什么我将其写作 n × m。但这也不是绝对重要的,只是一个惯例。
就像我在第一节课中已经谈到的,我们将使用下标表示特征索引,使用上标表示训练数据点索引或训练样本索引。
如果我们有 n 个训练样本,那么我们的索引将从 1 到 n。对于特征,我们的索引将从 1 到 m。例如,在鸢尾花数据集中,我们有 150 个训练样本和 4 个特征。
高阶张量
在上一节中,我们看到了零阶张量,称之为标量。对于一阶张量,我们称之为向量。对于二阶张量,我们称之为矩阵。那么,对于三阶张量,我们该如何称呼呢?遗憾的是,并没有专门的词。通常,当我们指代一个三阶张量时,我们使用“3D 张量”这个词,有时甚至直接称为“张量”。
那么,什么是 3D 张量呢?你可以将其想象成一叠矩阵。如果这里的每个方框代表一个矩阵(一个二阶张量),那么在深度学习的上下文中,一个 3D 张量就像是多个矩阵堆叠在一起。例如,这里可能堆叠了 p 个矩阵,每个矩阵是 n × m 维的。
请注意,这里的 m 和 n 是任意的,它们并不特指特征或训练样本。这些只是常用的字母,不要将它们与训练样本或特征数量混淆。它们只是通用的术语或字母。
在实践中,我们确实会在深度学习中遇到这些 3D 张量,例如当我们处理图像数据时。你可以将单张图像(如彩色图像)看作一个 3D 张量。我们有三个颜色通道:红色、绿色和蓝色。每个颜色通道由像素值组成,例如可以是一个 100 × 100 的矩阵,包含每个颜色通道的像素值。组合起来,它们基本上就是一个 3D 张量。这就是我们在深度学习中处理图像数据时会遇到 3D 张量的情况。
然而,在实践中,我们并不止步于 3D 张量。实际上,我们在深度学习中会使用 4D 张量,因为我们有一个由多个这样的 3D 图像组成的训练集。你可以将这里的 4D 张量想象成多个 3D 张量的堆叠。
例如,右侧是 CIFAR-10 数据集的一个例子。其中每一个都是一个 RGB 3D 张量。关于维度,通常在深度学习中,我们将这些 4D 张量的维度表示为 (n, c, h, w),其中:
n是训练样本的数量。c是颜色通道的数量,例如 RGB 图像中为 3。h是图像的高度。w是图像的宽度。
这样,最后的三个维度 (c, h, w) 基本上就是我们的特征 m。在这种情况下,我们有 c × h × w 个特征,而 n 仍然是我们的训练样本数量。如果我们不使用深度学习,我们会将其重塑为一个 n × m 维的设计矩阵,其中 m 实际上是 c × h × w。
不过,我们目前还没有处理彩色图像。这将在我们讨论卷积网络时遇到,我计划在探索更简单的彩色图像模型时也会涉及这种重塑操作,但现在不必担心。
框架中的张量实现
关于张量,你可能听说过 TensorFlow 这个工具。TensorFlow 的名字中就包含了“张量”。在这些工具(如 TensorFlow、NumPy 和 PyTorch)的上下文中,张量实际上指的是多维数组的概念。
你可以将多维数组看作一种用于存储或表示张量的数据结构或计算数据结构。在数学中,我们称之为张量,但在实际编码中,这些本质上就是多维数组。
在 TensorFlow 中,这些多维数组被称为 Tensor。在 NumPy 中,它们被称为 ndarray(n 维数组)。在 PyTorch 中,它们也被称为 Tensor。但它们本质上都是同一个东西。
以下是一个简单的例子。这里我导入 PyTorch,创建一个张量,这个张量是一个 2 × 3 的矩阵(两行三列)。然后,我使用 t.shape 来查看其形状。在 NumPy 中也有 t.shape,你可能知道。这会返回多维数组或张量的维度。你可以看到这里的 2 和 3 就是其维度。实际上,它是每个维度中值的数量。这将是一个二维数组,因为有两个维度:行维度和列维度。因此,这将是第一维和第二维。
形状中的索引数量就是维度的数量。方便的是,你也可以调用 ndim,它代表维度的数量,同样会返回 2。所以,ndim 本质上等同于在 Python 中调用 len(t.shape),只是一个快捷方式。
我只是想简要强调一下,在 PyTorch、TensorFlow 和 NumPy 的上下文中,张量与多维数组是同一个概念。
核心概念总结
这里有一张来自《Deep Learning with PyTorch》一书的很好的总结图,将所有术语一目了然地展示出来:标量、向量、矩阵、张量(这里是 3D 张量),以及更高维的张量(n 维张量)。
在这种情况下,你实际上不知道有多少个维度,可能是任意的。这也只是开个玩笑,表示这是我们无法在脑海中可视化的东西,因为作为人类,我们最多只能真正思考三个维度,到了第四个维度,一切就变得难以想象了。
以上只是对我们刚才在前几张幻灯片中讨论内容的一个简短总结。
在下一个视频中,我想简要地更多地讨论张量和 PyTorch。如果这次内容有点简短,我将在下一个视频中详细讨论。


总结

本节课中,我们一起学习了深度学习中的核心数据结构——张量。我们从数学基础出发,明确了标量、向量、矩阵与张量之间的关系。我们了解到,在深度学习的实际应用中,数据通常以设计矩阵或更高维的张量形式组织,例如处理图像时的 3D 张量(通道、高度、宽度)或包含批量数据的 4D 张量。最后,我们认识到在主流深度学习框架中,张量在代码层面被实现为多维数组,并掌握了查看其形状和维度的基本方法。理解张量是理解后续所有数据流动和模型计算的基础。

课程 P28:L4.2 - PyTorch 中的张量 🧮
在本节课中,我们将要学习如何在 PyTorch 中操作张量。如果你熟悉 NumPy,你会发现使用 PyTorch 的张量非常简单,因为两者的操作几乎完全相同。
张量简介
正如之前提到的,在计算语境中,张量和多维数组是同义词。在 NumPy 中,我们使用 numpy.array 来构建多维数组。而在 PyTorch 中,我们使用 torch.tensor。
需要注意的是,PyTorch 中还有两个相关的调用:torch.Tensor(大写 T)和 torch.tensor(小写 t)。torch.tensor 是一个函数,它在内部调用 torch.Tensor,但会进行额外的输入检查以确保数据格式正确。因此,在实践中,推荐使用 torch.tensor 函数。

创建张量
以下是创建一维向量(即秩为1的张量)的示例。
在 NumPy 中,我们这样创建:

import numpy as np
a = np.array([1., 2., 3.])
print(a.dtype) # 输出:float64
print(a.shape) # 输出:(3,)
在 PyTorch 中,我们这样创建:

import torch
b = torch.tensor([1., 2., 3.])
print(b.dtype) # 输出:torch.float32
print(b.shape) # 输出:torch.Size([3])
可以看到,NumPy 默认创建 64 位浮点数(float64),而 PyTorch 默认创建 32 位浮点数(float32)。在深度学习中,32 位精度通常已经足够,并且占用更少的内存。
基本运算
PyTorch 和 NumPy 的运算方式非常相似。例如,计算点积:
在 NumPy 中:
result_np = np.dot(a, a)
在 PyTorch 中,有几种等效方式:
result_torch1 = torch.matmul(b, b)
result_torch2 = b @ b # 使用 @ 运算符
result_torch3 = torch.dot(b, b) # 对于一维张量
此外,你可以将 PyTorch 张量转换为 NumPy 数组,这在某些需要与 Matplotlib 等库交互时很有用:
b_numpy = b.numpy()
数据类型详解
理解不同的数据类型对于高效计算很重要。以下是常见的数据类型及其含义:
- 整数类型:
uint8:无符号 8 位整数(0 到 255)。int16,int32,int64:有符号 16、32、64 位整数。
- 浮点数类型:
float16(半精度):16 位浮点数。float32(单精度):32 位浮点数。float64(双精度):64 位浮点数。

在深度学习中,float32 是默认和推荐的选择,因为它在 GPU 上运行更快,且精度足够。float16 有时用于混合精度训练以节省内存。
在 PyTorch 中,这些类型有特定的名称:
torch.uint8对应uint8。torch.int32或torch.int对应int32。torch.int64或torch.long对应int64。torch.float32或torch.float对应float32。torch.float64或torch.double对应float64。torch.float16或torch.half对应float16。

指定与转换数据类型
你可以在创建张量时指定数据类型:

c = torch.tensor([1., 2., 3.], dtype=torch.float64) # 创建双精度张量
也可以在创建后转换数据类型:
d = torch.tensor([1, 2, 3]) # 默认为 torch.int64
e = d.double() # 转换为 torch.float64
# 或者使用更通用的方法
f = d.to(torch.float64)

为什么使用 PyTorch?
既然 PyTorch 和 NumPy 如此相似,为什么我们还需要 PyTorch?主要有以下几个原因:
- GPU 加速:PyTorch 可以轻松地将计算转移到 GPU 上,利用其并行计算能力,使大型矩阵运算速度提升成百上千倍。
- 自动微分:PyTorch 内置了自动梯度计算功能,这对于训练神经网络、计算损失函数的梯度至关重要。
- 深度学习工具包:PyTorch 提供了大量预构建的神经网络层、损失函数和优化器,使得构建和训练深度学习模型更加高效和便捷。
使用 GPU

要检查你的 PyTorch 是否支持 GPU 并利用它,可以这样做:
# 检查 GPU 是否可用
print(torch.cuda.is_available())
# 如果可用,将张量移动到 GPU
if torch.cuda.is_available():
device = torch.device("cuda:0") # 使用第一个 GPU
b_gpu = b.to(device)
print(b_gpu)

# 将张量移回 CPU
b_cpu = b_gpu.to("cpu")
你可以通过在终端运行 nvidia-smi 命令来查看 GPU 的状态(仅限 NVIDIA GPU)。
安装 PyTorch
对于大多数学习和开发工作,在个人电脑上安装 CPU 版本的 PyTorch 就足够了。建议访问 pytorch.org,根据你的操作系统、包管理器和需求(选择 “CUDA=None” 以安装 CPU 版本)获取安装命令。通常,使用 Conda 或 Pip 安装即可。
例如,一个常见的 CPU 版本安装命令是:
conda install pytorch torchvision torchaudio cpuonly -c pytorch
总结


本节课中,我们一起学习了 PyTorch 张量的基础知识。我们了解了如何创建和操作张量,探讨了不同的数据类型及其在深度学习中的意义,比较了 PyTorch 与 NumPy 的异同,并介绍了使用 GPU 加速计算以及安装 PyTorch 的基本步骤。掌握这些内容是进行后续深度学习实践的重要基础。
课程 P29:L4.3 - 向量、矩阵和广播 🧮

在本节课中,我们将要学习一个在计算机上处理向量和矩阵运算时非常便利的概念——广播。与传统的纸笔线性代数运算相比,广播机制能让我们的代码更简洁、更高效。
从向量运算说起

上一节我们介绍了感知机的基本概念。让我们回顾一下净输入的计算公式:
Z = Wᵀx + b
其中,x 和 w 是向量,b 是标量。在传统线性代数中,支持的向量基本运算包括向量加法(或减法)、内积(点积)和标量乘法。
然而,在计算机实践中,我们有一些更便利的操作,它们可能不符合传统线性代数的严格定义,但能极大简化代码。

以下是两种常见的便利操作:

-
向量与标量相加:在 PyTorch 等框架中,可以直接将一个标量加到一个向量上,该标量会自动加到向量的每一个元素上。
# 传统线性代数做法:需创建全1向量 # 计算机便利做法:直接相加 vector + scalar -
逐元素向量乘法:两个形状相同的向量可以直接进行逐元素相乘,这不同于点积。
# 这不是点积,而是逐元素乘法 vector_a * vector_b
这些便利操作都与广播概念相关,我们将在后续详细解释。
矩阵运算与并行化优势
现在,让我们来看看矩阵。回想一下感知机算法,它通常被视为一种在线算法,即一次处理一个训练数据点。
但是,在进行预测时,我们可以一次性处理所有测试样本。这意味着我们可以输入一个包含 N 个样本的巨型矩阵(设计矩阵),并一次性完成所有计算,从而避免使用循环。这种方式效率更高,因为计算机可以利用底层例程进行并行计算。
并行化主要在两个层面发生:

- 计算单个点积时的并行:计算权重向量
w与一个特征向量x的点积时,向量各元素的乘法可以同时进行。 - 计算多个点积时的并行:如果有多个处理器,可以同时计算权重向量与多个不同特征向量的点积。
假设我们有一个 N x M 的设计矩阵 X(N个样本,M个特征)和一个 M x 1 的权重向量 w,那么净输入 Z 的计算可以表示为:
Z = Xw + b
这里,b 作为一个标量,会被加到结果向量的每一个元素上。最终,Z 是一个 N x 1 的向量,包含了所有样本的净输入。
PyTorch 中的灵活实践
在 PyTorch(和 NumPy)中,对于维度的要求比传统线性代数宽松。例如,我们可以直接用矩阵乘以一个一维向量。

import torch
# 一个 2x3 的矩阵
X = torch.tensor([[0, 1, 2],
[3, 4, 5]])
# 一个一维向量
w = torch.tensor([1, 2, 3])
# PyTorch 允许这样的乘法
result = torch.mm(X, w.view(-1, 1)) # 使用 view 将 w 变为列向量
# 或者更直接地(利用广播)
result = X @ w # 结果是一个一维张量
需要注意的是,在传统线性代数中,Z = Xw + b 里的 b 是标量,不能直接与向量相加。但在深度学习的实践中,我们普遍接受这种“不严谨”但极其方便的写法。其背后的等价传统写法是创建一个全1向量 1,然后计算 Z = Xw + b * 1。
广播机制详解

本节中我们来看看广播的核心机制。广播允许在不同形状的数组(张量)之间进行算术运算。
广播的规则是:从尾部维度开始,向前逐维比较。如果两个数组在某个维度上大小相等,或其中一个数组在该维度的大小为1,或其中一个数组没有该维度,则它们是可广播的。广播时,大小为1的维度会被扩展以匹配另一个数组的对应维度。


以下是一个广播的典型例子:

# 一个 2x3 的矩阵
matrix = torch.tensor([[4, 5, 6],
[7, 8, 9]])
# 一个一维向量
vector = torch.tensor([1, 2, 3])

# 广播发生:vector 被隐式复制,仿佛变成了一个 2x3 的矩阵
# [[1, 2, 3],
# [1, 2, 3]]
result = matrix + vector
print(result)
# 输出:
# tensor([[ 5, 7, 9],
# [ 8, 10, 12]])
在这个例子中,向量 [1, 2, 3] 被广播(复制)以匹配矩阵 matrix 的形状,然后执行逐元素加法。这种机制使得代码无需显式复制数据就能工作,既简洁又高效。
总结
本节课中我们一起学习了广播这一核心概念。我们了解到:

- 在计算机上进行线性代数运算(尤其是深度学习)时,规则比传统纸笔运算更宽松,旨在提升编码便利性。
- 使用矩阵一次性处理多个数据样本,可以利用硬件并行能力提升计算效率。
- 广播机制允许不同形状的数组进行算术运算,系统会自动复制(扩展)维度较小的数组以匹配较大的数组,这是实现代码简洁性的关键。

理解这些计算上的便利和广播机制,为我们后续学习更复杂的多层神经网络打下了重要的基础。在接下来的课程中,我们将应用这些知识来理解全连接层和相关的 PyTorch 实现惯例。
📚 课程 P3:L1.1.2 - 课程组织与概述
在本节课中,我们将详细介绍本学期的课程组织方式、使用的平台、评分构成以及沟通渠道。我们将使用 Canvas 作为核心平台,以简化信息获取流程。
🖥️ 课程平台与材料组织
我们将主要使用 Canvas 平台。这能让我和你们的工作都更轻松,因为所有内容都会集中在一个地方。公告、成绩以及 Piazza 论坛等信息都会在此发布。
我们也会使用其他技术,如 GitHub。但我会在 Canvas 上发布相关链接。因此,你们无需自己去不同地方搜索资料,所有内容都会集中在 Canvas 的模块中。

以下是课程材料的组织示例:
Canvas 课程页面
├── 公告
├── 成绩
├── 模块(每周内容)
└── Piazza(讨论区)
🐍 关于 Python 的先修要求
本课程要求你曾修读过一门编程课程,但不一定是 Python。然而,我们将在课程中使用 Python。你无需成为 Python 专家,我们只会使用其非常基础的功能。但提前了解 Python 的基本工作原理对你将大有裨益。
我在寒假期间分享了一些可选的预备材料,供有编程背景但未使用过 Python 的同学学习。这些资源学习起来并不费力,只涉及基础知识。我们不会在开学前几周立即使用 Python,预计从第三周开始。因此,你没有压力必须今天完成学习,但如果你毫无 Python 经验,最好在接下来几天浏览这些资源。


📄 教学大纲与课程信息
几天前,我已经发布了大量关于教学大纲的信息。在此我不再赘述所有细节,你们自行阅读发布的内容会更高效。内容主要包括:本课程将涵盖的主题(你可能已有所了解)、课程信息的主要位置(主要在 Canvas 上)、可能对你有帮助的可选教材资源,以及本课程的沟通方式。我将在接下来的几张幻灯片中进一步说明。
📅 课程安排与内容发布
关于课程的后勤安排和期望:内容方面,我将在每周二和周四上传视频。选择这个时间是因为这符合我以往线下授课的节奏,也便于我安排工作:周一我开始录制周二的视频,周二处理邮件和工作,周三录制周四的课程,依此类推。
每周的典型安排如下所示(虽然我录制时尚未上传,但你们观看此视频时可能已经上传):
- 办公时间:我们将通过 Zoom 进行办公时间。我和助教都会在晚上安排时间(例如,我可能在周二晚上6-7点,助教在7-8点),以方便不同时区的同学参与。
- 内容发布:周二和周四,我会上传课程视频到 Canvas 的特定页面,并嵌入所有视频。同时,我也会发布或链接一些额外的阅读材料,例如与讲座相关的有趣论文。
- 每周自测测验:我计划在每周结束时发布一个自测测验。该测验包含3到5个简单的选择题,旨在帮助你跟上进度,并计入少量分数。测验会在周末发布,但截止日期是一周后,这样你就有大约7天的时间在观看视频后完成它。
📊 课程评分构成
本学期的评分构成如下:
- 作业与测验(30%):包括家庭作业和每周的自测测验。家庭作业大约每两周发布一次,比测验更长,涉及自由回答或少量编码。预计本学期有5到7次作业。
- 期中考试(20%):将在 Canvas 上进行线上考试。具体日期已发布在教学大纲中,我稍后会再次通知。
- 课程项目(50%):这是课程中最有趣的部分。你们需要组建3人小组,提出一个与深度学习相关的项目主题。我将提供数据集、资源网站、项目想法以及往届示例。项目提案是一份约两页的报告,仅占总成绩的5%,主要用于获取反馈以确保项目方向可行。学期末,需要提交项目展示视频(8-10分钟)和项目报告(8页,会议论文风格)。此外,本学期将引入同行评审环节,每位同学将评审其他三位同学的报告和展示并提供反馈。
💬 课程沟通方式
本课程的沟通主要通过以下两种方式进行:
- Zoom 办公时间:用于面对面的实时答疑。
- Canvas/Piazza 论坛:用于异步沟通。Piazza 是一个集成在 Canvas 中的在线论坛,仅本课程学生可访问。你可以在上面公开(显示姓名)或匿名提问。我也鼓励公开提问,例如寻找组员。所有与课程相关的问题都可以在此提出。与电子邮件相比,我更喜欢 Piazza,因为它更集中,便于我跟踪和回答所有课程问题,不易遗漏。你也可以通过 Piazza 的私信功能向我发送私人问题。
强烈建议你在 Canvas 中启用通知功能。这样,当有新内容、新截止日期等信息时,你会自动收到邮件提醒,无需手动刷新页面。
✨ 总结
本节课我们一起学习了本学期的课程组织框架。我们明确了将使用 Canvas 作为核心信息枢纽,了解了 Python 的先修要求,熟悉了以周为单位的内容发布与测验节奏,掌握了评分构成(重点是占比较大的课程项目),并知道了主要通过 Zoom 办公时间 和 Piazza 论坛 进行沟通。请务必在 Canvas 上设置好通知,以便及时获取课程更新。如有任何问题,欢迎在本周的办公时间面对面交流,或在 Piazza 上发帖提问。

课程 P30:L4.4 - 神经网络的符号约定 🧮
在本节课中,我们将学习深度学习中关于线性代数的符号约定。我们将从感知机的基本计算开始,逐步扩展到具有多个输入和多个输出的神经网络层,并解释如何用矩阵和向量简洁地表示这些计算。
从感知机到矩阵计算
上一节我们回顾了感知机的基本结构。本节中我们来看看如何用线性代数表示其计算过程。
在感知机中,对一个输入特征向量进行推理(即预测其标签)的过程如下:首先计算净输入 z = x^T W + b,其中 x 是输入特征向量,W 是权重向量,b 是偏置标量。然后将 z 输入到激活函数(如阈值函数)中得到预测结果。
以下是单个数据点的计算过程:

# 假设 x 是 M 维特征向量,W 是 M 维权重向量,b 是标量偏置
z = np.dot(x.T, W) + b
处理多个数据点


我们可以将上述计算扩展到同时处理多个数据点(例如 n 个训练或测试样本)。

我们使用一个 n × M 维的设计矩阵 X 来表示数据,其中 n 是样本数量,M 是特征数量。此时的计算变为:
# X 是 n × M 的设计矩阵
# W 是 M × 1 的权重列向量
# b 是标量偏置(通过广播机制加到每个样本上)
Z = np.dot(X, W) + b # 结果 Z 是 n × 1 的向量
公式表示为:
Z = X W + b
其中 X ∈ R^(n×M), W ∈ R^(M×1), b ∈ R, Z ∈ R^(n×1)。
这样,向量 Z 中的每个值就对应一个输入样本的净输入。

神经网络中的多个输出
在深度学习中,神经网络通常包含具有多个神经元的隐藏层。这意味着一个输入可以产生多个输出。
假设我们有一个输入特征向量(单个数据点),但隐藏层有 H 个神经元(即 H 个输出)。此时,我们需要一个权重矩阵 W 来处理这种多输出的情况。
以下是处理单个数据点但产生多个输出的计算:
# x 是 M × 1 的输入向量
# W 是 H × M 的权重矩阵(H 个神经元,每个神经元接收 M 个输入)
# b 是 H × 1 的偏置向量(每个神经元一个偏置)
h = np.dot(W, x) + b # 结果 h 是 H × 1 的向量
公式表示为:
h = W x + b
其中 x ∈ R^(M×1), W ∈ R^(H×M), b ∈ R^(H×1), h ∈ R^(H×1)。
结合多个数据点与多个输出
现在,我们将前两个概念结合起来:同时处理 n 个数据点,并且每个数据点产生 H 个输出(即隐藏层有 H 个神经元)。
我们有一个 n × M 的设计矩阵 X 和一个 H × M 的权重矩阵 W。为了进行矩阵乘法并使结果维度便于作为下一层的输入(保持样本数 n 作为第一维),我们通常按如下方式计算:
# X 是 n × M 的设计矩阵
# W 是 H × M 的权重矩阵
# b 是 H × 1 的偏置向量(通过广播机制加到每个样本上)
H = np.dot(X, W.T) + b.T # 或者 H = X W^T + b^T
# 结果 H 是 n × H 的矩阵,每一行是一个样本的 H 维隐藏层表示
公式表示为:
H = X W^T + b^T
其中 X ∈ R^(n×M), W ∈ R^(H×M), b ∈ R^(H×1), H ∈ R^(n×H)。
这种表示方式使得每一层输出的第一维始终是样本数量 n,这在组织数据流时非常直观和方便。

关于符号约定的说明:在一些较早的教材中,可能会将设计矩阵表示为
M × n(即特征为行,样本为列)。在这种情况下,公式中就不需要转置操作(即H = W X + b)。现代深度学习框架和教材更倾向于使用n × M的表示,虽然在线性代数乘法中需要引入转置,但更符合“每一行是一个样本”的直观理解。
为什么使用 Wx 而非 xW 的表示
你可能会注意到,在上述对单个样本的计算中,我们使用了 h = W x + b 的形式,即将权重矩阵 W 放在输入向量 x 之前。这与一些线性代数中的传统表示一致。
这种表示(变换矩阵在前,向量在后)在几何视角下有时更直观。我们可以将 W 视为一个线性变换矩阵。例如,一个单位矩阵 I 对向量 x 进行变换时,不会改变 x。
# 单位矩阵不改变输入向量
I = np.eye(2) # 2x2 单位矩阵
x = np.array([x1, x2])
transformed_x = np.dot(I, x) # 结果仍是 [x1, x2]

更一般地,矩阵 W 中的元素可以理解为对输入空间进行缩放和平移(如果包含仿射变换)的操作。这种视角有助于理解神经网络层如何对输入特征进行变换和组合。
然而,值得注意的是,在实际的深度学习框架(如 PyTorch)中,全连接层的实现通常采用 output = input @ W^T + b 的形式,这与我们上面为了保持维度一致而使用的 X W^T 是等价的。框架会帮我们处理这些细节。

总结
本节课中我们一起学习了神经网络中的核心符号约定:
- 单个数据点:使用特征向量
x和权重向量W进行计算。 - 多个数据点:使用设计矩阵
X,计算变为矩阵乘法,高效处理批量数据。 - 多个输出(神经元):使用权重矩阵
W,为每个神经元定义一组权重。 - 结合批量与多输出:使用
H = X W^T + b^T公式,得到维度为(n_samples, n_neurons)的输出,便于在网络中传递。 - 符号的直观性:
Wx的表示源于线性变换的几何视角,而现代框架的实现细节可能略有不同。


理解这些符号约定是阅读深度学习文献、理解模型架构和进行代码实现的重要基础。在接下来的课程中,我们将看到这些约定如何在 PyTorch 等框架中具体应用。

📚 课程 P31:L4.5 - PyTorch 中的全连接(线性)层
在本节课中,我们将学习 PyTorch 中的全连接层(也称为线性层或稠密层)。我们将了解其数学原理、PyTorch 的实现方式,以及它与传统线性代数表示法的区别。
🔍 什么是全连接/线性层?

上一节我们介绍了神经网络的基本结构,本节中我们来看看其中的核心组件——全连接层。
在一个多层神经网络中,如果不考虑激活函数,层与层之间的变换就是一种线性变换。这种层有时被称为全连接层,有时也被称为稠密层。在 PyTorch 中,它被称为线性层,因为它执行的是线性变换。在 Keras 和 TensorFlow 的语境中,人们则称之为稠密层。这些术语是等价的。
神经网络本质上就是多个这样的全连接层与非线性激活函数交错连接而成的。


🧮 PyTorch 中实现全连接层
了解了基本概念后,我们来看看如何在 PyTorch 中具体实现一个全连接层。

在 PyTorch 中实现全连接层非常简单,有一个名为 torch.nn.Linear 的函数。我们从一个数据集开始。假设我们创建一些随机数据作为有效的训练数据。

以下是一个设计矩阵,维度为 10×5(10个样本,5个特征)。当我们初始化这个线性层时,需要指定输入特征数和输出特征数。这里我们有5个输入特征,假设我们想要3个输出特征。

每个线性层在初始化后,都附有一个权重矩阵和一个偏置向量作为其属性。权重矩阵的维度是 输出特征数 × 输入特征数(本例中为 3×5)。偏置向量的维度等于输出特征数(本例中为3),因为每个输出都连接着一个偏置单元。
以下是初始化后的维度信息:
- 输入
X的维度:10 × 5 - 权重
W的维度:3 × 5 - 偏置
B的维度:3
当我们对这个 10×5 的输入 X 应用线性层(即进行 X @ W.T + B 运算)后,输出是一个 10×3 的矩阵。

🔄 理解 PyTorch 的矩阵乘法约定
看到上面的计算过程,你可能会疑惑维度是如何匹配的。这涉及到 PyTorch 所遵循的特定矩阵乘法约定。
在上一节中,我们提到了将权重矩阵 W 放在输入 X 之前的传统表示法。而 PyTorch 则采用将 X 放在前面的约定。这并非出于几何视角,而是出于数据流视角,因为这样可以使用更少的转置操作,并且更形象地表示了数据在网络中的流动:从 X 开始,乘以权重矩阵 W,得到输出 A。
以下是两种约定的对比:
传统约定(单样本):
- 输入
x为M × 1的列向量。 - 运算为
W @ x,其中W为H × M矩阵。 - 输出为
H × 1的列向量。
PyTorch 约定(单样本):
- 输入
x为1 × M的行向量。 - 运算为
x @ W.T,其中W为H × M矩阵。 - 输出为
1 × H的行向量。

PyTorch 约定(多样本):
- 输入
X为N × M的设计矩阵。 - 运算为
X @ W.T,其中W为H × M矩阵。 - 输出为
N × H的矩阵。
PyTorch 约定的优势在于,无论处理单个还是多个数据点,我们都可以使用相同的运算形式,这在计算上非常方便。
📝 重要注意事项与总结
在编写和实现矩阵乘法时,始终思考点积是如何计算的,这一点非常重要。因为即使维度匹配,计算也可能并非你的本意。务必写下维度、你正在计算的内容以及你期望的输出。
理论直觉和传统约定(将 W 作为变换矩阵放在前面)并不总是与代码中的实际便利性相符。为了帮助你在阅读教材(可能使用不同约定)和编写代码之间转换,以下是一些有用的等式规则:

(A @ B) == (B.T @ A.T).TW @ x == x.T @ W.T
最后,总结一下传统约定与 PyTorch 约定:
- 传统方式:
A = W @ X(需要处理X的转置以适应多样本) - PyTorch 方式:
A = X @ W.T(更简洁,易于处理单样本和多样本)
PyTorch 的选择是基于深度学习通常处理大量输入和输出的实际情况,它减少了所需的转置操作。

🧪 课后思考与实验
作为一个小练习,你可以重新审视感知机的代码。在不运行代码的情况下,思考一下:如果我们一次性输入一个 N×M 的设计矩阵(多个训练样本)进行预测,感知机能否预测类别标签?如果可以,为什么?如果不可以,需要对代码做出什么修改?
你可以运行代码,用设计矩阵作为预测输入,来验证你的直觉。此外,也可以思考训练方法:我们是否也能在训练方法中通过矩阵乘法实现某种并行化(同时处理多个训练样本)?在不根本改变感知机学习规则的前提下,这样做有意义吗?
🎯 本节课总结

在本节课中,我们一起学习了:
- 全连接层(线性层/稠密层)的基本概念。
- 如何在 PyTorch 中使用
torch.nn.Linear实现它。 - PyTorch 在矩阵乘法中采用的
X @ W.T + B约定及其相对于传统约定的优势。 - 在实现线性代数运算时,仔细检查维度的重要性。

下一讲,我们将讨论一个更适合神经网络的学习算法。我们上周学习了感知机规则,但它并非一个非常优秀的学习规则,我们将在下一讲中开发更好的方法。
课程 P32:L5.0 - 梯度下降 🎯
概述
在本节课中,我们将学习一种比感知机更稳健的学习规则——梯度下降。我们将探讨如何训练一个即使在数据非线性可分时也能保证收敛的神经网络模型。本节课的核心是为后续学习多层神经网络和解决更复杂问题(如多分类、图像生成等)奠定算法基础。所有后续的深度学习内容都将是本节课所涵盖的线性代数与微积分基本概念的延伸与应用。

学习模式:在线、批量与小批量 📊
上一节我们介绍了课程的整体目标,本节中我们来看看神经网络训练的几种不同模式。这些模式是通用的概念,适用于所有类型的神经网络,包括单层网络、多层网络、卷积网络等深度学习模型。
以下是三种主要的学习模式:

- 在线学习:每次只使用一个训练样本更新模型权重。
- 批量学习:每次使用整个训练数据集计算梯度并更新权重。
- 小批量学习:每次使用一个小的、随机抽取的数据子集(小批量)来更新权重。
感知机与线性回归的关系 🔗
了解了基本的学习模式后,我们来看看感知机与另一个经典模型——线性回归之间的联系。理解这种关系有助于我们过渡到新的学习算法。
感知机是一个用于二元分类的模型,而线性回归则用于预测连续值。尽管任务不同,但它们在模型形式(一个线性函数)上具有相似性。我们将学习一种用于线性回归的迭代训练算法,这能帮助我们理解如何训练神经网络,因为两者在原理上密切相关。
微积分复习(可选) 📚
在深入梯度下降算法之前,我们需要一些微积分知识作为工具。本节是一个可选的复习环节,旨在帮助那些很久没有接触微积分或对相关概念不太确定的同学。如果你已经非常熟悉,可以跳过本节以缩短学习时间。
微积分的核心概念是导数和梯度,它们描述了函数的变化率。在机器学习中,我们使用梯度来指示如何调整模型参数以减小误差。
对于一个单变量函数 f(x),其在点 x 的导数定义为:
f'(x) = lim (h->0) [f(x+h) - f(x)] / h
对于一个多变量函数 f(w1, w2, ..., wn),其梯度是一个向量,包含对所有参数的偏导数:
∇f = [∂f/∂w1, ∂f/∂w2, ..., ∂f/∂wn]
梯度下降算法 ⬇️
现在,我们进入本节课的核心:梯度下降算法。这是一种通过迭代调整参数来最小化损失函数(或称成本函数)的优化方法。
梯度下降的基本思想是:要找到函数的最小值,我们可以沿着当前点梯度(即最陡上升方向)的反方向移动一小步。因为梯度的反方向是函数值下降最快的方向。

权重更新公式如下:
w_new = w_old - η * ∇J(w_old)
其中:
w代表模型参数(权重)。η是学习率,控制每次更新的步长。∇J(w)是损失函数J在权重w处的梯度。
训练线性神经元:Adaline 🧠
我们将梯度下降算法应用到一个具体的模型上:自适应线性神经元(Adaline)。Adaline 使用线性激活函数,并通过最小化实际输出与连续目标值之间的平方误差和来学习,这本质上就是一个线性回归问题。

以下是训练 Adaline 的关键步骤:
- 初始化权重:为所有权重和偏置项设置小的随机值或零。
- 计算网络输出:对于给定的输入,计算线性加权和
z = w^T * x + b。 - 计算损失:使用均方误差等损失函数计算预测输出与实际目标之间的误差。
- 计算梯度:计算损失函数相对于每个权重和偏置的梯度。
- 更新参数:使用梯度下降公式更新所有权重和偏置。
- 重复:对多个周期(遍历整个训练集)重复步骤 2-5,直到模型收敛。
PyTorch 代码示例 💻
最后,我们将通过一个实际的 PyTorch 代码示例,展示如何使用梯度下降来训练一个线性神经元(Adaline 模型)。这将帮助我们巩固对理论的理解,并了解如何在实践中实现它。
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 准备模拟数据
torch.manual_seed(1)
X = torch.randn(100, 2) # 100个样本,2个特征
y = 3 * X[:, 0] - 2 * X[:, 1] + 1 + 0.1 * torch.randn(100) # 真实关系 + 噪声
# 2. 定义线性模型 (Adaline)
class LinearNeuron(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.linear = nn.Linear(input_dim, 1) # 线性层
def forward(self, x):
return self.linear(x) # 线性激活,直接输出加权和
model = LinearNeuron(input_dim=2)
# 3. 定义损失函数和优化器(梯度下降)
criterion = nn.MSELoss() # 均方误差损失
optimizer = optim.SGD(model.parameters(), lr=0.01) # 随机梯度下降优化器
# 4. 训练循环
num_epochs = 50
for epoch in range(num_epochs):
# 前向传播
predictions = model(X).squeeze()
loss = criterion(predictions, y)
# 反向传播与优化
optimizer.zero_grad() # 清零历史梯度
loss.backward() # 计算梯度
optimizer.step() # 更新参数 (w = w - lr * gradient)
if (epoch+1) % 10 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
# 5. 查看学习到的参数
print("\nLearned weights and bias:")
print(f"Weights: {model.linear.weight.data}")
print(f"Bias: {model.linear.bias.data}")
总结

本节课我们一起学习了机器学习中的核心优化算法——梯度下降。我们从比较不同的学习模式开始,建立了感知机与线性回归的联系,并复习了必要的微积分知识。随后,我们深入探讨了梯度下降的原理,并将其应用于训练线性神经元(Adaline)。最后,通过 PyTorch 代码示例,我们实践了完整的训练流程。这些概念是理解所有现代深度学习模型如何被训练的基础,在后续课程中,我们将把这些知识扩展到更复杂的网络结构和任务中。
课程 P33:L5.1 - 在线、批处理和小批量模式 🧠

在本节课中,我们将学习训练神经网络模型的三种不同方式:在线模式、批处理模式和小批量模式。我们将探讨它们如何利用数据、如何更新模型,以及各自的优缺点。这些概念适用于任何类型的神经网络。
感知机模型回顾 🔄
上一节我们介绍了神经网络训练的基本概念,本节中我们来看看具体的训练模式。首先,让我们简要回顾一下感知机模型。

上图左侧是感知机的示意图,我们计算净输入,然后更新权重。假设我们有一个数据集 D,包含 n 个带标签的训练样本。每个样本都有一个特征向量 x 和一个类别标签 y。
训练开始时,我们初始化感知机的参数。例如,可以将所有权重设置为零,偏置单元也设为零。

# 参数初始化示例
weights = [0, 0, ..., 0]
bias = 0
然后,我们进入训练循环。一个训练周期指的是完整遍历一次训练集的过程。在实践中,训练周期的数量可以是任意的,取决于数据集和问题的复杂度。在深度学习中,训练成百上千个周期也很常见。
在每个训练周期内,我们逐个遍历每个训练样本。这包含两个循环:外层循环遍历训练周期,内层循环遍历数据集中的每个数据点。
以下是训练过程的伪代码描述:
对于 每个训练周期:
对于 数据集中的每个数据点 (x_i, y_i):
计算预测值 y_hat = activation(w * x_i + b)
计算误差 error = y_i - y_hat
根据误差更新权重 w 和偏置 b

关键点在于,更新发生在处理完每个数据点之后。处理第一个样本后,我们更新权重和偏置,然后继续处理第二个样本,并再次更新。这种模式被称为在线模式。
在线模式 🚀
在线模式,有时也称为随机模式,其核心特点是模型在处理完每个单独的训练样本后立即更新。

以下是其步骤总结:
- 遍历每个训练周期。
- 遍历数据集中的每个数据点。
- 对每个数据点,计算输出、误差。
- 立即更新权重和偏置。
这种方法的优点是模型可以快速适应新数据。例如,在Web应用程序中,每获得一个新用户的数据,就可以立即更新模型,而无需等待收集一批用户数据。
在实践中,为了防止算法因数据顺序而陷入循环或振荡,通常在每个训练周期开始前打乱数据集顺序。例如,在鸢尾花数据集中,如果数据按类别排序,模型会先集中学习一个类别,再学习下一个,这不利于学习。打乱数据可以带来更好的多样性和学习效果。

批处理模式 📦
接下来,我们看看批处理模式。它与在线模式的主要区别在于更新的时机。


在批处理模式中:
- 我们仍然遍历训练周期。
- 在每个周期内,我们遍历所有数据点,计算每个点的预测和误差。
- 但是,我们不会在每个点后立即更新模型参数。
- 相反,我们初始化一些“占位符”(例如
delta_w和delta_b)来累积每个数据点计算出的更新信息。 - 在遍历完整个数据集后,我们才使用累积的信息一次性更新权重和偏置。
对比与优缺点:
- 在线模式:更新频繁,学习可能更快,但每次更新只基于一个样本,因此噪声较大,不稳定。
- 批处理模式:每个周期只更新一次,这次更新基于整个数据集的信息,因此更稳健、更准确。但缺点是学习速度较慢,因为必须遍历完所有数据才能进行一次更新。
在深度学习中,这两种模式都不太常用,原因我们将在介绍小批量模式后揭晓。
小批量模式 ⚖️

现在,让我们进入深度学习中最常用的训练模式——小批量模式。它是在线模式和批处理模式的折中方案。

在小批量模式中:
- 我们初始化参数,并遍历训练周期。
- 在每个周期内,我们不再逐个遍历数据点,而是将数据集划分为若干个较小的小批量。
- 每个小批量是数据集的一个子集,常见的大小有32、64、128、256等,这是一个可以调整的超参数。
- 对于每个小批量,我们执行类似批处理模式的操作:
- 初始化更新占位符。
- 遍历该小批量内的每个数据点,计算预测和误差,并累积更新信息。
- 处理完该小批量所有数据后,用累积的信息更新模型参数。
为什么小批量模式成为深度学习的主流?
以下是几个关键原因:

-
向量化计算效率:通过将一个小批量的数据组成矩阵(例如,将
m个样本的特征堆叠成m x n的矩阵),我们可以利用线性代数库进行高效的矩阵运算,这比在循环中逐个处理样本要快得多。# 小批量向量化计算示例 (伪代码) # X_batch 形状: (batch_size, n_features) # W 形状: (n_features, ) predictions = activation(np.dot(X_batch, W) + b) -
平衡噪声与稳定性:相比在线模式(噪声过大),小批量模式通过一次使用多个样本进行更新,减少了更新的方差,使训练过程更稳定。同时,相比批处理模式(噪声过小),它保留了一定的噪声,这有助于模型跳出局部最优解。
-
内存与可行性:现代深度学习数据集非常庞大(数百万样本)。使用全批处理模式意味着需要将整个数据集加载到内存中进行一次矩阵运算,这在计算资源和内存上通常是不可行的。小批量模式则解决了这个问题。
-
更新频率更高:相比每个周期只更新一次的批处理模式,小批量模式在一个周期内会进行多次更新(次数等于小批量的数量),这通常能加速学习过程。
总结 📝
本节课中,我们一起学习了神经网络的三种训练模式:
- 在线模式:逐个样本处理并立即更新。更新快,但噪声大。
- 批处理模式:遍历整个数据集后一次性更新。更新稳健,但速度慢,且对大数据集不友好。
- 小批量模式:将数据分成小批量,对每个小批量进行类似批处理的更新。它结合了计算效率、适度的噪声控制以及对大规模数据的可行性,因此成为深度学习中训练模型的标准方法。

理解这些数据利用方式,为我们接下来学习梯度下降和随机梯度下降等优化算法奠定了重要基础。在后续课程中,我们将主要使用小批量模式来训练复杂的神经网络。


课程 P34:L5.2 - 感知器与线性回归的关系 📊
在本节课中,我们将探讨感知器与线性回归之间的紧密联系。理解这种关系有助于我们更好地掌握梯度下降等概念,并为后续学习自适应线性神经元(Adaline)奠定基础。

感知器模型回顾 🔄
上一节我们介绍了感知器算法,本节中我们来看看它的基本结构。感知器模型包含多个计算步骤。
以下是感知器算法的核心流程:
- 计算净输入:基于数据集特征、权重以及偏置单元计算净输入。
- 应用激活函数:在感知器中,激活函数是阈值函数。其规则为:如果净输入
z > 0,则输出1;否则输出0。公式表示为:if z > 0: return 1 else: return 0
线性回归作为单层神经网络 🧠
线性回归可以看作是感知器的一个特例。在感知器模型中,如果我们用恒等函数替换阈值激活函数,就得到了线性回归模型。
恒等函数意味着直接输出输入值,不做任何变换。因此,线性回归的预测值就是其净输入值。其计算过程与感知器完全相同:

- 净输入
z的计算公式为:z = w1*x1 + w2*x2 + ... + wn*xn + b - 预测输出
y_hat即为z:y_hat = z

通过这种方式,我们可以将线性回归理解为一个单层神经网络或线性神经元。
线性回归的参数求解方法 ⚙️
在传统的统计学课程中,线性回归通常通过解析法求解。
以下是求解最小二乘线性回归的解析方法:


- 使用正规方程可以直接计算出最优参数。其公式为:
其中,w = (X^T * X)^(-1) * X^T * yX是设计矩阵(通常包含一列1以将偏置项b并入权重向量w中),y是目标值向量。
这种方法在大多数数据集上高效且直接,是实践中推荐使用的方法。然而,当数据集非常庞大时,矩阵求逆运算可能带来计算挑战。

引入迭代学习算法的原因 🚀
尽管正规方程是线性回归的推荐解法,但在本课程中,我们将学习一种不同的、迭代式的参数学习方法。
以下是采用迭代方法的主要原因:
- 为深度学习做准备:深度神经网络通常具有大规模数据集、大量连接(神经元)以及非凸的损失函数,不存在封闭形式的解析解,必须依赖迭代优化算法。
- 降低学习门槛:由于大家已从其他课程熟悉线性回归,用它来引入梯度下降等迭代算法,将使概念理解变得更容易。
在接下来的视频中,我们将详细介绍这种迭代算法,并结合一些微积分概念,观察其在实际中如何工作。
总结 📝

本节课我们一起学习了感知器与线性回归之间的关系。我们了解到线性回归本质上是使用恒等函数作为激活函数的单层感知器。同时,我们回顾了通过正规方程解析求解线性回归参数的方法,并阐述了为何在本课程中需要转向学习迭代优化算法,这为我们进入梯度下降和神经网络的核心训练方法做好了准备。
📊 课程 P35:L5.3 - 线性回归的迭代训练算法
在本节课中,我们将学习如何使用迭代算法来训练线性回归模型。我们将从一种非常朴素的方法开始,然后介绍一种更高效的方法——随机梯度下降,并解释其背后的原理。
🧠 迭代训练算法概述
上一节我们介绍了线性回归模型的基本概念。本节中,我们来看看如何通过迭代算法来训练这个模型。
一种训练最小二乘线性回归模型的迭代方法是使用暴力搜索。这是一种非常朴素的方法,适用于线性回归或任何类型的神经网络。
以下是该方法的步骤:
- 将参数(权重和偏置)初始化为全零或小的随机数。
- 进行 K 轮循环。
- 在每一轮中,随机选择一组新的权重。
- 计算线性回归模型的预测值。
- 如果新权重使模型性能更好,则保留这组权重。
- 如果新权重使模型性能更差,则丢弃它们。
这种方法理论上可以保证找到最优解,因为只要尝试足够多次,总有机会找到最优的权重组合。然而,这种方法效率极低,速度非常慢,因此不推荐在实践中使用。
🚀 更优的迭代方法:梯度分析
幸运的是,存在一种更好的迭代方法来拟合线性回归模型。我们可以分析改变参数对模型预测性能的影响。
具体方法是观察平方误差损失函数,当我们以特定方式改变权重和偏置时,误差如何变化。然后,我们可以朝着能提升性能的方向,对权重和偏置进行微小的调整。
如果我们理解了权重与损失之间的关系,就可以调整权重以使损失下降,从而减小误差。我们可以多次重复这个小步骤,直到损失不再进一步降低。
事实证明,这实际上就是我们之前讨论过的在线学习模式,只是写法略有不同。
🔄 随机梯度下降
对于线性回归,有一个称为随机梯度下降的算法。它是感知器学习规则在凸损失函数上的类比(我们也可以在后续课程中将其用于神经网络的非凸损失函数)。
这里我们聚焦于线性回归模型,并在本课末尾简要提及自适应线性神经元。
以下是随机梯度下降与感知器学习规则的异同比较:
-
相同点:
- 权重初始化方式相同。
- 都迭代训练周期。
- 都在数据集中迭代遍历训练样本。
- 计算预测值的方式相同(都是计算净输入
z)。区别在于,感知器使用阈值函数,而线性回归使用恒等函数。
-
不同点:
- 误差计算:感知器是预测类别与实际类别的差值;线性回归是预测值与实际连续值的差值,但形式相似。
- 权重更新:感知器规则是
w := w + Δw,其中Δw = η * (y - ŷ) * x。随机梯度下降的更新规则基于损失函数的梯度。
随机梯度下降的权重更新公式为:
w := w - η * ∇L(w)
其中 η 是学习率,∇L(w) 是损失函数 L 关于权重 w 的梯度。我们通过减去梯度(即负梯度方向)来更新参数。
这种方式与感知器规则相似,但梯度是基于微积分计算得出的。对于像阈值函数这样的非光滑函数,我们无法计算导数,因此感知器规则并非直接源于梯度。
📝 循环实现与向量化实现
上一张幻灯片展示了线性回归在线学习模式的向量化实现,其中 x 是特征向量,梯度也是向量。
我们也可以使用循环来展开这个过程,这样就不需要直接处理梯度,而是讨论偏导数。
假设输入数据的维度为 M,即有 M 个特征。那么权重的数量也等于 M。我们可以对每个权重单独计算损失函数关于该权重的偏导数,然后以类似的方式进行更新。
以下是循环版本的简化表示:
for j in range(M): w_j := w_j - η * (∂L/∂w_j)
这种基于偏导数的循环版本在概念上可能更容易理解。然而,正如上一讲所解释的,向量化实现速度更快,这就是我们通常使用梯度和基于向量的实现的原因。
🧮 学习规则的由来
我展示了随机梯度下降的学习规则,但并未说明这个规则从何而来。为了理解其推导过程,需要一些微积分知识。
如果你对此有些生疏,可以参考本课后的两个补充视频来复习微积分概念。当然,你也可以直接跳到下一个视频,我会在那里解释这个学习规则的来源。
✅ 总结


本节课中,我们一起学习了线性回归的迭代训练算法。我们从低效的暴力搜索方法入手,进而介绍了通过分析梯度来高效调整参数的原理。我们重点讲解了随机梯度下降算法,比较了它与感知器学习规则的异同,并区分了其向量化实现与循环实现。最后,我们指出了理解该算法需要微积分基础,为后续的推导学习做好了铺垫。
📚 课程 P36:L5.4- (选修)微积分复习 1- 导数
在本节课中,我们将要学习导数的基本概念。导数是微积分的核心,它描述了函数值随输入变化的速率,也就是我们常说的“斜率”。理解导数对于后续学习梯度下降等机器学习算法至关重要。

📈 什么是导数?
上一节我们介绍了课程目标,本节中我们来看看导数的基本定义。
导数可以理解为函数的“变化率”或“斜率”。我们通过一个简单的例子来理解:函数 F(x) = 2x。
假设我们选取输入值 x = 3,那么输出值 F(3) = 2 * 3 = 6。
我们再选取第二个点,将输入值增加 Δx = 4,得到 x = 7,此时输出值 F(7) = 2 * 7 = 14。
在这两个点之间,输出值的变化是 14 - 6 = 8,输入值的变化是 7 - 3 = 4。因此,斜率(变化率)为 8 / 4 = 2。
更正式地,我们可以用以下公式计算两点间的平均斜率:
斜率 = [F(a + Δa) - F(a)] / [(a + Δa) - a] = [F(a + Δa) - F(a)] / Δa
对于函数 F(x) = 2x,其斜率恒为 2。这意味着输入值每增加1个单位,输出值就增加2个单位。

🔬 导数的正式定义
上一节我们通过一个较大的变化量理解了斜率的概念,本节中我们来看看导数的正式定义,它关注的是输入值发生无穷小变化时的瞬时变化率。
导数的正式定义是当 Δx 趋近于0时,上述斜率公式的极限值。它有两种常见的表示法:
- 拉格朗日表示法:F'(x)
- 莱布尼茨表示法:dF(x)/dx
莱布尼茨表示法在深度学习中讨论偏导数和梯度时会更加有用。
让我们用这个正式定义来计算 F(x) = 2x 的导数:
dF(x)/dx = lim(Δx -> 0) [F(x + Δx) - F(x)] / Δx
= lim(Δx -> 0) [2(x + Δx) - 2x] / Δx
= lim(Δx -> 0) [2x + 2Δx - 2x] / Δx
= lim(Δx -> 0) [2Δx] / Δx
= lim(Δx -> 0) 2
= 2
计算结果表明,函数 2x 的导数确实是 2。

🧮 计算更复杂函数的导数
上一节我们计算了线性函数的导数,本节中我们来看看如何计算更复杂函数的导数,例如 F(x) = x²。
我们同样使用导数的定义公式:
dF(x)/dx = lim(Δx -> 0) [F(x + Δx) - F(x)] / Δx
= lim(Δx -> 0) [(x + Δx)² - x²] / Δx
= lim(Δx -> 0) [x² + 2xΔx + (Δx)² - x²] / Δx
= lim(Δx -> 0) [2xΔx + (Δx)²] / Δx
= lim(Δx -> 0) [2x + Δx]
= 2x

因此,函数 x² 的导数是 2x。
🎨 导数的几何意义
上一节我们进行了符号计算,本节中我们从几何角度直观理解导数的概念。
对于函数 F(x) = x²(一个抛物线),在某一点(例如 x=1)的导数,就是该点处切线的斜率。
我们之前用 Δx 计算斜率的方法,实际上是连接点 (x, F(x)) 和点 (x+Δx, F(x+Δx)) 的割线的斜率。
当 Δx 非常大时,这条割线是切线的一个粗糙近似。
随着 Δx 不断减小并趋近于0,这条割线会无限逼近该点的切线。因此,导数在几何上就是函数图像在某一点切线的斜率。

📋 常用导数公式

以下是机器学习中最常用的一些导数公式,可以作为速查表使用:
- d/dx (c) = 0 (常数函数的导数为0)
- d/dx (xⁿ) = n*xⁿ⁻¹ (幂函数法则)
- d/dx (eˣ) = eˣ
- d/dx (ln(x)) = 1/x
- d/dx (sin(x)) = cos(x)
- d/dx (cos(x)) = -sin(x)

⚙️ 重要的导数运算法则
除了基本公式,掌握导数的运算法则对于处理复杂函数至关重要。
以下是核心的运算法则:
- 加法法则:d/dx [f(x) + g(x)] = f'(x) + g'(x)
- 减法法则:d/dx [f(x) - g(x)] = f'(x) - g'(x)
- 乘法法则:d/dx [f(x) * g(x)] = f'(x)g(x) + f(x)g'(x)
- 链式法则:d/dx [f(g(x))] = f'(g(x)) * g'(x)
其中,链式法则是深度学习的基石。在计算损失函数相对于权重的导数(即梯度下降的核心)时,本质上就是链式法则的反复应用。

🔗 链式法则详解
上一节我们提到了链式法则的重要性,本节中我们深入理解其工作原理。
假设有一个复合函数 F(x) = f(g(x))。计算其导数的链式法则为:
dF/dx = (df/dg) * (dg/dx)
我们可以将其视为一个计算图:
- 输入 x 首先进入内层函数 g,得到中间结果 u = g(x)。
- 中间结果 u 再进入外层函数 f,得到最终输出 F(x) = f(u)。
- 求导时,先计算外层函数 f 对中间结果 u 的导数 df/du。
- 再计算内层函数 g 对输入 x 的导数 dg/dx。
- 最后将这两个导数相乘,得到最终结果。

幸运的是,在PyTorch等现代深度学习框架中,autograd(自动求导)包会自动构建这样的计算图并为我们计算导数。

📝 链式法则应用示例
让我们通过一个具体例子来应用链式法则:计算函数 F(x) = ln(√x) 的导数。
- 识别内外函数:外层 f(u) = ln(u),内层 g(x) = √x = x^(1/2)。
- 分别求导:
- 外层导数:df/du = 1/u = 1/√x
- 内层导数:dg/dx = (1/2) * x^(-1/2) = 1/(2√x)
- 应用链式法则:
dF/dx = (df/du) * (dg/dx) = (1/√x) * (1/(2√x)) = 1/(2x)
链式法则可以推广到任意多层嵌套的函数。对于函数 F(x) = f₅(f₄(f₃(f₂(f₁(x))))),其导数是所有层导数从外到内的连续乘积。

🎯 课程总结

本节课中我们一起学习了导数的核心概念。
我们首先从直观的“斜率”和“变化率”入手,然后学习了导数的正式定义和计算方法。
我们探讨了常见函数的导数公式、重要的运算法则(尤其是链式法则),并通过几何视角和计算图加深了理解。
掌握这些基础知识,将为后续学习梯度下降和神经网络的反向传播打下坚实的数学基础。

📚 课程 P37:L5.5- (选修)微积分复习 2- 梯度
在本节课中,我们将要学习多变量函数的导数,即梯度。我们将从梯度的基本概念开始,逐步深入到链式法则在多元函数中的应用,并简要介绍雅可比矩阵。理解这些概念对于后续学习机器学习和深度学习中的优化算法至关重要。
🧭 梯度的概念
上一节我们介绍了单变量函数的导数。本节中,我们来看看当函数有多个输入变量时,导数该如何定义。
我们关注的是具有多个输入但只有一个输出的函数。例如,在线性回归中,一个训练样本可以有多个特征(如花萼长度、宽度等),但输出只有一个连续值或类别标签。
对于一个多变量函数,其导数是一个向量,称为梯度,用符号 ∇(读作“Nabla”)表示。梯度中的每一个元素都是一个偏导数。

偏导数的计算与全导数类似,但有一个关键区别:在计算函数对某个特定变量的偏导数时,我们将其他所有输入变量都视为常数。计算规则与普通导数相同。

以下是计算梯度的通用公式:
∇f(x, y, z) = [ ∂f/∂x, ∂f/∂y, ∂f/∂z ]
📝 梯度计算示例
让我们通过一个具体的例子来理解如何计算梯度。
考虑函数 f(x, y) = x²y + y。这个函数有两个输入变量 x 和 y。
要计算其梯度,我们需要分别计算两个偏导数。

首先,计算 f 对 x 的偏导数。此时,我们将 y 视为常数。
∂f/∂x = ∂(x²y)/∂x + ∂(y)/∂x
= 2x * y + 0
= 2xy
接着,计算 f 对 y 的偏导数。此时,我们将 x 视为常数。
∂f/∂y = ∂(x²y)/∂y + ∂(y)/∂y
= x² * 1 + 1
= x² + 1
因此,函数 f(x, y) 的梯度为:
∇f = [ 2xy, x² + 1 ]
🔗 多元链式法则
现在,我们考虑一种更复杂的情况:复合函数。假设有一个函数 f,它本身是两个函数 g 和 h 的输出,而 g 和 h 又都依赖于同一个变量 x。
函数结构如下:f(g(x), h(x))。
为了计算 f 对最终输入 x 的导数,我们需要使用多元链式法则。其核心思想是,x 的变化会同时通过 g 和 h 两条路径影响 f,因此总导数是这两条路径贡献之和。

多元链式法则的公式如下:
df/dx = (∂f/∂g) * (dg/dx) + (∂f/∂h) * (dh/dx)
🧮 多元链式法则应用示例
让我们通过一个例子来应用这个法则。
假设函数定义为:f(g, h) = g²h + h。
其中,g(x) = 3x,h(x) = x²。
我们的目标是计算 df/dx。
根据链式法则,我们需要计算四个部分:
- ∂f/∂g:将 h 视为常数,对 g 求偏导。
∂f/∂g = ∂(g²h)/∂g + ∂(h)/∂g = 2gh + 0 = 2gh - dg/dx:g(x) = 3x 对 x 的导数。
dg/dx = 3 - ∂f/∂h:将 g 视为常数,对 h 求偏导。
∂f/∂h = ∂(g²h)/∂h + ∂(h)/∂h = g² * 1 + 1 = g² + 1 - dh/dx:h(x) = x² 对 x 的导数。
dh/dx = 2x
现在,将它们组合起来:
df/dx = (∂f/∂g)*(dg/dx) + (∂f/∂h)*(dh/dx)
= (2gh) * 3 + (g² + 1) * 2x

最后,将 g = 3x 和 h = x² 代入并简化:
df/dx = 2*(3x)*(x²)*3 + ((3x)² + 1)*2x
= 18x³ + (9x² + 1)*2x
= 18x³ + 18x³ + 2x
= 36x³ + 2x
📐 向量表示法
上述链式法则可以用更紧凑的向量形式表示。定义向量 v = [g, h]ᵀ。
那么,导数可以写成梯度向量与内函数导数向量的点积:
df/dx = ∇f · dv/dx = [ ∂f/∂g, ∂f/∂h ] · [ dg/dx, dh/dx ]ᵀ
这等价于我们之前展开的计算:(∂f/∂g)*(dg/dx) + (∂f/∂h)*(dh/dx)。向量表示法在书写复杂表达式时更加简洁。

🧩 雅可比矩阵简介
当函数的输出本身也是一个向量时(即多输入、多输出),梯度的概念就扩展为雅可比矩阵。
假设有一个函数向量 F = [f₁, f₂, ..., fₘ]ᵀ,每个 fᵢ 都依赖于输入向量 X = [x₁, x₂, ..., xₙ]。

雅可比矩阵 J 是一个 m × n 的矩阵,包含了所有输出对所有输入的偏导数:
J = [ ∂f₁/∂x₁ ∂f₁/∂x₂ ... ∂f₁/∂xₙ
∂f₂/∂x₁ ∂f₂/∂x₂ ... ∂f₂/∂xₙ
...
∂fₘ/∂x₁ ∂fₘ/∂x₂ ... ∂fₘ/∂xₙ ]
雅可比矩阵的每一行,其实就是单个输出函数 fᵢ 的梯度向量。

⏭️ 关于二阶导数
在机器学习和深度学习的当前实践中,我们主要使用一阶导数(梯度)进行优化,例如梯度下降法。虽然二阶导数(如海森矩阵)在理论上能提供更优的优化信息,但其计算成本非常高,且在实际应用中并未展现出相对于一阶方法的明显优势。因此,本课程将不深入探讨二阶优化方法。


本节课中我们一起学习了多变量函数微积分的核心概念。我们定义了梯度,它是由偏导数组成的向量。我们探讨了多元链式法则,用于计算复合函数的导数,并学会了用向量形式简洁地表示它。最后,我们简要介绍了雅可比矩阵,它是梯度向量的高阶推广。掌握这些知识,将为理解神经网络中的反向传播算法奠定坚实的基础。
课程 P38:L5.6 - 理解梯度下降 🧠

在本节课中,我们将学习如何使用梯度下降法来训练一个线性回归模型。我们将从理解损失函数和梯度的概念开始,逐步推导出权重更新的公式,并讨论学习率、批量大小以及数据标准化等关键因素对训练过程的影响。
线性回归与损失函数
上一节我们介绍了函数导数和梯度的概念。本节中,我们来看看如何将这些概念应用于线性回归模型的训练。
考虑一个简单的线性回归模型,其激活函数是恒等函数。模型的预测输出为:

公式: y_hat = w^T * x + b
为了找到最优的权重 w 和偏置 b,我们需要最小化模型的预测误差。在线性回归中,通常使用最小二乘法,对应的损失函数是平方误差损失:
公式: L = Σ (y_hat_i - y_i)^2
这个损失函数是凸函数,其图像呈碗状(或抛物线状)。我们的目标是找到使损失函数值最小的权重,即这个“碗”的底部。
梯度下降的核心思想

理解了损失函数的形状后,我们来看看如何使用梯度下降法找到这个最小值点。
梯度下降法的核心是利用函数的梯度(或导数)来指导参数的更新方向。在损失函数的图像上,梯度指向函数值增长最快的方向。因此,负梯度方向就是函数值下降最快的方向。
我们从某个初始权重(例如全零或小的随机数)开始。计算该点处损失函数关于权重的梯度,然后沿着负梯度方向更新权重:
公式: w_new = w_old - η * ∇L(w_old)
其中,η 是学习率,它控制着每次更新的步长。∇L(w) 是损失函数在 w 处的梯度。

学习率的选择
学习率 η 是一个超参数,其选择至关重要。
以下是学习率选择不当可能带来的问题:

- 学习率过大:更新步长太大,可能导致在最小值点附近震荡甚至发散,无法收敛。
- 学习率过小:更新步长太小,收敛速度非常慢,需要很多次迭代才能到达最小值点。
在实践中,需要尝试不同的学习率,以找到一个能使模型有效且稳定收敛的值。
权重更新公式推导
现在,我们来具体推导线性回归中权重的更新公式。我们考虑损失函数对单个权重 w_j 的偏导数。
损失函数为:
L = 1/(2n) * Σ (y_hat_i - y_i)^2
其中 y_hat_i = Σ (w_k * x_ik) + b。为了简化,我们暂时忽略偏置 b,或将其视为一个特殊的权重。
根据链式法则,损失函数 L 对权重 w_j 的偏导数为:
∂L/∂w_j = 1/n * Σ ( (y_hat_i - y_i) * x_ij )
因此,权重 w_j 的更新规则为:
公式: w_j := w_j - η * [ 1/n * Σ ( (y_hat_i - y_i) * x_ij ) ]
对于偏置 b,其更新规则类似,只是对应的特征值 x_ij 为 1。

批量梯度下降 vs 随机/小批量梯度下降
根据计算梯度时使用的数据量不同,梯度下降主要有三种模式:
- 批量梯度下降:使用整个训练集的数据计算梯度。每次更新方向准确,但计算开销大,对于大数据集不实用。
- 随机梯度下降:每次仅使用一个训练样本计算梯度并更新权重。更新频繁且带有噪声,有助于跳出局部极小值,但收敛路径不稳定。
- 小批量梯度下降:折中方案。每次使用一小批(mini-batch)数据计算梯度。这是深度学习中最常用的方法,在计算效率和收敛稳定性之间取得了平衡。

对于凸优化问题(如线性回归),批量梯度下降可以沿直线路径收敛到全局最优。而对于非凸问题(如深度神经网络),SGD 或 Mini-batch GD 的噪声特性反而有助于逃离局部最优解。
数据标准化的重要性

在训练开始前,对输入特征进行标准化(或归一化)是一个重要的步骤。
如果特征尺度差异很大(例如,一个特征是年龄(0-100),另一个特征是收入(0-1000000)),损失函数的等高线会变得又长又窄(椭圆形)。这会导致梯度下降的路径变得曲折,需要很多次迭代才能收敛,或者需要非常小心地调整学习率。

将特征标准化到相近的尺度(例如均值为0,方差为1),可以使损失函数的等高线更接近圆形,从而让梯度下降更高效、更稳定地收敛。
总结与延伸
本节课中,我们一起学习了梯度下降法的基本原理及其在线性回归中的应用。
我们掌握了以下核心内容:
- 梯度下降通过计算损失函数的负梯度来迭代更新模型参数,以最小化损失。
- 更新公式为:
w := w - η * ∇L(w)。 - 学习率
η控制更新步长,需要仔细调优。 - 根据数据使用量,梯度下降分为批量、随机和小批量三种模式,各有适用场景。
- 对输入特征进行标准化能显著改善训练过程的效率和稳定性。


这些概念是训练几乎所有机器学习模型,尤其是复杂深度神经网络的基础。在后续课程中,我们将看到如何将这些完全相同的原理应用到具有多层结构和非线性激活函数的深度网络中。
课程 P39:L5.7 - 训练自适应线性神经元 (Adaline) 🧠

概述
在本节课中,我们将学习如何用代码实现并训练一个自适应线性神经元模型。我们将回顾其与感知机的区别,理解其数学原理,并通过一个简单的例子来直观地展示其工作过程。

回顾 Adaline 模型
上一节我们介绍了神经网络的历史背景。现在,我们来看看 Adaline 模型的具体细节。
Adaline 是由 Widrow 和 Hoff 在 1960 年代左右提出的一个模型。最初它是一个物理设备,但如今我们可以在软件中轻松实现。与之前讨论的感知机不同,Adaline 是一个可微分的神经元模型。感知机由于使用了阶跃函数而不可微,而 Adaline 的改进使其即使在数据非线性可分的情况下也能收敛。
为了更清晰地比较,以下是感知机与 Adaline 的核心区别:
-
感知机流程:
- 计算净输入:
z = w^T x。 - 将净输入通过一个阶跃函数,得到预测的类别标签(0 或 1)。
- 比较预测标签与实际标签。
- 如果预测错误,则根据误差更新权重。
- 计算净输入:
-
Adaline 流程:
- 计算净输入:
z = w^T x。 - 使用一个恒等函数作为激活函数,因此输出
a = z(一个连续值)。 - 将连续输出
a与真实标签(也是连续值,在分类中常为 0 或 1)比较,在阶跃函数之前计算误差。 - 利用这个误差和梯度下降法来更新权重。
- 在预测时,将最终的连续输出
a通过一个阶跃函数转换为类别标签。
- 计算净输入:
关键区别在于误差计算的位置。感知机在阶跃函数后计算误差,而 Adaline 在阶跃函数前计算误差。这使得 Adaline 能够利用可微分的特性,使用梯度下降等优化算法来学习权重。
这里引入“激活函数”的概念是为了通用性。目前我们使用恒等函数,但在后续构建多层神经网络以解决复杂问题时,我们将使用非线性激活函数(如 Sigmoid 函数)。如果多层网络只使用线性激活函数,那么整个网络仍然等价于一个线性模型,无法获得更强的表达能力。
模型工作原理图解
理解了基本流程后,我们通过一个图示来直观感受 Adaline 是如何拟合数据的。
假设我们有一个简单的二分类问题,只使用一个特征值。我们可以将类别标签(0 或 1)绘制在 y 轴上,特征值绘制在 x 轴上。

Adaline 作为一个线性模型,其学习过程类似于线性回归:它会尝试找到一条最佳拟合直线,使得连续输出值尽可能接近真实的类别标签值(这里被视作连续的 0 和 1)。
在模型训练完成后,为了进行分类预测,我们需要一个决策阈值。例如,我们可以设定阈值为 0.5:
- 如果模型的连续输出
a大于 0.5,则预测类别为 1。 - 如果模型的连续输出
a小于或等于 0.5,则预测类别为 0。
这样,我们就将回归问题得到的连续预测值,转换为了分类任务所需的离散类别标签。
总结
本节课我们一起学习了自适应线性神经元模型。我们回顾了 Adaline 的历史,并重点比较了它与感知机在误差计算和可微分性上的核心区别。我们了解到,Adaline 通过在阶跃函数前计算误差,使得能够应用基于梯度的优化方法。最后,我们通过一个简单的图示,理解了 Adaline 如何拟合数据并通过阈值函数完成分类决策。

在接下来的视频中,我们将通过具体的 Python 代码示例,亲手实现并训练一个 Adaline 模型,将理论付诸实践。

课程 P4:L1.2 - 什么是机器学习? 🧠
在本节课中,我们将要学习机器学习的基本概念,了解它与传统编程的区别,并初步认识其与人工智能、深度学习的关系。
概述
机器学习是人工智能的一个重要分支,它赋予计算机从数据中自动学习并做出决策的能力,而无需进行明确的编程。本节将介绍机器学习的核心思想,并通过实例说明其工作原理。
传统编程范式
在深入机器学习之前,我们先回顾传统的编程范式。传统编程的目的是开发软件,使常见任务变得更加便捷。例如,电子邮件取代了传统邮件,使通信更加方便。
假设我们想设计一个垃圾邮件过滤器。作为程序员,我们需要设计这个过滤器程序。程序接收输入(电子邮件),并产生输出(标签),即判断邮件是否为垃圾邮件,或将其放入正确的文件夹(收件箱或垃圾箱)。
设计这样的过滤器可能非常繁琐。我们需要查看大量邮件样本,对比垃圾邮件和非垃圾邮件,然后制定规则。例如,如果邮件主题包含“赢钱”等词语,则将其归类为垃圾邮件。我们需要不断测试和添加规则,这个过程工作量巨大。

机器学习范式
与传统编程不同,机器学习提供了一种更高效的解决方案。在机器学习范式中,我们不再手动编写规则,而是向算法提供输入和对应的输出(标签)。

我们提供一个已标记的数据集,其中包含电子邮件及其对应的“垃圾邮件”或“非垃圾邮件”标签。人类需要先标记这些数据,以向计算机展示我们期望的结果。然后,计算机可以自动从这些示例中学习规则,生成一个能够进行分类的模型。
这个机器学习模型实际上取代了程序员手动开发程序的过程。我们可以直接将模型交给计算机,使其能在新数据上产生新的输出。因此,机器学习在某种程度上是从示例中自动学习。
正如 Arthur Samuel 的著名定义:机器学习是赋予计算机无需明确编程即可学习能力的研究领域。这意味着我们提供示例,算法基于数据自动学习,而无需人类思考具体规则。
机器学习与人工智能、深度学习的关系
你可能会问,机器学习与深度学习和人工智能有何关系?如今,深度学习和人工智能经常被互换使用,但它们之间存在区别。

机器学习是最大的领域,它包含了深度学习。深度学习严格来说是机器学习的一个子领域,专注于深度神经网络。人工智能则与两者都有交集。
需要指出的是,并非所有人工智能方法都使用机器学习。例如,早期的一些AI系统是基于硬编码规则的。
人工智能的类型
人工智能通常可以分为两类:
- 狭义人工智能:专注于解决特定任务,例如图像分类、玩游戏或驾驶汽车。它一次只能完成一项任务。
- 通用人工智能:多用途的人工智能,能够模仿人类在多种任务上的智能。这是许多人的远期目标,但目前尚未实现。

不依赖机器学习的AI

人工智能不一定总是涉及机器学习。回顾之前手动设计垃圾邮件过滤器的例子:如果我添加足够多的 if-else 规则,这个系统也能变得“智能”。这就是所谓的“传统人工智能”,它不依赖于机器学习。
如今,大多数人工智能系统确实使用了机器学习,并且越来越多地使用深度学习,因为深度学习在处理自然语言和计算机视觉任务上通常表现优异。

传统机器学习与深度学习
传统的非深度学习机器学习方法包括广义线性模型(如线性/逻辑回归)、基于树的方法(如随机森林或梯度提升)、支持向量机和K近邻算法等。这些方法在表格数据上仍然非常强大和有用。
深度学习则专注于深度神经网络,它更擅长从原始数据(如图像、语音)中学习。两者并非孰优孰劣,而是适用场景不同。

机器学习的应用实例
机器学习和深度学习在实践中有着广泛的应用:
- 垃圾邮件检测:如前所述。
- 指纹与人脸识别:用于智能手机解锁。
- 网络搜索:谷歌、必应等搜索引擎的核心技术。
- 手写识别:用于邮政系统分拣信件、ATM机读取支票。这是深度学习的早期成功应用之一(如20世纪90年代的LeNet卷积神经网络)。
- 智能助手:如苹果Siri、亚马逊Alexa。
- 产品推荐:如Netflix、亚马逊的推荐系统。
- 自动驾驶汽车:结合了强化学习、图像监督学习等多种技术。
- 语言翻译、情感分析、药物设计、医疗诊断等。
总结

本节课我们一起学习了机器学习的基本概念。我们了解到,机器学习是通过提供数据和期望输出来让计算机自动学习规则的方法,这与需要手动编写规则的传统编程范式形成对比。我们还梳理了机器学习、深度学习和人工智能之间的关系,并列举了机器学习在现实世界中的多种应用。

在接下来的课程中,我们将更深入地探讨机器学习的子类别、工作流程和相关工具。
📘 课程 P40:L5.8 - Adaline 代码示例




在本节课中,我们将通过代码示例学习线性回归和Adaline模型,并分别使用梯度下降和随机梯度下降进行训练。我们将从数据准备、模型实现、训练过程到结果评估,一步步进行讲解。

概述






我们将实现两个模型:一个使用梯度下降训练的线性回归模型,另一个使用随机梯度下降训练的Adaline模型。这两个模型的核心计算步骤非常相似,主要区别在于训练时更新权重的方式。Adaline模型在训练完成后,会使用一个阈值函数来预测类别标签。





1. 环境与数据准备



首先,我们需要导入必要的库并准备数据集。



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



我们使用一个简单的玩具数据集,它包含两个特征 x1、x2 和一个连续的输出值 y,这是一个回归问题。数据集共有1000个样本点。



# 读取数据
df = pd.read_csv('toy_dataset.csv')
print(df.tail()) # 查看最后5行数据



2. 构建设计矩阵与数据划分




接下来,我们将特征和标签转换为PyTorch张量,并划分为训练集和测试集。



上一节我们介绍了数据集,本节中我们来看看如何将其转换为模型可用的格式并进行划分。




# 构建特征矩阵X和标签向量y
X = df[['x1', 'x2']].values
y = df['y'].values


X_tensor = torch.tensor(X, dtype=torch.float32)
y_tensor = torch.tensor(y, dtype=torch.float32).view(-1, 1) # 确保y是列向量



# 生成随机索引以打乱数据
indices = torch.randperm(len(X_tensor))
X_shuffled = X_tensor[indices]
y_shuffled = y_tensor[indices]





# 按7:3比例划分训练集和测试集
train_size = int(0.7 * len(X_shuffled))
X_train = X_shuffled[:train_size]
y_train = y_shuffled[:train_size]
X_test = X_shuffled[train_size:]
y_test = y_shuffled[train_size:]


# 标准化特征(零均值,单位方差)
X_mean = X_train.mean(dim=0)
X_std = X_train.std(dim=0)
X_train = (X_train - X_mean) / X_std
X_test = (X_test - X_mean) / X_std


使用一致的随机索引打乱数据,可以确保特征和标签的对应关系不被破坏。标准化处理有助于模型更快、更稳定地收敛。




3. 实现线性回归模型(梯度下降)





现在,我们来实现线性回归模型。我们将采用面向对象的方式,手动实现前向传播和反向传播(梯度计算)。






以下是线性回归模型类的核心组成部分:



class LinearRegressionGD:
def __init__(self, num_features):
# 初始化权重和偏置为0
self.weights = torch.zeros(num_features, 1, dtype=torch.float32)
self.bias = torch.zeros(1, dtype=torch.float32)
def forward(self, x):
# 计算净输入(线性组合)
net_input = torch.matmul(x, self.weights) + self.bias
# 线性激活函数,直接输出净输入
activation = net_input
return activation.view(-1) # 返回一维向量
def backward(self, x, y_true, y_pred):
# 计算损失函数关于预测值y_pred的梯度
# 损失函数为均方误差:L = (1/n) * Σ(y_true - y_pred)^2
# ∂L/∂y_pred = (2/n) * (y_pred - y_true)
n = x.shape[0]
grad_loss_y_pred = (2.0 / n) * (y_pred - y_true)
# 计算预测值关于权重和偏置的梯度
# y_pred = X * W + b
# ∂y_pred/∂W = X^T
# ∂y_pred/∂b = 1
grad_y_pred_weights = x.t()
grad_y_pred_bias = torch.ones(n)
# 根据链式法则,计算损失关于权重和偏置的梯度
grad_loss_weights = torch.matmul(grad_y_pred_weights, grad_loss_y_pred.view(-1, 1))
grad_loss_bias = torch.sum(grad_loss_y_pred * grad_y_pred_bias)
# 返回负梯度(因为我们要沿梯度反方向更新)
return -grad_loss_weights, -grad_loss_bias




模型解释:
forward方法计算模型的预测输出,即y_pred = X * W + b。backward方法根据链式法则,手动计算损失函数关于权重W和偏置b的梯度。这是梯度下降算法的核心。

4. 训练与评估函数



有了模型,我们需要编写训练循环来迭代更新参数。




def train(model, X_train, y_train, epochs=100, learning_rate=0.01):
cost_history = []
for epoch in range(epochs):
# 前向传播,计算预测值
y_pred = model.forward(X_train)
# 计算当前损失(均方误差)
loss = torch.mean((y_train.view(-1) - y_pred) ** 2)
cost_history.append(loss.item())
# 反向传播,计算梯度
grad_weights, grad_bias = model.backward(X_train, y_train.view(-1), y_pred)
# 更新权重和偏置:W = W - η * ∇W
model.weights += learning_rate * grad_weights
model.bias += learning_rate * grad_bias
# 每10个epoch打印一次损失
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch+1:3d}/{epochs} | Loss: {loss.item():.2f}')
return cost_history




# 实例化并训练模型
model = LinearRegressionGD(num_features=2)
cost_history = train(model, X_train, y_train, epochs=100, learning_rate=0.1)




# 绘制损失下降曲线
plt.plot(cost_history)
plt.xlabel('Epoch')
plt.ylabel('Mean Squared Error Loss')
plt.title('Gradient Descent Training Loss')
plt.show()





训练过程中,损失应随着迭代次数增加而平稳下降,最终收敛。






5. 模型预测与解析解对比



训练完成后,我们可以在训练集和测试集上进行预测,并评估性能。



def evaluate_mse(model, X, y):
y_pred = model.forward(X)
mse = torch.mean((y.view(-1) - y_pred) ** 2)
return mse.item()






train_mse = evaluate_mse(model, X_train, y_train)
test_mse = evaluate_mse(model, X_test, y_test)
print(f'Training MSE: {train_mse:.2f}')
print(f'Test MSE: {test_mse:.2f}')



# 打印模型学到的参数
print(f'Model weights: {model.weights.view(-1).tolist()}')
print(f'Model bias: {model.bias.item()}')





为了验证梯度下降的正确性,我们可以与线性回归的解析解(闭式解)进行对比。解析解公式为:
W* = (X^T * X)^(-1) * X^T * y




# 计算解析解
X_train_np = X_train.numpy()
y_train_np = y_train.numpy().reshape(-1, 1)
# 为X添加一列1,用于计算偏置项
X_b = np.c_[np.ones((X_train_np.shape[0], 1)), X_train_np]
weights_analytic = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y_train_np)



print(f'Analytic solution weights: {weights_analytic[1:].flatten()}')
print(f'Analytic solution bias: {weights_analytic[0].item()}')




理想情况下,梯度下降得到的参数应与解析解非常接近。





6. 实现Adaline模型(随机梯度下降)





现在,我们转向Adaline模型。其代码结构与线性回归几乎完全相同,关键区别在于使用随机梯度下降进行训练,即每次使用一个小批量数据来更新权重。



我们使用鸢尾花数据集的一个子集,并将其转换为二分类问题。


# 数据准备(使用二分类鸢尾花数据)
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data[50:150, :2] # 只取前两维特征和中间两类
y = iris.target[50:150] - 1 # 将标签转换为0和1






# ... (数据转换为张量、划分、标准化的步骤与之前类似)




Adaline模型类的实现与线性回归模型完全一致。




训练函数的区别在于引入了小批量迭代:


def train_sgd(model, X_train, y_train, epochs=50, learning_rate=0.01, batch_size=10):
cost_history = []
n_samples = X_train.shape[0]
for epoch in range(epochs):
# 每个epoch开始时打乱数据
indices = torch.randperm(n_samples)
X_shuffled = X_train[indices]
y_shuffled = y_train[indices]
epoch_loss = 0
# 将数据划分为小批量
for i in range(0, n_samples, batch_size):
X_batch = X_shuffled[i:i+batch_size]
y_batch = y_shuffled[i:i+batch_size]
# 前向传播
y_pred = model.forward(X_batch)
# 计算批次损失
batch_loss = torch.mean((y_batch.view(-1) - y_pred) ** 2)
epoch_loss += batch_loss.item()
# 反向传播
grad_weights, grad_bias = model.backward(X_batch, y_batch.view(-1), y_pred)
# 更新权重
model.weights += learning_rate * grad_weights
model.bias += learning_rate * grad_bias
avg_epoch_loss = epoch_loss / (n_samples // batch_size)
cost_history.append(avg_epoch_loss)
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch+1:3d}/{epochs} | Avg Loss: {avg_epoch_loss:.2f}')
return cost_history





与批量梯度下降相比,随机梯度下降的损失曲线下降更快,但会有更多的波动(噪声)。



7. Adaline的分类与评估


Adaline训练完成后,我们需要使用阈值函数将其连续输出转换为类别预测。




def predict_class(model, X, threshold=0.5):
# 获取连续预测值
y_pred = model.forward(X)
# 应用阈值函数
class_labels = torch.where(y_pred > threshold, torch.ones_like(y_pred), torch.zeros_like(y_pred))
return class_labels





def accuracy(y_true, y_pred):
# 计算预测准确的百分比
correct = (y_true.view(-1) == y_pred).float()
return correct.mean().item()

# 训练模型
adaline_model = LinearRegressionGD(num_features=2) # 使用相同的模型类
cost_history_sgd = train_sgd(adaline_model, X_train, y_train, epochs=100, learning_rate=0.01)

# 预测并计算准确率
y_train_pred = predict_class(adaline_model, X_train)
y_test_pred = predict_class(adaline_model, X_test)


train_acc = accuracy(y_train, y_train_pred)
test_acc = accuracy(y_test, y_test_pred)


print(f'Training Accuracy: {train_acc * 100:.1f}%')
print(f'Test Accuracy: {test_acc * 100:.1f}%')


最后,我们可以绘制Adaline模型在数据集上的决策边界,直观地查看其分类效果。


总结






本节课中我们一起学习了:
- 线性回归与Adaline的关联:两者共享相同的核心模型
y_pred = X*W + b,区别在于Adaline在预测时使用了阈值函数。 - 手动实现梯度计算:我们通过链式法则,手动推导并编码实现了均方误差损失函数关于模型权重的梯度。
- 两种优化算法:
- 批量梯度下降:使用全部训练数据计算梯度,更新平稳,但计算开销大。
- 随机梯度下降:使用小批量数据计算梯度,更新更快且能避免局部极小值,但损失曲线有噪声。
- 完整的机器学习流程:涵盖了从数据准备、模型构建、训练循环到性能评估和结果可视化的所有步骤。
- 与解析解的对比:验证了我们实现的梯度下降算法能够有效找到(接近)最优解。




通过本教程的实践,你应该对梯度下降的工作原理以及如何从零开始实现一个简单的学习算法有了扎实的理解。在接下来的课程中,我们将看到PyTorch如何利用自动微分功能,自动完成繁琐的梯度计算。
深度学习新闻 #3:2021年2月13日计算机视觉前沿 🚀
在本节课中,我们将回顾2021年2月13日当周深度学习领域,特别是计算机视觉方向的最新研究动态与有趣应用。我们将探讨无需归一化的高性能图像识别、深度学习理论假说、提升模型公平性的数据方法、训练数据影响力估计、生成对抗网络在基因组学中的应用,以及一个有趣的计算机视觉估价项目。
1. 无需归一化的高性能图像识别 🖼️
上一节我们介绍了本周新闻的概览,本节中我们来看看一项关于图像识别的重要研究。
研究人员开发了一种深度神经网络,在不使用批归一化(Batch Normalization)的情况下,取得了图像分类任务的新State of the Art(SOTA)结果。SOTA通常指在特定任务上达到了当前最佳或接近最佳的性能。
这项工作的新颖之处在于,他们通过一种自适应梯度裁剪(adaptive gradient clipping)技术,克服了不使用批归一化带来的训练不稳定性问题,从而构建了所谓的归一化自由残差网络(Normalizer-Free Residual Networks)。
以下是该研究性能评估的一个关键点:
- 评估指标:研究主要关注ImageNet Top-1准确率。在ImageNet数据集中,由于图像可能对应多个合理标签,研究者有时也会计算Top-5准确率。
- 计算效率:研究还评估了模型的计算效率,即训练延迟(每秒/步)。这有助于理解模型在不同规模(从计算成本低的F0到成本高的F5)下的表现。
研究结果表明,对于不同规模的模型,他们的方法在准确率上都优于其他高效架构(如EfficientNet、LambdaNet等),证明了无需批归一化也能获得优异性能的可能性。
2. 深度学习理论前沿:假说与讨论 🧠
在了解了具体的模型改进后,我们转向一些更理论性的讨论。本周Reddit机器学习板块有一些关于深度学习理论进展的有趣讨论。
以下是两个被广泛讨论的假说:
- 彩票假说(Lottery Ticket Hypothesis):该假说认为,在一个庞大的、过参数化的网络中,随机初始化的子网络(即“中奖彩票”)本身就具有优异的性能潜力。近期有研究为该假说提供了支持和反对的证据,其有效性仍在讨论中。
- 双下降现象(Double Descent Hypothesis):这一现象描述了模型误差随复杂度变化的非典型曲线。与传统观点认为测试误差会随模型变复杂而先降后升(过拟合)不同,实践发现误差在上升后可能会再次下降,形成一个“双下降”的走势。目前这仍是一个活跃的研究领域。
3. 通过移除偏见数据提升模型公平性与准确率 ⚖️
从理论回到实践,我们来看一个关于机器学习公平性的重要研究。该研究探讨了通过移除训练数据中的偏见数据来提升模型公平性甚至准确率的方法。
研究人员发现,在多个数据集上,通过识别并移除对模型造成不公平影响的训练样本,他们不仅能够将模型的歧视率降低到接近0%,同时还观察到了相比使用全部数据训练的模型更高的准确率。这与通常为提升公平性而牺牲准确率的做法相反,是一个“双赢”的结果。
他们的方法步骤如下:
- 从带有偏见的训练数据开始。
- 使用特征提取器生成相似对(similar pairs),即敏感属性(如性别、种族)不同但其他特征相同的个体对。
- 基于初始模型,按对模型预测的影响力对训练样本进行排序。
- 移除影响力高的偏见数据点,在新数据上重新训练模型并测量歧视率。
- 重复步骤3和4,直到歧视率不再改善。
4. 估计训练数据影响力的新方法:TracIn 🔍
与上一节的数据影响力概念相关,Google AI团队提出了一种名为TracIn的简单方法,用于估计单个训练样本对最终模型的影响。
与大多数可解释性方法关注特征重要性不同,TracIn关注训练样本本身的影响力。该方法在训练过程中记录每个训练样本引起的预测变化,特别有助于检测异常值和解释模型预测。
例如,在一个多分类任务中,TracIn可以显示,展示一个“安全带”图片可能会提高模型对“安全带”的分类能力,但同时可能会暂时增加模型对“西葫芦”分类的损失。这有助于理解不同数据点之间的权衡与影响。
5. GAN的新应用:生成“不存在”的基因组 🧬
接下来,我们将目光转向生成模型。生成对抗网络(GAN)曾被用于生成不存在的人脸(如“This Person Does Not Exist”网站)和猫脸。现在,研究人员将这一思路应用到了基因组学领域。
他们训练GAN来生成高质量、逼真但完全虚构的人类基因组序列。这项工作具有重要意义,因为真实的基因组数据涉及严重的隐私问题。使用这种生成的、不指向任何真实个体的基因组数据,可以在保护隐私的前提下,促进需要大量基因组数据的研究(如训练大规模基因分类模型),缓解数据分享和模型发布中的隐私顾虑。
6. 计算机视觉项目灵感:手袋价格评估应用 💼
最后,我们来看一个有趣的计算机视觉应用实例,或许能为大家的课程项目带来灵感。
一家名为Rebag的公司开发了一款应用,利用计算机视觉模型来估计手袋的转售价格。用户只需拍摄手袋的照片,模型即可给出估价。该项目历时六年开发,使用了数百万数据点进行训练。
这个项目展示了如何将一个直接的计算机视觉想法(图像识别与估价)通过大规模数据收集和模型优化,转化为一个实用的产品。这可以启发类似的课程项目,例如开发用于估计手机、笔记本电脑等其他商品价值的视觉模型。

总结 📝
本节课中我们一起学习了2021年2月中旬深度学习与计算机视觉领域的多项进展:
- 介绍了无需批归一化也能达到SOTA性能的图像识别新方法。
- 探讨了“彩票假说”和“双下降现象”等深度学习理论前沿讨论。
- 学习了一种通过移除偏见数据来同时提升模型公平性与准确率的创新方法。
- 了解了TracIn方法,它通过估计训练样本影响力来帮助解释模型和发现异常值。
- 看到了GAN在生成虚构基因组数据以解决生物信息学隐私问题上的新颖应用。
- 获得了一个关于构建计算机视觉估价应用的实用项目灵感。


希望这些内容能帮助你了解当前的研究动态并激发你的学习兴趣。
课程 P42:L6.0 - PyTorch 中的自动微分 🧠
在本节课中,我们将要学习 PyTorch 框架中的核心功能——自动微分。我们将了解它如何自动计算梯度,从而省去手动推导复杂损失函数导数的繁琐过程,并探索其背后的计算图原理。
第一部分:PyTorch 学习资源 📚
上一节我们介绍了本课程的目标,本节中我们来看看如何获取更多关于 PyTorch 的信息和帮助。
除了本课程,以下资源可以帮助你找到更多信息、教程,并跟进 PyTorch 的新版本发布与发展动态。
以下是 PyTorch 的主要学习资源:
- 官方文档:最权威的 API 参考和教程来源。
- PyTorch 论坛:社区讨论和问题解答的平台。
- GitHub 仓库:查看源代码、提交问题或参与贡献。
- 相关课程与博客:许多大学和开发者提供了额外的学习材料。
第二部分:理解计算图 🕸️
了解了资源获取途径后,本节中我们来看看自动微分背后的核心概念——计算图。
计算图是一种用于可视化一系列计算(如神经网络的前向预测过程)的数据结构。在 PyTorch 内部,它会自动构建这样的计算图。
一个计算图由节点(代表变量或操作)和边(代表数据流)组成。例如,一个简单的运算 c = a + b 可以表示为两个输入节点 a、b 通过一个 “+” 操作节点,连接到输出节点 c。

第三部分:PyTorch 的自动微分机制 ⚙️
基于计算图的概念,本节我们来了解 PyTorch 的 Autograd 模块如何实现自动微分。
PyTorch 在张量(Tensor)上进行运算时会动态构建计算图。当我们调用 .backward() 函数时,它会依据这个计算图,利用反向传播算法,从输出开始反向遍历,自动计算所有相关参数的梯度。
关键步骤如下:
- 在前向传播中记录所有操作。
- 在损失张量上调用
.backward()。 - Autograd 沿计算图反向传播,计算每个参数的梯度并存储在张量的
.grad属性中。
第四部分:应用于 Adaline 模型 🔄
上一讲我们手动训练了 Adaline 模型,并手工计算了导数。本节我们将看到如何使用 PyTorch 自动完成这些繁琐的工作。
我们将使用 PyTorch 重写 Adeline 的训练过程。核心是使用 torch.autograd 功能,只需定义前向传播和损失函数,然后调用 loss.backward(),PyTorch 便会自动计算所有权重参数的梯度。
以下是关键代码对比:
# 手动更新权重(上节课)
# 需要手动推导公式并编码
gradient = ... # 复杂的导数计算
weights = weights - learning_rate * gradient
# 自动更新权重(本节课)
loss = criterion(output, target)
loss.backward() # 自动计算梯度
with torch.no_grad():
weights -= learning_rate * weights.grad
weights.grad.zero_() # 梯度清零
第五部分:PyTorch API 设计模式 🏗️
最后,我们来更深入地了解 PyTorch 的 API 设计,这有助于我们后续实现更复杂的多层神经网络。
PyTorch 主要提供两种子 API 风格:
以下是两种主要的 API:
- 面向对象 API(
torch.nn.Module):通过创建类(继承自nn.Module)来定义模型,将层作为属性,前向传播定义为类方法。这种方式结构清晰,易于管理复杂模型。 - 函数式 API(
torch.nn.functional):提供一系列函数式接口(如F.relu,F.linear),更适用于需要精细控制的前向传播过程,例如在自定义层中。
在实际项目中,通常结合使用两者:用 Module 类组织模型结构,在类内部的 forward 方法中使用函数式 API 进行具体计算。
总结 📝

本节课中我们一起学习了 PyTorch 的自动微分机制。我们首先了解了如何获取 PyTorch 学习资源,然后引入了计算图的概念来解释自动微分的原理。接着,我们将其应用于 Adaline 模型,体验了 .backward() 函数如何替代繁琐的手动求导。最后,我们概览了 PyTorch 面向对象和函数式两种 API 设计模式,为后续构建更复杂的神经网络打下基础。掌握自动微分是高效进行深度学习开发的关键一步。

课程 P43:L6.1 - 深入了解 PyTorch 🧠
在本节课中,我们将要学习 PyTorch 的更多信息,包括其核心概念、优势、安装方法以及如何获取更多学习资源。PyTorch 是本课程及未来项目中会频繁使用的深度学习库,了解其背景和特性将帮助你更高效地使用它。
PyTorch 概览 📖
PyTorch 是一个基于 Python 的深度学习库。正如你在之前的课程中所见,它与 NumPy 非常相似,都使用多维数组(在 PyTorch 中称为张量)作为核心数据结构。
然而,PyTorch 在此基础上为深度学习提供了更多便利功能。
PyTorch 的起源与发展 🔄
上一节我们介绍了 PyTorch 的基本概念,本节中我们来看看它的历史。PyTorch 基于 Torch 7 构建,后者是一个在 5 到 10 年前非常流行的深度学习库。Torch 7 有一个主要弱点:它基于 Lua 语言实现。
虽然 Lua 与 Python 相似且便于与 C 语言文件交互,但它毕竟不是 Python。由于许多开发者更倾向于使用 Python,因此在 2016 年左右,社区开始将 Torch 7 移植到 Python 中,这个项目就是 PyTorch。
PyTorch 最初使用了大量 Torch 7 的代码,但后来逐渐被重写的代码所取代。它的设计重点在于灵活性和最小化认知负担。
一个编程框架可以通过抽象所有复杂性来变得易于使用,但如果过于简化并提供固定的构建模块,在进行自定义研究(例如开发自己的网络层)时就会变得困难。PyTorch 在这两者之间取得了平衡。

核心特性 ⚙️
以下是 PyTorch 的几个核心特性:
- 自动微分:当你执行计算时,PyTorch 可以自动计算导数,你无需深入了解背后的数学原理,这非常方便。
- 动态计算图:这与静态计算图形成对比。早期的 TensorFlow 或 Theano 等库使用静态图,你需要先定义整个计算流程,然后才能运行,这使得调试变得困难。PyTorch 采用动态方法,更像 NumPy,代码定义后立即执行,无需编译任何计算图,因此更易于使用。
- NumPy 集成:你可以在 PyTorch 张量和 NumPy 数组之间进行转换。然而,通常我们尽量避免在 PyTorch 中混用 NumPy,因为来回转换会使代码变得冗长。如今,大多数工作都可以直接使用 PyTorch 张量完成。
性能与实现 🚀

PyTorch 的大部分底层代码是用 C++ 和 CUDA 编写的,这就是为什么它比普通的 Python 代码高效得多。
- C++ 是一种底层语言,非常适合科学计算。
- CUDA 可以看作是专门为 GPU 设计的类 C++ 语言。
Python 更像是包裹在 C++ 和 CUDA 代码之上的“可用性粘合剂”,它使一切变得方便易用。
为何选择 PyTorch 而非 NumPy? 🤔

既然 PyTorch 和 NumPy 如此相似,我们为什么选择 PyTorch 呢?原因如下:
- GPU 支持:这对于高效训练深度神经网络至关重要,速度可能比 CPU 快数百甚至数千倍。
- 多设备分布式计算:支持跨多个 GPU 进行计算,对于训练大型模型尤其有帮助。
- 计算图追踪:PyTorch 会记录创建的操作和计算图。虽然这在不需要计算梯度时会占用额外内存,但在深度学习中,我们通常需要计算梯度以最小化损失函数。PyTorch 允许我们在需要时启用或禁用这个功能,非常灵活。
本质上,PyTorch 就是 NumPy 加上了一系列让深度学习研究变得更简单的便利功能。
安装指南 💻
关于安装,我在 Canvas 上发布过详细的说明和建议。这里简要总结一下:
- 笔记本电脑:建议安装 CPU 版本。除非你拥有高端游戏本,否则在笔记本电脑的 GPU 上运行可能会导致过热。我个人只在笔记本电脑上调试代码,确保在 CPU 上运行无误后,再通过更改一行代码切换到 GPU 环境运行。
- 安装方法:访问 PyTorch 官方网站,根据你的操作系统(Mac、Windows、Linux)、包管理器(推荐 Conda)和是否需要 CUDA 来获取安装命令。对于本课程,建议一并安装
torchvision和torchaudio库。 - 免费 GPU 资源:在本课程中,你无需购买昂贵的 GPU。我将在后续课程中展示如何使用一些免费的在线 GPU 资源。
- 重要提示:导入 PyTorch 时,使用
import torch,而不是import pytorch。
学习资源与社区 📚

PyTorch 拥有丰富的学习资源和活跃的社区:
- 官方教程:网站上的“Tutorials”板块包含许多有用内容,特别推荐《Deep Learning with PyTorch: A 60 Minute Blitz》入门教程。
- PyTorch 书籍:有一本名为《Deep Learning with PyTorch》的书籍,作者包括 Eli Stevens、Luca Antiga 和 Thomas Viehmann。网站上提供免费在线阅读版本。
- 讨论论坛:PyTorch 拥有一个非常友好和活跃的社区。如果你遇到深入的技术问题,可以在官方论坛提问,通常能很快得到解答。

总结 🎯

本节课中,我们一起学习了 PyTorch 的核心概念、历史、特性以及其相对于 NumPy 的优势。我们还了解了其高性能背后的实现原理,获得了安装指南,并探索了进一步学习的宝贵资源。


PyTorch 以其灵活性、动态计算图和强大的社区支持,成为深度学习研究和应用的优秀工具。在接下来的视频中,我们将深入探讨计算图以及 PyTorch 如何利用它们进行自动微分。
课程 P44:L6.2 - 理解基于计算图的自动微分 🧮

在本节课中,我们将要学习计算图的概念,它是理解现代深度学习框架(如 PyTorch)中自动微分机制的基础。我们将通过一个具体的例子,一步步拆解计算图如何表示计算过程,以及如何利用链式法则在图上进行反向传播以计算梯度。

概述
在 PyTorch 中实现神经网络模型时,通常会定义两个核心方法:forward 和 backward。forward 方法用于构建计算图,而 backward 方法则通过反向遍历该图来计算梯度。理解计算图的结构和工作原理,是掌握自动微分的关键。
从神经网络到计算图

在深度学习和 PyTorch 的语境下,将神经网络视为计算图是非常有帮助的,因为神经网络本质上就是一系列嵌套的计算操作链。
为了有一个具体的例子,我们来看一个非线性激活函数。回想一下 Adaline 模型,我们计算净输入,然后将其传递给一个阈值函数。现在,我们不再使用恒等函数作为激活函数,而是使用 ReLU 函数 来让例子更有趣。
ReLU 函数的定义如下:
a = relu(z) = max(0, z)
其数学含义是:如果输入 z 大于 0,则输出 z;否则输出 0。
在计算图中,ReLU 函数在 z > 0 时斜率为 1,在 z <= 0 时斜率为 0。虽然在数学上 z = 0 这一点不可导,但在计算实践中,我们通常将其导数定义为:
relu'(z) = 1 if z > 0 else 0
这种调整在计算中是可行的,因为实践中我们很少会精确地遇到 z = 0 的情况。

构建一个计算图示例

现在,我们考虑一个具有三个输入的函数:一个特征 x、一个偏置项 b 和一个权重 w。其计算过程是:先计算净输入 z = w*x + b,然后将其输入 ReLU 激活函数。
我们可以用计算图清晰地表示这个过程。我们引入两个中间变量来简化图示:
u = w * xv = u + ba = relu(v)
对应的计算图如下所示:


在深度学习中,我们通常关心损失函数对权重或偏置的偏导数。我们可以使用链式法则,将这个复杂的导数分解为图上多个简单步骤的导数组合。例如,计算输出 a 对权重 w 的导数,可以分解为:
a对v的导数v对u的导数u对w的导数
然后利用链式法则将它们相乘:da/dw = (da/dv) * (dv/du) * (du/dw)。


通过具体数值理解反向传播


让我们用一个具体例子来实践。假设:b = 1, w = 2, x = 3。
- 前向传播:
u = w * x = 2 * 3 = 6v = u + b = 6 + 1 = 7a = relu(7) = 7
- 反向传播(计算梯度):
da/dv = relu'(7) = 1(因为输入大于0)dv/du = 1(加法的导数)dv/db = 1(加法的导数)du/dw = x = 3(乘法的导数)

现在,我们可以组合这些梯度:
- 输出
a对权重w的梯度:da/dw = (da/dv) * (dv/du) * (du/dw) = 1 * 1 * 3 = 3 - 输出
a对偏置b的梯度:da/db = (da/dv) * (dv/db) = 1 * 1 = 1


这就是我们利用计算图和链式法则计算梯度的方式。
更复杂的计算图结构

上面的例子是一个简单的单路径计算图。在实践中,我们会遇到更复杂的结构。

1. 带有损失函数的单路径图:
一个完整的模型通常包括损失计算。例如,在 Adaline 中,我们在激活函数输出 a 后,会计算预测输出 o 与真实标签 y 之间的均方误差损失 L。此时,计算损失 L 对权重 w 的梯度,路径会变得更长:dL/dw = (dL/do) * (do/da) * (da/dv) * (dv/du) * (du/dw)。


2. 权重共享的图:
在某些架构(如卷积神经网络)中,同一个权重参数可能被用在多个地方。在这种情况下,计算该权重的梯度时,需要考虑所有使用到它的路径,并使用多元链式法则将来自不同路径的梯度相加。


3. 多层感知机(MLP)的图:
在一个具有多个隐藏层的 MLP 中,计算底层某个权重的梯度会涉及更长的、可能分叉又合并的路径。例如,为了计算第一层权重 W1 的梯度,梯度信号需要从损失函数开始,反向流经第二层激活、第一层激活,最终到达 W1,并且可能通过该层神经元的多个后续连接进行汇聚。

手动为复杂网络推导和编码这些梯度计算非常繁琐且容易出错。这正是深度学习框架的价值所在:我们只需要定义前向计算(构建计算图),框架的自动微分引擎会自动为我们完成反向传播和梯度计算。
总结
本节课中,我们一起学习了计算图的核心概念。我们了解到:
- 计算图是表示计算过程的有向无环图,其中节点代表变量或操作,边代表数据流。
- 在计算图上,前向传播执行计算,反向传播利用链式法则计算梯度。
- 通过将复杂的导数分解为图上局部操作的导数,我们可以系统地计算任何参数的梯度。
- 实际神经网络的计算图可能非常复杂,包含损失计算、权重共享和多条路径,手动计算梯度不切实际。
- 现代深度学习框架(如 PyTorch)的自动微分功能,正是基于计算图这一抽象,自动为我们管理了梯度的计算,极大提高了开发效率。


在接下来的课程中,我们将看到如何在 PyTorch 中具体实现自动微分,并将其应用于像 Adaline 这样的实际模型中。
课程 P45:L6.3 - PyTorch 中的自动微分 🧠
在本节课中,我们将要学习 PyTorch 中自动微分的基本概念。我们将了解计算图是如何构建的,以及如何使用 autograd API 来计算梯度。这些知识是理解后续模型训练的基础。





上一节我们介绍了计算图的概念,本节中我们来看看如何在 PyTorch 中具体实现它。
首先,我们导入必要的库并查看环境版本。
import torch
import torch.nn.functional as F
接下来,我们定义一个简单的计算图。这个图与我们之前讨论的示例相同,包含输入 x、权重 w、偏置 b 和一个激活函数。
x = torch.tensor([1.0, 2.0])
w = torch.tensor([1.0, -1.0], requires_grad=True)
b = torch.tensor([0.5], requires_grad=True)
z = torch.dot(x, w) + b
a = F.relu(z)
请注意,我们为 w 和 b 设置了 requires_grad=True。这告诉 PyTorch 我们需要计算这些张量的梯度,因此它会为此构建并保存计算图。默认情况下,requires_grad 是 False,这是一个节省内存的好设计,因为并非所有操作都需要计算梯度。
当我们运行上述代码时,PyTorch 会在后台创建一个类似下图的内部计算图。

现在,让我们看看如何计算梯度。我们将使用 torch.autograd.grad 函数。
以下是计算 a 对 w 的梯度的方法:
grad_w = torch.autograd.grad(outputs=a, inputs=w, retain_graph=True)
print(grad_w) # 输出应为 tensor([3., 3.])
参数 retain_graph=True 表示在计算梯度后保留计算图。如果不设置,计算图会在调用后立即被销毁,这是为了在深度神经网络训练中防止内存无限增长。每次前向传播都会动态重建计算图。
接着,我们计算 a 对 b 的梯度:
grad_b = torch.autograd.grad(outputs=a, inputs=b)
print(grad_b) # 输出应为 tensor([1.])
这次我们没有保留计算图。如果尝试再次计算梯度,将会出错,因为图已被销毁。
PyTorch 的自动微分机制非常灵活,甚至可以处理包含条件语句的自定义函数。
例如,我们可以自己实现一个 ReLU 函数:
def my_relu(x):
return torch.where(x > 0, x, torch.tensor(0.0))
# 使用自定义函数
a_custom = my_relu(z)
grad_w_custom = torch.autograd.grad(outputs=a_custom, inputs=w, retain_graph=True)
即使函数在某个点(如 x=0)的数学导数未定义,PyTorch 也会采用合理的处理方式(例如将梯度设为 0),而不会导致程序崩溃。这体现了其自动微分的鲁棒性。

本节课中我们一起学习了 PyTorch 自动微分的核心机制。我们了解到:

- 通过设置
requires_grad=True来声明需要计算梯度的张量。 - PyTorch 会为此自动构建计算图。
- 使用
torch.autograd.grad函数可以计算指定输出对指定输入的梯度。 - 计算图默认在梯度计算后被销毁以节省内存,可通过
retain_graph=True保留。 - 自动微分系统能够灵活处理包括自定义函数在内的各种操作。

在下一节课中,我们将把这些概念应用于一个完整的例子:训练 Adaline 自适应线性神经元模型。

🧠 P46:L6.4 - 使用 PyTorch 训练 ADALINE



在本节课中,我们将学习如何使用 PyTorch 的自动微分功能来训练 ADALINE 模型。我们将从手动实现梯度计算开始,逐步过渡到使用 PyTorch 提供的更自动化的方法,最终展示 PyTorch 的标准训练流程。通过对比不同实现方式,你将理解自动微分的便利性及其在深度学习中的核心作用。











📝 概述与准备工作


首先,我们导入必要的库并准备数据。我们将使用鸢尾花数据集,因为它结构简单,便于我们专注于代码实现而非数据处理。








import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import grad



加载和准备数据的代码与之前课程中的完全一致。






# 假设 X_train, y_train, X_test, y_test 已按之前课程的方式加载并准备好
# X_train: 训练特征, y_train: 训练标签
# X_test: 测试特征, y_test: 测试标签









1️⃣ 手动实现 ADALINE



上一节我们回顾了 ADALINE 的基本原理。本节中,我们先来看看完全手动计算梯度的实现方式。





以下是 ADALINE 模型的手动实现,包括初始化、前向传播和手动计算梯度的反向传播。

class AdalineManual:
def __init__(self, num_features):
self.num_features = num_features
self.weights = torch.zeros(num_features, 1, dtype=torch.float32)
self.bias = torch.zeros(1, dtype=torch.float32)
def forward(self, x):
# 净输入: z = w^T x + b
net_input = torch.matmul(x, self.weights) + self.bias
# 激活函数(恒等函数)
activation = net_input
return activation
def backward(self, x, target, activation):
# 手动计算损失关于权重和偏置的梯度
error = target - activation
grad_w = -2.0 * torch.matmul(x.t(), error) / x.size(0)
grad_b = -2.0 * torch.mean(error)
return grad_w, grad_b
def predict(self, x):
# 阈值函数进行预测
activation = self.forward(x)
predictions = torch.where(activation >= 0.0, 1, -1)
return predictions





训练循环包括前向传播、手动梯度计算和参数更新。





def train_manual(model, X, y, epochs=20, lr=0.01, batch_size=32, seed=1):
torch.manual_seed(seed)
cost_history = []
for epoch in range(epochs):
# 打乱数据并创建小批量
indices = torch.randperm(X.size(0))
X_shuffled = X[indices]
y_shuffled = y[indices]
epoch_loss = 0.0
for i in range(0, X.size(0), batch_size):
batch_x = X_shuffled[i:i+batch_size]
batch_y = y_shuffled[i:i+batch_size].view(-1, 1)
# 前向传播
activations = model.forward(batch_x)
# 计算损失(MSE)
loss = torch.mean((batch_y - activations) ** 2)
epoch_loss += loss.item()
# 手动反向传播计算梯度
grad_w, grad_b = model.backward(batch_x, batch_y, activations)
# 更新参数: w = w - lr * grad_w
model.weights -= lr * grad_w
model.bias -= lr * grad_b
avg_loss = epoch_loss / (X.size(0) // batch_size)
cost_history.append(avg_loss)
print(f'Epoch: {epoch+1:03d} | Loss: {avg_loss:.4f}')
return cost_history



运行手动训练并评估。


# 初始化模型
model_manual = AdalineManual(num_features=X_train.size(1))
# 训练
costs_manual = train_manual(model_manual, X_train, y_train)
# 评估
train_pred = model_manual.predict(X_train)
test_pred = model_manual.predict(X_test)
train_acc = torch.mean((train_pred == y_train.view(-1,1)).float())
test_acc = torch.mean((test_pred == y_test.view(-1,1)).float())
print(f'Training Accuracy: {train_acc:.4f}')
print(f'Test Accuracy: {test_acc:.4f}')








2️⃣ 使用 torch.autograd.grad 实现半自动梯度计算





上一节我们手动推导并编写了梯度公式。本节中,我们来看看如何利用 PyTorch 的 grad 函数自动计算梯度,从而避免繁琐的手动推导。



以下是修改后的训练函数,关键区别在于使用 torch.autograd.grad 计算梯度。






def train_semi_auto(model, X, y, epochs=20, lr=0.01, batch_size=32, seed=1):
torch.manual_seed(seed)
cost_history = []
for epoch in range(epochs):
indices = torch.randperm(X.size(0))
X_shuffled = X[indices]
y_shuffled = y[indices]
epoch_loss = 0.0
for i in range(0, X.size(0), batch_size):
batch_x = X_shuffled[i:i+batch_size]
batch_y = y_shuffled[i:i+batch_size].view(-1, 1)
# 前向传播
activations = model.forward(batch_x)
loss = torch.mean((batch_y - activations) ** 2)
epoch_loss += loss.item()
# 使用 autograd.grad 自动计算梯度
grad_w = grad(loss, model.weights, retain_graph=True)[0]
grad_b = grad(loss, model.bias)[0] # 计算偏置梯度后无需保留计算图
# 更新参数(注意负号)
model.weights -= lr * grad_w
model.bias -= lr * grad_b
avg_loss = epoch_loss / (X.size(0) // batch_size)
cost_history.append(avg_loss)
# 使用 torch.no_grad() 上下文管理器避免在日志记录时构建计算图
with torch.no_grad():
print(f'Epoch: {epoch+1:03d} | Loss: {avg_loss:.4f}')
return cost_history

模型类 AdalineSemiAuto 与前一个 AdalineManual 完全相同,只是训练函数不同。运行此版本会得到与手动实现完全相同的损失和准确率,验证了自动微分的正确性。





3️⃣ 使用 PyTorch 标准 API 全自动训练


前面两种方法仍然需要我们显式地调用梯度计算和更新。本节中,我们来看看 PyTorch 最常用、最便捷的训练范式,它利用 nn.Module、损失函数和优化器将流程完全自动化。




以下是使用 PyTorch 高层 API 实现的 ADALINE。




class AdalineTorch(nn.Module):
def __init__(self, num_features):
super(AdalineTorch, self).__init__()
# 使用线性层自动管理权重和偏置
self.linear = nn.Linear(num_features, 1, bias=True)
# 为了与之前对比,将权重初始化为0(非标准做法,仅用于演示)
self.linear.weight.data.zero_()
self.linear.bias.data.zero_()
def forward(self, x):
# 线性层计算净输入
net_input = self.linear(x)
# 恒等激活函数
activation = net_input
return activation
def predict(self, x):
activation = self.forward(x)
predictions = torch.where(activation >= 0.0, 1, -1)
return predictions
以下是标准的 PyTorch 训练循环。




def train_torch(model, X, y, epochs=20, lr=0.01, batch_size=32, seed=1):
torch.manual_seed(seed)
# 定义损失函数和优化器
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
cost_history = []
for epoch in range(epochs):
indices = torch.randperm(X.size(0))
X_shuffled = X[indices]
y_shuffled = y[indices]
epoch_loss = 0.0
for i in range(0, X.size(0), batch_size):
batch_x = X_shuffled[i:i+batch_size]
batch_y = y_shuffled[i:i+batch_size].view(-1, 1)
# 1. 前向传播
outputs = model(batch_x)
# 2. 计算损失
loss = loss_fn(outputs, batch_y)
epoch_loss += loss.item()
# 3. 梯度清零(防止累积)
optimizer.zero_grad()
# 4. 反向传播(自动计算所有参数的梯度)
loss.backward()
# 5. 优化器更新参数
optimizer.step()
avg_loss = epoch_loss / (X.size(0) // batch_size)
cost_history.append(avg_loss)
print(f'Epoch: {epoch+1:03d} | Loss: {avg_loss:.4f}')
return cost_history





运行全自动训练。





# 初始化模型
model_torch = AdalineTorch(num_features=X_train.size(1))
# 训练
costs_torch = train_torch(model_torch, X_train, y_train)
# 评估
train_pred_torch = model_torch.predict(X_train)
test_pred_torch = model_torch.predict(X_test)
train_acc_torch = torch.mean((train_pred_torch == y_train.view(-1,1)).float())
test_acc_torch = torch.mean((test_pred_torch == y_test.view(-1,1)).float())
print(f'Training Accuracy: {train_acc_torch:.4f}')
print(f'Test Accuracy: {test_acc_torch:.4f}')




你会观察到,最终的训练和测试准确率与手动实现的结果完全一致。这证明了 PyTorch 自动微分系统的正确性,也展示了其 API 的高效与便捷。






关键点说明:
optimizer.zero_grad():在每次反向传播前清零梯度,防止梯度在不同批次间累积。loss.backward():自动计算损失相对于所有requires_grad=True的参数的梯度,并存储在对应张量的.grad属性中。optimizer.step():根据存储的梯度(model.parameters()提供的参数)和优化算法(如 SGD)更新参数。nn.Module.parameters():返回模型中所有可训练参数(如线性层的权重和偏置),方便传递给优化器。











📊 总结







本节课中我们一起学习了使用 PyTorch 训练 ADALINE 模型的三种渐进式方法:






- 手动计算梯度:我们完全手动实现了梯度公式,并更新参数。这种方法有助于深入理解原理,但对于复杂模型容易出错且繁琐。
- 使用
autograd.grad半自动计算:我们利用 PyTorch 的自动微分功能计算梯度,但仍需手动执行参数更新步骤。这是对手动和全自动方法之间的良好过渡。 - 使用 PyTorch 标准 API 全自动训练:我们采用了 PyTorch 的标准范式,利用
nn.Module定义模型,使用内置损失函数,并通过优化器自动完成梯度计算和参数更新。这是实践中最高效、最常用的方法。





通过对比,我们验证了三种方法在结果上的一致性,并深刻体会到 PyTorch 自动微分和优化器 API 带来的巨大便利。forward -> compute loss -> zero_grad -> backward -> step 这个循环是 PyTorch 模型训练的核心模式,适用于从简单线性模型到复杂深度神经网络的各种场景。

课程 P47:L6.5 - 深入了解 PyTorch API 🧠
在本节课中,我们将深入学习 PyTorch 的 API,特别是讨论面向对象 API 和函数式 API 之间的细微差别,这将在未来的课程中变得非常有用。
概述
我们将以一个多层感知机为例,介绍在 PyTorch 中定义和训练神经网络的标准流程。虽然我们尚未详细讨论其工作原理,但你可以将其视为一个多层神经网络。在 PyTorch 中构建网络时,我们通常遵循一个通用模板。

1. 定义模型类


首先,我们通过定义一个继承自 torch.nn.Module 的类来设置网络。这为我们自动提供了一些便利功能。
以下是定义模型类的步骤:


import torch.nn as nn


class MultiLayerPerceptron(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
# 定义需要更新的模型参数(层)
self.layer_1 = nn.Linear(input_size, hidden_size)
self.layer_2 = nn.Linear(hidden_size, hidden_size)
self.layer_out = nn.Linear(hidden_size, num_classes)
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_out(x)
return x

在 __init__ 方法中,我们定义模型的参数(例如,全连接层)。在 forward 方法中,我们定义这些参数的使用顺序和计算方式。这就是模型定义的基本框架。
2. 实例化模型与设置
定义好模型类后,下一步是实例化模型并进行相关设置。
以下是实例化模型与设置的步骤:
- 设置随机种子:为了确保代码的可复现性,通常在初始化模型前设置一个固定的随机种子。
- 初始化模型:创建模型类的实例。
- 指定设备:将模型移动到 GPU(如果可用)可以显著加速训练。只需一行代码即可实现。
- 设置优化器:优化器(如随机梯度下降)负责在训练过程中更新模型参数。我们需要告诉优化器哪些参数需要更新。

import torch

# 设置随机种子以确保可复现性
torch.manual_seed(123)
# 实例化模型
model = MultiLayerPerceptron(input_size=784, hidden_size=128, num_classes=10)
# 指定运行设备(CPU 或 GPU)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)

# 设置优化器,传入需要更新的模型参数
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

3. 训练循环
模型和优化器设置完成后,我们进入训练循环。这一步与之前介绍 Adaline 算法时的概念非常相似。
以下是训练循环的核心步骤:

- 设置模型为训练模式:使用
model.train()。这是一个好习惯,因为某些层(如批归一化、Dropout)在训练和评估阶段的行为不同。 - 遍历数据批次:在每个训练周期(epoch)中,遍历训练数据的小批次(mini-batch)。
- 前向传播:将数据输入模型,计算预测输出。
- 计算损失:使用损失函数(如交叉熵损失)比较预测输出与真实标签。
- 梯度清零:在反向传播前,将优化器中累积的梯度清零。
- 反向传播:计算损失相对于模型参数的梯度。
- 参数更新:优化器根据计算出的梯度更新模型参数。
# 假设我们有一些训练数据 `features` 和标签 `labels`
num_epochs = 10
criterion = nn.CrossEntropyLoss() # 损失函数
for epoch in range(num_epochs):
model.train() # 设置为训练模式
# 这里假设我们按批次遍历数据
for batch_features, batch_labels in train_loader:
# 将数据移动到与模型相同的设备
batch_features = batch_features.to(device)
batch_labels = batch_labels.to(device)
# 前向传播
outputs = model(batch_features)
# 计算损失
loss = criterion(outputs, batch_labels)
# 反向传播与优化
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新参数
理解这个流程至关重要,但在实际项目中,你通常不需要从头开始编写所有代码,而是基于现有模板进行修改。

4. 面向对象 API 与函数式 API


PyTorch 提供了两种主要的 API 风格:面向对象 API 和函数式 API,你可以混合使用它们。
函数式 API 指的是没有内部状态的函数。例如,ReLU 激活函数只是一个简单的数学运算(max(0, x)),它本身没有需要学习的参数。因此,使用函数式 API 更为合适。

面向对象 API 通常用于包含需要学习参数的组件(如线性层)。然而,PyTorch 也提供了 nn.Sequential 容器,它允许我们以更简洁的面向对象方式定义网络。


以下是两种风格的对比示例:


# 方式一:混合使用(在 __init__ 中定义参数,在 forward 中调用函数式 API)
class Net1(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(10, 5)
self.fc2 = nn.Linear(5, 2)
def forward(self, x):
x = torch.relu(self.fc1(x)) # 使用 torch.relu (函数式)
x = self.fc2(x)
return x
# 方式二:使用 nn.Sequential (面向对象,更紧凑)
class Net2(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(10, 5),
nn.ReLU(), # 使用 nn.ReLU() 模块
nn.Linear(5, 2)
)
def forward(self, x):
return self.net(x)
nn.Sequential 的优点是定义顺序清晰,不易出错,特别适合层数很多的网络。缺点是如果需要获取中间层的输出进行调试,不如第一种方式直接插入 print 语句方便,而需要用到“钩子”(hook)机制。你可以根据项目需求和个人偏好选择使用哪种风格。
5. 开发风格建议
最后,分享一些 PyTorch 开发的最佳实践:

- Jupyter Notebook 与 Python 脚本:建议在探索性分析和初步编码时使用 Jupyter Notebook,因为它便于展示中间结果和图表。对于更复杂、需要长时间运行或部署的项目,建议使用 Python 脚本,因为它们更稳健,且与版本控制工具(如 Git)和集成开发环境(如 VS Code, PyCharm)的调试功能配合得更好。
- 命名规范:遵循常见的 Python 命名约定有助于保持代码清晰。
- 包、模块、函数、变量:使用小写字母和下划线(
snake_case)。 - 类:使用大写字母开头的单词(
CamelCase)。 - 常量:使用全大写字母和下划线(
UPPER_CASE)。
- 包、模块、函数、变量:使用小写字母和下划线(
随着课程的深入,我们将在需要时逐步介绍 PyTorch 的更多特性,例如在处理卷积神经网络时。

总结

本节课我们一起学习了 PyTorch 的核心 API 使用流程。我们介绍了如何通过继承 nn.Module 来定义模型,在 __init__ 中初始化参数,在 forward 中定义前向传播。我们了解了实例化模型、设置设备与优化器、以及编写训练循环的步骤。我们还探讨了面向对象 API 与函数式 API(包括 nn.Sequential)的区别与选择。最后,我们简要讨论了 Jupyter Notebook 与 Python 脚本的使用场景以及代码命名规范。这个通用的 PyTorch 框架将是我们后续构建更复杂神经网络的基础。
🚀 课程 P48:L7.0 - GPU 资源与 Google Colab 使用教程
在本节课中,我们将学习如何利用免费的云端 GPU 资源来加速深度学习模型的训练,特别是通过 Google Colab 平台。这对于完成课后作业和课程项目非常有帮助。
📖 概述
我们将首先介绍一些可用的云端计算资源,然后重点演示如何在 Google Colab 中设置和使用 GPU,包括如何安装包、加载数据以及将模型和数据转移到 GPU 上进行训练。
🌐 可用的云端 GPU 资源
上一节我们介绍了课程目标,本节中我们来看看有哪些可用的免费云端 GPU 资源。

我发现了一个名为“Deep learning in the cloud”的优秀 GitHub 仓库。该仓库的贡献者和维护者整理了一份非常棒的列表,列出了所有可用于在云端训练深度神经网络的不同工具。它们中的大多数都支持 GPU,这正是其核心价值所在。
以下是该列表中一些最受欢迎且免费的服务选项:
- Google Colaboratory:提供免费的 GPU 和 TPU 资源,运行时间有限制。
- Kaggle Kernels:同样提供免费的 GPU 资源,适合数据科学竞赛和项目。
- 其他选项:列表中还包括一些提供初始信用额度(例如 300 美元)或固定运行时间(例如 3 小时)的服务。
对于在校学生,还有一些额外的特别优惠,值得关注。

🛠️ 使用 Google Colab


上一节我们了解了可用的资源,本节中我们来看看如何具体使用 Google Colab。
为了简化操作,我将引导你完成使用 Google Colab 进行项目的全过程。
首先,点击链接打开一个新的 Colab 笔记本环境。这是一个基于笔记本的环境,易于使用,并且可以直接在界面中查看代码运行结果。不过,当代码变得复杂时,拥有 Python 脚本的云端环境可能更合适。你可以探索列表并选择最适合你课程项目的环境,没有强制要求。

启用 GPU

一个重要的步骤是启用 GPU 支持。为此,你需要转到“运行时”菜单,选择“更改运行时类型”,然后在“硬件加速器”下拉菜单中选择“GPU”。你也可以选择 TPU,但对于本课程,我推荐使用 GPU。

选择完成后,我们可以检查 PyTorch 是否可用,并确认 GPU 已被识别。

import torch
print(torch.__version__)
print(torch.cuda.is_available())


如果 torch.cuda.is_available() 返回 False,请确保你已在运行时设置中正确选择了 GPU。

打开和运行笔记本
我们可以在 Colab 中直接打开 GitHub 上的笔记本。例如,你可以导航到课程仓库,找到相应的笔记本文件(如关于自动求导和 PyTorch 的笔记本),然后使用“在 Colab 中打开”功能。
打开笔记本后,再次确保运行时类型设置为 GPU,然后就可以运行代码了。
安装缺失的包
Colab 预装了许多 Python 包,但并非全部。如果遇到“ModuleNotFoundError”(例如找不到 watermark 包),我们需要手动安装。
我们可以使用感叹号 ! 来执行终端命令:

!pip install watermark
安装成功后,相应的代码单元格就可以正常运行了。
加载数据
直接从 GitHub 打开笔记本时,笔记本可能无法访问仓库中的数据集文件。有几种方法可以解决这个问题。
方法一:直接使用原始数据链接
如果数据集是托管在网上的单个文件(如 CSV),你可以直接使用其 URL 进行加载。
方法二:挂载 Google 云端硬盘(推荐)
更通用的方法是挂载你的 Google Drive,这样你就可以访问存储在 Drive 上的任何数据文件。
以下是挂载 Google Drive 的代码:

from google.colab import drive
drive.mount('/content/drive')
执行这段代码后,系统会提示你授权访问。按照链接完成授权,并将返回的验证码粘贴到输入框中即可。
挂载后,你的 Drive 内容会出现在 /content/drive/MyDrive/ 目录下。假设你的数据集在 Drive 的 Colab Notebooks/datasets/ 文件夹中,加载路径应类似:
file_path = '/content/drive/MyDrive/Colab Notebooks/datasets/iris.csv'
方法三:复制数据到本地环境(针对大型数据集)
为了获得更快的 I/O 速度,特别是处理大型数据集(如图像集)时,建议将数据从 Drive 复制到 Colab 虚拟机本身的本地存储中。
你可以使用以下命令复制整个文件夹或压缩包:
# 复制文件夹
!cp -r /content/drive/MyDrive/Colab\ Notebooks/datasets /content/
# 或者,更高效的方式是处理压缩包
!cp /content/drive/MyDrive/Colab\ Notebooks/datasets.zip /content/
!unzip /content/datasets.zip -d /content/datasets
这样,数据就位于 Colab 实例的本地路径(如 /content/datasets/),数据加载器的读取速度会更快。
在 GPU 上运行模型
最后,要让模型在 GPU 上训练,需要将模型和张量数据都转移到 GPU 设备上。
首先,定义设备:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
然后,将模型转移到该设备:
model = YourModel()
model.to(device)
同样,在训练时,也需要将每个批次的输入数据和标签转移到 GPU:
inputs, labels = inputs.to(device), labels.to(device)
在后续的代码示例中,我会设置一个全局标志来自动处理设备转移,你只需要在脚本开头指定一次即可。
📝 总结

本节课中我们一起学习了如何寻找并使用免费的云端 GPU 资源,重点掌握了 Google Colab 的基本工作流程:如何启用 GPU、安装包、通过挂载 Google Drive 或复制到本地来管理数据集,以及如何将 PyTorch 模型和数据转移到 GPU 上进行加速训练。希望本教程对你有所帮助。
📰 深度学习新闻 #4,2021年2月20日
在本节课中,我们将回顾2021年2月20日这一周内深度学习领域的重要新闻。内容涵盖新的PyTorch工具、大规模数据集、模型训练成本、环境友好型AI、伦理问题以及实用的模型调试工具。
🧠 自由连接神经网络:Free-Wired
上一节我们介绍了本周的概述,本节中我们来看看一个有趣的PyTorch扩展库。
Free-Wired是一个用于创建可在CUDA GPU上运行的、经过优化的自由连接神经网络的PyTorch扩展库。在传统的全连接层中,一层的每个单元都与下一层的所有单元相连。而自由连接网络允许我们任意地连接这些节点。
例如,考虑一个输入单元,它可能只连接到下一层的一个特定单元,而不是所有单元。这种连接方式比卷积网络中的残差连接或Transformer中的连接更为任意。这个工具为尝试各种非常规的神经网络架构提供了可能。
核心概念:自由连接网络允许任意定义层与层之间的连接,而非标准的全连接模式。
潜在应用场景
以下是自由连接网络的一个潜在应用方向。
- 随机连接网络:一篇名为《Exploring Randomly Wired Neural Networks for Image Recognition》的论文提出,使用随机连接的网络在ImageNet数据集上的表现(77.1%准确率)甚至优于精心设计的ResNet架构。虽然这类方法因实现复杂和可解释性差而未成为主流,但Free-Wired这样的工具可能降低其探索门槛。
🖼️ 新计算机视觉数据集:Things
从新的网络架构转向新的数据,本周研究人员发布了一个名为“Things”的新数据集。
该数据集包含来自565个类别的150万张图像。其新颖之处在于,它专注于对人类重要的物体类别。研究人员提供了在该数据集上预训练的卷积网络代码以及数据加载示例。
数据集特点
以下是该数据集的一些关键统计信息和特点。
- 类别构成:60%的标签属于人造物(如汽车),40%属于自然物(如树木)。
- 数据不平衡:每个类别的图像数量不同,这给评估带来了挑战,可能需要使用平衡准确率而非常规准确率。
- 图像多样性:图像具有不同的长宽比和分辨率,在使用有固定输入尺寸要求的架构时需要注意。
数据收集方法
该数据集的收集方法也颇具启发性。
- 标签筛选:研究人员从大型文本语料库中筛选出高频且具体的名词。他们使用了“具体性”评分,即人类评估者对一个名词所指代事物的可视化难易程度进行打分(例如,“草莓”得分高,“希望”得分低)。
- 图像获取:基于筛选出的类别标签,从ImageNet、Flickr和Bing下载对应图像。
- 数据清洗:
- 使用主成分分析(PCA) 和因子载荷的相关性来去除重复图像。
- 对每个类别随机采样100张图像进行人工检查,剔除错误标签超过4%的类别。
- 最终整合:合并来自不同来源的图像,再次去重,并修剪数据集使每个类别的图像数大致在7到5000张之间。最终数据集中,94%的图像来自ImageNet。
💸 模型训练的成本与环境影响
从大规模数据集,我们自然过渡到训练大规模模型所面临的巨大成本问题。
当前,训练大型AI模型的成本正变得极其高昂。例如,训练一个拥有2000亿参数的模型,可能需要价值约7500万美元的硬件系统(如280台DGX A100)。若在AWS上租用同等算力三年,成本也可能高达约8000万美元(不含电费)。

并行计算技术
为了应对巨大的计算需求,主要有两种并行化技术:
- 数据并行:将大批量数据拆分到多个GPU上,每个GPU计算一部分梯度,然后汇总平均以更新模型。但随着GPU数量增加,其收益会递减。
- 张量并行:将矩阵乘法等运算高效地分布到多个GPU上。这对GPU间的连接速度有很高要求,实现起来更为复杂。
联邦学习与能效
高昂的成本也引发了对其环境影响的关注。一项研究表明,联邦学习可能有助于减少碳排放。
联邦学习意味着在多个设备(如手机、不同数据中心)上进行计算,然后汇总结果。其潜在优势在于,分散的设备可能不需要像大型数据中心那样耗费巨资进行集中冷却。然而,联邦学习也可能因训练时间延长、数据传输能耗以及设备本身能效差异而变得低效。
专用AI芯片
另一个趋势是开发更高效的专用AI芯片。例如,一家名为“NeuReality”的初创公司声称其芯片在每美元性能上比竞争对手高出15倍。这虽然不意味着绝对速度更快,但强调了在成本和能效方面的优化。

⚖️ AI伦理:有问题的应用案例

在关注技术效率的同时,我们也必须审视其应用伦理。本周有一个关于AI在招聘中应用的争议性案例。
研究人员开发了一个AI系统,用于在视频面试中评估求职者。该系统在超过12000人的视频数据上训练,并由2500人对视频中人物的开放性、尽责性、外向性等性格维度进行评分,AI学习这些评分。

然而,测试发现了严重问题:
- 佩戴眼镜:同一位演员在佩戴眼镜时,AI对其多项性格特质的评分显著降低。
- 背景书架:同一位演员在有书架背景和无书架背景时,AI给出的评分存在巨大差异。
这些因素与求职者的能力毫无关系,此类偏差会导致非常不公平的结果。这凸显了AI系统,特别是基于深度学习的“黑箱”模型,在敏感领域应用时可能存在的风险。

问题根源与思考
为什么会出现这种问题?一个核心评论指出:“人脸识别机器学习的根本问题在于,我们永远无法确切知道机器对图像中的哪种模式做出了反应。”

虽然可以通过数据增强(如随机更换虚拟背景)来缓解部分问题,但开发此类系统时必须进行严格的偏差测试,确保公平性。在某些领域,或许应谨慎考虑是否要使用AI。

🔧 模型调试工具:Cockpit
最后,从发现问题的角度,我们转向一个帮助理解和调试模型训练过程的工具。
Cockpit是一个针对PyTorch的实用调试工具,旨在帮助工程师更深入地了解深度学习模型的内部工作状态,而不仅仅是“盲目飞行”。
学习率调试示例
该工具的一个应用是辅助学习率调优。通常,学习率需要手动尝试调整。
- 学习率过小(橙色曲线):参数更新幅度很小,训练可能停滞。
- 学习率过大(蓝色曲线):参数更新幅度过大,可能在最优点附近震荡甚至发散。
Cockpit提供了如“归一化步长”等指标进行可视化。理论上,最优的更新方向应直接指向损失最低点(对应步长为0)。有趣的是,实验发现,在测试集上泛化性能最好的模型,其平均归一化步长略大于0。这为学习率调优提供了一个潜在的参考区间。
📝 总结

本节课中我们一起学习了2021年2月20日当周的深度学习动态。我们介绍了一个用于构建自由连接神经网络的PyTorch工具,探讨了一个以人类为中心的新图像数据集,分析了训练大模型的高昂成本与环境影响,审视了一个AI在招聘中产生伦理偏差的案例,最后了解了一个用于调试模型训练过程的实用工具。这些内容涵盖了技术前沿、资源挑战、伦理责任和开发实践,展现了深度学习领域广阔而多维的发展图景。

机器学习基础课程 P5:L1.3.1 - 机器学习的三大类别(第1部分):监督学习 🧠
在本节课中,我们将学习机器学习的三大主要类别,并重点介绍其中最重要的一类:监督学习。我们将了解监督学习的基本概念、常见类型及其应用场景。
机器学习的三大类别
机器学习主要分为三大类别:监督学习、无监督学习和强化学习。
上一节我们介绍了机器学习的整体框架,本节中我们来看看这三大类别的具体含义。
什么是监督学习?
监督学习是机器学习或深度学习中最常见的形式。它是机器学习中最大的子类别,其核心特点是使用带有标签的数据。

如果你回想垃圾邮件分类的例子,那就是一个监督学习问题。我们的目标是预测一封电子邮件是否是垃圾邮件,而标签数据则是垃圾邮件和非垃圾邮件的示例。监督学习还涉及直接反馈,我们可以判断预测是否正确。例如,在训练过程中,模型对一封已知标签的邮件进行预测,我们可以根据已知的正确标签提供直接反馈。
以下是监督学习的关键特点:
- 使用标签数据:每个训练样本都有对应的正确答案(标签)。
- 提供直接反馈:模型可以根据预测结果与真实标签的差异进行学习和调整。
- 目标明确:旨在学习从输入到输出的映射关系。

监督学习的类型
监督学习主要包含几种类型:回归、分类以及序数回归。

1. 回归 📈
回归是监督学习的一种。你可能已经从其他统计学课程中见过回归的例子,例如线性回归模型。
在机器学习中,我们通常将自变量 x 称为特征,将因变量 y 称为目标。模型的目标是拟合一个函数(例如线性函数 y = wx + b),以便当我们有一个新的观测值(只知道其特征 x)时,可以预测其目标值 y。
公式示例(简单线性回归):
y_pred = w * x + b
其中,w 是权重,b 是偏置项。
2. 分类 🏷️
分类是比回归更常见的监督学习任务,尤其在深度学习领域。大多数系统都专注于分类,例如识别图像中的物体(是猫还是狗)。
回想之前展示的ATM机识别手写数字的例子,那就是一个分类任务,旨在将数字分类为0、1、2等。

以下是一个二分类问题的简单示例,我们只有两个可能的标签(例如“+”和“-”),以及两个特征(x1 和 x2)。目标是学习一个决策边界,以便对新的数据点进行分类。例如,决策边界左侧的点被预测为“-”,右侧的点被预测为“+”。
在实践中,我们通常处理高维数据,决策边界也可能非常复杂。深度神经网络正是通过学习这些复杂的决策边界来工作的。
3. 序数回归与排序 🔢

除了回归和分类,还存在第三种类型,称为序数回归或序数分类。这与分类相似,但标签之间存在顺序关系。
例如,电影评级(优秀 > 良好 > 一般 > 较差 > 很差)就具有顺序信息。虽然你可以使用分类器将其分类到这些类别中,但序数回归模型会额外利用类别之间的排序信息。

另一个紧密相关的任务是排序,它更侧重于预测正确的顺序,而不仅仅是单个标签。
一个具体的例子是年龄预测。年龄具有顺序性,但不同年龄段的“距离”意义可能不同(例如,6岁到10岁的变化远大于70岁到74岁的变化)。因此,使用标准的度量回归(假设所有间隔相等)可能不合适,而序数回归是更自然的选择。
不过,在本课程中,我们将主要关注监督学习中的分类任务。
总结
本节课我们一起学习了机器学习的三大类别,并深入探讨了其中最重要的一类——监督学习。
我们了解到:
- 监督学习使用带有标签的数据进行训练,并提供直接反馈。
- 监督学习主要包括回归(预测连续值)、分类(预测离散类别)和序数回归(预测有序类别)等类型。
- 在后续课程中,我们将更深入地学习分类等具体任务。


在下一个视频中,我们将继续介绍另外两个类别:无监督学习和强化学习。
课程 P50:L8.0 - 逻辑回归 🧠
概述
在本节课中,我们将学习逻辑回归,并将其视为一个单层神经网络。这有助于理解神经网络中前向传播与反向传播的基本概念。我们还将把逻辑回归从二分类问题推广到多分类问题,为后续理解多层感知机打下基础。
1. 逻辑回归作为人工神经元
上一节我们介绍了感知机和自适应线性神经元(Adaline)。本节中,我们来看看逻辑回归如何作为一个人工神经元。
逻辑回归可以看作是Adaline的改进版本。它使用不同的激活函数和损失函数,以更好地处理分类问题。
以下是逻辑回归神经元的基本结构:
# 逻辑回归神经元的前向传播
z = w1*x1 + w2*x2 + ... + wn*xn + b
a = sigmoid(z)
其中,sigmoid 函数将线性输出 z 映射到 (0, 1) 区间,可以解释为概率。
2. 逻辑回归的损失函数
上一节我们了解了模型结构,本节中我们来看看逻辑回归使用的损失函数。
逻辑回归使用负对数似然作为损失函数,这比Adaline中使用的均方误差更适合分类任务。
对于一个样本,其损失公式如下:
L = -[y * log(a) + (1-y) * log(1-a)]
其中:
y是真实标签(0或1)。a是模型预测为正类的概率(即sigmoid(z)的输出)。
模型的目标是最小化所有训练样本的平均损失。
3. 使用梯度下降训练逻辑回归
定义了损失函数后,我们需要一种方法来优化模型参数。本节将介绍如何使用梯度下降法训练逻辑回归模型。
梯度下降通过计算损失函数相对于每个参数(权重 w 和偏置 b)的梯度,并沿梯度反方向更新参数,以最小化损失。
以下是参数更新的核心公式:
w := w - learning_rate * dL/dw
b := b - learning_rate * dL/db
其中 dL/dw 和 dL/db 需要通过反向传播计算得出。

4. 逻辑与交叉熵:术语解析
在深入多分类之前,我们先厘清两个在深度学习中常见的术语:逻辑值(Logits)和交叉熵(Cross-Entropy)。
- 逻辑值(Logits):指的是神经元在激活函数之前的原始线性输出
z。 - 交叉熵(Cross-Entropy):二分类逻辑回归中使用的负对数似然损失,就是二元交叉熵损失的一种特例。
理解这些术语有助于阅读更广泛的深度学习文献。
5. 逻辑回归代码示例
理论需要实践来巩固。以下是使用逻辑回归进行二分类的一个简单代码框架:
import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def compute_loss(y_true, y_pred):
# 二元交叉熵损失
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
# 训练循环(简化版)
for epoch in range(num_epochs):
# 前向传播
z = np.dot(X, w) + b
a = sigmoid(z)
# 计算损失
loss = compute_loss(y, a)
# 反向传播计算梯度 (此处省略梯度计算细节)
# dw = ...
# db = ...
# 更新参数
w -= learning_rate * dw
b -= learning_rate * db
6. 从二分类到多分类:模型扩展

现实世界的数据往往包含多于两个类别。本节开始,我们将逻辑回归扩展为多项逻辑回归,也称为 Softmax 回归。
Softmax回归模型具有多个输出神经元,每个神经元对应一个类别。
7. 独热编码:类别数据表示
为了处理多分类标签,我们需要一种数值化的表示方法。这里我们引入独热编码。
独热编码将类别标签转换为一个向量,该向量长度等于类别总数,其中真实类别对应的位置为1,其余位置为0。
例如,三个类别(猫、狗、鸟)的标签“狗”可以编码为 [0, 1, 0]。
8. 多类别交叉熵损失
对于多分类问题,我们需要将二元交叉熵推广为多类别交叉熵损失。
其公式为:
L = - Σ (y_i * log(a_i))
其中求和遍历所有类别。y_i 是独热编码标签的第 i 个元素,a_i 是模型预测为第 i 个类别的概率(由Softmax函数产生)。
9. Softmax回归的学习规则
与二分类逻辑回归类似,我们使用梯度下降来训练Softmax回归模型。核心思想不变:计算损失对参数的梯度并更新参数。
参数更新公式在形式上与之前相同,但梯度 dL/dw 和 dL/db 的计算会因Softmax函数和多类别交叉熵而有所不同。
10. Softmax回归代码示例
最后,我们来看一个Softmax回归的简化代码示例,以整合所有概念:
def softmax(z):
# z 是一个向量
exp_z = np.exp(z - np.max(z)) # 防止数值溢出
return exp_z / np.sum(exp_z, axis=0)
def compute_loss_multi(y_true_one_hot, y_pred_probs):
# 多类别交叉熵损失
return -np.mean(np.sum(y_true_one_hot * np.log(y_pred_probs), axis=1))
# 假设有3个类别
num_classes = 3
# 权重矩阵形状: (特征数, 类别数)
W = np.random.randn(num_features, num_classes)
b = np.zeros(num_classes)
# 训练循环中...
# 前向传播
z = np.dot(X, W) + b # X: (样本数, 特征数)
a = softmax(z.T).T # a: (样本数, 类别数),每个样本的类别概率分布
# 计算损失 (y需要是独热编码形式)
loss = compute_loss_multi(y_one_hot, a)
# 反向传播与参数更新(略)
总结
本节课中我们一起学习了:
- 将逻辑回归视为一个单层神经网络。
- 逻辑回归使用的负对数似然(二元交叉熵)损失函数及其优化方法。
- 逻辑值与交叉熵等核心术语。
- 如何将二分类逻辑回归扩展为多分类的Softmax回归。
- 处理多分类标签的独热编码方法。
- 多类别交叉熵损失函数。
- Softmax回归模型的基本训练过程。

通过本课的学习,你掌握了分类任务的基础模型。在下一讲中,我们只需在Softmax回归模型中加入一个隐藏层,就能得到多层感知机,即第一个简单的深度神经网络。

课程 P51:L8.1 - 逻辑回归作为单层神经网络 🧠
在本节课中,我们将学习逻辑回归,并将其视为一种单层神经网络。我们将探讨其核心组成部分,特别是与之前学习的Adaline模型的主要区别。
逻辑回归与Adaline的区别 🔄
上一节我们介绍了Adaline模型。逻辑回归与Adaline的主要区别在于激活函数和损失函数。
以下是逻辑回归与Adaline的核心区别:
- 激活函数不同:Adaline使用恒等函数(Identity Function),而逻辑回归使用逻辑S型函数(Logistic Sigmoid Function)。
- 损失函数不同:Adaline使用均方误差(Mean Squared Error),而逻辑回归使用不同的损失函数(将在下一节介绍)。

在Adaline中,激活函数是恒等函数,即 a = z。在逻辑回归中,我们使用非线性激活函数。
逻辑S型激活函数 📈
本节中我们来看看逻辑回归使用的激活函数——逻辑S型函数。
该函数有两种常见写法,在深度学习领域通常使用右侧形式:

σ(z) = 1 / (1 + e^{-z})σ(z) = e^{z} / (e^{z} + 1)
从左侧形式推导到右侧形式,只需分子分母同时除以 e^{z}。
该函数的图像以0为中心。当输入 z = 0 时,输出为 0.5。函数值域在 (0, 1) 之间,当 z 趋近正无穷时,输出趋近于 1;当 z 趋近负无穷时,输出趋近于 0。
逻辑回归模型与概率解释 🎯
逻辑回归的模型可以总结为以下公式:
h(x) = σ(z) = 1 / (1 + e^{-z})
其中 z = w^T x(即净输入)。
我们可以将整个模型 h(x) 的输出解释为给定特征向量 x 时,类别标签 y 的后验概率。即:
P(y=1 | x) = h(x)
P(y=0 | x) = 1 - h(x)
以一个简化的鸢尾花二分类问题为例(假设类别1为山鸢尾,类别0为杂色鸢尾)。如果模型对某个样本预测 P(y=1 | x) = 0.8,这意味着模型认为该样本有80%的概率属于山鸢尾,同时有20%的概率属于杂色鸢尾。
模型训练的目标是:对于给定的真实标签,我们希望其对应的预测概率尽可能接近1。
- 如果真实标签
y = 1,我们希望P(y=1 | x)接近1。 - 如果真实标签
y = 0,我们希望P(y=0 | x)(即1 - h(x))接近1。
下一节我们将学习如何通过一个合适的损失函数来实现这个目标。
总结 📝

本节课中我们一起学习了逻辑回归作为单层神经网络的基本框架。我们明确了它与Adaline模型在激活函数上的核心区别,引入了逻辑S型函数,并理解了该模型输出的概率意义。下一节,我们将深入探讨逻辑回归的损失函数。

逻辑回归课程 P52:L8.2 - 逻辑回归损失函数 📉
在本节课中,我们将学习如何计算逻辑回归模型的损失函数。我们将从回顾核心概念开始,逐步推导出用于模型训练的损失函数形式,并解释为何在实践中最常使用负对数似然损失。
核心概念回顾 🔄

上一节我们介绍了逻辑回归模型如何计算类别成员概率。本节中,我们来看看如何基于这些概率构建损失函数。
逻辑回归的目标是最大化训练数据中正确类别的预测概率。对于单个样本,我们希望:
- 如果真实标签
y = 0,则类别成员概率P(y=0|x)应尽可能高(接近1)。 - 如果真实标签
y = 1,则类别成员概率P(y=1|x)应尽可能高(接近1)。

这些概率通过逻辑激活函数(Sigmoid)计算得出:
P(y=1|x) = σ(z),其中z是净输入。P(y=0|x) = 1 - σ(z)。


我们可以用一个紧凑的公式来统一表示上述分段函数:
P(y|x) = (σ(z))^y * (1 - σ(z))^(1-y)
其中 y 是真实标签(0或1)。通过代入验证可知,当 y=1 时,公式简化为 σ(z);当 y=0 时,公式简化为 1-σ(z)。



从最大似然估计到损失函数 🎯

对于一个包含 n 个样本的训练集,我们希望找到一组模型参数,使得所有样本的联合概率(似然)最大。这被称为最大似然估计。
我们希望最大化的似然函数为:
L(θ) = ∏_{i=1}^{n} P(y^{(i)} | x^{(i)})
为了计算方便和数值稳定性,我们通常取自然对数,将对数似然函数:
log L(θ) = ∑_{i=1}^{n} [ y^{(i)} log(σ(z^{(i)})) + (1-y^{(i)}) log(1-σ(z^{(i)})) ]

然而,大多数优化库(如PyTorch中的随机梯度下降)被设计为最小化一个函数,而非最大化。因此,我们通过添加一个负号,将对数似然最大化问题转化为最小化问题,得到负对数似然损失函数。
此外,我们通常会除以样本数量 n(或批次大小)进行缩放,这使得训练过程在数值上更稳定。最终,逻辑回归常用的损失函数形式为:
J(θ) = - (1/n) ∑_{i=1}^{n} [ y^{(i)} log(a^{(i)}) + (1-y^{(i)}) log(1-a^{(i)}) ]
其中 a^{(i)} = σ(z^{(i)}) 是模型对第 i 个样本的预测输出(激活值)。
以下是逻辑回归训练与之前所学模型的对比要点:

- 与Adaline的对比:在Adaline中,我们最小化均方误差。在逻辑回归中,我们本质上是最大化似然(或等价地最小化负对数似然)。
- 优化技巧:为了利用现有的随机梯度下降优化器,我们将最大化问题转化为最小化问题。
- 数值稳定性:使用对数似然而非原始似然,并添加缩放因子
1/n,都是为了提高计算的数值稳定性。
总结 📝

本节课中我们一起学习了逻辑回归损失函数的构建过程。我们从最大化正确类别的概率这一目标出发,引入了最大似然估计的概念。为了便于优化和计算,我们将其转化为最小化负对数似然函数的问题,这也就是逻辑回归模型最终使用的损失函数。在下一节中,我们将进一步探讨这个损失函数的具体形态和训练过程。
机器学习课程 P53:L8.3 - Logistic 回归损失导数和训练 🧮

在本节课中,我们将学习如何训练一个逻辑回归模型。具体来说,我们将计算损失函数相对于逻辑回归模型中权重和偏置参数的梯度,以便后续使用梯度下降法来更新模型参数。在后续的视频中,我们将看到如何在代码中实现这一过程,完成实际的模型训练。
逻辑 Sigmoid 函数及其导数 📈
上一节我们介绍了逻辑回归模型的基本概念,本节中我们来看看其核心激活函数——逻辑 Sigmoid 函数及其导数。
逻辑 Sigmoid 函数的形状如下图所示,其输入(净输入 z)在 x 轴,输出(激活值 a)在 y 轴,呈现 S 形曲线。观察函数图像,我们可以直观地理解其导数的特性:在函数两端相对平坦的区域,梯度接近于 0;而在中间最陡峭的部分,梯度达到最大。


逻辑 Sigmoid 函数 σ(z) 的公式为:
σ(z) = 1 / (1 + e^{-z})
其导数 σ'(z) 可以通过微积分规则计算得出,结果非常简洁:
σ'(z) = σ(z) * (1 - σ(z))
这个导数函数的特点是:当输入 z 为 0 时,导数值最大,为 0.25;当输入 z 的绝对值很大(正或负)时,导数值趋近于 0。这种特性在构建多层神经网络时可能会带来梯度消失的问题,但这将在后续课程中讨论。
负对数似然损失函数 📉
理解了激活函数的导数后,我们接下来分析逻辑回归使用的损失函数——负对数似然损失。
该损失函数 L 的公式如下,它根据真实标签 y 的不同取值(0 或 1)分为两部分:
L = -[y * log(a) + (1 - y) * log(1 - a)]
其中,a 是模型预测的类别 1 的概率(即 σ(z))。
以下是损失函数在不同情况下的表现:
- 当真实标签
y = 1时,损失函数简化为L = -log(a)。我们希望模型预测的概率a越高(接近 1),损失L就越低(接近 0)。如果模型预测的概率a很低(接近 0),损失L会急剧增大至无穷大。 - 当真实标签
y = 0时,损失函数简化为L = -log(1 - a)。我们希望模型预测的概率a越低(接近 0),损失L就越低(接近 0)。如果模型预测的概率a很高(接近 1),损失L同样会急剧增大。

该损失函数的核心思想是:对错误的预测施加巨大的惩罚(损失值趋近无穷),从而驱使模型学习做出正确的预测。

梯度计算与参数更新规则 🔄
现在,我们来看如何计算梯度并更新参数。这与我们之前训练线性回归和 Adaline 模型时使用的梯度下降法原理相同。
我们需要计算损失函数 L 相对于权重 w_j 的偏导数。根据链式法则,这可以分解为三个部分的乘积:
- 损失
L对激活输出a的导数。 - 激活函数
a对其输入z的导数(即 Sigmoid 函数的导数)。 - 净输入
z对权重w_j的导数(结果为输入特征x_j)。
将这三部分相乘后,一个巧妙的现象发生了:公式得到了极大的简化。最终,权重 w_j 的梯度更新公式为:
Δw_j = η * (a^{(i)} - y^{(i)}) * x_j^{(i)}
其中,η 是学习率,(i) 表示第 i 个训练样本。

这个公式看起来与 Adaline 和线性回归的更新规则完全一样!这正是逻辑 Sigmoid 函数导数“友好”特性的体现。唯一的区别在于,这里的预测值 a 是通过非线性 Sigmoid 函数 σ(z) 计算得到的,而 Adaline 中则是线性恒等函数。

因此,我们可以复用之前 Adaline 的随机梯度下降学习规则框架,只需改变预测值 a 的计算方式即可。

训练流程与预测推断 🎯

让我们将整个逻辑回归的训练和预测过程梳理一遍。

下图展示了逻辑回归的计算图。训练阶段(左半部分):输入特征 x 和真实标签 y,通过前向传播计算预测概率 a,然后计算负对数似然损失 L,接着通过反向传播计算损失相对于权重和偏置的梯度,最后使用梯度下降法更新参数。


训练完成后,进入预测阶段(右半部分)。我们可以使用一个阈值函数将连续的概率值 a 转换为离散的类别标签 ŷ。通常的规则是:
如果 a > 0.5,则 ŷ = 1;否则 ŷ = 0。
由于 Sigmoid 函数在 z=0 时输出 a=0.5,我们还可以进行一个更高效的计算优化:在预测时,可以直接比较净输入 z 与 0 的大小,而无需完整计算 Sigmoid 函数。
如果 z > 0,则 ŷ = 1;否则 ŷ = 0。


总结与预告 📚
本节课中我们一起学习了逻辑回归模型训练的核心数学原理。我们回顾了逻辑 Sigmoid 函数及其简洁的导数形式,分析了负对数似然损失函数如何对错误预测进行惩罚,并推导出了与线性模型形式一致的梯度更新公式。我们还梳理了从训练到预测的完整流程。

在接下来的视频中,我们将简要介绍“对数几率”和“交叉熵”这两个与逻辑回归相关的术语,然后通过一个具体的代码示例,将本节课的所有理论付诸实践,实现完整的前向传播、梯度计算和参数更新过程。代码实践将帮助大家更好地理解和巩固这些概念。



课程 P54:L8.4 - Logits 与交叉熵 🧠
在本节课中,我们将学习深度学习文献中两个常见的术语:Logits 和 交叉熵。我们将阐明它们与之前讨论的逻辑回归概念之间的关系,并解释它们在深度学习中的具体含义。

Logits:网络净输入
上一节我们介绍了逻辑回归,本节中我们来看看 Logits 这个术语。在深度学习中,当我们构建多层神经网络时,例如一个简单的全连接多层感知机,网络在最终输出层之前会计算净输入。
在深度学习中,通常将输出层之前的这些净输入值称为 Logits。在统计学的逻辑回归背景下,Logits 特指几率(odds)的对数,其公式为:
公式: logit(p) = ln(p / (1 - p))
这个函数实际上是逻辑斯蒂(Sigmoid)函数的反函数。然而在深度学习的通用语境下,无论最后一层是否使用Sigmoid激活函数,我们都习惯性地将最后一层的净输入称为Logits。
以下是Logits与Sigmoid函数关系的图示说明:





交叉熵:负对数似然的别名
理解了Logits后,我们再来看看损失函数。在之前的课程中,我们为逻辑回归推导了负对数似然损失函数。在深度学习和信息论领域,这个函数有一个更常用的名字:交叉熵。
具体来说,二分类问题中的负对数似然损失,完全等同于二元交叉熵损失。它们只是在不同学科背景下(统计学 vs. 信息论/计算机科学)对同一概念的不同称呼。
对于多分类问题,这个概念可以推广为多类别交叉熵,它是二元交叉熵的自然扩展。为了使其适用于多类,我们通常使用独热编码来表示类别标签。
以下是交叉熵与负对数似然等价性的图示说明:


为了更清晰地总结这两个概念,以下是关键点列表:
- Logits:在深度学习中,通常指代神经网络最后一层(输出层之前)的净输入值。
- 交叉熵:在深度学习语境下,通常指代用于训练分类模型的损失函数,在二分类情况下等同于负对数似然。
总结与预告
本节课中我们一起学习了深度学习中的两个核心术语:Logits 和 交叉熵。我们明确了Logits即网络最后一层的净输入,而交叉熵则是我们熟悉的负对数似然损失在深度学习领域的常用名称。


在接下来的课程中,我们将通过一个逻辑回归的代码示例来巩固这些概念,并进一步探讨多类别交叉熵及其在多项式逻辑回归中的应用。


课程 P55:L8.5 - PyTorch 中的逻辑回归 - 代码示例 📝


在本节课中,我们将通过一个完整的代码示例,来回顾并实践之前四节视频中讨论的逻辑回归核心概念。我们将从零开始实现一个逻辑回归模型,然后使用 PyTorch 的高级 API 来实现相同的功能,以加深对算法原理和实际应用的理解。









概述与准备工作
在开始编写代码之前,我们需要导入必要的库并准备一个简单的二分类数据集。这个数据集与我们作业中使用的类似,非常适合用于演示。

以下是导入库和加载数据的代码:





import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn

# 加载一个简单的二分类玩具数据集
# 这里使用 numpy 生成数据,实际中可以从文件加载
X_train, y_train = ... # 训练数据
X_test, y_test = ... # 测试数据





从零开始实现逻辑回归 🔨



上一节我们讨论了逻辑回归的数学原理,本节中我们来看看如何用代码实现它。我们将手动实现前向传播、反向传播(梯度计算)和参数更新。

以下是我们将实现的 LogisticRegression 类的核心方法:





__init__: 初始化权重和偏置,这里我们将权重初始化为零向量。forward: 计算净输入z和经过 Sigmoid 激活后的概率a。backward: 根据损失函数计算关于权重和偏置的梯度。predict: 根据概率进行阈值判断(如 >0.5 为类别1),得到预测标签。train: 整合上述步骤,执行多轮训练。





以下是 LogisticRegression 类的关键代码框架:






class LogisticRegression:
def __init__(self, num_features):
self.weights = torch.zeros(1, num_features)
self.bias = torch.zeros(1)
def forward(self, x):
# 计算净输入 z = w*x + b
z = torch.mm(x, self.weights.t()) + self.bias
# 应用Sigmoid激活函数得到概率 a = σ(z)
a = self._sigmoid(z)
return a
def backward(self, x, a, y):
# 计算损失关于输出的梯度:∂L/∂a = (a - y)
grad_a = a - y
# 计算损失关于权重的梯度:∂L/∂w = (∂L/∂a) * (∂a/∂z) * x^T ≈ (a - y) * x^T
grad_w = torch.mm(grad_a.t(), x)
# 计算损失关于偏置的梯度:∂L/∂b = sum(a - y)
grad_b = torch.sum(grad_a)
return grad_w, grad_b
def _sigmoid(self, z):
return 1. / (1. + torch.exp(-z))
def predict(self, x):
# 获取概率
a = self.forward(x)
# 应用阈值函数
y_pred = (a > 0.5).float()
return y_pred



训练循环的代码如下,它整合了前向传播、损失计算、反向传播和参数更新:



def train(model, X, y, epochs=30, lr=0.1):
cost_list = []
for epoch in range(epochs):
# 1. 前向传播,计算输出概率 a
a = model.forward(X)
# 2. 计算损失(负对数似然)
cost = -torch.sum(y * torch.log(a) + (1 - y) * torch.log(1 - a))
cost_list.append(cost.item())
# 3. 反向传播,计算梯度
grad_w, grad_b = model.backward(X, a, y)
# 4. 更新参数:w = w - lr * ∂L/∂w
model.weights -= lr * grad_w
model.bias -= lr * grad_b
return cost_list






运行这个从零开始的模型,我们可以看到它在训练集上快速收敛,并绘制出损失下降曲线和决策边界。







使用 PyTorch 模块 API 实现逻辑回归 ⚙️


手动实现有助于理解原理,但在实际项目中,我们更常使用深度学习框架提供的高级API。上一节我们手动计算了梯度,本节中我们来看看如何利用 PyTorch 的自动微分功能来简化这一过程。

使用 PyTorch 的 nn.Module 可以极大地简化代码。我们只需要定义网络结构,梯度计算和参数更新将由框架自动处理。






以下是使用 PyTorch API 实现的等效逻辑回归模型:


class LogisticRegressionPyTorch(nn.Module):
def __init__(self, num_features):
super().__init__()
# 定义一个线性层:y = xA^T + b
self.linear = nn.Linear(num_features, 1)
# 为了与手动实现对比,将权重和偏置初始化为0
self.linear.weight.data.fill_(0.0)
self.linear.bias.data.fill_(0.0)
def forward(self, x):
# 线性层输出后接Sigmoid激活函数
logits = self.linear(x)
outputs = torch.sigmoid(logits)
return outputs




使用 PyTorch 进行训练的流程更加简洁和标准化:




# 初始化模型、损失函数和优化器
model = LogisticRegressionPyTorch(num_features)
criterion = nn.BCELoss(reduction='sum') # 二元交叉熵损失,等价于负对数似然
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)






# 训练循环
for epoch in range(epochs):
# 前向传播
outputs = model(X_train_tensor)
# 计算损失
loss = criterion(outputs, y_train_tensor)
# 反向传播和优化
optimizer.zero_grad() # 清空过往梯度
loss.backward() # 自动计算梯度
optimizer.step() # 更新参数




运行 PyTorch 版本的模型,我们会发现其学习到的权重和偏置与手动实现的版本完全相同,这验证了我们手动实现的正确性,并且测试准确率也保持一致。







总结





本节课中我们一起学习了逻辑回归的两种代码实现方式。





- 我们首先从零开始实现了逻辑回归模型,手动编写了前向传播、梯度计算(反向传播)和参数更新的代码。这个过程帮助我们深入理解了逻辑回归的核心公式,例如梯度
∂L/∂w = (a - y) * x^T。 - 接着,我们使用了 PyTorch 的高级模块 API 来构建相同的模型。利用
nn.Linear、nn.BCELoss和torch.optim.SGD,我们以更简洁、更高效的方式完成了模型的构建和训练,并且依赖 PyTorch 的自动微分机制自动计算梯度。




通过对比两种方法,我们不仅巩固了理论知识,也掌握了在实际项目中使用专业工具进行开发的技能。在接下来的课程中,我们将把这些概念推广到多类别分类问题,即 Softmax 回归。

课程 P56:L8.6 - 多项 Logistic 回归 / Softmax 回归 📊
在本节课中,我们将要学习如何将二分类的逻辑回归概念推广到多分类问题。这被称为多项逻辑回归或 Softmax 回归。我们将以经典的 MNIST 手写数字数据集为例,详细介绍其原理和实现方式。
MNIST 数据集简介

上一节我们介绍了逻辑回归的基本概念,本节中我们来看看如何将其应用于多分类任务。首先,我们需要一个合适的数据集。

MNIST 数据集是一个包含 60,000 张训练图像和 10,000 张测试图像的手写数字数据集。每个数字(0 到 9)代表一个类别,因此这是一个 10 分类问题。数据集是平衡的,意味着每个类别都有相同数量的样本(训练集中每个数字有 6,000 张图片)。
每张图片的尺寸是 28 像素 × 28 像素,并且是黑白的(只有一个颜色通道)。在 PyTorch 中,我们通常使用 NCHW 格式来表示一个图像批次,其中:
N是批次大小(例如 128)。C是颜色通道数(对于 MNIST,C=1)。H是图像高度(28)。W是图像宽度(28)。
因此,每张图片在输入模型前,会被展平成一个包含 784 个特征(28*28)的长向量。特征值通常是像素强度,范围在 0 到 255 之间。为了优化梯度下降的性能,我们通常会对输入进行归一化,例如将像素值除以 255,使其范围在 0 到 1 之间,或者进一步减去 0.5 使其范围在 -0.5 到 0.5 之间。
从二分类到多分类的挑战

在标准的二分类逻辑回归中,模型接收一个特征向量,计算其与权重向量的点积并加上偏置,然后通过 Sigmoid 激活函数得到一个概率值。这个概率表示样本属于类别 1 的可能性,属于类别 0 的概率则是 1 - P(class=1)。


当我们面对超过两个类别时,一个直观的想法是使用多个独立的二分类逻辑回归模型,每个模型负责判断样本是否属于某一个特定类别。以下是这种方法的示意图和公式描述:


假设我们有 M 个输入特征和 H 个输出类别(例如 H=10)。我们可以为每个类别 j 设置一组权重 W_j 和一个偏置 b_j。
对于第 j 个输出节点,其净输入 z_j 为:
z_j = w_j · x + b_j
然后,我们对该净输入应用 Sigmoid 函数,得到该样本属于类别 j 的概率 P_j:
P_j = σ(z_j) = 1 / (1 + exp(-z_j))
然而,这种方法存在一个问题:每个模型输出的概率是独立的,所有类别的概率之和不一定等于 1。这在类别互斥(如一个数字只能是 0 或 1,不能同时是两者)的场景下不够直观。
Softmax 回归模型



为了解决上述问题,我们引入 Softmax 回归 模型。它与多个独立逻辑回归模型的关键区别在于激活函数。
在 Softmax 回归中,我们移除每个输出节点上的独立 Sigmoid 函数,取而代之的是一个共享的 Softmax 函数。该函数作用于所有输出节点的净输入向量 z。
以下是 Softmax 函数的工作方式:
- 对于每个类别
j,我们像之前一样计算净输入z_j。 - 然后,Softmax 函数接收所有
z_j(j从 1 到H),并计算每个类别的最终概率。
Softmax 函数的公式如下,它将任意实数值的向量“压缩”为一个概率分布向量,其中每个元素值在 (0,1) 之间,且所有元素之和为 1:
P(class=j) = exp(z_j) / Σ_{k=1}^{H} exp(z_k)
这样,对于一张图片,模型会输出一个长度为 H(类别数)的概率向量。例如,对于数字“3”,输出可能是 [0.1, 0.05, 0.7, ..., 0.02],其中第三个位置(对应数字‘2’,因为索引常从0开始)的概率 0.7 最高。
为了得到最终的类别预测,我们采用 argmax 操作,即选择概率向量中值最大的索引作为预测类别:
predicted_class = argmax(P)
核心步骤总结



本节课中我们一起学习了如何将逻辑回归扩展到多分类问题。以下是实现 Softmax 回归的核心步骤:
- 数据准备:将图像数据展平为特征向量,并进行适当的归一化处理。
- 模型设计:为每个类别设置独立的权重和偏置,计算净输入。
- 概率转换:使用 Softmax 函数将所有类别的净输入转换为一个总和为 1 的概率分布。
- 做出预测:通过 argmax 函数从概率分布中选出最可能的类别。

在接下来的课程中,我们将深入探讨用于训练 Softmax 模型的损失函数(通常是交叉熵损失)及其优化方法。
课程 P57:L8.7.1 - OneHot 编码与多类别交叉熵 🧮

在本节课中,我们将学习 Softmax 回归模型训练所需的多类别交叉熵损失函数。为了使用这个损失函数,我们还需要了解一种表示类别标签的编码方案——OneHot 编码。

1. Softmax 激活函数回顾 🔄
上一节我们介绍了 Softmax 回归模型的基本结构。本节中,我们来看看用于计算类别概率的 Softmax 激活函数。

Softmax 函数本质上是一个归一化函数,可以看作是逻辑 Sigmoid 函数在多类别情况下的推广。它确保所有输出类别的概率之和为 1。
假设我们有 H 个类别,对于第 t 个类别,其概率计算公式如下:

公式:
[
a_t = \frac{e{z_t}}{\sum_{j=1} e^{z_j}}
]
其中:
z_t是第t个类别的净输入(即线性层的输出)。- 分母是所有类别净输入指数值的总和,起到归一化的作用。

应用 Softmax 后,每个输出 a_t 都是一个介于 0 和 1 之间的值,并且所有 H 个输出的总和为 1。


2. OneHot 编码 🏷️
在讨论损失函数之前,我们需要先理解如何表示类别标签。OneHot 编码是一种将分类变量转换为机器学习模型易于处理格式的常用方法。
OneHot 编码为每个可能的类别值创建一个新的二进制列(特征)。对于每个样本,只有其真实类别对应的列值为 1,其他所有列的值都为 0。
以下是一个具体的例子:
假设我们有一个包含 4 个样本的数据集,可能的类别标签是 {0, 1, 2, 3}。
原始类别标签向量:
[0, 1, 3, 2]
应用 OneHot 编码后,我们得到一个矩阵:
| 样本 | 类别_0 | 类别_1 | 类别_2 | 类别_3 |
|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 0 |
| 2 | 0 | 1 | 0 | 0 |
| 3 | 0 | 0 | 0 | 1 |
| 4 | 0 | 0 | 1 | 0 |

可以看到,每个样本的标签都被转换成了一个仅在其真实类别位置为 1,其余位置为 0 的向量。
3. 多类别交叉熵损失函数 ⚖️
现在,我们可以结合 Softmax 输出和 OneHot 编码的标签来定义损失函数。多类别交叉熵损失是二元交叉熵损失在多类别问题上的自然推广。
损失函数的目标是:当模型对真实类别的预测概率很高时,损失值很低;当预测概率很低时,损失值很高。
对于一个有 H 个类别的数据集,其损失计算公式如下:
公式:
[
L = -\frac{1}{N} \sum_{i=1}^{N} \sum_{j=1}^{H} y_{ij} \cdot \log(a_{ij})
]

其中:
N是样本数量。H是类别数量。y_{ij}是第i个样本在第j个类别上的 OneHot 编码值(0 或 1)。a_{ij}是模型预测的第i个样本属于第j个类别的概率(Softmax 输出)。
关键点: 由于 y_{ij} 是 OneHot 编码,对于每个样本 i,只有一个 j 使得 y_{ij} = 1(对应真实类别),其他 j 的 y_{ij} 都为 0。因此,对于每个样本,内部求和实际上只计算了真实类别对应的那个 -log(a) 项。
4. 计算示例 📝
为了更清晰地理解,让我们通过一个具体例子来计算损失。
假设我们有 4 个样本,3 个类别。经过 OneHot 编码的真实标签 y 和模型 Softmax 输出的预测概率 a 如下:

真实标签 (y):
[ [1, 0, 0], # 样本1,真实类别为0
[0, 1, 0], # 样本2,真实类别为1
[0, 0, 1], # 样本3,真实类别为2
[0, 0, 1] ] # 样本4,真实类别为2
预测概率 (a):
[ [0.7, 0.2, 0.1], # 样本1的预测概率分布
[0.1, 0.8, 0.1], # 样本2的预测概率分布
[0.2, 0.3, 0.5], # 样本3的预测概率分布
[0.1, 0.1, 0.8] ] # 样本4的预测概率分布
现在,我们计算每个样本的损失:
- 样本1: 真实类别是0,对应预测概率为 0.7。
- 损失 =
-log(0.7) ≈ 0.357
- 损失 =
- 样本2: 真实类别是1,对应预测概率为 0.8。
- 损失 =
-log(0.8) ≈ 0.223
- 损失 =
- 样本3: 真实类别是2,对应预测概率为 0.5。
- 损失 =
-log(0.5) ≈ 0.693
- 损失 =
- 样本4: 真实类别是2,对应预测概率为 0.8。
- 损失 =
-log(0.8) ≈ 0.223
- 损失 =

最后,计算整个批次的平均损失:
[
L = \frac{0.357 + 0.223 + 0.693 + 0.223}{4} \approx 0.374
]
在深度学习中,我们通常计算平均损失而非总和,因为这有助于在使用不同批次大小时,更稳定地设置学习率。

5. 与二元交叉熵的联系 🔗
多类别交叉熵是二元交叉熵的直接扩展。当类别数 H=2 时,两者是等价的。

二元交叉熵公式为:
[
L_{binary} = -[y \cdot \log(a) + (1-y) \cdot \log(1-a)]
]
在二分类且使用 OneHot 编码(例如,类别表示为 [1,0] 和 [0,1])的情况下,多类别交叉熵公式会退化为与上述公式相同的形式。这证明了其一致性。
总结 📚
本节课中我们一起学习了:
- Softmax 函数:用于将神经网络的净输入转换为代表类别概率的分布,确保所有概率之和为 1。
- OneHot 编码:一种将类别标签转换为二进制向量的方法,其中只有真实类别对应的位置为 1。
- 多类别交叉熵损失:衡量模型预测概率分布与真实 OneHot 标签之间差异的函数。它是训练多类别分类模型(如 Softmax 回归)的核心。


理解这些概念是构建和训练有效分类神经网络的基础。在接下来的实践中,我们将看到如何在 PyTorch 等框架中轻松实现这些计算。


课程 P58:L8.7.2 - OneHot 编码与多类别交叉熵代码示例 🧮

在本节课中,我们将通过一个 PyTorch 代码示例,详细讲解 OneHot 编码和多类别交叉熵损失的计算过程。我们将从基础概念入手,逐步实现并解释代码中的关键步骤。
概述

我们将首先介绍如何手动实现 OneHot 编码,然后计算 Softmax 激活值,最后使用交叉熵损失函数评估模型预测。过程中会对比手动实现与 PyTorch 内置函数的使用方法。


OneHot 编码实现



上一节我们介绍了多类别分类的基本概念,本节中我们来看看如何在代码中实现 OneHot 编码。PyTorch 提供了一个 scatter 函数来实现此功能,虽然其用法有些特殊,但我们可以将其封装为一个便捷函数。


以下是 OneHot 编码函数的实现代码:



def one_hot_encoding(labels, num_classes):
# 此函数使用 scatter 方法将类别标签转换为 OneHot 编码矩阵
# labels: 包含类别索引的张量
# num_classes: 总的类别数量
return torch.zeros(labels.size(0), num_classes).scatter_(1, labels.unsqueeze(1), 1)

需要注意的是,在实际训练 Softmax 回归或多层感知机模型时,PyTorch 的损失函数通常会内部处理 OneHot 编码,因此我们通常不需要手动调用此函数。但了解其原理是有益的。



让我们用一个具体例子来演示。假设我们有四个训练样本,其类别标签分别为 0, 1, 2, 2,且总共有三个类别(0, 1, 2)。



labels = torch.tensor([0, 1, 2, 2])
num_classes = 3
one_hot = one_hot_encoding(labels, num_classes)


执行后,one_hot 是一个 4x3 的矩阵。每一行代表一个样本,其中数字 1 的位置指示了该样本所属的类别。



Softmax 激活函数计算

得到 OneHot 编码后,我们需要计算模型的净输入(net inputs)并通过 Softmax 函数将其转换为概率分布。



假设我们有一个任意生成的净输入矩阵 Z,其形状为 (4, 3),对应四个样本和三个类别。


Softmax 函数的公式如下:




公式: softmax(z_i) = exp(z_i) / sum(exp(z_j)) for j in all classes


以下是 Softmax 函数的代码实现:


def softmax(z):
# 计算 Softmax 激活值
# z: 净输入张量
exp_z = torch.exp(z - torch.max(z, dim=1, keepdim=True).values) # 数值稳定版本
return exp_z / torch.sum(exp_z, dim=1, keepdim=True)


同样,PyTorch 内置了更高效、更优化的 torch.nn.Softmax 函数,但此处的实现有助于理解其原理。




应用 Softmax 后,我们得到一个概率矩阵,其中每一行的所有值之和为 1。


获取预测类别并计算损失

接下来,我们需要从 Softmax 输出中获取预测的类别标签,并计算交叉熵损失。




我们使用 argmax 函数来获取每一行中概率最大的索引,即预测的类别。


predictions = torch.argmax(softmax_activations, dim=1)



现在,我们可以将预测结果与真实标签进行比较。在本例中,有一个预测是错误的。

交叉熵损失的公式包含两个求和:一个是对所有训练样本求和,另一个是对所有类别求和(但在 OneHot 编码下,只有正确类别的项为非零)。



对于单个样本,其损失计算为:loss_i = -log(p_correct_class)


以下是手动计算每个样本损失的过程:



# 计算每个样本的交叉熵损失
loss_per_sample = -torch.sum(one_hot * torch.log(softmax_activations + 1e-8), dim=1)


这里,one_hot * torch.log(softmax_activations) 操作会选取出正确类别对应的预测概率的对数值。


使用 PyTorch 内置损失函数

在实践中最推荐使用 PyTorch 内置的 torch.nn.CrossEntropyLoss。它计算更高效,数值稳定性也更好。

需要特别注意两个函数的输入要求:
torch.nn.NLLLoss(负对数似然损失)期望输入是 log(Softmax) 之后的值。torch.nn.CrossEntropyLoss期望输入是 原始的净输入(net inputs),它会在内部组合 Softmax 和 Log 操作。

以下是使用 CrossEntropyLoss 的示例:
import torch.nn as nn




# 创建损失函数实例,默认 reduction='mean' 会计算所有样本损失的平均值
loss_fn = nn.CrossEntropyLoss(reduction='none') # 设置为 ‘none‘ 以获取每个样本的损失
losses = loss_fn(net_inputs, labels) # net_inputs 是原始净输入,labels 是类别索引(非OneHot)


# 如果想使用默认的均值损失,可以这样写
loss_fn_mean = nn.CrossEntropyLoss() # 默认 reduction='mean'
total_loss = loss_fn_mean(net_inputs, labels)




使用 reduction='mean'(计算平均值)通常比 reduction='sum'(计算总和)更有利于训练稳定性和学习率的选择。


总结

本节课中我们一起学习了:
- 如何手动实现 OneHot 编码。
- Softmax 函数的原理与代码实现。
- 如何从 Softmax 输出中获取预测类别。
- 交叉熵损失的手动计算方式及其含义。
- 如何正确使用 PyTorch 内置的
CrossEntropyLoss函数,并注意其输入要求。


理解这些基础组件的运作方式,对于后续实现和调试更复杂的模型(如 Softmax 回归、多层感知机和卷积神经网络)至关重要。在接下来的课程中,我们将学习如何计算 Softmax 回归的梯度。

课程 P59:L8.8 - 梯度下降的 Softmax 回归导数 🧮
在本节课中,我们将要学习如何计算 Softmax 回归的梯度,即损失函数相对于权重和偏置单元的导数。掌握这些导数后,我们就能使用梯度下降或随机梯度下降来训练 Softmax 回归分类器。我们将首先介绍其核心概念,然后通过代码实现来验证其工作原理。
核心概念与计算图 📊
上一节我们介绍了 Softmax 回归的基本形式。本节中我们来看看其梯度计算的核心概念。Softmax 回归的学习规则与之前讨论的 Adaline 和逻辑回归分类器在概念上是相同的,因为它仍然是一个单层神经网络。

我们首先需要的是损失函数相对于某个权重 ( w_{ij} ) 的偏导数,或者更一般地,损失函数相对于整个权重向量的梯度。我们同样需要计算损失函数相对于偏置单元的偏导数。有了这些偏导数,我们就可以使用随机梯度下降来更新权重,这与我们在 Adaline、线性回归和逻辑回归中的做法一致。
我们可以使用链式法则将这个导数分解为几个部分:损失相对于激活函数的导数、激活函数相对于净输入的导数,以及净输入相对于权重 ( w_{ij} ) 的导数。与常规逻辑回归的唯一区别在于,我们现在使用的是 Softmax 激活函数。在 Adaline 中,激活函数是恒等函数;在常规逻辑回归中,激活函数是 Sigmoid 函数。因此,真正改变的是激活函数的形式。
为了更直观地理解,我们可以查看 Softmax 回归的计算图。请注意,这并非多层神经网络(我们将在下一讲讨论),它仍然只有一层,没有隐藏层。图中将净输入和激活函数作为中间步骤明确画出,这有助于理解。在 Softmax 激活中,每个输出单元的激活值都依赖于所有的净输入,因为存在归一化过程。

多元链式法则的应用 🔗
由于 Softmax 函数的特性,当我们计算损失相对于某个权重(例如 ( w_{1,2} ))的偏导数时,情况比逻辑回归更复杂。该权重不仅通过路径 ( z_1 \rightarrow a_1 \rightarrow L ) 影响损失,还通过路径 ( z_1 \rightarrow a_2 \rightarrow L ) 影响损失,因为 ( z_1 ) 参与了所有 Softmax 单元的计算。
因此,我们必须使用多元链式法则。对于上方的路径(涉及 ( a_1 )),我们将偏导数分解为:( \frac{\partial L}{\partial a_1} \cdot \frac{\partial a_1}{\partial z_1} \cdot \frac{\partial z_1}{\partial w_{1,2}} )。对于下方的路径(涉及 ( a_2 )),我们分解为:( \frac{\partial L}{\partial a_2} \cdot \frac{\partial a_2}{\partial z_1} \cdot \frac{\partial z_1}{\partial w_{1,2}} )。然后将两部分相加。
接下来,我们将逐步推导这些项。

1. 损失函数对激活的导数 📉
首先计算 ( \frac{\partial L}{\partial a_1} )。损失函数是多类别交叉熵(这里省略了对训练样本的求和,仅看一个样本):
[
L = -\sum_{j=1}^{H} y_j \log(a_j)
]
其中 ( H ) 是类别数。当我们对 ( a_1 ) 求偏导时,求和中除了 ( j=1 ) 的项,其他项都是常数,导数为零。因此可以去掉求和符号,得到:
[
\frac{\partial L}{\partial a_1} = -\frac{y_1}{a_1}
]
同理,对于 ( \frac{\partial L}{\partial a_2} ),有:
[
\frac{\partial L}{\partial a_2} = -\frac{y_2}{a_2}
]
2. Softmax 激活对净输入的导数 🧮
这部分计算稍复杂,但遵循明确的数学规则。我们需要计算 ( \frac{\partial a_1}{\partial z_1} ) 和 ( \frac{\partial a_2}{\partial z_1} )。

对于 ( \frac{\partial a_1}{\partial z_1} ):
Softmax 函数为 ( a_1 = \frac{e{z_1}}{\sum_{k=1} e^{z_k}} )。这是一个商的形式,我们使用商的求导法则。令 ( f = e^{z_1} ),( g = \sum_{k=1}^{H} e^{z_k} )。
根据法则 ( (f/g)' = (f'g - fg') / g^2 ):
- ( f' = e^{z_1} )
- ( g' = e^{z_1} ) (因为对 ( z_1 ) 求导时,求和项中只有 ( e^{z_1} ) 这项有导数)
代入公式并化简后,可以得到:
[
\frac{\partial a_1}{\partial z_1} = a_1 (1 - a_1)
]
对于 ( \frac{\partial a_2}{\partial z_1} ):
此时 ( a_2 = \frac{e{z_2}}{\sum_{k=1} e^{z_k}} )。同样应用商的求导法则,但注意分子 ( e^{z_2} ) 相对于 ( z_1 ) 是常数,其导数为 0。化简后得到:
[
\frac{\partial a_2}{\partial z_1} = -a_1 a_2
]
3. 净输入对权重的导数 ⚖️
最后一项 ( \frac{\partial z_1}{\partial w_{1,2}} ) 非常简单。因为 ( z_1 = w_{1,1}x_1 + w_{1,2}x_2 + ... + b_1 ),所以:
[
\frac{\partial z_1}{\partial w_{1,2}} = x_2
]
组合结果与简化公式 ✨

现在,我们将所有部分组合起来,计算 ( \frac{\partial L}{\partial w_{1,2}} ):
[
\frac{\partial L}{\partial w_{1,2}} = \left( \frac{\partial L}{\partial a_1} \cdot \frac{\partial a_1}{\partial z_1} + \frac{\partial L}{\partial a_2} \cdot \frac{\partial a_2}{\partial z_1} \right) \cdot \frac{\partial z_1}{\partial w_{1,2}}
]

代入我们推导出的各项:
- ( \frac{\partial L}{\partial a_1} = -\frac{y_1}{a_1} )
- ( \frac{\partial a_1}{\partial z_1} = a_1(1 - a_1) )
- ( \frac{\partial L}{\partial a_2} = -\frac{y_2}{a_2} )
- ( \frac{\partial a_2}{\partial z_1} = -a_1 a_2 )
- ( \frac{\partial z_1}{\partial w_{1,2}} = x_2 )

进行代数和化简(过程略),一个非常简洁的结果出现了:
[
\frac{\partial L}{\partial w_{1,2}} = (a_1 - y_1) \cdot x_2
]
这令人惊讶地简单!它看起来与 Adaline 和逻辑回归的学习规则完全相同,只是现在有了针对不同输出单元的索引。这表明 Softmax 激活函数与交叉熵损失函数是完美搭配,它们的导数形式非常简洁,易于代码实现且数值稳定。如果使用其他损失函数(如均方误差),导数就不会如此优雅地简化。
矩阵形式表示 🧩

最后,我们可以用更紧凑的线性代数形式来书写这个学习规则。对于所有权重矩阵 ( \mathbf{W} ),损失函数的梯度可以写为:
[
\nabla_{\mathbf{W}} L = \mathbf{X}^T (\mathbf{A} - \mathbf{Y})
]
其中:
- ( \mathbf{X} ) 是 ( n \times m ) 的设计矩阵(( n ) 个样本,( m ) 个特征)。
- ( \mathbf{Y} ) 是 ( n \times H ) 的 one-hot 编码真实标签矩阵(( H ) 个类别)。
- ( \mathbf{A} ) 是 ( n \times H ) 的模型预测概率矩阵(Softmax 输出)。
这种矩阵形式高效且便于在代码中利用优化过的线性代数库。在实践中,我建议先使用简单的 Python 循环实现标量形式以确保正确性,然后再将其转换为矩阵点乘操作。能够手动推导并实现这些梯度,让我们对早期神经网络研究者的工作充满敬意。如今,我们可以借助 PyTorch 等框架的自动微分功能轻松完成。
总结 🎯


本节课中我们一起学习了 Softmax 回归的梯度计算。我们回顾了核心概念,通过计算图和多元链式法则逐步推导了损失函数相对于权重的偏导数。关键的发现是,Softmax 函数与交叉熵损失结合后,梯度公式变得异常简洁:( \frac{\partial L}{\partial w_{ij}} = (a_i - y_i) x_j )。最后,我们看到了该规则的矩阵表示形式 ( \nabla_{\mathbf{W}} L = \mathbf{X}^T (\mathbf{A} - \mathbf{Y}) )。这个简洁而强大的公式是训练 Softmax 分类器乃至后续更复杂神经网络的基础。在接下来的课程中,我们将利用这个知识进行代码实现,并验证其与自动微分结果的一致性。
🧠 课程 P6:L1.3.2 - 机器学习的广泛类别第 2 部分:无监督学习
在本节课中,我们将要学习机器学习的第二个主要子类别——无监督学习。与监督学习不同,无监督学习不使用任何标签或目标值,其核心在于从数据中发现隐藏的结构或模式。

上一节我们介绍了监督学习,本节中我们来看看无监督学习。
📚 无监督学习概述
无监督学习不使用任何标签或目标值,因此没有明确的反馈信号。它的核心目标是发现数据中隐藏的结构。
从这个意义上讲,我们也可以将无监督学习视为表示学习。它有时也用于降维,但这并非其唯一应用。
🔍 降维与表示学习示例:主成分分析
以下是降维的一个经典示例。
你可能在统计学课程中听说过主成分分析,简称 PCA。它是一种线性变换技术,通过旋转数据并提取输入的线性组合来实现降维。
考虑以下示例,这里有两个特征:特征1和特征2,这些是我们的输入。图中的圆圈代表数据点在这个空间中的分布。

通过主成分分析,我们找到这个数据集的特征向量。我们利用这些特征向量来旋转数据,从而得到两个主成分,它们本质上是特征向量。
通常,我们按特征值的大小降序排列这些主成分,并保留对应较大特征值的特征向量。在本例中,第一个主成分对应的特征值大于第二个。
在实际应用中,我们可能会使用第一个主成分来获得数据的压缩表示。如果你不了解PCA的工作原理,无需担心,这只是一个无监督学习的示例。
🤖 另一个示例:自编码器
无监督学习的另一个例子是自编码器,我们将在本课程的第5部分深入讨论它。
自编码器的工作原理如下,这是一个非常简单的概念。你有一个输入数据,这是你的输入数据。然后,自编码器会产生一个输出。
自编码器由两部分组成:一个编码器和一个解码器。首先,输入的维度通常很高。然后,通过隐藏层,学习到一个维度更小的表示,我们称之为潜在表示或嵌入。
例如,你可以将图像中的每个像素视为一个特征。如果我们有一个100x100像素的图像,我们就有了10000个输入特征,这是一个非常高维的输入。然后,编码器(一个神经网络)可以将其压缩到一个更低的维度,比如10维。
接着,解码器执行相反的操作,它将低维的潜在表示作为输入,并尝试重建出高维的输出(例如原来的10000维)。目标就是重建输入。
那么,为什么这样做是有用的呢?关键在于这个潜在表示。你可以将其视为数据的压缩版本。如果解码器能够从这个低维表示中成功重建原始图像,那就意味着这个低维空间足以编码数据中的重要信息。因此,它也是一种降维技术。
实际上,如果自编码器使用线性激活函数,它与主成分分析有着密切的关系。我们将在第5部分更详细地讨论自编码器。
🆚 与分类任务的对比

为了更清晰地对比,我们回顾一下分类任务是如何工作的。深度学习的多数应用是关于分类的。
考虑我们有一个输入数据点,比如一张猫的图片。在训练期间,我们有一个标签(这里是“猫”),网络会输出一个概率,即该图像是猫的概率。当我们输入一个新的数据(可能是狗或猫)时,它会输出一个属于“猫”类别的概率。
分类器是根据已有的标签对图像进行分类。而无监督学习则完全不同,它不预测任何标签,而是学习可以用于重建输入的隐藏表示。
📊 无监督学习的另一示例:聚类


无监督学习的另一个例子是聚类。你可能之前听说过聚类,它旨在为未标记的样本分配组别成员身份。

考虑一个数据集,同样有两个特征X1和X2。这里没有任何聚类标签,我们只是根据相似性对它们进行分组。仅通过观察,你可以认为这些靠得更近的点可能属于一个簇,而那些点属于另一个簇。
在现实中,簇的边界有时并不清晰,数据也通常是高维的。聚类的核心是发现组别成员关系。
这里与分类的关键区别在于,我们不知道真实的标签。我们只能说这些点可能是一个簇,那些点是另一个簇。存在一些衡量聚类质量的内部度量标准,但我们并没有一个绝对正确的答案。
需要说明的是,聚类在本课程中不会扮演重要角色,我们不会深入讨论它,它更像是数据挖掘领域的主题。
📝 课程总结
本节课中我们一起学习了机器学习的第二个主要类别——无监督学习。我们了解到,无监督学习不依赖于标签,其核心目标是从数据中发现隐藏的结构或模式。
我们探讨了无监督学习的两个主要应用方向:
- 表示学习与降维:例如主成分分析和自编码器,它们旨在学习数据的压缩、低维表示。
- 聚类:根据相似性将数据点分组。


下一节视频,我们将简要介绍强化学习。
课程 P60:L8.9 - 使用 PyTorch 的 Softmax 回归代码示例 📚
在本节课中,我们将学习如何从零开始实现 Softmax 回归模型,并使用向量化形式展示其工作原理。我们还将使用 PyTorch 的高级 API 实现相同的模型,并最终在 MNIST 数据集上应用 Softmax 回归。
概述
本节课是本次讲座的最后一个视频,我们将通过代码示例来展示如何实现 Softmax 回归模型。我们将首先使用一个简单的 Iris 数据集进行从零开始的实现,然后使用 PyTorch 的模块化 API 实现相同的模型。最后,我们将在 MNIST 数据集上应用 Softmax 回归,作为线性分类器的基线模型。
1. 从零开始实现 Softmax 回归
上一节我们介绍了 Softmax 回归的数学原理,本节中我们来看看如何将其转化为代码。我们将使用 Iris 数据集,并手动计算梯度。
以下是实现所需的一些辅助函数:

to_onehot:将类别标签转换为独热编码格式。softmax:实现 Softmax 函数。cross_entropy:实现交叉熵损失函数。
请注意,在常规使用 PyTorch 时,你通常不需要自己实现这些函数,因为 PyTorch 内部已经提供了这些功能。但在这里,我们手动实现它们,以展示数学概念与代码之间的一一对应关系。
1.1 定义模型类

与逻辑回归类似,我们首先定义一个类。在 __init__ 构造函数中,我们初始化权重和偏置。
class SoftmaxRegression:
def __init__(self, num_features, num_classes):
self.num_classes = num_classes
self.weights = torch.zeros(num_classes, num_features) # 权重矩阵:H x M
self.bias = torch.zeros(num_classes) # 偏置向量:H 维

这里,H 是类别数量,M 是特征数量。权重 self.weights 是一个 H x M 的矩阵,偏置 self.bias 是一个 H 维的向量。
1.2 前向传播方法
forward 方法用于计算净输入(logits)和应用 Softmax 得到概率。
def forward(self, x):
# 计算净输入 Z = XW^T + b
logits = torch.mm(x, self.weights.T) + self.bias
# 应用 Softmax 得到概率(激活值)
probas = self.softmax(logits)
return logits, probas
其中,净输入 logits 的计算公式为:
Z = XW^T + b
1.3 反向传播方法
backward 方法是核心,我们在这里使用向量化形式计算梯度,而不是使用 for 循环。
def backward(self, x, y, probas):
# 计算权重梯度
grad_w = -torch.mm((y - probas).T, x)
# 计算偏置梯度
grad_b = -torch.sum(y - probas, dim=0)
return grad_w, grad_b
权重梯度 grad_w 的计算对应于公式:
∇_w J = -X^T (Y - A)
偏置梯度 grad_b 的计算对应于对 (Y - A) 按列求和。
1.4 预测与训练
predict_labels 函数通过取概率最高的索引来获得预测的类别标签。
def predict_labels(self, probas):
labels = torch.argmax(probas, dim=1)
return labels
训练函数 train 与逻辑回归类似,主要区别在于需要将标签进行独热编码,然后调用 forward 和 backward 方法获取梯度,并使用学习率更新权重。
def train(model, x_train, y_train, epochs, learning_rate, batch_size):
# ... (数据分批等操作)
for epoch in range(epochs):
# 独热编码标签
y_onehot = to_onehot(y_batch, model.num_classes)
# 前向传播
logits, probas = model.forward(x_batch)
# 计算损失
loss = cross_entropy(probas, y_onehot)
# 反向传播
grad_w, grad_b = model.backward(x_batch, y_onehot, probas)
# 更新参数
model.weights -= learning_rate * grad_w / batch_size
model.bias -= learning_rate * grad_b / batch_size
训练模型后,我们可以在测试集上评估其性能,并绘制决策区域来可视化分类结果。对于这个简单的三分类 Iris 数据集,Softmax 回归分类器能够相对较好地区分三个类别。
2. 使用 PyTorch 模块 API 实现
上一节我们手动实现了所有细节,本节中我们来看看如何使用 PyTorch 的高级 API 来简化实现。torch.nn.Module API 使许多事情变得更加容易。
2.1 定义模型
我们定义一个继承自 torch.nn.Module 的类。在 __init__ 中,我们使用 nn.Linear 层来计算净输入。
import torch.nn as nn
import torch.nn.functional as F


class SoftmaxRegressionPyTorch(nn.Module):
def __init__(self, num_features, num_classes):
super().__init__()
# 线性层自动处理权重和偏置的初始化
self.linear = nn.Linear(num_features, num_classes)
# 为了与从零实现对比,这里将权重初始化为零
nn.init.constant_(self.linear.weight, 0)
nn.init.constant_(self.linear.bias, 0)
def forward(self, x):
# 计算净输入
logits = self.linear(x)
# 应用 Softmax 得到概率
probas = F.softmax(logits, dim=1)
return logits, probas

F 是 torch.nn.functional 的常用别名,它提供了各种神经网络函数,如 softmax。

2.2 训练模型
使用 PyTorch 训练模型更加简洁。我们定义优化器(如 SGD)和损失函数(交叉熵损失)。
model = SoftmaxRegressionPyTorch(num_features, num_classes)
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 注意:PyTorch 的交叉熵损失函数期望输入是 logits,而不是概率
criterion = nn.CrossEntropyLoss()
for epoch in range(epochs):
# 前向传播
logits, probas = model(x_batch)
# 计算损失(标签无需独热编码)
loss = criterion(logits, y_batch)
# 反向传播
optimizer.zero_grad()
loss.backward()
# 更新参数
optimizer.step()
关键点在于,PyTorch 的 nn.CrossEntropyLoss 内部已经结合了 Softmax 和对数运算,因此它期望的输入是 logits(净输入),而不是经过 Softmax 后的概率。同时,它也不需要将标签进行独热编码。
使用 PyTorch API 实现的模型,其训练结果和决策区域与我们从零开始实现的模型几乎完全相同,只有微小的数值舍入误差。
3. 在 MNIST 数据集上应用 Softmax 回归
之前我们使用了简单的 Iris 数据集,本节中我们来看看如何在更复杂的 MNIST 手写数字数据集上应用 Softmax 回归。这通常是我们在深度学习中使用数据的方式。
3.1 使用 DataLoader 加载数据
在深度学习中,我们通常使用 torch.utils.data.DataLoader 来加载数据。
from torchvision import datasets, transforms
# 定义数据转换:将图像转换为张量并归一化到 [0,1] 范围
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)) # 对于单通道图像,均值和标准差都是 0.5
])
# 加载训练集和测试集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
# 创建 DataLoader
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=256, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=256, shuffle=False)
DataLoader 会自动处理数据的分批、打乱等操作。MNIST 图像是 28x28 的灰度图,有一个颜色通道。
3.2 调整输入维度
Softmax 回归期望的输入是一个特征向量。因此,我们需要将每张 28x28 的图像展平成一个 784 维的向量。
for images, labels in train_loader:
# 将图像从 [batch_size, 1, 28, 28] 展平为 [batch_size, 784]
features = images.view(images.shape[0], -1)
# ... 后续训练步骤
3.3 训练与评估
使用与之前相同的 PyTorch 模型进行训练。在 MNIST 数据集上,仅使用线性 Softmax 回归分类器,我们就可以获得大约 92% 的测试准确率。
# 训练循环
for epoch in range(epochs):
for images, labels in train_loader:
features = images.view(images.shape[0], -1)
logits, _ = model(features)
loss = criterion(logits, labels)
# ... 反向传播和优化步骤
# 评估测试集准确率
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
features = images.view(images.shape[0], -1)
logits, _ = model(features)
_, predicted = torch.max(logits, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / total
print(f'Test Accuracy: {accuracy:.2f}%')
这个结果相当不错,因为它仅使用了一个线性分类器。在处理分类任务时,始终运行逻辑回归或 Softmax 回归作为基线是一个好习惯,这有助于你了解分类问题的难度。例如,如果你训练一个复杂的卷积神经网络只得到 94% 的准确率,而线性模型已经达到 92%,那么你可能需要重新评估你的复杂模型。
总结

本节课中我们一起学习了 Softmax 回归的代码实现。我们从零开始,手动实现了前向传播、反向传播和参数更新,深入理解了其数学原理如何转化为代码。接着,我们使用 PyTorch 的高级 nn.Module API 简化了实现过程,展示了现代深度学习框架的便捷性。最后,我们将 Softmax 回归应用于 MNIST 数据集,使用 DataLoader 加载数据,并获得了不错的分类性能,这为后续更复杂的模型(如多层感知机)提供了一个有价值的性能基线。
📰 深度学习新闻 #5,2021 年 2 月 27 日
在本节课中,我们将回顾2021年2月底深度学习领域的一些重要新闻。我们将重点关注如何让深度学习模型的训练变得更高效,包括联邦学习、隐私保护以及如何在单GPU上训练大型模型等核心话题。
🤝 联邦学习与隐私保护
上一节我们讨论了深度学习面临的计算挑战。本节中我们来看看如何通过联邦学习来分散计算,以应对这些挑战。
联邦学习是一种在多个设备上分散计算的学习过程。苹果公司的研究人员发表了一篇题为《Federated evaluation and tuning for on-device personalization: system design and applications》的论文,介绍了他们的设备端机器学习系统。
联邦学习并非全新概念,许多公司和人员都在使用它。通常,其他公司的做法是利用联邦学习来调优一个全局神经网络模型。例如,服务器上有一个深度神经网络模型(如图像分类器),这个全局模型可以从个体用户那里学习。用户通过智能手机访问服务器上的模型,并通过标注数据为其提供训练数据。这种方式通常维护一个全局模型。

然而,苹果在这篇论文中描述的系统采用了不同的方法。该系统存在全局参数,但模型是在本地进行训练的。所有用户数据对服务器端都是不可访问的。这意味着他们专注于保护用户隐私,不向服务器发送任何用户数据,所有训练都在本地完成。用户可能会从全局模型获得一些参数,但最终会在自己的设备(例如手机)上获得一个个性化模型,而无需与服务器共享个人数据。
以下是该系统工作原理的示意图:


图中展示了三种类型的信息共享:
- 红色箭头代表任务配置和附件信息。
- 绿色箭头代表任务结果和遥测数据。
- 蓝色箭头代表设备端记录。
左侧方框代表终端用户设备。可以看到,蓝色的设备端记录(即用户数据)从未真正离开设备。右侧是开发者界面,开发者可以访问任务配置和任务结果,以评估模型学习效果,但无法访问任何用户数据,从而避免了隐私问题。

上图展示了该系统如何用于新闻个性化调优。他们有不同的模型(例如运行一和运行二),涉及不同的参数和指标。通过A/B测试结果,他们只能访问诸如改进百分比等信息(例如,运行二使每日文章浏览量增加了1.87%),而无需知道用户阅读了哪些类型的文章。
🔒 差分隐私
上一节我们对比了联邦学习的两种根本性不同方法:一种是在服务器上维护一个全局模型并发送用户数据,从隐私角度看可能存在问题;另一种是在设备上进行训练,用户数据永不离开设备,显得更友好。然而,如果采取一些预防措施,第一种使用全局模型的方法实际上并不那么糟糕。

有一个研究领域叫做差分隐私。这个领域致力于开发方法,通过向数据集中添加噪声,使得无法从数据中识别出具体个人。微软发布了一篇相关文章,但这只是一个广泛的领域,许多人都在进行研究。

为了说明问题,回顾一下Netflix的百万美元竞赛。Netflix在Kaggle上分享了一个数据集,包含电影标题、用户ID、评分日期和评分本身。用户ID并非真实身份,但研究人员通过结合IMDb电影评论数据库,利用评分日期和评分本身进行匹配,成功识别出了数据集中的部分用户。这构成了重大的隐私侵犯。
差分隐私就是处理这个问题,旨在保持数据集的效用(使其仍可使用),同时避免识别个体。其核心方法本质上是向数据集中添加或合成噪声,使数据集具有大致相同的统计特征。例如,平均评分可能不变,但对于特定用户,某些评分会比真实情况略高或略低,这样就不容易通过IMDb明确识别用户。
微软发布了一个名为 SmartNoise 的工具集,包含Python API等,使差分隐私技术更易于在实践中使用。还有一个GitHub示例仓库可供参考。


从SmartNoise实现的技术列表中可以发现,许多技术都涉及GANs(生成对抗网络)。GANs用于学习训练数据分布并从中生成新样本。有趣的是,目前许多差分隐私技术似乎都采用了生成对抗网络,至少SmartNoise方法中实现的技术是如此。
⚖️ 数据偏见与公平性
本周还有一篇题为《Hundreds of AI tools have been built to catch covid. None of them helped.》的文章,提到面部识别程序中的偏见问题。一个可能的原因是数据集中缺乏多样性。常见的缓解方法是向算法提供代表所有群体且公平的数据集。

然而,一篇名为《1 Label, 1 Billion Faces: Usage and Consistency of Racial Categories in Computer Vision》的论文研究了这个问题。他们发现,使用更多样化的数据集可能只在非常刻板的公平意义上起作用,实际上并不能真正解决问题。例如,算法更可能将金发的人标记为白人。因此,仅仅使数据更多样化并不能真正解决偏见问题,刻板印象仍然存在。需要更多工作来开发更公平的系统。


🤖 自动化机器学习
另一个有趣但不相关的话题是AutoML(自动化机器学习)。AutoML旨在为给定问题自动寻找良好的机器学习算法、超参数设置,有时还包括预处理步骤。传统上,人类需要尝试不同的算法、超参数和数据归一化步骤,而AutoML则自动化这一过程,减少人力。
AutoML的一个特定分支是神经架构搜索,这是针对神经网络的,有时缩写为NAS。本周有一篇题为《Introducing Model Search: An Open Source Platform for Finding Optimal Machine Learning Models》的文章。虽然已有一些其他AutoML开源平台,但这个方法略有不同。以往的方法通常基于强化学习、进化算法或组合搜索,而该方法似乎是这些方法的结合。


该方法异步训练多个模型,然后进行波束搜索,查看结果并考虑最佳模型,对其进行微小改动(类似于进化算法),还涉及知识蒸馏和加权迁移。他们将表现良好的模型的权重迁移到想要探索的新模型中,而不是从头开始训练。
结果显示该方法表现相当不错。虚线代表以前的方法,新方法(模型搜索)表现更好。虽然其构建模块并非全新,但组合方式使其成为一个性能优异的系统。如果你想在实践中使用表现良好的方法,这可能是一个不错的选择。
不过需要注意的是,神经架构搜索本身计算成本非常高,因为训练单个深度神经网络已经很昂贵,而这种方法需要异步训练多个模型,对于没有数百或数千个GPU的用户来说是另一个计算挑战。
🚀 大规模模型训练与单GPU技巧

现在让我们进入真正有趣的部分:在单GPU上训练大规模模型。我看到了Fast AI的Zilvane Guar(注:应为Sylvain Gugger)的演讲截图,总结了在单GPU上实现大规模模型训练的主要方法。

以下是一些核心方法:

1. 减小批次大小
这是最简单的方法。减小输入批次大小意味着更小的矩阵乘法,有助于缓解内存限制。GPU通常有固定的内存大小(常见卡在12到20GB之间),对于大型模型来说可能成为瓶颈,尤其是全连接层(如PyTorch中的线性层)。

2. 梯度累积
这与批次大小相关。如果批次非常小,更新会非常嘈杂。梯度累积是一种在多次反向传播中累积梯度,然后再进行更新的方法。这样允许使用更小的迷你批次。


3. 梯度检查点
我为此制作了一个简图。在传统训练中,前向传播的所有中间计算结果(用于反向传播)都保存在内存中。梯度检查点的工作方式是:计算前向传播,然后删除这些中间信息,只在需要更新特定节点时重新计算它们。这会在内存和计算效率之间进行权衡:训练速度变慢,但有助于处理内存限制。
以下是传统方法与梯度检查点方法的对比示意图:

传统方法:所有橙色节点始终保存在内存中。


梯度检查点:只将部分橙色节点保存在内存中,需要时重新计算蓝色节点。

4. ZeRO(零冗余优化器)
这是微软DeepSpeed库开发的一种技术。它本质上是一种内存优化技术,使用16位浮点运算。其优点在于,与一些其他提高训练效率的方法不同,ZeRO不需要对模型代码进行重大修改,更像是一个模型包装器。它还将不同的状态(权重、梯度和优化器状态)分区到可用的GPU和CPU上。因此,它需要多个GPU。
5. 模型并行与流水线并行
这些是相关的方法,将模型的不同部分放在不同的GPU上。如果你有一个参数众多的模型,无法放入单个GPU,可以简单地将模型拆分到不同的GPU上。

Facebook AI Research开发的PyTorch扩展库FairScale,也提供了高效的流水线并行实现。它使用PyTorch的顺序API,可以非常方便地将模型的不同层自动分配到不同设备上。例如,如果你有层A、B、C、D,可以将它们全部放入顺序流水线,并指定如何平衡地分布在两个GPU上。

📝 总结

本节课中我们一起学习了2021年2月底深度学习领域的多项进展。我们探讨了联邦学习如何通过分散计算来提高效率,并比较了不同隐私保护方法的优劣。我们还了解了数据偏见问题的复杂性,以及自动化机器学习(尤其是神经架构搜索)的进展。最后,我们重点学习了在资源有限(特别是单GPU)情况下训练大型模型的一系列实用技巧,包括减小批次大小、梯度累积、梯度检查点、ZeRO优化器以及模型/流水线并行等。这些技术对于在实际环境中高效地进行深度学习研究和应用至关重要。
课程 P62:L9.0 - 多层感知器 🧠
概述
在本节课中,我们将要学习多层感知器的基本概念、架构及其应用。我们将从解决经典的异或问题开始,进而探讨如何将其应用于更实际的图像分类任务,例如区分猫和狗。课程将涵盖非线性激活函数、模型实现、以及过拟合与欠拟合等核心主题。
多层感知器架构
上一节我们介绍了课程的整体安排,本节中我们来看看多层感知器的基本架构。
多层感知器是一种前馈人工神经网络,它包含一个输入层、一个或多个隐藏层以及一个输出层。与单层感知器不同,多层感知器通过引入隐藏层和非线性激活函数,能够学习更复杂的模式。
其核心思想是,每一层的神经元接收前一层神经元的输出,进行加权求和并加上偏置,然后通过一个非线性激活函数产生该层的输出。这个过程可以形式化地表示为:
公式:
对于第 ( l ) 层中的第 ( j ) 个神经元,其输入 ( z_j^{(l)} ) 和输出 ( a_j^{(l)} ) 计算如下:
[
z_j^{(l)} = \sum_{i} w_{ji}^{(l)} a_i^{(l-1)} + b_j^{(l)}
]
[
a_j^{(l)} = \phi(z_j^{(l)})
]
其中,( w_{ji}^{(l)} ) 是权重,( b_j^{(l)} ) 是偏置,( \phi ) 是非线性激活函数。
正是这种层级结构和非线性变换,使得多层感知器能够解决像异或这样的线性不可分问题。

非线性激活函数
了解了基本架构后,我们需要深入理解其核心组件之一:非线性激活函数。激活函数决定了神经元是否应该被激活,并将输入信号转换为输出信号。
以下是几种常见的非线性激活函数:
-
Sigmoid 函数
- 它将输入压缩到 (0, 1) 区间。公式为:( \sigma(z) = \frac{1}{1 + e^{-z}} )
- 常用于输出层进行二分类,但作为隐藏层激活函数时可能面临梯度消失问题。
-
Tanh 函数
- 双曲正切函数,将输入压缩到 (-1, 1) 区间。公式为:( \tanh(z) = \frac{e^{z} - e{-z}}{e + e^{-z}} )
- 其输出以零为中心,通常比 Sigmoid 函数表现更好。
-
ReLU 函数
- 整流线性单元,是目前最常用的激活函数。公式为:( \text{ReLU}(z) = \max(0, z) )
- 它计算简单,能有效缓解梯度消失问题,但可能导致“神经元死亡”。

选择合适的激活函数对网络的训练效率和最终性能至关重要。
代码实现示例

理论需要实践来巩固。本节我们将通过一个简单的代码示例,展示如何在 PyTorch 中实现一个多层感知器。
以下是一个用于二分类任务的双层感知器示例:
import torch
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleMLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size) # 第一层:输入层 -> 隐藏层
self.relu = nn.ReLU() # 激活函数
self.fc2 = nn.Linear(hidden_size, output_size) # 第二层:隐藏层 -> 输出层
self.sigmoid = nn.Sigmoid() # 输出层激活函数
def forward(self, x):
out = self.fc1(x)
out = self.relu(out)
out = self.fc2(out)
out = self.sigmoid(out)
return out
# 实例化模型
model = SimpleMLP(input_size=10, hidden_size=5, output_size=1)
这段代码定义了一个包含一个隐藏层的神经网络。nn.Linear 定义了线性变换层,nn.ReLU 和 nn.Sigmoid 是激活函数。在 forward 方法中,我们明确了数据从输入到输出的流动路径。
过拟合与欠拟合
成功实现模型后,我们必须关注模型在数据上的表现。这里的关键概念是过拟合与欠拟合。
- 欠拟合:指模型过于简单,无法捕捉数据中的基本规律。表现为在训练集和测试集上的性能都很差。
- 过拟合:指模型过于复杂,不仅学习了数据中的规律,还学习了噪声和随机波动。表现为在训练集上性能很好,但在测试集上性能显著下降。
为了评估和改善模型的拟合情况,我们可以采取以下策略:
以下是几种常见的策略:
- 划分数据集:将数据分为训练集、验证集和测试集。
- 观察学习曲线:绘制模型在训练集和验证集上的损失/准确率随训练轮次变化的曲线。
- 使用正则化:如 L1/L2 正则化、Dropout 等,为模型增加约束,防止其变得过于复杂。
- 获取更多数据:更多的数据有助于模型学习更通用的模式。
理解并处理过拟合与欠拟合是构建高效机器学习模型的核心技能。
应用:猫狗图像分类
现在,让我们将所学知识应用于一个实际场景:使用多层感知器对猫和狗的图片进行分类。
虽然对于图像数据,卷积神经网络通常是更优的选择,但多层感知器是一个很好的起点。这个过程会涉及数据加载和预处理。
以下是实现自定义数据加载的关键步骤:
- 组织数据:将猫和狗的图片分别放入名为“cat”和“dog”的文件夹中。
- 使用
torchvision.datasets.ImageFolder:这个工具能自动根据文件夹结构创建带标签的数据集。 - 定义数据变换:包括调整图片大小、转换为张量、归一化等。
- 创建 DataLoader:用于批量加载数据,并支持打乱顺序和多线程加载。
通过自定义数据加载器,你可以轻松地将自己的数据集集成到 PyTorch 训练流程中,这对于你的个人项目至关重要。
总结
本节课中我们一起学习了多层感知器。我们从其基本架构和解决异或问题的能力讲起,深入探讨了不同的非线性激活函数及其作用。随后,我们通过 PyTorch 代码实现了简单的多层感知器,并讨论了机器学习中至关重要的过拟合与欠拟合问题及其应对策略。最后,我们将理论应用于实践,学习了如何构建自定义数据加载器来处理猫狗分类任务。

多层感知器是深度学习的基础,理解它为学习更复杂的网络结构,如卷积神经网络和循环神经网络,奠定了坚实的基础。
课程 P63:L9.1 - 多层感知器结构 🧠
在本节课中,我们将要学习多层感知器的基本结构。多层感知器是一种经典的前馈神经网络,它是构建更复杂深度学习模型的基础。我们将了解其组成部分、工作原理以及与之前学过的逻辑回归和Softmax回归的区别。
多层感知器是什么?🤔
多层感知器是一种全连接的前馈神经网络,它包含一个或多个隐藏层。
“全连接”意味着在给定层中的每个神经元,都与下一层中的所有神经元相连接。例如,在逻辑回归的上下文中,输入层的每个特征都连接到输出层,这类似于一个没有隐藏层的多层感知器。
上一节我们介绍了多层感知器的基本定义,本节中我们来看看它与Softmax回归的主要区别。

多层感知器与Softmax回归的主要区别在于,它在输入层和输出层之间引入了隐藏层。同时,它使用一种称为反向传播的学习算法,该算法本质上是使用链式法则的梯度下降法,与我们上一讲在Softmax回归中使用的原理非常相似,只是现在有了隐藏层。
“前馈”指的是信息从输入到输出单向流动(例如在本图中从左到右)。后续课程中我们将看到并非所有网络都是前馈的(例如循环神经网络),也并非所有网络都是全连接的(例如卷积神经网络)。
网络结构详解 🏗️
下图展示了一个具有两个隐藏层的多层感知器。

左侧是输入层,接着是两个隐藏层,最后是输出层。由于只有一个输出单元(类似于逻辑回归,不同于Softmax回归),这对应一个二分类问题。因此,输出层可以使用Sigmoid函数,类似于逻辑回归。
除了这些隐藏层,这里并没有太多新概念。我们仍然有连接各层神经元之间的权重。在计算用于训练网络的偏导数时,我们同样可以使用多元链式法则。

例如,如果我们想计算损失函数相对于某个权重(如 W_11)的偏导数,我们需要沿着从损失到该权重的所有路径使用链式法则。在下图中,我用紫色高亮显示了其中两条路径。


可以看到,路径之间是共享的。这意味着,如果你先计算了某一部分梯度,在进一步反向传播时就可以复用这部分结果,这是实际实现中的一种优化技巧。但根本上,这与应用于Softmax回归的概念是相同的。
命名与层数约定 📝
现在让我们谈谈命名约定。下图中使用了圆括号 ( ) 来表示层索引。

之前我们使用方括号 [ ] 通常指代第 i 个训练样本。而这里的圆括号 ( ) 指的是隐藏层的索引。例如,a^(1) 表示第一个隐藏层的激活值,a^(2) 表示第二个隐藏层的激活值。输出层也可以写作 a^(3),因为其计算方式(如果使用Sigmoid函数)与隐藏层激活值的计算方式是相同的。
每个激活值 a 是基于净输入 z 计算得出的,而 z 的计算公式为:
z = W^T * x + b
对于第一个隐藏层,x 是原始输入。对于后续的层,x 则是前一层的激活值。
关于层数的计数方式:有时人们将输入层称为第一层,但这会导致逻辑回归被视为一个两层网络,这不太直观。更常见的做法是:
- 输入层(第0层)
- 第一个隐藏层(第1层)
- 第二个隐藏层(第2层)
- 输出层(第3层)
这样,逻辑回归就可以被视为单层神经网络。
在激活函数方面,我们可以在所有层使用逻辑Sigmoid函数。损失函数通常使用负对数似然,也称为二元交叉熵。对于多分类问题,输出层则使用Softmax函数。

这是“深度学习”吗?🤨

一个有趣的问题是:这真的算深度学习吗?多层感知器已经存在了大约50年。
根据近期的趋势,在论文中越来越常见地将多层感知器称为深度学习。传统上,“深度学习”一词刚出现时,通常指的是利用一些超越普通多层感知器的技巧进行特征学习。但如今,多层感知器本身在某种意义上已被视为深度学习。


非凸损失函数与优化挑战 ⛰️
一个非常重要的点是,对于多层感知器,损失函数不再是凸函数。
在线性回归、Adaline、逻辑回归和Softmax回归中,我们拥有凸损失函数。例如,线性回归的损失函数相对于一个权重是碗状的,通过梯度下降可以平滑地到达全局最小值。

然而,对于多层感知器,我们面对的是高度非凸的损失曲面。下图展示了仅考虑两个权重时的损失景观,可以看到存在许多局部最小值。

Global Minimum 表示全局最小值,Local Minima 表示局部最小值。损失值越高(颜色越暖),模型性能越差;损失值越低(颜色越冷),性能越好。
在神经网络训练中,我们通常从小的随机权重开始。根据初始权重的不同,优化过程可能收敛到不同的局部最小值,甚至可能陷入其中而无法到达全局最小值。因此,在实践中,几乎不可能找到真正的全局最小值。
常见的做法是使用不同的随机种子多次运行网络训练,然后选择最佳的几个结果进行平均和报告。在接下来的课程中,我们将讨论如何选择学习率、初始权重以及优化算法,以帮助模型跳出局部最小值。
激活函数与损失函数的选择 ⚖️
除了选择好的初始权重、学习率和优化器,选择合适的激活函数以及激活函数与损失函数的组合也至关重要。
这里用一个例子说明某些组合可能存在的问题:考虑使用逻辑Sigmoid激活函数和均方误差损失函数。
我们可能会遇到梯度消失的问题,尤其是在预测非常错误的时候。当我们希望进行大幅修正时,梯度却可能变得非常平缓,导致权重几乎不更新。

回忆一下Sigmoid函数的形状及其导数。当网络输入 z 为很大的负数时,Sigmoid输出接近0,其导数也接近0。如果此时真实标签是1,但预测概率极低(如0.001),损失会很大。但在计算梯度时:
∂Loss/∂W = ∂Loss/∂a * ∂a/∂z * ∂z/∂W
其中 ∂a/∂z(Sigmoid的导数)会非常小,从而导致整个梯度很小,权重更新微乎其微,无法纠正错误预测。
相比之下,如果使用二元交叉熵损失配合Sigmoid激活,在求导时会产生良好的抵消效果,得到更合理的梯度更新规则。因此,仔细考虑激活函数和损失函数的搭配非常重要。
对于多分类问题,输出层使用Softmax激活是更自然的选择,但同样需要注意与损失函数(如分类交叉熵)的配合。

思考题 💭
作为本讲的练习或讨论点,请思考以下问题:
如果我们将多层感知器的所有权重初始化为0,会发生什么?
之前提到我们使用小的随机权重来帮助逃离局部最小值或获得不同的起点。但如果所有权重初始值相同(例如全为0),会对训练过程产生什么影响?为什么我们不这样做?


总结 📚
本节课中我们一起学习了:
- 多层感知器的定义:它是一种全连接、前馈的神经网络,包含一个或多个隐藏层。
- 核心结构:包括输入层、隐藏层和输出层,使用反向传播算法进行训练。
- 命名约定:了解了层索引的表示方法和计数惯例。
- 与深度学习的关系:讨论了多层感知器在现代语境下如何被归入深度学习范畴。
- 优化挑战:认识到其损失函数的非凸性,以及由此带来的陷入局部最小值和训练不稳定的挑战。
- 组件选择的重要性:强调了激活函数与损失函数需要谨慎搭配,以避免如梯度消失等问题。
在下一讲中,我们将探讨比逻辑Sigmoid函数更适用于多层感知器的其他激活函数。




深度学习课程 P64:L9.2 - 非线性激活函数 🧠
在本节课中,我们将要学习神经网络中的非线性激活函数。我们将了解为什么需要它们,它们如何与隐藏层协同工作以创建复杂的非线性决策边界,并介绍几种常见的激活函数及其特性。


PyTorch API 简要回顾
在深入探讨非线性激活函数之前,让我们简要回顾一下PyTorch的API,以便更好地理解如何在代码中构建多层感知机。
上一节我们介绍了神经网络的基本结构,本节中我们来看看如何在PyTorch中实现它。以下是两种常见的构建网络方式。
常规方式(自定义类):
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = nn.Linear(...) # 第一个隐藏层
self.linear2 = nn.Linear(...) # 第二个隐藏层
self.out = nn.Linear(...) # 输出层
def forward(self, x):
x = self.linear1(x)
x = F.relu(x) # 非线性激活
x = self.linear2(x)
x = F.relu(x) # 非线性激活
x = self.out(x)
return F.log_softmax(x, dim=1) # 输出log概率
这种方式在__init__中定义层,在forward方法中显式定义数据流。
Sequential API方式(更简洁):
model = nn.Sequential(
nn.Linear(...), # 第一个隐藏层
nn.ReLU(), # 非线性激活
nn.Linear(...), # 第二个隐藏层
nn.ReLU(), # 非线性激活
nn.Linear(...) # 输出层
)
# 注意:使用CrossEntropyLoss损失函数时,它内部已包含Softmax,因此网络输出层通常不需要显式添加Softmax。
nn.Sequential容器按顺序执行定义的层,使代码更紧凑、易读且不易出错。两种方式构建的网络功能是相同的。

为什么需要非线性激活函数?🔑

现在,让我们回到核心问题:为什么神经网络需要非线性激活函数?
为了理解这一点,我们来看一个简单的“异或”(XOR)分类问题。下图展示了一个简单的二维数据集,包含两个类别(橙色方块和蓝色圆点),它们的分布构成了一个非线性的分类问题。
如果我们使用一个带有一个隐藏层但使用线性激活函数的多层感知机(MLP)来尝试解决这个问题,结果会如何?
- 即使有隐藏层,模型也只能产生一个线性的决策边界(如图中左侧的直线),无法正确分类所有数据点。
这个现象揭示了两个关键点:
- 单独的隐藏层(使用线性激活)不足以产生非线性决策边界。
- 单独的非线性激活函数(如逻辑回归中的Sigmoid,但没有隐藏层)也只能产生线性边界。
结论是:要建模复杂的非线性关系,我们必须同时使用隐藏层和非线性激活函数。只有它们的组合才能让神经网络学习到像右侧那样的非线性决策边界,从而解决XOR这类问题。
这是因为,如果没有非线性激活函数,多个线性层的堆叠本质上等价于一个单一的线性变换:f(f(x)) 仍然是线性的。非线性激活函数在层与层之间引入了“弯曲”,使得网络能够组合出复杂的函数。

常见的非线性激活函数 📈
理解了必要性后,我们来具体看看几种历史上和当前流行的非线性激活函数。
以下是几种经典激活函数的对比:
-
Sigmoid (Logistic):
σ(x) = 1 / (1 + e^{-x})- 特点:将输入压缩到(0, 1)区间,输出可视为概率。
- 问题:在两端饱和区域梯度非常小(“梯度消失”),不利于深层网络训练;输出不以0为中心。
-
Tanh (Hyperbolic Tangent):
tanh(x) = (e^{x} - e^{-x}) / (e^{x} + e^{-x})- 特点:输出范围(-1, 1),以0为中心。梯度比Sigmoid更陡峭一些。
- 优点:相比Sigmoid,通常能使收敛更快。
- 问题:同样存在梯度饱和问题。
-
ReLU (Rectified Linear Unit):
ReLU(x) = max(0, x)- 特点:计算简单高效。当输入为正时,梯度恒为1,缓解了梯度消失问题。
- 问题:可能导致“神经元死亡”(Dead ReLU),即输入始终为负的神经元梯度永远为0,无法更新。

ReLU的变体与现代激活函数 🚀
由于ReLU的广泛使用及其存在的一些问题,研究者提出了多种改进版本。
以下是ReLU家族的一些重要成员:
-
Leaky ReLU:
f(x) = max(αx, x),其中α是一个小的正数(如0.01)。- 改进:为负输入提供了一个小的斜率α,避免了梯度为零的情况,理论上可以防止“神经元死亡”。
-
Parametric ReLU (PReLU): 形式与Leaky ReLU相同,但斜率α作为一个参数,可以通过训练学习,而不是预先固定。
-
Exponential Linear Unit (ELU):
f(x) = x if x>0 else α(e^x - 1)- 改进:对负值的处理更平滑,输出均值更接近0,可能带来更快的收敛速度和更好的泛化性能。
-
SELU (Scaled ELU): 经过精心缩放(scale)的ELU。在特定条件下,它能够使网络在训练中保持“自归一化”属性,有助于深层网络的稳定训练。
实践建议:对于大多数初学者和一般任务,ReLU是一个强大且默认的好选择。如果遇到疑似“神经元死亡”或训练不稳定的问题,可以尝试Leaky ReLU或ELU。这些变体之间的性能差异通常不大,但有时能带来提升。

前沿视角:平滑激活函数与对抗鲁棒性 🛡️
激活函数的选择不仅影响普通训练,还与模型的对抗鲁棒性(抵抗恶意干扰的能力)密切相关。
最近的研究表明,广泛使用的ReLU由于其非平滑性(在x=0处不可导),可能会削弱对抗训练的效果。而使用ReLU的平滑近似(如Softplus函数:f(x) = log(1 + e^x))进行“平滑对抗训练”,可以在不损失太多标准准确率的情况下,显著提升模型的对抗鲁棒性。
这意味着,在某些对安全性要求高的场景下,将ReLU替换为更平滑的变体可能是一个简单而有效的改进策略。

总结 🎯
本节课中我们一起学习了非线性激活函数的核心知识:

- 必要性:非线性激活函数与隐藏层结合,是神经网络能够学习复杂非线性模式的关键。
- 发展历程:从传统的Sigmoid、Tanh,到现代默认的ReLU及其众多变体(Leaky ReLU, PReLU, ELU, SELU)。
- 核心特性:我们关注激活函数的形状、输出范围、梯度行为(是否易饱和)以及对训练动态的影响。
- 实践选择:ReLU是通用的强大起点。可根据具体问题尝试其变体以优化性能或稳定性。
- 前沿联系:激活函数的平滑性被发现与模型的对抗鲁棒性有关,平滑的近似版本可能在此方面更具优势。


记住,激活函数为神经网络注入了“非线性”的灵魂,是使其从线性模型跃升为强大函数逼近器的核心组件之一。
课程 P65:L9.3.1 - 多层感知器代码第 1/3 部分(幻灯片概述) 📚

在本节课中,我们将学习多层感知器的代码实现。首先,我们会通过几张幻灯片来总结即将在代码中看到的内容,并补充一些之前未提及的关于多层感知器的重要概念。
代码示例与性能对比 📊
上一节我们介绍了多层感知器的基本概念,本节中我们来看看具体的代码实现和不同配置下的性能表现。
我准备了两份代码笔记本,并会展示一些Python脚本作为替代方案。在这些笔记本中,我实现了一个使用Sigmoid激活函数和均方误差损失的多层感知器,以及另一个结构相同但使用Softmax激活函数和交叉熵损失的多层感知器。
以下是两张性能对比图:
- 左侧图表展示了使用Sigmoid和均方误差组合的训练过程。你可以看到每个小批量的损失在下降,这是期望的结果,但由于是随机梯度下降,更新过程存在噪声。图表底部绘制了在整个训练集上计算的损失,它在每个训练周期后计算,因此曲线更平滑。该模型最终达到约90%的训练准确率和91%的测试准确率,没有过拟合。但回想一下,在Softmax回归课程中,我们仅用Softmax回归就达到了约92%的准确率。
- 右侧图表展示了使用Softmax和交叉熵组合的训练过程。损失同样在下降,但训练准确率达到了99%,测试准确率接近98%,存在轻微的过拟合。这表明在此场景下,交叉熵损失比均方误差损失表现更好。
我认为Sigmoid与均方误差组合表现不佳的原因可能是它们不是最佳搭配。正如之前概述的,与Softmax和交叉熵(甚至Sigmoid和交叉熵)相比,它们的梯度项不能很好地抵消。在链式法则中,Sigmoid函数的导数是一个小于1的数,这可能导致梯度消失问题,使得梯度变得非常小,从而影响参数更新。
交叉熵损失通常与Sigmoid激活函数搭配更好,或者我推荐使用Softmax激活函数,因为我们处理的是互斥的类别。实际上,你可以尝试修改代码,将Softmax改为Sigmoid,但可能不会看到太大差异。
激活函数选择:ReLU与“死亡神经元” ⚡️

上一节我们对比了不同损失函数,本节我们来讨论另一个关键组件:激活函数。
我已经提到了“死亡神经元”的问题。我们也可以使用ReLU激活函数,它非常流行。你可以在代码笔记本中尝试它。正如我所说,ReLU可能是我最常使用的激活函数,因为它通常效果很好。
然而,理论上ReLU存在“死亡神经元”问题。如果在训练中,某个神经元的输入加权和(净输入)为很大的负数,那么ReLU的输出将始终为0,其梯度也为0。这意味着,如果输入过大或过小导致净输入为负,并且负得特别极端,那么该神经元可能永远无法被激活,其对应的权重也可能永远得不到更新,从而形成“死亡神经元”。
不过,这并不一定是坏事。如果你的网络有很多神经元,容易过拟合,那么“死亡神经元”实际上减少了一些参数,可能通过简化网络(类似于剪枝)来帮助提升性能。
与Sigmoid或Tanh函数相比,ReLU的一个优势是它更少受到梯度消失问题的影响,因为它的梯度要么是0,要么是1。在最坏的情况下,你得到的是死亡神经元(梯度为0);而在其他情况下(净输入为正),你总是能得到一个强梯度1。但理论上,如果网络其他部分的值大于1,它也可能导致梯度爆炸问题。
关于梯度的消失和爆炸,我们将在讨论循环神经网络时更详细地探讨,那里是讨论这个问题的好时机。
网络结构:深度与宽度的权衡 🏗️

了解了激活函数的选择后,我们接下来思考一个结构性问题:应该使用深层还是宽层的多层感知器?
假设我们需要设计一个多层感知器。我们可以选择让每层只有少量神经元(例如每层5个),但堆叠很多层(深度网络);或者,我们也可以选择只用一个隐藏层,但该层包含大量神经元(例如100个,宽度网络)。
那么,哪种更可取呢?理论上,存在一些关于“通用近似定理”的研究。该定理表明,一个具有单个足够大隐藏层的多层感知器,就能够近似任意函数。既然如此,为什么我们还要关心使用多层网络呢?
能够近似任意函数并不意味着训练这样的网络是实用的。首先,训练本身存在挑战,大型矩阵乘法计算量大。其次,要达到与使用更多层(但每层参数更少)的网络相同的表达能力,你可能需要更多的参数。使用更少的参数但更多的层,可以在组合上提供更多的可能性,从而获得相同的表达能力。
但是,如果你有很多层,就可能遭受前面提到的梯度消失或爆炸问题。因此,在实践中通常需要权衡。对于传统的多层感知器,你通常不会使用超过一两层的隐藏层,否则就会遇到梯度问题。
稍后我们将讨论卷积神经网络等其他类型的网络,它们可以通过一些技巧设计,使我们能够构建更深的网络而无需担心梯度问题。这基本上就是深度学习的核心之一:巧妙地设计架构,以便能够进行更深的训练。对于多层感知器这种并非真正的深度学习架构来说,一到两个隐藏层通常就足够了。如果你尝试实现一个有三到四层隐藏层的多层感知器,通常会注意到它训练得不太好,因为误差无法有效地反向传播那么远。
一个实际的考虑是,在多层感知器中通常使用一到两个隐藏层。而对于卷积层,我们可以构建多达50、100甚至200层的网络,这在当今非常普遍。
总结与预告 📝
本节课中我们一起学习了多层感知器代码实现前的理论概述。

我们对比了不同激活函数和损失函数组合的性能,分析了ReLU激活函数的优缺点及其可能导致的“死亡神经元”现象。我们还探讨了网络深度与宽度之间的权衡,理解了为什么传统的多层感知器不宜过深,以及更深网络的潜力所在。
最后,我们了解到,虽然理论上更深的网络可以用更少的参数实现相同的表达能力,并且更深的结构本身可能带来某种形式的正则化(因为后层受前层行为的约束),但我们也必须面对梯度消失和爆炸的挑战。

现在,是时候向你展示代码示例了。让我暂停这个视频,然后启动我的Jupyter笔记本。
🧠 PyTorch 教程 P66:L9.3.2 - 多层感知器实现(Jupyter Notebook)
在本节课中,我们将学习如何使用 PyTorch 实现一个多层感知器(MLP),并将其应用于 MNIST 手写数字分类任务。我们将重点关注 MLP 与之前学习的 Softmax 回归在代码结构上的关键区别,并理解如何构建包含隐藏层的神经网络。


📦 导入与超参数设置


首先,我们需要导入必要的库并设置一些超参数。以下是代码的初始部分。
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms




# 超参数设置
batch_size = 100
num_epochs = 100

我们设置批量大小为 100,这是一个常用值,通常也使用 2 的幂次方,如 32、64 或 128。训练轮数设置为 100。



📊 加载 MNIST 数据集
接下来,我们加载 MNIST 数据集。其加载方式与我们在 Softmax 回归课程中使用的方法相同。
# 数据转换:将图像转换为张量并归一化
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
# 下载并加载训练集和测试集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
MNIST 数据集已包含在 torchvision 中。我们关注的重点是后续的多层感知器实现。


🏗️ 定义多层感知器模型


上一节我们介绍了数据准备,本节中我们来看看模型的核心部分。与 Softmax 回归相比,多层感知器的关键区别在于增加了一个隐藏层。

以下是模型定义:

class MultilayerPerceptron(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super(MultilayerPerceptron, self).__init__()
# 第一个全连接层(输入层 -> 隐藏层)
self.fc1 = nn.Linear(input_size, hidden_size)
# Sigmoid 激活函数
self.sigmoid = nn.Sigmoid()
# 第二个全连接层(隐藏层 -> 输出层)
self.fc2 = nn.Linear(hidden_size, num_classes)
# 注意:我们不在输出层显式使用 Softmax
def forward(self, x):
# 将图像展平
x = x.view(-1, 28*28)
# 输入到隐藏层并应用激活函数
out = self.sigmoid(self.fc1(x))
# 隐藏层到输出层(输出为 logits)
out = self.fc2(out)
return out


核心概念解析:
- 隐藏层:
self.fc1是输入层和输出层之间的额外层,赋予模型学习非线性关系的能力。 - 激活函数:
nn.Sigmoid()为网络引入非线性。我们使用较小的随机权重初始化(均值为0,标准差为0.1),以防止 Sigmoid 函数饱和导致梯度消失。 - 输出 Logits:模型直接输出
fc2的结果,即 logits(未归一化的分数)。我们不在前向传播中显式计算 Softmax。


如果移除这个隐藏层,模型就退化成了 Softmax 回归模型。

⚙️ 模型初始化与训练设置
现在我们来初始化模型并设置训练所需的损失函数和优化器。

# 初始化模型
input_size = 28 * 28 # MNIST 图像尺寸
hidden_size = 100 # 隐藏层神经元数量,这是一个可调整的超参数
num_classes = 10 # 输出类别数(数字0-9)
model = MultilayerPerceptron(input_size, hidden_size, num_classes)


# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 内部已包含 LogSoftmax
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)


关键点说明:
- 损失函数:使用
nn.CrossEntropyLoss()。这是推荐的做法,因为它将LogSoftmax和负对数似然损失 (NLLLoss) 组合在一起,比手动计算更稳定高效。 - 优化器:使用随机梯度下降(SGD)。



🔄 训练循环与损失计算

训练循环的结构与 Softmax 回归相似。我们额外实现一个函数来计算整个训练集上的损失,用于监控训练过程。

以下是计算整个数据集损失的工具函数:


def compute_loss(data_loader, model):
total_loss = 0.0
# 禁用梯度计算以节省内存和计算资源
with torch.no_grad():
for images, labels in data_loader:
outputs = model(images)
loss = criterion(outputs, labels)
total_loss += loss.item()
return total_loss / len(data_loader)
使用 with torch.no_grad(): 上下文管理器是因为我们只进行前向传播计算损失,不需要构建计算图进行反向传播,这样可以提升效率。


以下是主要的训练循环:

train_losses = []


for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播与优化
optimizer.zero_grad() # 清空过往梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新模型参数
# 记录日志(可选)
if (i+1) % 100 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {loss.item():.4f}')
# 每个 epoch 结束后,计算整个训练集的平均损失
epoch_loss = compute_loss(train_loader, model)
train_losses.append(epoch_loss)
print(f'Epoch [{epoch+1}/{num_epochs}] completed. Average Training Loss: {epoch_loss:.4f}')


训练过程在 CPU 上执行。对于 MNIST 这样的数据集,这已经足够。代码运行后,我们可以绘制 train_losses 列表来观察训练损失随轮数下降的趋势。



📝 总结

本节课中我们一起学习了如何使用 PyTorch 实现一个基础的多层感知器。
我们主要涵盖了以下内容:
- 模型结构:在 Softmax 回归的基础上增加了隐藏层和 Sigmoid 激活函数,构成了基本的 MLP。
- 代码实现:定义了
MultilayerPerceptron类,并解释了前向传播过程。 - 训练流程:使用了
CrossEntropyLoss损失函数和 SGD 优化器,并实现了完整的训练循环。 - 实用技巧:介绍了使用
torch.no_grad()来高效计算验证损失的方法。

这个实现虽然简单,但包含了神经网络训练的核心要素。通过调整隐藏层大小、激活函数或优化器等超参数,你可以进一步探索并提升模型性能。
🧠 P67:L9.3.3 - PyTorch 中的多层感知器(脚本设置)第 3/3 部分
在本节课中,我们将学习如何将多层感知器(MLP)的训练代码组织成更模块化、可维护的脚本形式。我们将介绍如何使用独立的设置文件、辅助函数文件以及主训练脚本,来构建一个适合大型项目的代码结构。

📊 回顾上一节结果

上一节我们介绍了在Jupyter Notebook中训练MLP模型。运行大约4分钟后,损失函数持续下降,模型取得了不错的准确率。这表明我们的模型训练过程是有效的。
🗂️ 项目代码组织
在大型项目或需要进行大量调优时,将代码组织成脚本形式更为高效。以下是推荐的项目文件结构:
helper.py:存放可复用的通用函数。settings.yaml:存放模型超参数和配置。train_model.py:主训练脚本。
这种结构有助于代码复用和项目管理。
🔧 详解脚本文件
接下来,我们详细看看每个文件的作用。
train_model.py 主训练脚本
这个脚本是训练流程的入口。它从其他文件导入功能,并协调整个训练过程。
# 示例:从helper.py导入通用函数
from helper import data_loader, set_deterministic, compute_accuracy, plot_results
脚本使用 argparse 库来接收命令行参数,例如设置文件路径和结果保存路径。这样做使得在不修改代码的情况下,能灵活地运行不同配置的实验。
# 示例:解析命令行参数
parser.add_argument('--settings_path', type=str, required=True)
parser.add_argument('--results_path', type=str, required=True)
一个有用的技巧是同时将输出打印到控制台并写入日志文件,便于后续查看和记录。
# 示例:同时打印和记录日志
import sys
sys.stdout = open('training.log', 'w')
settings.yaml 配置文件
我们使用YAML格式文件来管理所有超参数和设置。YAML文件易于人类阅读和编写,并且可以通过 PyYAML 库轻松加载为Python字典。
# settings.yaml 示例
batch_size: 64
learning_rate: 0.001
num_epochs: 10
hidden_units: [128, 64]
在主脚本中,我们这样加载配置:
import yaml
with open(settings_path, 'r') as f:
settings = yaml.safe_load(f)
这种方式的好处是,进行超参数调优时,只需修改YAML文件,而无需触及主训练代码。
helper.py 辅助函数文件

这个文件包含了在各个项目中都可能用到的通用工具函数,例如:
set_deterministic:设置随机种子,确保实验结果可复现。compute_accuracy:计算模型准确率。plot_results:绘制损失和准确率曲线图。

将通用功能模块化,避免了在不同项目或脚本中重复编写相同代码。
🚀 运行训练脚本
在命令行中,我们可以通过指定配置文件和结果目录来运行训练。

python train_model.py --settings_path ./settings.yaml --results_path ./results
运行过程中,控制台会输出训练信息,同时这些信息也会被实时写入 results/training.log 文件。训练结束后,结果字典(包含最终测试准确率、各轮次的训练/验证准确率等)会被保存为YAML文件,损失和准确率曲线图也会被保存下来。




📓 与Jupyter Notebook的混合使用


即使在使用Jupyter Notebook进行交互式开发时,我们也可以利用这种模块化思想。你可以在Notebook中导入 helper.py 中的函数,从而保持Notebook代码的简洁。
# 在Jupyter Notebook中
from helper import data_loader, train_model
# ... 然后使用这些函数

这结合了脚本的模块化优势和Notebook的交互式探索便利性。

📝 本节总结

本节课我们一起学习了如何将PyTorch多层感知器的训练代码组织成更专业的脚本形式。我们介绍了使用独立配置文件(YAML)管理超参数、创建辅助函数文件封装通用逻辑、以及编写主训练脚本并通过命令行运行的方法。这种结构提高了代码的可维护性、可复用性和实验管理的便捷性,是进行严肃机器学习项目开发的良好实践。所有代码示例都将上传至Github供大家参考。接下来,我们将继续探讨多层感知器的其他方面。
🧠 P68:L9.4- 过拟合和欠拟合
在本节课中,我们将学习机器学习中的两个核心概念:过拟合和欠拟合。我们将探讨模型过于简单或过于复杂时会出现的问题,并了解如何通过模型容量、偏差和方差等概念来理解这些现象。课程最后,我们还会介绍一个有趣的新发现——“双下降”现象。
模型表现与错误案例
我们已经见识了像逻辑回归这样的简单模型,以及像多层感知机这样更复杂的模型。现在是讨论过拟合和欠拟合的好时机。具体来说,我们将探讨模型过于简单(如逻辑回归)和模型对数据拟合得“太好”(如具有过多特征的多层感知机)所带来的问题。

首先,我们来看一个关于手写数字识别的小测验。以下是MNIST数据集中的一些图像,请判断它们代表什么数字。
实际上,这些图像的识别存在一定模糊性。例如,一个真实标签为“8”的图像,模型可能预测为“4”。另一个真实标签为“2”的图像,可能被误认为“7”。这些案例表明,即使是人类,有时也难以100%准确判断,这也意味着模型在该数据集上达到100%准确率几乎是不可能的。
下图展示了一个在MNIST上训练的多层感知机的一些失败预测案例。这个模型使用了Sigmoid激活函数和均方误差损失,准确率约为93%。但这里的重点不是准确率,而是可视化错误预测的价值。通过查看模型预测错误的样本,我们有时能发现数据本身存在错误标签等问题,从而获得对数据的额外洞察。


📈 理解过拟合与欠拟合
上一节我们看到了模型可能犯的错误,本节中我们来深入理解其背后的核心概念。
过拟合指的是模型记住了训练集中某些过于特定、无法推广到测试集的细节。
欠拟合则是指模型不够复杂,无法捕捉数据中的基本趋势。
为了量化这些概念,我们引入模型容量。模型容量可以粗略地理解为模型的参数数量或拟合复杂数据集的能力。例如,拥有更多隐藏层或更宽隐藏层的模型,其容量通常更大。

我们可以绘制误差随模型容量变化的曲线来观察:
- 训练误差(橙色曲线):通常随着模型容量增加而下降。
- 泛化误差/测试误差(蓝色曲线):在模型容量较小时,由于模型太简单,误差较高。随着容量增加,误差下降。但当容量过大时,模型开始过拟合,测试误差会再次上升。
欠拟合通常发生在曲线左侧,即模型容量过低、训练和测试误差都很高的区域。
过拟合通常发生在曲线右侧,即训练误差很低但测试误差很高的区域。过拟合的程度通常可以通过训练误差与测试误差之间的差距来评估。

🎯 偏差与方差分解
在实践中,你可能会听到“模型具有高偏差”或“模型具有高方差”的说法。这与过拟合和欠拟合密切相关。这里我们简要介绍偏差-方差分解的概念。
假设我们有一个回归模型,其预测值为 \hat{\theta},真实值为 \theta。我们使用均方误差进行评估。设想我们拥有大量不同的训练集,在每个训练集上训练模型后,对同一个数据点进行预测。
- 偏差:所有模型预测值的平均值
E[\hat{\theta}]与真实值\theta之间的差距。它衡量了模型的系统性误差,即预测平均偏离目标多远。 - 方差:各个模型的预测值
\hat{\theta}围绕其平均值E[\hat{\theta}]的离散程度。它衡量了模型预测的稳定性。
一个形象的比喻是射箭:
- 高偏差:所有箭都射偏了(但可能扎堆)。
- 高方差:箭的落点非常分散(但平均落点可能在靶心附近)。
通常:
- 高偏差、低方差的模型往往欠拟合(模型太简单,预测不准但稳定)。
- 低偏差、高方差的模型往往过拟合(模型复杂,在训练集上预测准,但对数据变动敏感,不稳定)。

下图总结了这些关系。随着模型容量增加,偏差下降,方差上升。欠拟合区域对应高偏差,过拟合区域对应高方差。


🔧 模型评估实践
对于深度学习,由于数据集通常很大,我们最常用的是保留法,即将数据分为三部分:
- 训练集:用于训练模型。
- 验证集:用于在训练过程中调整超参数、监控模型性能并防止过拟合(例如早停法)。
- 测试集:用于在模型训练和调优完成后,提供最终的、无偏的性能评估。
核心原则是:测试集在整个流程中只使用一次,以确保评估的公正性。在后续的代码示例中,我们将使用这种方法。

📊 数据规模与模型性能
深度学习的一个特点是在大型数据集上表现尤为出色。下图对比了传统机器学习方法(如决策树、SVM)和深度学习模型的泛化误差随训练数据规模变化的趋势。
- 在数据量较小时,传统方法往往表现更好。
- 随着数据量增加,两种方法的性能都会提升,但深度学习模型的误差下降曲线通常更为陡峭,显示出其从海量数据中学习强大表征的能力。

因此,如果你的模型性能不佳,在调整超参数收效甚微时,获取更多数据可能是一个有效的策略。

🤔 有趣的现象:双下降

传统的认知是,随着模型容量增加,测试误差会先下降后上升(即过拟合)。但2019年底的一篇论文提出了“双下降”现象。
研究者发现,在CNN、ResNet等模型中,当模型容量持续增加时,测试误差会出现先下降,后上升,然后再下降的情况。第一个下降是预期中的,中间的上升对应经典过拟合区,而随后的再次下降则令人意外。



这种现象也出现在训练周期上。对于中等复杂度的模型,训练过久会导致测试误差先降后升(此时需要早停)。但对于非常大的模型,测试误差在上升之后会再次下降。


一种理论解释是:在临界区域(第一次上升处),可能只有一个特定的权重组合能很好拟合数据,但这个解对噪声非常敏感,难以稳定找到。而在过参数化区域(模型极大),存在许多能很好拟合训练集的权重子集,随机梯度下降算法更有可能找到其中一个同时也能在测试集上表现良好的解。

尽管原因尚未完全明晰,但“双下降”现象已被多个实验独立观察到。它挑战了我们对模型容量与泛化能力的传统理解。

🎓 课程总结
本节课中,我们一起学习了机器学习中的核心挑战:

- 过拟合与欠拟合:理解了模型过于复杂和过于简单时出现的问题,以及如何通过训练/测试误差曲线进行识别。
- 偏差与方差:从理论角度了解了模型误差的分解,以及它们与过拟合(高方差)、欠拟合(高偏差)的关联。
- 评估方法:掌握了在深度学习中常用的训练集、验证集、测试集三分割的保留法。
- 数据规模的重要性:认识到深度学习模型性能随数据量增长而显著提升的特点。
- 前沿洞察:初步了解了“双下降”这一有趣的研究现象,它揭示了超大模型可能具备意想不到的泛化优势。

理解这些概念是构建有效机器学习模型的基础。在接下来的课程中,我们将学习如何将多层感知机应用于自定义数据集。

🐱🐶 P69:L9.5.1 - 猫狗分类与自定义数据加载器
在本节课中,我们将学习如何为猫狗图像分类任务创建一个自定义的数据加载器。我们将讨论训练、验证和测试集的作用,理解过拟合现象,并简要介绍超参数的概念。最后,我们会通过一个直观的动画了解PyTorch数据加载器的工作原理。
📊 模型训练与过拟合分析
上一节我们介绍了神经网络的基本概念,本节中我们来看看一个实际的猫狗分类模型训练过程。我使用Kaggle上的数据集训练了一个VGG16卷积神经网络。在训练过程中,我们绘制了损失和准确率曲线。

在左侧的交叉熵损失图中,训练损失如预期般下降。然而,在验证集上,损失最初下降,随后便停滞不前。这意味着模型在训练集上持续改进,但在验证集上不再提升,这表明出现了过拟合。
在右侧的准确率图中,情况类似。训练准确率持续上升,甚至可能达到97%,而验证准确率在达到约88%后便趋于稳定。在独立的测试集上评估后,也得到了约88%的准确率,这与验证集的表现一致。
验证集为我们提供了模型泛化性能的早期指示,使我们无需动用最终测试集就能发现过拟合。测试集应仅在最终阶段使用一次,以获取无偏的性能估计。
🎯 数据集划分策略

在深度学习中,我们通常将数据划分为训练集、验证集和测试集,而不是使用传统的K折交叉验证,因为后者对于深度神经网络来说计算成本过高。
以下是关于如何划分数据的一些建议:
- 训练集:用于训练模型参数。通常分配最大的比例,例如80%。
- 验证集:用于在训练过程中监控模型性能,并调整超参数。可以分配较小的比例,例如5%。
- 测试集:用于在模型训练和调优完成后,进行最终的一次性评估,以获得对泛化性能的无偏估计。例如,可以分配15%。
没有绝对正确的划分比例,但通常围绕上述值进行调整。验证集性能通常会略优于在真正未见数据上的性能,因为我们可能会无意中根据验证集表现来选择模型。
🔧 训练监控与工具
在训练过程中,绘制训练和验证的准确率/损失曲线非常有用,它能直观展示过拟合的程度。当然,你也可以选择只打印这些数值。
如果你希望在训练时实时查看这些曲线,可以使用一些专门的工具。以下是两个流行的选择:
- MLflow:一个用于管理机器学习生命周期的平台,支持实验跟踪。
- TensorBoard:最初为TensorFlow设计,但现在也与PyTorch兼容,广泛用于可视化训练过程。

你可以根据自己的兴趣和项目需求探索这些工具。
⚙️ 参数与超参数
现在,我们来区分一下模型参数和超参数。
模型参数是模型从训练数据中学习到的值,例如神经网络中的权重和偏置。它们通过反向传播算法进行优化。
超参数则是在训练开始前,由研究人员手动设置或调整的配置。它们控制着训练过程本身。以下是一些常见的超参数:
- 学习率 (
learning_rate) - 训练轮数 (
num_epochs) - 批次大小 (
batch_size) - 隐藏层数量 (
num_hidden_layers) - 隐藏单元数量 (
num_hidden_units) - 激活函数类型(如
ReLU,Sigmoid) - 正则化方法
- 权重初始化方案
- 优化算法选择
选择超参数没有固定的理论公式,通常需要通过实验和经验来确定。为了获得可靠的结果,建议使用不同的随机种子多次运行模型,然后取平均性能。
📦 PyTorch数据加载器工作原理
在进入代码实践之前,我们通过一个动画来直观理解PyTorch数据加载器(DataLoader)的工作流程。

概念流程如下:
- 数据集:包含原始的输入和标签对。
- 变换:对数据进行预处理(如缩放、裁剪)。
- 采样器:从数据集中抽取单个样本。
- 批次采样器:将多个样本组合成一个批次。
- 整理函数:将批次内的样本堆叠成张量,形成模型可用的批量输入和标签。
这个过程会重复进行,直到遍历完所有数据(一个周期)。如果启用了shuffle参数,每个周期中样本的顺序会不同。

🛡️ 过拟合与正则化
我们目前的好消息是,多层神经网络可以解决复杂的非线性问题。但随之而来的坏消息是,这些网络拥有大量参数,很容易导致过拟合。

上图展示了模型复杂度的权衡:
- 左侧:模型过于简单(欠拟合),无法捕捉数据中的模式。
- 中间:模型过于复杂(过拟合),完美拟合训练数据但泛化能力差。
- 右侧:通过正则化等技术得到的模型,在复杂度和泛化能力之间取得了更好的平衡。

在接下来的课程中,我们将详细讨论正则化技术,以帮助防止过拟合。
📝 总结
本节课中我们一起学习了:
- 通过猫狗分类任务观察了模型训练中的过拟合现象。
- 理解了训练集、验证集和测试集的划分与作用。
- 认识了模型参数与超参数的区别。
- 了解了PyTorch
DataLoader的内部工作流程。 - 认识到过拟合是复杂模型面临的主要挑战,并预告了正则化作为解决方案。

在下一个视频中,我们将进入代码实践,亲手构建一个自定义的数据加载器。
机器学习课程 P7:L1.3.3 - 强化学习 🎮
在本节课中,我们将要学习机器学习的第三个主要子类别:强化学习。我们将了解它的基本概念、工作原理以及一个具体的应用实例。
概述
强化学习是机器学习的一个重要分支,它关注的是智能体(Agent)如何通过与环境(Environment)的交互来学习一系列动作(Actions),以达成某个目标或最大化累积奖励(Reward)。虽然我们不会在本课程中深入探讨其技术细节,但了解其基本思想对于构建完整的机器学习知识体系很有帮助。
上一节我们介绍了监督学习和无监督学习,本节中我们来看看强化学习。
强化学习的基本概念
强化学习的核心是智能体、环境、状态、动作和奖励这几个要素的交互循环。其目标是通过学习一个策略(Policy),使得智能体在特定状态下选择的动作能获得最大的长期回报。
一个经典的强化学习循环可以表示为以下公式:
状态(S_t) -> 动作(A_t) -> 奖励(R_t) -> 新状态(S_{t+1})
在这个循环中,智能体观察当前状态,执行一个动作,环境对此做出反应并给出一个即时奖励,同时转移到下一个状态。
工作原理:以药物设计为例
为了更好地理解,我们来看一个药物分子设计的例子。
智能体面对的环境初始状态可能是一个简单的苯环分子。智能体观察这个状态后,需要采取一个动作,例如为这个苯环添加一个甲基化学基团。
这个动作会改变环境,生成一个新的分子状态(苯环加上甲基)。同时,系统会根据这个动作的结果(例如,新分子的某种特性)给予智能体一个奖励信号。
以下是这个交互过程的关键步骤:

- 当前状态:智能体观察环境,例如一个苯环结构。
- 采取动作:智能体从可能的动作集合中选择一个,如“添加甲基”。
- 环境更新:动作执行后,环境进入新的状态(甲基化苯环)。
- 获得奖励:环境根据新状态的质量(如药物活性)给出一个奖励值。
这个过程会重复多轮,直到形成一个完整的小分子化合物。每一步的奖励(可能是零或负值)会引导智能体学习如何构建出具有理想属性的分子。
更复杂的实例:星际争霸2
一个更具体且令人印象深刻的例子是使用强化学习来玩《星际争霸2》这款复杂的即时战略游戏。
在这个环境中:
- 状态 是当前游戏画面所包含的所有信息(单位、资源、地图等)。
- 动作 是玩家在任一时刻可以执行的所有操作(建造、移动、攻击等)。
- 奖励 通常是延迟的,最终体现在游戏的胜利或失败上。
几年前,DeepMind公司开始研究用强化学习训练AI玩《星际争霸2》。这个游戏的复杂性极高,有大量同时发生的事件和可能的动作。令人印象深刻的是,最终开发的AI系统能够击败世界上绝大多数顶尖的人类玩家。
这个项目的终极目标并非仅仅是成为游戏高手,而是为了开发能够泛化到其他重要领域(如自动驾驶)的通用强化学习系统。
总结
本节课中我们一起学习了机器学习的第三个广泛类别——强化学习。我们了解到强化学习关注的是智能体通过与环境交互、根据获得的奖励来学习最优行动序列的范式。我们通过药物分子设计的简化例子和《星际争霸2》的复杂实例,直观地理解了其“状态-动作-奖励”的核心循环。

需要再次说明的是,强化学习本身是一个庞大而独立的领域,本课程不会深入其技术细节。但理解它的基本思想,有助于我们看清机器学习解决不同问题的全貌。
📚 P70:L9.5.2 - PyTorch 中的自定义数据加载器(代码示例) 🚀
在本节课中,我们将学习如何在 PyTorch 中创建自定义数据加载器。数据加载器是一个 PyTorch 类,它能让我们比手动操作更方便地批量加载数据。我们将通过两个笔记本示例来演示:一个展示数据加载器本身的基本用法,另一个展示如何在模型训练中使用它。

概述与数据准备
我们将使用 MNIST 数据集作为示例。选择 MNIST 的原因是它相对较小,便于上传到 GitHub 供大家快速下载。虽然 PyTorch 的 torchvision 库中已有现成的 MNIST 数据集和数据加载器,但这里我们将其视为一个由 PNG 文件和 CSV 文件组成的通用数据集,以演示如何处理自定义数据。


我已经将 MNIST 数据集分成了三个部分:训练集、验证集和测试集。每个部分都存放在独立的文件夹中,包含 PNG 图像文件和一个对应的 CSV 文件。CSV 文件中记录了文件名和对应的类别标签。

以下是数据集的目录结构示例:
mnist_train/
├── 0.png
├── 1.png
└── ...
mnist_train.csv


当然,你也可以选择将所有图像放在一个文件夹中,然后在 CSV 文件中用额外的列来区分训练集、验证集和测试集。具体如何组织取决于你的偏好。


第一步:检查数据集







在开始构建数据加载器之前,通常需要先检查数据集。我们可以使用 Python 的 PIL(Pillow)库来打开和查看图像。



from PIL import Image
import matplotlib.pyplot as plt



# 打开一张图像
img = Image.open('mnist_train/0.png')
print(f'图像尺寸: {img.size}') # 输出: (28, 28)
print(f'图像模式: {img.mode}') # 输出: L (灰度图)


# 显示图像
plt.imshow(img, cmap='binary')
plt.show()

通过检查,我们了解到图像是 28x28 像素的灰度图,像素值范围在 0 到 255 之间。
接下来,我们使用 pandas 库来查看 CSV 文件的内容。

import pandas as pd




# 加载 CSV 文件
df_train = pd.read_csv('mnist_train.csv')
print(df_train.head())
CSV 文件包含 filename 和 label 两列。为了演示方便,我特意将每个子集的数据量缩小到了 256 张图像。




第二步:创建自定义 Dataset 类



PyTorch 的数据加载需要两个核心部分:Dataset 和 DataLoader。Dataset 类负责定义如何读取单个数据样本,而 DataLoader 则负责批量加载和打乱数据。



我们需要继承 PyTorch 的 torch.utils.data.Dataset 类来创建自己的数据集类。


import torch
from torch.utils.data import Dataset
from PIL import Image






class MyDataset(Dataset):
def __init__(self, csv_path, img_dir, transform=None):
"""
初始化函数。
csv_path: CSV 文件路径。
img_dir: 图像文件夹路径。
transform: 可选的图像变换函数。
"""
self.df = pd.read_csv(csv_path)
self.img_dir = img_dir
self.transform = transform
# 保存图像文件名和标签
self.img_names = self.df['filename'].values
self.y = self.df['label'].values
def __len__(self):
"""返回数据集的大小。"""
return self.y.shape[0]
def __getitem__(self, index):
"""根据索引加载并返回一个数据样本(图像和标签)。"""
# 构建图像完整路径
img_path = os.path.join(self.img_dir, self.img_names[index])
# 打开图像
image = Image.open(img_path)
# 应用变换(如果有)
if self.transform is not None:
image = self.transform(image)
# 获取对应标签
label = self.y[index]
return image, label




在上面的代码中:
__init__方法用于初始化,读取 CSV 文件并保存必要的信息。__len__方法返回数据集的样本总数,这对于 DataLoader 确定何时完成一个 epoch 至关重要。__getitem__方法是核心,它根据给定的索引index加载对应的图像和标签。DataLoader会随机生成索引序列来调用此方法,从而实现数据的随机打乱。




第三步:设置数据变换与创建 DataLoader


创建好 Dataset 后,我们就可以用它来实例化 DataLoader。在将图像输入神经网络之前,通常需要进行一些预处理,例如转换为张量(Tensor)和归一化。我们可以使用 torchvision.transforms 来定义这些变换。
from torchvision import transforms


# 定义一个简单的变换:将图像转换为张量(同时自动除以255进行归一化)
transform = transforms.Compose([
transforms.ToTensor()
])


# 创建训练、验证和测试数据集实例
train_dataset = MyDataset(csv_path='mnist_train.csv',
img_dir='mnist_train',
transform=transform)
val_dataset = MyDataset(csv_path='mnist_valid.csv',
img_dir='mnist_valid',
transform=transform)
test_dataset = MyDataset(csv_path='mnist_test.csv',
img_dir='mnist_test',
transform=transform)
现在,我们可以使用这些数据集来创建 DataLoader。
from torch.utils.data import DataLoader

BATCH_SIZE = 32
train_loader = DataLoader(dataset=train_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=0, # 对于MNIST这样的小数据集,设为0以避免潜在问题
drop_last=True)




val_loader = DataLoader(dataset=val_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=0)




test_loader = DataLoader(dataset=test_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=0)





以下是 DataLoader 关键参数的解释:
batch_size: 每个小批量(mini-batch)包含的样本数。shuffle: 是否在每个 epoch 开始时打乱数据顺序(通常训练集设为True,验证和测试集设为False)。num_workers: 用于数据加载的子进程数量。大于 0 时可以利用多 CPU 核心并行预加载下一个批次的数据,提高 GPU 利用率。但对于 MNIST 这类极小的数据集,有时设为 0 可以避免“打开文件过多”的错误。drop_last: 当数据集样本数不能被batch_size整除时,是否丢弃最后一个不完整的小批量。有时丢弃它可以避免最后一个小批量噪声过大影响训练。




我们可以测试一下 DataLoader 是否工作正常。




# 模拟一个训练循环
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
for epoch in range(2):
for batch_idx, (features, targets) in enumerate(train_loader):
features = features.to(device)
targets = targets.to(device)
# 这里应该是你的模型前向传播、计算损失和反向传播的代码
print(f'Epoch: {epoch+1:03d} | Batch: {batch_idx:03d} | Features Shape: {features.shape}')
# 只打印前几个批次
if batch_idx > 2:
break



输出一个批次的形状:
features, targets = next(iter(train_loader))
print(f'一个批次的图像形状: {features.shape}') # 输出: torch.Size([32, 1, 28, 28])
print(f'一个批次的标签形状: {targets.shape}') # 输出: torch.Size([32])
形状 [32, 1, 28, 28] 表示:批量大小 32,1个颜色通道(灰度),图像高 28 像素,宽 28 像素。



第四步:在模型训练中使用自定义 DataLoader
上一节我们创建了可用的数据加载器,本节我们来看看如何将其整合到一个完整的模型训练流程中。为了保持代码整洁和可复用,我将一些通用功能(如训练循环、准确率计算、绘图)写在了单独的辅助脚本中。


首先,我们定义一个简单的多层感知机(MLP)模型。注意,我们使用了 nn.Flatten() 层,它可以自动将输入的 [batch, 1, 28, 28] 形状展平为 [batch, 784],省去了手动重塑的步骤。





import torch.nn as nn


class MultilayerPerceptron(nn.Module):
def __init__(self, num_features, num_hidden, num_classes):
super().__init__()
self.model = nn.Sequential(
nn.Flatten(), # 将 [batch, 1, 28, 28] 展平为 [batch, 784]
nn.Linear(num_features, num_hidden),
nn.Sigmoid(),
nn.Linear(num_hidden, num_classes)
# 注意:这里没有 Softmax,因为 CrossEntropyLoss 内部会处理
)
def forward(self, x):
logits = self.model(x)
return logits




接下来是训练设置和训练循环。我们使用之前创建的 train_loader, val_loader 和 test_loader。


import torch.optim as optim
from helper_train import train_model # 假设训练循环封装在 helper_train.py 中
from helper_evaluate import compute_accuracy # 假设准确率计算封装在 helper_evaluate.py 中

# 超参数设置
RANDOM_SEED = 123
BATCH_SIZE = 32
NUM_EPOCHS = 10
LEARNING_RATE = 0.1



# 设置随机种子以保证结果可复现
torch.manual_seed(RANDOM_SEED)

# 初始化模型、损失函数和优化器
model = MultilayerPerceptron(num_features=28*28, num_hidden=100, num_classes=10)
model = model.to(device)
optimizer = optim.SGD(model.parameters(), lr=LEARNING_RATE)
criterion = nn.CrossEntropyLoss()




# 开始训练
train_losses, val_losses, train_accs, val_accs = train_model(
model=model,
num_epochs=NUM_EPOCHS,
train_loader=train_loader,
val_loader=val_loader,
test_loader=test_loader,
optimizer=optimizer,
criterion=criterion,
device=device
)


训练完成后,我们可以绘制损失和准确率曲线来观察训练过程。





from helper_plotting import plot_training_loss, plot_accuracy




plot_training_loss(train_losses, val_losses)
plot_accuracy(train_accs, val_accs)


最后,我们可以展示一些测试样本的预测结果,直观地查看模型的性能。


from helper_plotting import show_examples




show_examples(model, test_loader, device)
该函数会显示一批测试图像,并在每张图下方标注模型的预测结果(P)和真实标签(T)。




总结



本节课我们一起学习了 PyTorch 中自定义数据加载器的完整流程。





我们首先了解了如何通过继承 torch.utils.data.Dataset 类来创建自定义数据集,核心是实现 __len__ 和 __getitem__ 方法。接着,我们使用 torch.utils.data.DataLoader 将数据集包装起来,以便进行批量加载、打乱和多进程预读取。我们还探讨了 torchvision.transforms 在图像预处理中的应用。




最后,我们将自定义的数据加载器整合到了一个完整的模型训练流程中,包括模型定义、训练循环、评估和可视化。通过将通用代码模块化到辅助脚本中,我们使得主训练代码更加清晰和易于维护。


掌握自定义数据加载器是处理真实世界、非标准数据集的关键一步,它为你使用 PyTorch 进行各种深度学习项目打下了坚实的基础。
深度学习新闻 #6,2021 年 3 月 7 日 📰
在本节课中,我们将学习2021年3月7日发布的深度学习领域最新动态。主要内容包括PyTorch 1.8版本的新特性,以及一篇关于神经网络训练实用技巧的文章。我们将详细介绍这些更新,并探讨如何在实际项目中应用这些技巧来优化模型训练。
PyTorch 1.8 版本发布 🚀
上一节我们介绍了本节课的概述,本节中我们来看看本周深度学习领域最引人注目的新闻——PyTorch 1.8版本的发布。PyTorch大约每半年发布一次新版本,每次都会带来许多实用的新功能和改进。由于本课程也以PyTorch为重点,因此了解这些新特性非常有价值。
PyTorch官方发布了一篇文章,总结了1.8版本的主要亮点。更详细的更改和新增功能列表可以在GitHub上找到,其中包含了约700次提交。
以下是三个我个人认为最值得关注的新特性:

- 对AMD GPU的原生支持:通过名为ROCm的库(类似于NVIDIA的CUDA),现在可以更方便地使用AMD显卡进行深度学习计算。虽然之前也有支持,但需要自行编译PyTorch,过程繁琐。现在,安装程序菜单中直接提供了预编译的二进制文件,使得在Linux系统上的安装更加便捷(目前仅支持Linux,但预计macOS和Windows的支持也会很快跟进)。
- 无需外部库的模型并行训练:上周我们讨论了FairScale和Microsoft DeepSpeed等用于跨多个GPU分布模型的库。PyTorch本身已有
DataParallel和DistributedDataParallel用于数据并行(即分割批次)。新版本引入了更便捷的功能,可以将单个过大的模型拆分到多个GPU上运行,而无需依赖外部库。 torch.linalg线性代数模块的扩展:新增了许多通常只在NumPy中才有的线性代数函数,例如计算行列式或特征值。这减少了我们在训练神经网络时在NumPy和PyTorch之间切换的频率,使工作流程更加顺畅。

注:安装CUDA版本的PyTorch时,它会捆绑自己的CUDA工具包,这简化了安装过程,但也是安装包体积(现在约2.48 GB)较大的原因之一。
深入理解模型并行与流水线执行 🔄
上一节我们提到了PyTorch 1.8对模型并行的支持,本节中我们来深入了解一下其原理,特别是流水线执行技术。
传统的模型并行是将模型的不同层放在不同的GPU上。例如,一个四层网络,每层放在一个独立的GPU上(GPU 0, 1, 2, 3)。前向传播(F)时,数据依次通过各GPU;反向传播(B)时,梯度反向传递。
这种方法的主要缺点是GPU利用率低。如下图所示,在任一时刻,只有一个GPU在工作,其他GPU处于空闲状态(图中曲线下的区域),造成了资源浪费。

改进的方法是使用微批次(Micro-batches)的流水线执行。如下图所示,系统将一个小批次拆分成多个微批次。当第一个微批次在GPU 1上计算时,第二个微批次可以开始在GPU 0上计算,依此类推。这样实现了部分操作的并行,提高了GPU利用率。尽管图中仍有所谓的“气泡(Bubble)”代表空闲时间,但这比传统模型并行效率更高。


更多关于流水线并行的细节,可以参考这篇论文的介绍。
实战:在PyTorch中实现流水线并行 💻
理论介绍完毕,本节我们通过一个代码示例,看看如何在PyTorch 1.8中实际应用流水线并行。我以VGG16网络为例进行演示(虽然它本可以放入单个GPU,但用于演示目的很合适)。
以下是实现的核心步骤:

- 定义模型块:将VGG16网络划分为多个块(Block),每个块包含卷积层、ReLU激活函数和池化层。
# 示例:定义一个块 block1 = nn.Sequential( nn.Conv2d(...), nn.ReLU(), nn.Conv2d(...), nn.ReLU(), nn.MaxPool2d(...) ) - 分配块到GPU:将不同的块放置到不同的GPU设备上。注意,输入数据所在的GPU(通常是GPU 0)应放置第一个块,最终输出也应放回GPU 0以便计算损失。
device0 = torch.device('cuda:0') device2 = torch.device('cuda:2') device3 = torch.device('cuda:3') block1.to(device0) block2.to(device2) block3.to(device3) # ... 分类器层放回 device0 - 组合模型并创建流水线:使用
torch.nn.Sequential组合所有块,然后将其包装进torch.distributed.pipeline.sync.Pipe中。model = nn.Sequential(block1, block2, block3, ...) model = Pipe(model, chunks=8) # chunks 指定微批次数量 - 训练循环中的小修改:使用
Pipe后,模型返回的是一个包含输出的RRef对象,需要获取其本地值。# 在训练循环中 output = model(batch_data) loss = criterion(output.local_value(), labels)

完整的可运行代码示例已分享在GitHub上。在实际测试中,这种方法的训练速度比单GPU训练慢约一倍,但其核心目的不是加速,而是使得训练超出单GPU内存容量的大型模型成为可能,这对于大型模型研究非常有用。
其他新闻:Virus-MNIST 数据集 🦠
除了PyTorch更新,本周在arXiv上还看到了一个有趣的数据集——Virus-MNIST。它旨在为深度学习模型提供一个新颖的基准测试数据。
这个数据集模仿了经典MNIST的格式(10个类别,约数万张训练图像),但内容是关于病毒/恶意软件的。研究人员将恶意软件文件(如.exe)的二进制信息转换为灰度图像(例如,通过中间表示或校验和生成JPEG图像),然后使用卷积网络进行分类。其中一类是“良性软件”(如PuTTY终端程序),其余九类是不同类型的恶意软件。


这种将非图像数据(如代码、文本)转换为图像格式以供卷积网络处理的想法很有创意。虽然不确定这是否是最优表示方法,但论文报告了不错的分类性能,为模型测试提供了一个新的有趣方向。
神经网络训练实用技巧 📝
现在,我们进入本节课的另一个核心部分:一篇题为《给构建复杂神经网络的简单人们的简单考量》的文章。它总结了在构建和调试模型时应考虑的一系列实用技巧,对大家正在进行的课程项目尤其有帮助。
以下是文章要点的梳理和解读:
第一步:深入理解你的数据

在应用任何模型之前,请先彻底审视你的数据。
- 检查标签平衡性:查看各个类别的样本数量是否均衡。
- 审视标注质量:“黄金标签”(即标注的真实值)是否准确?是否存在你不同意的错误标注?
- 了解数据来源:数据是如何收集和标注的?例如,著名的IBM人脸数据集“Diversity in Faces”的许多属性标签是由另一个模型预测生成的,而非人工标注,这可能会限制基于它开发的模型的上限。
- 评估数据多样性:样本是否足够多样,能代表真实场景?
- 构思规则基线:能否想出一个简单的、基于规则的算法(例如,判断垃圾邮件:“如果邮件标题全是大写字母,则标记为垃圾邮件”)?实现这样的基线非常重要,因为你的模型性能必须超越它才有意义。
第二步:评估任务难度
理解数据后,需要评估学习任务本身的难度。
- 建立标准基线:对于分类任务,逻辑回归是一个必须尝试的简单强基线。例如,在MNIST上,一个精心调优的卷积网络可能达到99%的准确率,但逻辑回归也能轻松达到92%以上。如果没有这个基线,92%的准确率可能看起来很高,但有了对比,你才能客观评价神经网络带来的提升。
- 思考随机预测器的表现:
- 对于平衡的C分类问题,一个纯随机猜测的准确率是 1/C。
- 对于不平衡数据,要考虑多数类预测器的表现。例如,一个数据集中80%是垃圾邮件,20%不是。如果一个模型总是预测“垃圾邮件”,它的准确率就能达到80%。如果你的模型只达到82%,那么其提升就非常有限。
- 计算随机预测的损失值:了解一个随机模型的理论损失值有助于调试。例如,对于C分类的交叉熵损失,随机预测(每个类概率为1/C)的理论损失是 -log(1/C)。如果训练时损失值停滞在这个数值附近,说明模型可能没有学到任何东西。
- 选择合适的评估指标:根据任务选择准确率、精确率、召回率、F1分数等。
- 匹配模型架构与数据特性(归纳偏置):根据数据的内在结构选择模型。例如,时间序列数据适合用循环神经网络(RNN),图像数据适合用卷积神经网络(CNN)。
第三步:模型调试技巧
如果你理解了数据和任务,但模型性能仍然不佳(例如,损失值停留在随机预测水平),就需要进行调试。
一个非常有效的技巧是:尝试让模型过拟合一个极小批次的数据。具体做法是,在训练循环中,只使用第一个小批次的数据进行多轮训练,并在此处跳出循环。
for epoch in range(num_epochs):
for batch_idx, (data, target) in enumerate(train_loader):
# 仅用第一个小批次反复训练
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
break # 关键:只训练一个小批次
理论上,模型应该能迅速将这个极小批次的损失降到接近0。如果做不到,很可能代码存在以下问题:

以下是常见的调试检查点:
- 模式设置错误:在训练/评估时忘记切换
model.train()和model.eval()模式(本周讲到Dropout时会详细说明)。 - 梯度未清零:忘记调用
optimizer.zero_grad(),导致梯度累积。 - 输入预处理问题:例如,忘记对输入数据进行标准化。
- 损失函数输入错误:PyTorch的
nn.CrossEntropyLoss接收的是logits(未归一化的分数),而不是经过softmax的概率。 - 权重初始化问题:将所有权重初始化为相同的值(如全0)会破坏对称性,阻碍学习。必须使用随机的小数进行初始化。关于这一点,课程Piazza上有同学做了精彩的解答,推荐阅读。
- 参数未参与前向传播:如果某些模型参数在前向计算中从未被使用,它们就不会被更新。
- 优化器参数错误:在脚本中有多个模型时,容易错误地将优化器指向了另一个模型的参数。
- 学习率异常:学习率被意外设置为0,或者学习率调度器工作不正常。
总结 🎯
本节课中我们一起学习了2021年3月初的深度学习重要进展。
首先,我们探讨了PyTorch 1.8的主要更新:对AMD GPU更便捷的支持、内置的模型并行与流水线执行功能以训练超大模型,以及扩展的线性代数模块。
接着,我们介绍了一个新颖的Virus-MNIST数据集,它通过将恶意软件转换为图像,为模型基准测试提供了新思路。
最后,也是最具实践指导意义的部分,我们系统性地学习了一篇关于神经网络训练与调试技巧的文章。其核心思想可以归纳为三个步骤:1) 深入理解数据,检查质量、平衡性和来源;2) 科学评估任务难度,通过建立随机预测器、多数类预测器和简单模型(如逻辑回归)基线来设定合理的性能期望;3) 有效进行模型调试,特别是通过“过拟合一个小批次”的技巧来快速定位代码中的问题,并逐一排查学习率、梯度、损失函数输入等常见陷阱。



希望这些最新的工具信息和实用的方法论能帮助大家更高效地开展深度学习项目。
课程 P72:L10.0 - 神经网络的正则化方法 🧠
在本节课中,我们将学习如何通过正则化技术来改善神经网络的训练效果,核心目标是减少过拟合。我们将探讨多种方法,包括数据增强、模型复杂度控制、经典的L2正则化以及目前深度学习中最常用的Dropout技术。
概述 📋
上一讲我们介绍了多层感知机,现在你已经能够训练深度神经网络了。这自然引出了如何让神经网络训练得更好、如何得到更优模型的问题。上一讲我们简要提到了过拟合问题,本节课将介绍一些应对过拟合的技术。
首先,我们将讨论如何通过数据增强来扩充训练数据点。结合自定义数据模型,我们可以方便地修改图像数据,例如随机旋转图像,从而廉价地生成新的训练样本,帮助模型对数据集中的微小扰动变得更加鲁棒。
接着,我们将介绍一系列被称为“正则化”的技术。正则化旨在减少过拟合,使模型对数据集的微小扰动不那么敏感。其中一个技术是“早停”,即在训练早期停止。然而,考虑到上一讲解释的“双下降”现象,在实践中可能不再推荐此方法。
我们还将介绍L2正则化,你可能在其他统计学课程中听说过L1和L2正则化,其核心思想是收缩权重。我们将展示如何将这一概念应用于神经网络。
最后,我们将介绍目前最常用和流行的神经网络正则化技术之一——Dropout。在Dropout中,我们随机丢弃神经网络隐藏层中的单元,这有助于防止网络过度依赖特定的神经元,从而使网络更加鲁棒。
当然,还有许多其他技术可以改善神经网络训练,例如不同的权重初始化方案、批归一化以及选择不同的优化器等。我们将在后续课程中讨论这些。本节课介绍的技术主要影响神经网络的结构或权重。
过拟合与偏差-方差分解 📊
本节课的一个主要目标是减少过拟合。在上一讲中,我们简要提到了偏差-方差分解。例如,想象一个回归问题,我们有一个连续目标变量 Y 和一些输入特征 X。模型预测为 Ŷ。
数据背后可能存在一个真实的底层函数 f(x),但在实践中,我们只能访问从一个分布中采样的训练数据集,并且通常存在一些噪声。我们训练模型的目标通常是在训练集上获得低误差或高准确率。
然而,训练集中通常也存在噪声,不能完全代表或捕捉真实的底层数据。我们学到的模型可能过于复杂、过于灵活,导致方差过高。如果我们从同一真实函数分布中收集一个略有不同的数据集,使用相同算法可能会得到非常不同的模型。我们可以将其视为具有较大的方差。
如果我们有无限多个这样的模型并对它们取平均,应该得到接近真实函数 f(x) 的结果。但每个模型本身都具有高方差。
本节课将学习可以降低这种方差的技术。例如,与其学习一个复杂的模型(如之前展示的红线),我们可能学习一个更简单的模型,它具有更低的方法。当然,我们也不希望将模型简化得太多,否则它可能无法很好地拟合训练数据。我们需要找到一个平衡点,在减少方差以降低过拟合的同时,避免使模型过于简单。
正则化:定义与目标 🎯
正则化是一个广义术语,指添加信息或修改目标函数以防止过拟合的过程。例如,L2正则化通常就是我们在损失函数中添加的一个约束项。
广义上说,正则化是指对学习过程进行修改以防止过拟合。有许多不同的技术可以实现正则化效果,其共同目标是减少过拟合。
常用的技术包括早停、L1和L2正则化。在深度学习背景下,L1正则化不那么常用,L2正则化更常见一些,但最常用的技术通常是Dropout,我们将在今天讨论它。
传统的正则化定义是为了解决不适定问题或防止过拟合而添加信息的过程。在机器学习和深度学习领域,这个术语的含义有所演变和调整。有时,人们也将任何旨在降低模型泛化误差(而非训练误差)的学习算法修改都称为正则化。本节课我们主要关注如何降低泛化误差。
课程大纲与核心主题 📝
以下是本节课将涵盖的五个主要主题:
- 改善泛化性能的技术概述:我们将进行一个简短的头脑风暴,列出一些可能对你有用的方法。虽然本课程无法涵盖所有技术,但了解它们可能对你的课程项目有所帮助。
- 通过数据避免过拟合:第一种方法是添加更多数据。第二种方法是修改现有数据,这通常也非常有效。
- 降低模型复杂度:这包括减少网络容量和使用早停技术。早停意味着当我们观察到验证集性能良好,而继续训练会使性能变差时,提前停止训练过程。
- 经典的正则化技术:我们将讨论经典的L2正则化,并简要提及L1正则化(尽管它不那么常用)。这些方法主要通过修改损失函数,添加惩罚项来实现。
- Dropout方法:这可能是目前深度学习中最常用的方法之一。它本质上是一种随机丢弃神经元的技术,我们将看到它如何帮助提升神经网络训练性能和泛化性能。
总结 🏁


本节课我们一起学习了神经网络的正则化方法。我们从过拟合问题和偏差-方差分解入手,理解了正则化的核心目标。我们概述了多种改善泛化的技术,重点探讨了通过数据增强和修改数据来避免过拟合。我们还学习了通过降低模型复杂度(如早停)和修改损失函数(如L2正则化)来进行正则化。最后,我们深入介绍了目前最流行的正则化技术之一——Dropout,它通过随机丢弃神经元来增强网络的鲁棒性。掌握这些技术将帮助你训练出泛化能力更强的神经网络模型。

课程 P73:L10.1 - 减少过拟合的技术 🛡️
在本节课中,我们将学习一系列用于提升模型泛化性能、减少过拟合的技术。我们将从宏观视角了解这些技术,并对其中的核心方法进行简要介绍。
上一节我们讨论了过拟合的概念,本节中我们来看看有哪些具体的技术可以帮助我们应对它。
数据相关技术 📊
以下技术主要涉及对数据集的特征、标签进行修改,或利用新的、不同的数据集。
- 收集更多数据:如果条件允许,收集更多数据通常是提升模型性能最有效的方法之一。更多数据通常有助于模型学习更通用的模式。
- 数据增强:通过修改输入特征来人工扩展数据集。例如,对图像进行旋转、裁剪、翻转等操作,以创建新的训练样本。
- 标签平滑:防止分类器变得过于自信。具体做法是,不使用硬性的0或1标签,而是使用更“软”的版本,例如将标签0替换为0.1,将标签1替换为0.9。这在生成对抗网络和某些分类任务中被证明是有效的。
- 利用未标注数据:
- 半监督学习:在已标注数据上训练分类器,然后将其应用于未标注数据。对于那些分类器预测置信度很高的未标注样本,可以考虑将其预测标签视为真实标签,从而扩大训练集。
- 自监督学习:通过设计一个“前置任务”来利用未标注数据。例如,将一张图像分割成若干小块,然后训练一个网络来预测这些小块的正确排列顺序。这个任务本身不需要人工标注。
- 利用相关数据:
- 元学习:从多个小数据集中学习如何学习,这在少样本学习场景中很常见。另一种定义是学习元数据(关于数据的数据)。
- 迁移学习:在一个大型相关数据集(例如,用于诊断其他肺部疾病的X光图像)上预训练一个模型,然后将该模型微调以适应目标任务的小型数据集(例如,COVID-19胸部X光图像)。这是一种非常实用的技术。
架构与初始化相关技术 🏗️
以下技术涉及如何设计和构建深度神经网络架构。
- 权重初始化策略:采用合适的策略初始化网络权重,对训练过程的稳定性和收敛速度至关重要。
- 激活函数选择:例如,ReLU激活函数有助于缓解梯度消失问题。
- 残差层/跳跃连接:通过在网络中添加跨层连接,可以缓解梯度消失和爆炸问题,并帮助训练更深的网络。
- 知识蒸馏:首先训练一个大型的“教师”网络,然后训练一个较小的“学生”网络来学习模仿教师网络的预测。学生网络可以在教师网络生成的大量(甚至是无限的)预测上进行训练。
归一化相关技术 ⚖️
以下技术涉及对网络内部不同部分的数据进行标准化处理。
- 输入标准化:对输入特征进行标准化处理,使其均值为0,方差为1。
- 批量归一化:不仅标准化网络输入,还对隐藏层的激活值进行归一化,以稳定训练过程。此外还有组归一化、实例归一化、层归一化等变体。
- 权重标准化:对网络权重进行标准化的技术。
- 梯度中心化:对梯度进行归一化,使其均值为0,有助于稳定训练。
训练过程相关技术 🔄
以下技术涉及对训练循环、优化器等过程进行修改。
- 优化器选择:使用自适应学习率优化器(如Adam)等高级优化算法。
- 辅助损失函数:在网络的中间层添加额外的损失函数。例如,Inception网络就采用了这种方法。公式可以表示为:
总损失 = 主输出损失 + Σ (辅助输出损失)
这有助于确保深层网络中的中间层也能得到良好训练。 - 梯度裁剪:为避免梯度爆炸,当梯度值超过某个阈值时,将其裁剪到该最大值。
本课重点技术 🎯
最后,我们将重点介绍本课程中会详细讲解的几种技术,它们也是实践中非常流行的方法。
- L1与L2正则化:通过在损失函数中添加对权重大小的惩罚项,促使网络权重变小,从而降低模型复杂度,减少对噪声的敏感度。L2正则化公式为:
损失 = 原始损失 + λ * Σ (权重²) - 早停法:在训练过程中持续监控模型在验证集上的性能,一旦性能不再提升甚至开始下降,就停止训练,以防止过拟合。
- Dropout:在训练过程中随机“丢弃”(即暂时禁用)网络中的一部分神经元单元。这相当于向网络中添加噪声,是一种有效的正则化手段。在代码中通常体现为一个Dropout层:
torch.nn.Dropout(p=0.5) # 以50%的概率丢弃神经元


本节课中我们一起学习了减少过拟合、提升模型泛化能力的多种技术路线图。我们从数据、架构、归一化和训练过程四个维度进行了梳理,并重点指出了L1/L2正则化、早停法和Dropout等核心方法。理解这些技术的原理和应用场景,将帮助你构建更强大、更稳健的机器学习模型。在接下来的视频中,我们将首先深入探讨如何通过数据增强来扩大数据集。

📊 P74:L10.2 - PyTorch 中的数据增强
在本节课中,我们将学习两种通过优化数据集来提升模型性能的方法:绘制学习曲线以判断是否需要更多数据,以及使用数据增强技术来扩充现有数据。
📈 学习曲线:判断是否需要更多数据
在实践中,提升模型性能的最佳方法之一是在尝试更复杂的模型之前,先专注于数据集本身。一种判断收集更多数据是否有益的技术是绘制学习曲线。
上一节我们介绍了优化数据集的重要性,本节中我们来看看如何通过绘制学习曲线来诊断模型性能。
以下是绘制学习曲线的步骤:
- 使用不同大小的训练子集(例如,从100个样本到3500个样本)训练同一个模型。
- 保持测试集大小不变,评估每个模型在测试集上的准确率。
- 将训练集大小作为横坐标,将训练集和测试集的准确率作为纵坐标,绘制曲线。

通过观察测试集准确率曲线的斜率,可以判断增加数据是否可能带来性能提升。如果测试集准确率随着训练数据增加而持续上升,并且训练集与测试集性能的差距在缩小,那么收集更多数据很可能是有益的。需要注意的是,为了不污染最终评估,建议使用验证集而非测试集来绘制学习曲线。
🔄 数据增强:扩充现有数据

如果收集更多数据成本高昂或不可行,数据增强是一种廉价且有效的替代方案。它通过对现有训练数据进行一系列随机变换(如旋转、缩放、裁剪),生成新的、多样化的训练样本,从而帮助模型学习更鲁棒的特征,而不是记忆具体的像素位置。
上一节我们介绍了如何判断是否需要更多数据,本节中我们来看看如何利用现有数据进行增强。
在PyTorch中,数据增强可以通过 torchvision.transforms 模块方便地实现。以下是一个为MNIST数据集定义训练时数据增强流程的示例:
import torchvision.transforms as transforms


training_transforms = transforms.Compose([
transforms.Resize((32, 32)), # 1. 调整大小
transforms.RandomCrop((28, 28)), # 2. 随机裁剪
transforms.RandomRotation(degrees=30, interpolation=transforms.InterpolationMode.BILINEAR), # 3. 随机旋转
transforms.ToTensor(), # 4. 转换为张量
transforms.Normalize(mean=(0.5,), std=(0.5,)) # 5. 标准化
])

以下是上述代码中每个变换步骤的详细说明:
- 调整大小 (
Resize):将图像从原始尺寸(如28x28)调整为稍大的尺寸(如32x32),为后续裁剪做准备。 - 随机裁剪 (
RandomCrop):从调整后的图像中随机裁剪出目标尺寸(28x28)的区域。这模拟了图像的平移和局部放大效果。 - 随机旋转 (
RandomRotation):将图像在指定角度范围内(如±30度)进行随机旋转,增加模型对物体方向变化的鲁棒性。 - 转换为张量 (
ToTensor):将PIL图像或NumPy数组转换为PyTorch张量,并将像素值从[0, 255]范围缩放到[0.0, 1.0]。 - 标准化 (
Normalize):对张量进行标准化,使其具有零均值和单位方差。对于单通道MNIST图像,使用mean=0.5,std=0.5可将像素值范围从[0,1]变换到[-1,1]。标准化公式为:
normalized_pixel = (pixel - mean) / std

对于测试集,我们需要移除所有随机性以保证评估的一致性,但通常仍需进行中心裁剪和相同的标准化处理。
test_transforms = transforms.Compose([
transforms.Resize((32, 32)),
transforms.CenterCrop((28, 28)), # 使用中心裁剪而非随机裁剪
transforms.ToTensor(),
transforms.Normalize(mean=(0.5,), std=(0.5,))
])
最后,在创建数据集时应用对应的变换:

from torchvision import datasets
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=training_transforms)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=test_transforms)


对于RGB图像(三通道),标准化时需要对每个通道指定均值和标准差,例如 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225](ImageNet数据集统计值)。
🎯 总结


本节课中我们一起学习了两种通过优化数据来提升模型性能的核心方法。首先,我们了解了如何绘制学习曲线来诊断模型是否受益于更多训练数据。其次,我们详细探讨了在PyTorch中实现数据增强的技术,包括如何使用 torchvision.transforms 定义包含随机裁剪、旋转和标准化的增强流程,并区分了训练集和测试集所需的不同变换策略。这些方法是提升模型泛化能力、防止过拟合的有效工具。
深度学习课程 P75:L10.3 - 提前停止 🛑
在本节课中,我们将要学习一种用于处理模型过拟合的技术——提前停止。我们将了解其基本原理、如何实施,以及它在实际应用中的考量。
上一节我们介绍了过拟合的概念以及数据增强等方法。本节中我们来看看另一种策略:提前停止。
概述
提前停止是一种通过监控模型在验证集上的表现来决定何时终止训练的技术。其核心思想是,在模型开始对训练数据过拟合之前停止训练,从而获得泛化能力更好的模型。
数据集划分策略

实施提前停止需要一个验证集。以下是推荐的数据集划分方法:
- 训练集:用于模型训练的主要数据集。
- 验证集:用于模型调优和决定提前停止的时机。这是模型在训练过程中未见过的数据。
- 测试集:仅在最终模型确定后,用于评估其最终性能。理想情况下只使用一次。
验证集本质上用于模型调优,而提前停止正是调优过程的一部分。
提前停止的工作原理

上周我们观察了模型在训练过程中,训练集和验证集上的准确率与损失变化。为了简化说明,这里展示一个理想化的示意图。

提前停止的方法是观察训练集和验证集的性能曲线,并从中推断出停止训练的最佳时机。
验证集用于估计模型的泛化性能。你可能会发现,验证集准确率起初会上升,这意味着泛化性能在提升。但到达某个点后,验证集性能开始下降。这表明继续训练可能会使性能变差。因此,提前停止训练有时能获得更好的模型性能。
实践建议与总结
在实际应用中,提前停止已不再是首选的主流方法。通常建议先尝试L2正则化或更有效的Dropout技术来提升泛化性能。如果使用了这些技术后,发现提前停止仍有帮助,再考虑将其纳入流程。尽管如此,提前停止仍然是一项值得了解的有用技术。
本节课中我们一起学习了提前停止技术。我们了解了其依赖于验证集监控的核心思想,以及它在实际工作流中的定位。下一节,我们将介绍L2正则化,这是一种通过惩罚模型复杂度来防止过拟合的方法。




🧠 课程 P76:L10.4 - 神经网络的 L2 正则化
在本节课中,我们将要学习如何通过 L2 正则化来防止神经网络模型过拟合。我们将了解其核心思想、数学原理、几何解释以及在 PyTorch 中的实现方法。
📌 核心思想:惩罚模型复杂度
上一节我们介绍了模型过拟合的问题,本节中我们来看看如何通过惩罚模型的复杂度来缓解它。

我们可以向损失函数中添加一个惩罚项,这通常被称为正则化。有两种常见的形式:L1 范数和 L2 范数。
想象一个多层感知机,它有一个包含 3 个单元的隐藏层和 2 个输出单元,以及 2 个输入特征。网络是全连接的。
如果其中某个权重(例如 W1)非常大,那么即使只有一个特征拥有大权重,它也会对网络产生巨大影响。当存在许多输入时,一个过大的权重可能会压倒所有其他特征,导致模型对输入中的微小变化非常敏感,从而产生噪声或高方差。
通过添加一个惩罚项来抑制大权重,使权重保持较小且更均匀,可以帮助防止网络输出的剧烈波动。

📐 L2 正则化的数学形式
以下是 L2 正则化的核心公式。我们首先以逻辑回归为例进行说明,其思想可以很容易地推广到神经网络。
对于一个逻辑回归模型,其损失函数(如二元交叉熵)为:
原始损失:J(w) = (1/n) * Σ L(y_i, ŷ_i)
添加 L2 正则化项后,总损失变为:
正则化损失:J_reg(w) = (1/n) * Σ L(y_i, ŷ_i) + (λ / n) * Σ w_j²
Σ w_j²是所有权重平方的和,这就是 L2 范数惩罚。λ是一个超参数,称为正则化强度。λ越大,对权重的惩罚就越强。n是样本数量,用于归一化。
L1 正则化的概念与此类似,只是惩罚项使用的是权重的绝对值之和 Σ |w_j|。

🧩 几何解释:约束优化
我们可以从几何角度理解 L2 正则化。
假设一个简单的线性回归模型有两个权重(忽略偏置项)。损失函数(如均方误差)形成一个曲面,其中心谷底是损失最小值,即我们想要找到的最佳权重组合。
L2 正则化项 λ * Σ w_j² 在权重空间中表现为一个圆(或球体),圆心在原点。这个圆代表了惩罚项的大小:权重离原点越远,惩罚越大。
因此,训练过程变成了一个带约束的优化问题。优化算法(如梯度下降)需要同时最小化原始损失和正则化惩罚。最终找到的权重解会是原始损失曲面和正则化圆环之间的一个折中点,即权重既不会太大,又能较好地拟合数据。
🖼️ 对模型的影响:简化决策边界
在实践中,L2 正则化的效果是使模型的决策边界变得更加平滑和简单。
对于一个具有复杂决策边界的多层感知机,施加 L2 正则化后,其决策边界可能会变得更平滑。如果正则化强度 λ 设置得非常大,模型甚至可能退化为一个接近线性的简单模型。
因此,选择合适的 λ 值至关重要,它需要在模型复杂度和拟合能力之间取得良好平衡。

🔗 推广到神经网络:弗罗贝尼乌斯范数
之前我们讨论了在广义线性模型(如逻辑回归)中如何应用 L2 正则化。那么如何将其推广到具有多层权重矩阵的神经网络呢?

方法非常简单,我们使用弗罗贝尼乌斯范数。对于神经网络的某一层权重矩阵 W^[l],其 L2 正则化项是该矩阵所有元素平方的和。
对于整个神经网络,总的正则化损失函数为:
J_reg = (1/n) * Σ L(y_i, ŷ_i) + (λ / n) * Σ_l ||W^[l]||_F²
其中,||W^[l]||_F² 表示第 l 层权重矩阵的弗罗贝尼乌斯范数的平方。你可以为每一层设置不同的 λ,但通常对所有层使用相同的值。
⚙️ 训练与梯度计算

既然我们修改了损失函数,那么在使用梯度下降法训练时该如何操作呢?这实际上非常直接。
根据求导的加法法则,正则化损失对权重的梯度等于原始损失梯度与正则化项梯度之和。

对于权重 w_ij,正则化项的梯度计算如下:
∂/∂w_ij [ (λ / n) * Σ w_j² ] = (2λ / n) * w_ij

因此,在权重更新时,我们不仅会按照原始损失梯度的方向调整权重,还会额外施加一个将权重向零缩小的力,这就是“权重衰减”名称的由来。

💻 在 PyTorch 中的实现
在 PyTorch 中实现 L2 正则化非常简便。主要有两种方式:
方法一:手动添加到损失函数
在训练循环中,遍历模型的所有参数,筛选出权重(通常忽略偏置项),计算其平方和并加到损失中。
l2_lambda = 0.01
l2_reg = torch.tensor(0.)
for name, param in model.named_parameters():
if ‘weight‘ in name:
l2_reg += torch.norm(param, p=2) # 计算L2范数的平方
loss = original_loss + l2_lambda * l2_reg

方法二:使用优化器的 weight_decay 参数(推荐)

这是更简单且常用的方法。在定义优化器(如 SGD 或 Adam)时,直接指定 weight_decay 参数,其值即为正则化强度 λ。
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=0.01)
优化器会在内部自动完成 L2 正则化的梯度计算和权重更新。


📝 总结


本节课中我们一起学习了神经网络中 L2 正则化的核心知识。
我们了解到,L2 正则化通过在损失函数中添加所有权重平方和作为惩罚项,来抑制过大的权重值,从而降低模型复杂度、缓解过拟合。我们从数学公式和几何视角理解了其工作原理,并掌握了将其从简单模型推广到多层神经网络的方法。最后,我们学习了在 PyTorch 中实现 L2 正则化的两种实用技巧,其中使用优化器的 weight_decay 参数是最为便捷的方式。

虽然 L2 正则化是控制模型复杂度的经典技术,但在现代深度学习中,像 Dropout 这样的方法可能更为常见。在接下来的课程中,我们将继续探讨其他有效的正则化技术。
课程 P77:L10.5.1 - Dropout 背后的主要概念 🧠
在本节课中,我们将要学习 Dropout 这一神经网络中用于防止过拟合的重要技术。我们将从核心概念开始,逐步理解其工作原理、实现方式以及背后的理论解释。

概述

Dropout 是一种专门为神经网络设计的正则化方法。它的核心思想是在训练过程中随机“丢弃”网络中的一部分神经元。本节将首先介绍 Dropout 的基本概念和直观操作。
Dropout 的核心概念
上一节我们介绍了神经网络训练中的过拟合问题,本节中我们来看看 Dropout 如何提供一种独特的解决方案。

Dropout 的主要概念是在训练阶段,以前向传播的每次迭代为单位,随机地将网络隐藏层中的一部分神经元临时“关闭”或“丢弃”。
假设我们有一个多层感知机。Dropout 在训练时,会以设定的概率 p(例如 0.5,即 50%)随机选择每个神经元,并将其在此次前向传播和反向传播中暂时移除。
这意味着每次训练迭代时,网络结构都会略有不同,因为被丢弃的神经元组合是随机的。一次迭代可能丢弃神经元 A,下一次则可能丢弃神经元 B 和 C。
如何高效实现 Dropout
理解了基本概念后,我们自然会问:如何在代码中高效实现它,而不必真正修改网络架构?
一种高效的方式是使用伯努利采样。我们可以通过一个与隐藏层大小相同的随机掩码向量来实现。

以下是实现步骤的简要描述:
- 定义一个丢弃概率
p。 - 生成一个与隐藏层大小相同的随机向量,其元素值在 [0, 1) 区间均匀分布。
- 将该随机向量转换为二进制掩码:将小于
p的值设为 0(丢弃),大于等于p的值设为 1(保留)。 - 将隐藏层的激活值与该二进制掩码逐元素相乘,即可实现随机丢弃的效果。
用伪代码表示如下:
# p: 丢弃概率
# a: 隐藏层激活值向量
mask = (np.random.rand(*a.shape) > p).astype(float) # 生成二进制掩码
a_dropped = a * mask # 应用Dropout
推理阶段的调整
Dropout 仅用于训练阶段。在推理(测试)时,我们使用完整的网络,不进行任何随机丢弃。但这会带来一个问题:训练时由于部分神经元被丢弃,激活值的总强度被削弱;而测试时所有神经元都参与,激活值会更强。
为了保证训练和测试时激活值的尺度一致,需要在测试时对激活值进行缩放。一个简单的做法是将测试时的激活值乘以 (1 - p),即保留概率。
例如,训练时某个神经元有 4 个输入,丢弃概率 p=0.5,平均只有 2 个输入是活跃的。测试时,4 个输入全部活跃,其净输入是训练时的 2 倍。因此,在测试时将该神经元的激活值乘以 (1 - 0.5) = 0.5,即可与训练时的尺度匹配。
后续内容预告
以上我们介绍了 Dropout 的基本操作和实现。在接下来的视频中,我们将深入探讨 Dropout 为何有效的两种理论解释:
- 防止特征检测器共适应:Dropout 可以打破神经元之间的复杂依赖,迫使每个神经元都能独立产生有用的特征。
- 集成学习解释:每次前向传播都相当于训练了一个不同的“子网络”,而 Dropout 训练过程可以看作是在同时训练大量子网络的集成,测试时则近似于对这些子网络预测结果进行平均。
最后,我们还将学习如何在 PyTorch 中实现 Dropout,并了解一种称为“反向 Dropout”的实用变体,它通过调整训练时的计算来简化推理过程。

总结

本节课中我们一起学习了 Dropout 正则化技术。我们了解到,Dropout 通过在训练时随机丢弃神经元来防止过拟合,其实现依赖于伯努利采样生成的二进制掩码。同时,为了确保训练和测试阶段行为的一致性,需要在测试时对神经元的激活值进行缩放。Dropout 是一种强大而直观的技术,为提升神经网络泛化能力提供了有效工具。
深度学习课程 P78:L10.5.2 - Dropout 共适应解释 🧠
在本节课中,我们将要学习 Dropout 正则化技术的一种重要理论解释——共适应理论。我们将探讨 Dropout 如何通过防止神经元之间的过度依赖来提升神经网络的泛化能力。

上一节我们介绍了 Dropout 的基本概念,本节中我们来看看 Dropout 在实践中为何有效,即它如何帮助模型更好地泛化到新数据(例如测试集数据)。一种解释是共适应理论,我们将在本节简要讨论它。
共适应解释的核心观点是:Dropout 会促使网络学习不过度依赖特定的连接。
这意味着,在一个全连接的多层感知机中,所有神经元通常都彼此相连。在训练过程中,Dropout 会随机地“丢弃”一些神经元。例如,如果某个神经元在训练时被随机屏蔽,网络就必须更多地依赖其他神经元来完成计算。反之亦然。
由于在训练过程中,被屏蔽的神经元组合总是在变化(可能是一个、多个,甚至没有神经元被屏蔽),网络无法过度依赖任何一个特定的神经元。这种方式会促使网络更均衡地考虑所有神经元。
这可能导致权重分布更加分散。网络会尝试将权重分配到所有神经元上,而不是让某个神经元的权重过大而其他神经元的权重过小。因为网络永远无法确保那个特定神经元一定会存在。
因此,从效果上看,Dropout 可能与 L2 正则化 有相似之处,都能使权重分布更均匀,而不是高度集中于少数神经元。权重分布可以表示为对权重向量的约束:
公式: L2 正则化项 = λ * ||w||²
一个有趣但尚未被广泛验证的想法是:可以绘制每层权重的分布图来观察这种效果。例如,在 Y 轴表示权重大小,X 轴表示权重索引。在应用 Dropout 之前,权重分布可能不均匀;而在应用 Dropout 之后,权重分布可能会变得更加均匀。这值得进一步探究。
此外,需要补充的是,你可以为不同的网络层设置不同的 Dropout 概率。以下是设置不同丢弃率的代码示例:
代码:
# 示例:为不同层设置不同的 Dropout 率
model.add(Dense(128, activation='relu'))
model.add(Dropout(rate=0.5)) # 隐藏层1使用50%丢弃率
model.add(Dense(64, activation='relu'))
model.add(Dropout(rate=0.2)) # 隐藏层2使用20%丢弃率
你不必对每个隐藏层都使用 50% 的丢弃率。你可以使用 20%、80% 等不同的值。这也是一个需要选择的超参数。如何选择取决于具体的实验。50% 是原始论文中使用的值,但现在人们也常使用 20% 或 80%,这需要通过实验来确定。
总之,Dropout 之所以表现良好,一种解释是它防止了网络对特定节点的过度强调,从而可能使网络对输入波动更加鲁棒。


本节课中我们一起学习了 Dropout 的共适应理论解释。我们了解到,Dropout 通过随机屏蔽神经元,迫使网络学习更均衡、更分散的特征表示,这有助于提升模型的泛化能力。在下一节视频中,我们将简要讨论关于 Dropout 的另一种解释——集成方法解释。
课程 P79:L10.5.3 - (选修) Dropout 的集成方法解释 🧠
在本节课中,我们将要学习 Dropout 技术为何有效的另一种解释。我们将从集成方法的角度来理解 Dropout,并探讨它如何通过近似模型集成来提升神经网络的性能。
上一节我们介绍了 Dropout 的基本概念,本节中我们来看看 Dropout 与集成方法之间的联系。
集成方法的核心思想是组合多个模型,并对它们的预测结果进行平均。直观地理解,这就像在做重要决策时咨询多位专家,而非仅依赖一位专家的意见。通常,综合多位专家的共识会比单一个人的意见更可靠。
以下是集成方法的几个常见例子:
- 多数投票
- Bagging
- 随机森林
- Boosting
然而,在深度学习中,我们通常专注于训练单个模型,因为训练多个模型的成本非常高昂。

那么,为什么可以将 Dropout 视为一种集成方法呢?
在 Dropout 的训练过程中,每个前向传播都会随机丢弃一部分神经元。这意味着每个小批量数据所“看到”的神经网络结构都略有不同。
从本质上讲,Dropout 可以被看作是在采样大量不同的子网络模型。对于一个具有 H 个隐藏单元的层,可能的子网络组合数量高达 2^H 个。因此,Dropout 可以被视为一种模型集成。
但这种集成有一个关键限制:时间维度。我们并非同时并行地使用所有这些子网络,而是在训练循环中依次使用它们。第一个小批量使用一个子网络,更新权重后,下一个小批量使用另一个子网络,并基于更新后的权重继续训练。
这种限制可以看作是一种权重共享机制。不同的子网络在训练过程中共享并迭代更新同一组权重参数。这种权重共享本身也是一种正则化约束,为模型添加了额外的信息。
现在,让我们进行一个思想实验。假设在模型训练完成后,我们真的为推理阶段创建了所有 M 个可能的子网络模型。
为了结合这些模型的预测,一个简单的方法是平均它们的输出。对于二分类问题,这相当于计算所有模型预测似然的几何平均值。

公式表示如下:
对于测试数据点 i,其综合概率 P_i 可以计算为所有 M 个模型预测概率乘积的 1/M 次方:
P_i = (∏_{m=1}^{M} p_m)^{1/M}
取对数后,等价于对对数似然求算术平均:
log(P_i) = (1/M) * Σ_{m=1}^{M} log(p_m)
对于多分类问题,我们还需要对结果进行归一化,使得所有类别的概率之和为 1,然后取概率最高的类别作为预测标签。
然而,上述方法在实际中并不可行。即使对于一个只有 10 个隐藏单元的层,也需要处理 2^10 个模型,计算成本过高。

那么,我们如何解决这个问题呢?
实际上,我们之前讨论的标准 Dropout 技术已经近似地计算了这个几何平均。在训练完成后,我们在测试阶段对神经元的激活值乘以缩放因子 1/(1-p)(其中 p 是丢弃概率)。这个操作正是对集成模型中几何平均的一种近似。
根据原始的 Dropout 论文论证,对于一个线性模型,这种缩放操作得到的结果与计算所有子网络几何平均的结果是精确等价的。因此,Dropout 可以被解释为一种高效且计算可行的模型集成近似方法。

本节课中我们一起学习了 Dropout 的集成方法解释。我们了解到 Dropout 可以被视为在训练过程中采样大量子网络,并通过权重共享机制进行近似集成。在推理阶段,通过对激活值进行缩放,我们能够近似地获得这些子网络预测的几何平均,从而以较低的计算成本获得了集成方法带来的鲁棒性提升。这就是 Dropout 为何能够有效防止过拟合、提升模型泛化能力的一种理论解释。
📚 课程 P8:L1.3.4 - 机器学习的广泛类别第 4 部分:监督学习的特殊案例
在本节课中,我们将要学习监督学习中的两个特殊案例:半监督学习和自监督学习。它们是机器学习领域非常有趣且重要的研究方向,尤其在大数据时代,为解决标注数据稀缺的问题提供了有效思路。
上一节我们介绍了机器学习的三大主要类别:监督学习、无监督学习和强化学习。本节中,我们来看看监督学习框架下的两个特殊变体。

🔄 半监督学习
半监督学习是监督学习和无监督学习的混合体。在这种场景下,我们拥有部分训练样本的标签,但另一部分样本没有标签。
半监督学习的核心思想是:利用已标注的训练数据子集,来为未标注部分的训练数据生成标签,从而帮助我们构建一个更好的决策边界。
以下是一个简单的图示示例:


在左侧,我们有一个严格意义上的监督学习数据集,只有两个类别(类别1和类别2)。我们据此绘制了一条决策边界。
现在,考虑一个情况:我们拥有更多数据,但这些数据是未标注的。如右侧所示,蓝色圆点代表未标注的数据点。
一个半监督学习系统在考虑了这些未标注数据点后,可能会推导出一条略有不同的决策边界。虽然类别1和类别2示例的位置没有改变,但系统可以利用这些未标注数据做出假设(例如,假设某个蓝色点属于菱形类别1),从而将决策边界调整得更像一条对角线。

即使假设不一定完全正确,我们仍然可以利用这些数据来改进系统。在这种情况下,新的决策边界可能略有不同,并且可能是一个更好的边界。
🤖 自监督学习
自监督学习是当前深度学习研究中一个非常热门和前沿的领域。它对于训练需要大量数据的大型模型特别有用,尤其是在我们无法获取海量标注数据的情况下。
自监督学习通常涉及在一个大型数据集上预训练一个模型,然后在一个较小的目标任务数据集上对其进行微调。这个过程有时也被称为迁移学习,因为我们将模型从一个任务的知识迁移到另一个任务上。
它之所以被称为“自监督”,是因为我们直接从数据本身推导或利用标签信息,而不是依赖人工进行标注。
以下是自监督学习的两个典型例子:
1. 自然语言处理示例(如BERT模型)
假设你想构建一个垃圾邮件分类系统,但只有10万封标注邮件,这对于一个参数众多的大型模型来说数据量不足。
你可以先在互联网的所有文本上预训练模型。如何获得这些文本的“标签”呢?你可以从数据集中自行构造标注任务。
- 掩码语言建模:研究人员会随机掩盖(例如15%)文本中的单词,然后让系统预测这些被掩盖的单词。这样,你就利用数据本身(被掩盖单词的正确答案)作为监督信号。
- 下一句预测:给系统两个句子,让它预测第二个句子是否是第一个句子的原文后续。通过从不同文档组合句子,可以自行构造这个分类任务。

通过这些任务,模型学会了理解语言。之后,你可以将这个预训练好的系统,应用到较小的垃圾邮件分类数据集上进行微调,从而完成最终的目标任务。

2. 计算机视觉示例
假设你想训练一个猫狗分类器,但标注数据很少。你可以利用未标注的图像进行自监督预训练。
例如,你可以将一张图像分割成9个部分(3x3网格)。然后,你拿走其中一部分(比如第9块),并向系统呈现另一部分(比如第7块),让系统预测被拿走的那个部分在原图中的位置(是位置1、位置5还是位置3?)。
你通过一个简单的Python算法进行图像分割,因此你知道所有“块”的正确位置标签。通过让模型学习拼图,它必须理解图像中物体的结构(比如猫的耳朵、眼睛的位置),从而学会识别物体。
完成这种预训练后,你就可以将网络应用到较小的目标数据集(如动物分类)上进行微调。

自监督学习是一个前沿话题,我们可能会在本课程末尾关于Transformer的部分再次简要提及。对于这门入门课程来说,它算是一个稍微进阶的主题,在此介绍是为了让你知道它的存在,或许你可以在课程项目中探索它。
📝 总结与预告

本节课中,我们一起学习了监督学习的两个特殊案例:半监督学习和自监督学习。它们扩展了监督学习的边界,使其在数据标注不完全或稀缺的情况下仍能有效工作。
至此,我们已经涵盖了机器学习的所有主要广泛类别。
在接下来的课程中,我们将更深入地探讨监督学习的工作流程,详细介绍训练一个监督学习系统的不同步骤。同时,我会介绍一些相关的数学符号和术语(例如如何表示训练集、损失函数等),并简要介绍我们将要使用的主要工具PyTorch。这些内容都只是概述,如果有些细节目前看起来复杂,请不要担心,我们会在后续课程中更缓慢、细致地展开讲解。
下一讲,我们将聚焦于监督学习的工作流程。



课程 P80:L10.5.4 - PyTorch 中的 Dropout 🧠
在本节课中,我们将学习如何在 PyTorch 中实际实现和应用 Dropout 技术。我们将从理解“反转 Dropout”的概念开始,然后通过代码示例展示如何在神经网络中添加 Dropout 层,并讨论相关的实践技巧。
反转 Dropout 的概念
上一节我们介绍了 Dropout 的基本原理,本节中我们来看看它在现代框架中的具体实现方式,即“反转 Dropout”。
在早期的常规 Dropout 中,我们在训练时随机丢弃神经元,而在测试时,为了补偿训练时因丢弃神经元而导致的激活值期望降低,我们需要将激活值按 1 - p 的比例进行缩放,其中 p 是丢弃概率。
公式: 测试时缩放因子 = 1 - p
然而,在反转 Dropout 中,我们改变了这一操作的时间点。我们在训练时就对保留的激活值进行缩放(同样乘以 1 - p),而在测试时则无需进行任何缩放操作。PyTorch 等现代框架采用的都是反转 Dropout。
这样做的主要优势在于,它节省了测试或推理阶段的计算资源。对于需要处理海量预测请求的服务(如搜索引擎),避免在每次预测时都进行缩放操作可以显著减少计算开销。无论是常规 Dropout 还是反转 Dropout,最终效果是等价的,区别仅在于缩放操作是在训练阶段还是测试阶段执行。

在 PyTorch 中使用 Dropout
理解了反转 Dropout 后,我们现在来看看如何在 PyTorch 模型中具体添加 Dropout 层。
以下是一个包含两个隐藏层的多层感知机示例代码片段,展示了如何在激活函数后添加 Dropout 层:
import torch.nn as nn
class MLPWithDropout(nn.Module):
def __init__(self, input_size, hidden_size1, hidden_size2, output_size, drop_prob):
super(MLPWithDropout, self).__init__()
self.model = nn.Sequential(
nn.Linear(input_size, hidden_size1),
nn.ReLU(),
nn.Dropout(drop_prob), # 在第一个隐藏层后添加 Dropout
nn.Linear(hidden_size1, hidden_size2),
nn.ReLU(),
nn.Dropout(drop_prob), # 在第二个隐藏层后添加 Dropout
nn.Linear(hidden_size2, output_size)
)
以下是关于代码的一些关键说明:
- 放置位置:通常将
nn.Dropout放在激活函数(如nn.ReLU)之后。对于 ReLU,放在之前或之后效果相同,因为输入为 0 时输出也为 0。但对于像 Sigmoid 这样的激活函数,输入为 0 时输出为 0.5,因此将 Dropout 放在激活之后是更一致和稳妥的做法。 - 输出层:我们通常不对输出层使用 Dropout,因为丢弃代表类别标签的神经元没有意义。
- 丢弃概率:
drop_prob是一个可以调节的超参数。不同层可以使用不同的丢弃概率。

训练与评估模式的切换

在 PyTorch 中使用 Dropout 时,一个至关重要的步骤是正确切换模型的模式。


我们必须使用 model.train() 和 model.eval() 来明确告知模型当前是处于训练模式还是评估(测试)模式。这是因为 nn.Dropout 层的行为会随模式不同而改变:
- 在训练模式 (
model.train()) 下,Dropout 层会按照设定的概率随机丢弃神经元。 - 在评估模式 (
model.eval()) 下,Dropout 层会失效(相当于丢弃概率为 0),所有神经元都会参与前向传播。
这是一个良好的编程实践,即使不使用 Dropout,也建议在训练和评估时进行模式切换,以避免其他具有不同训练/评估行为的层(如 BatchNorm)出现意外结果。




实践效果与技巧
我们通过一个在 MNIST 数据集上的实验来观察 Dropout 的效果。一个没有使用 Dropout 的模型在训练后期出现了明显的过拟合现象(训练准确率持续上升,但验证准确率停滞不前)。而添加了 50% Dropout 的模型有效地减少了过拟合,训练曲线和验证曲线更加接近。
基于此,我们可以总结出一些实践技巧:


- 何时使用:如果模型没有出现过拟合,则没有必要使用 Dropout。建议先训练一个基线模型观察其表现。
- 模型容量:Dropout 的提出者建议,如果模型未过拟合,可以尝试增加模型容量(更多参数),使其先有过拟合的倾向,然后再加入 Dropout 来进行正则化。这通常比直接使用一个不会过拟合的小模型性能更好。
- 超参数调整:示例中 50% 的丢弃概率可能过高,虽然抑制了过拟合,但也可能导致最终性能略有下降。需要根据任务调整合适的丢弃概率。



相关概念:DropConnect
在听课时你可能已经想到一个相关的思路:既然可以随机丢弃激活值,那么是否可以随机丢弃权重呢?

这个想法已经被研究并称为 DropConnect。它可以看作是 Dropout 的一种泛化。在 DropConnect 中,随机被设置为零的是层之间的权重,而不是神经元的输出。如果将一个神经元的所有输入权重都丢弃,其效果就等同于丢弃该神经元(即 Dropout)。
尽管这是一个有趣的想法,但在实践中,DropConnect 并不像 Dropout 那样常用和有效,大多数情况下 Dropout 仍然是首选的 regularization 技术。

总结与延伸阅读
本节课中我们一起学习了如何在 PyTorch 中实现和使用 Dropout。

- 我们了解了反转 Dropout 的概念及其优势。
- 我们掌握了使用
nn.Dropout模块在神经网络中添加 Dropout 层的代码实现,并注意了其放置位置和模式切换。 - 我们讨论了 Dropout 的实践效果,包括其抑制过拟合的能力,以及何时使用、如何与模型容量配合等实用技巧。
- 我们简要了解了与 Dropout 相关的 DropConnect 概念。
如果你想深入了解,推荐阅读原始的 Dropout 研究论文。它行文直观,易于理解,非常适合作为阅读科研论文的入门材料。

在接下来的课程中,我们将探讨权重初始化技术、学习率设置以及不同的优化器,这些都是提升神经网络训练效果的重要方法。
课程 P81:L11.0 - 输入归一化与权重初始化 🧠
在本节课中,我们将学习如何通过输入归一化和权重初始化技术来改善深度神经网络的训练效果与泛化性能。首先,我们会介绍输入归一化的基本概念,然后深入探讨批量归一化(Batch Normalization)的原理与实现。接着,我们将讨论不同的权重初始化策略,并解释它们为何在实践中至关重要。
输入归一化 📊
上一节我们概述了本节课的主要内容,现在让我们首先看看输入归一化。输入归一化是一种预处理技术,旨在将输入数据调整到相似的尺度范围内。
以下是进行输入归一化的常见步骤:
- 计算训练数据集中每个特征维度(例如,图像中的每个像素位置)的均值。
- 计算训练数据集中每个特征维度的标准差。
- 使用以下公式对每个数据样本进行归一化:
x_normalized = (x - mean) / std
这种处理有助于加速训练过程的收敛,因为优化算法(如梯度下降)在不同尺度特征上的更新步长会更加均衡。
批量归一化(Batch Norm)⚙️
了解了基础的输入归一化后,本节中我们来看看一种更强大的技术——批量归一化。批量归一化不仅对输入层有效,更常用于神经网络中的隐藏层,以稳定和加速深度网络的训练。
在PyTorch中,实现批量归一化非常简单。以下是一个示例代码片段:
import torch.nn as nn
# 为具有100个输入特征的线性层添加批量归一化
batch_norm_layer = nn.BatchNorm1d(100)
# 在网络的前向传播中使用
# normalized_output = batch_norm_layer(linear_layer_output)
批量归一化为何有效?🤔
我们已经介绍了批量归一化的操作方法,现在我们来探讨它为何能提升训练效果。其背后的理论仍在讨论中,但主要有以下几种观点:
- 减少内部协变量偏移:该理论认为,网络中间层输入的分布会随着前面层参数的更新而不断变化,这给训练带来困难。批量归一化通过固定每层输入的均值和方差来缓解这个问题。
- 平滑优化地形:有研究表明,批量归一化能使损失函数对参数的变化更加平滑,从而允许使用更大的学习率,并使得优化过程更稳定。
- 轻微的正则化效果:由于批量归一化在训练时使用小批量的统计量(均值、方差)进行归一化,这为模型注入了一些噪声,可能起到了类似Dropout的正则化作用,有助于防止过拟合。

权重初始化策略 🔑
在讨论了归一化技术之后,我们转向另一个关键主题:权重初始化。恰当的权重初始化对于深度网络能否成功训练至关重要,它能帮助缓解梯度消失或爆炸等问题。
以下是两种广泛使用的权重初始化方法:
-
Xavier/Glorot 初始化:这种方法旨在使每一层输出的方差尽可能保持一致。它通常与tanh或Sigmoid激活函数搭配使用。对于一个线性层,其公式为:
W ~ Uniform(-sqrt(6/(fan_in + fan_out)), sqrt(6/(fan_in + fan_out)))
其中fan_in和fan_out分别是该层的输入和输出单元数。 -
Kaiming/He 初始化:这种方法专门为ReLU及其变体(如Leaky ReLU)激活函数设计。它通过考虑ReLU激活函数的特性来调整方差。对于使用ReLU的网络,初始化公式为:
W ~ Normal(0, sqrt(2 / fan_in))
在PyTorch中,我们可以方便地应用这些初始化:
import torch.nn as nn
import torch.nn.init as init
def weights_init(m):
if isinstance(m, nn.Linear):
# 应用 Xavier 初始化
init.xavier_uniform_(m.weight)
# 或应用 Kaiming 初始化
# init.kaiming_uniform_(m.weight, mode='fan_in', nonlinearity='relu')
if m.bias is not None:
init.constant_(m.bias, 0)
# 将初始化函数应用到模型
model.apply(weights_init)
总结 📝

本节课中,我们一起学习了改善神经网络训练的两组重要技术。首先,我们探讨了输入归一化及其进阶版——批量归一化,了解了它们的操作方式、在PyTorch中的实现以及可能的工作原理。随后,我们讨论了权重初始化的必要性,并介绍了Xavier和Kaiming这两种针对不同激活函数的初始化策略及其代码实现。掌握这些技巧将帮助你更有效地训练深度神经网络。在下一讲中,我们将继续学习优化算法来进一步提升梯度下降的学习过程。

📊 课程 P82:L11.1 - 输入规范化
在本节课中,我们将学习输入规范化的概念,了解它如何帮助改善梯度下降算法的性能,并简要介绍其在深度神经网络中的应用背景。
为什么需要规范化输入?
上一节我们介绍了梯度下降的基本原理,本节中我们来看看为什么输入特征的尺度差异会影响其性能。
假设我们有一个简单的凸损失函数曲面。在深度神经网络中,损失函数通常并非凸函数,但为了简化说明,我们以此为例。如果我们使用梯度下降法,更新权重的公式通常涉及输入特征本身。例如,权重更新公式为:
w = w - α * ∂L/∂w
其中,∂L/∂w 的计算通常包含输入特征 x。如果输入特征处于不同的尺度(例如,年龄范围是10-50岁,而收入范围是10,000-500,000美元),那么为所有权重找到一个合适的学习率 α 将变得非常困难。尺度大的特征可能需要更小的学习率,否则优化过程容易在某个维度上“超调”,导致训练不稳定,出现锯齿状的优化路径。
此外,大多数优化算法在输入特征以零为中心且数值较小时表现更好。大数值还可能引发计算上的不稳定性问题。
标准化:Z-Score 方法
为了解决上述问题,实践中一个常用且有效的方法是 Z-Score 标准化(或称标准化)。此方法旨在使数据具备标准正态分布的特性,即均值为0,方差为1(标准差为1)。需要注意的是,如果原始数据本身不服从正态分布,此方法不会改变其分布形状,只会调整其均值和尺度。
以下是标准化的具体操作步骤,针对每个特征单独进行:
对于一个特定的数据点 i 的某个特征 j(例如,鸢尾花数据集中第 i 朵花的“花萼长度”),我们按以下公式进行转换:
x_norm[i][j] = (x[i][j] - μ[j]) / σ[j]
其中:
μ[j]是特征j在所有样本上的均值。σ[j]是特征j在所有样本上的标准差。
通过这种处理,我们通常能获得一个更对称、行为更良好的损失曲面,从而有助于找到更优的学习率,并使训练过程更加稳定。
从输入到隐藏层激活值


我们刚刚讨论了如何标准化神经网络的输入层特征。现在,让我们将视角转向网络的内部。
考虑一个简单的深度神经网络,它具有输入 x1、x2,隐藏层激活值 a1、a2、a3,以及输出 o1、o2。对于后续的层(例如输出层)而言,前一层的激活值 a1、a2、a3 就相当于它的“输入”。
因此,一个自然的想法是:标准化隐藏层的激活值 可能同样有益,因为这可以确保每一层接收到的输入都处于相对稳定和规范的尺度上,从而缓解深度网络训练中的“内部协变量偏移”等问题。

预告:批标准化 (Batch Normalization)
在下一节视频中,我们将深入探讨一种专门用于标准化隐藏层激活值的技术——批标准化 (Batch Normalization)。
你可以将其视为隐藏层激活值标准化的一个变体,但它引入了一个关键的“转折”:它包含了可学习的参数(缩放参数 γ 和平移参数 β),使得网络在标准化的同时,仍能保留原有的表达能力。这将是我们接下来要学习的重点内容。


本节课中我们一起学习了输入规范化的重要性及其实现方法(Z-Score标准化),并理解了将规范化思想延伸到神经网络隐藏层的动机,为学习更高级的规范化技术(如批标准化)奠定了基础。
📚 课程 P83:L11.2 - BatchNorm 的工作原理

在本节课中,我们将学习一种名为 批量归一化 的技术。我们将了解它如何将输入归一化的思想扩展到神经网络的隐藏层,其工作原理,以及它为何能帮助提升训练过程的稳定性和速度。

🧠 什么是批量归一化?
上一节我们讨论了输入数据的标准化。本节中,我们来看看如何将类似的思想应用到网络的内部。
批量归一化技术源于2015年的一篇论文,其核心思想是减少训练过程中网络内部特征的分布变化。这种内部特征分布的变化,有时被称为“内部协变量偏移”,可能导致梯度爆炸或消失等问题。

批量归一化通过在网络的每个隐藏层前增加一个归一化层来解决这个问题。它有助于稳定梯度,使训练过程更平滑,并通常能加快网络的收敛速度。你可以将其视为一个额外的、带有可学习参数的归一化层。

🔍 批量归一化的工作原理
接下来,我们将逐步拆解批量归一化的具体操作流程。为了便于理解,我们假设有一个多层感知机,并聚焦于第二个隐藏层中某个神经元的净输入。

假设我们有一个小批量数据,对于该层中第 i 个训练样本的净输入,我们记为 z_i。为了简化表示,我们在接下来的讨论中暂时省略层索引。
第一步:归一化净输入

批量归一化的第一步是对净输入进行标准化处理,这与之前讨论的输入标准化原理相似,但对象是隐藏层的输入。

以下是计算步骤:
- 计算均值:针对当前小批量数据,计算该层每个特征(即来自前一层的所有激活值)的均值。
μ_j = (1/m) * Σ_{i=1}^{m} z_{i,j}

- 计算方差:计算每个特征的方差。
σ_j^2 = (1/m) * Σ_{i=1}^{m} (z_{i,j} - μ_j)^2

- 标准化:使用计算出的均值和方差对每个特征进行标准化。
ẑ_{i,j} = (z_{i,j} - μ_j) / √(σ_j^2 + ε)

请注意,在方差项中我们添加了一个很小的常数 ε(例如 1e-5)。这是一个用于数值稳定性的技巧,目的是防止方差为零时出现除以零的错误。

第二步:缩放与偏移
第一步的标准化可能会限制网络的表达能力。因此,批量归一化引入了第二步:可学习的缩放与偏移。
以下是第二步的操作:
z̃_{i,j} = γ_j * ẑ_{i,j} + β_j
在这里:
γ_j(伽马)是缩放参数,用于控制该特征分布的尺度(方差)。β_j(贝塔)是偏移参数,用于控制该特征分布的中心(均值)。
这两个参数 γ 和 β 是可学习的,它们会像网络中的权重一样,通过反向传播算法进行更新。
这个设计的巧妙之处在于,网络可以学习是否需要以及如何调整第一步标准化后的结果。理论上,如果网络认为原始的、未标准化的分布更优,它可以通过学习令 γ 等于标准差、β 等于均值,从而“撤销”第一步的标准化操作。这使得批量归一化比单纯的标准化更加灵活和强大。


🧩 整合到网络前向传播中
现在,让我们把这两个步骤整合到网络的前向传播流程中来看。假设我们有一个简单的三层网络。
以下是包含批量归一化的前向传播过程:
- 对于第一层,计算净输入:
z1 = W1 * x + b1 - 对
z1应用批量归一化(第一步+第二步),得到z̃1。 - 计算第一层的激活值:
a1 = σ(z̃1),其中σ是激活函数。 - 对于第二层,计算净输入:
z2 = W2 * a1 + b2 - 对
z2应用批量归一化,得到z̃2。 - 计算第二层的激活值:
a2 = σ(z̃2)。

通过这种方式,批量归一化被无缝地集成到了每一层的计算中。
💡 重要细节与影响
在实施批量归一化时,有几个关键点需要注意:
- 偏置项变得冗余:由于批量归一化中的
β参数已经起到了偏移作用,原始层中的偏置项b就变得不再必要。在实践中,我们通常在使用了批量归一化的层中省略偏置项,以使模型更简洁。 - 增加了可学习参数:批量归一化为每个特征引入了两个可学习参数(
γ和β)。虽然这略微增加了模型的复杂度,但带来的训练稳定性和速度提升通常是值得的。 - 反向传播:批量归一化层的梯度计算比普通层更复杂,因为它依赖于整个小批量的统计信息。不过,在现代深度学习框架(如PyTorch、TensorFlow)中,这些计算都由自动微分系统自动处理,我们无需手动实现。

📝 总结
本节课中我们一起学习了批量归一化的工作原理。我们了解到:
- 批量归一化通过标准化每一隐藏层的输入,来缓解训练中的内部协变量偏移问题。
- 其操作分为两步:标准化净输入(减去均值、除以标准差)和可学习的缩放与偏移(通过
γ和β参数)。 - 这使得训练过程更稳定,收敛更快,并减少了对参数初始化的依赖。
- 在实践中,使用批量归一化的层通常可以省略偏置项。

在下一节课中,我们将在代码中实际应用批量归一化,并进一步探讨其有效性的理论解释。
📚 课程 P84:L11.3 - PyTorch 中的 BatchNorm 🧠
在本节课中,我们将学习如何在 PyTorch 中实际使用批归一化。我们还将简要讨论批归一化在模型推理阶段的行为,因为训练时我们使用小批量数据,但在预测时可能只有单个数据点。

🛠️ 在 PyTorch 中使用 BatchNorm
上一节我们介绍了批归一化的概念,本节中我们来看看如何在 PyTorch 中实现它。
我使用了上周实现 Dropout 的代码,并稍作修改:移除了 Dropout 并添加了批归一化层。这里将复用相同的代码,不深入讨论细节,你可以在 Giub 上找到完整代码。
以下是主要代码,即我们需要修改的多层感知机。
# 示例:在 PyTorch 中添加 BatchNorm1d 层
import torch.nn as nn
class MultilayerPerceptron(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.hidden_1 = nn.Linear(784, 128)
self.bn1 = nn.BatchNorm1d(128) # 在第一个线性层后添加 BatchNorm
self.hidden_2 = nn.Linear(128, 256)
self.bn2 = nn.BatchNorm1d(256) # 在第二个线性层后添加 BatchNorm
self.out = nn.Linear(256, 10)
def forward(self, x):
x = self.flatten(x)
x = self.hidden_1(x)
x = self.bn1(x)
x = torch.relu(x)
x = self.hidden_2(x)
x = self.bn2(x)
x = torch.relu(x)
x = self.out(x)
return x
我们有两个隐藏层和一个输出层。首先,我使用了 Flatten,因为我处理的是 MNIST 数据,其维度为 n × 1 × 28 × 28。Flatten 会将其展平为 n × 784 的向量,其中 n 是批次大小,然后全连接线性层才能正常工作。
我在线性层之后插入了 BatchNorm1d。这里的 1d 可能会令人困惑,这是因为卷积网络有稍有不同的版本,称为 BatchNorm2d。BatchNorm1d 就是我们之前视频中讨论的标准批归一化,1d 只是为了区分它们。
我在第二个隐藏层也做了同样的事情。注意,我将线性层的偏置设置为 False,因为如果我们在计算净输入时使用 权重 × 特征 + 偏置,而批归一化中又添加了可学习的 β 参数,那么偏置就变得冗余了。但保留它也没有问题,我测试过,在这个案例中没有发现差异。
这就是在 PyTorch 中使用批归一化的方式。完整的代码示例可以在链接中找到,但这真的没什么可多说的,因为它只是这里的两行代码。

🔬 实验:BatchNorm 的位置与 Dropout
上一节我们看到了基础用法,本节中我们通过一些实验来探索不同设置的影响。
我运行了一些实验。在我刚刚展示的代码中,默认启用了偏置,并且将批归一化放在激活函数之前,这通常是原始论文中提出的方式。
但如今,人们更常见的是将其放在激活函数之后。我将在下一个视频中详细讨论这一点。这里,当我运行多层感知机代码时,我发现在激活函数之前或之后使用批归一化,性能上没有注意到任何差异。
我还运行了结合 Dropout 的实验。在这种情况下,除了网络不再过拟合之外,我也没有注意到太大差异。与不使用 Dropout 相比,两种 Dropout 情况下的测试准确率都略低,可能是我使用了太多的 Dropout。但至少可以看到不再有过拟合。
以下是具体的比较:
- 在激活函数前使用 BatchNorm,然后使用 Dropout。
- 在激活函数后使用 BatchNorm,然后使用 Dropout。
在实践中,我也没有注意到任何差异。如今,如果使用 Dropout,更常见的建议是将 BatchNorm 放在激活函数之后。一个有趣的记忆方法是:如果你有 BatchNorm -> 激活函数 -> Dropout,你可以称之为“BAD”(BatchNorm, Activation, Dropout 的首字母),而将 BatchNorm 放在激活函数之后可能“更好”。这通常在卷积网络等架构中可能带来微小差异,因此这是一个可以尝试的超参数。
🧪 训练与推理模式
现在,让我们看看训练函数。这与上周在 Dropout 中使用的训练函数完全相同,但它再次强调了 train() 和 eval() 模式的重要性。
在训练期间,我们将模型设置为训练模式,因为批归一化层会在此模式下计算运行均值和运行方差。我将在下一张幻灯片中讨论这一点。

# 训练循环示例
model.train() # 设置为训练模式
for epoch in range(num_epochs):
# ... 训练步骤 ...
# BatchNorm 会在此计算并更新运行统计量
# 评估循环示例
model.eval() # 设置为评估模式
with torch.no_grad():
# ... 评估步骤 ...
# BatchNorm 将使用训练阶段计算好的运行统计量,而不是当前批次的统计量
在评估模式下,我们模拟的是推理场景。在推理时,你可能只有一个数据点。例如,谷歌搜索引擎可能只有一个用户进行查询,而你的网络使用了批归一化,你需要进行归一化,但你并没有一个批次的用户数据。那么,我们如何处理这种情况呢?
📊 推理时的处理方式
有两种方式来处理推理时的场景。

简单的方法是使用整个训练集的全局均值和方差。你可以在进行输入标准化时计算这些特征均值和方差。但这在实践中并不非常常见。
更常见的方法是使用指数加权平均或移动平均。在实践中,人们通常在训练期间保持均值和方差的移动平均值,也可以称之为运行均值。
它的计算方式是通过一个动量项,通常是一个像 0.1 这样的小值。
运行均值更新公式:
running_mean = momentum * previous_running_mean + (1 - momentum) * current_batch_mean
运行方差更新公式:
running_var = momentum * previous_running_var + (1 - momentum) * current_batch_var
这里本质上就是一个移动平均或运行均值。你对方差也做同样的事情,并保存这些值。在推理时,你就使用这些值来缩放你要进行预测的数据点。
你不需要手动做这件事。顺便说一下,通过使用 model.eval(),这会自动发生。以上只是对底层发生事情的解释。
这就是批归一化在 PyTorch 中的工作方式。


📝 总结

本节课中我们一起学习了:
- 如何在 PyTorch 中实现 BatchNorm:使用
nn.BatchNorm1d层,通常将其插入在线性层之后、激活函数之前或之后。 - BatchNorm 与 Dropout 的配合:实验表明,将 BatchNorm 置于激活函数之后并与 Dropout 结合是更常见的做法,可作为超参数进行尝试。
- 训练与推理模式的区别:必须使用
model.train()和model.eval()来正确切换模式,以控制 BatchNorm 是计算批次统计量还是使用运行统计量。 - 推理时的处理机制:BatchNorm 在训练阶段通过移动平均累积运行均值和方差,并在推理阶段使用这些固定统计量对单个数据点进行归一化,这个过程在调用
eval()时会自动处理。

在下一个视频中,我将简要概述试图解释批归一化如何工作的各类文献。

📚 深度学习课程 P85:L11.4 - 为什么 BatchNorm 有效
在本节课中,我们将探讨批标准化(Batch Normalization)为何能有效提升深度神经网络的训练效果。我们将回顾其提出的初衷,分析几种主流理论,并介绍一些相关的实证研究。
🧠 BatchNorm 为何有效:理论与证据
在之前的课程中,我们讨论了 BatchNorm 的工作原理。本节我们将探讨其为何有效。虽然无法给出确切的答案,但我们可以汇总一些理论和实证证据来支持某些观点。
内部协变量偏移理论

最初提出 BatchNorm 的论文提到了 内部协变量偏移 的概念。作者认为,BatchNorm 通过减少这种内部协变量偏移来加速训练。
内部协变量偏移 指的是网络中某一层的输入分布会随着训练过程而发生变化。简单来说,就是特征的分布在训练过程中发生了偏移。BatchNorm 通过对这些输入进行重新标准化,理论上可以防止这种偏移。

然而,这一理论并没有得到强有力的证据支持。后续有论文对这一观点提出了质疑。
层间解耦理论
另一种推测是,BatchNorm 使得网络各层之间的依赖性降低。在深度网络中,如果某一层(尤其是早期层)的输出出现问题,它会像多米诺骨牌一样影响后续所有层。BatchNorm 可能通过标准化每一层的输入,使得各层对前一层错误的鲁棒性增强,从而实现一定程度的解耦。
但这同样只是众多理论中的一种。
📄 关键研究:BatchNorm 如何帮助优化?
一篇2019年的论文《How Does Batch Normalization Help Optimization?》提供了新的见解。该论文指出,虽然 BatchNorm 有助于稳定训练,但其确切原因仍不明确。

论文反驳了“减少内部协变量偏移”是其主要作用的观点。他们通过实验证明,即使人为引入分布偏移,带有 BatchNorm 的网络依然表现良好。
该研究的主要发现是:BatchNorm 使得优化问题的损失曲面(loss landscape)变得更加平滑。 这带来了两个好处:
- 训练更加稳定。
- 允许使用更大的学习率,从而可能实现更快的收敛。
公式表示: 平滑的损失曲面意味着梯度 ∇L 的变化更稳定,减少了训练中的剧烈波动。
📊 实证观察:学习率与训练稳定性

该论文通过实验展示了 BatchNorm 对训练的影响:
以下是其实验的关键发现:
- 允许更大的学习率: 在没有 BatchNorm 的标准网络中,过大的学习率(如0.5)会导致训练崩溃。而加入 BatchNorm 后,网络能够以相同的学习率稳定训练。
- 加速收敛: 在训练集上,使用 BatchNorm 的网络(无论学习率大小)达到相同精度所需的步骤更少,即收敛更快。
- 提升测试性能: 在测试集上,使用 BatchNorm 的网络最终性能通常更好或相当,并且训练过程更加稳定。

核心结论: BatchNorm 的主要优势可能不在于直接提升最终精度,而在于 使训练过程更稳定、更鲁棒,降低了超参数调优的难度。

📚 其他相关理论与研究
关于 BatchNorm 为何有效,还有许多其他研究,以下按时间顺序简要列出:
- 减少内部协变量偏移: 原始论文理论。
- 平滑优化曲面: 上文详述的2019年论文观点。
- 减少单方向依赖: 有论文认为 BatchNorm 隐式地鼓励网络不过度依赖单个特征方向。
- 隐式正则化: 2018年的研究提出 BatchNorm 作为一种隐式正则化器,有助于提升泛化能力。
- 导致梯度爆炸: 也有论文指出,在某些架构(如带有跳跃连接的残差网络)中,BatchNorm 可能导致梯度爆炸问题。

⚙️ 实践建议与技巧

了解理论后,我们来看看如何在实践中更好地使用 BatchNorm。
BatchNorm 的位置:激活函数之前还是之后?
原始论文将 BatchNorm 层放在 线性变换之后,激活函数之前。其流程为:
计算净输入 Z -> BatchNorm -> 激活函数 -> 输出

然而,也有实践将 BatchNorm 放在 激活函数之后。流程变为:
计算净输入 Z -> 激活函数 -> BatchNorm -> 输出
代码示例对比:
# 原始顺序:BN -> Activation
z = layer(x)
z_bn = batch_norm(z)
a = activation(z_bn)
# 调整后顺序:Activation -> BN
z = layer(x)
a = activation(z)
a_bn = batch_norm(a)
理由: 如果激活函数(如 Sigmoid)会改变数据的分布(例如将均值从0变为0.5),那么将 BatchNorm 放在其后可以确保输入下一层的数据仍然是标准化的。一些实验表明,将 BatchNorm 置于激活函数之后可能带来轻微的性能提升。
批大小的影响
BatchNorm 的效果严重依赖于批大小。因为其计算依赖于当前批次的均值和方差统计量。
核心建议: 使用足够大的批大小(通常建议大于16,实践中32或64更常见)。当批大小过小(如2, 4, 8)时,估计的统计量噪声过大,会导致 BatchNorm 效果变差,甚至损害性能。
批大小为何常选2的幂次?
在实践中,我们经常看到批大小被设置为32、64、128等2的幂次方。这主要有两个原因:
- 硬件优化: GPU 的并行计算单元(CUDA核心)数量通常是2的幂次方。使用2的幂次作为批大小可以更好地平衡工作负载,提高计算资源的利用率。
- 简化超参数搜索: 将搜索空间限制在2的幂次方(如32, 64, 128, 256)而非连续值(如30, 35, 40),可以大大减少需要尝试的超参数组合数量,简化调优过程。



🔍 延伸阅读与前沿动态
如果你对 BatchNorm 及其替代方案感兴趣,以下是一些进阶阅读方向:
- 条件批标准化: 将类别信息融入 BatchNorm,为不同类别学习不同的缩放和偏移参数。
- 大批次训练研究: 探讨批大小对泛化性能的影响,有研究认为大批次并不必然损害性能。
- 指数移动平均标准化: 一种2021年提出的 BatchNorm 替代方案,旨在提升自监督和半监督学习的性能。
- 无 BatchNorm 训练: 2021年的研究通过自适应梯度裁剪技术,成功训练了不依赖 BatchNorm 且性能优异的深度网络,这为架构设计提供了新思路。
🎯 课程总结
本节课我们一起深入探讨了批标准化有效的可能原因。
- 我们首先回顾了 内部协变量偏移 这一原始理论,但也了解到后续研究对其提出了质疑。
- 关键的研究指出,BatchNorm 的主要作用可能是 平滑优化问题的损失曲面,从而稳定训练、允许使用更大的学习率。
- 在实践部分,我们讨论了 BatchNorm 的放置顺序、批大小的重要性(建议使用大于16的批大小),以及选择2的幂次作为批大小的实用原因。
- 最后,我们列举了一些前沿研究方向,为你进一步探索提供了线索。


理解这些理论有助于我们更明智地在实际项目中应用和调整 BatchNorm,从而构建更稳定、高效的深度学习模型。
课程 P86:L11.5 - 权重初始化:为何重要? 🎯

在本节课中,我们将要学习神经网络中权重初始化的重要性。我们将探讨不恰当的初始化如何导致梯度消失等问题,并简要介绍两种常见的初始化方法及其背后的动机。
上一节我们讨论了输入归一化的重要性。本节中,我们来看看权重初始化为何同样关键。除了打破多层感知机中的对称性,我们还需要确保权重处于一个合适的尺度上。

打破对称性与控制尺度 🔄
我们之前提到,将权重初始化为小的随机数,是为了打破多层感知机中的对称性。除此之外,保持权重相对较小也是另一个需要考虑的因素。这与之前视频中提到的输入归一化概念有关。
为了解释和说明这一点,让我们再次思考多层感知机中更新某个特定权重的过程。
以下是从之前幻灯片中复制的公式,这里没有新内容,但希望大家重点关注激活函数对其输入的导数部分:

∂a / ∂z

我们可以将其展开。关键部分在于计算激活函数相对于其输入的导数。例如,如果我们使用逻辑Sigmoid函数,其导数在其输入为0时最大,但最大值也仅为0.25。

σ'(z) = σ(z) * (1 - σ(z)), 当 z=0 时,σ'(0) = 0.25
梯度消失问题 📉

在反向传播中,我们会多次乘以这个小于1的导数项。如果我们有很多层,例如10层,并且假设这是逻辑Sigmoid函数能得到的最大梯度,那么我们将与一个最大值仅为0.25的值相乘多次。

实际上,对于10层网络,梯度值可能会衰减到大约10的-6次方。梯度将变得非常小,以至于网络可能无法有效学习。

梯度衰减 ≈ (0.25)^10 ≈ 9.5e-7
因此,通常建议使用其他类型的激活函数,或者通过调整学习率来调节。这也是为什么将权重初始化为以0为中心很重要的一个动机。例如,如果使用逻辑Sigmoid函数,至少能获得最大的梯度。

初始化策略建议 💡

如果输入或净输入值很小或很大,梯度甚至会变得更小。因此,我们希望权重初始化后,输入能大致集中在0附近。
以下是几种初始化策略:
- 可以从一个随机均匀分布中初始化权重,例如范围在
[-0.5, 0.5]或更小的[-0.05, 0.05]。 - 也可以选择正数范围。对于ReLU函数,这可能不那么重要。然而,如果输入同时包含正负值,使用正负范围能提供更多组合可能。
- 另一种非常常见的方法是,从一个均值为0、方差较小的随机高斯分布中初始化权重。
这些只是过去常用的一些建议。如今,更常见的做法是使用Xavier初始化或Kaiming He初始化等方法。我们将在下一个视频中讨论这些内容。


本节课中,我们一起学习了权重初始化的重要性。我们了解到,不恰当的初始化(尤其是使用Sigmoid等激活函数时)可能导致梯度消失问题,从而阻碍深层网络的学习。因此,选择以0为中心的小随机值进行初始化是一个良好的起点。现代深度学习框架通常提供了更先进的初始化方法,帮助我们更有效地训练神经网络。

深度学习教程 P87:L11.6 - Xavier Glorot 与 Kaiming He 初始化 🧠
在本节课中,我们将学习深度神经网络中两种最常用的权重初始化方案:Xavier Glorot 初始化和 Kaiming He 初始化。我们将了解它们的基本原理、适用场景以及如何通过简单的数学公式实现它们。
Xavier Glorot 初始化 🔄
上一节我们介绍了权重初始化的重要性,本节中我们来看看第一种常用方案:Xavier Glorot 初始化。这种方法有时简称为 Xavier 初始化或 Glorot 初始化,名称来源于提出该方法的论文的第一作者。

Xavier 初始化通常与双曲正切激活函数配合使用。双曲正切函数是一种类似逻辑 S 型函数的 S 型激活函数,但其输出以 0 为中心。

逻辑 S 型函数在输入为 0 时输出 0.5,而双曲正切函数在输入为 0 时输出 0,其输出范围在 -1 到 1 之间。双曲正切函数在中心点的导数为 1,相比逻辑 S 型函数在中心点导数为 0.25,其梯度消失问题有所缓解。然而,在极端值附近,它仍然存在饱和区,梯度非常小或为零,这仍然是一个问题。Xavier 初始化可以作为一项改进,有助于防止权重落入这些极端值区域。
Xavier 初始化的步骤
以下是 Xavier 初始化的具体步骤:

- 从均值为 0、方差较小的正态分布或均匀分布中初始化权重。
- 根据该层输入单元的数量(即前一层的特征数)对权重进行缩放。

数学公式
假设权重从一个均值为 0、方差为 σ² 的正态分布中初始化。那么,缩放后的权重可以表示为:
公式:
W_scaled = W * sqrt(1 / n_in)

其中,n_in 是前一层的单元数(也称为“扇入”)。
原理简述
这种缩放背后的原理是考虑网络净输入的方差。当我们计算一个单元的净输入时,它是权重与前一层的激活值的乘积之和。如果我们将权重和激活值视为独立变量,那么随着前一层单元数(即“样本”数)的增加,和的方差会增加(因为独立变量之和的方差等于方差之和)。通过乘以 1 / sqrt(n_in) 进行缩放,我们旨在保持各层激活值方差的稳定性,从而缓解梯度消失或爆炸问题。



在实践中,有时也会考虑“扇出”(即该层的输出单元数),但使用“扇入”更为常见。
效果对比
在 Xavier 的原始论文中,通过可视化展示了使用与不使用 Xavier 初始化的区别。在不使用 Xavier 初始化、仅使用双曲正切激活函数的网络中,深层网络的激活值分布会严重集中在 0 附近。在反向传播时,较早的层几乎得不到有效的梯度更新(梯度消失),导致网络主要更新后面的层,而几乎忽略了前面的层。
而使用 Xavier 初始化后,所有层的梯度都保持在合理的范围内,有效缓解了不同层学习速度差异过大的问题。

Kaiming He 初始化 ⚡
上一节我们介绍了适用于双曲正切函数的 Xavier 初始化,本节中我们来看看另一种针对 ReLU 族激活函数设计的初始化方案:Kaiming He 初始化。该名称来源于论文的第一作者何恺明。
Xavier 初始化假设激活值的均值为 0,这对于以 0 为中心的双曲正切函数是合理的。但对于 ReLU 函数,情况则不同。ReLU 函数将所有负输入置为 0,因此其输出并非以 0 为中心,而是非负的。这破坏了 Xavier 初始化所依赖的假设。
Kaiming He 初始化论文中包含复杂的数学推导,但其核心结论非常简单:在 Xavier 初始化的基础上,为缩放因子增加一个增益系数 sqrt(2)。

数学公式

对于使用 ReLU 激活函数的层,其权重初始化公式为:
公式:
W_scaled = W * sqrt(2 / n_in)
这个额外的 sqrt(2) 因子专门用于补偿 ReLU 函数将一半的激活值置零所导致的方差减半效应,从而在正向传播过程中更好地保持激活值的方差。
实践建议与总结 🛠️

本节课我们一起学习了 Xavier Glorot 和 Kaiming He 这两种重要的权重初始化方法。Xavier 初始化适用于类似双曲正切的 S 型激活函数,而 Kaiming He 初始化则专门为 ReLU 及其变体设计。
不过,在实际应用中,你通常不需要手动实现这些初始化。现代深度学习框架(如 PyTorch、TensorFlow)已经为各种层和激活函数设置了合理的默认初始化方式。理解这些原理有助于你在需要调试网络或自定义架构时做出明智的选择。在接下来的课程中,我们将看到如何在 PyTorch 中具体应用和修改权重初始化方案。

总结要点:
- Xavier 初始化:使用公式
W * sqrt(1 / n_in),常用于双曲正切等 S 型函数。 - Kaiming He 初始化:使用公式
W * sqrt(2 / n_in),专为 ReLU 激活函数设计。 - 核心目标:通过适当的缩放,使各层激活值的方差在正向和反向传播中保持稳定,促进网络的有效训练。

🧠 课程 P88:L11.7 - PyTorch 中的权重初始化
在本节课中,我们将学习 PyTorch 如何处理权重初始化,以及如何根据需要自定义初始化方案。权重初始化是神经网络训练中的重要环节,它影响着模型训练的收敛速度和最终性能。
🔍 PyTorch 的默认初始化方案
上一节我们介绍了权重初始化的概念,本节中我们来看看 PyTorch 的默认做法。PyTorch 的默认权重初始化方案在不同版本中有所变化。在当前的 1.8 版本中,对于线性层(nn.Linear)和卷积层(nn.Conv2d),PyTorch 默认使用 Kaiming Uniform 初始化方法。
查看 PyTorch 源代码可以发现,其默认初始化假设用户将使用 Leaky ReLU 作为激活函数。初始化公式中包含了 math.sqrt(5) 这一项,就是为 Leaky ReLU 设计的。

代码示例:PyTorch 默认初始化(Leaky ReLU 假设)
# 这是 PyTorch 内部实现的简化示意
def kaiming_uniform_(tensor, a=math.sqrt(5)):
# ... 初始化逻辑
在实践中,即使你使用普通的 ReLU 激活函数,使用这个默认初始化通常也能工作良好,差异可能不明显。


🛠️ 如何手动覆盖权重初始化
如果你想自定义初始化方案,可以手动覆盖 PyTorch 的默认行为。以下是一种常用的方法:在定义网络后,遍历其所有模块,并对特定类型的层(如线性层)应用新的初始化。
以下是具体步骤:


- 遍历网络中的所有模块。
- 筛选出你希望初始化的特定层类型(例如
nn.Linear)。 - 对该层的权重(
.weight)应用新的初始化函数。 - 你也可以选择性地初始化偏置项(
.bias)。


代码示例:手动应用 Kaiming Uniform 初始化(针对 ReLU)
import torch.nn as nn
import torch.nn.init as init

# 假设 `model` 是你的神经网络
for m in model.modules():
if isinstance(m, nn.Linear):
# 使用针对 ReLU 的 Kaiming Uniform 初始化权重
init.kaiming_uniform_(m.weight, mode='fan_in', nonlinearity='relu')
# 将偏置初始化为 0
if m.bias is not None:
init.constant_(m.bias, 0)
注意:默认的 PyTorch 初始化是针对 Leaky ReLU 的(
nonlinearity='leaky_relu')。上面的代码将其改为了针对普通 ReLU。根据经验,两者差异不大,但你可以根据自己使用的激活函数进行选择。


⚙️ 其他初始化方法示例:高斯初始化
除了 Kaiming 初始化,我们也可以使用其他分布,例如高斯(正态)分布来初始化权重。

代码示例:使用高斯分布初始化
for m in model.modules():
if isinstance(m, nn.Linear):
# 使用均值为0,标准差为0.001的高斯分布初始化权重
m.weight.data = torch.randn_like(m.weight.data) * 0.001
# 使用 .detach() 防止该操作被计入计算图
m.weight.data = m.weight.data.detach()
if m.bias is not None:
init.constant_(m.bias, 0)

这里使用了 .detach() 方法。这是因为 PyTorch 中的可学习参数(nn.Parameter)默认会跟踪梯度计算。权重初始化应该是训练的起点,而不应被视为计算图的一部分。使用 .detach() 可以将其从当前计算图中分离,避免不必要的梯度计算。


📊 初始化方法的影响与批归一化(Batch Norm)的作用
不同的初始化方法对训练过程有显著影响。例如,在使用 ReLU 的网络中,不合适的初始化(如标准差过小的高斯初始化)可能导致前几个训练周期损失下降缓慢,模型似乎“停滞”不前,之后才突然开始学习。

然而,当网络中使用 批归一化 时,权重初始化的选择变得不那么关键。批归一化通过对每一层的输入进行标准化,减少了内部协变量偏移,使得网络对初始权重的尺度不那么敏感,训练过程更加稳定和鲁棒。
实践对比:
- 没有批归一化时,Kaiming 初始化 通常比随意的高斯初始化表现更好。
- 引入批归一化后,即使使用简单的高斯初始化,模型也能快速、稳定地训练。

✅ 总结
本节课中我们一起学习了 PyTorch 中的权重初始化:
- PyTorch 默认使用 Kaiming Uniform 初始化,并假设激活函数为 Leaky ReLU。
- 我们可以通过遍历模型模块并调用
torch.nn.init中的函数来手动覆盖初始化方案。 - 初始化时使用
.detach()可以防止初始化操作被错误地加入计算图。 - 虽然权重初始化很重要,但当网络中使用 批归一化 时,其重要性会降低,因为批归一化增强了模型对初始化的鲁棒性。

对于大多数使用 ReLU 族激活函数和批归一化的现代网络,PyTorch 的默认初始化通常是一个良好的起点。当你使用特殊的激活函数或架构时,再考虑查阅文献并自定义初始化策略。
📰 深度学习新闻 #7 2021年3月13日
在本节课中,我们将回顾2021年3月13日发布的一期深度学习新闻。内容涵盖从生成对抗网络的有趣应用到大型多模态模型,再到深度学习系统的安全性与公平性等广泛主题。我们将以简单直白的方式介绍这些概念,并确保初学者能够理解。
🤖 项目一:文本盒生成器
首先介绍的项目是一个文本盒生成器。这是一个生成对抗网络的应用实例,用于生成包含特定文本的新图像。
生成对抗网络通常由两部分组成:生成器和判别器。生成器的目标是生成模仿训练集数据的新数据,而判别器的任务是区分生成器产生的“假”数据和真实的训练集数据。生成器通过学习来“欺骗”判别器,使其无法区分生成图像和真实图像。
在这个项目中,生成器不仅接收噪声向量作为输入,还接收一个文本编码器输入的单词。为了确保生成的图像中包含指定的文本,项目还引入了一个OCR模块来验证输出图像中的文本是否与输入单词匹配。
核心流程公式化描述如下:
- 噪声向量
z和单词编码w作为输入。 - 生成器
G(z, w)输出合成图像。 - 判别器
D(x)判断图像x是真实的还是生成的。 - OCR模块
O(x)提取并验证图像中的文本。
以下是该项目的一些关键点:
- 这是一个将GAN应用于特定领域(文本嵌入图像)的简洁示例。
- 作者在GitHub仓库中提供了清晰的工作原理描述。
- 这种方法通过附加约束(如指定文本)来控制生成内容。

🐉 项目二:M6 - 中文多模态预训练模型
上一节我们介绍了一个小型的GAN应用,本节中我们来看看一个大型项目:M6。这是一个由阿里巴巴和清华大学合作开发的中文多模态预训练模型。

M6模型在1.9万亿张图像和292GB文本数据上进行了训练。其新颖之处在于它是针对中文,而非英文开发的。中文的字符系统比英文的26个字母更为复杂,因此这是一个有趣的研究案例。
他们训练了两个版本的Transformer模型,参数量分别为100亿和1000亿。该模型采用自监督预训练,预训练后的模型可以用于多种下游任务,如图像描述生成、图像搜索、问答和诗歌生成等。
那么,他们是如何让一个模型适应这么多不同任务的呢?关键在于掩码策略。模型被称为“统一编码器-解码器”。根据不同的任务,他们对输入进行不同类型的掩码。

以下是不同任务对应的输入掩码示例:
- 文本去噪:使用编码器令牌和解码器令牌,掩码掉部分图像块。
- 语言建模:仅使用解码器令牌。
- 图像描述生成:使用图像块和解码器令牌。
通过这种灵活的掩码方式,他们得以用单一模型处理多种任务。
🚗 项目三 & 四:自动驾驶系统的脆弱性


接下来,我们转向深度学习在现实世界应用中的安全性问题。有两个项目揭示了自动驾驶系统可能面临的攻击。
第一个项目展示了如何通过将街道标志投影到树木等物体上来欺骗特斯拉的自动驾驶系统。攻击者甚至可以将 Elon Musk 的轮廓投影到路上,导致车辆误认为有行人而紧急刹车。


第二个更新的项目则使用激光束进行攻击。研究者将激光束投射到摄像头的视野中,同样能误导自动驾驶汽车。这种攻击方式可能更令人担忧,因为激光设备更易于携带和隐藏。
这些研究突出表明,要确保自动驾驶汽车的安全,我们仍有很长的路要走。
⚖️ 项目五:机器学习中的公平性
与安全性相关的一个重要议题是公平性。Facebook本周发布了一篇关于算法公平性的博客文章,并链接了一篇研究论文。
实现公平的机器学习系统是一个复杂的挑战。其中一个关键点是标签公平性。在实践中,我们通常假设数据集中的标签是正确且客观的“地面真相”。但在现实世界中,情况并非总是如此。
例如,测量用户是否点击了网页按钮是明确且可客观度量的。然而,对于识别网络欺凌这类任务,标签通常由人工标注者根据既定政策来判断。不同的人可能对政策有不同的理解和应用,从而引入显性或隐性的偏见。因此,这些标签可能并不总是绝对正确的“真相”,这给机器学习应用带来了额外的复杂性。

对于课程项目,一个有益的做法是:从你的数据集中抽取一个样本(例如100张图像),在不看标签的情况下尝试自己进行分类,然后计算你的准确率。这能帮助你直观感受任务的难度,并反思数据标签的可靠性。
📊 项目六:从在线与离线学习视角理解泛化
本周课程讨论了提升模型泛化性能的方法,与此相关的一篇有趣论文探讨了从在线学习和离线学习角度理解泛化。
论文标题为《好的在线学习者也是好的离线泛化者》。研究者比较了两种场景:
- 现实世界:我们拥有固定的训练集,使用小批量随机梯度下降进行多轮训练。
- 理想世界:拥有无限数据流,每次从小批量都从数据分布中全新采样,没有重复的“轮次”。
研究发现,在这两种场景下,模型在测试集上最终达到的泛化性能没有显著差异。这意味着,在固定数据集上重复训练(离线学习)与持续接收新数据训练(在线学习),最终得到的模型能力是相近的。
研究的一个注意事项:他们的“理想世界”数据是通过一个生成模型合成的CIFAR-10类图像,并使用另一个高精度模型为其打上标签。此外,他们报告的是“测试软错误”(基于softmax概率的准确率),而非传统的测试准确率。
为了确保结论的普适性,他们在不同模型架构上也观察到了相同的效应。

🔄 项目七:预训练与自监督学习的作用
上一节的研究还涉及了预训练的影响。论文发现,使用在ImageNet上预训练的模型权重进行初始化,相比随机初始化,能更快地达到更高的性能,并且最终性能也显著更优。
预训练,特别是自监督学习,是提升模型性能的强大工具。自监督学习是一种利用无标签数据自身结构来生成监督信号进行预训练的方法。
本周另一个相关项目是SEER。研究者利用从Instagram获取的10亿张无标签图像,通过自监督学习(使用高效的在线聚类算法SwAV)训练了一个十亿参数的模型。该模型在ImageNet上达到了84.2%的top-1准确率,创造了自监督学习的新纪录。
为了方便研究者进行自监督学习的研究和比较,社区还推出了一个基准测试库。该库提供了预训练的自监督模型和标准评测流程,支持PyTorch框架,是探索自监督学习的一个实用工具。

🎯 总结

本节课中我们一起学习了2021年3月中旬深度学习领域的多项进展。我们从一个小型的文本盒生成器GAN应用开始,探讨了大型中文多模态模型M6的架构与训练策略。接着,我们审视了深度学习在自动驾驶系统中暴露出的安全性脆弱点,并讨论了构建公平机器学习系统所面临的挑战。最后,我们通过研究理解了在线与离线学习在泛化性能上的关系,并看到了预训练与自监督学习对于提升模型性能的关键作用。这些内容展示了深度学习领域的活力与多样性,以及其在迈向实际应用过程中需要持续关注的技术与伦理问题。
课程 P9:L1.4 - 监督学习工作流程 📊
在本节课中,我们将详细学习监督学习的工作流程,并介绍机器学习中必要的符号和术语,为后续课程打下基础。

概述
上一节我们介绍了机器学习及其三大类别。本节中,我们将更深入地探讨监督学习的工作流程,并学习一些在本课程中会用到的核心数学符号和术语。
监督学习工作流程
监督学习是回归或分类的过程,主要包含两个步骤:训练步骤和推理步骤。在深入推理步骤之前,我们先看看训练步骤。
想象你有一个训练数据集,任务是进行分类。我们以一个简单的花朵分类为例。你收集了多种花朵的图像作为训练数据集,并咨询专家为这些图像提供了标签。这些标签是你想要预测的目标,而花朵图片则是你的观测数据。
在传统机器学习中,你需要从图像数据中提取特征。这通常需要领域专家的意见,以确定哪些特征是有效的。例如,潜在的特征可能包括花朵的颜色、高度或叶片的数量。提取特征后,结合标签,你就可以训练一个机器学习模型来进行预测。
训练模型后,我们的目标是使用它对新数据进行预测。例如,开发一个网站,用户可以上传花朵图片,你的模型就能预测花朵类型。对于新图像,你需要使用与训练时相同的特征提取步骤,然后将数据输入模型以预测新标签。这个过程如今常被称为“推理”。

模型评估
在将模型部署到现实世界之前,我们需要评估其性能。评估过程与推理步骤类似,但我们使用一个独立的测试集。


如果你有一个大型数据集,通常的做法是将其随机打乱,然后划分为训练集(例如70%)、测试集(例如20%)和验证集(例如10%)。验证集用于调整模型参数,我们将在后续看到代码示例时详细讨论。
对于测试集,我们将其观测数据输入模型进行预测,同时保留其真实标签。然后,我们将模型预测的标签与已知的真实标签进行比较。通过计算正确预测的次数占总测试数据点的比例,我们可以得到模型的准确率。相应地,错误率则是1减去准确率。

结构化与非结构化数据

接下来,我们简要讨论结构化数据和非结构化数据的区别。

结构化数据集可以看作是一个表格。在这个表格中,列代表特征,行代表观测数据。例如,在花朵分类中,经过特征提取后,你可能会得到一个包含花萼长度、花萼宽度、花瓣长度和花瓣宽度等特征列的表格,以及一个标签列。这就是一个结构化数据集的例子。
相比之下,非结构化数据就是原始数据本身,例如原始的图像。你可以从非结构化数据中提取出结构化数据,但这并不总是简单的过程,有时需要人工协助。
结构化数据通常用于传统机器学习,而非结构化数据则适用于深度学习。因为深度学习模型内部隐含了特征提取的步骤。然而,深度学习的缺点是通常需要更多的数据。例如,一个只有150个样本的花朵数据集,使用随机森林等传统算法可能轻松达到99%的准确率;但若想用深度学习达到相近的性能,则可能需要至少上万张图像。
工作流程对比
下图很好地总结了传统机器学习与深度学习在工作流程上的区别。

在深度学习流程中,你将原始数据直接输入网络这个“黑箱”,然后得到预测输出。而在传统机器学习流程中,需要人工从数据中提取手工设计的特征,然后将这些特征提供给机器学习系统进行预测。
结合花朵分类的例子:在深度学习情境下,你只需将原始图像输入模型,模型会自行学习如何提取有效特征。而在传统机器学习流程中,你需要先手工测量并提取特征(如花萼和花瓣的尺寸),再将它们输入模型。

总结


本节课我们一起学习了监督学习的完整工作流程,包括训练、推理和模型评估。我们还区分了结构化数据与非结构化数据,并对比了传统机器学习与深度学习在处理这两种数据时的不同路径。理解这些基础概念和流程,是掌握后续更复杂机器学习与深度学习主题的关键。
课程 P90:L12.0 - 改进基于梯度下降的优化 🚀
在本节课中,我们将学习如何改进神经网络训练中的优化算法。我们将重点讨论学习率衰减、动量学习以及自适应学习率等技巧,这些方法都是对随机梯度下降的改进,旨在提升模型的收敛速度和泛化性能。
学习率衰减 📉
上一节我们介绍了优化算法的重要性,本节中我们来看看学习率衰减。学习率衰减是一种在训练过程中逐步降低学习率的技术,这有助于减少随机梯度下降中的噪声,使模型在后期更稳定地收敛到最优解。
在PyTorch中,我们可以使用 torch.optim.lr_scheduler 模块来实现学习率衰减。以下是一个简单的示例代码:
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR
# 假设我们有一个模型和优化器
model = ...
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 创建学习率调度器,每30个epoch将学习率乘以0.1
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
# 训练代码...
scheduler.step() # 更新学习率
动量学习 ⚡
在讨论了学习率衰减后,我们接下来看看动量学习。动量项通过累积过去梯度的指数加权平均来加速梯度下降在正确方向上的收敛,并在其他方向上减缓更新,从而更有效地引导优化过程。
动量更新的公式如下:
[
v_t = \beta v_{t-1} + (1 - \beta) \nabla J(\theta_t)
]
[
\theta_{t+1} = \theta_t - \alpha v_t
]
其中,( v_t ) 是当前动量,( \beta ) 是动量系数,( \alpha ) 是学习率,( \nabla J(\theta_t) ) 是当前梯度。
自适应学习率 🔄
上一节我们介绍了动量学习,本节中我们来看看自适应学习率。自适应学习率算法(如Adam)结合了动量学习和自适应学习率的优点,能够自动调整每个参数的学习率,从而减少超参数调优的工作量,并 often 在训练中表现更好。
Adam算法的更新规则如下:
[
m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t
]
[
v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2
]
[
\hat{m}_t = \frac{m_t}{1 - \beta_1^t}
]
[
\hat{v}t = \frac{v_t}{1 - \beta_2^t}
]
[
\theta = \theta_t - \alpha \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}
]

在PyTorch中,使用Adam优化器非常简单:
import torch.optim as optim
optimizer = optim.Adam(model.parameters(), lr=0.001)
其他高级优化主题 📚
除了Adam和动量学习,近年来还涌现了许多其他优化算法。以下是一些值得关注的高级主题:
- AdaGrad:为每个参数自适应地调整学习率。
- RMSProp:改进了AdaGrad,解决了学习率过早衰减的问题。
- AdaDelta:进一步优化了自适应学习率,无需手动设置全局学习率。
- Nadam:结合了Nesterov动量和Adam算法。
- Lookahead:通过维护两组权重来稳定训练过程。
对于有兴趣深入研究的同学,可以参考相关论文和开源实现以了解更多细节。
总结 🎯


本节课中我们一起学习了改进基于梯度下降优化的几种关键技术。我们介绍了学习率衰减的原理与实现,探讨了动量学习如何加速收敛,并详细讲解了自适应学习率算法(如Adam)的优势与应用。这些技巧不仅适用于标准的神经网络,也适用于我们将要学习的卷积网络、循环网络等复杂架构。掌握这些优化方法将帮助你更高效地训练模型,并获得更好的性能。

📉 课程 P91:L12.1 - 学习率衰减
在本节课中,我们将要学习学习率衰减,即在训练过程中逐步降低学习率的技术。我们将探讨其原理、常见方法以及实践中的注意事项。
🔄 回顾小批量学习
上一节我们介绍了优化算法的基础,本节中我们来看看学习率衰减。首先,让我们简要回顾一下小批量学习。
小批量学习是随机梯度下降的一种形式,我们从训练集中抽取小批量数据。每个小批量可以被视为从训练集中抽取的一个样本,而训练集本身又是从总体分布中抽取的一个样本。在小批量学习中,我们对每个小批量执行前向传播和反向传播。
如果我们抽取这些小批量样本,与使用整个训练集相比,我们实际上会得到噪声更大的梯度。这既有优点也有缺点。
- 优点:噪声梯度有助于模型逃离局部极小值。
- 缺点:噪声梯度可能导致优化路径振荡,不如直接路径高效。
使用小批量学习的另一个优势是收敛速度。与每次更新只使用一个训练样本的随机梯度下降相比,小批量梯度下降更快。与使用整个训练集的梯度下降相比,小批量梯度下降也可能更快,因为它能更频繁地更新参数。

⚖️ 学习率与噪声的权衡
如果学习率设置得过大,可能会导致严重的振荡和“超调”现象,即更新步长过大,越过最优点,导致收敛低效。
反之,如果学习率设置得过小,虽然路径稳定,但收敛速度会非常慢,需要很多小步才能到达最优点。在深度学习的非凸损失函数中,过小的学习率还可能导致模型陷入局部极小值而无法跳出。
因此,需要在噪声过大和噪声过小之间找到一个平衡点。在训练初期,较大的噪声有助于逃离局部极小值;而在训练后期,减小噪声有助于稳定收敛。学习率衰减正是为了实现这一目标而设计的技术。

📊 关于批量大小的实践建议
在深入讨论学习率衰减之前,我们先介绍一个关于批量大小的实用技巧。
通常建议使用较大的批量大小(例如 256、512、1024)。原因如下:
- 更好地利用硬件:较大的批量能更高效地利用 GPU 的并行计算能力。
- 梯度更稳定:较大的样本能更好地保持数据集的原始分布,从而提供更具代表性的梯度估计,减少更新噪声。

以下是一个在 MNIST 数据集上的实验对比:
- 使用较大的批量(如 1024)时,训练曲线更平滑,噪声更小。
- 使用较小的批量(如 64)时,由于更新次数更多,初期收敛可能更快,但噪声更大。
选择批量大小时,也可以考虑数据集的类别数。对于类别较多的数据集,使用更大的批量可能更有利。
📉 什么是学习率衰减?
学习率衰减的核心思想是:在训练后期,逐步降低学习率,以抑制优化过程中的振荡,使模型更平滑地收敛到最优点。

想象一下损失曲线:在训练后期,损失已经较低,但梯度更新仍在振荡。此时如果降低学习率,更新的步长就会变小,振荡幅度随之减小,有助于稳定训练。
然而,这里存在一个风险:如果过早地衰减学习率,可能会使模型过早停止学习,损失停滞在一个较高的平台期,无法进一步下降。因此,在实践中,通常建议先在不使用学习率衰减的情况下训练模型,建立一个性能基线,然后再尝试加入学习率衰减,观察是否能提升性能。
🛠️ 常见的学习率衰减方法
以下是几种常见的学习率衰减策略:

1. 指数衰减
学习率按指数函数衰减。
公式:η_t = η_0 * e^{-k * t}
其中,η_0 是初始学习率,k 是衰减率,t 是时间步(通常是周期数)。
2. 按步长衰减(减半)
每隔固定的训练周期(如每 10 个周期),将学习率减半。
这是一种简单且常用的策略。
3. 反时衰减
学习率与时间步成反比关系。
公式:η_t = η_0 / (1 + k * t)
其效果与指数衰减类似。
除了上述方法,还有一些更复杂的调度策略,例如循环学习率,它让学习率在一个范围内周期性循环变化,有时能帮助模型跳出尖锐的局部极小值。

🔄 另一种思路:增加批量大小

有一篇论文提出了一个有趣的观点:与其衰减学习率,不如在训练过程中逐步增加批量大小。这可以达到类似的效果——在训练后期减小更新步长的方差。
优点:
- 能达到相似的测试精度。
- 由于批量更大、更新次数减少,可能提高并行效率,缩短训练时间。


这为优化训练过程提供了另一种值得尝试的思路。


🎯 总结与预告
本节课中我们一起学习了学习率衰减。我们了解到,通过在训练后期降低学习率,可以减小优化过程中的振荡,帮助模型更稳定地收敛。我们介绍了指数衰减、按步衰减等常见方法,并讨论了过早衰减的风险。此外,我们还了解了增加批量大小作为替代方案的思路。


在下一节课中,我将向大家展示我个人在实践中最喜欢的一种学习率衰减方法(一种改进的按步衰减策略),并演示如何具体实现它。

📚 课程 P92:L12.2 - PyTorch 中的学习率调度器
在本节课中,我们将要学习如何在 PyTorch 中实现学习率衰减。我们将从手动实现开始,然后介绍 PyTorch 内置的调度器,并探讨一种在实践中非常有效的学习率调整策略。最后,我们还会学习如何保存和加载模型,以便复用训练结果。
🔧 手动实现学习率衰减
上一节我们介绍了什么是学习率衰减及其作用。本节中我们来看看如何在 PyTorch 中手动实现它。
以下是手动实现指数衰减的示例代码:
def adjust_learning_rate(optimizer, epoch, initial_lr, decay_rate):
"""每10个epoch将学习率乘以衰减率"""
if epoch % 10 == 0:
for param_group in optimizer.param_groups:
param_group['lr'] = initial_lr * (decay_rate ** (epoch // 10))
这段代码定义了一个函数,它检查当前 epoch 数。如果 epoch 数是 10 的倍数,它会遍历优化器中的所有参数组,并将每个参数组的学习率乘以衰减率的相应次方。这种实现方式考虑了优化器可能为不同参数组设置不同学习率的情况。

在训练循环中,你可以在每个 epoch 结束后调用此函数。
# 在训练循环中
for epoch in range(num_epochs):
# ... 在每个mini-batch上进行训练 ...
adjust_learning_rate(optimizer, epoch, initial_lr=0.1, decay_rate=0.9)
然而,手动实现比较繁琐。接下来,我们将看看如何使用 PyTorch 内置的工具来简化这个过程。


🧰 使用 PyTorch 内置调度器
PyTorch 在 torch.optim.lr_scheduler 模块中提供了多种学习率调度器。使用它们通常只需要两个步骤:初始化和在训练循环中调用 .step()。

以下是使用 ExponentialLR 调度器的示例:
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler
# 1. 初始化模型和优化器
model = MyModel()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 2. 初始化调度器
scheduler = lr_scheduler.ExponentialLR(optimizer, gamma=0.9)
# 训练循环
for epoch in range(num_epochs):
model.train()
for batch in train_loader:
# ... 前向传播、计算损失、反向传播 ...
optimizer.step() # 更新权重
# 3. 在每个epoch后更新学习率
scheduler.step()
首先,我们像往常一样初始化模型和优化器。然后,我们创建一个调度器实例,并将优化器传递给它。参数 gamma 是每个 epoch 后学习率相乘的因子。在训练循环中,我们在每个 epoch 结束后调用 scheduler.step(),学习率就会自动按指数规律衰减。

你可以在 PyTorch 官方文档 中找到所有可用的调度器。


⭐ 一种有效的实践策略:在误差平台期降低学习率
除了标准的指数衰减,还有一种在实践中非常有效的策略。这种策略不按固定周期衰减学习率,而是在模型性能(如验证集准确率)停止提升(即达到“平台期”)时,将学习率大幅降低(例如除以10)。

这种方法在2016年提出 ResNet 的论文中被使用。其核心思想是:使用 SGD 优化器,初始学习率设为 0.1(这对 SGD 来说较大)。当训练损失或验证误差在一段时间内不再显著下降时,就将学习率除以 10。这通常配合权重衰减(L2正则化)和动量一起使用。
在 PyTorch 中,我们可以使用 ReduceLROnPlateau 调度器来实现这一策略。

from torch.optim.lr_scheduler import ReduceLROnPlateau
# 初始化优化器
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)
# 初始化调度器,监控验证集准确率
scheduler = ReduceLROnPlateau(optimizer,
mode='max', # 我们希望准确率‘max’imize
factor=0.1, # 学习率衰减因子:乘以0.1(即除以10)
patience=3) # 容忍3个epoch性能无提升
# 在训练循环中
for epoch in range(num_epochs):
train(...)
val_acc = validate(...) # 计算当前epoch的验证集准确率
# 根据监控指标更新学习率
scheduler.step(val_acc)



这里,mode='max' 表示我们监控的指标(验证准确率)是越大越好。factor=0.1 指定了衰减因子。patience=3 意味着如果准确率连续 3 个 epoch 没有提升,就触发学习率衰减。根据经验,基于验证集准确率来调整学习率通常效果很好。
💾 模型的保存与加载
在使用调度器或长时间训练模型时,保存和加载模型至关重要。这可以避免重复训练,也便于后续继续训练或部署。

PyTorch 推荐的方式是保存模型的 state_dict(状态字典),它包含了模型的所有可学习参数。
以下是保存和加载模型的代码:
import torch
# 保存模型
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict() if scheduler else None,
'loss': loss,
}, 'model_checkpoint.pth')
# 加载模型
checkpoint = torch.load('model_checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
if scheduler and checkpoint['scheduler_state_dict']:
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
start_epoch = checkpoint['epoch']
保存:我们使用 torch.save 将一个字典保存到文件。这个字典不仅包含模型的 state_dict,还可以包含优化器的 state_dict、调度器的 state_dict、当前的 epoch 和损失值等。这样,所有与训练状态相关的信息都被保存下来。
加载:加载时,首先需要重新初始化模型、优化器和调度器(结构必须与保存时相同)。然后使用 torch.load 读取文件,并分别调用各自的 .load_state_dict() 方法将参数载入。这样,你就可以从保存的点继续训练,而学习率等状态也会被正确恢复。
📝 总结
本节课中我们一起学习了 PyTorch 中学习率调度器的使用。
- 我们首先介绍了如何手动实现一个简单的学习率衰减函数。
- 然后,我们学习了如何使用 PyTorch 内置的调度器(如
ExponentialLR)来自动化这个过程,这大大简化了代码。 - 接着,我们探讨了一种基于性能平台期的动态调整策略,并使用
ReduceLROnPlateau调度器来实现它。这种方法通常在模型训练后期非常有效。 - 最后,我们学习了如何保存和加载模型及其完整的训练状态(包括优化器和调度器),这对于实际项目中的模型持久化和继续训练至关重要。

掌握这些工具和技巧,能帮助你更高效地训练神经网络模型,并管理整个训练生命周期。
课程 P93:L12.3 - 具有动量的新元 🚀

在本节课中,我们将要学习一种名为“动量”的优化技术。动量是随机梯度下降(SGD)的一种改进方法,它通过考虑之前的更新历史来稳定梯度下降过程,减少噪声,从而加速收敛。
什么是动量?📈
上一节我们介绍了基础的随机梯度下降。本节中我们来看看如何通过动量来改进它。
动量是一个可以添加到SGD中的项,它考虑了之前的更新,从而稳定了随机梯度下降过程,使其噪声更小。这个概念源自物理学中的动量定义:在牛顿力学中,线性动量是物体的质量与速度的乘积。在神经网络权重更新的语境中,我们没有“质量”,但可以考虑更新的“速度”,即之前更新的移动速率。
在动量学习中,我们试图通过利用这个“速度”来抑制振荡,从而加速收敛。速度代表了之前更新过程中的移动速率。通过考虑更新在时间进程中的进展,我们可以用它来抑制这些振荡。这项技术可以追溯到1999年的一篇论文,至今仍是深度学习中非常流行的技术,尤其是与学习率衰减结合使用时。

动量如何工作?🔧
以下是动量如何抑制振荡的示意图。


左侧是使用SGD进行小批量学习的草图,由于噪声,路径呈锯齿状。但平均来看,存在一个向某个方向的作用力。我们可以利用这个“速度”将其添加到更新中,以抑制振荡,使得振荡减弱,不像之前那样嘈杂。

关键要点是:在常规梯度下降中,我们通常沿梯度的相反方向移动。这是常规的更新方式。但现在有了动量,我们还会沿最近几次更新的平均方向移动。我们关注的是之前更新的平均方向,这就是我们的“速度”。

为什么动量有帮助?💡
动量不仅有助于抑制振荡,还能帮助我们跳出局部最小值。想象一下,如果你的更新有噪声,但某个时刻你落在了平坦的表面上,此时梯度将为零。你如何从这个平坦表面出来呢?额外的速度项实际上可以帮助推动你离开这个平坦表面。
下图试图说明这一点。

这里的红线是某个权重 W_i 的损失曲线(一个简化的损失曲线)。这些圆圈代表不同时间步的当前位置(例如时间步1、2、3、4)。你可能沿着斜坡下降,这很好。但下一个更新可能到达一个平坦表面,那里本质上没有梯度(斜率为零)。常规梯度下降会在此停止。然而,由于动量考虑了之前更新的平均方向,它可能会提供必要的推力,使你继续下降。因此,使用速度项可以帮助我们逃离局部最小值或梯度平坦的鞍点。
基于速度的更新具体是怎样的?📝
以下是具体的更新公式示意图,让我们逐步分析。


从底部开始,如果不看幻灯片上的文字,这本质上就是我们在SGD中执行的常规梯度下降更新。
假设我们要更新网络中的权重 W_ij。t 是时间步,t+1 是下一个时间步。用于下一次迭代的权重是当前时间步的权重减去变化量 ΔW_ij(t),其中 ΔW_ij(t) 是损失函数对该权重的偏导数乘以学习率 η。这整个部分就是我们改变权重的量。
现在,我们不直接使用损失函数对权重的偏导数乘以学习率 η,而是修改它。我们在常规更新中添加了一个额外的项(如上图左侧所示)。我们称这整个部分为“速度”。因此,我们不再只使用之前的部分,而是现在有了左侧的这个部分。
这个部分具体是什么?它是前一个时间步 t-1 的 ΔW 项(即前一个时间步的速度)乘以 α。这里的 α 是动量率,在实践中通常设置在0.9到0.999之间。你可以将其视为摩擦或阻尼参数。α 越大,前一个速度项的影响就越大。因此,α 越大,常规更新受之前更新的影响就越大。这本质上就像一个移动平均。我们本质上只是将之前的更新乘以一个特定数字后加进来,你可以将其视为一个移动平均,也就是速度项或动量项。然后我们称其为速度,并用它来更新权重。这就是动量的工作原理。
动量在实践中看起来如何?🎯
以下是一个动量的实际模拟,展示了它与常规梯度下降的区别。

这里展示了两个动量值的情况。零动量就是常规梯度下降(在一个理想的损失曲面上,实际深度学习场景会更复杂)。你可以看到它非常嘈杂,振荡剧烈。而使用动量后,振荡减少了,更加稳定,因为考虑了平均方向。你还可以看到更新点可能更少(这些点彼此更接近,而常规更新的点间距更大)。这就是动量工作的方式和外观。
核心公式总结 📚
以下是带有动量的随机梯度下降(SGD)的核心更新公式:
速度计算:
v(t) = α * v(t-1) - η * ∇J(θ(t))
权重更新:
θ(t+1) = θ(t) + v(t)
其中:
v(t)是当前时间步的速度。α是动量系数(通常为0.9)。η是学习率。∇J(θ(t))是当前参数θ(t)处的损失梯度。

下一步是什么?➡️
在下一个视频中,我将讨论自适应学习率。首先会介绍一种称为RMSProp的算法,然后我们将这些自适应学习率的概念与动量结合起来,这将产生Adam优化器,它可能是目前应用最广泛的基于SGD的优化器。


总结 ✨
本节课中我们一起学习了动量优化技术。我们了解到,动量通过引入一个“速度”项,将之前更新的方向以移动平均的方式纳入当前更新。这样做的主要好处是:
- 抑制振荡:使梯度下降路径更平滑、更稳定。
- 加速收敛:有助于在正确的方向上积累速度。
- 逃离平坦区域:在梯度接近零的局部最小值或鞍点附近,积累的速度可以提供推力,帮助模型继续优化。

动量是优化神经网络训练过程的一个简单而强大的工具,常作为后续更高级优化器(如Adam)的基础组件。
深度学习优化算法课程 P94:L12.4 - Adam:结合自适应学习率与动量 🚀
在本节课中,我们将学习一种名为 Adam 的强大优化算法。它是动量与自适应学习率两种技术的结合,旨在更高效、更稳定地训练神经网络。

回顾:动量法
上一节我们介绍了动量法,它通过引入一个“速度”项来帮助抑制随机梯度下降中的振荡,并有助于克服损失曲面上的平坦区域,例如鞍点或局部最小值。

除了动量项,我们现在将学习一个略有不同但相关的概念:自适应学习率。

自适应学习率概念
自适应学习率的核心思想是在正确的时刻加速或减速学习过程。当我们朝着正确的方向前进时,就加快学习速度;当我们改变方向时,就减慢学习速度。
以下是其工作原理的一个简单示例:
- 方向一致时加速:如果连续几次更新都朝着大致相同的方向,我们便加速,因为这很可能是正确的方向,从而能更快收敛。
- 方向改变时减速:如果更新方向频繁改变,我们便减速。这可以防止因噪声或错误方向而偏离太远。
自适应学习率的实现步骤

实现自适应学习率主要分为两步:
第一步:初始化局部增益
为网络中的每一个权重 w_ij 初始化一个局部增益 g_ij。权重更新公式变为:
Δw_ij = -η * g_ij * (∂L/∂w_ij)
这里的 g_ij 可以看作是该权重独有的学习率。
第二步:在训练中动态调整增益
在训练过程中,我们根据梯度方向的一致性来修改每个增益 g_ij:
- 方向一致:小幅增加增益(例如加上一个常数
β),实现平缓加速。 - 方向改变:大幅减小增益(例如乘以一个小于1的因子
β),实现快速减速。
这种设计的直觉类似于驾驶:路况好时平稳加速,需要急转弯或避障时则大力刹车。

RMSprop:一种流行的自适应学习率算法
在深入Adam之前,我们先了解一个重要的自适应学习率变体:RMSprop。它没有正式发表的论文,但在Geoffrey Hinton的课程中被提出并广泛使用。
RMSprop的核心思想是:将学习率除以一个平方梯度的指数移动平均值。这比之前介绍的基础版本更复杂,因为它考虑了梯度幅值的大范围变化。
其更新规则如下:
- 计算平方梯度的移动平均(第二动量):
MS_t = β * MS_{t-1} + (1 - β) * (∂L/∂w_t)^2 - 使用该平均值缩放梯度更新权重:
w_{t+1} = w_t - η / (√MS_t + ε) * (∂L/∂w_t)
这里的 ε 是一个极小值,用于防止除以零的错误。RMSprop不仅实现了自适应学习率,对振荡也有阻尼作用。

Adam:自适应矩估计
现在,我们来学习本节课的核心——Adam 算法。它结合了动量法(一阶矩估计)和RMSprop(二阶矩估计)的思想,是目前深度学习中最常用的优化器之一,因其通常能取得优异性能且需要更少的手动调参。
以下是Adam算法的计算步骤:
1. 计算动量(一阶矩)
m_t = α * m_{t-1} + (1 - α) * (∂L/∂w_t)
这类似于带偏差修正的动量项。
2. 计算RMSprop项(二阶矩)
v_t = β * v_{t-1} + (1 - β) * (∂L/∂w_t)^2
这是平方梯度的指数移动平均。

3. 偏差修正(可选但推荐)
由于 m_t 和 v_t 在初始阶段偏向于0,可以进行修正:
m̂_t = m_t / (1 - α^t)
v̂_t = v_t / (1 - β^t)
4. 更新参数
w_{t+1} = w_t - η / (√v̂_t + ε) * m̂_t
最终更新是用经过自适应学习率(√v̂_t)缩放后的动量(m̂_t) 来调整权重。


总结

本节课我们一起学习了:
- 自适应学习率的概念:根据梯度方向的一致性动态调整每个参数的学习速度。
- RMSprop算法:通过除以平方梯度的移动平均来实现自适应学习率。
- Adam算法:将动量法(捕捉梯度方向)与RMSprop(自适应调整步长)相结合,形成了强大且鲁棒的优化器,在实践中通常能取得优异效果并减少调参负担。


在下一节中,我们将学习如何在PyTorch中实际使用这些优化算法。

课程 P95:L12.5 - 在 PyTorch 中选择不同的优化器 🚀
在本节课中,我们将学习如何在 PyTorch 中使用不同的优化算法。PyTorch 使得应用这些算法变得非常简单。
概述
最常见的优化算法仍然是 SGD(随机梯度下降)及其带动量的版本,以及 Adam。当然也存在其他优化算法,我们将在下一个视频中提及。但在实践中,SGD 和 Adam 是大多数人最常用的两种。
如果你对其他类型的优化器感兴趣并想探索它们,可以参考相关网站,那里有关于可用参数的更详细描述。
在 PyTorch 中使用优化器
之前我们在代码示例和作业中已经使用过 torch.optim.SGD。现在的主要区别是,这里我们为其添加了动量项。
通常,我发现使用 0.01 或 0.001 的学习率效果不错,但你当然需要根据实际情况进行实验。找到合适的学习率很大程度上取决于数据集、权重初始化、数据归一化、批归一化等诸多因素。这是你必须在实践中尝试的事情。
当我使用 SGD 时,我通常也会使用学习率调度器。稍后我将在幻灯片中提供参考代码。如果使用学习率调度器,我通常将初始学习率设为 0.1。
对于 Adam,我通常不做太多调整。我通常使用 0.001 或 0.005 的学习率,这些值对我来说效果很好。但同样,这取决于具体问题。我建议也尝试一些不同的学习率,但大多数情况下,像 0.005 这样的值对我来说效果不错。有时,与 SGD 相比,Adam 更容易找到一个好的学习率。使用 SGD 时,通常需要尝试更多设置才能使其正常工作,而使用 Adam 时,实践中多个学习率都可能表现良好。

保存和加载优化器状态
如果你使用任何带动量的版本(如带动量的 SGD 或 Adam),并且希望保存模型以便后续继续训练,你也必须保存和加载优化器。这是因为优化器现在具有状态,包括动量状态和自适应学习率组件(RMS 组件)的状态。我在大约两三个视频前的早期视频中讨论过保存和加载优化器。因此,如果你使用带动量的版本或 Adam,并希望在之后的时间点继续训练,请确保你知道如何保存和加载优化器状态。
关于 Adam 参数的说明

在之前的视频中,我提到 Adam 有两个参数:用于动量项的 alpha 参数和用于 RMSprop 项的 beta 参数。在 Adam 优化器中,这些参数也可以修改。在 PyTorch 的实现中,它们都被称为 betas,正如论文中一样。第一个值是动量项的 beta(beta1),第二个值是 RMSprop 项的 beta(beta2)。
就我个人而言,我从不更改这些参数。保持其默认值通常对我来说效果很好。在许多深度学习论文中,几乎没有人更改这些参数,他们总是说使用默认参数,因为它们在实践中通常表现相当不错。
实践示例
以下是使用不同优化算法的两个示例。
在第一个示例中,我使用了带有学习率调度器的 SGD,当验证准确率停滞时降低学习率,同时动量项设为 0.9。初始学习率为 0.1。每当验证准确率不再提升(即波动)时,我就降低学习率以稳定训练,使其更接近微调阶段。你可以看到,在开始时训练曲线相当嘈杂,可能在这次降低学习率后变得更加稳定,因为不再有改进,因此我们可以减半学习率以消除振荡。通过此设置,我在测试集上达到了 97.34% 的准确率。
在右侧的第二个示例中,我仅使用了具有默认参数的 Adam。我想我在这里使用了 0.005 的学习率。我也获得了基本相同的性能。但你可以看到,由于我们没有使用学习率调度器,可能在该区域附近看到曲线稍微嘈杂一些。但说实话,两者在实践中都完全可行。我通常发现 Adam 对我来说效果稍好一些,但我也是一个非常没有耐心的人。我想如果我更多地调整 SGD,我可能也能获得类似甚至更好的性能。但 Adam 是,如果你像我一样“懒”,它通常总是表现得很好,我因此而喜欢它。

总结

本节课中,我们一起学习了如何在 PyTorch 中应用 SGD(含动量)和 Adam 优化器。我们了解了如何设置其参数,特别是学习率的选择策略,以及在使用带动量的优化器时保存和加载状态的重要性。我们还通过示例对比了使用学习率调度器的 SGD 与默认 Adam 的表现。对于初学者,Adam 因其对超参数相对不敏感的特性,通常是更简单直接的选择。
优化算法附加主题与研究 📚
在本节课中,我们将探讨优化算法的一些附加主题和研究。我们将回顾不同优化算法的性能比较,讨论它们在训练和泛化方面的表现,并了解一些最新的研究进展。

Adam 算法回顾 🔄
上一节我们介绍了多种优化算法,本节中我们来看看 Adam 算法的原始论文及其背景。
在 2014 年的 Adam 原始论文中,作者比较了多种优化方法。如今,人们提出的优化方法数量可能比当时多出至少五倍。
我们之前没有讨论的一个方法是 SGD with Nesterov Momentum,它是带动量的 SGD 的一个改进版本。理论上,它可能比常规动量方法效果更好。虽然我为此准备了幻灯片,但考虑到课程时长,最终决定不包含它。在实践中,大多数人仍然使用常规的动量方法。
带动量的常规 SGD 即使是最简单的算法之一,也属于最佳算法之列。我将在后续幻灯片中解释原因。
Adam 算法的一个优点是它不需要过多的调参。但 SGD 在泛化性能方面也表现得非常出色。
训练损失与泛化性能 📉
为了说明这一点,我们来看一个关于训练成本的图表。在这篇论文中,作者发现 Adam 在降低训练成本或损失方面表现最佳。

然而,低的训练成本并不意味着最终模型对新数据的泛化能力好。模型可能只是过拟合了。我们当然希望获得低的训练成本,但这还不够。我们可能得到一个低训练成本的模型,但如果过拟合程度很高,那么最终的模型仍然不够好。
相比之下,一个训练损失较高但过拟合程度较低的模型,可能具有更好的泛化性能。
为了更详细地解释,这里有一篇论文的可视化结果(具体年份不详,可能是 2017 年)。

在这篇论文中,作者也研究了不同的优化算法。他们观察了训练误差和测试误差。
有趣的是,他们发现对于 Adam 算法,经过超参数调优的版本和默认版本表现不同。大多数人使用默认版本,但在这个案例中,默认版本表现很差,而调优后的版本表现更好。
与之前幻灯片展示的结果不同,他们发现在训练集上,SGD 的表现优于 Adam。HB(Heavy Ball)是另一种方法。这与之前幻灯片展示的结果略有不同。
尽管如此,这里的关键收获在于测试集上的表现。在测试集上,他们同样发现常规 SGD 的表现优于 Adam。这并非个例,其他研究者也观察到了类似现象:如果调优得当,SGD 通常具有最佳的泛化性能。

一个可能的解释是 SGD 的噪声更大。由于这种噪声,它可能更容易避开某些尖锐的最小值,或者更容易跳出局部最小值。同时,找到一个非常低的损失值可能导致过拟合,因此有时拥有较高的训练损失未必是坏事。
算法比较与混合策略 ⚖️
以下是不同优化算法的一些比较要点。


还有一篇有趣的论文提出,在训练过程中从 Adam 切换到 SGD。论文指出,尽管自适应优化方法(如 Adam、Adagrad 或 RMSprop)在训练结果上表现优异,但它们的泛化能力通常不如 SGD。

这些方法在训练初期表现良好,但在训练后期阶段,SGD 的表现会超越它们。作者研究了一种混合策略:开始时使用自适应方法(如 Adam),然后在适当时机切换到 SGD。
以下是相关比较图表。

图表显示,使用 SGD 可以获得更低的测试误差,即更好的泛化性能。

这里有一篇博客文章,客观地列出了不同算法的优缺点。通常,提出新方法的论文会宣称新方法优于旧方法,但这篇文章只是总结了现有方法并给出了一些结论。
以下是关于内存占用和需调优超参数的比较:
- SGD:状态内存需求最低,但需要调优学习率(可能还有动量项)。它泛化性能最佳,但需要大量的训练和调优。
- SGD with Momentum:加速训练并克服了 SGD 的一些弱点。
- Adam:泛化能力通常不如 SGD。
- AdamW:在泛化方面改进了 Adam,但需要与 Adam 相同的状态内存。它并不比常规 Adam 差太多,并且最近也有人使用。
如果你感兴趣,可以阅读这篇文章以获取不同方法的更详细比较。

架构与优化算法的影响 🏗️
这里有一个关于 Adam 的有趣观点。人们经常使用 Adam 并认为它在许多不同架构上都能表现良好。这篇文章的作者认为,这可能是因为架构的演变使得 Adam 成为了最佳优化器。
作者指出,众所周知 Adam 并不总能提供最佳性能,但大多数时候人们知道可以使用其默认参数,通常就能在问题上获得良好的开箱即用性能。这也是我在实践中观察到的现象。
但这里的论点是,也许不是因为 Adam 本身效果如此之好,而是因为 Adam 在过去效果很好,现在随着新架构的出现,人们继续使用 Adam,而这些新架构的演变预期人们会使用 Adam。因此,Adam 本质上非常适合这些架构,因为这些架构是在考虑使用 Adam 的情况下演变而来的。这里存在一种演化偏差。
作者还说,通常人们在尝试新架构时,会保持优化算法不变。大多数时候,选择的算法是 Adam。这是因为 Adam 是默认优化器。这在某种程度上是一个鸡生蛋还是蛋生鸡的问题。


最近,在一次演讲中看到了这个图表。这是对不同优化算法的一些额外独立评估。这里的论点实际上是,使用什么类型的优化算法并不那么重要。
在左侧,这是一个 ResNet-18(残差网络,我们将在下周讨论),它是一种卷积网络。右侧是一个多层感知机。它们在 CIFAR-10 数据集上进行训练。
蓝色表示训练集准确率,红色表示测试集准确率。你可以看到,对于卷积网络和全连接网络(多层感知机),训练集准确率基本相同。然而,测试集准确率却存在巨大差异。
架构选择对泛化性能的影响非常巨大,即使训练准确率保持不变。因此,架构选择非常重要。
相比之下,对于优化算法的选择,他们使用 VGG-13(另一种卷积网络)进行训练,并采用了不同的优化算法,例如 SGD、带动量的 SGD、带动量的 Nesterov SGD 或 Adam。你可以看到,虽然训练性能略有不同,但测试性能几乎相同。测试性能几乎没有差异,可能只有一两个百分点。
这里我们真正看到的是,我们所使用的优化算法类型并不像我们想象的那么重要。如果你想要获得更好的性能,关注不同的架构可能比关注不同的优化算法更有效。
新兴优化器:Adabelief 🚀
话虽如此,最近也有一个新的优化器变得非常流行。我在几个讨论论坛上都看到了它,那就是 Adabelief 优化器,它本质上是 Adam 的一个修改版本。


左侧是常规的 Adam 优化器,右侧是这个 Adabelief 优化器。在蓝色字体中,你可以看到一些微小的改动。我不想在此讨论太多细节,因为我想结束这节已经足够长的课程。
当然,你可以看到它比其他优化器表现更好。蓝色线代表 Adabelief 优化器,它超越了其他优化器。测试使用了 VGG-11 和 ResNet-34,这两种都是我们将会讨论的卷积网络,并且我们也将开始使用 CIFAR-10 数据集。

总结 📝

本节课中我们一起学习了优化算法的一些附加主题和研究。我们回顾了 Adam 算法及其与其他算法的比较,深入探讨了训练损失与泛化性能的关系,并了解了混合训练策略。我们还讨论了模型架构选择相对于优化算法选择的重要性,并简要介绍了一个新兴的优化器 Adabelief。希望这些内容能帮助你更全面地理解优化算法在深度学习中的应用和最新进展。
课程 P97:L13.0 - 卷积网络简介 🧠
在本节课中,我们将学习卷积神经网络的基本概念、工作原理及其主要应用。卷积神经网络特别擅长处理图像数据,是深度学习领域的重要架构。
应用领域 🌍
卷积神经网络的应用非常广泛。以下是其主要应用方向:
- 图像分类:识别图像中的主要物体。
- 目标检测:定位并识别图像中的多个物体。
- 图像分割:为图像的每个像素分配类别标签。
- 风格迁移:将一种图像的风格应用到另一张图像上。
- 图像生成:创建新的、逼真的图像。
聚焦图像分类 🖼️
为了更清晰地理解卷积网络,我们首先聚焦于图像分类任务。图像分类是卷积网络最基础且流行的应用之一,它有助于我们循序渐进地学习核心概念。
卷积网络基础 ⚙️
上一节我们了解了卷积网络的应用,本节中我们来看看它的核心思想。卷积网络的设计灵感来源于生物视觉皮层,它通过以下关键机制高效处理图像:
- 局部连接:神经元只与输入图像的局部区域连接,而非全连接。
- 权重共享:同一组滤波器(或权重)在输入的不同位置上滑动并重复使用。
- 空间下采样:通过池化等操作逐步降低特征图的空间尺寸。
卷积滤波器与权重共享 🔍
卷积操作的核心是滤波器(或卷积核)。以下是其工作方式:
- 一个小的滤波器(例如 3x3 的矩阵)在输入图像上滑动。
- 在每一个位置,计算滤波器与对应图像区域的点积,生成输出特征图的一个值。
- 这个过程可以形式化表示为:
output[i, j] = sum( input[i+m, j+n] * filter[m, n] )
其中(i, j)是输出位置,(m, n)是滤波器内的索引。
由于同一个滤波器遍历整个图像,参数被共享,这极大地减少了模型的参数量。
互相关与卷积的细微区别 📝
在深度学习的语境中,我们通常所说的“卷积”在技术上是指“互相关”。两者唯一的区别在于卷积操作在计算前会将滤波器旋转180度。然而,由于网络会在训练中学习滤波器权重,因此这个区别在实践中并不重要,可以忽略。
卷积网络中的反向传播 ⬅️
了解了前向传播后,我们自然关心梯度如何反向传播。卷积层反向传播的原理与全连接层类似,但涉及对卷积操作的梯度计算。具体推导较为复杂,但幸运的是,现代深度学习框架(如PyTorch)的自动微分功能可以自动处理这一切,因此我们无需手动实现。

主流卷积网络架构概览 🏗️
卷积网络发展出了多种强大的架构。本节我们简要列举,下一讲将深入探讨:
- VGGNet:通过堆叠简单的3x3卷积层构建深度网络。
- ResNet:引入残差连接,解决了极深网络中的梯度消失问题。
- Inception:在单层内使用不同尺寸的滤波器并行提取特征。
- 全卷积网络:用于图像分割等稠密预测任务。
卷积网络看到了什么? 👀
我们可以通过可视化技术来理解卷积网络内部的工作。例如,可视化第一层的滤波器,通常可以看到它们学习到了边缘、颜色和纹理等基础特征。更深层的神经元则可能对更复杂的图案或物体部件产生响应。
在PyTorch中使用卷积网络 💻
在PyTorch中实现卷积网络非常简单。主要使用 torch.nn.Conv2d 模块。以下是一个简单的示例代码框架:
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.fc = nn.Linear(16 * 16 * 16, 10) # 假设输入图像为32x32
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x)))
x = x.view(-1, 16 * 16 * 16)
x = self.fc(x)
return x
总结 📚


本节课我们一起学习了卷积神经网络的基础知识。我们了解了其广泛的应用场景,深入探讨了其核心机制——局部连接和权重共享,并简要介绍了互相关与卷积的区别、反向传播原理以及主流网络架构。最后,我们还看到了如何在PyTorch中快速搭建一个简单的卷积网络。在下一讲中,我们将深入更高级的主题,如残差网络、Inception模块以及迁移学习。
卷积神经网络(CNN)课程 P98:L13.1 - 常见应用 🖼️🤖

在本节课中,我们将要学习卷积神经网络(CNN)的几种常见应用。CNN是处理图像数据的一种强大神经网络架构,它在计算机视觉领域有着广泛的应用。
图像分类 📸
上一节我们介绍了CNN的基本概念,本节中我们来看看其最直接的应用:图像分类。图像分类任务的目标是让模型识别出输入图像所属的类别。

最简单的应用是二分类问题,例如区分猫和狗。其原理类似于我们之前讨论的逻辑回归,但输入是图像而非简单的特征。图像被输入到网络中,经过一系列卷积层(通常最后还有全连接层),最终输出一个概率分数,表示图像属于某个类别(例如是猫)的可能性。
当然,我们也可以将其扩展为多类别分类,只需在最后使用我们之前讨论过的Softmax层即可。本质上,这与我们使用多层感知机(MLP)所做的类似,区别在于我们使用了卷积层,这将在本课程中详细讨论。
目标检测 🎯
除了常规的分类,CNN另一个常见的应用是目标检测。你可以将其视为分类和目标定位的结合。
目标检测不仅需要识别图像中的物体(例如一辆车),还需要用一个边界框(Bounding Box)将其位置标出,并为该框分配一个类别标签。因此,网络本质上同时学习两个任务:定位物体和分类。
你可以将其视为一种回归任务,网络学习预测边界框的坐标。一个著名的例子是YOLO(You Only Look Once)算法。虽然目标检测是一个重要主题,但由于课程范围限制,我们不会深入探讨。如果你对此感兴趣,YOLO的原始论文是一个很好的起点。
实例分割 ✂️
与目标检测相关的任务是实例分割。它比目标检测更进一步。
实例分割的目标不是仅仅用一个矩形框出物体,而是为图像中的每个目标物体生成一个精确的像素级掩码(Mask)。例如,对于一个人,分割模型会勾勒出人的精确轮廓,而不仅仅是一个包围盒。因此,这是一种更精确的目标检测形式。
Mask R-CNN是解决该问题的经典方法之一。这同样是一个更高级的主题,本课程不会详细展开,但了解其存在是很有帮助的。
人脸识别 👤
另一个重要的应用是人脸识别。我过去在这方面有深入的研究。虽然人脸识别与分类有关,但并不完全相同。
一种方式是将人脸识别构建为一个超多类别的分类问题(例如有10,000个不同的人)。但这种方法计算成本高,且每个类别的样本通常很少,训练起来非常困难。
因此,业界通常采用基于成对或三元组损失(Pairwise/Triplet Loss)的方法。网络学习的是图像的“特征表示”或“嵌入”。其核心思想是:同一人的不同照片,其特征表示应非常相似(距离近);不同人的照片,其特征表示应差异较大(距离远)。然后,我们可以设定一个阈值来判断是否为同一人。

实现这种相似度比较的一种经典网络结构是孪生网络(Siamese Networks)。同样,这是一个更深入的主题,本课程将主要聚焦于用于分类的CNN。
生成对抗网络 🎨
我们将在本课程后续部分讨论的另一个重要应用是生成对抗网络(GANs)。CNN可以用于图像合成。
GANs包含两个部分:生成器(Generator)和判别器(Discriminator)。
- 生成器(G):接收一个随机噪声向量
z(例如从正态分布中采样),通过一个类似“反向”的卷积网络,生成一张图片G(z)。 - 判别器(D):接收一张图片,判断它是“真实的”(来自训练集)还是“生成的”(来自生成器)。
训练过程是一个“博弈”:
- 训练判别器
D更好地区分真实与生成图像。 - 训练生成器
G生成更逼真的图像以“欺骗”判别器。
通过这种对抗训练,生成器最终能合成出看起来非常真实、但并非训练集中原有图像的新图片。GANs用途广泛,例如创造合成训练数据、进行图像编辑等,我们将在后续专门课程中详细讨论。
总结 📝
本节课我们一起学习了卷积神经网络(CNN)的几种常见应用:
- 图像分类:识别图像的整体类别。
- 目标检测:在识别类别的同时,用边界框定位物体。
- 实例分割:为目标物体生成像素级的精确掩码。
- 人脸识别:通过比较特征相似度来识别身份。
- 生成对抗网络(GANs):用于生成新的、逼真的图像。


本课程作为入门,将主要深入讲解图像分类这一基础且核心的应用,为理解更复杂的模型打下坚实的基础。
🖼️ 课程 P99:L13.2 - 图像分类的挑战
在本节课中,我们将要学习图像分类任务所面临的核心挑战。理解这些挑战,有助于我们更好地欣赏卷积神经网络(CNN)的价值,并了解在CNN出现之前,人们是如何尝试解决这些问题的。

图像分类为何困难?
在深入探讨卷积神经网络之前,我们首先需要理解计算机视觉和图像分类本身是一项非常困难的任务。只有认识到其难度,才能充分体会卷积网络所取得的优异性能。
例如,观察下面这张猫的图片。作为人类,你可以轻松地识别出这是同一只猫。

但对于计算机或传统的神经网络而言,这实际上是一项相当困难的任务。为什么会这样呢?
回想一下多层感知机(MLP)的工作原理。我们通常会将图像的行像素拼接成一个很长的向量作为输入。假设第一行、第二行、第三行像素被拉平成一个长向量 X,然后输入到MLP中。网络的第一层隐藏层与输入层是全连接的,形成一个非常密集的网络。
激活值的计算本质上是输入与权重的加权和再加上偏置:activation = sum(weights * inputs) + bias。这意味着我们是在对特征值进行求和。
现在考虑另一种情况:同一张图像,但整体变暗了。由于图像变暗,所有像素值都降低了,这会导致计算出的激活值也相应降低。因此,光照、对比度等条件的变化会严重影响网络的性能。当然,对图像进行归一化处理很重要,但这只是挑战之一。
即使处理了光照问题,图像还可能存在轻微的形变或物体位置的变化。人类依然能认出这是同一只猫。

但在MLP中,假设左边图像某个像素区域在向量中的位置是 X[100:150],而右边图像中同一物体区域可能位于向量的 X[300:350] 位置。由于网络是全连接的,连接这两个不同位置的权重完全不同(W1 vs W2),因此计算出的激活值也会天差地别。物体的位置变化会严重影响传统神经网络的性能。
综上所述,在多层感知机的框架下思考,图像分类实际上是一项非常艰巨的任务。
卷积网络出现前的传统策略
那么,如何确保获得良好的分类性能呢?卷积网络是解决方案之一。但在卷积网络被发明之前,人们有哪些其他策略呢?
策略一:手动特征提取
一种经典策略是手动进行特征提取。这意味着,我们不直接将原始图像输入网络,而是由领域专家研究图像,思考并手动提取出一些关键特征,然后用这些特征进行工作。

以鸢尾花分类为例,专家可能会假设不同类别的鸢尾花在尺寸上存在差异。因此,手动提取的特征可以是:
- 花萼长度
- 花萼宽度
- 花瓣长度
- 花瓣宽度

策略二:提取面部关键点
另一种传统方法应用于人脸识别。人们不直接输入完整的人脸图像,而是开发算法来定位面部关键 landmarks,例如眼睛、鼻子、嘴巴的位置。然后基于这些关键点的几何关系进行比较和识别。

策略三:图像预处理

第三种传统技术是对图像进行预处理,这对于卷积网络至今仍有益处。例如,在MNIST手写数字数据集中,所有数字都已从背景中清理出来,并且基本位于图像中心。
但在现实世界的图像中,目标物体通常不会单独出现。例如,一张猫的图片可能包含公园或森林作为背景。


在传统计算机视觉中,人们会手动或开发一些背景去除工具,先提取出目标物体,再进行分类,这简化了任务。基本操作包括提取、居中、裁剪等。
当然,这些预处理对于卷积网络,尤其是在数据集较小时,仍然是有益的。然而如今,如果拥有海量数据(例如数百万张图像),卷积网络即使不进行精细的预处理,也能表现得很好。因为网络会从数据中学习如何聚焦于图像中的重要部分。例如,在动物分类任务中,网络将学会关注图像中的动物区域。
本质上,卷积网络内部会隐式地学习如何进行特征提取。这也是为什么深度学习有时被称为“特征学习”或“自动特征学习”。深度学习能够隐式地学习如何提取特征,从而替代了繁琐的手动特征工程。

过渡与预告
上一节我们介绍了图像分类的挑战以及卷积网络出现前的解决方案。我们看到,手动特征提取依赖专家知识且费时费力,而传统神经网络又难以处理图像中的平移、光照等变化。
本节中,我们了解到卷积神经网络通过其独特的结构,能够自动、隐式地从数据中学习有效的特征表示,从而克服了这些挑战。
在接下来的视频中,我们将终于可以开始探讨卷积神经网络的工作原理。

我将深入讲解最早的卷积网络架构之一,并逐步描述其中的各个组件。
总结

本节课中,我们一起学习了:
- 图像分类的核心挑战:对于传统全连接网络,图像的光照变化、对比度调整以及物体位置平移都会严重影响分类性能,因为网络缺乏对这类空间不变性的内置处理机制。
- 卷积网络前的传统策略:主要包括手动特征提取(依赖领域知识)、面部关键点检测以及图像预处理(如背景去除、居中裁剪)。这些方法需要大量人工干预,且泛化能力有限。
- 深度学习的优势:卷积神经网络能够通过训练,自动学习到具有平移不变性等特性的特征提取器,实现了“特征学习”,从而避免了繁琐且需要专业知识的手动特征工程。

理解了这些背景和挑战,我们就能带着更明确的问题意识,进入卷积神经网络精彩的世界。


浙公网安备 33010602011771号