Deep Learning

Machine Learning RoadmapDrawn By SJTU-XHW

References: EECS 498.008 / 598.008 Deep Learning for Computer Vision;

Deep LearningImage ClassificationFirst Classifier: Nearest NeighborImprovementsHyper-parametersUniversal ApproximationSummaryLecture 2. Linear Classifier2.1 Definition2.2 Views & Shortcomings2.0 数学知识回顾2.0.1 The Definition for Entropy2.0.2 Joint Entropy & Conditional Entropy2.0.3 Cross Entropy & Relative Entropy (KL Divergence)2.0.4 Mutual Information2.3 Choose the Parameter 2.3.1 How to Choose Loss FunctionMulticlass SVM Loss补充:Regularization(正则化)Cross-Entropy Loss (Multinomial Logistic Regression)补充:Softmax 函数的数值稳定性2.3.2 How to Do OptimizationsRandom SearchFollow the Slop (Gradient Descent)Further: Stochastic Gradient Descent (SGD)Adaptive Learning Rate Algorithms: AdaGrad, RMSProp, AdamOptimizations With Regularization: Weight Decay & AdamWFurther: Second-Order OptimizationsSummaryLecture 3. Neural Networks3.1 Definitions3.2 Activation Functions3.3 Regularization in Neural Networks3.4 Revisit: Universal Approximation3.5 Gradient Descent in Neural Networks & Optimizations3.5.1 Computational Graphs & Back-propagation3.5.2 Patterns in Gradient Flows3.5.3 How to Implement Back-propagation: "Flat" Gradient Code (Scalars)3.5.4 How to Implement Back-propagation: Modular API (Scalars)3.5.5 How to Implement Back-propagation: Vector-valued Functions3.5.6 Reverse-Mode & Forward-Mode Automatic Differentiation3.5.7 Higher-Order Derivatives using Back-propagation3.5.8 ConclusionLecture 4. Convolutional Networks4.0 Concepts4.1 A Closer Look At Spatial Dimensions4.1.1 Padding4.1.2 Receptive Fields4.2 Other Dimensions4.3 Pooling Layer4.4 Classic Convolutional Networks

Image Classification

图像分类是计算机视觉要解决的核心问题之一。不过有一些问题:

但图像识别能作为其他图像处理任务的基石,例如:

实现方法:

 

于是引入机器学习:Data-Driven Approach;

  1. 收集一组图像和标签;

  2. 使用机器学习算法训练分类器;

  3. 在新的图像上评估这个分类器;

 

常见数据集是 MNIST(识别 28x28 灰度图片中的 0~9 手写数字,50K 用于训练,10K 用于测试),被称为 “CV 界的黑腹果蝇”,它很简单,但不能证明算法的优秀性;

还有一个常见数据集是 CIFAR-10(识别 10 类 32x32 RGB 图片,从生物到非生物 50K 训练、每类 5K,以及 10K 测试、每类 1K),虽然量小但难度大于 MNIST;以及 CIFAR-100,数据量和 CIFAR-10 一样,但有 100 类,其中 20 个超类每个里面有 5 个细分的类别。

还有一个图片分类的 “金标准” 是 ImageNet 数据集,包含 1000 类图片,约 1.3M 训练集、50K 验证集、100K 测试集。其中测试指标为 Top 5 Accuracy(算法预测可能性前 5 的 labels 必须有一个是正确的);

还有一些特殊的数据集针对某些特殊需求,例如 MIT Places 针对的是场景识别;而 Omnigot 则要求算法以尽量少的数据集得到尽可能精准的模型(1623 类图片,每类只有 20 张),这被称为 “low-shot classification problem”(小样本分类问题);

 

First Classifier: Nearest Neighbor

先来个最简单的算法(甚至不配叫深度学习算法):最近邻。

算法描述:

  1. 选取邻居数 k

  2. 对测试集中的每个需要被分类的样本 xi,计算到所有训练数据 xti 的距离(定义如上);

  3. 按距离数据排序,取最近的 k 个测试数据,根据这 k 个数据的 majority vote 决定当前的样本 xi 的类型;

这个方法有致命缺陷:训练 O(1),但测试 O(N)。现实中我们宁愿训练过程耗时,也不愿意测试的时间耗费多。

此外,使用曼哈顿距离并不是很聪明(只是把看起来相近的图片归为一类,例如白色背景的猫和白色背景的青蛙会被归成一类)。

我们引入 decision boundaries(决策边界)来描述,这幅图被称为 Voronoi Tessellation(维诺图,沃罗诺伊分割):

对于一个二维数据(例如 2 个像素的图像),点代表训练集,颜色代表标注的 labels。没有标注点的是其他的可能数据。而黑线就是按照模型算法判断的决策边界。所谓区分两个类型就是确定两个类的决策边界(黑线)。下次测试输入数据时查看落在哪块区域内即可。

某些异常情况(例如噪点)可能导致真实的分类边界参差不齐。

Improvements

改进 1:为了解决最近邻不准确的问题(直接使用最近邻居的 label),我们引申出 K-Nearest Neighbor(KNN),从 K 个最近邻居中选出得票最多的 label 作为测试数据的 label。这个改进我们可以从决策边界图看到:

可以看到,这使得模型的决策边界变得平滑,并且减小了测试数据受到个别噪声点的影响

但 K-Nearest Neighbor 的问题是可能存在投票平局的问题,此时在图中显示是空白的颜色。你可以使用 heuristics(例如 fall back 到一个的情况)。

总结:

K 过小:算法效率提升,但更易受到噪声点的影响;

K 过大:算法效率不高,且包含太多其他类的 votes,over-smoothing 分类结果,模型效果不佳;

改进 2:更改 Distance Metrics。在 KNN 的基础上,我们将判断图像相似度的距离指标从曼哈顿距离(p=1)更换为欧几里得距离(p=2),发现:

曼哈顿距离的边界都是由垂直或水平或 45 度倾角的线段组成;而欧几里得距离的边界虽然还是由线段组成,但是可以朝向任意方向(相当于用计算量换取更灵活准确的分类方式)。

这里虽然看不出来二者的优势究竟在哪,但是这告诉我们一个好消息:对于任何一个数据类型,只要给一个 distance matrics,就能将 KNN 算法应用上去!例如衡量两个 PDF 文件的相似性(作为 distance),可以使用 TF-IDF(Term Frequency-Inverse Document Frequency,词频-逆文件频率)similarity,它通常被用在一些 NLP 应用中。

TF-IDF 指出,字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降

前者因为它更能代表一篇文章的主题,后者因为它更有可能是停用词、或者是不能很好作出区分的词汇。

这再次证明了,不考虑性能的情况下,调整 KNN 的距离指标计算方法可以在不同的应用场景下获得很好的分类效果。

Hyper-parameters

另一方面,除了 training data,还有一类参数例如 邻居数 K、最佳的距离指标(各种距离还是其他算法?),它们被称为学习算法的 “超参数”(hyper-parameters),意指在训练前就应该为学习算法确定的(而不是训练时获得),能对算法的运作方式产生间接深远影响的参数。

通常情况下,超参数的最优取法与具体问题有关,没有 silver bullet;有几种做法:

Universal Approximation

由于 KNN 对于处理的函数几乎没有做前提假设,因此在训练集大到接近无穷时,KNN 甚至可以表示几乎任何常见的函数(有点像泰勒级数的逼近)。

问题是随着维度的增加,需要逼近真实解到同样程度的训练集大小会呈指数级增加,这被称为 “curse of dimensionality”(维度诅咒)。这是不可接受的,因为上面图的情况仅仅是一维的数据。如果是一张 32x32 的图片,训练集大小的数量级会在 232×3210308 左右。

正因如此,KNN 在 raw pixels 的图像处理领域很少被用到,总结:

但某些情况下(不是 raw pixels)KNN 能很好的识别图像特征,例如在深度卷积神经网络计算得到的特征向量(特征提取)中进行 KNN 并以此做图像分类/看图说话等等,可以得到很好的效果(从容应对 various viewpoints、bg clutter、illumination changes 等干扰因素),这个后面讨论。

Summary

优势:

劣势:

 

Lecture 2. Linear Classifier

2.1 Definition

线性分类器实际上是一种参数化方法(Parametric Approach),使用这种方法的分类器被称为 Parametric Classifier。考虑对图像进行分类的实质:

参数分类器将成为学习神经网络方法的基础。本节将正式介绍最简单的 parametric classifier pipeline 实例:线性分类器。

首先给出线性分类器的运算方法:定义 f(x,W)=W×flattenvec(x)+b;注意:

线性分类器还有一个特点:我们先忽略 bias 部分,f(x,W)=Wxf(cx,W)=cf(x,W),这告诉我们即便对图像乘以固定的 scale 也会影响最后的 score。但是实际上对图像所有像素乘以 scale 从人眼来看只是改变了图像的饱和度,并不影响辨认它的结果。(bug or feature?)

这个特点是否对效果有重要影响,取决于损失函数的选取,我们后面讨论。

现在我们从 3 个角度来理解这个分类器,并且从中感性地分析出它的缺点。

2.2 Views & Shortcomings

我们从线性代数的角度理解,实际上是将权重矩阵的每一行与输入图像的 flatten vector 分别内积并添加 bias。

