斯坦福-CS231-机器视觉进阶课程笔记-全-
斯坦福 CS231 机器视觉进阶课程笔记(全)
课程 P1:L1 - 计算机视觉中的深度学习介绍 🧠👁️
在本节课中,我们将要学习计算机视觉与深度学习的基本概念,了解它们如何结合并推动人工智能领域的发展。我们将回顾这两个领域的历史脉络,并概述本课程的结构与目标。
概述
计算机视觉旨在构建能够处理、感知和理解视觉数据的人工系统。深度学习则是一种受大脑结构启发、包含多层结构的机器学习方法。本课程将聚焦于这两个领域的交叉点,探讨如何利用深度学习技术解决复杂的视觉问题。
计算机视觉简史
上一节我们介绍了计算机视觉的广义定义,本节中我们来看看这个领域是如何发展起来的。
计算机视觉的研究可以追溯到1959年。Hubel和Wiesel通过研究猫的视觉皮层,发现了“简单细胞”和“复杂细胞”,这些细胞分别对特定方向的边缘和更复杂的运动模式产生反应。这项关于视觉信息分层处理的研究,为后来的计算模型提供了重要灵感。
1963年,Larry Roberts完成了可能是第一篇关于计算机视觉的博士论文,其工作涉及从图像中提取边缘和推断三维几何结构。
1966年,MIT提出了“夏季视觉项目”,其雄心勃勃的目标是在一个暑假内“解决”视觉问题,这凸显了早期研究者对问题复杂性的低估。
进入20世纪70年代,David Marr提出了视觉处理的阶段理论,从二维图像逐步推导到三维场景理解。同时,研究者开始尝试识别更复杂的物体,如使用“广义圆柱体”和“图示结构”来表示人体。
80年代,随着数字相机和计算能力的提升,基于边缘检测的物体识别成为主流。John Canny在1986年提出了著名的Canny边缘检测算法,而David Lowe在1987年提出了通过匹配模板边缘来识别物体的方法。
90年代,研究的重点转向通过“分组”进行物体识别,即先将图像分割成语义上有意义的区域,再对这些区域进行识别。
2000年代初期,“特征匹配”成为关键。David Lowe提出的SIFT(尺度不变特征变换)算法,能够提取对旋转、光照变化鲁棒的关键点特征,极大地推动了物体识别的发展。同年,Viola和Jones提出了用于实时人脸检测的算法,这是机器学习在计算机视觉中的一次重要成功应用,并迅速实现了商业化。
随后,大规模标注数据集的出现,如PASCAL Visual Object Challenge,使得数据驱动的机器学习方法成为可能。而ImageNet大型视觉识别挑战赛(ILSVRC)的设立,通过众包方式标注了超过140万张图像,成为了推动领域发展的关键基准。
深度学习简史
在计算机视觉发展的同时,另一条研究主线——深度学习也在并行演进。
1958年,Frank Rosenblatt发明了感知机,这是一种能够从数据中学习的早期硬件系统。从现代视角看,它可以被理解为一个线性分类器。
1969年,Minsky和Papert出版了《感知机》一书,指出了单层感知机的局限性,这在一定程度上导致了相关研究的降温。但书中也提到了多层感知机的潜力,这一点在当时被忽视了。
1980年,福岛邦彦提出了神经认知机,其结构(交替的“S细胞”和“C细胞”)已非常接近现代的卷积和池化操作,但缺乏有效的训练算法。
1986年,Rumelhart等人发表了关于反向传播算法的著名论文,为高效训练多层神经网络(即多层感知机)奠定了基础。
1998年,Yann LeCun等人提出了卷积神经网络,并将其成功应用于手写数字识别,该系统一度被用于处理美国约10%的支票。
2000年代,“深度学习”这一术语开始流行,指代那些具有多层结构的神经网络算法。尽管此时它仍是一个相对小众的研究领域,但许多核心技术和思想都在此期间得以发展。
历史性的交汇:2012年及以后
上一节我们分别回顾了两个领域的历史,本节中我们来看看它们是如何产生革命性交汇的。
2012年,Alex Krizhevsky、Ilya Sutskever和Geoffrey Hinton提出的AlexNet卷积神经网络,在ImageNet竞赛中大幅降低了错误率,震惊了整个计算机视觉界。这标志着深度学习正式成为主流。
自此以后,卷积神经网络被广泛应用于几乎所有计算机视觉任务中,例如:
以下是卷积神经网络的一些典型应用:
- 图像分类:为整张图像分配一个标签。
- 物体检测:在图像中定位并识别多个物体。
- 图像分割:为图像中的每个像素分配语义标签。
- 视频分析:识别视频中的动作和行为。
- 图像生成与艺术风格迁移:创造新的图像或将名画风格应用于照片。
推动这次突破的因素主要有三个:
以下是促成2012年突破的三大要素:
- 算法:长期积累的神经网络与深度学习算法,特别是反向传播。
- 数据:互联网和众包催生的大规模标注数据集,如ImageNet。
- 算力:GPU计算性能的指数级增长,使得训练大型神经网络成为可能。
2018年,Yoshua Bengio、Geoffrey Hinton和Yann LeCun因在深度学习领域的奠基性贡献被授予图灵奖,这充分认可了该技术的巨大影响力。
现状与挑战
尽管深度学习在计算机视觉中取得了巨大成功,但我们距离构建具有人类水平理解能力的视觉系统依然遥远。人类能够从图像中理解物理规律、心理活动和社会关系,而当前的系统还远远做不到这一点。
然而,计算机视觉技术已经并将在以下方面持续改善我们的生活:
以下是计算机视觉技术的一些重要应用方向:
- 娱乐与交互:增强现实、虚拟现实、图像滤镜。
- 安全与交通:自动驾驶汽车、安防监控。
- 医疗健康:医学影像分析、疾病辅助诊断。
- 科学研究:生物多样性监测、天文图像分析。
课程结构与安排
现在,让我们将目光从历史拉回到本课程。我们将采用以下教学理念:
以下是本课程的核心教学理念:
- 注重基础:我们将从零开始实现神经网络,深入理解梯度计算和反向传播,而非仅仅调用高级API。
- 结合实践:在掌握原理后,我们会学习使用PyTorch/TensorFlow等现代工具,并探讨调试与训练大型网络的实际技巧。
- 关注前沿:课程内容将涵盖近5-10年的最新研究成果。
- 保持趣味:我们会探讨图像描述、深度学习艺术等有趣的应用。
课程分为两个部分:
以下是本课程的两个主要部分:
- 基础模块:深入讲解全连接网络、卷积神经网络、循环神经网络的实现与训练细节。
- 应用模块:概述物体检测、图像分割、3D视觉、视频理解、视觉与语言、生成模型等前沿应用。
课程评分将基于六次编程作业、一次期中考试和一次期末考试。我们鼓励同学们通过Piazza进行讨论与合作,但所有提交的代码必须是个人独立完成的。
总结


本节课中我们一起学习了计算机视觉与深度学习的基本定义,回顾了它们从各自起源到2012年产生革命性交汇的历史进程。我们看到了算法、数据和算力的结合如何催生了当前的人工智能浪潮,同时也认识到现有技术面临的挑战。最后,我们了解了本课程注重基础、兼顾前沿与实践的教学理念与整体安排。希望这门课能帮助大家打下坚实的理论基础,并激发你们在这一快速发展的领域中进行探索的兴趣。


🧠 P10:L10- 训练神经网络(上) - ShowMeAI
在本节课中,我们将学习训练神经网络所需的一系列实用技巧和细节。我们将从激活函数的选择开始,探讨数据预处理、权重初始化以及正则化方法,这些都是构建和优化高效神经网络模型的关键步骤。


🔥 激活函数
上一节我们介绍了神经网络的基本架构,本节中我们来看看激活函数的选择及其影响。激活函数为神经网络引入了非线性,使其能够学习复杂模式。然而,不同的激活函数具有不同的特性,选择不当会影响训练效果。
以下是几种常见激活函数的分析:

-
Sigmoid:
σ(x) = 1 / (1 + e^{-x})- 优点:输出范围在(0,1),可解释为概率。
- 缺点:
- 在输入值很大或很小时,梯度会趋近于0(梯度饱和),导致梯度消失,学习缓慢甚至停止。
- 输出不是零中心的,这可能导致权重更新出现“之字形”路径,降低优化效率。
- 涉及指数运算,计算成本较高(在CPU上尤其明显)。
-
Tanh:
tanh(x)- 优点:输出范围在(-1,1),是零中心的。
- 缺点:和Sigmoid一样,存在梯度饱和问题。

- ReLU (Rectified Linear Unit):
f(x) = max(0, x)- 优点:
- 在正区间不饱和,计算非常高效。
- 在实践中收敛速度通常比Sigmoid/Tanh快得多。
- 缺点:
- 输出不是零中心的。
- 在负区间梯度严格为0,可能导致“神经元死亡”(Dead ReLU),即某些神经元永远无法被激活。
- 优点:

- Leaky ReLU:
f(x) = max(αx, x),其中α是一个小的正常数(如0.01)。- 优点:解决了ReLU在负区间梯度为零的问题,避免了神经元死亡。
- 缺点:引入了超参数α需要调整。

-
Parametric ReLU (PReLU):
f(x) = max(αx, x),其中α作为可学习的参数。- 优点:将Leaky ReLU中的斜率α变为可学习的参数,更具灵活性。
-
ELU (Exponential Linear Unit):
f(x) = x if x>0 else α(e^x - 1)- 优点:接近零中心输出,负区间有非零梯度。
- 缺点:涉及指数运算,计算成本高,且引入了超参数α。

- SELU (Scaled ELU):
f(x) = λ * (x if x>0 else α(e^x - 1)),其中λ和α是特定常数。- 优点:具有自归一化特性,有助于训练非常深的网络。

核心建议:对于大多数情况,直接使用ReLU即可,它简单高效。如果你的网络很深或遇到了Dead ReLU问题,可以尝试Leaky ReLU或ELU。避免使用Sigmoid和Tanh作为深层网络的激活函数。
📊 数据预处理


在将数据输入神经网络之前,进行预处理是标准做法。其目的是使数据分布更规整,从而加速训练并提高稳定性。
常见的图像数据预处理步骤包括:
- 零中心化 (Mean Subtraction):减去整个训练集的均值。可以是逐像素的均值图像,也可以是逐通道的RGB均值。
- 归一化 (Normalization):将每个特征(如图像的每个通道)除以其标准差,使其具有单位方差。
预处理公式示例(逐通道):
X_processed = (X - mean) / std
其中,mean和std是仅从训练集计算得到的统计量,然后在训练和测试时使用相同的值进行变换。

为什么有效? 预处理使优化问题的条件更好。如果所有输入数据都是正值(如图像原始像素值0-255),可能导致权重更新全部同号,优化路径曲折。将数据移动到原点附近可以缓解此问题。


⚖️ 权重初始化

权重初始化对深度网络的训练至关重要。糟糕的初始化可能导致梯度消失或爆炸。
- 全零初始化:非常糟糕,会导致所有神经元学习相同的特征,梯度为零。
- 小随机数初始化:例如从高斯分布
N(0, 0.01^2)采样。对于浅层网络可行,但对深层网络,过小的权重可能导致激活值逐层收缩至0(梯度消失),过大的权重可能导致激活值进入饱和区(如Tanh的梯度为0)。


为了在深层网络中保持激活值的方差稳定,研究者提出了特定的初始化方法:

- Xavier初始化 (适用于Tanh等):
W ~ N(0, 1/n_in),其中n_in是层的输入维度。目标是使每一层输出的方差与输入的方差大致相等。 - He初始化 (MSRA初始化,适用于ReLU):
W ~ N(0, 2/n_in)。考虑到ReLU会将一半的激活值置零,因此将方差加倍以进行补偿。

对于残差网络 (ResNet),常见的做法是使用He初始化,并将每个残差块最后一个卷积层的权重初始化为0。这样在初始时,残差块近似于恒等映射,有助于稳定训练初期各层的激活分布。
🛡️ 正则化
正则化用于防止模型在训练数据上过拟合,提高其泛化能力。除了常见的L2正则化(权重衰减),深度学习中还有一些独特的正则化技术。





- Dropout:在训练时,以前向传播的每个神经元都有概率
p(例如0.5)被随机“丢弃”(输出置为0)。这可以防止神经元之间的复杂共适应,相当于训练了一个共享权重的子网络集合。- 训练时:对激活值应用随机掩码。
- 测试时:为了消除随机性并获得确定性输出,所有神经元都参与计算,但激活值要乘以保留概率
(1-p)(或者等价地在训练时对保留的激活值除以(1-p),测试时不做处理,即“反向Dropout”)。
- 批量归一化 (Batch Normalization):本身是一种帮助训练的技术,但其在训练时使用小批量统计量带来的随机性,也起到了正则化的效果。测试时使用移动平均统计量,平均掉了这种随机性。
- 数据增强 (Data Augmentation):通过对训练图像进行随机变换(如水平翻转、随机裁剪、颜色抖动)来人工增加训练数据量,是一种非常有效且免费的正则化手段。测试时,通常对图像进行固定方式的裁剪和缩放(如五宫格裁剪+水平翻转),然后对多个预测结果取平均。


其他基于“训练时添加随机性,测试时平均掉”模式的正则化方法还包括:DropConnect、随机深度、Cutout、Mixup等。对于现代架构(如ResNet),L2权重衰减、批量归一化和数据增强是核心的正则化组合。







本节课中我们一起学习了训练神经网络上半部分的关键实践知识:如何选择合适的激活函数(推荐ReLU),如何进行数据预处理,如何正确地初始化网络权重(使用Xavier或He初始化),以及如何利用Dropout、批量归一化和数据增强等技术来正则化模型,防止过拟合。掌握这些基础技巧是成功训练神经网络的第一步。在下节课中,我们将继续探讨训练过程中的优化策略、超参数调优以及模型评估等进阶主题。
P11:L11- 训练神经网络(下) 🧠
在本节课中,我们将继续探讨训练神经网络时所需的各种实用技巧和策略。上一节我们介绍了一次性设置选择,如激活函数、数据预处理、权重初始化和正则化等。本节中,我们将关注训练过程中的动态调整、超参数选择、模型集成与迁移学习,以及如何将训练扩展到大规模计算环境。
学习率调度 📉
上一节我们介绍了优化算法中的学习率,它是深度学习模型中最重要的超参数之一。本节中我们来看看如何通过调整学习率来优化训练过程。
学习率设置不当可能导致训练失败。设置过高,损失可能迅速爆炸至无穷大;设置过低,训练过程会非常缓慢。理想的学习率应能快速降低损失,同时避免爆炸。
一个常见的策略是使用学习率调度:在训练初期使用较高的学习率以快速进展,随后逐渐降低学习率以收敛到更低的损失值。以下是几种常见的学习率调度方法:
- 阶梯衰减:在预定的训练轮次(如每30个epoch)将学习率突然降低一个固定因子(如乘以0.1)。这种方法在ResNet等模型中常用,但引入了额外的超参数(衰减时机和幅度)。
- 余弦衰减:学习率随训练过程按余弦函数的一半周期从初始值衰减到0。它只依赖初始学习率和总训练轮次这两个原本就需要设置的超参数,因此更易于调优。
- 线性衰减:学习率从初始值线性衰减到0。这种方法在自然语言处理任务中较为常见。
- 常数调度:在整个训练过程中保持学习率不变。对于许多问题,尤其是使用Adam等自适应优化器时,这种方法简单有效,通常是快速实现模型的首选。
核心公式(余弦衰减):
learning_rate(t) = 0.5 * initial_lr * (1 + cos(pi * t / T))
其中 t 是当前epoch,T 是总epoch数。
在训练过程中,监控损失曲线至关重要。建议同时绘制每个迭代的原始损失(散点图)和其移动平均(线图),以观察长期趋势和方差。
早停与模型检查点 ⏹️
为了防止模型在训练后期过拟合或性能下降,我们引入早停策略。
早停的核心思想是:在训练过程中定期(如每5或10个epoch)在验证集上评估模型性能,并保存模型参数。训练结束后,选择在验证集上性能最佳的那个检查点作为最终模型。这样,即使训练后期损失上升,我们也能保留之前的最佳模型。
超参数调优 🎛️
选择合适的超参数对模型性能至关重要。以下是两种主要的搜索策略:
- 网格搜索:为每个超参数指定一组候选值,尝试所有可能的组合。当超参数数量较多时,这种方法计算成本呈指数级增长。
- 随机搜索:为每个超参数指定一个范围,每次试验随机从中采样一个值。研究表明,当某些超参数对性能影响较大而另一些影响较小时,随机搜索比网格搜索更高效,因为它能对重要参数进行更充分的探索。
一个实用的超参数调优流程如下:
- 检查初始损失:在关闭正则化的情况下,检查模型在随机初始化后的损失是否符合预期(例如,交叉熵损失应为
-log(类别数))。不符则说明存在bug。 - 在小样本上过拟合:使用极小的训练数据子集(如1-10个批次),关闭正则化,调整模型架构和学习率,目标是在几分钟内达到100%的训练准确率。这用于验证优化循环和代码的正确性。
- 寻找合适的学习率:使用全部训练数据,仅调整学习率,目标是让损失在最初的100-1000次迭代内显著下降。
- 进行粗略网格搜索:基于前几步的经验,设置一个包含少数几个学习率和正则化强度的粗略网格。训练模型几个epoch,观察验证集性能。
- 迭代优化:分析学习曲线,根据观察结果调整超参数网格,并训练更长时间,循环此过程直至达到满意性能或时间限制。
分析学习曲线 📊
通过观察训练损失和训练/验证准确率曲线,可以诊断模型状态:
- 损失曲线初始平坦后骤降:可能表明初始化不佳。
- 损失快速下降后平台期:考虑引入学习率衰减。
- 学习率衰减后损失立即平台:可能衰减过早。
- 训练和验证准确率持续同步缓慢上升:训练顺利,可考虑延长训练时间。
- 训练准确率持续上升,验证准确率停滞或下降:这是过拟合的典型标志,需要增加正则化强度或收集更多数据。
- 训练和验证准确率几乎相同且较低:这通常是欠拟合的标志,需要增加模型容量或减少正则化。
模型集成与提升 🚀
在训练出单个模型后,可以通过以下方法进一步提升性能:
- 模型集成:训练多个独立模型,在测试时对它们的预测进行平均。对于分类任务,可以平均输出概率分布。集成通常能带来1-2%的性能提升。
- 检查点集成:保存单个模型训练过程中的多个检查点,并对这些检查点的预测进行平均。
- Polyak平均:在训练过程中维护模型权重的指数移动平均,并在测试时使用这个平均权重,有助于平滑训练中的迭代噪声。
迁移学习 🔄
迁移学习允许我们利用在大数据集(如ImageNet)上预训练的模型来解决数据量较小的新任务,这已成为计算机视觉的主流方法。
基本步骤如下:
- 在大型数据集(如ImageNet)上训练一个卷积神经网络。
- 移除该网络的最后一层(分类层)。
- 将剩余部分作为特征提取器,为新任务的数据提取固定特征向量。
- 在这些特征之上训练一个简单的线性分类器(如逻辑回归或SVM)。即使新数据集很小,这种方法也往往能取得良好效果。
如果新数据集规模中等,可以进行微调:
- 同样使用预训练模型,但替换最后一层以适应新任务的类别数。
- 以较低的学习率继续训练整个网络(或部分层),使其适应新数据。
迁移学习的效果很大程度上取决于新任务与预训练任务的相似性以及新数据集的规模。当数据量小且与ImageNet差异大时,迁移学习最具挑战性,但通常仍能提供不错的基线。
大规模训练 🖥️
为了利用多GPU或数据中心级计算资源加速训练,主要采用数据并行策略:
- 将完整的模型复制到每个GPU上。
- 将训练批次数据在批次维度上分割,每个GPU处理一部分。
- 每个GPU独立进行前向和反向传播,计算梯度。
- 将所有GPU上的梯度求和,然后同步更新每个GPU上的模型参数。
进行大规模批次训练时,一个关键技巧是线性缩放规则:如果将批次大小扩大 K 倍(使用 K 个GPU),学习率也应大约扩大 K 倍。此外,在训练初期常采用学习率预热,即从很小的学习率开始,在最初几千次迭代中逐渐增加到目标学习率,以避免初期训练不稳定。
总结 📝


本节课中我们一起学习了训练神经网络下半部分的核心内容。我们探讨了如何通过不同的学习率调度策略动态优化训练过程,介绍了使用早停和检查点来保存最佳模型的方法。我们详细讲解了超参数调优的实用流程,包括网格搜索、随机搜索以及基于学习曲线分析的迭代优化策略。此外,我们还了解了通过模型集成来提升性能的技巧,以及强大的迁移学习范式如何让我们能够利用预训练模型高效解决新任务。最后,我们简要介绍了如何利用数据并行技术将训练扩展到大规模计算环境。掌握这些实践技巧对于成功训练和部署深度学习模型至关重要。
📘 课程 P12:L12 - 循环神经网络

在本节课中,我们将要学习一种新的神经网络类型——循环神经网络。循环神经网络是处理序列数据(如文本、视频、时间序列等)的强大工具,它使我们能够解决许多传统前馈神经网络难以处理的问题,例如机器翻译、图像描述生成和语言建模等。


🔄 循环神经网络简介













上一节我们介绍了前馈神经网络,它处理的是单一的输入和输出。本节中,我们来看看循环神经网络,它专门设计用于处理序列数据。



循环神经网络的核心思想是引入“隐藏状态”,该状态在时间步之间传递信息,使得网络能够记住之前的输入信息。以下是循环神经网络的基本结构:
- 输入序列:
x_1, x_2, ..., x_T - 隐藏状态:
h_t,在时间步t更新 - 输出序列:
y_1, y_2, ..., y_T

隐藏状态的更新遵循一个递归公式,该公式在每个时间步使用相同的权重矩阵,这使得网络能够处理任意长度的序列。

公式:
h_t = f_W(h_{t-1}, x_t)
其中 f_W 是一个带有可学习权重 W 的函数。
🧠 循环神经网络的应用类型
循环神经网络可以应用于多种序列任务,根据输入和输出的结构,主要分为以下几类:

以下是几种常见的应用类型:
- 一对一:单一输入,单一输出(例如图像分类)。
- 一对多:单一输入,序列输出(例如图像描述生成)。
- 多对一:序列输入,单一输出(例如视频分类)。
- 多对多:序列输入,序列输出(例如机器翻译、视频帧分类)。

🏗️ 循环神经网络的基本结构:Vanilla RNN
最简单的循环神经网络称为 Vanilla RNN 或 Elman RNN。它的隐藏状态更新公式如下:
公式:
h_t = tanh(W_{hh} * h_{t-1} + W_{xh} * x_t + b)
y_t = W_{hy} * h_t
其中:
W_{hh}和W_{xh}是权重矩阵。b是偏置项。tanh是激活函数。
代码描述(伪代码):
# 初始化隐藏状态
h = zeros(hidden_size)

for t in range(sequence_length):
# 更新隐藏状态
h = tanh(dot(W_hh, h) + dot(W_xh, x[t]) + b)
# 计算输出
y[t] = dot(W_hy, h)


📊 循环神经网络的训练:随时间反向传播
训练循环神经网络需要使用随时间反向传播算法。该算法通过展开循环神经网络的计算图,将序列处理转化为一个深度前馈网络,然后使用标准反向传播计算梯度。

然而,对于非常长的序列,完全展开计算图会占用大量内存。为了解决这个问题,我们使用截断随时间反向传播算法:


以下是截断随时间反向传播的步骤:

- 将长序列分割成较短的块。
- 对每个块执行前向传播,并记录最后一个时间步的隐藏状态。
- 计算该块的损失,并执行反向传播(仅在该块内)。
- 将记录的隐藏状态作为下一个块的初始隐藏状态。
- 重复此过程,直到处理完整个序列。

🧩 循环神经网络的变体:长短期记忆网络

Vanilla RNN 在处理长序列时容易遇到梯度消失或爆炸的问题。为了解决这个问题,我们引入了长短期记忆网络。
LSTM 通过引入“细胞状态”和三个门控机制(输入门、遗忘门、输出门)来更好地控制信息的流动和记忆。
公式:
i_t = σ(W_i * [h_{t-1}, x_t] + b_i) # 输入门
f_t = σ(W_f * [h_{t-1}, x_t] + b_f) # 遗忘门
o_t = σ(W_o * [h_{t-1}, x_t] + b_o) # 输出门
g_t = tanh(W_g * [h_{t-1}, x_t] + b_g) # 候选细胞状态
c_t = f_t ⊙ c_{t-1} + i_t ⊙ g_t # 更新细胞状态
h_t = o_t ⊙ tanh(c_t) # 更新隐藏状态
其中 σ 是 sigmoid 函数,⊙ 表示逐元素乘法。
🖼️ 循环神经网络在计算机视觉中的应用:图像描述生成
循环神经网络可以与卷积神经网络结合,用于图像描述生成任务。这是一个“一对多”问题的典型例子。
以下是图像描述生成模型的基本流程:
- 使用预训练的 CNN(如 ResNet)提取图像特征。
- 将图像特征作为循环神经网络(如 LSTM)的额外输入。
- LSTM 以序列方式生成描述图像的单词。
- 训练时,使用交叉熵损失函数;推理时,通过采样生成单词序列。

