密西根大学-EECS498-007-深度学习计算机视觉笔记-全-

密西根大学 EECS498-007 深度学习计算机视觉笔记(全)

01:课程介绍与历史背景 🎓

在本节课中,我们将学习本课程的基本介绍,并了解计算机视觉与深度学习领域的发展简史。我们将从这两个领域的定义出发,回顾它们从诞生到如今蓬勃发展的关键节点。

课程概述与欢迎 👋

欢迎来到EECS 498-007/598-005课程,这是密歇根大学首次开设的“深度学习用于计算机视觉”专题课程。

在开始课程内容之前,需要说明一下候补名单的情况。本课程名额有限,但申请人数远超容量。我们将尽力让更多同学加入,并会在后续通过邮件通知名额更新。对于可能无法选上课的同学,我们深表歉意。

什么是计算机视觉与深度学习? 🤔

本节我们将拆解课程标题中的核心概念,理解计算机视觉与深度学习分别是什么,以及它们如何结合。

计算机视觉是研究构建能够处理、感知和理解视觉数据的人工系统的学科。这是一个非常宽泛的定义。“视觉数据”可以是图像、视频、医学扫描图像,或任何你能想到的连续值信号。

计算机视觉之所以重要,是因为它无处不在。我们每天都会产生和分享海量的视觉数据。例如,Instagram每天有约1亿张照片和视频上传,YouTube每分钟有约300小时的视频上传。如此庞大的数据量,使得我们必须构建能够自动处理和理解这些数据的系统。随着自动驾驶、增强/虚拟现实、无人机等新技术的出现,计算机视觉的作用将愈发重要。

深度学习是机器学习的一个子集。它指的是一种具有多层结构的、分层的学习算法。这些算法在某种程度上非常粗略地受到了哺乳动物大脑(有时是视觉系统)结构的启发。但请注意,这种类比非常宽泛,不应过分严肃地看待。

计算机视觉和机器学习都属于更广泛的人工智能研究领域。人工智能的目标是构建能够完成通常由人类完成的任务的计算机系统。本课程将聚焦于计算机视觉、机器学习和深度学习三者的交叉领域。

需要强调的是,尽管本课程聚焦于此,但人工智能领域远不止这些。还存在不依赖于学习或深度学习的AI方法,以及不使用机器学习的计算机视觉方法。这个广阔的领域还包括自然语言处理、语音识别、机器人学等众多子学科。

今日议程与历史背景的重要性 📜

今天的课程安排与后续大部分课程不同。在深入探讨技术细节之前,我们认为了解领域的历史背景至关重要。近年来深度学习的巨大成功,是建立在数十年来计算机视觉和机器学习研究的基础之上的。

我们将分两条线索进行回顾:首先是计算机视觉的历史,然后是深度学习的历史。

计算机视觉简史 🖼️

以下是计算机视觉发展历程中的一些关键里程碑。

  • 1959年:Hubel & Wiesel的猫实验 🐱
    这项研究并非由计算机科学家完成,而是旨在理解大脑如何工作。他们将电极植入猫的视觉皮层,发现某些神经元会对特定朝向、特定位置的明暗边缘做出反应(“简单细胞”),而另一些神经元则对更复杂的模式(如运动)有反应(“复杂细胞”)。这项关于边缘检测分层视觉处理的研究获得了1981年诺贝尔奖,对后来的视觉处理思想产生了深远影响。

  • 1963年:Larry Roberts的博士论文 📚
    这被认为是第一篇关于计算机视觉的博士论文。他的工作包括将照片信息输入计算机、检测图像中的边缘,并尝试理解图像中物体的三维几何结构。有趣的是,Roberts后来成为了互联网的奠基人之一。

  • 1966年:MIT的“夏季视觉项目” ☀️
    Seymour Papert提出了一个雄心勃勃的计划:雇佣一些本科生,用一个夏天的时间构建出视觉系统的“主要部分”。这显然未能实现,但反映了当时的乐观情绪。

  • 1970年代:David Marr的视觉表示阶段理论 🧠
    他提出了视觉处理的层次化阶段:从原始图像,到提取边缘(“要素图”),再到2.5维草图(包含深度和表面信息),最后到完整的3D模型表示。这再次呼应了分层处理的理念。

  • 1980年代:基于边缘的物体识别 ✂️
    随着计算能力和数字相机的提升,研究变得更加实际。John Canny在1986年提出了强大的边缘检测算法。David Lowe在1987年提出通过匹配模板与图像中的边缘来识别物体(如剃须刀)。

  • 1990年代:基于分组的物体识别 🧩
    研究重点转向更复杂的场景。思路是先通过图像分割将图像分成有语义意义的块(如人、伞),然后再对这些块进行识别和标注。

  • 2000年代:基于特征匹配的物体识别 🔍
    David Lowe在1999年提出了SIFT(尺度不变特征变换)算法。该算法提取图像中关键点的鲁棒特征向量,这些特征对旋转、光照变化具有一定不变性,从而可以实现可靠的图像匹配和物体识别。

  • 2001年:Viola-Jones人脸检测算法 😀
    这是一个里程碑式的工作。首先,它首次在计算机视觉中大规模成功应用了机器学习(使用AdaBoost和决策树)。其次,该算法被迅速商业化,集成到了当时的数码相机中,用于人脸检测和对焦。

  • 2000年代中期:PASCAL VOC挑战赛 🏆
    随着互联网的发展,研究人员可以下载并标注大量图像数据(PASCAL VOC数据集),并举办年度竞赛。这推动了基于数据的机器学习方法在视觉识别中的广泛应用,性能逐年稳步提升。

  • 2010年至今:ImageNet挑战赛与深度学习的崛起 🚀
    ImageNet是一个超大规模数据集(超过140万张图像,1000个类别),它利用众包(如Amazon Mechanical Turk)进行标注,并成为了计算机视觉领域的核心基准测试。
    在2012年的ImageNet竞赛中,发生了一件轰动性的事件:由Alex Krizhevsky、Ilya Sutskever和Geoffrey Hinton提出的AlexNet(一个深度卷积神经网络)将错误率从上一年的约25%大幅降至约16%,以巨大优势获胜。
    此后,基于深度学习的模型性能持续快速提升,到2017年左右,某些系统在该任务上的表现已超过人类。2012年成为了深度学习在计算机视觉领域爆发的转折点。

深度学习简史 🧠

现在,让我们回顾深度学习这条并行发展的线索。

  • 1958年:感知机(Perceptron) 🤖
    这是最早的能够学习的计算机系统之一,当时是一个庞大的硬件设备。它本质上是一个线性分类器,能够学习识别字母,引发了人们对机器学习的巨大热情。

  • 1969年:《感知机》一书出版 📖
    Marvin Minsky和Seymour Papert在书中指出了单层感知机的局限性(例如无法学习异或XOR函数)。这在一定程度上导致了相关研究的降温。但值得注意的是,书中也提到了多层感知机的潜力,这一点在当时被忽视了。

  • 1980年:Neocognitron 🕸️
    福岛邦彦受Hubel & Wiesel的启发,提出了Neocognitron模型。它交替使用两种操作:S-cells(类似于现代卷积)和C-cells(类似于现代池化)。其整体架构与后来的卷积神经网络非常相似,但当时缺乏有效的训练算法来学习网络中的所有参数。

  • 1986年:反向传播算法 🔄
    Rumelhart, Hinton和Williams发表了关于反向传播算法的著名论文,为高效训练多层感知机(即现代的全连接神经网络)提供了关键方法。

  • 1998年:LeNet-5与卷积神经网络 📇
    Yann LeCun等人将福岛邦彦的卷积/池化思想与反向传播算法结合,提出了LeNet-5卷积神经网络,并成功应用于银行支票手写数字识别,取得了商业上的成功。其架构与后来的AlexNet非常相似。

  • 2000年代:“深度学习”术语兴起与持续发展 📈
    在这段时间里,相对小众的研究群体持续推动神经网络变得更深、更宽、更强大。“深度学习”中的“深度”即指网络中的多层结构。许多现代训练技巧和理论基础都在这个时期得以发展。

  • 2012年:历史性的交汇
    算法(多年发展的深度学习)、数据(ImageNet等大规模标注数据集)和计算(GPU计算能力的指数级增长)三大要素在2012年汇聚,催生了AlexNet的突破性成功,并开启了深度学习的新时代。

深度学习的现状与未来展望 🌟

自2012年以来,卷积神经网络和其他深度神经网络被应用于计算机视觉的几乎所有方面,并扩展到其他领域。

以下是其应用的一些例子:

  • 核心视觉任务:图像分类、图像检索、物体检测、图像分割、视频分类、姿态估计。
  • 游戏与AI:处理视觉输入玩Atari游戏。
  • 科学与医疗:医学影像诊断、星系分类、野生动物监测。
  • 创意应用:图像描述生成、艺术风格迁移(如DeepDream)。

尽管取得了巨大成功,但我们距离构建具有人类水平视觉理解能力的系统仍然很远。人类看一张图,不仅能识别物体,还能理解物理关系、心理活动、社会互动等复杂信息。这是当前技术尚未达到的。

然而,计算机视觉技术无疑有巨大潜力改善我们的生活,例如让娱乐更有趣、让交通更安全、让医疗诊断更精准。

课程后勤与政策 📋

上一节我们回顾了激动人心的历史,本节我们来看看本课程的具体安排。

课程团队与联系方式

  • 讲师:Justin Johnson(计算机科学与工程系新任助理教授)。
  • 研究生助教:Yusak J.、KiA、Luway。
  • 主要沟通平台Piazza。请务必尽快注册。所有课程内容问题、公告都将通过Piazza发布。请勿在公开帖子中发布代码,代码相关问题请使用私密帖子。
  • 课程网站:包含教学大纲、课程表、作业链接、讲座视频等所有信息。
  • Canvas:主要用于提交作业。
  • 办公室时间:下周开始,具体时间地点请查看课程网站上的谷歌日历。

课程内容与评分

  • 教材:无强制教材,网站会提供每讲的推荐阅读材料(可免费在线获取)。
  • 主要内容:6个编程作业(使用Python、PyTorch和Google Colab)、1次期中考试、无期末考试、无课程项目。
  • 迟交政策:每位同学有3个自由迟交日,可在任何作业中使用,无需提前说明。用尽迟交日后,将按日扣分。

合作政策

我们鼓励同学们讨论课程概念,但需遵守以下规则:

  1. 所有提交的作业必须是自己的独立成果。可以讨论思路,但不应共享或查看彼此的代码。
  2. 不要将自己的解决方案代码分享给他人(包括发布在Piazza、GitHub或给室友)。
  3. 提交作业时,需注明一起讨论过的同学。
    总之,迟交或不完整提交远胜于违反学术诚信

课程理念与结构

本课程的目标不是“90天学会PyTorch”或“深度学习头条速成”。我们专注于基础概念,让你理解API背后的实现原理和原因。

课程前半部分(基础):我们将深入细节,学习如何实现、调试和训练各种神经网络(全连接网络、卷积神经网络等)。你将亲手实现自己的卷积神经网络系统,期间不会使用自动求导,以深入理解梯度计算和反向传播。

课程后半部分(应用与前沿):我们将探讨更广泛的应用和新兴研究主题,如物体检测、分割、3D视觉、视频、注意力机制、Transformer、视觉与语言、生成模型等。讲座风格会更偏向概述和导读。

第一份作业将于本周末发布,内容是关于PyTorch和Google Colab环境的入门热身,难度不高,旨在帮助大家熟悉工具。

总结 🎯

本节课中,我们一起学习了本课程的介绍,并回顾了计算机视觉与深度学习波澜壮阔的发展历史。我们从它们的定义出发,看到了这两个领域如何从各自独立的起点,最终在算法、数据和算力的共同推动下于2012年交汇,引发了当今的人工智能革命。同时,我们也了解了本课程的具体安排、政策与核心理念。下节课,我们将正式深入技术细节,从“图像分类”问题开始我们的深度学习实践之旅。

再次欢迎来到“深度学习用于计算机视觉”课堂!

02:图像分类

概述

在本节课中,我们将要学习计算机视觉中的核心任务——图像分类。我们将探讨其定义、面临的挑战、应用场景,并深入介绍我们的第一个学习算法:K最近邻算法。通过本节课,你将理解机器学习方法如何通过数据驱动的方式解决图像识别问题。

课程管理事项

在进入今天的正式内容之前,我们需要先处理几项课程管理事项。

以下是关于候补名单状态的说明:

  • 我们昨天发放了一批选课许可。
  • 如果你收到了许可但尚未选课,请立即完成选课,因为许可将于明天过期。
  • 如果你收到许可但未选课且许可过期,你将不会获得新的许可,我们将把许可发放给候补名单上的其他同学。

以下是关于办公时间的说明:

  • 我和助教的办公时间将于本周开始。
  • 具体时间安排可在Google日历上找到,日历链接位于课程网站上,课程网站链接可在校园网上找到。

以下是关于课程交流平台Piazza的说明:

  • Piazza是本学期我们与大家沟通的主要平台。
  • 目前,课程注册学生数与Piazza注册学生数差异很大。
  • 如果你尚未注册Piazza,请立即注册。

以下是关于第一次作业的说明:

  • 我们已于昨天发布了课程的第一次作业。
  • 作业链接可在课程大纲、Canvas等地方找到。
  • 本次作业将使用Python和PyTorch,并在Google Colab上完成。
  • 作业涵盖两个主要主题:PyTorch张量简介和K最近邻分类算法。
  • 作业提交截止日期为9月15日(周日)晚上11:59。

以下是关于使用Google Colab完成作业的说明:

  • 在作业网站上,点击“在Colab上打开”按钮。
  • 笔记本将在你的浏览器中打开,并自动连接到后端带GPU的虚拟机。
  • 你无需安装任何软件,可以直接在浏览器中完成作业。
  • 完成后,可以将副本保存到自己的Google云端硬盘,并下载为IPython笔记本文件提交到Canvas。

图像分类任务

上一节我们介绍了课程的管理事项,本节中我们来看看图像分类任务本身。

图像分类是计算机视觉乃至更广泛的机器学习领域中一项非常重要的核心任务。

图像分类的任务陈述非常简单。我们的算法将接收一张输入图像(例如左侧的图片),然后需要为该图像分配一个类别标签。当我们讨论图像分类时,通常预设算法知晓一组固定的类别标签。在这个例子中,算法可能知晓“猫”、“鸟”、“鹿”、“狗”、“卡车”这五个标签。算法在执行图像分类时,需要做的就是简单地为看到的图像分配这五个标签中的一个,本例中就是“猫”。

对我们所有人来说,这是一个非常简单的任务。你几乎可以不假思索地完成它,看一眼图像就能立刻知道这是一只猫。

但对计算机来说,这并不容易。当我们尝试在机器上实现图像识别和分类时,面临的主要挑战是一个被称为“语义鸿沟”的问题。当我们看这张图像时,我们立刻认出它是一只猫。光子击中我们的视网膜,经过大脑的复杂处理,但我们看图像时并没有意识到这个过程,只是直观地知道我们看到的是什么。但计算机没有这种直觉。当计算机看这样一张图像时,它得到的只是一个巨大的数字网格。对于这样一张图像,它只是一个800x600x3的巨大数字网格,其中每个像素都有一个由0到255之间的三个数字表示的颜色值。

问题是,如果你看这个数字网格,完全不清楚这个数字网格应该代表一只猫。没有明显的方法将这个原始像素值网格转换成具有语义意义的“猫”这个类别标签。

更糟糕的是,当我们对图像进行相对简单的更改时,整个数字网格可能会发生剧烈变化。

图像分类的挑战

上一节我们定义了图像分类任务并指出了“语义鸿沟”问题,本节中我们来看看实现稳健图像分类所必须应对的具体挑战。

以下是图像分类面临的主要挑战:

视角变化
例如,如果我们从稍微不同的角度拍摄同一只猫的照片,我们所有人肯定仍然会认出它是一只猫,甚至可能认出是同一只猫。但由于语义鸿沟的存在,即使是这样简单的图像变化,也会导致所有像素值以一种非常不直观的方式改变。我们需要设计的算法必须能够应对这些由图像本身相对简单的变化所引起的原始像素值的巨大变化。

类内差异
不同的猫看起来非常不同。这些不同的可爱猫咪在相机原始传感器上都会产生非常不同的像素值网格。我们需要构建能够应对类别内部可能出现的这些巨大差异的系统。

细粒度分类
有时我们需要识别视觉上非常相似的细粒度类别。例如,在某些应用中,我们可能希望识别不同品种的猫。这同样是一个巨大的实际问题。

背景干扰
有时图像中我们想要识别的物体会融入背景中,可能是由于自然伪装或场景中其他复杂情况。我们的分类器需要对此保持稳健。

光照变化
随着场景中光照条件的改变,例如开关灯、在黑暗中或在日光下拍照,图像中物体的底层语义并不会改变。因此,我们的算法应该能够应对不同光照条件下的这些巨大变化。

形变
我们想要识别的物体在图像中可能以非常不同的姿态、非常不同的位置出现。猫是特别容易形变的物体类别。

遮挡
有时我们想要识别的物体在图像中可能几乎完全不可见。右侧的例子非常有趣,这基本上是一个沙发,我们看到一个尾巴从沙发垫下面伸出来。你可能直觉上认为那是一只猫,因为你见过很多猫的图片,知道猫通常住在房子里,知道猫有时喜欢钻到东西下面。但实际上,如果只考虑这张图像的原始图像证据,我们并不知道这是一只猫。这可能是浣熊,也可能是其他有尾巴的动物。因此,即使是为图像中的物体分配类别标签这个相对简单的问题,也可能涉及大量关于世界的常识推理。

如果我们想要识别世界上存在的所有类别,以及这些物体在图像中可能出现的所有变化、位置和外观方式,这个相对简单的图像分类问题很快就会变得非常具有挑战性。

图像分类的应用

上一节我们探讨了图像分类面临的诸多挑战,本节中我们来看看为什么克服这些挑战、实现稳健的图像分类会如此有用。

如果我们能够克服所有这些问题,编写出能够执行稳健图像分类、在各种情况下识别大量物体类别的算法,那将非常非常有用。

我们已经在上一讲中看到计算机视觉的一些应用如何解锁许多不同的科学问题。我们可以将图像分类用于医疗影像、医学诊断等领域。例如,拍摄皮肤病变图片并诊断其为恶性或非恶性肿瘤;拍摄X光片并尝试分类医学图像中可能存在的问题类型。

稳健的图像分类对天文学家也很有用,他们希望从望远镜和其他类型的传感器收集视觉数据,然后分类天空中存在的现象类型。这对于许多其他科学应用也很有用,例如识别鲸鱼或对传感器中可能出现的许多不同类型的动物进行分类。

图像分类本身就是一个非常非常有用的难题,如果我们能解决它,可以解锁许多真正强大且有用的应用。

但我觉得可能更有趣、也许不那么直观的是,图像分类也是我们希望在计算机视觉内部执行的不同算法的基本构建模块。

作为构建模块的图像分类

上一节我们看到了图像分类的直接应用,本节中我们来看看它如何作为更复杂视觉任务的基石。

作为一个例子,我们之前讨论了图像分类。计算机视觉中有一个相关的任务叫做目标检测。在目标检测中,我们想要做的是在图像中的物体周围绘制框,并不仅说明图像中存在什么物体,还要说明它们在图像中的位置。事实证明,图像分类本身可以作为一个子部分,用于构建更复杂的应用,如目标检测。

例如,执行目标检测的一种方法是通过对图像中不同的滑动窗口进行图像分类。执行目标检测的一种方法就是分类图像的不同子区域。我们可以看这里的一个子区域,然后将其分类为背景、人、马、汽车或卡车。在这个例子中,它被分类为背景,因为这里没有物体。如果我们分类这个框,我们应该将其分类为人,等等。可以看到,如果我们有能力构建真正强大的图像分类器,那将再次让我们能够构建像目标检测器这样的其他应用。

即使是像图像描述这样的任务,也常常被构建为图像分类问题。这里的想法是,给定一张输入图像,我们可能希望写一个自然语言句子来描述图像中的内容。这可以像目标检测案例一样,被构建为一系列分类问题。这里,算法可能知晓英语中一组固定的词汇。问题是,我接下来应该说哪个词。这同样是一个分类问题。首先,我们会使用图像分类器分类并选择“男人”这个词,然后选择“骑”这个词,选择“马”这个词,最后选择“停止”这个词来表示句子结束。

可以看到,图像分类这个相对简单的任务,是一个真正强大的构建模块,我们可以用它来构建计算机视觉中许多更有趣的问题。

数据驱动方法

上一节我们看到了图像分类作为基础任务的重要性,本节中我们来看看如何着手解决这个“如何编写分类算法”的难题。

考虑到所有这些,我们确实希望能够编写出能够很好执行图像分类的算法,但完全不清楚我们应该如何做。如果你只是坐在电脑前开始编写代码,你需要编写这个神奇的Python函数,它输入这个巨大的像素值网格,执行一些神奇的计算,然后以某种方式输出“猫”,或者在围棋棋盘的第5行第9列落子,或者判断这片图像是或不是背景。完全不清楚你应该在这里输入什么代码,因为与对整数列表排序这样的任务不同,确实没有明确定义的算法来将数字网格转换成“猫”。

我们需要做点什么,但完全不清楚我们应该如何解决这个问题。

你可能会做的一件事是,尝试利用你自己关于猫和其他物体外观的人类知识,来手动编写尝试挑选出不同物体类别的分类器。

传统方法的局限性

上一节我们提到了手动编码规则的想法,本节中我们来看看为什么这种方法不可行。

你可能会想象这样做:我们在上一讲中谈到图像中的边缘非常重要。所以,也许我们可以尝试先获取图像,然后使用某种边缘检测算法提取边缘。接着,你可能会尝试在这些边缘中寻找角点或其他可解释的模式。你知道猫可能有三角形的尖耳朵,你希望这些耳朵能在边缘中显现出来,所以也许你可以寻找角点,然后编写关于猫耳朵允许角度的规则。猫的胡须可能在不同位置,胡须可能会在边缘中显现。你可以想象,真的尝试深入其中,硬编码你所有关于猫外观的人类知识,并尝试编写一些明确的算法来检测它们。

但这不是一个很好的方法。它会很脆弱。可能会有没有胡须的猫,或者没有尖耳朵的猫。有时边缘检测器会失败,无法检测到你希望它检测到的边缘。也许你花了很多时间试图弄清楚猫的所有边界情况。但明天,我们想对星系进行分类。很可能你为从图像中识别猫而投入的所有艰苦工作,在我们明天想识别星系时将被完全抛弃。因此,我们真的需要一种更稳健、更可扩展的方法,一种不需要我们写下关于不同类型物体外观的所有人类知识的方法。

机器学习方法

上一节我们看到了手动编码规则的局限性,本节中我们来看看机器学习的解决方案。

这就是我们转向机器学习的原因。其理念是,与其尝试明确编码我们自己关于不同类型物体外观的人类知识,不如采用数据驱动的方法,让算法能够从数据中学习如何识别图像中不同类型的物体。

我们要构建的这个机器学习系统的基本流程是:首先,收集一个包含大量图像的数据集,并用我们希望算法预测的标签类型为它们打上标签。例如,如果我们想构建一个猫狗分类器,我们需要出去收集很多猫和狗的图片,然后收集人工标注,说明哪些图片是猫,哪些是狗。一旦我们收集了这个大数据集,我们将部署某种机器学习算法,该算法将尝试学习输入图像与数据以及我们在数据收集过程中写下的输出标签之间的统计依赖关系。

然后,一旦我们使用机器学习算法提取了这些统计依赖关系,我们就可以在新图像上评估这个分类器。那么,这看起来是怎样的呢?基本上,我们不是编写一个名为classify_image的单一整体函数,而是拥有这个两部分的API。一个是我们需要编写一个名为train的函数,它将输入一组图像及其关联的标签,执行一些神奇的机器学习,然后返回某个统计模型。然后我们的第二部分API是这个predict函数,它将输入我们在训练阶段产生的模型以及要评估该模型的新图像。这将在新图像上运行模型,然后输出从训练集中学习到的标签。

这种方法真正有趣的地方在于,它是一种不同的计算机编程方式。当你考虑编写算法来对数字列表排序或执行其他类型的经典算法时,你基本上是在使用你自己的人类知识来告诉计算机它需要执行的确切步骤,以产生你希望它产生的输出。但现在,当我们采用数据驱动的机器学习方法时,我们基本上是通过我们输入的数据来对计算机进行编程。现在,如果我们想对计算机进行编程以识别猫,我们就输入猫的图片。如果我们明天想识别星系,我们需要做的就是收集一个新的星系数据集,我们不需要重新编写我们的机器学习算法(希望如此),相反,我们只需要输入新数据,然后改变程序的行为。

现在,这对于许多我们不知道如何编写明确程序来解决的问题来说,是一个非常强大的范式。这就是我们将要采用的方法。这已成为基本上所有视觉识别问题(包括图像分类)的主流方法。

常用数据集

上一节我们确定了采用机器学习数据驱动的方法来识别图像,本节中我们需要讨论数据来源。

以下是几个你经常会遇到的常见图像分类数据集:

MNIST数据集

  • MNIST有10个类别,数字0到9。
  • 图像是28x28像素的灰度图像,非常小。
  • 它提供50,000张训练图像和10,000张测试图像。
  • 这个MNIST数据集确实用于识别支票上手写数字的工业应用,并在世界范围内部署。
  • MNIST数据集有时被称为计算机视觉的“果蝇”,因为很多从业者先在MNIST上尝试新想法,但需要注意,几乎所有合理的机器学习算法在MNIST上都能取得很高的性能。

CIFAR-10数据集

  • CIFAR-10的图像也很小,32x32像素,但是彩色的。
  • 类别不再是手写数字,而是更有趣的类别,如飞机、汽车、鸟、猫、鹿等。
  • 这是一个规模相当不错的数据集,有50,000张训练图像和10,000张测试图像。
  • 尽管与其他大规模数据集相比相对较小,但由于这些类别识别起来相当困难,它被认为是具有合理挑战性的。
  • 因此,我们将在本学期的大部分作业中使用CIFAR-10数据集。

CIFAR-100数据集

  • CIFAR-100基本上是类似的统计数据,只是有100个类别而不是10个。
  • 人们使用CIFAR-100比CIFAR-10少一些,但有时你会看到人们研究它。

ImageNet数据集

  • ImageNet已成为图像分类数据集的黄金标准。
  • 基本上,当你提交一篇提出图像分类算法新调整的研究论文时,如果不展示在ImageNet上的结果,审稿人可能会抱怨,你的论文可能会被拒绝。
  • ImageNet非常有趣且具有挑战性,因为它包含1000个不同的类别,比CIFAR或MNIST的10个类别多得多,而且规模非常大。
  • 大约有130万张训练图像,每个类别大约有1300张训练图像,并提供标准的验证集和测试集。
  • 关于ImageNet的一个有趣之处是报告的准确率指标。由于ImageNet有1000个类别,期望算法精确挑选出一个正确类别非常困难,因此实践中人们让算法预测五个类别标签,如果正确类别出现在这五个预测中的任何一个,就算算法预测正确。

MIT Places数据集

  • ImageNet图像倾向于关注猫、狗、鱼、卡车等物体。
  • 另一个相关的数据集试图关注场景类别,如教室、田野、建筑物等。

比较这些分类数据集的大小(假设ImageNet和Places为256x256分辨率)可以发现,CIFAR大约比MNIST大一个数量级,ImageNet大约比CIFAR大两个数量级,而Places又比ImageNet大一个数量级。这说明了为什么ImageNet在某种程度上是与这些其他数据集性质不同的数据集,也使得在ImageNet上的结果更有说服力,但不幸的是,有时计算成本非常高。因此,在本课程中,我们坚持使用CIFAR作为一种折中的选择,在ImageNet上显示的视觉识别任务的复杂性与像MNIST这样较小数据集的计算可负担性之间取得了平衡。

从这张图表中还可以看到数据规模随时间增长的趋势。这绝对是研究的一个有趣方向:我们如何利用越来越大的数据集来增强算法执行稳健分类的能力。

但人们也开始思考另一个方向。一个需要注意的有趣数据集是Omniglot数据集。

Omniglot数据集

  • Omniglot将事情推向极端,希望基准测试算法在相对较少数据下学习的能力。
  • Omniglot有超过1600个不同的类别,每个类别是地球上某种语言字母表中的字母。
  • 它包含了50多种不同书写语言的字母。
  • Omniglot真正有趣的地方在于,它不是为每个图像类别提供大量示例,而是每个字母只提供20个示例。挑战在于构建能够从每个图像类别相对较少的示例中稳健学习的算法。
  • 这种所谓的“少样本分类”问题是一个真正巨大且新兴的研究领域,许多研究人员现在开始思考这个问题。

K最近邻算法

上一节我们讨论了一些常见的图像分类数据集,本节中我们来看看第一个分类算法,因为数据只能用到这一步,你需要某种算法来实际利用这些数据。

我们要讨论的第一个学习算法是最近邻算法。这个算法非常简单,甚至可能不配被称为学习算法。它的工作原理是:记得我说过,当我们实现机器学习系统时,需要实现两个函数,一个叫train,一个叫predict。对于最近邻算法,train函数是微不足道的。我们只是简单地记住所有的训练数据。我们不会处理它,不会对它做任何事情,只是记住我们所有的训练数据。

predict阶段,我们要做的是:获取我们想要预测标签的新图像,使用某种比较或相似性函数将其与训练集中的每一张图像进行比较,然后跟踪训练集中与测试图像最相似的图像,最后简单地返回最相似训练图像的标签。正如我所说,这是一个非常非常简单直接的学习算法,它只在记忆训练数据的意义上进行学习。

但为了实现这个算法,我们实际上需要写下一些可以计算两幅输入图像之间相似度的函数。

一个非常常见的选择是使用图像像素之间的L1距离或曼哈顿距离。我们要做的是:取测试图像(这里我们想象一个非常简单的4x4测试图像,我们明确写出了它所有像素的值),为了将其与训练图像进行比较,我们只需取两幅图像中所有对应像素的差值的绝对值,然后将所有这些对应像素差值的绝对值求和,这样就得到了一个表示两幅图像之间距离或差异的单一数字。这里需要指出的一点是,这满足数学中度量(metric)的所有常规规则。如果我们有两幅完全相同的图像,距离为零,三角不等式等规则也满足,这是一个在数学上定义合理的度量。

基本上,有了这几条信息,就足以实现你的第一个学习算法了。事实上,最近邻分类器是如此简单直接,以至于我们可以将完整的实现放在一张幻灯片上,甚至加上一些注释。更好的是,我想你甚至能读懂它。

在我们的最近邻分类器中,我说过我们需要实现两件事,一个是train步骤,这里很简单,我们只是记忆训练数据,所以我们将图像X和它们的标签y分配给类的某些成员变量。然后在predict中,同样非常简单,我们接收一些新的测试图像X_test,简单地遍历训练集中的所有图像,计算这个L1距离,然后返回最相似图像的标签。就是这样,这就是最近邻算法。你现在可以实现你自己的机器学习系统了。

算法复杂度分析

上一节我们介绍了K最近邻算法的基本思想,本节中我们来分析一下它的计算复杂度。

关于这个最近邻分类器,如果我们有N个训练样本,那么训练速度有多快?这似乎有点棘手。如果我们只是存储指向所有训练数据的指针,那么这可以在常数时间内完成;如果你要进行深拷贝,那可能是线性时间,但我们不这样做。

那么,测试速度有多快?这将是线性时间,因为我们将图像大小和范数计算视为常数,这意味着对于每个测试样本,我们需要将其与N个训练样本中的每一个进行比较,因此在测试时,我们将付出与训练集大小成线性关系的性能代价。

这实际上非常糟糕。这实际上与我们希望机器学习系统具备的特性相反。因为如果你考虑如何部署一个机器学习系统,我们希望做的是尽可能收集关于手头任务的大量数据,然后也许利用这些大量数据训练一个强大的模型。训练那个模型花费很长时间是可以接受的。但当我们最终部署该模型并实际在测试时使用它时,我们希望它非常快。我们希望能够在你的手机上实时运行这些模型,或者为网络上数百万或数十亿用户运行它,处理互联网上传播的所有照片。因此,这通常是我们希望机器学习系统具备的完全相反的特性。我们将看到,当我们转向基于神经网络的方法时,它们会反转这种层次结构,我们最终使用的这些神经网络系统训练时间相对较长,但在推理时相对较快。

当然,为了完整性,我也需要指出,有许多有趣的算法用于计算近似最近邻,当你执行近似最近邻计算时,这可能比这些完全的暴力方法快得多,这超出了本课程的范围,但如果你发现自己因某种原因确实需要执行大规模最近邻搜索,了解一下是很好的。

算法性能与可视化

上一节我们分析了算法的复杂度,本节中我们来看看最近邻分类器在图像上的实际表现。

这里展示的是在CIFAR-10数据集上最近邻分类的结果。左侧列我们看到一堆测试样本,来自CIFAR-10测试集的一堆例子。然后沿着每一行,我们看到训练集中与每个测试样本的最近邻。

正如你可能直觉的那样,因为我们通过字面比较图像像素值来计算图像之间的距离,所以最近邻往往是看起来视觉上非常相似的图像。例如,看第三行,中间这个橙色斑点是我们的测试图像。然后如果你看这一行,你看到的其他图像,我们检索到的最近邻是那些中间可能有橙色或红色斑点、背景是绿色或棕色的东西。我们用来计算最近邻的这个L1距离真的不是很智能,它不知道自己在看什么。