从视觉角度理解,其实会发现 W 的每一行行向量就是每个分类的 “template vector”,再注意到将两个单位向量内积,得到的值越大二者相似度越高(余弦相似度,数学证明上暂时先放一放),所以线性分类器的做法是有意义的!

但同时我们从 template vector 中可以发现,实际上虽然我们想要做的是识别某个物体,但模型仍然对输入图像的细节、周边的 context 这些 evidence 关注较多(而不仅仅是图片中的物体本身),这可能会出现 “过拟合” 的嫌疑。

例如你将棕色的汽车放在草原中图片交给线性分类器,它很有可能会认为图中的物体是一头鹿。

线性分类器还有一个问题是同一个类型的不同模式的识别,例如训练数据中 “马” 类图片中马的不同朝向、“车” 类图片的不同颜色。主要原因是线性分类器对每个类只准备了一个 template,没有办法 disentangle 这些不同的模式(导致 template vector 将同一类型的不同模式的特征糅合在一起);

我们再考虑新的角度:代数几何的角度。我们对图中的任意一个像素,例如 (15,8,0),线性分类器所做的事本质上就是对这个像素取值与各个类型得分构建了函数关系,并且一定是线性的(由模型的线性性可知);

现在考虑扩展一下 “视野”,引入另一个像素,例如 (11,11,0),这样我们可以构成一个 contour graph(等高线图),其中高度代表得分,不同的类型则有不同的等高线图。然后对于某个类型的等高线图,我们可以画出得分为 0 的边界线(线性性所以是直线),沿这条线指向得分增大的方向指向的就是这个类的 template vector 所在的位置(最大相似度)。这个情况是 2 维的情况。

所以在几何的角度上,线性分类器就是使用多个超平面(hyper-plane)分割欧式超空间上的样本点。

但这个方法只能在低维上用作启发,因为高维度上的空间我们无法想象和理解,它是反直觉的。

在几何的角度,我们又能分析出来线性分类器对于某些特定的数据类型没法很好地分类:

它们在数学上有个统一的名称:线性不可分(non-linear separable)

类似地,我们回忆一下历史上第一个机器学习算法(感知机 perceptron),我们发现这个 perceptron 的数学模型就是线性分类器!

它的重要缺陷就是没法学习 XOR(异或)的数据关系,原因我们可以在二维图像上画出来,验证了之前我们在几何上的结论:

 

2.0 数学知识回顾

2.0.1 The Definition for Entropy

香农(Shannon)认为,一个事件的不确定性(或者称 surprisal / self-information)越大,信息量就越大;重复的 Message 传递的信息量更少。他引入了衡量信息量的量 —— Shannon Information Content;一件发生概率为 p 的事件的香农信息量由下面的式子给出:

SIC=log21p (bits)

事件的信息由 SIC(香农信息熵)衡量,那么 随机变量的概率分布 的信息 由什么衡量?

假定 X 是一个有限样本空间 X={x1,,xM} 的一维离散型随机变量。其概率在各个值的分布为 (p1,,pM),那么定义:该随机变量所遵循的概率分布的信息量大小由 信息熵(Information Entropy)衡量,大小由下面的式子给出

H(X):=E{log1p(X)}=xXp(x)log2p(x)

离散型随机变量概率分布负对数的期望。信息熵越大,意味着随机变量的概率分布的不确定性越强(在一维离散型随机变量的概率分布中的体现就是,随机变量的每个可能取值的概率接近),说明它的信息量越大。

通俗地说,信息熵描述了有多少信息是无法确定的。信息接收了多少,熵就降低了多少。

或者说,熵是要消解事件不确定性的平均代价

数学小提示:

limx0+xlogbx=0

你会在很多场合用到,可以通过换元法证明。简记为 0 log 0 = 0

2.0.2 Joint Entropy & Conditional Entropy

说完了一维的情况,我们再讨论二维,或者更高维的离散型随机变量的信息熵。

高维离散型随机变量的概率分布是联合概率分布,以二维随机变量为例,联合熵定义为:

H(X,Y):=E{logp(X,Y)}=xXyYp(x,y)log2p(x,y)

那么条件概率分布呢?这是我们已知条件(降低了信息熵)的基础上,求一件事物的信息熵。它的定义如下:

(X,Y)p(x,y),则:

H(Y|X):=xXp(x)H(Y|X=x)=xXp(x)yYp(y|x)log21p(y|x)=xXyYp(x,y)log2p(y|x)

式子的含义是,在所有给定 X 的条件下的 H(Y|X=x) 的期望。根据定义求解的方法是,找到 P(Y|X)P(X,Y),代入公式即可。

如果是给定 x 的情况下(也比较好理解,上面的 H(Y|X) 定义式不对 X 求和、p(x,y) 更改成已知 x 的条件概率即可):

H(Y|X=x)=yYp(y|x)log2p(y|x)

但是,最好理解的方法还是 “熵的含义”:熵是对事件的不确定性。我们已知了 X 事件的信息,就相当于消解了联合概率分布 P(X,Y)X 的信息熵,也就是从两个事件的联合熵中减去了 X 的信息熵,推导如下:

H(Y|X)=EX,Y{log21p(Y|X)}=EX,Y{log2p(X)p(X,Y)}=EX,Y{log21p(X,Y)log21p(X)}=EX,Y{log21p(X,Y)}EX{log21p(X)}=H(X,Y)H(X)

所以条件熵第二种算法,也是最简单、最通俗易懂的计算方法,就是 H(X,Y) 减去 H(X)

这就是信息熵的链式法则:

H(X1,X2,,Xn)=i=1nH(Xi|X1,,Xi1)

类比条件概率的链式法则:

P(X1,X2,,Xn)=i=1nP(Xi|X1,,Xi1)

说明熵的对数运算让乘除变为了加减。

以下是条件熵的特性:

2.0.3 Cross Entropy & Relative Entropy (KL Divergence)

那么,再考虑一种情况。我们之前都是正确掌握了随机变量的概率分布,并用这些信息构建了概率分布的信息熵的定义。

如果对于一个离散型随机变量,我们并不知道它的具体分布、只是模拟了近似的概率分布。这样的信息和真正的概率分布有差距,那这个概率分布的不确定性又该如何描述?

人们定义了 “交叉熵” 的概念,用来描述:在实际概率分布为 p,但却作出概率分布 q 的假设下,消解该随机变量不确定性的平均代价就是交叉熵,定义如下:

CE(p,q):=Exp(x){log21q(x)}=xXp(x)log2q(x)

准确的定义是,对于同一个事件集上的两种概率分布 pq 间的交叉熵,描述的是:如果编码方案采用针对 q 的优化,而非真实分布 p 的优化,那么平均需要从事件集中取出多少 bit 才能发现真实分布的情况

这就相当于我们不知道 p、但先当作 q 估计的时候,所要了解事件的真实分布需要的平均代价

现在假设我们后来知道了 p,那么我们由于原来作出的假设 q 分布,而造成与现实情况的偏差,导致要花费的更多的代价才消解不确定性。那么如何衡量这个代价的增量?这就是相对熵(Relative Entropy),又称 KL 散度(KL Divergence)

D(p||q):=CE(p,q)H(p)=xXp(x)log2p(x)q(x)

显然 D(p||q)D(q||p)

数学上,可以用优化理论证明,D(p||q)=0 当且仅当 p=q 成立。

交叉熵和相对熵在机器学习领域(例如损失函数)使用颇多。它可以描述 当前估计的数据分布 相较于 真实数据分布 的偏差情况。

2.0.4 Mutual Information

如果两个随机变量不是相互独立的,那么它们必定在概率和信息熵上相关联。

就是说,一旦两个随机变量不相互独立,那么得到一个随机变量的信息,就会影响另一个随机变量的信息熵(就是从熵的角度理解两个非独立随机变量的条件概率)。

如何描述这两个随机变量的 “信息关联性” 呢?

人们定义了 “互信息” 的概念:一个随机变量所携带另一个随机变量信息的信息量。这个携带的信息量越大,那么这两个随机变量的相关性越强。

I(X;Y):=D(p(x,y)||p(x)p(y))=xXyYp(x,y)log2p(x,y)p(x)p(y)

这个定义的含义是,假设分布 p(x)p(y)(二者独立)相对于 真正的分布 p(x,y) 的偏差代价。

除了直接计算 P(X,Y)P(X)P(Y),我们还能通过推论更直观的方法理解互信息:

I(X;Y)=H(X)H(X|Y)=H(X)+H(Y)H(X,Y)

两个信息熵相交的部分。

2.3 Choose the Parameter W

现在我们如何确定模型参数 W

如果我们随机地选取一个 W,然后运行线性分类的算法,得到的得分向量的值不能向我们指出正确的分类情况。因此我们需要通过训练来确定合适的 W,这里就需要确定两个问题:

进一步形式化这个问题:

这就转换成了优化问题,我们需要最小化目标函数(Data Loss):w=argminwL(w)

首先我们现在研究 “选择什么样的损失函数对特定问题更合适” 这个问题。

2.3.1 How to Choose Loss Function

Multiclass SVM Loss

这个损失函数的思想是,正确的类型的得分,会远远高出不正确类型的得分。即不关心具体得分,只关心能否给出准确的 label。

注:我们将使用 Multi-class SVM Loss 作为损失函数的线性分类器称为 “SVM classifier”;

对于一个图像样本 xi 而言,我们记客观上正确的分类 label(即 ground truth)为 yi,其他参与选择的类比标签为 l1,l2,,lN

