UCB-全栈深度学习课程笔记-全-

UCB 全栈深度学习课程笔记(全)

深度学习基础课程 P1:L1 🧠

在本节课中,我们将学习深度学习的基础知识。课程内容涵盖神经网络、万能近似定理、机器学习问题类型、损失函数与梯度下降、网络架构考量以及GPU计算等核心概念。虽然这些内容对许多同学来说是复习,但它们是构建深度学习知识体系的基石。

神经网络:从生物灵感出发 🧬

神经网络之所以被称为“神经”,是因为其设计灵感来源于我们身体中进行计算的生物神经元。神经元是一种细胞,从主体延伸出许多被称为树突的结构,可以将其视为信息的接收器。如果树突接收到足够的刺激,整个神经元就会产生一个称为“放电”的动作,本质上是一个电脉冲。这个脉冲从细胞体开始,沿着被称为轴突的长分支传播,轴突末端的分支会连接到其他神经元的树突。因此,这是一个由神经元组成的网络,当它们受到足够刺激时就会放电,电信号沿着长分支传播并依次刺激其他神经元。

从数学角度看,这可以简化为一个相当简单的函数,通常称为感知机,其概念可以追溯到20世纪50年代。我们可以将到达树突的刺激视为输入,即 x₀, x₁, x₂ 等。树突本身(即与输入信号接触的部分)受该输入刺激的程度由一个权重决定,即 w₀, w₁, w₂。我们将所有这些加权输入求和,即 Σᵢ (wᵢ * xᵢ),这就像是神经元被输入刺激的过程。此外,还有一个偏置项 b,因为这是一个线性函数,我们通常需要一个偏移量作为截距。最后,整个求和结果被包裹在某种激活函数中。因为神经元的工作原理是:如果受到足够刺激,它就完全“开启”(放电);如果刺激不足,它就保持“关闭”。因此,激活函数本质上是一个阈值函数:如果求和结果超过某个阈值,就传递信号;否则,大部分情况下保持关闭。

激活函数:从Sigmoid到ReLU 📈

那么,有哪些好的激活函数呢?经典的神经网络文献主要使用左侧的Sigmoid函数。它是一个简单的函数,无论输入是什么,输出都会被“压缩”到0和1之间。对于负输入,它渐近于0;对于正输入,它渐近于1;在0附近,它会迅速从0变为1。作为一个激活函数,它有一个很好的导数(也称为梯度),图中用橙色线表示。双曲正切函数是另一个曾被使用的函数,但我们将跳过它。

近年来,人们主要使用右侧的激活函数,称为线性整流单元,简称 ReLU。它本质上是一个最大值函数,因为它直接规定:无论输入是什么,如果大于0,就原样输出;如果小于或等于0,则输出0。它的梯度存在一个不连续点,但这没关系。基本上,如果输入大于0,梯度就是1;否则为0。这实际上是2013年开启深度学习革命的部分创新所在。

网络架构:从神经元到网络 🕸️

我们讨论了神经元,但什么构成了神经网络呢?如果你像图中所示那样将感知机排列成层,就形成了“网络”这一术语的来源。通常,有一个输入层,对应你的输入数据。输入层连接到一个隐藏层,这个隐藏层再连接到另一个隐藏层,隐藏层的数量可以是多个。最后,有一个输出层。构成这个网络的每一个感知机都有自己的权重和偏置。正是这些权重和偏置的设置,决定了神经网络如何响应输入。

万能近似定理:网络的强大表达能力 🌐

接下来我们要讨论的是万能性。这个神经网络代表了某个函数 y,即 y = f(x; w),其中 x 是输入,w 是所有权重的设置。让我们看看左侧这个函数 f(x),它有很多峰和谷。我们如何知道是否存在一个神经网络,以及一组权重设置,能够基本上表示这个函数呢?

总结一些理论结果,你可以证明任何具有一个隐藏层的两层神经网络(即输入层到一个隐藏层再到输出层),都可以找到一组权重,能够近似任何函数。这就是所谓的万能近似定理。关于为什么这是成立的,可以有一些直观理解。如果你访问那个 neuralnetworksanddeeplearning.com 网站,查看第四章,基本上你可以将隐藏层中的每一个感知机(可能有成千上万个)视为一个脉冲函数。如果这些“柱子”(图中橙色部分)足够多,并且它们能上下移动,你就能表示任何东西。这几乎就像是这个函数的傅里叶变换。所以这相当有趣。

关键要点是:神经网络具有难以置信的通用性。至少在理论上,你可以使用神经网络来表示任何函数。

机器学习问题类型:三大类别 🎯

那么,我们用神经网络来做什么呢?我们将其用于机器学习。机器学习问题有哪些类型呢?大致可以分为三个主要类别:监督学习无监督学习强化学习。当然还有迁移学习、元学习、模仿学习等不同类型,但这是三个大的类别。

  • 无监督学习:你获得的是未标记的数据 xx 可能是音频片段、文本字符串或图像,但没有其他关联信息。目标是从数据中学习其结构,即学习 x 本身。这样做的原因是你可能希望生成虚假的音频片段、图像或评论,或者获得对数据可能包含内容的洞察。例如,你可以使用神经网络生成一些虚假的文本(如亚马逊评论)。另一个概念是聚类:你有一些数据,对其一无所知,也没有标签,但仅仅根据其结构,你可能会推断出存在一些聚类。这意味着某个过程产生了这些数据,使得这边的数据来自一个过程,那边的数据来自另一个过程。
  • 监督学习:你获得的是成对的 (x, y) 数据,其中 x 是原始输入数据,y 是其标签。例如,x 可能是一张图像,y 可能是“它里面有一只猫吗?”这样的标签。目标是学习一个从 x(图像)到 y(标签)的函数。这样做的目的就是为了能够进行预测。例如,给定一张图像,我可以说它是一只猫;给定一个音频片段,我可能能够识别出是某人在说“嘿,Siri”。这样的例子不胜枚举。
  • 强化学习:目标是学习如何采取行动。有一个智能体(可能是一个机器人、计算机病毒等),它可以采取行动(例如向前移动、看向某处)。当它采取行动时,环境会对其做出反应,你可以将其解释为环境向智能体提供奖励(或不提供),并改变智能体所处的状态。例如,如果一个机器人在某个位置,它采取了一个向前移动的动作,那么现在它就在另一个位置了,并且可能(也可能没有)获得相关奖励。你可以使用强化学习来训练游戏智能体。例如,动作可能是在围棋棋盘上放置一枚棋子,奖励可能是你最终是否赢得了游戏,而状态显然就是围棋棋盘的状态。

目前,商业上可行的主要是监督学习。强化学习无疑是下一个重点。无监督学习在现阶段也正在兴起,例如OpenAI将GPT-3产品化及其所带来的一切可能性,这都属于无监督学习的范畴。

无监督学习示例:从语言到图像 🖼️

无监督学习问题的一个例子可能是预测文本字符串中的下一个字符。Andrej Karpathy 有一篇名为“Char-RNN”的博客文章,基本上就是使用循环神经网络一次输入一个字符,然后RNN也可以输出字符。最终你学习到的是一个语言模型,如果你用一个词启动它,它就会通过一个接一个地生成字符来持续写作,其输出能力令人印象深刻。

另一个无监督学习问题可能是理解词与词之间的关系。这里的输入实际上是词汇表中的单词。想象你的词汇表有30,000个不同的单词,那么每个单词可以被表示为一个向量(在对应单词位置为1,其他位置为0的列表)。如果你将一堆这样的向量输入到一个经过适当设置和训练的系统中,你实际上可以确定单词之间存在某些关系,例如经典的例子:男人与女人的关系,就像国王与王后的关系。这非常有趣。

在计算机视觉领域,你可以尝试预测下一个像素,而不是文本中的下一个字符。你可以从图像的一小部分开始,然后通过例如将图像压缩到一个非常小的表示(称为潜在向量),再将这个潜在向量扩展回图像的方式来训练这类模型。这可以学习到一个高度压缩的表示。

最后,这条研究路线的巅峰可能是生成对抗网络。其思想是,有一个生成器,用于生成这类虚假的图像或文本;但同时还有另一个模型,一个称为判别器的神经网络。判别器的目标是能够区分生成的图像/文本与真实的图像/文本。而生成器的目标是产生能够欺骗判别器的图像或文本。如果你设置好这个系统并进行训练,你会得到非常令人印象深刻的结果,并且这些结果每年都在变得更加出色。我们都见过深度伪造视频。如果你访问“This Person Does Not Exist”网站,你会看到无限多个由GAN生成的人脸,看起来非常有趣。我今天还看到了“This Anime Does Not Exist”网站。

在强化学习中也有很多例子。

损失函数与风险最小化:从线性回归说起 📉

接下来,让我们谈谈所谓的风险最小化损失函数的概念。

我们先简单讨论一下线性回归。这里展示的是一维数据:x轴上有一个维度(某个数值),y轴上是另一个维度(输出)。所以这是一维输入数据产生一维输出。我们可能想问的问题是:如果我们得到一个输入,比如30,我们如何预测输出可能是什么?给定我们已经看到的所有这些数据,当我们看到一个新的数据点(但我们只看到输入部分,不知道输出应该是什么)时,我们能预测输出应该是什么吗?

答案是肯定的。数学上一种稳健的方法是给这些数据拟合一条最佳拟合线。之所以是一条线,是因为没有理由相信它应该是线以外的其他形式。但我们如何找到这条线应该是什么样子呢?这条线能够告诉我们,如果我们输入 x,它会给出 y,因为它会将 x 乘以某个数再加上另一个数,即 y = ax + b。但 ab 应该设置为什么值呢?我们如何设置这条线?

我们可以做的是,最小化我们观察到的所有数据点与某条候选线之间的平方误差。给定一条由两个数字 ab 定义的线,我们可以计算在所有已见数据上的平方误差,公式如图所示。然后,我们可以尝试找到使平方误差最小化的线参数 ab,那将是最佳拟合线。

更一般地,我们可以称这个平方误差函数为损失函数,我们的目标是最小化这个损失函数。我们找到使损失函数最小化的权重和偏置(如 ab)的设置。这就是经验风险最小化的全部思想。

在神经网络中,函数 f(给定权重 w 和偏置 b)作用于 x,这就是神经网络。权重和偏置是神经网络的参数。损失函数可以是均方误差,也可能是其他类型的损失函数。但这基本上就是你如何训练或确定神经网络是否解决问题的方式。

对于分类任务,所有东西都会改变。回归是试图从输入预测某个实值输出,而分类是从输入预测某个类别输出。输出永远不会是像2.3这样的值,而会是确切的0、1或2,这些值对应于数据的标签。对于分类,我们通常使用交叉熵损失。你们将在本周的阅读材料中详细了解这一点。

梯度下降:优化权重的核心方法 ⬇️

好的,我们有了损失函数,这样我们就能看到,如果我们有一些权重,我们可以理解模型的好坏。但我们实际上用它来做什么呢?我们的目标是找到优化(即最小化损失)的权重和偏置。这个损失函数(给定数据)可能看起来非常复杂。我们能做的是,通过以下方式更新每个权重:将权重设置为当前权重值减去某个 α(可称为学习率)乘以损失函数关于该权重的梯度

我们从一些随机参数开始,在观察到的数据上评估它们,计算损失函数。然后,为了改善神经网络对数据的拟合,我们将通过上述操作更新每个权重:只需减去损失函数关于该权重的梯度乘以某个学习率。这就是全部。我们希望的方式是,始终朝着最陡下降的方向移动。有一些技巧可以确保,如果你的数据存在于空间中某个相对较小的区域,梯度下降会比数据处于所谓的“良好条件”(通常意味着在所有维度上具有零均值和相等方差)时更困难,因为后者为梯度下降提供了最有利的条件。

我们可以讨论权重初始化,可以讨论归一化。这些都是一阶方法:梯度下降就是计算一阶梯度,然后用该梯度更新权重。也有二阶方法,你可以计算损失函数关于权重的二阶导数,但我们通常不使用它们,因为它们的计算量非常大。不过,有一些近似的二阶方法可以在更快地训练神经网络方面发挥作用。如果你只记住一个名字:Adam,那将是我们实验中要使用的优化器,它试图做的就是近似二阶优化。

最后,我们可以查看所有见过的数据,计算损失,更新每个权重。但在实践中,可能更好的是只在一个数据子集上计算梯度,而不是整个数据集。这被称为批量梯度下降随机梯度下降。随机梯度下降可能使用大小为1的批次:你查看一个数据点,计算其损失,然后用该损失更新所有权重,然后查看下一个样本。这就是随机梯度下降。这样做的原因是,每一步的计算量要少得多。你不需要遍历所有数据(例如ImageNet上有100万张图像)来第一次更新权重;你可以只看32张图像,计算损失,更新权重。然后,在下一批32张图像中,模型将更适合数据。因此,它使用更少的计算量训练得更快,但噪声更大。不过,这基本上就是我们实际所做的。

反向传播:高效计算梯度 🔄

现在我们可以谈谈反向传播,因为我们已经将整个学习的概念简化为仅仅优化一个损失函数。我们只是试图找到最小化这个损失函数的权重和偏置,并且我们已经想出如何通过随机梯度下降来实现:取一批数据,计算其损失函数,计算每个权重相对于该损失函数的梯度,然后用该梯度乘以学习率 α 来更新每个权重。但是,我们如何高效地计算这些梯度呢?说起来容易,“我们计算梯度”,但我们实际上怎么做呢?

梯度只是导数的另一个说法,而导数你在微积分课上学过。你可以符号化地进行,给定一个函数,你可以找出它的梯度是什么。但神经网络从来不只是像 e^x 那样简单的函数,计算它的梯度永远不会那么容易。它由许多计算组成,而每个计算确实都有梯度,因为它可能只是一个像 ax + b 这样的线性函数,这确实有梯度。然后,我们可以应用链式法则来处理所有内容,这样我们就可以得到损失函数相对于神经网络底部(离损失函数非常远)的权重的梯度,只需通过神经网络的所有层进行链式传递。这就叫做反向传播。

好消息是,我们甚至不需要自己编写导数代码,因为我们使用自动微分软件。像 PyTorch 或 TensorFlow 这样的框架会自动为你计算梯度。所以,你只需要编程实现前向函数 f(x)(给定权重 w),然后 PyTorch 就会自动为你计算梯度。

网络架构考量:超越基础MLP 🏗️

最简单的神经网络架构就是我们一直在讨论的,也称为多层感知机。它实际上就是感知机排列成层。有时这被称为全连接层。我们知道,理论上我们只需要这个就能表示任何类型的函数,但我们可能需要一个无限大的网络和极其大量的数据来实际学习到能正确工作的权重。

因此,我们可以做的是,将我们对世界的已有知识编码到神经网络的架构中。例如,对于计算机视觉,我们使用卷积网络。这意味着有一组权重以某种方式绑定在一起,因此无论它们应用于输入的哪个位置,它们总是处于一个局部结构中。这实际上发生在我们眼睛中(我们从对眼睛和大脑的研究中得知),并且这对世界来说也是有意义的,因为世界是由物体组成的,当你围绕它们移动或它们靠近你时,这些物体不会发生剧烈变化。边缘就是边缘,即使它离你更近,它也不会改变。

对于序列处理(如自然语言处理),我们经常使用循环网络,它们具有时间不变性。这通常是正确的:语言的规则不会随着序列的进行而改变。因此,你的神经网络可以通过以某种特定方式构建来“知道”这一点。

当我们优化这些神经网络时,我们可以有10层,每层不宽(例如只有10个通道),但有100层;或者我们有10层,但每层有100个通道。这就是深度与宽度的权衡。哪个更好?这在某种程度上取决于我们在实践中观察到的结果,目前没有理论能真正帮助你。但某些方法在实践中效果更好。因此,成为深度学习从业者的一部分,就是通过实践、阅读论文、参加像这样的课程来获取这些知识。

跳跃连接:你可以将输入绕过处理它的层进行连接,使得该层的输出被加到输入本身上。这往往在反向传播中很有帮助。我们还有很多其他技巧。

GPU计算:深度学习的加速引擎 ⚡

最后,为什么事情在2013年爆发?我们有了更大的数据集,但我们也获得了在GPU上进行矩阵计算的优秀库,特别是NVIDIA的CUDA。这意味着使用图形处理单元,在此之前它们只用于游戏。但随着NVIDIA发布CUDA库,你可以使用图形处理单元进行通用矩阵计算,这非常适用于包括深度学习在内的许多科学计算。它对深度学习如此关键的原因是,神经网络中的所有计算(我们见过的所有计算)都只是矩阵乘法,而矩阵乘法很容易并行化。


本节课中,我们一起学习了深度学习的基础构建模块:从受生物启发的神经网络和激活函数,到其强大的万能近似能力。我们探讨了监督、无监督和强化学习三大问题类型,并理解了通过损失函数和梯度下降(特别是反向传播)来训练模型的核心理念。我们还简要了解了如何通过特定的网络架构(如CNN、RNN)和利用GPU计算来更高效地解决实际问题。这些概念为后续更深入的学习奠定了坚实的基础。

课程 P10:【Lab4】Transformers 应用 🧠

在本节课中,我们将学习如何使用 Transformer 架构来识别合成文本序列。我们将介绍 LineCNNTransformer 类和 TransformerLitModel 类,并了解如何将它们应用于文本识别任务。


概述

在 Lab3 中,我们训练了一个 LineCNN 模型,它结合了 CNN 和 LSTM,并使用 CTC 损失进行训练,取得了约 17% 的字符错误率。本节实验将采用相似的思路,但使用 Transformer 解码器层替代 LSTM,并采用不同的训练方式。


LineCNNTransformer 类详解

上一节我们回顾了基于 LSTM 的模型,本节中我们来看看新的 LineCNNTransformer 类。这个类位于 lab4/text_recognizer/models/ 目录下。

首先,我们遇到一个名为 PositionalEncoding 的类。它的作用是为序列添加位置信息,这是 Transformer 架构中的标准做法。

接下来是 generate_square_subsequent_mask 函数。这个函数生成一个三角形的注意力掩码,用于在解码时屏蔽未来的位置信息,确保模型在预测当前位置时无法看到未来的信息。

然后我们进入主要的 LineCNNTransformer 类。其初始化部分与之前的 LineCNNLSTM 类似,我们实例化了底层的 LineCNN 模型。此外,还引入了几个新组件:

  • 嵌入层 (Embedding):用于将目标字符(即我们要预测的文本序列中的每个字符)转换为向量表示。
  • 全连接层 (nn.Linear):位于模型的输出端,用于将 Transformer 解码器的输出映射到最终的字符类别概率。
  • 位置编码器 (PositionalEncoding):为序列添加位置信息。
  • 注意力掩码 (mask):我们在这里实例化那个三角形的未来掩码,以便后续使用。
  • Transformer 解码器 (nn.TransformerDecoder):这是 PyTorch 提供的 Transformer 解码器模块。我们默认使用 4 个解码层,每个层有 4 个注意力头,输出维度为 128。

forward 方法中,模型接收两个输入:

  • x:图像张量,形状为 (batch_size, height, width)
  • y:目标输出序列,形状为 (batch_size, sequence_length),其中每个元素是 0 到 C-1 之间的整数(C 是字符类别数)。

模型输出的张量形状为 (batch_size, num_classes, sequence_length),表示每个位置上各个字符类别的 logits。

以下是 forward 方法的核心步骤:

  1. 图像 x 通过 LineCNN,得到形状为 (batch_size, embedding_size, sequence_length) 的特征。
  2. 对该特征进行归一化(这是 Transformer 文献中的常见做法)。
  3. 为特征添加位置编码。
  4. 目标序列 y 通过嵌入层转换为向量。
  5. 为嵌入后的序列添加位置编码。
  6. 应用未来掩码。
  7. 将处理后的图像特征 (x) 和目标序列 (y) 一起输入 Transformer 解码器。
  8. 解码器的输出通过一个全连接层,得到最终的 logits。


TransformerLitModel 类

现在,我们来看看用于训练和验证的 TransformerLitModel 类,它位于 lit_models/transformer.py 中。

这个模型使用交叉熵损失作为损失函数,而不再使用 CTC 损失。我们仍然使用字符错误率作为评估指标。在计算错误率时,我们会忽略序列起始符和填充符。

以下是训练和验证步骤的关键点:

  • 训练步骤 (training_step)
    • 输入是图像 x 和真实序列 y
    • 我们将图像 x 和序列 y 中除最后一个字符外的所有部分输入模型。
    • 计算模型输出与真实序列(从第一个字符开始到结束)之间的交叉熵损失。这样做的逻辑是,模型应该根据已看到的内容预测下一个字符,因此我们需要将真实序列偏移一位来计算损失。

  • 验证步骤 (validation_step)
    • 过程与训练步骤类似。
    • 此外,我们还需要计算字符错误率。为此,我们不仅需要模型的 logits,还需要模型对完整序列的预测结果。


序列预测方法

在 CTC 模型中,我们需要通过贪婪解码等方式从 logits 得到预测序列。在 Transformer 模型中,我们同样需要一个单独的预测步骤。

让我们查看 LineCNNTransformer 类中的 predict 方法。该方法接收一个图像(没有真实标签),并输出一个形状与真实标签相同的预测序列张量。

以下是 predict 方法的工作原理:

  1. 图像通过 CNN 和位置编码得到图像特征。
  2. 初始化一个输出序列,其中所有位置都是填充符,只有第一个位置是序列起始符。这告诉 Transformer 序列从此开始。
  3. 进入一个循环,对于输出序列中的每个位置:
    • 将当前已预测的序列(初始只有起始符)通过嵌入层和位置编码。
    • 将这个序列与图像特征一起输入 Transformer 解码器,得到当前步骤的 logits。
    • 取 logits 中概率最大的字符,作为下一个预测字符,添加到输出序列中。
  4. 循环持续进行,直到达到最大输出长度。在作业中,你可以优化这一点:当模型第一次预测出填充符或序列结束符时,就停止解码,这样可以加速预测过程。

模型训练与总结

要训练这个模型,我们运行命令:

python run_experiment.py

我发现需要训练更多轮次(例如设置为 40 轮),并在 GPU 上运行。模型约有 1900 万个可训练参数,比之前的 LSTM 模型要多。经过 40 轮训练后,我得到了约 17% 的字符错误率,这与之前 LSTM+CTC 模型的结果基本相同。

在作业中,你可以尝试不同的超参数,例如不同的滑动窗口宽度。你也可以在此处添加命令行参数,以便设置 Transformer 特有的变量(如层数、注意力头数)。如前所述,优化 predict 方法也是一个很好的练习。


本节课中,我们一起学习了如何构建一个基于 Transformer 的文本识别模型。我们了解了 LineCNNTransformer 的结构、TransformerLitModel 的训练逻辑,以及如何进行序列预测。通过本实验,你将 Transformer 架构应用到了一个具体的序列预测任务中。

机器学习项目实战课程 P11:L5 - 机器学习项目全流程 🚀

在本节课中,我们将学习如何成功启动和管理一个端到端的机器学习项目。课程将涵盖项目生命周期、项目选择与优先级评估、项目类型、关键指标设定以及基线建立等核心内容。我们将通过一个“姿态估计”的案例研究来贯穿始终,帮助初学者理解如何将理论应用于实践。

机器学习项目生命周期 🔄

上一节我们介绍了课程概览,本节中我们来看看一个机器学习项目从构思到上线的完整生命周期。这个过程并非线性,而是一个需要不断迭代和回溯的循环。

一个典型的机器学习项目生命周期包含以下几个主要阶段:

  1. 规划与项目启动:决定项目方向,明确需求和目标,分配资源,并考虑项目的伦理影响。
  2. 数据收集与标注:收集原始数据,设置传感器(如摄像头),并找到方法为数据标注真实值(Ground Truth)。
  3. 模型训练与调试:实现基线模型,复现前沿研究,花费大量时间调试模型,并尝试改进模型架构。
  4. 部署与测试:运行试点项目,编写回归测试以防止后续改动破坏模型,并评估模型的偏见问题。
  5. 生产发布与迭代:将模型部署到生产环境,并根据实际表现循环回到之前的阶段进行优化。

关键点:机器学习项目是一个迭代循环。例如,在数据收集阶段,你可能会发现任务定义得太难,从而需要回到规划阶段重新调整。同样,在部署后,你可能会发现数据分布偏移,需要回到数据收集阶段。

除了项目层面的活动,作为一个机器学习团队,还需要解决跨项目的基础设施和工具建设问题。

如何选择与评估机器学习项目 📊

了解了项目生命周期后,我们需要知道如何选择一个好的起点。本节我们将探讨如何评估项目的潜在影响和可行性,从而进行优先级排序。

一个通用的项目优先级评估框架是寻找高潜在影响相对可行的项目。我们可以从两个维度来评估:

  • 潜在影响:项目成功能带来多大价值?
  • 可行性:项目成功的可能性有多大?

以下是评估项目潜在影响的几种思路:

  • 利用廉价预测:寻找那些原本需要专家判断,但通过机器学习可以低成本自动化的预测任务。
  • 消除产品摩擦点:从产品体验出发,寻找用户操作中高摩擦、低效的环节,用机器学习将其自动化。
  • 替代复杂规则系统(软件2.0):寻找现有系统中由复杂、手工规则驱动的部分,用从数据中学习到的模型来替代。
  • 借鉴行业实践:参考其他成功公司(如Netflix、Spotify)或行业报告中的机器学习用例,获取灵感。

评估项目可行性时,主要考虑以下三个成本驱动因素:

  1. 数据可用性:数据是否容易获取?标注成本高吗?需要多少数据?数据是否稳定(分布是否随时间变化)?是否有数据安全限制?
  2. 准确率要求:错误预测的代价有多大(如自动驾驶 vs. 推荐系统)?系统需要多高的准确率才能有用?是否有公平性等伦理考量?
  3. 问题内在难度:问题是否被明确定义为机器学习问题?是否有类似问题的已发表工作?这些工作的计算成本(训练和推理)如何?人类能否仅凭模型可用的输入就解决该问题?

重要提示:项目成本通常随准确率要求呈超线性增长。将准确率从99.9%提升到99.99%所需的努力和资源,可能比从90%提升到99%还要多。

案例研究:姿态估计的可行性评估

  • 影响:可靠的姿态估计是实现机器人抓取的关键,可以替代传统复杂、脆弱的机器人流程。
  • 可行性
    • 数据:相对容易在实验室收集,但3D姿态标注可能有挑战(可通过仪器辅助解决)。
    • 准确率:需要较高精度(如位置误差<0.5厘米)才能成功抓取,但单次失败成本低。
    • 问题难度:有相关研究,但需适配到特定机器人和物体上,属于中等偏低难度。

机器学习项目类型及其策略 🏗️

根据项目目标的不同,机器学习项目可以分为几种典型类型,每种类型的管理策略和成功要点也不同。

以下是三种常见的项目类型:

  1. 软件2.0项目:用机器学习改进现有规则系统。例如,改进代码补全算法、推荐系统或游戏AI。
    • 关键问题:性能提升是否转化为业务价值?是否能形成数据飞轮?
  2. 人在环路项目:模型的输出需要经过人工审核。例如,草图转幻灯片、邮件自动补全、辅助放射科医生。
    • 关键问题:系统需要达到多好才有用?如何收集足够数据使其达到可用水平?
  3. 自治系统项目:系统在现实世界中自主做出决策。例如,全自动驾驶、全自动客服。
    • 关键问题:可接受的失败率是多少?如何保证系统不超出该失败率?能否低成本地标注系统产生的数据?

数据飞轮:这是项目成功的黄金标准。它指“更好的模型 → 更好的产品 → 更多用户 → 更多数据 → 更好的模型”这样一个正向反馈循环。建立数据飞轮能显著提升项目的可行性和长期价值。

通过产品设计降低难度:优秀的产品设计可以降低对模型准确率的苛刻要求,从而提高项目可行性。

  • 示例:Facebook照片标签是“建议”而非“自动执行”;Grammarly提供修改解释而非强制修改;推荐系统提供多个选项而非单个推荐。

资源推荐:苹果的《机器学习产品设计指南》和微软的《人机交互指南》提供了如何设计优秀机器学习产品的宝贵启发。

使自治系统更可行:可以通过使其“不那么自治”来降低难度,例如加入安全员(人在环路)、设置运行护栏(如限定运行区域),或大幅缩小问题初始范围(如先解决养老社区内的自动驾驶)。

如何选择与优化评估指标 📈

确定了项目方向后,我们需要一个明确的“指挥棒”来指导模型优化。本节我们讨论如何从众多关心的指标中,选出一个当前需要集中优化的单一指标。

为什么需要单一优化指标?
现实世界是复杂的,我们几乎总是关心多个指标(如准确率、延迟、模型大小)。但在模型训练、评估和比较过程中,如果同时优化多个目标,会难以抉择。拥有一个单一指标能极大简化迭代过程。

如何将多个指标合并为单一指标?
以下是几种常见策略:

  1. 取平均值或加权平均值:例如,对精确率和召回率取平均(F1分数)。
  2. 设定满意度指标:这是最常用的策略。对N个关心的指标,为其中N-1个设定性能阈值(满意度指标),然后优化剩下的那一个指标。
    • 示例:在满足“模型大小 < 100MB”和“推理延迟 < 50ms”的前提下,尽可能优化准确率。
  3. 使用领域特定复合指标:某些领域有公认的复合指标。例如,在目标检测中常用平均精度均值

如何选择要优化的指标?
这需要结合领域知识和项目阶段来判断:

  • 领域依赖性:某些指标可能更容易通过工程手段优化(如缓存可以优化延迟),而准确率则必须通过改进模型。
  • 指标敏感性:选择那些对模型架构变化更敏感的指标进行优化。
  • 项目阶段:在早期,让模型“能工作”可能比满足所有生产约束更重要。随着进展,再切换优化重点。

案例研究:姿态估计的指标选择

  • 需求清单:位置误差 < 1厘米,角度误差 < 10度,推理时间 < 100毫秒。
  • 现状评估:初步模型的位置误差接近1厘米,角度误差高达60度,推理时间远超100毫秒。
  • 决策
    1. 阈值指标:位置误差(设定阈值1厘米,避免模型变差)。
    2. 优化指标:角度误差(当前差距最大,是主要瓶颈)。
    3. 暂时忽略:推理时间(先解决功能问题,再优化性能)。
  • 迭代:当角度误差降低到接近10度后,再将优化重点切换到推理时间上。

核心公式示例:F1分数(精确率与召回率的调和平均数)
F1 = 2 * (Precision * Recall) / (Precision + Recall)

建立有效的性能基线 🎯

最后,为了判断模型性能的好坏,我们需要一个参照物——基线。本节我们学习为什么需要基线以及如何建立好的基线。

为什么基线很重要?
基线给出了模型性能的下限期望。一个好的基线能告诉你模型是否真的学到了东西,并指导你下一步的优化方向。

示例:相同的训练/验证损失曲线,如果基线(如人类性能)很高,说明模型欠拟合,应优先提升模型容量;如果基线很低且训练损失已接近基线,但验证损失高,说明模型过拟合,应优先解决泛化问题。

基线来源有哪些?

  • 外部基线
    • 项目需求:将产品需求直接作为基线。
    • 已发表成果:查阅论文中在相似问题上的结果(需注意数据差异可能造成不公平比较)。
  • 内部基线
    • 基于规则的脚本:使用启发式方法或传统算法(如OpenCV函数)构建一个解决方案。这是非常强大的基线。
    • 简单机器学习模型:使用线性回归等简单模型作为基线。如果你的复杂模型不如它,说明数据或模型有问题。
    • 简单统计值:例如,对于回归任务,使用目标值的平均值作为预测。这是一个最低限度的合理性检查。
    • 人类表现:这是非常有力的基线,尤其当你的标签也来自人类时。它几乎代表了模型性能的上限。

如何获取人类表现基线?
在标注质量、成本和可扩展性之间存在权衡:

  • 众包平台:成本低,易扩展,但质量可能不高。
  • 众包+集成:让多人标注同一数据并集成结果,质量更好,成本稍高。
  • 领域专家:质量最高,但成本昂贵且难以扩展。

选择策略:应选择在项目预算内,既能提供高质量基线,又能在未来相对容易地扩展数据标注的方案。

课程总结 🎓

本节课中我们一起学习了成功启动和管理机器学习项目的核心框架:

  1. 项目生命周期:理解机器学习项目是一个非线性的迭代循环,尽早完成循环(快速部署)有助于加速迭代。
  2. 项目选择与评估:寻找高影响(如利用廉价预测、消除产品摩擦)且相对可行(数据易得、错误成本低、问题定义清晰)的项目。
  3. 项目类型与策略:识别项目属于软件2.0人在环路还是自治系统类型,并采取相应策略(如建立数据飞轮、通过产品设计降低难度、添加安全护栏)来提高成功率。
  4. 评估指标:在现实中关心多个指标,但在任一给定时间点,应集中优化一个单一指标(通常通过为其他指标设定阈值来实现),并随着项目进展重新评估优化重点。
  5. 性能基线:建立好的基线(如规则系统、人类表现)至关重要,它能提供性能下限,帮助你诊断问题是欠拟合还是过拟合,从而将优化精力投入到正确的地方。

通过将这些原则应用于像“全栈机器人公司姿态估计”这样的案例,我们可以更有条理、更高效地将机器学习想法转化为实际可用的生产系统。

课程 P12:L6 - 基础设置与工具 🛠️

在本节课中,我们将学习构建和运行机器学习系统所需的基础设施与工具。我们将从理想的工作流程开始,探讨现实中的挑战,并详细介绍从软件开发、计算资源管理到实验跟踪和模型部署的各个环节。

概述

机器学习从业者的理想是:提供数据,然后自动获得一个优秀的预测系统,该系统可以作为可扩展的API或边缘部署使用。这个部署会为我们生成更多数据,用于改进系统。

然而,现实情况是,你不能只是将数据交给系统。你需要寻找、聚合、处理、清洗、标注、版本化数据。然后,你需要编写和调试模型代码,寻找计算资源,运行大量实验,审查结果,发现错误或尝试不同的架构,编写更多代码,配置更多计算资源,再次重复。当你对模型满意时,需要部署它,然后监控模型在生产数据上的预测,以便收集好的样本,聚合、处理、清洗、标注它们,将它们添加到数据集中,进行版本控制,然后重新开始整个过程。

这个观察在几年前的一篇著名论文《机器学习:技术债务的高息信用卡》中被提出。论文指出,代码库中的机器学习部分实际上只占很小一部分。为了交付这部分功能,你需要构建和维护许多其他系统组件,从而承担大量技术债务。

我们可以将所需的基础设施大致分为三类:数据模型训练与评估以及部署。今天,我们将重点讨论中间部分:训练与评估

软件工程基础 🐍

上一节我们概述了机器学习系统的全貌,本节中我们来看看最基础的部分:软件工程。在深度学习和机器学习领域,我们主要使用的编程语言是 Python。这主要是因为围绕Python构建的丰富库生态,而非语言本身的特性。Python在科学计算和数据计算领域是明确的赢家。它易于与C等低级语言互操作,但这也意味着要编写高性能的科学计算代码,通常需要使用Cython或C++编写,然后通过库与Python集成。不过,Python对于编写脚本和快速原型开发非常友好,其语法接近自然语言,易于理解。

为了编辑代码,我们使用文本编辑器。以下是常见的选项:

  • 传统编辑器:如 Vim、Emacs。
  • Notebooks:如 Jupyter Notebook,这是一种交互式计算环境,可以逐个单元执行代码并查看结果。
  • 现代编辑器:如 Visual Studio Code,这是一个来自微软的开源项目,提供了优秀的Python开发体验。
  • Python专用IDE:如 PyCharm。