如果我们看看这些最近邻中哪些是正确的或错误的,我们可能能感觉到这个算法表现有多差。有时仅仅通过观察很难分辨这些图像是什么,因为它们分辨率相对较低。但我尝试做的是在错误的最近邻周围画上红框,在正确的最近邻周围画上绿框。这让你感觉到,即使图像通过这个L1距离测量看起来视觉上非常相似,它们实际上有时可能有非常不同的语义含义。这在第四行可能很明显,当你看到这种被白色背景包围的棕色斑点时,我认为测试图像是一只青蛙。但它的最近邻实际上是一只猫。所以猫也是一个棕色斑点和白色背景,因此通过这个L1度量看起来视觉上非常相似,但标签不同。所以我们会做出错误的分类。

这是思考最近邻分类器在做什么的一种方式。另一种思考最近邻分类器的方式是通过决策边界的概念,我们可以在这个需要稍作解释的图中看到。

我们在这里展示的是,我们想象在具有两个像素的图像上执行最近邻分类。那么这里的x轴可能是我们一个像素的强度值,y轴是另一个像素的强度值。

现在,我们看到的这些彩色点中的每一个都是训练图像的例子,其中点的颜色可能代表训练图像的类别。也许红点是猫,蓝点是狗,等等。

现在,背景区域的颜色表示如果我们对其中一个测试图像运行最近邻分类,将分配给该空间点的类别标签。例如,在这个红色的X处,我们可以看到训练集中最近的邻居可能是这个红点,这意味着如果我们对这个红色的X执行最近邻分类,我们将预测红色类别。

那么,这里有趣的是看不同类别之间的决策边界。我们在这里画出了空间中划分将被分类为绿色和将被分类为紫色的区域。

当我们以这种方式观察最近邻分类器时,我们可以认识到几个有趣的事情。一是这些决策区域可能非常嘈杂,并且容易受到异常值的影响。例如,我们看到在训练集中,有一个黄点位于一大堆绿点中间。也许是噪声,也许它本应被标记为绿色而不是黄色,这很难说。但当我们使用这个最近邻分类器时,这个绿点云中单个黄点的存在将导致该黄点周围的一堆测试样本被分类为黄色。这可能是好的,也可能是坏的。我们还可以在屏幕左侧看到,红色类和蓝色类之间有一个锯齿状的决策边界。同样,这是因为仅依赖最近邻来执行分类可能有点嘈杂。

那么问题是,我们能做些什么来平滑这些决策边界,也许给我们一个更稳健的分类?

K值的影响与距离度量

上一节我们通过决策边界看到了最近邻分类器的表现,本节中我们来看看如何通过调整参数来改进它。

一个想法是简单地使用更多的邻居。到目前为止,我们讨论的最近邻分类器只是简单地复述与每个测试样本最近的训练样本所附的标签。但我们可以做的是使用不止一个最近邻。相反,我们可以考虑一组K个最近邻,然后想象某种方式组合我们K个最近检索结果的类别标签。有很多不同的方式可以想象这样做,但一个简单的想法是简单地对所有K个最近邻的类别标签进行多数投票。

那么,一旦有了最近邻分类器使用决策边界的图像,让我们看看K=1和K=3之间的一些差异。一是我们的决策边界变得平滑了很多。你可以看到,当我们使用K=1时,由于只使用一个邻居,我们的决策边界非常嘈杂。现在,如果我们在相同的数据集上使用三个邻居来执行分类,我们可以看到这两个类别之间的决策边界已经平滑了很多。

我们还可以看到,这有助于减少异常值对我们分类性能的影响。现在,即使我们仍然有一个黄点悬停在一堆绿点中,它不再影响分类区域。类似地,红色和蓝色之间的区域也因使用多于一个最近邻而变得平滑了一些。

但还有另一个问题,即当K大于1时,类别之间可能会出现平局。在这个可视化中,这些白色区域都有三个最近邻,它们都属于不同的类别。因此,你需要某种机制来打破平局。也许你可以想象基于距离的某种启发式方法,也许退一步使用一个最近邻的结果。在这种情况下,你可以想象不同的启发式方法。

当我们做最近邻分类器时,我们可能想要改变或调整的另一件事是改变我们用来计算图像之间相似度的距离度量。

到目前为止,我们讨论的是使用图像之间的L1距离,即两幅图像所有对应像素的绝对差值之和。另一个常见的选择是图像像素之间的L2或欧几里得距离。我们在这里所做的本质上是将图像的像素拉伸成一个长向量,然后想象在高维空间中计算这两幅图像点之间的欧几里得距离。

有趣的是,如果我们翻回到使用决策边界的最近邻图像,你可以看到,当我们使用不同的距离度量时,我们得到的决策边界具有定性上不同的属性。我将把这留给读者作为练习。但对于L1分类,我们可以看到所有类别之间的决策边界都由坐标轴对齐的块组成,它们要么是垂直线段,要么是水平线段,要么是45度角的线段。但当我们使用L2或欧几里得距离来计算最近邻时,现在我们的决策边界仍然是分段线性的,但这些线可以出现在输入中的任何方向。因此,使用不同的距离度量在某种程度上是你作为人类专家可以将一些你自己的人类知识嵌入到你希望算法考虑的结构中的一种方式。对于图像分类来说,L1与L2距离度量会导致什么语义差异,直觉上并不十分清楚。

算法的通用性与局限性

上一节我们探讨了K值和距离度量的选择,本节中我们来看看K最近邻算法的通用性及其根本局限性。

关于K最近邻算法真正有趣的是,基本上如果我们选择不同类型的距离度量,我们可以想象将K最近邻应用于几乎任何类型的数据。到目前为止,我们讨论的是使用传统的向量范数或向量度量来计算点之间的距离。但你可以想象使用非常奇怪或有趣的数据类型,并为它们编写非常复杂的距离函数,以便在许多不同的数据集上执行最近邻分类。

这里的一个例子是比较研究论文。有一个很酷的网站叫Archive Sanity,可以让你每天对新出现的研究论文进行有趣的探索。这个网站的一个有趣功能是,它可以显示与另一篇论文相似的论文。在这里,我在Archive Sanity上查找了我去年写的一篇名为Mesh R-CNN的论文。然后如果我们点击“显示相似”,那么它所做的基本上是在这些PDF文件上进行最近邻检索。它实现的方式是使用一个有趣的距离度量。这里的距离度量称为TF-IDF相似度(词频-逆文档频率),这在很多NLP应用中非常常用,我不会告诉你它是如何工作的,但它只是一种适用于文本片段、编码了单词在不同文档中出现频率的人类知识的距离度量。有趣的是,使用这种TF-IDF度量对研究论文进行最近邻检索实际上给出了非常好的结果。如果我查看我自己最近论文的四个最近邻,我们看到这四篇论文,它们对你来说毫无意义。但实际上,其中三篇是我们直接比较、引用并努力确保在发表论文时超越的东西。但有趣的是,这里的最近邻是我们没有引用的东西。所以也许我应该回去读一下那篇。

但这里的重点是,最近邻算法,尽管看起来相对简单,但可以相当强大,并且可以通过改变计算元素之间距离的方式,应用于相当稳健和不同类型的数据。

这也挺有趣的。几年前,我用JavaScript写了一个交互式网络演示,可以让你为最近邻生成这些可视化效果。你可以访问这个链接并尝试一下,可以交互式地拖动点并观察决策边界的移动。你可以更改类别数量、训练点数量、使用的最近邻K值,并可以尝试在L1和L2度量之间切换,以尝试了解所有这些选择在定性上做了什么,以及它们如何改变你的K最近邻分类器的决策边界。编写这个东西花了我两天时间,所以我真的希望有人能看看它。我认为这是一个有用的工具,可以帮助你获得一些关于这个K最近邻分类器在做什么的直觉。

超参数与模型评估

上一节我们看到了K最近邻算法的灵活性与一个演示工具,本节中我们来讨论算法中需要人为设定的关键选择——超参数,以及如何正确地评估模型。

到目前为止,我们已经看到了在执行K最近邻分类时必须做出的几个不同选择。我们已经看到,除了训练数据,我们需要选择一个K值,即在进行算法时要考虑多少个不同的邻居。我们还看到需要选择距离度量:应该使用L1、L2,还是尝试设计一些融入我们自己领域知识的东西?对于不同的问题,如何设置这些并不清楚。

因此,K和距离度量的这些选择,就是我们称之为超参数的例子。超参数是我们在学习算法中需要做出的选择,我们不能直接从训练数据中学习它们,因为它们以某种深刻的基本方式与算法的工作方式相互作用。这些超参数我们不能直接通过学习来设置,所以我们需要其他机制来选择哪些超参数值在我们的数据上效果最好。

不幸的是,在实践中没有很多很好的方法来选择超参数。最简单的方法是它们非常依赖于具体问题。所以我们基本上需要尝试不同的值,看看什么对我们的数据和任务最有效。

但这里有一些细微差别,关于“尝试不同值”和“决定哪个效果最好”到底意味着什么。

以下是关于如何设置超参数的几个想法:

想法一:根据训练集准确率选择
也许我们应该选择使我们的学习算法在训练集上给我们最高准确率的超参数值。这似乎是合理的,我们希望我们的算法表现良好。我们有一个训练集,训练集用于训练,那么也许我们应该将超参数设置为在训练集上给我们最佳性能。尽管这看起来合理,但实际上是一个可怕的想法。永远不要这样做。原因是这会导致你误入歧途。对于K最近邻分类的具体例子,如果你试图通过最大化训练集准确率来设置超参数,你总是会选择K=1。因为想象一下,如果你在K最近邻分类器中使用一个训练点,如果你使用K=1,它会尝试找到最近的训练点,也就是它自己,然后它总是返回正确的标签,所以K=1的K最近邻分类器在训练集上总是得到100%。但正如我们从一些定性例子中看到的,直觉上这可能并不正确,因为我们看到也许设置更高的K值可以使决策边界平滑,这对于某些问题实际上可能是正确的做法。但仅通过查看训练集准确率,我们永远无法知道这一点。

想法二:根据测试集准确率选择
也许我们需要做的是将数据分成两部分。我们可能保留大约90%的数据并称之为训练集,然后保留大约10%的数据并称之为测试集。然后我们要做的是尝试不同的超参数值,使用学习算法从训练集中学习,然后查看测试集上的准确率。然后随着我们改变超参数的值,我们将选择在测试集上效果最好的超参数值。现在,这更合理,因为使用机器学习算法的总体目标是泛化到未见过的数据。我们不关心训练集上的性能,因为我们的数据集中已经有那些标签。我们关心的是在未见过的数据上的性能,而这种方法在某种程度上给了我们算法在训练期间未见过的数据上性能的估计。但即使我告诉你这看起来很合理、很符合逻辑,这也是错误的。你不应该这样做。这实际上和直接在训练集上训练一样糟糕。如果你这样做,你会对你的学习算法的性能得出不正确的结论,因为基本上我们在这个方法中所做的是以不同的方式在测试集上学习。因为一旦你查看了测试集,你的算法就被该测试集的知识污染了,如果你以任何方式使用测试集来对你的学习算法做出决策,那么你就是在作弊,因为那样它就不再是未见过的数据,你也不再有任何关于你的算法在实际部署到野外、运行在数据集中未出现的新图像上时表现如何的估计。因此,尽管想法二看起来符合逻辑且似乎合理,但这是机器学习模型中的一个基本禁忌。如果你这样做,你在准备模型的方式上犯了一个根本性的错误。

想法三:使用验证集
一个更好的方法是想法三。这里我们要做的是将数据分成三个集合。我们将有一个用于训练算法的训练集,一个用于设置超参数值的验证集,然后我们保留一个测试集,只在最后使用一次。那么,基本上我们做的是:在训练集上训练我们的算法,尝试不同的超参数值,通过检查验证集上的准确率来评估不同超参数值的性能,然后我们选择在验证集上具有最高性能的超参数值。现在,一旦你为所有超参数选择了这些值,一旦你固定了一切,那么只有在管道的最后,你才接触测试集,而且只接触一次,你在测试集上只运行一次算法。这给了你一个单一的数字,现在它给了你一个关于算法在真正未见过的数据上性能的非常恰当的估计。

尽管这是正确的做法,但在实践中这实际上非常可怕。当你写研究论文时,你已经在这个项目上工作了几个月,甚至几年。在整个过程中,你一直在调整你的算法,精心地改进它。在整个算法开发过程中,作为一个优秀的机器学习实践者,你从未接触过测试集,你只评估验证集。然后是截止日期的那一周,你所有的辛勤工作终于有了成果,终于到了看看你的算法实际表现如何的时候了。在截止日期前一周是你唯一应该在测试集上运行它的时候,尽管这很可怕,你会想:如果我的数字不好怎么办?如果我毕生的心血都浪费在这个算法上了怎么办?嗯,如果事实证明你在测试集上表现不佳,那意味着你的算法不好,也许不应该发表。所以这实际上在实践中非常可怕,但这是进行机器学习和数据处理的正确方式,项目可能因为做错这一点而失败。如果你做错了,你不仅可能让论文被接受,而且会从根本上不诚实地描述你的算法在野外的表现。而这实际上是构建机器学习模型的全部意义所在。因此,尽管这很可怕,但这是正确的事情,当你在处理机器学习问题时,你应该总是这样做。

交叉验证

上一节我们介绍了使用训练集、验证集和测试集的标准流程,本节中我们来看看一种更稳健的超参数选择方法——交叉验证。

我们说过想法三更好。在想法三中,基本技巧是将数据分成三块,但我们可以做得更好。为什么停在那里?我们可以将数据分成更多块,并获得对泛化性能的更好估计。

这就是想法四,称为交叉验证,这可能是我们应该都做的最好的想法。这里的理念是,我们将数据分成许多不同的块,称为折(fold)。现在我们要做的是遍历它们,在这个例子中我们有五折,所以我们将尝试算法的五个不同版本:一个使用第5折作为验证集并在第1到4折上训练;一个使用第4折作为验证集并在第1、2、3、5折上训练,等等。然后你可以做的是,现在你对超参数在未见过的数据上的表现有了稍微更稳健的了解,因为现在对于每个超参数设置,你每个折可能得到一个样本,然后也许你可以使用某个指标(例如所有折上的最高准确率)来选择你的最佳超参数。这可能是选择超参数最稳健的方式,但成本相当高,因为它需要实际在数据的许多不同折上训练你的算法。因此,尽管这绝对是你在实践中最正确的事情,但在大多数机器学习项目中通常不会这样做,仅仅是因为对于许多模型来说训练可能非常非常昂贵。但如果你使用较小的模型或较小的数据集,或者如果你在计算上负担得起,那么某种交叉验证确实是为你机器学习模型设置超参数的正确方法。

当你运行交叉验证时,最终会得到像这样的图。那么这里,也许X轴是我们的一个超参数K的不同值,Y轴上我们看到每个点是在我们运行算法的不同试验中,每个超参数设置的验证集性能之一。那么,这是对K进行5折交叉验证的例子,这里的线可能给出了每个超参数设置下所有折的平均值。那么在这里我们可以看到,也许在K约等于7时达到峰值。那么基于这个交叉验证的例子,K=7是为这个超参数设置的正确值,然后我们应该运行我们的模型。那么一旦我们设置了K=7这个值,我们应该然后在测试集上只运行一次我们的模型,这就是我们为算法报告的数字。

算法的理论性质与维度灾难

上一节我们介绍了交叉验证这种更稳健的评估方法,本节中我们来看看K最近邻算法的一个有趣理论性质及其根本限制。

K最近邻算法的另一个有趣特性是这种通用逼近性质。真正有趣的是,K最近邻实际上对其所能表示的函数类型做了很少的假设。事实上,当我们将训练样本数量趋近于无穷大时,K最近邻实际上可以表示任何函数。当然,这里的“任何”带有数学上的星号,因为每当你做出这样的陈述时,上过实分析课程的人就会开始指出它可能失败的所有边界情况。所以我在这里试图稍微掩饰一下。但基本上,对于你在自然界中可能遇到的所有实际函数,你可以预期这工作得相当好。作为K最近邻这种通用逼近性质如何工作的直观例子,这里有一个使用最近邻方法进行连续值预测的例子。这里我们可能有一个单像素图像,所以只是一个单一的浮点数作为我们的输入X。现在我们想预测一个单一的浮点数Y。那么这里的蓝色曲线显示了我们希望机器学习模型学习的一些底层真实函数。但我们只能访问有限数量的数据样本。这里,黑点代表来自这个底层真实函数的有限数量的样本。

现在,绿色曲线表示一个最近邻分类器(在这种情况下,我想是一个最近邻回归器)的值。如果我们使用这个有限的训练样本来近似这个底层真实函数,并且因为它是一个最近邻,它在每个训练样本周围有一个平坦的常数区域,并且在恰好两个训练样本之间的任何地方都有不连续性。

这个例子只使用了五个点进行训练,所以我们函数

03:线性分类器

在本节课中,我们将要学习线性分类器。线性分类器是构建更复杂神经网络模型的基础模块。我们将探讨其工作原理、不同的理解视角,以及如何通过损失函数来评估和优化分类器的性能。

课程概述

首先,我们回顾一下上一讲的内容。上一讲我们介绍了图像分类问题及其挑战,并讨论了数据驱动的方法,特别是K最近邻分类器。然而,K最近邻分类器在实践中存在一些局限性,例如评估速度慢,以及基于原始像素值的距离度量在感知上不够有意义。

本节中,我们将介绍一种不同类型的分类器——线性分类器。线性分类器虽然结构简单,但它是理解后续神经网络模型的关键。我们将从代数、视觉和几何三个视角来理解线性分类器,并学习如何通过损失函数(如多类支持向量机损失和交叉熵损失)来量化其性能,为下一讲的优化方法打下基础。

线性分类器简介

线性分类器是参数化方法的一个具体实例。其核心思想是,系统包含一组可学习的权重参数 W。分类函数 f 接收图像像素 X 和权重 W,输出一个包含每个类别得分的向量。

对于CIFAR-10数据集,输入图像是32x32x3的像素矩阵,总计3072个标量值。我们首先将图像像素展平为一个3072维的列向量 x。权重矩阵 W 的形状为10x3072,其中10是类别数量。分类得分通过矩阵向量乘法计算:s = Wx + b,其中 b 是一个10维的偏置向量,为每个类别提供偏移量。

代码示例:线性分类器前向传播

# 假设 x 是展平后的图像向量,形状为 (3072,)
# W 是权重矩阵,形状为 (10, 3072)
# b 是偏置向量,形状为 (10,)
scores = W.dot(x) + b

理解线性分类器的三种视角

为了更深入地理解线性分类器,我们可以从三个不同的视角来看待它。

1. 代数视角

从代数视角看,线性分类器就是一个简单的矩阵向量乘法加上偏置项:f(x, W) = Wx + b

这个视角下,有两个特性变得很明显:

  • 偏置技巧:我们可以通过在输入向量 x 末尾添加一个常数1,并在权重矩阵 W 中添加一列来合并偏置项 b,从而消除独立的偏置参数。公式变为 f(x, W) = W'x',其中 x' = [x; 1]W' 包含了原来的 Wb
  • 预测的线性性:分类器的输出是输入的线性函数。例如,如果将输入图像的所有像素值乘以一个常数 c,那么输出得分也会乘以相同的常数 c

2. 视觉视角

从视觉视角看,我们可以将权重矩阵 W 的每一行重塑成与输入图像相同的形状(例如,对于CIFAR-10是32x32x3)。这样,每一行就变成了一个“模板图像”。

以下是理解视觉视角的关键点:

  • 每个类别对应一个学习到的模板。
  • 通过计算输入图像与每个模板的内积(即点积)来得到该类别的得分。内积越大,表示图像与该模板越匹配。
  • 通过可视化这些模板,我们可以直观地看到分类器在寻找什么特征。例如,一个“飞机”模板可能包含大量蓝色,“鹿”模板可能包含绿色背景和棕色斑点。

然而,视觉视角也揭示了线性分类器的一些固有缺陷:

  • 依赖上下文线索:分类器可能过度依赖背景(如“鹿”模板中的绿色),而非物体本身。
  • 模式混合:一个类别(如“马”)可能有多种外观(向左看、向右看)。线性分类器只能学习一个模板,因此它学到的模板可能是这些不同模式的模糊混合体,导致识别能力受限。

3. 几何视角

从几何视角看,我们将每张图像视为高维像素空间(例如3072维)中的一个点。线性分类器为每个类别定义了一个决策超平面。

以下是几何视角的核心概念:

  • 每个超平面将整个像素空间划分为两个半空间,分别对应“属于该类”和“不属于该类”的区域。
  • 分类器的得分决定了点距离超平面的远近和方位。

这个视角有助于我们理解线性分类器的表达能力限制。例如,它无法完美区分以下情况:

  • 异或问题:当两类数据点像棋盘格一样交错分布时,无法用单个超平面(直线)将其分开。
  • 多模态数据:如果同一类别的样本在像素空间中形成多个离散的簇(例如“马”的不同姿态),单个超平面也难以将其与其他类别有效分离。

损失函数

到目前为止,我们讨论了如何用权重 W 进行预测,但还没有说明如何从数据中学习到好的 W。这就需要引入损失函数。

损失函数(有时也称为目标函数或成本函数)用于量化分类器在给定数据上的“糟糕”程度。损失值高表示性能差,损失值低表示性能好。整个机器学习的目标可以看作是寻找使损失函数最小化的参数 W

对于包含N个样本的数据集,总损失 L 通常是所有样本损失的平均值:
L = (1/N) * Σ L_i(f(x_i, W), y_i)
其中 L_i 是第i个样本的损失,f(x_i, W) 是模型预测,y_i 是真实标签。

多类支持向量机损失

多类支持向量机损失基于一个直观思想:正确类别的得分应该比所有错误类别的得分高出至少一个边界值(通常设为1)。

其数学公式为:
L_i = Σ_{j≠y_i} max(0, s_j - s_{y_i} + 1)
其中 s_j 是第j类的得分,s_{y_i} 是正确类别的得分。

公式解释:对于每个错误类别 j,如果其得分 s_j 没有比正确类别得分 s_{y_i} 低至少1,就会产生损失。损失量是 s_j - s_{y_i} + 1;如果低得足够多,损失为0。最后将所有错误类别的损失相加。

示例计算
假设一个“猫”图像,得分向量为 s = [3.2, 5.1, -1.7](分别对应猫、汽车、青蛙)。

  • 对于错误类别“汽车”(j=1):max(0, 5.1 - 3.2 + 1) = max(0, 2.9) = 2.9
  • 对于错误类别“青蛙”(j=2):max(0, -1.7 - 3.2 + 1) = max(0, -3.9) = 0
  • 该样本的SVM损失为 2.9 + 0 = 2.9

正则化

我们可能会发现,存在多个不同的 W 都能使训练集上的损失为0。为了表达我们对“更好”的 W 的偏好,并防止模型过拟合(即在训练集上表现太好而在新数据上表现差),我们引入正则化项。

总损失函数变为:
L = (1/N) * Σ L_i(f(x_i, W), y_i) + λ R(W)
其中 R(W) 是正则化项,λ 是控制两者权重的超参数。

常见的正则化项有:

  • L2正则化R(W) = Σ Σ W_{i,j}^2,倾向于让权重向量更分散,利用所有特征。
  • L1正则化R(W) = Σ Σ |W_{i,j}|,倾向于产生稀疏权重,让模型只依赖少数重要特征。
  • 弹性网络:L1和L2正则化的结合。

正则化不仅有助于选择更简单、泛化能力更强的模型,有时也能使优化过程更稳定。

交叉熵损失

交叉熵损失是神经网络中最常用的损失函数之一。它首先通过Softmax函数将分类器输出的原始得分(称为逻辑单元)转化为概率分布。

Softmax函数公式为:
P(y = k | x) = exp(s_k) / Σ_j exp(s_j)
其中 s_k 是类别k的得分。该函数确保所有输出概率为正且和为1。

然后,交叉熵损失衡量预测概率分布与真实“one-hot”分布(正确类别概率为1,其余为0)之间的差异。对于单个样本,其损失为:
L_i = -log( P(y = y_i | x) )
即正确类别预测概率的负对数。

示例计算
沿用之前的得分 s = [3.2, 5.1, -1.7]

  1. 计算Softmax概率:
    • exp(3.2)=24.53, exp(5.1)=164.0, exp(-1.7)=0.183
    • 总和 = 24.53+164.0+0.183 = 188.7
    • 概率:P(cat)=24.53/188.7≈0.13, P(car)=164.0/188.7≈0.87, P(frog)=0.183/188.7≈0.001
  2. 计算交叉熵损失(真实类别为“猫”,索引0):
    • L_i = -log(0.13) ≈ 2.04

重要提示:与SVM损失不同,交叉熵损失永远不会达到零。即使预测完全正确(正确类别概率接近1),损失也只是趋近于0,这驱使模型不断优化。此外,对于一个随机初始化的分类器,其预测概率大致均匀,初始交叉熵损失应接近 -log(1/C),其中C是类别数(对于CIFAR-10约为2.3)。这是一个有用的调试参考。

SVM损失与交叉熵损失比较

以下是两种损失函数的主要区别:

  • SVM损失:更“满足”。一旦正确类别得分比错误类别高出边界值,损失就为0,微调得分不会改变损失。
  • 交叉熵损失:永远“不满足”。它总是希望正确类别的概率尽可能高(接近1),错误类别的概率尽可能低(接近0),因此会持续推动得分分离。

在实践中,交叉熵损失通常与Softmax函数结合使用,是训练深度神经网络分类任务的标准选择。

总结

本节课中我们一起学习了线性分类器的基础知识。我们首先介绍了线性分类器作为参数化模型的基本形式。然后,我们从代数、视觉和几何三个视角深入探讨了其工作原理和内在限制。接着,我们引入了损失函数的概念,它是衡量模型性能并指导其学习的关键。我们详细讲解了多类支持向量机损失和交叉熵损失这两种重要的损失函数,并比较了它们的特性。最后,我们讨论了正则化技术,它通过向损失函数中添加一个与数据无关的项,来防止过拟合并引导模型选择更优的解决方案。

通过本节课的学习,我们已经掌握了如何用数学公式描述一个分类器,以及如何定义它的好坏。在下一讲中,我们将解决最关键的问题:如何通过优化算法,自动地从数据中找到那个能使损失函数最小化的最佳权重参数 W

04:优化 🎯

在本节课中,我们将要学习如何为线性分类器找到最优的权重矩阵。具体来说,我们将探讨优化这一核心主题,即如何最小化损失函数。我们将从最简单的随机搜索开始,逐步深入到梯度下降及其更高效的变体,如带动量的随机梯度下降和Adam优化器。


优化概述 🌄

上一节我们介绍了如何使用损失函数来量化对权重矩阵的偏好。本节中,我们来看看如何实际找到能最小化该损失函数的权重矩阵W。这个过程被称为优化。

我们可以将优化问题形式化地写为:寻找权重矩阵W*,使得损失函数L(W)最小。直观上,这就像在一个高维地形中,蒙着眼睛寻找最低点。


迭代优化方法 🔄

由于通常无法直接写出损失函数最小值的解析解,我们需要使用迭代方法来逐步改进解。

随机搜索

以下是随机搜索的基本思路:

  • 生成大量随机的权重矩阵。
  • 在训练集上评估每个权重矩阵对应的损失值。
  • 记录找到的最低损失值。

虽然这是一个非常低效的算法,但它能让我们对优化问题有一个初步的认识。

梯度下降

一个更聪明的策略是沿着地形的坡度向下走。这需要计算梯度。

对于标量函数,梯度(导数)告诉我们函数在某点的斜率。对于向量输入、标量输出的函数(如我们的损失函数),梯度是一个向量,指向函数值增长最快的方向。因此,负梯度方向就是函数值下降最快的方向。

计算梯度有两种主要方法:

  1. 数值梯度:通过极限定义直接计算近似值。对于权重矩阵W的每个元素,进行微小扰动,计算损失的变化率。

    # 数值梯度近似计算示例(伪代码)
    grad_approx = (L(W + h) - L(W)) / h
    

    这种方法简单直观,但计算缓慢(需要前向传播的次数与参数数量成正比)且是近似值。

  2. 解析梯度:利用微积分知识,直接推导出梯度关于W的精确数学表达式。

    # 解析梯度计算示例(伪代码)
    grad_exact = dL/dW  # 通过推导得到的梯度公式
    

    这种方法精确且快速,但推导过程容易出错。

在实践中,我们通常使用解析梯度进行训练,但会使用数值梯度作为调试工具来验证解析梯度的正确性。


梯度下降算法 📉

掌握了梯度计算后,我们可以正式介绍梯度下降算法。

梯度下降算法的核心步骤如下:

  1. 初始化权重W。
  2. 循环迭代一定次数:
    • 计算当前W处的梯度 dL/dW
    • 沿负梯度方向更新权重:W = W - learning_rate * dL/dW

这个算法引入了几个重要的超参数:

  • 权重初始化:如何设置W的初始值。
  • 迭代步数:运行算法的轮数。
  • 学习率:控制沿梯度方向更新步长的大小。

从直观上看,梯度下降算法就像沿着最陡的下坡方向小步前进。在接近谷底(最小值)时,梯度变小,步长也会自然减小。


随机梯度下降 ⚡

标准的(全批量)梯度下降需要在整个训练集上计算损失和梯度,当数据集很大时,这非常耗时。

随机梯度下降通过使用小批量数据来近似整个数据集的梯度,从而大幅提高效率。其更新公式为:
W = W - learning_rate * dL/dW_minibatch

这引入了新的超参数:

  • 批量大小:每个小批量中的样本数量。通常设置为32、64、128等,在GPU内存允许的情况下尽可能大。
  • 数据采样方法:如何从数据集中抽取小批量。通常随机打乱后顺序遍历。

“随机”一词来源于我们将训练数据视为从某个真实数据分布中采样得到的。小批量损失是对该分布期望的蒙特卡洛估计。


梯度下降的挑战与改进 🚧

基本的随机梯度下降在面对某些损失函数地形时可能遇到问题。

常见问题

  1. 高条件数问题:损失函数在不同方向上的曲率差异很大,导致优化路径曲折,收敛缓慢。
  2. 局部极小值与鞍点:在这些点上梯度为零,算法可能停滞。在高维空间中,鞍点比局部极小值更为常见。
  3. 梯度噪声:由于使用小批量估计梯度,更新方向存在噪声,可能导致优化路径不稳定。

带动量的随机梯度下降

动量法模拟了物理中球滚下山坡的惯性。它引入一个速度变量v,用于累积历史梯度信息。
更新规则如下:
v = rho * v + dL/dW
W = W - learning_rate * v
其中,rho是摩擦系数或衰减率(通常接近1,如0.9)。

动量法有助于:

  • 平滑高条件数问题中的振荡。
  • 凭借积累的速度冲过平坦的鞍点区域。
  • 抑制梯度噪声的影响。

自适应学习率算法:AdaGrad / RMSProp

这类算法通过调整每个参数的学习率来应对高条件数问题。

  • AdaGrad:累积历史梯度平方和,更新时除以该平方和的平方根。这使得在梯度大的方向步长变小,在梯度小的方向步长相对变大。但累积和会持续增长,可能导致学习率过早衰减至零。
  • RMSProp:是AdaGrad的改进,使用指数衰减移动平均来累积梯度平方和,解决了学习率持续衰减的问题。
    # RMSProp 核心更新(简化示意)
    cache = decay_rate * cache + (1 - decay_rate) * (dL/dW)**2
    W = W - learning_rate * dL/dW / (sqrt(cache) + eps)
    

Adam 优化器 🏆

Adam结合了动量法和RMSProp的思想,同时跟踪梯度的一阶矩(均值,类似动量)和二阶矩(未中心化的方差,类似RMSProp),并进行偏差校正。
其超参数通常设置为:beta1=0.9, beta2=0.999, learning_rate=1e-31e-4

Adam因其鲁棒性和较少的超参数调优需求,成为深度学习实践中常用的默认优化器。


二阶优化方法 📐

上述方法均为一阶优化算法,仅使用梯度信息。理论上,使用海森矩阵(二阶导数)的二阶优化方法可以构建更精确的局部二次近似,从而更智能地选择步长。

然而,对于参数数量为N的模型,海森矩阵是N×N的,存储和求逆的计算复杂度(O(N²)和O(N³))对于大规模深度学习模型来说是不可行的。因此,二阶优化方法在实践中较少用于训练大型神经网络。


总结 📚

本节课中我们一起学习了优化的核心概念与方法:

  1. 我们明确了优化的目标:找到最小化损失函数的权重矩阵。
  2. 我们介绍了梯度下降的基本原理,以及计算梯度的数值法和解析法。
  3. 我们探讨了全批量梯度下降的局限性,并引入了更高效的随机梯度下降。
  4. 我们分析了SGD可能遇到的问题(高条件数、鞍点、噪声),并学习了两种主要的改进策略:
    • 动量法:通过累积历史梯度来加速收敛并逃离平坦区域。
    • 自适应学习率法:如AdaGrad/RMSProp,为每个参数调整学习率。
  5. 我们重点介绍了结合两者优点的Adam优化器,它通常是实践中的首选。
  6. 我们简要讨论了一阶优化与二阶优化的区别,以及后者在大规模应用中的局限性。

现在,我们已经掌握了使用线性模型、损失函数和优化算法来解决图像分类问题的完整流程。在接下来的课程中,我们将把简单的线性分类器替换为更强大的神经网络分类器,并应用这些优化技术来训练它们。

05:神经网络

在本节课中,我们将要学习神经网络。我们将首先回顾线性分类器的局限性,然后介绍特征变换的概念,最后深入探讨神经网络的基本结构、工作原理及其强大的函数表示能力。

从线性模型到特征变换

上一节我们介绍了如何使用随机梯度下降及其变体来优化线性模型的权重。本节中,我们来看看如何克服线性模型本身的局限性。