模型结构简化表示:
图像 -> CNN -> 特征向量 -> LSTM -> 单词序列
📝 总结

本节课中我们一起学习了循环神经网络的基本概念、结构和应用。我们了解了 Vanilla RNN 和 LSTM 的工作原理,以及它们如何用于处理序列数据和解决像图像描述生成这样的复杂任务。循环神经网络通过其递归结构和隐藏状态,为深度学习开辟了处理时序和序列数据的新途径。
课程 P13:L13- 注意力机制 🧠
在本节课中,我们将要学习注意力机制。这是一种强大的技术,它允许神经网络在处理序列数据(如机器翻译或图像描述生成)时,动态地关注输入的不同部分。我们将从序列到序列模型的问题出发,逐步推导出注意力机制,并最终介绍基于自注意力的Transformer架构。
从序列到序列模型的问题出发
上一节我们介绍了用于处理序列的循环神经网络。本节中我们来看看如何用RNN构建序列到序列模型,以及它存在的瓶颈。
在序列到序列任务中(例如机器翻译),我们接收一个输入序列(如英文句子),并希望生成一个输出序列(如对应的西班牙语句子)。输入和输出序列的长度可能不同。
我们之前讨论过一种使用编码器-解码器架构的循环神经网络。编码器RNN接收输入序列,并产生一系列隐藏状态。解码器RNN则利用这些信息,逐个生成输出序列的单词。
在这种架构中,编码器需要将整个输入序列的信息“压缩”成一个单一的上下文向量,然后传递给解码器的每一步。这带来了一个问题:对于很长的输入序列(如整个段落或书籍),将所有信息塞进一个向量中是不现实的,这会形成一个信息瓶颈。
引入注意力机制


为了克服上述瓶颈,我们引入注意力机制。其核心思想是:不让解码器在每一步都使用同一个固定的上下文向量,而是允许它在生成每个输出单词时,动态地计算一个新的上下文向量,这个新向量可以聚焦于输入序列的不同部分。
以下是注意力机制的工作步骤:


-
计算对齐分数:在解码器的每个时间步
t,我们有一个解码器隐藏状态s_t。我们使用一个可微分的对齐函数f_att,将s_t与编码器的每一个隐藏状态h_i进行比较,得到一个标量对齐分数e_{t,i}。这个分数表示在生成当前输出时,输入位置i的重要性。- 一个常见且高效的选择是使用缩放点积作为对齐函数:
e_{t,i} = (s_t · h_i) / sqrt(d_k)
其中d_k是向量的维度。除以sqrt(d_k)是为了防止点积结果过大,导致Softmax梯度消失。
- 一个常见且高效的选择是使用缩放点积作为对齐函数:
-
转换为注意力权重:将所有对齐分数
e_{t,i}通过Softmax函数,得到一个概率分布,即注意力权重a_{t,i}。这确保了所有权重之和为1,且更高的分数对应更高的权重。
a_{t,i} = softmax(e_{t})_i -
计算上下文向量:将编码器的所有隐藏状态
h_i按照对应的注意力权重a_{t,i}进行加权求和,得到当前时间步的上下文向量c_t。
c_t = Σ_i (a_{t,i} * h_i) -
生成输出:解码器RNN将上一步的隐藏状态、上一步生成的单词(或起始标记)以及新计算的上下文向量
c_t作为输入,生成当前时间步的输出单词y_t。

这个过程在解码器的每一步重复进行。其优势在于:
- 解决瓶颈:模型不再需要将长序列的所有信息压缩进一个向量。
- 可解释性:通过可视化注意力权重,我们可以看到模型在生成每个输出词时关注了输入序列的哪些部分,这提供了宝贵的模型决策洞察。
- 端到端训练:整个计算图(对齐、Softmax、加权求和)都是可微分的,因此模型可以通过反向传播自动学习在哪里“集中注意力”,而无需人工标注对齐信息。
注意力机制的应用与泛化


注意力机制非常通用,并不局限于序列数据。以下是其应用和泛化的几个方向。
应用于图像描述生成
我们可以将注意力机制用于图像描述生成。模型输入是一张图像,输出是描述文本。
- 使用卷积神经网络处理图像,得到一组空间位置上的特征向量网格(例如,
H x W个C维向量)。 - 解码器RNN在生成每个单词时,使用注意力机制计算一个上下文向量。这个向量是图像特征网格的加权和,权重由解码器当前隐藏状态与每个图像特征向量的对齐分数决定。
- 这样,模型在生成“鸟”这个词时,注意力会集中在图像中鸟的区域;生成“水”时,注意力会转移到水面区域。这模仿了人类观察图像时视线焦点的移动。
抽象为通用层:键、查询、值
我们可以将注意力机制抽象成一个通用的神经网络层。其输入包括:
- 查询:一组向量
Q,代表“我想知道什么”。 - 键:一组向量
K,代表“我可以提供什么信息”。 - 值:一组向量
V,代表“我实际提供的信息”。
在之前的序列翻译例子中:
- 查询是解码器的隐藏状态
s_t。 - 键和值最初都是编码器的隐藏状态
h_i。
抽象层的工作流程如下:
- 计算所有查询与所有键之间的相似度(如缩放点积),得到一个相似度矩阵。
- 对每个查询对应的相似度行进行Softmax,得到注意力权重矩阵。
- 输出是值向量的加权和,权重即注意力权重。
这种抽象允许我们将输入信息用于两个不同目的:匹配(通过键)和贡献内容(通过值),为模型提供了更大的灵活性。
自注意力层
当查询、键、值都来自同一组输入向量时,就得到了自注意力层。它让输入序列中的每个元素都能直接与其他所有元素交互。
自注意力层的一个关键特性是排列等价性:改变输入向量的顺序,输出向量也会以相同的方式改变顺序,但内部的值关系不变。这意味着自注意力层本身不感知顺序。为了给模型注入位置信息,我们通常需要加入位置编码,例如为每个位置学习一个向量并加到输入上。
自注意力层还有几种变体:
- 掩码自注意力:在类似语言建模的任务中,我们要求模型在预测当前位置时只能看到之前的信息(不能看到未来)。这可以通过在相似度矩阵中,将未来位置的分数设置为负无穷大(在Softmax后变为0)来实现。
- 多头自注意力:我们并行运行多个自注意力层(称为“头”),每个头使用不同的可学习投影矩阵将输入映射到键、查询、值空间。最后将所有头的输出拼接起来。这允许模型同时关注来自不同表示子空间的信息。
注意力机制与其他序列处理方式的对比
到目前为止,我们学习了三种处理序列的主要方式:
-
循环神经网络:
- 优点:能很好地处理长序列依赖,单层即可让最终输出依赖于整个输入序列。
- 缺点:计算本质上是顺序的,难以并行化,限制了在GPU等硬件上的训练速度。
-
一维卷积:
- 优点:高度可并行化,每个输出可以独立计算。
- 缺点:单层卷积的感受野有限,需要堆叠很多层才能让一个输出位置看到整个输入序列。
-
自注意力:
- 优点:
- 高度可并行化(核心是矩阵运算)。
- 单层即可让每个输出位置直接看到所有输入位置的信息。
- 缺点:计算所有对之间的交互,内存和计算复杂度随序列长度平方增长,但对于中等长度序列和现代硬件来说通常是可管理的。
- 优点:
Transformer:基于自注意力的强大架构
基于自注意力的优势,研究人员提出了 Transformer 架构。其核心构建块是 Transformer块,它完全依赖于自注意力来进行序列元素间的交互。
一个Transformer块通常包含以下层:
- 多头自注意力层:进行序列内元素间的交互。
- 残差连接与层归一化:添加在自注意力层前后,有助于稳定和加速深度网络的训练。
- 前馈神经网络:一个独立应用于每个位置的全连接网络,用于进行非线性变换。
- 另一个残差连接与层归一化。
通过堆叠多个这样的Transformer块,我们可以构建非常深的模型。Transformer因其卓越的并行能力和对长程依赖的有效建模,已成为自然语言处理领域的基石。
近年来,通过在海量互联网文本上预训练巨大的Transformer模型(如BERT、GPT系列),然后在特定任务上进行微调,取得了革命性的成果。这些模型参数规模已达数十亿甚至上百亿,展示了注意力机制和Transformer架构的强大可扩展性。

总结


本节课中我们一起学习了注意力机制。我们从序列到序列模型的瓶颈出发,引入了动态计算上下文向量的注意力机制。随后,我们将其泛化为一个通用的键-查询-值操作,并深入探讨了自注意力层及其变体(如多头注意力)。通过比较,我们看到了自注意力在并行性和长程依赖建模上的优势。最后,我们介绍了以自注意力为核心的Transformer架构,它已成为现代自然语言处理乃至其他序列建模任务中最强大的工具之一。注意力机制不仅提升了模型性能,还通过可解释的注意力权重,为我们打开了理解模型决策过程的一扇窗。
🎨 P14:L14- 可视化与模型理解
在本节课中,我们将学习如何可视化卷积神经网络(CNN)的内部工作,并理解它们从训练数据中学到了什么。我们还将探讨如何利用这些可视化技术进行一些有趣的应用,如DeepDream和风格迁移。
1️⃣ 可视化第一层卷积滤波器
上一节我们介绍了如何通过可视化第一层卷积滤波器来初步理解CNN。本节中,我们来看看这些滤波器具体捕捉了哪些特征。
卷积神经网络的第一层学习一组滤波器,这些滤波器在输入图像上滑动并计算内积。通过将这些滤波器可视化为RGB图像,我们可以了解网络在第一层寻找的特征。
以下是第一层滤波器可视化的一些观察:
- 不同网络架构(如AlexNet、ResNet、DenseNet)在第一层学习的滤波器通常非常相似。
- 常见的模式包括不同方向的边缘检测滤波器。
- 也常见到检测不同颜色或对比色的滤波器。
- 这与哺乳动物视觉系统中检测定向边缘的细胞有相似之处。
2️⃣ 理解高层特征表示
上一节我们看到了第一层的特征,本节中我们来看看如何理解网络更深层的特征表示。
直接可视化高层卷积层的权重并不直观,因为它们的输入是前一层的特征图,而非原始RGB像素。因此,我们需要其他技术来理解高层特征。
一种方法是关注网络最后的全连接层(例如AlexNet的fc7层)。该层将图像转换为一个高维向量(如4096维),然后通过线性分类器输出类别分数。我们可以通过分析这个向量来理解网络的“理解”。

以下是几种分析该特征空间的方法:
- 最近邻检索:在特征空间中,计算查询图像的特征向量,并在测试集中寻找与之最接近的特征向量。这可以显示网络认为哪些图像在语义上相似,即使像素值差异很大。
- 降维可视化:使用如PCA或t-SNE等降维算法,将高维特征向量投影到2D或3D空间进行可视化。这可以揭示不同类别在特征空间中的分布结构。
3️⃣ 可视化中间层激活与最大激活图像块
上一节我们通过分析最终特征向量来理解网络,本节中我们来看看如何直接窥视中间卷积层的激活。

我们可以可视化网络中间层的激活图。对于给定图像,将特定卷积层的输出特征图(每个通道对应一个滤波器)可视化为灰度图像。通过将激活图与原始图像对齐,可以观察不同滤波器对输入图像中哪些区域有响应。
另一种更深入的方法是寻找最大激活图像块。对于网络中某个选定的神经元(或滤波器),在整个数据集中寻找那些能最大化激活该神经元的图像区域(图像块)。
以下是具体步骤:
- 选择网络中的某一层和该层中的一个特定滤波器。
- 将整个测试集图像输入网络。
- 记录该滤波器在每张图像上激活值最高的空间位置。
- 提取并可视化这些位置对应的原始图像块。
通过观察这些最大激活的图像块,我们可以推断该神经元正在寻找什么模式(例如,狗鼻子、文本、人脸)。
4️⃣ 显著性图:识别重要像素
上一节我们关注了网络内部神经元的激活,本节中我们来看看如何确定输入图像中哪些像素对网络的最终决策最重要。
我们可以通过计算显著性图来识别对分类决策至关重要的图像区域。其核心思想是:遮挡图像的某些部分,观察网络置信度的变化。
一种计算方法是遮挡实验:
- 使用一个灰色方块(或均值图像块)依次遮挡输入图像的不同区域。
- 每次遮挡后,将图像输入网络,记录目标类别(如图像真实类别)的得分。
- 为每个遮挡位置生成一个热力图,颜色深浅表示遮挡该区域后类别得分下降的程度。得分下降越大的区域越重要。
另一种更高效的方法是使用梯度信息。通过反向传播,计算类别分数相对于输入图像每个像素的梯度。梯度绝对值大的像素,意味着微小变化会对类别分数产生较大影响,因此这些像素更重要。
公式表示为:Saliency Map = abs(∇_x f_c(x)),其中 f_c(x) 是类别 c 的分数,x 是输入图像。
这种方法生成的显著性图可以粗略勾勒出图像中物体的轮廓,表明网络确实在关注物体本身,而非背景等无关信息。
5️⃣ 梯度上升:生成最大化激活的图像
上一节我们使用梯度来找出重要像素,本节中我们来看看如何利用梯度主动生成图像,以最大化特定神经元或类别的激活。
我们可以不局限于现有图像,而是通过梯度上升从头合成一张新图像,使其能够最大化激活网络中的某个目标(如某个类别分数或中间层神经元)。
其优化目标可以表示为:
I* = argmax_I [f(I) - R(I)]
其中:
I*是我们要生成的图像。f(I)是目标神经元在图像I上的激活值。R(I)是一个正则化项,用于迫使生成的图像看起来更“自然”(例如,惩罚图像像素值的L2范数)。
优化过程:
- 初始化一张图像(如随机噪声)。
- 将图像输入固定权重的预训练网络,计算目标激活值
f(I)。 - 反向传播,计算目标激活值相对于图像像素的梯度。
- 沿着梯度方向更新图像像素(梯度上升),以增加
f(I)。 - 同时,应用正则化
R(I)来约束图像。 - 重复步骤2-5,直到生成满意的图像。
通过这种方法,我们可以生成一些抽象但能反映网络所学概念的图像(例如,最大化“狗”类别会生成包含狗相关纹理和形状的图像)。
6️⃣ 特征反演与网络信息流
上一节我们生成了最大化特定目标的图像,本节中我们来看看如何通过特征反演来理解网络不同层保留的信息。
特征反演旨在回答:给定网络某一层对某张图像的特征表示,我们能多大程度上重建出原始图像?这有助于理解不同层捕获的信息类型。
过程如下:
- 选取一张图像
y,将其输入网络,并提取某一中间层(如relu4_3)的特征Φ(y)。 - 初始化一张新图像
I(如随机噪声)。 - 优化目标:最小化生成图像
I在该层的特征Φ(I)与目标特征Φ(y)之间的差异,同时加入图像正则化R(I)。
I* = argmin_I [ ||Φ(I) - Φ(y)||^2 + λ R(I) ] - 通过梯度下降优化图像
I。
观察:
- 重建低层特征(如
relu2_2)几乎能完美恢复原始图像,说明低层保留了大部分像素级信息。 - 重建高层特征(如
relu5_3)只能恢复图像的粗略结构和轮廓,而丢失了颜色、纹理等细节,说明高层更关注高级语义信息。
7️⃣ 应用:DeepDream
基于梯度上升的思想,我们可以创建有趣的应用,如DeepDream。其核心思想是:选择一张现有图像,然后放大网络在图像中已经检测到的特征。
算法步骤:
- 将输入图像输入网络,前向传播至某一选定层。
- 将该层的激活值设置为梯度目标(即,让梯度等于激活值本身)。
- 反向传播至输入图像,计算梯度。
- 使用该梯度更新输入图像(实际上是执行梯度上升,以增强该层已激活的特征)。
- 重复此过程,并通常加入一些图像正则化(如多尺度处理、像素裁剪)以获得更美观的结果。
效果是:图像中那些被网络轻微识别出的模式(如边缘、纹理)会被显著增强和扭曲,产生迷幻、梦境般的视觉效果。如果在高层进行,网络可能会“看到”并生成更复杂的物体(如动物、建筑)。
8️⃣ 应用:神经风格迁移
另一个强大的应用是神经风格迁移,它结合了特征重建和纹理合成的思想。
核心思想:生成一张新图像,使其:
- 内容上匹配一张内容图像的高层特征(保留其结构和布局)。
- 风格上匹配一张风格图像的纹理统计信息(捕获其颜色、笔触等风格)。
关键组件:
- 内容损失:使用特征反演中的方法,衡量生成图像与内容图像在高层特征上的差异。
- 风格损失:使用Gram矩阵来捕获风格。Gram矩阵计算了某一层特征图中不同通道之间的相关性,它描述了哪些特征倾向于共同出现,从而捕捉了纹理信息,同时丢弃了空间位置信息。
- 对于一层特征
F(形状为C x H x W),其Gram矩阵G是一个C x C的矩阵,其中元素G_{ij}是特征通道i和j的内积(在空间维度上平均)。
- 对于一层特征
- 总损失:
L_total = α * L_content + β * L_style,其中α和β是权衡内容与风格权重的超参数。
优化过程:从一张随机噪声图像(或内容图像的副本)开始,通过梯度下降同时最小化内容损失和风格损失,逐步迭代生成最终图像。
通过调整内容层、风格层、损失权重等,可以控制输出效果。原始的优化方法速度较慢,后来出现了快速风格迁移方法,训练一个前馈网络来直接进行风格转换,实现了实时处理。
9️⃣ 总结
本节课中我们一起学习了多种理解和可视化卷积神经网络的技术:
- 直接可视化:观察第一层滤波器。
- 特征空间分析:通过最近邻和降维理解高层特征表示。
- 激活分析:可视化中间层激活和寻找最大激活图像块。
- 显著性分析:确定输入图像中对分类决策至关重要的像素。
- 梯度上升:生成能最大化激活网络特定部分的新图像。
- 特征反演:理解网络不同层保留的信息类型。


我们还看到,这些技术不仅有助于模型理解,还能催生出像DeepDream和神经风格迁移这样富有创意和艺术性的应用。这些应用展示了深度学习模型在图像生成和艺术创作方面的巨大潜力。
📚 课程 P15:L15- 目标检测
在本节课中,我们将要学习计算机视觉中的一个核心任务——目标检测。与之前学习的图像分类不同,目标检测不仅需要识别图像中存在哪些物体,还需要精确地定位出每个物体在图像中的位置(即输出边界框)。我们将从基础概念开始,逐步深入到经典的检测算法,并了解如何评估检测器的性能。
🎯 什么是目标检测?
上一节我们回顾了用于理解和可视化卷积神经网络内部工作的技术。本节中,我们来看看一个更实际的任务——目标检测。
目标检测任务输入一张RGB图像,输出则是一组检测到的物体。对于检测到的每个物体,模型需要输出两样东西:
- 类别标签:指明检测到的物体属于哪个预定义的类别(例如猫、狗、汽车)。
- 边界框:用一个矩形框标出物体在图像中的空间范围。
边界框通常用四个实数参数化:中心点的x、y坐标(以像素为单位),以及框的宽度w和高度h。因此,一个边界框可以表示为:
bbox = [center_x, center_y, width, height]
与仅输出单个类别标签的图像分类相比,目标检测带来了几个新的挑战:
- 多输出:图像中可能包含数量不定的物体,模型需要输出可变数量的检测结果。
- 输出类型混合:模型需要同时输出离散的类别标签和连续的边界框坐标。
- 高分辨率需求:为了准确定位小物体,通常需要处理分辨率更高的图像(如800x600像素),这带来了更大的计算负担。
🔍 从单物体检测到多物体检测
首先,让我们考虑一个简化问题:如何检测图像中的单个物体?
我们可以构建一个相对简单的架构:图像输入一个CNN主干网络(如ResNet),得到一个图像的特征向量表示。然后,从这个特征向量分出两个分支:
- 分类分支:一个全连接层,输出每个类别的得分,使用Softmax损失进行训练。这与标准的图像分类器相同。
- 回归分支:另一个全连接层,输出四个实数,代表边界框的坐标,使用回归损失(如L2损失)进行训练。
为了用梯度下降法训练这个同时做两件事的网络,我们需要将两个损失函数合并成一个标量。常见的做法是计算它们的加权和,这被称为多任务损失:
总损失 = w1 * 分类损失 + w2 * 回归损失
然而,现实世界的图像通常包含多个物体。我们需要一种机制,让模型能为每张图像输出可变数量的检测结果。
🪟 滑动窗口与区域提议
一种直观的想法是滑动窗口:训练一个CNN分类器,用于判断图像中某个固定大小的子区域(窗口)是否包含目标物体。然后,将这个分类器在图像的所有可能位置和尺度上滑动应用。
但这种方法计算量巨大。对于一个H x W的图像,考虑所有可能的窗口位置和大小,需要评估的窗口数量是O(H^2 * W^2)量级。对于一张800x600的图像,这可能意味着数千万次CNN前向传播,完全不可行。
为了解决这个问题,研究者引入了区域提议的概念。其核心思想是:使用一个快速、轻量的算法(而非CNN)预先找出图像中可能包含物体的候选区域,数量远少于穷举的滑动窗口。一个经典算法是选择性搜索,它能在CPU上几秒内为一张图像生成约2000个区域提议。
🚀 R-CNN 系列算法
有了区域提议,我们可以构建实用的目标检测器。以下是其演进过程:
1. R-CNN(Region-based CNN)
R-CNN是深度学习目标检测的开创性工作。其流程如下:
- 输入图像,使用选择性搜索生成约2000个区域提议。
- 将每个区域提议变形(Warp) 到固定大小(如224x224)。
- 将每个变形后的区域独立地输入一个CNN(例如在ImageNet上预训练的AlexNet)。
- CNN为每个区域输出两部分:
- 类别得分(C个目标类 + 1个背景类)。
- 一个边界框回归参数,用于微调输入的区域提议框,使其更贴合真实物体。
R-CNN的缺点是速度慢,因为需要为每个区域提议(约2000个)独立运行CNN前向传播。
2. Fast R-CNN
Fast R-CNN改进了流程,大幅提升了速度。关键改变是交换了卷积和区域裁剪的顺序:
- 将整张图像输入CNN主干网络,得到整张图的卷积特征图。
- 将选择性搜索生成的区域提议投影到这张特征图上。
- 对每个投影后的区域,在特征图上进行RoI池化操作,将其转换为固定大小的特征网格。
- 将这些固定大小的特征输入一个轻量的“每区域”网络,最终输出类别和精修后的边界框。
这样做的好处是,耗时的卷积计算只在整张图像上执行一次,所有区域提议共享这部分计算。
RoI池化是一种将不同大小的区域转换为固定大小特征的可微分操作。它将区域划分成固定数量的子窗口(如2x2),然后在每个子窗口内进行最大池化。
3. Faster R-CNN
在Fast R-CNN中,区域提议(选择性搜索)成了新的速度瓶颈。Faster R-CNN的创新在于用神经网络自己生成区域提议。
它在Fast R-CNN的基础上,在主干网络输出的特征图后,添加了一个区域提议网络:
- RPN在特征图的每个位置上放置
k个不同尺度和长宽比的锚点框。 - 对于每个锚点框,RPN预测两个值:
- 物体性得分:该锚点框内包含物体的概率(二分类:是物体/背景)。
- 边界框变换参数:用于将锚点框微调成更准确的区域提议。
这样,区域提议的生成也变成了可学习的、基于CNN的快速过程。Faster R-CNN因此成为一个两阶段检测器:
- 第一阶段(RPN):在整张图像上运行,生成区域提议。
- 第二阶段(Fast R-CNN头部):对每个区域提议进行分类和边界框精修。
⚡ 单阶段检测器
既然RPN已经能给出可能包含物体的位置(锚点框),那么能否直接在这些位置上完成分类,省去第二阶段?这就是单阶段检测器的思想,如SSD和YOLO。
单阶段检测器通常:
- 在特征图的每个位置预设锚点框。
- 使用一个卷积网络,直接为每个锚点框预测类别得分(C+1类)和边界框变换参数。
- 在单次前向传播中完成所有预测,速度通常更快。
与两阶段方法相比,单阶段检测器速度优势明显,但历史上精度略低。不过,随着技术发展(如特征金字塔网络FPN的引入),两者的性能差距已经大大缩小。
📊 评估指标:交并比与平均精度均值
如何衡量目标检测器的好坏?我们需要新的评估指标。
交并比
交并比是衡量两个边界框重叠程度的指标。计算公式为:
IoU = (预测框 ∩ 真实框) / (预测框 ∪ 真实框)
IoU值在0到1之间。通常,IoU > 0.5被认为是一个可以接受的匹配。
非极大值抑制
检测器通常会为同一个物体输出多个高度重叠的边界框。非极大值抑制是一种后处理算法,用于去除冗余检测框:
- 选择置信度最高的检测框。
- 计算该框与所有其他框的IoU。
- 移除所有IoU超过某个阈值(如0.7)的框(视为重复检测)。
- 在剩余的框中重复步骤1-3,直到没有框剩下。
平均精度均值
目标检测的常用综合评估指标是平均精度均值。计算过程如下:
- 对每个类别单独计算平均精度:
- 将模型在该类别上所有的检测结果按置信度排序。
- 从高到低遍历每个检测,计算当前的精确率和召回率,绘制P-R曲线。
- AP即是该P-R曲线下的面积。
- 对所有类别的AP取平均,即得到mAP。
在实际研究中,为了更全面评估定位精度,常计算不同IoU阈值(如0.5, 0.75)下的mAP,并取平均。
🏁 总结与实用建议
本节课中我们一起学习了目标检测的核心概念与方法。我们从图像分类与目标检测的区别讲起,探讨了单物体检测的多任务损失。为了应对多物体检测,我们介绍了滑动窗口的局限性以及区域提议的解决方案。随后,我们深入讲解了R-CNN系列的演进:从需要独立处理每个区域的R-CNN,到共享卷积计算的Fast R-CNN,再到引入RPN实现端到端学习的Faster R-CNN。我们还了解了更快的单阶段检测器思想。最后,我们学习了用于评估检测器性能的交并比、非极大值抑制和平均精度均值等关键指标。
目标检测是一个复杂且快速发展的领域,涉及大量工程细节和调优技巧。因此,在实践中,不建议从头开始实现目标检测系统。推荐使用成熟的开源框架,例如:
- Detectron2:Facebook基于PyTorch的下一代目标检测库,包含了当前众多先进算法的实现和预训练模型。
- TensorFlow Object Detection API:Google基于TensorFlow的目标检测框架。
这些工具能帮助你快速地将强大的目标检测能力应用到自己的项目中。