我们推荐使用 Visual Studio Code,因为它免费、易于设置,并且拥有强大的生态系统。它内置了版本控制、代码差异对比、智能代码补全、远程项目开发、代码检查等功能,特别是其集成的终端和远程开发支持,使得工作流程非常顺畅。

接下来,我们谈谈代码规范和类型提示。Linter 是定义代码风格规则的工具,例如行长度限制、变量命名规范等。自动化执行这些规则可以保持代码一致性。静态分析工具(如 Pylint)还能捕获未定义变量等潜在错误。类型提示 通过在函数签名中指定变量类型(例如 def train_model(model: Model, epochs: int)),既能作为代码文档,也能帮助在开发早期发现类型不匹配的bug。我们将在后续的实验中为代码库设置这些工具。

Jupyter Notebooks 与替代方案 📓

上一节我们介绍了代码编辑工具,本节中我们来看看在数据科学中广泛使用的 Jupyter Notebooks。Notebooks 非常适合项目的初稿和探索性分析。它们允许你交互式地执行代码、可视化结果,并混合代码、文本和输出。

然而,使用 Notebooks 构建可扩展、可复现和经过良好测试的代码库是困难的。原因包括:

  • 难以版本控制:代码和单元输出交织在一起。
  • 开发环境原始:不如专业IDE强大。
  • 难以测试:缺乏成熟的单元测试框架集成。
  • 无序执行:可能导致代码状态混乱,难以推理。
  • 难以运行长任务或分布式任务

尽管如此,一些成功的机器学习团队(如 fast.ai)的整个工作流都基于 Notebooks。他们开发了 nbdev 等项目,展示了如何在 Notebook 中开发结构良好、可测试的代码。

一个有趣的 Notebook 替代方案是 Streamlit。它专注于创建交互式应用。你只需编写普通的 Python 代码,并使用 Streamlit 提供的按钮、滑块、图表等组件,就能快速构建出可分享的 Web 应用。它让创建交互式演示变得非常简单。

依赖管理与环境配置 📦

在开始任何项目之前,我们需要定义和管理项目依赖。以下是推荐的工具链:

我们推荐使用 Conda 来管理 Python 解释器、CUDA 和 cuDNN 等底层环境。但是,对于 Python 包本身(如 PyTorch, pandas)的管理,我们使用 pip-tools

使用 pip-tools 的原因在于,它能很好地将依赖项分解到多个文件中(例如,生产环境、开发环境、测试环境的需求分开)。它还能“锁定”依赖的确切版本,确保项目的可复现性。我们将在示例项目和实验中使用这种方法。

深度学习计算需求 💻

上一节我们配置好了开发环境,本节中我们来看看运行深度学习模型所需的计算资源。我们可以将需求分为两个阶段:早期开发规模化训练与评估

在早期开发阶段,我们编写代码、调试模型、查看结果,希望快速迭代。理想情况下,我们能在本地或易于访问的计算机上使用 GPU。这可以是本地桌面电脑(配备 GPU),也可以是云上易于 SSH 连接的实例。

在规模化训练阶段,我们需要进行架构搜索、超参数调优,或者训练无法在少量 GPU 上容纳的大型模型。这时,我们需要能够轻松启动大量实验,并有效审查结果。这通常需要一个机器集群,可以是本地部署的,也可以是云上的。

深度学习研究使用的计算量正在快速增长。从 Transformer 模型(如 GPT-3, Switch Transformer)的发展可以看出,模型规模和所需算力都在急剧上升。因此,有效管理计算资源至关重要。

GPU 基础与选择策略 🎮

要决定是购买自有硬件还是直接使用云服务,我们需要了解 GPU 的基础知识、云选项和本地选项。

目前,NVIDIA GPU 是深度学习领域的主流选择(尽管 Google 的 TPU 在某些场景下速度更快)。NVIDIA 每年都会推出新的架构(如 Kepler, Pascal, Volta, Turing, Ampere)。通常先推出服务器版本,然后是高端消费级版本。

选择 GPU 时,显存大小 是关键,因为它决定了单次能处理的数据量(批次大小)。更大的批次通常意味着更快的训练速度。

在精度方面,深度学习通常使用 32位浮点数。但从 Volta 架构开始,NVIDIA 引入了 Tensor Cores,专门用于混合精度计算(16位和32位混合),能大幅加速矩阵运算。纯 16位训练 也能在几乎不影响精度的情况下,允许使用更大的批次。

以下是各代架构的简单对比:

  • Kepler/Maxwell:较慢,不建议购买新卡,但在云上(如 K80)可能因价格低廉而出现。
  • Pascal:如 GTX 1080 Ti,对于某些任务仍可接受。
  • Volta/Turing:支持 Tensor Cores 和混合精度训练,是性价比较高的选择(如 RTX 2080 Ti, Titan RTX)。云上的 V100 也属于此类。
  • Ampere:最新架构(如 RTX 3090, A100),拥有最多的 Tensor Cores 和最高的性能。

一个很好的参考资源是 Tim Dettmers 的博客,他会定期更新深度学习 GPU 购买指南。

云服务与本地硬件对比 ☁️ vs 🖥️

主要的云服务提供商有 Amazon Web Services, Google Cloud PlatformMicrosoft Azure。它们提供的 GPU 类型和价格大致相似。GCP 价格略低,并且提供独有的 TPU。此外,还有一些初创公司提供更具竞争力的价格,例如 Lambda LabsCoreWeave

在本地,你可以选择:

  • 自己组装机器:对于最多4张 RTX 2080 Ti 或 2张 RTX 3090 的配置是可行的。
  • 购买预装机器:如 Lambda Labs 或 NVIDIA 提供的整机。
  • 购买服务器级机器:如配备8张 V100 的服务器,价格昂贵。

进行成本分析时,以一台4x RTX 2080 Ti 的机器(约1万美元)为例,如果全天候使用云上4x V100实例(约12美元/小时),大约5周后云服务的花费就会超过本地机器的成本。如果每天使用16小时,每周5天,则需约10周。

因此,对于个人爱好者,我们推荐自建PC。如果想进行大规模实验,可以使用 Lambda LabsCoreWeave 等性价比更高的云服务。对于初创公司,可以为每位科学家配备一台强大的本地机器,并在需要时使用云实例进行扩展。对于大公司,资金充裕,可以直接使用云上最快、最便捷的实例。

云服务的另一个优势是抢占式实例,价格更低,适合同时启动大量实验,即使部分实例中途被终止也能接受。这可以显著加快实验周期。

计算资源管理 ⚙️

当我们拥有多台机器、多个科学家和多个实验时,需要有效管理资源。目标是让科学家能够轻松运行大量实验,每个实验都有其特定的依赖和资源需求(GPU、CPU、内存)。

解决方案包括:

  • 自定义脚本:手动检查并锁定可用资源。
  • Slurm:一个成熟的开源集群作业调度系统。用户提交定义资源需求的作业脚本,Slurm 负责调度执行。
  • Docker 与 Kubernetes:Docker 用于打包应用及其所有依赖;Kubernetes 用于在集群上编排和管理 Docker 容器。Kubeflow 是一个基于 Kubernetes 的机器学习工作流平台。
  • 专用解决方案:如 Amazon SageMaker, Domino Data Lab, Determined AI 等一体化平台。
  • 新兴初创公司:如 AnyscaleGrid AI,旨在让从本地开发无缝扩展到云端变得极其简单。

目前,对于大多数团队,Slurm 是一个可靠的选择。但随着生态发展,更高级的解决方案可能会成为主流。

深度学习框架与分布式训练 🧠

深度学习框架抽象了底层复杂性,让我们能专注于模型设计。主要框架有:

  • TensorFlow:早期以生产部署和静态计算图著称,但开发体验稍复杂。TensorFlow 2.0 引入了即时执行,改善了体验。
  • PyTorch:以动态计算图和优秀的开发体验(更“Pythonic”)而闻名,在学术界和新项目中非常流行。
  • 高级库:如基于 PyTorch 的 fast.aiPyTorch Lightning,它们封装了最佳实践,让训练循环、分布式训练等变得更简单。

目前,PyTorch 在新项目开发中占据主导地位。我们将在实验中使用 PyTorch Lightning

分布式训练 是利用多个 GPU 或多个机器训练单个模型的技术,对于大数据集和大模型至关重要。

  • 数据并行:将批次数据拆分到多个 GPU 上,每个 GPU 拥有完整的模型副本,计算梯度后同步平均。这是最常用的方式,能带来近乎线性的加速。
  • 模型并行:将模型本身拆分到多个 GPU 上。当模型太大,单个 GPU 无法容纳时使用,但实现更复杂。

在 PyTorch 中实现数据并行很简单:

if torch.cuda.device_count() > 1:
    model = nn.DataParallel(model)

在 PyTorch Lightning 中更简单,只需在训练器初始化时指定 gpus 参数即可。

实验管理 📊

即使只运行一个实验,也很容易忘记是哪个代码版本、哪些超参数、哪份数据产生了某个模型。当同时运行数十上百个实验时,问题会变得更严重。

低技术解决方案是使用电子表格手动记录。TensorBoard 可以自动跟踪单个实验的训练曲线和指标,是查看单个实验的好工具。

但对于管理大量实验,我们需要更强大的工具:

  • MLflow:一个开源的实验管理平台。
  • Comet, Neptune:提供实验仪表盘、可搜索的实验表格、超参数敏感性分析等功能。
  • Weights & Biases:一个功能全面的实验跟踪平台,我们将在实验中使用它。它可以记录超参数、指标、系统资源(GPU使用率)、输出图表,并能将结果组织成可分享的报告。

超参数优化 🔍

除了记录实验,我们还需要软件帮助我们决定运行哪些实验。这可以是从简单的网格搜索或随机搜索,到更智能的贝叶斯优化等方法。

智能超参数优化的优势在于:

  • 基于已有结果建议新参数:更高效地探索参数空间。
  • 提前终止:自动停止表现明显不佳的实验,节省计算资源。

一些解决方案包括:

  • SigOpt:提供超参数优化 API 服务。
  • Ray Tune:Ray 生态系统中的超参数调优库。
  • Weights & Biases Sweeps:W&B 内置的超参数优化工具,我们将在实验中使用。你可以定义一个参数搜索空间和优化策略,W&B 会协调多个“代理”来并行运行实验。

一体化机器学习平台 🚀

最后,市场上存在一些试图覆盖从数据到部署全流程的一体化平台。早期的例子是 Facebook 的 FBLearner。现在,各大云厂商和初创公司都提供了类似方案:

  • 云厂商平台:如 Google Cloud AI Platform, Amazon SageMaker, Microsoft Azure Machine Learning。它们提供数据标注、处理、笔记本开发、一键训练、实验跟踪、模型部署和监控等功能,但通常价格较高。
  • 初创公司平台:如 Domino Data Lab, Determined AI, Gradient。它们也提供类似的功能集,定价模式各异。Determined AI 是开源的。

这些平台的优势在于集成度和易用性,但可能会将你锁定在特定的生态系统中,并且有额外的费用。对于刚起步的团队,从 Weights & Biases 等专注于特定环节的优秀工具开始,可能更灵活。

总结

本节课我们一起学习了构建机器学习系统所需的基础设施和工具链。我们从理想的自动化工作流出发,探讨了现实中数据、训练、部署各环节的挑战。我们详细介绍了:

  1. 以 Python 和 VS Code 为核心的软件开发环境
  2. 依赖管理的最佳实践。
  3. 深度学习对计算资源的需求,以及如何根据自身情况在本地硬件云服务之间做出选择。
  4. 使用 SlurmKubernetes 等工具进行计算资源管理
  5. 主流的深度学习框架(PyTorch/TensorFlow)及其高级封装。
  6. 分布式训练的基本概念。
  7. 使用 Weights & Biases 等工具进行实验管理和跟踪
  8. 使用 W&B Sweeps 等进行超参数优化
  9. 市面上主要的一体化机器学习平台及其特点。

理解并合理运用这些工具,将帮助你更高效、更系统地构建、迭代和部署机器学习模型。

📚 课程 P13:【Lab5】实验管理

在本节课中,我们将学习实验管理。课程将介绍 IAM 手写数据集,并展示如何通过数据增强技术,使我们的合成数据集更接近真实数据。我们将引入 Weights & Biases 工具来跟踪实验,并在增强后的数据集上运行实验。最后,我们将启动一个超参数扫描任务。

🗂️ 数据集介绍:IAM

首先,我们来介绍一个新的数据集——IAM。这是一个真实手写文本的数据集,收集时间可能在 90 年代或 21 世纪初。我们可以通过查看笔记本来了解它的样子。

在查看笔记本之前,我们先检查一下代码仓库。仓库中包含了 IAM 的原始数据和一个 README 文件。该文件描述了数据来源、任何预处理步骤、数据应如何分割等信息。这个数据集包含大约 1500 页扫描文本,被分割成 13000 行文本。元数据中包含了数据下载链接、用于校验下载文件正确性的哈希值,以及应属于测试集的页面 ID。如果我们想与已发表的结果进行比较,这一点很重要。

运行以下命令可以下载并处理数据集:

python text_recognizer/data/iam_lines.py

该命令会将数据集下载到 data/ 目录下。下载的文件夹中包含 forms 目录,里面是原始图像。我们可以查看这些图像的样子。

这是人们提供的数据类型。他们被要求在一张纸上书写,然后数据标注员会为每一行甚至每个单词绘制矩形框。因此,在这个数据集中,我们拥有这些位置信息。

现在,我们切换到笔记本。我们将 augment_data 参数设为 False,以便先不查看增强效果。数据加载后,图像的尺寸是高 56 像素,长 1024 像素。最大序列长度是 89 个字符。我们可以通过绘图并将标签作为每个图的标题来查看数据。你会注意到,每个标签都以一个起始标记开始,以一个结束标记结束。我过滤掉了填充标记,所以每个序列都被填充到了最大长度 89。图像看起来是这样的。

这里有各种各样的手写体:有些在水平方向上被压缩,有些是倾斜的,有些整体方向可能有些旋转。查看数据的目的是了解可能存在的多样性。我们还将数据绘制在 0 到 1 之间,黑色是 0,纯白色是 1。我们看到大多数墨迹的灰度值大约在 0.7 到 0.8 之间,并非纯白。

🛠️ 合成数据增强:EmnistLines2

现在我们已经了解了真实数据,可以尝试让我们的合成数据集看起来更像它。我们之前构建了 EmnistLines 数据集。

notebook 02 中查看这个数据集是一个好习惯。记得我们有一些简单的字符串,然后我们引入了一些更复杂的字符串,比如包含字符重叠和更长序列的情况。但它们都很简单:文本总是填满行的整个垂直空间,总是从左侧边缘开始,并且都处于相同的水平维度,没有任何拉伸、倾斜或旋转。

为了让这些数据看起来更像真实数据,我们需要引入一些额外的变换。为此,我们创建了 EmnistLines2 数据集。它的代码大部分与 EmnistLines 相同,但现在看起来是这样的。

这里有旋转、颜色不那么纯白(有时更淡)、水平压缩、有些存在倾斜(倾斜方向可以是双向的),并且文本不一定从行的开头开始。这看起来真实多了。我们也可以查看测试集,它看起来仍然是旧的样子。

我想简要展示一下实现这些效果的代码。打开 emnist_lines2.py,关键代码是我们传递给数据集的 transform。为了创建训练集和验证集,我们传递这个 transform,它是 get_transform 函数的结果,并且 augment 参数为 True

那么什么是 transform 呢?这被称为数据增强。我们之前使用的 transform 只是将 NumPy 数组或 PIL 图像转换为 PyTorch 张量,即 transforms.ToTensor()。但现在,如果 augmentTrue,我们还会进行颜色抖动(使亮度在原始值的一半到原始值之间随机变化),并添加一个随机仿射变换。

我们可以搜索 torchvision.transforms.RandomAffine 来了解它。它可以围绕中心点旋转图像、在垂直和水平方向上平移图像、缩放图像以及剪切图像。我们会对这四种变换都应用一点点。它的美妙之处在于它是随机的,所以每次加载批次时,都会对图像应用不同的随机变换。这对于防止过拟合至关重要。

这就是我们的数据的样子。我们不对测试集应用数据增强,这就是为什么测试集看起来仍然是旧的样子。通常,保持测试集不受数据增强的影响是一种好的做法,这样每次在测试集上运行时,性能指标都是完全相同的,不依赖于任何随机因素。

📊 实验跟踪:Weights & Biases

现在我们已经准备好了 EmnistLines2 数据集,可以运行一些实验了。这次我们将使用 Weights & Biases 来跟踪实验。如果不记得了,它是我们在基础设施课程中介绍的实验管理工具之一,其他还有 Comet.ml、MLflow、Determined AI 等。我喜欢 Weights & Biases,因为它速度快,功能全面,并且有一些更高级的功能,比如我们将在本实验中使用的超参数扫描。

要设置 Weights & Biases,首先运行:

wandb init

这会弹出一条消息,要求你粘贴 API 密钥。然后它会引导你到 wandb.ai 进行授权。如果你还没有账户,可以通过 GitHub(我认为最简单)、Google 登录或注册一个新账户。账户是免费的,这些功能也都是免费的。

登录后,进入设置页面,复制 API 密钥,然后粘贴到你的 Colab 或终端中。接下来,它会要求你输入项目名称,你可以输入任何名称,但最好输入这个代码仓库的名称,例如 fsdl-text-recognizer-2021-labs

现在我们已经登录并设置了 Weights & Biases。那么,我们需要添加或已经为我们添加了哪些代码来使用 Weights & Biases 进行日志记录呢?

新的代码在 run_experiment.py 中。run_experiment.py 是我们的框架,它从命令行读取参数并加载数据。现在我们可以传递一个命令行标志 --wandb。如果这样做,它将用 Weights & Biases 的记录器替换默认的 TensorBoard 记录器。这是为了使用 Weights & Biases 所做的唯一更改,这非常方便。

此外,我们还可以(这是可选的)让记录器监视模型,这将跟踪模型中的所有梯度,并能够在 Weights & Biases 上绘制所有梯度的大小。我们还将记录所有超参数。对我们来说,所有超参数就是命令行中的所有内容。因此,如果我们想改变代码中的某些内容并希望搜索其不同值,我们就把它变成一个命令行参数,然后我们把所有的命令行参数都称为超参数。

集成的程度非常简洁。接下来,我们想运行一个实验。我们使用以下命令:

python run_experiment.py --wandb ... (其他参数)

在终端或 Colab 中运行(在 Colab 中记得在前面加上 !)。这是训练开始的标准输出,它会打印模型信息等等。但现在,一开始它会显示 Weights & Biases 的信息,比如我们当前已登录,并链接到这个项目和这个特定的运行,我们可以查看配置(所有超参数)、图表(很快就会在我们完成第一个周期后显示)以及媒体(我稍后会谈到)。

第一个周期完成后,我们有一个数据点用于训练损失、验证损失、验证字符错误率和周期。我喜欢做的一件事是将周期图表改为基于相对墙上时间,这样便于比较多个运行的时长。然后,训练损失图以周期数而不是步数(步数是批次的另一种说法)显示可能更好看一些。也许我还想稍微调整一下布局,让训练损失和验证损失并排显示。

我们记录了模型中所有层的梯度大小,这很酷。我们可以看到梯度大小的简要分布。这可以让你看到梯度爆炸(你会看到一条粗的、颜色均匀的线,而不是细的蓝线)或梯度消失(所有值基本上都为零,周围没有梯度大小的分布)。

现在训练正在进行中。我可以切换到一些已经运行完毕的实验。在这个特定的项目中,我有三个运行,其中一个刚刚启动(旁边有绿点),另外两个已经完成。我们可以比较它们,这非常有帮助。我们可以看到最终的测试字符错误率。

我想谈谈这个“媒体”部分。如果这个实验正在运行,你会看到一个叫做“验证预测示例”的东西,图像是静止的,预测是“the stare and the the at”,这不太准确。这是从哪里来的呢?

这来自 lab5/text_recognizer/line_cnn_transformer.py。在验证步骤和测试步骤中,现在有了一小段代码:我们在批次上进行预测,然后对于该批次的第一个元素,我们将预测转换为可读的字符串,然后使用 wandb.logger 将其记录为 val_pred_example。这很有帮助,因为 Weights & Biases 能够将这些记录为媒体。Weights & Biases 还支持音频和其他不同格式的数据。

如你所见,这个预测目前完全是垃圾,但有趣的是,它是英文垃圾。这告诉我们,在这短短的时间内,我们模型的 Transformer 部分实际上已经学会了如何生成文本,但还没有学会理解图像。它能生成任何文本已经很了不起了。

如果我们查看已经训练好的其他运行,比如这个橙色的运行 vital_valley_2,它得到了 4% 的测试字符错误率。点击进入,训练损失降得很低,验证损失也是如此。看看它实际产生的验证预测示例:图像是“something truly amazing”,预测也是“something truly amazing”,这很酷。我们可以逐步查看,这里有一个错误:“read for a few moments”,但模型预测的是“read for afu menendez”。但很酷的是,你可以回溯时间,看到模型在训练过程中是如何变得更好的。当然,在测试集评估运行完成后,我们也会得到测试预测示例。

在 Weights & Biases 实验管理中,这个图表视图很酷,因为你可以比较曲线。但这个表格视图也很酷,因为你可以添加笔记。在表格视图中,首先要做的是点击“自动显示最有用的列”按钮,这通常会显示运行之间所有不同的内容。在这个例子中,实际上没有超参数不同,但我们可以手动将它们从隐藏列拖到可见列,然后查看超参数是什么。

这个笔记功能非常有帮助,因为当你进行实验时,你可以按创建日期排序,最新的运行在最上面。也许这个运行有什么不同你想记录下来,你可以添加一个笔记,比如“尝试 precision=16”。这对你自己来说是一个记录,让你知道你实际在做什么。如果你有一个真正的项目在进行,这非常有用,因为通常你不仅在命令行上更改超参数,还在更改代码,而这些代码更改以后很难追踪。添加一个小笔记,甚至做一个 Git 提交,对于知道你明天到底做了什么(比如尝试 precision=16)至关重要。

现在,我们可以在命令行上尝试 precision=16,它将以混合精度模式运行相同的实验,使用 PyTorch 原生的 16 位支持。这可以通过降低内存占用显著加快实验速度。如果你设置 precision=16,你现在可以增加批次大小。我们的默认值是 128,但我们可以尝试增加到 256。这基本上总是值得作为默认设置开启的。

🔍 超参数扫描

最后,我想谈谈超参数扫描。我们已经运行了第一个 Weights & Biases 实验。现在假设我们达到了一个点,对我们的实验比较满意,比如可能收敛了,但花了很长时间;或者可能没有完全收敛,但感觉如果我们只是改变学习率或层数,它可能就会收敛。这时就是进行超参数搜索的好时机。

我们已经为你设置了一个扫描(Weights & Biases 对超参数优化实验的称呼)。让我们来看一个训练扫描配置文件:emnist_lines2_line_cnn_transformer.yml

重要的是,我们指定要运行的程序 training/run_experiment.py,超参数优化方法(可以是网格搜索、随机搜索或贝叶斯优化,Josh 都讲过),我们想要优化的指标(这里是最小化验证损失),以及一个很酷的功能——使用 Hyperband 算法进行早停。它会在前 20 个周期内,如果一个实验不比已存在的实验更好,就终止该实验。这需要注意,因为如果你使用较低的学习率,可能在 20 个周期内达到的点不如使用较高学习率在 60 个周期内达到的点。所以在这里保守一点比较好,也许将早停周期设为 60。

然后是我们想要传递给模型的参数列表。如果你不想改变某个参数,可以排除它;或者如果你想记住它是什么,可以给它一个单一的值,比如 window_stride: 8。但如果你想让它在一个范围内变化,比如浮点参数学习率,你可以设置 min: 0.0001max: 0.1,然后它会随机采样。当然,我们也可以手动列出几个值。总的来说,我认为对于我们正在做的事情,值得把它们都写出来,这样你可以看到所有将要尝试的配置。

我们将进行 16 位精度训练,使用 line_cnn_transformer 模型和 emnist_lines2 数据集,然后随机采样所有这些参数的设置。

要启动这个扫描,运行:

wandb sweep path/to/sweep.yml

这将联系 Weights & Biases 服务器并在服务器上设置一个扫描点,我们的代理现在可以调用它并请求一组参数。下一步是启动一个代理。我的机器上有两个 GPU,我喜欢做的一件事是使用 CUDA_VISIBLE_DEVICES 环境变量,只将一个 GPU 暴露给即将运行的进程。我会运行一个代理,然后打开另一个终端,暴露第二个 GPU 并启动另一个代理。这样我就有两个 Weights & Biases 代理,每个都调用 Weights & Biases 获取一组参数,然后开始运行。所有这些运行都会显示在我启动扫描时显示的 URL 中。

在 Weights & Biases 界面的“Sweeps”部分,可以看到扫描的运行情况。随着运行的完成,它们会根据你关心的指标(可能是验证损失或验证字符错误率)进行排序,我们希望这个值低。我们可以看到所有我们关心的参数的设置,这些设置导致了最低的验证损失。

你也可以在 Colab 中做这个,这很酷。我在 Colab 中创建了扫描,并在 Colab 中运行了代理,它正在愉快地运行。这个运行的验证错误率降到了 6%,我认为这已经很不错了。

🎯 总结与作业

在本节课中,我们一起学习了实验管理的核心内容。我们介绍了真实的 IAM 手写数据集,并通过数据增强技术创建了更逼真的合成数据集 EmnistLines2。我们集成了 Weights & Biases 工具来跟踪和比较实验,包括记录指标、梯度、超参数和预测示例。最后,我们启动了超参数扫描来自动化寻找最佳模型配置的过程。

你的作业是至少运行一个使用 Weights & Biases 记录日志的实验,并上传一张你的 Weights & Biases 运行表格的截图。你可以尝试为当前实验找到一组能最快完成训练的超参数设置(例如,减少 Transformer 层数或压缩维度)。你也可以尝试用 line_cnn_lstm 替换 line_cnn_transformer,或者在 IAM 真实数据上运行实验。基本上,你可以尝试任何你想做的事情,包括运行一个扫描。

P14:L7- 深度神经网络调试与故障处理 🐛🔧

在本节课中,我们将要学习如何系统地调试和优化深度神经网络。构建机器学习系统时,常常感觉像是在处理一堆随机数据,期望得到更好的结果。即使是顶尖的从业者,大部分时间也花在调试和调优模型上。本课程的目标不是消除这些必要的工作,而是提供一套策略,让这个过程更有条理,减少盲目性。

为什么调试如此困难?🤔

首先,我们需要理解为什么深度神经网络的调试如此具有挑战性。

一个常见场景是:你复现了一篇论文中的模型(例如 ResNet),但性能远不如论文中报告的结果。问题出在哪里?性能不佳可能有多种根源,且它们常常难以区分。

1. 实现中的 Bug
大多数深度学习 Bug 是隐形的,不会导致程序崩溃。例如,一个常见的 Bug 是数据加载顺序的非确定性(如 Python 中 glob 的返回顺序),这可能导致模型完全无法学习。

2. 超参数选择敏感
模型性能对超参数(如学习率、权重初始化)的微小变化可能极其敏感。例如,使用错误的学习率可能导致模型完全不收敛,而合适的权重初始化可能是模型成功训练的关键。

3. 数据与模型匹配问题
你使用的数据集(如自动驾驶图像)可能与原论文的数据集(如 ImageNet)分布不同,这自然会导致性能差异。

4. 数据集构建问题
在工业界,大部分时间花在处理数据上。数据集问题可能包括:数据量不足、类别不平衡、标注噪声、训练集与测试集分布不一致等。

总结来说,调试困难的原因在于:

  • 很难第一时间确定问题是否是 Bug。
  • 即使知道有问题,也难以定位具体原因(是 Bug、超参数还是数据?)。
  • 模型对超参数和数据集的微小变化非常敏感。

核心策略:从简到繁,逐步迭代 🔄

为了克服上述困难,我们推荐的核心策略是:保持悲观心态,从最简单的版本开始,逐步增加复杂性

这样,在每一步中,我们只引入微小的变化。如果性能出现意外下降,我们就可以将问题隔离到刚刚引入的变更上,从而高效地进行调试。

该策略的具体流程如下:

  1. 从简开始:选择简单的模型和数据集,精简问题。
  2. 实现与调试:实现所选模型,并确保其没有 Bug。
  3. 评估模型:使用评估结果决定下一步行动(改进模型、改进数据或调优超参数)。
  4. 迭代:循环执行上述步骤,直到模型达到预定目标。

接下来,我们将详细探讨每个步骤。

第一步:从简开始 🏁

在项目初期,我们的目标是建立一个尽可能简单、可调试的基线系统。

1. 选择简单的模型架构

对于不同的输入数据类型,可以遵循以下建议选择初始架构:

  • 图像数据:从简单的 CNN(如 LeNet) 开始。
  • 序列数据:可以从 单层 LSTM 开始。如今,从 Transformer 开始也是一个有效的选择。
  • 其他数据:从 单隐藏层的全连接神经网络 开始。
  • 多模态数据:将每种模态的数据分别通过适合的架构(如图像用 CNN,文本用 LSTM)映射到低维特征空间,然后将这些特征向量拼接(Concatenate)起来,最后通过一个或多个全连接层得到输出。这是一个强大且易于调试的基线。

2. 选择合理的默认超参数