线性分类器虽然简单易懂,但其功能表示能力有限。从几何角度看,它只能通过高维超平面来分割空间;从视觉角度看,它每个类别只能学习一个“模板”,无法表示同一类别的多种形态(例如,向左看的马和向右看的马)。

为了克服这些限制,我们可以引入特征变换的概念。其核心思想是:将原始输入数据通过一个精心设计的数学变换,映射到一个新的“特征空间”中,使得数据在这个新空间中更容易被线性分类器处理。

以下是一个简单的例子:

  • 原始空间:数据点以笛卡尔坐标 (x, y) 表示,无法用一条直线分开。
  • 特征变换:将坐标转换为极坐标 (r, θ)
  • 特征空间:在极坐标下,数据变得线性可分。

此时,在特征空间中训练的线性分类器,其决策边界映射回原始空间时,就对应着一条非线性的决策边界。通过选择合适的特征变换,我们可以显著提升模型的表达能力。

在计算机视觉领域,特征变换曾被广泛使用。以下是几种经典的特征表示方法:

  • 颜色直方图:将图像的颜色空间划分为多个区间,统计每个颜色区间内像素的数量,形成一个直方图向量。这种方法丢弃了空间信息,但对颜色分布敏感。
  • 方向梯度直方图:计算图像局部区域的边缘方向和强度,并统计成直方图。这种方法关注纹理和形状,而忽略了颜色信息。
  • 视觉词袋:这是一种数据驱动的特征表示。首先从大量训练图像中提取随机图像块,然后对这些块进行聚类,得到一组“视觉单词”(即聚类中心)。对于一张新图像,统计其中每个“视觉单词”出现的频率,形成特征向量。

此外,我们还可以将多种特征表示(如颜色直方图、HOG、视觉词袋)拼接成一个长向量,以捕获图像不同方面的信息。2011年ImageNet挑战赛的冠军就使用了复杂的特征提取流水线,最后在其上训练线性SVM。

然而,基于特征提取的流水线存在一个根本问题:特征提取阶段通常是固定的,或者其学习目标并非直接优化最终的分类性能。整个系统中,只有最后的线性分类器部分会根据分类任务调整其参数。

神经网络:端到端学习

与上述方法不同,神经网络构建了一个端到端的学习系统。它从原始像素输入开始,直接预测分类得分。在训练过程中,整个网络的所有参数(包括特征提取部分和分类部分)都会根据最终的分类性能进行联合优化

从这个角度看,神经网络可以看作是在联合学习一个适合当前任务的特征表示,以及一个作用于该特征表示之上的线性分类器

神经网络的结构

现在,让我们来看一个具体的神经网络例子。我们熟悉的线性分类器公式为:
s = Wx
其中 x 是输入向量,W 是权重矩阵,s 是得分向量。

最简单的神经网络(两层全连接网络)则将其扩展为:
h = max(0, W1 * x)
s = W2 * h
这里:

  • W1 是第一个权重矩阵,将输入 x 映射到隐藏层 h
  • max(0, ·) 是一个非线性函数,称为ReLU
  • W2 是第二个权重矩阵,将隐藏层 h 映射到输出得分 s
  • h 的维度称为网络的隐藏层大小

我们可以进一步推广到具有 L 层的深度网络:
h1 = f(W1 * x)
h2 = f(W2 * h1)
...
s = WL * h_{L-1}
其中 f 是非线性激活函数(如ReLU)。网络的深度是指权重矩阵的数量,宽度通常指各隐藏层的维度。

这种网络结构通常用图示表示:输入层、隐藏层、输出层从左到右排列,层与层之间由权重矩阵全连接。因此,这种网络常被称为全连接网络多层感知机

神经网络的解释

我们可以从两个角度来理解神经网络的工作方式。

模板匹配视角
类似于线性分类器,神经网络第一层的权重 W1 也可以看作学习到的一组“模板”。隐藏层向量 h 中的每个元素,表示输入图像 x 与对应模板的匹配程度。第二层的权重 W2 则将这些匹配程度以加权和的方式组合,得到最终的分类得分。这使得网络能够学习同一类别的多个模板(例如,向左看的马和向右看的马),从而克服了线性分类器的“双头马”问题。不过,这些模板通常对人类来说并不直观,被称为分布式表示

空间扭曲视角
我们可以将神经网络看作是对输入空间进行一系列非线性扭曲。第一层的每个ReLU单元定义了一个“折叠线”,将输入空间划分为被激活(正值通过)和未被激活(置为零)的区域。多个这样的单元共同作用,将原始输入空间折叠、扭曲成一个新的特征空间。在这个新的特征空间中,数据可能变得线性可分。一个线性决策边界在这个扭曲后的特征空间中,对应着原始输入空间中一个复杂的非线性决策边界。

隐藏层单元越多,这种空间扭曲能力就越强,可以形成更复杂的决策边界。为了防止过拟合,我们通常不是通过减少网络宽度来正则化,而是使用像L2正则化这样的可调参数来控制模型复杂度。

激活函数与通用近似定理

激活函数(如ReLU)是神经网络的关键。如果没有非线性激活函数,无论堆叠多少层,整个网络仍然等价于一个线性变换,无法获得更强的表示能力。

ReLU函数定义为:
ReLU(x) = max(0, x)
它是一个简单有效的默认选择。历史上也使用过Sigmoid、Tanh等其他激活函数。

神经网络一个强大的理论性质是通用近似定理:一个至少包含一层隐藏层的神经网络,只要隐藏层足够宽,就可以以任意精度逼近任何定义在紧致集上的连续函数。

为了直观理解,可以考虑一个单输入单输出的两层网络。其输出是多个经过缩放和平移的ReLU函数之和。通过精心设置权重,我们可以用四个ReLU单元组合成一个“脉冲”函数。使用足够多的这样的“脉冲”,并将它们排列在输入域上,就可以近似任何连续函数。

然而,这个定理主要是一个存在性证明。它告诉我们神经网络有能力表示复杂函数,但并没有说明:

  1. 如何通过训练找到这些权重。
  2. 需要多少数据才能学好。
  3. 学习过程是否容易。

事实上,像K近邻这样的简单模型也具备通用近似性质。因此,神经网络的“魔力”不仅仅在于其表示能力。

优化:凸性与非凸性

线性模型(如Softmax回归、SVM)的损失函数关于权重通常是凸函数。凸函数形如碗状,具有很好的性质:任何局部最小值就是全局最小值,并且存在理论保证的优化算法可以高效地收敛到最优解。

神经网络的损失函数则是非凸函数。这意味着损失函数的形状可能非常复杂,存在许多局部极小值、鞍点和平坦区域。从理论上讲,我们无法保证梯度下降算法一定能找到全局最优解,甚至无法保证收敛。

尽管如此,在实践中,通过精心设计的初始化、优化算法和大量数据,我们通常能够训练出性能优异的神经网络模型。为什么非凸优化在深度学习中行之有效,仍然是当前研究的热点问题。

总结

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

  1. 特征变换:通过将数据映射到新空间来增强线性模型的表达能力。
  2. 神经网络的核心思想:端到端地联合学习特征表示和分类器。
  3. 神经网络的基本结构:全连接层、激活函数(如ReLU)、深度与宽度。
  4. 对神经网络的理解:可以从模板匹配和空间扭曲两个视角来看。
  5. 神经网络的表示能力:通用近似定理表明其理论上可以逼近任何连续函数。
  6. 神经网络的优化挑战:面临的是非凸优化问题,缺乏理论收敛保证,但实践中效果良好。

遗留的一个关键问题是:对于如此庞大复杂的神经网络,我们如何高效地计算其梯度以进行训练?下一节课,我们将介绍解决这个问题的核心算法——反向传播

06:反向传播

欢迎回到课堂。这是第六讲,今天我们将讨论反向传播。和往常一样,我们首先需要处理一些课程管理事务。希望这是最后一次关于候补名单的更新。在我来这里之前,我查看了候补名单,发现过去几天里,名单上的每个人都已获得选课许可。这意味着,所有希望选修本课程的同学现在应该都能成功选课了。我们一起解决了这个问题,大家做得很好。

接下来是关于作业2的提醒。抱歉,这里有个笔误。作业2的截止日期是今天起一周后的周一,即9月30日晚上11点59分。

提醒大家,现在应该已经开始做作业2了。请注意,作业2比作业1要长一些,而作业3会更长。随着学期的推进,大家应该越来越早地开始这些作业。

在进入课程内容之前,关于这些管理事务还有什么问题吗?

好的。问题是关于课程幻灯片没有出现在网站上。我大约45分钟前刚刚上传了幻灯片。你可以尝试按Ctrl+Shift+R刷新并清除缓存,它们应该会显示出来。

如果不是这个问题,那可能是我把链接弄错了。你可以尝试查看之前课程的链接格式,然后把链接中的“5”改成“6”,因为我确实上传了文件,但可能在更新链接时打错了。这样行了吗?好的,如果不行,我会在这次讲座后确保修复幻灯片链接。


上节课我们讨论了神经网络。我们看到,神经网络是一类非常强大的分类器,它让我们能够进行比之前讨论的线性分类器更强大的计算。大家应该记得,神经网络具有相当简单的函数形式:一个矩阵乘法,加上一个我们称为激活函数的逐元素非线性运算,然后是另一个矩阵乘法。我们可以将这些操作链接起来,得到非常深的神经网络。我们还看到了“空间扭曲”的概念,它展示了神经网络如何能够通过输入空间中的非线性决策边界,从而比线性分类器强大得多。我们还讨论了神经网络作为通用逼近器,这从另一个角度说明了神经网络是一类非常强大的函数。我们也看到了非凸性的概念,即尽管神经网络能力强大,能表示许多函数,但也导致了非凸优化问题,这类问题在理论上很少有保证。

上节课结束时,我们留下了一个问题:现在我们可以写出这些非常复杂的表达式来描述损失函数,我们希望使用随机梯度下降来最小化这些损失函数,以训练我们的分类器(无论是神经网络、线性分类器还是其他类型的深度学习模型)。这里的问题是,我们如何实际计算这些模型中的梯度?

我们知道可以写出任意的损失函数。如果我们能找到某种方法计算损失相对于模型所有权重矩阵的梯度,那么我们就可以使用几节课前讨论的优化算法来实际最小化损失,并找到适合我们训练数据的好模型。

今天讲座的主题就是,我们如何实际计算这些梯度,即如何为任意复杂类型的神经网络或其他函数计算导数。

如果你只是天真地处理这个问题,可能会尝试的第一个策略是在纸上推导梯度。我们知道可以写出这些函数,这些损失函数,并在纸上将损失函数展开成一个包含许多项的方程。这里我展开了一个我认为是带有线性分类器的SVM损失函数。你可能采取的一种策略是,把所有东西都写在纸上,展开所有项,最终得到一个巨大的方程,该方程将损失表示为数据和模型权重的函数。然后,如果你非常熟悉矩阵微积分的规则,你可以想象尝试通过计算,在纸上为模型中出现的所有可学习权重矩阵计算出表达式。

事实证明,这不是一个非常可扩展的解决方案。如果有人真的在作业2中尝试了这种方法,如果你确实走了这条路并尝试在纸上计算这些权重矩阵,你会注意到这种方法的一些潜在缺点。

一是它极其繁琐。在处理像交叉熵损失函数或SVM损失这样的损失函数时,你可能需要很多纸才能算对。另一个问题是,对于复杂模型来说,这种方法不太可行。对于像线性模型这样的东西,你或许还能应付,但随着我们扩展到更复杂的模型,这种在纸上写下并推导梯度的方法将根本无法扩展到更复杂的模型。在纸上推导所有东西的最后一个有点微妙的问题是,它不会导致模块化的设计。假设你已经为带有SVM损失的线性分类器推导出了损失函数,明天你又想为带有Softmax损失的线性分类器、或带有Softmax损失的两层神经网络、或带有SVM损失的五层神经网络,或者你能想象到的任何其他损失函数、架构和正则化器的组合推导梯度。如果你为每一种损失函数和架构的组合都从头开始在纸上推导,那么每次你都必须从头开始重新推导所有东西。在实际情况下,拥有某种模块化的方法要好得多,这样你可以交换和插入不同类型的模型、架构和损失函数,从而在尝试为数据找到有效模型时能够更快地迭代。

我们在深度学习中通常采用的方法,你可能已经猜到了,不是在纸上推导梯度。既然我们是计算机科学家,我们喜欢尝试寻找数据结构和算法来帮助我们解决繁琐的问题。我们用来帮助解决计算梯度这个问题的数据结构被称为计算图

计算图是一种有向图,它表示我们在模型内部执行的计算。在左边,我们可以看到模型的输入:数据X和标签y(可能在这个图中没有标签y,但也许这里有数据X和可学习权重W作为图左侧的节点)。现在,当我们从图的左侧向右进行时,我们看到节点代表了在计算这个函数过程中我们想要执行的基本计算片段。我们看到有一个蓝色节点,表示输入X和权重矩阵W之间的矩阵乘法;有一个红色节点,表示如果我们使用SVM分类器时的铰链损失;有一个绿色节点,表示模型中的正则化项;有一个求和节点,表示数据损失和正则化损失的和;最后在右边,我们有计算图的输出,即标量损失L,这是我们在训练模型时想要计算的。

现在,将这种计算图形式应用于像线性模型这样的东西可能看起来有点傻,甚至有点微不足道,因为在线性模型中,正如我们所说,为了计算损失只需要执行几个操作,将它们写成图的形式可能看起来有点小题大做。但当我们转向更复杂、更大的模型时,这将变得至关重要。例如,像AlexNet这样的深度卷积神经网络,它有七个卷积层,每层都有非线性激活和正则化,最后还有一个损失函数。图像从顶部输入,经过许多层的处理,最终的标量损失从底部输出。对于这样的模型,你肯定不想在纸上推导梯度,而是真的想使用这种计算图形式来跟踪数据结构,构建一个表示模型为计算损失而执行的所有计算的数据结构。

这些图可以变得极其复杂。这里有一个叫做神经图灵机的模型示例。如果你还记得计算理论导论课,你会记得图灵机是一种形式化的计算模型。几年前,一些人写了一个神经网络,它有点像你在计算导论课上学到的图灵机的软可微分近似。屏幕上显示的是这个可微分神经图灵机产生的计算图,你可以看到它非常大且复杂。你绝对不想手动计算这个模型的梯度,你真的需要依赖计算图形式来为你计算梯度。但实际上,情况甚至比这更糟,因为对于神经图灵机,这里只显示了模型的一个时间步。在实践中,这种模型会像一种循环网络一样在许多时间步上展开。所以你可以看到,一旦进入这些非常复杂的模型,你很快就会得到计算图,它们大到甚至无法放在一张幻灯片上。因此,你肯定希望使用某种图遍历算法,来帮助我们在这种计算图结构上自动计算梯度。

希望这已经充分说明了为什么使用计算图来计算我们大型复杂神经网络模型中的梯度对我们来说至关重要。既然我们已经有了这个动机,现在让我们看一个具体的例子,看看如何使用计算图来帮助我们在一个非常小的神经网络模型中计算梯度。

为了将示例放在一张幻灯片上,我们不得不使用一个非常简单的计算。但正如你所见,在实际模型中,我们会进行更复杂的处理。这里我们展示了一个三个标量变量X、Y和Z的非常简单的函数,其输出是(X + Y) * Z。这可能是一个奇怪的损失函数,一个没有意义的学习问题,但希望这个简单的例子能帮助我们逐步理解在计算图中计算梯度的确切含义。

顺便说一下,反向传播就是我们用来在计算图中计算梯度的算法。

现在假设我们想在输入空间中的一个特定点评估这个函数,比如x = -2y = 5z = -4。使用这个计算图的第一步称为前向传播。在前向传播中,我们从左到右进行计算,执行图中节点指定的所有操作,以便从输入值计算输出值。在这个例子中,我们简单地将x和y相加,得到一个我们命名为q的中间值。然后,为了计算最终输出值f,我们将q乘以输入值z。通过运行图的前向传播,我们最终计算出本例中的最终输出值为-12

现在在反向传播中,我们的目标是计算所有梯度,即输出相对于每个输入的导数。所以在这个例子中,我们的输出是f,所以我们想计算导数df/dxdf/dydf/dz,它们是图左侧出现的三个输入。我们将从右向左进行,因为这是反向传播,所以它需要与前向传播相反的方向进行。

我们总是从一个基本情况开始。在右侧的基本情况下,我们想计算f相对于f的导数。有人知道这应该是什么吗?是的,这很简单,是1。因为如果我们稍微改变f一点,那么f也会改变相同的量,导数是1。顺便说一下,当我们在图中计算反向导数或使用反向传播时,我们经常会写一个像这样的小图,我们在每条对应线的上方写下每个节点计算出的值,然后在反向传播期间,在对应线的下方写下梯度或导数。

第二步,我们想计算f相对于z的导数。为了做到这一点,我们知道可以看看这个小的中间计算,我们知道f = q * z,所以f相对于z的导数应该就是q。然后我们可以回顾计算图,查找q的值,在本例中是3。所以现在,图中这一小部分的df/dz将是3。

现在我们已经计算出了我们需要计算的三个梯度中的一个。下一部分,我们需要计算f相对于q的导数。你可以看到我们正在以图的一种反向拓扑排序的方式向后推进。为了计算f相对于q的梯度导数,我们再次知道f = q * z,所以这个局部导数应该是z。我们可以从前向传播的图中查找z的值,并计算出导数为-4

继续向左推进,现在我们想计算f相对于y的导数。这里事情变得有点有趣,因为现在我们需要记住微积分中的链式法则,因为值y并不直接连接到输出值f。为了计算f相对于y的导数,我们需要考虑y对中间变量q的影响。那么,微积分中的单变量链式法则告诉我们:df/dy = (dq/dy) * (df/dq)

这非常直观:如果y改变一点点,那么q将改变一点点dq/dy;然后如果q改变,f将改变一点点,即另一个导数。为了考虑这两种效应,我们需要将它们相乘。现在,在神经网络的背景下,这个方程中的这三个不同项有我们将会反复使用的特定名称。

左边的项df/dy,我们通常称之为下游梯度,因为这是我们在过程这一步计算的导数值。值dq/dy将被称为局部梯度,因为这是y的值影响下一个中间输出q的局部效应。值df/dq将被称为上游梯度,因为这是上游的效应。现在我们可以想象放大图中y周围的这一小部分,上游梯度告诉我们这部分图的输出在多大程度上影响了图最末端的最终输出。所以这部分被称为上游梯度。当然,链式法则告诉我们,要得到下游梯度,我们只需要将局部导数和上游导数相乘。

当然,这里我们知道局部梯度,我们知道q = x + y,所以局部梯度或局部导数在这种情况下就是1。所以当我们将这两者相乘时,我们知道f相对于y的导数与f相对于q的导数相同,所以我们得到下游梯度为-4。大家都清楚了吗?好的。

现在计算另一个导数时非常相似。我们再次需要将上游梯度和局部梯度相乘。同样,局部梯度是1,因为这是一个简单的加法。我们计算出最终的输出值。

所以,在这个相对简单的例子中,你可以看到我们如何使用计算图来帮助我们机械化地计算非常复杂函数中的导数。在前向传播中,我们将从左到右计算所有内容。然后在反向传播中,我们将逐步向后遍历图,并在图中的每个点计算这些小的导数。

这种思考计算梯度的方式非常有用,因为它是模块化的。现在,一种思考方式是,我们可以放大计算图中的一个节点。使用反向传播计算梯度的这种机制的真正优点在于,图的每一小部分都不需要知道或关心图的其余部分。我们只需在每个节点内执行局部处理,然后通过聚合所有这些局部处理,我们最终可以计算出整个图中的这些全局导数。

如果我们逐步完成上一张幻灯片中讨论的完全相同的过程,但放在单个局部节点的上下文中,它看起来是这样的:对于图中的每个节点,我们计算某个小的局部函数f。这个局部函数f接受两个输入x和y,在前向传播期间,我们将应用局部函数来计算局部输出z。

现在,这是这个小独立节点的前向操作。在这个节点的前向操作运行之后,这个输出z将被传递给图的其他部分,并且可能被其他节点以任意复杂的方式重用。我们不知道,也不关心。从这个节点的角度来看,我们只知道我们计算了一个输出并将其传递给了别人。在过程结束时,最终在图末端的某个地方,有人会计算最终的损失L,然后反向传播开始。在我们之外的某个地方,梯度会传回给我们,超出了这个小节点的范围。最终,这个反向传播过程会到达我们关心的这个节点,这个节点将从图的上游接收一条消息,告诉我们损失相对于z的导数,即如果我们稍微改变节点的局部输出,这个可能离节点很远的损失会改变多少。这正是上游梯度dL/dz告诉我们的。

此时,我们可以计算节点内部的局部梯度,这些局部梯度告诉我们,对于节点的每个输出,节点的每个输入如何影响每个输出。然后,这个节点可以简单地通过将局部梯度和上游梯度相乘来计算下游梯度。然后,这些下游梯度被传递给图中更靠后的其他节点。同样,这个节点不需要知道或关心这些下游梯度将如何在图的其他地方被使用,它们只是被用在某个地方。当最终的反向传播过程终止时,我们将得到计算出的损失相对于图所有原始输入的梯度。我们能够计算这个全局属性,而无需真正推理我们试图计算的函数的全局结构。它只要求我们思考每个节点内部局部发生了什么,然后使用某种数据结构来跟踪所有这些节点是如何连接在一起的。

所以,希望这将比试图在纸上推导那些大的梯度表达式有巨大的改进。

这是另一个在计算图中运行的例子,这里看起来有点像逻辑分类器,如果你关心这些方程的话。但对于本讲座的目的来说,函数具体计算什么的细节有些无关紧要,我们只关心计算任意复杂函数的梯度。在左边,我们展示了一个接受五个输入的函数,我们的五个输入是w0x0w1x1w2。在前向传播中,我们将计算权重的前两个元素和x的前两个元素之间的内积,然后我们将计算这个偏置项加上w2,然后我们将计算某种e的负某次方。在这个计算图中,计算将像我们在上一个例子中看到的那样进行。在前向传播中,我们将通过评估每个节点的前向函数来计算图的输出,最终在右侧计算出这个最终的标量输出值。

然后在反向传播期间,我们将迭代地思考如何将上游梯度乘以图中每个节点的局部梯度来计算下游梯度。所以我们总是从这个基本情况开始:输出相对于自身的导数总是1。接下来,我们将看这个1/x,我们知道1/x的局部梯度是-1/x^2,这给了我们局部梯度,我们可以将它们相乘得到下游梯度。我们可以再次逐步进行,我们知道加上一个常数的局部梯度是1,所以我们可以轻松地将这些梯度向后传递。我们可以计算这个指数函数的局部梯度,这很简单,这让我们可以轻松地计算下游梯度。这种过程一步一步向后、向后、向后,每一步我们只是计算这些局部梯度,然后将上游和局部梯度相乘。

但这个特定计算图真正有趣的地方在于,我们本可以用多种方式来构建这个图中的计算结构。正如我所写的,我将其写成了非常基本、非常原始、非常基础的算术运算符:加法、乘法、指数、除法、加上常数。我已经将这种计算分解成了其最基础的算术原语片段。正如你通过观察这个图所注意到的,将所有东西分解成最基础的算术原语在每一层上最终会有点繁琐。有时,我们实际上有自由来定义我们希望在图中使用的原始操作类型。所以在这里,我们已经将所有东西分解成了基本原语,但我们也有自由定义任意新的节点类型,这些节点可以在内部计算更复杂的函数。

为什么这可能有用呢?举个例子,我用蓝色框出的这一小块图独立计算了这个所谓的sigmoid函数,即1 / (1 + e^{-参数})。这个sigmoid函数在机器学习中经常出现。我们在二元交叉熵的上下文中见过它,当你在做二类逻辑回归时。它也出现在许多其他上下文中。有点好处的是,作为这个小图语言的设计者,我们有自由选择对反向传播过程有用或易于计算的原始图元素。

特别是,我们可以选择分配给图中节点的原始函数,使得局部梯度易于计算。对于sigmoid函数来说就是这种情况。如果你在纸上进行一些数学运算,你可以看到(我不想在这里详细说明这个表达式),但如果你在纸上计算sigmoid函数相对于其输入的导数,你会发现sigmoid函数的局部梯度有一个非常简单的函数形式:sigmoid函数的局部梯度简单地等于sigmoid函数的输出乘以1减去sigmoid函数的输出。这意味着我们可以非常容易地计算整个蓝色图块的局部梯度,而无需存储这个中间的整个图块。这是我们作为图设计者,巧妙地选择我们希望在图中使用的原语,以便在反向传播期间更容易或更高效地计算导数的一个例子。这绝对是你应该考虑做的事情。

现在,如果我们想象一个聚合版本,你可以想象这个图的一个等效版本,它将整个蓝色框折叠成一个节点。然后,这个节点将在右侧接收上游梯度,然后使用我们在幻灯片底部推导出的表达式立即计算局部梯度,然后立即在左侧返回下游梯度,从而跳过框内的所有中间计算。所以,定义更复杂的原语用于我们的图中这个想法,我们通常会大量使用,以使我们的计算图更高效或具有更多的语义意义。

当我们看这些计算图时,我们可以开始注意到的另一件事是,存在一些模式变得明显。当你观察信息在前向传播期间如何向前传播,以及在反向传播期间如何向后传播的模式时,我有时会这样想:这就像一个小电路,在前向传播期间,信息从输入流向输出;然后在反向传播期间,信息从损失通过每个中间节点向后流向模型的原始参数,我们想为这些参数计算梯度。

当你有了这种计算图的电路解释时,你会开始注意到一些关于梯度流动的模式,以及前向和反向传播期间信息流动之间的一些对偶性。

最简单的例子是,这个加法门或加法函数在反向传播期间充当梯度分配器。如果我们有一个局部计算输出为其两个输入之和的小函数,也许这里的73+4,那么在反向传播期间,我们知道,正如我们在第一个计算图示例中看到的,x+y相对于x的导数是1,x+y相对于y的导数也是1。所以两个输入的局部梯度都是1。这意味着两个输入的下游梯度都等于上游梯度。这将推广到具有任意多项的和。这意味着在反向传播期间,当你有一个求和节点时,该节点将分配并复制上游梯度到下游梯度。这是关于模型中存在加法时会发生什么的一个很好的直觉。

与求和节点对偶的是复制节点。这是一个有点微不足道的节点,在其输入时,也许接收某个输入,然后有两个输出值,它们都是输入的相同副本。乍一看,这似乎是一个愚蠢的操作。为什么你会在图中引入这样的操作呢?如果你想在图中下游的多个地方使用模型的一个项,你可能想这样做。例如,你可能想象在正则化设置中,我们实际上希望以两种方式在模型中使用我们的每个权重矩阵:我们想使用权重矩阵一来计算模型主分支中的分数;其次,我们需要使用权重矩阵来计算我们的正则化项,如L2或L1正则化。为了在图中两个下游部分使用我们的权重矩阵,我们可能想象在图中某处插入一个复制节点,该节点现在生成权重矩阵的两个相同副本,现在可以在图的不同部分使用。重要的一点是,当我们产生这些副本时,因为它们可能以不同的方式被使用,我们最终可能会得到关于这两个副本的不同梯度。现在在反向传播期间,复制节点接收的上游梯度对于它产生的两个输出可能不同。但在反向传播期间,我们只需要将这两个梯度相加。这表明加法门的前向操作在某种程度上与复制门的反向操作相同,反之亦然。所以这两个操作在某种程度上是对偶的。

另一个有趣的事情是乘法门。你可以把它看作一种交换乘法器,因为你知道xy相对于x的导数是yxy相对于y的导数是x。这意味着一个输入的局部梯度是另一个输入,第二个输入的局部梯度是第一个输入。这意味着当我们计算下游梯度时,下游梯度等于上游梯度乘以另一个输入。如果你现在思考一下,如果你的模型中有一个乘法,它会以一种有趣的方式混合梯度,这有一个有趣的隐含意义。因为在反向传播期间,乘法门的反向传播也涉及乘法,你可以看到你最终会在反向传播中得到一些非常大的乘积,你可以想象在某些类型的模型中这可能是一个问题。

另一个你可能会经常看到的是最大值门。这里最大值门将接受两个标量输入并返回两个输入的最大值。这个函数看起来有点像ReLU函数。你可以想象,对于确实是最大值的输入,局部梯度是1;对于不是最大值的输入,局部梯度是0。那么这可以解释为:在反向传播期间,最大值门充当梯度路由器,它将获取上游梯度并将其路由到恰好是最大值的那个输入,而所有其他不是最大值的输入的下游梯度都将被设置为零。

那么你可以想象,如果我们有一个模型取很多很多事物的最大值,那么在反向传播期间,我们最终会得到一个大部分为零的梯度。所以你可以想象,也许这对于在整个模型中获得良好的梯度流动来说是一个问题,因此我们可能出于这个原因而不喜欢使用最大值。

所以这些显然都是微不足道的数学表达式,但思考这些标量函数的微不足道的导数如何对这些巨型神经网络模型中梯度流动的方式产生非平凡的影响,是很有趣的。

现在,希望我们已经对什么是反向传播以及它如何帮助我们自动化计算大型模型中的梯度过程有了一些直觉。我认为对你们来说,讨论如何在代码中实际实现这些东西是有帮助的,因为这是你们在作业中必须做的事情,希望你们能熟练掌握。

我认为至少有两种主要的方式来实现反向传播。第一种是我称之为扁平化实现的反向传播。这里的想法是,我们将编写一个单一的Python函数来计算整个计算图。也许这个Python函数正在计算一个线性分类器,它输入一个小批量数据和你的权重,然后计算该小批量数据上的损失。这听起来像作业2,希望看过作业的同学熟悉。现在要求你编写一个单一函数,既要计算损失,又要计算损失相对于每个权重矩阵的导数。

那么,你可以做的一件事是在纸上大干一场,做你的导数推导,希望最终能通过梯度检查。但你可以尝试以一种更简单的方式来构建这个计算,我认为这使得编写这个反向传播代码实际上非常简单。这里我们将以左边这个小计算图为例,这是几页幻灯片前的sigmoid例子。我们将输入两个权重w0w1,两个输入x0x1和我们的偏置项w2。我们代码的前向传播将简单地应用加法和乘法这些操作,并计算我们的损失L。然后,反向传播代码将紧接在前向传播代码之后。诀窍在于,反向传播代码看起来像是前向传播代码的反转版本

我这么说是什么意思呢?你可以看到,我们在反向传播中做的第一件事是计算这个微不足道的基本情况:输出相对于自身的梯度为1。在实际实现中,你可能应该省略这一行,但我想在这里让它超级具有教学意义。你可以看到,反向传播代码的第一行对应于计算图中最右边的东西。

反向传播代码的第二行对应于通过sigmoid函数反向传播。你可以看到,sigmoid函数的反向传播行对应于前向传播的最后一行。

我们注意到,在前向传播中,sigmoid函数以s3作为输入,并返回L作为输出。现在,反向实现中对应的行有点反转了:反向传播以grad_L作为输入,并产生grad_s3作为输出。所以你可以看到,前向传播中的这一行和反向传播中的这一行之间存在一一对应关系,并且这两行对应的输入和输出在某种程度上是互换的。

这种对应关系继续:在我们前向传播的倒数第二行,我们想将s2w2相加。因为这是一个有两个输入的操作,它在对应的反向代码中产生两行。再次,我们可以看到加法门作为梯度分配器的直觉,所以你可以看到我们只是将梯度分配或复制到这两个输入中。类似的事情发生在前向传播的倒数第三行。前向传播的倒数第四行也发生类似的事情,但这次我们有一个乘法门,我们有这个局部梯度乘法交换器的解释。最后,我们有这个最终输出,它做同样的事情,也是一个乘法。

现在这有点神奇,对吧?我们实际上在没有写下任何数学的情况下,写出了一个正确的反向传播实现。我们没有在纸上写下任何方程。我们所需要做的就是思考:我们编写了前向传播的代码,然后我们只是在脑海中转换我们为前向传播编写的代码,以生成反向传播的代码。如果你还没有完成作业2,这应该是你实际完成作业2的方式。当涉及到计算梯度时,这将使你的生活轻松得多。事实证明,一旦你在这方面有足够的练习,你几乎永远不需要在纸上做数学来编写梯度代码。你只需查看为前向传播编写的代码,然后使用你随着时间的推移积累的所有这些小局部规则来反转它。

这种通过反转前向传播代码来实现扁平化反向传播的想法,你应该在作业2中实践。例如,对于SVM,你可以想象我们可能会计算分数、计算边界、计算数据损失,然后在反向传播中,你只需以相反的顺序做所有这些事情,只是反向传播中每一行的确切操作将是这个局部计算,即上游梯度和局部梯度的乘法,以计算下游梯度。

你可以看到,你可以为SVM这样做,也可以为两层神经网络这样做。有趣的是我选择了这些例子。所以我强烈建议你熟悉这种通过转换前向传播代码来计算反向传播的方式。

当然,这种进行扁平化反向传播的机制在你只需要编写一个梯度函数时非常有用,但它有点不符合模块化测试。因为在这种扁平化反向传播的版本中,如果我们改变模型、改变激活函数、改变损失函数或改变正则化器,我们都必须重写代码。这将会有点痛苦和烦人。

所以有第二种方式,即更工业化的实现反向传播的方式,那就是使用更模块化的API。这与我们在节点周围看到的局部计算的想法非常吻合。在这种模块化的反向传播实现中,我们通常会定义某种计算图对象。这个计算图对象将能够通过对图的所有节点进行某种拓扑排序操作,然后在前向传播期间调用每个节点的前向操作,在反向传播期间调用每个节点对应的反向操作,从而在整个图中进行前向和反向传播。