下节预告:在下一讲中,我们将继续探讨与物体定位相关的计算机视觉任务,例如实例分割和语义分割。
🎯 课程 P16:L16 - 目标检测与图像分割
在本节课中,我们将深入学习计算机视觉中的目标检测与图像分割任务。我们将回顾目标检测的核心概念,并探讨不同类型的图像分割任务,包括语义分割、实例分割和全景分割。课程内容将涵盖从经典方法到现代深度学习方法的关键技术和原理。
📊 目标检测的深度学习进展
上一节我们介绍了目标检测的基本概念,本节中我们来看看深度学习如何推动该领域的巨大进步。
下图展示了从2007年到2015年目标检测在Pascal VOC数据集上的性能进展。纵轴是平均精度均值(mAP)指标。

从图中可以看出,在2007年至2012年间,人们主要使用非深度学习方法进行目标检测,性能提升缓慢,并在2010年至2012年间趋于平缓。然而,从2013年开始,当深度学习首次应用于目标检测任务时,性能出现了巨大的飞跃。这种提升并非一次性事件,随着更优的深度学习目标检测方法(如Fast R-CNN、Faster R-CNN)的出现,性能持续攀升。
值得注意的是,该图表在2016年结束,因为此后Pascal VOC数据集被认为过于简单,研究社区转向了更具挑战性的数据集。目前,该数据集上的最佳性能已远超80%,但大多数方法已不再在此数据集上进行测试。
🔍 R-CNN系列模型训练流程详解
上一节我们快速介绍了R-CNN系列模型,本节中我们将深入探讨这些模型的训练流程,特别是如何为区域建议分配标签。
在训练R-CNN风格网络时,我们接收一个RGB输入图像以及该图像中所有物体的真实边界框和类别标签。流程如下:
- 生成区域建议:在输入图像上运行区域建议方法(如选择性搜索),得到一组可能包含物体的区域。
- 匹配与标记:将生成的区域建议与真实边界框进行比较,为每个区域建议分配标签。这是上节课未详细讨论的关键步骤。
以下是匹配过程的详细说明:
- 真实边界框:图中亮绿色框代表图像中两个狗和一只猫的真实边界框。
- 区域建议:青色框代表区域建议方法生成的所有候选区域。
- 标签分配:
- 正样本:与某个真实边界框高度重叠(通常通过交并比IoU阈值判断,如IoU > 0.5)的区域建议。这些区域应被网络分类为包含物体。
- 负样本:与所有真实边界框重叠度很低(如IoU < 0.3)的区域建议。这些区域不包含目标物体,应被分类为背景。
- 中性样本:与真实边界框有部分重叠,但既不够正也不够负的区域建议(例如IoU在0.3到0.5之间)。在训练时通常忽略这些框。
在训练过程中,我们裁剪出所有正样本和负样本区域建议对应的像素,并将其调整为固定大小(如224x224)。此时,问题类似于图像分类任务,但输入是这些图像裁剪块。
对于每个区域建议,网络需要预测两件事:
- 类别标签:对于正样本,其目标类别标签是与之匹配的真实边界框的类别。对于负样本,其目标类别是“背景”。
- 边界框回归变换:一个将原始区域建议坐标变换到更精确的目标边界框坐标的变换参数。只有正样本才有回归损失,负样本没有回归目标。
标签来源的澄清:区域建议的类别标签来源于匹配过程。我们将每个区域建议与重叠度最高的真实边界框配对,并将该真实框的类别标签分配给该区域建议。回归目标则是计算能将区域建议坐标“对齐”到其匹配的真实边界框坐标所需的变换参数。
在训练时,分类损失应用于所有样本,而回归损失仅应用于正样本。正负样本的比例是训练中需要调整的超参数。
对于Faster R-CNN这类在线学习区域建议的网络,这种匹配需要在训练过程中动态进行,这增加了实现的复杂性。
🧮 从RoI Pooling到RoI Align
在从Slow R-CNN过渡到Fast R-CNN时,我们引入了特征裁剪操作。Fast R-CNN交换了卷积和裁剪的顺序,先计算整个图像的特征图,再从特征图中裁剪出每个区域建议对应的特征。
上节课我们介绍了RoI Pooling操作,但其存在两个问题:
- 特征未对齐:由于需要将区域建议映射到特征图的离散网格上并进行两次量化(取整)操作,导致特征位置与原始图像区域不精确对齐。
- 不可微分:由于量化操作,梯度无法从区域特征反向传播到区域建议的坐标输入。
RoI Align操作解决了这些问题。其核心思想是消除所有量化操作,使一切连续化。
RoI Align的工作流程:
- 将区域建议投影到特征图上,不进行任何取整。
- 将投影后的区域均匀划分为若干子区域(如2x2)。
- 在每个子区域内均匀采样固定数量的点(如4个)。
- 对于每个采样点,使用双线性插值根据其周围四个最近邻特征网格点的值来计算该点的特征值。
- 对每个子区域内的所有采样点特征进行聚合(如最大池化),得到该子区域的最终输出特征。
双线性插值公式(对于位置(x, y)的特征值f(x, y)):
f(x, y) ≈ f(Q11) * (x2 - x)(y2 - y) + f(Q21) * (x - x1)(y2 - y) + f(Q12) * (x2 - x)(y - y1) + f(Q22) * (x - x1)(y - y1)
其中,Q11=(x1,y1), Q21=(x2,y1), Q12=(x1,y2), Q22=(x2,y2)是(x,y)周围的四个整数坐标点。
RoI Align的优势:
- 更好的对齐:由于使用连续坐标和插值,特征与原始图像区域对齐更精确。
- 完全可微分:梯度可以通过插值操作反向传播到图像特征图和输入的区域建议坐标。
🔲 无锚框目标检测:CornerNet

之前讨论的Faster R-CNN和单阶段检测器都依赖于预设的锚框。一个有趣的问题是:能否设计一个完全不依赖锚框,直接预测边界框的目标检测系统?
CornerNet是一个创新的解决方案。它改变了边界框的表示方式,用左上角和右下角两个关键点来定义一个边界框。
网络架构与流程:
- 将图像通过一个骨干CNN,得到图像级特征。
- 左上角预测分支:预测一个热图,对于每个空间位置和每个物体类别,输出该位置是该类别物体边界框左上角的概率。
- 右下角预测分支:类似地,预测每个位置是右下角的概率。
- 嵌入向量预测:网络还为每个位置预测一个嵌入向量。关键思想是:属于同一个边界框的左上角和右下角,其嵌入向量应该非常相似。
- 匹配与输出:在测试时,通过计算左上角和右下角嵌入向量之间的距离,将配对的角点组合成最终的边界框输出。
这种方法提供了一种与锚框范式完全不同的目标检测思路。

🖼️ 语义分割
语义分割的任务是为输入图像中的每一个像素分配一个类别标签。它不区分同一类别的不同实例(例如,图像中的两只猫会被标记为同一个“猫”类别,而不是猫1和猫2)。
一种低效的方法是使用滑动窗口,为每个像素提取一个图像块并用CNN分类。但更高效的方法是使用全卷积网络。
全卷积网络是一种没有全连接层或全局池化层的CNN,仅由卷积层组成。输入是图像,输出是一个张量,其空间尺寸与输入相同(或按比例缩放),通道数等于类别数。每个空间位置的特征向量经过softmax后,表示该像素属于各个类别的概率分布。训练使用逐像素交叉熵损失。
然而,简单的全卷积网络存在两个问题:
- 感受野有限:堆叠多个3x3卷积虽能增大感受野,但需要很多层才能覆盖大区域。
- 计算成本高:在高分辨率图像上直接进行全尺寸卷积非常耗时。
因此,实际的语义分割网络通常采用编码器-解码器结构:
- 编码器(下采样):通过卷积和池化降低分辨率,扩大感受野,减少计算量。
- 解码器(上采样):将低分辨率特征图恢复到输入图像尺寸,以进行逐像素预测。
⬆️ 上采样方法
上采样是解码器的关键操作。以下是几种常见方法:
- 最近邻上采样:将输入特征图中的每个值复制到输出中对应的一个区域。
# 伪代码示例:最近邻上采样(2倍) output[x*2, y*2] = input[x, y] output[x*2+1, y*2] = input[x, y] output[x*2, y*2+1] = input[x, y] output[x*2+1, y*2+1] = input[x, y] - 双线性/双三次插值:在输出网格的连续位置采样,并使用周围输入点的加权平均值来计算输出值。双线性使用4个最近邻点,双三次使用16个点,能产生更平滑的结果。
- 最大反池化:与最大池化操作相关联。在下采样进行最大池化时,记录最大值所在位置。在上采样时,将值放回记录的位置,其他位置填零。这有助于保持特征的位置对齐。
- 转置卷积:一种可学习的上采样操作。其前向传播相当于普通卷积的反向传播。通过设置步长小于1,它可以在输出中为输入的每个元素生成多个值,从而实现上采样。
转置卷积操作示意(1维简化):
输入 [a, b], 卷积核 [x, y, z], 输出步长模式。
输出计算过程涉及将加权后的卷积核复制到输出张量的不同位置并求和。
选择哪种上采样方法通常与下采样方法配对:如果下采样使用平均池化,上采样可选用最近邻或插值;如果下采样使用最大池化,则最大反池化是更自然的选择。
🧩 从目标检测到实例分割:Mask R-CNN
实例分割结合了目标检测和语义分割:它需要检测出图像中的每个物体实例,并为每个实例预测一个精确的像素级掩码(只针对“物体”类别,不针对“背景”类别如天空、草地)。
Mask R-CNN 是建立在Faster R-CNN之上的经典实例分割框架。其核心思想是:在Faster R-CNN的现有分支(分类和边界框回归)基础上,增加一个并行的掩码预测分支。
网络流程:
- 输入图像经过骨干网络提取特征。
- 区域建议网络生成候选区域。
- 使用RoI Align从特征图中提取每个区域的特征。
- 对于每个区域,同时进行:
- 分类:预测物体类别。
- 边界框回归:微调边界框。
- 掩码预测:通过一个小型全卷积网络,预测一个固定大小(如28x28)的二值分割掩码,表示该区域内哪些像素属于该物体。
训练目标:对于每个区域建议,其掩码分支的训练目标是一个二值掩码,该掩码由真实实例掩码裁剪并缩放到区域建议框内得到,且仅针对该区域建议被分配到的类别。
Mask R-CNN能够同时输出高质量的检测框和分割掩码,是实例分割领域的强大基准模型。
🌈 其他高级分割与感知任务

-
全景分割:旨在统一语义分割和实例分割。它为图像中的每个像素分配一个标签:对于“物体”类别,标签包含实例ID以区分不同个体;对于“背景”类别,则只使用语义标签。这是一个更全面的分割任务。
-
关键点检测:例如人体姿态估计。Mask R-CNN也可以扩展到此任务,通过为每个区域增加一个关键点预测分支,输出每个关键点的热图。这可以实现联合的目标检测、实例分割和姿态估计。
-
密集描述生成:结合目标检测和图像描述,为图像中的多个区域生成自然语言描述。这可以通过在检测器区域特征上附加一个LSTM或Transformer描述模型来实现。

- 3D形状预测:在目标检测的基础上,为每个检测到的物体预测其3D形状。这同样遵循了“检测+附加预测头”的范式。
📝 课程总结
本节课我们一起深入学习了计算机视觉中的核心定位任务:
- 目标检测:回顾了R-CNN系列模型的训练细节,深入理解了区域建议的匹配与标签分配过程,以及从RoI Pooling到RoI Align的改进。还简要了解了无锚框检测器CornerNet的创新思路。
- 图像分割:
- 语义分割:学习了全卷积网络和编码器-解码器结构,以及各种上采样方法(最近邻、插值、反池化、转置卷积)。
- 实例分割:掌握了如何通过Mask R-CNN在目标检测框架上增加掩码预测分支来实现实例级别的分割。
- 全景分割:了解了这个统一分割任务的目标。
- 扩展任务:认识到目标检测框架可以作为基础,通过添加不同的预测头来支持多种高级感知任务,如关键点检测、密集描述和3D形状预测。


目标检测是本节课要求掌握的重点,而对其他任务的了解有助于拓宽在计算机视觉实际应用中的视野。下节课我们将探讨如何使用深度学习处理3D数据。
🧠 课程 P17:3D计算机视觉
在本节课中,我们将学习如何将三维信息融入神经网络模型。我们将重点探讨两类3D问题:从单张图像预测3D形状,以及基于3D输入数据进行分类或分割。课程将涵盖多种3D形状表示方法,并介绍相应的神经网络架构。
🌍 3D计算机视觉概述
上一节我们介绍了多种计算机视觉任务,如语义分割、目标检测和实例分割,这些任务主要关注图像中物体的二维形状或结构。然而,我们生活的世界实际上是三维的。因此,本节我们将探讨如何为神经网络模型添加第三维空间信息,以理解和处理三维数据。
我们将聚焦于两类3D问题:
- 从单张图像预测3D形状:输入为RGB图像,输出为图像中物体的3D形状表示。
- 基于3D输入数据进行分类或分割:输入为3D数据,输出为分类或分割结果。
本节所有任务均基于全监督学习假设,即我们拥有包含输入图像和对应3D形状(或标签)的训练集。
需要注意的是,3D计算机视觉领域非常广泛,本节仅涵盖形状预测和分类等基础问题。还有许多其他重要任务(如运动恢复结构)因时间限制无法详细展开。
📐 3D形状表示方法
在讨论3D形状预测和分类之前,我们需要明确“3D形状”的具体表示方式。实践中,人们常用五种不同的3D形状表示方法,每种都有其优缺点。我们将逐一介绍这些表示方法,并探讨如何用神经网络处理或预测它们。
以下是五种表示同一3D形状的不同方法:
1. 深度图(Depth Map)
深度图是一种简单的3D形状表示方法。它为输入图像的每个像素分配一个距离值,表示从相机到该像素对应真实世界点的距离(单位通常为米)。
公式:
深度图 = 高度 × 宽度 的网格,每个网格值为深度值
与传统的RGB图像(高度 × 宽度 × 3)不同,深度图是单通道图像(高度 × 宽度 × 1)。常将RGB图像与深度通道结合,形成RGB-D图像(四通道)。由于深度图无法表示被遮挡的物体部分,有时被称为“2.5D”表示。
深度图数据可通过3D传感器(如Microsoft Kinect、iPhone Face ID)直接捕获。任务之一是从RGB图像预测深度通道,即估计每个像素到物体的距离。
网络架构:可以使用全卷积网络(FCN),其最后一层卷积输出单通道深度图。训练时,需使用损失函数比较预测深度与真实深度。
尺度深度模糊性问题:从单张2D图像无法区分远处的大物体和近处的小物体。因此,绝对尺度和深度是模糊的。为解决这一问题,可以使用尺度不变损失函数,该函数允许预测深度与真实深度之间存在全局缩放因子。
参考论文:Eigen et al., "Depth Map Prediction from a Single Image using a Multi-Scale Deep Network", NIPS 2014.
2. 表面法线图(Surface Normal Map)
表面法线图为每个像素分配一个单位向量,表示该像素对应物体表面的方向。通常用RGB颜色表示法线方向(例如,蓝色表示向上,红色表示向左,绿色表示向右)。
预测方法:与深度图类似,使用全卷积网络,输出三通道图像表示法线向量。损失函数通常基于预测向量与真实向量之间的角度(如点积)。
联合训练:可以训练一个网络同时进行语义分割、表面法线估计和深度估计,从单张RGB图像预测所有信息。
3. 体素网格(Voxel Grid)
体素网格将3D空间划分为规则网格,每个网格单元表示是否被物体占据。这类似于《我的世界》中的方块表示法。
优点:概念简单,易于理解。
缺点:需要高分辨率才能捕捉细节,计算和内存开销大。
处理体素网格的神经网络:可以使用3D卷积神经网络。输入为3D体素网格(例如30×30×30),每个体素为二值(占据与否)。网络由多个3D卷积层组成,最后接全连接层或全局池化层进行分类。
公式:
输入维度:深度 × 高度 × 宽度 × 1(通道)
3D卷积核:深度 × 高度 × 宽度 × 输入通道 × 输出通道
从图像预测体素网格:输入为RGB图像(高度 × 宽度 × 3),输出为体素网格(深度 × 高度 × 宽度 × 1)。网络架构可先使用2D CNN处理图像,然后通过全连接层增加空间维度,最后使用3D卷积进行上采样。
体素管表示(Voxel Tube):为减少计算开销,可使用仅含2D卷积的网络预测体素。最后一层2D卷积的输出通道数等于体素网格的深度维度,从而将通道解释为深度。
内存优化:高分辨率体素网格内存消耗大。可采用多分辨率体素网格(如八叉树)或嵌套形状层(Nested Shape Layers)等技巧进行优化。
4. 隐式表面(Implicit Surface)
隐式表面将3D形状表示为一个函数,该函数输入3D空间坐标,输出该点是否被物体占据的概率。物体的表面由概率值为0.5的等值面表示。
符号距离函数(SDF):一种常见的隐式表示,输出为点到表面的有符号距离。
学习方法:使用神经网络学习该函数。训练时,采样3D空间点,并标注是否在物体内部。训练完成后,可通过采样和等值面提取得到显式形状。
优点:表示紧凑,可处理任意拓扑结构。
缺点:实现复杂,提取显式形状需额外步骤。
5. 点云(Point Cloud)
点云用一组3D点表示物体表面。点的密度可自适应调整,以捕捉细节。
优点:表示灵活,适用于精细结构。
缺点:需后处理才能可视化或用于某些应用。
处理点云输入的神经网络:常用PointNet架构。该架构对每个点独立应用多层感知机(MLP),然后通过最大池化聚合全局信息,最后接全连接层进行分类。
公式:
输入:P × 3(P个点,每个点3维坐标)
独立MLP:每个点 → D维特征
最大池化:P × D → D(全局特征)
分类层:D → 类别数
从图像生成点云:输入RGB图像,输出点云。损失函数常用倒角距离(Chamfer Distance),该距离度量两个点云之间的相似性。
倒角距离公式:
CD(S1, S2) = Σ_{x∈S1} min_{y∈S2} ||x-y||^2 + Σ_{y∈S2} min_{x∈S1} ||x-y||^2
6. 三角网格(Triangle Mesh)
三角网格由顶点和三角形面组成,是计算机图形学中常用的表示方法。
优点:显式表示表面,易于渲染和纹理映射。
缺点:处理拓扑变化复杂。
从图像预测三角网格:可使用Pixel2Mesh方法。关键思想包括:
- 迭代网格细化:从初始模板网格(如球体)开始,逐步变形以匹配目标形状。
- 图卷积:在网格结构上定义卷积操作,更新顶点特征。
- 顶点对齐特征:将图像特征通过投影和双线性插值对齐到网格顶点。
- 损失函数:使用倒角距离比较预测网格与真实网格。
倒角距离的采样:需从网格表面采样点云。训练时,需通过采样操作进行反向传播,具体方法可参考ICML 2019相关论文。
📊 3D形状评估指标
比较3D形状时,需要合适的评估指标。常用指标包括:
- 3D交并比(3D IoU):类似2D IoU,但可能不够敏感。
- 倒角距离(Chamfer Distance):如前述公式,但对异常值敏感。
- F1分数:基于点云采样的精度和召回率计算,更稳健。
F1分数计算:
- 精度:预测点周围阈值半径内存在真实点的比例。
- 召回:真实点周围阈值半径内存在预测点的比例。
- F1 = 2 * (精度 * 召回) / (精度 + 召回)
📷 坐标系选择
预测3D形状时,需选择坐标系:
- 规范坐标系(Canonical Coordinates):固定物体方向(如前、上、右)。
- 视图坐标系(View Coordinates):与输入图像对齐。
建议:使用视图坐标系,因为特征对齐更好,有助于泛化。
🗃️ 常用数据集
- ShapeNet:包含约5万个3D CAD模型,涵盖50个类别。常渲染成图像用于训练。
- Pix3D:包含真实世界图像及对齐的3D网格模型(主要是IKEA家具)。数据收集方法巧妙,结合了网络图像和3D模型。
🛠️ 综合应用:Mesh R-CNN
Mesh R-CNN结合了2D目标检测和3D形状预测。流程如下:
- 使用Mask R-CNN检测图像中的物体并预测掩码。
- 对每个检测到的物体,使用体素管网络预测粗略体素表示。
- 将体素转换为网格,作为初始网格。
- 通过迭代细化和图卷积得到精细三角网格。
损失函数:倒角距离 + 网格正则化(如边长的L2范数),以避免退化网格。
结果:能够预测具有复杂拓扑结构的网格,并推断被遮挡部分。
失败案例:2D识别失败通常导致3D预测失败,表明改进2D识别可能提升3D性能。
🎯 总结
本节课我们一起学习了如何将第三维空间信息融入神经网络。我们探讨了从单张图像预测3D形状和基于3D输入进行分类的两类问题,并详细介绍了五种3D形状表示方法:深度图、表面法线图、体素网格、隐式表面、点云和三角网格。每种表示方法都有相应的神经网络架构和损失函数。最后,我们介绍了Mesh R-CNN这一综合应用,展示了如何结合2D检测和3D形状预测。


下一节课我们将讨论视频处理,即如何为神经网络添加时间维度。

🎬 P18:L18- 深度学习中的视频处理

在本节课中,我们将要学习如何使用深度学习处理视频数据。视频本质上是随时间展开的图像序列,这为神经网络模型引入了一个新的维度——时间维度。我们将探讨如何构建能够理解视频内容(如识别动作或活动)的模型,并了解处理这类数据所面临的独特挑战。
📹 视频:带有时间维度的图像
上一节课我们介绍了如何将卷积网络从二维图像处理扩展到三维形状处理,我们通过添加第三个空间维度实现了这一点。今天,我们将探讨另一种为神经网络模型增加额外维度的方法:不是添加空间维度,而是添加时间维度。

视频可以看作是一个图像序列在时间上的展开。这是一种特殊的三维表示,它包含两个空间维度和一个时间维度。与上一节处理三个空间维度不同,这种“2D空间+1D时间”的结构带来了额外的挑战,因为我们可能希望以不同的方式处理空间和时间信息。

对于视频处理任务,我们的网络通常需要从处理三维张量(2D图像数据)转向处理四维张量。例如,我们可以将一个视频看作一个四维张量,其形状为 (T, C, H, W),其中:
- T 是时间或时间维度。
- C 是通道维度(例如,原始输入视频的RGB三个颜色通道)。
- H 和 W 是两个空间维度。