在第一次实现时,使用以下经过验证的默认值可以省去很多麻烦:

  • 优化器Adam
  • 学习率3e-4 (常被称为“魔法学习率”)
  • 激活函数ReLU (对于 RNN 可用 tanh
  • 权重初始化
    • CNN/FC 层:He normal 初始化
    • LSTM 层:Orthogonal 初始化
  • 重要建议:在第一个版本中,不要使用任何正则化(如 Dropout, BatchNorm)或复杂的数据标准化。这些是常见的 Bug 来源,应先确保核心实现无误后再加入。

3. 标准化输入

这是一个绝不能跳过的步骤。对于大多数数据,标准化意味着减去均值并除以方差。对于图像,简单地将像素值缩放到 [0, 1][-0.5, 0.5] 即可。忘记标准化或重复标准化是导致模型学习缓慢或失败的常见原因。

4. 简化问题本身

在解决最终目标之前,先在一个简化版本上验证你的流程:

  • 使用更小的训练集
  • 减少分类的类别数量
  • 降低输入尺寸(如图像分辨率)。
  • 甚至可以考虑使用合成数据集

示例:行人分类任务
假设我们的目标是 99% 的分类准确率。在项目初期,我们可以:

  • 使用 10,000 张图像的子集进行训练,1,000 张用于测试。
  • 选择简单的 LeNet 架构。
  • 使用 Adam 优化器和 3e-4 的学习率。
  • 暂时不使用任何正则化

第二步:实现与调试 🛠️

现在,我们开始实现模型并进行系统性调试。

阶段一:让模型运行起来

首先,确保模型能够正常执行前向和反向传播,不报错。

以下是常见的初期问题及解决方法:

  • 张量形状不匹配/类型错误:使用调试器(如 ipdb)逐步执行模型创建和前向传播过程,检查每一步输出的张量形状和数据类型。
  • 内存不足(OOM):逐步缩小内存密集型操作(如减小批大小、减少通道数),直到模型能够运行,从而定位问题。
  • 其他问题:运用标准的软件工程调试技巧(如搜索错误信息)。

调试技巧

  • 尽量保持第一个版本的代码轻量(目标少于 200 行),不包括已测试的基础设施代码。
  • 尽可能使用高级框架(如 Keras, PyTorch Lightning),利用其内置的、经过充分测试的层实现,避免自己编写容易出错的数学运算。
  • 初期使用可以全部加载到内存中的数据集,避免复杂数据管道引入的 Bug。

阶段二:过拟合一个批次

这是验证模型学习能力的关键测试。具体做法是:只用一小批数据(如 2, 4, 32 个样本),训练模型使其损失降到任意接近零

如果无法过拟合,说明存在 Bug。常见现象和可能原因:

  • 误差上升:可能损失函数符号取反。
  • 误差爆炸(先降后升):数值不稳定问题,或学习率过高。
  • 误差振荡(周期性升降)数据或标签损坏(如标签顺序被打乱),或学习率过高。
  • 误差平台期(降到某个值后不再下降):尝试增大学习率;检查是否误用了正则化;仔细检查损失函数计算和数据管道。

这个测试成本极低(几分钟),但能提前发现许多隐蔽的 Bug,强烈推荐。

阶段三:与已知结果对比

在增加复杂性之前,将你的模型结果与某个“已知良好”的结果进行对比。

按推荐度降序排列的对比基准:

  1. 官方实现:在同一数据集上对比,可以逐行比对代码和性能。
  2. 基准数据集:在 MNIST 等标准数据集上运行你的模型。如果准确率低于 99%,很可能存在 Bug。
  3. 论文结果:对比性能是否在同一量级,但无法帮助定位问题。
  4. 简单基线:至少确保你的模型性能优于随机猜测或预测均值。

实现与调试流程总结

  1. 让模型运行:处理形状、类型、内存错误。
  2. 过拟合一个批次:确保模型具备基本学习能力,排查数据、正则化、广播错误。
  3. 对比已知结果:持续迭代,直到模型性能达到预期。

第三步:模型评估与改进决策 📊

模型运行无误后,我们需要评估其性能,并决定下一步优化方向。这里我们使用 偏差-方差分解 作为分析框架。

偏差-方差分解

假设我们有一个目标性能(如人类水平),训练误差和验证误差曲线如下图所示。测试误差可以分解为:
测试误差 = 不可避免误差 + 偏差 + 方差 + 验证集过拟合

  • 不可避免误差:问题固有的难度(如人类水平)。
  • 偏差:训练误差与目标性能的差距,反映欠拟合程度。
  • 方差:验证误差与训练误差的差距,反映过拟合程度。
  • 验证集过拟合:测试误差与验证误差的差距,因在验证集上迭代过多导致。

处理分布偏移

现实中,训练集和测试集分布可能不同(如训练数据是白天场景,测试需要处理夜间场景)。建议使用两个验证集

  • 训练验证集:从训练分布中采样。
  • 测试验证集:从测试/目标分布中采样。

这样,误差分解中会多出一项:分布偏移误差(两个验证集性能的差距)。

示例:行人分类任务评估
假设我们训练初始模型后得到:

  • 目标误差:1%
  • 训练误差:20%
  • 验证误差:25%
  • 测试误差:26%

分析:

  • 偏差很大(20% - 1% = 19%):模型严重欠拟合。
  • 方差存在(25% - 20% = 5%):同时存在过拟合。
  • 分布偏移小(26% - 25% = 1%):暂不主要关注。
  • 验证集过拟合小:暂不关注。

结论:当前首要任务是解决欠拟合(高偏差)问题。

第四步:改进模型与数据 🚀

根据偏差-方差分解的结果,我们按以下优先级解决问题:先解决欠拟合(偏差),再解决过拟合(方差),接着处理分布偏移,最后处理验证集过拟合。

1. 解决欠拟合(减少偏差)

以下策略按推荐度排序:

  • 增大模型容量:添加更多层,或每层使用更多单元。
  • 减少正则化:如果初始版本加入了正则化,可以减弱或移除。
  • 错误分析:人工检查模型在训练集上分错的样本,寻找规律。
  • 选择更先进的架构:例如,从 LeNet 切换到 ResNet。
  • 调优超参数:特别是学习率。
  • 添加特征:在深度学习中不常手动进行,但有时很有效。

示例迭代

  1. 为 CNN 添加更多层 → 训练误差降至 7%。
  2. 换用 ResNet 架构 → 训练误差降至 3%。
  3. 调优学习率 → 训练误差达到目标 1%。

此时,训练误差达标,但验证误差可能仍很高(例如 10%),表明过拟合(高方差)成为主要矛盾。

2. 解决过拟合(减少方差)

以下策略按推荐度排序:

  • 添加更多训练数据:最根本的方法。
  • 添加数据增强:对图像进行旋转、裁剪等。
  • 添加正则化:如权重衰减(L2)、Dropout。
  • 添加归一化层:如 BatchNorm, LayerNorm。
  • 错误分析:检查验证集错误。
  • 使用更合适的架构
  • 调优超参数
  • (不推荐)使用早停、减少特征。

示例迭代

  1. 使用全量数据(25万张)→ 验证误差降低。
  2. 添加权重衰减 → 验证误差进一步降低。
  3. 添加数据增强(随机翻转、亮度调整)→ 验证误差接近训练误差。
  4. 再次调优超参数(学习率、正则化强度)→ 验证误差达到目标 1%。

3. 解决分布偏移

如果“测试验证集”误差显著高于“训练验证集”误差,说明存在分布偏移。

解决策略:

  1. 错误分析并收集数据:手动分析测试验证集上的错误样本,找出特有模式(如夜间图像),然后有针对性地收集更多此类数据。
  2. 合成数据:对现有训练数据进行修改以模拟测试分布(如将白天图像调暗)。
  3. 领域自适应:利用来自目标域的无标签或少量标签数据来调整模型。

错误分析表示例

错误类型 训练验证集占比 测试验证集占比 潜在解决方案 优先级
行人难以看清 使用更好传感器
图像反光 收集反光数据/数据增强
夜间场景 收集夜间数据/合成黑暗图像

4. 解决验证集过拟合

如果最终测试集性能显著低于验证集性能,说明可能对验证集进行了过拟合。解决方案是:重新采样或收集新的验证集

第五步:超参数调优 🎛️

超参数调优是模型改进中的重要环节。

选择调优哪些超参数?

模型对不同超参数的敏感度不同。以下是根据经验总结的调优优先级(在已使用合理默认值的前提下):

  1. 学习率及学习率调度:几乎总是最敏感。
  2. 模型深度(层数)层宽度(单元数):影响容量。
  3. 优化器相关(如 Adambeta1):有时有效。
  4. 正则化强度(如权重衰减系数)。
  5. 激活函数批大小权重初始化:调整它们有时有提升,但通常不是首选。

调优方法

以下是几种常见方法,按推荐度排序:

  • 手动调优(“研究生下降法”):依靠经验和直觉。计算效率可能最高,但耗时且需要深厚知识。可作为其他方法的补充。
  • 网格搜索:在定义好的网格上均匀采样。简单但效率低,尤其对多个超参数。
  • 随机搜索:在定义好的范围内随机采样。实证表明,在相同计算预算下,随机搜索通常优于网格搜索。 实现简单。
  • 由粗到精的随机搜索
    1. 在较宽的范围进行随机搜索。
    2. 锁定表现最好的若干次运行。
    3. 在这些点周围缩小范围,进行新一轮随机搜索。
    4. 可重复此过程。这是实践中强烈推荐的方法,能在手动干预和自动化之间取得良好平衡。
  • 贝叶斯超参数优化:使用概率模型指导搜索方向,效率最高。但实现和集成较复杂,适合成熟的项目或使用现有工具(如 OptunaHyperopt)。

总结:对于新项目,从由粗到精的随机搜索开始。随着项目成熟,可以考虑引入贝叶斯优化方法

课程总结 📝

本节课中,我们一起学习了深度神经网络调试与故障处理的系统化方法。

我们认识到调试困难的根本原因在于:性能下降的根源多种多样且难以区分。为此,我们介绍了一套核心策略:保持悲观,从最简单的可行方案开始,逐步、迭代地增加复杂性

我们详细探讨了该策略的四个阶段:

  1. 从简开始:选择简单架构、合理默认值、标准化输入、简化问题。
  2. 实现与调试:确保模型能运行、能过拟合一个批次、能与已知结果对比。
  3. 评估与决策:运用偏差-方差分解框架分析误差来源,确定优化优先级(偏差->方差->分布偏移->验证集过拟合)。
  4. 迭代改进:根据评估结果,有针对性地改进模型、数据,并运用由粗到精的随机搜索等方法进行超参数调优。

通过遵循这个流程,你可以更有信心、更高效地构建和优化深度学习模型,减少在“随机搅拌数据”中的迷茫时间。

📊 P15:L8- 数据管理

在本节课中,我们将要学习深度学习项目中数据管理的核心概念、工具与最佳实践。数据是机器学习项目的基石,高效地获取、处理、探索和版本化管理数据,对于项目的成功至关重要。

概述与动机

我们首先来看一些关于数据重要性的观点。许多初级机器学习工程师最大的失败之一,是对构建数据集缺乏兴趣。实际上,在整理数据集的过程中可以学到很多东西。

一项针对数据科学家的调查显示,他们大部分时间都花在了数据清洗和数据移动上。另一位从业者也指出,在最近的几个项目中,数据管理占据了绝大部分工作量。

当我们具体思考深度学习的数据管理时,其过程可能涉及许多环节。数据可能最初以日志文件的形式散落在文件系统甚至多台机器上,或者存储在数据库中。但最终,我们需要将所有数据转移到GPU(或多GPU)旁边的本地文件系统中,这是进行深度学习训练的一个关键约束条件。

将数据转换为可训练格式的路径,对于每个项目、每家公司而言都是独特的。例如,你可能在ImageNet上训练,所有图像都是S3链接,你只需将它们下载到本地文件系统。或者,你可能自己从某处爬取了一批文本文件,然后使用Spark在集群上处理它们,得到一个可供分析和任务选择的数据框,再将子集转移到GPU。又或者,你正在从数据库收集日志和记录到数据湖或数据仓库(如Snowflake),然后从中处理数据并转换为可训练格式。

数据管理的可能性非常多,我们无法在本讲座中完全覆盖。事实上,我们将更多地讨论数据管理的细节。但在深入之前,我想先强调几个关键点。

第一点,我们不应在数据集上花费过多时间,但我们应该花十倍于预期的时间去真正“理解”数据,让数据“流过”你的思维。

第二点,数据通常是提升整个机器学习项目性能的最佳途径。与其尝试新算法、新架构或启动超参数搜索,增加更多数据通常是改进性能的最好方法。在没有新数据的情况下,至少要想办法增强现有数据。

第三点,我们将讨论许多复杂的流程和术语,了解它们固然重要,但保持简单也同样关键。不要过度复杂化事情。归根结底,我们只是试图将数据转换成可以加载到GPU上的形式,这并非火箭科学。虽然这最终会占用我们大量时间,但我们仍应尽可能保持简单。

🗺️ 数据管理全景图

在几周前的基础设施讲座中,我们重点讨论了全景图中的训练和评估部分。今天,我们将重点关注数据部分,这包括数据源、数据湖/数据仓库、数据处理、数据探索、数据标注和数据版本控制。

对于大多数深度学习应用,我们需要大量专有数据。当然也有例外,例如强化学习、GANs,或者那些竞争壁垒不在于专有数据集,而在于专有计算方法的项目(例如需要数千个GPU和数百万美元,以及如何运行它们的专业知识,GPT-3可能就是这种情况)。但对于大多数其他情况,我们需要专有数据集。

公开可用的数据集很有用,但它们没有竞争优势,因为任何人都可以获得相同的公开数据集,并达到与你的模型相同的性能水平。然而,它们对我们仍然有用,可以作为基准测试的工具。

通常,公司或项目最终会花费资金和时间来标注自己的数据。例如,为金融、房地产或农业应用开发模型,你可能需要不同于其他人的标签。很多时候,你必须购买数据,并购买人力时间来标注它。

数据飞轮是一个有趣的概念。如果你能让模型面向用户,并以一种让用户实际贡献优质数据、清理数据甚至修正模型预测的方式来开发产品,这通常被称为数据飞轮,它可以在V1模型发布后实现快速改进。

半监督学习是当前非常流行的一个重要思想。其理念是你不一定需要花钱标注数据,而是可以通过重新定义问题,让任务略有不同。例如,对于文本,你可以用过去的数据预测未来的数据(如补全句子),或者用句子结尾预测开头,用周围词语预测中间词语,或者判断两个句子是否出现在训练语料库的同一段落中。有很多方法可以构建问题,使你实际上不需要标注任何东西,只需用数据自我监督。这同样适用于视觉领域。事实上,就在几天前,Facebook AI Research发布了一个名为SEER的模型,它在10亿张随机图像(而非ImageNet的100万张标注图像)上训练,却能在ImageNet的Top-1预测任务上达到最先进的准确率。他们发布了用于构建任务的库(称为Vissel),其中包含对比散度损失等损失函数,值得一看。

增强训练数据是必须做的事情,至少在视觉模型中如此。例如,你可以调整图像对比度、裁剪不同部分、遮挡图像块、像素化、旋转、剪切等。TensorFlow、PyTorch等框架都提供了帮助实现这些功能的函数。数据增强通常在CPU上与GPU训练并行进行:GPU训练时,CPU生成增强后的训练数据。

对于表格数据,你可以通过空白部分单元格来模拟缺失数据。对于文本数据,虽然没有非常成熟的技术,但你可以用同义词替换词语,或者改变某些内容的顺序。对于语音和视频等时序数据,你可以裁剪部分内容、改变速度、拉伸或压缩时间线、注入不同类型的噪声,或者在不同频率上进行掩码。

合成数据是一个有趣的想法,通常值得从它开始,但根据我的经验,实际使用并不频繁。这里链接了一篇来自Dropbox的博客文章,他们使用大量合成生成的单词图像构建了一个OCR(光学字符识别)流程,用于处理用户存储在Dropbox中的文档。

合成数据可以做得非常深入。例如,Andrew Moffett的一个项目,在收据图像上训练OCR,这些收据实际上是在Unreal Engine等3D引擎中渲染的。因此,这是一个完整的3D表面变形模拟,可以模拟疯狂的照明条件和阴影。右侧的所有图像都是合成生成的,但你仍然拥有完美的真实数据,因为你确切知道变形是如何完成的,从而知道图像中每个像素的标签。

对于驾驶和机器人技术,合成数据几乎是必须的,这通常被称为“仿真到现实”。Josh Tobin在这方面做过研究,如果你想了解更多可以与他交流。

🧱 数据存储基础:块、文件系统与数据库

接下来,我们讨论数据存储的基础:文件系统、对象存储、数据库、数据湖/数据仓库。什么数据应该放在哪里?以及我们如何了解更多。

文件系统是存储数据的基础层,我们习惯以文件为单位进行思考。文件可以是文本文件或二进制文件。文件系统本身不提供版本控制,文件容易被覆盖或删除。文件系统可以是你插入并格式化的硬盘,也可以是网络文件系统(如NFS),使得同一硬盘可从不同机器访问,还可以是分布式文件系统(如Hadoop文件系统),数据存储并访问自多台机器。

在存储速度方面,最新的NVMe SSD技术非常快。例如,SATA硬盘的吞吐量约为200 MB/s,SATA SSD约为500 MB/s,而NVMe SSD可达3 GB/s。寻道时间也是如此:硬盘约2毫秒,SSD更快,NVMe SSD则快得多。

我们应该以什么格式存储数据?对于图像、音频、视频等二进制数据,通常直接存储为文件,例如文件夹中的图像文件。TensorFlow有TFRecord格式,用于批处理二进制文件,将大量单个文件打包成批次文件,这在旧式硬盘上很有意义,但对于高速NVMe驱动器似乎不那么必要。

对于大型表格数据或文本数据,有一些选择。你可以将它们存储为文件,这完全可以。但如果你从不同来源加载文件到数据框中,并希望存储以便下次项目可以从某个中间格式开始,而不是从头开始,你有几个选择:HDF5功能强大但臃肿,似乎不再活跃开发;Parquet格式来自Hadoop和Spark大数据世界,目前我推荐使用它来存储这类数据;Feather是最新的好选择,由Apache Arrow开源项目支持,发展迅速,旨在成为数据分析的交换格式,它是一种内存映射的列式数据存储,操作速度非常快。

PyTorch等框架提供了自己的数据集接口(如DataLoader),应尽可能使用它们,因为这是官方支持的数据加载方式。偏离越远,越可能遇到问题。

对象存储可以看作是文件系统之上的API。Amazon S3是典型例子,我们可以通过REST API调用获取、存放、删除文件,而不必担心文件具体在哪个物理磁盘上。对象可以是文本文件或二进制文件(通常是二进制文件)。有趣的是,你可以在API中内置版本控制和冗余。存储文件时,后端可以递增版本号,而不是覆盖旧文件。对象存储的速度不如从本地驱动器读取文件快,但在云环境中(例如云计算实例访问S3文件)通常足够快。

实际上,Databricks有一张幻灯片建议人们将日志文件等存储在S3上,而不是配置自己的HDFS文件系统。他们给出的理由是S3具有弹性(可存储任意数量的文件)、相当便宜、高可用性、非常耐用,甚至可以有事务性保证,相当不错。

数据库这个词指的是持久、快速、可扩展的结构化数据存储和检索系统,这些数据会被反复访问。例如,日志是结构化数据(包含时间戳、访问页面、REST动词等),但通常不会被反复访问,它只是被存储以备不时之需。而在数据库中,你不会存储日志,你会存储像用户名、图像标签等可能变化且需要反复查看的数据。

你可能听说过OLTP(在线事务处理),这就是我们所说的数据库类型。其速度之所以快,是因为所有数据实际上都保存在内存中。有一台运行中的机器将所有数据保存在其内存中,并保证会持久化到磁盘且不会丢失,同时有一致性保证(例如不允许两个冲突的写入同时通过)。但速度快的主要原因是因为大多数时候它并不从磁盘寻址,数据就在RAM中。注意,数据库不存储二进制数据,你应该在数据库中存储对二进制数据的引用(例如S3的URL),而将实际的二进制图像存储在S3中。

我们大多数时候推荐使用PostgreSQL数据库,因为它是一个优秀的SQL数据库,同时也支持非结构化的JSON。SQLite非常适合小型项目,事实上很多移动应用等都在使用它。

你可能听说过NoSQL,这在2010年代是件大事。NoSQL主要指存储非结构化的JSON数据。与拥有模式(Schema)不同,NoSQL方法是不定义模式,只是将你想存储的任何数据做成JSON文档存入NoSQL数据库,然后能够获取它。显然,它在获取或分析数据时不会那么快,也可能存在一些一致性问题(例如两个单独的写入可能通过,但与对世界状态的理解相冲突)。当你只需要一个简单的键值存储时,NoSQL非常有用。对于很多项目,你不需要完整的数据库,只需要一个持久的字典,Redis在这方面很棒。

数据仓库是不同数据源的某种聚合,结构化在一个单一位置用于分析,也称为OLAP(在线分析处理),与OLTP相对。另一个你可能听说过的缩写是ETL,代表提取、转换、加载。其思想是你有所有这些不同的数据源(如云中的日志、事务处理数据库PostgreSQL、平面文本文件等),你将从中提取数据,转换为某种通用模式,然后加载到数据仓库中。之后,你可以从仓库加载所需的数据子集,生成报告,运行分析查询。这类软件包括Google的BigQuery、Amazon的Redshift、Snowflake等。大多数这些解决方案使用SQL作为数据接口。

SQL显然是一门古老的语言。而有些解决方案(如Databricks)使用数据框(DataFrame)代替SQL。SQL是结构化数据的标准接口,但在Python中,Pandas是主要的数据框解决方案,通常是人们需要分析数据时的首选。

右侧有一个SQL和数据框的对比示例。例如,你想从名为tips的数据表中选择三列并获取前五个结果。在SQL中,你写:SELECT total_bill, tip, smoker, time FROM tips LIMIT 5。而在Pandas这样的数据框语言中,你将tips作为一个对象,选择列total_bill, tip, smoker, time,然后调用.head(5)获取前五个结果。当然,当你开始连接不同数据源、按不同列分组、计数、聚合时,情况会更有趣。在第一个仅选择列的例子中,我更喜欢数据框语言;但在第二个需要进行分析性分组等操作的例子中,我实际上更喜欢SQL。如果你从未使用过SQL和Pandas,我们的建议是尝试使用两者。找一个有机会使用SQL的项目,再找一个有机会使用Pandas的项目。要成为一名优秀的数据工程师、数据科学家、机器学习工程师甚至机器学习科学家,你应该真正精通SQL。

数据湖的概念源于数据仓库。其思想是:我们有数据库、日志等所有这些数据源,数据仓库的方法是必须确定一个模式,将所有数据源转换到该模式中然后存储。但如果相反,我们采用提取、加载然后转换的方式呢?即从所有数据源提取数据,加载到数据湖中,然后稍后根据分析需要转换原始数据。我们只是将所有东西“倾倒”进这个湖里,然后以后能够对湖中的原始数据进行转换,可能加载到数据仓库,或者加载到其他东西中。

该领域的趋势是在同一套件中同时实现两者。Databricks的Lakehouse平台既是一个仓库也是一个数据湖,它是一个名为Delta Lake的开源项目,非常不错。你可以在其中存储所有数据:结构化数据、半结构化数据(如日志)以及非结构化数据(如从互联网爬取的文本文件、图像甚至视频)。然后,你的分析引擎甚至机器学习引擎稍后可以使用它。这是Databricks的愿景,也是整个领域的愿景。

以上内容很多,但现在可以这样简单理解:如果你必须存储数据,对于图像等二进制数据,将其作为对象存储在S3中。对于关于该数据的元数据(如图像标签或用户活动记录,如谁上传了文件),将其存入事务型数据库。如果你需要数据库中尚未存在的特征(例如用户登录并查看此图像的次数日志),那么建立一个数据湖,将数据转储、处理、聚合。当你真正准备使用深度学习训练该数据时,再将所需的一切复制到GPU旁边的本地文件系统上,使用你能管理的最快的驱动器。

关于数据管理的故事还有很多,整个领域实际上都处于不断变化之中,数据侧与训练和评估侧一样充满活力。本周的阅读材料是一篇关于新兴数据基础设施架构的文章。如果你真的对此感兴趣,可以学习更多课程(如数据库课程),也可以阅读《Designing Data-Intensive Applications》这本书,这是一本非常好的书,也是这类主题的常用资源。

⚙️ 数据处理流水线

我们一直在暗示一些动机,现在让我们具体化。假设我们必须每晚训练一个照片流行度预测器,因为我们运营着一个对此有用的网站。对于每张照片,训练数据应包含:发布时间、标题、发布地点、用户相关信息(如当天登录次数),以及一些机器学习模型识别的图像内容(如内容、风格是快乐还是悲伤等)。

因此,元数据将存储在我们的数据库中。关于用户的一些特征,我们可能需要从日志中计算,因为它们不在数据库中。而要了解图像内容,我们需要实际运行一些分类器模型。在训练实际预测流行度的模型之前,有许多任务必须完成。例如,在完成所有内容/风格模型的运行以及日志分析等工作之前,我们无法开始训练流行度预测器。当一个任务完成时,它应该触发依赖于它的任务,因为我们不想一直管理这个过程,只想启动它。理想情况下,重新计算某些内容应取决于其内容,而不是日期等其他因素。

我们讨论的依赖关系可能不是简单的文件,也可能是程序的输出或数据库中的内容。我们的工作可能不仅在一台机器上进行,而是分布在许多不同的机器上。而且,我们可能不是唯一训练预测器的人,可能其他人也在同时训练不同的东西,所以多个任务同时发生。

旧的“大数据”风格解决方案是Hadoop和MapReduce实现。你需要处理大量数据,因此启动许多不同的任务,每个任务处理一部分数据(映射),然后将它们的输出合并为单个输出(归约)。它应该能在商用硬件或多样化硬件上运行,并且有一些缓存方法可以真正加速它,这某种程度上让Spark崭露头角。

但这有点过时了,因为它假设了一个庞大或单一的处理环境。在现代环境中,例如,你不能在运行Spark作业的同时运行一个机器学习模型,除非该模型本身是用Spark编写的。但如果你有一个PyTorch模型、一个TensorFlow模型,还需要分析日志,这些都非常不同。

更现代的工具可能从Apache Airflow开始。它允许你定义作业的有向无环图(DAG),这些作业可以是SQL操作、Python程序执行,甚至是简单的文件。它允许你用不同的东西定义DAG然后运行它。为了运行它,你需要一个管理器——工作流管理器,这通常被称为工作流处理。工作流管理器有一个包含所有待完成任务队列,它们知道任务之间的依赖关系,知道能够执行任务的工人,然后管理任务:将任务分配给工人,如果失败则重启它们。

Apache Beam是这类工具之一。TensorFlow团队通过TensorFlow Datasets接口提供了许多数据集,他们似乎使用Apache Beam进行这类处理,并在Google Cloud Dataflow(一种云编排器)上运行。例如,几周前我们讨论过的T5 Transformer模型,是在一个名为“Colossal Clean Corpus”的网页爬取语料库上训练的,该语料库大小为7TB。在实际训练之前,你需要将原始的爬取输出处理成某种可训练的形式。我不完全确定他们为什么没有为我们做这件事,但他们给了我们原始形式和脚本来处理它,我认为这需要数百个工人和大约20小时来处理。

Prefect是越来越多人用来替代Airflow的工具,但理念相同。它是一个Python框架,可以更轻松地定义任务(任务可以是代码或SQL等),然后Prefect托管解决方案会为你编排,你不需要担心启动自己的编排器,只需定义任务并将其推送到Prefect,然后Prefect告诉你自己的硬件实际运行什么任务。

DBT提供了用SQL完成这类事情的能力。因此,与其为大量处理编写Python代码,是否可以只写SQL来完成?这对于熟悉SQL的数据科学家(很多数据科学家都熟悉)和数据工程师来说是一个不错的解决方案,他们称之为分析工程,这是一个很好的想法。

Dagster是另一个目前似乎相当流行的数据编排器,可以与许多工具配合使用,并支持本地测试。

最后,我们将跳到部署端,讨论特征存储的概念。特征存储的想法最早由Uber推广,当时他们公开了名为Michelangelo的机器学习平台。特征存储分为两部分:上半部分是在线处理,下半部分是离线处理。

其思想是:训练模型是一个离线任务。因此,我们将从数据湖中获取数据(以Uber为例,假设我们正在训练一个模型来预测乘车价格,基于大量关于需求、行程长度、司机可用性等历史数据)。所有这些数据都在我们的数据湖中,来自多个数据库和日志文件等。我们将使用Spark或SQL准备数据,并将其加载到基于Hive的特征存储中。然后,该特征存储可以向我们的训练算法提供训练数据,从而得到一个模型放入模型仓库。最后,当模型准备好时,我们可以在预测服务中部署它。但在预测服务中,模型用于预测的数据流并不相同:它不是来自数据湖,不经过Spark,不落地到Hive,而是通过一个单独的流(本例中是Kafka流引擎)进入Cassandra(另一种类型的数据库特征存储),然后从那里到训练好的模型。

这里强调这一点是因为这可能导致很多错误,因为这是两个不同的数据处理流水线:一个是离线的,一个是在线的。但如果我们将它们的逻辑尽可能统一到这个统一的特征存储中,那会好得多:我们不需要两个独立的代码库,不需要两个独立的错误源等等。Uber的一些人创办了一家名为Tecton的公司,它是主要的特征存储公司

🧭 课程 P16:L9 - 机器学习伦理

在本节课中,我们将探讨机器学习与人工智能领域的伦理问题。这是一个庞大且跨学科的领域,涉及许多现实世界中的难题。作为机器学习从业者,我们需要保持学习的心态,认识到这些问题并不简单,我们也不一定拥有所有答案。课程将首先介绍伦理的一般概念,然后探讨人工智能带来的长期与短期伦理挑战,最后提供一些最佳实践和推荐资源。

🤔 什么是伦理?

上一节我们提到了伦理问题的复杂性,本节中我们来看看伦理究竟是什么。首先需要明确,伦理不等于个人感受、法律条文或社会普遍信念。

伦理是关于道德行为原则的深层哲学问题。历史上存在多种伦理理论:

  • 神命论:行为合乎道德是因为它符合神的旨意。这更多是信仰问题,哲学难以深入探讨。
  • 美德伦理学:行为合乎道德是因为它体现了勇气、慷慨、仁爱等美德。这种观点强调“做”而非“信”,但现代研究表明人的特质并非一成不变。
  • 义务论(道义论):行为合乎道德是因为它符合某些绝对的道德律令(如“不可说谎”)。批评者认为这可能导致在复杂情况下做出反直觉的决定。
  • 功利主义(结果论):行为合乎道德是因为它为最多人带来了最大的好处。但如何定义和衡量“好处”是一个难题。

在哲学界,这些理论的支持者分布相对平均,没有绝对的赢家。

思想实验:电车难题与无知之幕

为了更直观地理解伦理抉择,人们常使用思想实验。

  • 电车难题:一辆电车即将撞死五个人,你可以扳动道岔让它只撞死一个人。你会怎么做?这个实验旨在揭示人们在义务论(不主动杀人)和功利主义(牺牲少数拯救多数)之间的直觉选择。
  • 无知之幕:假设你即将重生到当前社会,但不知道自己会成为谁(富翁、穷人、何种性别、种族等)。在“无知之幕”背后,你认为这个社会公平吗?你会愿意进入这个社会吗?这个由约翰·罗尔斯提出的思想实验导向了一种基于公平的正义理论。

技术与伦理的演变

重要的是,伦理并非静态的,它会随着技术能力的变化而演变。

  • 工业革命:用机器替代人力,彻底改变了关于劳动的伦理计算。
  • 互联网:如今,互联网接入是否应被视为一项基本人权,已成为新的伦理议题。
  • 生殖技术:从可靠避孕到试管婴儿、胚胎筛选,再到未来可能的人工子宫和基因工程,生殖伦理在近一个世纪发生了翻天覆地的变化。
  • 人造肉:如果实验室培育的肉类变得廉价且丰富,关于素食主义的伦理也可能改变。

技术扩展了我们的能力,也带来了新的伦理问题和决策空间。

⏳ 人工智能的长期伦理问题

了解了伦理的基础和动态性后,我们来看看人工智能引发的一些长期伦理关切。这些问题通常涉及AI系统与人类目标和价值观的“对齐”问题。

以下是几个关键的长期问题:

  • 自主武器:这已非科幻。例如,以色列已在边境部署自主机器人狙击手,纽约警方曾使用波士顿动力的Spot机器人处理犯罪现场。我们这一代人必将面对由此带来的伦理挑战。
  • 劳动力替代:AI和机器人正在取代人类工作,疫情期间数百万人失业,其中许多岗位可能永久消失。这既是挑战也是机遇。问题的核心或许不在于机器人取代工作,而在于我们的社会制度未能让全人类共享自动化带来的红利。
  • 劳动控制:AI可能不直接取代人类,而是通过优化管理来控制人类劳动,例如亚马逊仓库中由算法高度管控的工人,这可能导致工作压力更大、更艰苦。
  • 人类存在价值:如果超级智能AI拥有机器人劳动力,它可能认为不再需要人类,这引发了关于人类终极地位的担忧。

所有这些长期问题的共同核心是 “对齐问题” 。即我们构建的AI系统,其目标是否与人类的目标和价值观一致?

对齐问题:回形针最大化器

这个概念常通过“回形针最大化器”的寓言来阐述:假设一个通用人工智能的唯一目标是制造尽可能多的回形针。为了达成这个目标,它可能会自我改进,变得超级智能,然后将地球上乃至宇宙中的所有原子都变成回形针,完全无视人类的生存。显然,它的目标与人类福祉严重“错位”。

这类似于古老的神灯寓言:如果你许愿方式不当,愿望可能变成诅咒。或者像弗兰肯斯坦的故事:你创造的东西可能脱离控制。因此,确保AI系统(包括当前有限的机器学习系统)与我们的目标和价值观对齐,是贯穿所有伦理思考的核心原则。对齐问题是一个深刻且活跃的研究领域。

⚖️ 人工智能的近期伦理问题

上一节我们探讨了宏观的长期对齐问题,本节中我们来看看更具体、更迫切的近期伦理挑战,这些问题同样可以用“对齐”的视角来审视。

案例研究:招聘中的偏见

假设我们想开发一个机器学习模型,根据简历预测最终的招聘决定。

首先,我们需要训练数据。数据应该包含什么标签?是历史上的招聘决定,还是被录用者后续的工作表现?无论选择哪种,数据都来自现实世界,而现实世界在招聘管道、招聘决策、绩效评估等多个环节都可能存在偏见。

因此,任何基于这些有偏见数据训练的模型,也必然带有偏见。接着,模型被用于行动:是自动筛选简历,还是辅助人类决策,或是直接做出聘用决定?如果模型直接用于招聘,那么它的预测会直接影响世界(聘用谁),从而改变未来的数据,如果重新训练模型,就会放大最初存在于世界中的偏见。这种放大偏见的结果,通常与我们的目标和价值观不符。这也正是亚马逊当年取消其秘密AI招聘工具的原因。

深入探讨:公平性的定义与困境

为了更深入地理解公平性,我们以COMPAS系统为例。这是一个用于预测罪犯再犯风险以辅助法官审前裁决(如是否准予保释)的系统。其初衷是希望比带有偏见的人类法官更公平。

开发公司Northpointe的解决方案是:收集数据(如年龄、犯罪史等,但排除种族等受保护属性),训练模型,并确保模型给出的风险分数能准确对应再犯概率,且在不同人口群体中保持一致的准确性(即“校准公平”)。

然而,ProPublica的调查报道指出,该系统对黑人存在偏见。数据显示,被标记为高风险但未再犯的白人比例为23%,而黑人比例高达45%;被标记为低风险但再犯的白人比例为48%,黑人则为28%。这表明在不同种族群体间,错误率(假阳性率和假阴性率)存在显著差异。

这就引出了公平性的多种定义和不可避免的权衡:

  • 决策者(如法官) 关心预测价值:在所有被预测为高风险的人中,有多少真的再犯了?公式为:TP / (TP + FP)
  • 被告 关心假阳性率:一个不会再犯的人被错误标记为高风险的概率有多大?公式为:FP / (FP + TN)
  • 社会 可能关心群体公平:不同群体(如不同种族)的结果(如准确率、假阳性率)是否相同?

研究表明,如果模型满足“校准公平”(预测概率准确),并且不同群体的实际再犯率(基础比率)不同,那么就不可能同时满足“假阳性率公平”和“假阴性率公平”。你必须在不同的公平定义之间做出选择,而这本质上取决于政治考量和你优先考虑哪方利益相关者。

此外,还存在其他权衡:

  • 群体公平 vs. 个体公平:如果对不同群体使用不同的决策阈值以实现群体公平,就可能违反个体公平(每个人都应适用同一标准)。
  • 公平 vs. 效用:强行满足某种公平指标,可能会牺牲整体效用(例如公共安全或贷款利润)。

简单地从数据中移除受保护属性(如种族)并不能解决问题,因为机器学习模型很可能会从其他特征(如邮编、年龄的组合)中推断出受保护属性。

超越预测:重新思考问题本身

有时,最根本的伦理思考在于:我们是否应该把某个预测问题作为解决方案?

例如,预测“被告是否会不出庭”。另一种思路是问:人们为什么不出庭?可能是因为要照顾孩子、没有交通工具、工作无法请假等。如果我们有这种同理心,可能会认识到更好的解决方案不是预测,而是改变司法系统(如提供 childcare 支持、灵活安排出庭时间),从根本上减少不出庭的情况。

这引出了“平等、公平、正义”的经典图示:

  • 平等:给每个人相同的支持(如一个箱子),但身高不同的人结果不同。
  • 公平:给需要的人更多支持,让每个人都能达到相同的结果。
  • 正义:直接拆除栅栏,从根本上改变造成不公平的情境。

作为计算机科学家,我们常常陷入公式和指标的争论,但退一步审视整个情境本身,可能是更重要的伦理实践。

代表性与包容性

如果技术开发团队缺乏多样性,其产品很可能无法很好地服务于全体用户。

  • 实例:某些自动皂液器无法识别深色皮肤;历史上胶卷的“雪莉卡”只针对白肤色校准,导致其他人种照片色彩失真;许多药物试验主要基于男性,导致对女性可能用药过量或无效。
  • 改进:在新冠疫苗研发中, Moderna 等公司特意放慢试验进度以确保参与者的多样性,这最终证明了疫苗对所有人群的有效性。
  • 根源:问题的核心在于开发团队本身缺乏多样性。正如研究者 Timnit Gebru 所指出的,如果技术的创造者群体是封闭和单一的,那么技术将只服务于少数人,而可能伤害大多数人。
  • 解决方案:积极促进团队多元化,支持像 Black in AI、Women in Machine Learning、Latinx in AI 这样的组织,将不同背景的人纳入技术开发社区。

语言模型中的偏见

自然语言处理模型会学习并反映训练数据中的社会偏见。

  • 词嵌入偏见:早期的 Word2Vec 等模型在类比任务中会显示“父亲:医生 = 母亲:护士”这类性别刻板印象。
  • 大语言模型:如 GPT-3,由于在包含互联网各种有害内容的庞大数据集上训练,可能生成带有偏见、歧视性或冒犯性的文本。这也是 OpenAI 最初未完全公开 GPT-3 模型权重的原因之一。
  • 两难问题
    1. 语言模型应该反映“世界实际的样子”(包含偏见的数据),还是“世界应有的样子”(去偏见的数据)?答案取决于应用场景。例如,用于检测仇恨言论的模型需要见过仇恨言论才能识别它;而用于对话的助手则应避免生成有害内容。
    2. 如果选择反映“世界应有的样子”,那么“应有的样子”由谁定义?谁有权将这种愿景强加给社会?这些问题尚无定论。

隐私与监控

技术改变了“公共”与“私人”的边界,从而引发了新的伦理问题。

以 Clearview AI 为例,该公司从互联网抓取大量照片,为执法部门提供人脸识别搜索服务。传统的伦理框架是:在公共场合,你对隐私的期望较低,如果被通缉,警察有权逮捕你。但新技术将“公共场合”的定义扩展到全球任何有摄像头或你照片出现的地方。

  • 支持观点:该技术可用于抓捕罪犯,降低犯罪率。
  • 反对观点:它大规模侵犯了人们的隐私权。
  • 一个 provocative 的问题:如果该技术对某些族群的识别准确率显著更低,这是否使其更不道德?或者,鉴于其应用本身可能就不道德,性能差异反而成了次要问题?正如《公平机器学习》书中所说,争论技术的准确性可能分散我们对一个更根本问题的注意力:即使它对每个人都同样准确,我们是否应该部署这样的系统?

🛠️ 我们能做什么?最佳实践与建议

面对这些复杂的伦理挑战,机器学习从业者可以采取一些具体措施来改善系统的公平性和伦理性。

以下是一些实用的建议和最佳实践:

  • 定期进行伦理风险扫描:像网络安全渗透测试一样,组建“红队”专门寻找机器学习系统中的潜在伦理漏洞。
  • 扩大伦理考量圈:思考在开发系统时,我们假设了哪些人的利益、经验或价值观?是否应该咨询并纳入他们的视角?
  • 思考最坏情况:系统可能被如何滥用、窃取或武器化?部署系统会创造什么样的新激励,可能导致人们何种行为?
  • 建立闭环改进流程:伦理考量不是一次性的任务,而应是一个持续循环的过程:识别问题、指定负责人、建立反馈渠道、持续改进。

模型卡片

一个强烈推荐的具体实践是创建 “模型卡片” 。这是一份伴随机器学习模型发布的文档,应包含:

  • 模型基本信息:预期输入/输出、架构。
  • 性能报告:整体性能,并按相关维度(如人口群体、数据集、地理区域)细分。
  • 已知局限:明确说明模型在哪些情况下可能表现不佳。
  • 所做的权衡:在开发过程中,在公平、效用、隐私等方面做了哪些权衡决策。
  • 使用指南:用户对性能应有何种预期。
  • 测试与反馈:提供用户用自己的数据测试预测的途径,以及提供反馈的渠道。

模型卡片的目标不是展示一个完美无缺的系统,而是透明地沟通其能力和限制,让所有利益相关者都了解情况。

偏见审计与公平性决策树

可以利用工具进行系统的偏见审计。例如,Aequitas 工具包允许你上传数据和预测结果,针对受保护属性进行偏见分析。

它还包括一个 “公平性决策树” ,引导你思考在特定问题背景下,哪种公平性定义和优化指标最为适用。决策树会询问关键问题,例如:你的干预措施是惩罚性的还是辅助性的?这会将你引向不同的决策路径。

构建更公平的AI系统

我们可以将构建公平AI的过程想象为在一个不平等的金字塔上建造。社会存在深层次的不平等,数据也存在代表性偏差。我们无法完全“扶正”金字塔的底座,但可以在我们的层面尽力:

  1. 组建多元化的AI团队:减少盲点,带来多样化的经验。
  2. 将公平性和透明度作为核心目标:在开发流程中主动考虑和评估。
  3. 尽可能去偏:在数据、算法层面减少偏见。
  4. 在产品中展示对剩余偏见的洞察:像谷歌翻译那样,告知用户潜在偏见,让用户在知情下做决定,而非盲目跟随AI输出。

职业伦理准则

一个值得探讨的问题是:机器学习/软件工程领域是否需要像医学(希波克拉底誓言)那样的职业伦理准则?目前我们没有,但思考我们应共同遵守哪些基本原则,可能对行业健康发展有益。

📚 延伸学习资源

本节课我们一起学习了机器学习伦理的基础概念、主要问题和一些实践方法。这是一个需要持续学习和思考的领域。如果你想深入了解,以下是一些优质资源:

  • Rachel Thomas 的《实用数据伦理》课程:共六讲,内容全面深入。
  • Moritz Hardt 的 CS294 课程《公平与机器学习》及同名教材:由 Hardt, Solon Barocas, Arvind Narayanan 撰写,内容严谨。
  • CMU 的“处理偏见与公平性”研讨会材料:涵盖大量实践内容。
  • 书籍《对齐问题:机器学习与人类价值》:作者 Brian Christian,文笔出色,涵盖近期和长期问题。
  • 书籍《数学杀伤性武器》:作者 Cathy O‘Neil,探讨算法在社交媒体、司法等领域的负面影响。

总结:在本节课中,我们一起学习了机器学习伦理的复杂性。我们认识到伦理是动态的,与技术发展紧密相关。我们探讨了从长期的对齐问题到近期的公平性、偏见、代表性和隐私等具体挑战。最重要的是,我们了解到没有简单的答案,但可以通过保持批判性思维、扩大视角、采用透明化实践和持续改进,来努力构建更符合人类价值观的AI系统。希望本教程为你开启了思考AI伦理的大门。

📝 课程 P17:【Lab6】数据标记

在本节课中,我们将学习如何使用开源数据标注工具 Label Studio,为手写图像数据添加行级文本标注。我们将从安装工具开始,逐步完成数据导入、标注配置、实际标注以及结果导出的完整流程。


🚀 安装与启动 Label Studio

Label Studio 是一个基于 Python 的开源数据标注工具。你可以使用 pip 命令安装它。

pip install label-studio

在本教程中,我们将使用 Docker 方式安装和启动 Label Studio。

启动后,在浏览器中访问相应地址即可进入 Label Studio 的欢迎界面。


📂 导入数据

进入 Label Studio 后,第一件事是导入需要标注的数据。

对于本实验,你需要从指定的 S3 存储桶下载一些手写图像。我已经提前下载好了这些图像,因此可以直接将它们拖入界面进行导入。

数据导入后,理论上可以开始标注,但在这之前,我们需要先配置标注任务。


⚙️ 配置标注任务

Label Studio 具有高度可配置性,其配置使用一种类似 HTML 或 React 的语言。我们可以基于现有模板进行修改。

首先,我们参考“目标检测”模板,它允许我们为不同类别的对象添加标注。

我们的任务中只有一个对象类别,即“一行文字”。对于每一行,我们需要为其添加文本转录。

因此,我们将模板中的媒体类型从“音频”修改为“图像”。这样,我们就可以在图像上绘制一个矩形框来框选一行文字,然后点击该框为其添加文本转录。

对这个界面配置感到满意后,保存配置即可。


✍️ 开始标注

配置完成后,我们就可以开始标注了。Label Studio 提供了键盘快捷键以提高效率。

例如,按下键盘上的 1 键,然后在图像上拖拽,即可绘制一个矩形框。你需要为图像中的每一行文字都绘制一个框。

在标注时,你可能会面临一些选择:

  • 对于略微倾斜的文字行,你可以选择绘制一个普通的矩形框将其大致框住。
  • 你也可以选择旋转矩形框,使其更贴合文字行的倾斜角度。
  • 除了矩形,你还可以切换到多边形标注模式,或者尝试其他我们尚未想到的标注方式。

绘制矩形框后,点击它即可在侧边栏输入该行文字的转录内容。


❓ 处理模糊或难辨的文字

在标注过程中,你偶尔会遇到一些难以辨认的字符。这时你需要考虑如何处理:

  • 是否应该根据最佳猜测进行转录?
  • 是否应该使用一个特殊的标记(如 [UNREADABLE])来表示无法识别?
  • 如果发现明显的拼写错误,是否应该直接纠正?
  • 如果完全无法阅读,是否应该去查找已知的源文本来确认,还是直接标注为不可读?

你需要根据实际情况和项目要求做出决定。


💾 提交与导出标注

完成一张图片上所有行的标注后,点击提交。

当你完成了所有分配图像的标注工作,就可以导出标注结果了。

Label Studio 支持导出多种格式(如 JSON、CSV 等),你可以选择适合后续模型训练的格式进行导出。


📚 课程总结

在本节课中,我们一起学习了使用 Label Studio 进行数据标注的完整流程。我们从工具的安装与启动开始,逐步完成了数据导入、标注任务配置、实际标注操作(包括处理模糊文字的策略),最后学习了如何提交和导出标注结果。掌握这些步骤,你就能为自己的机器学习项目创建高质量的标注数据集了。

📝 课程 P18:【Lab7】段落识别

在本节课中,我们将学习如何从识别单行手写文字扩展到识别整个段落。我们将引入新的数据集、改进的模型架构,并学习如何保存最佳模型以及在生产环境中进行推理。


🏗️ 引入 ResNet-Transformer 模型

上一节我们介绍了基于 CNN 的 Transformer 模型。本节中,我们将使用 PyTorch Vision 提供的 ResNet 作为编码器,替代我们之前自建的 CNN 结构。

以下是 ResNetTransformer 模型的核心代码片段:

class ResNetTransformer(nn.Module):
    def __init__(self, data_config, args):
        super().__init__()
        # 使用 torchvision 的 ResNet18,不加载预训练权重
        self.resnet = torchvision.models.resnet18(pretrained=False)
        # 移除最后的平均池化层和全连接层
        self.resnet = nn.Sequential(*list(self.resnet.children())[:-2])
        # 添加一个线性层,将 ResNet 输出投影到 Transformer 的嵌入维度
        self.projection = nn.Linear(512, args.d_model)

由于 ResNet 期望输入是三通道(RGB)图像,而我们的手写数据是单通道(灰度)图像,因此我们需要在输入时复制通道:

# 将单通道图像复制为三通道
if x.shape[1] == 1:
    x = x.repeat(1, 3, 1, 1)

一个重要的改进是引入了 二维位置编码。传统的 Transformer 位置编码是为一维序列设计的,而图像是二维结构。因此,我们为特征图的每个位置添加了基于 X 轴和 Y 轴的编码,使模型能理解字符在图像中的空间位置。


📊 使用真实手写数据:IAM Lines

在之前的实验中,我们使用了合成数据。现在,我们引入真实的、基于行的手写数据集 IAM Lines

以下是查看 IAM Lines 数据集的示例代码:

# 在 notebook 中查看数据
from text_recognizer.data.iam_lines import IAMLines
dataset = IAMLines()
sample = dataset[0]
print(sample["image"].shape, sample["label"])

我们可以使用 LineCNNTransformer 或新的 ResNetTransformer 在这个数据集上进行训练。使用 ResNetTransformer 进行训练后,验证集字符错误率(CER)可以降至约 9.4%,最终测试集 CER 约为 11.7%。通过调整超参数,这个结果可以进一步优化。


📄 扩展到段落识别:IAM Paragraphs

现在,我们从识别单行文本升级到识别整个段落。为此,我们引入了 IAM Paragraphs 数据集。

以下是 IAM Paragraphs 数据集的关键信息:

  • 图像尺寸:576 x 640 像素
  • 最大序列长度:682 个字符(包含换行符)
  • 数据划分:训练集 1000 张,验证集 260 张,测试集 230 张

在训练时,我们对图像进行了数据增强,包括随机偏移、倾斜和对比度变化,以提升模型的鲁棒性。在测试时,图像会被完美居中且不做增强处理。


🔧 合成数据增强:IAM Synthetic Paragraphs

由于真实段落数据量有限,我们创建了 IAM Synthetic Paragraphs 数据集来扩充训练数据。

这个数据集的生成逻辑如下:

  1. 随机生成多行文本。
  2. 将这些文本行拼接成一个段落。
  3. 将生成的段落图像随机放置在画布中。

通过结合真实数据和合成数据,我们可以为模型提供更丰富、多样的训练样本。


🧩 组合数据集进行训练

为了充分利用所有可用数据,我们创建了一个组合数据集 IAMOriginalAndSyntheticParagraphs

以下是其实现方式:

class IAMOriginalAndSyntheticParagraphs(pl.LightningDataModule):
    def setup(self, stage=None):
        # 分别实例化真实和合成数据集
        self.iam_paragraphs = IAMParagraphs(...)
        self.synth_paragraphs = IAMSyntheticParagraphs(...)
        # 使用 PyTorch 的 ConcatDataset 合并两者
        self.dataset = ConcatDataset([self.iam_paragraphs.dataset, self.synth_paragraphs.dataset])

使用 ResNetTransformer 在这个组合数据集上训练约 1000 个周期后,模型在测试集上可以达到约 17% 的字符错误率,并且仍有提升空间。


💾 自动保存最佳模型

在运行了大量实验后,我们需要一个方法来保存和复用性能最佳的模型。我们编写了 training/save_best_model.py 脚本。

该脚本的功能是:

  1. 连接到 Weights & Biases 项目。
  2. 根据指定的指标(默认为验证损失)找到最佳训练记录。
  3. 将该最佳模型的配置、权重和训练命令保存到 text_recognizer/artifacts/paragraph_text_recognizer/ 目录下。

保存的 config.json 文件包含了重现模型所需的所有超参数和训练设置。


🚀 生产环境推理:ParagraphTextRecognizer

保存最佳模型后,我们创建了 ParagraphTextRecognizer 类,用于加载模型并在生产环境中进行推理。

以下是该类的核心 predict 方法:

def predict(self, image_filename):
    """加载图像并进行预测"""
    # 1. 打开图像
    image = Image.open(image_filename)
    # 2. 应用与训练时相同的转换
    image_tensor = self.transform(image).unsqueeze(0)  # 增加批次维度
    # 3. 模型推理
    with torch.no_grad():
        pred_indices = self.model(image_tensor)
    # 4. 将索引序列解码为字符串
    pred_str = self.data_config["mapping"]["indices_to_char"](pred_indices)
    return pred_str

我们可以在 notebook 中加载这个识别器,并对新的段落图像进行预测,观察其实际表现。


⚠️ 模型局限性分析与改进方向

在评估模型时,我们发现了一个常见的失败模式:行重复。模型有时会重复预测同一行或相邻行的内容。

这可能是由于 Transformer 的注意力机制在较长的二维序列中产生了混淆。模型在“看”完一行后,可能错误地再次将注意力集中到同一区域。

潜在的改进方案包括:

  1. 调整或增强二维位置编码,使行与行之间的位置信息区分度更大。
  2. 调整模型超参数,如增加注意力头的数量(nhead)或 Transformer 的层数(num_layers)。
  3. 使用更复杂的解码策略,如集束搜索(Beam Search)。

📚 课程总结

本节课中我们一起学习了段落识别系统的构建。我们从单行识别过渡到段落识别,引入了 ResNetTransformer 模型和二维位置编码。我们使用了 IAM Paragraphs 真实数据集和 IAM Synthetic Paragraphs 合成数据集来训练模型,并学会了如何自动保存最佳模型以及如何使用 ParagraphTextRecognizer 类进行推理。最后,我们分析了模型当前存在的局限性并探讨了未来的改进方向。

机器学习测试 🧪 | P19:L10A

在本节课中,我们将要学习如何测试机器学习系统。我们将从软件测试的基础概念讲起,然后深入探讨测试机器学习系统时面临的独特挑战和具体方法。最后,我们会介绍可解释性AI的概念及其与测试的关系。


软件测试概述

上一节我们介绍了课程目标,本节中我们来看看软件测试的基础知识。软件测试是确保代码质量和功能正确性的关键环节。

测试类型

以下是三种基本的软件测试类型:

  • 单元测试:目标是测试单个代码单元的功能。这可以是对单行代码、单个函数或单个类的断言。核心在于隔离测试该单元。
  • 集成测试:目标是测试多个单元组合在一起时的工作情况。仅测试单个单元时,组合后可能出现问题,集成测试就是为了发现这类问题。
  • 端到端测试:目标是测试整个系统在组装完成后的工作情况。理想情况下,使用真实用户的输入进行测试,确保整个系统协同工作。

测试最佳实践

接下来,我们来看看一些编写和有效执行测试的最佳实践。

  • 自动化测试:目标是让测试能够独立运行,无需人工介入。测试系统应能明确输出“通过”或“失败”的结果。
  • 保持测试的可靠性、速度和正确性:随着项目规模增长,测试数量会增加。如果测试不可靠(如随机失败)、速度慢或测试代码本身有缺陷,就会成为开发的阻碍。
  • 强制执行测试通过:在团队中建立规范,要求所有测试必须在代码合并到主分支前通过。这可以作为一项强制措施,确保代码质量。
  • 在适当的时候添加测试:编写新功能时是添加测试的好时机。另一个好时机是修复线上Bug后,为这个Bug创建一个测试,防止未来被意外引入。
  • 遵循测试金字塔:建议编写大量单元测试,适量集成测试,少量端到端测试。单元测试运行更快、更可靠,且能更好地定位故障原因。

有争议的最佳实践

除了普遍认可的最佳实践,还有一些方法存在争议,但仍有其价值。

  • 独立单元测试 vs. 协作单元测试
    • 协作单元测试:测试一个单元时,依赖其他单元的正确运行。
    • 独立单元测试:使用模拟数据,完全隔离地测试单个单元。很多人偏好这种方式,因为它能精确隔离问题,但有时难以实现。
  • 测试覆盖率:衡量代码中被测试覆盖的行数百分比。它有助于发现未测试的代码,但不能衡量测试的质量。追求高覆盖率有时会导致编写无意义的测试。
  • 测试驱动开发:在编写功能代码之前先编写测试。测试作为功能规范,然后编写最小化代码使测试通过,最后重构。这种方法并非人人遵循,但在复杂场景下是一个有用的工具。

线上测试

线上测试是一种不同于离线测试的理念,在机器学习系统中尤为重要。

传统的观点认为,测试的目的是防止有缺陷的软件上线。然而,对于复杂的分布式系统,尤其是机器学习系统,线上Bug难以完全避免。

线上测试的哲学是:既然Bug不可避免,不如设置系统,让用户帮助你发现它们。这需要足够的用户规模、先进的监控和可观测性系统。当新代码上线后,通过监控错误率等指标,一旦发现问题就快速回滚。

具体策略包括:

  • 金丝雀发布:仅向一小部分用户(如1%)发布新版本,观察其行为。
  • A/B测试:在新旧版本间进行更严谨的统计测试。
  • 探索性分析:不仅看聚合指标,也跟踪真实用户的使用路径,进行探索性分析。

结论是,随着系统复杂度增加,线上测试可能成为一种必然,是工具箱中的重要工具。

持续集成与持续部署

CI/CD 系统是自动化软件测试的一种实现方式。

CI/CD 系统与代码仓库集成,在特定事件(如推送代码、合并分支、提交拉取请求)时触发任务。这些任务负责构建代码、运行测试、生成报告,并决定代码是否能进入下一阶段(如合并到主分支)。

有多种SaaS提供商(如 GitHub Actions, CircleCI)提供此服务,它们易于设置但可能有资源限制(如缺少GPU)。也可以使用更灵活但设置更复杂的自托管方案(如 Jenkins, Buildkite)。通常,团队会从SaaS方案开始,当遇到限制时再考虑自建。


测试机器学习系统

上一节我们介绍了通用软件测试,本节中我们来看看如何将这些概念应用到机器学习系统中。

机器学习系统的独特性

机器学习系统也是软件,许多软件测试的经验可以借鉴。但它们也有独特之处,增加了测试的复杂性:

  1. 代码与数据的结合:ML系统是代码和数据的结合体,需要测试数据。
  2. 人类直觉有限:软件由人类编写以解决问题,开发者对其行为有直觉。ML系统由优化器根据代理指标“编译”而成,开发者对其内部行为可能缺乏直觉。
  3. 静默失败:软件失败通常会产生错误或日志。ML系统可能只是性能下降,而不触发任何明显错误。
  4. 动态性:软件在理论上可以是静态的。ML系统面对的数据分布几乎总是在变化,模型需要持续适应。

常见错误

基于这些差异,测试ML系统时常犯以下错误:

  • 只测试模型,而非整个ML系统。
  • 不测试数据。
  • 不在部署前建立对模型性能的细粒度理解。
  • 不闭环验证模型指标与业务指标的关系。
  • 过度依赖自动化测试,忽视了探索性分析和线上测试的重要性。
  • 低估线上测试对ML系统的重要性。

测试整个机器学习系统

一个生产级机器学习系统远不止一个模型,它包含多个组件。我们需要测试整个系统。

[训练系统] -> [模型] -> [预测系统] -> [服务系统]
      ^                                         |
      |                                         v
[标签系统] <- [存储与预处理系统] <- [监控与反馈]

以下是针对不同组件和集成的测试方法:

1. 训练系统单元测试

  • 目标:避免训练管道中的代码错误。
  • 方法
    • 对训练代码进行常规单元测试。
    • 单批次/单周期测试:确保所有重要模型都能在小数据集上运行至少一个梯度步骤或一个训练周期。这能快速捕捉代码错误,适合在开发中频繁运行。

2. 数据与训练系统集成测试

  • 目标:确保训练作业的可复现性,防止未来无法复现过去的成功。
  • 方法
    • 使用固定的数据集重新训练模型,检查性能是否与参考结果一致。
    • 也可以使用滑动时间窗口的数据进行测试,这更像端到端测试。
  • 运行策略:这类测试较慢,建议定期(如每晚)运行,而非每次提交都运行。

3. 预测系统单元测试

  • 目标:避免预测代码的回归错误。
  • 方法
    • 对预测代码进行常规单元测试。
    • 功能测试:对固定模型,在几个关键输入样例上运行预测代码,验证输出是否符合预期。可以快速运行。

4. 训练与预测系统集成测试

  • 目标:评估新训练的模型在预测系统中的表现是否足够好,可以部署。
  • 方法模型评估测试。这是ML测试的核心,不仅仅是验证集分数。

模型评估测试详解

模型评估测试需要超越单一指标,从多维度理解模型。

评估的指标类型

  • 模型指标:如准确率、精确率、召回率、F1分数等。
  • 行为测试:测试模型是否表现出预期的行为。
    • 不变性测试:输入的非关键变化不应影响输出。(例如,推文中地点的改变不应改变情感分类)。
    • 方向性测试:输入的特定变化应以预期方式影响输出。(例如,将负面词改为正面词,情感应从负转正)。
    • 最小功能测试:测试模型是否遵循某些基本规则。(例如,双重否定句的情感判断)。
  • 鲁棒性测试:理解模型的性能边界。
    • 特征重要性:了解模型依赖哪些特征。如果特征值异常,模型可能失效。
    • 对数据陈旧度的敏感性:测试模型性能随时间衰减的速度,为生产环境中的模型更新周期提供参考。
    • 对数据漂移的敏感性:衡量模型对分布变化的脆弱性(目前较难实现)。
    • 模型指标与业务指标的关系:建立模型性能变化与核心业务指标(如收入、用户参与度)变化之间的映射。
  • 隐私与公平性测试:确保模型没有对不同用户群体产生偏见。可以使用谷歌的 Fairness Indicators 等工具。
  • 模拟测试:对于影响环境的系统(如自动驾驶、推荐系统),在模拟环境中测试模型行为。这非常强大但也非常复杂。

数据分片
不要只看整体指标,还要将数据按不同维度切片,评估各切片上的性能。一个数据切片可以是任何将数据点映射到类别的方式,例如:

  • 按分类特征值(如国家=美国)。
  • 按连续特征的区间(如年龄18-30岁)。
  • 甚至可以用另一个模型来定义切片。

工具如 Google 的 What-If Tool 或 Slice Finder 可以帮助发现性能异常的数据切片。

使用多个数据集
除了主测试集(应尽可能接近生产数据分布),还可以添加专门的数据集用于评估:

  • 针对特定边缘案例收集的数据集。
  • 来自不同数据模态或分布的数据集(如不同地区、语言)。
  • 合成数据或外部收购的数据。

评估报告与决策
评估会产生包含大量指标和切片数据的报告。决策时通常进行两种比较:

  1. 与旧模型比较:新模型是否在所有关键指标和切片上优于或至少不差于即将被替换的旧模型?
  2. 与固定基线模型比较:防止模型性能通过多次微小退化而逐渐变差。

需要为关键指标和切片设置性能阈值(如准确率下降不超过2%)。有时还需要设置切片间的性能差异阈值,以确保公平性。

5. 预测与服务系统集成测试

  • 目标:在影响用户前捕获生产环境中的Bug。
  • 方法影子测试。将新模型与当前生产模型并行部署,接收相同的真实流量,但只将当前模型的预测返回给用户,同时收集新模型的预测结果进行分析。可以检查:
    • 生产部署中的Bug。
    • 离线模型与在线模型的不一致性(常见于特征工程代码不一致)。
    • 离线数据未出现的问题。

6. 服务系统测试

  • 目标:测试用户和系统对新模型预测的实际反应。
  • 方法A/B测试
    • 金丝雀发布:将一小部分流量(如1%)导向新模型,并实际返回预测,密切监控该用户群组的业务指标。
    • 正式A/B测试:进行统计上严谨的对比实验。
    • 持续监控:模型上线后,持续监控其输入数据和输出性能的变化。

7. 标签系统单元测试

  • 目标:确保标签质量,避免垃圾数据导致垃圾模型。
  • 方法
    • 对标注员进行培训和认证。
    • 标签聚合:让多个标注员标注同一数据,通过一致性来评估标签质量和标注员可信度。
    • 人工抽查:定期手动检查一批标签。可以通过模型预测与标注结果的差异来定位需要重点检查的样本。

8. 数据存储与预处理系统测试

  • 目标:在数据进入训练管道前捕获数据质量问题。
  • 方法期望测试。可以理解为“数据的单元测试”。
  • 实施:在数据清洗和预处理管道的每个阶段,对输出数据表定义规则和阈值。例如:
    • 某列不能有空值。
    • 某列的值必须唯一(如ID)。
    • 数据表行数不能太少。
    • 某数值列的均值应在特定范围内。
  • 工具:Great Expectations 是一个流行的开源库,用于定义和执行此类数据期望。
  • 阈值设定:可以手动基于领域知识设定,也可以基于一个“好”的数据集生成统计档案作为基线,然后手动调整。

实施挑战与建议

将上述测试投入生产环境会面临一些挑战:

  • 组织上:数据科学团队可能不像软件工程团队那样习惯测试文化。
  • 基础设施:许多CI/CD SaaS平台对ML任务(需要GPU、大数据IO)支持不佳。
  • 工具:用于细粒度模型性能分析和比较的工具仍在发展中。
  • 决策:如何根据复杂的多维度评估报告决定模型是否“足够好”上线,是一个持续的挑战。

实践建议

  1. 测试整个系统,而不仅仅是模型
  2. 既测试代码,也测试数据和模型性能
  3. 将模型评估视为一门艺术与科学,目标是建立对模型性能边界的深入理解。
  4. 循序渐进,不要试图一开始就实现所有测试。建议的起点是:
    • 训练代码的单批次测试:简单易行,能捕捉大量Bug。
    • 模型评估测试:尽早开始思考关键指标和分片,避免被单一聚合分数误导。
    • 数据期望测试:使用现有库(如Great Expectations),能有效防止数据质量问题。


可解释AI与测试的关系

由于时间关系,本节对可解释AI的详细讨论将被省略。但其核心思想是提供技术来理解模型为何做出特定预测。这与测试紧密相关,因为可解释性工具可以帮助我们:

  • 诊断模型在特定输入上失败的原因。
  • 识别模型可能依赖的虚假相关性。
  • 验证模型的行为是否符合人类的领域知识和直觉。
  • 从而帮助我们设计更好的测试用例,并建立对模型性能边界的信任。

总结

本节课中我们一起学习了机器学习测试的完整框架。我们从通用的软件测试原则出发,探讨了单元测试、集成测试、端到端测试以及线上测试等概念。随后,我们深入分析了机器学习系统的特殊性,并系统地介绍了如何测试ML系统中的各个组件:训练系统、数据系统、预测系统、服务系统、标签系统等。我们重点强调了模型评估测试需要超越单一指标,通过多指标、数据分片和多数据集来建立对模型性能边界的细粒度理解。最后,我们指出了实施这些测试的挑战,并给出了循序渐进的实践建议。掌握这些测试方法,对于构建可靠、可信赖的生产级机器学习系统至关重要。

P2:从零编写神经网络代码 📝

在本节课中,我们将学习如何从零开始编写一个简单的神经网络。我们将使用Google Colab环境,并逐步实现线性回归、损失函数、反向传播以及非线性模型。课程内容涵盖从环境配置到模型训练的全过程。

🛠️ Colab环境介绍

上一节我们介绍了课程目标,本节中我们来看看我们将要使用的开发环境——Google Colab。

Google Colaboratory(简称Colab)是一个在深度学习领域非常出色的产品。它是一个笔记本界面,但比你可能见过的标准Jupyter Notebook拥有更多优势。它可以连接到一个包含GPU的运行时环境。

以下是Colab环境的一些关键特性:

  • 它运行Python 3,并连接在Google计算引擎后端。
  • 你可以通过“运行时”->“更改运行时类型”来请求GPU硬件加速器。
  • 它预装了大量的Python包,包括TensorFlow和PyTorch。
  • 你可以通过添加标题(如# 标题)来生成目录,方便组织工作。

🔢 数值计算基础

在开始编写神经网络之前,我们需要回顾一下数值计算的基础知识。我们将使用NumPy库。

我们可以创建一个数组,并对其进行各种操作。

以下是NumPy数组的基本操作示例:

  • 创建数组:x = np.zeros((3, 2)) 创建一个3行2列的全零数组。
  • 设置值:x[0, :] = 1 将第0行的所有列设置为1。
  • 形状查看:x.shape 打印数组的形状。
  • 广播运算:X + x,其中X是矩阵,x是向量,NumPy会将向量x加到矩阵X的每一行。
  • 矩阵乘法:X @ xnp.dot(X, x) 执行矩阵乘法。
  • 索引与掩码:mask = x > 0.5 生成一个布尔掩码,x[mask] = 1 利用掩码进行赋值。

我们还可以使用Matplotlib进行绘图,例如绘制随机矩阵或函数图像。

📈 线性回归实现

现在,让我们回到在讲座中看到的线性回归例子。我们将生成一些一维数据,并尝试用线性函数去拟合它。

首先,我们生成50个一维数据点x,其值在-1到1之间。我们设定真实的权重w=5和偏置b=10,从而生成真实值y_true = x * w + b

接下来,我们编写一个Linear类来表示线性函数。

以下是Linear类的实现代码:

class Linear:
    def __init__(self, num_input, num_output):
        self.weights = np.random.randn(num_input, num_output) * np.sqrt(2. / num_input)
        self.bias = np.zeros(num_output)

    def __call__(self, x):
        return x @ self.weights + self.bias

在初始化时,我们随机生成权重并将偏置设为零。__call__方法实现了y = Wx + b的计算。

使用随机初始化的线性函数进行预测,结果与真实函数相差甚远。

⚖️ 损失函数与梯度下降

为了衡量预测的好坏,我们需要一个损失函数。这里我们使用均方误差(MSE)作为损失函数。

均方误差的公式为:
MSE = (1/n) * Σ(y_true - y_pred)²

以下是MSELoss类的实现代码:

class MSELoss:
    def __call__(self, y_pred, y_true):
        self.y_pred = y_pred
        self.y_true = y_true
        return np.mean((y_true - y_pred) ** 2)

    def backward(self):
        n = self.y_pred.shape[0]
        self.grad = 2 * (self.y_pred - self.y_true) / n
        return self.grad

__call__方法计算损失值,backward方法计算损失关于预测值的梯度。

现在,我们需要为Linear类添加反向传播功能,以计算权重和偏置的梯度。

以下是Linear类的backward方法实现代码:

def backward(self, grad):
    self.weights_grad = self.x.T @ grad
    self.bias_grad = np.sum(grad, axis=0)
    self.x_grad = grad @ self.weights.T
    return self.x_grad

__call__方法中,我们需要保存输入xbackward方法接收上游梯度,并根据链式法则计算权重梯度、偏置梯度和输入梯度。

最后,我们添加一个update方法来根据梯度更新参数。

以下是update方法的实现代码:

def update(self, learning_rate):
    self.weights -= learning_rate * self.weights_grad
    self.bias -= learning_rate * self.bias_grad

🔄 训练循环

现在,我们将所有部分组合起来,形成一个完整的训练循环。

我们实例化损失函数和线性层,设置学习率,然后进行多轮(epoch)训练。在每一轮中,我们执行前向传播计算损失,执行反向传播计算梯度,然后更新模型参数。

经过60轮训练后,损失从110降到了0.01,我们的一维线性模型成功拟合了数据。这套代码同样适用于二维数据,无需任何修改。

🌊 引入非线性

到目前为止,我们处理的数据都是线性的。但对于非线性数据,单一的线性层无法很好地拟合。

我们生成一个二维的非线性数据集。如果尝试用之前的线性模型去拟合,最好的结果也只能是一个穿过曲线中间的平面,无法捕捉曲线的形状。

为了表示曲线,我们需要在模型中添加非线性激活函数。这里我们使用修正线性单元(ReLU)。

以下是ReLU类的实现代码:

class ReLU:
    def __call__(self, x):
        self.x = x
        return np.maximum(x, 0)

    def backward(self, grad):
        return grad * (self.x > 0)

__call__方法返回输入和0之间的最大值。backward方法将梯度传递给那些输入大于0的位置。

现在,我们可以构建一个简单的两层神经网络。

以下是两层神经网络的Model类实现代码:

class Model:
    def __init__(self, input_dim, hidden_dim):
        self.linear1 = Linear(input_dim, hidden_dim)
        self.relu = ReLU()
        self.linear2 = Linear(hidden_dim, 1)

    def __call__(self, x):
        l1 = self.linear1(x)
        r = self.relu(l1)
        l2 = self.linear2(r)
        return l2

    def backward(self, grad):
        grad = self.linear2.backward(grad)
        grad = self.relu.backward(grad)
        grad = self.linear1.backward(grad)
        return grad

    def update(self, learning_rate):
        self.linear2.update(learning_rate)
        self.linear1.update(learning_rate)

该模型包含一个线性层、一个ReLU激活函数和另一个线性层。前向传播依次通过这些层。反向传播则按相反顺序调用各层的backward方法。

使用这个模型训练非线性数据,损失稳步下降,模型能够较好地拟合曲线。

⚡ 使用PyTorch实现

最后,我们看看如何使用PyTorch框架来实现相同的功能。代码结构与我们手写的版本非常相似,但框架自动处理了梯度计算。

以下是使用PyTorch实现的模型代码:

import torch
import torch.nn as nn

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_24.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_26.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_27.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_29.png)

class TorchModel(nn.Module):
    def __init__(self, input_dim, hidden_dim):
        super().__init__()
        self.linear1 = nn.Linear(input_dim, hidden_dim)
        self.relu = nn.ReLU()
        self.linear2 = nn.Linear(hidden_dim, 1)

    def forward(self, x):
        x = self.linear1(x)
        x = self.relu(x)
        x = self.linear2(x)
        return x

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_31.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_33.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_35.png)