Multi-class SVM Loss 给出:一个算法对 “ground truth 的得分大小和其他错误类比的得分大小关系” 与损失值的关系。绘图如下:

解读:当一个算法对 ground truth 判断的得分高出其他错误得分的差值越小(小于 margin 时),损失值会线性增大。

这种含有线性区域和零区域(水平区域)图像的损失函数,被称为 “Hinge Loss”(看起来像 door hinge);

我们记 sik 表示对于样本 xi,模型在类别 k 的评分。

因此这个损失函数的数学表达式可以写成:Li=kyimax(0,sijsiyi+Δ),其中 si=f(xi,W) 为模型计算得分,yi 是对应 xi 的 ground truth,Δ 就是上面所说的 “margin”,也就是 SVM 希望正确类别应该高出不正确类别的得分差。

解读:所有对于任意样本 xi,评分系统 sxi 上的损失 Li 由所有不正确类型的得分(sik, kyi)和 ground truth 得分(siyi)决定。

意味着只有正确类别比其他每个类别得分都要高出 Δ 才能实现无损失。

注意到:在一开始没有进行训练时,一个样本的损失值大约为 (C1)Δ,其中 C 为分类数。因为 sijsiyi0

为什么需要知道这个?因为我们需要一些 debugging 的手段(大概知道自己的模型有没有写错)。

显然,Δ 就是 SVM classifier 的超参数。

拓展:Δ 应该设置成什么值,是否需要 cross validation?这里先给出答案,你可以先看 “正则化” 一节后再来理解:

事实证明,在任何情况下都可以放心地将这个超参数设置为 Δ=1.0。超参数 Δ 和正则化参数 λ 似乎是两个不同的超参数,但实际上它们控制着相同的权衡:目标中 Data Loss 和正则化损失之间的权衡。理解这一点的关键在于,权重 W 的大小对分数(以及它们之间的差异)有直接影响:当我们缩小 W 内的所有值时,分数差异会变小,而当我们扩大权重时分数差异就会增大。因此确切值(例如 Δ=1Δ=100) 在某种意义上是没有意义的,因为权重可以任意缩小或拉伸差异。因此,唯一真正需要权衡的是正则化参数 λ)。

Tips 1:如果把上面损失函数的求和换成求平均,这个分类器会不会有明显变化?答案是不会,因为将所有样本损失值同时乘以 1(C1)Δ,仍然是单调映射,不会改变对某个类别的 preference。不过总体来说训练效果会稍差一点,因为损失值的差距被减小了。

Tips 2:如果将损失函数换成 Li=kyimax(0,sijsiyi+Δ)2 呢?答案是会有明显不同!这是个对损失值进行了非线性映射,对每个类别的 preferences 会出现非平凡的改变。

Tips 3:假设我们找到一个 W 使得 i,Li=0,那么这个 W 唯一吗?答案很简单:不唯一,因为如果 W 满足,那么 2W 也满足。

补充:Regularization(正则化)

在上面的 Tips 3 中,我们知道多个不同的模型参数 W 可以得到相同的损失值,那么我们如何表示在这些参数间的 preferences 呢?我们就需要超越原本的损失函数,引入新的评价指标。这就是正则化

可以将正则化理解为,向目标函数(Data Loss)添加一部分,这部分不取决于训练数据,主要用来区分原来相同损失值的参数,也就是说,防止模型过拟合,阻止模型在训练数据集上 “做的太好”。例如新的 Data Loss:

L(W)=1Ni=1NLi(f(xi,W),yi)+λR(W)

这里 λ 就是正则化参数(也是模型的超参数),R(W) 是正则项。

感性理解:让模型在训练时做些其他事,防止仅仅为了获得更低的损失而拟合。

对于线性模型(linear models),R(W) 通常可选:

之后我们在神经网络模型中还会遇到例如 Dropout、Batch Normalization、Cutout 等等正则项的形式。

总结一下,目的具体如下:

Cross-Entropy Loss (Multinomial Logistic Regression)

上面的 Multi-class SVM Loss 对应的损失函数只是简单地说明 “正确的类型有更高的得分”,并没有对模型具体得分的含义进行解读。如果我们希望让模型计算出的得分代表 “当前类别是正确的概率”,诸如此类的 preferences,则可以通过更换损失函数来实现。

这里介绍另一个经典的损失函数:交叉熵损失函数,使用它来做分类的方法又称为 “多项式逻辑回归”。

多项式逻辑回归是一种分类方法,它将逻辑回归概括为多类问题,即具有两种以上可能离散结果的问题。

也就是说,它是一种模型,用于预测给定一组自变量(可能是实值、二元值、分类值等)的分类分布因变量的不同可能结果的概率。

多项式逻辑回归还有很多其他名称,包括多项式 LR、多类 LR、softmax 回归、多项式 logit(mlogit)、最大熵(MaxEnt)分类器和条件最大熵模型。

多项式逻辑回归适用于因变量为 nominal 的(等价于分类变量 categorical,即两两间不能以任何有意义的方式排序的类别中的任何一类,通俗来说是不可比)。

我们将使用 Cross-Entropy Loss 的线性分类器称为 “Softmax Classifier”;

对任意一个样本-标签对 (xi,yi),记它在模型下的各个类类别的得分向量为 Si=(si1,si2,,siN)=f(xi,W),我们可以将这些分数翻译为概率:

P(Y=k|X=xi)=esikjesij

softmax 函数:f(zi)=ezik=1Kezk(之所以称之为 “softmax”,是指 “max” 函数的可导的近似函数,a soft differentiable approximation to hard max function);

  1. 可以让向量的各项介于 0~1 间,并且总和为 1(不至于像 max 函数一样大部分是 0,这使得网络中的一些计算难以进行);

  2. 可以借助指数函数特性(ex),让两个不同的数值 zi 大的更大、小的更小(凸显不同数据的差异);

  3. 其导数有很好的性质,方便计算交叉熵损失函数;

它在 “需要计算 max 特征”、“需要可导”的这些场景下比较有用。

注意到这是 softmax 函数。我们考虑模型对 (xi,yi) 而言 k 标签的打分 sik 对应的损失值,组成得分向量 Si=(si1,si2,),通过 softmax 函数规范后为概率向量:Pi=(pi1,pi2,);易知 pik=P(Y=k|X=xi)=esikjesij;设 ground truth 的对应单位向量为 Ti=(0,0,,1,0,)(第 yi 项为 1 的单位向量),则我们只需要将 Tipik 的差距比较一下就能衡量。

我们回想一下,在概率学中,想要衡量 “在实际概率分布为 p,但却作出概率分布 q 的假设下,消解该随机变量不确定性的平均代价”,这不正是交叉熵的定义吗!因此我们使用交叉熵作为模型打分的损失函数,其中输入实际概率分布由 softmax 得出。

Li=CE(Pi,Ti)=kKt(k)logp(k)=logpiyi=logesiyijesij=siyi+logjesij

注意到 yixi 这个样本提供的 ground truth;

交叉熵损失值最小为 0(一般不会达到),最大为 +

注意到有意思的点:和 Multi-class SVM Loss 不一样,只有模型输出是 one-hot encoding(无比确定是某一类),并且恰与 ground truth 一致,损失值才会为 0。而前者只要求正确的类型得分远高于错误的类型得分。

debugging trick:在一开始没训练时(W 随机),一个样本的损失值大约为 logC(每个类型的概率相当);

补充:Softmax 函数的数值稳定性

在实际场景中,计算 softmax 函数的 ex 作为分母很可能导致数值不稳定性(numerically unstable),这样的问题同样会发生在 “绝对值极小的小数除以另外一个极小的数” 这种情况上,这往往是因为计算机保存数字手段的局限性。

你可以在 Python 中试试:

很不幸地,p 由于出现了 inf 溢出没法继续正常参与运算了!

我们一般需要通过一些标准化的措施(例如数学变换)来缓解这样的问题,就 softmax 这个函数而言,一般的做法是同乘一个任意非零常数 C

esiyijesij=CesiyiCjesij=esiyi+logCjesij+logC

并且取 logC=maxjsij,这样能让分数向量 S 的最大元素是 0,极大降低上面数值不稳定问题的发生可能。

在 PyTorch 里面,有专门帮助 softmax 这样可能出现数值稳定性问题的运算函数避免问题发生,例如 torch.logsumexp(),里面就实施了一系列保证数值稳定性的措施,你可以放心地调用它来计算 logjevj

 

2.3.2 How to Do Optimizations

现在我们为线性分类器模型形式化了优化问题、建立了损失函数的数学模型。现在我们需要了解如何解决这个优化问题,也就是说,如何利用损失函数来优化获得更好的 W

回想之前优化问题的定义:w=argminwL(w),现在我们将 L(w) 看作一个 “传入权重矩阵,输出总体标量损失值” 的函数,然后在此基础上讨论如何求解这个优化问题。由于 L 在目前讨论范围内是一个凸函数,因此我们可以用凸优化的方法求解。

随机生成参数矩阵 W。每次生成后计算损失,重复多次后找到最小损失的 W,对于 CIFAR-10 的成功率保持在 15% 左右。

注意到随机生成实际上不能解决我们的问题,我们需要更加智能的算法。

Follow the Slop (Gradient Descent)

另一种比较常用的方法是梯度下降,我们一般可以通过这个方法至少获得一个局部最优解。