根据所使用的具体架构,我们有时会交换前两个维度的顺序。但基本思想是,视频就是一个四维张量,我们需要找到构建深度神经网络的方法来处理这类数据。

🎯 核心任务:视频分类
为了引出后续的各种架构,我们将使用视频分类任务作为主要动机。这基本上是图像分类任务在时间上的延伸。
网络将接受一个输入视频(即一堆RGB帧),系统需要选择一个类别标签来对该视频中的动作、活动或语义类别进行分类。与图像分类一样,系统在训练时知晓一组固定的类别,并拥有一个将视频与类别标签相关联的训练数据集。
训练时,我们通常使用交叉熵损失函数。整个任务的核心技巧在于:如何将这个四维输入张量转换为我们可用于训练交叉熵损失函数的分数向量。

🏃 识别对象 vs. 识别动作
在二维识别任务中,我们通常识别的是对象(名词),例如狗、猫、瓶子或汽车。而在三维视频序列中,我们通常想要分类的是动作或活动(动词),例如游泳、跑步、跳跃、进食或站立。

视频分类数据集中大多数类别标签都对应着人类可以执行的不同类型的动作或活动,因为分析人们的行为是深度学习在视频分析中的一个重要应用方向。

💾 视频数据的巨大挑战
处理视频数据的一个主要约束是其数据量非常庞大。如果处理不当,计算开销将难以承受。


例如,一个标准清晰度(640x480像素)的视频流,以每秒30帧(fps)播放,其未经压缩的原始数据每分钟约为1.5GB。如果是全高清视频(1920x1080像素),这个数字会上升到每分钟10GB。如此庞大的数据量几乎不可能直接装入GPU内存进行处理。
因此,解决方案是必须大幅减小数据规模。在视频分类中,我们通常训练神经网络来分类非常短的视频片段(通常为3-5秒)。为了使其更易于处理,我们还会:
- 时间下采样:将帧率从30 fps降至很低的水平(例如5 fps)。
- 空间下采样:将视频帧的空间分辨率降至很低(例如112x112像素)。
由于计算限制,我们的神经网络目前只能处理这种非常短小、低分辨率的视频片段。
在训练时,我们从较长的视频中截取这些短片段进行训练。在测试时,我们通常将训练好的模型在原始视频的不同位置(不同子片段)上多次应用,然后对所有子片段的预测结果进行平均,以得到整个视频的最终分类预测。这是一种在训练和测试之间进行权衡的技巧。

🖼️ 方法一:单帧基线模型
我们的第一个视频分类模型看似简单,但效果出奇地好:忽略视频特性,训练一个标准的二维CNN来独立分类视频的每一帧。
具体做法是:将视频序列的所有帧分割成独立的2D RGB图像,然后使用你最喜欢的标准2D图像识别模型(如ResNet)在每一帧上独立训练,训练目标是对应整个视频片段的标签。
在测试时,我们只需在视频的每一帧上运行这个单帧模型,然后对所有帧的预测结果进行平均。
这个模型基本上忽略了数据中的所有时间结构,但它实际上是许多视频分类任务中非常强大的基线模型。在构建实际的视频分类系统时,应该首先尝试这个简单的单帧基线模型,因为它通常已经足够好,而更复杂的时间建模方法往往只是将准确率从“很好”提升到“更好一点”。


🔗 方法二:晚期融合

一个稍微复杂、并考虑了视频时间结构的模型是晚期融合。
在单帧基线中,我们在视频的每一帧上独立运行CNN,提取独立的分类决策,然后在测试时进行平均。晚期融合方法与此类似,但我们将“跨时间平均”的概念构建到网络本身中,让网络在训练时就能意识到测试时会进行这种平均。
具体操作如下:
- 使用你最喜欢的2D CNN架构(如ResNet)独立处理视频序列的每一帧,提取每帧的特征。这样我们得到了一系列独立的每帧特征图。
- 我们需要一种方式来聚合或组合所有这些独立的每帧特征。一种方法是使用全连接层:将每帧特征展平成一个巨大的向量(形状为
T x D x H‘ x W’),然后应用一些全连接层,将其映射到最终的类别分数C。 - 使用交叉熵损失进行训练。
这种方法被称为“晚期融合”,因为我们在分类流程的很晚的阶段才融合时间信息(先独立进行每帧建模,最后才融合)。
另一种晚期融合的方法是使用全局平均池化。在提取了独立的每帧特征后,我们应用一个全局平均池化层,该层在所有空间维度和所有时间维度上进行平均池化。这样我们就得到了一个D维向量,然后可以应用线性层来获得最终的类别分数。这种方法更简单,参数更少,有助于防止过拟合。

晚期融合的局限性:这种方法难以建模相邻视频帧之间非常低级的像素或特征运动。因为网络将每帧的所有信息汇总为一个向量,所以很难比较相邻帧之间的低级像素值。
⚡ 方法三:早期融合
既然有“晚期融合”,自然就有“早期融合”。其思想是:在CNN的第一层就融合所有的时间信息。
具体做法是:将输入视频帧序列(四维张量)重新塑形,将时间维度解释为一组通道。也就是说,将所有RGB帧沿着通道维度堆叠起来。这样我们就得到了一个三维张量,其通道维度为 3 * T(T是帧数),空间维度不变。

然后,我们可以使用一个单一的二维卷积操作来处理这个张量,该卷积的输入通道数为 3T。在这个第一层完成了“早期融合”之后,网络的其余部分可以是任何标准的二维卷积架构,因为时间信息已经在第一层被“折叠”了。

这种方法有望更好地建模相邻视频帧之间的低级像素运动,因为网络可以学习比较相邻帧局部像素的卷积核。但问题在于,这种方法可能过于激进:仅通过一个2D卷积层就破坏了所有的时间信息,而一个卷积层的计算量可能不足以正确建模视频序列中所有类型的时间交互。
🐌 方法四:3D CNN(慢速融合)
我们需要一种机制,允许我们在处理视频序列的过程中缓慢地融合信息,这就是3D CNN,有时也称为慢速融合网络。
其思想是:在CNN的每一层,我们都维护四维特征张量(通道、时间、高度、宽度)。在每一层的处理中,我们使用三维卷积和三维池化的类比操作,允许我们在许多层的处理过程中缓慢地在空间和时间上融合信息。
与早期融合(立即在整个时间范围上建立感受野)和晚期融合(在最后才建立时间感受野)不同,3D CNN允许网络在多个处理层中缓慢地在空间和时间上建立感受野。

2D卷积(早期融合) vs. 3D卷积(3D CNN)的核心区别:
- 2D卷积(早期融合):卷积核在空间上很小,但在时间上覆盖了整个输入长度。这意味着它不具备时间平移不变性。要检测不同时间点发生的相同变化(如颜色从蓝变橙),需要学习不同的滤波器。
- 3D卷积(3D CNN):卷积核在空间和时间上都很小。它在所有空间维度和时间维度上滑动。这解决了时间平移不变性问题。要检测不同时间点发生的相同变化,只需要一个三维卷积滤波器,因为它会在所有时间位置上滑动。这使得3D卷积在表征上更高效。

一个有趣的点是,我们可以将这些学习到的3D卷积滤波器可视化为小视频片段,从而了解网络学习了哪些类型的时空特征。
📊 基准测试与模型演进
为了比较这些不同方法的优劣,我们需要在数据集上进行基准测试。一个常用的数据集是 Sports-1M,它包含一百万个YouTube体育视频,标注了各种细粒度的体育类别。
比较结果令人惊讶:单帧基线模型的性能非常强大,准确率超过77%。早期融合模型的表现略差于单帧基线,而晚期融合和3D CNN方法比单帧方法稍好,但优势并不巨大。这再次证明了单帧基线在视频识别任务中的强大实力。

一个著名的改进版3D CNN架构是 C3D,它被称为“3D CNN中的VGG”。它完全由3x3x3的卷积和2x2x2的池化组成。C3D影响深远,因为作者发布了预训练权重,许多人将其作为固定的视频特征提取器用于下游任务。

但3D CNN模型的计算成本非常高。例如,C3D模型处理一个很小的输入(16帧,112x112分辨率),其前向传播的总计算成本接近40 GigaFLOPs,是VGG16的三倍。这是视频模型的一个普遍问题。

🌊 显式建模运动:双流网络
空间和时间也许应该在我们的模型中区别对待。人类仅凭运动信息就能识别很多动作(例如,仅凭几个移动的点就能识别行走的人)。受此启发,有一类神经网络架构尝试更显式地将运动信息作为网络内部的原始特征。
光流是量化运动信息的一种常用方法。它接收一对相邻的视频帧,计算一个流场或畸变场,描述每个像素从第一帧到第二帧的位移向量。光流为我们提供了关于像素在相邻帧之间如何移动的局部运动线索。
双流网络是一个著名的利用运动信息的视频识别架构:
- 空间流:处理输入视频的外观。它输入视频片段中的单帧图像,并尝试预测类别分布。这本质上就是强大的单帧基线。
- 时间流:仅处理运动信息。给定一个视频片段,我们计算每一对相邻帧之间的光流,得到一个通道数为
2*(T-1)的张量(每个光流有x和y两个分量)。然后,我们使用早期融合方法(在第一层卷积融合所有光流场),网络的其余部分使用正常的2D CNN操作。时间流也独立地尝试仅根据运动信息预测分类决策。 - 在测试时,空间流和时间流会分别预测一个类别概率分布,我们对这两个分布进行平均,得到最终预测。


实验表明,仅使用时间流(运动信息)就能取得很好的性能,甚至能超过空间流(外观信息)。将两者融合后,性能能得到进一步提升。
🔄 结合CNN与RNN处理长时依赖
到目前为止,我们看到的建模方法(2D卷积、3D卷积、光流)在处理时间信息时都非常局部。如果我们想要构建能够识别时间上跨度更大的模式的CNN,该怎么办?
我们已经在本课程中见过一种能够处理长时序列信息的神经网络架构:循环神经网络。我们可以将CNN和RNN结合起来。

具体思路是:在时间序列的底部应用某种CNN(2D或3D)来提取每个时间点的局部特征,然后使用RNN架构(如LSTM)根据这些独立CNN的输出在时间上融合信息。我们可以使用“多对一”操作在视频末尾做出单个分类决策,也可以使用“多对多”操作在视频的每个时间点做出一系列预测。


一个技巧是:我们可以使用预训练的C3D作为特征提取器,提取视频每个时间点的特征,然后在顶部使用RNN处理这些预提取的特征。这样我们就不需要向CNN反向传播,可以训练处理更长时间段的模型。
🧠 循环卷积网络
我们能否将局部融合(CNN内部)和长时融合(CNN顶部的RNN)两种方法结合起来?我们可以从RNN中获得灵感,看看多层RNN的结构。

我们可以构建一个多层循环卷积网络。在这个二维网格中,每个点的特征张量(三维:空间+通道)将由同一时间步的上一层特征和同一层的上一时间步特征共同计算得到。然后,我们可以使用卷积操作来融合这两部分信息。

这相当于将熟悉的RNN公式(如Vanilla RNN:h_t = tanh(W_x * x_t + W_h * h_{t-1} + b))中的矩阵乘法全部替换为2D卷积操作。这样,网络中的每一层、每一个神经元都变成了一个小型循环网络,空间和时间融合以非常巧妙的方式发生在网络的每一层。
尽管这个想法非常优美,但在实践中人们并没有过多使用,主要原因是RNN的计算效率不高。RNN在处理长序列时速度很慢,因为它们强制要求计算完前一个时间步才能计算下一个时间步,无法很好地并行化。

✨ 自注意力机制用于视频

我们之前讨论过处理序列的另一种机制:自注意力。它擅长处理长序列,并且高度可并行化(没有时间依赖)。那么,能否将自注意力机制应用于视频序列处理呢?答案是肯定的。
我们可以将自注意力机制移植到3D领域。在网络的一部分使用3D CNN处理后,我们会得到一个四维张量(通道、时间、高度、宽度)。我们可以将这个四维张量解释为一组向量(共有 T*H*W 个向量,每个向量维度为C),然后在这组向量上运行完全相同的自注意力操作符。

具体来说,这个操作(有时称为非局部块)可以插入到我们的3D CNN中,计算一种时空自注意力。一个巧妙的技巧是:我们可以通过将最后一个卷积层初始化为零,使整个块初始时计算恒等函数(由于残差连接)。这样,我们就可以将一个计算恒等函数的非局部块插入到已有的预训练3D CNN模型中,然后继续进行微调。

这种架构结合了3D CNN(缓慢融合时空信息)和非局部块(全局融合所有时空信息),是一种非常强大、接近最先进的视频识别架构。
🎈 膨胀2D网络:从图像到视频
我们仍然需要选择一个可以插入非局部块的3D CNN基础架构。一个非常酷的想法是:将现有的优秀2D网络架构“膨胀”成3D。
我们不想从头开始发明最好的3D CNN架构,而是希望利用在2D图像上设计好的架构。具体方法是:取一个现有的2D CNN架构(包含2D卷积和2D池化),将其中的每一个2D卷积替换为3D卷积,每一个2D池化替换为3D池化。
更进一步,我们不仅可以膨胀架构,还可以迁移权重。我们可以使用在图像上训练好的网络权重来初始化膨胀后的3D CNN。具体做法是:将每个卷积层的卷积核在时间维度上复制,然后除以复制因子。这样做的原因是,卷积是线性操作。如果我们有一个由单帧重复多次构成的“无聊”视频,使用复制权重的3D卷积在其上计算的结果,与在原图像上使用2D卷积计算的结果相同。

这个技巧允许我们在图像上预训练模型,然后膨胀架构和权重,再在视频数据集上继续微调。这让我们能够回收所有已知在图像上有效的架构和预训练模型。

实验表明,在视频数据集上,这种“膨胀CNN”的方法比双流CNN等模型表现更好。将两个膨胀网络分别用于外观流和光流流,构成的双流膨胀网络效果更佳。

👁️ 可视化视频模型所学内容

我们可以使用与可视化图像模型相同的技巧来可视化训练好的视频模型学到了什么。例如,我们可以随机初始化一个输入图像和一个光流场,然后通过网络前向传播计算特定类别的分类分数,再反向传播计算分数相对于输入图像和光流场的梯度,最后使用梯度上升来找到最大化该类别分数的图像和光流场。
可视化结果非常有趣。例如,对于“举重”类别,外观流学会在图像中寻找杠铃;慢速运动流学会寻找杠铃在顶部晃动;快速运动流则学会寻找将杠铃举过头顶的动作。对于“化妆”类别,外观流寻找眼睛,慢速运动流寻找头部或手部的运动,快速运动流则寻找局部涂抹的动作。
🚀 当前前沿:SlowFast 网络

双流网络仍然依赖于外部的光流算法。如果能放松这个约束,构建仅处理原始像素值而不依赖外部光流的网络,那将是非常好的。这引出了当前最先进的 SlowFast 网络。
SlowFast网络仍然有两个并行分支,但两者都处理原始像素。关键区别在于它们以不同的时间分辨率运行:
- 慢速通路:以低帧率运行,但网络“更胖”,每层使用大量通道进行深度处理。它主要捕捉外观和缓慢变化的语义信息。
- 快速通路:以高帧率运行,但网络“更瘦”,每层使用很少的通道进行轻量处理。它主要捕捉快速变化的运动信息。
- 横向连接:信息从快速通路融合回慢速通路,这与传统双流网络仅在最后平均融合不同,SlowFast在网络内部多个阶段进行融合。

每个通路本身都是一个膨胀的ResNet-50架构。这种设计结合了本课讨论的多种技术(双流、膨胀、多分辨率处理),是目前许多视频理解任务上的最先进方法。

🕵️ 超越分类:其他视频任务
我们主要讨论了分类短视频片段的任务,这有助于引出许多时空CNN架构。当然,还有许多其他视频理解任务。

- 时序动作定位:给定一个很长的未修剪视频序列,模型需要检测其中发生的多个活动,并告诉我们每个活动发生的时间段。可以构建类似于Faster R-CNN的架构来实现。
- 时空动作检测:这是一个更具挑战性的任务,输入是一个长视频序列,任务是需要检测每一帧中
🎨 课程 P19:L19- 生成模型(上)
在本节课中,我们将要学习生成模型的基本概念、分类以及两种具体的生成模型:自回归模型和变分自编码器。我们将从监督学习与无监督学习的区别开始,逐步深入到生成模型的数学原理和实现方法。
📚 概述
生成模型是机器学习中的一个重要分支,它旨在学习数据的概率分布,从而能够生成新的数据样本。与判别模型不同,生成模型不仅能够进行分类,还能够检测异常值、学习特征表示,甚至生成全新的数据。
🔍 监督学习与无监督学习
在机器学习中,我们通常区分监督学习和无监督学习。监督学习依赖于带有标签的数据集,例如图像分类任务中,我们既有图像(输入 X)也有对应的标签(输出 Y)。无监督学习则只使用原始数据,没有标签,目标是发现数据中的隐藏结构。
以下是几种常见的无监督学习任务:
- 聚类:将数据样本分成不同的组。
- 降维:将高维数据投影到低维空间,如主成分分析(PCA)。
- 自编码器:通过重建输入数据来学习潜在表示。
- 密度估计:学习一个概率分布,使得训练数据具有高概率。
🧮 判别模型与生成模型
在概率框架下,机器学习模型可以分为判别模型和生成模型。
- 判别模型:学习条件概率分布 P(Y|X),即给定输入 X 预测输出 Y 的概率。例如,图像分类器。
- 生成模型:学习联合概率分布 P(X),即数据 X 本身的概率分布。它能够评估任何输入数据的可能性。
- 条件生成模型:学习条件概率分布 P(X|Y),即在给定标签 Y 的情况下生成数据 X 的概率。
生成模型的一个关键特性是,由于概率密度函数必须归一化(积分为1),不同的数据样本会“竞争”概率质量。这使得生成模型能够识别并拒绝不合理的输入。
根据贝叶斯规则,这些模型之间存在联系:
P(X|Y) = P(Y|X) * P(X) / P(Y)
🌳 生成模型的分类
生成模型可以根据其密度函数的形式进行分类:
- 显式密度模型:能够直接计算密度函数的值。
- 可处理的密度模型:如自回归模型。
- 近似密度模型:如变分自编码器。
- 隐式密度模型:无法直接计算密度值,但可以从中采样。例如生成对抗网络(将在下节课介绍)。
🔢 自回归模型
自回归模型是一种具有显式且可处理密度函数的生成模型。其核心思想是将数据的联合概率分布分解为条件概率的乘积。
对于图像数据,我们可以将像素按顺序排列(例如从左到右、从上到下),然后使用链式法则:
P(X) = Π P(x_i | x_1, ..., x_{i-1})
这类似于我们在循环神经网络(RNN)中看到的结构。因此,我们可以使用RNN来建模图像像素的生成过程。
以下是两种具体的自回归模型:
- PixelRNN:使用RNN按顺序生成像素。虽然效果不错,但训练和采样速度较慢。
- PixelCNN:使用掩码卷积来并行计算像素的条件概率,训练速度更快,但采样仍然较慢。
自回归模型的优点是密度函数可计算,便于评估。然而,它们生成样本的速度较慢,且生成的图像质量有时不够高。
🌀 变分自编码器
变分自编码器是一种具有近似显式密度函数的生成模型。它结合了自编码器的结构和概率建模的思想。
自编码器基础
自编码器是一种无监督学习方法,旨在学习数据的压缩表示。它由两部分组成:
- 编码器:将输入数据 X 映射到潜在表示 Z。
- 解码器:从潜在表示 Z 重建输入数据 X。
训练目标是最小化重建误差,从而学习到有用的特征表示。然而,传统的自编码器不是概率模型,无法生成新样本。
变分自编码器的概率框架
变分自编码器引入了概率分布:
- 先验分布:假设潜在变量 Z 服从简单的分布,如标准正态分布 N(0, I)。
- 解码器:学习条件分布 P(X|Z),通常假设为对角高斯分布。
- 编码器:学习近似后验分布 Q(Z|X),以解决贝叶斯规则中难以计算的部分。
通过引入变分下界,我们可以最大化数据的对数似然的下界:
log P(X) ≥ 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散度项,衡量编码器输出与先验分布的差异。
通过最大化这个下界,我们可以同时训练编码器和解码器,从而学习到一个能够生成新样本的概率模型。
📝 总结
在本节课中,我们一起学习了生成模型的基本概念和两种具体实现:自回归模型和变分自编码器。
- 自回归模型通过顺序生成数据(如像素)来建模概率分布,具有显式且可处理的密度函数,但生成速度较慢。
- 变分自编码器结合了自编码器和概率建模,通过最大化变分下界来学习数据的潜在表示和生成新样本。


下节课我们将继续探讨生成模型,重点介绍生成对抗网络及其应用。
🖼️ 课程 P2:L2-图像分类
在本节课中,我们将要学习图像分类这一计算机视觉中的核心任务。我们将探讨图像分类的基本概念、面临的挑战,以及如何通过数据驱动的方法构建分类器。特别地,我们将详细介绍我们的第一个学习算法——K最近邻算法,并讨论其工作原理、优缺点以及在实际应用中的注意事项。
📖 概述:什么是图像分类?
图像分类是一个看似简单但极具挑战性的任务。算法的输入是一张图像,输出则是为该图像分配一个预定义的类别标签。
例如,算法可能知道“猫”、“鸟”、“鹿”、“狗”、“卡车”这五个标签。当它看到一张猫的图片时,需要输出“猫”这个标签。
对人类而言,识别图像中的物体是轻而易举的。然而,对计算机来说,这却是一个巨大的挑战。其核心困难在于“语义鸿沟”:我们看到图像时,能直观地理解其内容;但计算机看到的只是一个巨大的数字网格(例如800x600x3的像素值矩阵)。这个数字网格与“猫”这个语义概念之间没有直接、明显的联系。
更复杂的是,图像像素值会因各种因素发生剧烈变化,而语义内容却保持不变。我们的算法需要对这些变化具有鲁棒性。

🎯 图像分类面临的挑战

为了构建一个实用的图像分类系统,算法需要克服以下主要挑战:
- 视角变化:同一物体从不同角度拍摄,像素值会完全不同。
- 类内差异:同一类别的物体(如不同的猫)在视觉上可能千差万别。
- 细粒度分类:有时需要区分视觉上非常相似的类别(如不同品种的猫)。
- 背景干扰:目标物体可能与背景融为一体。
- 光照变化:不同的光照条件会彻底改变图像的像素值。
- 物体形变:许多物体(如猫)的姿态和形状可以发生很大变化。
- 遮挡:目标物体可能只露出一小部分。
克服这些挑战,实现鲁棒的图像分类,将能解锁许多强大的应用,例如医学影像诊断、天文现象识别、自动驾驶等。