model = TorchModel(2, 10)
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_37.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/ucb-fsdl/img/6a44be82f005da3db3e7664e2717af48_39.png)

# 训练循环
for epoch in range(20):
    optimizer.zero_grad()
    y_pred = model(x_tensor)
    loss = criterion(y_pred, y_true_tensor)
    loss.backward()
    optimizer.step()

我们定义一个继承自nn.Module的类,在forward方法中定义网络结构。使用PyTorch内置的损失函数和优化器,训练循环变得更加简洁。

🎯 课程总结

本节课中我们一起学习了如何从零开始编写神经网络代码。我们从配置Colab环境开始,回顾了NumPy数值计算基础。然后,我们逐步实现了线性回归模型、均方误差损失函数以及基于梯度下降的反向传播算法。为了处理非线性数据,我们引入了ReLU激活函数并构建了一个简单的两层神经网络。最后,我们还了解了如何使用PyTorch框架更高效地实现相同功能。通过本课,你掌握了神经网络核心组件的手动实现原理,这是理解深度学习框架内部工作机制的重要一步。

机器学习可解释性课程 P20:L10B 🧠

在本节课中,我们将要学习机器学习可解释性的核心概念、定义以及几种主流技术方法。我们会探讨可解释性与可理解性的区别,并分析不同技术在实际应用中的优缺点。


概述

机器学习可解释性是一个热门话题。本节课将首先明确可解释性的相关定义,然后介绍几种主要的技术类别,包括使用可解释模型族、模型蒸馏、特征贡献分析等。我们也会讨论这些技术在实际应用中的局限性。


核心定义

在深入技术细节之前,我们需要明确几个核心概念的定义。这些定义并非行业标准,但有助于我们更清晰地讨论问题。

领域可预测性 指的是我们对于新数据点是否超出模型能力范围的可知程度。即,我们能否预知模型在哪些数据上表现良好,在哪些数据上可能表现不佳。

可理解性 指的是我们作为人类能够预测模型在给定数据上会产生何种结果的程度。这关乎我们是否在头脑中有一个关于模型行为的准确“心智模型”。

可解释性 指的是人类不仅能预测模型在数据上的行为,还能理解模型做出特定决策的原因


主要技术类别概览

接下来,我们将快速概览几种主要的可解释性技术类别,并讨论它们分别对应上述哪种定义。