回忆一下多元微积分中的方向导数、梯度的定义。

方向导数:函数定义域的内点对某一方向求导得到的导数。以二维函数为例:

f(x,y)=lim(Δx,Δy)0f(x+Δx,y+Δy)f(x,y)Δx2+Δy2=fxcosα+fysinβ

方向导数代表了在某点 P0(x0,y0) 处沿某方向 v=(v1,v2) 的斜率值:

fv|P0=(fx,fy)(v1,v2)=|f|P0cosθ

注意到:

我们梯度下降算法就是给定步长,从某点选择梯度下降最快的方向迭代前进,直至梯度消失

不过我们给定的样本数据是离散的,如何在离散的数据集上计算?

一种方式是 Numeric Gradient(数值梯度)。我们建立一个与 W 形状相同的矩阵作为近似梯度,对 W 的每个元素(element-wise)加上一个偏移量 h,即 W+dW,计算出 Gi=dLdWL(wi+dwi)L(wi)h,最终 G 记为近似梯度。

问题:

 

另一种方法是 Analytic Gradient(解析梯度)。这里因为我们研究的是线性分类器,因此从解析式的角度考虑准确的情况是可能的!以后我们讨论任意复杂的模型时,解析法可能就不再好用了。目前我们只想得到 WL,恰好 L 函数我们又比较熟悉(softmax 函数容易求导):

L(W)=1NiLi(f(xi,W),yi)+λR(W)=1Nilogeiyijesij+λR(W)

如果解析解可行的话,我们通常同时使用解析梯度和数值梯度,并使用后者验证前者(gradient check)。

我们可以在 PyTorch 框架中看到 pytorch.autograd.gradcheck(func, inputs, eps, ...)(还有 gradgradcheck,检查二阶导)这样的方法来做梯度验证。

不需要验证三阶导,因为 PyTorch 的实现方式,只需要验证 gradcheckgradgradcheck,任意阶导的正确性就能保证了。

现在考虑 Gradient Descent 算法的整体框架:

算法本身比较简单,不过有一些超参数:

这些超参数和 KNN 的超参数不一样,后者在实践中调整的必要性和重要性不大,但是梯度下降的超参数会极大地影响模型的 performance 和效果(有时能达到 30% 左右的差距)。

其中,迭代步数在大多数深度学习应用中越大越好,主要的限制在于你的计算资源的预算以及希望的时间开销。

学习率决定了一次迭代前进的步长(模型一次学习多少),有一种方法是让它取决于当前函数的梯度大小。梯度较大时学习率越大,梯度较小时学习率小(接近最优解时不再 “大步流星”,而是“小碎步”接近)。

Further: Stochastic Gradient Descent (SGD)

上面的梯度下降算法实际上有个问题,在执行 compute_gradient 时,如果是解析梯度,那么就需要计算:

WL(W)=1Ni=1NWLi(f(xi,W),yi)+λWR(W)

如果数据集相当大(上亿),那么计算仅仅一轮迭代就要花费相当长的时间。于是人们想出一个好办法:仅仅从样本中抽出一部分(被称为 batch)来计算梯度,而这部分的大小被称为 batch size,伪代码如下:

使用数学表达:wk+1=wkαwL(wk)

原来的方法被称为 “full batch”。这时我们再次引入了两个超参数:batch size 和 采样方法(sample_data)。其中 batch size 人们在实践中一般使用 32 或 64 或 128 这样的数。

注:Batch Size 本身受限于 GPU memory 大小,过小的 batch size 无法起到很好的采样效果。不过也不是越大越好。有相关研究表明,在某些情景下过大的 batch size 会导致收敛到最优值的速度减慢,甚至无法到达最优值。

采样方法可以是随机选取,或者每轮 shuffle 再取,在图像分类这一情景下区别不大。但是在 structured prediction problem(结构化预测问题)、triplet matching problem、ranking problem 这些情景下,采样方法就比较重要了。

为什么这种方法被称为 “Stochastic”(随机的)?

我们可以将损失计算视作对全样本空间数据分布(pdata)的损失值期望的求解,然后我们实际上是通过取样来估计这个期望(蒙特卡洛估计):

L(W)=E(x,y)pdataLi(f(xi,W),yi)+λR(W)1ni=1nL(f(xi,W),yi)+λR(W)

但是 vanilla SGD 算法在某些场景/问题下效果不佳。

为了缓解以上问题,我们在训练神经网络时通常使用 SGD + momentum(有动量的、有惯性的)的策略。这里我们借鉴了物理学的思想,每次步进不是直接使用梯度,而是让梯度作为 “加速度”,来改变 “速度”(ρ 为 “摩擦系数”,也是超参数,通常取 0.9 或 0.99),进而让 “速度” 来驱动模型的步进。

vt+1=ρvt+WL(xt)xt+1=xtαvt+1

对应伪代码:

我们可以感性地了解为什么有动量的 SGD 可以缓解上述问题:对于 pool conditioning 的损失值分布,曲线会更加平滑、收敛也会更快(想象自己放下了一个遵循物理定律的小球);对于含有鞍点的损失值分布,模型能够有概率跨过鞍点的限制;对于取样噪声,则会因为速度的引入而被极大地克服。

还有另外一种处理方式,也就是更新速度时使用 look ahead 的方法(事先更新下一轮的损失值,让学习率参与其中):

vt+1=ρvtαWL(xt+ρvt)xt+1=xt+vt+1

这种方法被称为 Nesterov Momentum,它能够但是这样的计算方法没法很好地利用一些优化的函数。因此通常情况下我们会做一些数学变换,设 x~t=xt+ρvt

vt+1=ρvtαL(x~t)x~t+1=x~tρvt+(1+ρ)vt+1=x~t+vt+1+ρ(vt+1vt)

对应下面的伪代码:

对比一下两种动量实现的优劣势:

Nesterov Momentum 优势:

  1. 更快的收敛速度

    • Nesterov Momentum 在计算梯度时,先基于当前动量方向进行一次“预更新”(临时参数位置),再在此位置计算梯度。这使得梯度方向更贴近实际更新后的参数位置,减少了震荡,尤其在凸优化问题中理论收敛速率更优(从 O(1t) 提升到 O(1t2))。

    • 对于非凸问题(如神经网络),实践中也常观察到更快的收敛,尤其是在高曲率或复杂地形(如“峡谷”形损失面)中。

  2. 更稳定的更新方向

    • 通过预更新后的位置计算梯度,能更早感知参数更新方向的变化,减少过冲(Overshooting)风险,尤其在动量系数较大时表现更稳健。

  3. 更好的逃离局部最优能力

    • 提前的梯度计算可能帮助跳出局部极小或鞍点,因为动量方向结合了未来位置的梯度信息。

Nesterov Momentum 劣势:

  1. 实现复杂度略高

    • 需额外计算临时参数的梯度,但在现代深度学习框架(如 PyTorch、TensorFlow)中已封装,实际使用无显著差异。

  2. 超参数敏感性

    • 虽然动量系数通常与普通 Momentum一致(如 0.9),但最佳学习率可能需要微调,尤其在噪声较大的数据分布下。

  3. 理论保证局限

    • 对非凸问题的理论分析较困难,实际效果依赖任务和数据特性。

总而言之,我们在多数场景下应该优先使用 SGD + Nesterov Momentum 的方案。

Adaptive Learning Rate Algorithms: AdaGrad, RMSProp, Adam

上面我们讨论的 SGD + 各种 Momentum 的方法都是固定学习率的,我们来考虑如何将学习率也加入优化的讨论范围中。

之前我们提到了超参数 “学习率” 的一种取法,就是随着当前梯度大小来决定学习率大小。这种思想就是 “自适应学习率优化算法”(Adaptive Learning Rate Algorithm),主要针对不同参数的历史梯度信息来调整学习率。

现在我们在 SGD 的基础上考虑一种新的自适应学习率的优化策略:让 learning rate 除以历史梯度平方累积的开方,作为当前轮的实际学习率。这种方法也可以缓解 overshooting 的现象,并且在一定程度应对 pool condition 的损失面。

这种策略被称为 AdaGrad,其计算方法如下(在普通 SGD 前提下引入自适应学习率):

Gt=tw2L(w)wt+1=wtαGt+εwL(w)

它的思路是,在梯度变化剧烈的方向上减缓(damped)学习,在梯度变化剧烈的方向上加速(accelerated)学习。

至于为什么要累积历史梯度的平方,一是想消除符号影响,避免累积时正负抵消(就像方差一样);二是想放大梯度的影响,使算法更关注那些波动较大的参数。

至于为什么除以累积的开方,一是想保证量纲一致性,二是稳定学习率更新幅度,防止避免学习率过早衰减到零。

至于为什么要加上 1e-7,是为了防止除零问题。

但这么做还是有问题,因为随着训练时间延长,grad_squared 会不可避免地非常大(单调增长),这会导致在很多情况下,到达最优解之前学习率减为 0。

于是人们在 AdaGrad 的基础上提出 RMSProp 来缓解这个问题。RMSProp 的思想是仍然计算梯度平方的历史累积值,但是让历史值自身逐渐衰减,防止学习率快速衰减(就是普通 SGD + 历史衰减的 AdaGrad):

Gt=δGt1+(1δ)w2L(w)wt+1=wtαGt+εwL(w)