🧱 图像分类作为基础模块
图像分类不仅是独立的任务,更是构建更复杂计算机视觉应用的基础模块。
- 目标检测:可以通过对图像中不同滑动窗口进行分类来实现。
- 图像描述:可以将其视为一系列分类问题,依次选择最合适的词语来描述图像。
- 游戏AI:例如围棋AI,可以将棋盘状态视为一张“图像”,将下一步落子位置的选择视为一个分类问题。
由此可见,图像分类是计算机视觉中一个极其重要的基础性任务。
🤖 数据驱动的机器学习方法
鉴于直接为“识别猫”编写明确规则的算法非常困难且不具扩展性,我们转向数据驱动的机器学习方法。
其基本流程如下:
- 收集数据集:收集大量带有标签的图像(例如,猫和狗的图片及其对应标签)。
- 训练模型:使用机器学习算法,学习输入图像和输出标签之间的统计关系。
- 评估模型:使用训练好的模型对新的、未见过的图像进行预测。
这改变了我们编程的方式:我们不再直接告诉计算机每一步该怎么做,而是通过提供数据来“教”计算机如何完成任务。如果想识别新的类别(如星系),我们只需要收集新的数据集,而无需重写算法。
📊 常用图像分类数据集
以下是几个在研究和教学中常用的图像分类数据集:
- MNIST:包含0-9的手写数字,28x28灰度图。包含50,000张训练图像和10,000张测试图像。常被用作算法验证的“果蝇”式数据集。
- CIFAR-10:包含10个类别(飞机、汽车、鸟、猫等)的物体,32x32彩色图。包含50,000张训练图像和10,000张测试图像。本课程作业将主要使用此数据集。
- CIFAR-100:CIFAR-10的扩展,包含100个类别。
- ImageNet:当前图像分类领域的“黄金标准”。包含1000个类别,约130万张训练图像。通常使用Top-5准确率(预测的5个标签中包含正确答案即算对)进行评估。
- MIT Places:专注于场景分类(如教室、田野、建筑)的数据集。
- Omniglot:专注于“小样本学习”的数据集,包含50多种书写语言的1600多个字符类别,每个类别只有20个示例。
数据集的大小和复杂性随时间不断增长,这对算法提出了更高的要求。
🧮 第一个学习算法:K最近邻
K最近邻是一种非常简单直观的学习算法。
算法流程:
- 训练阶段:简单地记忆所有训练数据和标签。用代码表示即
self.X_train = X; self.y_train = y。 - 预测阶段:对于一张新的测试图像,在训练集中找到与其“最相似”的K张图像,然后通过投票(多数表决)决定其类别标签。
核心问题:如何定义两张图像之间的“相似性”?我们需要一个距离度量。
常用距离度量:
- L1距离(曼哈顿距离):计算两个图像像素值之差的绝对值之和。
distance = sum(abs(I1 - I2)) - L2距离(欧几里得距离):计算两个图像像素值之差的平方和再开方。
distance = sqrt(sum((I1 - I2)^2))
KNN的特点:
- 训练极快(O(1)),但测试很慢(O(N)),因为需要与所有训练样本比较。这与我们通常希望的“训练慢、测试快”的特性相反。
- 决策边界可能不平滑(K=1时)或存在平局问题(K>1时)。
- 使用不同的距离度量(L1 vs L2)会产生不同形状的决策边界。
⚙️ 超参数选择与数据划分
KNN算法中的K值和距离度量类型都是超参数——它们不是从数据中学到的,而是需要我们在算法运行前设定的选择。
如何选择超参数?错误的做法:
- 根据训练集准确率选择:这会导致选择K=1(因为训练集上总能达到100%准确率),但通常会过拟合,泛化能力差。
- 根据测试集准确率选择:这相当于在测试集上“学习”,会污染我们对算法真实泛化能力的评估,是严重的错误。
正确的做法:
将数据集划分为三部分:
- 训练集:用于训练模型参数。
- 验证集:用于调整和选择超参数。
- 测试集:仅在最终评估时使用一次,以得到对模型泛化能力的无偏估计。
更优的做法:交叉验证
将训练集进一步划分为多个“折”。轮流将其中一折作为验证集,其余作为训练集,重复训练和验证多次。最后取各次验证结果的平均值来选择超参数。这种方法评估更稳健,但计算成本更高。
💡 KNN的深入思考
万能近似定理:理论上,当训练数据无限多时,KNN可以近似任何函数。这意味着只要有足够的数据,KNN可以解决任何分类问题。
维度灾难:为了在高维空间(如图像像素空间)中“密集”覆盖,所需的数据量随维度指数级增长。对于32x32的二进制图像,其可能状态的数量(2^(1024))远远超过了宇宙中基本粒子的总数。因此,我们永远无法收集到足够的数据来在原始像素空间中进行有效的最近邻搜索。
像素距离的局限性:在原始像素上计算的L1/L2距离通常不能反映图像的语义相似性。例如,一张图像和它平移几个像素后的版本,在像素距离上可能很远,但语义上完全相同。
改进方向:使用更好的特征
虽然原始像素上的KNN效果有限,但如果我们能先计算图像的高级特征(例如,使用后续课程将讲的卷积神经网络提取的特征),再在这些特征向量上使用KNN,往往能得到非常好的效果。这证明了特征表示的重要性。
📝 总结
本节课我们一起学习了计算机视觉的核心任务——图像分类。我们了解了其定义、挑战、广泛应用以及作为基础模块的重要性。我们重点介绍了第一个机器学习算法——K最近邻,详细探讨了其工作原理、实现方式、超参数选择以及数据划分的正确方法。最后,我们讨论了KNN的理论能力(万能近似)与实际限制(维度灾难),并指出了通过使用高级特征来提升其性能的方向。


掌握了这些知识,你已经可以开始动手实现并评估一个简单的图像分类器了。下节课,我们将学习一个更强大的学习算法——线性分类器。

课程P20:L20- 生成模型(下) 🧠

在本节课中,我们将继续探讨生成模型,这是深度学习领域中一个非常活跃且富有挑战性的研究方向。我们将重点介绍变分自编码器 和生成对抗网络 这两种强大的生成模型,并理解它们的工作原理、优势与挑战。





概述:从判别模型到生成模型

上一节我们介绍了生成模型的基本概念,并区分了判别模型 与生成模型。判别模型学习的是条件概率分布 P(Y|X),而生成模型则试图直接对数据本身的分布 P(X) 进行建模。我们还回顾了自回归模型,它通过逐个预测像素来显式地定义图像的概率分布。
本节中,我们将深入探讨另外两类重要的生成模型:变分自编码器和生成对抗网络。它们采用了不同的策略来学习复杂的数据分布。

变分自编码器:学习潜在表示
自回归模型直接对像素进行建模,但它们没有学习到一个压缩的、有意义的潜在表示。变分自编码器则引入了一个潜在变量 z,旨在学习数据的高层语义特征。



核心思想与架构

变分自编码器的目标是学习数据的生成过程 P(X)。它通过引入潜在变量 z,将问题转化为学习 P(X|z) 和 P(z|X)。由于直接最大化数据似然 P(X) 是难解的,我们转而最大化其变分下界。
模型包含两个神经网络:
- 编码器 (Encoder): 输入数据 x,输出潜在变量 z 的分布参数(例如高斯分布的均值和方差)。我们记其分布为 q_φ(z|x)。
- 解码器 (Decoder): 输入潜在变量 z,输出重构数据 x‘ 的分布参数。我们记其分布为 p_θ(x|z)。
为了便于计算,我们通常假设这些分布是对角高斯分布。编码器网络输出均值向量 μ 和对角协方差矩阵 σ²I。
一个简单的全连接VAE架构示例(用于MNIST数据集):
# 编码器: 输入784维(28x28图像展平),输出20维潜在变量的均值和方差
encoder_fc1 = Linear(784, 400) -> ReLU
encoder_mean = Linear(400, 20) # 输出均值 μ
encoder_logvar = Linear(400, 20) # 输出方差的对数 log(σ²),保证正值

# 解码器: 输入20维潜在变量,输出784维重构图像的均值和方差
decoder_fc1 = Linear(20, 400) -> ReLU
decoder_mean = Linear(400, 784) # 输出重构像素的均值
decoder_logvar = Linear(400, 784) # 输出重构像素的方差
训练目标:最大化变分下界
变分自编码器的训练目标是最大化证据下界:
ELBO = E_{z~q_φ(z|x)}[log p_θ(x|z)] - D_{KL}(q_φ(z|x) || p(z))
这个目标包含两项:
- 重构损失: 期望潜在变量 z(从编码器分布采样)能通过解码器很好地重构原始数据 x。这鼓励潜在编码包含足够的信息。
- KL散度项: 鼓励编码器输出的分布 q_φ(z|x) 接近我们预设的简单先验分布 p(z)(通常为标准正态分布 N(0, I))。这起到了正则化的作用,让潜在空间更规整、易于采样。
由于我们选择了高斯分布,KL散度项可以解析计算。训练时,我们使用重参数化技巧从 q_φ(z|x) 中采样 z,从而使梯度可以通过采样过程反向传播。
以下是训练一个VAE的步骤:
- 从训练集中取一个批次的数据 x。
- 通过编码器得到 μ 和 log(σ²)。
- 使用重参数化采样潜在变量:z = μ + ε * σ,其中 ε ~ N(0, I)。
- 将 z 输入解码器,得到重构数据的分布参数。
- 计算重构损失(如二元交叉熵或均方误差)和KL散度损失。
- 将两项损失相加,通过反向传播同时更新编码器和解码器的参数。
VAE的能力与应用
一旦VAE训练完成,我们可以用它做几件很酷的事情:
- 生成新数据: 从先验分布 p(z)(如标准正态分布)中随机采样一个 z,输入解码器,就能生成一张新的图像。
- 探索潜在空间: 由于我们规范了潜在空间,在其间进行插值会产生语义上平滑过渡的图像。例如,从一个数字“7”的编码逐渐变化到数字“1”的编码,生成的图像会平滑地从“7” morph 成“1”。
- 图像编辑: 给定一张图像,可以通过编码器得到其潜在编码 z,然后有目的地修改 z 的某些维度,再通过解码器生成编辑后的图像。研究发现,潜在空间的某些维度可能对应着有意义的属性,如人脸图像的“表情”或“光照方向”。
VAE的优点是提供了一个 principled 的概率框架,并学习了有用的潜在表示。但其生成的图像有时会比较模糊,这是其简化分布假设(如高斯分布)带来的一个局限。
生成对抗网络:通过对抗学习生成
生成对抗网络采取了一种完全不同的思路:它放弃了显式地对数据分布 P(X) 进行建模,而是专注于学习如何从该分布中采样。GAN的核心思想是通过一个“对抗”的游戏来训练模型。
核心思想:双人博弈
GAN框架中包含两个相互对抗的神经网络:
- 生成器 G: 输入一个从简单分布(如均匀分布或正态分布)中采样的随机噪声向量 z,输出一张合成图像 G(z)。它的目标是生成足以“以假乱真”的图像。
- 判别器 D: 输入一张图像,输出一个标量,表示该图像是来自真实数据分布(输出接近1)还是来自生成器(输出接近0)。它的目标是成为一个优秀的“鉴定师”。
它们的训练过程形成一个极小极大博弈:
min_G max_D V(D, G) = E_{x~p_data(x)}[log D(x)] + E_{z~p_z(z)}[log(1 - D(G(z)))]
- 判别器 D 试图最大化 V: 它要最大化对真实图像判为“真”(log D(x))和对生成图像判为“假”(log(1-D(G(z))))的概率。
- 生成器 G 试图最小化 V: 具体是最小化第二项,即让判别器对自己生成的图像判为“真”(D(G(z))接近1),这样 log(1-D(G(z))) 就会变小。
在实际训练中,我们交替优化D和G。理论上,当博弈达到纳什均衡时,生成器学到的分布 P_G 将无限接近真实数据分布 P_data,而判别器则无法区分真假(始终输出0.5)。
训练技巧与挑战
原始的GAN目标函数在训练初期可能存在梯度消失问题(当生成器很差时,判别器很容易识别,导致生成器梯度很小)。一个常见的改进是训练生成器去最大化 log(D(G(z))),而不是最小化 log(1-D(G(z)))。这提供了更稳定的梯度。
训练GAN非常具有挑战性:
- 模式崩溃: 生成器可能只学会生成少数几种样本,缺乏多样性。
- 训练不稳定: 生成器和判别器的损失波动很大,难以监控。
- 超参数敏感: 对学习率、网络架构等非常敏感。
研究人员提出了许多改进,如使用 Wasserstein 距离的 WGAN、谱归一化、自注意力机制等,极大地提升了GAN的稳定性和生成质量。
GAN的惊人进展与应用
自2014年提出以来,GAN的发展日新月异,生成图像的质量和分辨率飞速提升:
- DCGAN: 首次成功将卷积网络用于GAN,生成了结构复杂的卧室图像。
- Progressive GAN: 通过渐进式增长网络,生成了高清(1024x1024)的人脸图像。
- StyleGAN: 通过对潜在空间进行更精细的控制,生成了极其逼真且多样化的人脸和物体图像。
GAN的应用远不止无条件图像生成:
- 条件生成: 通过将类别标签等信息输入生成器和判别器,可以实现可控生成。例如,指定生成“火烈鸟”或“校车”的图像。这通常通过条件批归一化等技术实现。
- 图像到图像翻译: 学习从一个图像域到另一个图像域的映射。例如,将语义分割图转换为真实照片、将素描上色、将马变成斑马(CycleGAN)、将低分辨率图像超分辨为高清图像等。
- 其他领域: GAN也被用于生成文本、音乐、3D模型,甚至预测行人未来轨迹。
总结与展望
本节课我们一起深入学习了两种主流的深度生成模型:
- 变分自编码器:一种基于概率框架的生成模型,通过最大化变分下界来同时学习数据的潜在表示和生成过程。它结构清晰,能进行有意义的潜在空间操作,但生成的图像可能较模糊。
- 生成对抗网络:一种通过对抗博弈训练的生成模型,放弃了显式的似然计算,专注于生成高质量的样本。它能够生成极其逼真的图像,并广泛应用于各种条件生成和图像翻译任务,但训练过程不稳定且难以调试。
此外,我们还看到了结合自回归模型和VAE思想的VQ-VAE-2等前沿工作,它们试图融合不同模型的优点。
生成模型是通向无监督学习这一“圣杯”的重要路径,它使机器能够理解并创造数据的内在结构和模式。从自回归模型到VAE,再到GAN,每一种方法都为我们提供了不同的视角和工具。尽管挑战依然存在,但这个领域的快速发展持续为我们带来惊喜,并推动着人工智能在艺术、设计、仿真等众多领域的应用边界。
🧠 P21:强化学习入门教程
在本节课中,我们将学习强化学习的基本概念、核心算法及其应用。强化学习是机器学习的第三大范式,与监督学习和无监督学习有本质区别。我们将从强化学习的定义出发,逐步介绍其数学形式化表示、核心算法(Q学习和策略梯度),并通过实例展示其应用。
📚 概述:什么是强化学习?
强化学习涉及智能体与环境的交互。智能体通过观察环境状态、执行动作并获得奖励信号来学习如何最大化累积奖励。与监督学习不同,强化学习没有标注数据;与无监督学习不同,强化学习的目标是通过交互获得高奖励。

上一节我们介绍了强化学习的基本概念,本节中我们来看看强化学习与监督学习的区别。