以下是几种主要的技术路径:

  1. 使用可解释的模型族
  2. 将复杂模型蒸馏为简单模型
  3. 理解单个特征对预测的贡献
  4. 理解单个数据点对预测的贡献

1. 使用可解释的模型族

上一节我们介绍了核心定义,本节中我们来看看第一种技术路径:直接使用本身具有可解释性的模型。

一些可解释的模型族包括线性回归、逻辑回归、广义线性模型和决策树。这些模型之所以可解释,是因为如果你理解其数学原理,就很容易理解它们为何做出某个特定决策。

例如,对于线性回归或逻辑回归模型,其决策过程是权重矩阵与特征向量的乘法运算。因此,你可以通过公式 y = w1*x1 + w2*x2 + ... + b 来理解不同特征值及其权重对输出的贡献,这本身就是模型的决策解释。

这类模型的主要优点是它们既具有可理解性,在某种意义上也具有真正的可解释性。但需要注意的是,这种解释只对“合适的用户”有效。如果你向非技术用户展示回归系数,这可能并不能构成有效的解释。

这类模型的一个主要缺点是性能往往不够强大。你会注意到,深度学习模型并不在此列。

有人可能会问:注意力机制呢?注意力图可视化后难道不是可解释的吗?这确实是一个有用的工具,可以帮助我们建立关于模型行为的直觉。但其主要缺陷在于,很多人误将其用作获取可解释性的方式。

注意力图不能提供可靠解释的原因有两点:

  • 解释不完整:例如,在一张“站在硬木地板上的狗”的图片中,注意力图可能主要指向狗,解释了“狗”的预测,但无法解释“站立”和“硬木地板”的预测来源。
  • 解释不可靠:研究表明,注意力图显示的是模型“在看哪里”,但这并不等同于“它为何做出这个决策”。模型可能对不同的预测问题(例如“这是哈士奇” vs “这是长笛”)产生几乎相同的注意力图。

因此,注意力图不是解释


2. 将复杂模型蒸馏为简单模型

我们讨论了直接使用简单模型,接下来看看另一种思路:先训练一个能解决任务的复杂模型,然后将其“蒸馏”或简化为一个可解释的模型,并用后者来生成解释。

这类技术的核心是代理模型。其思想是:在训练好用于预测的复杂模型后,使用该模型的训练数据,再训练一个更简单的模型(如线性模型或决策树)。关键区别在于,这个简单模型不是去预测真实标签,而是去预测原始复杂模型的预测结果。当需要解释时,我们就用这个代理模型的解释来近似理解原始模型。

这种技术的优点是易于使用且通用,几乎适用于任何模型。但存在一个根本性问题:如果代理模型非常准确,那你为什么不直接用它作为主模型?如果它不准确,你又如何能信任它的解释?此外,如果我们的目标是真正的可解释性(理解原因),我们如何知道简单模型的决策方式与复杂模型相同?

另一类代理模型技术是局部代理模型,其中最常用的方法是 LIME。它的思路不是全局解释模型,而是针对单个数据点,在其周围采样生成扰动数据点,然后用原始模型对这些扰动点进行预测,最后用一个简单的局部模型(如线性模型)来拟合这些扰动点与预测结果之间的映射关系。

从概念上讲,LIME 比全局代理模型更合理,因为我们有理由相信,在某个数据点的局部邻域内,简单模型可以较好地近似复杂模型的行为。LIME 在实践中应用广泛,并且可以扩展到文本和图像数据。

LIME 的挑战在于如何正确定义“扰动”和“邻域”。实践中,研究表明 LIME 产生的解释可能不稳定,对输入条件的微小改变可能导致解释的巨大变化,因此也容易被操纵。


3. 理解特征对预测的贡献

我们介绍了可解释模型族和模型蒸馏,下一类技术则专注于理解输入特征对模型预测的贡献。这个类别包含很多方法。

一种方式是通过数据可视化

以下是两种常见的可视化图表:

  • 部分依赖图:展示某个特定特征的值与模型预测输出之间的关联,可以看出改变该特征如何影响预测函数。
  • 个体条件期望图:与PDP类似,但它为每个数据点单独绘制一条线,可以展示特征与预测关系的变化分布。

在更数值化地评估特征重要性方面,一个常见的方法是排列特征重要性。其原理很简单:选择要评估的特征,在数据集中随机打乱该特征值的顺序,然后评估模型在特征被打乱的数据集上的性能下降程度。性能下降越多,说明该特征越重要。

这是一种易于使用且在实践中相对广泛的方法。但其挑战在于,对于高维数据(如图像)效果不佳,并且无法捕捉特征之间的相互依赖关系。

一个更理论化的方法是 SHAP。其核心思想是衡量在控制所有其他特征值的情况下,某个特征的存在与否(或值的变化)对模型预测的影响有多大。SHAP 的优点是适用于多种数据类型,数学原理较为严谨,得到的特征重要性具有可加性等良好性质。缺点是实现起来比较复杂,并且它更像是一种可理解性工具而非完全的可解释性工具。

显著性图是另一种评估特征重要性的常见方法,其中最常用的是基于梯度的显著性图。其基本原理是:对于给定的输入(如图像),计算模型输出相对于每个输入像素的梯度,并将该梯度可视化。这衡量了每个像素值的单位变化对模型预测的影响程度。

这种方法易于使用,存在许多变体以产生更可解释或更理论化的结果,在研究界应用广泛。但其挑战与使用注意力机制类似:我们如何知道这是否是正确的解释?这种解释与模型实际做出预测的方式之间有何对应关系?


4. 理解数据点对预测的贡献

除了特征,另一类技术试图理解单个数据点对模型或特定预测的贡献。即,哪些数据点对模型至关重要?

以下是两类技术(在本课程中不深入展开):

  • 原型与批评:原型是能解释数据集中大量变体的聚类中心;批评则是那些无法被原型很好解释的数据点。
  • 影响实例:寻找那些如果从数据集中移除,会导致最终生成的分类器发生重大变化的数据点。

反思:可解释性是正确的目标吗?

在快速浏览了主要技术后,我们需要退一步思考一个高层次问题:可解释性真的是我们正确的目标吗?

人们追求模型可解释性通常有几个原因:

  1. 监管要求:某些行业规定模型必须可解释。对此,务实的回应可能是进行一些可解释性分析以确保合规。
  2. 用户需求:用户希望信任模型的预测,因此需要解释。这里需要考虑产品设计的作用,良好的产品设计(如允许用户覆盖预测)可以减少对解释的绝对需求。此外,用户与模型的交互频率也很关键。对于低频、高影响的决策(如贷款审批),可靠的解释可能非常重要;对于高频交互场景(如推荐系统),帮助用户建立对模型行为的直觉(即可理解性)可能更为重要。
  3. 模型可信度:开发者不确定是否应该信任模型并将其部署到生产环境。我认为,在这种情况下,我们真正需要的目标不是可解释性,而是之前定义的“领域可预测性”。我们想知道模型性能的边界在哪里,希望尽可能减少“未知的未知”。一些可解释性文献中的可视化工具可能有助于建立领域可预测性或进行调试。

关于当前(2021年中)深度学习模型的可解释性,我得出的结论是:真正的可解释性目前对于深度学习系统来说还不是一个现实的目标。现有方法在忠实反映原始模型行为方面并不可靠,它们往往比较脆弱,且通常无法完整解释模型的决策。

因此,如果你确实需要解释模型的预测,目前唯一可靠的答案是使用本身就可解释的模型族,如线性模型、决策树等。你可以尝试解释深度学习模型,甚至训练一个模型来为深度学习模型的预测输出解释语句,这是一个有趣的研究方向,但就生产环境所需的可靠可解释性而言,目前尚不成熟。

对于可理解性方法,如 SHAP 和 LIME,在实践中应用最多。我认为它们非常有用的一个场景是:帮助用户更快地建立对模型的直觉,从而信任模型的预测。这些可视化可以帮助用户理解模型在哪些方面表现好或不好。

最后,尽管我对该领域在深度学习中的应用价值持一定怀疑态度,但这并不意味着这些可视化毫无用处。事实上,许多可视化工具在调试模型和确定改进模型性能的优先级方面非常有用。我只是不确定它们是否已经准备好为模型的决策提供准确、完整的解释


总结

本节课中我们一起学习了机器学习可解释性的核心概念。我们首先区分了领域可预测性、可理解性和可解释性的定义。然后,我们快速概览了四种主要的技术路径:使用可解释模型族、模型蒸馏、分析特征贡献以及分析数据点贡献。最后,我们深入反思了追求可解释性这一目标本身的合理性,并指出在当前阶段,对于深度学习模型,实现真正的、可靠的可解释性仍面临挑战,而可理解性工具和领域可预测性可能是更实际和重要的目标。

🧪 P21:【Lab8】测试与持续集成

在本节课中,我们将学习如何为机器学习项目添加代码规范检查、系统功能测试、模型评估测试以及训练系统测试。我们还将设置持续集成流程,确保每次向代码仓库推送代码时,这些测试都能自动运行。我们将使用 CircleCI 作为持续集成工具。

📝 代码规范检查

上一节我们介绍了课程目标,本节中我们来看看代码规范检查。代码规范检查指使用静态分析工具检查代码文件的风格、文档和一些基本错误。这在多人协作的代码库中非常必要,可以统一代码风格,并提前发现一些运行时才会暴露的错误。

我们有一个用于设置所有检查的脚本文件 tasks/lint。它是一个 Bash 脚本,会依次运行 safetypylintpycodestylepydocstylemypybanditshellcheck。如果其中任何一个检查失败(除了 safety),整个规范检查就会失败。如果全部通过,则显示“linting passed”。

以下是各个工具的作用:

  • safety:扫描 Python 包依赖项中的安全漏洞。
  • pylint:进行静态分析,检查代码错误和风格问题。
  • pycodestyle:专门检查代码风格,与 pylint 部分重叠但包含一些额外规则。
  • pydocstyle:检查函数、类和模块的文档字符串。
  • mypy:利用静态类型提示进行类型检查。
  • bandit:查找 Python 代码中常见的安全漏洞,例如使用 eval 函数。
  • shellcheck:对 Bash 脚本进行规范检查。

这些检查工具的配置位于几个文件中,最主要的是 .pylintrcsetup.cfg。它们定义了诸如最大行长度、要禁用的警告消息等规则。

此外,我们还可以使用一个名为 black 的自动化代码格式化工具。它可以应用于 Python 文件,你也可以将其设置为在保存代码时自动运行。它会执行一系列更改,使不同开发者的代码风格高度统一,例如将单引号替换为双引号、强制行长度限制、对过长的函数调用进行缩进等。它与 pylint 兼容,值得使用。

🧪 系统功能测试

了解了代码规范检查后,本节我们来看看系统功能测试。我们正在开发一个应用程序,它可以拍摄手写文本的图片并返回其中的文字。我们的代码库包含多个模块:模型训练模块、预测模型模块和 Web 后端模块。

在之前的 Lab 7 中,我们添加了 ParagraphTextRecognizer 模块,它接收一张图片并返回其中的文字。在本实验中,我们将为该模块添加一个测试。

测试位于 text_recognizer/test/test_paragraph_text_recognizer.py 中。首先,我们禁用 CUDA,以便在测试时不使用 GPU。测试中使用了一些位于 tests/support/ 目录下的支持文件,这些图片模拟了生产环境中可能遇到的用例,实际上来自 IAM 数据集。同时,还有一个 JSON 文件为每张图片指定了真实文本和预期的错误率。

该测试的基本流程是:将这些支持文件通过段落识别器运行,然后检查预测文本是否与预期匹配,字符错误率是否符合预期,并报告运行时间。这些测试的目标是运行时间不应超过一分钟,以便我们可以频繁地运行它们。

📊 模型评估测试

完成了系统功能测试,接下来我们关注模型评估测试。该测试位于 text_recognizer/evaluation/evaluate_paragraph_text_recognizer.py。这个测试运行时间较长,因此我们希望使用 GPU。

该测试会评估我们的 ParagraphTextRecognizer 模型(它会加载训练好的权重和配置)在 IAM 段落测试数据集上的性能。最终,它会报告字符错误率和所用时间,并断言字符错误率低于预期值,且所用时间也少于预期。目前,我们的模型预期字符错误率约为 17%。

测试过程很简单:加载数据集,加载 ParagraphTextRecognizer(这是一个 PyTorch Lightning 模型),然后使用 PyTorch Lightning 的 Trainer 进行评估。

⚙️ 训练系统测试

在模型评估之后,我们还需要确保训练流程本身是可靠的。我们称之为基础设施测试,位于 training/tests/test_run_experiment.py

这个测试实际上会运行一个 Python 命令行,在一个名为 FakeImageDataModule 的新数据集上进行训练。这个数据集提供合成图像,这样我们就不需要下载真实数据并花费大量时间。我们训练一个 ConvNet,并传入一些参数以测试对参数的理解。我们只训练几个周期,目的是验证训练过程能够成功完成。

这个测试并不检查模型的正确性,只是检查我们是否能够进行训练。因为有时引入的某些错误可能会导致训练系统无法运行。我们可以通过改进这个测试,确保它能在该数据上训练出一个高精度的模型,但这可以作为课后练习。

🔄 设置持续集成

最后,我们将设置持续集成,确保每次将代码推送到 GitHub 仓库时都能自动运行上述测试。我们将使用 CircleCI 来实现。

首先,我们需要 Fork 本实验的代码仓库到自己的 GitHub 账户。然后,登录 CircleCI 并使用 GitHub 授权。接着,在 CircleCI 中选择我们 Fork 的仓库。CircleCI 会自动检测仓库中的配置文件(.circleci/config.yml),然后我们可以点击“Start Building”开始构建。

构建成功后,所有测试项都会显示为绿色的对勾。如果出现问题,则会显示红色的叉号。这能让我们及时知道持续集成失败了。如果我们创建拉取请求,GitHub 也会集成显示 CircleCI 的状态。

让我们看一下 .circleci/config.yml 文件的内容。它使用一个 Python 3.6 的 Docker 镜像,安装了 git-lfs(用于获取大文件)、shellcheck(用于规范检查)以及我们指定的依赖包版本。然后,它首先运行代码规范检查,接着运行测试。即使其中一项失败,另一项仍会运行,这样如果规范检查失败,我们仍然可以看到测试是否通过。

配置非常简单,这就是设置持续集成所需的基本步骤。我们没有在 CircleCI 中运行评估测试,因为 CircleCI 不提供 GPU,而没有 GPU 运行该测试会耗时过长。

📚 课程总结

本节课中我们一起学习了如何为机器学习项目构建完整的测试与持续集成流程。我们介绍了代码规范检查的重要性及工具,为预测系统、模型评估和训练流程添加了不同类型的测试,并最终使用 CircleCI 搭建了自动化的持续集成管道。这套流程能有效保障代码质量,并在团队协作中确保项目的稳定性和可维护性。

🚀 课程 P22:L11A - 部署机器学习模型

在本节课中,我们将要学习如何将训练好的机器学习模型部署到生产环境,并探讨部署后的监控与维护。我们将从部署的不同模式讲起,涵盖从简单的批处理预测到复杂的模型服务化部署,并讨论如何优化性能、管理依赖以及进行水平扩展。


📦 部署模式概览

部署机器学习模型时,一个核心的决策点是将模型放置在应用架构的哪个位置。下图展示了一个典型Web应用的简化架构:

用户通过客户端(如浏览器)连接到运行代码的服务器,服务器处理后端逻辑并可能与数据库交互,最终将结果返回给用户。

1. 批处理预测

第一种部署模式是批处理预测。在这种模式下,我们不实时运行模型,而是定期在新数据上运行模型,并将预测结果缓存到数据库中。当用户请求时,应用直接从数据库读取缓存的结果。

以下是批处理预测的优缺点:

  • 优点
    • 对应用代码改动极小。
    • 对用户而言延迟很低,因为响应只是快速的数据库查询。
  • 缺点
    • 适用范围有限,仅适用于输入空间可枚举的场景(例如,每天为每个用户生成一个推荐)。
    • 用户无法获得最新的预测,预测结果总是“过时”的。
    • 如果缓存更新失败,很难被检测到,用户会持续收到旧的预测。

上一节我们介绍了离线批处理的模式,本节中我们来看看如何将模型集成到在线服务中。

2. 模型内置于Web服务

第二种模式是将模型直接打包,作为现有Web服务的一部分进行部署。具体做法是,将模型权重文件(或从如S3的存储中下载)加载到Web服务器中,当请求到来时直接调用模型进行预测。

以下是这种模式的优缺点:

  • 优点
    • 实现简单。
    • 复用现有的Web服务基础设施。
  • 缺点
    • 技术栈冲突:Web服务器可能由不同团队用不同语言(如Node.js)编写,而模型可能是Python代码,需要额外的桥接。
    • 耦合部署:模型更新需要重新部署整个Web服务。
    • 资源竞争:模型推理可能消耗大量计算资源(如GPU),影响Web服务的其他功能。
    • 伸缩性一致:Web服务和模型必须按相同方式伸缩,无法根据各自需求独立优化。

鉴于这些限制,更常见的模式是将模型部署为独立的服务。

3. 独立的模型服务

第三种,也是最常见的模式,是将模型部署为独立的Web服务。后端服务器或客户端通过向这个模型服务发送请求并接收响应来获取预测结果。

以下是这种模式的优缺点:

  • 优点
    • 解耦与容错:模型的问题(如内存泄漏)不会导致整个Web应用崩溃。
    • 独立伸缩:可以为模型服务选择专用硬件(如GPU服务器),并根据模型负载独立进行水平扩展。
    • 灵活复用:多个应用可以共享同一个模型服务。
  • 缺点
    • 增加延迟:需要额外的网络调用。
    • 增加基础设施复杂度:需要部署和维护一个新的服务。
    • 运维责任:需要负责运行和维护自己的模型服务。

接下来,我们将深入探讨如何构建和管理这样一个独立的模型服务。


🔧 构建模型服务

构建模型服务涉及多个方面。以下是核心内容:

1. REST API

REST API是一种通过HTTP请求/响应提供预测服务的标准方式。虽然存在其他协议(如gRPC、GraphQL),但REST是目前实践中最常用的。

一个调用模型REST API的示例使用 curl 命令:

curl -X POST https://api.example.com/predict \
  -H “Content-Type: application/json” \
  -d ‘{“input”: [1, 2, 3]}’

响应可能是JSON格式:

{“prediction”: 0.85}

目前,机器学习模型的REST API请求/响应格式尚无统一标准。各大云平台(Google Cloud, Azure, AWS)的格式也各不相同。建议选择一种对你有意义或参考主流平台的格式。

2. 依赖管理

模型服务需要包含以下元素才能运行:

  • 服务代码:处理请求和调用模型的逻辑。
  • 模型权重:训练好的模型参数。
  • 代码依赖:运行代码所需的所有库(如TensorFlow、PyTorch)。

模型权重和代码可以通过常规的代码部署流程复制到服务器。对于大型权重文件,通常会在服务启动时从远程存储(如S3)下载。

依赖管理更具挑战性,因为库版本的不一致可能影响模型行为。有两种主要策略:

策略一:约束依赖
尝试使用标准格式保存模型,以减少对特定机器学习库版本的依赖。ONNX 是一个开放格式,旨在让模型能在任何地方运行。然而在实践中,转换层可能存在bug,且模型相关的预处理代码无法被ONNX捕获,依赖问题依然存在。

策略二:使用容器
更健壮的方法是使用容器技术,如 Docker

3. Docker 容器

Docker容器是一种轻量级的虚拟化技术。与传统的虚拟机不同,容器共享主机操作系统内核,因此更加轻量和快速。

在微服务架构中,应用的每个组件(Web服务器、数据库、模型服务)都可以运行在独立的容器中,各自管理自己的依赖。

通过 Dockerfile 定义容器环境:

FROM python:3.8-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY model_weights.pth .
COPY app.py .
EXPOSE 8080
ENV MODEL_PATH=“model_weights.pth”
CMD [“python”, “app.py”]

容器镜像可以推送到 Docker Registry(如Docker Hub),确保环境的一致性。Docker拥有强大的生态系统,有大量预构建的镜像可供使用。

当应用由多个容器组成时,需要 容器编排 工具来管理它们的部署、伸缩和通信。Kubernetes 是目前的主流选择,Docker Compose 则适用于本地开发和简单场景。


⚡ 性能优化

模型服务部署后,我们需要让它运行得更快、更高效。以下是在单机上可以采用的优化技术:

1. GPU vs CPU 推理

  • 使用GPU推理
    • 优点:通常能获得最高吞吐量;与训练硬件一致。
    • 缺点:设置更复杂;在实践中通常更昂贵。
  • 使用CPU推理
    • 优点:更简单,成本可能更低。
    • 缺点:对于大型模型,吞吐量可能较低。

决策取决于你的延迟、吞吐量和预算要求。

2. 并发

即使使用CPU,也可以通过并发来利用多核优势。即在同一台机器上运行模型的多个副本(或进程),每个处理不同的请求。关键是要进行线程调优,确保每个模型副本只使用必要的线程数,避免资源争用。

3. 模型蒸馏与量化

  • 模型蒸馏:训练一个更小的“学生”模型来模仿大型“教师”模型的行为,以牺牲少量精度换取更小的模型尺寸和更快的推理速度。在实践中可能较难稳定实现,但可以优先考虑使用预训练的蒸馏模型(如DistilBERT)。
  • 量化:将模型权重从32位浮点数转换为更低精度的表示(如8位整数),从而减少模型大小、提升推理速度。PyTorch和TensorFlow Lite都提供了量化工具。可以采用量化感知训练来减少精度损失。

4. 缓存与批处理

  • 缓存:对于频繁出现的输入,可以缓存其预测结果,避免重复运行模型。Python的 functools.lru_cache 可以用于实现简单的内存缓存。
  • 批处理:将多个输入请求组合成一个批次送入模型,能极大提升GPU利用率和平行计算效率。这需要服务端累积请求,并处理好批次拆分与结果返回,需要在吞吐量和延迟之间进行权衡。

5. GPU共享与专用库

  • GPU共享:当一个模型的批处理不足以占满GPU时,可以让多个模型或多个任务的推理共享同一块GPU,提高资源利用率。这是一项前沿技术。
  • 使用专用服务库:避免重复造轮子。TensorFlow ServingTorchServeNVIDIA Triton 等库内置了批处理、并发等优化,是进行GPU推理的推荐选择。

📈 水平扩展

当单个服务器的流量过大时,需要进行水平扩展:创建模型的多个副本,并使用负载均衡器将流量分发到这些副本上。

主要有两种实现方式:

1. 基于容器编排(如Kubernetes)

使用Kubernetes可以方便地管理模型服务容器的多个副本,并自动进行负载均衡和故障恢复。一些开源项目如 KServe (原KFServing) 和 Seldon Core 在Kubernetes上提供了更高级的机器学习模型服务功能。

2. 无服务器函数(Serverless)

AWS LambdaGoogle Cloud Functions。你将模型代码和依赖打包,云服务商负责运行、伸缩和负载均衡。你只需按请求付费。

  • 优点:无需管理服务器;成本效益高(尤其适用于流量波动的场景);伸缩完全自动化。
  • 缺点:部署包大小和函数运行时间有限制;状态管理困难;部署工具链可能不如传统方式完善。

选择建议

  • 如果进行CPU推理且流量不大或呈波动态,无服务器函数是简单易用的选择。
  • 如果进行GPU推理或需要细粒度控制,建议使用专用服务库并结合容器编排进行部署。

🚢 部署流程与托管选项

部署流程

模型部署不仅关乎技术,还涉及流程。最佳实践包括:

  • 渐进式发布:先将少量流量(如1%)导向新模型,逐步增加至100%。
  • 快速回滚:一旦发现问题,能立即切换回旧版本。
  • 管道部署:支持部署由多个模型组成的推理管道。

托管服务

如果你不想管理任何基础设施,可以考虑云厂商或第三方提供的全托管机器学习部署服务,如:

  • Google Cloud AI Platform Prediction
  • Amazon SageMaker
  • Azure Machine Learning
  • 初创公司:Algorithmia, Cortex

这些服务简化了部署,但通常价格昂贵。


📱 边缘/客户端部署

最后一种部署模式是将模型直接部署到客户端设备上,如手机、浏览器或物联网设备。这被称为边缘预测

优缺点

  • 优点
    • 延迟最低:推理在本地完成。
    • 离线工作:无需网络连接。
    • 隐私保护:用户数据无需离开设备。
  • 缺点
    • 资源受限:客户端硬件(算力、内存)有限。
    • 框架受限:需使用精简版框架(如TensorFlow Lite)。
    • 更新困难:模型权重已分发到设备,更新麻烦。
    • 调试困难:难以获取设备上的模型运行数据和性能指标。

工具与框架

针对不同的边缘设备,有不同的优化工具:

  • NVIDIA设备TensorRT
  • 跨框架/跨硬件编译Apache TVM
  • TensorFlow移动端TensorFlow Lite
  • PyTorch移动端PyTorch Mobile
  • 浏览器TensorFlow.js
  • 苹果设备Core ML
  • 安卓设备ML Kit

开发心态调整

进行边缘部署时,需要调整开发心态:

  1. 早期考虑硬件约束:从一开始就选择适合目标硬件的模型架构(如MobileNet, DistilBERT)。优化技术(蒸馏、量化)通常只能带来2-10倍的提升,如果初始模型不在目标范围内,后续会非常困难。
  2. 将边缘优化视为风险点:在测试阶段,必须在目标硬件或模拟环境中充分测试编译后的模型,确保性能和准确性。
  3. 设计降级方案:为模型可能出现的失败或延迟设计后备方案(如使用规则系统或简化模型),这对安全关键应用尤为重要。

总体建议:Web部署通常更容易。只有在确实需要超低延迟、离线功能或强隐私保护时,才考虑边缘部署。选择工具时,尽量匹配目标硬件对应的优化框架。


🎯 总结

本节课中我们一起学习了机器学习模型部署的核心知识。我们探讨了三种主要部署模式:批处理预测、模型内置Web服务以及独立的模型服务,并详细分析了各自的适用场景和优劣。我们深入研究了如何构建模型服务,包括设计REST API、使用Docker管理依赖、进行性能优化(如并发、量化、批处理)以及通过容器编排或无服务器函数实现水平扩展。最后,我们简要介绍了边缘部署的概念、工具链以及与之相关的特殊开发考量。掌握这些内容,将帮助你更顺利地将机器学习模型从实验环境推向实际生产,并保障其稳定、高效地运行。

📊 P23:L11B - 监控机器学习模型

在本节课中,我们将要学习如何监控已部署的机器学习模型。模型部署后,确保其持续健康运行至关重要,因为许多因素都可能导致模型性能下降。我们将探讨性能下降的原因、如何检测这些变化,以及构建监控系统的最佳实践。

🎯 概述:为什么需要监控模型

上一节我们讨论了模型训练和评估,本节中我们来看看模型部署后的情况。模型部署后,许多因素可能导致其性能下降。理论上,问题可能源于输入数据分布 P(x) 的变化(数据漂移),或条件概率 P(y|x) 的变化(概念漂移),亦或是训练数据采样过程引入的偏差。

🔍 监控什么:关键信号

为了判断模型性能是否发生变化,我们需要监控几个关键信号。以下是四种值得监控的信号,它们在信息价值与获取难度之间存在权衡。

  • 模型性能指标:最直接的监控对象。通过对比预测值与真实标签,可以准确评估模型表现。然而,获取及时、低成本的标注数据通常是最大挑战。
  • 业务指标:例如推荐系统的用户点击率或参与度。这些指标易于测量,但受多种因素影响,不能直接反映模型准确率。
  • 模型输入与预测分布:通过分析输入特征和输出预测的分布变化来检测漂移。即使没有真实标签,这种方法也颇具参考价值。
  • 系统性能指标:监控GPU利用率、请求延迟等系统健康指标。这有助于发现严重的系统级故障,但无法捕捉模型特有的性能问题。

📊 如何测量变化:检测数据漂移

我们讨论了可以监控哪些信号,接下来看看如何实际测量这些信号是否发生了变化。检测分布变化的总体策略是:选择一个代表“健康”状态的参考数据窗口,再选择一个待测量的当前数据窗口,然后使用某种距离度量来比较这两个窗口。

选择参考窗口与测量窗口

  • 参考窗口:通常可以使用训练集或评估集作为参考,因为它们代表了模型被设计和优化的数据分布。
  • 测量窗口:根据具体问题选择,例如监控过去一小时、一天或一周的数据。也可以设置多个滑动窗口进行持续监控。

一维数据的距离度量

对于连续的一维数据,主要有两类度量方法:

基于规则的度量(数据质量检查)
以下是常见的基于规则的检查项:

  • 检查特征的最小值、最大值、均值是否在参考窗口定义的允许范围内。
  • 检查数据点数量是否充足,或缺失值、NaN值是否过多。
  • 检查更复杂的规则,例如某一列的值是否始终大于另一列。

统计距离度量
以下是几种常用的统计距离:

  • KL散度D_KL(P||Q) = Σ P(i) * log(P(i)/Q(i))。不推荐用于监控,因为它对分布尾部的噪声非常敏感,且难以解释。
  • KS统计量D_KS = max_x |CDF_P(x) - CDF_Q(x)|。它度量了两个累积分布函数之间的最大距离,解释性强,推荐使用。
  • PSI(群体稳定性指数)D1距离D1 = Σ |P(i) - Q(i)|。计算两个概率密度函数差异的绝对值之和,直观且易于解释。

高维数据的处理方法

处理高维数据的漂移检测仍是一个开放性问题。以下是一些可行策略:

  • 进行多个一维比较:分别计算每个特征的距离度量,然后取最大值或关注最重要的特征。
  • 使用多维检验:例如最大均值差异(MMD),它比较两个分布在高维特征空间中的均值差异。
  • 使用投影方法:先将高维数据投影到低维空间,再在低维空间进行检验。投影方式可以是:
    • 领域知识投影:如对图像计算平均像素值,对文本计算句子长度。
    • 随机投影:使用随机矩阵进行线性投影。
    • 统计模型投影:使用PCA、自编码器或密度模型(如计算数据点的似然)进行投影。

⚠️ 如何判断变化是否严重:设置警报阈值

我们知道了如何测量变化,但如何判断这种变化是否需要干预呢?不幸的是,目前还没有非常完美的自动化方案。

  • 统计检验的局限性:像KS检验这类给出p值的方法,在数据量很大时,即使微小的、无关紧要的分布变化也会导致极低的p值,从而产生大量误报。
  • 常见的实践方法
    • 手动设置规则:根据领域知识,为关键指标设置固定的允许范围(如“特征A的均值必须在X和Y之间”)。
    • 在时间序列上应用异常检测:将距离度量值本身视为一个时间序列,对其应用异常检测算法来发现异常点。
  • 未来方向:理想的方法是能够近似估计数据分布变化对模型性能的具体影响,而不仅仅是给出一个抽象的“漂移分数”。这将是该领域一个有价值的研究方向。

🛠️ 监控工具概览

了解核心概念后,我们快速浏览一下可用的工具类别。

  • 系统监控工具:用于设置系统级警报(如CPU、内存、延迟)。例如云服务商提供的Amazon CloudWatch、以及Datadog、New Relic等。
  • 数据质量工具:专注于基于规则的数据质量检查。例如开源库Great Expectations,以及商业产品Monte Carlo和Anomalo。
  • 机器学习监控平台:新兴的、专门为ML模型监控设计的平台,如Arize AI、Arthur AI和Fiddler。这个领域正在快速发展。

🔄 监控在ML系统中的地位:从监控到评估存储

最后,我们将监控置于更广阔的机器学习系统中来看。在传统软件中,监控主要用于故障检测和诊断。但在机器学习中,监控扮演着更核心的角色。

  1. 问题的隐蔽性:ML模型的故障通常是“静默”的性能退化,而非系统崩溃,因此更需要主动监控来发现。
  2. 数据飞轮的核心:监控收集的数据(模型输入、预测、预估性能)本身就是迭代和改进模型的关键燃料。因此,监控系统应更紧密地集成到整个ML工作流中,我倾向于称之为 “评估存储”

这个“评估存储”系统可以帮助:

  • 训练时:记录基准数据分布和模型性能。
  • 评估时:用于A/B测试和离线评估。
  • 监控时:持续评估模型在线上数据上的近似性能。
  • 数据收集:根据预估性能低的区域,有针对性地采样更多数据用于标注和重新训练。
  • 重训练决策:通过评估性能下降程度,辅助决定何时需要重新训练模型。

📝 总结与要点

本节课中,我们一起学习了机器学习模型监控的核心知识。

  • 监控是必要的:模型部署后性能会随时间退化,必须通过监控来发现和解决问题。
  • 从简单开始:建议从监控数据质量规则(特征范围、缺失值)和系统指标入手,这能捕获大部分问题。
  • 理解漂移类型:关注数据漂移和概念漂移,并使用合适的统计量(如KS统计量、PSI)进行测量。
  • 阈值设置是挑战:判断变化是否严重目前仍需人工参与,未来需要能关联漂移与性能影响的技术。
  • 融入系统循环:理想的监控系统应超越告警,成为“评估存储”,紧密集成到训练、评估、数据收集和重训练的整个迭代循环中,助力闭合数据飞轮。
  • 工具与研究方向:该领域工具正在成熟,同时存在大量未解决的研究问题,例如如何量化漂移对性能的影响,是一个非常有价值的探索方向。

课程 P24:L12 - 研究方向 📚

在本节课中,我们将探讨深度学习的研究方向。虽然本课程主要关注实践应用,但理解研究前沿至关重要,因为深度学习领域的研究与实践联系紧密,新成果往往在一年内就能投入实际应用。我们将概览几个关键研究方向,并讨论如何跟上快速发展的研究步伐。

无监督学习 🤖

上一节我们介绍了深度学习在实践中的应用,本节中我们来看看如何减少对标注数据的依赖。深度监督学习需要大量标注数据,而无监督学习旨在从无标签或部分标签的数据中学习。

半监督学习

半监督学习结合了有监督和无监督学习。我们仍然处理分类问题,但只拥有部分数据的标签。

以下是其核心思想:

  • 标签传播:如果未标记的数据点靠近已标记的同类数据点,则它们很可能属于同一类别。通过这种方式,标签可以从已标记数据“传播”到其邻近的未标记数据。
  • 噪声学生方法:首先用有标签数据训练一个“教师”模型。然后,用这个教师模型为无标签数据生成“伪标签”。将有信心的伪标签数据加入训练集,并加入噪声(如Dropout、数据增强)重新训练一个更鲁棒的“学生”模型。

研究表明,这种方法可以在ImageNet等任务上显著提升模型性能。

无监督学习

无监督学习更进一步,完全不依赖任务相关的标注数据。其核心思想是设计一个辅助的无监督预测任务,让模型从中学习通用的特征表示,这些特征随后可用于下游的有监督任务。

常见的无监督预测任务包括:

  • 语言:预测句子中的下一个词(如GPT系列)或填充被掩盖的词(如BERT)。
  • 图像:预测图像缺失的补丁、解决拼图游戏或预测图像的旋转角度。

一个突破性的例子是GPT-2。它通过海量文本训练来预测下一个词,展示了强大的文本生成和理解能力,甚至能在少量示例下完成情感分类、常识推理等任务。公式上,其目标是最大化序列的似然估计:
P(x_t | x_1, ..., x_{t-1})
其中 x_t 是当前位置的单词,模型基于之前的所有单词进行预测。