这可以理解成一个 “leaky AdaGrad”,每次丢失一定比例的历史梯度累积数据,保证学习效果。实践证明,在一般的损失面上,RMSProp 比 SGD + Momentum 的收敛速率慢点,但是很少有 overshooting 的情况。

那么,为什么不强强联合,让 RMSProp 和 SGD + Momentum 一起使用呢?是的。人们提出另一种优化学习率的方法就是将 RMSProp 的思想和 Momentum 的思想结合,这被称为 Adam 算法。其大致思想如下:

这样就可以了吗?不对,考虑一种情况,β2=0.999,此时在优化刚开始的时候(t0)moment2 会接近 0,这会导致在一开始时出现很大的学习率,这样的结果通常比较糟糕。于是人们在此基础上添加一个 bias(SGD + Momentum + KMSProp w/ bias):

gt=wL(w)=ft(w)+λR(w)vt=β1vt1+(1β1)gtmt=β2mt1+(1β2)gt2v~t=vt1β1tm~t=mt1β2twt=wt1αv~tm~t+ε

引入了随时间负指数变化的偏移量 moment1_unbiasmoment2_unbias 有助于克服原来算法在 t=0 处可能出现的学习率过大的问题。

好了!目前这个 Adam 算法,加上常用的超参数(β1=0.9, β2=0.999, α=103 or 104 or 5×104)已经能够用在大多数深度学习的模型上(各种类型的任务,不仅仅是图像分类),为他们提供不错的自适应学习率的优化效果。

Optimizations With Regularization: Weight Decay & AdamW

这一节我们讨论优化的时候都是将 L 损失函数作为一个整体来做求导操作。现在我们再次考虑正则化的问题。

我们在选取损失函数时曾经提到,可以在损失函数中使用正则化项来对模型进行正则化:L(w)=Ldata(w)+λ2R(w),如果取 L2 Regularization,那么在 Gradient 中就要加 λwt,这在 SGD 和 SGD + Momentum 中是一样的:

wt=wt1αLdata(w)αλwt1

人们常将正则项写成 λ2R(w),就是因为常常取 L2 Regularization,为了在运算梯度时不需要前缀一个 2 这个常量。

这时,正则项就成为 “削减” 权重的项,因此这种传统的正则化手段又称为 权重衰减(Weight Decay)

我们注意到,这种正则化方法有个缺点,就是权重衰减项是和学习率耦合的!如果我们采用自适应学习率优化算法,那么正则化的效果势必会受到学习率 / 历史梯度数据(如果用的是 Adam 这样的自适应学习率算法)的变化的影响,正则化效果不稳定。

因此,为了防止 Adam 这样的自适应学习率优化算法无法稳定地正则化,人们基于 Adam 设计出了新的方法:AdamW。它的思想是将权重衰减项(正则化项)与梯度更新(学习率)解耦,也就是,将权重衰减项不再放在损失函数中实现(损失函数中不包含正则化项,只含原始 Data Loss),而是在更新权重的优化算法中直接引入权重衰减项:

人们发现:

Further: Second-Order Optimizations

上面我们讨论过的优化算法 SGD 及其变式,其实都是一阶优化,也就是按照损失面的一阶(偏)导来决定更新的方向,这相当于对原来的损失面采用了线性近似的方法来训练和迭代权重。

不过实际上我们也可以进行二阶优化,相当于用二次曲线/二次高维曲面和 Hessian Matrix 对损失面做二次近似,这在某些情况下可以提高收敛速率并提高鲁棒性。我们用在 w=w0 处的泰勒级数展开损失函数:

L(w)=L(w0)+(ww0)TwL(w0)+12(ww0)THwL(w0)(ww0)

其中 Hw 是函数 L 的 Hessian Matrix(二阶偏导矩阵),于是最优解 w=w0HwL(w0)1wL(w0)

这种方法被称为 L-BFGS 二阶优化方法。

但这种方法并不常用,主要是因为 Hw Hessian Matrix 元素数量级在 O(N2),计算一次它的逆矩阵时间复杂度 O(N3),而 N 的数量级是上亿级别的,这个时间开销是不可接受的!因此 Second-Order Optimizations 通常用在低维数据优化上,大模型上基本难以见到。

Summary

总而言之,Adam 和 AdamW 是一个在各个场景下比较通用的优化算法;SGD + Momentum 有时可以超过 Adam,但是需要更多调优(tuning)的工作。

如果你面临的问题是低维的,或者并不服从概率学规律(不是 stochastic 的),你可以尝试使用 L-BFGS。

 

 

Lecture 3. Neural Networks

在 2.2 中,我们已经认识到了线性分类器的局限性:

人们曾经想对线性分类器进行改进,试图克服上面的局限性。一种解决方法是 Feature Transforms,它主要想解决线性分类器对特定数据类型无法划分的问题。

它的思想是,通过对样本数据(例如以半径为分布特征的数据)进行某些数学上巧妙的变换(例如直角坐标到极坐标),从 original space 变换到 feature space,最终让线性分类器有办法分类它,有种专家系统的意味在里面。这种技巧被用在很多机器视觉算法中(例如 color diagram、Histogram of Oriented Gradients (HoG))。

这种方法的问题也很明显,就是你需要非常了解样本数据的特征,并且还需要非常了解各种变换的数学手段。

在图像领域,人们还讨论了 bag of words 方法,试图通过构建 “code book of visual word” 来不依靠专家指定转换方程。人们甚至可以将 color diagram、HoG、bag of words 结合起来作为一个图片对应的特征向量,以此利用线性分类器的效果会更加好。

这个过程就被称为 特征提取(feature extraction)

这种特征提取 + trainable model(线性分类器)的方法也能组成一个分类器流水线(classifier pipeline),但是它仅仅能在 model 这一 final layer 中 tuning 自身(训练权重),而特征提取的 layer 则没法优化自身的参数。这也说明了这个模型有待改进。

因此,人们希望构建出一个 end-to-end pipeline,接受 raw pixels,输出类别的评分,并且实现系统内全部参数自调节(不仅是含有线性分类器的这个 layer),这就是神经网络最初的目标,它会同时学习特征提取和表示,以及像线性分类器一样的多个参数。

3.1 Definitions

现在对比之前的线性分类器 f=Wx, xRD,WRC×D,我们引入具有两层的神经网络作为新的数学模型:

f=W2max(0,W1x),W2RC×H, W1RH×D, xRD

其中 H 被称为隐藏层大小,并且每个矩阵乘法运算间(即每个神经网络层间)在实际实现时,默认存在一个 bias 偏置量(例如 f=W2max(0,W1x+b1)+b2),只不过为了理论讨论方便而省略。在这个基础上,我们甚至可以构造出一个 3 层神经网络:

f=W3max(0,W2max(0,W1x))

问什么神经网络要这么设计(max 函数、多次矩阵相乘)?我们等会来分析一下。

回到那个两层神经网络的例子,每层神经网络中的权重(Wi)则表示第 i1 层的元素如何影响第 i 层的元素(通过神经元激活的方式),例如 W[i,j] 表示 xj 如何影响 hi

如果我们运算的方法就是上面的矩阵乘法(就是上面定义的方法),也就是说上一层的每个元素都能影响到下一层的每个元素,这时我们称这层网络为全连接网络(fully connected network),这种网络结构也被称为 “多层感知机”(Multi-Layer Perceptron,MLP)

那么 f=W2max(0,W1x) 具体应该如何理解呢?看着上面的图片,第一层参数 W1 的学习可以类比线性分类器,就是在学习 template vectors,但是因为隐藏层的数量实际上多于分类数,因此能够解析出更多的 template vectors,相当于增强了模式匹配的能力(能够 disentangle 之前同类型数据不同模式,例如能够把朝向右边的马和朝向左边的马划分出两个 template vectors),这就在一定程度上证明神经网络可能会克服线性分类器的一些固有缺陷。

但大多数情况下,这个划分不一定是 human interpretable 的,可能只是有一些特殊的 spatial structures。

这就是神经网络神奇的地方,你并不知道具体需要学习哪些特征,只是通过优化的方法让这个分类器学习那些能最大化分类准确性的特征。

神经网络就是利用这些 “distributed representations”,通过第二层矩阵乘法对这些 template vectors 再次进行线性组合,试图将这些同类不同模式的 template vectors 打分更高。

这些第一层网络学习特征的关键就是图像中的 “oriented edges”(边缘朝向)和 “opposing colors”(高对比度的颜色)。类比人类的视觉识别,在暴露在不同图像曲线的情况下大脑中的不同部分的神经元会被差异化地激活,并且这些激活的情况和图像的边缘朝向有强烈关系。

第二层网络则是学习组合这些特征来解释图片中出现不同结构的类型,这样说神经网络似乎直觉上就更有合理性一些。

这些中间学习到的抽象的、不是 human interpretable 的特征信息的层又称为 “隐藏层”(hidden layers)

这期间可能学习到一些冗余的信息,我们后续可以通过正则化,或者手动设计网络结构/filters、微调/量化 来缓解这个问题。

在这个基础上,人们就不由得会尝试泛化到更多层神经网络,这就是深度神经网络(Deep Neural Networks

深度神经网络的深度就是包含可训练的参数矩阵 Wi 的个数(上图就是 6 层),宽度就是每层网络(隐藏层)的单元 / 特征维度的大小

 

3.2 Activation Functions

现在我们回到之前的问题上,全连接神经网络 f=W2max(0,W1x)max 究竟代表什么含义?这个问题非常敏锐、精妙(astute)地把握住了神经网络的核心特征(或者说组成部分)。它就是激活函数Activation Function)。f(x)=max(0,x) 被称为 ReLU(Rectified Linear Unit,修正线性单元)函数,和它类似的被用在神经网络激活函数方面的数学函数还有:

我们从两个方面考虑为什么需要激活函数、为什么要这么设计神经网络。

 

但人工模仿的神经元和生物神经元间还是有相当大的差异,例如不同的连接方式、不同的神经元类型、每种神经元处理信号的方式等等。

我们不能从理论上说明这样做的正确性以及有效性。那么为什么我们还是选择这种方式来构建网络作为分类器呢?

另一种视角的说法是 Space Warping(空间扭曲)

回想我们在线性分类器中讨论过代数几何的视角,每个数据点都对应于高维空间中的某点,线性分类器的参数矩阵一行则代表高维欧式空间的一系列超平面的划分,沿这个超平面垂直方向前后移动则代表对应类型得分的增减。

线性分类器做的参数矩阵乘法,就相当于对整个数据空间以这些垂直方向为新坐标系进行的线性变换(变成 “feature space”)。

如上图,由于变换的线性性,原来非线性分布的数据点,在关于特征超平面的线性变换后仍然是非线性的,因此这种数据始终没法使用线性分类器很好地描述与分类(线性不可分的)。

这也解释了为什么之前说 deep linear networks 对增强模型描述特征的能力方面没有任何帮助。

但是如果我们将激活函数作用于数据点后,数据空间中负值象限(注意这里只是方便理解,实际在高维空间很难这么描述)的所有数据点全部 collapse 到坐标轴或者原点上:

我们发现原来线性不可分的数据分布在 ReLU 的转换下变得线性可分(在 feature space 下线性可分。原来的 decision boundary 在 input space 中不再是线性的了),我们可以通过简单地再加一层线性分类器就能出色地完成分类任务!

这个例子有力地说明了神经网络及其非线性的激活函数,相较于线性分类器有更强大的特征描述和分类能力。这也是激活函数、神经网络更有意义的原因。

我们再继续从感性角度理解一下多层神经网络的意义。对于一组特定的二维数据,不同深度的神经网络能达到的分类效果:

注意到更多的层数意味着 decision boundary 可以描述的特征越复杂。

这里敏锐的读者可能发现一个问题,层数越深(模型复杂度越大),网络描述细节的能力越强,那么深层神经网络在层数很大的时候会不会出现过拟合的现象?尤其是数据集中的噪声影响,所以和线性分类器一样,这涉及到了模型的正则化。

3.3 Regularization in Neural Networks

在神经网络中如何进行正则化?神经网络中的正则化在感性上来看是什么样的?

首先明确一点,在神经网络中,我们不应该对网络的大小(即宽度和深度的统称,理解为网络的架构)直接正则化,而是在网络中设置一些 tunable regularization parameters(可微调的正则化参数),然后直接对参数调整。

这时候我们可以复用之前定义的 L1、L2 regularization / elastic net 这样的正则函数。同样的,它们的作用是 smoothen decision boundary、降低模型冗余复杂度、防止过拟合、表达对 unseen data 的 preferences,并在某种程度上提升模型分类效果。如下图所示:

 

3.4 Revisit: Universal Approximation

在建立对神经网络的直觉后,我们发现神经网络能够描述比线性分类器更多、更复杂的分类情况。我们将这个特性规范化一下,称之为网络的拟合通用性。

数学上可以证明,含有一个隐藏层的神经网络可以以任意精度,拟合任何函数 f:RNRM

这里实分析会定义 “任意精度”、说明函数的连续性、RN 的实子集等等,这里略过理论知识。

这里我们认为神经网络模型已经本质上超越了线性分类器模型,拟合任意线性不可分的数据情形。我们看一个最简单的例子,对于一个 f:RR 的两层神经网络,其运算表达式:

y=u1max(0,w1x+b1)+u2max(0,w2x+b2)+u3max(0,w3x+b3)+p

注意到它们是一系列非线性函数的线性组合,这样只要我们将非线性函数作为 “基底”,且 “基底” 足够多,总能拟合出目标函数。

再以 ReLU 激活函数为例,假设需要拟合一个 bump function,我们一定可以由四层神经网络完美拟合:

根据信号与系统的知识,我们知道由多个这样的 bump function 作为基本单元并线性组合,就能拟合出任意连续函数。也就是说,只要神经网络的隐藏层足够多,这样的基本单元就足够多,我们就能够更准确地拟合各种信号,这也从直觉上解释了神经网络多层的意义。

这里只是直觉上。实际理论上确实可以证明它的通用性,但需要严格证明,如何适用于其他非线性函数、如何适用于更高维的情况,事情就比较复杂了。感兴趣查看:Proof

不过上面只是理论上神经网络可以拟合任意非线性情形,没有告诉我们如何设置这些权重、需要多少数据集以多少计算复杂度来得到这个结果。因此不能因为它具有这个性质就说神经网络是最好的数学模型(就像 KNN 有 Universal Approximation 性质一样)。

但实际训练这些神经网络时,它们在收敛时并不会像理论那样(比如组合出所谓的 bump function),而是某个解决方案,但实际效果也确实比线性网络更好。

另外值得一提的是,目前为止无论是线性模型、L1/L2 正则项、softmax 函数、SVM 模型,它们都是凸函数,意味着线性分类器问题总是在优化凸函数,它注定有局部最优等于全局最优,并且不依赖于初始条件,永远是收敛到解的。

但神经网络模型对应的优化模型通常不是凸函数,因此没有收敛性和最优解的保证。

 

3.5 Gradient Descent in Neural Networks & Optimizations

不像线性模型,想要在神经网络中计算梯度并学习是个比较难的事情。注意,如果你希望写出神经网络的梯度的解析形式并且计算,这个做法就是错误的。原因比较容易理解:

从计算机学家的角度来说,我们要做的不是怎么设计出推导通用的计算梯度的工具(事实上这对于数学家也很难),而是应该利用恰当的数据结构和算法来帮我们完成这些繁琐的任务。于是人们提出了 “计算图” 的概念。

3.5.1 Computational Graphs & Back-propagation

计算图是一个有向图,用于表示一个模型内部需要进行的运算,通过模块化、流水线化的形式充分利用机器计算资源(调度、并行)、减小计算成本。

神经网络可以使用计算图方法来动态计算网络的梯度值并进行学习,可以说这个数据结构使得 NN 优化成为可能。

例如一个使用 Multi-class SVM Loss 作为损失函数的线性分类器的损失函数计算图如下:

在计算图中,我们能够通过 “反向传播”(back-propagation)的方法计算输出变量关于输入变量的偏导数。例如对于 f(x,w)=11+e(w0x0+w1x2+w2)

  1. 在计算图中按拓扑序的方向正向计算结果;

  2. 第一列代入计算得到损失结果后,从输出结点向前反向传播计算数值梯度。例如 upstream gradient 是 LL=1(如下图),经过结点 1x 算子后,我们考虑链式法则,记计算前的变量 p,计算后的变量 q=1p,那么我们现在为了让反向传播进行下去(一直算到输入变量),就需要知道 Lp;由链式法则 Lp=qpLq,注意到这里是 corner case,q 就是 L,因此 Lq=LL=1,我们只需要计算 local gradient qp 就行。注意到 q=1p 是由算子决定,因此对这个算子求偏导 qp=1p2,即可计算出 downstream gradient 了!

    同理一直这样计算下去直到输入源结点,例如最后 I0=w0x0,已知 LI0,通过链式法则有 Lw0=LI0I0w0Lx0=LI0I0x0;预先得到 I0w0=x0I0x0=w0 的解析解计算方法(local gradients)后,直接代入即可得到 L 关于输入 w0,x0 的偏导数值。

  3. 上面正向 + 反向传播算作完成一轮梯度下降迭代运算,我们可以将梯度代入梯度下降法的优化方法中优化参数,并重复直至达到 metrics 或者步数足够。

我们发现,对每个结点,只需要知道这个结点算子的偏导函数,即可快速地进行反向传播和代入运算,这在大规模计算时无疑非常方便。

再注意到一点,上面的函数恰好是 sigmoid 激活函数(参见 3.2)的一部分,而我们注意到,由于链式法则的性质,嵌套的函数也满足这个计算性质:

这就说明,一个模型的计算图并不是唯一的,我们可以用一系列已知的函数(尤其是像 sigmoid 这样方便计算导数的)作为积木(而不是最原始的运算符)进行反向传播,这就是计算图的算子优化。它能节省不必要的计算、极大地提升反向传播计算效率

注意到 sigmoid 函数的导数非常非常容易计算,这也是为什么它被作为激活函数之一:既满足 “两头饱和、中间快速增长” 的阈值特性(符合网络计算需求),又容易计算(偏)导数。

现在我们了解了如何计算某个区域如何计算偏导数,我们现在形式化一下以便对于更普遍的结点进行计算。对于网络中的任意一个结点,从网络反向传播上游计算的 upstream gradient(如下例 Lz),我们可以根据这个结点中的信息得到结点 local gradients(链式法则需要的内容),并作为下游结点的 downstream gradient 递归地传递下去(如下例 LxLy)。