我这里展示的这段代码只是伪代码,不是真正的代码,可能有拼写错误。但这个实际上是真实代码。在PyTorch中,你可以使用这个API定义你自己的函数。通过子类化torch.autograd.Function,你正在定义你自己的小计算节点,一个代表计算图中节点的小对象。

你可以看到,这个对象定义了两个函数:forwardbackwardforward接受三个输入,有趣的是xy,它们对应于该节点在前向传播期间将接收的输入值,这些将指定为torch张量。在这个例子中,我们只处理标量,所以这将是torch标量。我们还接收这个上下文对象,我们可以用它来存储我们想在反向传播中记住的任意信息。你可以看到,在前向传播中,我们只是定义输出z = x + y并返回z。这些都是你在前两个作业中熟悉的torch张量操作。现在在反向传播中,我们编写这个名为backward的函数,它接收来自前向传播的相同上下文对象,这样我们就可以使用该上下文对象弹出我们需要从前向传播中记住的任何东西,以便计算我们的导数。在这种情况下,我们需要记住xy。现在我们还接收grad_z,这也是存储在torch张量中的上游梯度。

在内部,我们局部计算局部梯度和上游梯度的乘积,以计算我们的下游梯度,即两个输入的导数,然后我们简单地返回它们。这就像真实的torch代码,你可以用它来实现你自己的两个标量的标量和。这实际上已经在torch中实现了,所以我不建议你实际使用这个实现。但如果出于某种原因,你想在torch中定义自己的任意函数,并定义新操作的前向和反向传播,这实际上是你在PyTorch中如何做到这一点。

然后,如果你深入研究PyTorch代码库的内部,基本上PyTorch就是这个autograd引擎和一大堆定义配对的前向和反向函数的这些小函数。我这里向你展示的只是像PyTorch GitHub仓库中的许多文件之一。如果我们放大其中一个文件,这实际上是sigmoid的众多实现之一,它实际上在PyTorch内部的某个深处。你可以看到,这里我们实际上定义了sigmoid的前向传播,使用像C++或C这样的语言,在PyTorch的深处。不幸的是,它调用了另一个在其他地方定义的函数,如果你真的看过PyTorch的后端,会有点意大利面条代码。

但我们可以忽略这一点,现在有第二个配对函数,即THNN_sigmoid_updateGradInput,它计算反向传播,并做一些样板代码,比如解包张量并检查输入。这是一个真正的工业化代码库,但现在有一行关键的代码,你可以看到,就是在这里,PyTorch实际上正在计算sigmoid层的反向传播,就像在C语言深处,嵌套在一些宏里面,有一些疯狂的事情发生。但基本上,PyTorch就是这些配对的前向和反向函数,然后可以将它们链接成这些大的计算图。

基本上,到目前为止,我们只讨论了使用标量进行反向传播和计算图的概念,如果你还记得单变量微积分的一切,这真的很容易和直观。但在实践中,我们经常需要处理向量值函数,或者操作向量、矩阵或任意维度张量的函数。所以我们也需要思考,在具有向量或张量值的计算图中进行反向传播意味着什么。

这里我们需要稍微回顾一下这些不同类型的多变量导数。你记得普通的单变量导数:给定一个标量输入和标量输出的函数,输出相对于输入的导数告诉我们这个局部线性近似:如果我们稍微改变输入,那么输出会改变多少。

我们有这个熟悉的梯度操作。梯度是一种导数类型,适用于我们的函数以向量作为输入并产生标量作为输出的情况。那么在这种情况下,梯度dy/dx是一个与输入大小相同的向量,其中梯度向量的每个元素表示,如果输入的相应元素改变一点点,输出会改变多少。所以它是这些经典单值导数的向量。现在推广到输入向量并输出可能不同维度的向量的函数。这些东西都有不同的名字,但它们基本上都是相同的概念,它们都是导数。这个被称为雅可比矩阵。雅可比矩阵是一个矩阵,它有n * m个元素,如果这些是我们的输入和输出的维度。雅可比矩阵的思想是,它对于输入的每个元素和输出的每个元素,说明改变输入的一个元素如何影响输出的那个元素。因为我们有n个输入元素和m个输出元素,我们需要n * m个标量值来表示输入对所有输出的所有可能影响。

好的,那么现在假设我们有相同的图景,我们知道我们真的不需要整体思考图。我们只需要一次思考一个节点的反向传播如何工作。那么在向量值的情况下进行反向传播意味着什么呢?再次放大一个节点。

现在我们的函数f输入两个向量值:X现在是维度为dx的向量,Y现在是具有dy个元素的向量,我们产生一个具有dz个元素的向量输出Z。这是我们的前向传播,事情很容易。最终我们将从上游接收这个梯度。现在这个上游梯度。在向量值的情况下,重要的是要记住,我们计算的损失始终是一个标量,无论我们处理的是向量、张量还是其他什么,我们在图末端计算的最终损失始终是一个标量。现在我们接收的这个上游梯度将是损失相对于我们输出的梯度。所以这将告诉我们,对于来自该节点的每个输出,如果我们稍微改变这些输出中的每一个,它们将如何影响图右侧、我们计算最末端的损失?

现在,这种情况下的局部梯度变成了这些雅可比矩阵,因为现在我们的函数是一个向量值函数,它接受两个向量作为输入,产生一个向量作为输出。所以现在我们的局部导数变成了这些雅可比矩阵,它们再次告诉我们,对于该节点的每个输出元素,改变该节点的每个输入元素会如何影响它。

现在在反向传播期间,我们想要产生的下游梯度始终是损失相对于输入的导数。现在损失相对于向量输入的导数现在将再次是一个与输入大小相同的向量。所以我们为x产生的下游梯度将是dL/dx,这是一个与X大小相同的向量。我们为y产生的下游梯度将是dL/dy,这是一个与y大小相同的向量。现在为了实际产生这些下游梯度,我们知道我们需要将局部梯度和上游梯度相乘。但现在我们处理的是向量,不再是标量乘法了。现在这变成了一个矩阵向量乘积,其中局部梯度现在是这个雅可比矩阵,上游梯度是这个梯度向量。下游梯度是梯度向量,我们产生这个下游梯度的方式是在上游梯度向量和局部雅可比矩阵之间进行矩阵向量乘法,使得形状匹配。如果你对此感到困惑,我总是建议写出所有东西的形状,希望这能帮助你澄清发生了什么。

现在,作为使用向量进行反向传播的具体例子,我们可以想象一下ReLU函数的情况。对于ReLU函数,请记住它是逐元素的最大值,我们将所有低于0的值裁剪为0。给定一个示例输入向量x = [1, -2, 3, -1],对这个向量应用ReLU函数会将所有负值替换为0,所以我们的输出y将是向量[1, 0, 3, 0]。现在这个向量值ReLU函数是我们图中某处嵌入的一个小计算节点。最终,我们将收到这个上游梯度,它告诉我们如果我们稍微改变来自ReLU函数的任何小输出,最终损失会改变多少。这些可以是任意值,正或负,我们不知道也不关心它们是如何计算的,它们只是由微分引擎传递给我们。

现在这个雅可比矩阵告诉我们,对于我们局部函数的每个输入,我们局部函数的每个输出如何变化?

我们可以开始注意到,这个逐元素ReLU函数的雅可比矩阵具有一些特殊的结构。因为这是一个逐元素函数,我们知道函数的第一个输出仅取决于第一个输入,第二个输出仅取决于第二个输入。特别是,第一个输入不影响第二个输出或第三个输出或第四个输出。每个输入只影响向量中相同位置的对应输出。这种结构在雅可比矩阵中看起来像什么?这意味着雅可比矩阵是对角矩阵。因为雅可比矩阵的非对角线元素告诉我们,也许输入的第i个元素如何影响输出的第j个元素,其中i和j不相等。所以对于这个逐元素函数,雅可比矩阵的所有非对角线元素都是0。现在对于对角线上的元素,这变成了这个标量值导数,它告诉我们ReLU作为输入变化的函数如何变化。那么对于正值输入,它们在对角线上有梯度1;对于负值输入,它们在对角线上有局部梯度或局部导数0。然后通过计算,它让我们为输入形成这个完整的雅可比矩阵。

现在,请记住,为了计算下游梯度,我们需要计算上游梯度向量和局部雅可比矩阵之间的这个矩阵向量乘法。你可以在线下计算这个东西,然后我们可以产生这个下游梯度向量,它将传递给向我们提供输入的其他节点。

现在我们开始意识到一些有趣的事情:在这种情况下,对于这个ReLU函数,我们看到雅可比矩阵是稀疏的,雅可比矩阵有很多零。这实际上是我们在深度学习中使用的绝大多数函数的常见情况。一般来说,我们使用的大多数局部雅可比矩阵都将非常、非常、非常稀疏。所以在实践中,我们几乎永远不会显式地形成这个雅可比矩阵,也几乎永远不会显式地执行雅可比矩阵和上游梯度之间的这个矩阵向量乘法。你可以想象,对于这个ReLU例子,为四个输入的向量形成雅可比矩阵可能没问题。但如果我们的输入是一个128个元素的小批量,每个元素都是一个4096维的向量呢?那么这个雅可比矩阵将超级、超级巨大,并且超级、超级稀疏,只有对角线非零。所以一般来说,显式地形成这些矩阵将是超级浪费的,并且显式地使用通用矩阵乘法函数执行该乘法将是超级低效的。所以实际上,反向传播中的大技巧是找到一种方法,以高效隐式的方式表达这些雅可比向量乘法。

对于ReLU的例子,这很简单,因为我们知道ReLU具有这种结构,其中对角线元素要么是1要么是0。所以这意味着对于ReLU,我们可以通过传递上游梯度或根据输入对应值的符号将上游梯度置零来裁剪它,从而计算下游梯度。你应该这样思考这个表达式:这是对这个大型稀疏局部雅可比矩阵和这个上游梯度向量进行隐式乘法的一种非常高效的实现。

清楚了吗?那么现在,这已经讨论了向量,但当然,我们需要处理秩大于1的张量,我们需要处理矩阵、三维张量、四维张量以及任意维度的东西。现在情况非常相似。要理解如何使用任意维度的矩阵或张量进行反向传播,我们有非常相似的图景:现在我们的局部函数(不小心删掉了f)输入两个值x和y,在这种情况下它们将是矩阵。x将是大小为dx * Mx的矩阵,y将是大小为dy * My的矩阵,输出也将是一个矩阵。

现在记住损失仍然是一个标量,并且损失相对于某物的所有梯度将始终是一个与该物形状相同的张量,它将告诉我们,当我们改变该张量的每个独立元素时,最终的下游损失如何变化。

那么当我们最终收到这个

07:卷积神经网络

在本节课中,我们将学习卷积神经网络,这是我们将用于处理图像的主要模型类别。我们将从回顾计算图和反向传播开始,然后深入探讨卷积层、池化层和归一化层的核心概念,了解它们如何共同构建出能够有效处理图像空间结构的网络。


概述

上一节我们介绍了反向传播算法,它使我们能够在任意复杂的计算图中计算梯度。本节中,我们将利用这一机制,引入能够处理图像空间结构的新操作符,从而构建卷积神经网络。我们将重点学习卷积层、池化层和归一化层,它们是构成现代CNN的基础模块。


从全连接层到卷积层

到目前为止,我们讨论的线性分类器和全连接神经网络在处理图像时存在一个问题:它们会破坏输入图像的二维空间结构。具体来说,我们需要将图像展平成一个长向量,才能输入到这些模型中。为了构建能够利用图像空间结构的神经网络,我们需要定义一些能够直接在空间结构化数据上操作的新操作符。

全连接层的回顾

在全连接网络中,核心组件是全连接层和非线性激活函数(如ReLU)。全连接层接收一个输入向量(例如,展平后的CIFAR-10图像,大小为3072),并通过矩阵乘法将其转换为一个输出向量。这个过程完全忽略了输入数据的原始空间布局。

卷积层的引入

卷积层旨在解决这个问题。与全连接层不同,卷积层直接接收一个三维张量作为输入,例如一个CIFAR-10图像的形状为 3 x 32 x 32(通道 x 高度 x 宽度)。卷积层通过一组称为“滤波器”或“卷积核”的可学习权重来操作,这些权重本身也是三维的。

核心概念:卷积操作可以理解为将一个小的三维滤波器在输入张量的所有空间位置上滑动,并在每个位置计算滤波器和输入局部区域的内积(点积)。

公式:对于一个输入位置,其输出值计算如下(忽略偏置):
output[x, y] = sum_{i, j, c} (input[x+i, y+j, c] * filter[i, j, c])
其中,(i, j) 遍历滤波器的空间维度,c 遍历通道维度。

卷积层的输出

  • 单个滤波器:将一个形状为 C_in x K x K 的滤波器与输入(C_in x H x W)进行卷积,会生成一个二维的“激活图”(1 x H' x W'),表示输入图像在不同位置对该滤波器的响应程度。
  • 多个滤波器:一个卷积层通常包含多个滤波器(例如 C_out 个)。每个滤波器独立地与输入卷积,产生一个激活图。将所有 C_out 个激活图堆叠起来,就得到了卷积层的输出:一个新的三维张量,形状为 C_out x H' x W'
  • 偏置:每个滤波器通常还有一个对应的偏置项,会在计算完内积后加到每个输出元素上。

因此,卷积层将一个三维输入张量转换为另一个三维输出张量,同时保留了数据的空间结构。输出可以理解为:

  1. 一组特征图:每个图对应一个滤波器在整个输入上的响应。
  2. 一个特征向量网格:在输出的每个空间位置(H' x W'),都有一个 C_out 维的特征向量,描述了输入在该局部区域的外观。

批处理

在实践中,我们通常处理一批图像。因此,卷积层的输入是一个四维张量,形状为 N x C_in x H x W(批量大小 x 通道 x 高度 x 宽度)。输出相应地是 N x C_out x H' x W',卷积操作独立地应用于批次中的每个样本。


卷积层的超参数与空间尺寸计算

卷积层有几个关键的超参数,它们决定了输出的空间尺寸。

以下是影响输出尺寸的主要参数:

  1. 输入尺寸 (W):输入的空间宽度(假设为正方形)。
  2. 滤波器尺寸 (K):卷积核的空间大小(例如3x3)。
  3. 填充 (P):在输入边框周围添加的零值像素数。
  4. 步幅 (S):滤波器每次滑动的像素数。

输出尺寸计算公式
W_out = floor((W - K + 2P) / S) + 1
其中 floor 表示向下取整。

填充的作用

如果不使用填充(P=0),每次卷积操作都会使特征图的空间尺寸缩小(W_out = W - K + 1)。这限制了网络的深度。为了解决这个问题,我们引入填充

  • 零填充:最常见的做法是在输入边框周围填充零。
  • 相同填充:一种常见的设置是使 P = (K - 1) / 2(当K为奇数时)。这样,在步幅S=1的情况下,输出尺寸将与输入尺寸相同(W_out = W)。这简化了网络的设计。

步幅的作用

步幅S控制滤波器滑动的间隔。S=1是默认值,滤波器遍历每个位置。S>1会使输出在空间上下采样,尺寸减小。

  • 下采样:使用步幅大于1的卷积层可以快速减小特征图尺寸,并有效增加后续层在原始输入上的感受野(即输出中一个点所能“看到”的输入区域的大小)。
  • 感受野:单个卷积层的感受野等于其滤波器尺寸K。当堆叠多个卷积层时,深层神经元的感受野会线性增长(对于S=1的3x3卷积,堆叠L层,感受野约为 2L + 1)。使用步幅卷积可以更快地增大感受野。

1x1 卷积

滤波器尺寸为1x1的卷积是一种特殊情况。它不聚合空间信息,而是在每个空间位置上独立地跨通道进行线性组合(相当于一个微型的全连接层)。它常用于调整通道数(C_in -> C_out)或构建“网络中的网络”结构。


池化层

池化层是卷积网络中另一种用于空间下采样的操作,它没有可学习的参数

最大池化

最常见的池化类型是最大池化。例如,一个2x2、步幅为2的最大池化层,会将输入划分为不重叠的2x2区域,并输出每个区域中的最大值。

作用

  1. 降维:减少特征图的空间尺寸,从而减少计算量和参数。
  2. 平移不变性:由于取的是局部区域的最大值,输入特征发生微小平移时,输出可能保持不变,这为网络提供了一定程度的平移鲁棒性。

平均池化

另一种选择是平均池化,它输出每个局部区域的平均值。

池化层总结

池化层与卷积层类似,有核尺寸和步幅等超参数,但它应用的是固定的池化函数(如最大或平均),而不是可学习的卷积运算。


构建经典的卷积网络

结合卷积层、激活函数(ReLU)和池化层,我们可以构建经典的卷积网络架构。

一个非常经典的模式是重复的 Conv -> ReLU -> Pool 块,最后连接一个或多个全连接层用于分类。

示例:LeNet-5 (1998)
这是一个早期成功的CNN,用于手写数字识别:

  1. 输入:1x28x28(灰度图)
  2. Conv1: 20个5x5滤波器 -> ReLU -> 输出:20x24x24
  3. Pool1: 2x2最大池化,步幅2 -> 输出:20x12x12
  4. Conv2: 50个5x5滤波器 -> ReLU -> 输出:50x8x8
  5. Pool2: 2x2最大池化,步幅2 -> 输出:50x4x4
  6. Flatten: 将50x4x4展平为800维向量
  7. FC1: 全连接层,500个神经元 -> ReLU
  8. FC2: 全连接层,10个神经元(对应10个数字类别)

观察:在网络前向传播过程中,特征图的空间尺寸逐渐减小(通过池化或步幅卷积),而通道数(深度)逐渐增加。这是一种常见的“空间压缩,特征扩展”的设计模式。


归一化层

随着网络加深,训练变得困难。归一化层(如批归一化)被引入以稳定和加速深度网络的训练。

批归一化

核心思想:对每一层的输入进行归一化,使其均值为0,方差为1。这有助于缓解“内部协变量偏移”问题(即网络中间层输入分布随着训练而发生变化的问题)。

操作步骤(对于全连接层)
对于一个批次的输入 x(形状 N x D):

  1. 计算批次均值:mu = mean(x, dim=0) (形状 1 x D
  2. 计算批次方差:sigma^2 = var(x, dim=0) (形状 1 x D
  3. 归一化:x_hat = (x - mu) / sqrt(sigma^2 + epsilon)
  4. 缩放与偏移:y = gamma * x_hat + beta
    其中 gammabeta 是可学习的参数(形状 1 x D),epsilon 是一个小的常数用于数值稳定性。

训练与测试时的差异

  • 训练时:使用当前小批次的统计数据(mu, sigma)进行归一化。
  • 测试时:使用在训练过程中通过移动平均计算得到的全局统计数据(固定的 mu_running, sigma_running)进行归一化。这使得测试时每个样本的预测独立于批次中的其他样本,并且BN层在测试时可以与前一个线性层融合,实现零计算开销。

优点

  • 允许使用更高的学习率。
  • 对初始化不那么敏感。
  • 在一定程度上起到正则化的作用。

缺点

  • 理论解释不清晰。
  • 训练/测试行为不一致可能引入bug。
  • 在小批量上计算统计数据,其效果受批量大小影响。

其他归一化变体

为了克服批归一化的某些缺点,提出了其他归一化方法:

  • 层归一化:沿特征维度(D)进行归一化,不依赖批次维度。常用于循环神经网络和Transformer。
  • 实例归一化:对每个样本的每个通道单独进行归一化(在空间维度上求均值和方差)。常用于风格迁移等任务。
  • 组归一化:将通道分组,并在每组内进行归一化。在批量较小时(如目标检测)表现良好。

这些方法的不同之处在于对输入四维张量(N, C, H, W)的哪些维度进行归一化统计量的计算。


总结

本节课我们一起学习了卷积神经网络的核心构建模块。

  1. 卷积层:使用可学习的滤波器在输入图像上滑动计算,提取局部特征,并保持空间结构。
  2. 池化层(如最大池化):进行空间下采样,提供一定的平移不变性,且无参数。
  3. 归一化层(如批归一化):对层间激活进行标准化,极大地促进了深度网络的训练稳定性和速度。

我们已经掌握了构建CNN的“原材料”。然而,如何有效地组合这些层以构建强大且高效的网络架构,本身是一门需要深入研究的学问。这将是下一节课的重点,我们将回顾卷积神经网络架构的发展历史与设计原则。

08:CNN架构演进 🏛️

在本节课中,我们将学习卷积神经网络(CNN)架构的历史演进。我们将从2012年具有里程碑意义的AlexNet开始,逐步了解后续几年中出现的各种重要架构,如VGG、GoogLeNet、ResNet等。通过分析这些架构的设计原则、计算复杂度和性能表现,我们将理解如何构建高效且强大的CNN模型。


从AlexNet到ZFNet:更大网络的趋势

上一讲我们介绍了CNN的基本构建模块。本节中,我们来看看如何将这些模块组合成高性能的网络。一个很好的切入点是通过ImageNet图像分类挑战赛来观察CNN架构的演变。

在2012年,AlexNet架构在ImageNet挑战赛中取得了突破性胜利,首次将CNN带入计算机视觉研究的主流视野。

AlexNet架构详解

AlexNet是一个深度卷积神经网络。它接受227x227像素的RGB图像输入,包含5个卷积层、3个全连接层,并全程使用ReLU激活函数和最大池化。

以下是AlexNet第一层卷积的计算示例:

  • 输入: 227x227x3
  • 卷积层参数: 64个滤波器,核大小 11x11,步长 4,填充 4
  • 输出空间尺寸计算公式: (输入尺寸 - 核尺寸 + 2*填充) / 步长 + 1
  • 输出尺寸: (227 - 11 + 2*4) / 4 + 1 = 56,因此输出为 56x56x64
  • 参数量: 输出通道数 x 输入通道数 x 核高 x 核宽 = 64 x 3 x 11 x 11 ≈ 23k
  • 计算量(FLOPs): 约 73 MFLOPs(一次乘加计为1次浮点运算)

AlexNet的设计很大程度上依赖于大量的试错。然而,分析其内存、参数量和计算量的分布,可以发现一些普遍趋势:

  • 内存消耗:大部分内存用于存储早期卷积层的高分辨率激活值。
  • 参数量:绝大多数可学习参数集中在最后的全连接层。
  • 计算量:主要的计算开销来自卷积层,尤其是那些在高空间分辨率上使用大量滤波器的层。

2013年的冠军网络ZFNet本质上是AlexNet的“更大”版本,通过调整层配置(如将第一层卷积步长从4改为2,增加后续层的滤波器数量)来提升性能,这进一步印证了“更大网络往往表现更好”的早期趋势。


VGGNet:引入模块化设计原则

上一节我们看到早期网络设计缺乏统一原则。本节中,我们来看看VGGNet如何通过引入清晰的设计规则来改变这一状况。

VGGNet遵循几个简洁的设计原则,使其易于理解和扩展:

  1. 所有卷积层使用 3x3 卷积核,步长为 1
  2. 所有池化层使用 2x2 最大池化,步长为 2
  3. 每次池化后,将通道数翻倍。
  4. 网络由多个重复的“卷积阶段”构成,每个阶段包含若干卷积层和一个池化层。

为什么使用 3x3 卷积?

使用小卷积核堆叠替代大卷积核是VGG的核心洞察。例如:

  • 一个 5x5 卷积层的参数量为 25 * C^2,计算量约为 25 * C^2 * H * W
  • 两个堆叠的 3x3 卷积层具有等效的 5x5 感受野,但其参数量仅为 18 * C^2,计算量约为 18 * C^2 * H * W

因此,堆叠小卷积核可以用更少的参数和计算量实现相同的感受野,同时还能在层间插入更多的非线性激活函数(ReLU),从而增加模型的表达能力。

VGG16(16层)和VGG19(19层)是其中最著名的变体。与AlexNet相比,VGG网络在参数量、内存消耗和计算量上都大幅增加,但性能也显著提升,这得益于其更深、更规则的结构。


GoogLeNet与ResNet:追求效率与深度

上一节我们看到了通过规则设计加深网络的方法。本节中,我们来看看2014年及以后出现的、更关注计算效率和训练极深网络的架构。

GoogLeNet:专注于效率

GoogLeNet的设计目标是在保持高性能的同时,极大降低计算成本和参数量。其主要创新点包括:

  1. 轻量级起始模块(Stem):在网络的开始几层就快速对输入进行下采样,避免在后续高分辨率特征图上进行昂贵的卷积运算。
  2. Inception模块:采用并行分支结构,在同一层中同时使用 1x13x35x5卷积和池化,让网络自行学习最佳组合。同时,使用 1x1 卷积在昂贵的大核卷积之前减少通道数(称为“瓶颈”层),以降低计算量。
  3. 全局平均池化(Global Average Pooling):在网络末端,使用全局平均池化替代传统的全连接层,将每个通道的空间信息压缩为一个标量。这极大地减少了参数量(例如,VGG16最后三个全连接层有约1.2亿参数,而GoogLeNet的全局平均池化后仅需一个约100万参数的全连接层)。

ResNet:残差网络与极深网络训练

随着批归一化(Batch Normalization)的出现,训练更深的网络成为可能。但人们发现,简单地堆叠更多层数超过一定深度后,性能反而会下降(训练误差和测试误差都升高),这并非过拟合,而是优化困难。

ResNet的核心思想是引入残差连接(Residual Connection),使网络更容易学习恒等映射。其基本构建块是残差块(Residual Block)

# 残差块前向计算示意
output = F(x) + x
# 其中 F(x) 是两层或多层卷积、BN、ReLU等操作

如果 F(x) 的权重被学习为零,则该块直接输出 x(恒等映射)。这使得极深的网络能够轻松地模拟较浅网络的行为,并改善了梯度在反向传播中的流动。

ResNet同样借鉴了VGG的模块化思想和GoogLeNet的全局平均池化。通过使用基础块(两个3x3卷积) 和更深的瓶颈块(1x1 -> 3x3 -> 1x1结构),ResNet可以扩展到50、101甚至152层,并在ImageNet等多个竞赛中取得统治性成绩。


后续发展:架构搜索与移动端网络

上一节我们看到了通过残差连接训练极深网络的方法。本节中,我们快速浏览一下ResNet之后的一些重要发展方向。

ResNeXt与架构演进

ResNeXt在残差块中引入了并行路径(或分组卷积) 的概念。它没有增加单个路径的复杂度,而是通过增加路径数量(“基数”)来提升模型能力,同时保持总体计算量不变。这为网络设计提供了深度、宽度之外的另一个可扩展维度。

移动端高效网络(如MobileNet)

当应用场景转向手机或嵌入式设备时,目标变为在可接受的精度损失下,追求极致的计算效率和模型小型化。MobileNet等网络使用深度可分离卷积(Depthwise Separable Convolution) 等技巧,将标准卷积分解为深度卷积和逐点卷积,大幅减少了计算量和参数量。

神经架构搜索(NAS)

手动设计网络架构需要大量专业知识和试错。神经架构搜索旨在自动化这一过程:使用一个控制器网络(Controller Network)来生成子网络(Child Network)的架构,然后根据子网络在任务上的表现来更新控制器。尽管早期方法(如使用800块GPU训练28天)计算代价极高,但后续研究已大幅提升了搜索效率,并且NAS发现的网络在精度-计算量权衡上往往优于人工设计的网络。


总结与实践建议 🎯

本节课我们一起学习了卷积神经网络架构的演进历史:

  1. 早期(AlexNet, ZFNet):通过试错设计,趋势是构建更大的网络。
  2. 模块化设计(VGG):引入 3x3 卷积堆叠、阶段式设计等规则,使网络设计更系统化。
  3. 效率与深度(GoogLeNet, ResNet):GoogLeNet通过Inception模块和全局平均池化关注效率;ResNet通过残差连接解决了极深网络的训练难题,成为长期基准。
  4. 后续发展:包括增加并行路径的ResNeXt、面向移动端的轻量级网络(MobileNet)以及自动设计网络的神经架构搜索(NAS)。

给实践者的建议:对于大多数应用,不要试图从头设计自己的网络架构。应该利用现有的、经过充分验证的架构。ResNet-50/101仍然是强大且可靠的选择。如果对计算资源有严格限制,可以考虑MobileNet或ShuffleNet等高效架构。

09:硬件与软件

在本节课中,我们将学习深度学习背后的硬件与软件系统。我们将探讨不同类型的计算设备(如CPU、GPU、TPU)及其在深度学习中的角色,并深入了解主流深度学习框架(如PyTorch和TensorFlow)的核心概念与使用方法。课程内容将包含大量代码示例,帮助你理解如何在实际项目中应用这些工具。

硬件概览

首先,我们来了解深度学习常用的硬件设备。计算机内部有多种组件,其中最重要的是中央处理器(CPU)和图形处理器(GPU)。从物理尺寸上看,GPU通常比CPU占用更多空间,这暗示了其在计算密集型任务中的重要性。

在深度学习领域,NVIDIA的GPU占据主导地位。虽然AMD也生产优秀的硬件,但其在通用计算和深度学习方面的软件生态尚不及NVIDIA成熟。因此,当提到深度学习中的GPU时,通常指的是NVIDIA GPU。

计算成本趋势

观察CPU和GPU的计算能力随时间变化的趋势很有启发。我们以“每美元千兆浮点运算次数”(GigaFLOPS per dollar)作为衡量标准。自2006年NVIDIA推出支持CUDA通用计算框架的GeForce GTX 8800 GPU以来,GPU的计算成本开始显著下降。

到2012年,用于训练AlexNet的GTX 580 GPU出现时,在GPU上进行大规模计算已比在CPU上便宜得多。这解释了为何AlexNet能比之前的卷积神经网络模型规模更大。从2012年至今,GPU的计算能力持续快速提升,成本不断下降,这为深度学习模型的爆炸式增长提供了关键的廉价算力支持。

CPU与GPU对比

让我们具体比较当前顶级的消费级CPU和GPU。

  • CPU示例:AMD Ryzen 9 3950X(16核,基础频率3.5 GHz,依赖系统内存,售价约750美元)。其峰值计算能力约为 4.8 TeraFLOPS
  • GPU示例:NVIDIA Titan RTX(4608个CUDA核心,频率1.35 GHz,集成24GB专用显存,售价更高)。其32位浮点运算峰值约为 16.3 TeraFLOPS

两者的核心区别在于设计哲学:

  • CPU:核心数量较少,但每个核心非常强大、快速,拥有更好的分支预测和缓存策略。
  • GPU:拥有大量相对简单的核心,运行频率较低,但通过并行处理海量简单任务来实现极高的总体吞吐量。

GPU内部架构

以NVIDIA Titan RTX GPU为例,其内部像一个独立的微型计算机:

  • 拥有自己的散热风扇和显存模块(如6个4GB模块构成24GB显存)。
  • 核心处理器包含一个由众多重复计算单元组成的网格。

GPU的核心计算单元是流式多处理器(SM)。Titan RTX包含72个SM。每个SM内部又包含:

  • 64个32位浮点核心(FP32 Cores):执行标准浮点运算。每个核心每个时钟周期可完成一次乘加运算(计为2次浮点操作)。
  • 张量核心(Tensor Cores):专门为深度学习设计的硬件单元。

张量核心

张量核心是NVIDIA为优化深度学习计算引入的专用硬件。它能在一个时钟周期内完成一个4x4矩阵乘加运算(C = A * B + C)。这相当于128次浮点运算。

为了实现如此高的计算密度,张量核心采用混合精度计算:乘法使用16位浮点数(FP16),累加使用32位浮点数(FP32)。这种设计在保持数值稳定性的同时,大幅提升了计算效率和能效。

考虑张量核心后,Titan RTX的混合精度计算峰值可达 130 TeraFLOPS,远高于仅使用FP32核心时的算力。在PyTorch中,只需将输入数据类型转换为16位(如torch.float16),并配备正确的硬件和驱动,框架便会自动利用张量核心加速计算。

为何GPU擅长矩阵乘法?

矩阵乘法是GPU(尤其是张量核心)加速的典型例子。输出矩阵的每个元素都是输入矩阵行和列的内积,这些计算彼此完全独立,可以完美地并行化分配到GPU的数千个计算核心上。相反,传统的单核CPU必须顺序计算每个输出元素。

此外,矩阵乘法可以分解为多个4x4的小矩阵乘法,从而高效映射到张量核心上执行。这也解释了为什么神经网络中层的尺寸(如批量大小、通道数)常设置为2的幂次方,以更好地适配硬件,避免计算浪费。

编程与扩展

GPU使用CUDA语言进行编程。CUDA是C/C++的扩展,允许开发者编写直接在GPU上运行的代码。虽然CUDA编程很有趣且能提供极致优化,但对于大多数深度学习实践者来说并非必需,因为像PyTorch这样的框架已经集成了由NVIDIA高度优化的基础运算例程(如矩阵乘法和卷积)。

超越单GPU:分布式计算与TPU

在实际应用中,人们常常使用多GPU服务器(如8 GPU服务器)甚至多个服务器集群来分布式训练大型模型,形成多层次并行计算 hierarchy。

近年来,Google推出了专为深度学习设计的硬件——张量处理单元(TPU)。Cloud TPU v2提供约180 TeraFLOPS的计算能力,与高端GPU相当。用户可以通过Google Cloud租用,或在Colab中免费使用。

TPU的真正优势体现在TPU Pod中。TPU Pod将大量TPU芯片集成在机架级解决方案中,例如TPU v3 Pod包含256个TPU v3设备,提供超过100 PetaFLOPS的总计算能力。不过,使用TPU通常需要依赖Google的TensorFlow框架。

内存差异

消费级GPU(为游戏设计)与计算级GPU(为深度学习设计)的主要区别在于显存容量显存类型

  • 容量:训练深度学习模型需要在显存中存储前向传播的激活值以供反向传播使用,因此需要大容量显存。
  • 类型:计算级GPU常使用高带宽内存(HBM),其数据吞吐速度远高于消费级GPU使用的GDDR6内存。对于像ReLU这样计算量小但数据移动频繁的操作,内存带宽往往是性能瓶颈。

深度学习软件

深度学习领域涌现过许多框架。早期有Caffe、Torch、Theano等学术框架,如今主流框架多由大型科技公司开发和维护,如Facebook的PyTorch/Caffe2、Google的TensorFlow、Amazon支持的MXNet等。目前,PyTorchTensorFlow是两大主流框架。

一个优秀的深度学习框架应具备三个核心特性:

  1. 快速原型设计:提供丰富的预构建层和工具。
  2. 自动梯度计算:基于计算图抽象,自动进行反向传播。
  3. 无缝硬件支持:轻松在GPU、TPU等设备上运行。

PyTorch核心抽象

PyTorch提供了三个不同层次的抽象来构建神经网络模型:

  1. 张量(Tensor):基础的多维数组,类似于NumPy,但可在GPU上运行。这是最低层次的抽象。
  2. 自动求导(Autograd):自动构建计算图并计算梯度。
  3. 模块(Module):面向对象的神经网络层,内部可存储可学习参数。

在前三次作业中,我们仅使用了张量级别的接口。从第四次作业开始,将使用更高级的抽象。

使用张量进行训练(低级抽象)

以下代码展示了仅使用张量操作训练一个两层全连接网络:

# 初始化(设备可设置为‘cuda’以使用GPU)
device = ‘cpu‘
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in, device=device)
y = torch.randn(N, D_out, device=device)
w1 = torch.randn(D_in, H, device=device)
w2 = torch.randn(H, D_out, device=device)

learning_rate = 1e-6
for t in range(500):
    # 前向传播:计算预测值y_pred
    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)
    # 使用梯度下降更新权重
    w1 -= learning_rate * grad_w1
    w2 -= learning_rate * grad_w2