在计算机视觉领域,对比学习(如SimCLR、MoCo)取得了巨大成功。其核心思想是:

  • 对同一张图像进行两次不同的数据增强(如裁剪、变色),得到两个变体。
  • 训练模型使这两个变体在特征空间中的表示尽可能接近(正样本对)。
  • 同时,使来自不同原始图像的变体在特征空间中的表示尽可能远离(负样本对)。

通过这种方式,模型学会了提取图像的本质特征,这些特征可以很好地迁移到图像分类等有监督任务中。

无监督学习是近年来从研究到实践转化最快的领域之一,已广泛应用于NLP,并正在改变计算机视觉领域。

强化学习 🎮

现在让我们从被动感知转向主动决策。强化学习研究的是智能体如何通过与环境的交互来学习达成目标的最佳策略。

与监督学习不同,强化学习面临独特挑战:

  • 信用分配问题:智能体在完成一系列动作后获得一个总奖励,但很难确定每个具体动作的贡献。
  • 探索与利用的权衡:智能体需要在尝试新动作(探索)和利用已知有效动作(利用)之间取得平衡。
  • 稳定性:学习过程中的试错可能导致策略剧烈波动。

尽管如此,强化学习已取得瞩目成就:

  • 游戏:DeepMind的DQN学会了玩Atari游戏,AlphaGo/AlphaZero在围棋、象棋上超越人类。
  • 机器人控制:让机器人学习跑步、翻转、操作物体(如将积木放入对应形状的孔中、解魔方)。
  • 动画:DeepMimic等工作可以生成逼真的人物和生物运动,用于游戏和电影。

将无监督学习与强化学习结合是当前的研究热点。例如,在训练智能体时,同时使用对比学习从图像中提取更好的特征表示,可以显著加快从像素输入中学习的速度,使其性能接近甚至达到直接使用环境状态信息进行训练的水平。

元强化学习 🔄

上一节我们介绍了通用强化学习算法,本节中我们来看看能否让AI自己学会学习。通用RL算法(如PPO、DQN)适用于任何环境,但学习速度可能较慢。元强化学习的目标是学习一个更好的强化学习算法本身

其核心思想是:让一个“元智能体”在许多不同的训练环境中学习。通过这个过程,它能够掌握快速适应新环境的策略或学习机制。当被放入一个全新的但相关的测试环境时,它就能利用之前积累的经验快速学习。

一种实现方式是将智能体建模为一个循环神经网络。RNN的内部权重定义了其学习算法,而激活状态则代表了其当前的策略。通过在外层使用传统RL方法优化这个RNN,我们可以得到一个能快速适应新任务的智能体。实验表明,这种方法在诸如多臂老丨虎丨机、迷宫导航等任务上,可以超越一些传统的最优算法。

模拟到真实迁移 🏗️

在现实世界中训练机器人成本高、风险大。利用模拟器生成数据是一种高效的替代方案。关键挑战在于:如何在模拟器中训练,却能应用到真实世界?

主要有以下几种方法:

  1. 提高模拟器逼真度:尽可能让模拟器接近现实,但这通常会导致计算成本飙升。
  2. 域适应/域混淆:训练一个判别器来区分特征来自模拟器还是真实世界。同时训练主网络,使其提取的特征能够“欺骗”判别器,从而让后续网络无法区分数据来源,实现知识迁移。
  3. 域随机化:这是目前非常有效且简单的方法。其核心思想是:不在追求模拟器的绝对真实,而是在模拟器中创建大量随机变化(如物体颜色、纹理、光照、物理参数等)。虽然每个随机场景都不真实,但模型在见识了足够多的变化后,学到的策略反而能很好地泛化到从未见过的真实世界。

例如,在模拟器中用随机生成的形状和纹理训练机器人抓取模型,该模型可以直接在真实世界中抓取新物体。OpenAI的机械手解魔方项目也大量使用了域随机化技术。

深度学习用于科学与工程 🧬

最后,我们来看一个可能产生颠覆性影响的领域。与模仿人类能力的CV、NLP不同,AI用于科学和工程有望解决人类自身难以解决的问题。

一个标志性成果是DeepMind的AlphaFold 2。它能够根据蛋白质的氨基酸序列,高精度地预测其三维结构,在权威的CASP竞赛中取得了接近实验测量精度的成绩,解决了困扰生物学界数十年的“蛋白质折叠问题”。

其模型架构复杂,整合了多序列比对、注意力机制和几何约束推理。这展示了深度学习在推动基础科学发现方面的巨大潜力。

此外,深度学习还可用于:

  • 加速设计:用神经网络替代昂贵的仿真器,快速评估芯片、材料、药物分子等设计方案的性能。
  • 增强数据:用生成模型为科学计算补充数据。
  • 解决数学问题:用Transformer等模型进行符号积分或求解微分方程。

研究趋势与如何跟进 📈

当前研究的一个显著趋势是计算量呈指数级增长。最前沿实验所使用的算力(以PetaFLOP-day计)每年都在大幅增加。这意味着,许多过去因算力限制而无法探索的想法,现在成为了可能。

对于研究者而言,可以选择:

  • 依赖人类智慧的问题:需要精巧算法和深刻见解。
  • 依赖数据与算力的问题:利用前所未有的海量数据和强大算力开辟新领域,这里可能存在更多“低垂的果实”。

如何跟上研究步伐

面对每月数千篇的新论文,我们需要高效的学习策略。

首先,如何不读论文也能跟进前沿:
以下是几个高效的信息源:

  • 顶级会议的教程:通常能浓缩某个方向上百篇论文的精华。
  • 研究生课程与研讨会:内容系统,易于消化。
  • YouTube频道:如Yannic Kilcher的论文讲解、Two Minute Papers的快速概览。
  • 优质通讯:如Andrew Ng的《The Batch》,Jack Clark的《Import AI》。
  • 社交媒体:关注领域内的活跃研究者(如Sergey Levine, Josh Tobin)。

当你决定读论文时:

  • 利用工具筛选:如arxiv-sanity.com,可以查看最近最受欢迎、最受热议的论文。
  • 掌握阅读技巧:不要逐字阅读。先读标题、摘要、结论,再看图表,快速判断论文价值。
  • 组建阅读小组:与同伴一起学习,分工分享,事半功倍。

关于攻读博士学位的思考

最后,简要探讨是否要攻读博士学位。如今,AI领域实践机会丰富,不读博士同样可以构建出色的应用。攻读博士的理由在于:如果你渴望在某个精深的技术方向(如小样本学习、基于模型的强化学习)成为世界级专家,致力于创造新的工具和方法(发明“锤子”),而不仅仅是使用现有工具,那么博士生涯将提供这样的深度训练机会。


本节课中我们一起学习了深度学习的多个前沿研究方向,包括无监督学习、强化学习、元学习、模拟到真实迁移以及AI在科学中的应用。我们看到了研究如何快速推动实践,并讨论了在这个信息爆炸的时代如何高效地保持知识更新。希望本课能为你进一步探索AI的广阔世界提供有用的指引。

P25:【Lab9】Web 部署 🚀

在本节课中,我们将学习如何将 Lab 8 中开发的段落文本识别模型进行 Web 部署。我们将通过 TorchScript 加速模型推理,将其封装为 Web 应用,并最终打包为 Docker 容器,为部署到无服务器函数(如 AWS Lambda)做好准备。

准备工作

在开始之前,请确保完成以下准备工作:

以下是具体步骤:

  • 更新到最新的实验代码库:git pull
  • 激活 fsdl-text-recognizer-2021 环境。
  • 安装本实验所需的最新 Python 依赖包。

本实验新增了许多文件,主要集中在 api/ 目录下,包括 api/serverapi/serverless

使用 TorchScript 加速推理 🔥

上一节我们完成了准备工作,本节中我们来看看如何加速模型推理。

我们目前拥有一个在 Lab 8 中训练好的 PyTorch 模型。为了加速推理,我们可以使用 TorchScript

TorchScript 的本质是将动态定义的 PyTorch 代码编译成静态定义的格式,并利用更快的 C++ 前端路径执行。它最初是 Caffe2 项目的扩展,后与 PyTorch 项目合并。这能显著提升推理速度,而所需的代码改动非常小。

所有改动都集中在 text_recognizer/paragraph_text_recognizer.py 文件中。核心步骤如下:

以下是关键代码修改:

# 将 Lightning 模型转换为 TorchScript
self.model = self.model.to_torchscript()
# 设置为评估模式
self.model.eval()
# 进行推理时,像使用原模型一样使用脚本化模型
prediction = self.model(input_tensor)

只需添加几行代码。别忘了将模型设置为评估模式(eval),然后转换为 TorchScript。进行推理时,可以像使用原始模型一样使用脚本化模型。如果需要进行大量推理调用,这个步骤是值得的。我们也可以将脚本化模型保存到磁盘。

构建 Flask Web 服务器 🌐

上一节我们介绍了如何加速模型,本节中我们来看看如何构建一个 Web 服务。

我们将使用 Flask 库来搭建一个 Web 服务器并提供预测服务。Flask 是 Python 中标准的 Web 服务器库。在实际生产部署中,可能会选择更现代的 FastAPI,它支持异步和类型提示,性能可能更高。但 Flask 作为行业标准之一,足以满足本实验的需求。

运行服务器的主文件是 api/server/app.py。它是一个 Flask Web 服务器,用于提供段落文本识别预测。

以下是该应用的核心结构:

  • 导入与初始化:导入 Flask,初始化应用和模型,并设置日志。
  • 路由:通过装饰器定义 URL 端点。
    • @app.route(‘/‘):根路径,返回 “Hello World”。
    • @app.route(‘/v1/predict‘):预测路径,同时支持 GET 和 POST 请求。这是版本化 API 的良好实践。
  • 预测逻辑
    • 加载图像(POST 请求从 JSON 中读取 base64 编码图像;GET 请求从 URL 查询参数 image_url 读取)。
    • 使用模型进行预测。
    • 收集并打印统计信息。
    • 将预测结果以字符串形式返回。

启动服务器后,我们可以通过另一个终端标签页发送请求进行测试。

以下是发送请求的示例命令:

# 发送 POST 请求(base64 图像)
cat tests/support/example_image_base64.txt | curl -X POST -H “Content-Type: application/json“ -d @- http://localhost:8000/v1/predict

# 发送 GET 请求(图像 URL)
curl “http://localhost:8000/v1/predict?image_url=https://example.com/image.jpg“

两种方式都很常见,同时支持它们会很有用。测试完成后,可以按 Ctrl+C 关闭服务器。

我们还可以为 Web 服务器编写测试,确保其按预期工作。测试文件位于 api/server/ 目录下,可以像测试其他代码一样运行它们。

使用 Docker 容器化应用 📦

上一节我们构建了本地 Web 服务,本节中我们来看看如何将其标准化部署。

我们现在已经可以部署应用了。但为了确保生产环境的一致性并简化部署,我们将应用及其所有依赖打包到一个 Docker 镜像中。Docker 提供了一种轻量级、快速的虚拟化方式。

首先,将仅用于生产环境的依赖复制到 requirements.txt 中。然后,使用 api/server/Dockerfile 来构建镜像。

以下是构建和运行 Docker 镜像的命令:

# 构建 Docker 镜像
docker build -t text-recognizer-api-server -f api/server/Dockerfile .

# 运行 Docker 容器
docker run -p 8000:8000 -it --rm text-recognizer-api-server

Dockerfile 基于官方的 Python 3.6 镜像,执行了以下步骤:安装系统依赖、设置工作目录、安装 Python 依赖(将 CUDA 版本的 PyTorch 替换为 CPU 版本以减小镜像)、复制应用代码、暴露端口、设置环境变量,并指定容器启动命令。

Docker 使用层缓存机制。将代码复制步骤放在依赖安装步骤之后,可以充分利用缓存,加快后续构建速度。

运行容器后,我们可以使用与之前完全相同的 curl 命令来测试 API,因为我们已经将容器的 8000 端口映射到了本地主机的 8000 端口。

拥有这个 Dockerfile 后,我们就可以将应用部署到多种平台,例如 Render.com,只需几分钟即可完成。

准备无服务器函数部署 (AWS Lambda) ⚡

上一节我们将应用打包成了容器,本节中我们来看看另一种流行的部署方式。

除了作为常驻 Web 服务器运行,我们还可以将应用部署为 无服务器函数。AWS、GCP 和 Azure 都支持这种部署方式。这里我们将以 AWS Lambda 为目标。

相关代码位于 api/serverless/ 目录。app.py 文件的结构与 Flask 应用类似,但遵循 Lambda 的规范。

以下是 Lambda 处理函数的核心逻辑:

def handler(event, context):
    # 从 event 中获取 image_url
    image_url = event.get(‘image_url‘)
    # 读取图像并进行预测
    prediction = model.predict(image_url)
    # 返回结果
    return {‘prediction‘: prediction}

我们在模块级别加载模型,这样每次函数调用时都可以复用。handler 方法是 Lambda 的入口点,它接收 eventcontext 参数。我们从 event 中获取图像 URL,进行预测,然后返回结果。

api/serverless/Dockerfile 基于 AWS 提供的官方 Python 3.6 Lambda 镜像构建,安装依赖的方式与之前类似。最后,它指定启动命令为 app.handler

构建并运行此 Docker 镜像后,我们可以使用一个特定的 curl 命令来本地测试 Lambda 函数。

以下是本地测试 Lambda 容器的命令:

# 构建 Lambda Docker 镜像
docker build -t text-recognizer-lambda -f api/serverless/Dockerfile .

# 本地运行 Lambda 容器(使用 AWS 提供的运行时接口模拟器)
docker run -p 9000:8080 -it --rm text-recognizer-lambda

# 在另一个终端发送测试请求
curl -X POST “http://localhost:9000/2015-03-31/functions/function/invocations“ -d ‘{“image_url“:“https://example.com/image.jpg“}‘

Lambda Docker 镜像大小最多可达 10GB,足以容纳许多大型模型。我们可以轻松地将其与 AWS 服务集成,例如,配置为当图像上传到 S3 时自动触发 Lambda 执行,或者在其前面放置一个轻量级的 API 网关(如 Amazon API Gateway)以提供 RESTful 接口。我们将在下一个实验中设置基本的监控。

总结

本节课中我们一起学习了完整的 Web 部署流程。

我们首先使用 TorchScript 加速了训练好的模型。接着,我们使用 Flask 构建了一个简单的 Web 服务器,并通过 curl 命令本地测试了其 API。然后,我们创建了一个 Dockerfile,将整个应用及其依赖打包成容器镜像,实现了环境标准化。最后,我们调整了应用代码和 Dockerfile,为部署到 AWS Lambda 无服务器平台做好了准备。

通过本实验,你掌握了将机器学习模型从本地开发推向可部署的 Web 服务的关键步骤。在接下来的实验中,我们将实际完成云端部署。

🧠 机器学习团队管理教程 P27:L13

在本节课中,我们将要学习如何构建和管理一个高效的机器学习团队。我们将探讨团队中的不同角色、团队在组织中的定位、管理机器学习团队的特殊挑战,以及招聘机器学习人才的策略。


🤔 为什么需要讨论机器学习团队?

为什么我们要将机器学习团队作为本课程的一部分来讨论?这与构建可工作的机器学习系统有什么关系?

管理任何技术团队都具有挑战性。招聘优秀人才、管理团队并促进其成长、确保团队产出符合预期、做出明智的长期技术决策、管理技术债务以及应对领导的期望,这些对于任何技术团队来说都是难题。

然而,机器学习为这些挑战增加了额外的复杂性:

  • 机器学习人才通常昂贵且稀缺。
  • 需要多种不同的角色协同工作才能使机器学习项目成功。
  • 机器学习项目通常具有很高的不确定性,使得管理产出更加困难。
  • 机器学习领域发展迅速,技术债务问题尤为突出。
  • 在许多组织中,领导层可能并不真正理解人工智能及其与常规软件开发的差异,这使管理工作更具挑战性。

即使你目前不管理团队,了解这些内容也能帮助你理解管理者如何构建和管理机器学习团队。此外,本课中的许多建议也旨在帮助你如何在机器学习领域找到工作。


👥 机器学习团队中的角色

首先,我们将介绍机器学习组织中存在的不同角色,以及每个角色所需的技能。

以下是机器学习项目中常见的一些角色:

  • 机器学习产品经理
  • DevOps工程师
  • 数据工程师
  • 机器学习工程师
  • 机器学习研究员
  • 数据科学家

你可能会问,这些角色之间有什么区别?它们如何协同工作来构建系统?

以下是每个角色的简要说明:

  • 机器学习产品经理:负责与机器学习团队合作,帮助确定项目优先级并推动执行。他们的工作成果通常是设计文档、线框图和项目计划。
  • DevOps工程师:负责部署和监控生产系统。他们的工作成果是最终部署的机器学习系统。
  • 数据工程师:负责构建数据管道,包括数据的聚合、存储和监控,这些数据用于创建机器学习系统。他们本质上是在构建分布式系统。
  • 机器学习工程师:通常负责训练和部署预测模型本身。他们的工作成果是在生产环境中处理真实数据的预测系统。他们不仅使用TensorFlow等框架,也使用Docker等工具来将系统产品化。
  • 机器学习研究员:也训练预测模型,但这些模型通常更具前瞻性、探索性,或与机器学习工程师紧密合作将其产品化。他们产出一个模型和一份描述该模型的报告,但通常不负责部署到生产环境。
  • 数据科学家:这是一个涵盖性术语。在某些组织中,它可以指代上述任何角色;而在另一些组织中,它可能更像商业分析师,通过运行SQL查询来制作仪表板,回答关键业务问题。因此,看到这个术语时需要深入了解其在特定组织中的具体含义。

📊 不同角色所需的技能组合

接下来,我们来看看这些不同角色所需的技能组合。我们可以从两个维度来考虑:软件工程技能机器学习专业知识

以下是不同角色在这两个维度上的大致定位:

  • ML DevOps工程师:需要非常高的软件工程技能,对机器学习只需基本了解。他们通常来自传统的软件工程背景。
  • 数据工程师:需要很高的软件工程技能,并开始需要一些机器学习基础知识,因为机器学习团队是他们重要的“客户”。
  • 机器学习工程师:位于中间,既需要大量的机器学习技能和经验,也需要扎实的软件工程基础。这是比较罕见的组合,通常来自有软件工程经验并自学机器学习的人,或拥有理工科博士学位并接受了软件工程训练的人。
  • 机器学习研究员:他们是机器学习专家,通常拥有计算机科学或统计学的硕士或博士学位,或在工业研究项目中受过训练。
  • 数据科学家:背景非常广泛,从本科数据科学专业到物理学博士都有可能。
  • 机器学习产品经理:这是一个新兴角色,通常来自传统产品管理背景,但对机器学习流程有较多接触;或者是从机器学习领域转行而来。

在初创公司中,通常更倾向于雇佣能做多种事情的“通才”角色,比如机器学习工程师。专门的MLPM或ML研究员角色在初创公司中较少见,除非是自动驾驶等以机器学习为核心的公司。DevOps角色在初创公司中也相对少见,因为随着产品和工程团队复杂度的增加,这个角色才变得关键。数据工程则是初创公司应该尽早投资的领域。


🏢 机器学习团队在组织中的定位

上一节我们介绍了机器学习团队中的不同角色,本节中我们来看看机器学习团队本身在更广泛的组织环境中是如何定位的。

通过与许多从业者交流,我们发现目前对于如何构建机器学习团队的最佳结构尚未达成共识。不同的组织有不同的实践,不同的结构似乎适用于不同类型的组织。本节的目标是根据我们观察到的情况,提供一个基于组织成熟度的最佳实践分类。

我们可以用“攀登机器学习组织之山”这个比喻来描述不同阶段。

🏔️ 山脚:初生的机器学习组织

  • 特征:组织内无人或仅有少数人以临时方式从事机器学习工作,内部机器学习专业知识很少。
  • 典型组织:硅谷科技公司和财富500强公司之外的大多数中小型企业,尤其是在技术不那么前沿的行业。
  • 优势:对于希望加入此类组织的人来说,可能存在很多容易实现的目标,可以产生巨大影响。
  • 劣势:对机器学习项目的支持很少,组织可能不相信机器学习,招聘和留住优秀人才困难。

🧗 山腰阶段一:机器学习研发团队

  • 特征:公司对机器学习感到好奇,开始进行一些研发,有一些概念验证项目。机器学习工作通常集中在一个较小的团队中,隶属于研发部门。
  • 典型组织:一些采用技术较慢的大型行业,如石油天然气、制造业、电信公司。
  • 优势:可以雇佣经验丰富的研究员;团队有 mandate 去研究更长期、可能带来更大收益的业务重点。
  • 劣势:从组织其他部门获取数据可能非常困难;这种模式下的努力很少能转化为真正的商业价值。

🧗 山腰阶段二:嵌入产品团队的机器学习专家

  • 特征:有机器学习背景的个人被嵌入到不同的业务部门或产品线中,他们通常向同一个工程负责人汇报。
  • 典型组织:许多中小型或高增长的软件技术初创公司,以及金融科技公司。
  • 优势:机器学习专家的改进很可能带来真正的商业价值;想法与实际产品改进之间有紧密的反馈循环。
  • 劣势:难以招聘和培养顶尖人才;获取计算资源可能滞后;机器学习项目周期(高风险、不确定性)难以融入工程冲刺和规划周期。

🧗 山腰阶段三:独立的机器学习职能部门

  • 特征:一个集中化的机器学习组织,可能向高级领导层甚至CEO汇报。拥有MLPM、研究员和工程师,与内部客户合作构建机器学习产品。
  • 典型组织:大型金融服务公司、大银行,以及谷歌、Facebook、Uber之外的一些大型科技公司。
  • 优势:创建了机器学习卓越中心,人才密度高;由于汇报层级高,可以克服数据获取等障碍;可以投资于集中化的工具和基础设施。
  • 劣势:机器学习专家不嵌入产品团队,需要将模型移交给实际使用的团队,这可能具有挑战性;反馈周期可能更长。

🏔️ 山顶:机器学习优先的组织

  • 特征:整个组织都坚信机器学习的重要性。既有中央机器学习部门负责长期项目,也有机器学习专家嵌入各业务线,负责快速迭代。
  • 典型组织:谷歌、Facebook、Uber等大型科技公司,以及一些以机器学习为核心的初创公司。
  • 优势:数据获取便利;有利于招聘;产品团队具备机器学习思维;有团队投资于部署所需的基础设施。
  • 劣势:实施难度大;难以招募足够的人才;在文化上确保组织每个人都具备必要的机器学习理解水平具有挑战性。

⚙️ 机器学习团队的结构设计选择

在构建团队结构时,机器学习组织需要做出一些设计选择:

  • 软件工程与研究的平衡:团队在多大程度上负责构建和集成软件,还是仅仅交付模型?
  • 数据所有权:团队对数据收集、存储、标注和管道构建有多少控制权?
  • 模型所有权:团队是否负责将模型部署到生产环境?谁负责维护生产中的模型?

随着组织在“山”上攀登,专业化程度越来越高,机器学习团队对数据和模型的控制权也越来越大。


🧭 管理机器学习团队的特殊挑战

管理机器学习团队比管理传统软件团队更具挑战性,核心原因在于:通常很难提前判断机器学习任务的难易程度

机器学习项目的进展往往是非线性的,常见的情况是项目在数周甚至更长时间内完全停滞,没有任何可衡量的性能改进。因此,规划项目时间线极其困难。

此外,机器学习团队需要与工程团队协作,但这两个领域之间往往存在文化差异(不同的价值观、背景、目标和规范)。在有毒的文化中,双方可能互不尊重。

另一个挑战是,在许多组织中,领导者并不真正理解机器学习,他们可能不知道什么是可行的,或者对时间线的不确定性缺乏认识。


✅ 管理机器学习团队的最佳实践

虽然没有万能解决方案,但以下是一些来自优秀管理者的见解:

1. 采用概率性项目规划
不要使用传统的瀑布式规划,而是为机器学习项目分配成功概率。将项目视为一个投资组合,避免有单一关键路径的研究项目。可以并行尝试多种方法,或在团队内部进行友好的创意竞赛。

2. 基于投入而非结果衡量成功
在进行绩效管理时,重要的是评估团队成员执行尝试的好坏,而不是纠结于谁的想法最终成功了。在长期内,做出有效成果固然重要,但在单个项目上,执行过程是关键衡量标准。

3. 促进研究员与工程师紧密合作
常见的失败模式是过分强调工程而忽视研究,或者相反。两者需要紧密协作,平衡发展。

4. 快速构建端到端可工作的原型
这不仅能提高任务成功率,还能更好地向领导层沟通进展,因为有明确的指标和可展示的成果。

5. 教育组织领导层
机器学习团队有责任帮助领导层理解机器学习时间线的不确定性。避免过度乐观、炒作式的沟通,应清晰地传达风险和不确定性。


💼 机器学习人才招聘

最后,我们来谈谈招聘。我们将从招聘方和求职方两个角度来探讨。

📈 AI人才缺口

目前,机器学习人才市场供需紧张。据不同估计,全球具备相关技能的人才数量在数万到数十万之间,而全球软件开发者数量高达数千万。这导致了激烈的人才竞争,顶尖研究员的薪酬可达七位数。

🎯 如何寻找机器学习人才(招聘方视角)

对于核心的机器学习工程师和研究员角色,常见的错误是寻找“全能独角兽”——要求候选人同时具备顶尖的软件工程和机器学习研究能力。

更有效的方法是:

  • 招聘路径多样化:主要招聘软件工程技能强、有机器学习学习意愿的人进行培养;或招聘更多有机器学习背景的初级人才。
  • 明确具体需求:并非每个机器学习工程师都需要所有技能。
  • 针对研究员角色:更看重论文质量而非数量;寻找研究重要问题的研究员;考虑有业界经验的研究员;关注物理、统计等相邻领域的顶尖人才;考虑非传统学术背景的人才。

人才来源:除了标准渠道,可以关注顶级会议论文,联系感兴趣论文的第一作者;寻找优秀的论文复现代码作者;在机器学习研究会议上进行招聘。

🎣 如何吸引顶尖人才

顶尖人才通常看重:

  • 使用前沿工具和技术。
  • 在快速发展的领域构建技能和知识。
  • 与优秀的人共事。
  • 处理有趣的数据集。
  • 从事有实际影响力的工作。

相应地,公司可以:

  • 投资前沿项目并公开成果(论文、博客)。
  • 建立以学习为导向的团队文化(阅读小组、学习日、会议预算)。
  • 帮助团队成员建立个人品牌(发表博客、论文)。
  • 展示独特且有技术挑战的数据集。
  • 强调公司使命和机器学习能带来的巨大影响。

👨💻 机器学习面试流程

机器学习面试流程不如软件工程面试那样标准化。常见的评估类型包括:

  • 背景和文化契合度面试。
  • 白板编码/结对编程。
  • 结对调试:查看有bug的机器学习代码并修复。
  • 数学谜题(如线性代数)。
  • 带回家项目。
  • 应用机器学习评估:针对一个问题,解释如何用机器学习解决。
  • 深入探讨过往项目。
  • 机器学习理论问题(如偏差-方差权衡)。

🔍 如何找到机器学习工作(求职者视角)

除了标准渠道,可以关注机器学习研究会议。由于人才竞争激烈,公司对直接联系持更开放的态度。

如何脱颖而出

  • 具备基本的软件工程技能和相关工作经验。
  • 展示对机器学习的兴趣和知识:撰写综合研究领域的博客,或解释新兴主题。
  • 证明完成机器学习项目的能力:拥有个人项目、课程项目或论文复现。
  • 对于研究导向:赢得Kaggle比赛、发表论文以证明创造性思维。

面试准备

  • 复习机器学习理论和基础算法。
  • 同时准备常规的软件工程面试,因为许多公司也会测试这方面的能力。

📝 总结

在本节课中,我们一起学习了构建和管理机器学习团队的各个方面。

关键要点总结

  1. 角色多样性:生产级机器学习涉及多种技能,贡献方式多样。你可以在软件工程或机器学习任一维度深入,也可以两者平衡,都能找到用武之地。
  2. 组织定位:机器学习团队在组织中的结构没有唯一正确答案,但存在一个从“初生”到“机器学习优先”的成熟度演进路径。
  3. 管理挑战:管理机器学习团队的核心挑战在于项目的不确定性和非线性进展。采用概率性规划、基于投入衡量、促进研-工协作是有效实践。
  4. 招聘策略:市场人才竞争激烈。招聘方应明确需求、多样化招聘路径。求职者应通过项目(如本课程项目)来证明自己的能力,这是进入该领域的最佳途径之一。

希望本教程能帮助你更好地理解机器学习团队的运作,并为你的职业发展提供指导。

📚 课程 P28:期末项目前10名展示教程

在本节课中,我们将一起学习“全栈深度学习”课程中评选出的前10个优秀期末项目。这些项目涵盖了从数据生成、模型训练到系统部署的完整流程,是学习如何构建实际机器学习应用的绝佳案例。

我们将逐一解析每个项目的核心目标、技术方案和关键成果。请注意,教程内容已根据要求进行整理:删除了所有语气词,采用Markdown格式,保留了原文每一句话的核心含义,并为标题配上了合适的Emoji,核心概念使用公式代码描述。


🤖 项目一:人工漫画面板数据集

感谢大家抽出时间参加这次项目展示会。

我是这门课程的助教,在春季学期与Joshua、Sergey等人一起工作非常愉快,能够看到大家最终的项目成果。

今天我们将浏览被评为前10%的优秀解决方案之一:由Ashram Sunny创建的“人工漫画面板数据集”。

这个项目包含两个主要部分。

(屏幕共享显示问题,音频设置调整中……)

感谢大家的参与。

现在回到正题,这是关于“人工漫画面板数据集”的最终四步深度学习项目。

我决定开始这个项目是出于我的个人热情,因为我热爱漫画,但同时面临一个问题:我无法阅读日文。

我计划在未来学习日语。在寻找翻译解决方案时,我发现谷歌翻译在本地化日文文本方面效果不佳,并且没有提供免费的OCR翻译方案,让我能够批量处理数百页漫画并获得尚可的翻译结果。

当然,使用机器翻译将日语翻译成英语是困难的,但至少我能得到一些可理解的内容。

因此,我决定开始构建一些机器学习技术,比如一个检测对话气泡的目标检测系统,以及检测文本行的系统。

在检测日文文本时,我面临的问题是缺乏免费、公开可用的数据集来训练我的模型。所以我决定自己创建一个。

这是我的数据集生成器可以创建的样本,在我看来它看起来相当逼真,对话气泡在一定程度上模仿了真实漫画。

它的工作原理是:首先,我收集了各种原始材料来创建这个数据集,包括196种不同字体(覆盖80个字符)。

(提及斯坦福大学免费提供的日英句子对数据集,本项目仅使用其日语部分。)

以及来自Kaggle的约30万张插图,这是另一个我使用的数据集。

还有91个我手动制作的不同对话气泡。我下载了所有这些材料,将它们组合在一起。

布局引擎的工作方式是:它成功地在树状结构中创建子对象,以渲染出像这样的合理漫画面板。

以中间面板为例,它有一个顶级子对象,被随机不均等地划分为两个子对象,这种不均等性在生成过程中随机决定。

每个面板内部都随机裁剪并插入了一张来自30万张图片库的插图,并关联了0到2个围绕其渲染的对话气泡。

所有这些对象的信息都被转储到一个JSON文件中,随后用于并行渲染成图像。

除此之外,由于这花费了大量时间,我还为此数据集创建了一个非常快速的Faster R-CNN目标检测基线模型。

我稍后会展示结果。但在展示之前,需要说明的是这个生成过程相当快。

例如,如果你想生成一千个面板,在加载文件后只需要几秒钟就能开始运行,生成元数据大约需要几秒钟,生成图像大约需要一分钟。

我不确定它现在是否能正常工作,我将在下一个视频中尝试展示结果。

让我们快速看一下数据流。

(演示生成过程……)

快速继续我们之前的内容,这是我执行渲染1000个面板元数据的无延迟版本。因为我现在正在我的MacBook上录制,所以可能会比较慢。

与此同时,我可以向大家展示我的漫画页面扫描和对话气泡识别结果,该模型是在这个人工数据集上训练的。

我从网上下载了这部漫画,发现很多情况下模型会感到困惑,但它并非没有学习对话气泡的表征。

正如你在这些图片中看到的,它找到了它们,但也会被类似人脸的东西混淆,并且绝对没有足够的覆盖率让我称其为有效方案。

所以,这是一个问题多多的项目。

非常感谢大家的聆听,祝你们有美好的一天。

(确认音频正常……)

是的,一切听起来都很棒,很棒的项目。作者实际上……

那么,你打算继续研究它吗?还是暂时到此为止?

我正在休息,因为它花费了太多时间。我正试图掌握Manga109数据集,并希望将其用作可以增强我数据流水线的东西,希望能产生非常好的结果。


🩺 项目二:乳腺癌检测系统

下一个项目是Hannah Harris和Donald Hand的“乳腺癌检测系统”。

大家好,我是Harish,今天我将与大家谈谈我和另一位学生Daniel Hen在这门课程中合作的项目。

我们合作的项目叫做“Scancer”,是一个乳腺癌检测助手。

乳腺癌每年夺去许多生命,但事实证明,如果能够及早发现疾病,生存几率会大大提高。

仔细观察用于诊断疾病的各种测试和程序,如乳房X光检查和活检等,你会发现很多这些程序都涉及相当多的人类专业知识。

而这种人类专业知识在世界上不太可能公平分布。同时,事实证明,实际发生的行为是人类专家在视觉检查某种扫描图像。

无论是乳房X光片还是MRI,最终结果通常是一个人盯着某样东西,试图诊断发生了什么。

事实证明,基于深度学习的计算机视觉正是解决这个问题的好方法。过去几年,来自不同团体的众多论文试图展示深度学习如何帮助解决这个问题的不同方面。

但问题在于,没有一个中心位置可以让你去获取,比如说,用于进一步分析的最佳模型。这不容易获取,而且即使有,也不是最终专家(例如放射科医生)可以轻松使用的形式。

这正是Scancer项目试图做的事情。我们的目标是建立一个涵盖乳腺癌检测不同方面的深度学习模型集合。

我们设想这是一个模型存储中心,模型用PyTorch编写,模型存储使用TorchServe。所有这些都包装在一个API中,同样由TorchServe提供支持。最后,整个系统以一个面向最终用户的应用程序形式呈现,例如病理学家可以登录并做一些有用的事情。这就是Scancer项目的目标。

我描述的是一个相当开放和长期的目标。为了缩小我们的焦点,我们决定只专注于一件事:淋巴结活检扫描的分析。

这意味着什么?判断一个人是否患有癌症的众多方法之一是采集淋巴结组织样本,然后以某种方式染色或处理,在显微镜下观察。

最终会得到类似这样的东西:一张所谓的玻片扫描图,专家查看它并将某些区域标记为可能存在转移性生长的问题区域。

这就是我们选择关注的问题。我们关注的原因之一是存在良好的数据,这来自一个名为Camelyon 2016和2017的挑战赛。

但这个数据集的问题是每张图像都大得离谱,比如可能有数十万像素宽,而且实际文件大小非常大,每张文件都有许多GB。因此,即使打开其中一张并对其操作也是一个难题。

我们最终做的是使用一个更简单的数据集,称为PCam数据集。它将这个大文件切分成许多96x96像素的小块,本质上此时变成了一个二分类问题:每个小块,我们都有一个数据集说明这是否是转移组织。

我的合作者Daniel Hen详细介绍了我们如何使用这些数据来训练一个本质上的二分类问题。我将假设我们已经有了这样一个模型。