最终在反向传播结束后,计算图会得出损失函数关于所有输入变量的偏导数,这样我们可以用数值梯度信息对参数进行一轮训练迭代。

这样的计算方法可以让我们每次只关注一个局部的函数结构,而不需要关系系统全局信息,这非常利于进行大规模的并行计算。

 

3.5.2 Patterns in Gradient Flows

如果你理解了上面的计算过程,就能从直觉上领会到所谓反向传播就是,信息先从输入向前经过中间结点传播到输出位置,然后以相反的途径流回输入的位置,你就能发现这里有一些对偶性(duality)。我们将这个角度称为 “circuit perspective”。总结这些性质能帮助我们理解,这些计算图中的结点直观上究竟对梯度做了什么。

例如一个加法算子结点,无论它的输入参数有几个,由于偏导总是 1,因此它在反向传播梯度的时候,只是简单地将梯度复制给每个输入下游:

而对偶地,如果有一个算子是复制算子,只是将输入复制给后面的若干输出,那么在反向传播梯度的过程中,它就是将各个上游梯度累加起来!

复制算子会在哪里用到?例如你需要进行正则化时,希望将输入同时传给正则化算子,以及网络的下一层。

于是我们称加法算子和复制算子是对偶的

再来看一个乘法算子结点,由于乘法的关于某变量 x 的偏导,是除了 x 以外其他变量的乘积,因此它在反向传播梯度时,表现类似一个 “swap multiplier”,将 upstream gradient 和除了该变量以外的所有输入的乘积 作为该变量的 downstream gradient:

以及最大值算子结点,它表现成一个 “gradient router”,梯度只流向最大的输入那边,就像一个路由一样:

这就给了我们一些启发,例如,如果你的模型里面含有大量的 max 函数,可能造成反向传播时很多梯度都是 0,影响实际训练效果。

这也是人们在有些情况下使用 softmax 算子而不是 max 算子的重要原因之一!

 

3.5.3 How to Implement Back-propagation: "Flat" Gradient Code (Scalars)

代码上,我们如何实现上述的反向传播的计算?先从最 naive 的开始,我们仅仅将刚才总结出的反向传播的方法,针对特定的模型硬编码出来:

注意到,这就是利用了上面的计算方法,在正向计算的代码后反向计算一遍梯度。这个实现是正确的!你可以对着任意层神经网络使用这种方法。

但是这样做有个问题,它不模块化,一旦我们更改了模型结构、更换任意一个激活函数、更换任意一个正则项/损失函数,都需要重写代码。因此我们称之为最直白的实现。

3.5.4 How to Implement Back-propagation: Modular API (Scalars)

如果需要模块化,我们就需要为计算图准备一个类型,为每个结点也准备一个类型,并且实现它们的 forwardbackward 时计算的方法。

这样得益于反向传播的模块化计算的特性,我们可以通过正向和反向遍历并执行计算图中的各个结点的 forwardbackward 方法,就能计算出我们需要的梯度值。

事实上工业界的系统也是这么做的,例如 PyTorch 中,如果我们想在网络中定义一个算子结点,可以继承于 torch.audograd.Function 类型。比如一个标量乘法算子 z=x×y

这个代码非常简单,完美描述了之前我们如何对神经网络计算图中的一个结点进行梯度反向传播的计算。

 

3.5.5 How to Implement Back-propagation: Vector-valued Functions

以上我们讨论的都是最简单的情况,对标量计算。如果我们希望网络能对向量(矩阵,或者张量)计算,有什么区别吗?

我们回忆一下向量微分、矩阵微分的相关知识。

然后看一看,假如网络中每个算子计算的是向量,情况是怎样的。需要注意的是,损失函数的值仍然是标量

这个例子是一个两输入的向量函数 z=f(x,y)。假设反向传播上游来的 upstream gradient 是 Lz,它的含义是,对输出 z 中的每个元素 zi,单位的 zi 改变会如何影响 L。因此我们知道 Lz 是一个和 z 同样形状的张量。

然后我们通过求 f 的 Jacobian(雅可比)矩阵(对向量函数求一阶偏导)就可以得到 local gradients zxzy。接下来根据向量微分的链式法则,我们只需要做向量-矩阵间乘法就可求得 downstream gradients。

上面这张图就以 Dx 维向量和 Dy 维向量举例。下次如果搞不清楚输入和输出的维度时,可以尝试绘制一张这个图像。

再举一个具体的例子,假设 f 是 ReLU 函数,只有一个输入的 4 维向量和一个输出的 4 维向量。那么:

其中的 Jacobian 矩阵表示其中输入向量单位变化会如何影响输出向量的变化((yx)i,j 表示 input[i] 单位变化对 output[j] 的影响)。注意到 ReLU 函数是 element-wise 的,对于输入向量的第 i 元素的单位变化,只有输出向量的第 i 元素会被影响。因此这个函数的 Jacobian 矩阵是稀疏的(只有对角元才可能不为 0)。

事实上,这个例子中的 Jacobian 矩阵已经算密集的了。在大多数情况下,一个张量函数的 Jacobian 函数是非常非常稀疏的(尤其是维度很大的情况,每个元素越有可能统计学相互独立,再加上元素数量是维度数幂级增长的),因此我们不可能在实际应用时显式地构建 Jacobian 矩阵。幸运的是,对于 ReLU 函数,我们可以以解析式的方法写出它的 Jacobian 矩阵元素值:

(Lx)i={(Ly)i,if xi>00,otherwise

我们仍然需要继续 generalize,因为实际应用的时候,神经网络需要能够以秩大于 1 的张量为操作数进行计算(例如矩阵、高维张量)。我们看看如果网络中每个参数都是矩阵/高维张量,情况是怎样的。

这个例子是一个操作数为矩阵(二维张量)的结点,输入 Dx×Mx 大小和 Dy×My 大小的矩阵,输出 Dz×Mz 大小的矩阵。

同理(参见向量推导)可知,Lz 必然和 z 同形状;我们对这个矩阵函数求偏导可得它的 Jacobian 矩阵(是 3 维张量),大小分别是 [(Dx×Mx)×(Dz×Mz)][(Dy×My)×(Dz×Mz)]

通过链式法则,我们有广义上的 “矩阵-向量乘法”(即高维张量和二维矩阵的乘法),同样可以得到 downstream gradients。

这种高维的操作如果再想画图画出来就困难了。我们需要想一种方法清晰地表述它,方便我们实现代码。

举个实际的例子:

注意到这个 Jacobian 矩阵相当难以想象,我们就逐个 slice 构造,例如第一个元素就能快速解决:

收集完成整个 dydx11 矩阵的元素后,我们终于可以计算 dLdx11 了(: 是模仿 Matlab 和 NumPy 的取整行/列的写法):

dLdx11=(dydx11)(dLdy)=(w1,: )(dLdy1,:)=[3,2,1,1][2,3,3,9]

掌握了一个元素的计算后,其余的都是重复,因此我们不需要理解 Jacobian 的整体样貌,就能归纳出整体计算公式了:

dLdxij=(dydxi,j)(dLdy)=(wj,: )(dLdyi,:)

也就是说:Lx=(Ly)wT,仍然满足我们之前标量的 “swap multiplier” 的 pattern。

 

3.5.6 Reverse-Mode & Forward-Mode Automatic Differentiation

总结一下,我们将反向传播算法中的反向求微分的过程称为 Reverse-Mode Automatic Differentiation(反向自动微分)。这个反向算法非常精妙地避免了矩阵-矩阵乘法(请回想一下之前的运算过程),全是(广义的)矩阵-向量乘法,这通常有更高的效率。

拓展:如果有个系统输入标量,输出向量,想要求输出相对于输入的微分,这个时候,为了防止矩阵-矩阵乘法,我们需要 Forward-Mode Automatic Differentiation,也就是从前向后计算微分。

这在机器学习优化损失函数时基本不会见到,更常见的应用场景是物理引擎模拟计算。例如输入标量(如摩擦系数、重力等等),输出变化的物理向量。

因此 Forward-Mode 和 Reverse-Mode 这两种自动微分适用于很多需要机器求微分的计算场景,不仅仅是机器学习。

截止目前,PyTorch 2.6 已经放出了 beta 版本用于计算 Forward-Mode Automatic Differentiation,感兴趣可以尝试一下:Forward AD Usage - PyTorch

 

3.5.7 Higher-Order Derivatives using Back-propagation

反向传播算法不仅仅能应对求梯度的场景,我们还可以利用它求高阶微分。

我们将向量计算图抽象成一如下过程(假设输入为 D0 大小的向量):

如果现在我们想知道 2Lx02x0 的单位改变能引起 Lx0 的多大改变)而不是 Lx0,是否有办法利用原来反向传播的算法计算呢?注意 2Lx02 是一个 D0×D0 的 Hessian Matrix(一阶偏微分对应的是 Jacobian Matrix)。

在有些情况下,我们需要在计算图中计算一个关于 2Lx02 的乘法,即:2Lx02v(Hessian-Vector Multiply),例如你在用一个迭代算法近似计算一个矩阵的奇异值,就需要知道优化空间的二阶导信息,也就需要用到这个算法。

这里为了利用反向传播算法,我们需要用到一阶微分的线性性:

2Lx02v=x0[Lx0v]

当且仅当 v 不依赖于 x0。注意到这是在对一个向量 Lx0v 求一阶偏导!这不就正好能使用反向传播算法了吗?我们巧妙地扩展一下计算图:

其中 f2f1 正是反向传播时反过来计算 x1x0 定义的函数(回忆我们之前在 flat implementation 中手写过),它们作用是在反向传播时计算出 downstream gradients。得到 Lx0 数值后,我们再添加一个向量点积算子,得到 Lx0v,这样整个网络再反向传播,最终不就能计算出 x0(Lx0v) 了吗!

这样我们可以使用同样的技巧计算任意高阶的偏导数,只要偏导 primitive operations(上面这些函数)的实现我们是知道的。

这个高阶偏导的算法在 PyTorch 和 TensorFlow 中都已经实现了。

再例如,一个我们可能需要的例子:

假设我们需要在计算图中定义一个正则项(这个特殊的正则项不是 L1/L2/Elastic Net,而是来自于一篇论文):

R(W)=LW22=(LW)(LW)

并且想知道 x0R(W)=2(2Lx02)(Lx0) 来求出正则项关于自变量的偏导,用于迭代,这个时候,我们可以模仿上面的做法扩展一个计算图:

 

3.5.8 Conclusion

注意到,无论是线性分类器,还是反向传播算法支持的神经网络,它们在图像处理方面都有一个潜在问题:输入时总是将图像 flatten 为一维向量!这会丢失掉图像的空间结构和空间信息。因此我们可以继续优化网络结构:是时候了解卷积神经网络了!

 

Lecture 4. Convolutional Networks

4.0 Concepts

本章需要引入新的操作符:卷积层(convolution layers)、池化层(pooling layers)、正则化层。

考虑为了应对存在空间结构的输入,引入一个新的卷积计算方法:假设输入是一个 3 x 32 x 32 图像张量(深度为 3),引入 3 x 5 x 5 的 filter(之前的 weight matrix),并暂时假设 filter 的深度与输入图像的深度相同。

我们想象将 filter 沿着输入图像张量滑过,每经过一个重叠的位置时计算一次乘积(wTx+b),其中 b 对于一个 filter 是定值,最终能得到一个 1 x 28 x 28 的结果张量。我们称结果张量为 activation map;

当然只有一个 filter 的信息是有失偏颇的,我们不妨重复 6 次(有点像多头注意力?),每次的 b 和 filter 均不相同,我们可以得到 6 x 28 x 28 的结果张量。

现在这个结果张量有两个理解方法:

实际上在应用中我们会批量处理输入张量,假设每批 2 个,那么输入张量就能组成 4 维张量(2 x 3 x 32 x 32)。

现在拓展一下可以知道,一个 N 个输入张量(Cin 个 channel、spatial size H×W)构成的 batch(N×Cin×H×W)进入如下参数的卷积层:

输出矩阵 N×Cout×H×W(spatial size 可能与输入不同);

用数学方式表达:

out(Ni,Coutj)=bias(Coutj)+k=0Cin1weight(Coutj,k)input(Ni,k)

考虑一个问题,我们能否像这样将卷积层叠加起来:

答案是不行,就像我们在构建全连接层组成的神经网络中遇到的,f=W2W1x=W3x,两个线性分类器(全连接层)直接拼接而没有激活函数,得到的只是新的参数的线性分类器,模型表达能力没有实质性增强。这里的卷积层就是相当于多进行了一次线性代数乘法,没有新的数据表述角度。

因此我们需要在各层间加入激活函数(element-wise):

 

4.1 A Closer Look At Spatial Dimensions

4.1.1 Padding

我们先只考虑输入张量的某一个通道,很容易发现,每进行一次卷积操作,输出张量 H=HHfilter+1, W=WWfilter+1

这会导致:每经过一个卷积操作,feature map 的两个维度减小一定值,如果我们希望有多层卷积层而输入张量的维度并不足以支持的时候,就没法构建了(而且这需要考虑很多 corner cases)!

我们并不希望模型的层数受限于卷积层这样 “蒸发数据维度” 的特性。因此,人们为此引入了 padding,也就是在进行卷积层计算前,先在外围 padding 一圈 0 元素(假设 padding 的层数为 P),就能解决这个问题。

虽然 P 也是超参数,但一般情况下,人们希望让 feature map 的 HW 保持输入时的大小(这种 padding 方式被称为 “same padding”),因此 P 经常取 (Sfilter1)/2,其中 Sfilter 为 filter 的长宽取值(又称 kernel size)。

举个例子,下面是一个 H=W 的特殊情形:

这是因为人们通常不想 spatial size 因为卷积操作随意变化,除非有意为止,或者有理由这么做。

考虑一下,zero padding 会不会向网络引入额外的数据或信息?

我们的本意并没有想向网络中增添信息,只是一种防止 shrinking 的手段。但是实际上确实引入了一些额外信息:它告诉了网络,filter 当前位于输入张量的什么位置。这会使得卷积操作是 Translation Equivariance(平移等变)的。

这种平移等变的性质会使得,如果 input tensor 平移一段距离后,output tensor 也会移动相同的距离。

等变是数学表述,函数 f 相对于函数 g 是等变的,当且仅当:f(g(x))=g(f(x))

平移操作则是一种几何变换,它将所有点沿给定方向移动相同的距离,Tv(x)=x+v

 

4.1.2 Receptive Fields

注意到,之前我们提到 feature map 的一种解释是,“每个网格单元都是一个 Cout(kernel channel)维向量,描述特定位置的输入张量和 filter 组的数据关系”,我们还知道,一个 feature map 的网格是由 Hw×Ww(kernel size)大小的 input tensor 和 filter 计算出来的,因此将 input tensor 中每一个 Hw×Ww 的区域称为 “感受区”/“感受野”(Receptive Field)。

如下图,这是一个核的两个维度大小相同的特例:

如果在多层卷积层中,我们同样有感受区的概念,这能帮助我们理解数据信息的 “流动情况”:

这告诉我们两件事:

这第二个点会有个问题:如果我们像解析一个解析度非常大的图像构成的张量,要想一个 activation 获取到足够全局的数据(最终让 feature map 的数据完全利用和依赖于输入的数据),就需要很多很多卷积层堆叠在一起,这是不现实的。

因为直觉上让 feature map 获取更多的 global context 会对模型的泛化性有好处。

我们的解决方案是,引入另一个超参数:stride。也就是 filter 扫描时每次前进两个元素单元。

现在输出 activation map 的形状和 input tensor 的关系是 S=(SinputSfilter+2Spadding)/stride+1

common settings:设置 kernel size 各个维度相同、same padding(Spadding=(K1)/2);

例如输入一个 3 x 32 x 32 的 input tensor,使用 10 个 channel 相同的 5 x 5 filters,步长为 1,padding size 为 2,请问 activation map 的形状是怎样的?该层的可训练参数数量是多少?需要进行多少次 multiply-add 操作?

答:activation map 的形状是 10 x 32 x 32,可训练参数数量是 (3 x 5 x 5 + 1)*10 = 760。

注意到 activation map 中的每个元素都需要一次 3 x 5 x 5 的 element-wise 内积运算。计算为 768K 次 multiply-add ops;

研究表明 kernel size 1 x 1 的卷积层的行为和线性全连接层很像(都是将 “template vector” 单独作用于每个元素),但前者会改变 channel 数量,通常用在希望保留 input tensor 的空间结构、并且想调整 tensor 形状去适配其他层的时候。后者则会 flatten input tensor,通常用在网络的末端要生成 scores 的时候。

 

4.2 Other Dimensions

前面提到的都是 2D Convolution Layer,1D 卷积层通常做的操作像这样:

通常用在处理文本/音频数据(以序列的形式出现)的方面。

3D 卷积层则需要输入 4 维张量(包含 channels),Cout 个 channel 与 input tensor 相同的 filters,一般用于处理 3 维点云数据:

4.3 Pooling Layer

卷积神经网络中另一个重要的层是池化层,它的作用是 downsample(下采样),在不改变 channel 的情况下减小 input tensor 每个维度的大小(相当于将信息浓缩到更小规模的数据中)。

池化层不包含任何可训练参数,只有超参数 kernel size、stride 和 pooling function;

如何 “浓缩” 呢?我们考虑 pooling function。

Max Pooling

最大值池化方案,就是将最大值函数作为 pooling function,它的思想是选取每个 stride 对应的区域(pooling regions)中最大的元素来提取信息(比较一下一般的卷积层是做 element-wise inner product)。以 stride 为 2、kernel size 2x2 的情况为例:

注意到如果 stride 和 kernel size 的维度长相等,则 pooling regions 不会互相 overlap。

能注意到 Max Pooling 引入了一些平移不变性的性质。

Average Pooling

如果 pooling function 是取平均的话就是平均值池化。

所以说池化反而是利用了卷积层 shrink 的特性,它不进行 inner product 运算,只是利用 pooling function “浓缩特征”。

总而言之,池化层可以提供如下作用:

问:池化层是一个非线性层,使用它就可以不需要 ReLU 激活函数了?

理论上是的,但实际

但是也可以被普通卷积层替代:

4.4 Classic Convolutional Networks

一种比较经典的网络设计方式是,使用 (卷积层 + ReLU 激活函数 + 池化层) * N -> flatten -> (线性全连接层 + ReLU 激活函数) * M -> 线性全连接层 (输出 scores)。例如 LeNet-5 网络:

同样的问题是,这样深层的网络会很难训练(优化)。