使用Autograd进行训练(中级抽象)

通过设置requires_grad=True,PyTorch会自动跟踪张量操作并构建计算图:

# 初始化,对需要梯度的权重设置 requires_grad=True
w1 = torch.randn(D_in, H, device=device, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, requires_grad=True)

for t in range(500):
    # 前向传播:操作会记录在计算图中
    y_pred = x.mm(w1).clamp(min=0).mm(w2)
    loss = (y_pred - y).pow(2).sum()
    # 使用autograd计算梯度。这会计算loss对requires_grad=True的所有张量的梯度
    loss.backward()
    # 手动更新权重,不需要梯度跟踪
    with torch.no_grad():
        w1 -= learning_rate * w1.grad
        w2 -= learning_rate * w2.grad
        # 更新后必须手动将梯度置零
        w1.grad.zero_()
        w2.grad.zero_()

关键点

  • loss.backward()会遍历计算图,计算叶子节点(如w1, w2)的梯度,并存储在.grad属性中。
  • 梯度是累积的,因此每次迭代后必须调用.zero_()将梯度清零。
  • 权重更新等不应在计算图中的操作,需放在torch.no_grad()上下文管理器中。

定义新的Autograd函数

对于像Sigmoid这样有稳定梯度公式的函数,可以定义自定义的Autograd函数,使其在计算图中成为单个节点:

class Sigmoid(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x):
        y = 1 / (1 + torch.exp(-x))
        ctx.save_for_backward(y) # 保存供反向传播使用
        return y
    @staticmethod
    def backward(ctx, grad_y):
        y, = ctx.saved_tensors
        grad_x = grad_y * y * (1 - y) # Sigmoid的导数公式
        return grad_x

# 使用
sigmoid = Sigmoid.apply
y_pred = sigmoid(x.mm(w1)).mm(w2)

使用nn.Module进行训练(高级抽象)

torch.nn提供了面向对象的API来构建模型:

# 使用 Sequential 容器
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.Adam(model.parameters(), lr=learning_rate)

for t in range(500):
    y_pred = model(x) # 前向传播
    loss = loss_fn(y_pred, y)
    optimizer.zero_grad() # 清零梯度
    loss.backward() # 反向传播
    optimizer.step() # 更新参数

自定义nn.Module

通过子类化nn.Module可以构建更复杂的模型结构:

class TwoLayerNet(torch.nn.Module):
    def __init__(self, D_in, H, D_out):
        super(TwoLayerNet, self).__init__()
        self.linear1 = torch.nn.Linear(D_in, H)
        self.linear2 = torch.nn.Linear(H, D_out)
    def forward(self, x):
        h_relu = self.linear1(x).clamp(min=0)
        y_pred = self.linear2(h_relu)
        return y_pred

model = TwoLayerNet(D_in, H, D_out)
# ... 训练循环与上述类似

动态计算图 vs 静态计算图

这是深度学习框架的一个核心设计差异。

  • 动态计算图(PyTorch默认):每次前向传播都会实时构建一个新的计算图,并在反向传播后丢弃。优点:
    • 易于调试:代码执行流程直观,错误信息清晰。
    • 灵活性高:可以使用标准的Python控制流(如if、for循环)动态改变计算图结构,适用于RNN、递归网络等模型。
  • 静态计算图(TensorFlow 1.x默认):先定义完整的计算图,然后多次执行。优点:
    • 性能优化:可以对计算图进行全局优化(如算子融合)。
    • 易于部署:可将计算图序列化并部署到不含Python环境的生产系统(如移动端、C++服务器)。

PyTorch也通过TorchScript提供了静态图支持:

@torch.jit.script
def model(x, w1, w2):
    return x.mm(w1).clamp(min=0).mm(w2)
# 编译成静态图
graph = torch.jit.script(model)

TensorFlow简介

TensorFlow也在演进。TensorFlow 1.x主要使用静态图,编码和调试较为复杂。TensorFlow 2.0 默认采用了类似PyTorch的动态计算图(Eager Execution)模式,并保留了转换为静态图的能力。

TensorFlow 2.0动态图示例:

import tensorflow as tf

w1 = tf.Variable(tf.random.normal((D_in, H)))
w2 = tf.Variable(tf.random.normal((H, D_out)))
optimizer = tf.optimizers.Adam(learning_rate)

for t in range(500):
    with tf.GradientTape() as tape:
        y_pred = tf.matmul(tf.matmul(x, w1).numpy().clip(min=0), w2)
        loss = tf.reduce_sum((y_pred - y) ** 2)
    grads = tape.gradient(loss, [w1, w2])
    optimizer.apply_gradients(zip(grads, [w1, w2]))

TensorFlow 2.0也提供了高级API Keras,其风格与PyTorch的nn.Module类似。

TensorBoard可视化

TensorFlow的TensorBoard是一个强大的可视化工具,用于跟踪训练过程中的损失、准确率、权重分布等指标。它深受欢迎,以至于PyTorch也提供了接口将数据导出到TensorBoard。

总结

本节课我们一起学习了深度学习的硬件与软件基础。

在硬件方面,我们探讨了:

  • CPU:强于复杂串行任务。
  • GPU:凭借海量并行核心和张量核心,成为深度学习训练的主力。
  • TPU:Google专为深度学习设计的专用硬件,擅长大规模分布式训练。

在软件方面,我们深入了解了:

  • PyTorch:以动态计算图为主,灵活、易于调试,深受研究人员喜爱。
  • TensorFlow:早期以静态图著称,TensorFlow 2.0转向动态图优先,并拥有强大的生产部署工具链(如TensorBoard)。

理解这些硬件特性和软件抽象,将帮助你更高效地设计、实现和优化深度学习模型。

10:训练神经网络(第一部分) 🧠

在本节课中,我们将学习训练神经网络时所需的一系列实用技巧和细节。我们将从激活函数的选择开始,探讨数据预处理、权重初始化以及正则化策略。这些知识对于高效、稳定地训练深度网络至关重要。


激活函数的选择 ⚙️

上一节我们介绍了神经网络的硬件和软件基础。本节中,我们来看看构建网络时一个关键的模块:激活函数。激活函数为神经网络引入了非线性,使其能够学习复杂的模式。然而,不同的激活函数各有优缺点。

以下是几种常见激活函数的分析:

  • Sigmoid函数σ(x) = 1 / (1 + e^{-x})
    • 优点:输出在0到1之间,可解释为概率。
    • 缺点
      1. 梯度消失:当输入值非常大或非常小时,函数进入饱和区,梯度接近零,导致学习停滞。
      2. 输出非零中心化:所有输出均为正,可能导致权重更新方向单一,使优化过程低效、呈“之”字形。
      3. 计算成本高:涉及指数运算,在CPU上计算较慢。

  • Tanh函数tanh(x)

    • 优点:输出是零中心化的(范围在-1到1之间)。
    • 缺点:与Sigmoid类似,在饱和区也存在梯度消失问题。
  • ReLU函数f(x) = max(0, x)

    • 优点
      1. 计算高效:只需简单的阈值判断。
      2. 在正区间不饱和:梯度恒定,缓解了梯度消失问题。
      3. 实践中收敛快
    • 缺点
      1. 输出非零中心化
      2. “死亡ReLU”问题:如果某个神经元对所有训练数据的输出均为负,其梯度将恒为零,该神经元将永久失效。
  • Leaky ReLUf(x) = max(αx, x),其中α是一个小的正常数(如0.01)。

    • 优点:解决了“死亡ReLU”问题,在负区间也有一个小梯度。
    • 缺点:引入了超参数α。

  • Parametric ReLU (PReLU):与Leaky ReLU类似,但斜率α是可学习的参数。

  • ELU (Exponential Linear Unit):在负区间使用指数函数,试图使输出更接近零中心化,且平滑。

    • 优点:可能缓解梯度消失问题。
    • 缺点:计算涉及指数运算,并引入超参数。
  • SELU (Scaled Exponential Linear Unit):ELU的一个特定缩放版本,具有自归一化特性,有助于训练极深的网络。

核心建议:对于大多数情况,使用ReLU即可。它简单、高效,且在实践中效果良好。如果追求极致性能,可以尝试Leaky ReLU、ELU等变体,但性能提升通常有限(约1-2%)。避免使用Sigmoid和Tanh,因为它们会导致严重的梯度消失问题。


数据预处理 📊

在将数据输入网络之前,进行预处理可以优化训练过程。其核心思想是将数据调整到更利于模型学习的分布。

常见的预处理步骤包括:

  • 零中心化:减去整个训练集的均值。对于图像,可以减去“平均图像”或每个通道的均值。
  • 归一化:除以整个训练集的标准差,使每个特征的尺度大致相同。
  • 白化:一种更高级的预处理,先对数据进行去相关(旋转),再进行归一化,使数据分布更接近球形。这在图像处理中较少使用。

对于图像数据,常见的做法是计算训练集上每个通道(R, G, B)的均值和标准差,然后对每个像素进行 (像素值 - 通道均值) / 通道标准差 的操作。

重要原则:所有预处理统计量(均值、标准差)都必须仅从训练集计算,然后在测试集上应用相同的变换,以模拟真实部署场景。


权重初始化 🔑

网络权重的初始值对训练的成功至关重要。初始化不当可能导致梯度消失或爆炸。

  • 全零初始化:非常糟糕。所有神经元会学到相同的特征,且梯度为零,网络无法学习。
  • 小随机数初始化:使用高斯分布 N(0, 0.01^2)。对于浅层网络可行,但对于深层网络,可能导致激活值逐层收缩至零(梯度消失)或膨胀至饱和区(梯度爆炸)。

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

  • Xavier初始化:适用于Tanh等对称、线性区域在零附近的激活函数。权重从均值为0,标准差为 std = sqrt(1 / D_in) 的高斯分布中采样,其中 D_in 是层的输入维度。
  • He初始化 (MSRA初始化):专为ReLU设计。权重从均值为0,标准差为 std = sqrt(2 / D_in) 的高斯分布中采样。额外的 sqrt(2) 因子补偿了ReLU将一半激活置零的影响。
  • 残差网络初始化:对于ResNet,通常对残差块中的最后一层卷积使用零初始化,这使得整个残差块在初始时近似恒等映射,有助于稳定深层网络中的信号传播。

核心思想:好的初始化旨在使网络中每一层激活值的方差在向前传播过程中保持大致稳定。


正则化 🛡️

当模型在训练集上表现很好但在测试集上表现不佳时,就发生了过拟合。正则化是防止过拟合的关键技术。

我们已经熟悉了 L2正则化(权重衰减),它通过在损失函数中添加权重的平方和惩罚项来限制模型复杂度。

本节重点介绍另一类重要的正则化方法:在训练时引入随机性,在测试时通过平均(期望)来消除随机性

  • Dropout

    • 训练时:在前向传播的每一层,随机将一部分神经元(例如50%)的输出置零。
    • 测试时:使用所有神经元,但将层的输出乘以Dropout概率(例如0.5),或者采用更常见的 Inverted Dropout:在训练时对保留的神经元直接除以(1 - dropout概率),这样测试时无需做任何调整。
    • 作用:防止神经元之间的协同适应(co-adaptation),迫使网络学习更鲁棒的特征。可以看作是在训练一个共享权重的大型子网络集合。
    • 应用:在AlexNet、VGG等包含大型全连接层的网络中非常有效。在现代架构(如ResNet)中,由于使用了全局平均池化替代大全连接层,Dropout的重要性有所下降。
  • 批量归一化 (Batch Normalization)

    • 它本身也具有正则化效果,因为其输出依赖于每个小批量数据的统计量,这为训练过程引入了随机性。
    • 测试时,使用在训练过程中计算得到的移动平均均值和方差,从而“平均掉”这种随机性。
    • 在现代网络中,Batch Normalization + L2权重衰减已成为主要的正则化组合。

  • 数据增强 (Data Augmentation)
    • 训练时:对输入数据应用随机的、不改变其语义的变换(如图像的水平翻转、随机裁剪、颜色抖动等),从而“免费”扩大训练集。
    • 测试时:通常对测试图像进行几种固定的变换(如多尺度裁剪),然后对多个预测结果取平均。
    • 这是注入领域知识的好方法,其有效性高度依赖于具体任务。

其他遵循“训练随机,测试平均”模式的技巧还包括:DropConnect、随机深度、Cutout(随机遮挡图像区域)、Mixup(混合图像和标签)等。

实践建议:对于现代卷积神经网络(如ResNet),组合使用L2权重衰减、批量归一化和数据增强通常是足够且有效的正则化策略。Dropout在具有大型全连接层的网络中仍有价值。Cutout和Mixup在小数据集上可能带来额外提升。


本节课中,我们一起学习了训练神经网络第一部分的核心内容:如何选择激活函数、进行数据预处理、正确初始化权重以及应用有效的正则化策略来防止过拟合。掌握这些“炼丹”细节,是成功训练深度学习模型的基础。在下一讲中,我们将继续探讨训练过程中的其他重要主题,如学习率调度和优化技巧。

11:训练神经网络(第二部分)📚

在本节课中,我们将继续讨论训练神经网络所需的各种实用技巧和细节。上一讲我们介绍了一些关于架构和初始设置的一次性选择,本节中我们来看看在训练过程中以及训练完成后需要注意的事项。

学习率调度 📈

上一节我们介绍了激活函数、数据预处理、权重初始化和正则化等概念。本节中我们来看看如何动态调整学习率,这是训练过程中一个至关重要的环节。

学习率是大多数深度学习模型中最重要的超参数之一。设置不当会导致训练失败或效率低下。

以下是几种常见的学习率调度策略:

  • 步长衰减:在训练过程中,在预先设定的迭代次数(例如,每30个epoch)将学习率突然降低一个因子(例如,乘以0.1)。这是ResNet等经典模型中常用的策略。
    • 优点:直观,能有效应对训练后期的停滞。
    • 缺点:引入了新的超参数(何时衰减、衰减多少),需要大量试错来调整。
  • 余弦衰减:学习率根据余弦函数从初始值平滑衰减到0。公式为:α_t = 0.5 * α_0 * (1 + cos(π * t / T)),其中 α_0 是初始学习率,t 是当前迭代,T 是总迭代次数。
    • 优点:超参数少(只需初始学习率和总训练轮数),易于调整,训练更长时间通常效果更好。
  • 线性衰减:学习率从初始值线性衰减到0。
    • 优点:简单直接。
  • 常数学习率:在整个训练过程中保持学习率不变。
    • 优点:最简单,对于许多问题(尤其是使用Adam等自适应优化器时)效果很好。建议在项目初期优先使用,待模型稳定后再考虑更复杂的调度。

核心建议:对于新项目,建议先使用常数学习率或简单的线性/余弦衰减。调整学习率调度通常是模型开发后期为了追求极致性能才进行的步骤。

早停与模型检查点 ⏹️

在训练神经网络时,监控多个指标至关重要。

以下是需要监控的关键曲线:

  • 训练损失:应呈下降趋势。建议同时绘制每个迭代的原始损失(散点图,显示方差)和移动平均损失(平滑曲线,显示趋势)。
  • 训练准确率:应持续上升。
  • 验证准确率:这是评估模型泛化能力的关键。

早停策略:你应该在整个训练过程中定期(例如每5或10个epoch)将模型参数保存到磁盘。训练结束后,选择在验证集上性能最佳的那个检查点作为最终模型。这样即使模型在训练后期性能下降或“爆炸”,你也能保留之前的最佳状态。

超参数调优 🎛️

选择合适的超参数是训练成功的关键。以下是两种主要策略:

  • 网格搜索:为每个超参数指定一组候选值,尝试所有可能的组合。
    • 缺点:所需计算资源随超参数数量指数级增长,不实用。
  • 随机搜索:为每个超参数指定一个范围,每次试验随机从该范围内采样一个值。
    • 优点:在超参数重要性不均等的情况下,能更有效地探索重要参数的空间,通常比网格搜索更高效。

一个实用的调优流程

  1. 检查初始损失:关闭正则化,检查模型在随机初始化下的损失是否符合预期(例如,对于C类分类,交叉熵损失应接近 -log(1/C))。不符则说明有bug。
  2. 在小数据上过拟合:取一个极小的训练数据子集(如1-5个批次),关闭正则化,调整模型架构、学习率等,目标是在几分钟内达到100%的训练准确率。这能确保前向/反向传播代码正确。
  3. 在全部数据上找到合适的学习率:使用上一步确定的架构,关闭正则化,仅调整学习率。目标是在前100-1000次迭代内看到损失显著下降。
  4. 进行粗略的网格搜索:围绕上一步找到的“好”参数,设置一个很小的超参数网格(例如2x2),训练几个epoch,观察验证集性能。
  5. 迭代优化:根据第4步的结果和分析学习曲线,调整超参数范围或模型设置,训练更长时间,循环此过程直至满意或时间耗尽。

分析学习曲线 📊

通过观察学习曲线的形状,可以诊断训练中的问题:

  • 训练损失曲线
    • 初始平坦后骤降:可能初始化不佳。
    • 快速下降后平台期:考虑引入学习率衰减。
    • 学习率衰减后立即平台:可能衰减过早。
  • 训练/验证准确率曲线
    • 两者均持续缓慢上升:情况健康,可能需要训练更久。
    • 训练准确率持续上升,验证准确率停滞或下降:这是过拟合的典型标志。需要增加正则化强度、收集更多数据或减小模型容量。
    • 训练和验证准确率几乎相同且都很低:这是欠拟合的标志。需要减小正则化、增加模型容量或调整特征。

训练后的技巧:集成与迁移学习 🚀

成功训练模型后,还可以通过以下方法进一步提升性能或解决新问题:

  • 模型集成:训练多个独立模型,在测试时对它们的预测进行平均。这通常能稳定地提升1-2%的性能。
  • Polyak平均:在训练过程中,维护模型权重的指数移动平均值,并在测试时使用这个平均权重,有助于平滑优化过程中的噪声。
  • 迁移学习:这是计算机视觉中的主流方法。利用在大数据集(如ImageNet)上预训练的模型,将其知识迁移到新的、通常数据量较小的任务上。
    • 特征提取:冻结预训练模型的所有层,移除最后的分类层,将倒数第二层的输出作为“特征向量”,然后为其训练一个新的线性分类器(如SVM或逻辑回归)。
    • 微调:不仅替换最后的分类层,还用新数据继续训练整个网络(或后面几层)。此时通常需要使用更小的学习率。
  • 迁移学习指南
    • 数据少,与ImageNet相似:使用特征提取+线性分类器。
    • 数据多,与ImageNet相似:进行微调。
    • 数据多,与ImageNet不同:仍可尝试从ImageNet初始化并微调,可能有效。
    • 数据少,与ImageNet不同:最具挑战性的情况,可能需要更精巧的方法。

大规模分布式训练 🌐

当拥有多个GPU时,可以采用以下并行策略加速训练:

  • 数据并行:这是最主流且高效的方法。将训练批次N平均分配到K个GPU上,每个GPU拥有完整的模型副本,独立处理N/K个数据,计算梯度。最后将所有GPU上的梯度求和,同步更新每个GPU上的模型参数。
  • 大批次训练技巧:当使用K倍GPU和K倍批次大小时,应将学习率也线性缩放K倍(即新学习率 = K * 原学习率)。为避免初始阶段学习率过大导致不稳定,常配合使用学习率预热策略:在训练开始的前几千次迭代中,将学习率从0逐渐增加到目标值。

总结 🎯

本节课我们一起学习了训练神经网络第二部分的核心内容。我们探讨了如何通过学习率调度策略(如步长衰减、余弦衰减)来优化训练过程;介绍了使用早停和保存模型检查点来获得最佳模型;详细讲解了超参数调优的实用流程(从检查初始化、过拟合小数据到迭代搜索)以及如何通过分析学习曲线来诊断问题。最后,我们了解了在模型训练完成后,如何利用模型集成和强大的迁移学习技术来提升性能或解决新任务,并简要介绍了数据并行的大规模分布式训练基础。

掌握这些训练技巧、调优方法和后续处理策略,将帮助你更高效、更成功地构建和部署深度学习模型。

12:循环神经网络

在本节课中,我们将要学习一种新型的神经网络——循环神经网络。我们将探讨其工作原理、应用场景以及如何解决序列处理问题。

概述

在前面的课程中,我们深入学习了如何训练深度卷积神经网络进行图像分类。现在,我们将把目光转向能够处理序列数据的新问题类型。循环神经网络正是为此类问题设计的强大工具,它不仅能处理输入或输出为序列的任务,甚至还能对非序列数据进行顺序处理。

循环神经网络简介

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

循环神经网络的基本思想是,网络在每一个时间步接收一个输入,更新其内部隐藏状态,并产生一个输出。关键之处在于,网络使用相同的权重矩阵和更新规则来处理序列中的每一个元素,这使得它可以处理任意长度的序列。

以下是循环神经网络在一个时间步的核心更新公式:

h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + b)
y_t = W_hy * h_t

其中:

  • h_t 是当前时间步的隐藏状态。
  • h_{t-1} 是前一个时间步的隐藏状态。
  • x_t 是当前时间步的输入。
  • W_hh, W_xh, W_hy 是可学习的权重矩阵。
  • b 是偏置项。
  • tanh 是激活函数。

序列处理任务类型

循环神经网络可以应用于多种序列处理任务。以下是几种主要的任务类型:

  • 一对一:这是经典的前馈网络模式,例如图像分类(单张图像输入,单个标签输出)。
  • 一对多:输入是单个项目,输出是一个序列。例如,图像描述生成(输入一张图像,输出描述该图像的文字序列)。
  • 多对一:输入是一个序列,输出是单个项目。例如,视频分类(输入一系列视频帧,输出描述整个视频的单个标签)。
  • 多对多(序列到序列):输入和输出都是序列,且长度可能不同。例如,机器翻译(输入一个英语句子,输出对应的法语句子)。
  • 多对多(同步):为输入序列中的每个元素生成一个输出。例如,视频帧级分类(为每一帧视频分配一个动作标签)。

循环神经网络的应用

循环神经网络的应用非常广泛,不仅限于处理明显的序列数据。

在非序列数据上的应用

有时,人们会使用循环神经网络对非序列数据进行顺序处理,以获得新的视角或能力。

  • 顺序注意力图像分类:网络不是一次性观察整张图像,而是通过一系列“瞥视”来逐步查看图像的不同部分,每次瞥视的位置基于之前收集的信息决定,最后做出分类决策。
  • 顺序生成图像:网络通过一系列步骤来“绘制”图像,例如生成手写数字或油画风格的肖像。在每个时间步,网络决定在哪里画以及画什么,逐步构建出完整的图像。

循环神经网络的工作原理

理解了循环神经网络能做什么之后,我们来看看它是如何工作的。

展开计算图

为了更清晰地理解循环神经网络如何处理整个序列,我们可以将其在时间维度上“展开”。这意味着我们将每个时间步的重复操作显式地画出来,形成一个链式的计算图。在展开的图中,相同的权重矩阵 W 被用于每一个时间步。

在反向传播时,梯度会沿着这个展开的图从最后的时间步流回最初的时间步,这个过程有时被称为“随时间反向传播”。由于权重是共享的,梯度在流经每个时间步的权重节点时需要被累加。

语言建模示例

语言建模是循环神经网络的一个经典应用,其目标是预测序列中下一个出现的字符或单词。

在训练时,网络接收一个字符序列(如“hello”),并在每个时间步尝试预测下一个字符。输入字符通常被编码为独热向量,并可能通过一个嵌入层转换为密集向量。损失函数(如交叉熵损失)在每个时间步计算,并求和得到总损失。

训练完成后,我们可以通过“采样”来生成新文本:

  1. 给定一个起始字符(种子)。
  2. 网络预测下一个字符的概率分布。
  3. 从该分布中采样出一个字符。
  4. 将采样得到的字符作为下一个时间步的输入。
  5. 重复步骤2-4,直到生成足够长的文本或遇到停止符。

通过在不同数据集(如莎士比亚文集、Linux内核源代码)上训练,循环神经网络可以学习并生成具有相应风格和结构的新文本。

处理长序列:截断随时间反向传播

处理非常长的序列时,完全展开计算图会消耗大量内存。为了解决这个问题,我们使用“截断随时间反向传播”技术。

其思想是:

  1. 将长序列分成较短的块(例如每块100个字符)。
  2. 对第一块数据执行前向传播,计算损失,并执行反向传播以更新权重。记录下处理完该块后的最终隐藏状态。
  3. 将记录的隐藏状态作为初始状态,对下一块数据重复步骤2。
  4. 如此反复,网络可以在前向传播中保持对长序列上下文的记忆,同时在反向传播时只处理有限长度的数据块,从而节省内存。

图像描述生成

将循环神经网络应用于计算机视觉的一个典型例子是图像描述生成。这是一个“一对多”的任务。

模型通常由两部分组成:

  1. 卷积神经网络:作为编码器,处理输入图像并提取视觉特征向量。
  2. 循环神经网络:作为解码器,接收图像特征向量,并生成描述文字序列。

在循环神经网络的每一步,其输入包括:上一个时间步的隐藏状态、上一个时间步生成的单词(或起始符),以及从图像中提取的特征向量。其更新公式扩展为:

h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + W_vh * v + b)

其中 v 是图像特征向量。网络持续生成单词,直到预测出特殊的“结束”符。

虽然图像描述模型有时能产生惊人的准确描述,但它们也经常犯一些符合训练数据统计规律但不符合图像实际内容的错误,这表明它们离真正的视觉理解还有距离。

梯度流与长短期记忆网络

在训练循环神经网络,特别是处理长序列时,梯度流会遇到问题。

梯度消失与爆炸

在基本的循环神经网络中,梯度在反向传播时需要多次乘以相同的权重矩阵 W 的转置。这会导致:

  • 梯度爆炸:如果 W 的奇异值大于1,梯度会指数级增长。
  • 梯度消失:如果 W 的奇异值小于1,梯度会指数级衰减到0。

梯度爆炸的一个应对技巧是“梯度裁剪”,即在梯度范数超过阈值时将其缩放。但这只是一个启发式方法。

长短期记忆网络

为了解决梯度消失问题并更好地捕捉长期依赖关系,人们设计了长短期记忆网络。

LSTM 在每个时间步维护两个状态向量:

  • 细胞状态:一个“高速公路”,信息可以几乎不变地流过。
  • 隐藏状态:基于细胞状态和输出门产生的输出。

LSTM 通过三个“门”结构来控制信息流:

  • 遗忘门:决定从细胞状态中丢弃哪些信息。
  • 输入门:决定将哪些新信息存入细胞状态。
  • 输出门:决定基于细胞状态输出什么到隐藏状态。

LSTM 的关键优势在于,从细胞状态 c_tc_{t-1} 的梯度路径主要是通过加法节点和元素乘法(乘以接近1的遗忘门),避免了连续的非线性变换和矩阵乘法,从而极大地缓解了梯度消失问题。这与残差网络通过跳跃连接改善深度网络梯度流的思路相似。

其他架构与总结

深度循环神经网络

与卷积网络类似,我们可以堆叠多个循环神经网络层以构建更强大的模型,即深度循环神经网络。较低层的RNN输出其隐藏状态序列,该序列作为较高层RNN的输入序列。

其他变体

除了LSTM,还有其他循环神经网络变体,如门控循环单元,它提供了一个更简化的架构。此外,也有研究使用神经架构搜索或进化算法来自动发现高效的RNN单元结构,但LSTM和GRU在实践中仍然因其稳定性和良好性能而被广泛使用。

总结

本节课中我们一起学习了循环神经网络。我们了解了RNN如何通过共享权重处理可变长度序列,探索了其在语言建模和图像描述生成等任务中的应用。我们还深入探讨了训练RNN时面临的梯度挑战,并介绍了LSTM如何通过其门控机制和细胞状态来改善梯度流,从而能够学习长期依赖关系。循环神经网络为我们打开了一扇门,使我们能够用深度学习模型处理丰富多彩的序列数据世界。

13:注意力机制

概述

在本节课中,我们将要学习注意力机制。我们将从回顾序列到序列的循环神经网络开始,探讨其局限性,并引入注意力机制作为解决方案。随后,我们会将注意力机制推广为一种通用的神经网络层——自注意力层,并最终介绍基于自注意力构建的强大模型——Transformer。本节课内容旨在让初学者能够理解这些核心概念。


回顾:序列到序列模型

上一节我们介绍了用于处理序列数据的循环神经网络。本节中,我们来看看如何将其应用于序列到序列的任务,例如机器翻译。

在序列到序列模型中,我们使用一个编码器RNN处理输入序列(如英文句子),生成一系列隐藏状态。这些隐藏状态最终被汇总为一个上下文向量 C。然后,一个解码器RNN接收这个上下文向量和起始标记,逐步生成输出序列(如西班牙语句子)。

公式表示编码器步骤
h_t = f_W(h_{t-1}, x_t)

其中,h_t 是时间步 t 的隐藏状态,x_t 是输入,f_W 是带有参数 W 的RNN单元。

然而,这种方法存在一个瓶颈:无论输入序列多长,所有信息都必须压缩到单个上下文向量 C 中。这对于长段落或书籍的翻译来说是不现实的。


引入注意力机制

为了克服上述瓶颈,我们引入注意力机制。其核心思想是:解码器在生成每一个输出词时,都可以动态地关注输入序列的不同部分,而不是依赖一个固定的上下文向量。

以下是实现注意力的关键步骤:

  1. 计算对齐分数:在解码器的每个时间步 t,我们有一个查询向量(通常是解码器的当前隐藏状态 s_t)。我们将其与编码器所有时间步的隐藏状态 h_i 进行比较,计算出一个对齐分数 e_{ti},表示在生成当前输出词时,输入词 i 的重要性。
    常用对齐函数(缩放点积)
    e_{ti} = (s_t · h_i) / sqrt(d_k)
    其中 d_k 是向量的维度。缩放是为了防止点积结果过大导致Softmax梯度消失。

  2. 转换为注意力权重:将对齐分数序列 e_{t1}, e_{t2}, ..., e_{tT} 通过Softmax函数,得到归一化的注意力权重 α_{ti}。这形成了一个概率分布,总和为1。
    公式
    α_{ti} = exp(e_{ti}) / Σ_j exp(e_{tj})

  3. 计算上下文向量:将编码器的所有隐藏状态 h_i 按其对应的注意力权重 α_{ti} 进行加权求和,得到当前时间步专属的上下文向量 c_t
    公式
    c_t = Σ_i (α_{ti} * h_i)

  1. 生成输出:解码器RNN结合当前的隐藏状态 s_t、新计算的上下文向量 c_t 以及上一个输出词(或起始标记),来预测当前时间步的输出词 y_t

这个过程在解码器的每个时间步重复进行,使得模型能够灵活地将注意力集中在输入序列的不同部分。


注意力的应用与可视化

注意力机制不仅用于机器翻译,还可应用于图像描述生成等任务。

在图像描述任务中,编码器是一个卷积神经网络,其最后一层卷积特征图可以视为一个空间位置的网格,每个位置对应一个特征向量。解码器RNN在生成每个单词时,使用注意力机制在这个特征网格上进行“观察”,计算应该关注图像的哪个区域。

注意力权重提供了模型决策的可解释性。例如,在机器翻译中,我们可以可视化注意力矩阵,看到输出词的生成主要依赖于输入序列中的哪些词,有时甚至能发现语言之间的词序对应关系(如英语的“European economic area”对应法语的“zone économique européenne”)。


推广:自注意力层

我们可以将注意力机制抽象为一个通用的神经网络层。它不关心输入是否是序列,而是处理两组向量:查询向量集 Q 和输入向量集 X