一旦我们有了这样一个模型,我们称之为PCam分类模型,它看起来像经典的卷积神经网络模型。我们将其存储在我们的模型存储中。

在将其存储到模型存储之前,我们必须将其转换为特定的格式,称为模型归档文件,其中包含模型的权重和一个称为处理程序的东西,处理程序本质上是模型在生产中使用前后必须进行的预处理和后处理。

当所有这些都完成后,你可以将这个模型文件(一个.mar文件)推送到模型存储。

在这个演示中,我将展示模型已经在模型存储中。我们来看看它。这是我们模型存储中当前所有模型的列表,目前只有一个模型,即PCam分类模型。

你可以查看一些细节,例如模型的上传日期、为其服务的两个工作线程、工作线程的启动时间、是否使用GPU等。这样我们就知道这个模型已经以合适的格式归档并上传到网络。请注意,我现在是在实时操作,模型存储服务器位于云端的某个地方,支持SSL等。

一旦这个东西放在云端,我们就可以通过API访问它。这里我将展示两个测试文件。我们有两个测试文件,一个类别为0(无癌),一个类别为1(有癌)。你可以将这个文件通过网络发送到我们的API端点,同样支持SSL。我发送test0,返回0;发送test1,返回1。所以我们有了一些相当不错的东西:一个可以对这个小块数据集进行二分类的模型。

这对于在shell中工作来说很棒,但我们还有一个Django应用程序位于所有这些东西之上,你可以在其中做与shell中相同的事情。

你在这里看到的这个应用程序是实际工作的东西和更多用于从特定利益相关者那里收集用户反馈的演示性内容的结合。我们稍后会谈到这一点。

回到我们在shell中所做的,应用程序可以做同样的事情,例如告诉你模型正在被存储、模型正在以这些不同参数提供服务等。此外,你实际上可以在应用程序本身中测试它。

我提供了一些示例文件,我甚至可以从我的硬盘中选择相同的文件,通过网络发送它们。这个文件应该被检测为有癌,它显示“是,测量区域有转移组织”。你可以对另一个文件做同样的事情,模型仍然有效,但会告诉你相反的结果。

很好,所以我们有了模型,我们将其归档,放入模型存储,通过API提供服务,并且还通过Django应用程序进行了包装。

我们试图解决的实际问题不是这些小块,而是这个整体图像,我们试图判断它是否癌变。这里有一些我仅为演示目的上传的示例玻片。我将进入其中一个。这就是玻片的样子。

正如我之前所说,每张玻片都有数GB大,所以你甚至能在屏幕上看到它是通过类似谷歌地图的界面实现的:当我双击、尝试放大和移动到不同区域时,系统正在通过网络发送大量较小的图像。你可以在我的网络标签页中看到,当我点击不同区域时,有大量图像通过网络传输。

因此,我们的预期是,人类专家会进入这个系统,查看这个,然后做出某种诊断。但为了帮助他们,我们尝试使用那些小块。

就像在我们课程的文本识别器问题中,我们让模型在段落和句子上滑动一样,我们在这里做了同样的事情:我们让模型在这个更大的组织样本上滑动。不幸的是,因为这个图像很大,所以需要很长时间。我们离线完成了这个操作,生成了一个热图,你可以下载这个热图。

它会在我的硬盘上的某个地方。注意它非常大,所以我不会打开它。在这次演示之前,我下载了文件并将其重新采样到原始大小的十分之一到二十分之一,这样我就可以打开它而不会在录制视频时使电脑崩溃。

如果你看,某些区域被高亮显示为潜在问题区域。我再放大一点,你可以开始看到与我们分类器对应的原始方块。在这种情况下,我取消了二分类的嵌套,只取了Sigmoid输出(范围从0到1),这样你可以看到比只有0和1更好的颜色集。

回到这张玻片,你会发现它指出了一些有趣的东西:这里细胞的分布和差异,这可能是有问题的。

因此,我们的目标是,当扫描进入我们的系统时,它们可以被批量离线处理,被分类为可能有问题或没有问题。这样,当专家病理学家登录时,他们可以只说“显示所有高响应区域”,然后只关注那些对他们重要的部分。这样,如果他们一天的时间有限,就可以只关注重要的部分。

这涵盖了到目前为止的整体故事。我们未来的方向是,为了从实际专家那里获得更多输入,我们正在构建一个合适的登录页面等,以尝试引起病理学家的兴趣,看看他们对我们的工作有什么看法,是否有助于他们的工作流程,我们是否偏离了方向,或者我们实际上是否有用。根据他们的反馈,我们将尝试调整我们在深度学习方面的工作以满足他们的需求。

我将回到这个演示,展示一些我们正在尝试的、尚未完全工作但可能令人兴奋的东西。

注意,我之前告诉过你,当我进入不同区域时,它正在生成图像。那么,为什么不尝试实时对图像进行分类呢?我要做的是切换到一个分支并重启系统,现在它将尝试做同样的事情。

但当我重新加载这个图形时,请注意它正在尝试做什么:当它加载不同的块时,它正在通过分类系统实时传递它们,并尝试实时标记出这个区域是潜在问题区域,其周围的区域是绿色的。但这也需要一点时间,所以这是我们正在进行的工作的一部分。

回到正题,我们正试图向领域专家演示这个,看看他们是否喜欢。如果我遗漏了什么,请不要担心,我们在这里所做的一切都是开源的,可以在GitHub上找到。我们将其方便地拆分到不同的代码库中:API、Web应用程序本身、一个进入我们模型存储的示例模型以及一堆方便的配置脚本。如果你对任何事情感兴趣,比如如何设置TorchServe、如何启动Django、如何用SSL终止等方式包装所有东西,所有这些都详细记录在案。

我非常兴奋,也非常感谢这门课程给了我开始这个项目的动力,因为我非常想看看它未来会走向何方。我也非常兴奋地想看看你们为项目做了什么。

(讨论与反馈……)

是的,就像你说的,它就像一个登录页面,你可以去看看。我认为Harish也在Zoom上。干得真漂亮,谢谢,令人印象深刻。

你们俩都是在全职工作的同时做这个的吗?是的。

你们花了多长时间?我请了一些假来专注于这个。我也是,我们实际上不得不请一些假来完成这个。

你打算全职追求这个吗?还是在考虑中?我在考虑,这取决于我是否能……我们正试图将其定位为专家可能感兴趣的东西。如果有明确的兴趣,那么我将尝试筹集资金等。

很好。你在寻找贡献者吗?有贡献指南或开放问题列表吗?因为我认为通话中的很多人可能都受到启发想以某种方式贡献。

欢迎所有人。我还没有花时间写一个,但计划很简单:计划是建立一个与这个特定问题相关的模型动物园。任何愿意获取最先进的论文并帮助我们实现它们的人,对项目来说都将是极大的帮助。

很好。然后Adam举手了,你有问题吗?是的,我有几个问题。第一个是,你如何决定在哪里划清界限,比如提醒某人这个和那个?我认为这很重要,这可能不是一个简单的逻辑回归的是非问题。可能需要告知临床医生一个范围:这个需要观察,这个需要活检,或者这个需要立即用链锯切除,否则病人会在20分钟内死亡。

我没有一个明确的答案,因为我们确实不知道。但整个重点是让多个专家接入系统,每个专家都可以做他们通常做的事情,深度学习计算机视觉部分只是辅助指导这个过程,它们并不真正为你做最终决定。它更倾向于相对分级:如果有多个扫描进来,它会告诉你其中一些比其他具有更高的优先级,它不一定告诉你那个好那个坏。

考虑到时间,让我们继续讨论其他项目。但很多人对此感兴趣,我们可以线下讨论。很棒的工作,谢谢你们。


🧬 项目三:Kaggle单细胞分类竞赛

下一个项目是Darek Kleczek,他正在处理Kaggle单细胞分类竞赛。

大家好,我是Darek Kleczek,我想谈谈我的期末项目,该项目将数据集调优应用于最近的Kaggle单细胞分类竞赛。

我的项目灵感来自Andrew Ng最近的一次演讲,他谈到了数据的重要性以及以数据为中心的方法来提高模型性能。我想将这种方法论和思维方式应用到一个Kaggle竞赛中。

该竞赛的目标是对单个人类细胞进行分类。问题在于困难,因为我们没有单个细胞的标签,只有包含一堆细胞的图像的标签,因此这被定义为弱监督学习。

最终结果令我非常满意,我们获得了金牌,我们的团队在700多个团队中排名第六,我为此感到非常自豪。

我实际上应用了许多以数据为中心的方法来解决这个挑战,其中一些效果非常好。一个例子是将数据分割成训练集和验证集(或交叉验证集),以避免集合之间的泄漏。为此,我应用了主题建模技术(通常应用于文本)。

我们从文本嵌入开始,使用UMAP进行降维,然后使用DBSCAN对文本进行聚类,产生一组彼此接近的主题。我将此应用于图像,将图像簇视为组,并以一种方式划分数据集,使得每个组始终在同一个折叠中。这帮助我们避免了泄漏。

还有其他以数据为中心的干预措施,例如伪标签、尝试识别和修复标签错误。但在这个挑战中,我发现问题是,我自己不是专家,无法比模型更好地正确标记细胞。因此,我想在这个挑战中使用的这些方法效果有限。

然而,我们也发现模型很重要,尤其是在这个竞赛中,由于弱标签,它非常特殊。我们发现,开发基于对卷积神经网络工作原理理解的以模型为中心的方法,使我们能够提高分数。

一种方法是使用单细胞的掩码来预测单细胞类别,我们称之为GAP掩码方法。另一种方法是以一种与整个图像一致的方式转换单细胞图像,然后使用图像级模型。这些以模型为中心的干预措施实际上显著帮助我们提高了竞赛分数。

我们还发现,数据和模型仍然不够。我们发现,即使在Kaggle上,适当的工程也超级重要。推理速度至关重要,我们需要进行大量干预和加速模型推理,以使其符合Kaggle施加的限制。

拥有清晰的代码使我们能够在团队中更好地协作。确保实验的可重复性至关重要。

最后一套经验是关于多样性的:拥有一个多元化的团队比同质化团队效果更好。最终,我们与一个来自四大洲的团队合作,我们在北美、南美、欧洲和亚洲都有人。我们还发现,当我们结合来自这些不同群体的不同模型时,我们的性能得到了提高。

我的经验是,我仍然坚信以数据为中心的方法,我不想挑战Andrew Ng的观点。然而,我认为它最适用于已确立的任务,如分类或回归,以及在你能够访问可以确定正确标签的专家的情况下。对于像这里这样具有弱标签的任务,或者在专家标注者有限的情况下,模型架构可以发挥重要作用,对我们来说,它确实显著帮助我们提高了模型性能。最终,拥有多元化的团队和多样化的方法对提高性能有很大帮助。

(讨论与反馈……)

是的,其中一些经验肯定与我们课程的内容相关。非常酷。我不知道你是否看到他在Zoom上,但干得好,第六名。第六名真的很扎实,非常非常好,在760个团队中。


📝 项目四:弱监督学习案例研究

接下来的项目是一个关于弱监督学习的案例研究,由Jacques Tibordo、Ariane Pascali和Kevin Quinck完成。

大家好,我是Jacques,我是加拿大能源监管局的数据科学家。这是我们的团队项目视频。和我一起的还有Kevin,他是UiPath的机器学习工程师,以及这位漂亮的伙伴Ariane,他是Citizen Lab的NLP研究员。

我们的项目是关于什么的?我们着手要做的是接收文本数据(任何类型的文本数据),目标是创建一个带标签的数据集。最终目标是让用户登录我们的Web应用程序,然后输入他们的数据,输入他们感兴趣的所有标签,并提供更多关于标签的信息,然后我们的模型将基本上为他们输出一个带标签的数据集。我们试图做一些类似于Snorkel Flow的事情,因此我们将在项目中使用Snorkel。

我们使用的另一种方法是零样本学习。因此,通过弱监督和零样本学习这两种方法,我们处理一个多类分类模型问题,它有文本数据,你试图预测它是否是演员、运动员、书面作品等。

我们在该数据集上训练了一个基线模型,然后我们假装没有标签,这样我们就可以用Snorkel创建一个数据集,然后在用Snorkel创建的数据集上训练。另一种方法是使用零样本学习,它使用师生方法来创建标签,然后让一个学生模型在这些新标签上训练。

有了这两个基线,显然我们无法达到基线水平,因为基线有大约50万个示例,已经表现很好。但目标是,在没有标签数据的情况下,我们能有多接近。

工程工作方面,我们最终创建了一个Web应用程序。这个Web应用程序基本上是你给它一段数据,然后我们有我们部署的模型,我们会得到一个响应。你可以在这里看到,对于这段文本,“艺术家”是最可能的类别。我们这里有一个JSON文件。我们对DBpedia数据集做了这个,也对另一个名为毒性数据集的数据集做了。

那么,这样做的意义是什么?想法

📚 课程 P3:【Lab1】设置与介绍

在本节课中,我们将学习如何设置深度学习开发环境,并介绍一个用于手写段落内容识别的完整代码库的渐进式开发计划。我们将从最基础的代码开始,逐步构建一个功能完整的系统。

🎯 问题定义与目标

我们的目标是构建一个名为 Text Recognizer Pro 的服务。该服务能够接收一张手写段落的图像,并自动输出其文本转录。

具体实现流程如下:

  1. 用户通过手机或电脑发送包含图像的 POST 请求到 Web 后端。
  2. 后端加载图像,并检测其中的文本行。
  3. 将每个文本行图像输入预测模型,模型输出对应的文本。
  4. 系统整合所有行的预测结果,并附带置信度信息,最终将响应返回给调用者。

为了训练这个预测模型,我们需要以下核心组件:

  • 数据:包含文本行图像及其对应转录标签的数据集。
  • 训练代码:用于训练模型的程序。
  • 模型权重:训练完成后保存的、能产生良好结果的参数。

⚙️ 计算环境设置

我们将使用 Google Colab 作为主要的实验环境,它提供了免费的 GPU 资源。以下是设置步骤:

以下是具体操作步骤:

  1. 访问 Google Colab 并登录您的 Google 账户。
  2. 创建一个新的笔记本。
  3. 在菜单栏选择 运行时 -> 更改运行时类型,将硬件加速器设置为 GPU,然后保存。
  4. 在代码单元格中执行以下命令,以克隆代码库并安装依赖:
!git clone https://github.com/your-repo/text-recognizer.git
%cd text-recognizer
!pip install pytorch-lightning
import sys
sys.path.append('.')
  1. 进入 Lab1 目录并运行一个简单的测试脚本,以验证环境是否正常工作:

%cd lab1
!python -c "import torch; print(torch.cuda.is_available())"

🏗️ 代码库结构概览

我们的项目采用渐进式开发,每个实验(Lab)都会在前一个的基础上增加新功能。Lab1 包含了最基础的代码结构,主要分为两大部分:text_recognizertraining

text_recognizer

这是我们开发的核心 Python 包,旨在实现从文本图像到转录文本的功能。它包含以下子模块:

  • data/:负责与数据交互的代码。例如,MNISTDataModule 类负责下载 MNIST 数据集、进行预处理(如转换为张量和归一化)、以及划分训练集、验证集和测试集。它继承自 BaseDataModule,后者又继承自 PyTorch Lightning 的 LightningDataModule,提供了标准的数据处理接口。
  • models/:包含神经网络模型的定义。例如,MLP(多层感知机)模型,其结构可以用以下伪代码表示:
    class MLP(nn.Module):
        def __init__(self, input_dim, hidden_dim1, hidden_dim2, output_dim):
            self.fc1 = nn.Linear(input_dim, hidden_dim1)
            self.fc2 = nn.Linear(hidden_dim1, hidden_dim2)
            self.fc3 = nn.Linear(hidden_dim2, output_dim)
            self.relu = nn.ReLU()
            self.dropout = nn.Dropout()
        def forward(self, x):
            x = x.flatten()
            x = self.relu(self.fc1(x))
            x = self.dropout(x)
            x = self.relu(self.fc2(x))
            x = self.dropout(x)
            x = self.fc3(x)
            return x
    
  • lit_models/:包含 PyTorch Lightning 模块,用于封装训练循环。BaseLitModel 类接收一个模型(如 MLP),并自动设置优化器、损失函数和评估指标(如准确率)。我们只需实现 training_stepvalidation_step 等方法,PyTorch Lightning 便会自动处理设备放置(CPU/GPU)、多 GPU 训练等复杂细节。

training