🔄 强化学习 vs 监督学习
强化学习与监督学习有以下几个根本区别:
- 随机性:在强化学习中,状态、奖励和状态转移都可能具有随机性。
- 信用分配:奖励可能由过去多个动作共同导致,难以确定具体是哪个动作导致了当前奖励。
- 不可微分性:无法通过反向传播直接计算奖励对模型权重的梯度。
- 非平稳性:智能体训练的数据分布会随着其策略的改变而变化。
🧮 马尔可夫决策过程(MDP)
马尔可夫决策过程是强化学习的数学框架,由以下五元组定义:
- 状态集合 S:所有可能的状态。
- 动作集合 A:所有可能的动作。
- 奖励函数 R:给定状态和动作的奖励分布。
- 状态转移概率 P:给定当前状态和动作,转移到下一状态的概率。
- 折扣因子 γ:权衡即时奖励与未来奖励的重要性。
马尔可夫决策过程具有马尔可夫性质,即下一状态仅依赖于当前状态和动作,而与历史状态无关。
🎯 策略与价值函数
智能体的行为由策略 π 定义,策略给出了在给定状态下执行各个动作的概率分布。目标是找到最优策略 π*,以最大化期望累积折扣奖励。
以下是两种常用的价值函数:
- 状态价值函数 V^π(s):从状态 s 开始,遵循策略 π 的期望累积奖励。
- 动作价值函数 Q^π(s, a):从状态 s 执行动作 a,然后遵循策略 π 的期望累积奖励。
最优动作价值函数 Q* 满足贝尔曼方程:
Q*(s, a) = E[r + γ * max_a' Q*(s', a')]
🧠 Q学习算法
Q学习是一种基于价值函数的强化学习算法,通过迭代更新Q函数来逼近最优Q函数。以下是Q学习的核心步骤:
- 初始化Q函数。
- 在每一步中,根据当前状态选择动作(例如使用ε-贪婪策略)。
- 执行动作,观察奖励和下一状态。
- 使用贝尔曼方程更新Q函数:
Q(s, a) ← Q(s, a) + α * (r + γ * max_a' Q(s', a') - Q(s, a)) - 重复步骤2-4直到收敛。
当状态和动作空间较大时,可以使用神经网络近似Q函数,称为深度Q学习(DQN)。




📈 策略梯度算法


策略梯度算法直接优化策略函数,通过梯度上升最大化期望累积奖励。以下是策略梯度的核心公式:
∇_θ J(θ) = E[∑ ∇_θ log π_θ(a_t | s_t) * R_t]
其中,J(θ) 是期望累积奖励,π_θ 是参数化的策略,R_t 是累积奖励。
策略梯度算法的步骤如下:
- 初始化策略参数 θ。
- 使用当前策略在环境中采样轨迹。
- 计算轨迹的累积奖励。
- 使用策略梯度公式更新策略参数。
- 重复步骤2-4直到收敛。
🎮 强化学习应用实例
强化学习已成功应用于多个领域,以下是一些经典案例:
- 平衡杆问题:通过移动小车平衡杆子。
- 机器人行走:控制机器人关节运动以实现稳定行走。
- Atari游戏:通过屏幕像素输入学习玩游戏。
- 围棋:AlphaGo击败人类世界冠军。
- 硬注意力机制:在图像处理中选择特定区域进行特征提取。
🔮 总结与展望
本节课我们一起学习了强化学习的基本概念、核心算法及其应用。强化学习通过智能体与环境的交互学习最优策略,具有广泛的应用前景。尽管强化学习面临随机性、信用分配、不可微分性和非平稳性等挑战,但通过Q学习和策略梯度等算法,我们可以在许多复杂任务中取得显著成果。


未来,强化学习将继续在游戏、机器人控制、自然语言处理等领域发挥重要作用,并推动人工智能技术的进一步发展。



🎓 P22:L22- 课程总结
在本节课中,我们将回顾本学期计算机视觉与深度学习课程的核心内容,并探讨该领域当前面临的主要挑战与未来发展方向。
📚 学期内容回顾




计算机视觉的核心目标是让计算机能够处理、感知和理解各种类型的视觉数据。其根本挑战在于跨越语义鸿沟:计算机只能看到由数字组成的网格,而人类却能轻松理解图像内容(例如识别出一只猫)。视觉数据极为复杂,光照、视角、物体形变等因素都会导致像素网格发生巨大变化,但我们的语义理解需要保持一致。




🧠 数据驱动与机器学习范式




我们本学期探讨的解决方案是数据驱动方法,即使用机器学习来解决各类视觉问题。具体流程如下:



- 为特定任务收集包含图像和标签的数据集。
- 使用机器学习算法(尤其是神经网络)从数据集中提炼知识,构建分类器。
- 使用训练好的分类器对新的图像进行预测。



这种基于机器学习的范式已成为解决计算机视觉问题的主流方法。




🏗️ 核心模型:深度卷积神经网络



我们深入学习的核心模型是深度卷积神经网络。但模型本身并不足够,我们还需要大规模数据集(如ImageNet)和强大的计算硬件(如GPU)来训练这些模型。这三者的结合,特别是自2012年AlexNet取得突破以来,极大地推动了计算机视觉领域的进步。




该领域的活跃度可以从顶级会议CVPR的论文提交和接收数量呈指数级增长中窥见一斑。


📜 历史脉络


深度学习的成功并非一蹴而就,它建立在数十年来众多研究者的智慧之上:
- 1959年:Frank Rosenblatt的感知机模型,本质上是线性分类器。
- 1959年:Hubel和Wiesel对猫视觉皮层的研究,揭示了边缘、方向、运动等特征的重要性,这些特征在后来的神经网络可视化中反复出现。
- 1980年:Fukushima的Neocognitron模型,其架构已非常接近现代卷积神经网络。
- 1998年:Yann LeCun发表了其著名的卷积网络工作,基本具备了现代卷积网络的完整形态。
- 2012年:AlexNet在ImageNet数据集上取得突破性性能。其在思想上与LeCun 1998年的工作差异并不大,关键在于更快的计算机、更多的数据以及一些技巧(如使用ReLU替代Sigmoid,使用带动量的SGD)。
- 2018年:Yoshua Bengio, Geoffrey Hinton, 和 Yann LeCun因在普及和推动深度学习方面的贡献获得图灵奖。
- 2019年秋季:本课程。

📖 本学期知识体系梳理

以下是本学期我们循序渐进学习的核心内容:

上一节我们介绍了深度学习的历史背景,本节中我们来看看本学期具体的知识脉络。
以下是本学期涵盖的核心主题列表:
- 机器学习基础:我们从简单的数据驱动方法开始,熟悉了K近邻分类器,并由此深入讨论了训练/测试集划分、超参数调整等机器学习流程的关键组成部分。
- 线性分类器:作为第一个参数化分类器,我们学习了如何构建函数形式
y = f(x; W),通过数据学习权重矩阵W,这成为了后续所有机器学习方法的基本范式。 - 优化方法:为了找到最优的
W,我们学习了梯度下降和随机梯度下降,通过计算梯度并沿着损失函数的地形“下坡”来优化模型。我们还探讨了优化中遇到的问题(如局部极小值、鞍点)以及改进方法(如动量、Adam等优化器)。 - 神经网络:我们扩展到全连接神经网络,它通过组合可复用的模板来学习更强大的函数。其理论支撑是通用近似定理:具有单隐藏层的神经网络可以近似任何连续函数。
- 卷积神经网络:为了适应视觉数据的空间结构,我们引入了卷积层(保持空间结构并共享权重)、池化层(降采样并引入不变性)和归一化层(如批归一化,提升训练效率)。
- 经典网络架构:我们学习了如何组合基础模块以构建高性能模型,包括AlexNet、VGG、GoogLeNet以及具有里程碑意义的ResNet(允许训练数百层的网络)。
- 高效架构:随着模型容量增大,效率成为焦点。我们了解了如ResNeXt(使用分组卷积)和MobileNet等专注于提升计算效率的架构。
- 计算图与反向传播:我们将神经网络表示为计算图,并学习了反向传播算法,它能高效地计算任意复杂计算图中各节点的梯度。
- 硬件与软件:我们探讨了支撑深度学习的硬件(从CPU到GPU,再到专用的TPU)和软件框架(如PyTorch的动态计算图与TensorFlow的静态计算图范式)。
- 训练技巧:我们深入研究了使网络有效训练的大量细节,包括:
- 激活函数(ReLU, Sigmoid等)
- 数据预处理
- 权重初始化(如Xavier初始化)
- 数据增强(人工扩展数据集)
- 正则化技术(如Dropout、DropConnect、批归一化),通过引入随机性来防止过拟合。
- 学习率调度与超参数选择策略。

在掌握了图像分类的核心技术后,我们开始探索深度学习在其他视觉任务和数据类型上的应用。


以下是进阶应用与模型列表:



- 可视化与理解:我们学习了可视化神经网络所学特征的技术,并将其应用于艺术创作,如DeepDream和神经风格迁移。
- 序列数据处理:我们引入了循环神经网络来处理序列数据,并在图像描述任务中将其与CNN结合,让系统能为图像生成自然语言描述。
- 注意力机制:我们学习了注意力机制,它能让模型在生成描述的每一步聚焦于图像的不同部分。这进一步泛化为自注意力层,成为处理集合数据的基本组件。
- Transformer:我们了解了Transformer架构,它完全基于自注意力机制,在自然语言处理等领域取得了巨大成功。
- 高级视觉任务:我们回归计算机视觉,探讨了更复杂的任务:
- 目标检测:训练系统在图像中定位并框出物体(单阶段与两阶段方法)。
- 语义分割:为图像的每个像素分配类别标签,通常使用全卷积网络。
- 实例分割:结合目标检测和语义分割,区分同一类别的不同实例。
- 3D视觉:处理3D数据(如体素、点云、网格),需要专门的网络架构。
- 视频处理:我们探讨了处理时序数据的方法,如3D卷积网络、双流网络(结合RGB和光流)以及RNN。
- 生成模型:我们学习了让模型创造新数据的三种主要范式:
- 自回归模型:直接最大化数据似然。
- 变分自编码器:引入潜变量并最大化证据下界。
- 生成对抗网络:通过生成器和判别器的对抗训练来生成逼真图像。
- 强化学习:我们简要介绍了这一不同范式,其中智能体通过与环境交互来学习。我们接触了两种基本算法:Q学习和策略梯度。

🔮 未来展望与开放挑战
尽管深度学习取得了巨大成功,但该领域仍面临许多重大挑战和未来发展方向。
上一节我们总结了已学知识,本节中我们来看看该领域面临的挑战和未来可能的发展方向。


以下是相对明确的发展趋势:

- 新模型架构:我们将持续发现新颖有趣的深度学习模型。例如,神经ODE将残差网络视为微分方程的数值积分,提供了看待神经网络的全新视角,预示着模型形式的不断扩展。
- 新应用领域:深度学习将继续渗透到更多领域,特别是在科学和医疗领域(如医学影像分析),帮助研究人员分析复杂数据。它甚至可能改进计算机科学的基础构件,例如有研究尝试用神经网络学习更好的哈希函数。
- 更大规模的数据与计算:历史趋势表明,最先进AI系统训练所用的计算量呈指数增长。为了持续扩展,我们可能需要像Cerebras的晶圆级芯片这样的新型专用硬件。

以下是亟待解决的重大问题:
- 模型偏见:机器学习模型会吸收并放大训练数据中存在的社会偏见,导致对不同群体的不公平对待。例如,词向量模型会学习到性别刻板印象,图像分类器在识别不同文化或收入阶层的物品时表现失衡,甚至出现过将人错误分类为动物的严重种族偏见案例。构建公平、无偏的模型是紧迫的伦理和技术挑战。
- 理论理解缺失:当前深度学习缺乏坚实的理论基础,一些实验现象令人费解:
- 彩票假设:大型神经网络中存在一个随机初始化的子网络,经过训练后,仅该子网络就能达到良好性能。
- 泛化之谜:神经网络可以完美拟合随机标签的数据集,但在真实数据上却能很好地泛化,这与经典统计学习理论相悖。
- 双重下降现象:模型性能随复杂度增加先下降后上升,这超出了传统偏差-方差权衡的解释范围。这些“神秘”现象暗示我们可能需要新的理论。
- 对大量标注数据的依赖:收集大规模标注数据成本高昂。减少这种依赖是重要方向:
- 少样本学习:通过构建专门的数据集(如Omniglot、Kanji、以及新兴的LVIS实例分割数据集)来推动模型从少量样本中学习。
- 自监督学习:设计无需人工标注的“前置任务”来预训练模型,例如求解拼图、图像着色、图像修复等,让模型从数据本身的结构中学习表征,再在下游任务上微调。
- 缺乏真正的“理解”与常识:当前模型本质上是数据的模仿者,缺乏对人类世界的基本理解。例如,超大规模语言模型在需要常识推理的句子补全任务中会犯低级错误;目标检测模型在面对训练分布外的情况(如房间里出现一只大象)时,可能会完全失效或产生连锁错误,表现出脆弱性。如何让模型获得类似人类的、稳健的常识和理解能力,是通向更通用人工智能的根本性挑战。
📝 总结



本节课中我们一起回顾了本学期计算机视觉与深度学习的完整知识体系,从基础概念到前沿应用。同时,我们也审视了该领域在偏见、理论、数据依赖和本质理解等方面面临的深刻挑战。现在正是进入这一充满活力的领域的绝佳时机,希望本课程所学能为你未来探索和解决这些重大挑战奠定基础。



最后,衷心感谢为本课程顺利运行付出努力的助教们,也感谢所有同学的参与。


🧠 P3:L3- 线性分类器

在本节课中,我们将要学习线性分类器。线性分类器是计算机视觉和机器学习中最基础、最重要的模型之一。虽然它结构简单,但它是构建更复杂神经网络模型的核心组件。理解线性分类器的工作原理,将为我们后续学习深度神经网络打下坚实的基础。
📚 课程回顾与引入
在上一节课中,我们讨论了图像分类问题,并介绍了K最近邻(KNN)分类器。我们了解到,KNN分类器虽然简单,但在实践中存在两个主要问题:一是预测速度慢,二是基于原始像素值的距离度量(如L1或L2距离)在感知上并不直观。


本节中,我们将介绍一种不同的、更实用的方法——线性分类器。线性分类器是参数化方法的一个简单实例,它将学习过程融入到模型本身。
🧩 什么是线性分类器?

线性分类器是一种参数化模型。其核心思想是,我们定义一个函数 f,它接收输入图像 x(像素值)和一组可学习的参数 W(权重),然后输出一个分数向量,表示图像属于各个类别的可能性。


对于线性分类器,这个函数 f 采用最简单的形式:一个矩阵向量乘法。具体公式如下:
f(x, W) = Wx + b


其中:
- x 是输入图像,通常被拉伸成一个列向量(例如,对于CIFAR-10的32x32x3图像,x是一个3072维的向量)。
- W 是权重矩阵(例如,对于10个类别的CIFAR-10,W的形状是10 x 3072)。
- b 是偏置向量(长度为类别数,例如10),它为每个类别提供一个额外的偏移量。
- 输出是一个分数向量,每个元素对应一个类别的得分。
🔍 理解线性分类器的三种视角
为了更好地理解线性分类器在做什么,我们可以从三个不同的角度来看待它。

1. 代数视角
这是最直接的视角,即将分类器看作矩阵乘法 Wx + b。从这个视角,我们可以清楚地看到一些特性:

- 偏置技巧:我们可以通过向输入向量 x 末尾添加一个常数1,并将偏置 b 作为权重矩阵 W 的额外一列,从而将偏置项合并到权重中。公式变为 f(x, W') = W'x',其中 x' = [x; 1],W' 包含了原来的 W 和 b。
- 预测是线性的:如果我们将输入图像 x 的所有像素值乘以一个常数 c,那么输出分数也会乘以相同的常数 c。这意味着线性分类器对图像的绝对亮度或对比度变化是敏感的。

2. 视觉/模板匹配视角
我们可以将权重矩阵 W 的每一行重新塑造成与输入图像相同的形状。这样,每一行就变成了一个“模板”图像。

分数 = 模板(权重行)与输入图像(像素)的点积


从这个视角看,线性分类器就像在进行模板匹配:
- 它为每个类别学习一个模板。
- 分类时,计算输入图像与每个类别模板的匹配程度(通过点积),匹配度越高,该类别的得分就越高。
- 通过可视化这些学习到的模板,我们可以直观地看到分类器在寻找什么特征(例如,飞机模板可能有很多蓝色,鹿的模板可能有绿色背景和棕色斑点)。


然而,这个视角也揭示了线性分类器的局限性:
- 上下文依赖:分类器可能过度依赖图像背景等上下文信息,而不是物体本身。
- 模态混合:如果一个类别(如“马”)在数据集中以多种不同形态出现(向左看、向右看),线性分类器只能学习一个“平均”模板,可能导致模板看起来模糊或不合理(例如,一个有两个头的马)。
3. 几何视角

我们可以将图像视为高维空间(维度等于像素数)中的一个点。线性分类器则通过超平面来划分这个空间。
- 每个类别对应一个超平面。
- 分类器的决策规则是:点(图像)位于哪个超平面的“正侧”,就属于哪个类别(或者选择分数最高的那个)。
这个视角有助于我们理解线性分类器的表达能力限制。它只能学习用直线(或超平面)分割的空间。因此,它无法解决一些复杂的问题,例如:

以下是线性分类器无法完美分类的几种数据分布示例:
- 异或(XOR)问题:两个类别的点呈交叉分布。
- 多模态分布:同一个类别的点分布在空间中多个不相连的区域。

历史上,感知机(一种线性分类器)无法学习XOR函数,这推动了神经网络的发展。
⚖️ 损失函数:量化模型的好坏
定义了模型(线性分类器)之后,我们需要一种方法来衡量模型预测的好坏,这就是损失函数。损失函数值越高,表示模型表现越差;值越低,表示表现越好。
总的损失 L 通常是所有训练样本损失的平均值:L = (1/N) * Σ Lᵢ(f(xᵢ, W), yᵢ),其中 Lᵢ 是单个样本的损失。
1. 多类支持向量机损失


其核心思想是:正确类别的分数应该比所有错误类别的分数高出至少一个边界值(通常为1)。

对于单个样本,其损失计算公式为:
Lᵢ = Σⱼ max(0, sⱼ - s_yᵢ + 1),其中 j ≠ yᵢ
- s_yᵢ 是正确类别的分数。
- sⱼ 是错误类别 j 的分数。
- 这个函数被称为合页损失。
特性:
- 最小可能损失为0(当正确分数比所有错误分数至少高1时)。
- 最大可能损失为无穷大。
- 如果所有权重 W 初始化为很小的随机值,所有分数 s 都接近0,那么初始损失期望值约为(类别数 - 1)。这是一个有用的调试参考。
2. 交叉熵损失

这种损失函数为模型的输出分数赋予了概率解释。它希望模型输出的分数能经过转换后,形成一个有效的概率分布。
步骤如下:
- Softmax函数:将原始分数(称为 logits)转换为概率。
- 公式:P(y=k | x) = exp(s_k) / Σⱼ exp(s_j)
- 它取指数确保为正,然后归一化确保和为1。
- 计算损失:损失是正确类别预测概率的负对数。
- 公式:Lᵢ = -log(P(y=yᵢ | x))
特性:
- 最小可能损失为0(但理论上只有当正确类别的概率为1时才达到,实践中无法达到)。
- 最大可能损失为无穷大。
- 如果分数是小的随机值,Softmax输出近似均匀分布,初始损失期望值约为 -log(1/C) = log(C),其中C是类别数(对于CIFAR-10,约为2.3)。这同样是一个重要的调试基准。
SVM损失 vs. 交叉熵损失

- SVM损失 更“满足”:一旦正确分数比错误分数高出边界值,损失就为0,不再关心分数继续拉大。
- 交叉熵损失 更“贪婪”:它永远希望正确类别的概率越接近1越好,因此会持续推动正确分数升高,错误分数降低。
🛡️ 正则化:防止过拟合与表达偏好
我们可能会发现,许多不同的权重矩阵 W 在训练数据上都能达到相同的(甚至为零的)损失。为了从中选出我们更喜欢的模型,我们需要引入正则化。
总损失函数变为:L = (1/N) * Σ Lᵢ(f(xᵢ, W), yᵢ) + λ R(W)
- 第一部分是数据损失,衡量模型对训练数据的拟合程度。
- 第二部分是正则化损失,它不依赖于数据,用于惩罚复杂的模型。λ 是一个超参数,控制两者的权衡。

正则化的作用:
- 表达偏好:引导模型选择更“简单”或更符合我们先验知识的权重。
- 防止过拟合:避免模型过于复杂,只完美记忆训练数据而在新数据上表现糟糕。
- 改善优化:有时能使损失函数的形状更利于优化。


常见的正则化方法:
- L2正则化:R(W) = Σ Σ W_i,j²,倾向于让权重向量更分散,较小。
- L1正则化:R(W) = Σ Σ |W_i,j|,倾向于产生稀疏权重,让许多权重为0。
- 弹性网:L1和L2的结合。

🎯 总结
本节课中我们一起学习了:
- 线性分类器的基本形式:f(x, W) = Wx + b,它是参数化模型和神经网络的基础模块。
- 理解线性分类器的三种视角:代数视角、视觉模板视角和几何视角,每种视角都揭示了模型的不同特性和局限。
- 损失函数的概念,它用于量化模型预测的错误程度。我们重点介绍了多类SVM损失和交叉熵损失,并比较了它们的特性。
- 正则化的重要性,它通过在损失函数中添加一个与数据无关的项,来防止模型过拟合并引导模型选择更优的权重。




现在,我们已经知道了如何用一个损失函数来评价线性分类器。下一个关键问题是:如何找到那个使损失函数最小化的最优权重矩阵 W? 这就是我们下节课要讨论的主题——优化。


课程 P4:L4 - 训练与优化 🚀
在本节课中,我们将要学习如何为线性分类器找到最优的权重矩阵。具体来说,我们将探讨优化这一核心主题,即如何最小化损失函数,从而找到能使模型在训练数据上表现最佳的权重参数。
概述 📋
在之前的课程中,我们介绍了线性分类器,并从代数、视觉和几何三个角度理解了它。上一节课,我们讨论了如何使用损失函数(如Softmax和SVM损失)来量化对不同权重值的偏好,以及正则化如何帮助我们构建更简单、泛化能力更强的模型。
然而,我们留下了一个开放性问题:给定一个线性分类器设置和一个损失函数,我们如何实际找到能最小化该损失的权重矩阵 W?这正是本节课要解决的核心问题。
优化问题:寻找最优权重 🎯
广义上讲,寻找能最小化损失函数的权重矩阵属于优化的范畴。我们的通用框架是:我们有一个损失函数 L(W),它输入权重矩阵,输出一个标量损失值。
我们可以将优化过程直观地想象为在一个高维“地形”中行走。地面上的每个点(x, y)对应一个不同的权重矩阵 W 值,而该点的高度则对应损失函数 L(W) 的值。我们的目标是找到这个地形的最低点(即损失最小的点)。

迭代优化方法 🔄
对于某些特殊的优化问题(如线性回归),我们或许可以直接写出最优解的解析式。但在一般情况下,这是不可能的。因此,我们需要使用迭代方法来逐步改进我们的解,朝着目标地形的最低点前进。
方法一:随机搜索(一个糟糕但具有启发性的想法)

以下是随机搜索的基本思路:
- 生成大量随机的权重矩阵值。
- 在训练集上评估这些随机矩阵的损失值。
- 记录在整个随机搜索过程中找到的最低损失值。


虽然这是一个非常低效的算法,但令人惊讶的是,即使在CIFAR-10数据集上,它也能达到约15.5%的准确率(尽管当前最优水平约为95%)。这启发了我们使用更智能的算法。

梯度下降:沿着坡度下山 ⛰️
在实践中,我们将采用第二种思路:沿着地形的坡度下山。即使我们“蒙着眼睛”,也可以通过感受脚下地面的局部倾斜方向,朝着最陡的下降方向迈出一小步,并不断重复这个过程。
为了使这个概念更形式化,我们需要引入梯度的概念。
梯度的概念
对于一个输入向量、输出标量的函数(这正是我们的损失函数),其梯度是一个向量。它指向函数值增长最快的方向,其大小表示该方向上的斜率。
由于我们希望最小化损失,因此我们实际要移动的方向是梯度的反方向,即负梯度方向。
计算梯度 🧮


那么,我们如何计算任意函数的梯度呢?主要有两种方法。
数值梯度
我们可以直接通过软件实现梯度的极限定义。基本思想是:对权重矩阵 W 的每一个元素进行微小的扰动,计算损失函数的变化,然后用差分来近似该方向上的导数(斜率)。
公式:对于 W 的第 i 个元素,数值梯度近似为:
grad_approx[i] = (L(W + epsilon * e_i) - L(W)) / epsilon
其中 e_i 是第 i 个基向量,epsilon 是一个很小的数(如1e-5)。
然而,这种方法有两个主要缺点:
- 非常慢:对于有 N 个参数的模型,需要计算 N+1 次前向传播。
- 是近似值:依赖于有限差分,并非精确梯度。

解析梯度(分析梯度)
我们可以利用微积分知识,直接推导出损失函数关于权重矩阵 W 的梯度解析表达式。这样,我们就能通过一次计算得到精确的梯度值。
在实践中,对于复杂的函数(如后续的神经网络),我们将使用反向传播算法(将在第6课详细讲解)来高效、结构化地计算这些梯度。

梯度检查:确保正确性 ✅
由于推导和实现解析梯度可能出错,一个非常好的实践是使用数值梯度作为调试工具来验证解析梯度的正确性。


具体做法是:在一个小规模、低维的问题上,同时计算数值梯度和解析梯度,并比较它们是否足够接近。PyTorch等框架也提供了 gradcheck 函数来帮助完成这项工作。在完成作业和实际开发中,强烈建议使用梯度检查。
梯度下降算法 📉
现在,我们终于可以介绍梯度下降算法了。其核心思想非常简单,可以用几行伪代码概括:

# 梯度下降伪代码
W = random_initialization() # 初始化权重
for i in range(num_iterations):
grad = compute_gradient(L, W, data) # 计算损失L在W处的梯度
W = W - learning_rate * grad # 沿负梯度方向更新权重
这个算法引入了几个重要的超参数:
- 权重初始化方法:如何设置 W 的初始值。
- 迭代步数:算法运行多少次更新。
- 学习率 (learning_rate):控制每次沿着梯度方向更新权重时的步长大小。这是最重要的超参数之一。

随机梯度下降 (SGD) 🎲
上述“朴素”的梯度下降算法(又称全批量梯度下降)有一个严重问题:我们的损失函数通常是所有训练样本损失的总和。计算一次梯度就需要遍历整个数据集,当数据集很大时(例如数百万样本),这将极其缓慢。

解决方案是使用随机梯度下降。其核心思想是:在每次迭代中,我们不计算整个数据集的梯度,而是随机抽取一小部分样本(称为小批量,mini-batch),用这个小批量的平均梯度来近似整个数据集的梯度。
公式:损失函数可写为期望形式 L(W) = E_{(x,y)~p_data}[L_i(W)]。SGD 使用蒙特卡洛估计来近似这个期望和其梯度。
SGD 引入了新的超参数:
- 批量大小 (batch_size):每个小批量包含的样本数。经验法则是:在GPU内存允许的范围内,尽可能使用较大的批量。
- 数据采样方法:如何从数据集中抽取小批量。对于分类问题,通常在每个周期(epoch)开始时打乱数据顺序即可。

SGD 可能遇到的问题与改进 🛠️
基本的 SGD 算法在实践中可能会遇到一些问题:
- 病态条件(高条件数):损失函数在不同方向上的曲率差异很大。学习率设置大了会在陡峭方向振荡,设置小了在平缓方向进展缓慢。
- 局部最小值和鞍点:梯度为零的点可能导致算法停滞。在高维空间中,鞍点比局部最小值更常见。
- 梯度噪声:由于使用小批量估计梯度,更新方向存在噪声,导致收敛路径曲折。

为了克服这些问题,人们开发了 SGD 的改进变体。
SGD + 动量 (Momentum)
动量法引入了物理直觉:想象一个球滚下山坡。它不仅受当前坡度(梯度)影响,还积累了过去移动的“速度”。
算法核心:
- 维护一个速度变量 v,它是历史梯度的指数加权平均。
- 更新时,沿着速度 v 的方向移动,而不是仅仅沿着当前梯度。
公式(一种常见形式):
v = rho * v + grad
W = W - learning_rate * v
其中 rho 是动量系数(如0.9),控制历史速度的衰减。

动量法有助于:
- 平滑梯度噪声。
- 帮助冲出平坦区域或鞍点。
- 在条件数高的方向上加速。

涅斯捷罗夫动量 (Nesterov Momentum)
这是动量法的一个变体,具有“前瞻性”。它先根据当前速度“展望”一步,在那个展望点计算梯度,然后结合该梯度更新速度。
直观理解:它比标准动量法能更早地感知到地形的变化并进行调整,从而减少振荡。
自适应学习率算法

这类算法通过调整每个参数的学习率来改进 SGD。

AdaGrad:为每个参数累积历史梯度平方。更新时,用累积平方根的倒数来缩放学习率。这会使在频繁更新(梯度大)的参数上学习率变小,在稀疏更新(梯度小)的参数上学习率相对变大。但累积平方和会一直增长,可能导致学习率过早衰减至零。
RMSProp:解决了 AdaGrad 学习率衰减过快的问题。它使用指数衰减移动平均来累积历史梯度平方,而不是简单求和。这样,历史信息会逐渐淡忘。
Adam (Adaptive Moment Estimation):结合了动量和RMSProp的思想,是目前最流行、最鲁棒的优化器之一。
- 它同时计算梯度的一阶矩(动量)和二阶矩(梯度平方的移动平均)。
- 包含偏差校正,以解决训练初期矩估计偏向零的问题。
Adam 的超参数经验值(一个很好的起点):
beta1(一阶矩衰减率):0.9beta2(二阶矩衰减率):0.999learning_rate:1e-3 或 1e-4

Adam 因其对超参数相对不敏感、在许多任务上表现良好而成为深度学习中的默认优化器之一。另一个常用选择是 SGD + 动量,它有时能获得更好的最终性能,但通常需要更精细的超参数调优。
二阶优化方法(简要提及) ⚙️

上述方法都只使用了一阶导数(梯度)信息,属于一阶优化算法。我们还可以使用二阶导数(海森矩阵,Hessian)信息来构建局部二次近似,从而更智能地选择步长。

然而,对于高维模型(参数数量 N 很大),海森矩阵的大小是 N×N,存储和求逆的计算成本(O(N³))过高,使其不适用于大规模深度学习。像 L-BFGS 这样的拟牛顿法是低维问题或非随机问题的好选择,但在深度学习中远不如 SGD 和 Adam 系列算法常用。
总结 🎓
在本节课中,我们一起学习了机器学习和深度学习的核心——优化。
我们首先明确了优化问题的目标:找到最小化损失函数的权重。然后,我们从最简单的随机搜索出发,引入了强大的梯度下降框架,并解释了如何通过数值梯度和解析梯度来计算更新方向。
为了解决大数据集上的效率问题,我们学习了随机梯度下降 (SGD)。为了克服 SGD 的缺陷(如振荡、鞍点、噪声),我们深入探讨了几种改进算法:
- SGD + 动量:通过积累速度来平滑更新。
- 自适应学习率算法(AdaGrad, RMSProp):为每个参数调整学习率。
- Adam:结合动量和自适应学习率的优势,是实践中强大且鲁棒的选择。

最后,我们简要了解了二阶优化方法及其在大规模应用中的局限性。


现在,我们已经掌握了使用线性模型、损失函数和优化算法来解决图像分类问题的全套工具。在下一节课中,我们将把这些概念结合起来,最终开启神经网络的大门,用更强大的非线性模型来替代线性分类器。
课程 P5:L5 - 神经网络介绍 🧠
在本节课中,我们将要学习神经网络。这是本课程的核心内容,我们将首次探讨深度学习模型。在此之前,我们已经讨论了使用线性模型构建参数化分类器、使用不同的损失函数量化对权重设置的满意度,以及使用随机梯度下降及其变体(如动量、Adam、RMSProp)来最小化目标函数。今天,我们将从线性模型出发,首次探索基于神经网络的模型,这些模型将更加强大,使我们能够以更高的准确率对图像进行分类。
从线性模型到特征变换
上一节我们介绍了线性模型及其局限性。本节中,我们来看看如何通过特征变换来克服这些限制。
线性分类器虽然简单易懂,但其所能表示的函数类型非常有限。从几何角度看,线性分类器通过高维超平面分割空间,但在某些情况下(例如左侧的示例),线性分类器无法将绿色点与蓝色点分开。从视觉角度看,线性分类器为每个类别只学习一个模板,因此无法表示同一对象类别的多种模式(例如,同时看向左侧和右侧的马)。
在转向神经网络之前,我们需要讨论另一种克服线性分类器局限性的方法:特征变换。
特征变换的思想是,我们将原始数据从其原始的输入空间(左侧)通过某种精心选择的数学变换,转换到一种新的表示形式,希望这种新形式更易于分类。
例如,对于左侧的数据,我们凭直觉认为,将数据从笛卡尔坐标转换为极坐标可能是一种更好的表示方式。我们可以写下这个特征变换,将数据的笛卡尔表示转换为极坐标表示。应用此变换后,数据现在存在于一个由所选特征变换的数学形式定义的新空间(特征空间)中。
这种特征变换对这个问题的特别有用之处在于,将输入数据集从笛卡尔坐标转换为极坐标后,我们可以看到该数据集在极坐标下实际上变得线性可分了。因此,我们可以想象,不是在原始输入数据空间上训练线性分类器,而是在数据的特征空间表示上训练线性分类器。然后,如果我们把这个在特征空间中的线性决策边界映射回左侧的原始空间,我们会发现特征空间中的线性决策边界对应于原始空间中的某种非线性决策边界或非线性分类器。
因此,通过巧妙地选择适合数据特性的特征变换,有可能克服我们目前看到的线性分类器的一些局限性。
计算机视觉中的特征变换示例
这个从笛卡尔坐标到极坐标的变换例子看似简单,但更广泛地应用特征变换时,必须仔细考虑所处理数据的结构,并思考可能对输入数据应用哪些类型的函数变换,使其更易于下游的线性分类。
这不仅仅是假设,特征变换的概念在计算机视觉中曾被广泛使用,甚至在某些子领域中仍在应用。
以下是计算机视觉中可能使用的特征变换示例:
- 颜色直方图:我们可以将颜色空间(RGB光谱)划分为若干离散的区间,然后为输入图像中的每个像素分配其所属的区间。特征表示可以是图像中出现颜色的某种归一化直方图。这种颜色直方图表示丢弃了图像的所有空间信息,只关心图像中存在的颜色类型。因此,可以想象,颜色直方图表示可能更具空间不变性。
- 方向梯度直方图:其基本思想与颜色直方图有些相反。它丢弃了所有颜色信息,因为它只关心局部边缘的方向和强度。它告诉我们输入图像中每个位置的局部边缘方向和强度。例如,它可以告诉我们红色区域有较强的对角线边缘,蓝色区域(青蛙眼睛周围)有各种方向的边缘,而黄色区域(图像右上角)由于是模糊背景,边缘信息很少。这种表示在2000年代中后期被广泛用于目标检测等任务。
- 视觉词袋:这是一种数据驱动的特征变换方法。其思想是,我们有一个大型训练图像集,从中提取大量随机图像块(不同尺度、大小和长宽比)。然后,我们对所有这些训练图像的随机块进行聚类,得到一个码本或一组视觉词,用于表示图像中倾向于出现的特征类型。这样,如果图像中存在许多训练集中常见的结构类型,你就有望学习到能够捕获或识别这些常见特征的视觉词表示。然后,在构建视觉词码本后,第二步是使用学习到的码本对图像进行编码,计算每个输入图像中每个视觉词出现频率的直方图表示。这种方法比之前看到的一些特征表示更灵活,因为它不需要实践者完全指定特征表示的函数形式,而是允许特征(本例中的视觉词)从训练数据中学习,以更好地适应手头的问题。
另一个常见的技巧是,你不必只满足于一种特征表示。可以想象拥有输入图像的多种不同特征表示,然后将它们全部连接成一个长的特征向量。例如,你可以在顶部连接颜色直方图表示,中间连接词袋表示,底部连接方向梯度直方图,从而得到一个长的高维特征向量,从不同方面描述你的图像。这种结合多种特征表示的想法在2000年代末和2010年代初的计算机视觉中非常常用。
从特征提取到端到端学习
回顾特征提取,我们可以将其视为以下流程:我们将系统分解为两部分,一部分是特征提取器,另一部分是在该特征表示或特征空间之上运行的可学习模型。通常,即使特征提取阶段有某种数据驱动的组件(如视觉词袋示例),该阶段也不一定会自动调整自身以直接最大化整个系统的分类性能。
相反,当我们在固定的特征表示之上学习线性分类器时,我们最终会得到一个从原始图像像素到最终分类分数的非常庞大复杂的系统,但只有该系统末端的一小部分会根据分类准确率来调整其权重。
相比之下,我们可能希望做得更好,即自动调整系统的所有部分,以最大化图像分类任务的性能。这就是思考神经网络作用的一个动机。
当我们构建用于图像分类的深度神经网络系统时,我们所做的是构建一个单一的端到端流程:左侧接收图像的原始像素,右侧预测分类分数或分类概率。在训练过程中,我们不仅会调整使用线性分类器的最后一层,还会联合调整整个系统的所有部分,以最大化分类性能。从这个角度看,基于神经网络的系统与这些大型深度特征表示系统并没有太大不同。基本上,变化在于神经网络以某种方式联合学习特征表示和该特征表示之上的线性分类器,以最大化系统的分类性能。
神经网络模型详解
有了这个介绍,我们终于可以讨论神经网络的具体示例了。
到目前为止,在本课程中,我们非常熟悉线性分类器:给定存储在巨型列向量 x 中的输入数据,我们的线性分类器将有一个可学习的权重矩阵 W(大小为 输入维度 × 类别数),线性分类器就是输入数据与该矩阵的矩阵向量乘法。
我们最简单的神经网络模型并不比线性分类器复杂多少。我们仍然用单个长列向量表示输入数据(将图像的所有原始像素值拉伸成一个大向量),但现在我们有两个可学习的权重矩阵。
第一个可学习的权重矩阵是 W1,其形状为 输入维度 × H,其中 H 称为神经网络的隐藏层大小。我们首先在输入数据 x 和第一个可学习的权重矩阵 W1 之间进行矩阵向量乘法,生成一个维度为 H 的新向量。然后,我们对该向量应用逐元素的最大值函数(即 ReLU 激活函数)。接着,我们执行第二个矩阵向量乘法,使用第二个可学习的权重矩阵 W2(大小为 H × C,其中 C 是类别数)。实际上,每个矩阵向量乘法通常还会有一个相关的偏置项,但为了简化表示,在公式中常常省略偏置项,尽管在训练系统时确实会使用它们。
我们可以将神经网络的概念推广到任意数量的权重矩阵。在每个阶段,我们将获取当前输入向量,应用矩阵乘法并加上偏置,应用逐元素的最大值函数,然后重复此过程,直到应用了系统中所有可学习的权重矩阵。
我们经常看到这种系统用类似下图的图示表示:数据从左向右流经这个基于神经网络的系统,左侧是我们的输入数据,中间是隐藏向量 h(在此示例中可能有 100 个元素),右侧是我们的最终分数向量 s(可能给出 10 个类别的分类分数)。我们将这些权重矩阵想象为位于神经网络的每一层之间,可以将这些权重矩阵解释为告诉我们前一层每个元素如何影响下一层的每个元素。例如,如果我们查看第一个可学习权重矩阵 W1 的元素 (i, j),那么这个标量值告诉我们隐藏层中的元素 h_i 受输入元素 x_j 影响的程度。对于第二个权重矩阵 W2 也有类似的解释,显示隐藏向量的每个元素如何影响输出分数的每个元素。
由于这些是密集的通用矩阵,我们可以看到,在这种特定结构中,输入 x 的每个元素都会影响隐藏向量 h 的每个元素,同样,隐藏向量 h 的每个元素也会影响最终分数向量 s 的每个元素。由于这种密集的连接模式,这种类型的神经网络通常被称为全连接网络。这种结构有时也被称为多层感知机。
神经网络的解释与激活函数的作用
现在,我们可以为神经网络的计算添加一些额外的解释。回想一下,在线性分类器中,我们将其解释为学习每个类别的一组模板,然后分数是每个大模板与输入图像的内积,这表示输入图像与每个模板的匹配程度。
现在,我们可以用类似的方式解释神经网络的第一层权重矩阵。神经网络的第一个权重矩阵 W1 也学习一组模板。我们可以将隐藏层中的值解释为每个学习到的模板对输入图像 x 的响应程度。这些模板大多不易解释,你并不总能看出这些模板在做什么,但它们肯定学习到了一些可辨别的空间结构。但有时你很幸运,在第一层确实能得到一些漂亮的模板。例如,在展示的两个示例中,它们实际上看起来像一匹马朝向一个方向,另一匹马朝向另一个方向。这终于克服了困扰我们几周的“双头马”问题。然后,神经网络的第二层通过另一种加权组合来预测其分类分数,重新组合输入图像对这些模板的响应。
这意味着神经网络最终有望以某种方式识别一个类别的多种类型或多个子集。例如,它可以使用一个模板识别向左看的马,使用另一个模板识别向右看的马,然后使用第二层中的权重来重新组合来自这两个模板的信息。这有时被称为分布式表示,因为通过以某种线性组合这些模板,网络表示了图像的某些信息,但我们并不总能清楚地解释这些不同模板试图捕获什么。
一旦我们有了神经网络的概念,你肯定可以想象将其推广到许多层。神经网络的深度通常是它包含的层数。当我们计算层数时,通常计算的是权重矩阵的数量。因此,我们的两层网络将有两个可学习的权重矩阵。网络的宽度将是每个隐藏表示的单元数或维度。在实践中,原则上每一层可能具有不同的特征维度,但更常见的是在整个网络中设置相同的宽度。
激活函数:ReLU 及其重要性
之前我们提出了一个非常敏锐的问题:这个神经网络方程中出现的奇怪的 max 函数是做什么的?事实证明,这是神经网络中一个非常重要的组成部分。这个函数 max(0, 输入) 在输入和零之间取逐元素的最大值,这意味着我们输入一个向量,然后将任何负值丢弃并设为零。它如此重要且被广泛使用,以至于被赋予了特殊的名称 ReLU(修正线性单元)。
我们把这个函数想象为夹在两个可学习的权重矩阵之间,这个函数被称为神经网络的激活函数,它对于神经网络的功能实际上至关重要。
作为一个思考练习,你应该想一想:如果我们构建一个没有激活函数的神经网络系统,即在两个权重矩阵之间直接连接,会有什么问题?具体来说,如果我们取输入向量乘以第一个权重矩阵 W1,然后直接乘以第二个权重矩阵 W2,这种基于神经网络的方法会有什么问题?
答案很明确:它仍然是一个线性分类器。因为矩阵乘法具有结合律,你可以将这两个矩阵乘法组合在一起,这又退化回一个线性分类器。因此,如果没有某种非线性函数夹在我们的两个矩阵乘法之间,我们就无法获得超越线性分类器的额外表示能力。所以,激活函数的选择及其存在对于神经网络的功能绝对至关重要。
需要指出的是,有时这些没有激活函数的网络被称为深度线性网络,优化社区有时会研究它们,因为即使它们的表示能力与线性分类器相同,其优化动态实际上比线性分类器复杂得多。因此,人们有时确实会在优化理论背景下研究它们。
在这个神经网络的第一个例子中,我们选择激活函数为 ReLU 函数。但人们还使用许多其他不同的激活函数。一个在 2000 年代中期之前可能非常常用的激活函数是 Sigmoid 激活函数,它从零开始,然后上升到一。人们有时会使用一整套这类函数。我们将在后面的讲座中讨论选择这些函数的原因。简而言之,ReLU 是一个相当好的默认选择,对于大多数应用,坚持使用 ReLU 可能不会出错。它无疑是当今深度学习中使用最广泛的激活函数,在大多数深度学习应用中,你都应该考虑使用它。
神经网络的简单实现
我还想指出,这个神经网络系统实际上实现起来超级简单。我们可以用仅仅 20 行代码训练一个完整的神经网络系统。这里我使用 NumPy 而不是 PyTorch,因为我不能透露你们的作业内容。前几行代码设置了一些随机数据和随机权重。第二部分代码执行我们所说的前向传播,即根据输入计算分数函数。这里我们也使用了 Sigmoid 非线性激活函数。然后,我们计算关于权重矩阵的梯度,并在此处执行梯度下降步骤。你可以看到,这个神经网络系统实现起来相当简单。如果你实际复制粘贴这段代码并在终端中运行,你将用 20 行 Python 代码训练自己的神经网络。
“神经”一词的探讨与生物类比
在讨论神经网络时,有一个词人们常常感到困惑,那就是“神经”这个词。我认为,如果没有至少承认神经网络模型中“神经”这个词的存在,我们就不能开设深度学习课程。因此,我们必须稍微讨论一下,但我绝不是神经科学家。所以,我预计我可能会在神经科学方面说错一些话,但我会尽力,请不要问我太多关于神经科学的难题。
基本思想是,我们的大脑是惊人的生物有机体,大脑的基本构建单元是称为神经元的小细胞。神经元有几个重要组成部分:中间有细胞体,所有活动都在那里发生;右侧有一个长长的终端,称为轴突,神经元通过它将电脉冲从细胞体发送出去。这些神经元通过巨大的网络连接在一起,轴突将电信号发送给其他神经元。来自轴突的电信号被细胞体上其他小的突起(称为树突)接收。一个神经元的树突和另一个神经元的轴突之间的间隙称为突触。基本上,中间的这个神经元将收集从左传入神经元轴突传来的所有电脉冲。这些电脉冲会通过树突和轴突之间的突触连接以某种方式被调制或修改。然后,在某个时刻,基于电信号的速率和转换,这个神经元最终会向下游连接的其他神经元发送一些其他电信号。
我们可以通过表示神经元的放电频率来思考这些神经元,即神经元以某种速率放电电脉冲。放电电脉冲的速率可能是所有左侧输入神经元输入连接速率的某种非线性函数。然后,我们可以想象一个非常非常简单、粗糙的神经元数学模型:细胞体收集来自左侧所有神经元的输入信号,然后对左侧所有传入神经元的放电频率求和。然后,基于左侧所有传入放电频率的总和,我们应用某种非线性函数(可能是 Sigmoid、ReLU 或其他类型的非线性函数),计算该神经元作为所有输入神经元放电频率函数的放电频率。然后,这个放电频率将被发送到网络中其他部分的下游神经元。
这基本上就是相似之处的终点。我认为,右边这里基本上就是我们今天在神经网络系统中所做的事情。正如你所见,这是对生物神经元的一种粗略近似,但生物神经元和人工神经元以及人工神经网络之间存在许多许多差异。因此,你不应该过于纠结于这些相似之处。
一个相似之处(或者说不同之处)是,生物神经元倾向于组织成非常非常复杂的网络,这些网络可能高度不规则,甚至可能循环回来,一个神经元循环并将信号发送回自身,在时间上循环。因此,在真实的哺乳动物大脑中,神经元可以具有非常复杂的拓扑结构。相比之下,当我们使用基于人工神经网络的系统时,我们通常将神经元组织成层。这些层是一种人工构造,允许我们使用高效的矩阵和向量操作同时执行所有这些乘法和求和。因此,层的概念是对真实哺乳动物大脑中可能存在的大量神经元集合的一种抽象。
但人们正在变得有创造力,并开始探索具有非常疯狂甚至随机连接模式的人工神经网络系统,这实际上有时在某些情况下也能工作。例如,我在 Facebook AI Research 的一些同事去年发表了一篇论文,他们训练了具有右侧所示连接模式的神经网络,这些模式看起来完全疯狂,但它们实际上能够训练并获得接近最先进的性能。所以,我想如果小心处理,随机连接有时在这些人工系统中也能工作。
但总的来说,你应该非常小心地对待大脑的类比。尽管“神经”这个词出现在神经网络这个术语中,我认为在这一点上,它实际上更像是一个历史术语,你不应该过分看重我们的人工系统与实际生物神经元之间的类比。特别是,在我们的系统中,我们的神经元在某种程度上都是相同类型的;而在真实的大脑中,你可能想象有不同类型的神经元具有不同的专门功能。你可以想象,我们的神经元内部可以执行非常复杂的非线性计算,这些计算无法用我们简单的激活函数很好地建模。你还可以记住,我们用一个单一的标量放电频率来模拟这些神经元,这是我们用来表示大脑中神经元活动水平的主要抽象,这可能过于粗糙,无法真实地表示神经元内部和神经元之间实际发生的情况。
关于大脑,我想说的就这么多。所以,让我们回到数学和工程学,希望我对此略知一二。
神经网络为何强大:空间扭曲与通用近似定理
那么,问题是:如果我们不把这些大脑类比看得太重,那么为什么实际上应该选择神经网络作为强大的图像分类系统,或者更广泛地说,作为强大的函数逼近系统?
我们已经粗略地看到了神经网络如何在其第一层表示多个模板,然后在第二层重新组合它们。但我认为另一种有趣的方式来思考和理解为什么神经网络是如此强大的系统,是通过空间扭曲的概念。
我们想思考线性分类器的几何观点。记住,对于线性分类器,当我们从几何角度思考时,我们将数据点视为都存在于这个高维空间中,然后线性分类器的每一行在我们的输入空间中产生一个平面或直线。以前,我们总是从预测分类分数的角度来思考这里发生的事情,但另一种思考方式是将此视为扭曲输入空间。
我们可以想象做的是:我们获取具有特征 x1 和 x2 的输入空间,并将其转换为另一个特征空间,该空间具有坐标 h1 和 h2(对应于我们二维隐藏单元的两个维度)。如果我们有一个线性变换,你可以想象空间的所有这些区域都以线性方式变形。通过这个二维线性变换,我们在输入空间中得到了两条线,这两条线将输入空间划分为四个区域,每个区域都被转换到这个变换后的输出空间中的四个象限,现在空间将以这种线性方式被旋转和变换。


现在,我们可以思考当我们尝试训练线性分类器时会发生什么。如果我们尝试在线性变换之上训练线性分类器,你可以想象我们在左侧有一堆数据点,其中蓝色可能是一个类别的图像,橙色是另一个类别的图像。一旦我们应用这个线性变换来线性扭曲空间,现在我们将输入数据转换为某种新的表示。但是,因为这个特征变换只是线性地修改输入空间,我们可以看到,即使我们变换了空间,点在这个新的变换后的输出空间中仍然不是线性可分的。因此,以某种方式在线性特征变换之上应用线性分类器
📚 课程 P6:L6 - 反向传播
在本节课中,我们将要学习反向传播。这是一种用于计算复杂神经网络模型中梯度的核心算法。通过将计算过程表示为计算图,我们可以高效地自动计算损失函数相对于所有模型参数的导数,从而利用随机梯度下降等优化算法来训练模型。
🧠 背景与动机
上一节我们介绍了神经网络,并看到了其强大的非线性表达能力。然而,我们也面临一个问题:如何计算这些复杂模型损失函数的梯度?
一种朴素的方法是手动在纸上推导梯度表达式。然而,这种方法存在几个问题:
- 它极其繁琐,对于复杂的损失函数(如交叉熵损失或SVM损失)和模型结构,推导过程会变得非常冗长。
- 它无法扩展到复杂模型。对于像AlexNet这样的深度卷积神经网络,手动推导梯度是不现实的。
- 它缺乏模块化设计。每次更改模型架构、损失函数或正则化器时,都需要从头开始重新推导所有梯度。
在深度学习中,我们倾向于使用计算图这一数据结构来帮助我们解决梯度计算的难题。
🗺️ 计算图
计算图是一种有向图,用于表示模型内部执行的计算。图的左侧是输入节点(如数据 X 和权重 W),随着我们从左向右遍历,节点代表了计算过程中的基本操作(如矩阵乘法、激活函数、损失计算等),图的右侧最终输出标量损失 L。
对于线性模型,计算图可能看起来很简单,但对于复杂的深度模型(如包含多个卷积层和非线性的AlexNet,或更复杂的神经图灵机),计算图变得庞大而复杂。这时,我们绝对需要依赖计算图形式化和图遍历算法来自动计算梯度。
🔄 前向传播与反向传播:一个简单例子
为了理解如何利用计算图计算梯度,我们来看一个简单的标量函数例子:f(x, y, z) = (x + y) * z。
假设我们想在点 (x=-2, y=5, z=-4) 处计算函数值及其梯度。
前向传播
在前向传播中,我们按从左到右的顺序执行图中节点指定的所有操作,从输入值计算输出值。
- 计算
q = x + y = -2 + 5 = 3 - 计算
f = q * z = 3 * (-4) = -12
反向传播
在反向传播中,我们的目标是计算输出 f 相对于每个输入 (x, y, z) 的导数。我们从右向左遍历图,从输出开始。
以下是计算过程,我们通常将每个节点的前向计算值写在线上方,反向传播的梯度值写在线下方。
- 基础情况:计算
df/df = 1。 - 计算
df/dz:我们知道f = q * z。局部梯度(f对z的导数)是q。查前向传播结果,q = 3。因此,df/dz = q = 3。 - 计算
df/dq:同样,f = q * z。局部梯度(f对q的导数)是z。查前向传播结果,z = -4。因此,df/dq = z = -4。 - 计算
df/dy:这里y不直接与f相连,需要通过中间变量q。我们需要使用链式法则:df/dy = (dq/dy) * (df/dq)。dq/dy是局部梯度。由于q = x + y,所以dq/dy = 1。df/dq是上游梯度,我们已计算出为-4。- 因此,
df/dy = 1 * (-4) = -4。
- 计算
df/dx:过程与计算df/dy完全对称。df/dx = (dq/dx) * (df/dq) = 1 * (-4) = -4。
通过这个简单的例子,我们可以看到如何利用计算图来机械化地计算复杂函数的导数:前向传播从左到右计算所有值,反向传播从右到左逐步计算每个节点的梯度。
🧩 模块化视角:节点的局部计算
反向传播机制的一个强大之处在于其模块化。我们可以将整个计算图的计算分解为每个节点的局部计算。
对于图中的任何一个节点:
- 前向计算:节点接收输入,应用其局部函数,产生输出,并将输出传递给后续节点。
- 反向计算:节点从后续节点接收一个上游梯度(即最终损失相对于该节点输出的导数
dL/dz)。然后,节点计算其局部梯度(即其输出相对于每个输入的导数dz/dx,dz/dy)。最后,通过将上游梯度与每个局部梯度相乘,得到下游梯度(即最终损失相对于该节点每个输入的导数dL/dx,dL/dy),并将这些下游梯度传递给前驱节点。
这种设计的美妙之处在于,每个节点只需要关心自己的局部计算,而不需要了解整个图的全局结构。通过在所有节点上聚合这种局部处理,我们就能计算出整个图的全局梯度。
🧮 实现反向传播的模式
有两种主要方式来实现反向传播。
1. 扁平化实现
在这种模式下,我们编写一个单一的Python函数来计算整个计算图(如前向传播计算损失)。然后,我们编写对应的反向传播代码,它看起来像是前向传播代码的“倒置”版本。
以下是一个示意性的例子(对应于一个类似Sigmoid的计算图):
# 前向传播
def forward(x, w, b):
s0 = x * w
s1 = s0 + b
s2 = 1.0 / (1.0 + np.exp(-s1)) # Sigmoid
L = -np.log(s2) # 假设是二分类交叉熵损失的一部分
return L
# 反向传播 (扁平化实现)
def backward(x, w, b):
# 前向传播 (需要缓存中间变量)
s0 = x * w
s1 = s0 + b
s2 = 1.0 / (1.0 + np.exp(-s1))
L = -np.log(s2)
# 反向传播 (从后往前,每一步对应前向的一步)
grad_L = 1.0 # 基础情况 dL/dL
grad_s2 = -1.0 / s2 * grad_L # 对应 L = -log(s2)
grad_s1 = s2 * (1 - s2) * grad_s2 # 对应 s2 = sigmoid(s1),这是Sigmoid导数的简便形式
grad_b = 1.0 * grad_s1 # 对应 s1 = s0 + b
grad_s0 = 1.0 * grad_s1
grad_w = x * grad_s0 # 对应 s0 = x * w
grad_x = w * grad_s0
return grad_x, grad_w, grad_b
这种方法对于完成特定作业(如编写SVM或两层神经网络的梯度)非常直接有效。通过练习,你几乎可以不用在纸上推导数学公式,仅通过“反转”前向传播代码就能写出正确的梯度代码。
2. 模块化API实现
这是一种更工业级、模块化的方法,它更贴合我们之前讨论的“节点局部计算”视角。我们定义一个计算图对象,该对象能够通过对图中所有节点进行拓扑排序,在前向传播时调用每个节点的 forward 操作,在反向传播时调用每个节点的 backward 操作。
以PyTorch为例,你可以通过子类化 torch.autograd.Function 来定义自己的计算节点:
import torch
class MyAdd(torch.autograd.Function):
@staticmethod
def forward(ctx, x, y):
# ctx 是上下文对象,用于存储反向传播需要的信息
ctx.save_for_backward(x, y)
z = x + y
return z
@staticmethod
def backward(ctx, grad_z):
# grad_z 是上游梯度 dL/dz
x, y = ctx.saved_tensors
# 局部梯度: dz/dx = 1, dz/dy = 1
grad_x = 1 * grad_z
grad_y = 1 * grad_z
return grad_x, grad_y
# 使用自定义函数
x = torch.tensor([1.0], requires_grad=True)
y = torch.tensor([2.0], requires_grad=True)
z = MyAdd.apply(x, y)
loss = z.sum()
loss.backward()
print(x.grad, y.grad) # 输出: tensor([1.]) tensor([1.])
像PyTorch、TensorFlow这样的框架,其核心自动微分引擎就是由大量这样成对的、定义了前向和反向操作的函数构成的。
📐 向量与张量的反向传播
在实践中,我们处理的是向量、矩阵甚至更高维的张量,而不仅仅是标量。但核心思想不变。
- 梯度:当函数输入是向量,输出是标量(如损失
L)时,梯度dL/dx是一个与输入x形状相同的向量。 - 雅可比矩阵:当函数输入和输出都是向量时,导数是一个矩阵,称为雅可比矩阵,其元素
J[i, j]表示输出第i个元素对输入第j个元素的导数。 - 张量情况:对于输入输出为任意维张量的函数,其局部导数是一个高阶张量(可以理解为将输入和输出的所有元素展开后的雅可比矩阵)。
在反向传播中,对于向量/张量值的节点:
- 我们接收的上游梯度
dL/dz是一个与节点输出z形状相同的张量。 - 局部梯度是雅可比矩阵(或高阶张量)。
- 我们需要计算的下游梯度
dL/dx是与输入x形状相同的张量。 - 计算下游梯度的公式在本质上仍然是链式法则:
dL/dx = (dz/dx)^T * (dL/dz),这里dz/dx是雅可比矩阵,*表示矩阵乘法。对于张量,这对应于一种广义的“张量缩并”操作。
关键技巧:我们几乎从不显式构造庞大的(且通常是稀疏的)雅可比矩阵。相反,我们寻找一种隐式、高效的方式来计算这个“雅可比矩阵-向量乘积”。
示例:矩阵乘法的反向传播
假设节点计算 Y = X @ W,其中 X 形状为 (N, D),W 形状为 (D, M),Y 形状为 (N, M)。
- 接收上游梯度
dL/dY,形状为(N, M)。 - 经过推导(可通过分析单个输入元素对输出的影响,或使用形状匹配启发法),下游梯度为:
dL/dX = (dL/dY) @ W.T(形状(N, D))dL/dW = X.T @ (dL/dY)(形状(D, M))
这些公式正是高效计算隐式雅可比矩阵-向量积的方式。
🧪 其他主题
前向模式自动微分
我们讨论的反向传播算法,因为是从输出向输入反向计算梯度,被称为反向模式自动微分。还有一种前向模式自动微分,它从一个标量输入开始,向输出方向计算该输入对图中所有中间变量和输出的导数。虽然在某些科学计算场景中有用,但主流深度学习框架主要实现的是反向模式。
高阶导数
由于反向传播操作本身(如果实现得当)也是由可微的原始操作组成的,因此我们可以对计算图进行二次反向传播,从而计算损失函数的高阶导数(如Hessian矩阵向量积)。这为实现涉及梯度正则化等更复杂的优化技术提供了可能。
📝 总结
本节课我们一起学习了反向传播的核心思想:
- 我们使用计算图来表示复杂函数。
- 通过前向传播计算函数值,通过反向传播高效计算梯度。
- 反向传播的核心是链式法则的模块化应用,每个节点只需计算局部梯度并与上游梯度相乘。
- 实现上,可以采用扁平化(倒置前向代码)或模块化API的方式。
- 该方法可以推广到处理向量和张量,关键在于高效计算隐式的雅可比矩阵-向量积。


现在,我们已经掌握了计算线性分类器和神经网络梯度的方法。然而,目前我们的网络在处理图像时,需要将像素拉伸成向量,从而破坏了图像的空间结构信息。这看起来不是一个好方法,我们将在下一讲中解决这个问题。
卷积神经网络教程 📚
在本节课中,我们将学习卷积神经网络(Convolutional Neural Networks, CNNs)。这是一种专门用于处理图像数据的主要模型类别。我们将从回顾上一节的内容开始,逐步介绍卷积神经网络的核心概念、基本操作及其在图像处理中的应用。
回顾:反向传播与计算图 🔄
上一节我们介绍了反向传播算法,该算法可用于计算任意复杂计算图中的梯度。我们了解到,使用计算图数据结构可以非常方便地计算梯度,而无需推导复杂的表达式。通过前向传播计算输出,再通过反向传播计算梯度,我们可以轻松处理任意复杂度的表达式。
在反向传播算法中,每个函数都需要实现一个局部操作符,该操作符在前向传播中根据输入计算输出,在反向传播中根据上游梯度计算相对于输入的梯度。这种模块化的门API使得我们可以轻松地将新类型的函数插入到计算图中。
从全连接网络到卷积神经网络 🚀
到目前为止,我们已经多次讨论了线性分类器和全连接神经网络分类器。全连接神经网络分类器是一个非常强大的模型,可以灵活地表示许多不同的函数。然而,这两种分类器都存在一个问题:它们都没有尊重输入图像的二维空间结构。
无论是线性分类器还是全连接神经网络,都需要将具有空间结构的输入图像展平为一个长向量,然后输入到模型中。这种做法破坏了图像的空间结构,似乎不是处理图像数据的最佳方式。为了充分利用输入图像数据的空间结构,我们需要定义一些新的操作符,这些操作符能够处理图像或具有空间结构的数据。
卷积神经网络的基本操作 🛠️
在全连接神经网络中,我们熟悉两个基本组件:全连接层和非线性激活函数(如ReLU)。当我们从全连接神经网络转向卷积神经网络时,需要引入几个新的基本操作,这些操作可以在计算图或模型中使用。
以下是卷积神经网络中常用的三种操作:
- 卷积层(Convolution Layers)
- 池化层(Pooling Layers)
- 归一化层(Normalization Layers)
卷积层:保留空间结构 🧩
首先,让我们看看如何扩展全连接层的概念,使其能够保留输入的空间结构。全连接层在前向传播中接收一个向量(例如展平的CIFAR-10图像,大小为3072),并通过与权重矩阵相乘产生输出向量。
卷积层则输入一个三维张量(即三维体积),而不是展平的向量。例如,对于CIFAR-10图像,输入体积可能是一个3x32x32的三维张量,其中3表示通道数(红、绿、蓝颜色通道),32x32表示图像的高度和宽度。
卷积层的权重矩阵(有时称为滤波器)也具有三维空间结构。例如,一个卷积滤波器的大小可能为3x5x5。这意味着滤波器的深度维度必须与输入张量的深度维度匹配。卷积操作始终覆盖输入张量的整个深度。
为了计算输出,我们将这个3x5x5的滤波器滑动到输入张量的所有空间位置,并在每个位置计算滤波器与输入张量对应元素的点积。这类似于全连接网络中的内积,但现在是滤波器与输入张量局部空间块之间的内积。
例如,在一个3x5x5的输入图像块上,点积涉及75个元素。通常,我们还会添加一个偏置项。通过在每个位置计算点积,我们得到一个标量,表示该位置与滤波器的匹配程度。所有位置的点积结果组合成一个输出张量。
如果我们有多个滤波器,卷积层将涉及与一组不同滤波器的卷积。每个滤波器都会产生一个激活图,显示输入图像对该滤波器的响应程度。这些激活图可以连接成一个三维张量,其深度维度等于滤波器的数量。
卷积层通常包含偏置项,每个滤波器对应一个偏置值。输出张量可以看作是一组特征图,也可以看作是一个空间网格,其中每个位置都有一个特征向量,表示该位置输入张量的结构或外观。
卷积层的参数与计算 📊
卷积层接收一个四维张量作为输入,形状为(N, C_in, H, W),其中N是批次大小,C_in是输入通道数,H和W是空间维度。输出也是一个四维张量,形状为(N, C_out, H', W'),其中C_out是输出通道数,H'和W'是新的空间大小。
卷积层的超参数包括滤波器大小、滤波器数量、填充和步幅。滤波器大小通常为正方形(如3x3或5x5)。填充用于在图像边界周围添加零值,以防止空间尺寸缩小。步幅控制滤波器滑动的间隔。
输出空间大小的计算公式为:
H' = floor((H - K + 2P) / S) + 1
W' = floor((W - K + 2P) / S) + 1
其中,K是滤波器大小,P是填充大小,S是步幅。
卷积层的常见设置 ⚙️
以下是一些常见的卷积层设置:
- 使用方形滤波器(如3x3、5x5)。
- 使用相同填充(padding = (K-1)/2),使输出空间大小与输入相同。
- 步幅为1的卷积层保持空间大小不变。
- 步幅为2的卷积层将空间大小减半。
1x1卷积的特殊用途 🔍
1x1卷积是一种特殊的卷积操作,其滤波器大小为1x1。它用于改变输入张量的通道数,而不改变空间大小。1x1卷积可以看作是在每个空间位置独立应用的全连接层,用于调整特征向量的维度。
多维卷积 🌐
除了二维卷积,还有一维和三维卷积:
- 一维卷积:用于处理序列数据(如文本或音频波形)。输入为二维张量(通道数 x 序列长度),滤波器为一维。
- 三维卷积:用于处理三维数据(如点云或体积数据)。输入为四维张量(通道数 x 深度 x 高度 x 宽度),滤波器为三维。
池化层:下采样操作 📉
池化层用于在神经网络中进行下采样,不涉及可学习参数。池化操作类似于卷积,但在每个局部区域应用一个固定的池化函数(如最大值或平均值),将区域内的值合并为一个输出值。
常见的池化操作是2x2最大池化,步幅为2。这意味着将输入张量划分为2x2的非重叠区域,并在每个区域中取最大值作为输出。最大池化具有一定的平移不变性,因为即使输入图像中的物体稍有移动,区域内的最大值可能保持不变。
池化层的超参数包括核大小和步幅。与卷积层类似,池化操作会减少空间维度,同时保持深度不变。
归一化层:稳定训练过程 ⚖️
归一化层用于稳定深度神经网络的训练过程。最常见的归一化方法是批归一化(Batch Normalization)。批归一化的思想是对每个层的输出进行标准化,使其具有零均值和单位方差,从而减少内部协变量偏移(Internal Covariate Shift)。
批归一化在训练时使用批次数据的均值和方差进行标准化,并在测试时使用训练过程中累积的移动均值和方差。此外,批归一化还引入了可学习的缩放和偏移参数,允许网络自行调整均值和方差。
批归一化在卷积网络中的实现与全连接网络类似,但均值和方差是在批次和空间维度上计算的。批归一化可以显著加速训练过程,并允许使用更高的学习率。
然而,批归一化在训练和测试时的行为不同,这可能导致一些问题。为了解决这个问题,还有一些其他的归一化方法,如层归一化(Layer Normalization)、实例归一化(Instance Normalization)和组归一化(Group Normalization)。这些方法在不同维度上进行归一化,适用于不同的应用场景。
经典卷积网络设计 🏛️
经典的卷积网络设计通常由多个卷积层、激活函数、池化层和全连接层组成。例如,LeNet-5是一个早期的卷积网络,用于手写数字识别。其结构包括卷积层、ReLU激活函数、池化层和全连接层。
在卷积网络中,随着层数的增加,空间尺寸逐渐减小,而深度(通道数)逐渐增加。这种设计使得网络能够在不同尺度上提取特征,同时保持计算效率。
总结 🎯
本节课我们一起学习了卷积神经网络的基本概念和核心操作。我们从全连接网络的局限性出发,引入了卷积层、池化层和归一化层,这些操作使得神经网络能够有效处理图像数据的空间结构。卷积层通过滑动滤波器提取局部特征,池化层进行下采样并增加平移不变性,归一化层稳定训练过程并加速收敛。
通过组合这些操作,我们可以构建强大的卷积神经网络,用于图像分类、目标检测等任务。在下一节课中,我们将深入探讨如何设计和训练高效的卷积神经网络架构。


希望本教程能帮助你理解卷积神经网络的基本原理和操作。如果你有任何问题或需要进一步的解释,请随时提问! 😊


课程 P8:L8 - CNN典型结构 🏗️

在本节课中,我们将学习卷积神经网络(CNN)的典型架构。我们将回顾从AlexNet到ResNet等一系列具有里程碑意义的网络设计,理解它们的设计原则、演变过程以及如何在实际应用中选择合适的架构。
上一讲我们介绍了CNN的基本构建模块(卷积层、池化层、全连接层等)。本节中,我们将看看如何将这些模块组合起来,构建出高性能的卷积神经网络。


背景:ImageNet挑战赛 🏆

一个很好的讨论起点是ImageNet图像分类挑战赛。这是一个大规模数据集,包含约120万张训练图像,网络需要识别1000个不同类别。从2010年到2017年,该挑战赛每年举办,推动了CNN设计的快速进步。
- 2010-2011年:获胜系统并非基于神经网络。
- 2012年:卷积神经网络开始成为主流。
AlexNet (2012) 🚀
2012年,AlexNet架构在ImageNet挑战赛中取得了突破性胜利,大幅领先其他方法。

AlexNet是一个深度卷积神经网络(以当时的标准看)。它接受227x227像素的RGB图像输入,包含5个卷积层、3个全连接层,并使用了ReLU非线性激活函数。
以下是AlexNet的一些关键设计细节:
- 第一个卷积层:64个滤波器,核大小11x11,步长4,填充2。
- 使用了局部响应归一化(LRN),但该方法现已不常用。
- 由于当时GPU内存限制(如GTX 580仅有3GB内存),网络被拆分到两个GPU上运行。

我们可以计算AlexNet中某一层的具体开销。例如,对于第一个卷积层:
- 输出通道数:64(等于滤波器数量)。
- 输出空间尺寸:56x56(根据公式
(输入尺寸 - 核尺寸 + 2*填充) / 步长 + 1计算)。 - 内存占用:约784 KB(
64 * 56 * 56 * 4字节)。 - 参数量:约23K(
64 * 3 * 11 * 11 + 64)。 - 计算量(FLOPs):约73 MFLOPs(
64 * 56 * 56 * (3 * 11 * 11))。
AlexNet的整体趋势是:
- 内存:大部分内存消耗来自早期卷积层的激活值(高空间分辨率)。
- 参数:大部分参数位于全连接层(尤其是第一个全连接层有约3800万参数)。
- 计算:大部分计算量集中在卷积层。


ZFNet (2013) 🔬


2013年,几乎所有参赛者都转向使用神经网络。获胜者是ZFNet,它本质上是一个更大的AlexNet。
ZFNet的主要改进是通过调整超参数使网络更大:
- 将第一层卷积核从11x11(步长4)改为7x7(步长2),减缓了下采样速度。
- 增加了后面几层卷积的滤波器数量(如第3、4、5层)。
这使得网络具有更高的空间分辨率和更多的参数,从而获得了更好的性能(错误率从16.4%降至11.7%)。但此时网络设计仍缺乏统一原则。
VGGNet (2014) 📐



VGGNet首次提出了清晰、统一的设计原则,使得网络易于缩放。

VGGNet的设计原则非常简单:
- 所有卷积层使用 3x3卷积核,步长1,填充1。
- 所有池化层使用 2x2最大池化,步长2。
- 每次池化后,通道数翻倍。
- 网络包含5个卷积阶段,最后接3个全连接层。


为什么使用3x3卷积?
一个5x5卷积的感受野可以用两个3x3卷积堆叠来等效。对比两者:
- 参数量:5x5卷积为
25 * C^2;两个3x3卷积为2 * 9 * C^2 = 18 * C^2。 - 计算量:5x5卷积为
25 * C^2 * H * W;两个3x3卷积为18 * C^2 * H * W。 - 优势:堆叠的3x3卷积参数更少、计算量更低,并且中间可以插入ReLU增加非线性。




通道翻倍与空间减半
在每个阶段,空间尺寸减半,通道数翻倍。这大致保持了每层计算量的平衡,是一种高效的设计模式。


VGGNet(如VGG-16)比AlexNet大得多:
- 内存占用约为25倍。
- 参数量约为2倍(1.38亿 vs 6100万)。
- 计算量约为19倍。


GoogLeNet / Inception (2014) 🧩

同年,GoogLeNet(Inception v1)出现,其设计核心是效率。它希望在保持高性能的同时最小化计算复杂度。
GoogLeNet的主要创新包括:

1. 高效的“茎干”网络
在初始几层快速下采样输入图像的空间分辨率,避免在大型特征图上进行昂贵计算。例如,将224x224下采样到28x28,GoogLeNet仅需0.418 GFLOPs,而VGG-16需要7.3 GFLOPs。

2. Inception模块
该模块使用并行分支结构,同时进行1x1、3x3、5x5卷积和3x3最大池化,避免了手动调整卷积核大小的超参数。此外,在昂贵的3x3和5x5卷积之前,使用1x1卷积作为“瓶颈”来减少通道数,以降低计算量。

3. 全局平均池化
在网络末端,使用全局平均池化(核大小等于特征图空间尺寸)替代 flatten 操作 + 大型全连接层。这极大地减少了参数量(例如,从VGG的约1.2亿FC参数降至几乎为零)。


4. 辅助分类器
在训练深度网络时(早于批量归一化),为了帮助梯度传播,在网络中间层添加了辅助分类器进行额外的损失计算。

ResNet (2015) 🧱


ResNet(残差网络)是神经网络设计史上的一个重要里程碑。随着批量归一化的出现,训练更深网络成为可能,但人们发现单纯增加层数(如56层)反而比浅层网络(20层)性能更差,这并非过拟合,而是优化问题。

ResNet的核心思想是:让网络更容易学习恒等映射。如果更深网络的多余层能学习恒等函数,那么它至少能表现得和浅层网络一样好。

残差块
ResNet引入了残差块。传统块是 Conv -> ReLU -> Conv,输出 F(x)。残差块则是在此基础上添加一个快捷连接,输出 F(x) + x。
# 残差块的基本思想
output = convolutional_block(x) + x # 快捷连接
如果 F(x) 的权重学习为0,则该块直接输出 x(恒等映射)。这改善了梯度流动,使得训练数百层的网络成为可能。

ResNet结合了VGG的简单设计原则和GoogLeNet的高效技巧:
- 使用3x3卷积。
- 分阶段设计,每阶段后空间减半、通道翻倍。
- 使用高效的“茎干”和全局平均池化。
- 对于更深的网络(如ResNet-50/101/152),使用瓶颈块(1x1降维 -> 3x3卷积 -> 1x1升维)来控制计算成本。


ResNet-34(34层)仅用3.6 GFLOPs就达到了比VGG-16(19.6 GFLOPs)更低的错误率。ResNet在2015年横扫了多项视觉竞赛,成为至今仍广泛使用的基准网络。



后续发展 🚀

ResNeXt (2016)
在残差块中引入并行路径(分组卷积),在保持计算量不变的情况下,通过增加路径数量(“基数”)来提升性能。这提供了除深度和宽度外的另一个网络维度。
轻量级网络(如MobileNet)
专注于为移动和嵌入式设备设计极致高效的网络。采用深度可分离卷积等技术,大幅减少参数和计算量,以牺牲少量精度换取极高的效率。
# 深度可分离卷积近似计算量对比
标准3x3卷积计算量 ~ 9 * C^2 * H * W
深度可分离卷积计算量 ~ 9 * C * H * W + C^2 * H * W
神经架构搜索 (NAS) 🧠
自动化网络设计过程。使用一个控制器网络(RNN)来生成子网络架构,训练子网络后,根据其性能通过策略梯度更新控制器。虽然早期版本(如NASNet)需要海量计算资源(800 GPU * 28天),但后续研究大幅提升了搜索效率。NAS能够发现比人工设计更高效、性能更好的架构。
总结与建议 📝
本节课我们一起学习了卷积神经网络典型架构的演变历程:
- 早期(AlexNet, ZFNet):更大规模的网络带来更好性能,但设计依赖试错。
- 规范化设计(VGG):提出简单、重复的模块化设计原则(小卷积核、阶段式下采样)。
- 关注效率(GoogLeNet):通过并行分支、瓶颈层和全局池化,在保持性能的同时优化计算和参数效率。
- 深度突破(ResNet):引入残差连接,解决了深度网络的优化难题,使训练数百层的网络成为可能。
- 多样化发展:随后出现关注不同维度(如ResNeXt的基数)、极致轻量化(MobileNet)以及自动化设计(NAS)的网络。
给实践者的建议:不要试图从头设计网络架构。对于大多数应用:
- 通用高性能:选择 ResNet-50 或 ResNet-101。它们经过充分验证,性能稳定,是可靠的基准选择。
- 注重效率/移动端:考虑 MobileNetV2、ShuffleNetV2 等专为效率设计的架构。
- 保持更新:关注由NAS发现的高效架构(如EfficientNet),它们通常在精度和效率间有更好的平衡。
下一讲,我们将探讨用于训练这些网络的实际软件和硬件工具。
深度学习硬件与软件 🖥️⚙️
在本节课中,我们将学习深度学习模型运行所依赖的硬件和软件系统。我们将首先探讨硬件,特别是CPU和GPU的区别与演进,然后深入了解深度学习软件框架,如PyTorch和TensorFlow,以及它们如何利用计算图来简化模型构建和训练。
硬件:CPU vs. GPU vs. TPU 💻
上一节我们介绍了深度学习模型的各种架构,本节中我们来看看这些模型最终运行的实际硬件系统。

计算成本趋势 📈

以下是CPU和GPU在计算能力(以每美元千兆浮点运算次数衡量)上的历史趋势:

- CPU:多年来计算成本稳步下降,但进步相对平缓。
- GPU:自2006年NVIDIA推出支持CUDA的GPU后,计算成本急剧下降,尤其在2012年后与CPU拉开巨大差距。这使得训练像AlexNet这样的大型模型成为可能。
核心架构对比 🔍
CPU和GPU在设计哲学上有根本不同:
- CPU:核心数量较少(例如16核),但每个核心非常强大,时钟频率高(例如3.5 GHz),擅长处理复杂、串行的任务。
- GPU:拥有大量核心(例如4608个CUDA核心),但每个核心相对简单,时钟频率较低(例如1.35 GHz),专为大规模并行计算设计。
GPU内部揭秘 🧠
现代GPU(如NVIDIA RTX Titan)本身就像一个微型计算机:

- 独立设备:拥有自己的散热风扇和专用显存(例如24GB)。
- 流式多处理器:GPU的核心计算单元,一个GPU包含多个SMs(例如72个)。
- 计算核心:每个SM内部包含许多32位浮点核心(FP32 Cores)和张量核心。

张量核心是NVIDIA为深度学习专门设计的硬件,能在单个时钟周期内完成一个4x4矩阵乘法并加上偏置项(A * B + C)。它使用混合精度计算(乘法用16位浮点,累加用32位浮点),能极大提升矩阵乘法和卷积运算的速度。
公式:张量核心单次操作计算量 = 2 * (4 * 4 * 4) = 128次浮点运算(乘加各算一次)。
超越单卡:规模化与TPU 🚀
为了处理更庞大的模型和数据集,计算需要超越单块GPU:
- 多GPU服务器:常见的服务器配置包含8块GPU,通过并行计算加速训练。
- TPU:谷歌推出的专用深度学习硬件。Cloud TPU v3单个设备提供420 TeraFLOPS算力,并能组成TPU Pod(例如256个TPUv3),提供超过100 PetaFLOPS的聚合算力。目前TPU主要与TensorFlow框架配合使用。
注意:消费级GPU与计算级GPU在显存容量和显存带宽上都有差异,后者对于需要频繁进行数据交换的深度学习任务至关重要。
软件:深度学习框架 🛠️
了解了硬件基础后,我们来看看如何通过软件框架高效地利用这些硬件。
框架的核心诉求 ✅
一个优秀的深度学习框架应具备以下三个关键特性:
- 快速原型设计:提供丰富的预构建层和工具。
- 自动梯度计算:基于计算图抽象,自动通过反向传播计算梯度。
- 硬件透明性:轻松在GPU、TPU等不同硬件上运行代码,无需用户关心底层细节。
PyTorch 的核心抽象层次 🐍
PyTorch 提供了三种不同层次的抽象来构建神经网络:
- 张量 API:基础层次,类似于支持GPU的NumPy多维数组。在前三次作业中我们主要使用这一层。
- 自动求导:核心层次,通过设置
requires_grad=True自动构建计算图并计算梯度。 - 神经网络模块:面向对象的高级层次,通过
nn.Module等类来组织网络层和参数。
计算图:动态 vs. 静态 🔄
这是深度学习框架的一个核心设计选择:
- 动态计算图:PyTorch默认模式。每次前向传播都会实时构建一个新的计算图,执行完后丢弃。优点是与Python控制流(如循环、条件语句)无缝集成,调试直观。
- 静态计算图:TensorFlow 1.x 默认模式。先定义并编译一个固定的计算图结构,然后多次执行。优点是可以进行图优化提升性能,并且模型更容易序列化和部署到非Python环境(如C++)。
PyTorch 也通过 torch.jit.script 装饰器支持将代码编译为静态图。TensorFlow 2.0 则转向以动态图(Eager Execution)为默认模式,并通过 @tf.function 装饰器支持静态图编译。
框架代码示例对比 📝
以下是使用不同抽象层次和框架训练一个两层全连接网络的示例:
1. 仅使用PyTorch张量API(手动计算梯度)
# 初始化数据、权重
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H)
w2 = torch.randn(H, D_out)
# 前向传播
h = x.mm(w1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(w2)
loss = (y_pred - y).pow(2).sum()
# 手动反向传播(计算梯度)
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(w2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_w1 = x.t().mm(grad_h)
# 梯度下降更新(需在torch.no_grad()上下文中)
with torch.no_grad():
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2
2. 使用PyTorch自动求导
w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)
# 前向传播(自动记录计算图)
y_pred = x.mm(w1).clamp(min=0).mm(w2)
loss = (y_pred - y).pow(2).sum()
# 一键反向传播,自动计算w1.grad和w2.grad
loss.backward()
# 更新权重(需清零梯度)
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_() # 重要:清零梯度
w2.grad.zero_()
3. 使用PyTorch nn.Module 和优化器
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
)
loss_fn = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 训练循环
y_pred = model(x)
loss = loss_fn(y_pred, y)
optimizer.zero_grad() # 清零梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数
4. 使用TensorFlow 2.0 (Eager模式)
w1 = tf.Variable(tf.random.normal((D_in, H)))
w2 = tf.Variable(tf.random.normal((H, D_out)))
with tf.GradientTape() as tape:
y_pred = tf.matmul(tf.nn.relu(tf.matmul(x, w1)), w2)
loss = tf.reduce_sum(tf.square(y_pred - y))
grads = tape.gradient(loss, [w1, w2]) # 计算梯度
# 手动更新
for w, g in zip([w1, w2], grads):
w.assign_sub(learning_rate * g)
框架选择小结 🤔
- PyTorch:研究首选。动态图设计使得代码灵活、调试简单,与Python生态结合紧密。在学术界和研究中非常流行。
- TensorFlow:生产部署强大。拥有强大的生态系统(如TensorBoard可视化工具)、更好的移动端支持,并能使用TPU。TensorFlow 2.0 吸收了PyTorch的优点,变得更容易使用。
总结 📚
本节课中我们一起学习了深度学习的硬件和软件基础。
在硬件方面,我们了解了CPU、GPU和TPU的不同设计理念与性能特点,特别是GPU的并行架构和张量核心对深度学习的加速作用。
在软件方面,我们探讨了PyTorch和TensorFlow等框架如何通过计算图抽象来简化模型的构建、训练和部署,并比较了动态计算图与静态计算图的优缺点。



掌握这些知识将帮助你根据任务需求(快速实验 vs. 生产部署)选择合适的工具,并更深入地理解你的模型是如何被执行的。


浙公网安备 33010602011771号