以下是构建通用注意力层的步骤:

  1. 键值分离:为提供更多灵活性,我们将每个输入向量 x_i 通过可学习的权重矩阵 W^KW^V 分别转换为键向量 k_i 和值向量 v_i
    公式
    k_i = W^K * x_i
    v_i = W^V * x_i

  2. 计算相似度:对于每个查询向量 q(来自查询集 Q),计算它与所有键向量 k_i 的缩放点积,得到相似度分数。

  3. 应用Softmax:对每个查询对应的相似度分数序列应用Softmax,得到该查询对于所有输入的注意力权重。

  4. 加权求和:使用注意力权重对值向量 v_i 进行加权求和,得到该查询对应的输出向量。

当查询集 Q 和输入集 X 是同一组向量时,就得到了自注意力层。自注意力层允许序列中的每个元素直接与序列中的所有其他元素进行交互。

自注意力层是置换等变的:改变输入向量的顺序,输出向量的顺序会相应改变,但内容不变。为了融入位置信息,我们通常需要加入位置编码


Transformer:基于自注意力的模型

自注意力层高度可并行化,且能一次性捕获长距离依赖,这使其成为处理序列的理想选择。Transformer模型完全基于自注意力构建。

一个基本的Transformer块包含以下组件:

  1. 多头自注意力层:使用多个并行的自注意力头,以捕获不同子空间的信息。
  2. 残差连接与层归一化:添加在自注意力层和前馈网络之后,有助于稳定和加速训练。
  3. 前馈神经网络:一个应用于每个位置独立的小型全连接网络。

通过堆叠多个这样的Transformer块,我们可以构建出非常深且强大的模型。这种架构在自然语言处理领域取得了革命性成功,催生了如BERT、GPT等预训练大模型。


总结

本节课中我们一起学习了注意力机制的核心思想与发展脉络。我们从解决序列到序列模型的瓶颈出发,引入了动态计算上下文向量的注意力机制。随后,我们将其推广为通用的自注意力层,并最终介绍了完全基于自注意力构建的Transformer模型。注意力机制及其衍生模型为处理序列、图像甚至多模态数据提供了强大而灵活的工具,是当前深度学习研究的前沿方向。

对抗性机器学习:第14讲:对抗性攻击与防御概述

在本节课中,我们将学习对抗性机器学习的基本概念。我们将探讨机器学习模型,特别是深度学习模型,如何可能被精心设计的微小输入扰动所欺骗,导致错误的分类决策。理解这些攻击的原理以及潜在的防御策略,对于构建可信赖的、安全的AI系统至关重要。

问题定义与动机

上一节我们介绍了课程背景,本节中我们来看看对抗性攻击的核心问题。

对抗性攻击的核心思想是:对于一个训练有素、准确率很高的分类器,攻击者能否找到一个微小的扰动,添加到原始输入上,使得分类器做出完全错误的判断?这个扰动对人类观察者而言,输入看起来几乎没有变化,应保持相同的标签。

公式化描述:给定一个分类器 f 和一个正确分类的输入 x(即 f(x) = y),目标是找到一个扰动 δ,使得 f(x + δ) ≠ y(非目标攻击)或 f(x + δ) = y_target(目标攻击),同时约束 δ 的“大小”(例如,像素变化幅度)足够小。

这种现象引发了我们对机器学习模型,尤其是在自动驾驶、医疗诊断等安全关键领域应用的信任问题。如果模型如此脆弱,我们能否放心地部署它们?

对抗性攻击为何可能?

为了理解攻击为何可能成功,我们可以从一个简化的线性模型开始思考。

考虑一个高维线性分类器:y = Σ w_i * x_i。假设我们希望对 y 产生大的改变,但只允许对每个特征 x_i 进行微小调整。根据梯度的思想,我们应该沿着使 y 变化最快的方向调整 x。具体来说,对于权重 w_i 较大的特征,即使对 x_i 做微小改变,也会对 y 产生较大影响。通过协调地、有方向地对多个高维特征施加微小扰动,累积效应可能导致输出发生显著变化。

深度学习模型虽然非线性,但包含大量可操作的维度和线性成分,且攻击者可以利用模型的梯度信息。在正常的模型训练中,我们使用梯度下降来最小化损失函数。而在对抗性攻击中,我们可以进行“逆向”操作:寻找能最大化损失(或使损失朝向错误标签最小化)的最小输入扰动。

攻击实例与影响

以下是几种对抗性攻击的实例:

  • 数字图像攻击:对一张“熊猫”图片添加人眼难以察觉的噪声后,分类器可能以高置信度将其识别为“长臂猿”。
  • 物理世界攻击:在现实世界的停车标志上粘贴精心设计的贴纸,可以使车载摄像头系统将其误判为“限速45英里”标志。这证明了攻击可以从数字域迁移到物理世界。
  • 数据投毒攻击:在模型训练阶段注入恶意样本。例如,微软的聊天机器人Tay在2016年上线后,通过在线学习吸收了用户的恶意输入,在24小时内变成了发表种族主义和煽动性言论的机器人。
  • 逃避攻击:攻击者修改输入以绕过内容过滤器。例如,垃圾邮件发送者故意拼错关键词以逃避基于关键词的过滤器。

这些例子表明,对抗性威胁广泛存在于模型部署(逃避攻击)、训练(数据投毒)和测试(对抗性输入)各个阶段。

攻击方法分类与策略

根据攻击者对目标模型的了解程度,攻击可分为:

  • 白盒攻击:攻击者拥有模型的完整知识,包括架构、参数(权重),可以计算精确的梯度。这是最强的攻击设定。
  • 黑盒攻击:攻击者仅能向模型输入数据并观察输出(如分类结果和置信度),不知道内部细节。一种有效的策略是利用攻击的“可迁移性”:攻击者使用输入输出对训练一个替代模型,然后对这个替代模型进行白盒攻击,生成的对抗样本有很大概率也能欺骗原模型。

在白盒设定下,根据攻击目标,又可分为:

  • 非目标攻击:仅使分类结果出错,不关心具体错误类别。
  • 目标攻击:使模型将输入误分类为某个特定的目标类别。

核心攻击算法简介

攻击通常被形式化为一个约束优化问题:在扰动 δ 的范数(衡量其大小)不超过某个阈值 ε 的条件下,找到使模型针对目标标签的损失最小化(或针对原始标签的损失最大化)的 δ

以下是两个经典算法:

快速梯度符号法(FGSM)
这是一种一步攻击法,速度快但可能非最优。其核心思想是沿损失函数相对于输入数据的梯度方向,以最大允许步长 ε 移动。

代码描述

# x: 原始输入
# y_target: 目标标签
# epsilon: 扰动最大幅度
# loss_fn: 损失函数(如交叉熵)
# model: 目标模型

# 计算损失关于输入的梯度
x.requires_grad = True
loss = loss_fn(model(x), y_target)
loss.backward()
gradient = x.grad.data

# 生成对抗样本
perturbation = epsilon * torch.sign(gradient)
x_adv = x + perturbation

投影梯度下降法(PGD)
这是一种迭代攻击法,通常更强大。它在每一步沿梯度方向更新扰动,如果更新后的扰动超出了允许的范数球,就将其投影回球内。

代码描述(概念)

x_adv = x.clone()
for i in range(num_iterations):
    x_adv.requires_grad = True
    loss = loss_fn(model(x_adv), y_target)
    loss.backward()
    gradient = x_adv.grad.data

    # 沿梯度方向更新
    x_adv = x_adv + alpha * torch.sign(gradient) # alpha为步长

    # 计算当前扰动 delta = x_adv - x
    delta = x_adv - x
    # 将扰动投影到 epsilon 球内(例如 L∞ 范数)
    delta = torch.clamp(delta, -epsilon, epsilon)
    # 得到投影后的对抗样本
    x_adv = torch.clamp(x + delta, 0, 1) # 假设输入像素值在[0,1]

物理攻击与防御现状

物理攻击的挑战在于需要使攻击在不同视角、光照条件下仍然有效。解决思路通常是将问题建模为优化问题,要求生成的扰动在多个变换(如旋转、缩放)后的图像上都能成功欺骗模型,同时可能通过掩码矩阵将扰动限制在特定物理区域(如贴纸形状内)。

目前,最有效的防御策略是对抗训练。其核心思想是在模型训练过程中,不仅使用原始数据,还主动生成当前模型下的对抗样本,并将其加入训练集,迫使模型学会正确分类这些“困难”样本。

公式化描述(简化)
min_θ E_(x,y)~D [ max_(δ∈Δ) L( f_θ(x+δ), y ) ]
其中,内层的 max 是寻找针对当前模型参数 θ 的最强对抗扰动 δ,外层的 min 是调整模型参数 θ 以最小化在最坏扰动下的损失。

然而,对抗训练存在一些挑战:

  1. 计算开销大,训练时间可能增加一个数量级。
  2. 可能会在提升模型鲁棒性的同时,略微降低其在干净数据上的正常准确率。
  3. 目前尚无能完全免疫所有攻击的防御方法,攻击与防御处于持续的“军备竞赛”中。

总结与展望

本节课中我们一起学习了对抗性机器学习的基础知识。我们了解了对抗性攻击如何通过微小扰动欺骗深度学习模型,探讨了其背后的原理(高维模型与梯度利用),并介绍了白盒/黑盒、目标/非目标等攻击分类。我们还简要了解了快速梯度符号法(FGSM)和投影梯度下降法(PGD)等核心攻击算法,以及从数字域到物理世界的攻击实例。最后,我们讨论了当前主要的防御策略——对抗训练及其面临的挑战。

对抗性机器学习是一个快速发展的领域,仍然存在大量开放问题,例如:如何设计更高效且强大的黑盒攻击?如何构建理论上可证明鲁棒的模型?如何将鲁棒性考量集成到复杂的多模态、多传感器系统中?对这些问题的探索,不仅关乎AI系统的安全,也有助于我们更深入地理解机器学习模型本身的特性与局限。

14:可视化与理解

在本节课中,我们将要学习如何窥探卷积神经网络内部,理解它们从训练数据中学到了什么。我们还将探讨,许多用于可视化和理解神经网络的技术,也可以被用于一些有趣的应用,例如DeepDream和风格迁移。

概述

上一节我们介绍了注意力机制和Transformer模型。本节中,我们来看看如何理解神经网络内部的工作机制。我们将介绍一系列技术,从可视化网络权重和激活,到使用梯度信息生成图像,再到将这些技术应用于创造性的图像生成任务。

可视化第一层卷积核

理解神经网络最直接的方法之一是可视化其第一层卷积核。回想一下,线性分类器可以看作是在学习一组模板,每个类别对应一个。卷积神经网络的第一层延续了这个思想,它学习一组滤波器,这些滤波器在输入图像上滑动并进行内积运算。

通过将这些滤波器本身可视化为RGB图像,我们可以了解网络最底层在寻找什么。以下是四个在ImageNet上预训练的不同CNN模型的第一层卷积核可视化结果:

  • AlexNet
  • VGG
  • ResNet
  • DenseNet

尽管这些网络架构不同,但它们第一层学习到的滤波器通常非常相似。我们经常能看到:

  • 检测不同方向的边缘的滤波器。
  • 检测颜色互补色的滤波器。

这与哺乳动物视觉系统中初级视觉皮层细胞检测定向边缘的现象有相似之处。

可视化更高层的挑战

我们能否将同样的技术应用于更高层的卷积核呢?理论上可以,但信息量不大。

第一层卷积核的输入是RGB图像,因此可以直接可视化。然而,第二层卷积核的输入是第一层的输出特征图(例如16个通道),其滤波器跨越所有输入通道。我们无法将16通道的特征图直观地理解为RGB图像。

一种尝试是将每个输出滤波器的每个输入通道切片可视化为灰度图像。虽然能看出一些空间结构(如斑点或边缘),但这并不能清晰地告诉我们这些高层滤波器在寻找什么。因此,我们需要其他技术来理解网络更深层的行为。

理解最后一层全连接层

另一种常见方法是跳过中间卷积层,直接理解网络最后一层全连接层(例如AlexNet的fc7层,输出4096维向量)所表示的内容。这个向量可以看作是输入图像的一个高级“特征表示”,网络在其上应用一个线性分类器来得到最终类别分数。

我们可以通过分析这个特征空间来理解网络学到了什么。

最近邻检索

我们可以对测试集中的所有图像提取这个4096维特征向量,然后对查询图像进行最近邻检索。

以下是具体步骤:

  1. 将查询图像输入训练好的AlexNet,提取fc7层的4096维特征向量。
  2. 对测试集所有图像做同样操作,得到一组特征向量。
  3. 使用L2距离(欧几里得距离)在4096维特征空间中为查询图像寻找最近邻图像。

这种方法的结果非常有趣。例如,一张大象图像的最近邻可能都是其他大象图像,尽管它们在像素级别上(如位置、颜色、背景)差异很大。这表明网络学习到的特征表示捕捉了“大象”的语义概念,而忽略了低级的像素细节。

降维可视化

4096维空间难以直接理解。我们可以使用降维算法将其投影到2维或3维空间,以便可视化。

以下是两种常用方法:

  • 主成分分析(PCA):一种线性降维方法,旨在保留数据的主要结构。
  • t-SNE:一种非线性降维方法,能更好地保留高维空间中的局部邻域结构。

例如,在一个手写数字识别网络上,我们对测试集图像提取最后一层特征,并用t-SNE降至2维。可视化结果显示,不同数字的样本在2维空间中形成了清晰的簇,表明网络的特征空间有效地编码了类别信息。

对于ImageNet这样更复杂的任务,t-SNE可视化也能显示出语义上的聚类,例如不同种类的狗聚集在一起,并与船只、天空等图像区域分开。

可视化中间层激活

除了权重,我们还可以直接可视化网络前向传播时计算的中间激活值。

对于一个卷积层(例如AlexNet的conv5层),其输出是一个三维激活体积(例如 13x13x128)。我们可以将每个通道(128个中的每一个)的 13x13 激活图可视化为灰度图像。

具体操作如下:

  1. 将输入图像通过网络前向传播。
  2. 在目标层(如conv5)提取激活体积。
  3. 将每个通道的激活图与输入图像对齐显示。

例如,某个通道的激活图可能在输入图像中人脸区域有高响应,暗示该滤波器可能对人脸或肤色敏感。需要注意的是,由于ReLU激活函数会将负值置零,这些激活图中通常包含大量黑色(零值)区域。

寻找最大激活图像块

为了更系统地理解单个神经元(滤波器)在寻找什么,我们可以搜索训练集中能最大化激活该神经元的图像区域(patch)。

具体步骤如下:

  1. 选择一个目标层和目标通道(神经元)。
  2. 将整个测试集图像输入网络。
  3. 对于每个图像,记录目标神经元在所有空间位置上的激活值。
  4. 找出激活值最高的那些空间位置。
  5. 根据网络的感受野,回推这些位置在原始输入图像中对应的图像块。
  6. 将这些“最大激活图像块”收集并可视化。

例如,在某个中间层,一个神经元的最大激活图像块可能都是狗的鼻子或眼睛;另一个神经元的最大激活块可能都是各种颜色和方向的文本;在更深的层,一个神经元的最大激活块可能都是各种人脸。这直观地展示了不同层神经元所检测的语义复杂度在逐渐增加。

计算显著图(Saliency Maps)

我们还可以通过分析输入图像的哪些像素对最终分类决策最重要来理解网络。一种方法是使用遮挡(Occlusion)。

具体步骤如下:

  1. 将输入图像(如一张正确分类为大象的图片)输入网络,记录目标类别(大象)的分数。
  2. 在图像上放置一个灰色方块(遮挡块),遮挡住部分区域。
  3. 将遮挡后的图像再次输入网络,记录新的大象分数。
  4. 将遮挡块滑动到图像每个位置,重复步骤3。
  5. 生成一个热图(显著图),其中每个位置的颜色表示当该区域被遮挡时,目标类别分数的下降程度。

如果遮挡住物体本身(如大象),分数会大幅下降;如果遮挡住背景,分数变化不大。这表明网络确实在关注图像中与物体相关的区域来进行分类。

基于梯度的显著图

上述遮挡法计算量很大。一种更高效的方法是使用梯度

具体步骤如下:

  1. 将输入图像输入网络,计算目标类别分数。
  2. 通过反向传播,计算目标类别分数相对于输入图像每个像素的梯度
  3. 这个梯度图就构成了显著图,它表示每个像素的微小变化对类别分数的影响程度。

梯度大的像素(通常是物体轮廓内的像素)对分类决策更重要。这种方法与生成对抗样本的思想紧密相关。

引导式反向传播(Guided Backprop)

当我们想可视化中间神经元(而非最终分数)对输入图像的敏感性时,直接使用标准反向传播得到的梯度图可能很杂乱。引导式反向传播是一种改进技术。

其核心修改在于ReLU层的反向传播:

  • 标准反向传播通过ReLU时,会将前向传播中输入为负的位置对应的上游梯度置零。
  • 引导式反向传播额外增加一个约束:也将上游梯度中为负的部分置零

这样做的结果是,生成的梯度图更清晰,能更好地突出那些对激活目标神经元有正面贡献的输入像素。例如,对于之前那个激活狗眼睛的神经元,引导式反向传播生成的显著图会精确地高亮图像中狗眼睛的像素。

生成最大化激活的图像

我们可以更进一步,不局限于测试集中的图像,而是通过优化直接生成一个全新的合成图像,来最大化激活某个神经元或类别分数。

这通过在图像像素空间上进行梯度上升来实现。

目标函数可以表示为:

I* = argmax_I [ S_c(I) - λ * R(I) ]

其中:

  • S_c(I) 是图像I对于目标类别c(或目标神经元)的分数。
  • R(I) 是一个正则化项,用于迫使生成的图像看起来更“自然”。
  • λ 是权衡两项的超参数。

最简单的正则化器是L2范数正则化(惩罚图像像素值的L2范数)。使用这种方法,我们可以生成一些能高度激活“哑铃”、“斑点狗”、“鸵鸟”等类别的合成图像。这些图像虽然看起来不真实,但能揭示网络用于识别该类别的关键视觉特征(如哑铃的形状、斑点狗的斑纹)。

通过设计更复杂的图像正则化器(如加入平滑约束、使用更高级的先验),可以生成看起来更逼真的图像。但需要注意的是,正则化器越强,生成的结果可能越偏离网络“真正”在寻找的特征,而更多地反映了正则化器本身的偏好。

特征反演(Feature Inversion)

特征反演旨在回答:网络的某一层特征保留了多少原始图像的信息?

具体步骤如下:

  1. 给定一张输入图像,通过网络前向传播,提取某一中间层(如relu4_3)的特征。
  2. 固定网络权重,初始化一个随机噪声图像。
  3. 通过梯度上升优化这个噪声图像,使其在该层的特征表示与步骤1中提取的特征尽可能接近(例如,最小化L2距离),同时加入图像正则化。

可视化不同层的特征反演结果可以发现:

  • 低层(如relu2_2)的特征几乎能完美重建原始图像,说明低层保留了大部分像素信息。
  • 中层(如relu4_3)的特征重建的图像保留了物体的整体结构和形状,但丢失了细节纹理和颜色。
  • 高层(如relu5_3)的特征重建的图像只保留了非常粗略的全局结构,局部细节几乎全部丢失。

这表明,随着网络层数的加深,特征表示变得越来越抽象,逐渐抛弃了低级的视觉细节,转而编码更高级的语义信息。

应用:DeepDream

DeepDream利用梯度上升来放大和增强输入图像中网络已识别出的模式。

其核心思想是:

  1. 将输入图像输入网络,前向传播至某一目标层。
  2. 将该层的激活值本身设为梯度(即设置梯度等于激活值)。
  3. 执行反向传播至输入图像。
  4. 使用计算出的梯度更新输入图像(实际上是进行梯度上升),增强那些已被激活的特征。
  5. 重复此过程多次。

如果在低层(检测边缘)应用DeepDream,云彩中的边缘会被增强,产生漩涡状的艺术效果。如果在高层(检测复杂模式,如动物部件)应用DeepDream,网络会“幻想”出图像中本不存在的结构,产生各种 psychedelic(迷幻)的动物混合体,如“狗鱼”、“猪蜗牛”等,非常有趣。

应用:神经风格迁移(Neural Style Transfer)

风格迁移结合了特征重建和纹理合成的思想,能够将一幅图像(内容图像)的语义内容与另一幅图像(风格图像)的艺术风格相结合,生成全新的图像。

纹理合成与Gram矩阵

风格迁移的基础之一是纹理合成。传统方法使用非神经网络算法。而基于神经网络的方法使用Gram矩阵来表征纹理。

Gram矩阵的计算:

  1. 将风格图像输入预训练CNN,提取某一层的特征图(尺寸为 H x W x C)。
  2. 将该特征图重塑为 (H*W) x C 的矩阵 F
  3. 计算Gram矩阵 G = F^T * F,其尺寸为 C x C
  • G 中的元素 G_{ij} 计算了第 i 个特征通道与第 j 个特征通道在所有空间位置上的相关性(协方差)。
  • Gram矩阵丢弃了所有的空间位置信息,只保留了特征通道间的统计相关性,这恰好能描述图像的纹理风格。

通过梯度上升生成一个新图像,使其在多个网络层上的Gram矩阵与风格图像的Gram矩阵相匹配,就能实现神经纹理合成。

风格迁移算法

风格迁移的目标是生成图像 I,使其:

  1. 内容损失最小I 在较高层(如relu4_2)的特征与内容图像 C 的特征相近(保留内容结构)。
  2. 风格损失最小I 在多个层(如relu1_1, relu2_1, relu3_1, relu4_1, relu5_1)的Gram矩阵与风格图像 S 的Gram矩阵相近(捕获风格纹理)。

总损失函数为:

L_total(I, C, S) = α * L_content(I, C) + β * L_style(I, S)

其中 αβ 是权衡内容与风格权重的超参数。

通过梯度上升优化图像 I 的像素,最小化总损失,即可得到风格迁移的结果。这个过程是迭代的,速度较慢。

快速风格迁移

为了解决原始算法速度慢的问题,研究人员训练了一个前馈神经网络来直接进行风格迁移。

训练过程:

  1. 固定一个预训练好的VGG网络作为损失网络。
  2. 随机初始化一个风格转换网络(通常是一个编码器-解码器结构的CNN)。
  3. 使用与原始风格迁移相同的损失函数(内容损失+风格损失),但这次是对风格转换网络的权重进行优化,而不是对单张图像的像素。
  4. 训练完成后,风格转换网络可以在单次前向传播中快速对任何输入图像进行风格化。

实例归一化(Instance Normalization) 的提出正是为了提升快速风格迁移网络的质量。更进一步,条件实例归一化(Conditional Instance Normalization) 允许单个网络学习多种风格,只需为每种风格学习不同的缩放(scale)和偏移(shift)参数,而卷积权重共享。

总结

本节课中我们一起学习了多种可视化和理解卷积神经网络的技术:

  1. 基础可视化:可视化第一层卷积核、最后一层特征空间的最近邻和降维。
  2. 激活分析:可视化中间层激活、寻找最大激活图像块。
  3. 基于梯度的分析:计算显著图、使用引导式反向传播、生成最大化激活的合成图像、进行特征反演。
  4. 创造性应用:将这些技术应用于DeepDream和神经风格迁移,生成有趣的艺术图像。

这些技术不仅帮助我们理解神经网络内部的运作机制,也为图像生成和艺术创作开辟了新的可能性。下一节课,我们将探讨目标检测任务。

16:目标检测 🎯

在本节课中,我们将要学习计算机视觉中的一个核心任务——目标检测。我们将从图像分类任务出发,探讨目标检测的独特挑战,并介绍一系列经典的目标检测方法,包括R-CNN系列算法及其演进。

概述

之前我们讨论了如何理解和可视化卷积神经网络内部的工作机制。今天,我们将转向一个更实用的新任务:目标检测。与为整张图像分配单一标签的图像分类不同,目标检测要求模型识别图像中所有感兴趣的物体,并为每个物体输出其类别标签和空间位置(即边界框)。

从图像分类到目标检测

上一节我们介绍了图像分类,本节中我们来看看目标检测与其有何不同。

图像分类任务为整张输入图像分配一个单一的类别标签(例如,猫、狗、汽车)。这是一个非常有用且基础的任务,帮助我们理解了如何构建和训练卷积神经网络。

然而,计算机视觉领域包含一系列更复杂的任务,它们不仅识别图像中有什么,还要识别物体在哪里。目标检测就是其中之一。

什么是目标检测?

目标检测任务输入一张RGB图像,输出一组检测到的物体。对于每个被检测到的物体,模型需要输出两样东西:

  1. 类别标签:指明物体的类别(如“猫”、“狗”)。
  2. 边界框:一个矩形框,用四个数字 (x, y, w, h) 表示,其中 (x, y) 是框的中心坐标,(w, h) 是框的宽和高(均以像素为单位)。这些边界框通常与图像坐标轴对齐。

目标检测的挑战

与图像分类相比,输出边界框这一看似微小的变化带来了巨大的复杂性:

  • 可变数量的输出:图像中物体的数量是可变的,模型需要输出一个可变大小的检测结果集合。
  • 处理边界框:网络需要一种机制来生成和回归边界框坐标。
  • 高分辨率图像需求:为了准确定位多个物体,目标检测通常需要在更高分辨率的图像(如800x600像素)上工作,这带来了更大的计算负担。

尽管有这些挑战,目标检测在自动驾驶、监控等众多领域有至关重要的应用,是计算机视觉中仅次于图像分类的核心问题。

单目标检测:一个简单的起点

在深入多目标检测的复杂性之前,我们先考虑一个简化问题:如何检测图像中的单个物体?

我们可以构建一个相对简单的架构:

  1. 输入图像通过一个卷积神经网络(如AlexNet、VGG、ResNet)得到特征向量。
  2. 从这个特征向量分出两个分支:
    • 分类分支:一个全连接层,输出类别分数,使用Softmax损失进行训练。
    • 回归分支:另一个全连接层,输出四个实数 (x, y, w, h),表示边界框坐标,使用回归损失(如L2损失)进行训练。

为了用梯度下降法训练这个同时做两件事的网络,我们需要将两个损失函数合并为一个标量损失。这通过计算它们的加权和来实现:

总损失 = w1 * 分类损失 + w2 * 回归损失

这种为单一网络设计多个损失项以完成不同子任务的结构,被称为多任务损失。这种方法在已知图像中只有一个物体时效果很好,但无法处理现实世界中多物体的场景。

滑动窗口方法及其局限性

为了检测多个物体,一个直观的想法是使用滑动窗口方法:

  1. 训练一个CNN分类器,用于判断一个固定大小的图像区域(窗口)是否包含某个特定类别的物体,或者只是背景。
  2. 将这个分类器以滑动窗口的方式应用到输入图像的各个位置和不同尺度上。
  3. 所有被分类为非背景的窗口位置,就构成了检测结果。

然而,这种方法存在一个致命缺陷:计算量过大。对于一个 H x W 的图像,考虑所有可能位置、大小和长宽比的窗口,其数量是像素数量的四次方级。例如,对于一个800x600的图像,可能有约5800万个候选窗口。对每个窗口都运行一次CNN前向传播是完全不可行的。

区域提议与R-CNN

为了解决滑动窗口的计算瓶颈,研究者引入了区域提议的概念。其核心思想是:使用一个快速、轻量的算法(如选择性搜索)预先从图像中生成约2000个可能包含物体的候选区域,而不是穷举所有窗口。

这引出了2014年极具影响力的R-CNN方法:

  1. 生成区域提议:对输入图像运行选择性搜索算法,得到约2000个候选框。
  2. 变形与分类:将每个候选区域变形(warp)成固定大小(如224x224),然后分别输入一个CNN中进行特征提取。
  3. 分类与精修:CNN为每个区域输出两部分:
    • 一个 C+1 类的分类分数(C个物体类别 + 1个背景类)。
    • 一个边界框回归参数,用于对初始的区域提议框进行微调,使其更贴合真实物体。

R-CNN首次成功地将深度学习应用于目标检测,但它的速度很慢,因为需要为每个区域提议独立运行CNN前向传播(约2000次)。

加速之路:Fast R-CNN与Faster R-CNN

为了提升速度,Fast R-CNN改进了流程:

  1. 共享卷积计算:首先将整张图像输入一个CNN(称为骨干网络),得到整张图的卷积特征图。
  2. 特征图上裁剪:将选择性搜索得到的区域提议映射到特征图上,并通过一个可微的操作(如RoI Pooling)从特征图中裁剪出对应区域,并池化为固定大小的特征块。
  3. 区域分类与回归:将这些固定大小的特征块输入后续的全连接层,同时完成分类和边界框回归。

Fast R-CNN通过共享整图的卷积计算,大幅提升了速度。然而,区域提议生成(选择性搜索)仍然是CPU上的瓶颈。

Faster R-CNN进一步将区域提议的生成也整合进神经网络:

  1. 骨干网络提取图像特征。
  2. 一个轻量的区域提议网络(RPN)在特征图的每个位置上预设多个不同尺度和长宽比的锚框,并预测每个锚框是“物体”还是“背景”,同时输出一个微调锚框的变换参数。
  3. RPN生成的区域提议,再像Fast R-CNN一样,通过RoI Pooling和后续网络进行最终的分类和回归。

Faster R-CNN实现了端到端的训练,并将大部分计算都放在了GPU上,速度得到了极大提升。由于其包含“区域提议生成”和“区域分类回归”两个明显阶段,它被称为两阶段检测器

更快的单阶段检测器

既然RPN已经可以生成带有类别和框信息的输出,能否去掉第二阶段,只用一阶段就完成检测?这就是单阶段检测器(如SSD, YOLO)的思想。

单阶段检测器在特征图的每个锚点位置上,直接通过卷积层预测:

  • 所有类别的分类分数(包括背景)。
  • 每个类别对应的边界框回归参数(或共享的参数)。

单阶段检测器结构更简单、速度更快,因为完全避免了每区域的独立计算。但其精度通常略低于两阶段检测器,因为它没有对候选区域进行“二次审视”的机会。

评估指标:交并比与平均精度均值

为了衡量目标检测器的性能,我们需要专门的评估指标。

交并比

交并比(IoU)用于衡量两个边界框的重叠程度。计算公式为两个框交集面积与并集面积的比值:

IoU(A, B) = 面积(A ∩ B) / 面积(A ∪ B)

IoU值在0到1之间,值越大表示重叠越好。通常IoU > 0.5被认为是一个可以接受的匹配。

非极大值抑制

检测器通常会为同一个物体输出多个高度重叠的框。非极大值抑制(NMS)是一种后处理算法,用于去除冗余检测框:

  1. 选择置信度最高的检测框。
  2. 计算该框与所有其他框的IoU。
  3. 移除所有IoU超过某个阈值(如0.7)的其他框(视为重复检测)。
  4. 在剩下的框中重复步骤1-3,直到处理完所有框。

平均精度均值

平均精度均值(mAP)是目标检测中最核心的综合评估指标。计算过程如下:

  1. 对每个类别单独计算平均精度(AP):
    • 将模型在该类别下所有测试图片上的所有检测框,按置信度从高到低排序。
    • 从最高置信度开始,逐个判断检测框是否为真正例(与某个真实框IoU > 阈值,如0.5,且该真实框未被匹配过)。
    • 随着检测框数量的增加,计算精确率(Precision,真正例数 / 已检测数)和召回率(Recall,真正例数 / 总真实数),绘制P-R曲线。
    • AP即为该P-R曲线下的面积。
  2. 对所有类别的AP取平均,即得到mAP。

mAP综合考虑了模型在不同召回率下的精确率表现,是衡量检测器性能的黄金标准。在实际研究中,常会报告在不同IoU阈值(如0.5:0.95)下计算的平均mAP。

现代目标检测现状与实用建议

目标检测领域发展迅速。如今,通过使用更强大的骨干网络(如ResNet、ResNeXt)、多尺度特征金字塔(FPN)以及更长的训练策略,两阶段和单阶段检测器的性能差距正在缩小,且整体精度大幅提升。

对于实践者的建议:

  • 不要从头实现:现代目标检测系统涉及大量复杂的技巧和调参,自行实现很难达到最优性能。
  • 使用现有框架:推荐使用高质量的开源实现,例如:
    • PyTorch:Detectron2(Facebook发布,包含最新模型)。
    • TensorFlow:TensorFlow Object Detection API。
      这些框架提供了预训练模型和易于使用的接口,可以快速应用于实际项目。

总结

本节课中我们一起学习了目标检测这一核心计算机视觉任务。我们从图像分类的局限出发,探讨了目标检测的独特挑战。随后,我们沿着历史脉络,介绍了从滑动窗口、R-CNN、Fast R-CNN到Faster R-CNN的演进过程,以及更快的单阶段检测器。我们还学习了用于评估检测性能的关键指标:IoU、NMS和mAP。最后,我们了解了当前目标检测的发展现状,并给出了实用的工具选择建议。目标检测是一个复杂但极其重要的领域,为图像理解和现实世界应用奠定了坚实基础。

16:目标检测与分割

在本节课中,我们将继续学习计算机视觉中的定位任务,特别是目标检测的深入细节,并介绍几种不同类型的图像分割任务。我们将从回顾R-CNN系列模型的训练细节开始,然后探讨语义分割、实例分割等任务的核心概念与方法。

目标检测训练流程回顾

上一节我们介绍了R-CNN系列模型,本节中我们来看看这些模型在训练时的具体流程,特别是如何为区域建议分配标签。

在R-CNN风格的模型中,训练时我们接收一张RGB输入图像以及该图像中所有目标的真实边界框(ground truth bounding boxes)和类别标签。

首先,我们会使用一个区域建议方法(如选择性搜索)在输入图像上生成一系列可能包含目标的区域建议框。