此部分包含用于驱动训练过程的代码。目前主要是一个 run.py 脚本,它允许我们通过命令行参数灵活指定:

  • 使用的数据类(如 mnist
  • 使用的模型类(如 mlp
  • PyTorch Lightning Trainer 的参数(如使用的 GPU 数量、训练轮数)
  • 模型或数据特有的参数(如 MLP 的隐藏层维度)

🚀 运行训练示例

现在,让我们利用已设置的代码库在 MNIST 数据集上训练一个 MLP 模型。我们将演示如何在单 GPU 和双 GPU 环境下运行训练。

在终端中,进入 lab1 目录,并执行以下命令启动训练:

# 单 GPU 训练示例
python training/run.py \
    --data_class mnist \
    --model_class mlp \
    --max_epochs 5 \
    --fc1 128 \
    --fc2 256 \
    --batch_size 32 \
    --gpus 0

# 双 GPU 数据并行训练示例
python training/run.py \
    --data_class mnist \
    --model_class mlp \
    --max_epochs 5 \
    --fc1 128 \
    --fc2 256 \
    --batch_size 32 \
    --gpus 0,1 \
    --accelerator ddp

执行命令后,PyTorch Lightning 将开始训练过程,并在终端打印损失和准确率日志。同时,它会在后台启动 TensorBoard 来可视化训练过程。使用 watch -n 1 nvidia-smi 命令可以监控 GPU 的使用情况。

📝 总结

本节课中,我们一起学习了深度学习项目 Text Recognizer Pro 的初步设置与代码结构。我们明确了项目的最终目标是识别手写段落,并制定了通过 10 个实验逐步实现该目标的路线图。在 Lab1 中,我们成功设置了 Google Colab 开发环境,熟悉了项目代码库以数据、模型、训练模块为核心的组织方式,并实际运行了一个在 MNIST 数据集上训练 MLP 模型的例子,体验了 PyTorch Lightning 带来的便捷性,例如简单的多 GPU 训练支持。

建议课后尝试修改模型参数(如隐藏层大小、层数、是否使用 Dropout),观察其对训练结果的影响,以加深对代码模块的理解。

P4:【Lab2】CNN 与数据实验教程 🧠📊

在本教程中,我们将学习卷积神经网络(CNN)的基础知识,并使用 EMNIST 数据集进行实践。我们还将创建一个用于文本行识别的合成数据集,为后续的序列处理任务做准备。

实验设置与环境准备 🔧

在开始之前,请确保完成实验环境设置。以下是具体步骤:

  1. 克隆课程代码仓库。
  2. 使用 pip install pytorch lightning 安装必要的依赖库。
  3. 如果您在自己的机器上运行(而非 Colab),请确保拉取最新的代码更新,您将看到新增的 lab2 目录。

认识 EMNIST 数据集 🔤

上一节我们完成了环境设置,本节中我们来了解一下本次实验将使用的 EMNIST 数据集。

MNIST 是著名的手写数字数据集,包含 10 个数字类别。EMNIST(Extended MNIST)是其扩展版本,它不仅包含数字,还包含了字母字符,并以 MNIST 的格式进行了重新打包。

我们将通过代码来查看这个数据集。首先,导入新的数据模块 Emnist,这是一个新的数据类。

from text_recognizer.data import Emnist

以下是数据加载和查看的基本流程:

  1. 实例化数据模块data = Emnist()
  2. 准备数据:调用 data.prepare_data() 下载数据。
  3. 设置数据:调用 data.setup() 将数据分割为训练集、验证集和测试集。
  4. 查看信息:打印 data 对象可以查看类别数量等信息。EMNIST 包含 83 个类别,涵盖数字、大写字母、小写字母以及一些为未来任务预留的基本标点符号。
  5. 数据形状:训练集包含约 260,000 张图像,每张图像的形状为 (1, 28, 28),其中 1 代表灰度图像的通道数。

我们可以通过数据加载器获取一个批次的数据,并检查其数据类型和数值范围,以确保数据已正确归一化(例如,像素值应在 0 到 1 之间)。

test_dataloader = data.test_dataloader()
batch = next(iter(test_dataloader))
print(batch[0].shape, batch[0].dtype, batch[0].min(), batch[0].max())

最后,我们可以绘制一些图像样本及其对应的标签,直观地观察数据。

训练一个基础的 CNN 模型 🚂

了解了数据之后,本节我们将使用一个简单的卷积神经网络模型在 EMNIST 上进行训练。

我们有两种方式进行训练:

  1. 在 Notebook 中直接训练,便于快速检查和调试。
  2. 通过命令行运行训练脚本,这是更正式和可复现的方式。

首先,我们看看模型结构。我们有一个 CNN 模型,其结构类似于经典的 LeNet,但使用了自定义的卷积块。

# CNN 模型结构示例
class CNN(nn.Module):
    def __init__(self, ...):
        super().__init__()
        self.conv_block1 = ConvBlock(...)
        self.conv_block2 = ConvBlock(...)
        self.pool = nn.MaxPool2d(...)
        self.dropout = nn.Dropout(...)
        self.fc1 = nn.Linear(...)
        self.fc2 = nn.Linear(...)

    def forward(self, x):
        x = self.conv_block1(x)
        x = self.pool(x)
        x = self.conv_block2(x)
        x = self.dropout(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = self.fc2(x)
        return x

其中,ConvBlock 本身也是一个 nn.Module,它封装了一个卷积层和一个激活函数。这种模块化设计是 PyTorch 的强大特性之一。

class ConvBlock(nn.Module):
    def __init__(self, ...):
        super().__init__()
        self.conv = nn.Conv2d(...)
        self.act = nn.ReLU()

    def forward(self, x):
        return self.act(self.conv(x))

在命令行中,我们可以使用以下命令启动训练,并添加一些有用的参数:

python training/run_experiment.py --max_epochs=5 --gpus=1 --data_class=Emnist --model_class=CNN --num_workers=4
  • --num_workers:此参数可以加速训练。它允许使用多个 CPU 进程预加载和预处理数据,使 GPU 无需等待,始终保持忙碌状态。通常将其设置为机器上的 CPU 核心数。
  • --overfit_batches:这是一个有用的调试标志。例如,设置为 2 意味着只使用 2 个批次的数据进行训练和测试。如果模型无法在这个极小的数据集上过拟合(达到接近 100% 的准确率),则可能意味着模型架构或代码存在问题,无法学习数据。

训练完成后,我们可以在测试集上抽样查看模型的预测结果,将预测标签与真实标签对比,以评估模型性能。

作业任务:改进 CNN 模型 💡

我们已经实现了一个基础的 CNN,本节的任务是尝试改进它。请修改 lab2/text_recognizer/models/cnn.py 文件中的模型。

以下是几个改进方向的建议:

  1. 将卷积块改为残差块:参考 ResNet 论文,将现有的 ConvBlock 改造成带有恒等快捷连接(identity shortcut)的残差块。官方 PyTorch ResNet 实现可以作为参考。
  2. 添加批归一化:在残差块中考虑加入批归一化层。
  3. 用步进卷积替代最大池化:正如课程中所讨论的,步进卷积正逐渐取代池化层。尝试移除 MaxPool2d,改用 stride=2 的卷积来实现下采样。
  4. 增加模型深度:目前模型只有两个卷积块。可以添加命令行参数(如 --num_conv_blocks),使其能够动态创建指定数量的卷积块。
  5. 调整超参数:通过命令行参数灵活调整卷积层的通道数、全连接层的维度等。

请尝试上述部分或全部改进,并在提交作业时说明你所做的修改。

创建合成文本行数据集 📝

为了向下一周的序列处理任务迈进,本节我们将创建一个用于文本行识别的合成数据集 EmnistLines

其核心思想是:

  1. 使用 SentenceGenerator(基于布朗语料库)生成逼真的英文句子。
  2. 从 EMNIST 数据集中随机抽取对应字符的图像。
  3. 将这些字符图像按顺序拼接(允许随机重叠)成一行,模拟手写文本行。

我们可以通过调整参数来控制生成文本行的难度:

  • max_length:每行的最大字符数。
  • min_overlap, max_overlap:字符间的最小和最大重叠比例,重叠越多,识别越难。
  • 填充:生成的句子长度不一,我们会使用特殊的填充令牌将较短句子填充到统一长度。

以下是通过不同参数生成的合成文本行图像示例:

  • max_length=16, max_overlap=0:字符无重叠,清晰易读。
  • max_length=34, max_overlap=0.33:字符间允许部分重叠,更接近真实连笔手写,难度增加。

这个数据集将在下周的实验中用于训练识别整行文本的模型。

总结 🎯

在本教程中,我们一起学习了以下内容:

  1. 设置实验环境并认识了 EMNIST 数据集
  2. 实现并训练了一个基础的 CNN 模型 进行字符分类,并学习了使用 num_workers 加速训练和 overfit_batches 进行模型调试的技巧。
  3. 明确了本次的作业任务:通过引入残差连接、调整网络结构等方式改进 CNN 模型。
  4. 介绍了如何创建 EmnistLines 合成数据集,该数据集通过拼接字符并引入随机重叠来模拟真实文本行,为后续的序列识别任务打下基础。

希望你能在改进 CNN 模型的作业中有所收获,并期待在下周处理更复杂的序列数据时应用这些知识。

🧠 P5:L2A-卷积神经网络

在本节课中,我们将要学习卷积神经网络(CNN)的核心概念。我们将从卷积运算这一基础数学操作开始,然后探讨构成CNN的其他重要操作,最后简要介绍一个经典的CNN架构。在下一部分课程中,Sergey将深入讲解计算机视觉应用。

📊 卷积运算

上一节我们介绍了CNN的动机,本节中我们来看看其核心操作——卷积。

卷积运算的灵感源于解决全连接神经网络在图像处理上的不足。将一张32x32像素的灰度图像展平,会得到一个1024维的向量。若使用一个全连接层进行分类,权重矩阵的大小将是1024x10。当图像尺寸增大时,例如变为64x64,权重数量将变为原来的4倍,这导致模型参数量随图像尺寸急剧增长,效率低下。

此外,全连接网络为每个输入像素学习独立的权重,这可能是一种过度参数化。更重要的是,这种结构不具备平移不变性:图像稍微平移后,网络看到的像素位置完全不同,难以识别为同一物体。

卷积滤波器通过另一种方式工作。它不处理整张图像,而是提取图像的一个小局部区域(例如一个5x5的像素块),将其展平为一个向量,并与另一个相同长度的滤波器向量进行点积运算,得到一个标量输出。

公式:对于一个图像块 x(展平后)和滤波器 w,卷积运算可表示为 output = dot(x, w)

为了处理整张图像,这个5x5的窗口会在图像上滑动,遍历所有可能的位置(先从左到右,再从上到下)。在每个位置都执行相同的点积操作,最终将所有输出组合成一个二维的特征图。

🎨 卷积滤波器的作用

一个自然的问题是:这种操作能做什么?实际上,在深度学习流行之前,卷积就已广泛应用于计算机视觉。通过精心选择滤波器中的权重值,可以实现各种图像处理效果,例如模糊图像。

CNN的核心理念是:我们不手动设计这些滤波器的权重,而是让模型从数据中自动学习它们。这样,网络就能学会提取对任务(如分类)最有用的特征。

🌈 多通道输入与输出

之前的例子是单通道(灰度)图像。对于RGB彩色图像,输入是三维张量,尺寸为 [高度, 宽度, 3](3代表红、绿、蓝三个通道)。

此时,提取的5x5图像块将包含所有三个通道的信息,因此展平后的向量长度是 5 * 5 * 3 = 75。滤波器也需要调整为75维的向量,点积运算保持不变。

输出的特征图也可以有多个通道。这可以通过使用多个独立的滤波器来实现。我们可以将75维的图像块向量与一个 [75, 10] 的权重矩阵相乘,而不是进行点积。这样,每个滤波器会产生一个输出通道,最终得到一个尺寸为 [新高度, 新宽度, 10] 的三维输出张量。

公式:多滤波器卷积可表示为 Output = X_patches * W,其中 X_patches 是所有图像块向量组成的矩阵,W 是滤波器权重矩阵。

🧱 堆叠卷积层

一个关键观察是:卷积层的输入和输出都是三维张量(高度、宽度、通道数)。因此,一个卷积层的输出可以直接作为另一个卷积层的输入。

我们可以像堆叠全连接层一样堆叠卷积层,构建深层网络。在实践中,我们不会连续使用多个线性卷积层,而是在每个卷积层之后立即应用一个非线性激活函数(如ReLU),这与全连接网络中的做法一致。

🚶‍♂️ 步长与填充

接下来我们讨论改变卷积操作方式的两种技术:步长和填充。

在之前的例子中,滤波器每次滑动1个像素。步长定义了滤波器每次滑动的距离。步长为1是默认情况。如果使用更大的步长(例如2),滤波器会跳过一些位置,这相当于对图像进行下采样,能减少输出特征图的空间尺寸。

图示:步长为2时,滤波器在水平和垂直方向上都每隔一个像素操作一次。

使用大步长时,滤波器可能会“滑出”图像边界。为了解决这个问题,我们可以使用填充。填充是在输入图像的边缘添加额外的像素(通常值为0),以确保滤波器能在所有位置有效操作。

一种常见的填充策略是“相同填充”,其目标是让输出特征图的空间尺寸与输入保持一致(在步长为1时)。另一种是“有效填充”,即不添加任何填充。

以下是计算卷积输出尺寸的公式:

公式
output_height = floor((input_height + 2*pad - filter_height) / stride) + 1
output_width = floor((input_width + 2*pad - filter_width) / stride) + 1

其中 floor 表示向下取整。

关于滤波器大小、步长等超参数的选择,通常基于经验和实验。虽然可以通过神经架构搜索等技术学习这些参数,但由于计算成本高,在实践中更常见的是手动设置。滤波器通常是正方形(如3x3, 5x5),大小会影响模型的感受野。

⚙️ 实现方式

在深度学习库中,卷积运算通常通过一种称为“im2col”的技术高效实现。其思想是将输入图像中所有需要卷积的局部块提取出来,排列成一个巨大的二维矩阵的列。然后,将滤波器权重也排列成矩阵,进行一次大规模的矩阵乘法,即可得到所有位置的结果,最后再重塑为输出特征图的形状。

代码概念

# 伪代码示意
X_patches = im2col(input_image, filter_size, stride)  # 形状: (filter_h*filter_w*C_in, num_patches)
W = filter_weights  # 形状: (C_out, filter_h*filter_w*C_in)
Output_matrix = W @ X_patches  # 矩阵乘法,形状: (C_out, num_patches)
Output_feature_map = reshape(Output_matrix, (C_out, out_h, out_w))

🔍 扩展操作:扩大感受野与降维

除了基础卷积,CNN中还有其他重要操作。

感受野 是指输出特征图上的一个点,对应输入图像上的区域大小。堆叠多个小卷积核(如两个3x3)可以等效于一个更大卷积核(如5x5)的感受野,且参数更少、非线性更强,通常效果更好。

空洞卷积 是另一种扩大感受野而不增加参数的方法。它在卷积核元素之间插入“空洞”(跳过输入像素),使卷积核在覆盖更大区域的同时保持权重数量不变。

为了将大型输入图像最终转换为小的输出向量(如图像分类),我们需要在网络中逐步降低张量的空间尺寸。

池化 是一种下采样操作。最常见的是2x2最大池化:它将每个2x2区域替换为该区域的最大值。这能显著减小特征图尺寸并提供一定的平移不变性。不过,在现代架构中,池化的使用已减少。

1x1卷积 是一种用于改变通道数的操作。它本质上是对每个空间位置的所有通道应用一个全连接层。它可以用来减少或增加通道数,是构建复杂模块(如Inception)的关键组件。

🏗️ 经典架构:LeNet

在深入现代架构之前,我们先看一个经典且基础的CNN架构——LeNet。它提供了一个很好的基线模型结构。

一个标准的类LeNet架构遵循以下模式:

  1. 输入图像。
  2. 重复N次:卷积层 -> 激活函数(如ReLU)。
  3. 池化层(如2x2最大池化)。
  4. 将步骤2-3重复M次,逐步提取特征并降低分辨率。
  5. 将最终的特征图展平。
  6. 通过K个全连接层(每层后通常有激活函数)。
  7. 输出层(如Softmax用于分类)。

原始的LeNet用于MNIST手写数字识别,使用了5x5卷积、Tanh激活和平均池化。你可以根据任务调整卷积层数、滤波器数量、是否使用池化等。

堆叠更多层(增加N或M)可以增加网络的感受野、非线性和表达能力,使其能够学习更复杂的特征。


本节课中我们一起学习了卷积神经网络的基础。我们从卷积运算的动机和定义开始,探讨了多通道处理、层堆叠、步长与填充等关键概念,并简要介绍了感受野、空洞卷积、池化等扩展操作,最后回顾了LeNet这一经典架构。理解这些基础知识是掌握更现代、更复杂CNN模型的前提。

计算机视觉应用教程 P6:L2B 🖼️

在本节课中,我们将学习深度学习与计算机视觉领域的一系列重要应用和核心架构。我们将回顾从经典网络到现代检测、分割任务的演进,并了解相关的技巧与工具,以便未来能将其应用于实际问题。

经典卷积神经网络架构巡礼 🏛️

上一节我们概述了课程目标,本节中我们将深入探讨计算机视觉发展历程中一些标志性的卷积神经网络架构。

这些架构的演进与ImageNet数据集紧密相关。ImageNet大规模视觉识别挑战赛始于2010年,包含1000个物体类别和超过一百万张训练图像。其核心任务是图像分类,即在给定图像中预测其中物体的类别。

在深度学习兴起之前(2010-2011年),主流方法是诸如SVM之类的浅层模型。2012年,AlexNet的发表开启了深度学习革命。它是一个8层网络,将错误率大幅降低至16%,相比之前的技术实现了巨大飞跃。

AlexNet与LeNet非常相似,但引入了ReLU激活函数、一种称为Dropout的新层(在训练中随机将一部分权重置零)以及大量的数据增强(如水平翻转、轻微旋转等)。其结构图显示为两部分,主要是因为当时的GPU内存有限(仅3GB),无法容纳整个网络,因此需要进行分布式训练。其架构可简述为:输入 → 11x11卷积 → 5x5卷积 → 最大池化 → 3x3卷积 → 最大池化 → 3x3卷积 → 3x3卷积 → 最大池化 → 全连接层。其参数量为数百万。

2013年,错误率进一步降低了约5%,这主要归功于对AlexNet超参数的优化调整。同年的一篇著名论文引入了反卷积可视化技术,展示了网络不同层学习到的特征:浅层学习边缘和纹理,深层则能检测物体的部件(如耳朵、眼睛、车轮)。

2014年,VGG网络将错误率再降低4%。其特点是网络更深,且只使用3x3的小卷积核和2x2的最大池化。随着层数加深,通道数逐渐增加(例如从64到512)。作者指出,堆叠多个3x3卷积可以获得与大卷积核相同的感受野,但总参数量更少。VGG拥有1.38亿参数,大部分内存消耗在早期的卷积层,而大部分参数则存在于末端的全连接层。

同年出现的GoogLeNet(或称Inception Net)与VGG精度相当,但参数量仅为500万(约为VGG的3%),且没有全连接层。其核心是堆叠“Inception模块”。该模块的假设是:特征的通道相关性和空间相关性是解耦的,可以分开处理。1x1卷积用于处理通道维度(如降维或升维),而不涉及空间相关性。GoogLeNet还引入了辅助分类器,在网络中间层也添加分类输出,以帮助梯度传播。

2015年,ResNet(残差网络)出现,其深度达到152层,将错误率再次提升3%。ResNet解决了极深网络训练中的梯度消失问题,其方法是引入“快捷连接”或“跳跃连接”,允许输入绕过某些层直接与输出相加。其核心公式可表示为:
输出 = F(x) + x
其中F(x)是残差映射。ResNet还使用带步长的卷积进行下采样,而非最大池化。此后出现了多种变体,如DenseNet增加了更多密集连接,ResNeXt结合了Inception的多路径思想。

近年来,Squeeze-and-Excitation网络通过全局池化和全连接层自适应地重新加权特征通道,类似于注意力机制。而SqueezeNet则专注于极致压缩,用比AlexNet少50倍的参数达到了相近的精度,大量使用1x1卷积来压缩通道数。

以下是这些网络在精度、计算量和参数量上的权衡概览:

  • 精度 vs 计算量 (GFLOPs):ResNet系列通常在精度和计算效率之间取得了较好的平衡。
  • 精度 vs 参数量:GoogLeNet、SqueezeNet等展示了用更少参数获得高精度的方法。

除了追求精度,另一个研究方向是训练速度。关键之一是使用尽可能大的批次大小并进行分布式训练。例如,可以在数千个GPU上使用超大批次,在15分钟内完成ResNet50在ImageNet上的训练。DAWNBench等基准测试就专注于衡量训练到指定精度所需的时间或成本。

超越分类:定位、检测与分割 🎯

上一节我们回顾了用于图像分类的经典架构,本节中我们来看看计算机视觉中更复杂的任务:定位、检测和分割。

这些任务的定义如下:

  • 分类:给定图像,输出其中主要物体的类别。
  • 定位:在分类基础上,还需标出该物体在图像中的位置(通常用边界框表示)。
  • 检测:图像中可能有多个物体,需要输出每个物体的类别和位置。
  • 分割:为图像中的每一个像素分配一个标签,指明它属于哪个物体或背景。
  • 实例分割:在分割的基础上,区分同一类别的不同个体(例如,区分图像中的多只狗)。

对于定位任务,一个直观的思路是扩展分类网络。在网络的末端,除了分类输出头,可以额外添加4个输出头来预测边界框的坐标(x1, y1, x2, y2)。但这仅适用于已知图像中只有一个物体的场景。

对于检测任务,由于物体数量未知,无法预先确定输出头的数量。一种早期方法是滑动窗口:使用训练好的分类器,在图像的不同区域( patches )上依次进行扫描和分类。但这非常耗时,因为重叠区域的计算被重复执行。

高效的实现利用了卷积网络的全卷积特性。全连接层可以等价地转换为1x1卷积层。因此,整个网络可以处理任意大小的输入图像,并输出一个空间网格,其中每个网格单元都对应原图的一个区域,并预测该区域是否存在物体及其类别和边界框偏移量。这样,单次前向传播就能得到大量候选检测框。

以下是处理这些候选框的关键后续步骤:

  • 非极大值抑制:对于大量重叠的检测框,只保留置信度最高的那个,并抑制与其高度重叠的其他框。
  • 交并比:用于评估检测框的质量。它是预测框与真实框的交集面积与并集面积的比值。通常设定一个阈值(如0.5),高于该阈值即认为检测正确。

YOLO(You Only Look Once)和SSD(Single Shot Detector)是这类“单次”检测方法的代表。它们在图像上放置固定网格,每个网格单元直接预测物体的存在、类别和边界框,然后应用非极大值抑制。YOLO v3/v4等版本在COCO数据集上实现了高精度和实时速度。

COCO数据集是当前物体检测、分割等任务的主流基准,包含33万张图像、150万个物体实例、80个类别,并提供实例分割标注。

除了“看遍全图”的方法,还有“先找感兴趣区域”的策略。R-CNN系列是代表:

  • R-CNN:使用传统方法(如选择性搜索)生成候选区域,然后将每个区域变形后送入CNN(如AlexNet)进行分类和边界框回归。
  • Fast R-CNN:改进为先将整个图像送入CNN提取特征图,然后对每个候选区域在特征图上进行“兴趣区域池化”,再统一分类。
  • Faster R-CNN:核心创新是引入了区域提议网络,该网络与主CNN共享特征,直接预测出候选区域,使流程完全端到端且更快速。

在Faster R-CNN基础上,可以进一步增加分割任务分支,这就是Mask R-CNN。它在每个兴趣区域上添加一个小的全卷积网络分支,输出该区域的二进制分割掩码,实现了出色的实例分割效果。

对于全景分割(为图像每个像素分类),需要使用编码器-解码器结构的全卷积网络,如U-Net。网络先通过卷积和池化“编码”降维,再通过上采样操作“解码”恢复至原图尺寸。上采样方法包括:

  • 反池化:最大池化的逆操作,需记录池化时最大值的位置。
  • 转置卷积:可学习的上采样操作。
  • 空洞卷积:在卷积核元素间插入空格,以增大感受野而不丢失分辨率。

扩展应用与前沿话题 🚀

上一节我们探讨了目标检测与分割,本节中我们来看看计算机视觉如何扩展到其他具体任务,并了解一些有趣且重要的前沿话题。

基于相似的“添加任务分支”思想,可以解决多种视觉问题。例如:

  • Mesh R-CNN:在Mask R-CNN基础上增加一个分支,预测物体的3D网格模型。这需要像ShapeNet这样包含3D模型标注的数据集。
  • 人脸关键点检测:检测人脸特征点(如鼻尖、嘴角、眼角)。需要AFW等标注了关键点的数据集。
  • 人体姿态估计:检测人体关节位置。COCO数据集也提供了大量人体关键点标注。

对抗性攻击揭示了神经网络的脆弱性。通过对输入添加精心构造的微小扰动(人眼难以察觉),可以使网络以高置信度做出错误分类。这既包括数字图像上的“白盒”攻击(已知模型参数),也包括物理世界的“黑盒”攻击(如干扰自动驾驶汽车的标志识别)。防御方法包括在训练中加入对抗样本、或用“防御性蒸馏”等技术平滑决策边界。

风格迁移是一项有趣的应用,它将一幅图像的艺术风格迁移到另一幅图像的内容上。其原理是,利用预训练CNN提取内容图像的内容特征和风格图像的风格特征(通过计算特征的相关矩阵),然后优化一张新图像,使其特征同时接近内容特征和风格特征。

生成对抗网络能够生成极其逼真的图像,我们将在后续课程中详细讨论。

想要了解更多最新进展,可以访问“Papers With Code”网站,它汇总了各计算机视觉任务上的最新基准测试结果和对应论文。

总结 📝

本节课中我们一起学习了深度学习在计算机视觉中的应用全景。我们从ImageNet竞赛驱动的经典CNN架构演进讲起,了解了AlexNet、VGG、GoogLeNet、ResNet等网络的设计思想与贡献。接着,我们探讨了超越分类的视觉任务——定位、检测与分割,学习了滑动窗口、R-CNN系列、YOLO、Mask R-CNN等核心方法。最后,我们瞥见了3D重建、姿态估计等扩展应用,以及对抗攻击、风格迁移等前沿话题。这些知识和工具构成了现代计算机视觉应用的坚实基础。

课程P7:L3-循环神经网络 🧠

在本节课中,我们将要学习循环神经网络。这是一种专门为处理序列数据而设计的神经网络架构。我们将探讨其工作原理、解决的问题、存在的挑战以及一些改进方案。


序列问题概述 📊

上一节我们介绍了循环神经网络的基本概念,本节中我们来看看它旨在解决哪些问题。序列问题是指输入或输出(或两者)是数据序列的任务。

以下是几种主要的序列问题类型:

  • 一对一:输入和输出都是单个数据点。
  • 一对多:输入是单个数据点,输出是一个序列。例如,根据一张图片生成描述文本。
  • 多对一:输入是一个序列,输出是单个数据点。例如,根据一段文本判断其情感倾向。
  • 多对多:输入和输出都是序列。例如,机器翻译或语音识别。

为何不使用前馈网络? 🤔

一个自然的问题是:为什么不直接使用标准的前馈神经网络来处理序列?一种方法是将整个序列拼接成一个长向量作为输入。

这种方法存在几个问题:

  • 无法处理任意长度:需要将序列填充到固定长度。
  • 计算效率低:模型参数量随序列最大长度线性增长。
  • 数据效率低:模型需要为序列中每个位置独立学习相同的模式,无法有效利用序列的重复性结构。

循环神经网络核心思想 🔄

循环神经网络的核心思想是引入“状态”或“记忆”。模型在每个时间步的输出不仅取决于当前输入,还取决于一个随时间传递的隐藏状态

其计算过程可以形式化地描述。在每个时间步 t,RNN 执行以下操作:

  1. 计算新的隐藏状态:h_t = activation(W_hh * h_{t-1} + W_xh * x_t + b_h)
  2. 计算当前输出:y_t = W_hy * h_t + b_y

其中,h_t 是当前隐藏状态,h_{t-1} 是上一时刻隐藏状态,x_t 是当前输入,W_*b_* 是可学习的权重和偏置,activation 是激活函数(如 tanh)。

这种结构使得 RNN 能够捕捉序列中的时间依赖性。


处理多对一问题:编码器-解码器模式 🏗️

上一节我们介绍了基础的 RNN 结构,本节中我们来看看如何用它解决更复杂的问题。对于“多对一”问题(如情感分析),我们只关心序列的最终输出。

解决方案是使用编码器-解码器架构:

  1. 编码器:一个 RNN 处理整个输入序列,其最终隐藏状态 h_T 被视作整个输入序列的“摘要”或“编码”。
  2. 解码器:一个简单的分类器(如前馈网络)接收这个编码 h_T,并产生最终的输出(如情感标签)。

虽然概念上是两个网络,但在训练时,梯度会从解码器一直反向传播回编码器,整个系统是端到端训练的。


处理一对多问题:序列生成 🎨

对于“一对多”问题(如图像描述生成),输入是单个数据点(如图像),输出是一个序列。

以下是实现步骤:

  1. 编码:使用一个卷积神经网络处理输入图像,提取一个特征向量作为初始隐藏状态 h_0
  2. 解码:一个 RNN 解码器以 h_0 为起点开始生成序列。在每个时间步,它将上一个时间步的生成结果作为当前输入,并结合当前隐藏状态,预测序列中的下一个元素(如一个词)。
  3. 终止:在词汇表中引入一个特殊的“结束符”。当解码器生成该符号时,序列生成停止。

训练时,通常使用交叉熵损失,将每个时间步的预测与真实序列的对应元素进行比较,并将所有时间步的损失求和。


处理多对多问题:机器翻译案例研究 🌐

最一般的情况是“多对多”问题,且输入输出序列长度可能不同,例如机器翻译。

基础的编码器-解码器架构如下:

  1. 编码器 RNN:逐词读入源语言句子,最终隐藏状态 h_T^enc 编码了整个句子的信息。
  2. 状态传递h_T^enc 作为解码器 RNN 的初始隐藏状态 h_0^dec
  3. 解码器 RNN:从 h_0^dec 开始,以上一步的输出为输入,逐步生成目标语言句子。

然而,这种方法存在信息瓶颈:无论源句子多长,所有信息都必须压缩进一个固定维度的向量 h_T^enc 中,这对于长句子来说非常困难。


RNN 的挑战与 LSTM 🚧

基础 RNN 在处理长序列时面临梯度消失/爆炸问题,导致其难以学习长期依赖关系。这是因为在反向传播时,梯度需要跨多个时间步连乘,如果每个局部梯度很小(对于 tanh/sigmoid)或很大(对于 ReLU),整体梯度就会迅速衰减或激增。

长短期记忆网络 是 RNN 的一个成功变体,它通过引入“细胞状态”和“门控机制”来更好地控制信息的流动和记忆。

LSTM 单元的核心组件包括:

  • 遗忘门:决定从细胞状态中丢弃哪些旧信息。f_t = sigmoid(W_f * [h_{t-1}, x_t] + b_f)
  • 输入门:决定将哪些新信息存入细胞状态。i_t = sigmoid(W_i * [h_{t-1}, x_t] + b_i)\tilde{C}_t = tanh(W_C * [h_{t-1}, x_t] + b_C)
  • 细胞状态更新C_t = f_t * C_{t-1} + i_t * \tilde{C}_t
  • 输出门:基于细胞状态决定输出什么。o_t = sigmoid(W_o * [h_{t-1}, x_t] + b_o)h_t = o_t * tanh(C_t)

这些门使 LSTM 能够有选择地保留长期信息,有效缓解了梯度消失问题。GRU 是 LSTM 的一个流行简化版本,在实践中表现也常与 LSTM 相当。


改进机器翻译:注意力机制与双向性 ✨

为了解决信息瓶颈问题,注意力机制被引入。其核心思想是:在解码器生成每个词时,允许它直接“查看”编码器在所有时间步的隐藏状态,并动态地决定关注输入序列的哪些部分。

具体来说:

  1. 解码器在时间步 t 计算一个注意力分数(相关性权重)α_{t,i},对应于编码器每个隐藏状态 h_i^enc
  2. 这些分数通过 softmax 归一化,形成注意力分布。
  3. 计算上下文向量 c_t 作为编码器隐藏状态的加权和:c_t = Σ_i (α_{t,i} * h_i^enc)
  4. 将上下文向量 c_t 与解码器当前状态结合,用于预测输出。

此外,使用双向 LSTM 作为编码器可以让模型同时考虑过去和未来的上下文,这对理解当前词的含义很有帮助。

一个2016年的先进机器翻译系统结合了:堆叠的 LSTM残差连接(便于训练深层网络)、注意力机制双向编码器,并使用大规模平行语料库和交叉熵损失进行训练。


连接时序分类损失 📝

在一些任务中,输入和输出序列之间存在不对齐的问题,例如手写文字识别:输入是图像序列(滑动窗口),输出是字符序列,但两者长度和对应关系不确定。

CTC损失 专门用于处理这类问题。它允许模型输出一个长度等于输入长度的序列,其中包含:

  • 有效字符
  • 一个特殊的“空白”符(ε

解码时,应用两条规则将模型输出转换为最终序列:

  1. 合并重复的连续字符。
  2. 删除所有的空白符 ε

例如,模型输出 [c, ε, a, a, t, ε] 经过 CTC 解码后变为 cat。CTC 损失在训练时考虑了所有可能对齐路径的概率,是可微的。


RNN 的优缺点 ⚖️

优点

  • 架构灵活:编码器-解码器模式可适应多种序列任务(一对一、一对多、多对多)。
  • 历史成功:在 NLP、语音等领域曾有广泛应用,并在 Transformer 出现前长期占据主导地位。

缺点

  • 训练并行性差:由于状态的顺序依赖性,训练本质上是串行的,难以充分利用现代硬件。
  • 训练调优难:LSTM/GRU 等模型通常需要仔细的调参和训练技巧。
  • 长程依赖仍具挑战:尽管 LSTM 有所改善,但处理非常长的序列依然困难。

非循环序列模型预览:WaveNet 🌊

序列建模不一定必须使用循环网络。WaveNet 是一个使用因果膨胀卷积的卷积序列模型,用于生成高质量音频。

其核心思想:

  • 因果卷积:每个输出仅依赖于当前及过去的输入,不依赖未来,满足自回归生成的要求。
  • 膨胀卷积:通过跳跃式地连接输入,指数级地增大模型的感受野,使其能够捕捉很长期的依赖关系。

优势

  • 高度并行化训练:所有时间步的输出可以同时计算,训练速度远快于 RNN。
  • 有效捕捉长程依赖:通过堆叠膨胀卷积层实现。

劣势

  • 推理速度慢:生成时仍需按顺序进行,因为每一步的输出是下一步的输入。后续有研究专门优化其推理效率。

总结 🎯

本节课中我们一起学习了循环神经网络。我们从序列问题入手,探讨了基础 RNN 的原理及其在处理多对一、一对多和多对多问题时的编码器-解码器框架。我们深入分析了 RNN 在训练长序列时面临的梯度消失问题,并介绍了 LSTM 的门控机制作为解决方案。通过机器翻译的案例,我们看到了注意力机制和双向性如何显著提升模型性能。此外,我们还了解了用于处理未对齐序列的 CTC 损失,并讨论了 RNN 架构的优缺点。最后,我们预览了非循环的序列模型 WaveNet,它展示了使用卷积网络处理序列数据的另一种强大范式。这些知识为我们理解更现代的序列模型(如 Transformer)奠定了基础。

📘 课程 P8:【Lab3】RNN 序列模型

在本课程中,我们将学习如何构建一个用于识别手写文本行的序列模型。我们将从简单的滑动窗口方法开始,逐步引入更高效的卷积网络结构,并最终结合CTC损失函数和LSTM网络来处理可变长度和重叠字符的复杂情况。


🧠 概述与问题定义

Lab3 的核心是构建序列模型。在之前的实验中,我们使用MLP处理MNIST(Lab1),并引入了扩展的字符数据集,使用CNN进行训练(Lab2)。本周,我们将训练一个LSTM模型来识别我们生成的手写文本行。

我们的目标是构建一个完整的“文本行识别器”。


📁 项目结构与数据

项目文件结构与Lab2类似,但新增了一些关键文件。所有数据集,包括 emnist_lines,都与Lab2相同。在 lit_models 目录下,新增了一个 CTCLitModel 包装器。在 models 目录中,新增了三个模型:line_cnn_simpleline_cnnline_cnn_lstm。我们将按顺序讲解它们。

首先,我们回顾一下标签映射。模型输出的整数标签需要映射到可打印的字符,其中包含一些特殊标记:

  • B:序列开始
  • S:空格
  • E:序列结束
  • P:填充符

我们使用 P 将所有生成的字符串填充到最大长度。

我们的数据是通过将28x28像素的EMNIST字符拼接成行而生成的。初始时,我们生成无重叠的字符行,这看起来还不太像真实笔迹,但这是我们起步的基础。


🔍 模型一:LineCNNSimple(简单滑动窗口CNN)

首先,我们来看 line_cnn_simple 模型。这个模型实例化了一个与上周识别单个字符相同的CNN网络。

模型的 forward 方法接收一个输入图像,其尺寸为 (batch_size, channels, height, width)。对于文本行图像,高度仍是28,但宽度可能达到400以上。模型需要输出 (batch_size, num_classes, sequence_length)

该模型的核心思想是使用滑动窗口。它接收两个超参数:

  • window_width:滑动窗口的宽度。
  • window_stride:滑动窗口的步长。

窗口数量计算公式为:
num_windows = (image_width - window_width) / window_stride + 1

对于每个窗口,我们将其裁剪出来,并通过相同的CNN网络前向传播,得到该窗口对应位置的字符分类激活值。

我们可以运行一个实验,在无重叠的数据上(min_overlap=0, max_overlap=0),设置 window_width=28window_stride=28,这相当于逐字符进行识别。模型很快能达到约89%的验证准确率,效果与识别单个字符时类似。

此时我们使用的损失函数是默认的交叉熵损失(CrossEntropyLoss)。需要注意的是,现在的目标(target)不再是一个单独的整数,而是一个向量(例如 [T, H, E, P, P, P...]),对应一行中的每个字符位置。PyTorch的交叉熵损失可以直接处理这种多目标情况(通过指定 ignore_index 来忽略填充符 P),这被称为 K维损失


🔄 引入重叠与挑战

接下来,我们让滑动窗口重叠。例如,设置 window_width=28, window_stride=20。这意味着相邻窗口有8个像素的重叠。

如果数据本身是无重叠生成的,模型窗口与字符不再精确对齐,准确率会下降到约60%。为了解决这个问题,我们可以让生成的数据也带有重叠(例如 overlap=0.25),这样准确率能回升到80%以上。

然而,真实手写面临更大的挑战:

  1. 字符宽度不同(如‘i’和‘w’)。
  2. 字符间距因人而异,时疏时密。

我们可以通过设置可变的字符重叠来模拟这种情况(例如 min_overlap=0, max_overlap=0.33)。这生成了看起来更真实的手写行。但对于固定的 window_widthwindow_strideLineCNNSimple 模型,这成为了一个挑战,最佳准确率也只能达到60%左右。

我们知道解决方案是:使用更小的步长进行密集采样,并引入CTC损失函数来处理可能重复的字符输出


⚡ 模型二:LineCNN(高效全卷积网络)

在介绍CTC之前,我们先优化模型效率。LineCNNSimple 在重叠窗口上重复计算了大量卷积操作,效率低下。

LineCNN 模型采用了不同的结构。它使用步长为2的卷积层进行下采样,替代了之前的最大池化层。最关键的是,它引入了一个核高度与特征图高度相同的卷积层。

代码示例:

# 这相当于一个在宽度方向上进行操作的“全连接层”
self.conv4 = nn.Conv2d(in_channels=64, out_channels=num_classes, kernel_size=(feature_height, 8))

这个卷积层的作用类似于一个全连接层,但它是在整个特征图的宽度上并行操作的,一次性输出整个序列的激活图,避免了重复计算。forward 方法依次通过各层卷积,并进行一些尺寸计算以确保输出序列长度符合预期,最后可能再添加全连接层进行微调。

它的效果与 LineCNNSimple 相同,但运行速度更快。


🎯 模型三:引入CTC损失函数

真正的改进来自于CTC(Connectionist Temporal Classification)损失函数。我们将使用一个新的LitModel包装器:CTCLitModel

BaseLitModel 负责初始化优化器、损失函数和评估指标(如准确率)。CTCLitModel 的不同之处在于:

  • 损失函数:使用 torch.nn.CTCLoss
  • 评估指标:除了准确率,新增了字符错误率(Character Error Rate, CER)

在训练步骤中,CTCLitModel 将模型输出的 logits 转换为概率(使用 softmax),并调整维度顺序以满足 CTCLoss 的输入要求 (sequence_length, batch_size, num_classes)。同时,它还需要提供输入序列长度和目标序列长度。

在验证和测试步骤中,除了计算损失,还需要计算字符错误率。这需要一个 greedy_decode 函数,其作用是在推理时,将模型输出的序列进行解码:合并重复的字符,并移除空白标记(blank token)。理解这个函数是作业的一部分。

使用相同的 LineCNN 模型,但将损失函数换为CTC,并在可变重叠的数据上训练,字符错误率可以从99%迅速下降到18%左右,效果显著提升。


🧬 模型四:LineCNN LSTM(结合上下文信息)

最后,我们在CNN提取的特征序列之上添加一个LSTM网络,以利用上下文信息进行更好的预测。

LineCNNLSTM 模型内部初始化了一个 LineCNN 和一个 双向LSTM。在前向传播过程中:

  1. 输入图像先通过 LineCNN,得到 (batch, classes, seq_len) 的特征序列。
  2. 调整维度为 (seq_len, batch, classes) 以适应LSTM输入。
  3. 特征序列通过双向LSTM,输出两个方向的隐藏状态。
  4. 将两个方向的输出相加,以融合上下文信息。
  5. 通过一个全连接层进行最终映射。
  6. 将输出维度调整回 (batch, classes, seq_len) 以供 CTCLitModel 使用。

添加LSTM后,模型参数从170万增加到230万。在CTC损失的基础上,结合LSTM的上下文建模能力,有望获得更低的字符错误率。


📝 作业与总结

在本节课中,我们一起学习了构建手写文本行识别器的完整流程:

  1. 从简单的滑动窗口CNN(LineCNNSimple)开始,理解序列建模的基本思想。
  2. 为了提高效率,将其改进为全卷积网络(LineCNN)。
  3. 为了处理可变长度和重叠字符,引入了强大的CTC损失函数。
  4. 最后,通过添加LSTM网络来利用序列的上下文信息,进一步提升模型性能。

课后作业包括:

  • 实验不同的超参数(如 window_width, window_stride, lstm_dim)。
  • 尝试修改模型结构(例如,在LSTM中添加注意力机制)。
  • 描述 character_error_rate 指标的计算方式。
  • 解释 greedy_decode 函数的具体作用和工作原理。

通过本实验,你应该对如何使用CNN、CTC和RNN(LSTM)来解决序列识别问题有了扎实的理解。

课程 P9:L4 - 迁移学习和 Transformer 🚀

在本节课中,我们将要学习迁移学习的概念,了解它如何从计算机视觉领域兴起并应用于自然语言处理。我们将探讨词嵌入、语言模型,以及被称为 NLP 的“ImageNet 时刻”的重要转折点。最后,我们将深入解析 Transformer 模型的核心机制——注意力机制,并介绍一系列基于 Transformer 的著名模型。

从计算机视觉到迁移学习

上一节我们概述了课程内容,本节中我们来看看迁移学习在计算机视觉中的起源。假设我们想对鸟类图像进行分类,但只有 10,000 张带标签的图像。相比之下,ImageNet 数据集拥有百万级图像,像 ResNet50 这样的深度神经网络在其中表现优异。然而,ResNet50 模型层数多、参数量大,在我们的小数据集上直接训练容易导致过拟合。

一种解决方案是,先在完整的 ImageNet 数据集上训练神经网络,然后在我们的小型鸟类数据上进行微调。这种方法通常能获得比任何其他方法都更好的性能。这就是迁移学习的核心概念。

在传统机器学习中,我们使用大量数据训练出一个大模型。而在迁移学习中,我们先在大数据上训练一个大模型,然后只需用少得多的数据,通过替换或添加新层的方式继续训练。这耗时更少且效果出色。

以下是迁移学习的一个典型流程:

  1. 获取一个在大型数据集(如 ImageNet)上预训练好的模型。
  2. 保留模型的前面若干层(特征提取器),移除最后的全连接层和 softmax 分类层。
  3. 根据新任务,添加新的分类层。
  4. 在小型新数据集上训练时,冻结预训练层的权重,只更新新添加层的权重。

在 PyTorch 中,实现迁移学习非常简便。以下是一个使用 PyTorch Lightning 的示例代码片段:

import torchvision.models as models

class BirdClassifier(pl.LightningModule):
    def __init__(self, num_classes, pretrained=True):
        super().__init__()
        # 加载在 ImageNet 上预训练的 ResNet18
        self.feature_extractor = models.resnet18(pretrained=pretrained)
        # 冻结特征提取器的权重
        self.feature_extractor.eval()
        for param in self.feature_extractor.parameters():
            param.requires_grad = False
        # 替换最后的分类层
        num_features = self.feature_extractor.fc.in_features
        self.classifier = nn.Linear(num_features, num_classes)

PyTorch 的 torchvision 和 TensorFlow 的 tf.keras.applications 等模型库提供了大量现成的预训练模型,方便我们直接使用。

自然语言处理中的词向量表示

上一节我们介绍了计算机视觉中的迁移学习,本节中我们来看看如何将其思想应用到语言世界。首先需要解决的问题是,深度神经网络处理的是向量,而我们的输入通常是单词,因此需要将单词转换为向量。

一种简单的方法是独热编码。假设词典有 10,000 个词,要编码某个词,就生成一个长度为 10,000 的向量,仅在对应词的位置为 1,其余全为 0。这种方法存在几个问题:向量维度随词汇表增大而增高;向量非常稀疏;并且无法体现单词之间的语义相似性(例如,“跑”和“跑步”的距离与“跑”和“诗歌”的距离一样远)。

更好的方法是使用词嵌入,将稀疏的高维独热向量映射到稠密的低维向量空间。这通过一个嵌入矩阵实现:嵌入向量 = 独热向量 × 嵌入矩阵。嵌入矩阵的维度是 词汇表大小 × 嵌入维度

接下来的关键问题是,如何设定嵌入矩阵的值?一种方法是在特定任务中将其作为模型的一部分进行学习。另一种更通用的方法是预训练词嵌入,使其对多种任务都有用。这可以通过在大规模文本语料库(如维基百科)上训练一个语言模型来实现,例如训练模型预测下一个单词。

以下是构建训练数据的一种方式:

  • N-gram 方法:在语料库上滑动一个固定大小的窗口,形成 (上下文词,目标词) 对。
  • Skip-gram 方法(更高效):对于中心词,同时用其周围的前后文词作为正样本,并采样一些非周围词作为负样本,将其构建为一个二分类问题(判断两个词是否相邻)。

通过这种方式学习到的词嵌入具有有趣的向量空间特性,例如可以进行向量运算(国王 - 男人 + 女人 ≈ 女王),并能捕捉动词时态、国家-首都等语义关系。

NLP 的“ImageNet 时刻”与早期预训练模型

我们讨论了基于 ImageNet 的预训练和传统的词嵌入方法。现在,我们将探讨 NLP 的“ImageNet 时刻”,它结合了这两种趋势,大约发生在 2017-2018 年。

此前,Word2Vec 和 GloVe 等词嵌入方法通过预训练提升了各种 NLP 任务的性能,但提升幅度有限(通常不到 10%)。这些表示是“浅层”的,意味着只有模型的第一层(嵌入层)受益于预训练知识,更深层的网络仍需从头在小数据集上训练。

为了捕获更复杂的语言现象(如词性消歧、语法规则),我们需要预训练更深层的网络。ELMo 是这一方向的先驱之一(2018年)。它使用了一个双向堆叠的 LSTM 模型,通过预测单词来学习上下文相关的词表示。ELMo 在多项任务上显著提升了当时的最高水平,特别是在 SQuAD 问答数据集上。

为了理解这些进展,我们需要了解一些核心的 NLP 评测数据集:

  • SQuAD:阅读理解数据集,包含约10万个问题-答案对,答案均来自给定的原文段落。
  • SNLI:自然语言推理数据集,包含57万对句子,任务是判断两个句子之间的关系(蕴含、矛盾、中立)。
  • GLUE:通用语言理解评估基准,包含9个不同的任务(如情感分析、文本相似度、自然语言推理),旨在全面评估模型的语言理解能力。

紧随 ELMo 之后,ULMFiT 模型也采用了类似的双向 LSTM 架构,并进一步推动了迁移学习在 NLP 中的应用。2018 年夏天,人们普遍认为 NLP 的“ImageNet 时刻”已经到来,预训练语言模型将成为未来处理所有 NLP 任务的标准方法。这一点也反映在 PyTorch 和 TensorFlow 的模型库中,开始涌现大量如 BERT、ALBERT、Transformer-XL 等预训练模型。

Transformer 模型的核心:注意力机制

上一节我们看到了预训练语言模型的兴起,本节中我们来看看其背后的核心架构——Transformer。Transformer 始于 2017 年的论文《Attention Is All You Need》。它是一个编码器-解码器架构,但完全摒弃了 RNN 或 LSTM,仅使用注意力机制和全连接层,并在翻译任务上取得了突破性成果。

为了简化,我们聚焦于 Transformer 的编码器部分。其核心组件包括:

  1. 自注意力
  2. 位置编码
  3. 前馈神经网络
  4. 层归一化

首先,让我们详细探讨自注意力。其基本思想是:输入是一个序列的向量 [x1, x2, ..., xt],输出也是一个序列 [y1, y2, ..., yt]。但每个输出向量 yi 都是所有输入向量的加权和。最初,权重可以是输入向量 xixj 的点积,并通过 softmax 归一化,使其和为 1。

为了引入可学习的参数,我们将每个输入向量 xi 通过三个不同的权重矩阵进行线性变换,生成三个新向量:

  • 查询向量 qi = Wq * xi
  • 键向量 ki = Wk * xi
  • 值向量 vi = Wv * xi

然后,计算输出 yi 的公式为:
注意力权重 αij = softmax(qi · kj / √dk)
yi = Σ(αij * vj)
其中 dk 是键向量的维度,用于缩放点积。

多头注意力意味着我们并行地学习多组 (Wq, Wk, Wv) 矩阵,让模型能够同时关注来自不同表示子空间的信息。

然而,基本的自注意力对输入序列的顺序不敏感。为了解决这个问题,Transformer 引入了位置编码。我们将一个仅与单词在序列中位置相关的向量(位置嵌入)加到单词嵌入向量上。这样,输入向量就同时包含了语义信息和位置信息。

层归一化是一种技术,用于稳定深度网络的训练。它在每个子层(自注意力和前馈网络)之后应用,将激活值重新调整为均值为 0、方差为 1,有助于缓解训练过程中的梯度问题。

对于像 GPT 这样的生成式模型,还需要使用掩码。在训练时,为了防止模型在预测当前位置时“偷看”未来的信息,会在计算注意力权重时,将未来位置的权重掩码掉(设为负无穷),迫使模型只能关注过去的信息。

基于 Transformer 的著名模型演进

理解了 Transformer 的核心机制后,本节中我们来看看一系列基于它构建的、影响深远的模型。

  • GPT(生成式预训练 Transformer): 使用 Transformer 的解码器部分,通过预测下一个单词进行训练(自回归)。它采用了掩码注意力,只能关注左侧上下文。初代 GPT 在约 800 万个网页上训练。
  • BERT(双向编码器表示): 使用 Transformer 的编码器部分,是双向的(无掩码)。其训练采用了两个任务:1) 掩码语言模型:随机遮盖输入中的一些词,让模型预测它们;2) 下一句预测:判断两个句子是否连续。BERT-large 拥有 3.4 亿参数。
  • T5(文本到文本传输 Transformer): 回归完整的编码器-解码器架构。其创新在于将所有 NLP 任务都统一为“文本到文本”的格式。例如,输入可以是 “翻译英文为法文:That is good.”,输出则是 “C‘est bon.”。T5 在巨大的 “Colossal Clean Crawled Corpus” 上训练,参数达 110 亿。
  • GPT-3: GPT 系列的第三代,模型规模急剧扩大,达到 1750 亿参数。它展示了惊人的上下文学习能力,只需在输入中给出几个示例(少样本学习),就能执行新任务。由于其强大的生成能力和潜在滥用风险,OpenAI 未开源其权重,仅通过 API 提供访问。
  • 模型小型化探索: 鉴于大模型训练成本高昂,研究者也在探索“以小博大”。例如 DistilBERT 使用知识蒸馏技术,用一个更小的模型去学习 BERT 的输出,在参数减少 40% 的情况下保留了 97% 的性能。

这些模型的演进呈现出一个明显趋势:扩大模型规模(参数和数据)能持续提升性能。这被强化学习先驱 Rich Sutton 称为“苦涩的教训”,即长期来看,利用计算能力的通用方法(如缩放神经网络)最终会胜过依赖大量人类精心设计的算法。

总结与工具推荐

本节课中,我们一起学习了迁移学习的思想及其从计算机视觉到自然语言处理的应用历程。我们深入探讨了将单词表示为向量的方法,见证了以 ELMo、BERT 为代表的 NLP“ImageNet 时刻”。我们重点剖析了 Transformer 模型,理解了其核心——自注意力机制、位置编码等组件的工作原理。最后,我们回顾了 GPT、BERT、T5、GPT-3 等基于 Transformer 的里程碑式模型,并看到了模型规模化发展的趋势与挑战。

对于希望跟进最新研究或应用这些模型的学习者,以下工具和资源非常有用:

  • Hugging Face Transformers: 一个提供了数千个预训练 Transformer 模型的 Python 库,支持 PyTorch 和 TensorFlow,极大简化了使用和实验过程。
  • Papers With Code: 网站,将最新的研究论文与其代码实现链接起来,并按任务和数据集排名。
  • NLP Progress: 网站,追踪各 NLP 任务上的最新技术水平(SOTA)。

通过本课的学习,你应该对现代 NLP 的基石——迁移学习和 Transformer 架构——有了一个清晰而全面的认识。

posted @ 2026-02-04 18:22  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报