接下来,我们需要将每个区域建议框与真实边界框进行匹配,以决定它是正样本、负样本还是中性样本。这个过程通过计算交并比(Intersection over Union, IoU)来完成。

  • 正样本:区域建议框与某个真实边界框的IoU大于一个较高的阈值(例如0.7)。这些框应该被网络分类为包含目标。
  • 负样本:区域建议框与所有真实边界框的IoU都低于一个较低的阈值(例如0.3)。这些框应该被分类为背景。
  • 中性样本:IoU介于高低阈值之间的区域建议框。在训练时通常被忽略,以避免混淆网络。

匹配完成后,每个正样本区域建议框会被分配与其最匹配的真实边界框相同的类别标签。负样本则被分配为“背景”类别。

对于边界框回归任务,只有正样本区域建议框有回归目标。回归目标是能够将该区域建议框的坐标变换到其匹配的真实边界框坐标所需的变换参数。负样本没有回归损失。

在训练时,我们会裁剪出所有正负样本区域对应的图像像素(或特征,如Fast R-CNN),调整到固定尺寸(如224x224),然后输入卷积神经网络。网络需要为每个区域预测两个输出:类别标签和边界框回归变换参数。

R-CNN系列模型训练对比

了解了基础流程后,我们来对比一下R-CNN家族各成员在训练上的异同。

Slow R-CNN与Fast R-CNN:两者的训练目标(分类和回归标签)计算方式完全相同。主要区别在于特征提取和裁剪的顺序。Slow R-CNN先独立裁剪每个区域建议的图像块,再分别进行卷积计算,计算开销大。Fast R-CNN则先对整个图像进行卷积得到特征图,然后从特征图上裁剪出每个区域建议对应的特征,实现了特征共享,大大提升了速度。

Faster R-CNN:这是一个两阶段方法,训练也更为复杂。

  1. 区域建议网络(RPN)阶段:输入是一组预定义的锚点框(anchor boxes)。RPN需要判断每个锚点框是正样本(包含目标)还是负样本(背景),并预测一个将锚点框修正为更准确区域建议的回归变换。其正负样本的匹配逻辑与上述流程一致,只不过匹配对象从外部区域建议换成了锚点框。
  2. 检测网络(Fast R-CNN)阶段:输入是RPN生成的区域建议。此阶段的训练流程与Fast R-CNN完全一样,需要将RPN生成的区域建议与真实边界框进行匹配,以得到分类和回归目标。由于RPN在训练过程中不断更新,此阶段的匹配需要在训练过程中在线(online)进行。

ROI Align操作详解

上一节我们提到了ROI Pooling,本节中我们来看看它的改进版本——ROI Align,这是实现精确定位的关键。

在Fast R-CNN和Faster R-CNN中,我们需要从整张图像的特征图上裁剪出每个区域建议对应的特征。ROI Pooling的做法是:

  1. 将区域建议的坐标投影到特征图上。
  2. 将投影后的区域边界取整(snap)到特征图的网格单元上。
  3. 将取整后的区域均匀划分成固定数量的子区域(如2x2)。
  4. 在每个子区域内进行最大池化,得到固定大小的输出特征。

ROI Pooling存在两个问题:

  1. 特征错位:两次取整操作(边界取整和子区域划分取整)导致池化特征的位置与原始区域建议在图像中的位置存在偏差。
  2. 不可微分性:由于取整操作,梯度无法通过坐标位置回传,即无法对输入的区域建议框坐标进行端到端的优化。

ROI Align旨在解决这些问题,其核心是取消所有取整操作,使用双线性插值进行精确的特征采样

ROI Align的步骤:

  1. 将区域建议的坐标连续地(不取整)投影到特征图上。
  2. 将投影后的区域均匀划分成固定数量的子区域(如2x2)。
  3. 在每个子区域内均匀采样固定数量的点(如每个子区域采样4个点)。
  4. 对于每个采样点,使用双线性插值根据其周围四个特征网格点的值计算该点的特征值。
  5. 对每个子区域内所有采样点的特征值进行池化(如最大池化),得到该子区域的最终输出特征。

公式:对于采样点 (x, y),其双线性插值特征值 f(x, y) 计算如下:
i = floor(x), j = floor(y), u = x - i, v = y - j
f(x, y) = (1-u)*(1-v)*f(i, j) + u*(1-v)*f(i+1, j) + (1-u)*v*f(i, j+1) + u*v*f(i+1, j+1)
其中 f(i, j) 等是特征图上整数坐标 (i, j) 处的特征向量。

ROI Align的优势在于消除了错位,并且整个操作是可微分的,允许梯度流向特征图以及区域建议框的坐标,从而实现了更精确的定位。

语义分割

现在,让我们从检测单个目标转向对图像中每个像素进行分类的任务,即语义分割。

语义分割的目标是为输入图像中的每一个像素分配一个类别标签(如“人”、“车”、“天空”)。需要注意的是,语义分割不区分同一类别的不同实例,它只关心像素的语义类别。

一种低效的方法是使用滑动窗口:以每个像素为中心提取图像块,分别送入CNN分类。但这计算量巨大。

实践中,我们使用全卷积网络(Fully Convolutional Network, FCN)。FCN由卷积层堆叠而成,没有全连接层。输入图像经过一系列卷积、下采样(池化或步长卷积)操作后,再经过一系列上采样操作,最终输出一个与输入图像空间分辨率对应的特征图,其通道数等于类别数。每个空间位置的特征向量经过softmax后,即表示该像素属于各个类别的概率。

训练使用逐像素交叉熵损失

网络中的下采样操作可以扩大感受野并减少计算量,但会降低空间分辨率。为了输出与原图相同分辨率的预测,我们需要进行上采样。

以下是几种常见的上采样方法:

  • 最近邻上采样:将输入特征图中的每个值复制到输出中更大的对应区域。
  • 双线性/双三次插值:使用插值算法计算输出网格点上的值,能产生更平滑的结果。
  • 最大反池化:与最大池化操作配对使用。在最大池化时记录最大值的位置,在反池化时将值放回该位置,有助于保持特征结构的对齐。
  • 转置卷积:一种可学习的上采样方式。其前向传播可以看作是普通卷积反向传播的某种形式。它通过让输入特征图中的每个元素与一个卷积核相乘,并将结果以一定的步长“绘制”到输出特征图上,重叠部分相加,从而实现上采样。

实例分割与Mask R-CNN

语义分割不区分实例,而目标检测只提供边界框。实例分割结合了两者的优点:检测出每个目标实例,并为每个实例预测一个精细的分割掩码

实例分割通常只处理“物体”类别,而不处理“背景”或“材质”类别。

Mask R-CNN 是实例分割的经典框架,它在Faster R-CNN的基础上增加了一个分支。

Mask R-CNN的流程:

  1. 骨干网络提取图像特征。
  2. RPN生成区域建议。
  3. 使用ROI Align从特征图中提取每个区域建议对应的特征。
  4. 每个区域建议的特征被送入两个并行的头网络:
    • 分类与回归头:与Faster R-CNN一样,预测类别和边界框微调。
    • 掩码预测头:一个小型的FCN,为每个区域预测一个 K x m x m 的二值分割掩码,其中 K 是类别数,m 是掩码分辨率(如28x28)。该头对每个类别独立预测一个掩码,并在推理时根据分类头预测的类别选择对应的掩码输出。

训练时,掩码头的目标是在区域建议框内,该实例所属类别对应的前景-背景二值掩码。损失函数是分类损失、回归损失和掩码损失(逐像素sigmoid交叉熵损失)的加权和。

其他相关任务

基于目标检测和分割的框架,可以衍生出许多其他有趣的视觉任务:

  • 全景分割:统一语义分割和实例分割,目标是为图像中所有像素分配标签,并对“物体”类别区分实例,对“材质”类别则不区分。
  • 关键点检测:在实例分割的基础上,进一步预测每个实例上特定关键点(如人的关节、面部特征点)的位置。这可以通过在Mask R-CNN上增加一个关键点头来实现,该头为每个关键点预测一个热力图。
  • 密集描述生成:结合目标检测和图像描述,为图像中多个区域生成自然语言描述。可以在检测器上为每个区域附加一个RNN(如LSTM)来生成句子。
  • 3D形状预测:在检测目标的同时,预测每个目标的3D形状。这同样可以通过在检测框架上增加一个3D形状预测头来实现。

总结

本节课中我们一起深入探讨了目标检测的训练细节,特别是R-CNN系列模型中区域建议与真实框的匹配、标签分配以及损失计算。我们还学习了ROI Align这一改进特征对齐的关键操作。随后,我们介绍了语义分割、实例分割(Mask R-CNN)等像素级预测任务的基本原理和方法。最后,我们了解了如何以目标检测框架为基础,通过添加不同的预测头,来解决关键点检测、密集描述生成等一系列复杂的计算机视觉任务。这些任务构成了现代计算机视觉在定位和分割方向上的核心内容。

18:三维视觉 🎥

在本节课中,我们将要学习如何将第三维空间信息融入神经网络模型。我们将探讨如何从单张图像预测三维形状,以及如何对三维数据进行分类等任务。


课程结构与回顾

上一讲我们快速浏览了多种计算机视觉任务,如语义分割、目标检测和实例分割,这些任务主要关注图像中物体的二维形状和结构。

然而,我们生活的世界是三维的。因此,本节课程将聚焦于如何将第三维空间信息添加到神经网络模型中,使其不仅能处理二维数据,还能理解和处理三维结构。


三维视觉任务概览

我们将主要关注两类三维问题:

  1. 从单张图像预测三维形状:输入一张RGB图像,输出图像中物体的三维形状表示。
  2. 对三维形状进行分类:输入三维数据(如一个三维形状),输出其类别标签。

对于这两类问题,我们均假设处于全监督学习场景,即拥有包含输入图像和对应三维形状(或三维形状和对应标签)的训练集。

请注意,三维计算机视觉领域非常广阔,包含许多非深度学习方法(如运动恢复结构)。本节课仅聚焦于上述两个特定问题。


三维形状的表示方法

“三维形状”是一个宽泛的术语。在实践中,人们使用多种不同的表示方法,各有优缺点。我们将介绍五种常见的表示方法,并探讨如何用神经网络处理和预测它们。

以下是五种表示同一三维形状的不同方式:


1. 深度图

深度图是一种简单的三维形状表示。它为输入图像的每个像素分配一个值,表示该像素对应的真实世界点到相机的距离(单位通常为米)。

  • 公式/表示深度图 = 高度 × 宽度 的网格,每个网格值为深度 d
  • RGB-D图像:将RGB图像与深度通道结合,形成四通道图像。有时被称为 2.5D 图像,因为它无法表示被遮挡物体的部分。

深度图之所以重要,是因为我们可以通过传感器(如微软Kinect、iPhone Face ID)直接捕获这类数据。

从图像预测深度图

我们可以使用全卷积网络来预测深度图,其架构与上节课语义分割任务中使用的类似。

  • 网络架构:输入RGB图像,经过全卷积网络处理,最后一层卷积层输出单通道特征图,将其解释为预测的深度值。
  • 损失函数:需要比较每个像素的预测深度与真实深度。然而,这里存在一个根本性问题:尺度/深度模糊性。从单张2D图像无法区分远处的大物体和近处的小物体。

为了解决尺度模糊性问题,可以使用一种尺度不变损失函数。该函数不惩罚预测深度与真实深度之间的全局乘法尺度差异。

详细数学原理可参考2014年NIPS论文《Depth Map Prediction from a Single Image using a Multi-Scale Deep Network》。

表面法线图

与深度图密切相关的是表面法线图。它为每个像素分配一个三维单位向量,表示该像素对应物体表面的朝向。

  • 预测方法:同样可以使用全卷积网络,输出三通道图像(表示法向量的x, y, z分量)。
  • 损失函数:通常使用基于预测法向量与真实法向量之间夹角的损失函数(如负点积)。

可以训练一个联合网络,同时进行语义分割、深度估计和表面法线估计。

深度图和法线图的局限性:它们都无法表示图像中被遮挡的部分。


2. 体素网格

体素网格将三维空间划分为规则的网格,每个网格单元(体素)用二进制值表示是否被物体占据(类似于3D版的“我的世界”)。

  • 优点:概念简单直观。
  • 缺点:要捕捉物体的精细细节,需要非常高的分辨率,这会带来巨大的计算和内存开销。低分辨率体素网格会丢失细节(如图中椅子的平滑曲线变得块状化)。

对体素网格进行分类

要对体素网格进行分类(例如判断一个形状是椅子还是飞机),可以使用三维卷积神经网络

  • 网络架构:与2D CNN类似,但使用3D卷积作为基本构建块。卷积核是一个三维立方体,在三维空间滑动计算内积。
  • 输入维度:输入是一个四维张量 (通道C, 深度D, 高度H, 宽度W)。对于初始输入,通道数通常为1(表示占用与否)。

从图像预测体素网格

任务是从输入图像预测一个体素网格。

  • 架构挑战:需要将二维特征(H x W x C)转换为三维体素网格(D x H x W x 1)。
  • 一种方法:使用2D CNN处理图像,在末尾通过全连接层“增加”一个空间维度,然后使用3D卷积和上采样层进行细化。
  • 计算问题:3D卷积计算成本极高(复杂度随空间尺寸立方增长)。

体素管表示:一种更高效的方法,仅使用2D卷积来预测体素。将输出通道解释为深度维度。但这种方法牺牲了在深度(Z)方向上的平移不变性。

体素网格的内存问题

高分辨率体素网格内存消耗巨大。例如,一个1024³的网格,每个体素用32位浮点数表示,将占用近4GB内存。

解决方案

  • 八叉树:使用多分辨率表示,稀疏存储高分辨率细节。
  • 嵌套形状层:将形状表示为内外多层体素层的和与差,可以稀疏表示。

3. 隐式表面(符号距离函数)

隐式表面将三维形状表示为一个函数 f(x, y, z)。该函数输入一个三维坐标,输出该点位于物体内部还是外部(或到表面的有符号距离)。

  • 表面:函数值为0.5(或距离为0)的等值面即为物体表面。
  • 表示:使用一个神经网络来学习这个函数。
  • 训练:使用包含内外点样本的数据集训练网络进行二分类。
  • 提取形状:训练后,可以通过在空间网格上采样函数值,并使用等值面提取算法(如Marching Cubes)得到显式网格。

这是一种非常灵活和紧凑的表示方法,但实现和形状提取过程较为复杂。


4. 点云

点云将三维形状表示为一组三维空间中的点,这些点覆盖在物体表面。

  • 优点自适应。可以通过在不同区域分配不同密度的点来表示精细或粗糙的细节。内存使用效率高。是许多传感器(如自动驾驶汽车的激光雷达)的直接输出格式。
  • 缺点:点本身是无穷小的,为了可视化或后续处理,通常需要进行后处理(如转换为网格)。

处理点云输入:PointNet

点云是无序集合。处理点云的神经网络需要对点的顺序保持不变性。PointNet 是一种经典架构。

  • 架构
    1. 使用一个共享的多层感知机独立处理每个点,为每个点提取一个特征向量。
    2. 使用最大池化聚合所有点的特征,得到一个全局特征向量。最大池化对输入顺序不变。
    3. 将全局特征输入到另一个MLP中进行分类或回归。
  • 变体:更复杂的版本会进行多次“独立处理 -> 池化 -> 特征拼接回各点”的迭代。

从图像预测点云

需要一种损失函数来比较两个点云(预测的和真实的)。

倒角距离:一种常用的、可微分的点云比较损失。

  • 公式Chamfer(P, Q) = Σ_{p in P} min_{q in Q} ||p - q||^2 + Σ_{q in Q} min_{p in P} ||p - q||^2
  • 解释:对于P中的每个点,找到Q中最近点的距离并求和;反之亦然,然后相加。
  • 缺点:对异常点敏感(因为使用L2距离)。


5. 三角网格

三角网格是计算机图形学中最常用的表示。它由顶点(三维点)和(连接顶点的三角形)组成。

  • 优点:显式表示表面,非常适合渲染、纹理映射等图形学应用。可以通过顶点密度自适应表示细节。
  • 挑战:用神经网络处理网格结构数据非易事。

Pixel2Mesh:从图像预测网格

一篇2018年的论文提出了一个从单张RGB图像预测三角网格的框架,包含几个关键思想:

  1. 迭代网格细化:不是从头生成网格,而是从一个初始模板网格(如球体)开始,让网络逐步变形顶点位置,最终匹配目标形状。
  2. 图卷积:用于处理网格数据的基本层。它在网格的顶点上操作,每个顶点有特征向量。图卷积层根据每个顶点邻居的特征来更新该顶点的特征,类似于CNN在网格上的滑动。
  3. 顶点对齐特征:为了融入图像信息,将图像通过2D CNN得到特征图。然后将网格的每个顶点投影到图像平面上,通过双线性插值从特征图中采样对应位置的特征,附加到该顶点上。
  4. 损失函数:使用倒角距离。将预测网格和真实网格分别采样成点云,然后计算点云之间的倒角距离。这需要对网格采样操作实现可微分的反向传播。

三维形状的评估与坐标系

评估指标

比较三维形状的质量需要合适的指标:

  • 三维交并比:类似于2D IoU,但可能不够敏感。
  • 倒角距离:如上所述,但对异常值敏感。
  • F1分数(基于点云):更鲁棒的指标。
    • 精度:预测点中有多少在真实点的一定半径内。
    • 召回率:真实点中有多少被预测点在一定半径内覆盖。
    • F1分数:精度和召回率的调和平均数。值在0到1之间,1为完美匹配。

坐标系选择

预测三维形状时,需要决定在哪个坐标系中表示输出。

  • 规范坐标系:为每个物体类别定义固定的前、上、左方向。易于实现,但特征与输入图像对齐较差。
  • 视图坐标系:输出坐标系与输入图像的相机视图对齐。这使得输出特征与输入特征更好对齐,实验表明能带来更好的泛化性能。体素管表示在视图坐标系下非常自然。


数据集与综合应用

常用数据集

  • ShapeNet:大规模3D CAD模型数据集,包含约5万个模型,50个类别。常被渲染成多视角图像用于训练。类似于3D领域的ImageNet,但数据是合成的。
  • Pix3D:包含真实世界图像及其对齐的3D网格模型(主要是IKEA家具)。数据更真实,具有杂乱背景。

综合应用:Mesh R-CNN

Mesh R-CNN结合了目标检测和三维形状预测。

  1. 检测阶段:使用Mask R-CNN检测图像中的物体并预测其掩码。
  2. 三维预测阶段
    • 首先,对每个检测到的物体,使用体素管网络预测一个粗糙的体素形状。
    • 然后,将该体素转换为一个粗糙的网格。
    • 最后,以这个粗糙网格为起点,使用迭代网格细化(类似Pixel2Mesh)生成高保真的三角网格。
  3. 优势:通过体素中间表示,可以预测具有复杂拓扑(如孔洞)的形状,克服了单纯从模板网格变形无法改变拓扑的限制。
  4. 正则化:仅使用倒角损失训练会导致网格畸形。添加一个边长度正则化项(最小化每条边的L2范数)可以使预测的网格更平滑、更美观。
  5. 全模态预测:能够预测物体的完整三维形状,包括图像中被遮挡的部分。

总结 🎯

本节课中,我们一起学习了三维计算机视觉的基础。我们探讨了如何为神经网络添加第三维空间信息,涵盖了预测三维形状对三维形状分类两大任务。

我们详细介绍了五种主要的三维形状表示方法:

  1. 深度图/法线图:使用全卷积网络预测,存在尺度模糊性问题。
  2. 体素网格:使用3D CNN处理或预测,计算和内存成本高。
  3. 隐式表面:用神经网络表示函数,灵活但提取形状复杂。
  4. 点云:使用PointNet等架构处理无序集合,用倒角距离作为损失。
  5. 三角网格:使用迭代细化、图卷积和对齐特征等技术进行预测。

我们还讨论了评估指标(F1分数)、坐标系选择(视图坐标系更优)以及综合系统Mesh R-CNN。三维视觉是一个广阔而活跃的领域,本节课仅为入门提供了基础概念和工具。

19:视频处理

在本节课中,我们将要学习如何使用深度学习处理视频数据。视频本质上是随时间展开的图像序列,这为我们的神经网络模型引入了一个新的维度——时间维度。我们将探讨如何构建能够处理这种时空数据的模型,并了解视频分类等任务的具体实现方法。

从2D图像到3D视频

上一节我们介绍了如何将卷积网络从二维空间扩展到三维空间。本节中,我们来看看另一种增加维度的方法:时间维度。

视频可以看作是一个四维张量,其维度为:时间(T)、通道(C,如RGB)、高度(H)和宽度(W)。我们的目标是构建能够处理这种四维数据的深度神经网络。

视频分类任务

为了具体说明视频架构,我们将以视频分类任务作为主要示例。这个任务类似于图像分类,但输入是一段RGB帧序列,系统需要预测视频中发生的动作或活动的类别标签。

与图像分类类似,系统在训练时知晓一组固定的类别,并使用带有类别标签的视频数据集进行训练。损失函数通常使用交叉熵损失。核心挑战在于如何将四维输入张量转换为可用于训练的分类得分向量。

视频中的识别对象:名词与动词

在处理2D图像时,我们通常识别物体(名词),如狗、猫或汽车。而在处理3D视频序列时,我们通常识别的是动作或活动(动词),如游泳、跑步或跳跃。这种类别性质的差异是视频分类的一个重要特点,且大多数视频数据集的标签都对应人类执行的动作。

视频数据的大规模挑战

视频数据量非常庞大,这是处理视频时的主要约束。例如,未经压缩的标准清晰度视频流每分钟可达约1.5GB,而高清视频则更高。如此大的数据量无法直接放入GPU内存进行处理。

因此,在实际的视频分类中,我们通常处理非常短的视频片段(如3-5秒),并对其进行时间和空间的下采样(例如,将帧率降至5帧/秒,空间分辨率降至112x112)。在训练时,模型在这些短片段上训练;在测试时,模型在原始视频的不同子片段上运行,并通过平均这些子片段的预测结果来得到最终分类。

基线模型:单帧分类

以下是构建视频分类模型时首先应该尝试的简单基线方法。

  • 方法:完全忽略视频的时间结构,将视频的每一帧视为独立的2D图像。使用标准的2D图像识别模型(如ResNet)对每一帧进行独立分类,训练时使用整个视频片段的标签。
  • 测试:在测试时,将此单帧模型运行于长视频的每一帧,然后对所有帧的预测结果进行平均。
  • 重要性:尽管这个方法忽略了时间信息,但它通常是许多视频分类任务中非常强大的基线模型,在实践中往往效果足够好。

融合时间信息:晚期融合

上一节我们介绍了完全独立处理帧的方法。本节中,我们来看看如何将时间信息的融合过程整合到网络架构中,即晚期融合。

晚期融合模型先使用2D CNN独立处理每一帧,提取每帧的特征。然后,在网络的后期(例如,通过全连接层或全局平均池化层)将这些跨时间的特征融合起来,以做出最终的分类决策。这种方法允许网络在训练时就知道测试时会进行时间上的聚合。

然而,晚期融合的一个潜在缺点是,由于在早期就将每帧信息概括为一个向量,网络可能难以建模相邻帧之间低级别的像素运动。

早期融合与3D CNN

鉴于晚期融合可能难以捕捉低级运动,很自然地会想到早期融合方案。

  • 早期融合:在网络的第一个卷积层就将所有时间维度的帧堆叠在通道维度上(形成一个 3*T 通道的“图像”),然后使用标准的2D卷积进行处理。这种方法允许网络在第一层就接触到所有时间信息,但可能过于激进,因为在一层之后就破坏了时间结构。
  • 3D CNN(慢速融合):为了更渐进地融合时空信息,我们可以使用三维卷积神经网络。在网络每一层,我们都保持一个四维张量(通道、时间、高度、宽度),并使用3D卷积和3D池化操作。这样,感受野可以在空间和时间维度上随着网络层数的增加而缓慢增长。

2D卷积与3D卷积的对比

理解2D卷积(用于早期融合)和3D卷积在处理时空数据时的区别非常重要。

在早期融合中使用的2D卷积,其滤波器在空间上是局部的,但在时间上覆盖了整个输入长度。这导致其缺乏时间平移不变性:要检测相同的时间模式(如颜色变化)发生在不同时间点,需要学习不同的滤波器。

而3D卷积的滤波器在空间和时间上都是局部的。通过在整个时空网格上滑动,单个3D卷积滤波器就能检测到发生在不同时间点的相同时间模式,因此表征效率更高。此外,学习到的3D卷积滤波器本身可以可视化为小的运动视频片段。

利用显式运动信息:双流网络

人类仅凭运动信息就能识别很多动作。受此启发,双流网络显式地将运动信息作为网络的一个输入源。

双流网络包含两个并行的CNN分支:

  1. 空间流:处理视频的外观信息,输入是视频的单个帧。
  2. 时间流:处理视频的运动信息,输入是相邻帧之间计算出的光流场(包含x和y方向的位移)。多个光流帧在通道维度堆叠后,使用早期融合的2D CNN进行处理。

两个分支独立进行预测,在测试时对它们的预测概率分布进行平均。实验表明,仅使用时间流(运动信息)就能取得很好的效果,结合空间流后性能进一步提升。

处理长时依赖:RNN与自注意力

之前介绍的方法主要关注局部时间建模。为了处理更长范围的时间依赖,我们可以结合循环神经网络(RNN)。

方法:先用CNN(2D或3D)提取每帧或每个短片段的空间(或时空)特征,然后将这些特征序列输入到RNN(如LSTM)中,由RNN来融合长时信息并做出最终预测。一个实用的技巧是使用预训练好的CNN(如C3D)作为固定特征提取器,只训练顶部的RNN,以节省内存。

另一种更强大的方法是使用自注意力机制。我们可以将3D CNN中间层的四维特征张量重塑为一组向量,然后应用标准的自注意力操作(通过1x1x1卷积计算Key、Query、Value)。这种被称为非局部块的模块可以插入到3D CNN中,实现全局的时空信息融合。通常会用残差连接,并将最后一个卷积层初始化为零,使整个块初始化为恒等映射,便于插入预训练模型。

从2D到3D的架构迁移:膨胀网络

设计优秀的3D CNN架构并非易事。一个巧妙的思路是“膨胀”现有的成熟2D CNN架构。

膨胀方法:将一个2D CNN架构中的所有2D卷积核和2D池化核,直接沿着时间维度复制,变成3D卷积核和3D池化核。例如,一个3x3卷积核膨胀为3x3x3卷积核。

权重初始化:为了利用在图像上预训练的2D模型权重,我们可以将2D卷积核在时间维度上复制T次,然后除以T进行初始化。这样,如果一个视频的所有帧都是同一张图像,那么膨胀后的3D CNN的输出将与原始2D CNN在该图像上的输出相同。这允许我们用图像预训练模型来初始化视频模型,并取得良好效果。

当前前沿:SlowFast 网络

最新的研究趋向于摒弃外部计算的光流,并更优雅地结合多速率处理。SlowFast 网络是当前视频识别领域的先进方法。

SlowFast 网络包含两个并行分支,都处理原始像素:

  1. 慢通路:以低帧率运行,但使用高容量的网络(更多通道),主要捕捉外观和语义信息。
  2. 快通路:以高帧率运行,但使用轻量级的网络(更少通道),主要捕捉快速变化的运动信息。

两个分支之间通过横向连接在多个阶段进行信息融合。这两个分支本身可以是膨胀后的ResNet等架构,并可加入非局部块。SlowFast 网络集成了本节课讨论的多种技术,在多项基准测试中达到了领先水平。

超越分类:其他视频任务

视频分类只是视频理解的一个起点。还有许多其他任务,例如:

  • 时序动作定位:在长视频中检测动作发生的起止时间。
  • 时空动作检测:在视频的每一帧中检测人物的边界框,并同时判断他们在整个时间段内执行的动作。这结合了空间检测和时间检测,是一个极具挑战性的任务。

可视化视频模型

与图像模型类似,我们可以通过优化输入来可视化视频模型关注的内容。例如,可以分别优化外观流(生成最大化某类得分的图像)和时间流(生成最大化某类得分的光流场),从而直观理解模型如何利用外观和运动信息进行决策。

本节课中我们一起学习了处理视频数据的多种深度学习模型。我们从简单的单帧基线开始,逐步深入到融合时间信息的晚期融合、早期融合和3D CNN。接着,我们探讨了利用显式运动信息的双流网络、处理长序列的RNN和自注意力机制,以及通过膨胀利用现有2D架构的技巧。最后,我们了解了当前先进的SlowFast网络和一些更复杂的视频理解任务。这些方法为分析和理解视频内容提供了强大的工具。

19:生成模型(第一部分) 🧠

在本节课中,我们将要学习生成模型的基本概念。我们将探讨监督学习与无监督学习的区别,以及判别式模型与生成式模型的核心差异。然后,我们将深入介绍两种具体的生成模型:自回归模型和变分自编码器。通过本讲,你将理解生成模型如何学习数据的概率分布,并能够用于生成新数据或学习数据的潜在结构。


监督学习与无监督学习 📊

上一讲我们讨论了处理视频的卷积神经网络。本节中,我们来看看机器学习中一个更根本的区分:监督学习与无监督学习。

监督学习

监督学习是我们本学期大部分时间都在使用的方法。其设置是,我们获得的数据由样本对 (X, Y) 组成。其中 X 是输入数据(如图像),Y 是我们希望从该输入预测的输出(如“猫”的标签)。监督学习的目标是学习一个从输入 X 映射到输出 Y 的函数。

以下是监督学习的几个典型例子:

  • 图像分类X 是图像,Y 是类别标签。
  • 目标检测X 是图像,Y 是图像中带有类别标签的一组边界框。
  • 语义分割X 是图像,Y 是图像中每个像素的语义类别标签。
  • 图像描述X 是图像,Y 是由人撰写的对该图像的自然语言描述。

监督学习的关键在于,它需要大量由人工标注的数据集。

无监督学习

与监督学习不同,无监督学习只提供原始数据 X(例如大量图像),而不提供任何真实标签或输出 Y。其目标是从这些数据中构建一个模型,以揭示数据中隐藏的结构。

无监督学习的优势在于它不需要人工标注。其理想目标是能够利用网络上所有可用的数据,训练出能发现其中结构的模型,而无需人工逐一标注。

以下是几个无监督学习任务的例子:

  • 聚类:将数据样本分成不同的簇。
  • 降维:将高维数据投影到低维子空间,如主成分分析。
  • 自编码器:一种试图重建其输入的特殊神经网络,从而学习对下游任务有用的潜在表示。
  • 密度估计:学习一个概率分布,使其在训练数据样本上赋予高概率,而在未出现的数据点上赋予低概率。

总结:监督学习依赖大量标注数据,效果显著;无监督学习则旨在利用大量未标注数据学习有用的潜在结构。生成模型是我们尝试解决无监督学习任务的一种重要途径。


判别式模型与生成式模型 🎯

除了监督/无监督的区分,我们还需要理解机器学习模型的另一个重要区别:判别式模型与生成式模型。这个区别在于模型试图学习的底层概率结构类型。

判别式模型

判别式模型试图学习一个条件概率分布 P(Y|X)。它预测给定输入 X(如图像)时,标签 Y 的概率。这与监督学习紧密相连。例如,一个图像分类器输入图像,输出所有可能标签的概率分布。

生成式模型

生成式模型试图学习一个无条件概率分布 P(X)。它直接对数据 X(如图像)本身的概率分布进行建模。这意味着模型需要为任何可能的输入图像分配一个“可能性”值,表明该图像在世界上存在的可能性有多大。

条件生成式模型

条件生成式模型试图学习一个条件概率分布 P(X|Y)。它学习在给定特定标签 Y 的条件下,所有可能图像 X 的概率分布。

核心区别:概率归一化与竞争

概率密度函数 P(·) 必须满足归一化条件,即其积分(或求和)等于1。这个归一化约束意味着概率分布支持范围内的不同元素之间会竞争概率质量

  • 判别式模型 P(Y|X) 中,竞争发生在不同标签 Y 之间(针对同一个输入 X),而不同图像 X 之间没有竞争。这导致判别式模型无法判断输入图像是否“不合理”,因为它必须为任何输入图像输出一个归一化的标签概率分布。
  • 生成式模型 P(X) 中,竞争发生在所有可能的图像 X 之间。为了给合理图像分配高概率,它必须给不合理图像分配低概率。因此,生成式模型有能力拒绝它认为不合理的样本。
  • 条件生成式模型 P(X|Y) 中,对于每个可能的标签 Y,都会在所有图像 X 之间引发一场独立的竞争。因此,它既可以用于分类(通过比较不同 Y 下的 P(X|Y)),也有能力拒绝在所有类别下概率都很低的异常样本。

模型间的联系

根据贝叶斯定理,这些模型并非完全独立:
P(X|Y) = [P(Y|X) * P(X)] / P(Y)
其中:

  • P(Y|X) 是判别式模型。
  • P(X) 是无条件生成式模型。
  • P(Y) 是标签的先验分布(可从训练集中统计得到)。

这表明,如果我们能构建判别式模型和生成式模型,就可以组合它们来构建条件生成式模型。

模型的应用

  • 判别式模型:用于为新数据分配标签,也可用于监督特征学习(如使用ImageNet预训练模型作为特征提取器)。
  • 生成式模型
    1. 异常检测:识别不太可能出现的输入。
    2. 无监督特征学习:学习有用的特征表示。
    3. 数据合成:从学习到的分布中采样,生成新的数据样本。
  • 条件生成式模型
    1. 可用于分类,同时能拒绝异常值。
    2. 可根据条件(如标签、句子)合成新的数据样本。


生成式模型家族树 🌳

生成式模型是一个庞大的领域。一个重要的分类标准是模型是否具有显式密度函数

  • 显式密度模型:训练后,可以输入一个新图像 X,并计算出其概率密度值 P(X)
    • 易处理的密度模型:可以高效精确地计算密度值。
    • 近似密度模型:无法高效精确计算,但可以计算密度的近似值或下界(如变分方法)。
  • 隐式密度模型:训练后,无法计算出输入图像的确切密度值,但可以从学习到的分布中采样生成新图像(如生成对抗网络)。

在本讲和下一讲中,我们将介绍三种生成式模型:

  1. 自回归模型:属于显式且易处理的密度模型
  2. 变分自编码器:属于显式但近似密度模型
  3. 生成对抗网络(下一讲):属于隐式密度模型


自回归模型 🔄

自回归模型是一种具有显式且易处理密度函数的生成式模型。其核心思想是:我们希望写出一个参数化函数 f(X; W),它输入数据 X 和可学习的权重 W,输出该图像的密度函数值 P(X)。我们通过最大化训练数据集中所有样本的似然(或对数似然)来训练这个模型。

目标:找到权重 W,最大化 ∑ log P(X_i; W)

自回归分解

为了具体写出密度函数 P(X),自回归模型采用了一种特定的分解方式。假设数据 X 由多个部分构成(如图像的像素 x1, x2, ..., xn)。根据概率链式法则,联合分布可以分解为:
P(X) = P(x1) * P(x2|x1) * P(x3|x1, x2) * ... * P(xn|x1, ..., x_{n-1})
即,整个序列的概率等于第一个元素的概率,乘以给定第一个元素后第二个元素的概率,依此类推。

这正好是循环神经网络的结构!RNN在每个时间步输入一个标记(如一个像素),并预测下一个标记的概率,且当前预测隐式地依赖于之前的所有序列。

像素RNN与像素CNN

我们可以利用这个思想来对图像建模,将图像像素按某种顺序(如从左到右、从上到下)展开成序列。

  • 像素RNN:使用RNN(如LSTM)按序生成像素。每个像素的隐藏状态依赖于其上方和左侧像素的隐藏状态。这种方法建模能力强,但由于序列依赖性,训练和采样速度非常慢
  • 像素CNN:使用掩码卷积来模拟这种依赖关系。卷积核只查看当前像素左侧和上方的像素(在感受野内)。这使得训练过程可以并行化,速度远快于像素RNN,但在采样时仍需逐个像素生成,因此仍然较慢。

自回归模型的优缺点

优点

  • 具有显式密度函数,便于评估(例如,可以在未见过的测试图像上计算对数似然)。
  • 生成的样本具有合理的多样性和结构。

缺点

  • 采样速度慢,因为需要按序生成每个像素。
  • 生成的图像分辨率和高频细节可能有限。

变分自编码器 🌀

变分自编码器是一种具有显式但近似密度函数的生成式模型。与自回归模型不同,VAE无法高效计算精确的密度值 P(X),但可以计算其变分下界。我们通过最大化这个下界来训练模型,希望这也能提升真实的密度。

从自编码器到变分自编码器

首先,理解“自编码器”部分:

  • 标准自编码器:一种无监督学习方法,旨在学习数据的低维潜在表示 Z。它包含一个编码器(将输入 X 映射到 Z)和一个解码器(试图从 Z 重建 X)。通过最小化重建误差进行训练,并利用Z的维度远小于X这一“瓶颈”来迫使网络学习有意义的压缩表示。训练后,丢弃解码器,使用编码器为下游任务提取特征。然而,它不是概率模型,无法用于生成新样本

VAE是自编码器的概率升级版,它希望:

  1. 保留学习潜在特征 Z 的能力。
  2. 能够从训练好的模型中采样生成新数据。

VAE的核心思想

VAE假设每个训练数据 X 都是由某个我们无法观测的潜在变量 Z 生成的。模型包含两部分:

  1. 解码器(生成模型) P_θ(X|Z):输入潜在变量 Z,输出一个关于图像 X 的概率分布(通常假设为对角高斯分布,即每个像素独立,分别输出均值和方差)。θ 是解码器网络的参数。
  2. 先验分布 P(Z):通常假设为简单的标准正态分布 N(0, I)

在测试时,我们可以从先验 P(Z) 中采样一个 Z,然后通过解码器 P_θ(X|Z) 生成一个新的图像 X

训练挑战与变分推断

训练的目标是最大化数据似然 P(X)。根据模型定义,P(X) = ∫ P_θ(X|Z) P(Z) dZ。这个积分难以计算(因为 Z 是高维的)。

根据贝叶斯定理,P(X) 也可以写成 P_θ(X|Z) P(Z) / P_θ(Z|X)。其中后验分布 P_θ(Z|X) 同样难以计算。

VAE的解决方案是引入一个编码器网络(推断网络) Q_φ(Z|X),它输入图像 X,输出一个关于潜在变量 Z 的分布(通常也是对角高斯)。φ 是编码器网络的参数。这个编码器用于近似真实但难以计算的后验 P_θ(Z|X)

通过一些数学推导(应用贝叶斯定理、对数变换、期望和KL散度的性质),我们可以得到数据对数似然的一个变分下界
log P(X) ≥ E_{Z~Q_φ(·|X)}[log P_θ(X|Z)] - D_KL(Q_φ(Z|X) || P(Z))
这个下界称为证据下界

  • 第一项是重建项:在编码器产生的 Z 分布下,解码器重建输入 X 的期望对数似然。这鼓励解码器更好地重建数据。
  • 第二项是KL散度项:衡量编码器输出的分布 Q_φ(Z|X) 与先验分布 P(Z) 的差异。这鼓励潜在变量 Z 的分布接近我们设定的简单先验。

VAE的训练

VAE的训练过程就是联合优化编码器参数 φ 和解码器参数 θ,以最大化这个证据下界。虽然我们无法直接最大化真实的 log P(X),但最大化其下界是一种有效的近似方法。

通过这种训练,编码器学会了将图像映射到有意义的潜在空间,解码器学会了从该空间生成图像。同时,由于潜在空间被正则化到接近先验分布,我们可以在训练后轻松地从 P(Z) 中采样并输入解码器来生成新图像。

注意:VAE中“像素条件独立”的假设是一个较强的限制,这通常导致其生成的图像相对模糊。然而,其数学框架优美,并且学习到的潜在空间通常具有很好的解释性和插值特性。


总结 📝

本节课中我们一起学习了生成模型的基础知识。我们首先区分了监督学习与无监督学习,然后深入探讨了判别式模型、生成式模型和条件生成式模型在概率框架下的根本区别。接着,我们介绍了生成式模型的分类图谱。

我们详细讲解了两种生成式模型:

  1. 自回归模型:通过链式法则分解联合概率,利用RNN或掩码CNN构建显式且易处理的密度函数。其优点是密度可计算,但采样速度慢。
  2. 变分自编码器:通过引入编码器来近似难以计算的后验分布,并最大化证据下界来训练。它是一种显式但近似密度模型,能够学习有结构的潜在空间并用于生成,但生成的图像可能较模糊。

下一讲我们将继续探讨变分自编码器的训练细节,并介绍另一种强大的生成式模型——生成对抗网络。

20:生成模型(第二部分)🎨

在本节课中,我们将继续讨论生成模型,涵盖变分自编码器(VAE)和生成对抗网络(GAN)的核心概念、工作原理、训练方法以及应用实例。我们将从回顾上一讲的内容开始,逐步深入这两种强大的生成模型。

课程回顾与概述

上一讲我们介绍了生成模型的基本概念,并讨论了自回归模型。我们回顾了监督学习与无监督学习的区别,以及判别式模型与生成式模型在概率建模上的不同。自回归模型通过直接参数化数据的似然函数来生成数据,但其生成过程较慢,且不学习数据的潜在表示。

本节中,我们将探讨另外两类重要的生成模型:变分自编码器和生成对抗网络。它们采用了不同的策略来建模和生成数据。

变分自编码器(Variational Autoencoders)

变分自编码器是一种概率生成模型,它通过学习数据的潜在表示(latent representation)来生成新样本。与自回归模型不同,VAE引入了潜在变量 Z,旨在捕获数据的高层语义特征。

核心架构与目标

VAE包含两个神经网络:编码器(Encoder)和解码器(Decoder)。

  • 编码器 Q_φ(z|x):输入数据 x,输出潜在变量 z 的概率分布(通常假设为对角高斯分布)。
  • 解码器 P_θ(x|z):输入潜在变量 z,输出数据 x 的概率分布。

由于直接最大化数据似然 P(x) 在引入潜在变量后变得难以处理,VAE转而最大化其变分下界(Evidence Lower Bound, ELBO):

公式:ELBO

log P(x) ≥ E_{z∼Q_φ(z|x)}[log P_θ(x|z)] - D_KL(Q_φ(z|x) || P(z))

其中:

  • E_{z∼Q_φ(z|x)}[log P_θ(x|z)]重构项,鼓励解码器能根据潜在编码 z 较好地重构输入数据 x
  • D_KL(Q_φ(z|x) || P(z))KL散度项,鼓励编码器输出的分布 Q_φ(z|x) 接近我们预设的先验分布 P(z)(通常为标准正态分布 N(0, I))。

这两个目标相互制约:重构项希望潜在编码包含足够信息以重建输入,而KL散度项则希望潜在编码的分布尽量简单(接近标准正态)。

训练过程

以下是训练VAE的具体步骤:

  1. 前向传播:将小批量数据 x 输入编码器,得到潜在分布的均值 μ 和方差 σ^2(假设为对角高斯)。
  2. 采样:从该分布中采样一个潜在向量 z。为了允许梯度回传,我们使用“重参数化技巧”(Reparameterization Trick):z = μ + σ ⊙ ε,其中 ε ∼ N(0, I)
  3. 解码:将采样得到的 z 输入解码器,得到重构数据 的分布参数。
  4. 计算损失:损失函数即为负的ELBO,需要计算重构损失(如二元交叉熵或均方误差)和KL散度损失。
  5. 反向传播与优化:通过梯度下降同时更新编码器和解码器的参数,以最大化ELBO。

VAE的能力与应用

一旦VAE训练完成,我们可以利用它做以下几件事:

  • 生成新数据:从先验分布 P(z)(如 N(0, I))中采样一个 z,然后通过解码器生成新的数据样本 x
  • 数据编辑:将一张真实图片输入编码器得到其潜在编码 z,通过有目的地修改 z 的某些维度,再输入解码器,可以得到编辑后的图片(如改变人脸表情、光照方向等)。
  • 潜在空间插值:在两个潜在向量 z1z2 之间进行线性插值,并将插值点输入解码器,可以得到在两个原始图像之间平滑过渡的生成序列。

VAE的优点是提供了一个结构化的潜在空间,并且训练相对稳定。但其生成的图像有时会比较模糊。

生成对抗网络(Generative Adversarial Networks)

生成对抗网络采用了与VAE完全不同的思路:它放弃了显式建模数据分布 P(x),而是专注于学习如何从该分布中采样。GAN通过一个“对抗”的过程来训练生成器,使其能产生足以“欺骗”判别器的逼真样本。

核心思想:对抗博弈

GAN包含两个相互博弈的神经网络:

  • 生成器(Generator) G:输入一个从简单先验分布(如均匀分布或正态分布)中采样的噪声向量 z,输出一个合成数据样本 G(z)。其目标是生成尽可能真实的数据,以骗过判别器。
  • 判别器(Discriminator) D:输入一个数据样本(可以是真实的,也可以是生成器合成的),输出一个标量,表示该样本是来自真实数据分布(输出接近1)还是来自生成器分布(输出接近0)。其目标是尽可能准确地区分真实样本和伪造样本。

二者构成了一个极小极大博弈(minimax game),其价值函数 V(G, D) 为:

公式:GAN目标函数

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)))]

训练过程与挑战

GAN的训练是一个交替优化的过程:

  1. 固定生成器G,更新判别器D:最大化 V(D, G)。这相当于训练一个二分类器,使其能正确区分真实数据和当前生成器产生的假数据。
  2. 固定判别器D,更新生成器G:最小化 V(D, G)。这相当于训练生成器,使其产生的数据能让判别器 D 给出高的“真实”概率(即 D(G(z)) 接近1)。

在实际训练中,早期生成器能力很弱,D(G(z)) 接近0,导致 log(1 - D(G(z))) 的梯度很小(梯度消失问题)。因此,常采用一个非饱和(non-saturating)的替代目标来训练生成器:最大化 log D(G(z))

GAN训练极具挑战性,常面临模式崩溃(mode collapse)、训练不稳定、难以调试(损失曲线无法直观反映训练状态)等问题。

GAN的强大能力与应用

尽管训练困难,GAN在生成高质量、高分辨率图像方面取得了巨大成功,并催生了大量变体和应用:

  • 高质量图像生成:从DCGAN、Progressive GAN到最新的StyleGAN,生成的图像分辨率从64x64提升到1024x1024,逼真度惊人。
  • 条件生成:通过向生成器和判别器输入额外的条件信息(如图像类别、文本描述、另一张图片),可以实现可控的图像生成、图像到图像的翻译(如语义图→照片、马→斑马)、超分辨率、图像修复等。
  • 潜在空间操作:与VAE类似,GAN的潜在空间 z 也常常具有可解释性,可以通过向量运算(如“微笑女人 - 中性女人 + 中性男人 = 微笑男人”)来编辑生成图像的属性。
  • 跨领域应用:GAN不仅用于图像,还可用于生成视频、3D模型、音乐、文本,甚至用于预测行人未来轨迹等任务。

总结与展望

本节课我们一起学习了生成模型中两个至关重要的范式:

  1. 变分自编码器(VAE):通过最大化数据似然的变分下界,联合学习数据的潜在表示和生成模型。它提供了结构化的潜在空间,便于插值和编辑,但生成的图像可能较模糊。
  2. 生成对抗网络(GAN):通过生成器和判别器的对抗博弈,直接学习从噪声到逼真数据的映射。它放弃了显式的似然计算,专注于采样,能生成极其清晰、高质量的图像,但训练过程复杂且不稳定。

此外,我们还看到了结合两者优点的前沿工作(如VQ-VAE-2),这可能是未来生成模型发展的一个方向。生成模型是一个快速发展的领域,在图像合成、编辑、数据增强、跨模态理解等方面有着广阔的应用前景。

22:强化学习入门 🎮

在本节课中,我们将学习机器学习的第三种主要范式——强化学习。我们将了解强化学习的基本概念、其与监督学习和无监督学习的区别,并介绍两种基础的强化学习算法:Q学习和策略梯度。

概述

强化学习关注的是构建能够与世界(环境)交互的智能体。智能体通过观察环境状态、执行动作并接收奖励信号来学习如何最大化其在整个生命周期中获得的累积奖励。这与我们之前学习的监督学习(预测输入-输出对)和无监督学习(发现数据隐藏结构)有本质区别。

强化学习基础

强化学习问题涉及两个主要角色:智能体环境。它们通过以下方式进行交互:

  1. 状态:环境向智能体提供关于当前世界状况的信息 S_t
  2. 动作:智能体基于当前状态选择一个动作 A_t 来执行。
  3. 奖励:环境根据智能体的动作给予一个即时奖励信号 R_t,表明该动作的好坏。

这个过程会随时间不断重复,形成一个交互序列。智能体的目标是学习一个策略,以最大化其获得的累积折扣奖励

强化学习 vs. 监督学习

虽然形式上类似,但强化学习在几个关键方面比监督学习更具挑战性:

  • 随机性:状态、奖励和状态转移都可能具有随机性。
  • 信用分配:当前获得的奖励可能是由很久以前采取的动作导致的,智能体需要判断哪个动作应对奖励负责。
  • 不可微分性:我们无法通过真实世界进行反向传播来计算奖励相对于模型参数的梯度。
  • 非平稳性:智能体训练时所看到的数据分布会随着其自身策略的改进而改变。

马尔可夫决策过程

我们使用马尔可夫决策过程 来形式化描述强化学习环境。一个MDP由以下五元组定义:
(S, A, R, T, γ)

  • S: 所有可能状态的集合。
  • A: 所有可能动作的集合。
  • R: 奖励函数,给定状态和动作,输出一个奖励分布。
  • T: 状态转移函数,给定当前状态和动作,输出下一个状态的分布。
  • γ: 折扣因子,用于权衡即时奖励和未来奖励的重要性(0 ≤ γ ≤ 1)。

MDP具有马尔可夫性质,即下一状态和奖励的分布仅取决于当前状态和动作,而与历史状态无关。

智能体的目标被形式化为寻找一个最优策略 π*,以最大化期望累积折扣奖励:
E[∑_{t=0}^{∞} γ^t R_t]

价值函数与Q函数

为了评估策略的好坏,我们引入两个重要概念:

  • 价值函数 V^π(s):表示从状态 s 开始,遵循策略 π 所能获得的期望累积奖励。
  • Q函数 Q^π(s, a):表示从状态 s 开始,执行动作 a,然后遵循策略 π 所能获得的期望累积奖励。

最优Q函数 Q* 满足贝尔曼方程
Q*(s, a) = E_{s‘, r}[r + γ * max_{a’} Q*(s‘, a’)]

这个方程是许多强化学习算法的核心。

深度Q学习

当状态和动作空间很大或连续时,我们无法为每个(s, a)对存储Q值。解决方案是使用一个神经网络(如卷积神经网络)来近似Q函数,即深度Q网络

我们使用贝尔曼方程来构造损失函数,训练网络参数 θ
L(θ) = E_{(s,a,r,s‘)}[(Q(s, a; θ) - (r + γ * max_{a’} Q(s‘, a’; θ)))^2]

通过最小化这个损失函数,我们可以训练网络逼近最优Q函数。然后,最优策略就是对于每个状态 s,选择使 Q(s, a; θ) 最大的动作 a

一个著名的成功案例是使用DQN玩Atari游戏。网络以最近几帧游戏画面作为输入,输出每个游戏手柄动作对应的Q值,并通过上述方法进行训练。

策略梯度方法

另一种方法是直接学习策略本身。我们训练一个神经网络(策略网络),输入状态 s,输出动作的概率分布 π(a|s; θ)

目标是最大化期望奖励 J(θ) = E[∑ γ^t r_t]。虽然无法直接计算奖励对参数的梯度,但我们可以使用策略梯度定理推导出一个可计算的梯度估计:

∇_θ J(θ) ≈ E[ (∑ γ^t r_t) * ∇_θ log π(a_t|s_t; θ) ]

基于此的算法步骤如下:

  1. 用随机参数 θ 初始化策略网络。
  2. 运行当前策略 π_θ 与环境交互,收集多条轨迹(状态、动作、奖励序列)。
  3. 使用收集的数据和上述公式计算梯度估计 ∇_θ J(θ)
  4. 执行梯度上升步骤:θ ← θ + α * ∇_θ J(θ)
  5. 返回步骤2,重复进行。

策略梯度的直观解释是:如果一条轨迹获得了高奖励,就增加该轨迹中所有动作的概率;如果获得低奖励,则降低这些动作的概率。

其他方法与总结

除了Q学习和策略梯度,强化学习领域还有许多其他方法:

  • 演员-评论家方法:结合了价值函数(评论家)和策略函数(演员)。
  • 基于模型的强化学习:尝试学习环境的状态转移模型,然后基于模型进行规划。
  • 模仿学习:通过观察专家演示数据,以监督学习方式训练策略。
  • 逆强化学习:从专家演示中推断出潜在的奖励函数。

强化学习已取得显著成就,例如AlphaGo系列在围棋上超越人类冠军,以及AlphaStar在《星际争霸2》、OpenAI Five在《Dota 2》中的出色表现。

此外,强化学习的思想还可用于训练包含不可微分组件的复杂神经网络架构,例如硬注意力机制,其中网络需要决定在每一步关注输入的确切位置。

本节课总结

本节课我们一起快速浏览了强化学习领域。我们了解到强化学习是一种让智能体通过与环境交互来学习的独特范式。我们介绍了用于形式化问题的马尔可夫决策过程,并学习了两种基础的算法:深度Q学习和策略梯度。这些算法为构建能够玩视频游戏、下棋甚至控制机器人的智能系统奠定了基础。

23:课程回顾与开放性问题 🎓

在本节课中,我们将回顾本学期所涵盖的所有核心知识点,并探讨计算机视觉与深度学习领域当前面临的一些重大开放性问题与挑战。

课程内容回顾 📚

上一节我们介绍了本课程的目标,本节中我们来系统地回顾本学期学习的主要内容。

计算机视觉与数据驱动方法

计算机视觉的目标是构建能够处理、感知和理解各种视觉数据的人工系统。其核心挑战在于语义鸿沟:计算机只能看到巨大的数字网格,而我们需要让它们理解图像中的语义内容。视觉数据非常复杂,视角、光照、物体形变或遮挡都会导致像素网格发生巨大变化,但我们的语义理解需要保持一致。

为了解决这个问题,我们采用了数据驱动方法和机器学习。对于所有任务,我们收集带有标签的图像数据,使用机器学习算法(主要是神经网络)将数据集中的知识“压缩”成一个分类器,然后使用该分类器对新的图像进行预测和评估。这种基于机器学习的范式已成为解决大多数计算机视觉问题的主流方法。

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

我们熟悉的模型是使用深度卷积神经网络来解决计算机视觉中的各种问题。然而,仅有模型是不够的,我们还需要用于训练模型的大型数据集。ImageNet数据集(2009年发布)就是一个极具影响力的大规模数据集,为训练机器学习模型提供了基础。

2012年,我们将大型数据集与强大的卷积神经网络模型相结合,并借助GPU等计算设备速度的不断提升,推动了计算机视觉领域的巨大进步。以ImageNet数据集上的表现为衡量标准,错误率从2010年的高位持续下降,到2017年时已降至极低水平,这直接导致了全球计算机视觉研究的爆炸式增长。

历史脉络与理论基础

深度学习的成功并非一蹴而就,它建立在过去几十年众多研究者智慧的基础之上:

  • 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,以表彰他们在普及和开创深度学习方法方面的贡献。

本学期知识体系详解

以下是本学期我们学习的具体内容:

1. 机器学习基础
我们首先从简单的数据驱动方法开始。

  • K最近邻分类器:引入了训练/测试集划分、超参数调整等机器学习流程的关键概念。
  • 线性分类器:我们的第一个参数化分类器。其形式为 scores = W * X + b,我们通过数据学习权重矩阵 W。这种为模型设定参数化形式,然后使用数据拟合模型参数的模式,贯穿了本学期所有的机器学习方法。

2. 优化方法
一旦有了模型,我们需要找到最优的参数 W

  • 梯度下降/随机梯度下降:通过计算梯度并沿着损失函数的地形“下坡”来寻找模型权重,损失函数量化了模型在数据上的表现。
  • 优化器改进:原始SGD会遇到局部最小值、鞍点、病态优化和随机性等问题。我们引入了动量Adam等优化器改进来克服这些问题。从SGD到带动量的SGD,正是AlexNet(2012)与LeCun网络(1998)之间的关键小技巧之一。

3. 神经网络模型
我们掌握了优化思想后,开始学习神经网络模型。

  • 全连接神经网络:其基本思想与线性分类器相同,输入 X,输出得分 Y,并使用梯度下降学习权重。一种解释是,它们在第一层学习一组可重用的模板,然后在第二层重新组合这些模板以识别不同的类别。这显示了神经网络比线性分类器更强大。
  • 通用近似定理:从理论上证实,使用ReLU非线性激活函数的单隐藏层全连接神经网络,可以近似任何满足许多数学约束的连续函数。这表明神经网络是一类非常强大的函数近似算法。

4. 卷积神经网络
为了构建专门处理计算机视觉空间结构问题的模型,我们引入了卷积神经网络。

  • 核心组件:在激活函数基础上,增加了卷积算子(保持输入图像空间结构并在不同空间位置共享权重)、池化层(减少表征空间尺寸并引入不变性)以及归一化层(如批归一化、实例归一化、组归一化,使深度网络训练更高效)。

5. 网络架构设计
掌握了基本组件后,我们需要知道如何将它们组合起来。

  • 经典架构:我们讨论了从AlexNetVGGGoogLeNetResNet的演进。ResNet最终允许我们训练数百层深的模型。
  • 效率导向:近年来的一大趋势是关注模型效率。我们学习了如ResNeXtMobileNet等架构,它们通过使用分组卷积等技术来提升计算效率。

6. 计算图与软件硬件
为了形式化地理解这些复杂模型,我们引入了计算图的概念。

  • 计算图:将神经网络模型表示为图数据结构,节点是原始算子,边传递数据。
  • 反向传播:一种高效计算任意复杂计算图中梯度的算法。它将全局梯度计算问题转化为局部问题,每个节点只需知道如何计算其局部梯度。
  • 硬件与软件:硬件从CPU发展到GPU,再到专门的神经网络计算硬件(如Google的TPU)。软件方面,我们深入使用了PyTorch(动态计算图),也了解了TensorFlow(静态计算图)等框架。

7. 模型训练实用技巧
我们深入探讨了让模型实际工作的诸多细节。

  • 激活函数:如ReLU, Leaky ReLU等。
  • 预处理与权重初始化:正确的初始化方式至关重要。
  • 数据增强:通过在训练时人工扩展数据,将额外的不变性“烘焙”进模型。
  • 正则化:旨在约束模型容量,防止过拟合,提升在未见数据(测试集)上的表现。常见范式是向模型处理中添加随机性,然后在测试时平均掉这种随机性。具体技术包括:
    • 随机池化
    • Dropout
    • DropConnect
    • 批归一化(也属于此范式)
  • 学习率与超参数:我们讨论了学习率调度、超参数选择方法,以及通过观察损失和准确率曲线来诊断模型状态。

8. 模型理解与可视化
在擅长训练模型后,我们开始探索如何理解模型所学。

  • 可视化技术:用于理解训练好的神经网络在数据上学到了什么。
  • 艺术生成:一些可视化技术可用于生成艺术作品,如DeepDream神经风格迁移

9. 序列处理与注意力机制
我们将深度学习模型的应用扩展到其他类型的问题。

  • 循环神经网络:用于处理序列数据的通用机制,如语言、视频帧序列。
  • 图像描述:结合CNN(处理图像特征)和RNN(生成语言)来为图像生成自然语言描述。
  • 注意力机制:在图像描述中,让模型在每一步处理时关注图像的不同部分。这发展成了更通用的自注意力层,它输入一组向量,计算向量间的注意力,并输出一组新向量。自注意力已成为近年来深度学习架构中一个重要且通用的新基本组件。
  • Transformer:一个仅使用自注意力作为主要计算原语就能构建高性能自然语言处理系统的架构,体现了“注意力就是你所需要的一切”的理念。

10. 高级计算机视觉任务
我们回到计算机视觉,讨论了一系列更复杂的任务。

  • 目标检测:在图像中绘制物体边界框。分为单阶段(如YOLO, SSD)和两阶段(如R-CNN系列)方法。
  • 语义分割:为输入图像的每个像素分配一个类别标签。使用具有可学习下采样(步幅卷积)和上采样(转置卷积或插值)层的全卷积网络。
  • 实例分割:结合目标检测和语义分割,既要检测所有物体,又要指出每个检测到的物体包含哪些像素。通常通过在目标检测系统上附加一个掩码预测头来实现。
  • 3D视觉:如Mesh R-CNN,可以预测物体的完整3D三角网格。这涉及到点云、体素、网格等多种3D表征及相应的神经网络架构。
  • 视频处理:为模型添加时间维度。技术包括3D卷积网络、双流网络(结合光流和RGB)、自注意力机制和RNN。

11. 生成模型
我们学习了试图生成或输出新视觉数据的模型。

  • 自回归模型:直接最大化训练数据在某个参数化函数(神经网络)下的似然。
  • 变分自编码器:引入潜变量,通过最大化数据的变分下界来联合学习数据的分布和潜变量的分布,从而可以编辑图像或仅从图像数据学习潜表征。
  • 生成对抗网络:目前生成逼真图像的前沿方法,在许多视觉数据生成任务中表现出色。

12. 强化学习
最后,我们探讨了另一种机器学习范式。

  • 强化学习:训练智能体与世界交互,而非仅仅拟合数据集。我们简要介绍了Q学习策略梯度这两种解决强化学习问题的基本算法。

前沿展望与开放性问题 🔮

上一节我们回顾了本学期的知识体系,本节中我们来看看该领域未来可能的发展方向以及当前面临的重大挑战。当然,预测未来是困难的,以下仅是一些个人的思考和假设。

可能的发展趋势

1. 新的深度学习模型 🧠
我们将会持续发现新颖有趣的深度学习模型架构。一个例子是2018年获得NeurIPS最佳论文奖的神经ODE。它将残差网络中的离散层迭代(H_{t+1} = H_t + f(H_t, θ_t))视为对常微分方程的数值积分,并推广到连续深度的模型(dH(t)/dt = f(H(t), θ(t)))。这代表了一类全新的看待神经网络的方式。

2. 新的应用领域 🌐
深度学习将继续在更多领域找到应用。

  • 科学与医疗:医学影像分析、疾病辅助诊断、各科学领域的数据分析。
  • 计算机科学内部:甚至可用于改进传统数据结构,例如学习型哈希表,使用神经网络为特定数据类型学习最小化冲突的哈希函数。
  • 符号数学:将数学公式转换为图结构,使用图神经网络进行处理,应用于自动定理证明、符号积分等任务。这可以形式化为一个强化学习问题。

3. 更大的数据与算力 ⚡
深度学习将继续利用越来越多的数据和计算资源。

  • 算力增长:GPU等计算设备的性价比持续指数级提升,使得训练更大模型成为可能。OpenAI的分析显示,训练顶尖AI系统所需的计算量自20世纪50年代以来一直呈超指数增长。
  • 硬件创新:为了持续扩展,需要新的专用硬件,例如使用晶圆级芯片的初创公司Cerebras,其芯片面积远超现有最大GPU,旨在提供海量计算单元。

面临的重大挑战

1. 模型偏见与公平性 ⚖️
当前机器学习模型存在偏见问题,会对不同人群进行区别对待,这是不公平且需要避免的。

  • 词向量中的性别偏见:训练于大规模文本的词向量模型会反映出社会中的性别刻板印象(如“护士-女性”,“建筑师-男性”)。
  • 视觉分类器中的经济与种族偏见:研究表明,已部署的商业图像分类器对高收入西方家庭物品的识别更好,而对其他文化或低收入家庭的同类物品识别很差,甚至出现过将非裔美国人错误分类为“大猩猩”的严重事件。
  • 解决方案:需要构建能够公平对待所有人群的机器学习系统,这是一个重要且活跃的研究领域。

2. 理论理解的缺失 🤔
我们可能需要新的理论来理解机器学习模型内部的工作机制。一些实验现象暗示了当前理论的不足:

  • 彩票假设:训练好的神经网络中,大部分权重可以被剪枝移除而保持性能。更神秘的是,将剪枝模式应用于随机初始化的未训练网络,然后训练这个子网络,也能达到接近完整网络训练的性能。甚至在未经训练的网络中,就存在能完成图像分类任务的子网络。这表明我们对神经网络如何学习或表示函数可能缺乏根本性理解。
  • 泛化之谜
    • 深度神经网络可以完美拟合随机标签随机噪声的图像数据。经典统计学习理论(如Rademacher复杂度)认为,能完美拟合随机数据的模型可能过于复杂而无法泛化。但ResNet-50在拟合随机数据后,在真实数据上依然能良好泛化。
    • 双重下降现象:随着模型容量增加,测试误差先下降后上升(过拟合),但继续增加容量,测试误差会再次下降。这在全连接网络和ResNet等实际模型中都被观察到。这意味着我们可能不理解大型神经网络模型的泛化行为。

3. 对大量标注数据的依赖 📉
我们希望构建更少依赖大规模标注数据的深度学习模型。

  • 小样本学习数据集:出现了Omniglot(手写字符)、KMNIST(日文汉字)等专注于小样本学习的数据集。最新的LVIS数据集则将小样本学习推向复杂的实例分割任务,标注了超过1000个类别,旨在推动现实世界视觉任务的小样本学习进展。
  • 自监督学习:通过设计前置任务来利用无标签数据预训练模型,然后在少量标注数据上微调。前置任务示例包括:
    • 拼图游戏:将图像切块并打乱,让网络预测正确的排列。
    • 图像着色:将彩色图转为灰度,让网络预测颜色。
    • 图像修复:移除图像部分区域,让网络预测缺失部分。
      自监督学习是目前非常活跃的研究领域。

4. 缺乏真正的“理解”与常识 🐘
深度学习模型的学习方式与人类不同,它们只是模仿训练数据,缺乏对世界的真正理解和常识。

  • 语言模型的常识缺失:即使在大规模文本上训练的超大语言模型,也经常在需要常识推理的句子补全任务上失败(例如,无法正确推理“更大的动物颜色”、“父母的职业”等)。
  • 视觉模型的脆弱性:模型在训练分布之外的数据上可能完全失效。例如,在房间场景中PS上一头大象,目标检测器可能完全检测不到,或错误分类,甚至干扰对其他原有物体的检测。这表明卷积神经网络看待世界的方式与人类有质的区别,在遇到与训练数据稍有不同的情况时可能发生灾难性失败。

总结 🏁

本节课中我们一起回顾了本学期关于深度学习用于计算机视觉的核心知识脉络,从基础概念、经典模型、训练技巧到高级应用。同时,我们也探讨了该领域未来可能的发展方向(新模型、新应用、更大算力)以及当前面临的深刻挑战(偏见、理论缺失、数据依赖、缺乏常识理解)。

总结而言,现在是进入这个领域的绝佳时机。希望你们在本课程中学到的知识,能够助力你们在未来去解决这些领域面临的重大挑战。

posted @ 2026-03-26 13:08  布客飞龙IV  阅读(26)  评论(0)    收藏  举报