斯坦福-CS231n-全套笔记-ShowMeAI--全-

斯坦福 CS231n 全套笔记(ShowMeAI)(全)

深度学习与计算机视觉教程:斯坦福 CS231n · 全套笔记解读

原文:blog.csdn.net/ShowMeAI/article/details/124993058

ShowMeAI 研究中心


引言

本篇内容是ShowMeAI组织的深度学习与计算机视觉系列教程入口,本教程依托于斯坦福 Stanford 出品的【CS231n:深度学习与计算机视觉】方向专业课程,根据课程视频内容与课程笔记,结合补充资料,针对深度学习与计算机视觉方向的主题做了全面梳理与制作,希望给大家提供专业细致而直观易懂的学习教程。

本系列教程内容覆盖:图像分类神经网络反向传播计算图CNNRNN神经网络训练tensorflowpytorch注意力机制生成模型目标检测图像分割强化学习 等主题。

教程地址

点击查看完整教程学习路径

内容章节

1.深度学习与 CV 教程(1) | CV 引言与基础

CV 引言与基础; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-1

2.深度学习与 CV 教程(2) | 图像分类与机器学习基础

图像分类&机器学习基础; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-2

3.深度学习与 CV 教程(3) | 损失函数与最优化

损失函数&最优化; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-3

4.深度学习与 CV 教程(4) | 神经网络与反向传播

神经网络&反向传播; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-4

5.深度学习与 CV 教程(5) | 卷积神经网络

CNN 卷积神经网络; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-5

6.深度学习与 CV 教程(6) | 神经网络训练技巧 (上)

神经网络训练技巧(上); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-6

7.深度学习与 CV 教程(7) | 神经网络训练技巧 (下)

神经网络训练技巧(下); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-7

8.深度学习与 CV 教程(8) | 常见深度学习框架介绍

常见深度学习框架介绍; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-8

9.深度学习与 CV 教程(9) | 典型 CNN 架构 (Alexnet, VGG, Googlenet, Restnet 等)

典型 CNN 架构(VGG/ResNet 等); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-9

10.深度学习与 CV 教程(10) | 轻量化 CNN 架构 (SqueezeNet, ShuffleNet, MobileNet 等)

轻量化 CNN 架构(SqueezeNet 等); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-10

11.深度学习与 CV 教程(11) | 循环神经网络及视觉应用

RNN 循环神经网络&视觉应用; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-11

12.深度学习与 CV 教程(12) | 目标检测 (两阶段, R-CNN 系列)

目标检测(两阶段/R-CNN 等); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-12

13.深度学习与 CV 教程(13) | 目标检测 (SSD, YOLO 系列)

目标检测(SSD/YOLO 系列); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-13

14.深度学习与 CV 教程(14) | 图像分割 (FCN, SegNet, U-Net, PSPNet, DeepLab, RefineNet)

图像分割(FCN/SegNet/U-Net 等); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-14

15.深度学习与 CV 教程(15) | 视觉模型可视化与可解释性

视觉模型可视化与可解释性; 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-15

16.深度学习与 CV 教程(16) | 生成模型 (PixelRNN, PixelCNN, VAE, GAN)

生成模型(PixelCNN/GAN 等); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-16

17.深度学习与 CV 教程(17) | 深度强化学习 (马尔可夫决策过程, Q-Learning, DQN)

深度强化学习(Q-Learning/DQN 等); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-17

18.深度学习与 CV 教程(18) | 深度强化学习 (梯度策略, Actor-Critic, DDPG, A3C)

深度强化学习(Actor-Critic/DDPG 等); 计算机视觉 ComputerVision; 斯坦福 CS231n; 19-18

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(1) | 引言与知识基础(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/124932088

ShowMeAI 研究中心


Introduction; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


1.课程简介

CS231n 是顶级院校斯坦福出品的深度学习与计算机视觉方向专业课程,核心内容覆盖神经网络、CNN、图像识别、RNN、神经网络训练、注意力机制、生成模型、目标检测、图像分割等内容。

CV 引言与基础; 斯坦福 CS231n; 内容覆盖典型视觉任务; 1-1

2.课程内容介绍

2.1 第 1 部分 Lecture1-3 深度学习背景知识简单介绍

  • 课程引入与介绍
  • KNN 和线性分类器
  • Softmax 和 SVM 两种损失函数
  • 优化算法(SGD 等)

2.2 第 2 部分 Lecture4-9 卷积神经网络

  • CNN 及各种层次结构(卷积、池化、全连接)
  • 反向传播及计算方法
  • 优化的训练方法(Adam、Momentum、Dropout、Batch-Normalization)
  • 训练 CNN 的注意事项(参数初始化与调优)
  • 深度学习框架(TensorFlow、Caffe、Pytorch)
  • 线性 CNN 结构(AlexNet、VGGNet、GoogLeNet、ResNet)

2.3 第 3 部分 Lecture10-16 计算机视觉应用

  • RNN(语言模型,image captioning 等)
  • 目标检测(R-CNN、Fast / Faster R-CNN、YOLO、SSD 等)
  • 语义分割(FCN、Unet、SegNet、deeplab 等)
  • 神经网络可视化与可解释性
  • 生成模型与 GAN
  • 深度强化学习

3.课程学习目标

CV 引言与基础; 斯坦福 CS231n; 内容覆盖应用创作; 1-2

3.1 实用技能

理解如何从头开始编写、调试和训练卷积神经网络。

3.2 工具技术

集中于大规模训练这些网络的实用技术,以及 GPU(例如,将涉及分布式优化、CPU 与 GPU 之间的差异等),还可以查看诸如 Caffe、TensorFlow 和 (Py)Torch 等最先进的软件工具的现状。

3.3 应用创作

一些有趣的主题,如「看图说话」(结合 CNN + RNN),再如下图左边的 DeepDream,右边的神经风格迁移 NeuralStyle 等。

4.课程先修条件

1)熟悉 Python(并了解 numpy 的使用),本课都用 Python 编写,如果要阅读理解软件包的源代码 C++ 会有帮助。

2)大学微积分(如求导),线性代数(了解矩阵)。

3)有机器学习的背景,大概 CS229 水平,非常重要核心的机器学习概念会再介绍的,如果事先熟悉这些会对课程有帮助的,我们将制定成本函数,利用导数和梯度下降进行优化。可前往文末获取 ShowMeAI 原创的 CS229 课程速查表。

4)有计算机图像基础会更好,但不是非常严格。

5.计算机视觉简介

5.1 计算视觉历史

16 世纪最早的相机:暗箱

CV 引言与基础; 计算视觉历史; 1-3

1963 年第一篇计算机视觉博士论文「Block world-Larry Roberts」,视觉世界简化为简单的几何形状,识别它们,重建这些形状。

CV 引言与基础; 计算视觉历史; 1-4

1996 年 MIT 暑期项目「The Summer Vision Project」目的是构建视觉系统的重要组成部分。

CV 引言与基础; 计算视觉历史; 1-5

1970s 的 MIT 视觉科学家 David Marr 编写了《VISION》,内容有计算机视觉的理解、处理开发、识别算法,他提出了视觉表现的阶段,如原始草图的零交叉点,圆点,边缘,条形,末端,虚拟线,组,曲线边界等概念

CV 引言与基础; 计算视觉历史; 1-6

1973 年后对于如何识别和表示对象,斯坦福科学家提出「广义圆柱体」和「圆形结构」,每个对象都是由简单的几何图形单位组成。

CV 引言与基础; 计算视觉历史; 1-7

1987 年 David Lowe 尝试用 线边缘 来构建识别。

CV 引言与基础; 计算视觉历史; 1-8

1997 年 Shi & Malik 提出,若识别太难了,就先做目标分割,就是把一张图片的像素点归类到有意义的区域。

CV 引言与基础; 计算视觉历史; 1-9

2001 年此时的机器学习也快速发展了(尤其是统计学习方法),出现了 SVM(支持向量机模型)、boosting、图模型等方法。Viola & Jones 发表了使用 AdaBoost 算法进行实时面部检测的论文 「Face Detection」,而后 2006 年富士推出可以实时面部检测的数码相机。

CV 引言与基础; 计算视觉历史; 1-10

1999 年 David Lowe 发表 “SIFT” & Object Recognition,提出 SIFT 特征匹配,思路是先在目标上确认关键特征,再把这些特征与相似的目标进行匹配,来完成目标识别。从 90 年代到 2000 年的思想就是基于特征的目标识别。

CV 引言与基础; 计算视觉历史; 1-11

2006 年 Lazebnik, Schmid & Ponce 发表「Spatial Pyramid Matching」,图片里的各种特征描述了不同场景,空间金字塔匹配算法的思想就是从图片的各部分各像素抽取特征,并把他们放在一起作为一个特征描述符,然后在特征描述符上做一个支持向量机。

CV 引言与基础; 计算视觉历史; 1-12

2005 年后来的研究 方向梯度直方图可变形部件模型,目的是将特征放在一起后,如何辨认人体姿态。

CV 引言与基础; 计算视觉历史; 1-13

21 世纪早期,数码相机快速发展,图片质量提高,也真正有了标注的数据集,它能够衡量目标识别的成果。数据集 PASCAL Visual Object Challenge 有 20 个类别,每个种类有成千上万张图片,供团队开发算法来和数据测试集做对抗训练,来看检测效果有没有优化。

CV 引言与基础; 计算视觉历史; 1-14

而后普林斯顿和斯坦福提出怎么识别大部分物体,这个问题也是由机器学习中的一个现象驱动的,机器学习算法在训练过程中很可能会过拟合(只对现有的这些数据完美拟合,但对未知数据不一定完美)。部分原因是可视化的数据非常复杂(像是记住了每道题),从而模型维数比较高,输入是高维的模型,并且还有一堆参数要调优,当我们的训练数据量不够时很快就会产生过拟合现象,这样就无法很好的泛化。

因此有了两方面动力:① 识别万物② 克服机器学习的瓶颈-过拟合问题

CV 引言与基础; 计算视觉历史; ImageNet; 1-15

针对上述问题开展了 ImageNet(http://www.image-net.org/)项目,在网络上收集了上亿张图片,用 WordNet 字典来排序,这个字典有上万个物体类别,不得不用 Amazon Mechanical Turk 平台来排序、清洗数据、给每张图片打上标签,最终得到的 ImageNet 有 1500 万甚至 4000 万图片分成了 22000 多类的物体或场景。它将目标检测算法的发展推到了新高度。

CV 引言与基础; 计算视觉历史; ImageNet; 1-16

2009 年为了推动基准测试的进展,ImageNet 开始组织了 ImageNet 大规模视觉识别竞赛,筛选了更严格的测试集,140 万目标图像有 1000 种目标类别,分类识别来测试计算机视觉算法。

下图为图像分类结果,纵轴为比赛结果的错误率,2012 年的错误率下降的非常显著,这一年获头奖的算法是一种卷积神经网络模型。

CV 引言与基础; 计算视觉历史; ImageNet; 1-17

5.2 计算机视觉近代技术发展

卷积神经网络Convolutional Neural Networks,CNN)已成为图像识别中最重要的模型之一。

CV 引言与基础; 计算机视觉; 近代技术发展; 1-18

2010 年的 NEC-UIUC 仍然用到了层次结构检测边缘不变特征。在 2012 年才有重大突破,多伦多的博士生和导师创造了 7 层的 CNN,称为 SuperVision 现在叫做 AlexNet

2014 年谷歌的 GoogLeNet 和牛津大学的 VGG 有 19 层网络。

2015 年微软亚洲研究院发表了残差网络,有 152 层。

CV 引言与基础; 计算机视觉; 近代技术发展-CNN; 1-19

CNN 早在 1998 年由 Yann LeCun 团队在贝尔实验室发明的,他们使用 CNN 进行数字识别,用于识别手写支票和邮件地址,当时的 CNN 和后续的很多典型 CNN 模型结构是相似的,输入是原始像素,有很多卷积层和下采样以及全连接层。

随着计算机算力的提升,像 GPU 这种图像处理单元超高的并行计算能力引入,人们开发出了更大的 CNN 模型和架构

在算力的支撑下,只扩大模型的规模,沿用经典的方法和算法就能有很好的结果,这种增加计算的思想有着很重要的地位。还有数据的创新,现在有了很多标记的数据,我们可以实现更强大的模型。

后来也有很多创新的 CNN 结构引入,帮助模型可以在更大更深的情况下,也可以很好地训练和对抗过拟合。

CV 引言与基础; 计算机视觉; 近代技术发展; 1-20

对视觉智能的探索远远超出了图像识别的范围,如图像语义分割知觉分组他们没有给整张图片打上标签,我们要理解的是每个像素。这些任务是3D 重构动作识别增强现实虚拟现实等重要的支撑。

CV 引言与基础; 计算机视觉; 近代技术发展; 1-21

如老师 Johnson 在 2015CVPR 发表的「Image Retrieval using Scene Graphs」,视觉基因组这个数据集,不仅框出物体还要描述图像,作为整个大图形语义相关的概念,不仅包括对象的身份,还包括对象关系、对象属性、动作等,视觉系统可以做很多事情。

CV 引言与基础; 计算机视觉; 近代技术发展; 1-22

当看到上方的图片时人们可以丰富的描述这个场景,借助于他们的储备知识和过往经验又可以详细描述每个人的身份历程等。

这是典型的计算机视觉任务「看图说话 / image captioning」,它以一种非常丰富而深刻的方式去理解一张图片的故事,也是目前依旧在不断推进的研究领域之一。

6.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=1

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(2) | 图像分类与机器学习基础(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/124993990

ShowMeAI 研究中心


Image Classification pipeline; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

图像分类是计算机视觉的核心任务,计算机视觉领域中很多问题(比如 目标检测语义分割),都可以关联到图像分类问题。图像分类 问题,就是已有固定的分类标签集合,然后对于输入的图像,从分类标签集合中找出一个分类标签,最后把分类标签分配给该输入图像。在本篇内容汇总,ShowMeAI将给大家讲解数据驱动的模型算法,包括简单的 KNN 模型线性分类模型

本篇重点

  • 数据驱动方法
  • KNN 算法
  • 线性分类

1.图像分类的挑战

对于计算机而言,图像等同于一个像素矩阵;而对人类,图像是包含丰富语义信息的多媒体呈现,对应不同的物体类别,所以对计算机而言存在巨大的语义鸿沟。

比如,给计算机输入如下小猫的图片,计算机图像分类模型会读取该图片,并计算该图片属于集合 { 猫 , 狗 , 帽 子 , 杯 子 } {猫, 狗, 帽子, 杯子} {猫,狗,帽子,杯子} 中各个标签的概率。但读取的输入图像数据是一个由数字组成的巨大的 3 3 3 维数组。

在下图中,猫的图像大小高 600 600 600 像素,宽 800 800 800 像素,有 3 3 3 个颜色通道(红、绿和蓝,简称 RGB),因此它包含了 600 × 800 × 3 = 1440000 600 \times 800 \times 3=1440000 600×800×3=1440000 个数字,每个数字都是在范围 0 ∼ 255 0 \sim 255 0∼255 之间的整型,其中 0 0 0 表示全黑, 255 255 255 表示全白。

我们的任务就是把这些数字变成一个简单的标签,比如 「猫」 。

图像分类; 图像分类的挑战; 计算机[眼]中的图像; 2-1

图像分类算法要足够健壮(鲁棒,robust),我们希望它能够适应下述变化及组合:

  • 视角变化(Viewpoint variation):同一个物体,摄像机可以从多个角度来展现。
  • 大小变化(Scale variation):物体可视的大小通常是会变化的(不仅是在图片中,在真实世界中大小也是变化的)。
  • 形变(Deformation):很多东西的形状并非一成不变,会有很大变化。
  • 遮挡(Occlusion):目标物体可能被挡住。有时候只有物体的一小部分(可以小到几个像素)是可见的。
  • 光照条件(Illumination conditions:在像素层面上,光照的影响非常大。
  • 背景干扰(Background clutter):物体可能混入背景之中,使之难以被辨认。
  • 类内差异(Intra-class variation):一类物体的个体之间的外形差异很大,比如椅子。这一类物体有许多不同的对象,每个都有自己的外形。

如下图所示是一些变化和图像识别的挑战:

图像分类; 图像分类的挑战; 一些变化和识别挑战; 2-2

2.数据驱动的方式

一种实现方式是「硬编码」:先获取猫图像的边缘得到一些线条,然后定义规则比如三条线交叉是耳朵之类。然而这种方式的识别效果不好,并且不能识别新的物体。

图像分类; 数据驱动算法; 获取图像边缘得到线条; 2-3

我们会采用数据驱动算法:不具体写出识别每个物体对应的规则,而是针对每一类物体,找到大量样例图片,灌给计算机进行机器学习,归纳模式规律,生成一个分类器模型,总结出区分不同类物体的核心知识要素,然后用训练好的模型,识别新的图像

图像分类; 数据驱动算法; 输入/学习/评价; 2-4

数据驱动算法过程如下:

  • 输入:输入是包含 N N N 个图像的集合,每个图像的标签是 K K K 种分类标签中的一种。这个集合称为训练集。
  • 学习:这一步的任务是使用训练集来学习每个类的模式规律。一般该步骤叫做分类器训练或者模型学习。
  • 评价:让分类器对它未曾见过的图像进行分类,把分类器预测的标签和图像真正的分类标签 (基本事实) 对比,并以此来评价分类器的质量。

2.1 最邻近算法

本部分内容也可以参考ShowMeAI图解机器学习教程 中的文章详解 KNN 算法及其应用

我们这里介绍第 1 个分类器算法:最近邻算法。训练过程只是简单的记住图像数据和标签,预测的时候和训练数据中图片比较找出最接近的输出标签。这个分类器和卷积神经网络没有任何关系,实际中也极少使用,但通过实现它,可以对解决图像分类问题的方法有个基本认识。

1) 图像分类数据集:CIFAR-10

CIFAR-10 是一个非常流行的图像分类数据集。这个数据集包含 10 种分类标签,60000 张 32 × 32 32 \times 32 32×32 的小图像,每张图片含有一个标签。这 60000 张图像被分为包含 50000 张(每种分类 5000 张)图像的训练集和包含 10000 张图像的测试集。

假设现在我们用这 50000 张图片作为训练集,将余下的 10000 作为测试集并打上标签,Nearest Neighbor 算法将会拿着测试图片和训练集中每一张图片去比较,然后将它认为最相似的那个训练集图片的标签赋给这张测试图片。

结果如下图所示,效果并不是特别好。

图像分类; 图像分类数据集; CIFAR-10; 2-5

  • 左边:CIFAR-10 数据库的样本图像;
  • 右边:第一列是测试图像,后面是使用 Nearest Neighbor 算法,根据像素差异,从训练集中选出的 10 张最类似的图片

那么具体如何比较两张图片呢?我们有一些距离度量计算方法,下面展开介绍一下。

2) L1 距离(曼哈顿距离)

距离度量的数学知识也可以参考ShowMeAI的系列教程 图解 AI 数学基础 中的文章 线性代数与矩阵论 对各种距离度量的展开讲解

在本例中,就是比较 32 × 32 × 3 32 \times 32 \times 3 32×32×3 的像素块。最简单的方法就是逐个像素比较,最后将差异值全部加起来。即将两张图片先转化为两个向量 I 1 I_{1} I1​ 和 I 2 I_{2} I2​,然后计算他们的 L1 距离:

d 1 ( I 1 , I 2 ) = ∑ p ∣ I 1 p − I 2 p ∣ d_{1} (I_{1} ,I_{2} )=\sum_{p}\vert I_{1}^p -I_{2}^p \vert d1​(I1​,I2​)=p∑​∣I1p​−I2p​∣

  • 其中 p p p 为像素点, I p I^p Ip 表示第 p p p 个像素点的值。
  • 两张图片使用 L1 距离来进行比较,即逐个像素求差值,然后将所有差值加起来得到一个数值。如果两张图片一模一样,那么 L1 距离为 0 0 0;但是如果两张图片很是不同,那 L1 值将会非常大。

下图是仅一个 RGB 通道的 4 × 4 4 \times 4 4×4 图片计算 L1 距离。

图像分类; L1 距离; 4X4 图片计算; 2-6

下面看具体编程如何实现

① 首先,我们将 CIFAR-10 的数据加载到内存中,并分成 4 个数组:训练数据和标签,测试数据和标签

Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/') # 这个函数可以加载 CIFAR10 的数据
# Xtr 是一个 50000x32x32x3 的数组,一共 50000 个数据,
# 每条数据都是 32 行 32 列的数组,数组每个元素都是一个三维数组,表示 RGB。
# Xte 是一个 10000x32x32x3 的数组;
# Ytr 是一个长度为 50000 的一维数组,Yte 是一个长度为 10000 的一维数组。
Xtr_rows = Xtr.reshape(Xtr.shape[0], 32 * 32 * 3) 
# Xtr_rows 是 50000x3072 的数组,按每个像素点排列,每个像素点有三个值。
Xte_rows = Xte.reshape(Xte.shape[0], 32 * 32 * 3) 
# Xte_rows 是 10000x3072 的数组
''' shape 会返回数组的行和列数元组:(行数,列数),shape[0]表示行数, 
Xtr.shape[0]会返回 50000;Xtr.shape 会返回(50000,32,32,3)
Xtr.reshape(50000,3072)会将 Xtr 重构成 50000x3072 数组,等于 np.reshape(Xtr, (50000,3072))''' 
  • Xtr(大小是 50000x32x32x3)存有训练集中所有的图像
  • Xte(大小是 10000x3072)存有测试集中所有的图像
  • Ytr 是对应的长度为 50000 的 1 维数组,存有图像对应的分类标签(从 0 到 9)
  • Yte 对应长度为 10000 的 1 维数组

现在我们得到所有的图像数据,每张图片对应一个长度为 3072 的行向量。

② 接下来训练一个分类器并评估效果。我们常常使用准确率作为评价标准,它描述了我们预测正确的得分

本例中 OK,很多其他应用中准确率并不一定是最佳的评估准则,可以参考ShowMeAI图解机器学习教程 中的文章详解 模型评估方法与准则

nn = NearestNeighbor() # 创建一个最邻近分类器对象
nn.train(Xtr_rows, Ytr) # 用训练图片数据和标签训练分类器
Yte_predict = nn.predict(Xte_rows) # 预测测试图片的标签
# 并输出预测准确率,是一个平均值
print 'accuracy: %f' % ( np.mean(Yte_predict == Yte) ) 
  • 请注意以后我们实现的所有分类器都需要有这个接口函数(API):train(X, y) 函数。该函数使用训练集的数据和标签来进行训练。
  • 从其内部来看,类应该实现一些关于标签和标签如何被预测的模型。这里还有个 predict(X) 函数,它的作用是预测输入的新数据的分类标签。

下面就是使用 L1 距离的 Nearest Neighbor 分类器的实现:

import numpy as np

class NearestNeighbor(object):
  def __init__(self):
    pass

  def train(self, X, y):
    """ X 是 NxD 维的数组,每一行都是一个样本,比如一张图片,D 是样本的数据维度;
    Y 是长度为 N 的一维数组。"""
    # 最邻近分类器只是简单的记住所有的训练数据
    self.Xtr = X
    self.ytr = y

  def predict(self, X):
    """ X 是 NxD 维的数组,每一行都是一个希望预测其标签的样本 """
    num_test = X.shape[0]
    # 确保输出的标签数据类型和输入的标签格式一致,长度是测试样本数
    Ypred = np.zeros(num_test, dtype = self.ytr.dtype)

    # 循环所有测试样本数,即测试数组的行数
    for i in range(num_test):
      # 为第 i 张测试图片找到最接近的训练图片
      # 使用 L1 距离 (差值的绝对值求和)
      '''self.Xtr - X[i,:] 利用传播机制,求测试集第 i 张图片对应的行向量和
      训练集所有图片行向量的差值,得到一个一个 50000x3072 的差值矩阵;
      abs(self.Xtr - X[i,:] )会将矩阵所有元素求绝对值;
      然后 axis = 1 会对差值矩阵按行求和,最终得到一个长度为 50000 的一维
      数组,存放第 i 张图片和训练集所有 50000 张图片的 L1 距离。'''
      distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
      min_index = np.argmin(distances) # 获取距离最小的训练集图片索引
      Ypred[i] = self.ytr[min_index] # 预测第 i 张测试集图片的标签时与其最接近的训练集图片索引

    return Ypred 

这段代码的训练时间复杂度为 O ( 1 ) O(1) O(1),因为只是简单的存储数据,不管数据多大,都是一个相对固定的时间;如果训练集有 N N N 个样本,则预测时间复杂度为 O ( N ) O(N) O(N),因为测试图片要和训练集每张图片进行比较。

这是一个不太好的分类器,实际对分类器的要求是,我们希望它预测的时候要快,训练的时候可以慢

这段代码跑 CIFAR-10,准确率能达到 38.6 % 38.6% 38.6%。这比随机猜测的 10 % 10% 10% 要好,但是比人类识别的水平和卷积神经网络能达到的 95 % 95% 95% 还是差很多。

3) L2 距离(欧式距离)

距离度量的数学知识也可以参考ShowMeAI的系列教程图解 AI 数学基础中的文章线性代数与矩阵论对各种距离度量的展开讲解

另一个常用的方法是 L2 距离,从几何学的角度,可以理解为它在计算两个向量间的欧式距离。L2 距离的公式如下:

d 2 ( I 1 , I 2 ) = ∑ p ( I 1 p − I 2 p ) 2 d_{2} (I_{1},I_{2})=\sqrt{\sum_{p}(I_{1}^p - I_{2}^p )² } d2​(I1​,I2​)=p∑​(I1p​−I2p​)2 ​

  • 依旧是在计算像素间的差值,只是先求差值的平方,然后把这些平方全部加起来,最后对这个和开方。

此时的代码只需改动计算距离差异的一行:

distances = np.sqrt(np.sum(np.square(self.Xtr - X[i,:]), axis = 1))
'''np.square(self.Xtr - X[i,:]) 会对差值矩阵的每一个元素求平方''' 

注意在这里使用了 np.sqrt,但是在实际中可能不用。因为对不同距离的绝对值求平方根虽然改变了数值大小,但依然保持了不同距离大小的顺序。这个模型,正确率是 35.4 % 35.4% 35.4%,比刚才低了一点。

4) L1 和 L2 比较

在 L1 距离更依赖于坐标轴的选定,坐标轴选择不同 L1 距离也会跟着变化,判定的数据归类的边界会更趋向于贴近坐标系的轴来分割所属区域,而 L2 的话相对来说与坐标系的关联度没那么大,会形成一个圆,不跟随坐标轴变化。

图像分类; 最近邻算法; L1 距离 V.S.L2 距离; 2-7

在面对两个向量之间的差异时,L2 比 L1 更加不能容忍这些差异。也就是说,相对于 1 个巨大的差异,L2 距离更倾向于接受多个中等程度的差异(因为会把差值平方)

L1 和 L2 都是在 p-norm 常用的特殊形式。

当图像中有特别在意的特征时可以选择 L1 距离;当对图像中所有元素未知时,L2 距离会更自然一些。最好的方式是两种距离都尝试,然后找出最好的那一个。

2.2 k 最近邻分类器

本部分内容也可以参考ShowMeAI图解机器学习教程中的文章详解KNN 算法及其应用

只用最相似的 1 张图片的标签来作为测试图像的标签,有时候会因为参照不够多而效果不好,我们可以使用 k-Nearest Neighbor 分类器KNN 的思想是:找最相似的 k k k 个图片的标签, k k k 中数量最多的标签作为对测试图片的预测

当 k = 1 k=1 k=1 的时候,k-Nearest Neighbor 分类器就是上面所说的最邻近分类器。

如下图所示,例子使用了 2 维的点来表示图片,分成 3 类(红、绿、蓝)。不同颜色区域代表的是使用 L2 距离的分类器的决策边界。

图像分类; NN 分类器 V.S.KNN 分类器; 2-8

上面示例展示了 NN 分类器和 KNN( k = 5 k=5 k=5)分类器的区别。从直观感受上就可以看到,更高的 k k k 值可以让分类的效果更平滑,使得分类器对于异常值更有抵抗力。

  • 在 k = 1 k=1 k=1 时,异常的数据点(比如:在蓝色区域中的绿点)制造出一个不正确预测的孤岛。

  • 在 k = 5 k=5 k=5 时分类器将这些不规则都平滑了,使得它针对测试数据的泛化(generalization)能力更好。

    • 注意,5-NN 中也存在一些白色区域,这些区域是因为 5 个近邻标签中的最高数相同导致的分类模糊(即图像与两个以上的分类标签绑定)。
    • 比如:2 个邻居是红色,2 个邻居是蓝色,还有 1 个是绿色,所以无法判定是红色还是蓝色。

1) 超参数调优

模型调优,超参数的实验选择方法也可以参考ShowMeAI的文章 图解机器学习 | 模型评估方法与准则深度学习教程 | 网络优化:超参数调优、正则化、批归一化和程序框架

  • KNN 分类器需要设定 k k k 值,如何选择 k k k 值最合适
  • L1 距离和 L2 距离选哪个比较好(还是使用其他的距离度量准则例如点积)

所有这些选择,被称为超参数(hyperparameter)。在基于数据进行学习的机器学习算法设计中,超参数是很常见的。

超参数是需要提前设置的,设置完成后模型才可以训练学习,具体的设置方法通常要借助于实验,尝试不同的值,根据效果表现进行选择。

特别注意:不能使用测试集来进行调优

  • 如果使用测试集来调优,而且算法看起来效果不错,真正的危险在于:算法实际部署后,性能可能会远低于预期。这种情况,称之为算法对测试集过拟合。

  • 大家可以理解为,如果使用测试集来调优,实际上就是把测试集当做训练集,由测试集训练出来的算法再预测测试集,性能自然会看起来很好,但实际部署起来效果就会差很多。

  • 最终测试的时候再使用测试集,可以很好地近似度量分类器的泛化性能。

测试数据集只能使用一次,而且是在训练完成后评价最终模型时使用,不可用来调优

方法 1:设置验证集

从训练集中取出一部分数据用来调优,称之为 验证集(validation set)。以 CIFAR-10 为例,可以用 49000 个图像作为训练集,用 1000 个图像作为验证集。验证集其实就是作为假的测试集来调优。

图像分类; 超参数调优; 设置验证集; 2-9

代码如下:

# 假设 Xtr_rows, Ytr, Xte_rows, Yte 还是和之前一样
# Xtr_rows 是 50,000 x 3072 的矩阵
Xval_rows = Xtr_rows[:1000, :] # 取前 1000 个训练集样本作为验证集
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :] # 剩下的 49,000 个作为训练集
Ytr = Ytr[1000:]

# 找出在验证集表现最好的超参数 k 
validation_accuracies = []
for k in [1, 3, 5, 10, 20, 50, 100]:
  # 使用一个明确的 k 值评估验证集
  nn = NearestNeighbor()
  nn.train(Xtr_rows, Ytr)
  # 这里假设一个修正过的 NearestNeighbor 类,可以把 k 值作为参数输入
  Yval_predict = nn.predict(Xval_rows, k = k)
  acc = np.mean(Yval_predict == Yval)
  print 'accuracy: %f' % (acc,)

  # 把每个 k 值和相应的准确率保存起来
  validation_accuracies.append((k, acc)) 

程序结束后,作图分析出哪个 k k k 值表现最好,然后用这个 k k k 值来跑真正的测试集,并作出对算法的评价。

方法 2:交叉验证

训练集数量较小(因此验证集的数量更小)时,可以使用交叉验证的方法。还是用刚才的例子,如果是交叉验证集,我们就不是取 1000 个图像,而是将训练集平均分成 5 份,每份 10000 张图片,其中 4 份用来训练,1 份用来验证。然后我们循环着取其中 4 份来训练,其中 1 份来验证,最后取所有 5 次验证结果的平均值作为算法验证结果。

图像分类; 超参数调优; 交叉验证; 2-10

下面是 5 份交叉验证对 k k k 值调优的例子。针对每个 k k k 值,得到 5 次验证的准确率结果,取其平均值,然后对不同 k k k 值的平均表现画线连接。

图像分类; 超参数调优; k 折交叉验证效果; 2-11

上图可以看出,本例中,当 k = 7 k=7 k=7 的时算法表现最好(对应图中的准确率峰值)。如果我们将训练集分成更多份数,直线一般会更加平滑(噪音更少)。

实际情况下,深度学习不会使用交叉验证,主要是因为它会耗费较多的计算资源。一般直接把训练集按照 50 % ∼ 90 % 50% \sim 90% 50%∼90% 的比例分成训练集和验证集。但是训练集数量不多时可以使用交叉验证,一般都是分成 3、5 和 10 份。

2) KNN 分类器优点

① 易于理解,实现简单。
② 算法的训练不需要花时间,因为其训练过程只是将训练集数据存储起来。

3) KNN 分类器缺点

① 测试要花费大量时间

  • 因为每个测试图像需要和所有存储的训练图像进行比较在实际应用中,关注测试效率远远高于训练效率;

② 使用像素差异来比较图像是不够的,图片间 L2 距离小,更多的是被背景主导而不是图片语义内容本身主导,往往背景相似图片的 L2 距离就会小

  • 也就是说,在高维度数据上,基于像素的相似和基于感官上的相似非常不同。感官上不同的两张图片,可能有相同的 L2 距离。

③ 维度灾难

  • KNN 有点像训练数据把样本空间分成几块,我们需要训练数据密集的分布在样本空间里,否则测试图片的最邻近点可能实际距离会非常远,导致和最接近的训练集样本实际上完全不同。但是如果使训练数据密集分布,需要的训练集数量指数倍增加,是数据维度的平方。

4) 实际应用 KNN

下面是一些对于实际应用 KNN 算法的建议

① 预处理数据

  • 对数据中的特征进行归一化(normalize),让其具有零均值(zero mean)和单位方差(unit variance)。本小节不讨论,是因为图像中的像素都是同质的,不会表现出较大的差异分布,不需要标准化处理。

② 降维

  • 如果数据是高维数据,考虑使用降维方法,比如 PCA 或者随机投影。

③ 将数据随机分入训练集和验证集

  • 一般规律, 70 % ∼ 90 % 70% \sim 90% 70%∼90% 数据作为训练集。这个比例根据算法中有多少超参数,以及这些超参数对于算法的预期影响来决定。
  • 如果需要预测的超参数很多,那么就应该使用更大的验证集来有效地估计它们;如果担心验证集数量不够,那么就尝试交叉验证方法;如果计算资源足够,使用交叉验证更好(份数越多,效果越好,也更耗费计算资源)。

④ 在验证集上调优

  • 尝试足够多的 k k k 值,尝试 L1 和 L2 两种范数计算方式。

⑤ 加速分类器

  • 如果分类器跑得太慢,尝试使用 ANN 库(比如 FLANN 来加速这个过程,其代价是降低一些准确率。

⑥ 对最优的超参数做记录

  • 记录最优参数后,不要使用最优参数的算法在完整的训练集上运行并再次训练,这样做会破坏对于最优参数的估计。
  • 直接使用测试集来测试用最优参数设置好的最优模型,得到测试集数据的分类准确率,并以此作为你的 KNN 分类器在该数据上的性能表现。

3.线性分类:评分函数

3.1 线性分类概述

KNN 模型中训练过程中没有使用任何参数,只是单纯的把训练数据存储起来(参数 k 是在预测中使用的,找出 k k k 个接近的图片,然后找出标签最多的,并且 k k k 是超参数,是人为设定的)。

与之相对的是参数模型,参数模型往往会在训练完成后得到一组参数,之后就可以完全扔掉训练数据,预测的时候只需和这组参数做某种运算,即可根据运算结果做出判断。线性分类器是参数模型里最简单的一种,但却是神经网络里很重要的基础模块。

线性分类的方法由两部分组成:

① 评分函数(score function)

  • 它是原始图像数据到类别分值的映射。

② 损失函数(loss function)

  • 它用来量化评分函数计算的分数与真实标签之间的一致性。该方法可转化为一个最优化问题,在最优化过程中,通过更新评分函数的参数来最小化损失函数值。

3.2 评分函数

评分函数将图像的像素值映射为各个分类类别的得分,得分高低代表图像属于该类别的可能性高低。上面的所有说明都比较抽象,下面以具体的例子说明。

重新回到 KNN 使用的 CIFAR-10 图像分类数据集。

图像分类; 评分函数; 参数化方式-线性分类器; 2-12

假设我们的训练集有 N N N 个样本,这里 N = 50000 N=50000 N=50000,每个样本 x i b ∈ R D x_{i}b \in R^D xi​b∈RD,其中 i = 1 , 2 , ⋯   , N i = 1,2,\cdots,N i=1,2,⋯,N, D = 3072 D=3072 D=3072;每个 x i x_{i} xi​ 对应着一个标签 y i y_{i} yi​,$ y_{i}$ 在 [ 1 , K ] [1, K] [1,K] 上取值, K K K 表示总分类数,这里 K = 10 K=10 K=10。现在可以定义评分函数: f : R D → R K f:R^D \rightarrow R^K f:RD→RK,即把一个 D D D 维的图像映射为 K K K 个类别的分数。

最简单的模型是线性模型:参数和输入数据相乘。即:

f ( x i , W , b ) = W x i + b f(x_{i},W,b)=Wx_{i}+b f(xi​,W,b)=Wxi​+b

  • 上式中参数 W W W 被称为权重, b b b 被称为偏置项
  • 在上面的公式中,假设每个图像数据都被拉长为一个长度为 D D D 的列向量,大小为 [ D × 1 ] [D \times 1] [D×1]。其中大小为 [ K × D ] [K \times D] [K×D] 的矩阵 W W W 和大小为 [ K × 1 ] [K \times 1] [K×1] 的列向量 b b b 为该函数的参数(parameters)

还是以 CIFAR-10 为例, x i x_{i} xi​ 就包含了第 i i i 个图像的所有像素信息,这些信息被拉成为一个 [ 3072 × 1 ] [3072 \times 1] [3072×1] 的列向量, W W W 大小为 [ 10 × 3072 ] [10 \times 3072] [10×3072], b b b 的大小为 [ 10 × 1 ] [10 \times 1] [10×1]。因此,输入 3072 3072 3072 个数字(原始像素数值),函数输出 10 10 10 个数字(不同分类得到的分值),是一个 3072 3072 3072 维到 10 10 10 维的映射。

注意:

  • 常常混用权重(weights)和参数(parameters)这两个术语,实际上数据和参数相乘,就相当于数据占的比重,这个权重就是参数值;
  • 该方法的一个优势是训练数据是用来学习参数 W W W 和 b b b 的,一旦训练完成,训练数据就可以丢弃,留下学习到的参数即可。当测试图像时可以简单地把图像数据输入给函数,函数计算出的分类分值来进行分类;
  • 输入数据 ( x i , y i ) (x_{i},y_{i}) (xi​,yi​) 是给定且不可改变的,但参数 W W W 和 b b b 是可改变的。目标就是通过改变这些参数,使得计算出来的分类分值情况和训练集中图像数据的真实类别标签相符;
  • 只需一个矩阵乘法和一个矩阵加法就能对一个测试数据分类,这比 KNN 中将测试图像和所有训练数据做比较的方法要高效很多。

3.3 理解线性分类器

1) 理解一:W 是所有分类器的组合

图像分类; 线性分类器; 理解 1-计算评分函数; 2-13

如上图所示,将小猫的图像像素数据拉伸成一个列向量 x i x_i xi​,这里为方便说明,假设图像只有 4 个像素(都是黑白像素,不考虑 RGB 通道),即 D = 4 D=4 D=4;有 3 3 3 个分类(红色代表猫,绿色代表狗,蓝色代表船,颜色仅代表不同类别,和 RGB 通道没有关系),即 K = 3 K=3 K=3。 W W W 矩阵乘列向量 x i x_i xi​,得到各个分类的分值。

实际上,我们可以看到,参数矩阵 W W W 相当于是三个分类器的组合, W W W 的每一行都是一个分类器,分别对应猫、狗、船。在线性模型中每个分类器的参数个数与输入图像的维度相当,每个像素和对应的参数相乘,就表示该像素在该分类器中应占的比重。

需要注意的是,这个 W W W 一点也不好:猫分类的分值非常低。从上图来看,算法倒是觉得这个图像是一只狗。

我们可以这样理解,线性分类器会计算图像中 3 个颜色通道中所有像素的值与权重矩阵的乘积,进而得到每个类别分值。根据我们对权重设置的值,对于图像中的某些位置的某些颜色,函数表现出喜好或者厌恶(根据每个权重的符号而定)。

举例:可以想象 「船」 分类就是被大量的蓝色所包围(对应的就是水)。那么 「船」 分类器在蓝色通道上的权重就有很多的正权重(它们的出现提高了 「船」 分类的分值),而在绿色和红色通道上的权重为负的就比较多(它们的出现降低了 「船」 分类的分值)。

结合上面的小猫示例,猫分类器对第二个位置的像素比较 「厌恶」 ,而恰好输入的小猫图像第二个位置像素值很大,最终计算得到一个很低的分数(当然,这个分类器是错误的)。

2) 理解二:将线性分类器看做模板匹配

把权重 W W W 的每一行看作一个分类的模板,一张图像对应不同分类的得分,是通过使用内积(也叫点积)来比较图像和模板,然后找到和哪个模板最相似

这种理解角度下,线性分类器在利用学习到的模板,和输入图像做模板匹配。我们设置可以把其视作一种高效的 KNN,不同的是不再使用所有的训练集的图像来比较,而是每个类别只用了一张图片来表征(这张图片是我们学习到的模板,而不存在训练集中),而且我们会更换度量标准,使用(负)内积来计算向量间的距离,而不是使用 L1 或者 L2 距离。

图像分类; 线性分类器; 理解 2-10 个学习后模板; 2-14

上图是以 CIFAR-10 为训练集,学习结束后的权重的例子。可以看到:

  • 马的模板看起来似乎是两个头的马,这是因为训练集中的马的图像中马头朝向各有左右造成的。线性分类器将这两种情况融合到一起了;
  • 汽车的模板看起来也是将几个不同的模型融合到了一个模板中,这个模板上的车是红色的,是因为 CIFAR-10 中训练集的车大多是红色的。线性分类器对于不同颜色的车的分类能力是很弱的,但是后面可以看到神经网络是可以完成这一任务的;
  • 船的模板如期望的那样有很多蓝色像素。如果图像是一艘船行驶在大海上,那么这个模板利用内积计算图像将给出很高的分数。

3) 理解三:将图像看做高维空间的点

既然定义每个分类类别的分值是权重和图像的矩阵乘积,那么每个分类类别的分数就是这个空间中的一个线性函数的函数值。我们没办法可视化 3072 3072 3072 维空间中的线性函数,但假设把这些维度挤压到二维,那么就可以看看这些分类器在做什么了:

图像分类; 线性分类器; 理解 3-二维空间划分; 2-15

在上图中,每张输入图片是一个点,不同颜色的线代表 3 个不同的分类器。以红色的汽车分类器为例,红线表示空间中汽车分类分数为 0 0 0 的点的集合,红色的箭头表示分值上升的方向。所有红线右边的点的分数值均为正,且线性升高。红线左边的点分值为负,且线性降低。

从上面可以看到, W W W 的每一行都是一个分类类别的分类器。对于这些数字的几何解释是:

  • 如果改变 W W W 一行的数字取值,会看见分类器在空间中对应的直线开始向着不同方向旋转。而偏置项 b b b,则允许分类器对应的直线平移
  • 需要注意的是,如果没有偏置项,无论权重如何,在 x i = 0 x_{i}=0 xi​=0 时分类分值始终为 0 0 0。这样所有分类器的线都不得不穿过原点

3.4 偏置项和权重合并

上面的推导过程大家可以看到:实际我们有权重参数 W W W 和偏置项参数 b b b 两个参数,分开处理比较冗余,常用的优化方法是把两个参数放到同一个矩阵中,同时列向量 x i x_{i} xi​ 就要增加一个维度,这个维度的数值是常量 1 1 1,这就是默认的偏置项维度

如下图所示,新的公式就简化成如下形式:

f ( x i , W , b ) = W x i f(x_{i},W,b)=Wx_{i} f(xi​,W,b)=Wxi​

图像分类; 线性分类器; W 和 b 合并后示意图; 2-16

还是以 CIFAR-10 为例,那么 x i x_{i} xi​ 的大小就变成 [ 3073 × 1 ] [3073 \times 1] [3073×1],而不是 [ 3072 × 1 ] [3072 \times 1] [3072×1] 了,多出了包含常量 1 的 1 个维度; W W W 大小就是 [ 10 × 3073 ] [10 \times 3073] [10×3073] 了, W W W 中多出来的这一列对应的就是偏差值 b b b:

经过这样的处理,最终只需学习一个权重矩阵,无需学习两个分别装着权重和偏差的矩阵。

3.5 图像数据预处理

在上面的例子中,所有图像都是使用的原始像素值( 0 ∼ 255 0 \sim 255 0∼255)。在机器学习中,我们经常会对输入的特征做归一化(normalization)处理,对应到图像分类的例子中,图像上的每个像素可以看做一个特征。

在实践中,我们会有对每个特征减去平均值来中心化数据这样一个步骤。

在这些图片的例子中,该步骤是根据训练集中所有的图像计算出一个平均图像值,然后每个图像都减去这个平均值,这样图像的像素值就大约分布在 [ − 127 , 127 ] [-127, 127] [−127,127] 之间了。

后续可以操作的步骤包括归一化,即让所有数值分布的区间变为 [ − 1 , 1 ] [-1, 1] [−1,1]。

3.6 线性分类器失效的情形

图像分类; 线性分类器; 难以处理的情形; 2-17

线性分类器的分类能力实际是有限的,例如上图中的这三种情形都无法找到合适的直线区分开。其中第 1 个 case 是奇偶分类,第 3 个 case 是有多个模型。

4.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=2

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

5.要点总结

  • 图像分类中的困难与挑战
  • 数据驱动方法、最邻近算法、 L1 和 L2 距离
  • KNN 分类器、超参数调优、KNN 的优缺点与实际应用
  • 线性分类的概念、评分函数的理解、参数合并、数据预处理、线性分类器局限性

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(3) | 损失函数与最优化(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/124994019

ShowMeAI 研究中心


Loss Functions and Optimization; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

在上一篇 深度学习与计算机视觉教程(2) - 图像分类与机器学习基础 内容中,我们对线性分类器做了一些介绍,我们希望线性分类器能够准确地对图像进行分类,要有一套优化其权重参数的方法,这就是本篇ShowMeAI要给大家介绍到的损失函数与最优化相关的知识。

本篇重点

  • 损失函数
  • 数据损失与正则损失
  • SVM 损失
  • Softmax 损失
  • 优化策略
  • 梯度计算方法
  • 梯度下降

1.线性分类:损失函数

1.1 损失函数的概念

回到之前讲解过的小猫分类示例,这个例子中权重值 W W W 非常差,因为猫类别的得分非常低(-96.8),而狗(437.9)和船(61.95)比较高。

损失函数与最优化; 线性分类器; 损失函数的概念; 3-1

我们定义损失函数(Loss Function)(有时也叫代价函数 Cost Function目标函数 Objective) L L L 来衡量对预估结果的「不满意程度」。当评分函数输出结果与真实结果之间差异越大,损失函数越大,反之越小。

对于有 N N N 个训练样本对应 N N N 个标签的训练集数据 ( x i , y i ) (x_{i},y_{i}) (xi​,yi​)),损失函数定义为:

L = 1 N ∑ i = 1 N L i ( f ( x i , W ) , y i ) L=\frac{1}{N} \sum_{i=1}^NL_i(f(x_i,W), y_i) L=N1​i=1∑N​Li​(f(xi​,W),yi​)

  • 即每个样本损失函数求和取平均。目标就是找到一个合适的 W W W 使 L L L 最小。
  • 注意:真正的损失函数 L L L 还有一项正则损失 R ( W ) R(W) R(W),下面会有说明。

损失函数有很多种,下面介绍最常见的一些。

1.2 多类支持向量机损失 (Multiclass Support Vector Machine Loss)

SVM 的知识可以参考ShowMeAI图解机器学习教程中的文章支持向量机模型详解,多类 SVM 可以看作二分类 SVM 的一个推广,它可以把样本数据分为多个类别。

1) 数据损失(data loss)

SVM 的损失函数想要 SVM 在正确分类上的得分始终比不正确分类上的得分高出一个边界值 Δ \Delta Δ。

我们先看一条数据样本(一张图片)上的损失函数 L i L_i Li​ 如何定义,根据之前的描述,第 i i i 个数据 ( x i , y i ) (x_{i},y_{i}) (xi​,yi​) )中包含图像 x i x_i xi​ 的像素和代表正确类别的标签 y i y_i yi​。给评分函数输入像素数据,然后通过公式 f ( x i , W ) f(x_i, W) f(xi​,W) )来计算不同分类类别的分值。

这里我们将所有分值存放到 s s s 中,第 j j j 个类别的得分就是 s s s 的第 j j j 个元素: s j = f ( x i , W j ) s_j = f(x_i, W_j) sj​=f(xi​,Wj​)。针对第 i i i 条数据样本的多类 SVM 的损失函数定义如下:

L i = ∑ j ≠ y i max ⁡ ( 0 , s j − s y i + Δ ) L_i = \sum_{j\neq y_i} \max(0, s_j - s_{y_i} + \Delta) Li​=j​=yi​∑​max(0,sj​−syi​​+Δ)

直观来看,就是如果评分函数给真实标签的分数比其他某个标签的分数高出 Δ \Delta Δ,则对该其他标签的损失为 0 0 0;否则损失就是 s j − s y i + Δ s_j - s_{y_i}+ \Delta sj​−syi​​+Δ。要对所有不正确的分类循环一遍。

下面用一个示例来解释一下:

损失函数与最优化; 多类支持向量机损失; 损失𝐿_𝑖的计算; 3-2

简化计算起见,我们只使用 3 个训练样本,对应 3 个类别的分类, y i = 0 , 1 , 2 y_i =0,1,2 yi​=0,1,2 对于第 1 张图片 「小猫」 来说,评分 s = [ 3.2 , 5.1 , − 1.7 ] s=[3.2, 5.1, -1.7] s=[3.2,5.1,−1.7] 其中 s y i = 3.2 s_{y_i}=3.2 syi​​=3.2 如果把 Δ \Delta Δ 设为 1 1 1,则针对小猫的损失函数:

L 1 = m a x ( 0 , 5.1 − 3.2 + 1 ) + m a x ( 0 , − 1.7 − 3.2 + 1 ) = m a x ( 0 , 2.9 ) + m a x ( 0 , − 3.9 ) = 2.9 + 0 = 2.9 L_1 = max(0, 5.1 - 3.2 + 1) +max(0, -1.7 - 3.2 + 1) = max(0, 2.9) + max(0, -3.9) = 2.9 + 0 =2.9 L1​=max(0,5.1−3.2+1)+max(0,−1.7−3.2+1)=max(0,2.9)+max(0,−3.9)=2.9+0=2.9

同理可得 L 2 = 0 L_2 =0 L2​=0, L 3 = 12.9 L_3 =12.9 L3​=12.9,所以对整个训练集的损失: L = ( 2.9 + 0 + 12.9 ) / 3 = 5.27 L= (2.9 + 0 + 12.9)/3 =5.27 L=(2.9+0+12.9)/3=5.27。

上面可以看到 SVM 的损失函数不仅想要正确分类类别 y i y_i yi​ 的分数比不正确类别分数高,而且至少要高 Δ \Delta Δ。如果不满足这点,就开始计算损失值。

损失函数与最优化; 多类支持向量机损失; SVM 损失中安全间隔 Delta 示意图; 3-3

展开一点解释如下:之所以会加入一个 Δ \Delta Δ,是为了真实标签的分数比错误标签的分数高出一定的距离,如上图所示,如果其他分类分数进入了红色的区域,甚至更高,那么就开始计算损失;如果没有这些情况,损失值为 0 0 0:

  • 损失最小是 0 0 0,最大无穷;
  • 如果求和的时候,不加 j ≠ y i j\neq y_i j​=yi​ 这一条件, L L L 会加 Δ \Delta Δ;
  • 计算 L i L_i Li​ 时使用平均不用求和,只会缩放 L L L 不会影响好坏;而如果使用平方,就会打破平衡,会使坏的更坏, L L L 受到影响。

在训练最开始的时候,往往会给 W W W 一个比较小的初值,结果就是 s s s 中所有值都很小接近于 0 0 0,此时的损失 L L L 应该等于分类类别数 K − 1 K-1 K−1,这里是 2 2 2。可根据这个判断代码是否有问题;

非向量化和向量化多类 SVM 损失代码实现如下:

def L_i(x, y, W):
  """
  非向量化版本。
  计算单个例子(x,y)的多类 SVM 损失    
  - x 是表示图像的列向量(例如,CIFAR-10 中的 3073 x 1),附加偏置维度
  - y 是一个给出正确类索引的整数(例如,CIFAR-10 中的 0 到 9 之间)    
  - W 是权重矩阵(例如,CIFAR-10 中的 10 x 3073)  """
  delta = 1.0 # 间隔 delta
  scores = W.dot(x) # 得分数组,10 x 1
  correct_class_score = scores[y]
  D = W.shape[0] # 分类的总数,即为 10
  loss_i = 0.0
  for j in range(D): # 迭代所有错误分类   
    if j == y:
      # 跳过正确分类的
      continue
    # 第 i 个样本累加损失
    loss_i += max(0, scores[j] - correct_class_score + delta)
  return loss_i

def L_i_vectorized(x, y, W):
  '''
  更快的半向量化实现。
  half-vectorized 指的是这样一个事实:对于单个样本,实现不包含 for 循环,
  但是在样本外仍然有一个循环(在此函数之外)
  '''
  delta = 1.0
  scores = W.dot(x)
  # 用一个向量操作计算和所有类别的间隔
  margins = np.maximum(0, scores - scores[y] + delta)
  # y 处的值应该为 0  
  margins[y] = 0
  loss_i = np.sum(margins)
  return loss_i 

这里的评分函数 f ( x i ; W ) = W x i f(x_i; W) = W x_i f(xi​;W)=Wxi​,所以损失函数可以写为:

L i = ∑ j ≠ y i max ⁡ ( 0 , w j T x i − w y i T x i + Δ ) L_i = \sum_{j\neq y_i} \max(0, w_j^T x_i - w_{y_i}^T x_i + \Delta) Li​=j​=yi​∑​max(0,wjT​xi​−wyi​T​xi​+Δ)

  • 其中 w j w_j wj​ 是 W W W 的第 j j j 行,然后被拉成一个行列向量,与 $x_i $ 列向量做点积。

m a x ( 0 , − ) max(0,-) max(0,−) 函数,常被称为合页损失hinge loss)。比如平方合页损失 SVM (即 L2 - SVM ),它使用的是 m a x ( 0 , − ) 2 max(0,-)² max(0,−)2 ),将更强烈(平方地而不是线性地)地惩罚过界的边界值。不使用平方是更标准的版本,但是在某些数据集中,平方合页损失会工作得更好。可以通过交叉验证来决定到底使用哪个。

总结:我们对于预测训练集数据分类标签的结果,有一些不满意的地方,而损失函数就能将这些不满意的程度量化。

2) 正则化损失(regularization loss)

假设有 1 个数据集和 1 组权重 W W W 能够正确地分类每个数据,即所有 L i L_i Li​ 都为 0 0 0,这样的 W W W 是否唯一?其实只要是任意 λ > 1 \lambda >1 λ>1, λ W \lambda W λW 都可以满足 L i = 0 L_i = 0 Li​=0,因为把差值放大 λ \lambda λ 倍后,仍然会大于 Δ \Delta Δ。

所以,我们希望对某些 W W W 添加一些偏好,让我们的 W 更趋向于希望的形式,一个常见的做法是向损失函数增加一个正则化惩罚regularization penalty) R ( W ) R(W) R(W) ,它同时也能让模型更加泛化。

结合上述思路我们得到完整的多类 SVM 损失函数,它由两个部分组成:数据损失data loss),即所有样例的平均损失,以及正则化损失****(regularization loss)。完整公式如下:

L = 1 N ∑ i L i ⏟ data loss  + λ R ( W ) ⏟ regularization loss  L=\underbrace{\frac{1}{N} \sum_{i} L_{i}}{\text {data loss }}+\underbrace{\lambda R(W)}{\text {regularization loss }} L=data loss  N1​i∑​Li​​​+regularization loss  λR(W)​​

① 常用的正则化损失

  • 最常用的 R(W)是 L2 范式, W W W 每个元素平方后加起来作为惩罚项,可以限制大的权重,更希望 W W W 的元素分布比较均匀:

R ( W ) = ∑ k ∑ l W k , l 2 R(W) = \sum_k\sum_l W_{k,l}² R(W)=k∑​l∑​Wk,l2​

  • 除此之外还有 L1 范式,作为惩罚项更希望一个比较简单的模型,即 W W W 中有很多的 0 0 0:

R ( W ) = ∑ k ∑ l ∣ W k , l ∣ R(W) = \sum_k\sum_l \vert W_{k,l}\vert R(W)=k∑​l∑​∣Wk,l​∣

  • L1 和 L2 也可以组合起来:

R ( W ) = ∑ k ∑ l β W k , l 2 + ∣ W k , l ∣ R(W) = \sum_k\sum_l \beta W_{k,l}² + \vert W_{k,l}\vert R(W)=k∑​l∑​βWk,l2​+∣Wk,l​∣

② 对正则化损失的理解

引入 L2 范数正则化损失最好的性质就是对大数值权重进行惩罚,可以提升其泛化能力,因为这就意味着没有哪个维度能够独自对于整体分值有过大的影响。

举个例子,假设输入向量 x = [ 1 , 1 , 1 , 1 ] x = [1,1,1,1] x=[1,1,1,1],两个权重向量 w 1 = [ 1 , 0 , 0 , 0 ] w_1 = [1,0,0,0] w1​=[1,0,0,0], w 2 = [ 0.25 , 0.25 , 0.25 , 0.25 ] w_2 = [0.25,0.25,0.25,0.25] w2​=[0.25,0.25,0.25,0.25]。那么 w 1 T x = w 2 T x = 1 w_1^Tx = w_2^Tx = 1 w1T​x=w2T​x=1。两个权重向量都得到同样的内积,但是 w 1 w_1 w1​ 的 L2 惩罚是 1.0,而 w 2 w_2 w2​ 的 L2 惩罚是 0.25 0.25 0.25。因此,根据 L2 惩罚来看, w 2 w_2 w2​ 更好,因为它的正则化损失更小。从直观上来看,这是因为 w 2 w_2 w2​ 的权重值更小且更分散,这就会鼓励分类器最终将所有维度上的特征都用起来,而不是强烈依赖其中少数几个维度。这一效果将会提升分类器的泛化能力,并避免过拟合。

注意,和权重不同,偏置项没有这样的效果,因为它们并不控制输入维度上的影响强度。因此通常只对权重 W W W 正则化,而不正则化偏置项 b b b

同时,因为正则化惩罚的存在,不可能在所有的例子中得到 0 0 0 的损失值,这是因为只有当 W = 0 W=0 W=0 的特殊情况下,才能得到损失值为 0 0 0。

但是从 L1 惩罚来看, w 1 w_1 w1​ 可能会更好一些,当然这里 L1 惩罚相同,但是一般来说,L1 惩罚更希望 W W W 比较稀疏,最好是有很多为 0 0 0 的元素,这一特性可以用来在不改变模型的基础上防止过拟合。

比如下面的例子中:

损失函数与最优化; L1 惩罚; 用来防止过拟合; 3-4

假设我们的训练数据得到的模型是蓝色的曲线,可以看出应该是一个多项式函数,比如 f = w 1 x 1 + w 2 x 2 2 + w 3 x 3 3 + w 4 x 4 4 f=w_1x_1+w_2x_2²+w_3x_3³+w_4x_4⁴ f=w1​x1​+w2​x22​+w3​x33​+w4​x44​。但是当新的绿色数据输入时,显然模型是错误的,更准确的应该是绿色的线。

如果我们使用 L1 惩罚,由于 L1 惩罚的特性,会希望 W W W 变得稀疏,可让 w 2 , w 3 , w 4 w_2,w_3,w_4 w2​,w3​,w4​ 变成接近 0 0 0 的数,这样就可以在不改变模型的情况下,让模型变得简单泛化。

思考超参数 Δ \Delta Δ 和 λ \lambda λ 应该被设置成什么值需要通过交叉验证来求得吗

  • Δ \Delta Δ 在绝大多数情况下设为 1 都是安全的。
  • Δ \Delta Δ 和 λ \lambda λ 看起来是两个不同的超参数,但实际上他们一起控制同一个权衡:即损失函数中的数据损失和正则化损失之间的权衡。
  • 理解这一点的关键是,权重 W W W 的大小对于分类分值有直接影响(对他们的差异也有直接影响):当我们将 W W W 中值缩小,分类分值之间的差异也变小,反之亦然。
  • 因此,不同分类分值之间的边界的具体值 Δ = 1 \Delta=1 Δ=1 或 Δ = 100 \Delta=100 Δ=100 从某些角度来看是没意义的,因为权重自己就可以控制差异变大和缩小。也就是说,真正的权衡是我们允许权重能够变大到何种程度(通过正则化强度 λ \lambda λ 来控制)。

③ 与二元 SVM 的关系

二元 SVM 对于第 i i i 个数据的损失计算公式是:

L i = C max ⁡ ( 0 , 1 − y i w T x i ) + R ( W ) L_i = C \max(0, 1 - y_i w^Tx_i) + R(W) Li​=Cmax(0,1−yi​wTxi​)+R(W)

其中, C C C 是一个超参数,并且 y i ∈ { − 1 , 1 } y_i \in { -1,1 } yi​∈{−1,1},这个公式是多类 SVM 公式只有两个分类类别的特例, C C C 和 λ \lambda λ 的倒数正相关。比如对真实标签为 y i = 1 y_i=1 yi​=1 的数据得分是 50 50 50,则 L i = 0 L_i=0 Li​=0。这里只用到了 y i = 1 y_i=1 yi​=1 标签的得分,因为二元 SVM 的 W 只有一行,只有一个得分并且是自身分类的得分,只要这个得分和 y i y_i yi​ 的乘积大于 1 1 1 就是预测正确的了。

最终,我们得到了多类 SVM 损失的完整表达式:

L = 1 N ∑ i ∑ j ≠ y i [ max ⁡ ( 0 , f ( x i ; W ) j − f ( x i ; W ) y i + Δ ) ] + λ ∑ k ∑ l W k , l 2 L = \frac{1}{N} \sum_i \sum_{j\neq y_i} \left[ \max(0, f(x_i; W){j} - f(x_i; W) + \Delta) \right] + \lambda \sum_k\sum_l W_{k,l}² L=N1​i∑​j​=yi​∑​[max(0,f(xi​;W)j​−f(xi​;W)yi​​+Δ)]+λk∑​l∑​Wk,l2​

接下来要做的,就是找到能够使损失值最小化的权重了。

1.3 Softmax 分类器损失

SVM 是最常用的分类器之一,另一个常用的是 Softmax 分类器。Softmax 分类器可以理解为逻辑回归分类器面对多个分类的一般化归纳,又称为多项式逻辑回归((Multinomial Logistic Regression)。

1) 损失函数

还是以之前小猫的图片为例:

损失函数与最优化; Softmax 分类器损失; 3-5

图片上的公式初一看可能感觉有点复杂,下面逐个解释:

  • s s s 依然是存放所有分类分值的一维数组, s = f ( x i , W ) s=f(x_i,W) s=f(xi​,W), s j s_j sj​ 对应着第 j j j 个分类的得分,对数据 x i x_i xi​ 的真实标签得分还是 s y i s_{y_i} syi​​。现在这个分数被 Softmax 分类器称作非归一化 log 概率

  • 函数 f k ( s ) = e s k ∑ j e s j f_k(s)=\frac{e^{s_k}}{\sum_j e^{s_j}} fk​(s)=∑j​esj​esk​​ 是 Softmax 函数,其输入值是一个向量 s s s,向量中元素为任意实数的评分值,函数对其进行压缩,输出一个向量,其中每个元素值在 0 0 0 到 1 1 1 之间,且所有元素之和为 1 1 1。现在可以把这个压缩后的向量看作一个概率分布,分类标签是 k k k 的概率: P ( Y = k ∣ X = x i ) = e s k ∑ j e s j P(Y=k|X=x_i)=\frac{e^{s_k}}{\sum_j e^{s_j}} P(Y=k∣X=xi​)=∑j​esj​esk​​。这个概率被称作归一化概率,得分的指数形式被称作非归一化概率

  • 由上所述,真实分类标签的概率: P ( Y = y i ∣ X = x i ) = e s y i ∑ j e s j P(Y=y_i|X=x_i)=\frac{e^{s_{y_i}}}{\sum_j e^{s_j}} P(Y=yi​∣X=xi​)=∑j​esj​esyi​​​,如果这个概率为 1 1 1 就最好不过了。所以我们希望这个概率的对数似然最大化,也就是相当于负对数似然最小。由于概率 P P P 在 [ 0 , 1 ] [0, 1] [0,1] 之间,所以 − l o g ( P ) -log(P) −log(P) 在 0 0 0 到正无穷之间,所以我们可以用这个负对数似然作为对于 x i x_i xi​ 的损失函数

L i = − l o g P ( Y = y i ∣ X = x i ) = − l o g ( e s y i ∑ j e s j ) L_i=-logP(Y=y_i|X=x_i)=-log(\frac{e^{s_{y_i}}}{\sum_j e^{s_j}}) Li​=−logP(Y=yi​∣X=xi​)=−log(∑j​esj​esyi​​​)

  • 整个数据集的损失:

L = 1 N ∑ i [ − l o g ( e s y i ∑ j e s j ) ] + λ R ( W ) L = \frac{1}{N} \sum_i \left[ -log(\frac{e^{s_{y_i}}}{\sum_j e^{s_j}}) \right] + \lambda R(W) L=N1​i∑​[−log(∑j​esj​esyi​​​)]+λR(W)

  • SVM 中使用的是合页损失(hinge loss)有时候又被称为最大边界损失(max-margin loss),Softmax 分类器中使用的为交叉熵损失cross-entropy loss),因为使用的是 Softmax 函数,求一个归一化的概率。

根据上面的分析,可以计算出小猫的 Softmax 损失为 0.89 0.89 0.89。损失为 0 0 0 的时候最好,无穷大的时候最差。

损失函数与最优化; Softmax 损失示例; 3-6

其中:

  • Softmax 损失,最大无穷,最小是 0 0 0;
  • 给 W 一个比较小的初值, s s s 中所有值都很小接近于 0 0 0 时,此时的损失 L 应该等于分类类别数的对数: l o g K logK logK。可根据这个判断代码是否有问题;
  • 实际代码编写中,由于指数形式的存在,如果得分很高,会得到一个非常大的数。除以大数值可能导致数值计算的不稳定,所以学会使用归一化技巧非常重要。如果在分式的分子和分母都乘以一个常数 C C C,并把它变换到求和之中,就能得到一个从数学上等价的公式:

e s y i ∑ j e s j = C e s y i C ∑ j e s j = e s y i + log ⁡ C ∑ j e s j + log ⁡ C \frac{e^{s_{y_i}}}{\sum_j e^{s_j}} = \frac{Ce^{s_{y_i}}}{C\sum_j e^{s_j}} = \frac{e^{s_{y_i} + \log C}}{\sum_j e^{s_j + \log C}} ∑j​esj​esyi​​​=C∑j​esj​Cesyi​​​=∑j​esj​+logCesyi​​+logC​

  • 通常将 C C C 设为 l o g C = − max ⁡ j s j log C = -\max_j s_j logC=−maxj​sj​

该技巧简单地说,就是应该将向量 s s s 中的数值进行平移,使得最大值为 0 0 0。参考 python 实现代码如下:

s = np.array([123, 456, 789]) # 例子中有 3 个分类,每个评分的数值都很大
p = np.exp(s) / np.sum(np.exp(s)) # 不好:数值问题,可能导致数值爆炸

# 那么将 f 中的值平移到最大值为 0:
s -= np.max(s) # s 变成 [-666, -333, 0]
p = np.exp(s) / np.sum(np.exp(s)) # 现在可以了,将给出正确结果 

1.4 Softmax 和 SVM 比较

Softmax 和 SVM 这两类损失的对比如下图所示:

损失函数与最优化; SoftmaxV.S.SVM; 3-7

① 计算上有差异

SVM 和 Softmax 分类器对于数据有不同的处理方式。两个分类器都计算了同样的分值向量 s s s(本节中是通过矩阵乘来实现)。不同之处在于对 s s s 中分值的解释:

  • SVM 分类器将它们看做是类别评分,它的损失函数鼓励正确的类别(本例中是蓝色的类别 2)的分值比其他类别的分值高出至少一个安全边界值
  • Softmax 分类器将这些数值看做是每个类别没有归一化的对数概率,鼓励正确分类的归一化的对数概率变高,其余的变低

SVM 的最终的损失值是 1.58 1.58 1.58,Softmax 的最终的损失值是 0.452 0.452 0.452,注意这两个数值大小没有可比性。只在给定同样数据,在同样的分类器的损失值计算中,损失之间比较才有意义。

② 损失的绝对数值不可以直接解释

SVM 的计算是无标定的,而且难以针对所有分类的评分值给出直观解释。Softmax 分类器则不同,它允许我们计算出对于所有分类标签的 「概率」。

但这里要注意,「不同类别概率」 分布的集中或离散程度是由正则化参数 λ \lambda λ 直接决定的。随着正则化参数 λ \lambda λ 不断增强,权重数值会越来越小,最后输出的概率会接近于均匀分布。

也就是说,Softmax 分类器算出来的概率可以某种程度上视作一种对于分类正确性的自信。和 SVM 一样,数字间相互比较得出的大小顺序是可以解释的,但其绝对值则难以直观解释。

③ 实际应用时,SVM 和 Softmax 是相似的

两种分类器的表现差别很小。

  • 相对于 Softmax 分类器,SVM 更加 「局部目标化(local objective)」,只要看到正确分类相较于不正确分类,已经得到了比边界值还要高的分数,它就会认为损失值是 0 0 0,对于数字个体的细节是不关心的。
  • Softmax 分类器对于分数是永不满足的:正确分类总能得到更高的概率,错误分类总能得到更低的概率,损失值总是能够更小。

2.优化

截止目前,我们已知以下内容:

  • 评分函数: s = f ( W , x ) = W x s=f(W,x)=Wx s=f(W,x)=Wx

  • 损失函数

    • SVM 数据损失: L i = ∑ j ≠ y i max ⁡ ( 0 , s j − s y i + Δ ) L_i = \sum_{j\neq y_i} \max(0, s_j - s_{y_i} + \Delta) Li​=∑j​=yi​​max(0,sj​−syi​​+Δ)
    • Softmax 数据损失: L i = − l o g ( e s y i ∑ j e s j ) L_i=-log(\frac{e^{s_{y_i}}}{\sum_j e^{s_j}}) Li​=−log(∑j​esj​esyi​​​)
    • 全损失: L = 1 N ∑ i = 1 N L i + R ( W ) L=\frac{1}{N} \sum_{i=1}^NL_i+R(W) L=N1​∑i=1N​Li​+R(W)

它们之间的关系:

损失函数与最优化; 损失函数的构成; 3-8

下一步我们希望寻找最优的 W W W 让损失 loss 最小化。

2.1 损失函数可视化

损失函数一般都是定义在高维度的空间中(比如,在 CIFAR-10 中一个线性分类器的权重矩阵大小是 [ 10 × 3073 ] [10 \times 3073] [10×3073],就有 30730 个参数),这样要将其可视化就很困难。

解决办法是在 1 维或 2 维方向上对高维空间进行切片,就能得到一些直观感受。

  • 例如,随机生成一个权重矩阵 W W W,该矩阵就与高维空间中的一个点对应。然后沿着某个维度方向前进的同时记录损失函数值的变化。
  • 换句话说,就是生成一个随机的方向 W 1 W_1 W1​ 并且沿着此方向计算损失值,计算方法是根据不同的 a a a 值来计算 L ( W + a W 1 ) L(W + a W_1) L(W+aW1​)。这个过程将生成一个图表,其 x x x 轴是值 a a a, y y y 轴是损失函数值。
  • 对应到两维上,即通过改变 a , b a,b a,b 来计算损失值 L ( W + a W 1 + b W 2 ) L(W + a W_1 + b W_2) L(W+aW1​+bW2​),从而给出二维的图像。在图像中,可以分别用 x x x 和 y y y 轴表示 a , b a, b a,b,而损失函数的值可以用颜色变化表示。

下图是一个无正则化的多类 SVM 的损失函数的图示。左边和中间只有一个样本数据,右边是 CIFAR-10 中的 100 个数据,蓝色部分是低损失值区域,红色部分是高损失值区域:

损失函数与最优化; 损失函数可视化; 3-9

上图中注意损失函数的分段线性结构。多个样本的损失值是总体的平均值,所以右边的碗状结构是很多的分段线性结构的平均。可以通过数学公式来解释损失函数的分段线性结构。

对于 1 条单独的数据样本,有损失函数的计算公式如下:

L i = ∑ j ≠ y i [ max ⁡ ( 0 , w j T x i − w y i T x i + 1 ) ] L_i = \sum_{j\neq y_i} \left[ \max(0, w_j^Tx_i - w_{y_i}^Tx_i + 1) \right] Li​=j​=yi​∑​[max(0,wjT​xi​−wyi​T​xi​+1)]

每个样本的数据损失值是以 W W W 为参数的线性函数的总和。 W W W 的每一行( w j w_j wj​ ),有时候它前面是一个正号(比如当它对应非真实标签分类的时候),有时候它前面是一个负号(比如当它是正确分类的时候)。

比如,假设有一个简单的数据集,其中包含有 3 个只有 1 个维度的点,数据集数据点有 3 个类别。那么完整的无正则化 SVM 的损失值计算如下:

L 0 = max ⁡ ( 0 , w 1 T x 0 − w 0 T x 0 + 1 ) + max ⁡ ( 0 , w 2 T x 0 − w 0 T x 0 + 1 ) L 1 = max ⁡ ( 0 , w 0 T x 1 − w 1 T x 1 + 1 ) + max ⁡ ( 0 , w 2 T x 1 − w 1 T x 1 + 1 ) L 2 = max ⁡ ( 0 , w 0 T x 2 − w 2 T x 2 + 1 ) + max ⁡ ( 0 , w 1 T x 2 − w 2 T x 2 + 1 ) L = ( L 0 + L 1 + L 2 ) / 3 \begin{aligned} L_0 = & \max(0, w_1^Tx_0 - w_0^Tx_0 + 1) + \max(0, w_2^Tx_0 - w_0^Tx_0 + 1) \ L_1 = & \max(0, w_0^Tx_1 - w_1^Tx_1 + 1) + \max(0, w_2^Tx_1 - w_1^Tx_1 + 1) \ L_2 = & \max(0, w_0^Tx_2 - w_2^Tx_2 + 1) + \max(0, w_1^Tx_2 - w_2^Tx_2 + 1) \ L = & (L_0 + L_1 + L_2)/3 \end{aligned} L0​=L1​=L2​=L=​max(0,w1T​x0​−w0T​x0​+1)+max(0,w2T​x0​−w0T​x0​+1)max(0,w0T​x1​−w1T​x1​+1)+max(0,w2T​x1​−w1T​x1​+1)max(0,w0T​x2​−w2T​x2​+1)+max(0,w1T​x2​−w2T​x2​+1)(L0​+L1​+L2​)/3​

这些例子都是一维的,所以数据 x i x_i xi​ 和权重 w j w_j wj​ 都是数字。单看 w 0 w_0 w0​,可以看到最上面的三个式子每一个都含 w 0 w_0 w0​ 的线性函数,且每一项都会与 0 0 0 比较,取两者的最大值。第一个式子线性函数斜率是负的,后面两个斜率是正的,可作图如下:

损失函数与最优化; 损失函数可视化; 3-10

上图中,横轴是 w 0 w_0 w0​,纵轴是损失,三条线对应三个线性函数,加起来即为右图。

补充解释

  • 我们将上面的评分函数 f f f 扩展到神经网络,目标损失函数就就不再是凸函数了,图像也不会像上面那样是个碗状,而是凹凸不平的复杂地形形状。
  • 由于 max 操作,损失函数中存在一些不可导点(kinks),比如折点处,这些点使得损失函数不可微,因为在这些不可导点,梯度是没有定义的。但是次梯度(subgradient)依然存在且常常被使用。在本教程中,我们会交换使用次梯度和梯度两个术语。某点的次梯度是该点的左右导数之间的任意值。

2.2 优化策略(Optimization Strategy)

优化策略的目标是:找到能够最小化损失函数值的权重 W W W

随机尝试很多不同的权重,然后看其中哪个最好。这是一个差劲的初始方案。代码如下:

# 假设 X_train 的每一列都是一个数据样本(比如 3073 x 50000)
# 假设 Y_train 是数据样本的类别标签(比如一个长 50000 的一维数组)
# 假设函数 L 对损失函数进行评价

bestloss = float("inf") # 初始指定一个最高的损失
for num in range(1000):
  W = np.random.randn(10, 3073) * 0.0001 # 随机生成一个 10x3073 的 W 矩阵
                                         # 都接近为 0
  loss = L(X_train, Y_train, W) # 得到整个训练集的损失
  if loss < bestloss: # 保持最好的解决方式
    bestloss = loss
    bestW = W
  print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)

# 输出:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (trunctated: continues for 1000 lines) 

在上面的代码中,我们尝试了若干随机生成的权重矩阵 W W W,其中某些的损失值较小,而另一些的损失值大些。我们可以把这次随机搜索中找到的最好的权重 W W W 取出,然后去跑测试集:

# 假设 X_test 尺寸是[3073 x 10000], Y_test 尺寸是[10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, 每个样本对应 10 个类得分,共 10000
# 找到在每列中评分值最大的索引(即预测的分类)
Yte_predict = np.argmax(scores, axis = 0)
# 以及计算准确率
np.mean(Yte_predict == Yte)
# 返回 0.1555 

验证集上表现最好的权重 W 跑测试集的准确率是 15.5 % 15.5% 15.5%,而完全随机猜的准确率是 10 % 10% 10%,效果不好!

思路调整:新的策略是从随机权重 W 开始,然后迭代取优,每次都让它的损失值变得更小一点,从而获得更低的损失值。想象自己是一个蒙着眼睛的徒步者,正走在山地地形上,目标是要慢慢走到山底。在 CIFAR-10 的例子中,这山是 30730 30730 30730 维的(因为 W W W 是 3073 × 10 3073 \times 10 3073×10)。我们在山上踩的每一点都对应一个的损失值,该损失值可以看做该点的海拔高度。

2) 策略二:随机本地搜索

第一个策略可以看做是每走一步都尝试几个随机方向,如果是上山方向就停在原地,如果是下山方向,就向该方向走一步。这次我们从一个随机 W W W 开始,然后生成一个随机的扰动 a W aW aW,只有当 W + a W W+aW W+aW 的损失值变低,我们才会更新。

这个过程的参考实现代码如下:

W = np.random.randn(10, 3073) * 0.001 # 生成随机初始 W
bestloss = float("inf")
for i in xrange(1000):
  step_size = 0.0001
  Wtry = W + np.random.randn(10, 3073) * step_size
  loss = L(Xtr_cols, Ytr, Wtry)
  if loss < bestloss:
    W = Wtry
    bestloss = loss
  print 'iter %d loss is %f' % (i, bestloss) 

用上述方式迭代 1000 次,这个方法可以得到 21.4 % 21.4% 21.4% 的分类准确率。

3) 策略三:跟随梯度

前两个策略关键点都是在权重空间中找到合适的方向,使得沿其调整能降低损失函数的损失值。其实不需要随机寻找方向,我们可以直接计算出最好的方向,这个方向就是损失函数的梯度gradient)。这个方法就好比是感受我们脚下山体的倾斜程度,然后向着最陡峭的下降方向下山。

损失函数与最优化; 损失优化策略; 跟随梯度; 3-11

在一维函数中,斜率是函数在某一点的瞬时变化率。梯度是函数斜率的一般化表达,它是一个向量。

在输入空间中,梯度是各个维度的斜率组成的向量(或者称为导数 derivatives)。对一维函数的求导公式如下:

d f ( x ) d x = lim ⁡ h   → 0 f ( x + h ) − f ( x ) h \frac{df(x)}{dx} = \lim_{h\ \to 0} \frac{f(x + h) - f(x)}{h} dxdf(x)​=h →0lim​hf(x+h)−f(x)​

当函数有多个自变量的时候,我们称导数为偏导数,而梯度就是在每个维度上偏导数所形成的向量。设三元函数 f ( x , y , z ) f(x,y,z) f(x,y,z) 在空间区域 G G G 内具有一阶连续偏导数,点 P ( x , y , z ) ∈ G P(x,y,z)\in G P(x,y,z)∈G,称向量

{ ∂ f ∂ x , ∂ f ∂ y , ∂ f ∂ z } = ∂ f ∂ x i ⃗ + ∂ f ∂ y j ⃗ + ∂ f ∂ z k ⃗ = f x ( x , y , z ) i ⃗ + f y ( x , y , z ) j ⃗ + f z ( x , y , z ) k ⃗ { \frac{\partial f}{\partial x}, \frac{\partial f}{\partial y} ,\frac{\partial f}{\partial z} } = \frac{\partial f}{\partial x}\vec{i} +\frac{\partial f}{\partial y}\vec{j} +\frac{\partial f}{\partial z} \vec{k} = f_{x}(x,y,z)\vec{i}+f_{y}(x,y,z)\vec{j}+f_{z}(x,y,z)\vec{k} {∂x∂f​,∂y∂f​,∂z∂f​}=∂x∂f​i +∂y∂f​j ​+∂z∂f​k =fx​(x,y,z)i +fy​(x,y,z)j ​+fz​(x,y,z)k

为函数 f ( x , y , z ) f(x,y,z) f(x,y,z) )在点 P P P 的梯度

记为:

g r a d f ( x , y , z ) = f x ( x , y , z ) i ⃗ + f y ( x , y , z ) j ⃗ + f z ( x , y , z ) k ⃗ grad f(x,y,z)=f_{x}(x,y,z)\vec{i}+f_{y}(x,y,z)\vec{j}+f_{z}(x,y,z)\vec{k} gradf(x,y,z)=fx​(x,y,z)i +fy​(x,y,z)j ​+fz​(x,y,z)k

3.梯度计算

关于梯度计算与检查的详细知识也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面里对于「梯度检验 (Gradient checking)」部分的讲解

计算梯度有两种方法:

  • 缓慢的近似方法(数值梯度法),但实现相对简单。
  • 分析梯度法,计算迅速,结果精确,但是实现时容易出错,且需要使用微分。

下面我们展开介绍这两种方法

3.1 数值梯度法

数值梯度法是借助于梯度的定义对其进行逼近计算。

损失函数与最优化; 数值梯度计算示例; 3-12

下面代码中:

输入为函数 f f f 和矩阵 x x x,计算 f f f 的梯度的通用函数,它返回函数 f f f 在点 x x x 处的梯度,利用公式 d f ( x ) d x = lim ⁡ h   → 0 f ( x + h ) − f ( x ) h \frac{df(x)}{dx} = \lim_{h\ \to 0} \frac{f(x + h) - f(x)}{h} dxdf(x)​=limh →0​hf(x+h)−f(x)​,代码对 x x x 矩阵所有元素进行迭代,在每个元素上产生一个很小的变化 h h h,通过观察函数值变化,计算函数在该元素上的偏导数。最后,所有的梯度存储在变量 grad 中:

参考实现代码如下:

def eval_numerical_gradient(f, x):
  """  
  我们是求 L 关于 w 的梯度,f 就是损失 L,x 就是权重矩阵 w
  一个 f 在 x 处的数值梯度法的简单实现
  - f 是参数 x 的函数,x 是矩阵,比如之前的 w 是 10x3073  
  - x 是计算梯度的点
   """ 

  fx = f(x) # 计算 x 点处的函数值
  grad = np.zeros(x.shape)  # 梯度矩阵也是 10x3073
  h = 0.00001  # 近似为 0 的变化量

  # 对 x 中所有的索引进行迭代,比如从(0,0)到(9,3072)
  it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
  # np.nditer 是 np 自带的迭代器
  # flags=['multi_index']表示对 x 进行多重索引 比如(0,0)
  # op_flags=['readwrite']表示不仅可以对 x 进行 read(读取),还可以 write(写入)
  while not it.finished:

    # 计算 x+h 处的函数值
    ix = it.multi_index   #索引从(0,0)开始,即从 x 矩阵第一行第一列的元素开始
    old_value = x[ix]   # 先将 x(0,0)处原值保存
    x[ix] = old_value + h # 增加 h
    fxh = f(x) # 计算新的 f(x + h)
    x[ix] = old_value # 将 x(0,0)处改回原值

    # 计算偏导数
    grad[ix] = (fxh - fx) / h # x(0,0)处的偏导数
    it.iternext() # 到下个维度 x(0,1)

  return grad # 最终是计算好的 10x3073 的梯度矩阵 

实际中用中心差值公式centered difference formula) [ f ( x + h ) − f ( x − h ) ] / 2 h [f(x+h) - f(x-h)] / 2 h [f(x+h)−f(x−h)]/2h 效果会更好。下面计算权重空间中的某些随机点上,CIFAR-10 损失函数的梯度:

# 为了使用上面的代码,需要一个只有一个参数的函数
# (在这里参数就是权重 W)所以封装了 X_train 和 Y_train
def CIFAR10_loss_fun(W):
  return L(X_train, Y_train, W)

W = np.random.rand(10, 3073) * 0.001 # 随机权重矩阵
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # 得到梯度矩阵

梯度告诉我们损失函数在每个元素上的斜率,以此来进行更新:
loss_original = CIFAR10_loss_fun(W) # 初始损失值
print 'original loss: %f' % (loss_original, )

# 查看不同步长的效果
for step_size_log in [-10, -9, -8, -7, -6, -5,-4,-3,-2,-1]:
  step_size = 10 ** step_size_log
  W_new = W - step_size * df # 权重空间中的新位置,使用负梯度
  loss_new = CIFAR10_loss_fun(W_new)
  print 'for step size %f new loss: %f' % (step_size, loss_new)

# 输出:
# original loss: 2.200718
# for step size 1.000000e-10 new loss: 2.200652
# for step size 1.000000e-09 new loss: 2.200057
# for step size 1.000000e-08 new loss: 2.194116
# for step size 1.000000e-07 new loss: 2.135493
# for step size 1.000000e-06 new loss: 1.647802
# for step size 1.000000e-05 new loss: 2.844355
# for step size 1.000000e-04 new loss: 25.558142
# for step size 1.000000e-03 new loss: 254.086573
# for step size 1.000000e-02 new loss: 2539.370888
# for step size 1.000000e-01 new loss: 25392.214036 

① 在梯度负方向上更新

  • 在上面的代码中,为了计算 W_new,要注意我们是向着梯度 d f df df 的负方向去更新,这是因为我们希望损失函数值是降低而不是升高。(偏导大于 0 0 0,损失递增, W W W 需要减小;偏导小于 0 0 0,损失递减,W 需要增大。)

② 步长的影响

  • 从某个具体的点 W W W 开始计算梯度,梯度指明了函数在哪个方向是变化率最大的,即损失函数下降最陡峭的方向,但是没有指明在这个方向上应该迈多大的步子。
  • 小步长下降稳定但进度慢,大步长进展快但是风险更大,可能导致错过最优点,让损失值上升。
  • 在上面的代码中就能看见反例,在某些点如果步长过大,反而可能越过最低点导致更高的损失值。选择步长(也叫作学习率)将会是神经网络训练中最重要(也是最麻烦)的超参数设定之一。

③ 效率问题

  • 计算数值梯度的复杂性和参数的量线性相关。在本例中有 30730 个参数,所以损失函数每走一步就需要计算 30731 次损失函数(计算梯度时计算 30730 次,最终计算一次更新后的。)
  • 现代神经网络很容易就有上千万的参数,因此这个问题只会越发严峻。显然这个策略不适合大规模数据。

3.2 解析梯度法

数值梯度的计算比较简单,但缺点在于只是近似不够精确,且耗费计算资源太多。

得益于牛顿-莱布尼茨的微积分,我们可以利用微分来分析,得到计算梯度的公式(不是近似),用公式计算梯度速度很快,但在实现的时候容易出错。

为了解决这个问题,在实际操作时常常将分析梯度法的结果和数值梯度法的结果作比较,以此来检查其实现的正确性,这个步骤叫做梯度检查

比如我们已知多类 SVM 的数据损失 L i L_i Li​:

L i = ∑ j ≠ y i [ max ⁡ ( 0 , w j T x i − w y i T x i + Δ ) ] L_i = \sum_{j\neq y_i} \left[ \max(0, w_j^Tx_i - w_{y_i}^Tx_i + \Delta) \right] Li​=j​=yi​∑​[max(0,wjT​xi​−wyi​T​xi​+Δ)]

可以对函数进行微分。比如对 w y i w_{y_i} wyi​​ 微分:

∇ w y i L i = − ( ∑ j ≠ y i 1 ( w j T x i − w y i T x i + Δ > 0 ) ) x i \nabla_{w_{y_i}} L_i = - \left( \sum_{j\neq y_i} \mathbb{1}(w_j^Tx_i - w_{y_i}^Tx_i + \Delta > 0) \right) x_i ∇wyi​​​Li​=−⎝⎛​j​=yi​∑​1(wjT​xi​−wyi​T​xi​+Δ>0)⎠⎞​xi​

  • 其中 1 1 1 是一个示性函数,如果括号中的条件为真,那么函数值为 1 1 1,如果为假,则函数值为 0 0 0。

虽然上述公式看起来复杂,但在代码实现的时候比较简单:只需要计算没有满足边界值的即对损失函数产生贡献的分类的数量,然后乘以 x i x_i xi​ 就是梯度了。

  • 注意,这个梯度只是对应正确分类的 W W W 的行向量的梯度,那些 j ≠ y i j \neq y_i j​=yi​ 行的梯度是:

∇ w j L i = 1 ( w j T x i − w y i T x i + Δ > 0 ) x i \nabla_{w_j} L_i = \mathbb{1}(w_j^Tx_i - w_{y_i}^Tx_i + \Delta > 0) x_i ∇wj​​Li​=1(wjT​xi​−wyi​T​xi​+Δ>0)xi​

一旦将梯度的公式微分出来,代码实现公式并用于梯度更新就比较顺畅了。

4.梯度下降(Gradient Descent)

关于 Batch Gradient Descent、Mini-batch gradient descent、Stochastic Gradient Descent 的详细知识也可以参考ShowMeAI的的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章神经网络优化算法

现在可以利用微分公式计算损失函数梯度了,程序重复地计算梯度然后对参数进行更新,这一过程称为梯度下降。

4.1 普通梯度下降

# 普通的梯度下降
while True:
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # 进行梯度更新 

这个简单的循环在所有的神经网络核心库中都有。虽然也有其他实现最优化的方法(比如 LBFGS),但是到目前为止,梯度下降是对神经网络的损失函数最优化中最常用的方法。

损失函数与最优化; 普通梯度下降; Batchgradientdescent; 3-13

后面大家见到的新的优化算法也是在其基础上增加一些新的东西(比如更新的具体公式),但是核心思想不变,那就是我们一直跟着梯度走,直到结果不再变化。

4.2 小批量梯度下降(Mini-batch gradient descent)

在大规模的应用中(比如 ILSVRC 挑战赛),训练数据量 N N N 可以达到百万级量级。如果像这样计算整个训练集,来获得仅仅一个参数的更新就太浪费计算资源了。一个常用的方法通过训练集中的小批量(batches)数据来计算。

例如,在目前最高水平的卷积神经网络中,一个典型的小批量包含 256 个样本,而整个训练集是一百二十万个样本。(CIFAR-10,就有 50000 个训练样本。)比如这个小批量数据就用来实现一个参数更新:

# 普通的小批量数据梯度下降
while True:
  data_batch = sample_training_data(data, 256) # 从大规模训练样本中提取 256 个样本
  weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
  weights += - step_size * weights_grad # 参数更新 

这个方法之所以效果不错,是因为训练集中的数据都是相关的。

要理解这一点,可以想象一个极端情况:在 ILSVRC 中的 120 万个图像是 1000 张不同图片的复制(每个类别 1 张图片,每张图片复制 1200 次)。那么显然计算这 1200 张复制图像的梯度就应该是一样的。对比 120 万张图片的数据损失的均值与只计算 1000 张的子集的数据损失均值时,结果应该是一样的。

实际情况中,数据集肯定不会包含重复图像,那么小批量数据的梯度就是对整个数据集梯度的一个近似。因此,在实践中通过计算小批量数据的梯度可以实现更快速地收敛,并以此来进行更频繁的参数更新

损失函数与最优化; 小批量梯度下降; Mini-batchgradientdescent; 3-14

小批量数据策略有个极端情况:每批数据的样本量为 1,这种策略被称为随机梯度下降Stochastic Gradient Descent 简称 SGD),有时候也被称为在线梯度下降。SGD 在技术上是指每次使用 1 个样本来计算梯度,你还是会听到人们使用 SGD 来指代小批量数据梯度下降(或者用 MGD 来指代小批量数据梯度下降)。

损失函数与最优化; 随机梯度下降; StochasticGradientDescent; 3-15

小批量数据的大小是一个超参数,但是一般并不需要通过交叉验证来调参。它一般设置为同样大小,比如 32、64、128 等。之所以使用 2 的指数,是因为在实际中许多向量化操作实现的时候,如果输入数据量是 2 的指数,那么运算更快。

5.图像特征提取

直接输入原始像素,效果不好,可以将图像的特征计算出来,便于分类。

常用的特征计算方式:颜色直方图、词袋、计算边缘等,神经网络中是特征是训练过程中得到的。

6.在线程序

线性分类器各种细节,可在斯坦福大学开发的一个在线程序观看演示:点击这里

7.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=3

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

8.要点总结

  • 损失函数,包括数据损失与正则损失
  • 多类 SVM 损失与 Softmax 损失比较
  • 梯度计算方法(数值梯度与解析梯度)
  • 梯度下降优化算法

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(4) | 神经网络与反向传播(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125024917

ShowMeAI 研究中心


Backpropagation and Neural Networks

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

在上一篇 深度学习与 CV 教程(3) | 损失函数与最优化 内容中,我们给大家介绍了线性模型的损失函数构建与梯度下降等优化算法,【本篇内容】ShowMeAI给大家切入到神经网络,讲解神经网络计算图与反向传播以及神经网络结构等相关知识。

本篇重点

  • 神经网络计算图
  • 反向传播
  • 神经网络结构

1.反向传播算法

神经网络的训练,应用到的梯度下降等方法,需要计算损失函数的梯度,而其中最核心的知识之一是反向传播,它是利用数学中链式法则递归求解复杂函数梯度的方法。而像 tensorflow、pytorch 等主流 AI 工具库最核心的智能之处也是能够自动微分,在本节内容中ShowMeAI就结合 cs231n 的第 4 讲内容展开讲解一下神经网络的计算图和反向传播。

关于神经网络反向传播的解释也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 神经网络基础浅层神经网络深层神经网络 里对于不同深度的网络前向计算和反向传播的讲解

1.1 标量形式反向传播

1) 引例

我们来看一个简单的例子,函数为 f ( x , y , z ) = ( x + y ) z f(x,y,z) = (x + y) z f(x,y,z)=(x+y)z。初值 x = − 2 x = -2 x=−2, y = 5 y = 5 y=5, z = − 4 z = -4 z=−4。这是一个可以直接微分的表达式,但是我们使用一种有助于直观理解反向传播的方法来辅助理解。

下图是整个计算的线路图,绿字部分是函数值,红字是梯度。(梯度是一个向量,但通常将对 x x x 的偏导数称为 x x x 上的梯度。)

标量形式反向传播; 梯度计算线路图

上述公式可以分为 2 部分, q = x + y q = x + y q=x+y 和 f = q z f = q z f=qz。它们都很简单可以直接写出梯度表达式:

  • f f f 是 q q q 和 z z z 的乘积, 所以 ∂ f ∂ q = z = − 4 \frac{\partial f}{\partial q} = z=-4 ∂q∂f​=z=−4, ∂ f ∂ z = q = 3 \frac{\partial f}{\partial z} = q=3 ∂z∂f​=q=3
  • q q q 是 x x x 和 y y y 相加,所以 ∂ q ∂ x = 1 \frac{\partial q}{\partial x} = 1 ∂x∂q​=1, ∂ q ∂ y = 1 \frac{\partial q}{\partial y} = 1 ∂y∂q​=1

我们对 q q q 上的梯度不关心( ∂ f ∂ q \frac{\partial f}{\partial q} ∂q∂f​ 没有用处)。我们关心 f f f 对于 x , y , z x,y,z x,y,z 的梯度。链式法则告诉我们可以用「乘法」将这些梯度表达式链接起来,比如

∂ f ∂ x = ∂ f ∂ q ∂ q ∂ x = − 4 \frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \frac{\partial q}{\partial x} =-4 ∂x∂f​=∂q∂f​∂x∂q​=−4

  • 同理, ∂ f ∂ y = − 4 \frac{\partial f}{\partial y} =-4 ∂y∂f​=−4,还有一点是 ∂ f ∂ f = 1 \frac{\partial f}{\partial f}=1 ∂f∂f​=1

前向传播从输入计算到输出(绿色),反向传播从尾部开始,根据链式法则递归地向前计算梯度(显示为红色),一直到网络的输入端。可以认为,梯度是从计算链路中回流

上述计算的参考 python 实现代码如下:

# 设置输入值
x = -2; y = 5; z = -4

# 进行前向传播
q = x + y # q 是 3
f = q * z # f 是 -12

# 进行反向传播:
# 首先回传到 f = q * z
dfdz = q # df/dz = q, 所以关于 z 的梯度是 3
dfdq = z # df/dq = z, 所以关于 q 的梯度是-4
# 现在回传到 q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1\. 这里的乘法是因为链式法则。所以 df/dx 是-4
dfdy = 1.0 * dfdq # dq/dy = 1.所以 df/dy 是-4

'''一般可以省略 df''' 

2) 直观理解反向传播

反向传播是一个优美的局部过程。

以下图为例,在整个计算线路图中,会给每个门单元(也就是 f f f 结点)一些输入值 x x x , y y y 并立即计算这个门单元的输出值 z z z ,和当前节点输出值关于输入值的局部梯度(local gradient) ∂ z ∂ x \frac{\partial z}{\partial x} ∂x∂z​ 和 ∂ z ∂ y \frac{\partial z}{\partial y} ∂y∂z​ 。

标量形式反向传播; 反向传播门单元

门单元的这两个计算在前向传播中是完全独立的,它无需知道计算线路中的其他单元的计算细节。但在反向传播的过程中,门单元将获得整个网络的最终输出值在自己的输出值上的梯度 ∂ L ∂ z \frac{\partial L}{\partial z} ∂z∂L​ 。

根据链式法则,整个网络的输出对该门单元的每个输入值的梯度,要用回传梯度乘以它的输出对输入的局部梯度,得到 ∂ L ∂ x \frac{\partial L}{\partial x} ∂x∂L​ 和 ∂ L ∂ y \frac{\partial L}{\partial y} ∂y∂L​ 。这两个值又可以作为前面门单元的回传梯度。

因此,反向传播可以看做是门单元之间在通过梯度信号相互通信,只要让它们的输入沿着梯度方向变化,无论它们自己的输出值在何种程度上升或降低,都是为了让整个网络的输出值更高。

比如引例中 x , y x,y x,y 梯度都是 − 4 -4 −4,所以让 x , y x,y x,y 减小后, q q q 的值虽然也会减小,但最终的输出值 f f f 会增大(当然损失函数要的是最小)。

3) 加法门、乘法门和 max 门

引例中用到了两种门单元:加法和乘法。

  • 加法求偏导: f ( x , y ) = x + y → ∂ f ∂ x = 1 ∂ f ∂ y = 1 f(x,y) = x + y \rightarrow \frac{\partial f}{\partial x} = 1 \frac{\partial f}{\partial y} = 1 f(x,y)=x+y→∂x∂f​=1∂y∂f​=1
  • 乘法求偏导: f ( x , y ) = x y → ∂ f ∂ x = y ∂ f ∂ y = x f(x,y) = x y \rightarrow \frac{\partial f}{\partial x} = y \frac{\partial f}{\partial y} = x f(x,y)=xy→∂x∂f​=y∂y∂f​=x

除此之外,常用的操作还包括取最大值:

f ( x , y ) = max ⁡ ( x , y ) → ∂ f ∂ x = 1 ( x ≥ y ) ∂ f ∂ y 1 ( y ≥ x ) \begin{aligned} f(x,y) &= \max(x, y) \ \rightarrow \frac{\partial f}{\partial x} &= \mathbb{1}(x \ge y)\ \frac{\partial f}{\partial y} &\mathbb{1}(y \ge x) \end{aligned} f(x,y)→∂x∂f​∂y∂f​​=max(x,y)=1(x≥y)1(y≥x)​

上式含义为:若该变量比另一个变量大,那么梯度是 1 1 1,反之为 0 0 0。

标量形式反向传播; 加法门、乘法门和 max 门

  • 加法门单元是梯度分配器,输入的梯度都等于输出的梯度,这一行为与输入值在前向传播时的值无关;
  • 乘法门单元是梯度转换器,输入的梯度等于输出梯度乘以另一个输入的值,或者乘以倍数 a a a( a x ax ax 的形式乘法门单元);max 门单元是梯度路由器,输入值大的梯度等于输出梯度,小的为 0 0 0。

乘法门单元的局部梯度就是输入值,但是是相互交换之后的,然后根据链式法则乘以输出值的梯度。基于此,如果乘法门单元的其中一个输入非常小,而另一个输入非常大,那么乘法门会把大的梯度分配给小的输入,把小的梯度分配给大的输入。

以我们之前讲到的线性分类器为例,权重和输入进行点积 w T x i w^Tx_i wTxi​ ,这说明输入数据的大小对于权重梯度的大小有影响。具体的,如在计算过程中对所有输入数据样本 x i x_i xi​ 乘以 100,那么权重的梯度将会增大 100 倍,这样就必须降低学习率来弥补。

也说明了数据预处理有很重要的作用,它即使只是有微小变化,也会产生巨大影响

对于梯度在计算线路中是如何流动的有一个直观的理解,可以帮助调试神经网络。

4) 复杂示例

我们来看一个复杂一点的例子:

f ( w , x ) = 1 1 + e − ( w 0 x 0 + w 1 x 1 + w 2 ) f(w,x) = \frac{1}{1+e^{-(w_0x_0 + w_1x_1 + w_2)}} f(w,x)=1+e−(w0​x0​+w1​x1​+w2​)1​

这个表达式需要使用新的门单元:

f ( x ) = 1 x → d f d x = − 1 x 2   f c ( x ) = c + x → d f d x = 1   f ( x ) = e x → d f d x = e x   f a ( x ) = a x → d f d x = a \begin{aligned} f(x) &= \frac{1}{x} \ \rightarrow \frac{df}{dx} &=- \frac{1}{x²}\ f_c(x) = c + x \ \rightarrow \frac{df}{dx} &= 1 \ f(x) = e^x \ \rightarrow \frac{df}{dx} &= e^x \ f_a(x) = ax \ \rightarrow \frac{df}{dx} &= a \end{aligned} f(x)→dxdf​→dxdf​→dxdf​→dxdf​​=x1​=−x21​ fc​(x)=c+x=1 f(x)=ex=ex fa​(x)=ax=a​

计算过程如下:

神经网络&反向传播; 反向传播计算过程

  • 对于 1 / x 1/x 1/x 门单元,回传梯度是 1 1 1,局部梯度是 − 1 / x 2 = − 1 / 1.3 7 2 = − 0.53 -1/x²=-1/1.37²=-0.53 −1/x2=−1/1.372=−0.53 ,所以输入梯度为 1 × − 0.53 = − 0.53 1 \times -0.53 = -0.53 1×−0.53=−0.53; + 1 +1 +1 门单元不改变梯度还是 − 0.53 -0.53 −0.53
  • exp 门单元局部梯度是 e x = e − 1 ex=e ex=e−1 ,然后乘回传梯度 − 0.53 -0.53 −0.53 结果约为 − 0.2 -0.2 −0.2
  • 乘 − 1 -1 −1 门单元会将梯度加负号变为 0.2 0.2 0.2
  • 加法门单元会分配梯度,所以从上到下三个加法分支都是 0.2 0.2 0.2
  • 最后两个乘法单元会转换梯度,把回传梯度乘另一个输入值作为自己的梯度,得到 − 0.2 -0.2 −0.2、 0.4 0.4 0.4、 − 0.4 -0.4 −0.4、 − 0.6 -0.6 −0.6

5) Sigmoid 门单元

我们可以将任何可微分的函数视作「门」。可以将多个门组合成一个门,也可以根据需要将一个函数拆成多个门。我们观察可以发现,最右侧四个门单元可以合成一个门单元, σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1+e^{-x}} σ(x)=1+e−x1​ ,这个函数称为 sigmoid 函数

sigmoid 函数可以微分:

d σ ( x ) d x = e − x ( 1 + e − x ) 2 = ( 1 + e − x − 1 1 + e − x ) ( 1 1 + e − x ) = ( 1 − σ ( x ) ) σ ( x ) \frac{d\sigma(x)}{dx} = \frac{e{-x}}{(1+e)²} = \left( \frac{1 + e^{-x} - 1}{1 + e^{-x}} \right) \left( \frac{1}{1+e^{-x}} \right) = \left( 1 - \sigma(x) \right) \sigma(x) dxdσ(x)​=(1+e−x)2e−x​=(1+e−x1+e−x−1​)(1+e−x1​)=(1−σ(x))σ(x)

所以上面的例子中已经计算出 σ ( x ) = 0.73 \sigma(x)=0.73 σ(x)=0.73 ,可以直接计算出乘 − 1 -1 −1 门单元输入值的梯度为: 1 ∗ ( 1 − 0.73 ) ∗ 0.73   = 0.2 1 \ast (1-0.73) \ast0.73~=0.2 1∗(1−0.73)∗0.73 =0.2,计算简化很多。

上面这个例子的反向传播的参考 python 实现代码如下:

# 假设一些随机数据和权重
w = [2,-3,-3] 
x = [-1, -2]

# 前向传播,计算输出值
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid 函数

# 反向传播,计算梯度
ddot = (1 - f) * f # 点积变量的梯度, 使用 sigmoid 函数求导
dx = [w[0] * ddot, w[1] * ddot] # 回传到 x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # 回传到 w
# 最终得到输入的梯度 

在实际操作中,有时候我们会把前向传播分成不同的阶段,这样可以让反向传播过程更加简洁。比如创建一个中间变量 d o t dot dot,存放 w w w 和 x x x 的点乘结果。在反向传播时,可以很快计算出装着 w w w 和 x x x 等的梯度的对应的变量(比如 d d o t ddot ddot, d x dx dx 和 d w dw dw)。

本篇内容列了很多例子,我们希望通过这些例子讲解「前向传播」与「反向传播」过程,哪些函数可以被组合成门,如何简化,这样他们可以“链”在一起,让代码量更少,效率更高。

6) 分段计算示例

f ( x , y ) = x + σ ( y ) σ ( x ) + ( x + y ) 2 f(x,y) = \frac{x + \sigma(y)}{\sigma(x) + (x+y)²} f(x,y)=σ(x)+(x+y)2x+σ(y)​

这个表达式只是为了实践反向传播,如果直接对 x , y x,y x,y 求导,运算量将会很大。下面先代码实现前向传播:

x = 3  # 例子数值
y = -4

# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 分子中的 sigmoid         #(1)
num = x + sigy # 分子                                    #(2)
sigx = 1.0 / (1 + math.exp(-x)) # 分母中的 sigmoid         #(3)
xpy = x + y                                              #(4)
xpysqr = xpy**2                                          #(5)
den = sigx + xpysqr # 分母                                #(6)
invden = 1.0 / den                                       #(7)
f = num * invden 

代码创建了多个中间变量,每个都是比较简单的表达式,它们计算局部梯度的方法是已知的。可以给我们计算反向传播带来很多便利:

  • 我们对前向传播时产生的每个变量 $ (sigy, num, sigx, xpy, xpysqr, den, invden)$ 进行回传。
  • 我们用同样数量的变量(以 d 开头),存储对应变量的梯度。
  • 注意:反向传播的每一小块中都将包含了表达式的局部梯度,然后根据使用链式法则乘以上游梯度。对于每行代码,我们将指明其对应的是前向传播的哪部分,序号对应。
# 回传 f = num * invden
dnum = invden # 分子的梯度                                         #(8)
dinvden = num # 分母的梯度                                         #(8)
# 回传 invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                                #(7)
# 回传 den = sigx + xpysqr
dsigx = (1) * dden                                                #(6)
dxpysqr = (1) * dden                                              #(6)
# 回传 xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# 回传 xpy = x + y
dx = (1) * dxpy                                                   #(4)
dy = (1) * dxpy                                                   #(4)
# 回传 sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # 注意这里用的是+=,下面有解释    #(3)
# 回传 num = x + sigy
dx += (1) * dnum                                                  #(2)
dsigy = (1) * dnum                                                #(2)
# 回传 sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy 

补充解释

①对前向传播变量进行缓存

  • 在计算反向传播时,前向传播过程中得到的一些中间变量非常有用。
  • 实现过程中,在代码里对这些中间变量进行缓存,这样在反向传播的时候也能用上它们。

②在不同分支的梯度要相加

  • 如果变量 x , y x,y x,y 在前向传播的表达式中出现多次,那么进行反向传播的时候就要非常小心,要使用 + = += += 而不是 = = = 来累计这些变量的梯度。
  • 根据微积分中的多元链式法则,如果变量在线路中走向不同的分支,那么梯度在回传的时候,应该累加 。即:

∂ f ∂ x = ∑ q i ∂ f ∂ q i ∂ q i ∂ x \frac{\partial f}{\partial x} =\sum_{q_i}\frac{\partial f}{\partial q_i}\frac{\partial q_i}{\partial x} ∂x∂f​=qi​∑​∂qi​∂f​∂x∂qi​​

7) 实际应用

如果有一个计算图,已经拆分成门单元的形式,那么主类代码结构如下:

class ComputationalGraph(object):
    # ...
    def forward(self, inputs):
        # 把 inputs 传递给输入门单元
        # 前向传播计算图
        # 遍历所有从后向前按顺序排列的门单元
        for gate in self.graph.nodes_topologically_sorted(): 
            gate.forward()  # 每个门单元都有一个前向传播函数
        return loss  # 最终输出损失

    def backward(self):
        # 反向遍历门单元
        for gate in reversed(self.graph.nodes_topologically_sorted()): 
            gate.backward()  # 反向传播函数应用链式法则
        return inputs_gradients  # 输出梯度
        return inputs_gradients  # 输出梯度 

门单元类可以这么定义,比如一个乘法单元:

class MultiplyGate(object):
    def forward(self, x, y):
        z = x*y
        self.x = x
        self.y = y
        return z

    def backward(self, dz):
        dx = self.y * dz
        dy = self.x * dz
        return [dx, dy] 

1.2 向量形式反向传播

先考虑一个简单的例子,比如:

向量形式反向传播; 输入和输出都是 4096 维的 max 函数

这个 m a x max max 函数对输入向量 x x x 的每个元素都和 0 0 0 比较输出最大值,因此输出向量的维度也是 4096 4096 4096 维。此时的梯度是雅可比矩阵,即输出的每个元素对输入的每个元素求偏导组成的矩阵

假如输入 x x x 是 n n n 维的向量,输出 y y y 是 m m m 维的向量,则 y 1 , y 2 , ⋯   , y m y_1,y_2, \cdots,y_m y1​,y2​,⋯,ym​ 都是 ( x 1 − x n ) (x_1-x_n) (x1​−xn​) 的函数,得到的雅克比矩阵如下所示:

[ ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋯ ∂ y m ∂ x n ] \left[\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \ \vdots & \ddots & \vdots \ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right] ⎣⎢⎡​∂x1​∂y1​​⋮∂x1​∂ym​​​⋯⋱⋯​∂xn​∂y1​​⋮∂xn​∂ym​​​⎦⎥⎤​

那么这个例子的雅克比矩阵是 [ 4096 × 4096 ] [4096 \times 4096] [4096×4096] 维的,输出有 4096 4096 4096 个元素,每一个都要求 4096 4096 4096 次偏导。其实仔细观察发现,这个例子输出的每个元素都只和输入相应位置的元素有关,因此得到的是一个对角矩阵。

实际应用的时候,往往 100 个 x x x 同时输入,此时雅克比矩阵是一个 [ 409600 × 409600 ] [409600 \times 409600] [409600×409600] 的对角矩阵,当然只是针对这里的 f f f 函数。

实际上,完全写出并存储雅可比矩阵不太可能,因为维度极其大。

1) 一个例子

目标公式为: f ( x , W ) = ∣ ∣ W ⋅ x ∣ ∣ 2 = ∑ i = 1 n ( W ⋅ x ) i 2 f(x,W)=\vert \vert W\cdot x \vert \vert ²=\sum_{i=1}^n (W\cdot x)_{i}² f(x,W)=∣∣W⋅x∣∣2=∑i=1n​(W⋅x)i2​

其中 x x x 是 n n n 维的向量, W W W 是 n × n n \times n n×n 的矩阵。

设 q = W ⋅ x q=W\cdot x q=W⋅x ,于是得到下面的式子:

[ ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋯ ∂ y m ∂ x n ] \left[\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \ \vdots & \ddots & \vdots \ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right] ⎣⎢⎡​∂x1​∂y1​​⋮∂x1​∂ym​​​⋯⋱⋯​∂xn​∂y1​​⋮∂xn​∂ym​​​⎦⎥⎤​

q = W ⋅ x = ( W 1 , 1 x 1 + ⋯ + W 1 , n x n ⋮ W n , 1 x 1 + ⋯ + W n , n x n ) \begin{array}{l} q=W \cdot x=\left(\begin{array}{c} W_{1,1} x_{1}+\cdots+W_{1, n} x_{n} \ \vdots \ W_{n, 1} x_{1}+\cdots+W_{n, n} x_{n} \end{array}\right) \ \end{array} q=W⋅x=⎝⎜⎛​W1,1​x1​+⋯+W1,n​xn​⋮Wn,1​x1​+⋯+Wn,n​xn​​⎠⎟⎞​​

f ( q ) = ∥ q ∥ 2 = q 1 2 + ⋯ + q n 2 f(q)=|q|{2}=q_{1}+\cdots+q_{n}^{2} f(q)=∥q∥2=q12​+⋯+qn2​

可以看出:

  • ∂ f ∂ q i = 2 q i \frac{\partial f}{\partial q_i}=2q_i ∂qi​∂f​=2qi​ 从而得到 f f f 对 q q q 的梯度为 2 q 2q 2q ;

  • ∂ q k ∂ W i , j = 1 i = k x j \frac{\partial q_k}{\partial W_{i, j}}=1{i=k}x_j ∂Wi,j​∂qk​​=1i=kxj​, ∂ f ∂ W i , j = ∑ k = 1 n ∂ f ∂ q k ∂ q k ∂ W i , j = ∑ k = 1 n ( 2 q k ) 1 i = k x j = 2 q i x j \frac{\partial f}{\partial W_{i, j}}=\sum_{k=1}^n\frac{\partial f}{\partial q_k}\frac{\partial q_k}{\partial W_{i, j}}=\sum_{k=1}^n(2q_k)1{i=k}x_j=2q_ix_j ∂Wi,j​∂f​=∑k=1n​∂qk​∂f​∂Wi,j​∂qk​​=∑k=1n​(2qk​)1i=kxj​=2qi​xj​,从而得到 f f f 对 W W W 的梯度为 2 q ⋅ x T 2q\cdot x^T 2q⋅xT ;

  • ∂ q k ∂ x i = W k , i \frac{\partial q_k}{\partial x_i}=W_{k,i} ∂xi​∂qk​​=Wk,i​ , ∂ f ∂ x i = ∑ k = 1 n ∂ f ∂ q k ∂ q k ∂ x i = ∑ k = 1 n ( 2 q k ) W k , i \frac{\partial f}{\partial x_i}=\sum_{k=1}^n\frac{\partial f}{\partial q_k}\frac{\partial q_k}{\partial x_i}=\sum_{k=1}^n(2q_k)W_{k,i} ∂xi​∂f​=∑k=1n​∂qk​∂f​∂xi​∂qk​​=∑k=1n​(2qk​)Wk,i​ ,从而得到 f f f 对 x x x 的梯度为 2 W T ⋅ q 2W^T\cdot q 2WT⋅q

下面为计算图:

标量形式反向传播; 向量化计算图

2) 代码实现

import numpy as np

# 初值
W = np.array([[0.1, 0.5], [-0.3, 0.8]])
x = np.array([0.2, 0.4]).reshape((2, 1))  # 为了保证 dq.dot(x.T)是一个矩阵而不是实数

# 前向传播
q = W.dot(x)
f = np.sum(np.square(q), axis=0)

# 反向传播
# 回传 f = np.sum(np.square(q), axis=0)
dq = 2*q
# 回传 q = W.dot(x)
dW = dq.dot(x.T)  # x.T 就是对矩阵 x 进行转置
dx = W.T.dot(dq) 

注意:要分析维度!不要去记忆 d W dW dW 和 d x dx dx 的表达式,因为它们很容易通过维度推导出来。

权重的梯度 d W dW dW 的尺寸肯定和权重矩阵 W W W 的尺寸是一样的

  • 这里的 f f f 输出是一个实数,所以 d W dW dW 和 W W W 的形状一致。
  • 如果考虑 d q / d W dq/dW dq/dW 的话,如果按照雅克比矩阵的定义, d q / d w dq/dw dq/dw 应该是 2 × 2 × 2 2 \times 2 \times 2 2×2×2 维,为了减小计算量,就令其等于 x x x。
  • 其实完全不用考虑那么复杂,因为最终的损失函数一定是一个实数,所以每个门单元的输入梯度一定和原输入形状相同。 关于这点的说明,可以 点击这里,官网进行了详细的推导。
  • 而这又是由 x x x 和 d q dq dq 的矩阵乘法决定的,总有一个方式是能够让维度之间能够对的上的。

例如, x x x 的尺寸是 [ 2 × 1 ] [2 \times 1] [2×1], d q dq dq 的尺寸是 [ 2 × 1 ] [2 \times 1] [2×1],如果你想要 d W dW dW 和 W W W 的尺寸是 [ 2 × 2 ] [2 \times 2] [2×2],那就要 dq.dot(x.T),如果是 x.T.dot(dq) 结果就不对了。( d q dq dq 是回传梯度不能转置!)

2.神经网络简介

2.1 神经网络算法介绍

在不诉诸大脑的类比的情况下,依然是可以对神经网络算法进行介绍的。

在线性分类一节中,在给出图像的情况下,是使用 W x Wx Wx 来计算不同视觉类别的评分,其中 W W W 是一个矩阵, x x x 是一个输入列向量,它包含了图像的全部像素数据。在使用数据库 CIFAR-10 的案例中, x x x 是一个 [ 3072 × 1 ] [3072 \times 1] [3072×1] 的列向量, W W W 是一个 [ 10 × 3072 ] [10 \times 3072] [10×3072] 的矩阵,所以输出的评分是一个包含 10 个分类评分的向量。

一个两层的神经网络算法则不同,它的计算公式是 s = W 2 max ⁡ ( 0 , W 1 x ) s = W_2 \max(0, W_1 x) s=W2​max(0,W1​x) 。

W 1 W_1 W1​ 的含义:举例来说,它可以是一个 [ 100 × 3072 ] [100 \times 3072] [100×3072] 的矩阵,其作用是将图像转化为一个 100 维的过渡向量,比如马的图片有头朝左和朝右,会分别得到一个分数。

函数 m a x ( 0 , − ) max(0,-) max(0,−) 是非线性的,它会作用到每个元素。这个非线性函数有多种选择,大家在后续激活函数里会再看到。现在看到的这个函数是最常用的 ReLU 激活函数,它将所有小于 0 0 0 的值变成 0 0 0。

矩阵 W 2 W_2 W2​ 的尺寸是 [ 10 × 100 ] [10 \times 100] [10×100],会对中间层的得分进行加权求和,因此将得到 10 个数字,这 10 个数字可以解释为是分类的评分。

注意:非线性函数在计算上是至关重要的,如果略去这一步,那么两个矩阵将会合二为一,对于分类的评分计算将重新变成关于输入的线性函数。这个非线性函数就是改变的关键点。

参数 W 1 W_1 W1​ **,$ **W_2$ 将通过随机梯度下降来学习到,他们的梯度在反向传播过程中,通过链式法则来求导计算得出。

一个三层的神经网络可以类比地看做 s = W 3 max ⁡ ( 0 , W 2 max ⁡ ( 0 , W 1 x ) ) s = W_3 \max(0, W_2 \max(0, W_1 x)) s=W3​max(0,W2​max(0,W1​x)) ,其中 W 1 W_1 W1​, W 2 W_2 W2​ , W 3 W_3 W3​ 是需要进行学习的参数。中间隐层的尺寸是网络的超参数,后续将学习如何设置它们。现在让我们先从神经元或者网络的角度理解上述计算。

两层神经网络参考代码实现如下,中间层使用 sigmoid 函数:

import numpy as np
from numpy.random import randn

N, D_in, H, D_out = 64, 1000, 100, 10
# x 是 64x1000 的矩阵,y 是 64x10 的矩阵
x, y = randn(N, D_in), randn(N, D_out)
# w1 是 1000x100 的矩阵,w2 是 100x10 的矩阵
w1, w2 = randn(D_in, H), randn(H, D_out)

# 迭代 10000 次,损失达到 0.0001 级
for t in range(10000):
    h = 1 / (1 + np.exp(-x.dot(w1)))  # 激活函数使用 sigmoid 函数,中间层
    y_pred = h.dot(w2)
    loss = np.square(y_pred - y).sum()  # 损失使用 L2 范数
    print(str(t)+': '+str(loss))

    # 反向传播
    grad_y_pred = 2.0 * (y_pred - y)
    grad_w2 = h.T.dot(grad_y_pred)
    grad_h = grad_y_pred.dot(w2.T)
    # grad_xw1 = grad_h*h*(1-h)
    grad_w1 = x.T.dot(grad_h*h*(1-h))

    # 学习率是 0.0001
    w1 -= 1e-4 * grad_w1
    w2 -= 1e-4 * grad_w2 

2.2 神经网络与真实的神经对比

神经网络算法很多时候是受生物神经系统启发而简化模拟得到的。

大脑的基本计算单位是神经元(neuron) 。人类的神经系统中大约有 860 亿个神经元,它们被大约 1014 - 1015 个突触(synapses) 连接起来。下图的上方是一个生物学的神经元,下方是一个简化的常用数学模型。每个神经元都从它的树突(dendrites) 获得输入信号,然后沿着它唯一的轴突(axon) 产生输出信号。轴突在末端会逐渐分枝,通过突触和其他神经元的树突相连。

神经网络简介; 神经元 V.S. 数学模型

在神经元的计算模型中,沿着轴突传播的信号(比如 x 0 x_0 x0​ )将基于突触的突触强度(比如 w 0 w_0 w0​ ),与其他神经元的树突进行乘法交互(比如 w 0 x 0 w_0 x_0 w0​x0​ )。

对应的想法是,突触的强度(也就是权重 w w w ),是可学习的且可以控制一个神经元对于另一个神经元的影响强度(还可以控制影响方向:使其兴奋(正权重)或使其抑制(负权重))。

树突将信号传递到细胞体,信号在细胞体中相加。如果最终之和高于某个阈值,那么神经元将会「激活」,向其轴突输出一个峰值信号。

在计算模型中,我们假设峰值信号的准确时间点不重要,是激活信号的频率在交流信息。基于这个速率编码的观点,将神经元的激活率建模为激活函数(activation function) f f f ,它表达了轴突上激活信号的频率。

由于历史原因,激活函数常常选择使用sigmoid 函数 σ \sigma σ ,该函数输入实数值(求和后的信号强度),然后将输入值压缩到 0 ∼ 1 0\sim 1 0∼1 之间。在本节后面部分会看到这些激活函数的各种细节。

这里的激活函数 f f f 采用的是 sigmoid 函数,代码如下:

class Neuron:
    # ...
    def neuron_tick(self, inputs):
        # 假设输入和权重都是 1xD 的向量,偏差是一个数字
        cell_body_sum = np.sum(inputs*self.weights) + self.bias
        # 当和远大于 0 时,输出为 1,被激活
        firing_rate = 1.0 / (1.0 + np.exp(-cell_body_sum))
        return firing_rate 

2.3 常用的激活函数

神经网络简介; 常用的激活函数

3.神经网络结构

关于神经网络结构的知识也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 神经网络基础浅层神经网络深层神经网络 里对于不同深度的网络结构的讲解

对于普通神经网络,最普通的层级结构是全连接层(fully-connected layer) 。全连接层中的神经元与其前后两层的神经元是完全成对连接的,但是在同层内部的神经元之间没有连接。网络结构中没有循环(因为这样会导致前向传播的无限循环)。

下面是两个神经网络的图例,都使用的全连接层:

神经网络结构; 全连接神经网络

  • 左边:一个 2 层神经网络,隐层由 4 个神经元(也可称为单元(unit))组成,输出层由 2 个神经元组成,输入层是 3 个神经元(指的是输入图片的维度而不是图片的数量)。
  • 右边:一个 3 层神经网络,两个含 4 个神经元的隐层。

注意:当我们说 N N N 层神经网络的时候,我们并不计入输入层。单层的神经网络就是没有隐层的(输入直接映射到输出)。也会使用人工神经网络(Artificial Neural Networks 缩写 ANN)或者多层感知器(Multi-Layer Perceptrons 缩写 MLP)来指代全连接层构建的这种神经网络。此外,输出层的神经元一般不含激活函数。

用来度量神经网络的尺寸的标准主要有两个:一个是神经元的个数,另一个是参数的个数。用上面图示的两个网络举例:

  • 第一个网络有 4 + 2 = 6 4+2=6 4+2=6 个神经元(输入层不算), [ 3 × 4 ] + [ 4 × 2 ] = 20 [3 \times 4]+[4 \times 2]=20 [3×4]+[4×2]=20 个权重,还有 4 + 2 = 6 4+2=6 4+2=6 个偏置,共 26 26 26 个可学习的参数。
  • 第二个网络有 4 + 4 + 1 = 9 4+4+1=9 4+4+1=9 个神经元, [ 3 × 4 ] + [ 4 × 4 ] + [ 4 × 1 ] = 32 [3 \times 4]+[4 \times 4]+[4 \times 1]=32 [3×4]+[4×4]+[4×1]=32 个权重, 4 + 4 + 1 = 9 4+4+1=9 4+4+1=9 个偏置,共 41 41 41 个可学习的参数。

现代卷积神经网络能包含上亿个参数,可由几十上百层构成(这就是深度学习)。

3.1 三层神经网络代码示例

不断用相似的结构堆叠形成网络,这让神经网络算法使用矩阵向量操作变得简单和高效。我们回到上面那个 3 层神经网络,输入是 [ 3 × 1 ] [3 \times 1] [3×1] 的向量。一个层所有连接的权重可以存在一个单独的矩阵中。

比如第一个隐层的权重 W 1 W_1 W1​ 是 [ 4 × 3 ] [4 \times 3] [4×3],所有单元的偏置储存在 b 1 b_1 b1​ 中,尺寸 [ 4 × 1 ] [4 \times 1] [4×1]。这样,每个神经元的权重都在 W 1 W_1 W1​ 的一个行中,于是矩阵乘法 np.dot(W1, x)+b1 就能作为该层中所有神经元激活函数的输入数据。类似的, W 2 W_2 W2​ 将会是 [ 4 × 4 ] [4 \times 4] [4×4] 矩阵,存储着第二个隐层的连接, W 3 W_3 W3​ 是 [ 1 × 4 ] [1 \times 4] [1×4] 的矩阵,用于输出层。

完整的 3 层神经网络的前向传播就是简单的 3 次矩阵乘法,其中交织着激活函数的应用。

import numpy as np

# 三层神经网络的前向传播
# 激活函数
f = lambda x: 1.0/(1.0 + np.exp(-x))

# 随机输入向量 3x1
x = np.random.randn(3, 1)
# 设置权重和偏差
W1, W2, W3 = np.random.randn(4, 3), np.random.randn(4, 4), np.random.randn(1, 4),
b1, b2= np.random.randn(4, 1), np.random.randn(4, 1)
b3 = 1

# 计算第一个隐藏层激活 4x1
h1 = f(np.dot(W1, x) + b1)
# 计算第二个隐藏层激活 4x1
h2 = f(np.dot(W2, h1) + b2)
# 输出是一个数
out = np.dot(W3, h2) + b3 

在上面的代码中, W 1 W_1 W1​, W 2 W_2 W2​, W 3 W_3 W3​, b 1 b_1 b1​, b 2 b_2 b2​, b 3 b_3 b3​ 都是网络中可以学习的参数。注意 x x x 并不是一个单独的列向量,而可以是一个批量的训练数据(其中每个输入样本将会是 x x x 中的一列),所有的样本将会被并行化的高效计算出来。

注意神经网络最后一层通常是没有激活函数的(例如,在分类任务中它给出一个实数值的分类评分)。

全连接层的前向传播一般就是先进行一个矩阵乘法,然后加上偏置并运用激活函数。

3.2 理解神经网络

关于深度神经网络的解释也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 深层神经网络 里「深度网络其他优势」部分的讲解

全连接层的神经网络的一种理解是:

  • 它们定义了一个由一系列函数组成的函数族,网络的权重就是每个函数的参数。

拥有至少一个隐层的神经网络是一个通用的近似器,神经网络可以近似任何连续函数。

虽然一个 2 层网络在数学理论上能完美地近似所有连续函数,但在实际操作中效果相对较差。虽然在理论上深层网络(使用了多个隐层)和单层网络的表达能力是一样的,但是就实践经验而言,深度网络效果比单层网络好。

对于全连接神经网络而言,在实践中 3 层的神经网络会比 2 层的表现好,然而继续加深(做到 4,5,6 层)很少有太大帮助。卷积神经网络的情况却不同,在卷积神经网络中,对于一个良好的识别系统来说,深度是一个非常重要的因素(比如当今效果好的 CNN 都有几十上百层)。对于该现象的一种解释观点是:因为图像拥有层次化结构(比如脸是由眼睛等组成,眼睛又是由边缘组成),所以多层处理对于这种数据就有直观意义。

4.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=4

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

5.要点总结

  • 前向传播与反向传播
  • 标量与向量化形式计算
  • 求导链式法则应用
  • 神经网络结构
  • 激活函数
  • 理解神经网络

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(5) | 卷积神经网络(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125024904

ShowMeAI 研究中心


深度学习与计算机视觉

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

上篇 深度学习与 CV 教程(4) | 神经网络与反向传播 提到的神经网络是线性分类器的堆叠,只不过在中间加入非线性函数,对中间层产生的模板加权后得到最终的得分。计算机视觉中用到更多的神经网络结构是卷积神经网络(Convolutional Neural Networks) ,它与前面提到的前馈神经网络的构想是一致的,只是包含卷积层等特殊构建的神经网络层次结构。本篇ShowMeAI给大家详细展开介绍卷积神经网络。

关于卷积神经网络的详细知识也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章卷积神经网络解读

本篇重点

  • 卷积神经网络的历史
  • 卷积神经网络与常规神经网络的对比;卷积层、池化层、ReLU 层、全连接层;局部连接、参数共享、最大池化、步长、零填充、数据体尺寸等概念
  • 卷积神经网络层的规律与尺寸设置
  • 卷积神经网络经典案例

1.卷积神经网络的历史

1957 年,Frank Rosenblatt 发明了第一代感知器,第一次实现感知器算法。感知器算法和神经网络很相似,都有 w w w、 x x x、 b b b 等参数,也有参数更新规则。但是感知器只能输出 0 0 0、 1 1 1 两个数,参数更新规则也不是反向传播。

f ( x ) = { 0 i f    w ⋅ z + b < 0 1 o t h e r w i s e f(x)= \begin{cases} 0 & if \space\space w\cdot z+b<0 \ 1 & otherwise \end{cases} f(x)={01​if  w⋅z+b<0otherwise​

更新规则

w i ( t + 1 ) = w i ( t ) + α ( d j − y j ( t ) ) x j , i w_i(t+1)=w_i(t)+\alpha(d_j-y_j(t))x_{j,i} wi​(t+1)=wi​(t)+α(dj​−yj​(t))xj,i​

1960 年,Widrow 和 Hoff 的 Adaline/Madaline,首次尝试把线性层叠加,整合成多层感知器网络,与现代神经网络层的结构比较类似,但是仍然没有反向传播或其他训练方法。

1986 年 Rumelhart 才首次提出反向传播算法,然后我们熟悉的链式法则、更新规则等才逐渐出现。至此有了神经网络核心的训练方法,但是仍然无法训练大型的神经网络。

2006 年,Hinton 和 Salakhutdinov 的论文表明神经网络不仅可以训练并且可以高效的训练,但是需要谨慎的初始化,才能反向传播。他们先预先训练得到隐层的参数,再训练整个网络。

直到 2012 年,深度神经网络才得到大规模的应用。首先是 Geoffrey Hinton 等将 CNN 用于语音识别,然后其同实验室的 Alex Acero 等发表了里程碑式的论文,将 CNN 用于 Image net 大赛极大提高识别率,成为图像分类的标杆方法。

1.1 卷积网络的产生过程

从 1959 年开始 ,Hubel & Wiesel 做了一些列实验,试图弄明白神经元如何在视觉皮层上工作。他们把电极放进猫的脑袋中,然后给猫不同的视觉刺激,比如不同的边缘方向、不同的形状等,然后测量神经元的应激响应。

他们得出一些重要的结论:一是大脑皮层上的细胞与视觉中的区域相关联,有映射关系。二是神经元间存在分层关系。初级层次的细胞对光的方向产生反应,复杂一点的会对光的移动有反应,超复杂的可以反应端点,识别形状。

1980 年,Fukushima 的感知神经器首次将这种简单细胞与复杂细胞的概念形成实例,一种简单细胞与复杂细胞交替层结构。简单细胞会有一些可调参数,复杂细胞对简单细胞执行池化操作。

1998 年,LeCun, Bottou, Bengio, Haffner 等人首次展示一个实例,应用反向传播和基于梯度的学习方法来训练卷积神经网络,用于邮政编码识别,效果显著。但是有局限性,不能用到更复杂的数据中。

卷积神经网络; LeNet-5 神经网络

2012 年,Alex 等人提出一种现代化的卷积神经网络,称为 AlexNet。与 LeCun 的很相似,只是更大更深,可以充分利用大量图片数据比如 Image net 和 GPU 并行计算能力。

卷积神经网络; AlexNet 神经网络

今天,CNN 已经被广泛应用到图像分类、目标检测、图像分割等。这些技术被广泛用于自动驾驶领域,使用 GPU 驱动,将高性能的 GPU 置于嵌入式系统。应用到其他领域,比如人脸识别、视频分类、姿势识别、医学影像分析、星系分类、路标识别,也应用到游戏中,比如 AlfaGo。除了分类识别等任务,还可用于图像描述、艺术创作(Deep Dream,神经图像风格迁移)。

2.卷积神经网络详述

2.1 卷积神经网络和常规神经网络对比

卷积神经网络(CNN / ConvNet) 和常规神经网络非常相似:

  • 都是由神经元组成,神经元中有具有学习能力的权重和偏置项。每个神经元都得到一些输入数据,进行内积运算后再进行激活函数运算;
  • 整个网络依旧是一个可导的评分函数,该函数的输入是原始的图像像素,输出是不同类别的评分;
  • 在最后一层(往往是全连接层),网络依旧有一个损失函数(比如 SVM 或 Softmax),并且在神经网络中我们实现的各种技巧和要点依旧适用于卷积神经网络。

卷积神经网络的结构基于输入数据是图像,向结构中添加了一些特有的性质,使得前向传播函数实现起来更高效,并且大幅度降低了网络中参数的数量。

2.2 常规神经网络

常规神经网络的输入是一个向量,比如把一张 32 × 32 × 3 32 \times 32 \times 3 32×32×3 的图片延展成 3072 × 1 3072 \times 1 3072×1 的列向量 x x x,然后在一系列的隐层中对它做变换。

每个隐层都是由若干的神经元组成,每个神经元都与前一层中的所有神经元连接(这就是全连接的概念)。 但是在一个隐层中,神经元相互独立不进行任何连接。

最后的全连接层被称为「输出层」,在分类问题中,它输出的值被看做是不同类别的评分值。比如线性分类 W x Wx Wx , W W W 是 10 × 3072 10 \times 3072 10×3072 的权重矩阵,即 W W W 有 10 个行向量,最终输出是一个 10 × 1 10 \times 1 10×1 的得分向量,其中的每一个值是 W W W 的某一个行向量和 x x x 的点积结果,也就是一个神经元的输出。

最终会有 10 10 10 个神经元输出 10 10 10 个值( W 0 x , W 1 x , ⋯   , W 9 x W_0x, W_1x, \cdots, W_9x W0​x,W1​x,⋯,W9​x), x x x 和每一个神经元相连,因此是全连接的。

卷积神经网络; 全连接层神经元输出

缺点与限制

但是全连接神经网络在处理大的图片数据时参数会急速增加,同时效果也不尽如人意。

  • 比如在 CIFAR-10 中,图像的尺寸是 32 × 32 × 3 32 \times 32 \times 3 32×32×3,对应网络的第一个隐层中,每一个单独的全连接神经元的参数个数即 W W W 的一个行向量就有 32 × 32 × 3 = 3072 32 \times 32 \times 3=3072 32×32×3=3072 个。
  • 若是一个尺寸为 200 × 200 × 3 200 \times 200 \times 3 200×200×3 的图像,会让神经元包含 200 × 200 × 3 = 120 , 000 200 \times 200 \times 3=120,000 200×200×3=120,000 个权重值。而网络中肯定不止一个神经元,那么参数的量就会快速增加!

全连接方式效率不高,且参数量大,可能会导致网络过拟合。

2.3 卷积神经网络

关于卷积层的动图讲解也可以参考ShowMeAI的的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章卷积神经网络解读

与常规神经网络不同,卷积神经网络的各层中的神经元都是 3 维的:宽度、高度和深度(这里的深度指的是激活数据体的第三个维度,而不是整个网络的深度,整个网络的深度指的是网络的层数)。

  • 以第一层卷积层为例,输入数据仍然是 32 × 32 × 3 32 \times 32 \times 3 32×32×3(宽度 × \times × 高度 × \times ×深度)的,并不会将其延展成一个列向量,这样可以保持图像的空间结构(spatial structure)。
  • 与输入相连的神经元权重不再是 W W W 的一个行向量( 3072 3072 3072 个参数),而是与输入数据有同样深度的滤波器(filter,也称作卷积核),比如是 5 × 5 × 3 5 \times 5 \times 3 5×5×3 的滤波器 w w w。
  • 这时的神经元(卷积核)不再与输入图像 x x x 是全连接的,而是局部连接(local connectivity),只和 x x x 中一个 5 × 5 × 3 5 \times 5 \times 3 5×5×3 的小区域进行全连接(常规神经网络中每个神经元都和整个 x x x 全连接),滤波器和这个区域计算一个点积 w x wx wx(计算的时候会展成两个向量进行点积),然后加一个偏置项 b b b,就得到一个输出数据( w x + b wx+b wx+b)。这样的一个神经元共有 5 × 5 × 3 + 1 = 76 5 \times 5 \times 3+1=76 5×5×3+1=76 个参数。
  • 这个滤波器会在 x x x 上按一定的步长(stride) 依次滑动,比如步长为 1 1 1 时,最终会得到一个 28 × 28 × 1 28 \times 28 \times 1 28×28×1 的输出数据,称作激活映射(activation map)特征映射(feature map) ,对应 28 × 28 28 \times 28 28×28 个神经元的输出。

卷积神经网络; 卷积层示意

对于用来分类 CIFAR-10 中图像的卷积网络,其最后的输出层的维度是 1 × 1 × 10 1 \times 1 \times 10 1×1×10,因为在卷积神经网络结构的最后部分将会把全尺寸的图像压缩为包含分类评分的一个向量,向量是在深度方向排列的。

卷积神经网络; 神经网络结构对比 MLP V.S. CNN

上图左边是常规神经网络,每个神经元和上层的神经元都是全连接的;右图是卷积神经网络,每个神经元都有三个维度,网络每一层都将 3D 的输入数据变化为神经元 3D 的激活数据并输出。

在这个例子中,红色的输入层装的是图像,所以它的宽度和高度就是图像的宽度和高度,它的深度是 3(代表了 R/红、G/绿、B/蓝 3 个颜色通道)。

蓝色的部分是第一层卷积层的输出,这里的深度显然不为 1,表明有多种滤波器。如果我们有 6 6 6 个 5 × 5 5 \times 5 5×5 的滤波器,每个卷积核代表从输入捕捉某些信息的滤波器,那它们依次滑过整张图片,得到第一个卷积层的输出结果是 28 × 28 × 6 28 \times 28 \times 6 28×28×6 的。如下图所示:

卷积神经网络; 6 个滤波器对应的激活映射

3.卷积神经网络的结构

一个简单的卷积神经网络是由各种层按照顺序排列组成,卷积神经网络主要由三种类型的层构成:卷积层,池化(Pooling)层和全连接层(全连接层和常规神经网络中的一样)。通过将这些层叠加起来,就可以构建一个完整的卷积神经网络。

一个用于 CIFAR-10 图像数据分类的卷积神经网络的结构可以是「输入层-卷积层-ReLU 层-池化层-全连接层」,这四个层也是目前卷积神经网络比较常用的层。

  • 输入层是 [ 32 × 32 × 3 ] [32 \times 32 \times 3] [32×32×3] 存有图像的原始像素,本例中图像宽高均为 32,有 3 个颜色通道。
  • 卷积层中,神经元与输入层中的一个局部区域相连,每个神经元都计算输入层上与自己相连的区域与自己权重的内积。卷积层会计算所有神经元的输出。如果使用 12 个滤波器(也叫作卷积核),得到的输出数据体的维度就是 [ 32 × 32 × 12 ] [32 \times 32 \times 12] [32×32×12] 。
  • ReLU 层将会逐个元素地进行激活函数操作,比如使用以 0 0 0 为阈值的 ReLU 函数 m a x ( 0 , − ) max(0,-) max(0,−) 作为激活函数。该层对数据尺寸没有改变,还是 [ 32 × 32 × 12 ] [32 \times 32 \times 12] [32×32×12] 。
  • 池化层在空间维度(宽度和高度)上进行降采样(downsampling)操作,假设数据尺寸变为 [ 16 × 16 × 12 ] [16 \times 16 \times 12] [16×16×12] 。
  • 全连接层将会计算分类评分,数据尺寸变为 [ 1 × 1 × 10 ] [1 \times 1 \times 10] [1×1×10] ,其中 10 个数字对应的就是 CIFAR-10 中 10 个类别的分类评分值。全连接层与常规神经网络一样,其中每个神经元都与前一层中所有神经元相连接。

卷积神经网络一层一层地将图像从原始像素值变换成最终的分类评分值。

  • 卷积层和全连接层(CONV/FC)对输入执行变换操作的时候,不仅会用到激活函数,还会用到很多参数(神经元的权值和偏置项)
  • ReLU 层和池化层进行一个固定的函数操作。
  • 卷积层、全连接层和池化层有超参数,ReLU 层没有。卷积层和全连接层中的参数利用梯度下降训练。

实际应用的时候,卷积网络是由多个卷积层依次堆叠组成的序列,然后使用激活函数(比如 ReLU 函数)对其进行逐一处理。然后这些卷积层、激活层、池化层会依次堆叠,上一层的输出作为下一层的输入。每一层都会使用多个卷积核,每个卷积核对用一个激活映射。

卷积神经网络; 多个卷积层结构

3.1 卷积核可视化

卷积网络这些卷积层的所有卷积核完成训练后,会发现:

  • 前面几个卷积层的卷积核捕捉和匹配的是一些比较简单的特征,比如边缘;
  • 中间几层的卷积核代表的特征变得复杂一些,比如一些边角和斑点;
  • 最后几层的特征就会变得特别丰富和复杂。

这些卷积核是从简单到复杂的特征序列。这实际上和 Hubel & Wiesel 的实验结果比较相似,即使在我们并没有明确的让网络去学习这些从简单到复杂的特征,但是给它这种层次结构并经过反向传播训练后,这些类型的卷积核最终也能学到。

卷积神经网络; 卷积核可视化

3.2 激活映射与卷积核可视化联系

我们有 32 个已经在卷积网络中训练好的 5 × 5 5 \times 5 5×5 卷积核,每一个卷积核滑过原始图像得到一张激活映射,将它们可视化,我们可以看出卷积核在原图像匹配和寻找什么。

比如下图上方红框中的第一个卷积核对应得到红框的激活映射,卷积核看起来像是一个定向边缘的模板,所以当其滑过图像,在那些有定向边缘的地方会得到较高的值。

之所以称作卷积,只是计算形式上就是卷积,滤波器和信号(图像)的元素相乘后求和。

卷积神经网络; 激活映射与卷积核

3.3 整个卷积网络的结构

卷积神经网络; 图像输入整个 CNN; 输出得分的过程

左边的输入层存有原始图像,右边的输出层得到各类别评分。

图像经过一系列卷积层、RELU 层、池化层,最后经过全连接层得到针对不同类别的分类得分,这里只显示了得分最高的 5 个评分值和对应的类别。

整个网络包括输入层、输出层共有 17 层,架构是 [conv-relu-conv-relu-pool] x3-fc-softmax,共有 7000 个参数,使用 3 × 3 3 \times 3 3×3 卷积和 2 × 2 2 \times 2 2×2 池化区域。斯坦福大学 课程主页 上展示的就是这个 CNN 网络。

下面详细介绍卷积层、池化层等层次及其工作原理。

4.卷积网络各层详细介绍

4.1 卷积层(Convolutional Layer,Conv layer)

卷积层是构建卷积神经网络的核心层,网络中大部分的计算量都由它产生。

关于卷积层的动图讲解也可以参考ShowMeAI的的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章卷积神经网络解读

1) 概述

卷积层的参数是由一些可学习的滤波器(filter) 集合构成的。每个滤波器在宽度和高度上都比较小,但是深度和输入数据一致。

比如卷积神经网络第一层的一个典型的滤波器的尺寸可以是 5 × 5 × 3 5 \times 5 \times 3 5×5×3(宽高都是 5 5 5 像素,深度是 3 3 3 是因为图像应为颜色通道,所以有 3 的深度)。

在前向传播的时候,让每个滤波器都在输入数据的宽度和高度上滑动(更精确地说是做卷积),然后计算这个滤波器和输入数据对应每一个区域的内积,最终会生成一个 2 维的激活映射(也叫激活图)(activation map),激活图给出了在每个空间位置处滤波器的反应。

直观地来说,网络会让滤波器学习,结果是当它看到某些类型的视觉特征时就激活,具体的视觉特征可能是某些方位上的边界,或者在第一层上某些颜色的斑点,甚至可以是网络更高层上的蜂巢状或者车轮状图案。

在每个卷积层上,一般有多个滤波器组成集合(比如 12 个),每个都会生成一个不同的二维激活映射。将这些激活映射在深度方向上层叠起来就生成了这个卷积层的输出 3D 数据。

这个 3D 数据的每一个激活图,都是由一些参数相同的神经元在原图像的不同位置做内积得到的输出数据组成的。每张激活图对应的所有神经元参数都相同(因为实际上就是同一个滤波器在图像上不同位置滑动的结果,每到一个位置就是一个神经元),称为参数共享

2) 局部连接

卷积层每个神经元和原图像只在一个小区域进行全连接,称为 局部连接。因为在处理图像这样的高维度输入时,让每个神经元都与前一层中的所有神经元进行全连接是不现实的。

局部连接的空间大小叫做神经元的感受野(receptive field) ,它的尺寸(其实就是滤波器的空间尺寸)是一个超参数。在深度方向上,这个连接的大小总是和输入量的深度相等。即连接在空间(宽高)上是局部的,但是在深度上总是和输入数据的深度一致。

重复一下之前的例子,一张 32 × 32 × 3 32 \times 32 \times 3 32×32×3 的图片,滤波器大小为 5 × 5 × 3 5 \times 5 \times 3 5×5×3。此时感受野尺寸是 5 × 5 5 \times 5 5×5,滤波器的深度需要和原图像深度一致,为 3 3 3。那么神经元的权重个数为 5 × 5 × 3 = 75 5 \times 5 \times 3=75 5×5×3=75 个,再加一个偏置项,共 76 76 76 个。神经元和原图像一个同样大小的区域是全连接的,共有 75 75 75 个连接,但是与整个图像是局部连接的(只在 5 × 5 5 \times 5 5×5 的空间上连接),如果是全连接则需要有 3072 3072 3072 个连接。

卷积神经网络; 卷积层 - 神经元与原图像连接

  • 左边:红色的是输入数据体(比如 CIFAR-10 中的图像),蓝色的部分是第一个卷积层中的所有神经元。卷积层中的每个神经元都只是与输入数据体的一个局部空间相连,但是与输入数据体的所有深度维度全部相连(所有颜色通道)。在深度方向上有多个神经元(本例中 5 个),它们都接受输入数据的同一块区域(感受野相同)。深度方向上的神经元参数是不同的。
  • 右边:神经元内部计算还和以前一样,还是计算权重和输入的内积,然后进行激活函数运算,只是它们的连接被限制在一个局部空间,即输入数据只是原图像的一部分。

3) 神经元排列与输出数据尺寸

卷积层的所有神经元与原始图像卷积后,输出数据体的尺寸由三个超参数控制:深度(depth),步长(stride)零填充(zero-padding)

① 深度 :卷积层中使用的滤波器往往有多个,深度就是滤波器的数量

  • 每个滤波器在输入数据中匹配计算不同的模式。
  • 比如第一个卷积层的输入是原始图像,那么在深度维度上的不同神经元将可能被原图像上不同方向的边界,或者是颜色斑点激活。将这些沿着深度方向排列、感受野相同的神经元集合称为深度列(depth column),或者纤维(fibre)。

② 步长:步长就是滤波器每次移动跨越的像素数量

  • 当步长为 1,滤波器每次移动 1 个像素。当步长为 2(实际中很少使用比 2 大的步长),滤波器滑动时每次移动 2 个像素。这个操作会让输出数据体在空间上变小。

③ 零填充:在图像的边界外填充零像素点

  • 滑动时会使输出数据体在空间上变小,比如 32 × 32 32 \times 32 32×32 的图像经过一个卷积层输出数据在空间上可能是 28 × 28 28 \times 28 28×28 的,经过多层后会迅速收敛。我们不希望这样,于是引入了零填充,零填充有一个良好性质,可以控制输出数据体的空间尺寸(最常用的是用来保持输入数据体在空间上的尺寸,这样输入和输出的宽高都相等)。

比如有一个 7 × 7 7 \times 7 7×7 的原始图片,滤波器的尺寸是 3 × 3 3 \times 3 3×3,步长为 1 1 1 时的输出是 5 × 5 5 \times 5 5×5;步长为 2 2 2 时输出是 3 × 3 3 \times 3 3×3,但步长是 3 3 3 的时候尺寸不再适合。

卷积神经网络; 卷积层 - 各步长滑动示意图

假设图片的尺寸是 N × N N \times N N×N,滤波器尺寸是 F × F F \times F F×F,步长为 S S S,则输出数据的尺寸为: ( N − F ) / S + 1 (N-F)/S +1 (N−F)/S+1。所以当 N = 7 N=7 N=7, F = 3 F=3 F=3:

  • S = 1 S=1 S=1 时,输出为 5 5 5
  • S = 2 S=2 S=2 时,输出为 3 3 3
  • S = 3 S=3 S=3 时,输出为 2.333 2.333 2.333,显然不合理

所以步长的设置不合理会导致网络的异常,引入零填充可一定程度解决这个问题。

下面考虑加入零填充的情形。在 7 × 7 7 \times 7 7×7 的图像外面加入一圈零像素,滤波器尺寸仍为 3 × 3 3 \times 3 3×3,步长为 1,此时的输出尺寸应该是多少?答案是 7 × 7 7 \times 7 7×7,因为此时的原图像相当于变成 9 × 9 9 \times 9 9×9。此时的输出数据空间尺寸和输入的相同。

卷积神经网络; 卷积层 - padding 图像边界外加零填充

综上,可得输出数据尺寸的计算公式:

假如输入数据体尺寸 W × W W \times W W×W,卷积层中神经元的感受野尺寸 F × F F \times F F×F,步长 S S S 和零填充的数量 P P P,则输出数据体的空间尺寸为 ( W + 2 P − F ) / S + 1 (W+2P-F)/S+1 (W+2P−F)/S+1 。

  • 比如上面输入是 7 × 7 7 \times 7 7×7,滤波器是 3 × 3 3 \times 3 3×3,步长为 1 1 1,填充为 1 1 1,那么就能得到一个 7 × 7 7 \times 7 7×7 的输出。

一般来说,当步长 S = 1 S=1 S=1 时,为保证输入尺寸和输出尺寸相同,零填充的数量为: P = ( F − 1 ) / 2 P=(F-1)/2 P=(F−1)/2 。

  • 考虑最初的问题,一张 32 × 32 × 3 32 \times 32 \times 3 32×32×3 的图像,经过有 10 个 5 × 5 × 3 5 \times 5 \times 3 5×5×3 滤波器的卷积层,步长为 1 1 1,零填充数量为 2 2 2,则输出的尺寸为?显然是 32 × 32 × 10 32 \times 32 \times 10 32×32×10。这是因为滤波器尺寸 5 5 5 步长 1 1 1 填充 2 2 2 可以保持空间尺寸,滤波器的数量又决定了输出的深度。

那么这个卷积层有多少个参数呢?

  • 10 个滤波器每个有 5 × 5 × 3 + 1 = 76 5 \times 5 \times 3+1=76 5×5×3+1=76 个参数,所以共有 760 760 760 个参数。

那么这个卷积层一共有多少个神经元呢?

  • 答案是 32 × 32 × 10 32 \times 32 \times 10 32×32×10,因为输出数据的每个数据点,都由一个神经元产生。也就是说输出数据体的尺寸,就代表着神经元的排列方式

但是,既然有 32 × 32 × 10 32 \times 32 \times 10 32×32×10 个神经元,每个神经元的参数为 76,那为什么只有 760 个参数呢?大家可以在后面的参数共享部分可以找到答案。

AlexNet 神经网络架构,赢得了 2012 年的 ImageNet 挑战,它的结构中:

  • 输入图像的尺寸是 [ 227 × 227 × 3 ] [227 \times 227 \times 3] [227×227×3]
  • 在第一个卷积层,神经元使用的感受野尺寸 F = 11 F=11 F=11,步长 S = 4 S=4 S=4,不使用零填充 P = 0 P=0 P=0。因为 ( 227 − 11 ) / 4 + 1 = 55 (227-11)/4+1=55 (227−11)/4+1=55,卷积层的深度 K = 96 K=96 K=96,则卷积层的输出数据体尺寸为 [ 55 × 55 × 96 ] [55 \times 55 \times 96] [55×55×96] 。 55 × 55 × 96 55 \times 55 \times 96 55×55×96 个神经元中,每个都和输入数据体中一个尺寸为 [ 11 × 11 × 3 ] [11 \times 11 \times 3] [11×11×3] 的区域全连接。在深度列上的 96 个神经元都是与输入数据体中同一个 [ 11 × 11 × 3 ] [11 \times 11 \times 3] [11×11×3] 区域连接,但是权重不同。

4) 参数共享机制

在卷积层中使用参数共享是用来控制参数的数量。

就用上面的真实案例,在第一个卷积层就有 55 × 55 × 96 = 290 , 400 55 \times 55 \times 96=290,400 55×55×96=290,400 个神经元(假设神经元都是独立的)

  • 因为一个滤波器每滑到一个位置,就对应一个神经元,得到一个神经元输出。滑过所有位置后的输出数据空间尺寸为 55 × 55 55 \times 55 55×55,对应着有 55 × 55 55 \times 55 55×55 个神经元。再加上一共有 96 个滤波器,所以为 55 × 55 × 96 55 \times 55 \times 96 55×55×96。
  • 每个神经元有 11 × 11 × 3 + 1 = 364 11 \times 11 \times 3+1=364 11×11×3+1=364 个参数。将这些合起来就是 290400 × 364 = 105 , 705 , 600 290400 \times 364=105,705,600 290400×364=105,705,600 个参数。单单第一层就有这么多参数,显然这个数目是非常大的。

作一个合理的假设:如果一个特征在计算某个空间位置 ( x , y ) (x,y) (x,y)的时候有用,那么它在计算另一个不同位置 ( x 2 , y 2 ) (x_2,y_2) (x2​,y2​)的时候也有用

  • 参数共享的假设是有道理的:如果在图像某些地方探测到一个水平的边界是很重要的,那么在其他一些地方也会同样是有用的,这是因为图像结构具有平移不变性。

基于这个假设,可以显著地减少参数数量。也是基于这个假设,滤波器可以在原图片上滑动。

卷积神经网络; 卷积层 - 参数共享机制

如果我们将深度维度上一个单独的 2 维切片看做深度切片(depth slice),比如这个尺寸为 [ 55 × 55 × 96 ] [55 \times 55 \times 96] [55×55×96] 的输出数据体就有 96 个深度切片,每个尺寸为 [ 55 × 55 ] [55 \times 55] [55×55] 。在每个深度切片上的神经元都使用同样的权重和偏置项。

在这样的参数共享下,例子中的第一个卷积层就只有 96 个不同的参数集了,一个参数集对应一个深度切片,共有 96 × ( 11 × 11 × 3 + 1 ) = 34 , 944 96 \times (11 \times 11 \times 3+1)=34,944 96×(11×11×3+1)=34,944 个不同的参数(包括偏置项)。

在每个深度切片中的 55 × 55 55 \times 55 55×55 个权重使用的都是同样的参数。

在反向传播的时候,需要计算每个神经元对它的权重的梯度,所以需要把同一个深度切片上的所有神经元对权重的梯度进行累加,这样就得到了对这个共享权重的梯度。这样,每个切片只更新一个权重集

补充解释:正是因为参数共享,卷积层的前向传播在每个深度切片中可以看做是在计算神经元权重和输入数据体的卷积(这就是「卷积层」名字由来)。这也是为什么总是将这些权重集合称为滤波器(filter) (或卷积核(kernel) ),因为它们和输入进行了卷积。

有时候参数共享假设可能没有意义,特别是当卷积神经网络的输入图像是一些明确的中心结构时候。这时候我们就应该期望在图片的不同位置学习到完全不同的特征。一个具体的例子就是输入图像是人脸,人脸一般都处于图片中心。你可能期望不同的特征,比如眼睛特征或者头发特征可能(也应该)会在图片的不同位置被学习。在这个例子中,通常就放松参数共享的限制,将层称为局部连接层(Locally-Connected Layer)。

5) 卷积层演示

下面是一个卷积层的运行演示。因为 3D 数据难以可视化,所以所有的数据(输入数据体是蓝色,权重数据体是红色,输出数据体是绿色)都进行深度切片然后排成一列来展现。

  • 输入数据体的尺寸是 W 1 = 5 W_1 = 5 W1​=5, H 1 = 5 H_1 = 5 H1​=5, D 1 = 3 D_1 = 3 D1​=3 。
  • 卷积层的参数是 K = 2 K = 2 K=2, F = 3 F = 3 F=3, S = 2 S = 2 S=2, P = 1 P = 1 P=1 。也就是说,有 2 个滤波器,滤波器的尺寸是 3 × 3 3 \times 3 3×3 ,步长是 2。
  • 因此,输出数据体的空间尺寸是 ( 5 − 3 + 2 ) / 2 + 1 = 3 (5-3+2)/2+1=3 (5−3+2)/2+1=3。

注意输入数据体使用了零填充 P = 1 P=1 P=1,所以输入数据体外边缘一圈都是 0 0 0。下面的例子在绿色的输出激活图上循环演示,展示了其中每个元素都是蓝色的输入数据和红色的滤波器逐元素相乘,然后求其总和,最后加上偏置项得来。高清版展示,建议访问 课程官网

卷积神经网络; 卷积层 - 运行演示

6) 用矩阵乘法实现卷积

卷积运算本质上就是在滤波器和输入数据的局部区域做点积。卷积层的常用实现方式就是利用这一点,将卷积层的前向传播变成一个巨大的矩阵乘法:

  • 输入图像的局部区域被 im2col 操作拉伸为列。比如,如果输入是 [ 227 × 227 × 3 ] [227 \times 227 \times 3] [227×227×3] ,要与尺寸为 11 × 11 × 3 11 \times 11 \times 3 11×11×3 的滤波器以步长为 4 4 4 进行卷积,就取输入中的 [ 11 × 11 × 3 ] [11 \times 11 \times 3] [11×11×3] 数据块,然后将其拉伸为长度为 11 × 11 × 3 = 363 11 \times 11 \times 3=363 11×11×3=363 的列向量。重复进行这一过程,因为步长为 4 4 4,所以输出的宽高为 ( 227 − 11 ) / 4 + 1 = 55 (227-11)/4+1=55 (227−11)/4+1=55,即需要 55 \times 55=3025 个这样的列向量与滤波器做点积。所以输入数据 X X X 经过 im2col 操作后的输出矩阵 X_col 的尺寸是 [ 363 × 3025 ] [363 \times 3025] [363×3025] ,其中每列是 X X X 上拉伸的感受野,共有 55 × 55 = 3 , 025 55 \times 55=3,025 55×55=3,025 个。注意因为感受野之间有重叠,所以输入数据体中的数字在不同的列中可能有重复。

  • 卷积层的权重也同样被拉伸成行。举例,如果有 96 个尺寸为 [ 11 × 11 × 3 ] [11 \times 11 \times 3] [11×11×3] 的滤波器,就生成一个矩阵 W_row,尺寸为 [ 96 × 363 ] [96 \times 363] [96×363] 。

  • 现在卷积的结果和进行一个大矩阵乘法 np.dot(W_row, X_col) 是等价的了,能得到每个滤波器和每个感受野间的点积。在这个例子中,这个操作的输出是 [ 96 × 3025 ] [96 \times 3025] [96×3025] ,给出了每个滤波器在每个位置的点积输出。

  • 结果最后必须被重新变为合理的输出尺寸 [ 55 × 55 × 96 ] [55 \times 55 \times 96] [55×55×96] 。

这个方法的缺点就是占用内存太多,因为在输入数据体中的某些值在 X_col 中被复制了多次。但是,其优点是有非常多高效的矩阵乘法实现方式供我们可以使用,比如常用的 BLAS API。同样,im2col思路可以用在汇聚操作中。

反向传播:卷积操作的反向传播(同时对于数据和权重)还是一个卷积(但是是在空间上翻转的滤波器)。使用一个 1 维的例子比较容易演示(这里不再展开)。

7) 其它卷积方式

① 1x1 卷积

一些网络结构中会使用 1 × 1 1 \times 1 1×1 的卷积,这个方法最早是在论文 Network in Network 中出现。在后来的很多模型结构中,使用它主要是起到升降维的作用。

卷积神经网络; 卷积层 - 1x1 卷积 升降维

② 扩张卷积

大家也会看到扩张卷积(空洞卷积)这样的特殊结构。我们之前看过的卷积层滤波器是连续的,但让滤波器中元素之间有间隙也是合理的设计,这就叫做扩张。这种特殊的卷积可以帮助 CNN 有效扩大感受野。

如下图为普通卷积和空洞卷积的动图对比:

卷积神经网络; 卷积层 - 扩张卷积 空洞卷积

4.2 池化层(Pooling Layer,POOL Layer)

1) 概述

通常,在连续的卷积层之间会周期性地插入一个池化层。它的作用是逐渐降低数据体的空间(宽、高)尺寸,这样的话就能减少网络中参数的数量,使得计算资源耗费变少,也能有效控制过拟合。

池化层最常用的是 MAX 操作,对输入数据体的每一个深度切片独立进行操作,改变它的空间尺寸。最常见的形式是使用尺寸 2 × 2 2 \times 2 2×2 的滤波器,以步长为 2 2 2 来对每个深度切片进行降采样,将其中 B 75 % 75% 75% 的激活信息都丢掉。每个 MAX 操作是从 4 4 4 个数字中取最大值(也就是在深度切片中某个 2 × 2 2 \times 2 2×2 的区域)。深度方向保持不变,不进行降采样。

池化层也不用零填充,并且池化滤波器间一般没有重叠,步长等于滤波器尺寸。

2) 池化层的性质

  • 输入数据体尺寸: W 1 × H 1 × D 1 W_1 \times H_1 \times D_1 W1​×H1​×D1​

  • 有两个超参数:池化尺寸 F F F ,一般为 2 2 2、 3 3 3;步长 S S S ,一般为 2 2 2。实际上 m a x max max 池化层一般只有两种超参数设置方式: F = 3 F = 3 F=3, S = 2 S = 2 S=2 ,叫做重叠汇聚(overlapping pooling);另一种更常用的是 F = 2 F = 2 F=2, S = 2 S = 2 S=2

  • 输出数据体尺寸: W 2 × H 2 × D 2 W_2 \times H_2 \times D_2 W2​×H2​×D2​ ,其中:

    • W 2 = ( W 1 − F ) / S + 1 W_2 = (W_1 - F)/S + 1 W2​=(W1​−F)/S+1
    • H 2 = ( H 1 − F ) / S + 1 H_2 = (H_1 - F)/S + 1 H2​=(H1​−F)/S+1
    • D 2 = D 1 D_2 = D_1 D2​=D1​
  • 因为对输入进行的是固定函数计算,所以没有引入参数。此外,在池化层中很少使用零填充。

3) 池化方式

除了最大池化,池化单元还可以使用其他的函数,比如平均池化(average pooling)或 L2 范式池化(L2-norm pooling)。平均池化历史上比较常用,但是现在已经很少使用了。

卷积神经网络; 池化层 - CNN 池化层示意图

池化层在输入数据体的每个深度切片上,独立地对其进行空间上(高度、宽度)的降采样。

  • 图片左边:本例中,输入数据体尺寸 [ 224 × 224 × 64 ] [224 \times 224 \times 64] [224×224×64] 被降采样到了 [ 112 × 112 × 64 ] [112 \times 112 \times 64] [112×112×64] ,采取的滤波器尺寸是 2 2 2,步长为 2 2 2,而深度不变。
  • 图片右边:最常用的降采样操作是取最大值,也就是最大池化,这里步长为 2 2 2,每个取最大值操作是从 4 4 4 个数字中选取(即 2 × 2 2 \times 2 2×2 的方块区域中)。

反向传播

  • m a x ( x , y ) max(x,y) max(x,y) 函数的反向传播可以简单理解为将梯度只沿最大的数回传。
  • 在前向传播经过池化层的时候,通常会把池中最大元素的索引记录下来(有时这个也叫作道岔switches),这样在反向传播的时候梯度路由就很高效。

一些争议

  • 很多人认为可以不使用池化层。比如在 Striving for Simplicity: The All Convolutional Net 一文中,提出使用一种只有重复的卷积层组成的结构,不再使用池化层,通过在卷积层中使用更大的步长来降低数据体的尺寸。
  • 有发现认为,在训练一个良好的生成模型时,弃用池化层也是很重要的。比如变化自编码器(VAEs:variational autoencoders)和生成性对抗网络(GANs:generative adversarial networks)。现在看来,未来的卷积网络结构中,可能会很少使用甚至不使用池化层。

4.3 归一化层(Normalization Layer)

在卷积神经网络的结构中,提出了一些归一化层的概念,想法是为了实现在生物大脑中观测到的抑制机制。但是这些层渐渐都不再流行,因为实践证明它们的效果即使存在,也是极其有限的。

对于不同类型的归一化层,可以看看 nAlex Krizhevskyn 的关于 cuda-convnet library API 的讨论。

4.4 全连接层(Fully-connected Layer,FC Layer)

全连接层,顾名思义,神经元对于前一层中的所有激活数据是全连接的,这个和常规神经网络中一样,通常会把前一层数组拉成一个向量,与 W W W 的每个行向量进行点积,得到每一类的分数。

最后一个池化层输出的结果是数据经过整个网络累计得到的,前几个卷积层可能检测一些比较简单的特征比如边缘,得到边缘图后输入到下一个卷积层,然后进行更复杂的检测,这样层层下来,最后一层的结果可以看成是一组符合模板的激活情况,比较大的值表明之前的所有检测结果都比较大,激活程度高,这样就汇聚了大量的信息。

虽然输出的数据比较简单,但却是非常复杂的滤波器(或特征)激活后的情况,特征在卷积核中体现。

  • 第一层卷积网络输出的结果比较复杂,因为第一层的卷积核比较简单,很容易就激活了;
  • 最后一层的卷积核非常复杂,所以输出的激活图看起来就会很简单,因为激活比较困难。但是这个激活图却能说明复杂特征的激活程度,用来评分是非常合理的。

卷积神经网络; 池化层 - CNN 尾部全连接层

1) 全连接层转化为卷积层

全连接层和卷积层之间唯一的不同就是卷积层中的神经元只与输入数据中的一个局部区域连接,并且在同一个深度切片上的神经元共享参数。然而在两类层中,神经元都是计算点积,所以它们的函数形式是一样的。因此,将此两者相互转化是可能的:

① 对于任一个卷积层,都存在一个能实现和它一样的前向传播函数的全连接层

  • 权重矩阵是一个巨大的矩阵,除了某些特定块(这是因为有局部连接),其余部分都是零。而在其中大部分块中,元素都是相等的(因为参数共享)。

② 反过来,任何全连接层都可以被转化为卷积层

  • 比如,一个 K = 4096 K = 4096 K=4096(即有 4096 4096 4096 个类别, W W W 有 4096 4096 4096 个列向量)的全连接层,输入数据体的尺寸是 7 × 7 × 512 7 \times 7 \times 512 7×7×512 ,那么 W 的每个列向量长度为 7 × 7 × 512 7 \times 7 \times 512 7×7×512,全连接之后的输出为 1 × 4096 1 \times 4096 1×4096。
  • 这个全连接层可以被等效地看做一个 F = 7 F=7 F=7, P = 0 P=0 P=0, S = 1 S=1 S=1, K = 4096 K=4096 K=4096 的卷积层。换句话说,就是将滤波器的尺寸设置为和输入数据体的尺寸一致也是 7 × 7 × 512 7 \times 7 \times 512 7×7×512,这样两者卷积的结果就是一个实数。又因为有 4096 4096 4096 个滤波器,所以输出将变成 1 × 1 × 4096 1 \times 1 \times 4096 1×1×4096,这个结果就和使用初始的那个全连接层一样了。

两种转换的示意图如下图所示:

卷积神经网络; 全连接层 - 与卷积层的转换

上述两种转换中,全连接层转化为卷积层在实际运用中更加有用。

假设一个卷积神经网络的输入是 224 × 224 × 3 224 \times 224 \times 3 224×224×3 的图像,一系列的卷积层和池化层将图像数据变为尺寸为 7 × 7 × 512 7 \times 7 \times 512 7×7×512 的激活数据体(在 AlexNet 中就是这样,通过使用 5 个池化层来对输入数据进行空间上的降采样,每次尺寸下降一半,所以最终空间尺寸为 224/2/2/2/2/2=7)。

全连接层中,AlexNet 先使用了两个尺寸为 4096 4096 4096 的全连接层,然后又使用了一个有 1000 个神经元的全连接层用于计算分类评分。

我们可以将这 3 个全连接层中的任意一个转化为卷积层:

  • 针对第一个连接区域是 [ 7 × 7 × 512 ] [7 \times 7 \times 512] [7×7×512] 的全连接层,令其滤波器尺寸为 7 × 7 × 512 7 \times 7 \times 512 7×7×512, K = 4096 K=4096 K=4096,这样输出数据体就为 [ 1 × 1 × 4096 ] [1 \times 1 \times 4096] [1×1×4096] 了;
  • 针对第二个全连接层,令其滤波器尺寸为 1 × 1 × 4096 1 \times 1 \times 4096 1×1×4096, K = 4096 K=4096 K=4096,这样输出数据体仍为 [ 1 × 1 × 4096 ] [1 \times 1 \times 4096] [1×1×4096] ;
  • 对最后一个全连接层也做类似的,令其滤波器尺寸为 1 × 1 × 4096 1 \times 1 \times 4096 1×1×4096, K = 1000 K=1000 K=1000,最终输出为[1 \times 1 \times 1000]$ 。

卷积神经网络; 全连接层 - 与卷积层的转换

我们注意到,每次类似的变换,都需要把全连接层的权重 W W W 重塑成卷积层中和输入数据尺寸相同的滤波器。这个转化最大的意义是让一些计算更高效:

  • 让卷积网络在一张更大的输入图片上滑动(即把一张更大的图片的不同区域都分别带入到卷积网络,得到每个区域的得分),得到多个输出,这样的转化可以让我们在单个前向传播的过程中完成上述的操作。

我们来看看这个例子:

  • 将 224 × 224 × 3 224 \times 224 \times 3 224×224×3 的图片经过卷积网络(不包括最后三个全连接层)后得到 7 × 7 × 512 7 \times 7 \times 512 7×7×512 的激活数据体(降采样 5 次,除 32)。然后经过第一个全连接层,该全连接层的神经元需要 7 × 7 × 512 7 \times 7 \times 512 7×7×512 个参数。

  • 如果换成一张 384 × 384 384 \times 384 384×384 的大图片经过同样的网络(不包括最后三个全连接层)等效输出尺寸为 12 × 12 × 512 12 \times 12 \times 512 12×12×512( 384 / 32 = 12 384/32 = 12 384/32=12),如果直接用来通过全连接层,由于尺寸不同,会无法通过。

    • 这时就需要把 384 × 384 384 \times 384 384×384 的图片切成 6 × 6 6 \times 6 6×6 个 224 × 224 224 \times 224 224×224 的小图像依次通过卷积网络,这样全连接层之前的输出为 36 个 7 × 7 × 512 7 \times 7 \times 512 7×7×512 的激活数据体,远远大于 12 × 12 × 512 12 \times 12 \times 512 12×12×512,所以由于全连接层的存在,导致大量的重复运算。
    • 但是如果将 3 个全连接层转化来的 3 个卷积层,就不会存在尺寸的问题, 384 × 384 384 \times 384 384×384 的图片可以直接通过转化后的卷积网络,最终得到 6 × 6 × 1000 6 \times 6 \times 1000 6×6×1000 的输出(因为 ( 12 − 7 ) / 1 + 1 = 6 (12 - 7)/1 + 1 = 6 (12−7)/1+1=6 或 ( 384 − 224 ) / 32 + 1 = 6 (384-224)/32+1 = 6 (384−224)/32+1=6)。这样我们可以在 384 × 384 384 \times 384 384×384 图像上一次得到 6 × 6 6 \times 6 6×6 个分类得分数组,而不是独立的得到 36 个大小为 [ 1 × 1 × 1000 ] [1 \times 1 \times 1000] [1×1×1000] 的得分数组,大大节省计算量。

5.卷积神经网络层的排列与尺寸设置

5.1 层的排列规律

卷积神经网络通常是由三种层构成:卷积层,池化层和全连接层(简称 FC)。ReLU 激活函数也应该算是一层,它逐元素地进行激活函数操作。

卷积神经网络最常见的形式就是将一些卷积层和 ReLU 层放在一起,其后紧跟池化层,然后重复如此直到图像在空间上被缩小到一个足够小的尺寸,在某个地方过渡成成全连接层也较为常见。最后的全连接层得到输出,比如分类评分等。

换句话说,最常见的卷积神经网络结构如下:

INPUT → [[CONV → RELU]*N → POOL?]*M → [FC → RELU]*K → FC 

其中 * 指的是重复次数,POOL? 指的是一个可选的池化层。其中 N > = 0 N >=0 N>=0(通常 N < = 3 N<=3 N<=3), M > = 0 M>=0 M>=0, K > = 0 K>=0 K>=0(通常 K < 3 K<3 K<3)。

例如,下面是一些常见的网络结构规律:

  • INPUT → FC,实现一个线性分类器,此处 N = M = K = 0 N = M = K = 0 N=M=K=0;

  • INPUT → CONV → RELU → FC;

  • INPUT → [CONV → RELU → POOL]*2 → FC → RELU → FC,此时在每个池化层前只有一个卷积层;

  • INPUT → [CONV → RELU → CONV → RELU → POOL]3 → [FC → RELU]2 → FC,此时每个池化层前有两个卷积层,这个思路适用于更大更深的网络,因为在执行具有破坏性的池化操作前,多重的卷积层可以从输入数据中学习到更多的复杂特征。

经验几个小滤波器卷积层的组合比一个大滤波器卷积层好

假设你一层一层地重叠了 3 3 3 个 3 × 3 3 \times 3 3×3 的卷积层(层与层之间有非线性激活函数)。

  • 第一个卷积层中的每个神经元都对输入数据体有一个 3 × 3 3 \times 3 3×3 的感受野
  • 第二个卷积层上的神经元对第一个卷积层有一个 3 × 3 3 \times 3 3×3 的感受野,也就是对输入数据体有 5 × 5 5 \times 5 5×5 的感受野(32-30-28)。
  • 在第三个卷积层上的神经元对第二个卷积层有 3 × 3 3 \times 3 3×3 的感受野,也就是对输入数据体有 7 × 7 7 \times 7 7×7 的感受野。

下图是第 1 层和第 2 层卷积层的堆叠感受野示意图

卷积神经网络; 全连接层 - 卷积层的堆叠感受野

假设不采用这 3 个 3 × 3 3 \times 3 3×3 的卷积层,而是使用一个单独的有 7 × 7 7 \times 7 7×7 的感受野的卷积层,那么所有神经元的感受野也是 7 × 7 7 \times 7 7×7,但是就有一些缺点:

  • ① 多个卷积层与非线性的激活层交替的结构,比单一卷积层的结构更能提取出深层的更好的特征。
  • ② 假设所有的数据有 C C C 个通道,即输入输出数据深度均为 C C C,那么单独的 7 × 7 7 \times 7 7×7 卷积层将会包含 C × ( 7 × 7 × C ) = 49 C 2 C \times (7 \times 7 \times C) = 49 C² C×(7×7×C)=49C2 个参数,而 3 个 3 × 3 3 \times 3 3×3 的卷积层的组合仅有 3 × ( C × ( 3 × 3 × C ) ) = 27 C 2 3 \times (C \times (3 \times 3 \times C)) = 27 C² 3×(C×(3×3×C))=27C2 个参数。

直观说来,最好选择带有小滤波器的卷积层组合,而不是用一个带有大的滤波器的卷积层。前者可以表达出输入数据中更多个强力特征,使用的参数也更少。

唯一的不足是,在进行反向传播时,中间的卷积层可能会导致占用更多的内存。

5.2 层的尺寸设置规律

1) 输入层

原始输入图像,经常设置为 2 N 2^N 2N 形式。常用数字包括 32(比如 CIFAR-10),64,96(比如 STL-10)或 224(比如 ImageNet 卷积神经网络)、384 和 512。

2) 卷积层

  • 应该使用小尺寸滤波器(比如 3 × 3 3 \times 3 3×3 或最多 5 × 5 5 \times 5 5×5),使用步长 S = 1 S=1 S=1。
  • 要对输入数据进行零填充,这样卷积层就不会改变输入数据在空间维度上的尺寸。比如
    • 当 F = 3 F=3 F=3,那就使用 P = 1 P=1 P=1 来保持输入尺寸;
    • 当 F = 5 F=5 F=5,那就使用 P = 2 P=2 P=2 来保持输入尺寸。
    • 一般对于任意 F F F,当 P = ( F − 1 ) / 2 P=(F-1)/2 P=(F−1)/2 的时候能保持输入尺寸。
  • 如果必须使用更大的滤波器尺寸(比如 7 × 7 7 \times 7 7×7 之类),通常只用在第一个输入原始图像的卷积层上。

3) 池化层

  • 负责对输入数据的空间维度进行降采样。
  • 最常用的设置是用用 2 × 2 2 \times 2 2×2 感受野(即 F = 2 F=2 F=2 )的最大值汇聚,步长为 2 2 2( S = 2 S=2 S=2)。注意这一操作将会把输入数据中 75% 的激活数据丢弃(因为对宽度和高度都进行了 2 的降采样)。
  • 另一个不那么常用的设置是使用 3 × 3 3 \times 3 3×3 的感受野,步长为 2。最大值汇聚的感受野尺寸很少有超过 3 的,因为汇聚操作过于激烈,易造成数据信息丢失,这通常会导致算法性能变差。

在某些案例(尤其是早期的卷积神经网络结构)中,基于前面的各种规则,内存的使用量迅速飙升。

  • 例如,使用 64 个尺寸为 3 × 3 3 \times 3 3×3 的滤波器对 224 × 224 × 3 224 \times 224 \times 3 224×224×3 的图像进行卷积,零填充为 1,得到的激活数据体尺寸是 [ 224 × 224 × 64 ] [224 \times 224 \times 64] [224×224×64] 。这个数量就是一千万的激活数据,或者就是 72MB 的内存(每张图就是这么多,激活函数和梯度都是)。

因为 GPU 通常因为内存导致性能瓶颈,所以做出一些妥协是必须的。在实践中,人们倾向于在网络的第一个卷积层做出妥协。

  • 例如,可以妥协可能是在第一个卷积层使用步长为 2 2 2,尺寸为 7 × 7 7 \times 7 7×7 的滤波器(比如在 ZFnet 中)。在 AlexNet 中,滤波器的尺寸的 11 × 11 11 \times 11 11×11,步长为 4。

6.卷积神经网络经典案例

这些网络的详细结构会在后续再展开介绍。

关于详细的下述网络结构讲解也可以阅读ShowMeAI的的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章经典 CNN 网络实例详解

6.1 LeNet

第一个成功的卷积神经网络应用,是 Yann LeCun 在上世纪 90 年代实现的。当然,最著名还是被应用在识别数字和邮政编码等的 LeNet 结构。

6.2 AlexNet

AlexNet 卷积神经网络由 Alex Krizhevsky,Ilya Sutskever 和 Geoff Hinton 实现。AlexNet 在 2012 年的 ImageNet ILSVRC 竞赛 中夺冠,性能远远超出第二名(16%的 top5 错误率,第二名是 26% 的 top5 错误率)。这个网络的结构和 LeNet 非常类似,但是更深更大,并且使用了层叠的卷积层来获取特征。

6.3 ZF Net

Matthew Zeiler 和 Rob Fergus 发明的网络在 2013 年 ILSVRC 比赛中夺冠,它被称为 ZFNet(Zeiler & Fergus Net 的简称)。它通过修改结构中的超参数来实现对 AlexNet 的改良,具体说来就是增加了中间卷积层的尺寸,让第一层的步长和滤波器尺寸更小。

6.4 GoogLeNet

2014 年 ILSVRC 的胜利者是谷歌的 Szeged 等 实现的卷积神经网络。它主要的贡献就是实现了一个奠基模块,它能够显著地减少网络中参数的数量(AlexNet 中有 60M,该网络中只有 4M)。以及,GoogLeNet 没有在卷积神经网络的最后使用全连接层,而是使用了一个平均池化,把大量不是很重要的参数都去除掉了。GooLeNet 还有几种改进的版本,最新的一个是 Inception-v4

6.5 VGGNet

VGGNet 是 Karen Simonyan 和 Andrew Zisserman 实现的卷积神经网络,在 2014 年 ILSVRC 取得第二名的成绩。它主要的贡献是展示出网络的深度是算法优良性能的关键部分。他们最好的网络包含了 16 个卷积/全连接层。网络的结构非常一致,从头到尾全部使用的是 3 × 3 3 \times 3 3×3 的卷积和 2 × 2 2 \times 2 2×2 的池化。

6.6 ResNet

残差网络(Residual Network)是 2015 年 ILSVRC 的胜利者,由何恺明等实现。它使用了特殊的跳跃链接,大量使用了 批量归一化(batch normalization)。这个结构同样在最后没有使用全连接层。

6.7 计算上的考量

在构建卷积神经网络结构时,最大的瓶颈是内存瓶颈。大部分现代 GPU 的内存都不太大。要注意三种内存占用来源:

1) 来自中间数据体尺寸

卷积神经网络中的每一层中都有激活数据体的原始数值,以及损失函数对它们的梯度(和激活数据体尺寸一致)。通常,大部分激活数据都是在网络中靠前的层中(比如第一个卷积层)。

  • 在训练时,这些数据需要放在内存中,因为反向传播的时候还会用到。
  • 在测试时可以优化:让网络在测试运行时候每层都只存储当前的激活数据,然后丢弃前面层的激活数据,这样就能减少巨大的激活数据量。

2) 来自参数尺寸

  • 即整个网络的参数的数量、反向传播时它们的梯度值,以及使用 momentum、Adagrad 或 RMSProp 等方法进行最优化时的每一步计算缓存。
  • 因此,存储参数向量的内存通常需要在参数向量的容量基础上乘以 3 或者更多。

3) 卷积神经网络实现还有各种零散的内存占用,比如成批的训练数据,扩充的数据等

一旦对于所有这些数值的数量有了一个大略估计(包含激活数据,梯度和各种杂项),把这个值乘以 4,得到原始的字节数(因为每个浮点数占用 4 个字节,如果是双精度浮点数那就是占用 8 个字节),然后多次除以 1024 分别得到占用内存的 KB、MB,最后是 GB 计量。如果你的网络有内存问题,一个常用的方法是降低批尺寸(batch size),因为绝大多数的内存都是被激活数据消耗掉了。

6.8 拓展参考

ConvNetJS CIFAR-10 demo 可以在服务器上实时地调试卷积神经网络的结构,观察计算结果。

7.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=5

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

8.要点总结

  • 卷积神经网络的历史
  • 卷积神经网络与常规神经网络的对比;
  • CNN 卷积层、池化层、ReLU 层、全连接层;
  • CNN 局部连接、参数共享、最大池化、步长、零填充 、数据体尺寸
  • CNN 层的规律与尺寸设置
  • CNN 经典案例

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(6) | 神经网络训练技巧 (上)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125025041

ShowMeAI 研究中心


深度学习与计算机视觉

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

通过 ShowMeAI 前序文章 深度学习与 CV 教程(3) | 损失函数与最优化深度学习与 CV 教程(4) | 神经网络与反向传播深度学习与 CV 教程(5) | 卷积神经网络 我们已经学习掌握了以下内容:

  • 计算图:计算前向传播、反向传播
  • 神经网络:神经网络的层结构、非线性函数、损失函数
  • 优化策略:梯度下降使损失最小
  • 批梯度下降:小批量梯度下降,每次迭代只用训练数据中的一个小批量计算损失和梯度
  • 卷积神经网络:多个滤波器与原图像独立卷积得到多个独立的激活图

【本篇】【下篇】 ShowMeAI 讲解训练神经网络的核心方法与关键点,主要包括:

  • 初始化:激活函数选择、数据预处理、权重初始化、正则化、梯度检查
  • 训练动态:监控学习过程、参数更新、超参数优化
  • 模型评估:模型集成(model ensembles)

本篇重点

  • 激活函数
  • 数据预处理
  • 权重初始化
  • 批量归一化
  • 监控学习过程
  • 超参数调优

1.激活函数

关于激活函数的详细知识也可以参考阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 浅层神经网络 里【激活函数】板块内容。

在全连接层或者卷积层,输入数据与权重相乘后累加的结果送给一个非线性函数,即激活函数(activation function)。每个激活函数的输入都是一个数字,然后对其进行某种固定的数学操作。

激活函数 ; 神经元数学模型

下面是在实践中可能遇到的几种激活函数:

激活函数; 常见的激活函数

1.1 Sigmoid 函数

激活函数; Sigmoid 函数

数学公式: σ ( x ) = 1 / ( 1 + e − x ) \sigma(x) = 1 / (1 + e^{-x}) σ(x)=1/(1+e−x)

求导公式: d σ ( x ) d x = ( 1 − σ ( x ) ) σ ( x ) \frac{d\sigma(x)}{dx} = \left( 1 - \sigma(x) \right) \sigma(x) dxdσ(x)​=(1−σ(x))σ(x) (不小于 0 0 0 )

特点:把输入值「挤压」到 0 0 0 到 1 1 1 范围内。Sigmoid 函数把输入的实数值「挤压」到 0 0 0 到 1 1 1 范围内,很大的负数变成 0 0 0,很大的正数变成 1 1 1,在历史神经网络中,Sigmoid 函数很常用,因为它对神经元的激活频率有良好的解释:从完全不激活( 0 0 0)到假定最大频率处的完全饱和(saturated)的激活( 1 1 1)

然而现在 Sigmoid 函数已经很少使用了,因为它有三个主要缺点:

缺点①:Sigmoid 函数饱和时使梯度消失

  • 当神经元的激活在接近 0 0 0 或 1 1 1 处时(即门单元的输入过或过大时)会饱和:在这些区域,梯度几乎为 0 0 0。
  • 在反向传播的时候,这个局部梯度要与损失函数关于这个门单元输出的梯度相乘。因此,如果局部梯度非常小,那么相乘的结果也会接近零,这会「杀死」梯度,几乎就有没有信号通过神经元传到权重再到数据了。
  • 还有,为了防止饱和,必须对于权重矩阵初始化特别留意。比如,如果初始权重过大,那么大多数神经元将会饱和,导致网络就几乎不学习了。

缺点②:Sigmoid 函数的输出不是零中心的

  • 这个性质会导致神经网络后面层中的神经元得到的数据不是零中心的。
  • 这一情况将影响梯度下降的运作,因为如果输入神经元的数据总是正数(比如在 σ ( ∑ i w i x i + b ) \sigma(\sum_{i}w_ix_i+b) σ(∑i​wi​xi​+b) )中每个输入 x x x 都有 x > 0 x > 0 x>0),那么关于 w w w 的梯度在反向传播的过程中,将会要么全部是正数,要么全部是负数(根据该 Sigmoid 门单元的回传梯度来定,回传梯度可正可负,而 d σ d W = X T ⋅ σ ′ \frac{d\sigma}{dW}=X^T \cdot\sigma' dWdσ​=XT⋅σ′ 在 X X X 为正时恒为非负数)。
  • 这将会导致梯度下降权重更新时出现 z z z 字型的下降。该问题相对于上面的神经元饱和问题来说只是个小麻烦,没有那么严重。

缺点③: 指数型计算量比较大

1.2 tanh 函数

激活函数; tanh 函数

数学公式: tanh ⁡ ( x ) = 2 σ ( 2 x ) − 1 \tanh(x) = 2 \sigma(2x) -1 tanh(x)=2σ(2x)−1

特点:将实数值压缩到 [ − 1 , 1 ] [-1,1] [−1,1] 之间

和 S i g m o i d Sigmoid Sigmoid 神经元一样,它也存在饱和问题,但是和 S i g m o i d Sigmoid Sigmoid 神经元不同的是,它的输出是零中心的。因此,在实际操作中, t a n h tanh tanh 非线性函数比 S i g m o i d Sigmoid Sigmoid 非线性函数更受欢迎。注意 t a n h tanh tanh 神经元是一个简单放大的 S i g m o i d Sigmoid Sigmoid 神经元。

1.3 ReLU 函数

激活函数; ReLU 函数

数学公式: f ( x ) = max ⁡ ( 0 , x ) f(x) = \max(0, x) f(x)=max(0,x)

特点:一个关于 0 0 0 的阈值

优点

  • ReLU 只有负半轴会饱和;节省计算资源,不含指数运算,只对一个矩阵进行阈值计算;更符合生物学观念;加速随机梯度下降的收敛。
  • Krizhevsky 论文指出比 Sigmoid 和 tanh 函数快 6 倍之多,据称这是由它的线性,非饱和的公式导致的。

缺点

  • 仍有一半会饱和;非零中心;
  • 训练时,ReLU 单元比较脆弱并且可能「死掉」。
    • 举例来说,当一个很大的梯度流过 ReLU 的神经元的时候,由于梯度下降,可能会导致权重更新到一种特别的状态(比如大多数的 w w w 都小于 0 0 0 ),在这种状态下神经元将无法被其他任何数据点再次激活。如果这种情况发生,那么从此所有流过这个神经元的梯度将都变成 0 0 0,也就是说,这个 ReLU 单元在训练中将不可逆转的死亡,因为这导致了数据多样化的丢失。
    • 例如,如果学习率设置得太高(本来大多数大于 0 0 0 的 w w w 更新后都小于 0 0 0 了),可能会发现网络中 40%的神经元都会死掉(在整个训练集中这些神经元都不会被激活)。
    • 通过合理设置学习率,这种情况的发生概率会降低。

1.4 Leaky ReLU

激活函数; Leaky ReLU 函数

公式: f ( x ) = 1 ( x < 0 ) ( α x ) + 1 ( x > = 0 ) ( x ) f(x) = \mathbb{1}(x < 0) (\alpha x) + \mathbb{1}(x>=0) (x) f(x)=1(x<0)(αx)+1(x>=0)(x), α \alpha α 是小常量

特点:解决「 ReLU 死亡」问题, x < 0 x<0 x<0 时给出一个很小的梯度值,比如 0.01 0.01 0.01。

Leaky ReLU 修正了 x < 0 x<0 x<0 时 ReLU 的问题,有研究指出这个激活函数表现很不错,但是其效果并不是很稳定。Kaiming He 等人在 2015 年发布的论文 Delving Deep into Rectifiers 中介绍了一种新方法 PReLU,把负区间上的斜率当做每个神经元中的一个参数,然而无法确定该激活函数在不同任务中均有益处。

1.5 指数线性单元(Exponential Linear Units,ELU)

激活函数; 指定线性单元 ELU 函数

公式: f ( x ) = { x i f    x > 0 α ( e x p ( x ) − 1 ) o t h e r w i s e f(x)=\begin{cases} x & if \space\space x>0 \ \alpha(exp(x)-1) & otherwise \end{cases} f(x)={xα(exp(x)−1)​if  x>0otherwise​

特点:介于 ReLU 和 Leaky ReLU 之间

具有 ReLU 的所有优点,但是不包括计算量;介于 ReLU 和 Leaky ReLU 之间,有负饱和的问题,但是对噪声有较强的鲁棒性。

1.6 Maxout

max ⁡ ( w 1 T x + b 1 , w 2 T x + b 2 ) \max \left(w_{1}^{T} x+b_{1}, w_{2}^{T} x+b_{2}\right) max(w1T​x+b1​,w2T​x+b2​)

公式: m a x ( w 1 T x + b 1 , w 2 T x + b 2 ) max(w_1^Tx+b_1, w_2^Tx + b_2) max(w1T​x+b1​,w2T​x+b2​)

特点:是对 ReLU 和 leaky ReLU 的一般化归纳

对于权重和数据的内积结果不再使用非线性函数,直接比较两个线性函数。ReLU 和 Leaky ReLU 都是这个公式的特殊情况,比如 ReLU 就是当 w 1 = 1 w_1=1 w1​=1, b 1 = 0 b_1=0 b1​=0 的时候。

Maxout 拥有 ReLU 单元的所有优点(线性操作和不饱和),而没有它的缺点(死亡的 ReLU 单元)。然而和 ReLU 对比,它每个神经元的参数数量增加了一倍,这就导致整体参数量激增。

实际应用 Tips

  • 用 ReLU 函数。注意设置好学习率,你可以监控你的网络中死亡的神经元占的比例。
  • 如果单元死亡问题困扰你,就试试 Leaky ReLU 或者 Maxout,不要再用 Sigmoid 了。也可以试试 tanh,但是其效果应该不如 ReLU 或者 Maxout。

2.数据预处理

关于深度学习数据预处理的知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面里【标准化输入】板块内容。

关于数据预处理有 3 个常用的符号,数据矩阵 X X X,假设其尺寸是 [ N × D ] [N \times D] [N×D]( N N N 是数据样本的数量, D D D 是数据的维度)。

2.1 减均值(Mean Subtraction)

减均值法是数据预处理最常用的形式。它对数据中每个独立特征减去平均值,在每个维度上都将数据的中心都迁移到原点。

数据预处理; 减均值

在 numpy 中,该操作可以通过代码 X -= np.mean(X, axis=0) 实现。而对于图像,更常用的是对所有像素都减去一个值,可以用 X -= np.mean(X) 实现,也可以在 3 个颜色通道上分别操作。

具体来讲,假如训练数据是 50000 50000 50000 张 32 × 32 × 3 32 \times 32 \times 3 32×32×3 的图片:

  • 第一种做法是减去均值图像,即将每张图片拉成长为 3072 3072 3072 的向量, 50000 × 3072 50000 \times 3072 50000×3072 的矩阵按列求平均,得到一个含有 3072 3072 3072 个数的均值图像,训练集测试集验证集都要减去这个均值,AlexNet 是这种方式;
  • 第二种做法是按照通道求平均,RGB 三个通道每个通道一个均值,即每张图片的 3072 3072 3072 个数中,RGB 各有 32 × 32 32 \times 32 32×32 个数,要在 50000 × 32 × 32 50000 \times 32 \times 32 50000×32×32 个数中求一个通道的均值,最终的均值有 3 3 3 个数字,然后所有图片每个通道都要减去对应的通道均值,VGGNet 是这种方式。

之所以执行减均值操作,是因为解决输入数据大多数都是正或者负的问题。虽然经过这种操作,数据变成零中心的,但是仍然只能第一层解决 Sigmoid 非零均值的问题,后面会有更严重的问题。

2.2 归一化(Normalization)

归一化是指将数据的所有维度都归一化,使其数值范围都近似相等。

有两种常用方法可以实现归一化。

  • 第一种是先对数据做零中心化(zero-centered)处理,然后每个维度都除以其标准差,实现代码为 X /= np.std(X, axis=0)
  • 第二种是对每个维度都做归一化,使得每个维度的最大和最小值是 1 1 1 和 − 1 -1 −1。这个预处理操作只有在确信不同的输入特征有不同的数值范围(或计量单位)时才有意义,但要注意预处理操作的重要性几乎等同于学习算法本身

在图像处理中,由于像素的数值范围几乎是一致的(都在 0-255 之间),所以进行这个额外的预处理步骤并不是很必要。

数据预处理; 归一化

  • 左边:原始的 2 维输入数据。
  • 中间:在每个维度上都减去平均值后得到零中心化数据,现在数据云是以原点为中心的。
  • 右边:每个维度都除以其标准差来调整其数值范围,红色的线指出了数据各维度的数值范围。

在中间的零中心化数据的数值范围不同,但在右边归一化数据中数值范围相同。

2.3 主成分分析(PCA)

这是另一种机器学习中比较常用的预处理形式,但在图像处理中基本不用。在这种处理中,先对数据进行零中心化处理,然后计算协方差矩阵,它展示了数据中的相关性结构。

数据预处理; 主成分分析(PCA)

# 假设输入数据矩阵 X 的尺寸为[N x D]
X -= np.mean(X, axis = 0) # 对数据进行零中心化(重要)
cov = np.dot(X.T, X) / X.shape[0] # 得到数据的协方差矩阵,DxD 

数据协方差矩阵的第 ( i , j ) (i, j) (i,j) 个元素是数据第 i i i 个和第 j j j 个维度的协方差。具体来说,该矩阵的对角线上的元素是方差。还有,协方差矩阵是对称和半正定的。我们可以对数据协方差矩阵进行 SVD(奇异值分解)运算。

U,S,V = np.linalg.svd(cov) 

U U U 的列是特征向量, S S S 是装有奇异值的 1 维数组(因为 cov 是对称且半正定的,所以 S 中元素是特征值的平方)。为了去除数据相关性,将已经零中心化处理过的原始数据投影到特征基准上:

Xrot = np.dot(X,U) # 对数据去相关性 

np.linalg.svd 的一个良好性质是在它的返回值 U 中,特征向量是按照特征值的大小排列的。我们可以利用这个性质来对数据降维,只要使用前面的小部分特征向量,丢弃掉那些包含的数据没有方差的维度,这个操作也被称为 主成分分析(Principal Component Analysis 简称 PCA) 降维:

Xrot_reduced = np.dot(X, U[:,:100]) # Xrot_reduced 变成 [N x 100] 

经过上面的操作,将原始的数据集的大小由 [ N × D ] [N \times D] [N×D] 降到了 [ N × 100 ] [N \times 100] [N×100],留下了数据中包含最大方差的的 100 个维度。通常使用 PCA 降维过的数据训练线性分类器和神经网络会达到非常好的性能效果,同时还能节省时间和存储器空间。

有一问题是为什么使用协方差矩阵进行 SVD 分解而不是使用原 X X X 矩阵进行?

其实都是可以的,只对数据 X X X(可以不是方阵)进行 SVD 分解,做 PCA 降维(避免了求协方差矩阵)的话一般用到的是右奇异向量 V V V,即 V V V 的前几列是需要的特征向量(注意 np.linalg.svd 返回的是 V.T)。 X X X 是 N × D N \times D N×D,则 U U U 是 N × N N \times N N×N, V V V 是 D × D D \times D D×D;而对协方差矩阵( D × D D \times D D×D)做 SVD 分解用于 PCA 降维的话,可以随意取左右奇异向量 U U U、 V V V(都是 D × D D \times D D×D)之一,因为两个向量是一样的。

2.4 白化(Whitening)

最后一个在实践中会看见的变换是白化(whitening)。白化操作的输入是特征基准上的数据,然后对每个维度除以其特征值来对数值范围进行归一化。

数据预处理; 白化(Whitening)

白化变换的几何解释是:如果数据服从多变量的高斯分布,那么经过白化后,数据的分布将会是一个均值为零,且协方差相等的矩阵。

该操作的代码如下:

# 对数据进行白化操作:
# 除以特征值 
Xwhite = Xrot / np.sqrt(S + 1e-5) 

注意分母中添加了 1e-5(或一个更小的常量)来防止分母为 0 0 0,该变换的一个缺陷是在变换的过程中可能会夸大数据中的噪声,这是因为它将所有维度都拉伸到相同的数值范围,这些维度中也包含了那些只有极少差异性(方差小)而大多是噪声的维度。

在实际操作中,这个问题可以用更强的平滑来解决(例如:采用比 1e-5 更大的值)。

下图为 CIFAR-10 数据集上的 PCA白化等操作结果可视化。

数据预处理; PCA / 白化的可视化

从左往右 4 张子图:

  • 第 1 张:一个用于演示的图片集合,含 49 张图片。
  • 第 2 张:3072 个特征向量中的前 144 个。靠前面的特征向量解释了数据中大部分的方差。
  • 第 3 张:49 张经过了 PCA 降维处理的图片,只使用这里展示的这 144 个特征向量。为了让图片能够正常显示,需要将 144 维度重新变成基于像素基准的 3072 个数值。因为 U 是一个旋转,可以通过乘以 U.transpose()[:144,:] 来实现,然后将得到的 3072 个数值可视化。可以看见图像变得有点模糊了,然而,大多数信息还是保留了下来。
  • 第 4 张:将「白化」后的数据进行显示。其中 144 个 维度中的方差都被压缩到了相同的数值范围。然后 144 个白化后的数值通过乘以 U.transpose()[:144,:] 转换到图像像素基准上。

2.5 实际应用

实际上在卷积神经网络中并不会采用 PCA 和白化,对数据进行零中心化操作还是非常重要的,对每个像素进行归一化也很常见。

补充说明

进行预处理很重要的一点是:任何预处理策略(比如数据均值)都只能在训练集数据上进行计算,然后再应用到验证集或者测试集上

  • 一个常见的错误做法是先计算整个数据集图像的平均值然后每张图片都减去平均值,最后将整个数据集分成训练/验证/测试集。正确的做法是先分成训练/验证/测试集,只是从训练集中求图片平均值,然后各个集(训练/验证/测试集)中的图像再减去这个平均值

3.权重初始化

关于神经网络权重初始化的知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面里【权重初始化缓解梯度消失和爆炸】板块内容。

初始化网络参数是训练神经网络里非常重要的一步,有不同的初始化方式,我们来看看他们各自的特点。

3.1 全零初始化

对一个两层的全连接网络,如果输入给网络的所有参数初始化为 0 0 0 会怎样?

这种做法是错误的。 因为如果网络中的每个神经元都计算出同样的输出,然后它们就会在反向传播中计算出同样的梯度,从而进行同样的参数更新。换句话说,如果权重被初始化为同样的值,神经元之间就失去了不对称性的源头。

3.2 小随机数初始化

现在权重初始值要非常接近 0 0 0 又不能等于 0 0 0,解决方法就是将权重初始化为很小的数值,以此来打破对称性。

思路是:如果神经元刚开始的时候是随机且不相等的,那么它们将计算出不同的更新,并将自身变成整个网络的不同部分。

实现方法是:W = 0.01 * np.random.randn(D,H)。其中 randn 函数是基于零均值和标准差的一个高斯分布来生成随机数的。

小随机数初始化在简单的网络中效果比较好,但是网络结构比较深的情况不一定会得到好的结果。比如一个 10 层的全连接网络,每层 500 个神经元,使用 t a n h tanh tanh 激活函数,用小随机数初始化。

代码与输出图像如下:

import numpy as np
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

# 假设一些高斯分布单元
D = np.random.randn(1000, 500)
hidden_layer_sizes = [500]*10  # 隐藏层尺寸都是 500,10 层
nonlinearities = ['tanh']*len(hidden_layer_sizes)  # 非线性函数都是用 tanh 函数

act = {'relu': lambda x: np.maximum(0, x), 'tanh': lambda x: np.tanh(x)}
Hs = {}
for i in range(len(hidden_layer_sizes)):
    X = D if i == 0 else Hs[i-1]  # 当前隐藏层的输入
    fan_in = X.shape[1]
    fan_out = hidden_layer_sizes[i]
    W = np.random.randn(fan_in, fan_out) * 0.01  # 权重初始化

    H = np.dot(X, W)  # 得到当前层输出
    H = act[nonlinearities[i]](H)  # 激活函数
    Hs[i] = H  # 保存当前层的结果并作为下层的输入

# 观察每一层的分布
print('输入层的均值:%f 方差:%f'% (np.mean(D), np.std(D)))
layer_means = [np.mean(H) for i,H in Hs.items()]
layer_stds = [np.std(H) for i,H in Hs.items()]
for i,H in Hs.items():
    print('隐藏层%d 的均值:%f 方差:%f' % (i+1, layer_means[i], layer_stds[i]))

# 画图
plt.figure()
plt.subplot(121)
plt.plot(list(Hs.keys()), layer_means, 'ob-')
plt.title('layer mean')
plt.subplot(122)
plt.plot(Hs.keys(), layer_stds, 'or-')
plt.title('layer std')

# 绘制分布图
plt.figure()
for i,H in Hs.items():
    plt.subplot(1, len(Hs), i+1)
    plt.hist(H.ravel(), 30, range=(-1,1))

plt.show() 

权重初始化; 小权重每层的输出 - 均值方差

权重初始化; 小权重每层的输出 - 分布

可以看到只有第一层的输出均值方差比较好,输出接近高斯分布,后面几层均值方差基本为 0 0 0,这样导致的后果是正向传播的激活值基本为 0 0 0,反向传播时就会计算出非常小的梯度(因权重的梯度就是层的输入,输入接近 0 0 0,梯度接近 0 0 0 ),参数基本不会更新。

如果上面的例子不用小随机数,即 W = np.random.randn(fan_in, fan_out) * 1,此时会怎样呢?

此时,由于权重较大并且使用的 tanh 函数,所有神经元都会饱和,输出为 + 1 +1 +1 或 − 1 -1 −1,梯度为 0 0 0,如下图所示,均值在 0 0 0 附近波动,方差较大在 0.98 0.98 0.98 附近波动,神经元输出大多为 + 1 +1 +1 或 − 1 -1 −1。

权重初始化; 大权重每层的输出 - 均值方差

权重初始化; 大权重每层的输出 - 分布

3.3 Xavier/He 初始化(校准方差)

上述分析可以看出,权重过小可能会导致网络崩溃,权重过大可能会导致网络饱和,所以都在研究出一种合理的初始化方式。一种很好的经验是使用 Xavier 初始化:

W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)

这是 Glorot 等在 2010 年发表的 论文。这样就保证了网络中所有神经元起始时有近似同样的输出分布。实践经验证明,这样做可以提高收敛的速度。

原理:假设神经元的权重 w w w 与输入 x x x 的内积为 s = ∑ i n w i x i s = \sum_i^n w_i x_i s=∑in​wi​xi​,这是还没有进行非线性激活函数运算之前的原始数值。此时 s s s 的方差:

Var ( s ) = Var ( ∑ i n w i x i ) = ∑ i n Var ( w i x i ) = ∑ i n [ E ( w i ) ] 2 Var ( x i ) + E [ ( x i ) ] 2 Var ( w i ) + Var ( x i ) Var ( w i ) = ∑ i n Var ( x i ) Var ( w i ) = n Var ( w ) Var ( x ) \begin{aligned} \text{Var}(s) &= \text{Var}(\sum_i^n w_ix_i) \ &= \sum_i^n \text{Var}(w_ix_i) \ &= \sum_i^n [E(w_i)]²\text{Var}(x_i) + E[(x_i)]²\text{Var}(w_i) + \text{Var}(x_i)\text{Var}(w_i) \ &= \sum_i^n \text{Var}(x_i)\text{Var}(w_i) \ &= n \text{Var}(w) \text{Var}(x) \end{aligned} Var(s)​=Var(i∑n​wi​xi​)=i∑n​Var(wi​xi​)=i∑n​[E(wi​)]2Var(xi​)+E[(xi​)]2Var(wi​)+Var(xi​)Var(wi​)=i∑n​Var(xi​)Var(wi​)=nVar(w)Var(x)​

前三步使用的是方差的性质(累加性、独立变量相乘);

第三步中,假设输入和权重的均值都是 0 0 0,即 E [ x i ] = E [ w i ] = 0 E[x_i] = E[w_i] = 0 E[xi​]=E[wi​]=0,但是 ReLU 函数中均值应该是正数。在最后一步,我们假设所有的 w i , x i w_i,x_i wi​,xi​ 都服从同样的分布。从这个推导过程我们可以看见,如果想要 s s s 有和输入 x x x 一样的方差,那么在初始化的时候必须保证每个权重 w w w 的方差是 1 / n 1/n 1/n 。

又因为对于一个随机变量 X X X 和标量 a a a,有 Var ( a X ) = a 2 Var ( X ) \text{Var}(aX) = a²\text{Var}(X) Var(aX)=a2Var(X),这就说明可以让 w w w 基于标准高斯分布(方差为 1)取样,然后乘以 a = 1 / n a = \sqrt{1/n} a=1/n ​,即 Var ( 1 / n ⋅ w ) = 1 / n Var ( w ) = 1 / n \text{Var}( \sqrt{1/n}\cdot w) = 1/n\text{Var}(w)=1/n Var(1/n ​⋅w)=1/nVar(w)=1/n,此时就能保证 Var ( s ) = Var ( x ) \text{Var}(s) =\text{Var}(x) Var(s)=Var(x)。

代码为:W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in),其中fan_in就是上文的 n n n。

不过作者在论文中推荐的是: W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in + fan_out),使 Var ( w ) = 2 / ( n i n + n o u t ) \text{Var}(w) = 2/(n_{in} + n_{out}) Var(w)=2/(nin​+nout​),其中 n i n , n o u t n_{in}, n_{out} nin​,nout​ 是前一层和后一层中单元的个数,这是基于妥协和对反向传播中梯度的分析得出的结论)

输出结果为:

权重初始化; 校准方差后的输出 - 均值方差

权重初始化; 校准方差后的输出 - 分布

图上可以看出,后面几层的输入输出分布很接近高斯分布。

但是使用 ReLU 函数这种关系会被打破,同样 w w w 使用单位高斯并且校准方差,然而使用 ReLU 函数后每层会消除一半的神经元(置 0 0 0 ),结果会使方差每次减半,会有越来越多的神经元失活,输出为 0 0 0 的神经元越来越多。如下图所示:

权重初始化; 将 tanh 换成 ReLU - 均值方差

权重初始化; 将 tanh 换成 ReLU - 分布

解决方法是 W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in/2)。因为每次有一半的神经元失活,校准时除 2 即可,这样得到的结果会比较好。

这是 2015 年何凯明的论文 Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification 提到的方法,这个形式是神经网络算法使用 ReLU 神经元时的当前最佳推荐。结果如下:

权重初始化; ReLU 校准后的输出 - 均值方差

权重初始化; ReLU 校准后的输出 - 分布

3.4 稀疏初始化

另一个处理非标定方差的方法是将所有权重矩阵设为 0 0 0,但是为了打破对称性,每个神经元都同下一层固定数目的神经元随机连接(其权重数值由一个小的高斯分布生成)。一个比较典型的连接数目是 10 个。

偏置项(biases)的初始化:通常将偏置初始化为 0 0 0。

3.5 实际应用

合适的初始化设置仍然是现在比较活跃的研究领域,经典的论文有:

当前的推荐是使用 ReLU 激活函数,并且使用 w = np.random.randn(n) * sqrt(2.0/n) 来进行权重初始化,n 是上一层神经元的个数,这是何凯明的论文得出的结论,也称作 He 初始化

4.批量归一化(Batch Normalization)

关于 Batch Normalization 的详细图示讲解也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章网络优化:超参数调优、正则化、批归一化和程序框架里【Batch Normalization】板块内容。

4.1 概述

批量归一化 是 loffe 和 Szegedy 最近才提出的方法,该方法一定程度解决了如何合理初始化神经网络这个棘手问题,其做法是让激活数据在训练开始前通过一个网络,网络处理数据使其服从标准高斯分布。

归一化是一个简单可求导的操作,所以上述思路是可行的。在实现层面,应用这个技巧通常意味着全连接层(或者是卷积层,后续会讲)与激活函数之间添加一个 BatchNorm 层。在神经网络中使用批量归一化已经变得非常常见,在实践中使用了批量归一化的网络对于不好的初始值有更强的鲁棒性。

4.2 原理

具体来说,我们希望每一层网络的输入都近似符合标准高斯分布,考虑有 N N N 个激活数据的小批量输入,每个输入 x x x 有 D D D 维,即 x = ( x ( 1 ) ⋯ x ( d ) ) x = (x^{(1)} \cdots x^{(d)}) x=(x(1)⋯x(d)),那么对这个小批量数据的每个维度进行归一化,使符合单位高斯分布,应用下面的公式:

x ^ ( k ) = x ( k ) − E [ x ( k ) ] Var [ x ( k ) ] \hat{x}^{(k)} =\frac{x{(k)}-\text{E}[x]}{\sqrt{\text{Var}[x^{(k)}]}} x^(k)=Var[x(k)] ​x(k)−E[x(k)]​

  • 其中的均值和方差是根据整个训练集计算出来的;
  • 这个公式其实就是随机变量转化为标准高斯分布的公式,是可微的;
  • 前向传播与反向传播也是利用小批量梯度下降(SGD),也可以利用这个小批量进行归一化;
  • 在训练开始前进行归一化,而不是在初始化时;
  • 卷积层每个激活图都有一个均值和方差;
  • 对每个神经元分别进行批量归一化。

批量归一化会把输入限制在非线性函数的线性区域,有时候我们并不想没有一点饱和,所以希望能控制饱和程度,即在归一化完成后,我们在下一步添加两个参数去缩放和平移归一化后的激活数据:

y ( k ) = γ ( k ) x ^ ( k ) + β ( k ) y^{(k)} = \gamma ^{(k)}\hat{x} ^{(k)}+\beta ^{(k)} y(k)=γ(k)x^(k)+β(k)

这两个参数可以在网络中学习,并且能实现我们想要的效果。的确,通过设置: γ ( k ) = Var [ x ( k ) ] \gamma {(k)}=\sqrt{\text{Var}[x]} γ(k)=Var[x(k)] ​, β ( k ) = E [ x ( k ) ] \beta {(k)}=\text{E}[x] β(k)=E[x(k)] 可以恢复原始激活数据,如果这样做的确最优的话。现在网络有了为了让网络达到较好的训练效果而去学习控制让 tanh 具有更高或更低饱和程度的能力。

当使用随机优化时,我们不能基于整个训练集去计算。我们会做一个简化:由于我们在 SGD 中使用小批量,每个小批量都可以得到激活数据的均值和方差的估计。这样,用于归一化的数据完全可以参与梯度反向传播。

批量归一化的思想:考虑一个尺寸为 m m m 的小批量 B。由于归一化被独立地应用于激活数据 x x x 的每个维度,因此让我们关注特定激活数据维度 x ( k ) x(k) x(k) 并且为了清楚起见省略 k k k。在小批量中共有 m m m 个这种激活数据维度 x ( k ) x(k) x(k): B = x 1 ⋯ m \text{B} ={x_{1 \cdots m}} B=x1⋯m​

归一化后的值为: x ^ 1 ⋯ m \hat{x}_{1 \cdots m} x¹⋯m​

线性转化后的值为: y 1 ⋯ m y_{1 \cdots m} y1⋯m​

这种线性转化是批量归一化转化: BN γ , β : x 1 ⋯ m → y 1 ⋯ m \text{BN}{\gamma, \beta} : x → y_{1 \cdots m} BNγ,β​:x1⋯m​→y1⋯m​

于是,我们的小批量激活数据 B = x 1 ⋯ m \text{B} ={x_{1 \cdots m}} B=x1⋯m​ 通过 BN 层,有两个参数需要学习: γ \gamma γ, β \beta β ( ε \varepsilon ε 是为了维持数值稳定在小批量方差上添加的小常数)。

该 BN 层的输出为: y i = BN γ , β ( x i ) , i = 1 ⋯ m {y_i=\text{BN}_{\gamma, \beta}(x_i)},i=1 \cdots m yi​=BNγ,β​(xi​),i=1⋯m,该层的计算有:

  • 小批量均值: μ B ← 1 m ∑ i = 1 m x i \mu B\leftarrow \frac{1}{m} \sum^m x_i μB​←m1​∑i=1m​xi​

  • 小批量方差: σ B 2 ← 1 m ∑ i = 1 m ( x i − μ B ) 2 \sigma² B\leftarrow \frac{1}{m} \sum^m (x_i-\mu _B)² σB2​←m1​∑i=1m​(xi​−μB​)2

  • 归一化: x ^ i ← x i − μ B σ B 2 + ε \hat{x} _i\leftarrow \frac{x_i-\mu _B}{\sqrt{\sigma² _B+\varepsilon } } x^i​←σB2​+ε ​xi​−μB​​

  • 缩放和平移: y i ← γ x ^ i + β ≡ BN γ , β ( x i ) y_i\leftarrow \gamma \hat{x} i+\beta \equiv \text{BN}(x_i) yi​←γx^i​+β≡BNγ,β​(xi​)

批量归一化; Batch Normalization

4.3 优势

  • 改善通过网络的梯度流
  • 具有更高的鲁棒性:允许更大的学习速率范围、减少对初始化的依赖
  • 加快学习速率衰减,更容易训练
  • 可以看作是一种正则方式,在原始输入 X X X 上抖动
  • 可以不使用 Dropout,加快训练

补充说明:测试时不使用小批量中计算的均值和方差,相反,使用训练期间激活数据的一个固定的经验均值,例如可以使用在训练期间的平均值作为估计。

总结:批量归一化可以理解为在网络的每一层之前都做预处理,将输入数据转化为单位高斯数据或者进行平移伸缩,只是这种操作以另一种方式与网络集成在了一起。

5.层归一化(Layer Normalization)

事实证明,批量归一化能使网络更容易训练,但是对批量的大小有依赖性,批量太小效果不好,批量太大又受到硬件的限制。所以在对输入批量大小具有上限的复杂网络中不太有用。

目前已经提出了几种批量归一化的替代方案来缓解这个问题,其中一个就是层归一化。我们不再对这个小批量进行归一化,而是对特征向量进行归一化。换句话说,当使用层归一化时,基于该特征向量内的所有项的总和来归一化对应于单个数据点。

层归一化测试与训练的行为相同,都是计算每个样本的归一。可用于循环神经网络。

层归一化; Layer Normalization

6.卷积神经网络中归一化

空间批量归一化(Spatial Batch Normalization)是对深度进行归一化。

  • 全连接网络中的批量归一化输入尺寸为 ( N , D ) (N,D) (N,D) 输出是 ( N , D ) (N,D) (N,D),其中我们在小批量维度 N N N 上计算统计数据用于归一化 N N N 个特征点。
  • 卷积层输入的数据,批量归一化的输入尺寸是 ( N , C , H , W ) (N,C,H,W) (N,C,H,W) 并产生尺寸为 ( N , C , H , W ) (N,C,H,W) (N,C,H,W) 的输出,其中 N 是小批量大小, ( H , W ) (H,W) (H,W) 是输出特征图的空间大小。
  • 如果使用卷积生成特征图,我们期望每个特征通道的统计在不同图像和同一图像内的不同位置之间相对一致。因此,空间批量归一化通过计算小批量维度 N 和空间维度 H H H 和 W W W 的统计量来计算每个 C C C 特征通道的均值和方差。

卷积神经网络; 空间批量归一化

卷积神经网络中的层归一化是对每张图片进行归一化。

  • 然而在卷积神经网络中,层归一化效果不好。因为对于全连接层,层中的所有隐藏单元倾向于对最终预测做出类似的贡献,并且对层的求和输入重新定中心和重新缩放效果很好;而对于卷积神经网络,贡献类似的假设不再适用。其感受野位于图像边界附近的大量隐藏单元很少打开,因此与同一层内其余隐藏单元的统计数据非常不同(图片中间的位置贡献比较大,边缘的位置可能是背景或噪声)。

实例归一化既对图片又对数据进行归一化;

卷积神经网络; 层归一化

组归一化(Group Normalization)2018 年何凯明的论文 Group Normalization 提出了一种中间技术。

  • 与层归一化在每个数据点的整个特征上进行标准化相比,建议将每个数据点特征拆分为相同的 G G G 组,然后对每个数据点的每个数据组的标准化(简单来说,相对于层归一化将整张图片归一,这个将整张图片裁成 G G G 组,然后对每个组进行归一)。
  • 这样就可以假设每个组仍然做出相同的贡献,因为分组就是根据视觉识别的特征。比如将传统计算机视觉中的许多高性能人为特征在一起。其中一个定向梯度直方图就是在计算每个空间局部块的直方图之后,每个直方图块在被连接在一起形成最终特征向量之前被归一化。

CNN; 不同归一化方式对比

7.监控学习过程

7.1 监控学习过程的步骤

1) 数据预处理,减均值

2) 选择网络结构

两层神经网络,一个隐藏层有 50 个神经元,输入图像是 3072 维的向量,输出层有 10 个神经元,代表 10 种分类。

监控学习过程; 神经网络结构

3) 合理性(Sanity)检查

使用小参数进行初始化,使正则损失为 0 0 0,确保得到的损失值与期望一致。

例如,输入数据集为 CIFAR-10 的图像分类

  • 对于 Softmax 分类器,一般期望它的初始损失值是 2.302 2.302 2.302,这是因为初始时预计每个类别的概率是 0.1 0.1 0.1(因为有 10 个类别),然后 Softmax 损失值正确分类的负对数概率 − l n ( 0.1 ) = 2.302 -ln(0.1) = 2.302 −ln(0.1)=2.302。
  • 对于多类 SVM,假设所有的边界都被越过(因为所有的分值都近似为零),所以损失值是 9(因为对于每个错误分类,边界值是 1)。
  • 如果没看到这些损失值,那么初始化中就可能有问题。

提高正则化强度,损失值会变大。

def init_two_layer_model(input_size, hidden_size, output_size):
    model = {}
    model["W1"] = 0.0001 * np.random.randn(input_size, hidden_size)
    model['b1'] = np.zeros(hidden_size)
    model['W2'] = 0.0001 * np.random.randn(hidden_size, output_size)
    model['b2'] = np.zeros(output_size)
    return model

model = init_two_layer_model(32*32*3, 50, 10)
loss, grad = two_layer_net(X_train, model, y_train, 0)  # 0 没有正则损失
print(loss) 

对小数据子集过拟合

  • 这一步很重要,在整个数据集进行训练之前,尝试在一个很小的数据集上进行训练(比如 20 个数据),然后确保能到达 0 的损失值。此时让正则化强度为 0,不然它会阻止得到 0 的损失。除非能通过这一个正常性检查,不然进行整个数据集训练是没有意义的。
  • 但是注意,能对小数据集进行过拟合依然有可能存在不正确的实现。比如,因为某些错误,数据点的特征是随机的,这样算法也可能对小数据进行过拟合,但是在整个数据集上跑算法的时候,就没有任何泛化能力。
model = init_two_layer_model(32*32*3, 50, 10)
trainer = ClassifierTrainer()
X_tiny = X_train[:20]   # 选前 20 个作为样本
y_tiny = y_train[:20]
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny, 
                                  model, two_layer_net, verbose=True,
                                  num_epochs=200, reg=0.0, update='sgd',
                                  learning_rate=1e-3, learning_rate_decay=1,
                                  sample_batchs=False) 

监控学习过程; 小数据子集过拟合检测

4) 梯度检查(Gradient Checks)

理论上将进行梯度检查很简单,就是简单地把解析梯度和数值计算梯度进行比较。然而从实际操作层面上来说,这个过程更加复杂且容易出错。下面是一些常用的技巧:

① 使用中心化公式

在使用有限差值近似来计算数值梯度的时候,常见的公式是: d f ( x ) d x = f ( x + h ) − f ( x ) h \frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h} dxdf(x)​=hf(x+h)−f(x)​ 其中 h h h 是一个很小的数字,在实践中近似为 1e-5。但是在实践中证明,使用中心化公式效果更好: d f ( x ) d x = f ( x + h ) − f ( x − h ) 2 h \frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h} dxdf(x)​=2hf(x+h)−f(x−h)​ 该公式在检查梯度的每个维度的时候,会要求计算两次损失函数(所以计算资源的耗费也是两倍),但是梯度的近似值会准确很多。

② 使用相对误差来比较

数值梯度 f n ′ f'_n fn′​ 和解析梯度 f a ′ f'_a fa′​ 的绝对误差并不能准确的表明二者的差距,应当使用相对误差。 ∣ f a ′ − f n ′ ∣ max ⁡ ( ∣ f a ′ ∣ , ∣ f n ′ ∣ ) \frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)} max(∣fa′​∣,∣fn′​∣)∣fa′​−fn′​∣​ 在实践中:相对误差大于 1e-2 通常就意味着梯度可能出错;小于 1e-7 才是比较好的结果。但是网络的深度越深,相对误差就越高。所以对于一个 10 层网络,1e-2的相对误差值可能就行,因为误差一直在累积。相反,如果一个可微函数的相对误差值是 1e-2,那么通常说明梯度实现不正确。

③ 使用双精度

一个常见的错误是使用单精度浮点数来进行梯度检查,这样会导致即使梯度实现正确,相对误差值也会很高(比如1e-2)。保持在浮点数的有效范围。把原始的解析梯度和数值梯度数据打印出来,确保用来比较的数字的值不是过小。

④ 注意目标函数的不可导点(kinks)

在进行梯度检查时,一个导致不准确的原因是不可导点问题。不可导点是指目标函数不可导的部分,由 ReLU 函数、SVM 损失、Maxout 神经元等引入。考虑当 x=-1e-6 时,对 ReLU 函数进行梯度检查。因为 x < 0 x<0 x<0,所以解析梯度在该点的梯度为 0。然而,在这里数值梯度会突然计算出一个非零的梯度值,因为 f ( x + h ) f(x+h) f(x+h) 可能越过了不可导点(例如:如果 h>1e-6),导致了一个非零的结果。解决这个问题的有效方法是使用少量数据点。这样不可导点会减少,并且如果梯度检查对 2-3 个数据点都有效,那么基本上对整个批量数据也是没问题的。

⑤ 谨慎设置 h

并不是越小越好,如果无法进行梯度检查,可以试试试试将 h h h 调到 1e-4 或者 1e-6

在操作的特性模式中梯度检查。为了安全起见,最好让网络学习(「预热」)一小段时间,等到损失函数开始下降的之后再进行梯度检查。在第一次迭代就进行梯度检查的危险就在于,此时可能正处在不正常的边界情况,从而掩盖了梯度没有正确实现的事实。

⑥ 关闭正则损失

推荐先关掉正则化对数据损失做单独检查,然后对正则化做单独检查,防止正则化损失吞没掉数据损失。

5) 正式训练,数值跟踪,特征可视化

设置一个较小的正则强度,找到使损失下降的学习率。

best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
                                  model, two_layer_net, verbose=True,
                                  num_epochs=10, reg=0.000001, update='sgd',
                                  learning_rate=1e-6, learning_rate_decay=1,
                                  sample_batchs=False) 

监控学习过程; 学习速率过小,损失下降缓慢

学习率为 1 0 − 6 10^{-6} 10−6 时,损失下降缓慢,说明学习速率过小。

如果把学习率设为另一个极端: 1 0 6 10^{6} 106,如下图所示,会发生损失爆炸:

监控学习过程; 学习速率过大,损失爆照

NaN 通常意味着学习率过高,导致损失过大。设为 1 0 − 3 10^{-3} 10−3 时仍然爆炸,一个比较合理的范围是 [ 1 0 − 5 , 1 0 − 3 ] [10^{-5}, 10^{-3}] [10−5,10−3]。

7.2 训练过程中的数值跟踪

1) 跟踪损失函数

训练期间第一个要跟踪的数值就是损失值,它在前向传播时对每个独立的批数据进行计算。

在下面的图表中, x x x 轴通常都是表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观察过的次数的期望(一个 epoch 意味着每个样本数据都被观察过了一次)。相较于迭代次数(iterations) ,一般更倾向跟踪 epoch,这是因为迭代次数与数据的批尺寸(batchsize)有关,而批尺寸的设置又可以是任意的。

比如一共有 1000 个 训练样本,每次 SGD 使用的小批量是 10 个样本,一次迭代指的是用这 10 个样本训练一次,而 1000 个样本都被使用过一次才是一次 epoch,即这 1000 个样本全部被训练过一次需要 100 次 iterations,一次 epoch。

下图展示的是损失值随时间的变化,曲线形状会给出学习率设置的情况:

监控学习过程; 损失函数与学习率关系

左图展示了不同的学习率的效果。过低的学习率导致算法的改善是线性的。高一些的学习率会看起来呈几何指数下降,更高的学习率会让损失值很快下降,但是接着就停在一个不好的损失值上(绿线)。这是因为最优化的「能量」太大,参数随机震荡,不能最优化到一个很好的点上。过高的学习率又会导致损失爆炸。

右图显示了一个典型的随时间变化的损失函数值,在 CIFAR-10 数据集上面训练了一个小的网络,这个损失函数值曲线看起来比较合理(虽然可能学习率有点小,但是很难说),而且指出了批数据的数量可能有点太小(因为损失值的噪音很大)。损失值的震荡程度和批尺寸(batch size)有关,当批尺寸为 1,震荡会相对较大。当批尺寸就是整个数据集时震荡就会最小,因为每个梯度更新都是单调地优化损失函数(除非学习率设置得过高)。

下图这种开始损失不变,然后开始学习的情况,说明初始值设置的不合理。

监控学习过程; 跟踪损失函数

2) 跟踪训练集和验证集准确率

在训练分类器的时候,需要跟踪的第二重要的数值是验证集和训练集的准确率。这个图表能够展现知道模型过拟合的程度:

监控学习过程; 训练集和验证集准确率

训练集准确率和验证集准确率间的间距指明了模型过拟合的程度。在图中,蓝色的验证集曲线比训练集准确率低了很多,这就说明模型有很强的过拟合。遇到这种情况,就应该增大正则化强度(更强的 L2 权重惩罚,更多的随机失活等)或收集更多的数据。另一种可能就是验证集曲线和训练集曲线很接近,这种情况说明模型容量还不够大:应该通过增加参数数量让模型容量更大些。

3) 跟踪权重更新比例

最后一个应该跟踪的量是权重中更新值的数量和全部值的数量之间的比例。注意:是更新的,而不是原始梯度(比如,在普通 sgd 中就是梯度乘以学习率)。需要对每个参数集的更新比例进行单独的计算和跟踪。一个经验性的结论是这个比例应该在 1e-3 左右。如果更低,说明学习率可能太小,如果更高,说明学习率可能太高。下面是具体例子:

# 假设参数向量为 W,其梯度向量为 dW
param_scale = np.linalg.norm(W.ravel())  # ravel 将多维数组转化成一维;
                                         # np.linalg.norm 默认求 L2 范式
update = -learning_rate*dW # 简单 SGD 更新
update_scale = np.linalg.norm(update.ravel())
W += update # 实际更新
print update_scale / param_scale # 要得到 1e-3 左右 

4) 第一层可视化

如果数据是图像像素数据,那么把第一层特征可视化会有帮助:

监控学习过程; 第一层特征可视化

左图: 特征充满了噪音,这暗示了网络可能出现了问题:网络没有收敛,学习率设置不恰当,正则化惩罚的权重过低。

右图: 特征不错,平滑,干净而且种类繁多,说明训练过程进行良好。

8.超参数调优

关于超参数调优的讲解也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章网络优化:超参数调优、正则化、批归一化和程序框架里【超参数调优】板块内容。

如何进行超参数调优呢?常需要设置的超参数有三个:

  • 学习率
  • 学习率衰减方式(例如一个衰减常量)
  • 正则化强度(L2 惩罚,随机失活强度)

下面介绍几个常用的策略:

1) 比起交叉验证最好使用一个验证集

在大多数情况下,一个尺寸合理的验证集可以让代码更简单,不需要用几个数据集来交叉验证。

2) 分散初值,几次周期(epoch)

选择几个非常分散的数值,然后使用几次 epoch(完整数据集训练一轮是 1 个 epoch)去学习。经过几次 epoch,基本就能发现哪些数值较好哪些不好。比如很快就 nan(往往超过初始损失 3 倍就可以认为是 nan,就可以结束训练。),或者没有反应,然后进行调整。

3) 过程搜索:从粗到细

发现比较好的区间后,就可以精细搜索,epoch 次数更多,运行时间更长。比如之前的网络,每次进行 5 次 epoch,对较好的区间进行搜索,找到准确率比较高的值,然后进一步精确查找。注意,需要在对数尺度上进行超参数搜索

也就是说,我们从标准分布中随机生成了一个实数,然后让它成为 10 的次数。对于正则化强度,可以采用同样的策略。直观地说,这是因为学习率和正则化强度都对于训练的动态进程有乘的效果。

例如:当学习率是 0.001 的时候,如果对其固定地增加 0.01,那么对于学习进程会有很大影响。然而当学习率是 10 的时候,影响就微乎其微了。这就是因为学习率乘以了计算出的梯度。

比起加上或者减少某些值,思考学习率的范围是乘以或者除以某些值更加自然。但是有一些参数(比如随机失活)还是在原始尺度上进行搜索。

max_count = 100
for count in range(max_count):
    reg = 10**uniform(-5, 5)  # random 模块的函数 uniform,会在-5~5 范围内随机选择一个实数
                              # reg 在 10^-5~10⁵ 之间取值,指数函数
    lr = 10**uniform(-3, -6)

    model = init_two_layer_model(32 * 32 * 3, 50, 10)
    trainer = ClassifierTrainer()
    best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
                                      model, two_layer_net, verbose=False,
                                      num_epochs=5, reg=reg, update='momentum',
                                      learning_rate=lr, learning_rate_decay=0.9,
                                      sample_batchs=True, batch_size=100) 

超参数调优; 几个常用策略

比较好的结果在红框中,学习率在 10e-4 左右,正则强度在 10e-4~10e-1 左右,需要进一步精细搜索。修改代码:

max_count = 100
for count in range(max_count):
    reg = 10**uniform(-4, 0)
    lr = 10**uniform(-3, -4) 

超参数调优; 几个常用策略

有一个相对较好的准确率: 53 % 53% 53%。但是这里却有一个问题,这些比较高的准确率都是学习率在 10e-4附近,也就是说都在我们设置的区间边缘,或许 10e-510e-6 有更好的结果。所以在设置区间的时候,要把较好的值放在区间中间,而不是区间边缘

随机搜索优于网格搜索。Bergstra 和 Bengio 在文章 Random Search for Hyper-Parameter Optimization 中说「随机选择比网格化的选择更加有效」,而且在实践中也更容易实现。通常,有些超参数比其余的更重要,通过随机搜索,而不是网格化的搜索,可以让你更精确地发现那些比较重要的超参数的好数值。

超参数调优; 网格搜索 V.S. 随机搜索

上图中绿色函数部分是比较重要的参数影响,黄色是不重要的参数影响,同样取 9 个点,如果采用均匀采样就会错过很多重要的点,随机搜索就不会。

下一篇 深度学习与 CV 教程(7) | 神经网络训练技巧 (下) 会讲到的学习率衰减方案、更新类型、正则化、以及网络结构(深度、尺寸)等都需要超参数调优。

9.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=6

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

10.要点总结

  • 激活函数选择折叶函数
  • 数据预处理采用减均值
  • 权重初始化采用 Xavier 或 He 初始化
  • 使用批量归一化
  • 梯度检查;合理性检查;跟踪损失函数、准确率、更新比例等
  • 超参数调优采用随机搜索,对数间隔,不断细化范围,增加 epoch

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(7) | 神经网络训练技巧 (下)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125025085

ShowMeAI 研究中心


Training Neural Networks; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

ShowMeAI 在上一篇 深度学习与 CV 教程(6) | 神经网络训练技巧 (上) 介绍了激活函数选择, s i g m o i d sigmoid sigmoid 和 t a n h tanh tanh 都有饱和的问题;权重初始化不能太小也不能太大,最好使用 Xavier 初始化;数据预处理使用减去均值和归一化,线性分类中这两个操作会使分界线不那么敏感,即使稍微转动也可以,神经网络中也对权重的轻微改变没那么敏感,易于优化;也可以使用批量归一化,将输入数据变成单位高斯分布,或者缩放平移;学习过程跟踪损失、准确率;超参数体调优范围由粗到细,迭代次数逐渐增加,使用随机搜索。

本篇重点

  • 更好的优化
  • 正则化
  • 迁移学习
  • 模型集成

1.更好的优化(参数更新)

关于优化算法的详细知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 神经网络优化算法 的讲解。

1.1 批梯度下降(BGD)

批梯度下降即 batch gradient descent,在训练中每一步迭代都使用训练集的所有内容 { x 1 , ⋯   , x n } {x_1, \cdots ,x_n} {x1​,⋯,xn​} 以及每个样本对应的输出 y i y_i yi​,用于计算损失和梯度然后使用梯度下降更新参数。

当在整个数据集上进行计算时,只要学习率足够低,总是能在损失函数上得到非负的进展。参考代码如下(其中 learning_rate 是一个超参数):

# 普通梯度下降
while True:
    weights_grad = evaluate_gradient(loss_fun, data, weights)
    weights += -learning_rate * weights_grad  # 参数更新 

如下图为只有两个参数的损失函数,经过不断更新,如果足够幸运,函数最终收敛在红色部分是最低点:

批量梯度下降 BGD; 两个参数的损失函数

  • 优点:由于每一步都利用了训练集中的所有数据,因此当损失函数达到最小值以后,能够保证此时计算出的梯度为 0 0 0,能够收敛。因此,使用 BGD 时不需要逐渐减小学习率。
  • 缺点:随着数据集的增大,运行速度会越来越慢。

1.2 随机梯度下降(SGD)

这里的随机梯度下降(stochastic gradient descent)其实和之前介绍的 MBGD(minibatch gradient descent)是一个意思,即每次迭代随机抽取一批样本 { x 1 , ⋯   , x m } {x_1, \cdots ,x_m} {x1​,⋯,xm​} 及 y i y_i yi​,以此来反向传播计算出梯度,然后向负梯度方向更新参数。

SGD 的优点是训练速度快,对于很大的数据集,也能够以较快的速度收敛。但是实际应用 SGD 会有很多问题:

① 如果损失函数在一个参数方向下降的快另一个方向下降的慢,这样会导致 「 之字形 」下降到最低点,高维中很普遍

随机梯度下降 SGD; SGD 之字形路线

  • 上图是一个山沟状的区域,损失最小点沿着蓝色的线方向。考虑表面上的一个点 A 梯度,该点的梯度可以分解为两个分量,一个沿着方向 w 1 w_1 w1​,另一个沿着 w 2 w_2 w2​。
  • 梯度在 w 1 w_1 w1​ 方向上的分量要大得多,因为在 w 1 w_1 w1​ 方向上每走一步都比在 w 2 w_2 w2​ 方向损失值下降的多,虽然最小值在 w 2 w_2 w2​ 方向上。这样实际走一步在 w 1 w_1 w1​ 方向走的多, w 2 w_2 w2​ 走得少,就会导致在这个沟里反复震荡,「之字形」前往最小值点。

② 如果损失函数有局部极小值和鞍点(既不是极大值也不是极小值的临界点)时,此时的梯度为 0 0 0,参数更新会卡住,或在极小值附近震荡

随机梯度下降 SGD; 鞍点 Saddle points

  • 在高维数据中,鞍点的存在是个更普遍也更大的问题,极小值每个梯度方向损失都会变大,而鞍点有的方向变大,有的减小,接近鞍点时更新缓慢。

③ SGD 具有随机性,我们的梯度来自小批量数据(使用全部数据计算真实梯度速度太慢了),可能会有噪声,这样梯度下降的路线会很曲折,收敛的慢

随机梯度下降 SGD; 有噪声的 SGD 路线曲折

下面有一些「小批量梯度下降」基础上的优化算法。

1.3 动量(Momentum)更新

带动量的更新方法在深度网络上几乎总能得到更好的收敛速度。

损失值可以理解为是山的高度(因此高度势能是 U = m g h U=mgh U=mgh),用随机数字初始化参数等同于在某个位置给质点设定初始速度为 0 0 0 ,这样最优化过程可以看做是参数向量(即质点)在地形上滚动的过程。

质点滚动的力来源于高度势能 F = − ∇ U F = - \nabla U F=−∇U,即损失函数的负梯度(想象损失函数为凸函数,梯度为正时质点会向负方向滚动,对应参数减小;损失函数梯度为负时会向正方向滚动对应参数增大)。又因为 F = m a F=ma F=ma,质点的加速度和负梯度成正比,所以负梯度方向速度是逐渐增加的。

在 SGD 中,梯度直接影响质点的位置,在梯度为 0 0 0 的地方,位置就不会更新了;而在这里,梯度作为作用力影响的是速度,速度再改变位置,即使梯度为 0 0 0 ,但之前梯度累积下来的速度还在,一般而言,一个物体的动量指的是这个物体在它运动方向上保持运动的趋势,所以此时质点还是有动量的,位置仍然会更新,这样就可以冲出局部最小值或鞍点,继续更新参数。但是必须要给质点的速度一个衰减系数或者是摩擦系数,不然因为能量守恒,质点在谷底会不停的运动。

也就是说,参数更新的方向,不仅由当前点的梯度方向决定,而且由此前累积的梯度方向决定。

计算过程也是每次迭代随机抽取一批样本 x 1 , ⋯   , x m {x_1, \cdots ,x_m} x1​,⋯,xm​ 及 y i y_i yi​,计算梯度和损失,并更新速度和参数(假设质量为 1,v 即动量):

v=0
while True:
    dW =  compute_gradient(W, X_train, y_train)
    v = rho * v - learning_rate * dW
    W += v 
  • rho 表示每回合速度 v 的衰减程度,每次迭代得到的梯度都是 dW 那么最后得到的 v 的稳定值为: − l e a r n i n g r a t e ∗ d w 1 − r h o \frac{-learning_{rate} \ast dw}{1-rho} 1−rho−learningrate​∗dw​
  • rho 为 0 0 0 时表示 SGD,rho 一般取值 0.5 0.5 0.5、 0.9 0.9 0.9、 0.99 0.99 0.99,对应学习速度提高两倍、10 倍和 100 倍。

动量更新可以很好的解决上述 SGD 的几个问题:

  • 由于参数的更新要累积之前的梯度,所以如果我们分别累加这些梯度的两个分量,那么 w 1 w_1 w1​ 方向上的分量将互相抵消,而 w 2 w_2 w2​ 方向上的分量得到了加强。 但是由于衰减系数,不可能完全抵消,但是已经可以加速通过,很大程度缓解了「之字形」收敛慢的问题。这也是减少震荡的原理。

动量更新; 缓解之字形震荡

  • 局部最小值和鞍点由于还有之前的速度,会加速冲过去。

动量更新; 越过局部极小值点和鞍点

  • 面对梯度变化比较大的方向,即一些噪声,由于此时质点还有比较大的速度,这时的反方向需要先将速度减小为 0 0 0 才能改变参数更新方向,由于速度是累加的,所以个别的噪声的影响不会那么大,就可以平滑快速的收敛。

动量更新; 抑制噪声的影响

1.4 Nesterov 动量

Nesterov 动量与普通动量有些许不同,最近变得比较流行。在理论上对于凸函数它能得到更好的收敛,在实践中也确实比标准动量表现更好一些。

Nesterov 动量; 动量 V.S. Nesterov 动量

  • 普通的动量更新在某一点处有一个速度,然后计算该点的梯度,实际的更新方向会根据速度方向和梯度方向做一个权衡。
  • Nesterov 动量更新是既然我们知道动量将会将质点带到一个新的位置(即向前看),我们就不要在原来的位置计算梯度了,在这个「向前看」的地方计算梯度,更新参数。

这样代码变为:

v=0
while True:
    W_ahead = W + rho * v
    dW_ahead =  compute_gradient(W_ahead, X_train, y_train)
    v = rho * v - learning_rate * dW_ahead
    W += v 

动量还是之前的动量,只是梯度变成将来的点的梯度。

而在实践中,人们更喜欢和普通 SGD 或普通的动量方法一样简单的表达式。通过对 W_ahead = W + rho * v 使用变量变换进行改写是可以做到的,然后用 W_ahead 而不是 W 来表示上面的更新。

也就是说,实际存储的参数总是向前一步的那个版本。 代码如下:

v=0
while True:
    pre_v = v
    dW =  compute_gradient(W, X_train, y_train)
    v = rho * v - learning_rate * dW
    W += -rho * pre_v + (1 + rho) * v 

推导过程如下:

最初的 Nesterov 动量可以用下面的数学表达式代替:

v t + 1 = ρ v t − α ∇ f ( x t + ρ v t ) v_{t+1}=\rho v_t - \alpha \nabla f(x_t+\rho v_t) vt+1​=ρvt​−α∇f(xt​+ρvt​)

x t + 1 = x t + v t + 1 x_{t+1}=x_t+v_{t+1} xt+1​=xt​+vt+1​

现在令 x ~ t = x t + ρ v t \tilde{x}_t =x_t+\rho v_t x~t​=xt​+ρvt​,则:

v t + 1 = ρ v t − α ∇ f ( x t ~ ) v_{t+1}=\rho v_t-\alpha \nabla f(\tilde{x_t}) vt+1​=ρvt​−α∇f(xt​~​)

x ~ t + 1 = x t + 1 + ρ v t + 1 = x t + v t + 1 + ρ v t + 1 = x ~ t − ρ v t + v t + 1 + ρ v t + 1 \begin{aligned} \tilde{x}{t+1} &=x+\rho v_{t+1}\ &=x_{t}+v_{t+1}+\rho v_{t+1}\ &=\tilde{x}{t}-\rho v+v_{t+1}+\rho v_{t+1} \end{aligned} xt+1​​=xt+1​+ρvt+1​=xt​+vt+1​+ρvt+1​=xt​−ρvt​+vt+1​+ρvt+1​​

从而有:

x ~ t + 1 = x t ~ − ρ v t + ( ρ + 1 ) v t + 1 \tilde{x}{t+1}=\tilde{x_t}-\rho v_t+(\rho+1)v xt+1​=xt​​−ρvt​+(ρ+1)vt+1​

  • 只更新 v t v_t vt​ 和 x ~ t \tilde{x}_t x~t​ 即可

示意图如下:

Nesterov 动量; 还原推导示意图

Nesterov 动量; SGD V.S. 动量 V.S. Nesterov 动量

1.5 自适应梯度算法(Adagrad)

上面提到的方法对于所有参数都使用了同一个更新速率,但是同一个更新速率不一定适合所有参数。如果可以针对每个参数设置各自的学习率可能会更好,根据情况进行调整,Adagrad是一个由 Duchi 等 提出的适应性学习率算法。

代码如下:

eps = 1e-7
grad_squared =  $0$ 
while True:
    dW = compute_gradient(W)
    grad_squared += dW * dW
    W -= learning_rate * dW / (np.sqrt(grad_squared) + eps) 

AdaGrad 其实很简单,就是将每一维各自的历史梯度的平方叠加起来,然后更新的时候除以该历史梯度值即可。

变量 grad_squared 的尺寸和梯度矩阵的尺寸是一样的,用于累加每个参数的梯度的平方和。这个将用来归一化参数更新步长,归一化是逐元素进行的。eps(一般设为 1e-41e-8 之间)用于平滑,防止出现除以 0 0 0 的情况。

  • 优点:能够实现参数每一维的学习率的自动更改,如果某一维的梯度大,那么学习速率衰减的就快一些,延缓网络训练;如果某一维的梯度小,那么学习速率衰减的就慢一些,网络训练加快。
  • 缺点:如果梯度累加的很大,学习率就会变得非常小,就会陷在局部极小值点或提前停( RMSProp 算法可以很好的解决该问题)。

1.6 均方根支柱算法(RMSProp)

RMSProp 优化算法也可以自动调整学习率,并且 RMSProp 为每个参数选定不同的学习率。

RMSProp 算法在 AdaGrad 基础上引入了衰减因子,RMSProp 在梯度累积的时候,会对「过去」与「现在」做一个平衡,通过超参数 decay_rate 调节衰减量,常用的值是 [ 0.9 , 0.99 , 0.999 ] [0.9,0.99,0.999] [0.9,0.99,0.999]。其他不变,只是 grad_squared 类似于动量更新的形式:

grad_squared =  decay_rate * grad_squared + (1 - decay_rate) * dx * dx 

相比于 AdaGrad,这种方法很好的解决了训练过早结束的问题。和 Adagrad 不同,其更新不会让学习率单调变小。

均方根支柱算法; SGD V.S. 动量 V.S. RMSProp

1.7 自适应-动量优化(Adam)

动量更新在 SGD 基础上增加了一阶动量,AdaGrad 和 RMSProp 在 SGD 基础上增加了二阶动量。把一阶动量和二阶动量结合起来,就得到了 Adam 优化算法:Adaptive + Momentum

代码如下:

eps = 1e-8
first_moment = 0  # 第一动量,用于累积梯度,加速训练
second_moment = 0  # 第二动量,用于累积梯度平方,自动调整学习率
while True:
    dW = compute_gradient(W)
    first_moment = beta1 * first_moment + (1 - beta1) * dW  # Momentum
    second_moment = beta2 * second_moment + (1 - beta2) * dW * dW  # AdaGrad / RMSProp
    W -= learning_rate * first_moment / (np.sqrt(second_moment) + eps) 

上述参考代码看起来像是 RMSProp 的动量版,但是这个版本的 Adam 算法有个问题:第一步中 second_monent 可能会比较小,这样就可能导致学习率非常大,所以完整的 Adam 需要加入偏置。

代码如下:

eps = 1e-8
first_moment = 0  # 第一动量,用于累积梯度,加速训练
second_moment = 0  # 第二动量,用于累积梯度平方,自动调整学习率

for t in range(1, num_iterations+1):
    dW = compute_gradient(W)
    first_moment = beta1 * first_moment + (1 - beta1) * dW  # Momentum
    second_moment = beta2 * second_moment + (1 - beta2) * dW * dW  # AdaGrad / RMSProp
    first_unbias = first_moment / (1 - beta1 ** t)  # 加入偏置,随次数减小,防止初始值过小
    second_unbias = second_moment / (1 - beta2 ** t)
    W -= learning_rate * first_unbias / (np.sqrt(second_unbias) + eps) 

论文中推荐的参数值 eps=1e-8, beta1=0.9, beta2=0.999, learning_rate = 1e-35e-4,对大多数模型效果都不错。

在实际操作中,我们推荐 Adam 作为默认的算法,一般而言跑起来比 RMSProp 要好一点。

自适应动量优化; SGD V.S. 动量 V.S. RMSProp V.S. Adam

1.8 学习率退火

以上的所有优化方法,都需要使用超参数学习率。

在训练深度网络的时候,让学习率随着时间衰减通常是有帮助的。可以这样理解:

  • 如果学习率很高,系统的动能就过大,参数向量就会无规律地跳动,不能够稳定到损失函数更深更窄的部分去。
  • 知道什么时候开始衰减学习率是有技巧的:慢慢减小它,可能在很长时间内只能是浪费计算资源地看着它混沌地跳动,实际进展很少。但如果快速地减少它,系统可能过快地失去能量,不能到达原本可以到达的最好位置。

通常,实现学习率衰减有 3 种方式:

① 随步数衰减:每进行几个周期(epoch)就根据一些因素降低学习率。典型的值是每过 5 个周期就将学习率减少一半,或者每 20 个周期减少到之前的 10%。

这些数值的设定是严重依赖具体问题和模型的选择的。在实践中可能看见这么一种经验做法:使用一个固定的学习率来进行训练的同时观察验证集错误率,每当验证集错误率停止下降,就乘以一个常数(比如 0.5)来降低学习率。

② 指数衰减:数学公式是 α = α 0 e − k t \alpha=\alpha_0e^{-kt} α=α0​e−kt,其中 α 0 , k \alpha_0,k α0​,k 是超参数, t t t 是迭代次数(也可以使用周期作为单位)。
③ 1/t 衰减:数学公式是 α = α 0 / ( 1 + k t ) \alpha=\alpha_0/(1+kt) α=α0​/(1+kt)),其中 α 0 , k \alpha_0,k α0​,k 是超参数, t t t 是迭代次数。

在实践中随步数衰减的随机失活(Dropout)更受欢迎,因为它使用的超参数(衰减系数和以周期为时间单位的步数)比 k 更有解释性。

如果你有足够的计算资源,可以让衰减更加缓慢一些,让训练时间更长些。

一般像 SGD 这种需要使用学习率退火,Adam 等不需要。也不要一开始就使用,先不用,观察一下损失函数,然后确定什么地方需要减小学习率。

学习率退火; 损失函数

1.9 二阶方法(Second-Order)

在深度网络背景下,第二类常用的最优化方法是基于牛顿方法的,其迭代如下:

x ← x − [ H f ( x ) ] − 1 ∇ f ( x ) x \leftarrow x - [H f(x)]^{-1} \nabla f(x) x←x−[Hf(x)]−1∇f(x)

H f ( x ) H f(x) Hf(x) 是 Hessian 矩阵,由 f ( x ) f(x) f(x) 的二阶偏导数组成:

H = [ ∂ 2 f ∂ x 1 2 ∂ 2 f ∂ x 1 ∂ x 2 ⋯ ∂ 2 f ∂ x 1 ∂ x n ∂ 2 f ∂ x 2 ∂ x 1 ∂ 2 f ∂ x 2 2 ⋯ ∂ 2 f ∂ x 2 ∂ x n ⋮ ⋮ ⋱ ⋮ ∂ 2 f ∂ x n ∂ x 1 ∂ 2 f ∂ x n ∂ x 2 ⋯ ∂ 2 f ∂ x n 2 ] \mathbf{H}=\left[\begin{array}{cccc} \frac{\partial^{2} f}{\partial x_{1}^{2}} & \frac{\partial^{2} f}{\partial x_{1} \partial x_{2}} & \cdots & \frac{\partial^{2} f}{\partial x_{1} \partial x_{n}} \ \frac{\partial^{2} f}{\partial x_{2} \partial x_{1}} & \frac{\partial^{2} f}{\partial x_{2}^{2}} & \cdots & \frac{\partial^{2} f}{\partial x_{2} \partial x_{n}} \ \vdots & \vdots & \ddots & \vdots \ \frac{\partial^{2} f}{\partial x_{n} \partial x_{1}} & \frac{\partial^{2} f}{\partial x_{n} \partial x_{2}} & \cdots & \frac{\partial^{2} f}{\partial x_{n}^{2}} \end{array}\right] H=⎣⎢⎢⎢⎢⎢⎡​∂x12​∂2f​∂x2​∂x1​∂2f​⋮∂xn​∂x1​∂2f​​∂x1​∂x2​∂2f​∂x22​∂2f​⋮∂xn​∂x2​∂2f​​⋯⋯⋱⋯​∂x1​∂xn​∂2f​∂x2​∂xn​∂2f​⋮∂xn2​∂2f​​⎦⎥⎥⎥⎥⎥⎤​

x x x 是 n n n 维的向量, f ( x ) f(x) f(x) 是实数,所以海森矩阵是 n ∗ n n \ast n n∗n 的。

∇ f ( x ) \nabla f(x) ∇f(x) 是 n n n 维梯度向量,这和反向传播一样。

这个方法收敛速度很快,可以进行更高效的参数更新。在这个公式中是没有学习率这个超参数的,这相较于一阶方法是一个巨大的优势。

然而上述更新方法很难运用到实际的深度学习应用中去,这是因为计算(以及求逆)Hessian 矩阵操作非常耗费时间和空间。这样,各种各样的拟-牛顿法就被发明出来用于近似转置 Hessian 矩阵。

在这些方法中最流行的是 L-BFGS,该方法使用随时间的梯度中的信息来隐式地近似(也就是说整个矩阵是从来没有被计算的)。

然而,即使解决了存储空间的问题,L-BFGS 应用的一个巨大劣势是需要对整个训练集进行计算,而整个训练集一般包含几百万的样本。和小批量随机梯度下降(mini-batch SGD)不同,让 L-BFGS 在小批量上运行起来是很需要技巧,同时也是研究热点。

1.10 实际应用

Tips:默认选择 Adam;如果可以承担全批量更新,可以尝试使用 L-BFGS。

2.正则化

关于正则化的详细知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面中关于正则化的讲解。

2.1 正则化的动机

当我们增加神经网络隐藏层的数量和尺寸时,网络的容量会上升,即神经元可以合作表达许多复杂函数。例如,如果有一个在二维平面上的二分类问题。我们可以训练 3 个不同的神经网络,每个网络都只有一个隐藏层,但是隐藏层的神经元数目不同,结果如下:

正则化; 隐藏层神经元数目不同

在上图中,可以看见有更多神经元的神经网络可以表达更复杂的函数。然而这既是优势也是不足:

  • 优势是可以分类更复杂的数据
  • 不足是可能造成对训练数据的过拟合。

过拟合(Overfitting) 是网络对数据中的噪声有很强的拟合能力,而没有重视数据间(假设)的潜在基本关系。比如上图中:

  • 有 20 个神经元隐层的网络拟合了所有的训练数据,但是其代价是把决策边界变成了许多不相连的红绿区域。
  • 有 3 个神经元的模型的表达能力只能用比较宽泛的方式去分类数据。它将数据看做是两个大块,并把个别在绿色区域内的红色点看做噪声。在实际中,这样可以在测试数据中获得更好的泛化(generalization)能力。

那是不是说 「如果数据不是足够复杂,则小一点的网络似乎更好,因为可以防止过拟合」?

不是的,防止神经网络的过拟合有很多方法(L2 正则化,Dropout 和输入噪音等)。在实践中,使用这些方法来控制过拟合比减少网络神经元数目要好得多。

不应该因为害怕出现过拟合而使用小网络。相反,应该尽可能使用大网络,然后使用正则化技巧来控制过拟合。

正则化; 改变正则化强度

上图每个神经网络都有 20 个隐藏层神经元,但是随着正则化强度增加,网络的决策边界变得更加平滑。所以,正则化强度是控制神经网络过拟合的好方法

ConvNetsJS demo 上有一个小例子大家可以练练手。

2.2 正则化方法

有不少方法是通过控制神经网络的容量来防止其过拟合的:

L2 正则化:最常用的正则化,通过惩罚目标函数中所有参数的平方实现。

  • 对于网络中的每个权重 w w w,向目标函数中增加一个 1 2 λ w 2 \frac{1}{2} \lambda w² 21​λw2,1/2 为了方便求导, λ \lambda λ 是正则强度。
  • L2 正则化可以直观理解为它对于大数值的权重向量进行严厉惩罚,倾向于更加分散的权重向量。使网络更倾向于使用所有输入特征,而不是严重依赖输入特征中某些小部分特征。

L1 正则化:是另一个相对常用的正则化方法,对于每个 w w w 都向目标函数增加一个 λ ∣ w ∣ \lambda \mid w \mid λ∣w∣。

  • L1 正则化会让权重向量在最优化的过程中变得稀疏(即非常接近 0 0 0)。在实践中,如果不是特别关注某些明确的特征选择,一般说来 L2 正则化都会比 L1 正则化效果好。
  • L1 和 L2 正则化也可以进行组合: λ 1 ∣ w ∣ + λ 2 w 2 \lambda_1 \mid w \mid + \lambda_2 w² λ1​∣w∣+λ2​w2,称作 Elastic net regularization。

最大范式约束(Max norm constraints):要求权重向量 $w $ 必须满足 L2 范式 ∥ w ⃗ ∥ 2 < c \Vert \vec{w} \Vert_2 < c ∥w ∥2​<c, c c c 一般是 3 或 4。这种正则化还有一个良好的性质,即使在学习率设置过高的时候,网络中也不会出现数值「爆炸」,这是因为它的参数更新始终是被限制着的。

但是在神经网络中,最常用的正则化方式叫做 Dropout,下面我们详细展开介绍一下。

2.3 随机失活(Dropout)

1) Dropout 概述

Dropout 是一个简单又极其有效的正则化方法,由 Srivastava 在论文 [Dropout: A Simple Way to Prevent Neural Networks from Overfitting](http://www.cs.toronto.edu/~rsalakhu/papers/srivastava14a.pdf) 中提出,与 L1 正则化、L2 正则化和最大范式约束等方法互为补充。

在训练的时候,随机失活的实现方法是让神经元以超参数 p p p (一般是 0.5 0.5 0.5)的概率被激活或者被设置为 0 0 0 。常用在全连接层。

Dropout 随机失活; 核心思路

一个三层的神经网络 Dropout 示例代码实现:

""" 普通版随机失活"""

p = 0.5   # 神经元被激活的概率。p 值越高,失活数目越少

def train_step(X):
  """ X 中是输入数据 """
  # 前向传播
  H1 = np.maximum(0, np.dot(W1, X) + b1)
  U1 = np.random.rand(*H1.shape) < p # 第一个随机失活掩模
  # rand 可以返回一个或一组服从“0~1”均匀分布的随机样本值
  # 矩阵中满足小于 p 的元素为 True,不满足 False
  # rand()函数的参数是两个或一个整数,不是元组,所以需要*H1.shape 获取行列
  H1 *= U1 # U1 中 False 的 H1 对应位置置零
  H2 = np.maximum(0, np.dot(W2, H1) + b2)
  U2 = np.random.rand(*H2.shape) < p # 第二个随机失活掩模
  H2 *= U2 # drop!
  out = np.dot(W3, H2) + b3

  # 反向传播:计算梯度... (略)
  # 进行参数更新... (略) 

在上面的代码中,train_step 函数在第一个隐层和第二个隐层上进行了两次随机失活。在输入层上面进行随机失活也是可以的,为此需要为输入数据 X X X 创建一个二值(要么激活要么失活)的掩模。反向传播几乎保持不变,只需回传梯度乘以掩模得到 Dropout 层的梯度。

2) Dropout 的理解

为什么这个想法可取呢?一个解释是防止特征间的相互适应:

  • 比如每个神经元学到了猫的一个特征比如尾巴、胡须、爪子等,将这些特征全部组合起来可以判断是一只猫。
  • 加入随机失活后就只能依赖一些零散的特征去判断不能使用所有特征,这样可以一定程度上抑制过拟合。不然训练时正确率很高,测试时却很低。

Dropout 随机失活; 防止特征间的互相适应

另一个比较合理的解释是:

  • 在训练过程中,随机失活可以被认为是对完整的神经网络抽样出一些子集,每次基于输入数据只更新子网络的参数。
  • 每个二值掩模都是一个模型,有 n n n 个神经元的网络有 2 n 2n 2n 种掩模。Dropout 相当于数量巨大的网络模型(共享参数)在同时被训练。

Dropout 随机失活; 相当于抽样出一些子集

3) 测试时避免随机失活

在训练过程中,失活是随机的,但是在测试过程中要避免这种随机性,所以不使用随机失活,要对数量巨大的子网络们做模型集成(model ensemble),以此来计算出一个预测期望。

比如只有一个神经元 a a a:

Dropout 随机失活; 测试时避免随机失活

测试的时候由于不使用随机失活所以:

E ( a ) = w 1 x + w 2 y \text{E}(a)=w_1x+w_2y E(a)=w1​x+w2​y

假如训练时随机失活的概率为 0.5 0.5 0.5,那么:

E ( a ) = 1 4 ( w 1 x + w 2 y ) + 1 2 ( w 1 x + w 2 ⋅ 0 ) + 1 2 ( w 1 x ⋅ 0 + w 2 ) + 1 4 ⋅ 0 = 1 2 ( w 1 x + w 2 y ) \text{E}(a)=\frac{1}{4}(w_1x+w_2y)+\frac{1}{2}(w_1x+w_2\cdot 0)+\frac{1}{2}(w_1x\cdot 0+w_2)+\frac{1}{4}\cdot 0=\frac{1}{2}(w_1x+w_2y) E(a)=41​(w1​x+w2​y)+21​(w1​x+w2​⋅0)+21​(w1​x⋅0+w2​)+41​⋅0=21​(w1​x+w2​y)

所以一个不确切但是很实用的做法是在测试时承随机失活概率,这样就能保证预测时的输出和训练时的期望输出一致。所以测试代码:

def predict(X):
  # 前向传播时模型集成
  H1 = np.maximum(0, np.dot(W1, X) + b1) * p # 注意:激活数据要乘以 p
  H2 = np.maximum(0, np.dot(W2, H1) + b2) * p # 注意:激活数据要乘以 p
  out = np.dot(W3, H2) + b3 

上述操作不好的地方是必须在测试时对激活数据按照失活概率 p p p 进行数值范围调整。测试阶段性能是非常关键的,因此实际操作时更倾向使用反向随机失活(inverted dropout)

  • 在训练时就进行数值范围调整,从而让前向传播在测试时保持不变。

反向随机失活还有一个好处,无论是否在训练时使用 Dropout,预测的代码可以保持不变。参考实现代码如下:

""" 
反向随机失活: 推荐实现方式.
在训练的时候 drop 和调整数值范围,测试时不做任何事.
"""
p = 0.5
def train_step(X):
  # 前向传播
  H1 = np.maximum(0, np.dot(W1, X) + b1)
  U1 = (np.random.rand(*H1.shape) < p) / p # 第一个随机失活遮罩. 注意/p!
  H1 *= U1 # drop!
  H2 = np.maximum(0, np.dot(W2, H1) + b2)
  U2 = (np.random.rand(*H2.shape) < p) / p # 第二个随机失活遮罩. 注意/p!
  H2 *= U2 # drop!
  out = np.dot(W3, H2) + b3

def predict(X):
  # 前向传播时模型集成
  H1 = np.maximum(0, np.dot(W1, X) + b1) # 不用数值范围调整了
  H2 = np.maximum(0, np.dot(W2, H1) + b2)
  out = np.dot(W3, H2) + b3 

在更一般化的分类上,随机失活属于网络在前向传播中有随机行为的方法。这种在训练过程加入随机性,然后在测试过程中对这些随机性进行平均或近似的思想在很多地方都能见到:

  • 批量归一化:训练时的均值和方差来自随机的小批量;测试时使用的是整个训练过程中的经验方差和均值。

  • 数据增强(data augmentation) :比如一张猫的图片进行训练时,可以随机的裁剪翻转等操作再训练,然后测试过程再对一些固定的位置(四个角、中心及翻转)进行测试。也可以在训练的时候随机改变亮度对比度,色彩抖动 PCA 降维等。

  • DropConnect:另一个与 Dropout 类似的研究是 DropConnect,它在前向传播的时候,一系列权重被随机设置为 0 0 0 。

  • 部分最大池化(Fractional Max Pooling) :训练时随机区域池化,测试时固定区域或者取平均值。这个方法并不常用。

  • 随机深度(Stochastic Depth) :一个比较深的网络,训练时随机选取部分层去训练,测试时使用全部的层。这个研究非常前沿。

总之,这些方法都是在训练的时候增加随机噪声,测试时通过分析法(在使用随机失活的本例中就是乘以 p p p)或数值法(例如通过抽样出很多子网络,随机选择不同子网络进行前向传播,最后对它们取平均)将噪音边缘化。

4) 实践经验

一些常用的实践经验方法:

  • 可以通过交叉验证获得一个全局使用的 L2 正则化系数。
  • 使用 L2 正则化的同时在所有层后面使用随机失活

随机失活 p p p 值一般默认设为 0.5 0.5 0.5,也可能在验证集上调参

3.迁移学习(Transfer Learning)

关于迁移学习的详细知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 AI 应用实践策略(下) 中关于正则化的讲解。

另一个导致过拟合的原因可能是训练样本过少,这时可以使用迁移学习来解决这个问题,它允许使用很少的数据来训练 CNN。

3.1 迁移学习的思想

迁移学习; Transfer Learning

  • 第①步:在大量的数据集上训练一个 CNN,得到模型(比如使用 ImageNet,有 1000 个分类)
  • 第②步:使用一个少量的数据集,最后需要的得到的分类也不再是 1000 而是一个较小的值 C C C,比如 10。这时最后一个全连接层的参数矩阵变成 4096 × C 4096 \times C 4096×C,初始化这个矩阵,重新训练这个线性分类器,保持前面的所有层不变,因为前面的层已经训练好了,有了泛化能力。
  • 第③步:当得到较多的训练集后,训练的层数可以增多,比如可以训练最后三个全连接层。可以使用较低的学习率微调参数。

迁移学习; Transfer Learning

3.2 应用

在目标检测和图像标记中都会使用迁移学习,图像处理部分都使用一个已经用 ImageNet 数据预训练好的 CNN 模型,然后根据具体的任务微调这些参数。

所以对一批数据集感兴趣但是数量不够时,可以在网上找一个数据很相似的有大量数据的训练模型,然后针对自己的问题微调或重新训练某些层。一些常用的深度学习软件包都含有已经训练好的模型,直接应用就好。

4.模型集成(Model Ensembles)

在实践的时候,有一个总是能提升神经网络几个百分点准确率的办法,就是在训练的时候训练几个独立的模型,然后在测试的时候平均它们预测结果。

集成的模型数量增加,算法的结果也单调提升(但提升效果越来越少)。

模型之间的差异度越大,提升效果可能越好。

进行集成有以下几种方法:

  • 同一个模型,不同的初始化。使用交叉验证来得到最好的超参数,然后用最好的参数来训练不同初始化条件的模型。这种方法的风险在于多样性只来自于不同的初始化条件。
  • 在交叉验证中发现最好的模型。使用交叉验证来得到最好的超参数,然后取其中最好的几个(比如 10 个)模型来进行集成。这样就提高了集成的多样性,但风险在于可能会包含不够理想的模型。在实际操作中,这样操作起来比较简单,在交叉验证后就不需要额外的训练了。
  • 一个模型设置多个记录点 ( checkpoints ) 。如果训练非常耗时,那就在不同的训练时间对网络留下记录点(比如每个周期结束),然后用它们来进行模型集成。很显然,这样做多样性不足,但是在实践中效果还是不错的,这种方法的优势是代价比较小。
  • 在训练的时候跑参数的平均值。和上面一点相关的,还有一个也能得到 1-2 个百分点的提升的小代价方法,这个方法就是在训练过程中,如果损失值相较于前一次权重出现指数下降时,就在内存中对网络的权重进行一个备份。这样你就对前几次循环中的网络状态进行了平均。你会发现这个「平滑」过的版本的权重总是能得到更少的误差。直观的理解就是目标函数是一个碗状的,你的网络在这个周围跳跃,所以对它们平均一下,就更可能跳到中心去。

5.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=7

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

6.要点总结

  • 优化方式:SGD、动量更新、Nesterov 动量、Adagrad、RMSProp、Adam 等,一般无脑使用 Adam。此外还有学习率退火和二阶方法。
  • 正则化:L2 比较常用,Dropout 也是一个很好的正则方法。
  • 数据较少时可以使用迁移学习。
  • 模型集成。

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(8) | 常见深度学习框架介绍(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125025115

ShowMeAI 研究中心


深度学习与计算机视觉

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

大家在前序文章中学习了很多关于神经网络的原理知识和实战技巧,在本篇内容中 ShowMeAI 给大家展开介绍深度学习硬件知识,以及目前主流的深度学习框架 TensorFlow 和 pytorch 相关知识,借助于工具大家可以实际搭建与训练神经网络。

本篇重点

  • 深度学习硬件
    • CPU、GPU、TPU
  • 深度学习框架
    • PyTorch / TensorFlow
  • 静态与动态计算图

1.深度学习硬件

GPU(Graphics Processing Unit)是图形处理单元(又称显卡),在物理尺寸上就比 CPU(Central Processing Unit)大得多,有自己的冷却系统。最初用于渲染计算机图形,尤其是游戏。在深度学习上选择 NVIDIA(英伟达)的显卡,如果使用 AMD 的显卡会遇到很多问题。TPU(Tensor Processing Units)是专用的深度学习硬件。

1.1 CPU / GPU / TPU

深度学习硬件; CPU / GPU / TPU

  • CPU一般有多个核心,每个核心速度都很快都可以独立工作,可同时进行多个进程,内存与系统共享,完成序列任务时很有用。图上 CPU 的运行速度是每秒约 540 GFLOPs 浮点数运算,使用 32 位浮点数(注:一个 GFLOPS(gigaFLOPS)等于每秒十亿( = 1 0 9 =10⁹ =109)次的浮点运算)。
  • GPU有上千个核心数,但每个核心运行速度很慢,也不能独立工作,适合大量的并行完成类似的工作。GPU 一般自带内存,也有自己的缓存系统。图上 GPU 的运行速度是 CPU 的 20 多倍。
  • TPU是专门的深度学习硬件,运行速度非常快。TITANV 在技术上并不是一个「TPU」,因为这是一个谷歌术语,但两者都有专门用于深度学习的硬件。运行速度非常快。

若是将这些运行速度除以对应的价格,可得到下图:

深度学习硬件; 每美元对应运行速度

1.2 GPU 的优势与应用

GPU 在大矩阵的乘法运算中有很明显的优势。

GPU 的优势; 加速大矩阵运算

由于结果中的每一个元素都是相乘的两个矩阵的每一行和每一列的点积,所以并行的同时进行这些点积运算速度会非常快。卷积神经网络也类似,卷积核和图片的每个区域进行点积也是并行运算。

CPU 虽然也有多个核心,但是在大矩阵运算时只能串行运算,速度很慢。

可以写出在 GPU 上直接运行的代码,方法是使用 NVIDIA 自带的抽象代码 CUDA ,可以写出类似 C 的代码,并可以在 GPU 直接运行。

但是直接写 CUDA 代码是一件非常困难的事,好在可以直接使用 NVIDIA 已经高度优化并且开源的 API,比如 cuBLAS 包含很多矩阵运算, cuDNN 包含 CNN 前向传播、反向传播、批量归一化等操作;还有一种语言是 OpenCL,可以在 CPU、AMD 上通用,但是没人做优化,速度很慢;HIP 可以将 CUDA 代码自动转换成可以在 AMD 上运行的语言。以后可能会有跨平台的标准,但是现在来看 CUDA 是最好的选择。

在实际应用中,同样的计算任务,GPU 比 CPU 要快得多,当然 CPU 还能进一步优化。使用 cuDNN 也比不使用要快接近三倍。

GPU 的优势; CPU V.S. GPU

cuDNN 的优势; 运行时间对比

实际应用 GPU 还有一个问题是训练的模型一般存放在 GPU,而用于训练的数据存放在硬盘里,由于 GPU 运行快,而机械硬盘读取慢,就会拖累整个模型的训练速度。有多种解决方法:

  • 如果训练数据数量较小,可以把所有数据放到 GPU 的 RAM 中;
  • 用固态硬盘代替机械硬盘;
  • 使用多个 CPU 线程预读取数据,放到缓存供 GPU 使用。

2.深度学习软件

2.1 DL 软件概述

现在有很多种深度学习框架,目前最流行的是 TensorFlow。

第一代框架大多由学术界编写的,比如 Caffe 就是伯克利大学开发的。

第二代往往由工业界主导,比如 Caffe2 是由 Facebook 开发。这里主要讲解 PyTorch 和 TensorFlow。

深度学习软件; Caffe、PyTorch 和 TensorFlow

回顾之前计算图的概念,一个线性分类器可以用计算图表示,网络越复杂,计算图也越复杂。之所以使用这些深度学习框架有三个原因:

  • 构建大的计算图很容易,可以快速的开发和测试新想法;
  • 这些框架都可以自动计算梯度只需写出前向传播的代码;
  • 可以在 GPU 上高效的运行,已经扩展了 cuDNN 等包以及处理好数据如何在 CPU 和 GPU 中流动。

这样我们就不用从头开始完成这些工作了。

比如下面的一个计算图:

深度学习软件; 计算图示例

我们以前的做法是使用 Numpy 写出前向传播,然后计算梯度,代码如下:

import numpy as np
np.random.seed(0)  # 保证每次的随机数一致

N, D = 3, 4

x = np.random.randn(N, D)
y = np.random.randn(N, D)
z = np.random.randn(N, D)

a = x * y
b = a + z
c = np.sum(b)

grad_c = 1.0
grad_b = grad_c * np.ones((N, D))
grad_a = grad_b.copy()
grad_z = grad_b.copy()
grad_x = grad_a * y
grad_y = grad_a * x 

这种做法 API 干净,易于编写代码,但问题是没办法在 GPU 上运行,并且需要自己计算梯度。所以现在大部分深度学习框架的主要目标是自己写好前向传播代码,类似 Numpy,但能在 GPU 上运行且可以自动计算梯度。

TensorFlow 版本,前向传播构建计算图,梯度可以自动计算:

import numpy as np
np.random.seed(0)
import tensorflow as tf

N, D = 3, 4

# 创建前向计算图
x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)
z = tf.placeholder(tf.float32)

a = x * y
b = a + z
c = tf.reduce_sum(b)

# 计算梯度
grad_x, grad_y, grad_z = tf.gradients(c, [x, y, z])

with tf.Session() as sess:
    values = {
        x: np.random.randn(N, D),
        y: np.random.randn(N, D),
        z: np.random.randn(N, D),
    }
    out = sess.run([c, grad_x, grad_y, grad_z], feed_dict=values)
    c_val, grad_x_val, grad_y_val, grad_z_val = out
    print(c_val)
    print(grad_x_val) 

PyTorch 版本,前向传播与 Numpy 非常类似,但反向传播可以自动计算梯度,不用再去实现。

import torch

device = 'cuda:0'  # 在 GPU 上运行,即构建 GPU 版本的矩阵

# 前向传播与 Numpy 类似
N, D = 3, 4
x = torch.randn(N, D, requires_grad=True, device=device)
# requires_grad 要求自动计算梯度,默认为 True
y = torch.randn(N, D, device=device)
z = torch.randn(N, D, device=device)

a = x * y
b = a + z
c = torch.sum(b)

c.backward()  # 反向传播可以自动计算梯度
print(x.grad)
print(y.grad)
print(z.grad) 

可见这些框架都能自动计算梯度并且可以自动在 GPU 上运行。

2.2 TensoFlow

关于 TensorFlow 的用法也可以阅读ShowMeAI的制作的 TensorFlow 速查表,对应文章AI 建模工具速查 | TensorFlow 使用指南AI 建模工具速查 | Keras 使用指南

下面以一个两层的神经网络为例,非线性函数使用 ReLU 函数、损失函数使用 L2 范式(当然仅仅是一个学习示例)。

TensorFlow; 两层神经网络计算图

实现代码如下:

1) 神经网络

import numpy as np
import tensorflow as tf

N, D , H = 64, 1000, 100

# 创建前向计算图
x = tf.placeholder(tf.float32, shape=(N, D))
y = tf.placeholder(tf.float32, shape=(N, D))
w1 = tf.placeholder(tf.float32, shape=(D, H))
w2 = tf.placeholder(tf.float32, shape=(H, D))

h = tf.maximum(tf.matmul(x, w1), 0)  # 隐藏层使用折叶函数
y_pred = tf.matmul(h, w2)
diff = y_pred - y  # 差值矩阵
loss = tf.reduce_mean(tf.reduce_sum(diff ** 2, axis=1))  # 损失函数使用 L2 范数

# 计算梯度
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2])

# 多次运行计算图
with tf.Session() as sess:
    values = {
        x: np.random.randn(N, D),
        y: np.random.randn(N, D),
        w1: np.random.randn(D, H),
        w2: np.random.randn(H, D),
    }
    out = sess.run([loss, grad_w1, grad_w2], feed_dict=values)
    loss_val, grad_w1_val, grad_w2_val = out 

整个过程可以分成两部分,with 之前部分定义计算图,with 部分多次运行计算图。这种模式在 TensorFlow 中很常见。

  • 首先,我们创建了x,y,w1,w2四个 tf.placeholder 对象,这四个变量作为「输入槽」,下面再输入数据。
  • 然后使用这四个变量创建计算图,使用矩阵乘法 tf.matmul 和折叶函数 tf.maximum 计算 y_pred ,使用 L2 距离计算 s 损失。但是目前并没有实际的计算,因为只是构建了计算图并没有输入任何数据。
  • 然后通过一行神奇的代码计算损失值关于 w1w2 的梯度。此时仍然没有实际的运算,只是构建计算图,找到 loss 关于 w1w2 的路径,在原先的计算图上增加额外的关于梯度的计算。
  • 完成计算图后,创建一个会话 Session 来运行计算图和输入数据。进入到 Session 后,需要提供 Numpy 数组给上面创建的「输入槽」。
  • 最后两行代码才是真正的运行,执行 sess.run 需要提供 Numpy 数组字典 feed_dict和需要输出的计算值 loss ,grad_w1,grad_w2` ,最后通过解包获取 Numpy 数组。

上面的代码只是运行了一次,我们需要迭代多次,并设置超参数、参数更新方式等:

with tf.Session() as sess:
    values = {
        x: np.random.randn(N, D),
        y: np.random.randn(N, D),
        w1: np.random.randn(D, H),
        w2: np.random.randn(H, D),
    }
    learning_rate = 1e-5
    for t in range(50):
        out = sess.run([loss, grad_w1, grad_w2], feed_dict=values)
        loss_val, grad_w1_val, grad_w2_val = out
        values[w1] -= learning_rate * grad_w1_val
        values[w2] -= learning_rate * grad_w2_val 

这种迭代方式有一个问题是每一步需要将 Numpy 和数组提供给 GPU,GPU 计算完成后再解包成 Numpy 数组,但由于 CPU 与 GPU 之间的传输瓶颈,非常不方便。

解决方法是将 w1w2 作为变量而不再是「输入槽」,变量可以一直存在于计算图上。

由于现在 w1w2 变成了变量,所以就不能从外部输入 Numpy 数组来初始化,需要由 TensorFlow 来初始化,需要指明初始化方式。此时仍然没有具体的计算。

w1 = tf.Variable(tf.random_normal((D, H)))
w2 = tf.Variable(tf.random_normal((H, D))) 

现在需要将参数更新操作也添加到计算图中,使用赋值操作 assign 更新 w1w2,并保存在计算图中(位于计算梯度后面):

learning_rate = 1e-5
new_w1 = w1.assign(w1 - learning_rate * grad_w1)
new_w2 = w2.assign(w2 - learning_rate * grad_w2) 

现在运行这个网络,需要先运行一步参数的初始化 tf.global_variables_initializer(),然后运行多次代码计算损失值:

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    values = {
        x: np.random.randn(N, D),
        y: np.random.randn(N, D),
    }
    for t in range(50):
        loss_val, = sess.run([loss], feed_dict=values) 

2) 优化器

上面的代码,实际训练过程中损失值不会变。

原因是我们执行的 sess.run([loss], feed_dict=values) 语句只会计算 loss,TensorFlow 非常高效,与损失值无关的计算一律不会进行,所以参数就无法更新。

一个解决办法是在执行 run 时加入计算两个参数,这样就会强制执行参数更新,但是又会产生 CPU 与 GPU 的通信问题。

一个技巧是在计算图中加入两个参数的依赖,在执行时需要计算这个依赖,这样就会让参数更新。这个技巧是 group 操作,执行完参数赋值操作后,执行 updates = tf.group(new_w1, new_w2),这个操作会在计算图上创建一个节点;然后执行的代码修改为 loss_val, _ = sess.run([loss, updates], feed_dict=values),在实际运算时,updates 返回值为空。

这种方式仍然不够方便,好在 TensorFlow 提供了更便捷的操作,使用自带的优化器。优化器需要提供学习率参数,然后进行参数更新。有很多优化器可供选择,比如梯度下降、Adam 等。

optimizer = tf.train.GradientDescentOptimizer(1e-5)  # 使用优化器
updates = optimizer.minimize(loss)  # 更新方式是使 loss 下降,内部其实使用了 group 

执行的代码也是:loss_val, _ = sess.run([loss, updates], feed_dict=values)

3) 损失

计算损失的代码也可以使用 TensorFlow 自带的函数:

loss = tf.losses.mean_squared_error(y_pred, y)  # 损失函数使用 L2 范数 

4) 层

目前仍有一个很大的问题是 x,y,w1,w2 的形状需要我们自己去定义,还要保证它们能正确连接在一起,此外还有偏差。如果使用卷积层、批量归一化等层后,这些定义会更加麻烦。

TensorFlow 可以解决这些麻烦:

N, D , H = 64, 1000, 100
x = tf.placeholder(tf.float32, shape=(N, D))
y = tf.placeholder(tf.float32, shape=(N, D))

init = tf.variance_scaling_initializer(2.0)  # 权重初始化使用 He 初始化
h = tf.layers.dense(inputs=x, units=H, activation=tf.nn.relu, kernel_initializer=init)
# 隐藏层使用折叶函数
y_pred = tf.layers.dense(inputs=h, units=D, kernel_initializer=init)

loss = tf.losses.mean_squared_error(y_pred, y)  # 损失函数使用 L2 范数

optimizer = tf.train.GradientDescentOptimizer(1e-5)
updates = optimizer.minimize(loss)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    values = {
        x: np.random.randn(N, D),
        y: np.random.randn(N, D),
    }
    for t in range(50):
        loss_val, _ = sess.run([loss, updates], feed_dict=values) 

上面的代码,x,y 的初始化没有变化,但是参数 w1,w2 隐藏起来了,初始化使用 He 初始化。

前向传播的计算使用了全连接层 tf.layers.dense,该函数需要提供输入数据 inputs、该层的神经元数目 units、激活函数 activation、卷积核(权重)初始化方式 kernel_initializer 等参数,可以自动设置权重和偏差。

5) High level API:tensorflow.keras

Keras 是基于 TensorFlow 的更高层次的封装,会让整个过程变得简单,曾经是第三方库,现在已经被内置到了 TensorFlow。

使用 Keras 的部分代码如下,其他与上文一致:

N, D , H = 64, 1000, 100
x = tf.placeholder(tf.float32, shape=(N, D))
y = tf.placeholder(tf.float32, shape=(N, D))

model = tf.keras.Sequential()  # 使用一系列层的组合方式
# 添加一系列的层
model.add(tf.keras.layers.Dense(units=H, input_shape=(D,), activation=tf.nn.relu))
model.add(tf.keras.layers.Dense(D))
# 调用模型获取结果
y_pred = model(x)
loss = tf.losses.mean_squared_error(y_pred, y) 

这种模型已经简化了很多工作,最终版本代码如下:

import numpy as np
import tensorflow as tf

N, D , H = 64, 1000, 100

# 创建模型,添加层
model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(units=H, input_shape=(D,), activation=tf.nn.relu))
model.add(tf.keras.layers.Dense(D))

# 配置模型:损失函数、参数更新方式
model.compile(optimizer=tf.keras.optimizers.SGD(lr=1e-5), loss=tf.keras.losses.mean_squared_error)

x = np.random.randn(N, D)
y = np.random.randn(N, D)

# 训练
history = model.fit(x, y, epochs=50, batch_size=N) 

代码非常简洁:

  • 定义模型tf.keras.Sequential() 表明模型是一系列的层,然后添加两个全连接层,并设置激活函数、每层的神经元数目等;
  • 配置模型:用 model.compile 方法配置模型的优化器、损失函数等;
  • 基于数据训练模型:使用 model.fit,需要设置迭代周期次数、批量数等,可以直接用原始数据训练模型。

6) 其他知识

① 常见的拓展包

② 预训练模型

TensorFlow 已经有一些预训练好的模型可以直接拿来用,利用迁移学习,微调参数。

③ Tensorboard

  • 增加日志记录损失值和状态
  • 绘制图像

TensorFlow; Tensorboard 绘制 loss 图

④ 分布式操作

可以在多台机器上运行,谷歌比较擅长。

⑤ TPU(Tensor Processing Units)

TPU 是专用的深度学习硬件,运行速度非常快。Google Cloud TPU 算力为 180 TFLOPs ,NVIDIA Tesla V100 算力为 125 TFLOPs。

TensorFlow; 谷歌云 TPU

⑥Theano

TensorFlow 的前身,二者许多地方都很相似。

2.3 PyTorch

关于 PyTorch 的用法也可以阅读ShowMeAI的制作的 PyTorch 速查表,对应文章AI 建模工具速查 | Pytorch 使用指南

1) 基本概念

  • Tensor:与 Numpy 数组很相似,只是可以在 GPU 上运行;
  • Autograd:使用 Tensors 构建计算图并自动计算梯度的包;
  • Module:神经网络的层,可以存储状态和可学习的权重。

下面的代码使用的是 v0.4 版本。

2) Tensors

下面使用 Tensors 训练一个两层的神经网络,激活函数使用 ReLU、损失使用 L2 损失。

PyTorch; Tensors

代码如下:

import torch

# cpu 版本
device = torch.device('cpu')
#device = torch.device('cuda:0')  # 使用 gpu

# 为数据和参数创建随机的 Tensors
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in, device=device)
y = torch.randn(N, D_out, device=device)
w1 = torch.randn(D_in, H, device=device)
w2 = torch.randn(H, D_out, device=device)

learning_rate = 1e-6
for t in range(500):
    # 前向传播,计算预测值和损失
    h = x.mm(w1)
    h_relu = h.clamp(min=0)
    y_pred = h_relu.mm(w2)
    loss = (y_pred - y).pow(2).sum()

    # 反向传播手动计算梯度
    grad_y_pred = 2.0 * (y_pred - y)
    grad_w2 = h_relu.t().mm(grad_y_pred)
    grad_h_relu = grad_y_pred.mm(w2.t())
    grad_h = grad_h_relu.clone()
    grad_h[h < 0] = 0
    grad_w1 = x.t().mm(grad_h)

    # 梯度下降,参数更新
    w1 -= learning_rate * grad_w1
    w2 -= learning_rate * grad_w2 
  • 首先创建 x,y,w1,w2的随机 tensor,与 Numpy 数组的形式一致
  • 然后前向传播计算损失值和预测值
  • 然后手动计算梯度
  • 最后更新参数

上述代码很简单,和 Numpy 版本的写法很接近。但是需要手动计算梯度。

3) Autograd 自动梯度计算

PyTorch 可以自动计算梯度:

import torch

# 创建随机 tensors
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
    # 前向传播
    y_pred = x.mm(w1).clamp(min=0).mm(w2)
    loss = (y_pred - y).pow(2).sum()
    # 反向传播
    loss.backward()
    # 参数更新
    with torch.no_grad():
        w1 -= learning_rate * w1.grad
        w2 -= learning_rate * w2.grad
        w1.grad.zero_()
        w2.grad.zero_() 

与上一版代码的主要区别是:

  • 创建 w1,w2 时要求 requires_grad=True,这样会自动计算梯度,并创建计算图。x1,x2 不需要计算梯度。
  • 前向传播与之前的类似,但现在不用保存节点,PyTorch 可以帮助我们跟踪计算图。
  • 使用 loss.backward() 自动计算要求的梯度。
  • 按步对权重进行更新,然后将梯度归零。 Torch.no_grad 的意思是「不要为这部分构建计算图」。以下划线结尾的 PyTorch 方法是就地修改 Tensor,不返回新的 Tensor。

TensorFlow 与 PyTorch 的区别是 TensorFlow 需要先显式的构造一个计算图,然后重复运行;PyTorch 每次做前向传播时都要构建一个新的图,使程序看起来更加简洁。

PyTorch 支持定义自己的自动计算梯度函数,需要编写 forwardbackward 函数。与作业中很相似。可以直接用到计算图上,但是实际上自己定义的时候并不多。

PyTorch; Autograd 自动梯度计算

4) NN

与 Keras 类似的高层次封装,会使整个代码变得简单。

import torch

N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# 定义模型
model = torch.nn.Sequential(torch.nn.Linear(D_in, H),
                            torch.nn.ReLu(),
                            torch.nn.Linear(H, D_out))

learning_rate = 1e-2
for t in range(500):
    # 前向传播
    y_pred = model(x)
    loss = torch.nn.functional.mse_loss(y_pred, y)
    # 计算梯度
    loss.backward()

    with torch.no_grad():
        for param in model.parameters():
            param -= learning_rate * param.grad
    model.zero_grad() 
  • 定义模型是一系列的层组合,在模型中定义了层对象比如全连接层、折叶层等,里面包含可学习的权重;
  • 前向传播将数据给模型就可以直接计算预测值,进而计算损失;torch.nn.functional 含有很多有用的函数,比如损失函数;
  • 反向传播会计算模型中所有权重的梯度;
  • 最后每一步都更新模型的参数。

5) Optimizer

PyTorch 同样有自己的优化器:

import torch

N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# 定义模型
model = torch.nn.Sequential(torch.nn.Linear(D_in, H),
                            torch.nn.ReLu(),
                            torch.nn.Linear(H, D_out))
# 定义优化器
learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 迭代
for t in range(500):
    y_pred = model(x)
    loss = torch.nn.functional.mse_loss(y_pred, y)

    loss.backward()
    # 更新参数
    optimizer.step()
    optimizer.zero_grad() 
  • 使用不同规则的优化器,这里使用 Adam;
  • 计算完梯度后,使用优化器更新参数,再置零梯度。

6) 定义新的模块

PyTorch 中一个模块就是一个神经网络层,输入和输出都是 tensors。模块中可以包含权重和其他模块,可以使用 Autograd 定义自己的模块。

比如可以把上面代码中的两层神经网络改成一个模块:

import torch
# 定义上文的整个模块为单个模块
class TwoLayerNet(torch.nn.Module):
    # 初始化两个子模块,都是线性层
    def __init__(self, D_in, H, D_out):
        super(TwoLayerNet, self).__init__()
        self.linear1 = torch.nn.Linear(D_in, H)
        self.linear2 = torch.nn.Linear(H, D_out)
    # 使用子模块定义前向传播,不需要定义反向传播,autograd 会自动处理
    def forward(self, x):
        h_relu = self.linear1(x).clamp(min=0)
        y_pred = self.linear2(h_relu)
        return y_pred

N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 构建模型与训练和之前类似
model = TwoLayerNet(D_in, H, D_out)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
for t in range(500):
    y_pred = model(x)
    loss = torch.nn.functional.mse_loss(y_pred, y)

    loss.backward()
    optimizer.step()
    optimizer.zero_grad() 

这种混合自定义模块非常常见,定义一个模块子类,然后作为作为整个模型的一部分添加到模块序列中。

比如用定义一个下面这样的模块,输入数据先经过两个并列的全连接层得到的结果相乘后经过 ReLU:

class ParallelBlock(torch.nn.Module):
    def __init__(self, D_in, D_out):
        super(ParallelBlock, self).__init__()
        self.linear1 = torch.nn.Linear(D_in, D_out)
        self.linear2 = torch.nn.Linear(D_in, D_out)
    def forward(self, x):
        h1 = self.linear1(x)
        h2 = self.linear2(x)
        return (h1 * h2).clamp(min=0) 

然后在整个模型中应用:

model = torch.nn.Sequential(ParallelBlock(D_in, H),
                            ParallelBlock(H, H),
                            torch.nn.Linear(H, D_out)) 

使用 ParallelBlock 的新模型计算图如下:

PyTorch; ParallelBlock 计算图

7) DataLoader

DataLoader 包装数据集并提供获取小批量数据,重新排列,多线程读取等,当需要加载自定义数据时,只需编写自己的数据集类:

import torch
from torch.utils.data import TensorDataset, DataLoader

N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

loader = DataLoader(TensorDataset(x, y), batch_size=8)
model = TwoLayerNet(D_in, H, D_out)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)

for epoch in range(20):
    for x_batch, y_batch in loader:
        y_pred = model(x_batch)
        loss = torch.nn.functional.mse_loss(y_pred, y_batch)

        loss.backward()
        optimizer.step()
        optimizer.zero_grad() 

上面的代码仍然是两层神经完网络,使用了自定义的模块。这次使用了 DataLoader 来处理数据。最后更新的时候在小批量上更新,一个周期会迭代所有的小批量数据。一般的 PyTorch 模型基本都长成这个样子。

8) 预训练模型

使用预训练模型非常简单:github.com/pytorch/vision

import torch
import torchvision
alexnet = torchvision.models.alexnet(pretrained=True)
vgg16 = torchvision.models.vggl6(pretrained=-True)
resnet101 = torchvision.models.resnet101(pretrained=True) 

9) Visdom

可视化的包,类似 TensorBoard,但是不能像 TensorBoard 一样可视化计算图。

PyTorch; 使用 Visdom 可视化

10) Torch

PyTorch 的前身,不能使用 Python,没有 Autograd,但比较稳定,不推荐使用。

3.静态与动态图(Static vs Dynamic Graphs )

TensorFlow 使用的是静态图(Static Graphs):

  • 构建计算图描述计算,包括找到反向传播的路径;
  • 每次迭代执行计算,都使用同一张计算图。

与静态图相对应的是 PyTorch 使用的动态图(Dynamic Graphs),构建计算图与计算同时进行:

  • 创建 tensor 对象;
  • 每一次迭代构建计算图数据结构、寻找参数梯度路径、执行计算;
  • 每一次迭代抛出计算图,然后再重建。之后重复上一步。

3.1 静态图的优势

使用静态图形,由于一张图需要反复运行很多次,这样框架就有机会在计算图上做优化。

  • 比如下面的自己写的计算图可能经过多次运行后优化成右侧,提高运行效率。

静态图的优势; Static Graphs

静态图只需要构建一次计算图,所以一旦构建好了即使源代码使用 Python 写的,也可以部署在 C++上,不用依赖源代码;而动态图每次迭代都要使用源代码,构件图和运行是交织在一起的。

3.2 动态图的优势

动态图的代码比较简洁,很像 Python 操作。

在条件判断逻辑中,由于 PyTorch 可以动态构建图,所以可以使用正常的 Python 流操作;而 TensorFlow 只能一次性构建一个计算图,所以需要考虑到所有情况,只能使用 TensorFlow 流操作,这里使用的是和条件有关的。

动态图的优势; Dynamic Graphs

在循环结构中,也是如此。

  • PyTorch 只需按照 Python 的逻辑去写,每次会更新计算图而不用管最终的序列有多长;
  • TensorFlow 由于使用静态图必须把这个循环结构显示的作为节点添加到计算图中,所以需要用到 TensorFlow 的循环流 tf.foldl。并且大多数情况下,为了保证只构建一次循环图, TensorFlow 只能使用自己的控制流,比如循环流、条件流等,而不能使用 Python 语法,所以用起来需要学习 TensorFlow 特有的控制命令。

动态图的优势; Dynamic Graphs

3.3 动态图的应用

1) 循环网络(Recurrent Networks)

例如图像描述,需要使用循环网络在一个不同长度序列上运行,我们要生成的用于描述图像的语句是一个序列,依赖于输入数据的序列,即动态的取决于输入句子的长短。

2) 递归网络(Recursive Networks)

用于自然语言处理,递归训练整个语法解析树,所以不仅仅是层次结构,而是一种图或树结构,在每个不同的数据点都有不同的结构,使用 TensorFlow 很难实现。在 PyTorch 中可以使用 Python 控制流,很容易实现。

3) Modular Networks

一种用于询问图片上的内容的网络,问题不一样生成的动态图也就不一样。

3.4 TensorFlow 与 PyTorch 的相互靠拢

TensorFlow 与 PyTorch 的界限越来越模糊,PyTorch 正在添加静态功能,而 TensorFlow 正在添加动态功能。

  • TensorFlow Fold 可以把静态图的代码自动转化成静态图
  • TensorFlow 1.7 增加了 Eager Execution,允许使用动态图
import tensorflow as tf
import tensorflow.contrib.eager as tfe
tf.enable eager _execution()

N, D = 3, 4
x = tfe.Variable(tf.random_normal((N, D)))
y = tfe.Variable(tf.random_normal((N, D)))
z = tfe.Variable(tf.random_normal((N, D)))

with tfe.GradientTape() as tape:
    a=x * 2
    b=a + z
    c = tf.reduce_sum(b)

grad_x, grad_y, grad_z = tape.gradient(c, [x, y, 2])
print(grad_x) 
  • 在程序开始时使用 tf.enable_eager_execution 模式:它是一个全局开关
  • tf.random_normal 会产生具体的值,无需 placeholders / sessions,如果想要为它们计算梯度,要用 tfe.Variable 进行包装
  • GradientTape 下操作将构建一个动态图,类似于 PyTorch
  • 使用tape 计算梯度,类似 PyTorch 中的 backward。并且可以直接打印出来
  • 静态的 PyTorch 有 [Caffe2](https://caffe2.ai/)、[ONNX Support](https://caffe2.ai/)

4.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=8

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

5.要点总结

  • 深度学习硬件最好使用 GPU,然后需要解决 CPU 与 GPU 的通信问题。TPU 是专门用于深度学习的硬件,速度非常快。
  • PyTorch 与 TensorFlow 都是非常好的深度学习框架,都有可以在 GPU 上直接运行的数组,都可以自动计算梯度,都有很多已经写好的函数、层等可以直接使用。前者使用动态图,后者使用静态图,不过二者都在向对方发展。取舍取决于项目。

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(9) | 典型 CNN 架构 (Alexnet,VGG,Googlenet,Restnet 等)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125025145

ShowMeAI 研究中心


CNN Architectures; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

ShowMeAI 在文章 深度学习与 CV 教程(5) | 卷积神经网络 中已经给大家介绍过 CNN 的核心结构组件,在本篇中,我们给大家介绍目前最广泛使用的典型卷积神经网络结构。包括经典结构(AlexNet、VGG、GoogLeNet、ResNet)和一些新的结构(Network in Network、Resnet 改进、FractalNet、DenseNet 等)

关于典型 CNN 结构的详细知识也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 经典 CNN 网络实例详解

本篇重点

  • 经典 CNN 架构
    • AlexNet
  • VGG
  • GoogLeNet
  • ResNet
  • 其他结构
    • NIN(Network in Network)
  • ResNet 改进
  • FractalNet
  • DenseNet
  • NAS

1.经典架构

1.1 AlexNet

首先回顾一下在数字识别领域有巨大成功的 LeNet-5,该网络结构为 [CONV-POOL-CONV-POOL-FC-FC]。卷积层使用 5 × 5 5 \times 5 5×5 的卷积核,步长为 1 1 1;池化层使用 2 × 2 2 \times 2 2×2 的区域,步长为 2 2 2;后面是全连接层。如下图所示:

AlexNet; LeNet-5 架构

而 2012 年的 AlexNet 是第一个在 ImageNet 大赛上夺冠的大型 CNN 网络,它的结构和 LeNet-5 很相似,只是层数变多了——[CONV1-MAX POOL1-NORM1-CONV2-MAX POOL2-NORM2-CONV3-CONV4-CONV5-Max POOL3-FC6-FC7-FC8],共有 5 个卷积层、3 个池化层、2 个归一化层和三个全连接层。如下图所示:

AlexNet; AlexNet 架构

  • 输入: 227 × 227 × 3 227 \times 227 \times 3 227×227×3 的图片;
  • CONV1:使用 96 个 11 × 11 11 \times 11 11×11 大小的卷积核,步长为 4 4 4,由于 ( 227 − 11 ) / 4 + 1 = 55 (227-11)/4+1=55 (227−11)/4+1=55,所以输出的尺寸为 55 × 55 × 96 55 \times 55 \times 96 55×55×96,共有 96 × 11 × 11 × 3 96 \times 11 \times 11 \times 3 96×11×11×3 个参数;
  • POOL1:使用 3 × 3 3 \times 3 3×3 的池化区域,步长为 2 2 2,由于 ( 55 − 3 ) / 2 + 1 = 27 (55-3)/2+1=27 (55−3)/2+1=27,所以输出为 27 × 27 × 96 27 \times 27 \times 96 27×27×96,没有参数;
  • NORM1:归一化后仍然是 27 × 27 × 96 27 \times 27 \times 96 27×27×96;
  • CONV2:使用 256 个 5 × 5 5 \times 5 5×5 的卷积核,stride 1 1 1、pad 2 2 2 , ( 27 + 2 × 2 − 5 ) + 1 = 27 (27+2 \times 2-5)+1=27 (27+2×2−5)+1=27,所以输出为 27 × 27 × 256 27 \times 27 \times 256 27×27×256;
  • POOL2: 3 × 3 3 \times 3 3×3 filters,stride 2 2 2 , ( 27 − 3 ) / 2 + 1 = 13 (27-3)/2+1=13 (27−3)/2+1=13,所以输出为 13 × 13 × 256 13 \times 13 \times 256 13×13×256;
  • NORM2: 13 × 13 × 256 13 \times 13 \times 256 13×13×256;
  • CONV3:384 个 3 × 3 3 \times 3 3×3 filters,stride 1 1 1, pad 1 1 1,输出 [ 13 × 13 × 384 ] [13 \times 13 \times 384] [13×13×384];
  • CONV4:384 个 3 × 3 3 \times 3 3×3 filters,stride 1 1 1, pad 1 1 1,输出 [ 13 × 13 × 384 ] [13 \times 13 \times 384] [13×13×384];
  • CONV5:256 个 3 × 3 3 \times 3 3×3 filters,stride 1 1 1, pad 1 1 1,输出 [ 13 × 13 × 256 ] [13 \times 13 \times 256] [13×13×256];
  • POOL3: 3 × 3 3 \times 3 3×3 filters,stride 2 2 2 输出为 [ 6 × 6 × 256 ] [6 \times 6 \times 256] [6×6×256];
  • FC6: 4096 4096 4096 个神经元,输出为 [ 4096 ] [4096] [4096];
  • FC7: 4096 4096 4096 个神经元,输出为 [ 4096 ] [4096] [4096];
  • FC8: 1000 1000 1000 个神经元,(class scores)输出为 [ 1000 ] [1000] [1000]。

之所以在上图中分成上下两个部分,是因为当时的 GPU 容量太小,只能用两个来完成。还有一些细节是:

  • 第一次使用 ReLU 函数
  • 使用归一化层(现在不常用了)
  • 数据增强
  • dropout 0.5
  • batch size 128
  • SGD Momentum 0.9
  • 学习率 1e-2, 当验证准确率平稳时,手动减少 10
  • L2 权重衰减是 5e-4
  • 7 CNN ensemble: 18.2 % → 15.4 % 18.2% \to 15.4% 18.2%→15.4%

AlexNet 夺得 ImageNet 大赛 2012 的冠军时,将正确率几乎提高了 10%,2013 年的冠军是 ZFNet,和 AlexNet 使用相同的网络架构,只是对超参数进一步调优:

  • CONV1:将 (11x11 stride 4) 改为 (7x7 stride 2) ;
  • CONV3,4,5:不再使用 384, 384, 256 个滤波器,而是使用 512, 1024, 512 个。

这样将错误率从 16.4 % 16.4% 16.4% 降低到 11.7 % 11.7% 11.7%

AlexNet; ImageNet 大赛历届冠军

下面介绍 14 年的冠亚军 GoogLeNet(22 层网络)和 VGG(19 层网络)。

1.2 VGG

VGG 相对于 AlexNet 使用更小的卷积核,层数也更深。VGG 有 16 层和 19 层两种。卷积核只使用 3 × 3 3 \times 3 3×3,步长为 1 1 1,pad 为 1 1 1;池化区域 2 × 2 2 \times 2 2×2,步长为 2。

VGG; VGG V.S. AlexNet

那么为什么使用 3 × 3 3 \times 3 3×3 的小卷积核呢?

  • 多个卷积层堆叠时,第一层的感受野是 3 × 3 3 \times 3 3×3,第二层的感受野是 5 × 5 5 \times 5 5×5 (感受原图像),这样堆叠三层的有效感受野就变成 7 × 7 7 \times 7 7×7;
  • 多个 3 × 3 3 \times 3 3×3 的卷基层比一个大尺寸卷积核的卷积层有更多的非线性(更多层的非线性函数),使得判决函数更加具有判决性;
  • 多个 3 × 3 3 \times 3 3×3 的卷积层比一个大尺寸的卷积核有更少的参数,假设卷积层的输入和输出的特征图大小相同为 C C C,那么三个 3 × 3 3 \times 3 3×3 的卷积层参数个数 3 × ( 3 × 3 × C × C ) = 27 C 2 3 \times (3 \times 3 \times C \times C)=27C2 3×(3×3×C×C)=27C2;一个 7 × 7 7 \times 7 7×7 的卷积层参数为 7 × 7 × C × C = 49 C 2 7 \times 7 \times C \times C=49C2 7×7×C×C=49C2;所以可以把三个 3 × 3 3 \times 3 3×3 的 filter 看成是一个 7 × 7 7 \times 7 7×7 filter 的分解(中间层有非线性的分解, 并且起到隐式正则化的作用)。

下面看一下 VGG-16 的参数和内存使用情况:

VGG; VGG-16 参数与内存

  • 总内存占用:24M * 4 bytes,每张图片约 96MB,加上反向传播需要乘以 2;大多数内存都用在了前面几层卷积层;
  • 总参数个数:138M,大多都在全连接层,全连接层的第一层就有 100 多 M。

VGG 网络的一些细节是:

  • 14 年 ImageNet 大赛分类第二名,定位第一名
  • 训练过程和 AlexNet 很接近
  • 不使用局部响应归一化
  • 有 16 层和 19 层两种,19 层效果稍微好一些,但是占用更多内存,16 层应用的更广泛;
  • 使用模型集成
  • FC7 的特征泛化非常好,可以直接用到其他任务中

下面来看一下分类的第一名,GoogLeNet。

1.3 GoogLeNet

关于 GoogLeNet/Inception 的详细知识也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章经典 CNN 网络实例详解

先说明 GoogLeNet 的一些细节:

  • 网络有 22 层,比 VGG 深一些
  • 为了高效的计算,使用 「Inception」 模块
  • 不使用全连接层
  • 只有 500 万个参数,比 AlexNet 少了 12 倍
  • 14 年分类的冠军(6.7% top 5 error)

1) Inception Module

「Inception」模块是一种设计的比较好的局域网拓扑结构,然后将这些模块堆叠在一起。这种拓扑结构对来自前一层的输入,并行应用多种不同的滤波操作,比如 1 × 1 1 \times 1 1×1 卷积、 3 × 3 3 \times 3 3×3 卷积、 5 × 5 5 \times 5 5×5 卷积和 3 × 3 3 \times 3 3×3 池化。然后将所有滤波器的输出在深度上串联在一起。

如下图所示:

GoogLeNet; 未优化的 Inception Module 网络

但是这种结构的一个问题是计算复杂度大大增加。如下图所示是一个网络参数计算示例:

GoogLeNet; 未优化的 Inception Module 网络参数计算

输入为 28 × 28 × 256 28 \times 28 \times 256 28×28×256,而串联后的输出为 28 × 28 × 672 28 \times 28 \times 672 28×28×672。(假设每个滤波操作都通过零填充保持输入尺寸)并且运算花费也非常高:

  • [1x1 conv, 128] 28 × 28 × 128 × 1 × 1 × 256 28 \times 28 \times 128 \times 1 \times 1 \times 256 28×28×128×1×1×256 次乘法运算;
  • [3x3 conv, 192] 28 × 28 × 192 × 3 × 3 × 256 28 \times 28 \times 192 \times 3 \times 3 \times 256 28×28×192×3×3×256 次;
  • [5x5 conv, 96] 28 × 28 × 96 × 5 × 5 × 256 28 \times 28 \times 96 \times 5 \times 5 \times 256 28×28×96×5×5×256 次。

总计:854M 次乘法运算。

由于池化操作会保持原输入的深度,所以网络的输出一定会增加深度。

解决办法是在进行卷积操作前添加一个「瓶颈层」,该层使用 1 × 1 1 \times 1 1×1 卷积,目的是保留原输入空间尺寸的同时,减小深度,只要卷积核的数量小于原输入的深度即可。

GoogLeNet; 应用 1*1 卷积的 Inception Module

使用这种结构,同样的网络参数设置下,计算量会减少很多:

GoogLeNet; 使用 1*1 卷积结构减少网络参数量

最终得到的输出为 28 × 28 × 480 28 \times 28 \times 480 28×28×480。此时总运算量为:

  • [1x1 conv, 64] 28 × 28 × 64 × 1 × 1 × 256 28 \times 28 \times 64 \times 1 \times 1 \times 256 28×28×64×1×1×256
  • [1x1 conv, 64] 28 × 28 × 64 × 1 × 1 × 256 28 \times 28 \times 64 \times 1 \times 1 \times 256 28×28×64×1×1×256
  • [1x1 conv, 128] 28 × 28 × 128 × 1 × 1 × 256 28 \times 28 \times 128 \times 1 \times 1 \times 256 28×28×128×1×1×256
  • [3x3 conv, 192] 28 × 28 × 192 × 3 × 3 × 64 28 \times 28 \times 192 \times 3 \times 3 \times 64 28×28×192×3×3×64
  • [5x5 conv, 96] 28 × 28 × 96 × 5 × 5 × 64 28 \times 28 \times 96 \times 5 \times 5 \times 64 28×28×96×5×5×64
  • [1x1 conv, 64] 28 × 28 × 64 × 1 × 1 × 256 28 \times 28 \times 64 \times 1 \times 1 \times 256 28×28×64×1×1×256

总计: 358 M 358M 358M。减少了一倍多。

2) 完整结构

Inception module 堆叠成垂直结构,这里方便描述,将模型水平放置:

GoogLeNet; 完整的 GoogLeNet 结构

  • 蓝色部分主干网:

Input - Conv 7x7+2(S) - MaxPool 3x3+2(S) - LocalRespNorm - Conv 1x1+1(V) - Conv 3x3+1(S) - LocalRespNorm - MaxPool 3x3+2(S)

含参数的层只有 3 个卷积层;

  • 红色部分 Inception module 堆叠:
    • 并行层只算一层,所以一个 Inception module 只有两层,共有 9 个相同的模块 18 层。
  • 绿色部分的输出:
    • 移除昂贵的全连接层,只留一个分类用的 FC。
    • AveragePool 7x7+1(V) - FC - Softmax - Output

所以含参数的层总计 3 + 18 + 1 = 22 3+18+1 = 22 3+18+1=22 层。

此外,橙色部分的层不计入总层数,这两块的结构都是:AveragePool 5x5+3(V) - Conv 1x1+1(S) - FC - FC - Softmax - Output。

原论文对于橙色辅助部分的描述是:

「该相对较浅的网络在此分类任务上的强大表现表明,网络中间层产生的特征应该是非常有区别性的。 通过添加连接到这些中间层的辅助分类器,我们期望在分类器的较低阶段中鼓励区分,增加回传的梯度信号,并提供额外的正则化。 这些辅助分类器采用较小的卷积核,置于第三和第六个 Inception module 的输出之上。 在训练期间,它们的损失会加到折扣权重的网络总损失中(辅助分类的损失加权为 0.3)。 在预测时,这些辅助网络被丢弃。」

1.4 ResNet

关于 ResNet 的详细知识也可以参考ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章经典 CNN 网络实例详解

从 2015 年开始,神经网络的层数爆发式地增长,比如著名的 ResNet 有 152 层。

如下图所示,15-17 年的冠军网络都有 152 层之深。

ResNet; ResNet Revolution of depth

ResNet 是一种非常深的网络,使用了残差连接。细节是:

  • 152 层
  • ILSVRC’15 优胜者(3.57% top 5 error)
  • 横扫了所有 ILSVRC’15 和 COCO’15 分类/检测的竞赛!

表现这么好的 ResNet 并不仅仅是因为深,研究表明一个 56 层的卷积层堆叠网络训练误差和测试误差都比一个 20 层的网络要大,并且不是过拟合的原因,而是更深的网络更难训练和优化。

一个更深的模型至少能和一个较浅的模型表现一样好(学习能力会更强),如果想把一个较浅的层变成较深的层,可以用下面的方式来构建:将原来比较浅的层拷贝到较深的层中,然后添加一些等于本身的映射层。这样较深的模型可以更好的学习。

1) 核心思想

ResNet 通过使用多个有参层来学习输入与输入输出之间的残差映射( residual mapping ) ,而非像一般 CNN 网络(如 AlexNet/VGG 等)那样使用有参层来直接学习输入输出之间的底层映射( underlying mapping)

① 残差学习(Residual Learning)

若将输入设为 X,将某一有参网络层映射设为 H H H,那么以 X X X 为输入的该层的输出将为 H ( X ) H(X) H(X)。通常的 CNN 网络会直接通过训练学习出参数函数 H 的表达式,从而直接得到 X X X 到 H ( X ) H(X) H(X) 的映射。

ResNet; 残差学习

残差学习则是致力于使用多个有参网络层来学习输入到输入、输出间的残差 ( H ( X ) − X ) (H(X) - X) (H(X)−X) 的映射,即学习 X → ( H ( X ) − X ) X \to (H(X) - X) X→(H(X)−X) ,然后加上 X 的自身映射(identity mapping)

也就是说网络的输出仍然是 H ( X ) − X + X = H ( X ) H(X) - X + X = H(X) H(X)−X+X=H(X),只是学习的只是 ( H ( X ) − X ) (H(X) - X) (H(X)−X), X X X 部分直接是本身映射。

② 自身映射(Identity Mapping)

残差学习单元通过本身映射的引入在输入、输出之间建立了一条直接的关联通道,从而使得强大的有参层集中精力学习输入、输出之间的残差。

一般我们用 F ( X , W i ) F(X, W_i) F(X,Wi​) 来表示残差映射,那么残差学习单元的输出即为: Y = F ( X , W i ) + X Y = F(X, W_i) + X Y=F(X,Wi​)+X。

  • 当输入、输出通道数相同时,自然可以直接使用 X X X 进行相加。
  • 当它们之间的通道数目不同时,我们就需要考虑建立一种有效的自身映射函数从而可以使得处理后的输入 X X X 与输出 Y Y Y 的通道数目相同即 Y = F ( X , W i ) + W s X Y = F(X, W_i) + W_sX Y=F(X,Wi​)+Ws​X。

当 X 与 Y 通道数目不同时,有两种自身映射方式。

  • ① 简单地将 X X X 相对 Y Y Y 缺失的通道直接补零从而使其能够相对齐
  • ② 通过使用 1 × 1 1 \times 1 1×1 的卷积来表示 W s W_s Ws​ 映射从而使得最终输入与输出的通道一致。

对应的论文和实验表明,学习残差比直接学习输入到输出的映射要更加容易,收敛速度也更快,同时从最终分类精度效果上看也有提升。

我们想象 1 个极端极端情况,如果自身映射是最优的,那么将残差设为零相比使用一堆非线性层做自身映射显然更容易。

2) 完整结构

ResNet; ResNet 完整结构

完整的网络结构如下:

  • 残差块堆叠
  • 每个残差块有两个 3 × 3 3 \times 3 3×3 卷积层
  • 周期性的使用两倍的卷积核数量,降采样通过设置步长为 2 2 2
  • 在网络开始处有 7 × 7 7 \times 7 7×7 的卷积层和最大池化层(步长 2 2 2)
  • 在网络的最后不使用全连接层 (只有一个用于 1000 个分类的 FC)
  • 在最后一个卷积层后使用全局的平均池化
  • 总共的深度有 34、50、101 或 152

对于 ResNet-50+的网络,为提高计算效率,使用类似 GoogLeNet 的「瓶颈层」。

具体说,它也是通过使用 1 × 1 1 \times 1 1×1 卷积来缩减或扩张特征图维度,从而使得 3 × 3 3 \times 3 3×3 卷积的卷积核数目不受上一层输入的影响,对应的输出也不会影响到下一层,这种设计节省计算时间,且不影响最终的模型精度。

ResNet; Bottleneck 模块

3) ResNet 网络训练

ResNet 的实际训练的一些细节如下:

  • 每个 CONV 层后使用批量归一化
  • 权重使用 He 初始化
  • 更新方式使用 SGD + Momentum (0.9)
  • 学习率为 0.1, 验证错误率不变时除 10
  • Mini-batch size 为 256
  • 权重衰减是 1e-5
  • 未使用 dropout

实际的训练效果为可以堆叠很多的层而不使准确率下降:ImageNet 上 152 层网络,在 CIFAR 上 1202 层网络,表现都很好。经过结构改造后,和预想中的一致,网络越深,训练准确率越高。

ResNet 横扫了 2015 年所有的奖项,第一次超过人类的识别率。

ResNet 网络训练; ResNet 的成绩

2.几种网络的对比

ResNet; 各种网络的准确率与计算复杂度

  • 左图:通过 Top1 准确率来比较各种网络的准确性
  • 右图:是不同网络的运算复杂度,横轴为计算量,圆圈大小表示内存占用。其中 Inception-v4 是 Resnet + Inception

从图里可以看出:

  • Inception-v4 具有最高的准确率
  • VGG 内存占用最大,计算量最多
  • GoogLeNet 最高效,准确率较高,运算复杂度较小
  • AlexNet 计算量较小但内存占用较大,准确率也低
  • ResNet 准确率较高,效率取决于模型

前向传播时间和功率消耗对比如下:

ResNet; 各种网络前向传播时间和功率消耗与批量的关系

3.其他网络架构

3.1 Network in Network (NiN)

Network in Network; NiN 架构

Network In Network 发表于 ICLR 2014,由新加坡国立大学(NUS)提出,也是一个经常被大家提到的经典 CNN 结构,它的主要特点如下:

  • 在每个卷积层内的 Mlpconv 层具有「Micronetwork」用于计算局部区域的更抽象的特征;
  • Micronetwork 使用多层感知器(FC,即 1 × 1 1 \times 1 1×1 卷积层)
  • GoogLeNet 和 ResNet「瓶颈」层的先驱

3.2 ResNet 的改进

1) Identity Mappings in Deep Residual Networks

Identity Mappings in Deep Residual Networks; ResNet 的改进

  • ResNet 创造者自己改进了残差块设计
  • 创建更直接的路径(将激活函数移动到残差的映射路径),以便在整个网络中传播信息
  • 更好的性能

2)Wide Residual Networks

Wide Residual Networks; ResNet 的改进

  • 相比「深度」,认为「残差」是核心
  • 使用更宽的残差块( F × k F \times k F×k 个滤波器代替每层中的 F 个滤波器)
  • 50 层 Wide ResNet 优于 152 层原始 ResNet
  • 增加宽度而不是深度更具计算效率(可并行化)

3) ResNeXt

ResNeXt; ResNet 的改进

  • ResNet 创建者对结构改造
  • 通过多个平行路径增加残差块的宽度(cardinality)
  • 与 Inception 模块相似的并行路径
  • 单个分支「变窄」

4)Deep Networks with Stochastic Depth

Deep Networks with Stochastic Depth; ResNet 的改进

  • 动机:通过缩短网络减少梯度消失和网络训练时间
  • 在每次训练过程中随机丢弃一个层子集
  • 具有自身映射功能的旁路,丢弃的层权重为 1,恒等映射
  • 在测试时使用完整的深度网络

5)Network Ensembling(Fusion)

Network Ensembling (ion); ResNet 的改进

  • 多尺度集成 Inception、Inception-Resnet、Resnet、Wide Resnet 模型
  • ILSVRC’16 分类获胜者

6)Squeeze-and-Excitation Networks (SENet)

Squeeze-and-Excitation Networks (SENet); ResNet 的改进

Squeeze-and-Excitation Networks (SENet); ResNet 的改进

  • 添加「特征重新校准」模块,该模块学习自适应重新加权特征图
  • 全局信息(全局平均池化层)+ 2 个 FC 层,用于确定特征图权重,即「特征重新校准」模块
  • ILSVRC’17 分类获胜者(使用 ResNeXt-152 作为基础架构)

3.3 FractalNet

FractalNet; CNN

  • 动机:认为从浅层到深层有效地过渡最重要,残差表示不是最重要的
  • 具有浅和深路径输出的分形结构
  • 训练时随机抛弃子路径
  • 测试时使用完整网络

3.4 Densely Connected Convolutional Networks

Densely Connected Convolutional Networks; CNN

  • 密集块,其中每个层以前馈方式连接到之后的每个层
  • 减轻梯度消失、加强特征传播、鼓励特征重用

3.5 Efficient Networks —— SqueezeNet

对于 SqueezeNet 和其他轻量化网络感兴趣的同学也可以参考ShowMeAI计算机视觉教程中的文章轻量化 CNN 架构(SqueezeNet,ShuffleNet,MobileNet 等)

Efficient Networks - SqueezeNet; CNN

  • 轻量化网络
  • 1 × 1 1 \times 1 1×1 卷积核构建「挤压」层,进而组成 Fire 模块,由 1 × 1 1 \times 1 1×1 和 3 × 3 3 \times 3 3×3 卷积核组成「扩展」层
  • ImageNet 上的 AlexNet 级精度,参数减少 50 倍
  • 可以压缩到比 AlexNet 小 510 倍(0.5Mb 参数)

3.6 Learn network architectures —— Meta-learning

1) Neural Architecture Search with Reinforcement Learning (NAS)

Meta-learning; Neural Architecture Search with Reinforcement Learning (NAS)

  • 一种「控制器」网络,可以学习设计良好网络架构(输出与网络设计相对应的字符串)
  • 迭代:
    • 1)从搜索空间中采样架构
    • 2)训练架构以获得相应于准确度的「奖励」R
    • 3)计算样本概率的梯度,通过 R 进行缩放以执行控制器参数更新,增加被采样架构良好的可能性,减少不良架构的可能性

2) Learning Transferable Architectures for Scalable Image Recognition

  • 将神经架构搜索(NAS)应用于像 ImageNet 这样的大型数据集非常昂贵
  • 设计可以灵活堆叠的构建块(「单元」)的搜索空间
  • NASNet:使用 NAS 在较小的 CIFAR-10 数据集上找到最佳的单元结构,然后将架构转移到 ImageNet

4.推荐学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=9

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

5.要点总结

经典架构

  • AlexNet:开启 CNN 时代
  • VGG:减小卷积核尺寸、增加网络层数获得高准确率
  • GoogLeNet:引入 Inception module
  • ResNet:引入残差块,证明普通堆叠层数没意义,残差堆叠可以;目前应用最广泛的网络结构

其他架构

  • NiN (Network in Network) : 1 × 1 1 \times 1 1×1 卷积先驱
  • Wide ResNet:加大 ResNet 的宽度而不是深度
  • ResNeXT:使用多个分支加宽 ResNet
  • Stochastic Dept:Dropout 层
  • SENet:自适应特征图重新加权
  • DenseNet:每个层连接到之后的每个层
  • FractalNet:使用分形结构,不用残差
  • SqueezeNet:压缩网络,减少参数
  • NASNet:学习网络架构

网络应用总结

  • VGG、GoogLeNet、ResNet 均被广泛使用,可在模型族中获取
  • ResNet 是当前默认最佳的选择,也可考虑 SENet
  • 研究趋向于极深的网络
  • 研究重心围绕层/跳过连接的设计和改善梯度流
  • 努力研究深度、宽度与残差连接的必要性
  • 更近期的趋势是研究 meta-learning

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(10) | 轻量化 CNN 架构 (SqueezeNet,ShuffleNet,MobileNet 等)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125025764

ShowMeAI 研究中心


前言

卷积神经网络的结构优化和深度加深,带来非常显著的图像识别效果提升,但同时也带来了高计算复杂度和更长的计算时间,实际工程应用中对效率的考虑也很多,研究界与工业界近年都在努力「保持效果的情况下压缩网络复杂度」,也诞生了很多轻量化网络。在本篇内容中,ShowMeAI 对常见主流轻量级网络进行展开讲解。

本篇重点

  • 神经网络参数与复杂度计算
  • 轻量化网络
  • SqueezeNet
  • Xception
  • ShuffleNet V1~V2
  • MobileNet V1~V3

1.基础知识

我们先来做一点基础知识储备,本篇讲到的轻量化 CNN 框架,我们需要了解参数量和计算量的估测与计算方式。

1.1 复杂度分析

  • 理论计算量(FLOPs):浮点运算次数(FLoating-point Operation)
  • 参数数量(params):单位通常为 M M M,用 float32 表示。

1.2 典型结构对比

  • 标准卷积层 std conv(主要贡献计算量)
    • params: k h × k w × c i n × c o u t k_h\times k_w\times c_{in}\times c_{out} kh​×kw​×cin​×cout​
    • FLOPs: k h × k w × c i n × c o u t × H × W k_h\times k_w\times c_{in}\times c_{out}\times H\times W kh​×kw​×cin​×cout​×H×W
  • 全连接层 fc(主要贡献参数量)
    • params: c i n × c o u t c_{in}\times c_{out} cin​×cout​
    • FLOPs: c i n × c o u t c_{in}\times c_{out} cin​×cout​
  • group conv
    • params: ( k h × k w × c i n / g × c o u t / g ) × g = k h × k w × c i n × c o u t / g (k_h\times k_w\times c_{in}/g \times c_{out}/g)\times g=k_h\times k_w\times c_{in}\times c_{out}/g (kh​×kw​×cin​/g×cout​/g)×g=kh​×kw​×cin​×cout​/g
    • FLOPs: k h × k w × c i n × c o u t × H × W / g k_h\times k_w\times c_{in}\times c_{out}\times H\times W/g kh​×kw​×cin​×cout​×H×W/g
  • depth-wise conv
    • params: k h × k w × c i n × c o u t / c i n = k h × k w × c o u t k_h\times k_w\times c_{in}\times c_{out}/c_{in}=k_h\times k_w\times c_{out} kh​×kw​×cin​×cout​/cin​=kh​×kw​×cout​
    • FLOPs: k h × k w × c o u t × H × W k_h\times k_w\times c_{out}\times H\times W kh​×kw​×cout​×H×W

2.SqueezeNet

轻量化网络中一个著名的网络是 SqueezeNet ,它发表于 ICLR 2017,它拥有与 AlexNet 相同的精度,但只用了 AlexNet 1/50 的参数量。

SqueezeNet 的核心在于采用不同于常规的卷积方式来降低参数量,具体做法是使用 Fire Module,先用 1 × 1 1 \times 1 1×1 卷积降低通道数目,然后用 1 × 1 1 \times 1 1×1 卷积和 3 × 3 3 \times 3 3×3 卷积提升通道数。

2.1 压缩策略

SqueezeNet 采用如下 3 个策略:

  • ① 将 3 × 3 3 \times 3 3×3 卷积替换为 1 × 1 1 \times 1 1×1 卷积
  • ② 减少 3 × 3 3 \times 3 3×3 卷积的通道数
  • ③ 将降采样操作延后,这样可以给卷积提供更大的 activation map,从而保留更多的信息,提供更高的分类准确率。

其中,策略 1 和 2 可以显著减少模型参数量,策略 3 可以在模型参数量受限的情况下提高模型的性能。

2.2 Fire Module

Fire Module 是 SqueezeNet 网络的基础模块,设计如下图所示:

SqueezeNet; Fire Module 模块

一个 Fire Module 由 Squeeze 和 Extract 两部分组成

  • Squeeze 部分包括了一系列连续的 1 × 1 1 \times 1 1×1 卷积
  • Extract 部分包括了一系列连续的 1 × 1 1 \times 1 1×1 卷积和一系列连续的 3 × 3 3 \times 3 3×3 卷积,然后将 1 × 1 1 \times 1 1×1 和 3 × 3 3 \times 3 3×3 的卷积结果进行 concat。

记 Squeeze 部分的通道数为 C s 1 × 1 C_{s{1\times 1}} Cs1×1​,Extract 部分 1 × 1 1 \times 1 1×1 和 3 × 3 3 \times 3 3×3 的通道数分别为 C e 1 × 1 C_{e{1\times 1}} Ce1×1​ 和 C e 3 × 3 C_{e{3\times 3}} Ce3×3​,作者建议 C s 1 × 1 < C e 1 × 1 + C e 3 × 3 C_{s{1\times 1}} \lt C_{e{1\times 1}} + C_{e{3\times 3}} Cs1×1​<Ce1×1​+Ce3×3​ ,这样做相当于在 Squeeze 和 Extraxt 之间插入了 bottlenet。

2.3 网络结构

Fire Module的基础上搭建 SqueezeNet 神经网络。它以卷积层开始,后面是 8 个 Fire Module,最后以卷积层结束,每个 Fire Module 中的通道数目逐渐增加。另外网络在 conv1,fire4,fire8,conv10 的后面使用了 max-pooling。

SqueezeNet 结构如下图所示,左侧是不加 shortcut 的版本,中间是加了 shortcut 的版本,右侧是在不同通道的特征图之间加入 shortcut 的版本。

SqueezeNet; SqueezeNet 网络结构

SqueezeNet 的性能类似于 AlenNet,然而参数量只有后者的 1/50,使用 Deep Compression 可以进一步将模型大小压缩到仅仅有 0.5M。

2.4 SqueezeNet 缺点

SqueezeNet 缺点如下:

  • SqueezeNet 通过更深的网络置换更多的参数,虽然有更低的参数量,但是网络的测试阶段耗时会增加,考虑到轻量级模型倾向于应用在嵌入式场景,这一变化可能会带来新的问题。
  • AlaxNet 的参数量(50M)大部分由全连接层带来,加上一部分参数量进行对比,数字稍有夸张。

3.Xception

另一个需要提到的典型网络是 Xception,它的基本思想是,在 Inception V3 的基础上,引入沿着通道维度的解耦合,基本不增加网络复杂度的前提下提高了模型的效果,使用 Depthwise Seperable Convolution 实现。

Xception 虽然不是出于轻量级的考虑而设计的模型,但是由于使用了 pointwise convolution 和 depthwise convolution 的结合,实际上也起到了降低参数量的效果,我们也放在轻量模型里做个介绍。

3.1 设计动机

卷积在 HWC(高 × \times × 宽 × \times × 通道数)这 3 个维度上进行学习,既考虑空间相关性,又考虑通道相关性,可以考虑这两种相关性解耦分开。

Xception 的做法是使用 point-wise convolution 考虑 cross-channel correlation,使用 depthwise convolution 考虑 spatial correlation。

3.2 从 Inception 到 Extreme version of Inception

下图是一个 Inception V3 的基础模块,分别用 1 × 1 1 \times 1 1×1 卷积和 3 × 3 3 \times 3 3×3 卷积考虑通道相关性和空间相关性,基本结构是用 1 × 1 1 \times 1 1×1 卷积降维,用 3 × 3 3 \times 3 3×3 卷积提取特征:

Xception; Inception 网络结构

如果将上述结构简化,则可以得到如下的结构,可见每一个分支都包含了一个 1 × 1 1 \times 1 1×1 卷积和一个 3 × 3 3 \times 3 3×3 卷积:

Xception; 对 Inception 简化

从上图中可见,对于每一个分支,该模块使用 1 × 1 1 \times 1 1×1 卷积对输入特征图进行处理,然后使用 3 × 3 3 \times 3 3×3 卷积提取特征。

如果考虑空间相关性和通道相关性的解耦合,即用同一个 1 × 1 1 \times 1 1×1 卷积进行通道处理,将处理结果沿着通道维度拆解为若干部分,对于每一部分使用不同的 3 × 3 3 \times 3 3×3 卷积提取特征,则得到如下图所示的模块:

Xception; 解耦合 Inception

考虑一种更为极端的情况,在使用 1 × 1 1 \times 1 1×1 卷积之后,沿着通道维度进行最为极端的拆解,对于拆解后的每一个部分使用 3 × 3 3 \times 3 3×3 卷积提取特征,这一步可以使用 depthwise convolution 实现,最后将这些提取到的特征图进行 concat,这就是 Xception 的基础模块,如下图所示:

Xception; Inception 改造为 Xception 基础模块

通过上图可以看到,该模块将输入数据在「通道维度」上解耦,我们称之为 extreme version of inception module。这点与 depthwise seperable convolution 很相似。

3.3 Extreme version of Inception 与 Depthwise Seperable Convolution

这一操作与 Depthwise Seperable Convolution 十分相似,后者包含 Depthwise Convolution 和 Pointwise Convolution 两部分。

上图所示的基础模块与 Depthwise Seperable Convolution 有如下两点不同:

  • ① Depthwise Seperable Convolution 先使用 depthwise convolution,再使用 1 × 1 1 \times 1 1×1 卷积进行融合;上图所示的基础模块先使用 1 × 1 1 \times 1 1×1 卷积,再使用 depthwise convolution。
  • ② Depthwise Seperable Convolution 的 depthwise convolution 和 1 × 1 1 \times 1 1×1 卷积之间没有激活函数;上图所示的基础模块的这两个操作之间有激活函数。

在 Xception 中,作者直接使用了 Depthwise Seperable Convolution 作为基础模块。

3.4 Xception 网络结构

最后将这一基础模块叠加,并结合残差连接,就得到了 Xception 网络结构:

Xception; Xception 网络结构

4.ShuffleNet

ShuffleNet 是由旷世科技提出的轻量化 CNN 网络,论文名称《ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices》,目标是改造网络架构使其能应用在移动设备上。

4.1 设计动机

ShuffleNet 的动机在于大量的 1 × 1 1 \times 1 1×1 卷积会耗费很多计算资源,而 Group Conv 难以实现不同分组之间的信息交流;ShuffleNet 的解决方式是:使用 Group Conv 降低参数量;使用 Channel Shuffle 实现不同组之间的信息交流,进而对 ResNet 进行改进,可以看作 ResNet 的压缩版本。

4.2 Group Conv

我们再来看看 Group Conv 这个结构,它的基本思想是对输入层的不同特征图进行分组,再使用不同的卷积核对不同组的特征图进行卷积,通过分组降低卷积的计算量。

而 Depthwise Convolution 可以视作 Group Conv 的一种特殊情形。

假设输入通道为 C i C_i Ci​,输出通道为 C o C_o Co​,分组数目为 g g g,Group Conv 的操作如下:

  • 将输入特征图沿着通道分为 g g g 组,每一组的通道数目为 C i / g C_i/g Ci​/g。
  • 使用 g g g 个不同的卷积核,每一个卷积核的滤波器数量为 C o / g C_o/g Co​/g。
  • 使用这 g g g 个不同的卷积核,对 g g g 组特征图分别进行卷积,得到 g g g 组输出特征图,每一组的通道数为 C o / g C_o/g Co​/g。
  • 将这 g g g 组的输出特征图结合,得到最终的 C o C_o Co​ 通道的输出特征图。

4.3 Channel Shuffle

Group Conv 的一个缺点在于不同组之间难以实现通信。一个可能的解决方式是使用 1 × 1 1 \times 1 1×1 卷积进行通信,但是这样会引入很大的计算量。

文中提出的思路是对 Group Conv 之后的特征图沿着通道维度进行重组,这样信息就可以在不同组之间流转,即 Channel Shuffle,如下图©所示。

ShuffleNet; Channel Shuffle

其实现过程如下:

  • ① 输入特征图通道数目为 g × n g\times n g×n
  • ② 将特征图的通道维度 reshape 为 ( g , n ) (g,n ) (g,n)
  • ③ 转置为 ( n , g ) (n,g ) (n,g)
  • ④ 平坦化成 g × n g \times n g×n 个通道

4.4 ShuffleNet 基础模块

结合 Group Conv 和 Channel Shuffle,对 ResNet 的基础模块 bottleneck(下图(a))进行改进,就得到了 ShuffleNet 的基础模块(下图(b)和©)

ShuffleNet; ShuffleNet 基础模块

4.5 ShuffleNet 缺点

  • Channel Shuffle 操作较为耗时,导致 ShuffleNet 的实际运行速度没有那么理想。
  • Channel Shuffle 的规则是人为制定的,更接近于人工设计特征。

5.ShuffleNet V2

在 ShuffleNet 之后又有改进的版本 ShuffleNet V2,改进了上述提到的 ShuffleNet 缺点,减少其耗时。

5.1 设计动机

ShuffleNet 的轻量级网络设计,FLOPs 减少了很多,但实际的时间消耗并不短。原因是网络训练或者推理的过程中,FLOPs 仅仅是其耗时的一部分,其他操作(如内存读写、外部数据 IO 等)也会占用时间。

ShuffleNet V2 的作者分析了几种网络结构在 GPU/ARM 这两种平台上的计算性能指标,并提出了 4 条移动端卷积网络设计的准则,根据这些准则改进 ShuffleNet 得到了 ShuffleNet V2。

我们先来看看这 4 条移动端网络设计准则:

5.2 高效 CNN 设计的几个准则

使用的指标是内存访问时间(Memory Access Cost, MAC)。用理论和实验说明了以下几条准则。

1) 输入输出通道数目相同时,卷积层所需的 MAC 最小。

理论推导:假设 1 × 1 1 \times 1 1×1 卷积的输入通道数目为 c 1 c_1 c1​,输出通道数目为 c 2 c_2 c2​,特征图大小为 h × w h\times w h×w,则这样一个 1 × 1 1 \times 1 1×1 卷积的 FLOPs 为:

B = h w c 1 c 2 B=hwc_1 c_2 B=hwc1​c2​

所需的存储空间如下,其中 h w c 1 hwc_1 hwc1​ 表示输入特征图所需空间, h w c 2 hwc_2 hwc2​ 表示输出特征图所需空间, c 1 c 2 c_1c_2 c1​c2​ 表示卷积核所需空间:

M A C = h w ( c 1 + c 2 + c 1 c 2 ) MAC = hw(c_1 + c_2 + c_1 c_2) MAC=hw(c1​+c2​+c1​c2​)

根据均值不等式可得:

M A C ≥ 2 h w B + B h w MAC \ge 2 \sqrt {hwB} + \frac {B}{hw} MAC≥2hwB ​+hwB​

等式成立的条件是 c 1 = c 2 c_1 = c_2 c1​=c2​,即在给定 FLOPs,输入特征通道数和输出特征通道数相等时,MAC 达到取值的下界。 实验证明: c 1 c_1 c1​和 c 2 c_2 c2​越接近、速度越快,如下表。

高效 CNN 的设计准则; 当输入、输出通道数目相同时,卷积层所需的 MAC 最小

2) 过多的 group 操作会增大 MAC

理论推导:带 group 的 1 × 1 1 \times 1 1×1 卷积的 FLOPs 如下,其中 g g g 表示分组数目:

B = h w c 1 c 2 g B = \frac{hwc_1c_2}{g} B=ghwc1​c2​​

MAC 如下:

M A C = h w ( c 1 + c 2 + c 1 c 2 g ) MAC = hw(c_1 + c_2 + \frac {c_1c_2}{g} ) MAC=hw(c1​+c2​+gc1​c2​​)

因此:

M A C = h w ( c 1 + c 2 + c 1 c 2 g = h w c 1 + B g c 1 + B h w \begin{aligned} MAC &= hw(c_1 + c_2 + \frac {c_1c_2}{g} \ & =hwc_1 + \frac {Bg} {c_1} + \frac {B} {hw} \end{aligned} MAC​=hw(c1​+c2​+gc1​c2​​=hwc1​+c1​Bg​+hwB​​

可见,在一定 FLOPs 的情况下,分组数目 g g g 越大,MAC 也越大。

实验证明:在 FLOPs 基本不变的操作下,group 越大,速度越慢,如下图所示。

高效 CNN 的设计准则; 过多的 group 操作会增大 MAC

3) 模型的碎片化程度越低,模型速度越快

实验证明:fragment 表示碎片化程度的量化,serious 表示串行,即几个卷积层的叠加,parallel 表示并行,即类似于 Inception 的设计。可见在 FLOPs 不变的情况下,分支数量越多,网络的实际耗时越大。

高效 CNN 的设计准则; 模型的碎片化程度越低,模型速度越快

4) element-wise 操作所带来的时间消耗远比在 FLOPs 上的体现的数值要多。

element-wise 操作虽然基本不增加 FLOPs,但是在 ShuffleNet V1 和 MobileNet V2 中,其耗时是十分可观的,如下图:

高效 CNN 的设计准则; element-wise 操作所带来的时间消耗远比在 FLOPs 上的体现的数值要多

实验证明:基于 ResNet 的 bottleneck 进行了实验,short-cut 是一种 element-wise 操作。实验证明 short-cut 操作会带来耗时的增加。

高效 CNN 的设计准则; short-cut 操作会带来耗时的增加

5.3 ShuffleNet V2 基础模块

基于前面提到的 4 条准则,对 ShuffleNet 的基础模块(下图(a)(b))进行修改,得到 ShuffleNet V2 的基础模块(下图中©(d)):

ShuffleNet V2; ShuffleNet V2 基础模块

图中 © 和 (a) 相比,有如下不同之处:

  • ① 模块的开始处增加了一个 Channel Split 操作,将输入特征图沿着通道分为 c ′ c' c′ 和 c − c ′ c-c' c−c′ 两部分,文中 c ′ = c / 2 c'=c/2 c′=c/2 ,对应于「准则 1」。
  • ② 取消了 1 × 1 1 \times 1 1×1 卷积中的 Group 操作,对应于「准则 2」。
  • ③ Channel Shuffle 移到了 Concat 后面,对应「准则 3」。(因为 1 × 1 1 \times 1 1×1 卷积没有 Group 操作,没有必要在后面接 Channel Shuffle)
  • ④ element-wise add 替换成 concat,对应「准则 4」。

(b)、(d) 之间的区别也类似,另外(d) 的两个分支都进行了降采样,且最初没有 Channel Split 操作,因此 Concat 之后的通道数目翻倍。

5.4 ShuffleNet V2 整体结构

上述 ShuffleNet V2 基础模块级联,配合卷积、池化等衔接,就得到了如下图的 ShuffleNet V2 结构:

ShuffleNet V2; ShuffleNet V2

6.MobileNet

另外一个非常有名的轻量化移动端网络是 MobileNet,它是专用于移动和嵌入式视觉应用的卷积神经网络,是基于一个流线型的架构,使用深度可分离的卷积来构建轻量级的深层神经网络。 MobileNet 凭借其优秀的性能,广泛应用于各种场景中,包括物体检测、细粒度分类、人脸属性和大规模地理定位。

MobileNet 有 V1 到 V3 不同的版本,也逐步做了一些优化和效果提升,下面我们来分别看看它的细节。

6.1 MobileNet 核心思想

MobileNet V1 的核心是将卷积拆分成 Depthwise Conv 和 Pointwise Conv 两部分,我们来对比一下普通网络和 MobileNet 的基础模块

  • 普通网络(以 VGG 为例) : 3 × 3 3 \times 3 3×3 Conv BN ReLU
  • Mobilenet 基础模块: 3 × 3 3 \times 3 3×3 Depthwise Conv BN ReLU 和 1 × 1 1\times1 1×1 Pointwise Conv BN ReLU

MobileNet; MobileNet 基础模块

6.2 MobileNet 缺点

  • ① ReLU 激活函数用在低维特征图上,会破坏特征。
  • ② ReLU 输出为 0 时导致特征退化。用残差连接可以缓解这一问题。

7.MobileNet V2

MobileNet V2 针对 MobileNet 的上述 2 个问题,引入了 Inverted Residual 和 Linear Bottleneck 对其进行改造,网络为全卷积,使用 RELU6(最高输出为 6)激活函数。下面我们展开介绍一下核心结构:

7.1 Inverted Residual

我们对比一下普通残差模块和 Inverted Residual 的差别

1) 普通残差模块

先使用 1 × 1 1 \times 1 1×1 卷积降低通道数量,然后使用 3 × 3 3 \times 3 3×3 卷积提取特征,之后使用 1 × 1 1 \times 1 1×1 卷积提升通道数量,最后加上残差连接。整个过程是「压缩-卷积-扩张」。

2) Inverted Residual

先使用 1 × 1 1 \times 1 1×1 卷积提升通道数量,然后使用 3 × 3 3 \times 3 3×3 卷积提取特征,之后使用 1 × 1 1 \times 1 1×1 卷积降低通道数量,最后加上残差连接。整个过程是「扩张-卷积-压缩」。

对比两个结构块如下图所示:

轻量化 CNN 架构; Residual block & Inverted Residual block

7.2 Linear Bottleneck

相比于 MobileNet 的基础模块,MobileNet V2 在 Depthwise Convolution 的前面加了一个 1 × 1 1 \times 1 1×1 卷积,使用 ReLU6 代替 ReLU,且去掉了第二个 1 × 1 1 \times 1 1×1 卷积的激活函数(即使用线性的激活函数),防止 ReLU 对特征的破坏。

7.3 MobileNet V2 基础模块

使用上述的方法对 MobileNet 的基础模块进行改进,得到如下所示的 MobileNet V2 基础模块:

MobileNet V2; MobileNet V2 基础模块

8.MobileNet V3

在 MobileNet V2 的基础上,又提出了 MobileNet V3,它的优化之处包括:引入了 SE尾部结构改进通道数目调整h-swish 激活函数应用NAS 网络结构搜索等。我们来逐个看一下:

8.1 SE 结构

MobileNet V3 在 bottleneck 中引入了 SE 结构,放在 Depthwise Convolution 之后,并且将 Expansion Layer 的通道数目变为原来的 1 / 4 1/4 1/4 ,在提升精度的同时基本不增加时间消耗。

MobileNet V3; 引入 SE

8.2 尾部结构改进

MobileNet V3 对尾部结构做了 2 处修改,从下图中「上方结构」修改为「下方结构」:

MobileNet V3; 修改尾部结构

  • 将 1 × 1 1 \times 1 1×1 卷积移动到 avg pooling 后面,降低计算量。
  • 去掉了尾部结构中「扩张-卷积-压缩」中的 3 × 3 3 \times 3 3×3 卷积以及其后面的 1 × 1 1 \times 1 1×1 卷积,进一步减少计算量,精度没有损失。

8.3 通道数目调整

相比于 MobileNet V2,MobileNet V3 对头部卷积通道数目进行了进一步的降低。

8.4 h-swish 激活函数

MobileNet V3 采用了 h − s w i s h \mathbf{h-swish} h−swish 激活函数,对应的 s w i s h \mathbf{swish} swish 和 h − s w i s h \mathbf{h-swish} h−swish 激活函数计算公式如下:

s w i s h [ x ] = x ⋅ σ ( x ) \mathbf{swish}[x] = x \cdot \sigma(x) swish[x]=x⋅σ(x)

h − s w i s h [ x ] = x R e L U 6 ( x + 3 ) 6 \mathbf{h-swish}[x] = x \frac {\mathbf{ReLU6}(x + 3)}{6} h−swish[x]=x6ReLU6(x+3)​

8.5 NAS 网络结构搜索

MobileNet V3 先用 NAS 搜索各个模块,得到大致的网络结构,相当于整体结构搜索;然后用 NASAdapt 得到每个卷积层的通道数目,相当于局部搜索。

9.参考资料

10.要点总结

  • 神经网络参数与复杂度计算
  • 轻量化网络
  • SqueezeNet
  • Xception
  • ShuffleNet V1~V2
  • MobileNet V1~V3

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(11) | 循环神经网络及视觉应用(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125027623

ShowMeAI 研究中心


Recurrent Neural Networks; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


本篇重点

  • RNN 的概念与多种形式
  • 语言模型
  • 图像标注、视觉问答、注意力模型
  • RNN 梯度流

1.RNN 的概念与多种形式

关于 RNN 的详细知识也可以对比阅读ShowMeAI的以下内容

1.1 形式

普通的神经网络会有 1 个固定维度的输入(如 1 张图片、1 个向量),经过一系列隐层计算得到 1 个固定的输出(如不同类别的得分/概率向量)。我们今天提到的循环神经网络(Recurrent Neural Networks)是一种特殊的神经网络,可以对序列数据做很好的建模,RNN 很灵活,可以实现输入和输出的不同类型,如下图所示:

RNN; 多种形式

1) 1 对 1

这种情况,输入输出都是固定的。

2) 1 对多

这种情况,输入固定尺寸,比如 1 张图片;输出是可变长度的序列,比如 1 段描述文本。文本的单词数可能就会随图片不同而不同,因此输出值的长度需要是一个变量。 (「图片描述/看图说话」image captioning)

3) 多对 1

这种情况,输入的尺寸是变化的,输出是固定的。如情感分类任务,输入的一段长度可变的文本序列,得到一个文字情感属性的类别;再比如可以输入时间长度不同的视频,然后判断视频中的活动(固定)。

4) 多对多

这种情况,输入输出的尺寸都是可变的,如机器翻译任务,英文翻译成中文。输入输出的文本长度都是可变的,并且两者长度不要求相同。

5) 多对多(一一对应)

这种情况,输入是可变序列,输出是针对输入的每个元素做出判断。如帧级别视频分类任务,输入是帧数可变的视频,输出对每一帧进行决策。

即使是输入输出尺寸都是固定的情形也可以使用循环神经网络 RNN。

  • 比如 1 张写满数字的固定尺寸的图片,想要识别上面的数字,可以是经过一系列的观察,观察图片的不同部分,然后做出决策;
  • 再比如生成一张固定尺寸的图片,上面写满了数字,也可以经过一系列的步骤,一次只能生成一部分。

1.2 概念

如下图所示,RNN 有一个反复出现的小循环核心单元,输入 x x x 给 RNN,计算得到内部隐状态 hidden state,且其会在每次读取新的输入时进行更新。当模型下一次读取输入时,内部隐状态会将结果反馈给模型。

RNN; 小循环核心单元

通常,我们想让 RNN 在每一个时间步都给出输出,是这样的模式:

  • 读取输入 → 更新隐状态 → 基于隐状态计算得到输出。

RNN; 小循环核心单元

由于输入是一系列 x x x 向量,在每一个时刻绿色的 RNN 部分可以使用循环公式来表示:

h t = f W ( h t − 1 , x t h_t=f_W(h_{t-1}, x_t ht​=fW​(ht−1​,xt​

其中, x t x_t xt​ 是某一时刻输入的向量, h t − 1 h_{t-1} ht−1​ 是该时刻之前的隐状态, f W f_W fW​ 是参数 W W W 的函数, h t h_t ht​ 是更新后的隐状态。这样的结构不断循环。

如果想要在每一个时间步骤得到一个输出,那么可以把更新后的隐状态 h t h_t ht​ 作为输入,经过全连接网络,得到决策。注意: f W f_W fW​ 在每一个时间步都是相同的。

下面是一个最简单的例子,“Vanilla RNN”:

h t = t a n h ( W h h h t − 1 , W x h x t ) h_t=tanh(W_{hh}h_{t-1}, W_{xh}x_t) ht​=tanh(Whh​ht−1​,Wxh​xt​)

y t = W h y h t y_t=W_{hy}h_t yt​=Why​ht​

旧状态和输入 x x x 都是与权重矩阵相乘再求和经过非线性函数 t a n h ( ) tanh() tanh(),输出也是新状态与权重矩阵相乘。每个隐状态都只有一个唯一的 h h h 向量。

1.3 计算图

1) 多对多(xy 一一对应)

这里的多对多指的是输入 x x x 和输出 y y y 都是序列,且在时间步上有一一对应关系。如下是一个多对多的计算图:

RNN 计算图; 多对多 many to many

初始状态为 h 0 h_0 h0​,在时间步 t = 1 t=1 t=1 时刻有输入 x 1 x_1 x1​,将 h 0 h_0 h0​、 x 1 x_1 x1​ 带入参数函数 f f f 得到 h 1 h_1 h1​ 更新隐状态作为下一个时刻的状态,这样以此类推得到 x t x_t xt​ 时刻的隐状态 h t h_t ht​。

计算 h t h_t ht​ 的每一步都使用的相同的参数 W W W参数函数 f f f 也是完全相同的。这样在反向传播计算梯度时,需要将每一个时刻的梯度累加起来得到最终 W W W 的梯度。

除了更新隐状态,如果每一个时刻都想得到一个输出,可以直接将 h t h_t ht​ 经过计算(比如 softmax)得到 y t y_t yt​ ,如果对于输入 x x x 的每一个时刻都对应一个真实标签的话,在计算出 y t y_t yt​ 的同时可以得到每一步的损失 L t L_t Lt​ ,最终的损失也是所有的 L t L_t Lt​ 加起来。

2) 多对一

典型的应用之一是情感分析任务,根据最后一个隐状态得到输出。模型经过迭代,在最后一个隐状态包含了之前所有的信息。

RNN 计算图; 多对一 many to one

3) 一对多

一对多的情形会接受固定长度的输入项,输出不定长的输出项,这个固定长度的输入项会用来初始化初始隐状态,然后 RNN 会对输出的单元逐个处理,最终会得到不定长的输出序列,输出的每个元素都得以展现。

RNN 计算图; 一对多 one to many

4) 多对多

输入输出都是不定长序列的情形,典型应用如机器翻译任务,可以看作是多对一与一对多的组合。首先输入一个不定长的 x x x,将这个序列编码成一个单独的向量,然后作为输入,输入到一对多的模型中,得到输出序列,可能是用另一种语言表述的相同意思的句子。

然后对这个不定长的输出,每一个时间步都会做出预测,比如接下来使用什么词。想象一下整个训练过程和计算图的展开,对输出序列的损失求和,然后像之前一样反向传播。

RNN 计算图; 多对多 many to many

2.语言模型

关于语言模型的详细知识也可以对比阅读ShowMeAI的以下内容

语言模型(Language Model)问题中,我们让模型读取大量语料语句,让神经网络在一定程度上学会字词的组合规律并能生成自然语言。

举个字符层面上的例子(即对语料中的内容以字符粒度进行学习),比如网络会读取一串字符序列,然后模型需要预测这个字符流的下一个字符是什么。我们有一个很小的字符表 [h, e, l, o] 包含所有字母,以及有一个训练序列 hello ,使用循环公式:

h t = t a n h ( W h h h t − 1 , W x h x t ) h_t=tanh(W_{hh}h_{t-1}, W_{xh}x_t) ht​=tanh(Whh​ht−1​,Wxh​xt​)

语言模型训练阶段,我们将训练序列作为输入项,每一个时间步的输入都是一个字符,首先需要做的是在神经网络中表示这个单词。

语言模型; 训练阶段

我们在这里使用长为 4 的独热向量来表示每个字符(只有字符对应的位置是 1 1 1,其他位置为 0 0 0)。比如 h 可以用向量 [ 1000 ] [1 0 0 0] [1000] 表示,l 使用 [ 0010 ] [0 0 1 0] [0010] 表示。现在在第一个时间步中,网络会接收输入 h,进入第一个 RNN 单元,然后得到输出 y 1 y_1 y1​,作为对接下来的字符的一个预测,也就是网络认为的接下来应该输入的字符。

由于第一个输入的是 h,所以接下来输入的应该是 e,但是模型只是在做预测,如下图所示,模型可能认为接下来要输入的是 o。在这种错误预测下,我们可以基于 softmax 计算损失来度量我们对预测结果的不满意程度。

在下一个时间步,我们会输入 e,利用这个输入和之前的隐状态计算出新的隐状态,然后利用新的隐状态对接下来的字符进行预测,我们希望下一个字符是 l,但这里模型可能也预测错了,这里又可以计算损失。这样经过不断的训练,模型就会学会如何根据当前的输入预测接下来的输入。

在语言模型测试阶段,我们想用训练好的模型测试样本或者生成新的文本(类似于训练时使用的文本)。

语言模型; 测试阶段

方法是输入文本的前缀来测试模型,上述例子中的前缀是 h,现在在 RNN 的第一步输入 h,它会产生基于词库所有字母得分的一个 softmax 概率分布,然后使用这个概率分布预测接下来的输出(这个过程叫做 sample/采样),如果我们足够幸运得到了字符 e,然后把这个得到的 e 重新写成 01 01 01 向量的形式反馈给模型,作为下一个时间步的输入,以此类推。

2.1 截断反向传播(Truncated Backpropagation )

在前向传播中需要遍历整个序列累加计算损失,在反向传播中也需要遍历整个序列来计算梯度。我们可以想象一下,如果我们的语料库非常大(例如维基百科中所有文本),那么时间花费以及内存占用都是巨大的,如下图所示。

语言模型; 截断反向传播

实际应用中,通常使用沿时间的截断反向传播(Truncated Backpropagation),这样输入的序列可以接近无穷。

前向传播时不再使用整个序列计算损失,而是使用序列的一个块,比如 100 个时间步,计算出损失值,然后反向传播计算梯度。然后基于第 2 批数据再次计算和更新。

如上的过程循环操作,「截断」使得开销大大减小。

语言模型; 截断反向传播

这个过程的完整实现代码:点击这里

2.2 RNN 语言模型应用

  • 使用莎士比亚的文集对语言模型进行训练,然后生成新的文本。结果可以生成莎士比亚风格的文章(当然,结果不是完美的,其中有些部分有错误),如下图所示

语言模型; RNN 语言模型应用

  • 使用拓扑学教材,可以自动生成一些定理、公式甚至可以画图,虽然都没有意义,但可以学会这些结构。

语言模型; RNN 语言模型应用

  • 使用 linux 源码进行训练。可以生成 C 代码,有缩进有变量声明甚至会写注释等等,看起来非常像 C 代码(当然代码逻辑不一定正常,编译会有很多错误)。

语言模型; RNN 语言模型应用

2.3 RNN 语言模型解释

在 RNN 中有隐藏向量,每一步都会更新,我们在这些隐藏向量中寻找可以解释的单元。

比如在语言模型训练中观察隐向量中的某一个元素值,元素值会随着每一个时间步进行改变。大多数的元素值变化都是杂乱无章的,似乎在进行一些低级的语言建模。但是有一些元素却有特殊的表现:

  • 蓝色位置数值低,红色位置数值高
  • 比如某些元素遇到引号后,元素值会变得很低,然后一直保持很低直到下一个引号处被激活,元素值变大,然后保持到下一个引号再变低。所以有可能是检测引号的神经元

语言模型; RNN 语言模型解释

  • 还有某些神经元在统计回车符前的字符数量,即字符有多少时会自动换行。某一行开始处元素的值很低,然后慢慢增大,达到一定值后自动变成 0,然后文本换行。

语言模型; RNN 语言模型解释

  • 在训练代码的例子中,有些神经元似乎在判断是否在 if 语句,是否在注释内,以及表示不同的缩进层级。

语言模型; RNN 语言模型解释

上述细节内容可以看出,在训练文本的过程中,RNN 学到一些文本的结构。(虽然这些已经不是计算机视觉的内容了)

3.看图说话、视觉问答、注意力模型

之前提过很多次 图片描述/看图说话(Image Captioning),即训练一个模型,输入一张图片,然后得到它的自然语言语义描述。这里输出的结果文本可能长度不同,单词字符数不同,这个任务天生适合 RNN 模型建模。

如下图的一个例子,图像标注模型训练阶段一般都先通过卷积神经网络处理图像生成图像向量(用其作为图像内容的表征),然后输入到 RNN 语言模型的第一个时间步中,RNN 会计算调整隐状态,进而基于 softmax 得到结果并计算损失,后续语言模型在每个时间步都生成 1 个组成描述文本的单词。

看图说话 ; image captioning 图像标注训练阶段

测试阶段/推理阶段和之前字符级的语言模型类似。

  • 我们把测试图像输入到卷积神经网络,通过 CNN 得到模型最后 1 个全连接层之前的 1 个图像向量,作为整张图像的内容表征。
  • 之后会给语言模型输入一个开始标志,告诉模型开始生成以这个图像为条件的文本。不同于以往的隐状态公式,在这个任务中我们会把图像信息输入到每个时间步用于更新隐状态:

h = t a n h ( W x h ∗ x + W h h ∗ h + W i h ∗ v ) h = tanh(Wxh \ast x + Whh \ast h + Wih \ast v) h=tanh(Wxh∗x+Whh∗h+Wih∗v)

  • 现在就可以根据图像的内容生成一个词汇表(有很多词汇)中所有单词的一个 softmax 得分概率分布,sample/取样之后作为下一个时间步的输入。
  • 直到取样到「结束符号」整个预测过程结束,文本也生成完毕。

看图说话; 图像描述测试阶段

这些训练后的模型在测试时对和训练集类似的片会表现的很好,对和训练集差距大的图片可能变现不佳,比如对一些没见过的物体进行误判,以及分不清扔球还是接球等。

3.1 基于注意力的「看图说话」模型

下面我们来看看基于「注意力机制」的 图片描述/看图说话 模型,这个模型包含的 RNN 在生成单词时,会将注意力放在图像不同的部分。

在这个模型中,CNN 处理图像后,不再返回一个单独的向量,而是得到图像不同位置的特征向量,比如 L L L 个位置,每个位置的特征有 D D D 维,最终返回的 CNN 结果数据是一个 L × D L \times D L×D 的特征图。(想象 CNN 的中间层得到的特征图)

这样在 RNN 的每一个时间步,除了得到词汇表的采样,还会得到基于图片位置的分布,它代表了 RNN 想要观察图像的哪个部分。这种位置分布,就是 RNN 模型应该观察图像哪个位置的「注意力」。

在第 1 个隐状态会计算基于图片位置的分布 a 1 a_1 a1​,这个分布会返回到图像特征图 L × D L \times D L×D,给得出一个单一的具有统计性质的 D D D 维特征向量,即把注意力集中在图像的一部分。这个特征向量会作为下一个时间步的额外输入,另一个输入是单词。然后会得到两个输出,一个是基于词汇的分布,一个是基于位置的分布。这个过程会一直持续下去,每个步骤都有两个输入两个输出。

整个过程如下图所示:

看图说话; 基于注意力的看图说话模型

上图 z z z 的计算公式可以是:

z = ∑ i = 1 L p i v i z = \sum_{i=1}^L {p_iv_i} z=i=1∑L​pi​vi​

p i p_i pi​ 就是基于位置的分布, v i v_i vi​ 就是 L L L 个特征向量中的一个,最后得到一个统计向量 $ z$。

这种结合所有特征的分布方式称为软注意力(Soft attention),与之对应的是硬注意力(Hard attention)。

硬注意力每次只产生一个单独的特征向量,不是所有特征的组合,但它反向传播比较复杂,因为(区域)选择的过程本身不是一个可微的函数。

看图说话; 软注意力与硬注意力

这样模型会自己学习每一个时间步应该主要把注意力集中在图片的什么位置得到什么词汇,效果通常也很好。如下图所示:

看图说话; 模型结果

这个结构的模型也可以用于其他任务,比如视觉问答(Visual Question Answering)。

视觉问答; Visual Question Answering

在视觉问答任务中,会有两个输入,一个是图像,一个是关于图像的用自然语言描述的问题。模型从一些答案中选择一个正确的。

这是一个多对一模型,我们需要将问题作为序列输入,针对序列的每一个元素建立 RNN,可以将问题概括成一个向量。然后把图像也概括成一个向量,现在将两个向量结合通过 RNN 编程预测答案。结合方式可以是直接连接起来也可以是进行复杂的运算。

4.RNN 梯度流

4.1 多层 RNN(Multilayer RNNs)

我们之前看到的朴素 RNN,隐状态只有 1 层,在「多层 RNN」中隐状态有 3 层。一次运行得到 RNN 第 1 个隐状态的序列,然后作为第 2 个隐状态的输入。如下图所示:

RNN 梯度流; 多层 RNN

4.2 普通 RNN 梯度流

我们来看看普通 RNN 的梯度流,在前向传播过程中计算 h t h_t ht​ :

h t = t a n h ( W h h h t − 1 + W x h x t ) = t a n h ( ( W h h W x h ) ( h t − 1 x t ) ) = t a n h ( W ( h t − 1 x t ) ) \begin{aligned} h_t & = tanh(W_{hh}h_{t-1}+W_{xh}x_t) \ & = tanh \bigl (\bigl (\begin{matrix}W_{hh} \ &W_{xh} \end{matrix}\bigr)\bigl(\begin{matrix}h_{t-1}\x_{t} \end{matrix}\bigr)\bigr) \ & =tanh\bigl(W\bigl(\begin{matrix}h_{t-1}\ x_{t} \end{matrix}\bigr)\bigr) \end{aligned} ht​​=tanh(Whh​ht−1​+Wxh​xt​)=tanh((Whh​​Wxh​​)(ht−1​xt​​))=tanh(W(ht−1​xt​​))​

反向传播梯度流从 h t h_t ht​ 到 h t − 1 h_{t-1} ht−1​ 需要乘 W h h T W_{hh}^T WhhT​

RNN 梯度流; LSTM 梯度流

有一个问题,在上述 RNN 训练过程中,从最后一个隐状态传到第一个隐状态,中间要乘很多次权重,如下图所示。

如果累计相乘的值频繁大于 1 1 1,就可能会梯度爆炸;如果频繁小于 1 1 1,梯度就可能会慢慢趋近 0 0 0(梯度消失)。

RNN 梯度流; LSTM 梯度流

对于梯度爆炸,一种处理方法是给梯度设置一个阈值,如果梯度的 L2 范式超过这个阈值就要减小梯度,代码如下:

grad_num = np.sum(grad * grad)
if grad_num > threshold:
    grad *= (threshold / grad_num) 

对于梯度消失问题,我们可以改造 RNN 网络结构,得到更适合长距离建模的结构,如下面要展开讲解到的 LSTM 和 GRU。

4.3 LSTM(Long Short Term Memory )

关于 LSTM 的详细讲解也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 序列模型与 RNN 网络 中对于 LSTM 的讲解。

LSTM(长短期记忆)网络就是用来解决「梯度爆炸」和「梯度消失」问题的,与其在输出上限制梯度,LSTM 的网络结构更加复杂。

( i f o g ) = ( σ σ σ tanh ⁡ ) W ( h t − 1 x t ) c t = f ⊙ c t − 1 + i ⊙ g h t = o ⊙ tanh ⁡ ( c t ) \begin{aligned} \left(\begin{array}{l} i \ f \ o \ g \end{array}\right) &=\left(\begin{array}{c} \sigma \ \sigma \ \sigma \ \tanh \end{array}\right) W\left(\begin{array}{c} h_{t-1} \ x_{t} \end{array}\right) \ c_{t} &=f \odot c_{t-1}+i \odot g \ h_{t} &=o \odot \tanh \left(c_{t}\right) \end{aligned} ⎝⎜⎜⎛​ifog​⎠⎟⎟⎞​ct​ht​​=⎝⎜⎜⎛​σσσtanh​⎠⎟⎟⎞​W(ht−1​xt​​)=f⊙ct−1​+i⊙g=o⊙tanh(ct​)​

LSTM 在每一个时间步都会维持两个隐状态:

  • h t h_t ht​ 和普通 RNN 中的一致,是 RNN 网络的隐状态;
  • 单元状态向量 c t c_t ct​ ,是保留在 LSTM 内部的隐状态,不会完全暴露到外部去。计算公式可以看出,LSTM 会使用输入和之前的隐状态来更新四个组成 c t c_t ct​ 的门,然后使用 c t c_t ct​ 来更新 h t h_t ht​ .

与普通 RNN 不同的是,LSTM 不直接将权重矩阵 W W W 乘 x t x_t xt​ 和 h t − 1 h_{t-1} ht−1​ 拼接成的向量再经过 t a n h ( ) tanh() tanh() 函数得到隐状态 h t h_t ht​ 。

LSTM 中的权重矩阵计算会得到 4 个与隐状态 h h h 大小相同的向量,然后分别通过不同的非线性函数就得到了单元状态 $ c_t$ 的四个门: i , f , o , g i, f, o, g i,f,o,g

  • i i i 是输入门(Input gate) ,表示有多少内容被写到单元状态;
  • f f f 是遗忘门(Forget gate),表示对之前的单元状态的遗忘程度;
  • o o o 是输出门(Output gate) ,表示单元状态输出多少给隐状态;
  • g g g 是门值门(Gate gate ) ,控制写入到单元状态的信息。

从 c t c_t ct​ 的计算公式来看,之所采用不同的非线性函数,可以这么理解:

  • f f f 是对之前的单元状态的遗忘,如果是 0 0 0 全部遗忘,如果是 1 1 1 就全部保留,那个圆圈加点的符号表示逐元素相乘;
  • i i i 和 g g g 共同控制写入到单元状态向量的信息, i i i 在 0 ∼ 1 0 \sim1 0∼1 之间, g g g 在 − 1 -1 −1 到 1 1 1 之间,这样每个时间步,单元状态向量的每个元素最大自增 1 1 1 或最小自减 1 1 1。
  • 这样 c t c_t ct​ 可以看成是对 [ − 1 1 ] [-1 \quad 1] [−11] 的按时间步计数。然后 c t c_t ct​ 通过 t a n h ( ) tanh() tanh() 将这个计数压缩到 [ 0 1 ] [0 \quad 1] [01] 范围,然后 o o o 来控制将多少单元状态的信息输出给隐状态 h t h_t ht​ 。

RNN 梯度流; LSTM

LSTM 梯度流

如下图所示,我们用灰色的线表示 LSTM 前向传播,完全按照公式来的,圆圈加点运算符表示两个向量逐元素相乘,不是矩阵的乘法。这样从 c t c_t ct​ 到 c t − 1 c_{t-1} ct−1​ 的反向传播过程,只会与 f f f 进行逐元素相乘,与乘 W W W 相比要简单很多。

LSTM 不同的时间步 f f f 的值都不同,不像普通 RNN 每次都乘相同的 W W W,这样就一定程度避免梯度爆炸或锐减。而且 f f f 的值也是在 [ 0 , 1 ] [0, 1] [0,1] 性质非常好。

RNN 梯度流; LSTM 梯度流

当多个单元连起来的时候,就会为梯度在单元状态间流动提供了一条高速公路(这种形式与残差网络类似)。

RNN 梯度流; LSTM 梯度流

4.4 GRU(Gated Recurrent Unit)

关于 LSTM 的详细讲解也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章序列模型与 RNN 网络中对于 GRU 的讲解。

另一个改进普通 RNN 的方法得到「门循环单元(GRU)」模型,它总体和 LSTM 相似,也是应用了这种逐元素相乘与加法结合的形式,这样梯度也能高速流动。

r t = σ ( W x r x t + W h r h t − 1 + b r ) z t = σ ( W x z x t + W h z h t − 1 + b z ) h ~ t = tanh ⁡ ( W x h x t + W h h ( r t ⊙ h t − 1 ) + b h ) h t = z t ⊙ h t − 1 + ( 1 − z t ) ⊙ h ~ t \begin{aligned} r_{t} &=\sigma\left(W_{x r} x_{t}+W_{h r} h_{t-1}+b_{r}\right) \ z_{t} &=\sigma\left(W_{x z} x_{t}+W_{h z} h_{t-1}+b_{z}\right) \ \tilde{h}{t} &=\tanh \left(W x_{t}+W_{h h}\left(r_{t} \odot h_{t-1}\right)+b_{h}\right) \ h_{t} &=z_{t} \odot h_{t-1}+\left(1-z_{t}\right) \odot \tilde{h}_{t} \end{aligned} rt​zt​ht​ht​​=σ(Wxr​xt​+Whr​ht−1​+br​)=σ(Wxz​xt​+Whz​ht−1​+bz​)=tanh(Wxh​xt​+Whh​(rt​⊙ht−1​)+bh​)=zt​⊙ht−1​+(1−zt​)⊙ht​​

5.推荐学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=10

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

6.要点总结

  • RNN 是一种输入输出都很灵活的网络结构;
  • Vanilla RNNs 很简单,但是效果不好;比较普遍的是使用 LSTM 和 GRU,能有效的改善梯度流;
  • RNN 反向传播梯度爆炸可以给梯度设置阈值,而梯度锐减可以使用复杂的网络,比如 LSTM;
  • RNN 的应用:图像标注、视觉问答,都是将 CNN 与 RNN 结合起来;
  • 理解语言模型与注意力模型;

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(12) | 目标检测 (两阶段,R-CNN 系列)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125027635

ShowMeAI 研究中心


Detectionand Segmentation; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

ShowMeAI 在前面的内容中给大家做了很多图像分类的介绍,主要围绕卷积神经网络(LeNet / AlexNet / NIN / VGG / Google / ResNet / MobileNet / squeezenet)讲解,但计算机视觉领域有其他一些更为复杂的任务,例如本篇开始介绍的目标检测(object detection)问题。

1. 计算机视觉任务

大家知道人工智能领域的 3 大热点方向是计算机视觉(CV,computer vision)、自然语言处理(Natural Language Process, NLP )和语音识别(Speech Recognition) 应用 。而计算机视觉领域又有图像分类、目标检测、图像分割三大任务,如下图所示

计算机视觉任务; 图像分类 / 目标检测 / 图像分割

这 3 大任务其实对应机器视觉理解图像的 3 个主要层次:

1.1 图像分类(Classification)

图像分类任务中,我们要将图像识别判定为某个类别。它是最简单、最基础的图像理解任务,也是深度学习模型最先取得突破和实现大规模应用的任务。大家在前面也了解到了 ImageNet 这个权威评测集,每年的 ILSVRC 催生了大量的优秀深度网络结构,为其他任务提供了基础。

有一些其他的应用,包括人脸识别、场景识别等都可以化归为分类任务来解决。

1.2 目标检测(Detection)

图像分类任务关心整体图片类别,而目标检测则关注特定的物体目标,要求在图片中,同时识别出目标物的类别信息和位置信息(是一个 classification + localization 的问题)。

相比分类,目标检测任务要求我们需要从背景中分离出感兴趣的目标,并确定这一目标的描述(类别和位置),检测模型的输出形式通常是一个列表,列表的每一项使用一个数组给出检出目标的类别和位置(常用矩形检测框的坐标表示)。

1.3 图像分割(Segmentation)

图像分割包括语义分割(semantic segmentation)和实例分割(instance segmentation),前者是对前背景分离的拓展,要求分离开具有不同语义的图像部分(相当于像素级别的分类),而后者是检测任务的拓展,要求描述出目标的轮廓(相比检测框更为精细)。

分割是对图像的像素级描述,它赋予每个像素类别意义,适用于理解要求较高的场景,如无人驾驶中对道路和非道路的分割,医疗影像中对于不同区域的划分。

1.4 总结

图像分类对应将图像划分为单个类别的过程,它通常对应于图像中最突出的物体。实际现实世界的很多图像通常包含多个物体,如果仅仅使用图像分类模型分配单一标签是非常粗糙的,并不准确。而目标检测(object detection)模型可以识别一张图片的多个物体,并可以给出不同物体的具体位置(边界框)。目标检测在很多场景有用,如无人驾驶和安防系统。

2. 常用目标检测(Object Detection)算法综述

2.1 总体介绍

常见的经典目标检测算法如下图所示:

目标检测的常用算法; Object Detection

目标检测的基本思路是:解决定位(localization) + 识别(Recognition) 两个任务。

一个大致的 pipeline 如下图所示,我们可以用同样的特征抽取过程,借助两个不同的分支输出。

  • 一个分支用于做图像分类,即全连接 + Softmax 判断目标类别,和单纯图像分类区别在于这里还另外需要一个「背景」类。
  • 另一个分支用于识别目标位置,即完成回归任务输出四个数字标记包围盒位置(例如中心点横纵坐标和包围盒长宽),该分支输出结果只有在分类分支判断不为「背景」时才使用。

目标检测的基本思路; 定位 localization + 识别 Recognition

2.2 传统方法

传统的目标检测框架,主要包括三个步骤:

  • ① 利用不同尺寸的滑动窗口框住图中的某一部分作为候选区域;
  • ② 提取候选区域相关的视觉特征。比如人脸检测常用的 Harr 特征;行人检测和普通目标检测常用的 HOG 特征等;
  • ③ 利用分类器进行识别,比如常用的 SVM 模型。

2.3 两阶段 vs 一阶段 方法

现在主流的深度学习目标检测方法主要分为两类:两阶段(Two Stages)目标检测算法一阶段(One Stage)目标检测算法

两阶段模型结构; 一阶段模型结构

1) 两阶段(Two Stages)

  • 首先由算法(algorithm)生成一系列作为样本的候选框,再通过卷积神经网络进行分类。
  • 常见的算法有 R-CNN、Fast R-CNN、Faster R-CNN 等等。

2) 一阶段(One Stage )

  • 不需要产生候选框,直接将目标框定位的问题转化为回归(Regression)问题处理(Process)。
  • 常见的算法有 YOLO、SSD 等等。

上述两类方法,基于候选区域(Region Proposal)的方法(两阶段)在检测准确率和定位精度上占优,基于端到端(一阶段)的算法速度占优。相对于 R-CNN 系列的「两步走」(候选框提取和分类),YOLO 等方法只「看一遍」。

我们在本篇中给大家介绍两阶段的目标检测方法,主要是 R-CNN 系列目标检测方法,在下篇内容目标检测 (SSD,YOLO 系列)中给大家介绍一阶段的目标检测方法(YOLO 系列,SSD 等)。

3.两阶段目标检测算法发展史

两阶段目标检测算法; Two Stages

4.两阶段目标检测典型算法

4.1 R-CNN

如何将深度学习分类算法应用到目标检测?

  • 用深度学习分类模型提取特征方法代替传统图像特征提取算法。

R-CNN 核心思想: 对每张图片选取多个区域,然后每个区域作为一个样本进入一个卷积神经网络来抽取特征。

R-CNN; R-CNN 核心思想

1) R-CNN 网络结构

R-CNN 算法是较早提出的两阶段目标检测算法,它先找出 Region Proposal,再进行分类和回归。

  • 所谓 Region Proposal 就是图中目标可能出现的位置。
  • 因为传统方法需要枚举的区域太多了,所以通过利用图像中的纹理、边缘、颜色等信息,可以保证在选取较少窗口(几千甚至几百)的情况下保持较高的响应比。所以,问题就转变成找出可能含有物体的候选框,这些框之间是可以互相重叠互相包含的,这样我们就可以避免暴力枚举的所有框了。

2) R-CNN 应用流程

对于每张输入的图像,R-CNN 目标检测主要包括下述步骤:

  • ① 利用选择性搜索 Selective Search 算法在图像中从下到上提取 2000 个左右的可能包含物体的候选区域 Region Proposal
  • ② 因为获取到的候选区域大小各不相同,所以需要将每个 Region Proposal 缩放(warp)成统一的 227 × 227 227 \times 227 227×227 的大小并输入到 CNN,将 CNN 的 fc7 层的输出作为特征
  • ③ 将每个 Region Proposal 提取到的 CNN 特征输入到 SVM 进行分类
  • ④ 使用这些区域特征来训练线性回归器对区域位置进行调整

R-CNN; R-CNN 应用流程

3) R-CNN 不足与优化

R-CNN 的效果如下图所示,它有一些不足之处(也是系列算法后续改进的点):

  • R-CNN 虽然不需要穷举所有框了,但是它需要对所有 ss 算法选取出的候选框 region proposal (2000 多个)进行CNN 提取特征 + SVM 分类,计算量很大,导致 R-CNN 检测速度很慢,一张图都需要 47s。
  • Selective search 提取的区域质量不够好
  • 特征提取与后续 SVM 分类器是独立训练的,没有联合优化,且训练耗时长

优化方式为:

  • 2000 个 region proposal 是图像的一部分,可以对图像只进行一次卷积提取特征,然后将 region proposal 在原图的位置映射到卷积层特征图上,得到映射后的各个 proposal 的特征输入到全连接层做后续操作。
  • 每个 region proposal 的大小都不一样,而全连接层输入必须是固定的长度,因此不能将 proposal 的特征直接输入全连接层,后续改进向 R-CNN 模型引入了 SPP-Net(也因此诞生了 Fast R-CNN 模型)。

R-CNN; R-CNN 不足与优化

4.2 SPP-Net

1) 设计出发点

我们通过前面的 CNN 相关知识学习知道,CNN 的卷积层不需要固定尺寸的图像,而全连接层是需要固定大小的输入。所以当全连接层面对各种尺寸的输入数据时,就需要对输入数据进行 crop(抠图)或者 wrap(图像 resize)操作。

在 R-CNN 中,因为不同的 proposal 大小不同,所以需要先 resize 成相同大小再输入到 CNN 中。既然卷积层是可以接受任何尺寸的,可以在卷积层后面加上一部分结构使得后面全连接层的输入为固定的,这个「化腐朽为神奇」的结构就是 spatial pyramid pooling layer。

下图是 R-CNN 和 SPP-Net 检测流程的比较:

SPP-Net; SPP-Net 设计出发点

SPP-Net 和普通 CNN 的对比结构如下,在网络结构上,直接把 pool5 层替换成 SPP 层:

SPP-Net; SPP-Net V.S. 普通 CNN

SPP-Net 的具体细节如下,由 features map 上确定的 region proposal 大小不固定,将提取的 region proposal 分别经过三个卷积 4 ∗ 4 4 \ast 4 4∗4, 2 ∗ 2 2 \ast 2 2∗2, 1 ∗ 1 1 \ast 1 1∗1 ,都将得到一个长度为 21 的向量(21 是数据集类别数,可以通过调整卷积核大小来调整),因此不需要对 region proposal 进行尺寸调整:

SPP-Net; SPP-Net 的具体细节

相比 R-CNN,SPP-Net 有两大优点。

① 通过「特征金字塔池化」模块,实现了 CNN 的多尺度输入,使得网络的输入图像可以是任意尺寸的,输出则不变,同样是一个固定维数的向量。

② R-CNN 要对每个区域计算卷积,而 SPPNet 只需要计算一次卷积,从而节省了大量的计算时间。

  • R-CNN 流程中,先用 ss 算法得到 2000 个 proposal 分别做卷积操作
  • SPP-Net 只对原图进行一次卷积计算,得到整张图的卷积特征 feature map,然后找到每个候选框在 feature map 上的映射 patch,将此 patch 作为每个候选框的卷积特征,输入到 SPP 层以及之后的层,完成特征提取工作。

4.3 Fast R-CNN

对于 RCNN 速度过慢等问题,提出了基于 RCNN 的改善模型 Fast RCNN。

1) 核心改进

Fast RCNN 主要改进以下部分:

  • ① 将 classification 和 detection 的部分融合到 CNN 中,不再使用额外的 SVM 和 Regressor,极大地减少了计算量和训练速度。
  • ② Selective Search 后不再对 region proposal 得到的 2k 个候选框进行截取输入,改用 ROI Project,将 region proposal 映射到 feature map 上
  • ③ 使用 ROI pooling 将在 feature map 上不同尺度大小的 ROI 归一化成相同大小后就可以通过 FC 层。

2) 核心环节

如下图所示为 Fast R-CNN 流程与网络结构

Fast R-CNN; Fast R-CNN 核心环节

Fast R-CNN 具体包括的核心环节如下:

① Region Proposal:与 R-CNN 一致

跟 RCNN 一样,Fast-RCNN 采用的也是 Selective Search 的方法来产生 Region Proposal,每张图片生成 2k 张图片。但是不同的是,之后不会对 2k 个候选区域去原图截取,后输入 CNN,而是直接对原图进行一次 CNN,在 CNN 后的 feature map,通过 ROI project 在 feature map 上找到 Region Proposal 的位置。

② Convolution & ROI 映射

就是对原图输入到 CNN 中去计算,Fast-RCNN 的工具包提供提供了 3 种 CNN 的结构,默认是使用 VGG-16 作为 CNN 的主干结构。根据 VGG-16 的结构,Fast-RCNN 只用了 4 个 MaxPooling 层,最后一个换成了 ROI Pooling,因此,只需要对 Region Proposal 的在原图上的 4 元坐标 ( x , y , w , h ) (x, y, w, h) (x,y,w,h) 除以 16 16 16,并找到最近的整数,便是 ROI Project 在 feature map 上映射的坐标结果。最终得到 2 k 2k 2k 个 ROI。

③ ROI Pooling

对每一个 ROI 在 feature map 上截取后,进行 ROI Pooling,就是将每个 ROI 截取出的块,通过 MaxPooling 池化到相同维度。

ROI Pooling 的计算原理是,将每个不同大小的 ROI 平均划分成 7 × 7 7 \times 7 7×7 的 grid,在每个 grid 中取最大值,最后所有 ROI 都会池化成大小为 7 × 7 7 \times 7 7×7 维度。

④ 全连接层 & 输出

将每个 ROI Pooling 后的块,通过全连接层生成 ROI 特征向量,最后用一个 Softmax 和一个 bbox regressor 进行分类和回归预测,得到每个 ROI 的类别分数和 bbox 坐标。全连接层为矩阵相乘运算,运行消耗较多,速度较慢,作者在这里提出可以使用 SVD 矩阵分解来加快全连接层的计算。

⑤ 多任务损失

Fast-RCNN 的两个任务:

  • 一个是分类,分为 n ( 种 类 ) + 1 ( 背 景 ) n(种类) + 1(背景) n(种类)+1(背景) 类,使用的是Cross Entropy + Softmax 的损失函数
  • 第二个是 Bbox 的 Localization 回归,使用跟 Faster-RCNN 一样的基于 Offset 的回归,损失函数使用的是 Smooth L1 Loss,具体原理在下方 Faster-RCNN 中介绍。

3) Fast R-CNN 网络效果

Fast R-CNN; Fast R-CNN 网络效果

Fast R-CNN 效果如上图所示,相比之 R-CNN 它在训练和预测速度上都有了很大的提升,但它依旧有不足之处,大家观察整个流程,会发现在候选区域选择上,依旧使用的 Selective Search 方法,它是整个流程中的时间消耗瓶颈,无法用 GPU 硬件与网络进行加速。

4.4 Faster R-CNN

Faster-RCNN 在 Fast-RCNN 的基础上做了两个重大的创新改进:

  • ① 在 Region Proposal 阶段提出了 RPN(Region Proposal Network)来代替了 Selective Search
  • ② 使用到了 Anchor

Faster R-CNN; Faster R-CNN 核心思想

1) Faster R-CNN 网络结构

Faster R-CNN 的总体流程结构如下,可分为 Backbone、RPN、ROI+分类 / 回归 三个部分。

Faster R-CNN; Faster R-CNN 网络结构

Faster R-CNN; Faster R-CNN 网络结构

2) Anchor(锚框)

Anchor 是图像检测领域一个常用的结构,它可以用来表示原图中物体所在的区域,是一个以 feature map 上某个点为中心的矩形框。

Faster-RCNN 的 anchor,在 feature map 上每个点,生成 3 种尺度和 3 种比例共 9 个 anchor。

  • 下图是一个 anchor 的示意图,每个点会生成尺度为小( 128 × 128 128\times128 128×128)、中( 256 × 256 256\times256 256×256)、大( 512 × 512 512\times512 512×512),如图中红、绿、蓝色的 anchor, 1 : 1 1:1 1:1, 2 : 1 2:1 2:1, 1 : 2 1:2 1:2 三种比例共 9 个 anchor。
  • 这样充分考虑了被检测物体的大小和形状,保证物体都能由 anchor 生成 region proposal。

Faster R-CNN; Anchor 锚框

Faster R-CNN; Anchor 锚框

3) RPN 网络结构

RPN 是一个全卷积的神经网络,它的工作原理可以分成 classification,regression 和 proposal 三个部分

① Classification/分类

Classification 部分将得到的 feature map 通过一个 3 × 3 3 \times 3 3×3 和 1 × 1 1 \times 1 1×1 的卷积后,输出的维度为 [ 1 × 18 × 38 × 50 ] [1 \times 18 \times 38 \times 50] [1×18×38×50],这 18 个 channel 可以分解成 2 × 9 2\times9 2×9,2 代表着是否是感兴趣物体备选区域(region proposal)的 0/1 的 score,9 代表着 9 个 anchors。

因此,特征图维度 38 × 50 38\times50 38×50 的每一个点都会生成 9 个 anchor,每个 anchor 还会有 0/1 的 score。

② Regression/回归

Regression 部分原理和 Classification 部分差不多,feature map 通过一个 3 × 3 3 \times 3 3×3 和 1 × 1 1 \times 1 1×1 的卷积后,输出的维度为 [ 1 × 36 × 38 × 50 ] [1 \times 36 \times 38 \times 50] [1×36×38×50],其中 36 个 channel 可以分成 4 × 9 4 \times 9 4×9,9 就是跟 cls 部分一样的 9 个 anchor,4 是网络根据 anchor 生成的 bbox 的 4 元坐标 target 的 offset。通过 offset 做 bbox regression,再通过公式计算,算出预测 bbox 的 4 元坐标 ( x , y , w , h ) (x, y, w, h) (x,y,w,h) 来生成 region proposal。

③ Proposal/候选区

将前两部分的结果综合计算,便可以得出 Region Proposals。

  • 若 anchor 的 I o U > 0.7 IoU > 0.7 IoU>0.7,就认为是前景
  • 若 I o U < 0.3 IoU < 0.3 IoU<0.3,就认为是背景
  • 其他的 anchor 全都忽略

一般来说,前景和背景的 anchor 保留的比例为 1 : 3 1:3 1:3

Faster R-CNN; RPN 网络结构

① RPN 网络训练策略

RPN 网络的训练样本有如下的策略和方式:

Faster R-CNN; RPN 网络训练策略

② RPN 网络监督信息

RPN 网络是监督学习训练,包含分类和回归两个任务,分类分支和回归分支的预测值和 label 构建方式如下:

Faster R-CNN; RPN 网络监督信息

③ RPN 网络 LOSS

RPN 网络的总体 loss 由 2 部分构成,分别是分类 loss 和回归 loss,为其加权求和结构。其中分类 loss 使用常规的交叉熵损失,回归损失函数使用的是 Smooth L1 Loss,本质上就是 L1 Loss 和 L2 Loss 的结合。

Faster R-CNN; RPN 网络 LOSS

④ RPN 网络回归分支 Loss

特别说一下回归部分使用到的 Smooth L1 Loss,对比于 L1 Loss 和 L2 Loss,Smooth L1 Loss 可以从两方面限制梯度:

  • ① 当预测框与 ground truth 的 Loss 很大的时候,梯度不至于像 L2 Loss 那样过大
  • ② 当预测框与 ground truth 的 Loss 较小的时候,梯度值比 L1 Loss 更小,不至于跳出局部最优解。

Faster R-CNN; RPN 网络回归分支 Loss

4) 生成 Proposals

结合分类和回归结果得出 Region Proposals。若 anchor 的 I o U > 0.7 IoU > 0.7 IoU>0.7,就认为是前景;若 I o U < 0.3 IoU < 0.3 IoU<0.3,就认为是背景,其他的 anchor 全都忽略。一般来说,前景和背景的 anchor 保留的比例为 1 : 3 1:3 1:3 。

得到 Region Proposal 后,会先筛选除掉长宽小于 16 的预测框,根据预测框分数进行排序,取前 N(例如 6000)个送去 NMS,经过 NMS 后再取前 t o p k top_k topk​(例如 300)个作为 RPN 的输出结果。

Faster R-CNN; 生成 Proposals

5) Rol Pooling

① Roi pooling 核心思想

候选框共享特征图特征,并保持输出大小一致。

候选框分为若干子区域,将每个区域对应到输入特征图上,取每个区域内的最大值作为该区域的输出。

Faster R-CNN; Roi pooling 核心思想

② Rol Pooling 不足

在 ROI 映射中,涉及到 region proposal 的坐标映射变换问题,在这过程中难免会产生小数坐标。但是在 feature map 中的点相当于一个个的 pixel,是不存在小数的,因此会将小数坐标量化成向下取整,这就会造成一定的误差。

在 ROI Pooling 中,对每个 ROI 划分 grid 的时候又会有一次坐标量化向下取整。

这样,整个过程像素坐标会经过两次量化,导致 ROI 虽然在 feature map 上有不到 1 pixel 的误差,映射回原图后的误差可能会大于 10 pixel,甚至误差可能会大于整个物体,这对小物体的检测非常不友好。

Faster R-CNN; Rol Pooling 不足

6) Rol Align

Faster R-CNN 中通过 ROI Align 消除 RoI Pooling 中产生的误差。

Faster R-CNN; Rol Align

ROI Align 的原理是,先将 ROI Project 和 ROI Pooling 时计算出的 ROI 带小数的坐标存储在内存中,不直接量化成像素坐标。

随后,ROI Align 不取每个 grid 的最大值,而是再将每个 grid 划分成 2 × 2 2\times2 2×2 的小格,在每个小格中找到中心点,将离中心点最近的四个点的值进行双线性差值,求得中心点的值,再取每个 g r i d grid grid 中四个中心点的最大值作为 P o o l i n g Pooling Pooling 后的值。

Faster R-CNN; Rol Align

7) BBox Head

下面是分类与回归的 BBox 头部分,它的处理流程展开后如下图所示:

Faster R-CNN; BBox Head

而 BBox 训练阶段的样本构建方式如下,我们对比 RPN 阶段的样本构建方式:

Faster R-CNN; BBox Head

① BBox Head 中的监督信息

BBox 头的分类与回归任务的标签构建方式如下,其中分类分支是典型的分类问题,学习每个预测框的类别;回归分支则是学习每个 RoI 到真实框的偏移量。

Faster R-CNN; BBox Head 中的监督信息

② BBox Head Loss

BBox 头的总体 loss 由分类 loss 和回归 loss 加权组合构成。

Faster R-CNN; BBox Head Loss

8) Faster R-CNN 效果

Faster R-CNN 的效果如下图所示

Faster R-CNN; Faster R-CNN 效果

5.推荐学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=11

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(13) | 目标检测 (SSD,YOLO 系列)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125027651

ShowMeAI 研究中心


Detectionand Segmentation; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

目标检测 ( Object Detection )是计算机视觉领域非常重要的任务,目标检测模型要完成「预测出各个物体的边界框(bounding box)」和「给出每个物体的分类概率」两个子任务。

目标检测; Object detection

通常情况下,在对一张图片进行目标检测后,会得到许多物体的边界框和对应的置信度(代表其包含物体的可能性大小)。

两阶段模型结构; 一阶段模型结构

目标检测算法主要集中在 two-stage 算法和 one-stage 算法两大类:

① two-stage 算法

② one-stage 算法

  • 直接在网络中提取特征来预测物体分类和位置。

two-stage 算法速度相对较慢但是准确率高,one-stage 算法准确率没有 two-stage 算法高但是速度较快。在本篇我们将聚焦 one-stage 的目标检测方法进行讲解,主要包括 YOLO 系列算法和 SSD 等。

1.YOLO 算法(YOLO V1)

关于 YOLO 的详细知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章 CNN 应用: 目标检测 中对于 YOLO 的讲解。

1.1 算法核心思想

YOLO 算法采用一个单独的 CNN 模型实现 end-to-end 的目标检测。首先将输入图片 resize 到 448 × 448 448 \times 448 448×448,然后送入 CNN 网络,最后处理网络预测结果得到检测的目标。相比 R-CNN 算法,其是一个统一的框架,其速度更快,而且 YOLO 的训练过程也是 端到端 / end-to-end 的。所谓的 YOLO 全名是 You Only Look Once,意思是算法只需要一次的推断运算

YOLO V1; YOLO V1 算法核心思想

相比上述滑动窗口算法,YOLO 算法不再是窗口滑动,而是直接将原始图片分割成互不重合的小方块,然后通过卷积得到同等 size 的特征图,基于上面的分析,可以认为特征图的每个元素也是对应原始图片的一个小方块,可以用每个元素来可以预测那些中心点在该小方块内的目标。

YOLO 算法的 CNN 网络将输入的图片分割成 N × N N\times N N×N 网格,然后每个单元格负责去检测那些中心点落在该格子内的目标

YOLO V1; YOLO V1 算法核心思想

如图所示,可以看到,狗这个目标的中心落在左下角一个单元格内,那么该单元格负责预测这个狗。每个单元格会预测 $B $ 个边界框(bounding box)以及边界框的置信度(confidence score)。

所谓置信度其实包含两个方面,一是这个边界框含有目标的可能性大小,二是这个边界框的准确度

前者记为 P ( o b j e c t ) P(object) P(object),当该边界框是背景时(即不包含目标时), P ( o b j e c t ) = 0 P(object)=0 P(object)=0。而当该边界框包含目标时, P ( o b j e c t ) = 1 P(object)=1 P(object)=1。

边界框的准确度可以用预测框与 ground truth 的 IoU (交并比)来表征,记为 IoU p r e d t r u t h \text{IoU}^{truth}{pred} IoUpredtruth​。因此置信度可以定义为 P r ( o b j e c t ) ∗ IoU p r e d t r u t h Pr(object) \ast \text{IoU}^{truth} Pr(object)∗IoUpredtruth​。

边界框的大小与位置可以用 4 个值来表征: ( x , y , w , h ) (x, y,w,h) (x,y,w,h),其中 ( x , y ) (x,y) (x,y) 是边界框的中心坐标,而 w w w 和 h h h 是边界框的宽与高。

还有一点要注意,中心坐标的预测值 ( x , y ) (x,y) (x,y) 是相对于每个单元格左上角坐标点的偏移值,并且单位是相对于单元格大小的,单元格的坐标定义如上方图所示。

边界框的 w w w 和 h h h 预测值是相对于整个图片的宽与高的比例,因此理论上 4 个元素的大小应该在 [ 0 , 1 ] [0,1] [0,1] 范围。这样,每个边界框的预测值实际上包含 5 个元素: ( x , y , w , h , c ) (x,y,w,h,c) (x,y,w,h,c),其中前 4 个表征边界框的大小与位置,最后一个值是置信度。

除此之外,每一个单元格预测出 C C C 个类别概率值,其表征的是由该单元格负责预测的边界框中的目标属于各个类别的概率。这些概率值其实是在各个边界框置信度下类别的条件概率,即 P r ( c l a s s i ∣ o b j e c t ) Pr(class_{i}|object) Pr(classi​∣object)。

值得注意的是,不管一个单元格预测多少个边界框,其只预测一组类别概率值。同时,我们可以计算出各个边界框的类别置信度 :

P r ( c l a s s i ∣ o b j e c t ) P r ( o b j e c t ) IoU p r e d t r u t h = P r ( c l a s s i ) ∗ IoU p r e d t r u t h Pr(class_{i}|object)Pr(object)\text{IoU}^{truth}{pred}=Pr(class) \ast \text{IoU}^{truth}_{pred} Pr(classi​∣object)Pr(object)IoUpredtruth​=Pr(classi​)∗IoUpredtruth​

边界框类别置信度表征的是该边界框中目标属于各个类别的可能性大小以及边界框匹配目标的好坏。

1.2 YOLO 网络结构

YOLO 算法采用 CNN 来提取特征,使用全连接层来得到预测值。网络结构参考 GoogleNet,包含 24 个卷积层和 2 个全连接层,如下图所示。

YOLO V1; YOLO 网络结构

对于卷积层,主要使用 1 × 1 1 \times 1 1×1 卷积来做通道数降低,用 3 × 3 3 \times 3 3×3 卷积提取特征。对于卷积层和全连接层,采用 Leaky ReLU 激活函数: m a x ( x , 0.1 x ) max(x, 0.1x) max(x,0.1x),最后一层却采用线性激活函数。

1.3 YOLO 训练与预测

在训练之前,先在 ImageNet 上进行预训练,其预训练的分类模型采用上图中前 20 个卷积层,然后添加一个 average-pool 层和全连接层。

在预训练结束之后之后,在预训练得到的 20 层卷积层之上加上随机初始化的 4 个卷积层和 2 个全连接层进行 fine-tuning。由于检测任务一般需要更高清的图片,所以将网络的输入从 224 × 224 224 \times 224 224×224 增加到了 448 × 448 448 \times 448 448×448。

整个网络的流程如下图所示:

YOLO V1; YOLO 训练与预测

YOLO 算法将目标检测问题看作回归问题,所以采用的是 MSE loss,对不同的部分采用了不同的权重值。首先区分定位误差和分类误差。

  • 对于定位误差,即边界框坐标预测误差,采用较大的权重 λ = 5 \lambda=5 λ=5。
  • 然后其区分不包含目标的边界框与含有目标的边界框的置信度,对于前者,采用较小的权重值 λ = 0.5 \lambda =0.5 λ=0.5。其它权重值均设为 1 1 1。
  • 然后采用均方误差,其同等对待大小不同的边界框,但是实际上较小的边界框的坐标误差应该要比较大的边界框要更敏感。为了保证这一点,将网络的边界框的宽与高预测改为对其平方根的预测,即预测值变为 ( x , y , w , h ) (x,y,\sqrt{w}, \sqrt{h}) (x,y,w ​,h ​)。

由于每个单元格预测多个边界框。但是其对应类别只有一个。

训练时,如果该单元格内确实存在目标,那么只选择与 ground truth 的 IoU 最大的那个边界框来负责预测该目标,而其它边界框认为不存在目标。这样设置的一个结果将会使一个单元格对应的边界框更加专业化,其可以分别适用不同大小,不同高宽比的目标,从而提升模型性能。

YOLO V1; NMS 非极大值抑制

YOLO 算法预测时采用非极大值抑制 (NMS) 。NMS 算法主要解决的是一个目标被多次检测的问题,如图中的汽车检测,可以看到汽车被多次检测,但是其实我们希望最后仅仅输出其中一个最好的预测框。

比如对于上图中的汽车,只想要位置最正那个检测结果。那么可以采用 NMS 算法来实现这样的效果:

  • 首先从所有的检测框中找到置信度最大的那个框,然后挨个计算其与剩余框的 IoU,如果 IoU 大于一定阈值(重合度过高),那么就将该框(剩余框)剔除;
  • 然后对剩余的检测框重复上述过程,直到处理完所有的检测框。

1.4 YOLO 算法细节

1) bbox 生成步骤

① 输入图像分成 S × S S \times S S×S 的网格。现在是划分成了 7 × 7 7 \times 7 7×7 的,如果物品的中点落在某一个网格单元,这个网格单元将负责识别出这个物体。

YOLO V1; bbox 生成步骤

注意只是看该目标的中心点,而不是整体。比如 A ( 2 , 3 ) A(2, 3) A(2,3) 是狗的中心点,那么 A A A 就负责来负责预测狗

② 每个网格自身也要预测 n n n 个边界框 bounding box 和边界框的置信度 confidence。论文中 b = 2 b=2 b=2

边界框包含四个数据 x x x, y y y, w w w, h h h: ( x , y ) (x,y) (x,y) 框中心是相对于网格单元的坐标, w w w 和 h h h 是框相当于整幅图的宽和高。

置信度有两部分构成:含有物体的概率和边界框覆盖的准确性。

Pr ⁡ ( Object ) ∗ I o U pred  truth  \operatorname{Pr}(\text {Object}) \ast \mathrm{IoU}_{\text {pred }}^{\text {truth }} Pr(Object)∗IoUpred truth ​

  • $ IoU$ 交并比
  • P r Pr Pr 就是概率 p p p

如果有 object 落在一个 grid cell 里,第一项取 1 1 1,否则取 0 0 0。 第二项是预测的 bounding box 和实际的 ground truth 之间的 IoU 值。

每个边界框又要预测五个数值: x x x, y y y, w w w, h h h, c o n f i d e n c e confidence confidence。 ( x , y ) (x,y) (x,y) 框中心是相对于网格单元的坐标, w w w 和 h h h 是框相当于整幅图的宽和高,confidence 代表该框与 ground truth 之间的 IoU (框里没有物体分数直接为 0 0 0)。

YOLO V1; bbox 生成步骤

每个网格都要预测 b = 2 b= 2 b=2 个框,49 个网格就会输出 98 个边界框,每个框还有它的分数。每个格子最多只预测出一个物体。当物体占画面比例较小,如图像中包含畜群或鸟群时,每个格子包含多个物体,但却只能检测出其中一个。这是 YOLO 方法的一个缺陷。

最后每个单元格再预测他的 n n n 个边界框中的物体分类概率,有 c c c 个类别就要计算 c c c 个概率,和全连接层类似。

S × S × ( B ∗ 5 + C ) S \times S \times(B \ast 5+C) S×S×(B∗5+C)

本文中有 20 个类别:即 7 ∗ 7 ( 2 ∗ 5 + 20 ) 7 \ast 7(2 \ast 5+20) 7∗7(2∗5+20)

总结:每个方格要找到 n n n 个边界框,然后还要计算每个边界框的置信度,最后再计算每个边界框的分类的可能性。

YOLO V1; bbox 生成步骤

生成的 bounding box 的 ( x , y ) (x, y) (x,y) 被限制在 cell 里, 但长宽是没有限制的(即生成的 bounding box 可超出 cell 的边界)

2) 损失函数

YOLO V1 的损失函数就是把三类损失加权求和,用的也都是简单的平方差:

边缘中心点误差

λ coord  ∑ i = 0 S 2 ∑ j = 0 B 1 i j obj  [ ( x i − x ^ i ) 2 + ( y i − y ^ i ) 2 ] \lambda_{\text {coord }} \sum_{i=0}{S{2}} \sum_{j=0}^{B} \mathbb{1}{i j}^{\text {obj }}\left[\left(x-\hat{x}_{i}\right){2}+\left(y_{i}-\hat{y}_{i}\right)\right] λcoord ​i=0∑S2​j=0∑B​1ijobj ​[(xi​−xi​)2+(yi​−y​i​)2]

边框宽度、高度误差

  • λ coord  ∑ i = 0 S 2 ∑ j = 0 B 1 i j obj  [ ( w i − w ^ i ) 2 + ( h i − h ^ i ) 2 ] +\lambda_{\text {coord }} \sum_{i=0}{S{2}} \sum_{j=0}^{B} \mathbb{1}{i j}^{\text {obj }}\left[\left(\sqrt{w{i}}-\sqrt{\hat{w}_{i}}\right){2}+\left(\sqrt{h_{i}}-\sqrt{\hat{h}_{i}}\right)\right] +λcoord ​i=0∑S2​j=0∑B​1ijobj ​[(wi​ ​−w^i​ ​)2+(hi​ ​−h^i​ ​)2]

置信度误差(边框内有对象)

  • ∑ i = 0 S 2 ∑ j = 0 B 1 i j obj  ( C i − C ^ i ) 2 +\sum_{i=0}{S{2}} \sum_{j=0}^{B} \mathbb{1}{i j}^{\text {obj }}\left(C-\hat{C}_{i}\right)^{2} +i=0∑S2​j=0∑B​1ijobj ​(Ci​−C^i​)2

置信度误差(边框内无对象)

  • λ noobj  ∑ i = 0 S 2 ∑ j = 0 B 1 i j noobj  ( C i − C ^ i ) 2 +\lambda_{\text {noobj }} \sum_{i=0}{S{2}} \sum_{j=0}^{B} \mathbb{1}{i j}^{\text {noobj }}\left(C-\hat{C}_{i}\right)^{2} +λnoobj ​i=0∑S2​j=0∑B​1ijnoobj ​(Ci​−C^i​)2

对象分类误差

  • ∑ i = 0 S 2 1 i obj  ∑ c ∈  classes  ( p i ( c ) − p ^ i ( c ) ) 2 +\sum_{i=0}{S{2}} \mathbb{1}{i}^{\text {obj }} \sum{c \in \text { classes }}\left(p_{i}(c)-\hat{p}_{i}(c)\right)^{2} +i=0∑S2​1iobj ​c∈ classes ∑​(pi​(c)−p^​i​(c))2

其中

  • 1 i o b j 1_{i}^{o b j} 1iobj​ 意思是网格 i i i 中存在对象。
  • 1 i j o b j 1_{i j}^{o b j} 1ijobj​ 意思是网格的第 j j j 个 bounding box 中存在对象。
  • 1 i j n o o b j 1_{i j}^{n o o b j} 1ijnoobj​ 意思是网格 i i i 的第 个 bounding box 中不存在对象。

损失函数计算是有条件的,是否存在对象对损失函数的计算有影响。

先要计算位置误差:预测中点和实际中点之间的距离,再计算 bbox 宽度和高度之间的差距,权重为 5 调高位置误差的权重

置信度误差:要考虑两个情况:这个框里实际上有目标;这个框里没有目标,而且要成一个权重降低他的影响,调低不存在对象的 bounding box 的置信度误差的权重,论文中是 0.5 0.5 0.5

对象分类的误差:当该框中有目标时才计算,概率的二范数

YOLO V1; 损失构建

3) YOLO V1 的缺陷

不能解决小目标问题,YOLO 对边界框预测施加了严格的空间约束,因为每个网格单元只能预测两个边界框,并且只能有一个类。这个空间约束限制了我们模型能够预测的临近对象的数量。

YOLO V1 在处理以群体形式出现的小对象时会有困难,比如成群的鸟。

2. SSD 算法

SSD 算法全名是 Single Shot Multibox Detector,Single shot 指明了 SSD 算法属于 one-stage 方法,MultiBox 指明了 SSD 是多框预测。

SSD 算法在准确度和速度上都优于最原始的 YOLO 算法。对比 YOLO,SSD 主要改进了三点:多尺度特征图,利用卷积进行检测,设置先验框。

  • SSD 采用 CNN 来直接进行检测,而不是像 YOLO 那样在全连接层之后做检测。
  • SSD 提取了不同尺度的特征图来做检测,大尺度特征图(较靠前的特征图)可以用来检测小物体,而小尺度特征图(较靠后的特征图)用来检测大物体。
  • SSD 采用了不同尺度和长宽比的先验框(在 Faster R-CNN 中叫做 anchor )。

下面展开讲解 SSD 目标检测算法。

2.1 算法核心思想

SSD; SSD 算法核心思想

1) 采用多尺度特征图用于检测

所谓多尺度特征图,就是采用大小不同的特征图进行检测。

CNN 网络一般前面的特征图比较大,后面会逐渐采用 s t r i d e = 2 stride=2 stride=2 的卷积或者 pool 来降低特征图大小,如下图所示,一个比较大的特征图和一个比较小的特征图,它们都用来做检测。

  • 这样做的好处是比较大的特征图来用来检测相对较小的目标,而小的特征图负责检测大目标。
  • 8 × 8 8 \times 8 8×8 的特征图可以划分更多的单元,但是其每个单元的先验框尺度比较小。

SSD; SSD 多尺度特征图

2) 利用卷积进行检测

与 YOLO 最后采用全连接层不同,SSD 直接采用卷积对不同的特征图来进行提取检测结果。

对于形状为 m × n × p m\times n \times p m×n×p 的特征图,只需要采用 3 × 3 × p 3\times 3 \times p 3×3×p 这样比较小的卷积核得到检测值。

SSD 采用卷积特征图; 提取检测结果

3) 设置先验框

在 YOLO 中,每个单元预测多个边界框,但是其都是相对这个单元本身(正方块),但是真实目标的形状是多变的,YOLO 需要在训练过程中自适应目标的形状。

SSD 借鉴了 Faster R-CNN 中 anchor 的理念,每个单元设置尺度或者长宽比不同的先验框,预测的 bbox 是以这些先验框为基准的,在一定程度上减少训练难度。

SSD; SSD 设置先验框

一般情况下,每个单元会设置多个先验框,其尺度和长宽比存在差异,如图所示,可以看到每个单元使用了 4 个不同的先验框,图片中猫和狗分别采用最适合它们形状的先验框来进行训练,后面会详细讲解训练过程中的先验框匹配原则。

SSD 的检测值也与 YOLO 不太一样。对于每个单元的每个先验框,其都输出一套独立的检测值,对应一个边界框,主要分为两个部分。

  • 第 1 部分是各个类别的置信度,值得注意的是 SSD 将背景也当做了一个特殊的类别,如果检测目标共有 c c c 个类别,SSD 其实需要预测 c + 1 c+1 c+1 个置信度值,第一个置信度指的是不含目标或者属于背景的评分。在预测过程中,置信度最高的那个类别就是边界框所属的类别,特别地,当第一个置信度值最高时,表示边界框中并不包含目标。
  • 第 2 部分就是边界框的 location,包含 4 个值 ( c x , c y , w , h ) (cx, cy, w, h) (cx,cy,w,h),分别表示边界框的中心坐标以及宽和高。然而,真实预测值其实只是边界框相对于先验框的转换值。先验框位置用 d = ( d c x , d c y , d w , d h ) d=(d^{cx}, d^{cy}, d^w, d^h) d=(dcx,dcy,dw,dh) 表示,其对应边界框用 b = ( b c x , b c y , b w , b h ) b=(b^{cx}, b^{cy}, b^w, b^h) b=(bcx,bcy,bw,bh) 表示,那么边界框的预测值 l l l 其实是 b b b 相对于 d d d 的转换值:

l c x = ( b c x − d c x ) / d w ,   l c y = ( b c y − d c y ) / d h l^{cx} = (b^{cx} - d{cx})/dw, \space l^{cy} = (b^{cy} - d{cy})/dh lcx=(bcx−dcx)/dw, lcy=(bcy−dcy)/dh

l w = log ⁡ ( b w / d w ) ,   l h = log ⁡ ( b h / d h ) l^{w} = \log(b{w}/dw), \space l^{h} = \log(b{h}/dh) lw=log(bw/dw), lh=log(bh/dh)

习惯上,我们称上面这个过程为边界框的编码(encode),预测时,你需要反向这个过程,即进行解码(decode),从预测值 l l l 中得到边界框的真实位置 b b b :

b c x = d w l c x + d c x ,   b c y = d y l c y + d c y b{cx}=dw l^{cx} + d^{cx}, \space b{cy}=dy l^{cy} + d^{cy} bcx=dwlcx+dcx, bcy=dylcy+dcy

b w = d w exp ⁡ ( l w ) ,   b h = d h exp ⁡ ( l h ) b{w}=dw \exp(l^{w}), \space b{h}=dh \exp(l^{h}) bw=dwexp(lw), bh=dhexp(lh)

2.2 SSD 网络结构

SSD 采用 VGG16 作为基础模型,然后在 VGG16 的基础上新增了卷积层来获得更多的特征图以用于检测。SSD 的网络结构如下图所示

SSD 利用了多尺度的特征图做检测。模型的输入图片大小是 300 × 300 300 \times 300 300×300。

SSD; SSD 网络结构

采用 VGG16 做基础模型,首先 VGG16 是在 ILSVRC CLS-LOC 数据集上做预训练。

然后,分别将 VGG16 的全连接层 fc6 和 fc7 转换成 3 × 3 3 \times 3 3×3 卷积层 conv6 和 1 × 1 1 \times 1 1×1 卷积层 conv7,同时将池化层 pool5 由原来的 s t r i d e = 2 stride=2 stride=2 的 $2\times 2 $ 变成 s t r i d e = 1 stride=1 stride=1 的 3 × 3 3\times 3 3×3,为了配合这种变化,采用了一种 Atrous Algorithm,就是 conv6 采用扩张卷积(空洞卷积),在不增加参数与模型复杂度的条件下指数级扩大了卷积的视野,其使用扩张率(dilation rate)参数,来表示扩张的大小。

如下图所示:

  • (a)是普通的 3 × 3 3\times3 3×3 卷积,其视野就是 3 × 3 3\times3 3×3
  • (b)是扩张率为 2,此时视野变成 7 × 7 7\times7 7×7
  • ©扩张率为 4 时,视野扩大为 15 × 15 15\times15 15×15,但是视野的特征更稀疏了。

Conv6 采用 3 × 3 3\times3 3×3 大小但 d i l a t i o n r a t e = 6 dilation rate=6 dilationrate=6 的扩展卷积。然后移除 Dropout 层和 fc8 层,并新增一系列卷积层,在检测数据集上做 fine-tuning。

SSD; SSD 扩张卷积/空洞卷积

2.3 SSD 训练与预测

在训练过程中,首先要确定训练图片中的 ground truth (真实目标)与哪个先验框来进行匹配,与之匹配的先验框所对应的边界框将负责预测它。

YOLO 中:ground truth 的中心落在哪个单元格,该单元格中与其 IoU 最大的边界框负责预测它。

SSD 中:处理方式不一样,SSD 的先验框与 ground truth 有 2 个匹配原则。

  • 第 1 原则:对于图片中每个 ground truth,找到与其 IoU 最大的先验框,该先验框与其匹配,这样,可以保证每个 ground truth 一定与某个先验框匹配。通常称与 ground truth 匹配的先验框为正样本,反之,若一个先验框没有与任何 ground truth 进行匹配,那么该先验框只能与背景匹配,就是负样本。
    • 然而,由于一个图片中 ground truth 是非常少的,而先验框却很多,如果仅按上述原则匹配,很多先验框会是负样本,正负样本极其不平衡,所以有下述第 2 原则。
  • 第 2 原则:对于剩余的未匹配先验框,若某个 ground truth 的 IoU \text{IoU} IoU 大于某个阈值(一般是 0.5),那么该先验框也与这个 ground truth 进行匹配。这意味着某个 ground truth 可能与多个先验框匹配,这是可以的。但是反过来却不可以,因为一个先验框只能匹配一个 ground truth,如果多个 ground truth 与某个先验框 IoU \text{IoU} IoU 大于阈值,那么先验框只与 IoU 最大的那个 ground truth 进行匹配。

第 2 原则一定在第 1 原则之后进行。

仔细考虑一下这种情况,如果某个 ground truth 所对应最大 IoU \text{IoU} IoU 小于阈值,并且所匹配的先验框却与另外一个 ground truth 的 IoU \text{IoU} IoU 大于阈值,那么该先验框应该匹配谁,答案应该是前者,首先要确保每个 ground truth 一定有一个先验框与之匹配

但是,这种情况存在的概率很小。由于先验框很多,某个 ground truth 的最大 IoU \text{IoU} IoU 肯定大于阈值,所以可能只实施第二个原则既可以了。

SSD; SSD 训练与预测

上图为一个匹配示意图,其中绿色的 GT 是 ground truth,红色为先验框,FP 表示负样本,TP 表示正样本。

尽管一个 ground truth 可以与多个先验框匹配,但是 ground truth 相对先验框还是太少了,所以负样本相对正样本会很多。

为了保证正负样本尽量平衡,SSD 采用了 hard negative mining 算法,就是对负样本进行抽样,抽样时按照置信度误差(预测背景的置信度越小,误差越大)进行降序排列,选取误差的较大(置信度小)的 t o p − k top-k top−k 作为训练的负样本,以保证正负样本比例接近 1 : 3 1:3 1:3。

3.YOLO V2

论文链接:https://openaccess.thecvf.com/content_cvpr_2017/papers/Redmon_YOLO9000_Better_Faster_CVPR_2017_paper.pdf
代码链接:https://github.com/longcw/YOLO2-pytorch

YOLO V2; YOLO V2 算法简介

1) 算法简介

相比于 YOLO V1,YOLO V2 在精度、速度和分类数量上都有了很大的改进。YOLO V2 使用 DarkNet19 作为特征提取网络,该网络比 YOLO V2 所使用的 VGG-16 要更快。YOLO V2 使用目标分类和检测的联合训练技巧,结合 Word Tree 等方法,使得 YOLO V2 的检测种类扩充到了上千种,分类效果更好。

下图展示了 YOLO V2 相比于 YOLO V1 在提高检测精度上的改进策略。

YOLO V2; YOLO V2 改进策略

2) 性能效果

YOLO V2 算法在 VOC 2007 数据集上的表现为 67 FPS 时,mAP 为 76.8,在 40FPS 时,mAP 为 78.6。

3) 缺点不足

YOLO V2 算法只有一条检测分支,且该网络缺乏对多尺度上下文信息的捕获,所以对于不同尺寸的目标检测效果依然较差,尤其是对于小目标检测问题。

4. RetinaNet

论文链接:https://openaccess.thecvf.com/content_ICCV_2017/papers/Lin_Focal_Loss_for_ICCV_2017_paper.pdf
代码链接:https://github.com/yhenon/pytorch-retinanet

RetinaNet; RetinaNet 算法简介

1) 算法简介

尽管一阶段检测算推理速度快,但精度上与二阶段检测算法相比还是不足。RetinaNet论文分析了一阶段网络训练存在的类别不平衡问题,提出能根据 Loss 大小自动调节权重的 Focal loss,代替了标准的交叉熵损失函数,使得模型的训练更专注于困难样本。同时,基于 FPN 设计了 RetinaNet,在精度和速度上都有不俗的表现。

2) 性能效果

RetinaNet 在保持高速推理的同时,拥有与二阶段检测算法相媲美的精度( COCO m A P @ . 5 = 59.1 % mAP@.5=59.1% mAP@.5=59.1%, m A P @ [ . 5 , . 95 ] = 39.1 % mAP@[.5, .95]=39.1% mAP@[.5,.95]=39.1%)。

5.YOLO V3

论文链接:https://arxiv.org/pdf/1804.02767.pdf
代码链接:https://github.com/ultralytics/YOLOv3

YOLO V3; YOLO V3 算法简介

1) 算法简介

相比于 YOLO V2,YOLO V3 将特征提取网络换成了 DarkNet53,对象分类用 Logistic 取代了 Softmax,并借鉴了 FPN 思想采用三条分支(三个不同尺度/不同感受野的特征图)去检测具有不同尺寸的对象。

2) 性能效果

YOLO V3 在 VOC 数据集,Titan X 上处理 608 × 608 608 \times 608 608×608 图像速度达到 20FPS,在 COCO 的测试数据集上 m A P @ 0.5 mAP@0.5 mAP@0.5 达到 57.9 % 57.9% 57.9%。其精度比 SSD 高一些,比 Faster RCNN 相比略有逊色(几乎持平),比 RetinaNet 差,但速度是 SSD 、RetinaNet 和 Faster RCNN 至少 2 倍以上,而简化后的 YOLO V3 tiny 可以更快。

3) 缺点不足

YOLO V3 采用 MSE 作为边框回归损失函数,这使得 YOLO V3 对目标的定位并不精准,之后出现的 IOU,GIOU,DIOU 和 CIOU 等一系列边框回归损失大大改善了 YOLO V3 对目标的定位精度。

6.YOLO V4

论文链接:https://arxiv.org/pdf/2004.10934
代码链接:https://github.com/Tianxiaomo/pytorch-YOLOv4

YOLO V4; YOLO V4 算法简介

1) 算法简介

相比于 YOLO V4,YOLO V4 在输入端,引入了 Mosaic 数据增强、cmBN、SAT 自对抗训练;在特征提取网络上,YOLO V4 将各种新的方式结合起来,包括 CSPDarknet53,Mish 激活函数,Dropblock;在检测头中,引入了 SPP 模块,借鉴了 FPN+PAN 结构;在预测阶段,采用了 CIOU 作为网络的边界框损失函数,同时将 NMS 换成了 DIOU_NMS 等等。总体来说,YOLO V4 具有极大的工程意义,将近年来深度学习领域最新研究的 tricks 都引入到了 YOLO V4 做验证测试,在 YOLO V3 的基础上更进一大步。

2) 性能效果

YOLO V4 在 COCO 数据集上达到了 43.5 % A P 43.5%AP 43.5%AP( 65.7 % A P 50 65.7% AP50 65.7%AP50),在 Tesla V100 显卡上实现了 65 fps 的实时性能,下图展示了在 COCO 检测数据集上 YOLO V4 和其它 SOTA 检测算法的性能对比。

YOLO V4; YOLO V4 性能效果

7.YOLO V5

代码链接:https://github.com/ultralytics/YOLOv5

YOLO V5; YOLO V5 算法简介

1) 算法简介

YOLO V5 与 YOLO V4 有点相似,都大量整合了计算机视觉领域的前沿技巧,从而显著改善了 YOLO 对目标的检测性能。相比于 YOLO V4,YOLO V5 在性能上稍微逊色,但其灵活性与速度上远强于 YOLO V4,而且在模型的快速部署上也具有极强优势。

2) 性能效果

如下图展示了在 COCO 检测数据集上 YOLO V5 和其它 SOTA 检测算法的性能对比。

YOLO V5; YOLO V5 性能效果

8.推荐学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=11

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(14) | 图像分割 (FCN,SegNet,U-Net,PSPNet,DeepLab,RefineNet)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125039332

ShowMeAI 研究中心


Detectionand Segmentation; 深度学习与计算机视觉

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


1.图像语义分割定义

图像语义分割是计算机视觉中十分重要的领域,它是指像素级地识别图像,即标注出图像中每个像素所属的对象类别。下图为语义分割的一个实例,它清晰地把图中的骑行人员、自行车和背景对应的像素标注出来了。

图像语义分割; 图像分割

图像分割有语义分割和实例分割的差别。语义分割不分离同一类的实例,我们只关心每个像素的类别,如果输入对象中有两个相同类别的对象,语义分割不将他们区分为单独的对象。实例分割是需要对对象个体进行区分的。

2.语义分割常见应用

2.1 自动驾驶汽车

语义分割常见的应用场景之一是自动驾驶领域,我们希望自动驾驶汽车有「环境感知」的能力,以便其可以安全行驶;下图为自动驾驶过程中实时分割道路场景:

语义分割常见应用; 自动驾驶汽车

2.2 医学影像诊断

语义分割的另外一个大应用场景是医疗影像诊断,机器可以智能地对医疗影像进行分析,降低医生的工作负担,大大减少了运行诊断测试所需的时间;下图是胸部 X 光片的分割,心脏(红色),肺部(绿色以及锁骨(蓝色):

语义分割常见应用; 医学影像诊断

语义分割的目标是:将一张 RGB 图像(heightwidth3)或是灰度图(heightwidth1)作为输入,输出的是分割图,其中每一个像素包含了其类别的标签(heightwidth1)。

下图为典型示例,为了直观易懂,示例显示使用的低分辨率的预测图,但实际上分割图的分辨率应与原始输入的分辨率是一致的。

语义分割直观理解; 图像分割

从上图可以看到在语义分割任务中,像素级别的标签设置,我们会使用 one-hot 编码对类标签进行处理。

关于 one-hot 编码的详细知识也可以参考阅读ShowMeAI机器学习实战:手把手教你玩转机器学习系列 中的文章 机器学习实战 | 机器学习特征工程最全解读 里【独热向量编码(one hot encoding) 】板块内容。

语义分割直观理解; 图像分割

最后,可以通过 argmax 将每个深度方向像素矢量折叠成分割图,将它覆盖在原图上,可以区分图像中存在不同类别的区域,方便观测(也叫 mask/掩码)。

语义分割直观理解; 图像分割

3.语义分割任务评估

对于语义分割任务,我们会通过 mIoU(mean Intersection-Over-Union) 和 mAcc(mean Accuracy) 指标来进行效果评估。

语义分割性能指标; mIoU & mAcc

3.1 mIoU

分割网络的评价指标:mIoU

  • mloU:分割每一类别的交并比(IOU)

语义分割性能指标; mIoU & mAcc

3.2 mAcc

分割网络的评价指标:mAcc

  • mAcc:Pred 和 GT 对应位置的分类准确率

语义分割性能指标; mIoU & mAcc

4.语义分割方法综述

早期的一些语义分割方法包括使用 TextonForest 和随机森林分类器等。卷积神经网络(CNN)的引入不仅仅极大加速图像识别的进程,也对语义分割领域的发展起到巨大的促进作用。

语义分割任务最初流行的深度学习方法是图像块分类(patch classification),即利用像素周围的图像块对每一个像素进行独立的分类。使用图像块分类的主要原因是分类网络中包含全连接层(fully connected layer),它需要固定尺寸的图像。

2014 年,加州大学伯克利分校的 Long 等人提出全卷积网络(FCN),这使得卷积神经网络无需全连接层即可进行密集的像素预测。使用这种方法可生成任意大小的图像分割图,且该方法比图像块分类法要高效许多。之后,语义分割领域几乎所有先进方法都采用了类似结构。

使用卷积神经网络进行语义分割存在的另一个大问题是池化层。池化层虽然扩大了感受野、聚合语境,但因此造成了位置信息的丢失。但是,语义分割要求类别图完全贴合,因此需要保留位置信息。

有两种不同结构来解决该问题。

  • 第一个是编码器解码器结构。编码器逐渐减少池化层的空间维度,解码器逐步修复物体的细节和空间维度。编码器和解码器之间通常存在快捷连接,因此能帮助解码器更好地修复目标的细节。U-Net 是这种方法中最常用的结构。
  • 第二种方法使用空洞/扩张卷积(dilated/atrous convolutions)结构,来去除池化层。

关于全连接层和池化层的详细知识也可以参考ShowMeAI的文章

4.1 encoder-decoder 结构

针对语义分割任务构建神经网络架构的最简单的方法是简单地堆叠多个卷积层(使用 same 填充以维持维度)并输出最终的分割图。

这种结构通过特征映射的连续变换,直接去学习从输入图像到其对应分割的映射,缺点是在整个网络中保持全分辨率的计算成本非常高。

语义分割; encoder-decoder

对于深度卷积网络,浅层主要学习低级的信息,随着网络越深,学习到更高级的特征映射。为了保持表达能力,我们通常需要增加特征图 feature map 的数量(通道数),从而可以得到更深的网络。

在图像分类任务中,我们只关注图像是什么(而不是位置在哪),因此 CNN 的结构中会对特征图降采样(downsampling)或者应用带步长的卷积(例如,压缩空间分辨率)。但对于图像分割任务而言,我们希望模型产生全分辨率语义预测。

图像分割领域现在较为流行的是编码器解码器结构,其中我们对输入的空间分辨率进行下采样,生成分辨率较低的特征映射,它能高效地进行分类,而后使用上采样将特征还原为全分辨率分割图。

4.2 上采样方法

我们有许多方法可以对特征图进行上采样。

「池化」操作通过对将小区域的值取成单一值(例如平均或最大池化)进行下采样,对应的「上池化」操作就是将单一值分配到更高的分辨率进行上采样。

语义分割; 上采样方法

转置卷积(Transpose Convolution,有时也翻译为「反卷积」)是迄今为止最流行的上采样方法,这种结构允许我们在上采样的过程中进行参数学习。

语义分割; 上采样方法

典型的「卷积」运算将采用滤波器视图中当前值的点积并为相应的输出位置产生单个值,而「转置卷积」基本是相反的过程:我们从低分辨率特征图中获取单个值,并将滤波器中的所有权重乘以该值,将这些加权值投影到输出要素图中。

语义分割; 上采样方法

某些大小的滤波器会在输出特征映射中产生重叠(例如,具有步幅 2 2 2 的 3 × 3 3 \times 3 3×3 滤波器 - 如下面的示例所示),如果只是简单将重叠值加起来,往往会在输出中产生棋盘格子状的伪影(artifact)。

语义分割; 上采样方法

这并不是我们需要的,因此最好确保您的滤波器大小不会产生重叠。

下面我们对主流的模型进行介绍,包括 FCN、SegNet、U-Net、PSPNet、DeepLab V1~V3 等。

5.典型语义分割算法

5.1 FCN 全卷积网络

全卷积网络 FCN 在会议 CVPR 2015 的论文 Fully Convolutional Networks for Semantic Segmentation 中提出。

它将 CNN 分类网络(AlexNet, VGG 和 GoogLeNet)修改为全卷积网络,通过对分割任务进行微调,将它们学习的表征转移到网络中。然后,定义了一种新的架构,它将深的、粗糙的网络层的语义信息和浅的、精细的网络层的表层信息结合起来,来生成精确和详细的分割。

关于 CNN 的详细结构,以及卷积层和全连接层的变换等基础知识可以阅读ShowMeAI文章

全卷积网络在 PASCAL VOC(2012 年的数据,相对之前的方法提升了 20 % 20% 20% ,达到 62.2 % 62.2% 62.2% 的平均 IoU),NYUDv2 和 SIFT Flow 上实现了最优的分割结果,对于一个典型的图像,推断只需要 1 / 3 1/3 1/3 秒的时间。

语义分割算法; FCN 全卷积网络

FCN 的网络结构如下所示,典型的编码器解码器结构:

语义分割算法; FCN 网络结构

我们来看看 FCN 的中间层的一些数字,如下:

语义分割算法; FCN 网络结构

语义分割算法; FCN 网络结构

语义分割算法; 如何实现 FCN

关键特点

  • FCN 的特征由编码器中的不同阶段合并而成的,它们在语义信息的粗糙程度上有所不同。- 低分辨率语义特征图的上采样使用经双线性插值滤波器初始化的「反卷积」操作完成。- 从 VGG16、Alexnet 等分类器网络进行知识迁移来实现语义细分。

语义分割算法; FCN 端到端密集预测流程

如上图所示,预训练模型 VGG16 的全连接层(fc6fc7)被转换为全卷积层,通过它生成了低分辨率的类的热图,然后使用经双线性插值初始化的反卷积,并在上采样的每一个阶段通过融合(简单地相加) VGG16 中的低层(conv4conv3)的更加粗糙但是分辨率更高的特征图进一步细化特征。

在传统的分类 CNNs 中,池化操作用来增加视野,同时减少特征图的分辨率。对分类任务来说非常有效,分类模型关注图像总体类别,而对其空间位置并不关心。所以才会有频繁的卷积层之后接池化层的结构,保证能提取更多抽象、突出类的特征。

语义分割算法; FCN-8s 网络架构

另一方面,池化和带步长的卷积对语义分割是不利的,这些操作会带来空间信息的丢失。不同的语义分割模型在解码器中使用了不同机制,但目的都在于恢复在编码器中降低分辨率时丢失的信息。如上图所示,FCN-8s 融合了不同粗糙度(conv3conv4fc7)的特征,利用编码器不同阶段不同分辨率的空间信息来细化分割结果。

下图为训练 FCNs 时卷积层的梯度:

语义分割算法; 训练 FCNs 时卷积层的梯度

第 1 个卷积层捕捉低层次的几何信息,我们注意到梯度调整了第一层的权重,以便其能适应数据集。

VGG 中更深层的卷积层有非常小的梯度流,因为这里捕获的高层次的语义概念足够用于分割。

语义分割算法; 反卷积(转置卷积)

语义分割架构的另一个重要点是,对特征图使用「反卷积」(如上动图所示),将低分辨率分割图上采样至输入图像分辨率,或者花费大量计算成本,使用空洞卷积在编码器上部分避免分辨率下降。即使在现代 GPUs 上,空洞卷积的计算成本也很高。

最后,我们来看看 FCN 的优缺点:

语义分割算法; FCN 的优缺点

5.2 SegNet

SegNet 在 2015 的论文 SegNet: A Deep Convolutional Encoder-Decoder Architecture for Image Segmentation 中提出。

SegNet 的新颖之处在于解码器对其较低分辨率的输入特征图进行上采样的方式。

  • 解码器使用了在相应编码器的最大池化步骤中计算的池化索引来执行非线性上采样。

这种方法消除了学习上采样的需要。经上采样后的特征图是稀疏的,因此随后使用可训练的卷积核进行卷积操作,生成密集的特征图。

SegNet 与 FCN 等语义分割网络比较,结果揭示了在实现良好的分割性能时所涉及的内存与精度之间的权衡。

语义分割算法; SegNet 架构

关键特点

  • SegNet 在解码器中使用「反池化」对特征图进行上采样,并在分割中保持高频细节的完整性。- 编码器舍弃掉了全连接层(和 FCN 一样进行卷积),因此是拥有较少参数的轻量级网络。

语义分割算法; SegNet 反池化

如上图所示,编码器中的每一个最大池化层的索引都被存储起来,用于之后在解码器中使用那些存储的索引来对相应的特征图进行反池化操作。虽然这有助于保持高频信息的完整性,但当对低分辨率的特征图进行反池化时,它也会忽略邻近的信息。

5.3 U-Net

SegNet 在 2015 的论文 U-Net: Convolutional Networks for Biomedical Image Segmentation 中提出。

U-Net 架构包括一个「捕获上下文信息的收缩路径」和一个「支持精确本地化的对称扩展路径」。这样一个网络可以使用非常少的图像进行端到端的训练,它在 ISBI 神经元结构分割挑战赛中取得了比之前方法都更好的结果。

语义分割算法; U-Net 架构

语义分割算法; U-Net 架构

语义分割算法; U-Net 架构

语义分割算法; U-Net 架构

语义分割算法; U-Net 输出层

语义分割算法; 构建 U-Net 网络

关键特点

  • U-Net 简单地将编码器的特征图拼接至每个阶段解码器的上采样特征图,从而形成一个梯形结构。该网络非常类似于 Ladder Network 类型的架构。- 通过跳跃 拼接 连接的架构,在每个阶段都允许解码器学习在编码器池化中丢失的相关特征。- 上采样采用转置卷积。

U-Net 在 EM 数据集上取得了最优异的结果,该数据集只有 30 个密集标注的医学图像和其他医学图像数据集,U-Net 后来扩展到 3D 版的 3D-U-Net。虽然 U-Net 最初的发表在于其在生物医学领域的分割、网络的实用性以及从非常少的数据中学习的能力,但现在已经成功应用其他几个领域,例如 卫星图像分割等。

5.4 DeepLab V1

DeepLab V1 在 2015 的论文 Semantic Image Segmentation with deep convolutional nets and fully connected CRFs 中提出。

DeepLab V1 结合 DCNN 和概率图模型来解决语义分割问题。DCNN 最后一层的响应不足以精确定位目标边界,这是 DCNN 的不变性导致的。DeepLab V1 的解决方法是:在最后一层网络后结合全连接条件随机场。DeepLab V1 在 PASCAL VOC 2012 上达到了 71.6% 的 mIoU。

语义分割算法; DeepLab V1 结构

关键特点

  • 提出 空洞卷积(atrous convolution)(又称扩张卷积(dilated convolution)) 。- 在最后两个最大池化操作中不降低特征图的分辨率,并在倒数第二个最大池化之后的卷积中使用空洞卷积。- 使用 CRF(条件随机场) 作为后处理,恢复边界细节,达到准确定位效果。- 附加输入图像和前四个最大池化层的每个输出到一个两层卷积,然后拼接到主网络的最后一层,达到 多尺度预测 效果。

5.5 DeepLab V2

DeepLab V2 在 2017 的论文 DeepLab: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Convolution, and Fully Connected CRFs 中提出。

DeepLab V2 提出了一种空洞空间金字塔池化(ASPP)的多尺度鲁棒分割方法。

ASPP 使用多个采样率的过滤器和有效的视野探测传入的卷积特征层,从而在多个尺度上捕获目标和图像上下文。再结合 DCNNs 方法和概率图形模型,改进了目标边界的定位。

DCNNs 中常用的最大池化和下采样的组合实现了不变性,但对定位精度有一定的影响。DeepLab V2 通过将 DCNN 最后一层的响应与一个全连接条件随机场(CRF)相结合来克服这个问题。DeepLab V2 在 PASCAL VOC 2012 上得到了 79.7 % 79.7% 79.7% 的 mIoU。

DeepLab V2 的主干网络是 ResNet,整体网络如下图所示,核心的一些结构包括 空洞卷积组建的 ASPP 模块、空洞空间金字塔池化。

语义分割算法; DeepLab V2 结构

上图中的 ASPP 模块具体展开如下方 2 个图所示:

语义分割算法; DeepLab V2 ASPP

语义分割算法; DeepLab V2 ASPP

语义分割算法; DeepLab V2 Dilated Backbone

具体的,DeepLab V2 论文中提出了语义分割中的三个挑战:

  • ① 由于池化和卷积而减少的特征分辨率。
  • ② 多尺度目标的存在。
  • ③ 由于 DCNN 不变性而减少的定位准确率。

第①个挑战解决方法:减少特征图下采样的次数,但是会增加计算量。

第②个挑战解决方法:使用图像金字塔、空间金字塔等多尺度方法获取多尺度上下文信息。

第③个挑战解决方法:使用跳跃连接或者引入条件随机场。

DeepLab V2 使用 VGG 和 ResNet 作为主干网络分别进行了实验。

语义分割算法; DeepLab V2 结构

Deep LAB-ASPP employs multiple filters with different rates to capture objects and context at multiple scales.

关键特点

  • 提出了空洞空间金字塔池化(Atrous Spatial Pyramid Pooling) ,在不同的分支采用不同的空洞率以获得多尺度图像表征。

5.6 DeepLab V3

DeepLab V3 在论文 Rethinking Atrous Convolution for Semantic Image Segmentation 中提出。

DeepLab V3 依旧使用了 ResNet 作为主干网络,也依旧应用空洞卷积结构。

为了解决多尺度目标的分割问题,DeepLab V3 串行/并行设计了能够捕捉多尺度上下文的模块,模块中采用不同的空洞率。

此外,DeepLab V3 增强了先前提出的空洞空间金字塔池化模块,增加了图像级特征来编码全局上下文,使得模块可以在多尺度下探测卷积特征。

DeepLab V3 模型在没有 CRF 作为后处理的情况下显著提升了性能。

DeepLab V1-V3 的结构对比如下所示:

语义分割算法; DeepLab V3 结构

DeepLab V3 对 ASPP 模块进行了升级,升级后的结构细节如下图所示:

语义分割算法; DeepLab V3 ASPP 升级模块

DeepLab V3 的具体结构细节如下,包含多个残差块结构。

语义分割算法; DeepLab V3 Multi-Grid

DeepLab V3 中引入了 Multi-grid,可以输入大分辨率图片:

语义分割算法; DeepLab V3 Multi-Grid

DeepLab V3 包含 2 种实现结构:分别为 cascaded model 级联型 和 ASPP model 金字塔池化型。

两种模型分别如下的 2 幅图所示。

  • cascaded model 中 Block1,2,3,4 是 ResNet 网络的层结构(V3 主干网络采用 ResNet50 或 101),但 Block4 中将 3 × 3 3 \times 3 3×3 卷积和捷径分支 1 × 1 1 \times 1 1×1 卷积步长 Stride 由 2 2 2 改为 1 1 1,不进行下采样,且将 3 × 3 3 \times 3 3×3 卷积换成膨胀卷积,后面的 Block5,6,7 是对 Blockd 的 copy。(图中 rate 不是真正的膨胀系数,真正的膨胀系数 = r a t e ∗ M u l t i − g r i d =rate \ast Multi-grid =rate∗Multi−grid 参数)

语义分割算法; DeepLab V3 cascaded model 级联型

  • ASPP 模型的升级在前面介绍里提到了。

论文中使用较多的结构还是还是 ASPP 模型,两者模型在效果上差距不大。

语义分割算法; DeepLab V3 ASPP model 金字塔池化型

关键特点

  • 在残差块中使用多网格方法(MultiGrid),从而引入不同的空洞率。- 在空洞空间金字塔池化模块中加入图像级(Image-level)特征,并且使用 BatchNormalization 技巧。

5.7 Mask R-CNN

Mask R-CNN 在论文 Mask R-CNN 中被提出。

Mask R-CNN 以 Faster R-CNN 为基础,在现有的边界框识别分支基础上添加一个并行的预测目标掩码的分支。

Mask R-CNN 很容易训练,仅仅在 Faster R-CNN 上增加了一点小开销,运行速度为 5fps。

此外,Mask R-CNN 很容易泛化至其他任务,例如,可以使用相同的框架进行姿态估计。

Mask R-CNN 在 COCO 所有的挑战赛中都获得了最优结果,包括实例分割,边界框目标检测,和人关键点检测。在没有使用任何技巧的情况下,Mask R-CNN 在每项任务上都优于所有现有的单模型网络,包括 COCO 2016 挑战赛的获胜者。

语义分割算法; Mask R-CNN

语义分割算法; Mask R-CNN

Mask R-CNN 是在流行的 Faster R-CNN 架构基础上进行必要的修改,以执行语义分割。

语义分割算法; Mask R-CNN 组件

关键特点

在 Faster R-CNN 上添加辅助分支以执行语义分割- 对每个实例进行的 RoIPool 操作已经被修改为 RoIAlign ,它避免了特征提取的空间量化,因为在最高分辨率中保持空间特征不变对于语义分割很重要。- Mask R-CNN 与 Feature Pyramid Networks(类似于 PSPNet,它对特征使用了金字塔池化)相结合,在 MS COCO 数据集上取得了最优结果。

5.8 PSPNet

PSPNet 在论文 PSPNet: Pyramid Scene Parsing Network 中提出。

PSPNet 利用基于不同区域的上下文信息集合,通过我们的金字塔池化模块,使用提出的金字塔场景解析网络(PSPNet)来发挥全局上下文信息的能力。

全局先验表征在场景解析任务中产生了良好的质量结果,而 PSPNet 为像素级的预测提供了一个更好的框架,该方法在不同的数据集上达到了最优性能。它首次在 2016 ImageNet 场景解析挑战赛,PASCAL VOC 2012 基准和 Cityscapes 基准中出现。

语义分割算法; PSP 分割网络

语义分割算法; PSP 网络 Context Info

如上图所示,PSP 网络解决的主要问题是「缺少上下文信息」带来的不准确,其利用全局信息获取上下文,具体如下

之前的问题缺少上下文信息

如上图所示

  • 图中的 boat 区域和类别"car”的 appearance 相似
  • 模型只有 local 信息,Boat 容易被识别为"car"
  • Confusion categories: Building and skyscraper

应用上下文信息方法

  • 利用全局信息 (global information)
  • 全局信息 in CNN ~= feature/pyramid

语义分割算法; PSP 网络 Receptive Field

PSP 网络的一些细节如下几幅图中介绍:

语义分割算法; PSP 网络 RF → PSP

语义分割算法; PSP 网络 Pyramid Pooling

语义分割算法; PSP 网络 Pyramid Pooling

语义分割算法; PSP 网络 Pyramid Pooling

语义分割算法; PSP 网络结构

语义分割算法; PSP 分割网络 Backbone

语义分割算法; PSP 分割网络 Backbone

语义分割算法; PSP 网络

关键特点

  • PSPNet 通过引入空洞卷积来修改基础的 ResNet 架构,特征经过最初的池化,在整个编码器网络中以相同的分辨率进行处理(原始图像输入的 1/4),直到它到达空间池化模块。- 在 ResNet 的中间层中引入辅助损失,以优化整体学习。- 在修改后的 ResNet 编码器顶部的空间金字塔池化聚合全局上下文。

语义分割算法; PSP 网络

图片展示了全局空间上下文对语义分割的重要性。它显示了层之间感受野和大小的关系。在这个例子中,更大、更加可判别的感受野()相比于前一层()可能在细化表征中更加重要,这有助于解决歧义

5.9 RefineNet

RefineNet 在论文 RefineNet: Multi-Path Refinement Networks for High-Resolution Semantic Segmentation 中提出。

RefineNet 是一个通用的多路径优化网络,它明确利用了整个下采样过程中可用的所有信息,使用远程残差连接实现高分辨率的预测。通过这种方式,可以使用早期卷积中的细粒度特征来直接细化捕捉高级语义特征的更深的网络层。RefineNet 的各个组件使用遵循恒等映射思想的残差连接,这允许网络进行有效的端到端训练。

语义分割算法; RefineNet 架构

语义分割算法; RefineNet 架构

如上图所示,是建立 RefineNet 的块 - 残差卷积单元,多分辨率融合和链式残差池化。

RefineNet 解决了传统卷积网络中空间分辨率减少的问题,与 PSPNet(使用计算成本高的空洞卷积)使用的方法非常不同。提出的架构迭代地池化特征,利用特殊的 RefineNet 模块增加不同的分辨率,并最终生成高分辨率的分割图。

关键特点

  • 使用多分辨率作为输入,将提取的特征融合在一起,并将其传递到下一个阶段。
  • 引入链式残差池化,可以从一个大的图像区域获取背景信息。它通过多窗口尺寸有效地池化特性,利用残差连接和学习权重方式融合这些特征。
  • 所有的特征融合都是使用sum(ResNet 方式)来进行端到端训练。
  • 使用普通 ResNet 的残差层,没有计算成本高的空洞卷积

6.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=11

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

7.参考资料

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(15) | 视觉模型可视化与可解释性(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125052967

ShowMeAI 研究中心


Visualizing and Understanding; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


前言

深度可视化技术是深度学习中一个仍处于探索阶段的学术研究热点,它可以帮助我们更直观地理解模型做的事情。

以计算机视觉为例,CNN 中有着数以千计的卷积滤波器。深度神经网络中不同的滤波器会从输入图像中提取不同特征表示。

己有的研究表明低层的卷积核提取了图像的低级语义特性(如边缘、角点),高层的卷积滤波器提取了图像的高层语义特性(如图像类别)。

但是,由于深度神经网络会以逐层复合的方式从输入数据中提取特征,我们仍然无法像 Sobel 算子提取的图像边缘结果图一样直观地观察到深度神经网络中的卷积滤波器从输入图像中提取到的特征表示。

计算机视觉

计算机视觉; 不同卷积层可视化

本篇内容ShowMeAI和大家来看看对模型的理解,包括 CNN 可视化与理解的方法,也包含一些有趣的应用如 DeepDream、图像神经风格迁移等。

本篇重点

  • 特征/滤波器可视化
  • DeepDream
  • 图像神经风格迁移

1.特征可视化

我们在之前的课程里看到 CNN 的各种应用,在计算机视觉各项任务中发挥很大的作用,但我们一直把它当做黑盒应用,本节内容我们先来看看特征可视化,主要针对一些核心问题:

  • CNN 工作原理是什么样的
  • CNN 的中间层次都在寻找匹配哪些内容
  • 我们对模型理解和可视化有哪些方法

特征可视化; CNN 的工作原理

1.1 第一个卷积层

1) 可视化卷积核

第一个卷积层相对比较简单,可以把第一层的所有卷积核可视化来描述卷积层在原始图像匹配和关注什么。

可视化卷积核的背后原理是,卷积就是卷积核与图像区域做内积的结果,当图像上的区域和卷积核很相似时,卷积结果就会最大化。我们对卷积核可视化来观察卷积层在图像上匹配寻找什么。

常见的 CNN 架构第一层卷积核如下:

特征可视化; 第一个卷积层 可视化卷积核

从图中可以看到,不同网络的第一层似乎都在匹配有向边和颜色,这和动物视觉系统开始部分组织的功能很接近。

1.2 中间层

第二个卷积层就相对复杂一些,不是很好观察了。

第一个卷积层使用 16 16 16 个 7 × 7 × 3 7 \times 7 \times 3 7×7×3 的卷积核,第二层使用 20 20 20 个 7 × 7 × 16 7 \times 7 \times 16 7×7×16 的卷积核。由于第二层的数据深度变成 16 16 16 维,不能直接可视化。一种处理方法是对每个卷积核画出 16 16 16 个 7 × 7 7 \times 7 7×7 的灰度图,一共画 20 20 20 组。

然而第二层卷积不和图片直接相连,卷积核可视化后并不能直接观察到有清晰物理含义的信息。

特征可视化; 中间层

1) 可视化激活图

与可视化卷积核相比,将激活图可视化更有观察意义。

特征可视化; 中间层 可视化激活图

比如可视化 AlexNet 的第五个卷积层的 128 128 128 个 13 × 13 13 \times 13 13×13 的特征图,输入一张人脸照片,画出 Conv5 的 128 128 128 个特征灰度图,发现其中有激活图似乎在寻找人脸(不过大部分都是噪声)。

2) Maximally Activating Patches(最大激活区块)

可视化输入图片中什么类型的小块可以最大程度的激活不同的神经元。

  • 比如选择 AlexNet 的 Conv5 里的第 17 17 17 个激活图(共 128 128 128 个),然后输入很多的图片通过网络,并且记录它们在 Conv5 第 17 17 17 个激活图的值。
  • 这个特征图上部分值会被输入图片集最大激活,由于每个神经元的感受野有限,我们可以画出这些被最大激活的神经元对应在原始输入图片的小块,通过这些小块观察不同的神经元在寻找哪些信息。

如下图所示,每一行都是某个神经元被最大激活对应的图片块,可以看到:

  • 有的神经元在寻找类似眼睛的东西
  • 有的在寻找弯曲的曲线等

特征可视化; 中间层 最大激活区块

如果不使用 Conv5 的激活图,而是更后面的卷积层,由于卷积核视野的扩大,寻找的特征也会更加复杂,比如人脸、相机等,对应图中的下面部分。

1.3 倒数第二个全连接层

1) 最邻近

关于最近邻算法的详细知识也可以参考ShowMeAI的下述文章

另一个有价值的观察对象是输入到最后一层用于分类的全连接层的图片向量,比如 AlexNet 每张图片会得到一个 4096 4096 4096 维的向量。

使用一些图片来收集这些特征向量,然后在特征向量空间上使用最邻近的方法找出和测试图片最相似的图片。作为对比,是找出在原像素上最接近的图片。

可以看到,在特征向量空间中,即使原像素差距很大,但却能匹配到实际很相似的图片。

比如大象站在左侧和站在右侧在特征空间是很相似的。

特征可视化; 倒数第二个全连接层 最近邻

2) 降维

关于 PCA 降维算法的详细知识也可以参考ShowMeAI的下述文章

另一个观察的角度是将 4096 4096 4096 维的向量压缩到二维平面的点,方法有 PCA,还有更复杂的非线性降维算法比如 t-SNE(t-distributed stochastic neighbors embeddings,t-分布邻域嵌入)。我们把手写数字 0-9 的图片经过 CNN 提取特征降到 2 维画出后,发现都是按数字簇分布的,分成 10 簇。如下图所示:

特征可视化; 降维

同样可以把这个方法用到 AlexNet 的 4096 4096 4096 维特征向量降维中。

我们输入一些图片,得到它们的 4096 4096 4096 维特征向量,然后使用 t-SNE 降到二维,画出这些二维点的网格坐标,然后把这些坐标对应的原始图片放在这个网格里。

如果大家做这个实验,可以观察到相似内容的图片聚集在了一起,比如左下角都是一些花草,右上角聚集了蓝色的天空。

特征可视化; 降维

1.4 哪些像素对分类起作用?

1) 遮挡实验(Occlusion Experiments)

有一些方法可以判定原始图片的哪些位置(像素)对最后的结果起作用了,比如遮挡实验(Occlusion Experiments)是一种方法。

它在图片输入网络前,遮挡图片的部分区域,然后观察对预测概率的影响,可以想象得到,如果遮盖住核心部分内容,将会导致预测概率明显降低。

如下图所示,是遮挡大象的不同位置,对「大象」类别预测结果的影响。

特征可视化; 哪个像素对分类有用 遮挡实验

2) 显著图(Saliency Map)

除了前面介绍到的遮挡法,我们还有显著图(Saliency Map)方法,它从另一个角度来解决这个问题。

显著图(Saliency Map)方法是计算分类得分相对于图像像素的梯度,这将告诉我们在一阶近似意义上对于输入图片的每个像素如果我们进行小小的扰动,那么相应分类的分值会有多大的变化。

可以在下图看到,基本上找出了小狗的轮廓。

特征可视化; 哪个像素对分类有用 显著图

进行语义分割的时候也可以运用显著图的方法,可以在没有任何标签的情况下可以运用显著图进行语义分割。

3) 引导式反向传播

不像显著图那样使用分类得分对图片上的像素求导,而是使用卷积网络某一层的一个特定神经元的值对像素求导,这样就可以观察图像上的像素对特定神经元的影响。

但是这里的反向传播是引导式的,即 ReLU 函数的反向传播时,只回传大于 0 0 0 的梯度,具体如下图所示。这样的做法有点奇怪,但是效果很好,图像很清晰。

特征可视化; 哪个像素对分类有用 引导式反向传播

我们把引导式反向传播计算的梯度可视化和最大激活块进行对比,发现这两者的表现很相似。

下图左边是最大激活块,每一行代表一个神经元,右侧是该神经元计算得到的对原始像素的引导式反向传播梯度。

下图的第一行可以看到,最大激活该神经元的图像块都是一些圆形的区域,这表明该神经元可能在寻找蓝色圆形状物体,下图右侧可以看到圆形区域的像素会影响的神经元的值。

特征可视化; 哪个像素对分类有用 引导式反向传播

4) 梯度上升(Gradient Ascent)

引导式反向传播会寻找与神经元联系在一起的图像区域,另一种方法是梯度上升,合成一张使神经元最大激活或分类值最大的图片。

我们在训练神经网络时用梯度下降来使损失最小,现在我们要修正训练的卷积神经网络的权值,并且在图像的像素上执行梯度上升来合成图像,即最大化某些中间神将元和类的分值来改变像素值。

梯度上升的具体过程为:输入一张所有像素为 0 或者高斯分布的初始图片,训练过程中,神经网络的权重保持不变,计算神经元的值或这个类的分值相对于像素的梯度,使用梯度上升改变一些图像的像素使这个分值最大化。

同时,我们还会用正则项来阻止我们生成的图像过拟合。

总之,生成图像具备两个属性:

  • ① 使最大程度地激活分类得分或神经元的值
  • ② 使我们希望这个生成的图像看起来是自然的。

正则项强制生成的图像看起来是自然的图像,比如使用 L2 正则来约束像素,针对分类得分生成的图片如下所示:

特征可视化; 哪个像素对分类有用 梯度上升

也可以使用一些其他方法来优化正则,比如:

  • 对生成的图像进行高斯模糊处理
  • 去除像素值特别小或梯度值特别小的值

上述方法会使生成的图像更清晰。

也可以针对某个神经元进行梯度上升,层数越高,生成的结构越复杂。

特征可视化; 哪个像素对分类有用 梯度上升

添加多模态(multi-faceted)可视化可以提供更好的结果(加上更仔细的正则化,中心偏差)。通过优化 FC6 的特征而不是原始像素,会得到更加自然的图像。

一个有趣的实验是「愚弄网络」:

输入一张任意图像,比如大象,给它选择任意的分类,比如考拉,现在就通过梯度上升改变原始图像使考拉的得分变得最大,这样网络认为这是考拉以后观察修改后的图像,我们肉眼去看和原来的大象没什么区别,并没有被改变成考拉,但网络已经识别为考拉(图片在人眼看起来还是大象,然而网络分类已经把它分成考拉了)。

特征可视化; 哪个像素对分类有用 梯度上升

2.DeepDream

DeepDream 是一个有趣的 AI 应用实验,仍然利用梯度上升的原理,不再是通过最大化神经元激活来合成图片,而是直接放大某些层的神经元激活特征。

步骤如下:

  • ① 首先选择一张输入的图像,通过神经网络运行到某一层
  • ② 接着进行反向传播并且设置该层的梯度等于激活值,然后反向传播到图像并且不断更新图像。

对于以上步骤的解释:试图放大神经网络在这张图像中检测到的特征,无论那一层上存在什么样的特征,现在我们设置梯度等于特征值,以使神经网络放大它在图像中所检测到的特征。

DeepDream; 代码

下图:输入一张天空的图片,可以把网络中学到的特征在原图像上生成:

DeepDream; 生成结果图

代码实现可以参考 google 官方实现 github.com/google/deepdream

3.图像神经风格迁移

关于图像神经网络风格迁移的讲解也可以参考ShowMeAI的下述文章

3.1 特征反演(Feature Inversion)

我们有一个查看不同层的特征向量能保留多少原始的图片信息的方法,叫做「特征反演」。

具体想法是:任选 1 张图片,前向传播到已经训练好的 CNN,选取其在 CNN 某一层产生的特征向量,保留这个向量。我们希望生成 1 张图片,尽量让它在该层产生一样的特征向量。

我们依旧使用梯度上升方法来完成,这个任务的目标函数定义为「最小化生成图片的特征向量与给定特征向量的 L2 距离」,当然我们会加一些正则化项保证生成图片的平滑,总体如下图所示:

图像神经风格迁移; 特征反演

通过这个方法,我们可以看到不同层的特征向量所包含的信息完整度,如下图所示:

图像神经风格迁移; 特征反演

解释讲解

  • 在 relu2_2 层,可以根据特征向量几乎无损地恢复出原图片;
  • 从 ReLU4_3 ReLU5_1 重构图像时,可以看到图像的一般空间结构被保留了下来,仍可以分辨出大象,苹果和香蕉,但是许多低层次的细节并比如纹理、颜色在神经网路的较高层更容易损失。

3.2 纹理生成(Texture Synthesis)

下面我们聊到的是「纹理生成」,针对这个问题,传统的方法有「近邻法」:根据已经生成的像素查看当前像素周围的邻域,并在输入图像的图像块中计算近邻,然后从输入图像中复制像素。但是这类方法在面对复杂纹理时处理得并不好。

1) 格莱姆矩阵(Gram Matrix)

格莱姆矩阵计算方法

纹理生成的神经网络做法会涉及到格莱姆矩阵(Gram Matrix),我们来介绍一下它,我们先看看格莱姆矩阵怎么得到:

① 将一张图片传入一个已经训练好的 CNN,选定其中一层激活,其大小是 C × H × W C \times H \times W C×H×W,可以看做是 H × W H \times W H×W 个 C C C 维向量。

② 从这个激活图中任意选取两个 C 维向量,做矩阵乘法可以得到一个 C × C C \times C C×C 的矩阵。然后对激活图中任意两个 C C C 维向量的组合,都可以求出这样一个矩阵。把这些矩阵求和并平均,就得到 Gram Matrix。

格莱姆矩阵含义

格莱姆矩阵告诉我们两个点代表的不同特征的同现关系,矩阵中位置索引为 i j ij ij 的元素值非常大,这意味着这两个输入向量的位置索引为 i i i 和 j j j 的元素值非常大。

格莱姆矩阵捕获了一些二阶统计量,即“映射特征图中的哪些特征倾向于在空间的不同位置一起激活”。

图像神经风格迁移; 纹理生成 格莱姆矩阵

格莱姆矩阵其实是特征之间的偏心协方差矩阵(即没有减去均值的协方差矩阵)。其计算了每个通道特征之间的相关性,体现的是哪些特征此消彼长,哪些特征同时出现。

我们可以认为格莱姆矩阵度量了图片中的纹理特性,并且不包含图像的结构信息,因为我们对图像中的每一点所对应的特征向量取平均值,它只是捕获特征间的二阶同现统计量,这最终是一个很好的纹理描述符。

事实上,使用协方差矩阵代替格莱姆矩阵也能取得很好的效果,但是格莱姆矩阵有更高效的计算方法:

  • 将激活图张量 C × H × W C \times H \times W C×H×W 展开成 C × H W C \times HW C×HW 的形式,然后将其乘以其转置。

2) 神经纹理生成(Neural Texture Synthesis)

当我们有了格莱姆矩阵这一度量图像纹理特性的工具后,就可以使用类似于梯度上升算法来产生特定纹理的图像。

算法流程如下图所示:

图像神经风格迁移; 神经纹理生成

纹理生成步骤

  • ① 首先把含有纹理的图像输入到一个预训练网络中(例如 VGG),记录其每一层的激活图并计算每一层的格莱姆矩阵。
  • ② 接着随机初始化一张要生成的新的图像,同样把这张初始化图像通过预训练网络并且计算每一层的 gram 矩阵。
  • ③ 然后计算输入图像纹理矩阵和生成图像纹理矩阵之间的加权 L2 损失,进行反向传播,并计算相对于生成图像的像素的梯度。
  • ④ 最后根据梯度上升一点点更新图像的像素,不断重复这个过程,即计算两个格莱姆矩阵的 L2 范数损失和反向传播图像梯度,最终会生成与纹理图像相匹配的纹理图像。

生成的纹理效果如下图所示:

图像神经风格迁移; 神经纹理生成

上图说明,如果以更高层格莱姆矩阵的 L2 距离作为损失函数,那么生成图像就会更好地重建图像的纹理结构(这是因为更高层的神经元具有更大的感受野)。

3.3 图像神经风格迁移(Style Transfer)

如果我们结合特征反演和纹理生成,可以实现非常热门的一个网络应用「图像神经风格迁移(Style Transfer)」。它能根据指定的 1 张内容图片和 1 张风格图片,合并生成具有相似内容和风格的合成图。

具体的做法是:准备两张图像,一张图像称为内容图像,需要引导我们生成图像的主题;另一张图像称为风格图像,生成图像需要重建它的纹理结构。然后共同做特征识别,最小化内容图像的特征重构损失,以及风格图像的格莱姆矩阵损失。

使用下面的框架完成这个任务:

图像神经风格迁移; Style Transfer

上图所示的框架中,使用随机噪声初始化生成图像,同时优化特征反演和纹理生成的损失函数(生成图像与内容图像激活特征向量的 L2 距离以及与风格图像 gram 矩阵的 L2 距离的加权和),计算图像上的像素梯度,重复这些步骤,应用梯度上升对生成图像调整。

迭代完成后我们会得到风格迁移后的图像:它既有内容图像的空间结构,又有风格图像的纹理结构。

因为网络总损失是「特征反演」和「纹理生成」的两部分损失的加权和,我们调整损失中两者的权重可以得到不同倾向的输出,如下图所示:

图像神经风格迁移; 内容与风格相似度权重

也可以改变风格图像的尺寸:

图像神经风格迁移; 图像风格尺度

我们甚至可以使用不同风格的格莱姆矩阵的加权和,来生成多风格图:

图像神经风格迁移; 多风格合并

代码实现可以参考这里:github.com/jcjohnson/neural-style

3.4 快速图像风格迁移(Fast style Transfer)

上面的风格迁移框架,每生成一张新的图像都需要迭代数次,计算量非常大。因此有研究提出了下面的 Fast style Transfer 的框架:

快速图像风格迁移; Fast style Transfer

快速图像风格迁移方法,会在一开始训练好想要迁移的风格,得到一个可以输入内容图像的网络,直接前向运算,最终输出风格迁移后的结果。

训练前馈神经网络的方法是在训练期间计算相同内容图像和风格图像的损失,然后使用相同梯度来更新前馈神经网络的权重,一旦训练完成,只需在训练好的网络上进行一次前向传播。

代码实现可以参考这里:github.com/jcjohnson/fast-neural-style

4.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=12

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

5.参考资料

6.要点总结

  • 理解 CNN
    • 激活值:在激活值的基础上理解这些神经元在寻找什么特征,方法有最邻近、降维、最大化图像块、遮挡;
  • 梯度:使用梯度上升合成新图像来理解特征的意义,比如显著图、类可视化、愚弄图像、特征反演。
  • 风格迁移:特征反演+纹理生成。

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(16) | 生成模型(PixelRNN,PixelCNN,VAE,GAN)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125055672

ShowMeAI 研究中心


Generative Models; 深度学习与计算机视觉

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

之前了解到的都是监督学习(Supervised Learning):我们有数据 x 和标签 y,目标是学习到一个函数可以将数据 x 映射到标签 y,标签可以有很多形式。

典型的有监督学习有:分类问题中输入一张图片,输出图片的分类;目标检测中输入一张图片,输出目标物体的边框;语义分割中,给每个像素都打上标签。

CS231n 第 13 讲给大家介绍的是无监督学习(Unsupervised Learning)以及生成模型的一些知识。

本篇重点

  • 无监督学习
  • 生成模型
    • Pixel RNN/CNN
    • 变分自编码器(VAE)
    • 生成对抗网络(GAN)

1.无监督学习

无监督学习在我们只有一些没有标签的训练数据的情况下,学习数据中隐含的结构。无监督学习由于没有标签,数据获取也很容易。典型的无监督学习包括下述算法:

1.1 聚类(k-Means)

关于聚类算法的详细知识也可以参考ShowMeAI的下述文章

聚类(Clustering)是找到数据的分组,组内数据在某种度量方式下是相似的。随机初始 k 个中心位置,将每个样本分配到最近的中心位置,然后根据分配的样本更新中心位置。重复这个过程直至收敛(中心位置不再变化)。

无监督学习; 聚类 k-Means

1.2 PCA(主成分分析)

关于 PCA 降维算法的详细知识也可以参考ShowMeAI的下述文章

数据降维Dimensionality reduction):找出一些投影方向(轴),在这些轴上训练数据投影的方差最大。这些轴就是数据内潜在的结构。我们可以用这些轴来减少数据维度,数据在每个保留下来的维度上都有很大的方差。

无监督学习; 主成分分析 PCA

1.3 特征学习(Feature Learning)

我们还有一些特征学习的方法,比如自编码(Autoencoders):

无监督学习; 特征学习 Feature Learning

1.4 密度估计( Density Estimation)

密度估计( Density Estimation)也是一种无监督算法,我们会估计数据的内在分布情况,比如下图上方有一些一维和二维的点,我们用高斯函数来拟合这一密度分布,如下图所示:

无监督学习; 密度估计 Density Estimation

2.生成模型(Generative Models)

生成模型是一种无监督学习方法。它对应的任务是:根据一批由真实分布 p-data(x) 产生的训练数据,通过训练学习,得到一个可以以近似于真实的分布 p-model(x) 来产生新样本的模型。

生成模型; Generative Models

为什么生成模型重要,因为其可以支撑一系列问题的解决:生成样本,着色问题,强化学习应用,隐式表征推断等。

下图左边为生成的图片,中间生成的人脸,还可以做超分辨率或者着色之类的任务。

生成模型; Generative Models

生成模型分为「显式」和「隐式」的生成模型,往下分又可以分成很多子类。如下图所示。

我们在本篇内容中主要讨论 3 种模型:PixelRNN / CNN,变分自动编码器属于显示密度模型,生成对抗网络(GAN)属于隐式密度估计模型。

生成模型; Generative Models

2.1 PixelRNN 和 PixelCNN

PixelRNN 和 PixelCNN 使用概率链式法则来计算一张图片出现的概率。其中每一项为给定前 i − 1 i-1 i−1 个像素点后第 i i i 个像素点的条件概率分布。这个分布通过神经网络 RNN 或 CNN 来建模,再通过最大化图片 x x x 的似然概率来学习出 RNN 或 CNN 的参数。

条件概率公式为:

PixelRNN; PixelCNN

p ( x ) = ∏ i = 1 n p ( x i ∣ x 1 , … , x i − 1 ) p(x)=\prod_{i=1}^{n} p(x_{i} \mid x_{1}, \ldots, x_{i-1}) p(x)=i=1∏n​p(xi​∣x1​,…,xi−1​)

其中:

  • p ( x ) p(x) p(x): 图像 x 的似然概率
  • p ( x i ∣ x 1 , … , x i − 1 ) p(x_{i} \mid x_{1}, \ldots, x_{i-1}) p(xi​∣x1​,…,xi−1​): 条件概率

PixelRNN 中,从左上角开始定义「之前的像素」。由于 RNN 每个时间步的输出概率都依赖于之前所有输入,因此能够用来表示上面的条件概率分布。

PixelRNN; PixelCNN

我们训练这个 RNN 模型时,一次前向传播需要从左上到右下串行走一遍,然后根据上面的公式求出似然,并最大化似然以对参数做一轮更新。因此训练非常耗时。

PixelCNN 中,使用一个 CNN 来接收之前的所有像素,并预测下一个像素的出现概率:

p ( x ) = ∏ i = 1 n p ( x i ∣ x 1 , … , x i − 1 ) p(x)=\prod_{i=1}^{n} p(x_{i} \mid x_{1}, \ldots, x_{i-1}) p(x)=i=1∏n​p(xi​∣x1​,…,xi−1​)

PixelRNN; PixelCNN

对比 PixelRNN 和 PixelCNN,后者在训练时可以并行计算公式中的每一项,然后进行参数更新,因此训练速度远快于 PixelRNN。

不过,在测试阶段,我们会发现 PixelRNN 和 PixelCNN 都要从左上角开始逐个像素点地生成图片,实际应用阶段生成图像的速度是很慢的。

生成模型; 生成的图片样本

PixelRNN 和 PixelCNN 能显式地计算似然 p ( x ) p(x) p(x),是一种可优化的显式密度模型,该方法给出了一个很好的评估度量,可以通过计算的数据的似然来度量出生成样本有多好。

生成模型; PixelRNN 和 PixelCNN 优缺点

2.2 变分自编码器(VAE)

PixelCNN 定义了一个易于处理的密度函数,我们可以直接优化训练数据的似然;而我们下面介绍到的变分自编码器方法中,密度函数就不易处理了,我们要通过附加的隐变量 z z z 对密度函数进行建模:

p θ ( x ) = ∫ p θ ( z ) p θ ( x ∣ z ) d z p_{\theta}(x)=\int p_{\theta}(z) p_{\theta}(x \mid z) d z pθ​(x)=∫pθ​(z)pθ​(x∣z)dz

我们数据的似然 p ( x ) p(x) p(x) 是等式右边的积分形式,即对所有可能的 z z z 值取期望,但它是无法直接优化的,我们只能找出一个似然函数的下界然后再对该下界进行优化。

1) 自编码器

自编码器是为了无监督地学习出样本的特征表示,原理如下:

变分自编码器 VAE; 自编码器

如上图,自编码器由编码器和解码器组成,编码器将样本 x x x 映射到特征 z z z,解码器再 x x x 将特征 z z z 映射到重构样本。我们设定损失函数为 x x x 与重构样本之间的 L2 损失,训练出编码器和解码器的参数,希望能够使 z z z 解码后恢复出原来的 x x x。

编码器

编码器可以有多种形式,常用的是神经网络。最先提出的是非线性层的线性组合,然后有了深层的全连接网络(MLP),后来又使用 CNN,我们通过神经网络对输入数据 x x x 计算和映射,得到特征 z z z, z z z 的维度通常比 x x x 更小。这种降维压缩可以压缩保留 x x x 中最重要的特征。

解码器

解码器主要是为了重构数据,它输出一些跟 x x x 有相同维度的结果并尽量拟合 x x x 。解码器一般使用和编码器相同类型的网络(与编码器对称)。

训练好完整的网络后,我们会把解码器的部分去掉,使用训练好的编码器实现特征映射。

通过编码器得到输入数据的特征,编码器顶部有一个分类器,如果是分类问题我们可以用它来输出一个类标签,在这里使用了外部标签和标准的损失函数如 Softmax。

变分自编码器 VAE; 自编码器

无标签数据得到的模型,可以帮助我们得到普适特征(比如上述自编码器映射得到的特征),它们作为监督学习的输入是非常有效的(有些场景下监督学习可能只有很少的带标签的训练数据,少量的数据很难训练模型,可能会出现过拟合等其他一些问题),通过上述方式得到的特征可以很好地初始化下游监督学习任务的网络。

自编码器具有重构数据、学习数据特征、初始化一个监督模型的能力。这些学习到的特征具有能捕捉训练数据中蕴含的变化因素的能力。我们获得了一个含有训练数据中变化因子的隐变量 z z z 。

2) VAE 的思想

VAE 模型的思路是,如果我们无法直接获得样本 x x x 的分布,那么我们可以假设存在一个 x x x 对应的隐式表征 z z z , z z z 的分布是一个先验分布(比如高斯分布或其他简单的分布)。

举例来说,如果我们想要生成微笑的人脸, z z z 代表的是眉毛的位置,嘴角上扬的弧度,它经过解码网络后,能够映射得到 x x x 的近似真实分布。那在样本生成阶段,我们可以通过标准正态分布采样得到 z z z ,然后解码得到样本近似分布,再在此分布上采样来生成样本。

对于这个采样过程,真实的参数是 θ ∗ \theta \ast θ∗ ,是有关于先验假设和条件概率分布的参数,我们的目的在于获得一个样本生成式模型,从而利用它来生成新的数据,真实参数是我们想要估计并得出的。

我们表示这个生成式模型的方法是:选一个简单的关于 z z z 的先验分布,例如高斯分布,对于给定 z z z 的 x x x 的条件概率分布 p ( x ∣ z ) p(x \mid z) p(x∣z) 很复杂,我们会使用神经网络来对 p ( x ∣ z ) p(x \mid z) p(x∣z) 进行建模。

变分自编码器 VAE; VAE 的思想

3) 如何训练 VAE

我们的目标是:从一堆样本中学习出解码网络的参数,使得在标准高斯分布上采样得到的 z z z ,经过解码后得到的 x x x 的分布,刚好近似于 x x x 的真实分布。

我们通过「最大化样本 x x x 的似然 P ( x ) P(x) P(x)」来达到上述目标。 在已经给定隐变量 z z z 的情况下,写出 x x x 的分布 p p p 并对所有可能的 z z z 值取期望,因为 z z z 值是连续的所以表达式是一个积分:

p θ ( x ) = ∫ p θ ( z ) p θ ( x ∣ z ) d z p_{\theta}(x)=\int p_{\theta}(z) p_{\theta}(x \mid z) d z pθ​(x)=∫pθ​(z)pθ​(x∣z)dz

问题是利用求导来直接求最大化的似然,很不好解。

第一项是 z z z 的分布 p ( z ) p(z) p(z) ,这里将它简单地设定为高斯分布,所以很容易求; p ( x ∣ z ) p(x \mid z) p(x∣z) 是一个指定的神经网络解码器,也容易得到。

但是计算所有的 z z z 对应的 p ( x ∣ z ) p(x \mid z) p(x∣z) 很困难,所以无法计算该积分。这样也导致 p ( z ∣ x ) p(z \mid x) p(z∣x) 是难解的。

解决方法是,在使用神经网络解码器来定义一个对 p ( x ∣ z ) p(x \mid z) p(x∣z) 建模神经网络的同时,额外定义一个编码器 q ( z ∣ x ) q(z \mid x) q(z∣x) ,将输入 x x x 编码为 z z z ,从而得到似然 p ( z ∣ x ) p(z \mid x) p(z∣x) 。

也就是说我们定义该网络来估计出 p ( z ∣ x ) p(z \mid x) p(z∣x) ,这个后验密度分布项仍然是难解的,我们用该附加网络来估计该后验分布,这将使我们得到一个数据似然的下界,该下界易解也能优化。

变分自编码器 VAE; 如何训练 VAE

在变分自编码器中我们想得到一个生成数据的概率模型,将输入数据 x x x 送入编码器得到一些特征 z z z ,然后通过解码器网络把 z z z 映射到图像 x x x 。

我们这里有编码器网络和解码器网络,将一切参数随机化。参数是 ϕ \phi ϕ 的编码器网络 q ( z ∣ x ) q(z \mid x) q(z∣x) 输出一个均值和一个对角协方差矩阵;解码器网络输入 z z z ,输出均值和关于 x x x 的对角协方差矩阵。为了得到给定 x x x 下的 z z z 和给定 z z z 下的 x x x ,我们会从这些分布( p p p 和 q q q)中采样,现在我们的编码器和解码器网络所给出的分别是 z z z 和 x x x 的条件概率分布,并从这些分布中采样从而获得值。

下面是推导过程

log ⁡ L = log ⁡ p ( x ) = log ⁡ p ( x ) ⋅ 1 = log ⁡ p ( x ) ∫ z q ( z ∣ x ) d z \log \mathrm{L}=\log p(x)=\log p(x) \cdot 1=\log p(x) \int_{z} q(z \mid x) d z logL=logp(x)=logp(x)⋅1=logp(x)∫z​q(z∣x)dz

这里引入了一个分布 q ( z ∣ x ) q(z \mid x) q(z∣x) ,就是编码网络。这里我们暂时只把它当作一个符号,继续推导即可:

log ⁡ L = log ⁡ p ( x ) ∫ z q ( z ∣ x ) d z = ∫ z q ( z ∣ x ) log ⁡ p ( x , z ) p ( z ∣ x ) d z = ∫ z q ( z ∣ x ) [ log ⁡ p ( x , z ) q ( z ∣ x ) − log ⁡ p ( z ∣ x ) q ( z ∣ x ) ] d z = ∫ z q ( z ∣ x ) log ⁡ p ( x , z ) q ( z ∣ x ) d z + ∫ z q ( z ∣ x ) log ⁡ q ( z ∣ x ) p ( z ∣ x ) d z = ∫ z q ( z ∣ x ) log ⁡ p ( x , z ) q ( z ∣ x ) d z + D K L ( q ( z ∣ x ) ∥ p ( z ∣ x ) ) \begin{aligned} \log \mathrm{L} &=\log p(x) \int_{z} q(z \mid x) d z \ &=\int_{z} q(z \mid x) \log \frac{p(x, z)}{p(z \mid x)} d z \ &=\int_{z} q(z \mid x)\left[\log \frac{p(x, z)}{q(z \mid x)}-\log \frac{p(z \mid x)}{q(z \mid x)}\right] d z \ &=\int_{z} q(z \mid x) \log \frac{p(x, z)}{q(z \mid x)} d z+\int_{z} q(z \mid x) \log \frac{q(z \mid x)}{p(z \mid x)} d z \ &=\int_{z} q(z \mid x) \log \frac{p(x, z)}{q(z \mid x)} d z+D_{K L}(q(z \mid x) | p(z \mid x)) \end{aligned} logL​=logp(x)∫z​q(z∣x)dz=∫z​q(z∣x)logp(z∣x)p(x,z)​dz=∫z​q(z∣x)[logq(z∣x)p(x,z)​−logq(z∣x)p(z∣x)​]dz=∫z​q(z∣x)logq(z∣x)p(x,z)​dz+∫z​q(z∣x)logp(z∣x)q(z∣x)​dz=∫z​q(z∣x)logq(z∣x)p(x,z)​dz+DKL​(q(z∣x)∥p(z∣x))​

对第一项,我们有:

∫ z q ( z ∣ x ) log ⁡ p ( x , z ) q ( z ∣ x ) d z = ∫ z q ( z ∣ x ) log ⁡ p ( x ∣ z ) d z + ∫ z q ( z ∣ x ) log ⁡ p ( x ) q ( z ∣ x ) d z = E z ∼ q [ log ⁡ p ( x ∣ z ) ] − D K L ( q ( z ∣ x ) ∥ p ( z ) ) \begin{aligned} \int_{z} q(z \mid x) \log \frac{p(x, z)}{q(z \mid x)} d z &=\int_{z} q(z \mid x) \log p(x \mid z) d z+\int_{z} q(z \mid x) \log \frac{p(x)}{q(z \mid x)} d z \ &=\mathrm{E}{z \sim q}[\log p(x \mid z)]-D(q(z \mid x) | p(z)) \end{aligned} ∫z​q(z∣x)logq(z∣x)p(x,z)​dz​=∫z​q(z∣x)logp(x∣z)dz+∫z​q(z∣x)logq(z∣x)p(x)​dz=Ez∼q​[logp(x∣z)]−DKL​(q(z∣x)∥p(z))​

这样我们就得到了 VAE 的核心等式:

log ⁡ p ( x ) = E z ∼ q [ log ⁡ p ( x ∣ z ) ] − D K L ( q ( z ∣ x ) ∥ p ( z ) ) + D K L ( q ( z ∣ x ) ∥ p ( z ∣ x ) ) \log p(x)=\mathrm{E}{z \sim q}[\log p(x \mid z)]-D(q(z \mid x) | p(z))+D_{K L}(q(z \mid x) | p(z \mid x)) logp(x)=Ez∼q​[logp(x∣z)]−DKL​(q(z∣x)∥p(z))+DKL​(q(z∣x)∥p(z∣x))

注意到这个式子的第三项中,含有 p ( z ∣ x ) p(z \mid x) p(z∣x) ,而

p θ ( z ∣ x ) = p θ ( x ∣ z ) p θ ( z ) / p θ ( x ) p_{\theta}(z \mid x)=p_{\theta}(x \mid z) p_{\theta}(z) / p_{\theta}(x) pθ​(z∣x)=pθ​(x∣z)pθ​(z)/pθ​(x)

p θ ( x ) = ∫ p θ ( z ) p θ ( x ∣ z ) d z p_{\theta}(x)=\int p_{\theta}(z) p_{\theta}(x \mid z) d z pθ​(x)=∫pθ​(z)pθ​(x∣z)dz

变分自编码器 VAE; 如何训练 VAE

由于这个积分无法求解出来,因此我们没办法求第三项的梯度。幸运的是,由于第三项是一个 KL 散度,其恒大于等于 0 0 0,因此前两项的和是似然的一个下界。因此我们退而求其次,来最大化似然的下界,间接达到最大化似然的目的。

现在我们引入编码器网络来对 q ( z ∣ x ) q(z \mid x) q(z∣x) 建模,我们的训练框架如下:

变分自编码器 VAE; 如何训练 VAE

如何得到下界

① 第 1 项是对所有采样的 z z z 取期望, z z z 是 x x x 经过编码器网络采样得到,对 z z z 采样然后再求所有 z z z 对应的 p ( x ∣ z ) p(x \mid z) p(x∣z) 。让 p ( x ∣ z ) p(x \mid z) p(x∣z) 变大,就是最大限度地重构数据。

② 第 2 项是让 KL 的散度变小,让我们的近似后验分布和先验分布变得相似,意味着我们想让隐变量 z 遵循我们期望的分布类型。

这个框架就非常类似于自编码器。

其中最大化下界的第一项表示我们要能从解码器最大概率地重构出 x x x ,这一步等价于去最小化与样本 x x x 的均方误差。最小化下界的第二项则限定了 z z z 要遵循我们事先给它指定的分布。

公式是我们要优化及最大化的下界,前向传播按如上流程处理,对输入数据 x x x ,让小批量的数据传递经过编码器网络的到 q ( z ∣ x ) q(z \mid x) q(z∣x) ,通过 q ( z ∣ x ) q(z \mid x) q(z∣x) 来计算 KL 项,然后根据给定 x x x 的 z z z 分布对 z z z 进行采样,由此获得了隐变量的样本,这些样本可以根据 x x x 推断获得;然后把 z z z 传递给第二个解码器网络,通过解码器网络 x x x 在给定 z z z 的条件下的两个参数,均值和协方差,最终可以在给定 z z z 的条件下从这个分布中采样得到 x x x 。

训练时需要获得该分布,损失项是给定 z z z 条件下对训练像素值取对数,损失函数要做的是最大化被重构的原始输入数据的似然;对于每一个小批量的输入我们都计算这一个前向传播过程,取得所有我们需要的项,他们都是可微分的,接下来把他们全部反向传播回去并获得梯度,不断更新我们的参数,包括生成器和解码器网络的参数 θ \theta θ 和 ϕ \phi ϕ 从而最大化训练数据的似然。

训练好变分自编码器,当生成数据时只需要用解码器网络,我们在训练阶段就对 z z z 采样,而不用从后验分布中采样,在生成阶段会从真实的生成过程中采样。先从设定好的先验分布中采样,接下来对数据 x x x 采样。

需要注意的是,这个框架里面,梯度无法通过「采样」这个算子反向传播到编码器网络,因此我们使用一种叫做重采样的 trick。即将 z z z 采样的算子分解为:

变分自编码器 VAE; 如何训练 VAE

这样梯度不需要经过采样算子就能回流到编码器网络中。

4) VAE 的优缺点

总结一下,VAE 是在原来的自编码器上加了随机成分,我们使用 VAE 不是直接取得确定的输入 x x x 然后获得特征 z z z 最后再重构 x x x ,而是采用随机分布和采样的思想,这样我们就能生成数据。 为了训练模型 VAEs,我们定义了一个难解的密度分布,我们推导出一个下界然后优化下界,下界是变化的,「变分」指的是用近似来解决这些难解的表达式,这是模型被称为变分自动编码器的原因。

VAEs 优点

VAEs 就生成式模型来说是一种有据可循的方法,它使得查询推断称为可能,如此一来便能够推断出像 q ( z ∣ x ) q(z \mid x) q(z∣x) 这样的分布,这些东西对其他任务来说会是很有用的特征表征。

VAEs 缺点

最大化似然下界思想是 OK 的,但是不像 PixelRNN 和 PixelCNN 那样精准评估。而 VAE 相对后续会讲到的 GAN 等方法,生成的图像结果更模糊。

2.3 生成对抗网络(Generative Adversarial Nets, GAN)

1) GAN 的核心思路

我们之前的 PixelCNN 和 PixelRNN 定义了一个易于处理的密度函数,通过密度函数优化训练数据的似然;VAEs 有一个额外定义的隐变量 z z z ,有了 z z z 以后获得了很多的有利性质但是我们也有了一个难解的密度函数,对于该函数我们不能直接优化,我们推到了一个似然函数的下界,然后对它进行优化。

现在我们放弃显式地对密度函数建模,我们想要得到的是从分布中采样并获得质量良好的样本。GANs 中不再在显式的密度函数上花费精力,而是采用一个博弈论的方法,并且模型将会习得从训练分布中生成数据,具体的实现是基于「生成器」和「判别器」这一对博弈玩家。

相比变分自编码器,GAN 的核心思路非常简单。

在 GAN 中我们定义了两个网络:「生成器」和「判别器」。

  • 判别器负责辨别哪些样本是生成器生成的假样本,哪些是从真实训练集中抽出来的真样本。
  • 生成器负责利用随机噪声 z z z 生成假样本,它的职责是生成尽可能真的样本以骗过判别器。

生成对抗网络; 理解 GAN 如何工作

这种对抗形式的目标可以写成如下形式:

min ⁡ θ g max ⁡ θ d [ E x ∼ p data  log ⁡ D θ d ( x ) + E z ∼ p ( z ) log ⁡ ( 1 − D θ d ( G θ g ( z ) ) ) ] \min {\theta{g}} \max {\theta{d}}\left[\mathbb{E}{x \sim p{\text {data }}} \log D_{\theta_{d}}(x)+\mathbb{E}{z \sim p(z)} \log \left(1-D{\theta_{d}}\left(G_{\theta_{g}}(z)\right)\right)\right] θg​min​θd​max​[Ex∼pdata ​​logDθd​​(x)+Ez∼p(z)​log(1−Dθd​​(Gθg​​(z)))]

生成对抗网络; 理解 GAN 如何工作

现在我们有两个玩家,通过一个 min ⁡ max ⁡ \min \max minmax 博弈公式联合训练这两个网络,该 min ⁡ max ⁡ \min \max minmax 目标函数就是如图所示的公式,我们的目标是:

  • 让目标函数在 θ g \theta_ g θg​ 上取得最小值,同时要在 θ d \theta_ d θd​ 上取得最大值。
  • 其中: θ g \theta_g θg​ 是生成器网络 g 的参数, θ d \theta_d θd​指的是判别器网络的参数。

公式中各项的含义

  • 第 1 项是在训练数据的分布上 l o g ( D ( x ) ) log(D(x)) log(D(x)) 的期望, l o g ( D ( x ) ) log(D(x)) log(D(x)) 是判别器网络在输入为真实数据(训练数据)时的输出,该输出是真实数据从分布 p-data 中采样的似然概率;
  • 第 2 项是对 z z z 取期望, z z z 是从 p ( z ) p(z) p(z) 中采样获得的,这意味着从生成器网络中采样,同时 D ( G ( z ) ) D(G(z)) D(G(z)) 这一项代表了以生成的伪数据为输入判别器网路的输出,也就是判别器网络对于生成网络生成的数据给出的判定结果。

对该过程的解释:我们的判别器的目的是最大化目标函数也就是在 θ d \theta_d θd​ 上取最大值,这样一来 D ( x ) D(x) D(x) 就会接近 1,也就是使判别结果接近真,因而该值对于真实数据应该相当高,这样一来 D ( G ( z ) ) D(G(z)) D(G(z)) 的值也就是判别器对伪造数据输出就会相应减小,我们希望这一值接近于 0 0 0。

如果我们能最大化这一结果,就意味着判别器能够很好的区别真实数据和伪造数据。

对于生成器来说,我们希望它最小化该目标函数,也就是让 D ( G ( z ) ) D(G(z)) D(G(z)) 接近 1 1 1,如果 D ( G ( z ) ) D(G(z)) D(G(z)) 接近 1 1 1,那么用 1 1 1 减去它就会很小,判别器网络就会把伪造数据视为真实数据,也就意味着我们的生成器在生成真实样本。

从数据准备上看,整个过程是一个无监督学习,我们无需人工给每个图片打上标签。具体网络学习时候,我们会把生成器生成的图片标记为 0 0 0(对应假图片),训练集标记为 1 1 1(都是真图片)。

判别器的损失函数会使用上述信息,判别器是一个分类器,我们希望它能经过训练获得分辨能力:对生成器生成的图片输出 0 0 0,而对真实图片输出 1 1 1。

训练方法

对于 GAN,我们最初能想到的训练方式如下:

① 对判别器进行梯度上升,学习到 θ d \theta_d θd​ 来最大化该目标函数;

max ⁡ θ d [ E x ∼ p data  log ⁡ D θ d ( x ) + E z ∼ p ( z ) log ⁡ ( 1 − D θ d ( G θ g ( z ) ) ) ] \max {\theta{d}}\left[\mathbb{E}{x \sim p{\text {data }}} \log D_{\theta_{d}}(x)+\mathbb{E}{z \sim p(z)} \log \left(1-D{\theta_{d}}\left(G_{\theta_{g}}(z)\right)\right)\right] θd​max​[Ex∼pdata ​​logDθd​​(x)+Ez∼p(z)​log(1−Dθd​​(Gθg​​(z)))]

② 对生成器进行梯度下降, θ g \theta_g θg​ 进行梯度下降最小化目标函数(此时目标函数如下的部分,因为只有它与 θ g \theta_g θg​ 有关)

min ⁡ θ g E z ∼ p ( z ) log ⁡ ( 1 − D θ d ( G θ g ( z ) ) ) \min {\theta{g}} \mathbb{E}{z \sim p(z)} \log \left(1-D{\theta_{d}}\left(G_{\theta_{g}}(z)\right)\right) θg​min​Ez∼p(z)​log(1−Dθd​​(Gθg​​(z)))

不断在上述 ① 和 ② 之间重复。

这里有个 trick:我们观察生成器的损失函数形状如下:

生成对抗网络; 理解 GAN 如何工作

发现当生成器效果不好( D ( G ( z ) D(G(z) D(G(z) 接近 0 0 0)时,梯度非常平缓;当生成器效果好( D ( G ( z ) D(G(z) D(G(z)接近 1 1 1)时,梯度很陡峭。这就与我们期望的相反了,我们希望在生成器效果不好的时候梯度更陡峭,这样能学到更多。因此我们使用下面的目标函数来替代原来的生成器损失:

max ⁡ θ g E z ∼ p ( z ) log ⁡ ( D θ d ( G θ g ( z ) ) ) \max {\theta{g}} \mathbb{E}{z \sim p(z)} \log \left(D{\theta_{d}}\left(G_{\theta_{g}}(z)\right)\right) θg​max​Ez∼p(z)​log(Dθd​​(Gθg​​(z)))

这样就使得在生成器效果不好时具有较大的梯度。此外,联合训练两个网络很有挑战,交替训练的方式不可能一次训练两个网络,还有损失函数的函数空间会影响训练的动态过程。

在每一个训练迭代期都先训练判别器网络,然后训练生成器网络,GAN 的总体训练过程如下:

  • 训练判别器

    • 对于判别器网络的 k 个训练步,先从噪声先验分布 z z z 中采样得到一个小批量样本,接着从训练数据 x x x 中采样获得小批量的真实样本,下面要做的将噪声样本传给生成器网络,并在生成器的输出端获得生成的图像。
  • 此时我们有了一个小批量伪造图像和小批量真实图像,我们有这些小批量数据在判别器生进行一次梯度计算,接下来利用梯度信息更新判别器参数,按照以上步骤迭代几次来训练判别器。

  • 训练生成器

    • 在这一步采样获得一个小批量噪声样本,将它传入生成器,对生成器进行反向传播,来优化目标函数。

训练 GAN 的过程会交替进行上述两个步骤。

生成对抗网络; 理解 GAN 如何工作

训练完毕后,就可以用生成器来生成比较逼真的样本了。

2) GAN 的探索

生成对抗网络; GAN 的探索

  • 传统的 GAN 生成的样本还不是很好,这篇论文在 GAN 中使用了 CNN 架构,取得了惊艳的生成效果:[Radford et al, “Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks”, ICLR 2016]
  • Wasserstein GAN 一定程度解决了 GAN 训练中两个网络如何平衡的问题。
  • 用 GAN 来做 text -> image

3) GAN 的优缺点以及热门研究方向

GAN 的优点

  • GAN 通过一种博弈的方法来训练,通过两个玩家的博弈从训练数据的分布中学会生成数据。
  • GAN 可以生成目前最好的样本,还可以做很多其他的事情。

GAN 的缺点

  • GAN 没有显式的密度函数(它是利用样本来隐式表达该函数)
  • GAN 不好训练且不稳定,我们并不是直接优化目标函数,我们要努力地平衡两个网络。

GAN 的热门研究方法

  • 更好的损失函数设计,更稳定的训练方式(例如 Wasserstein GAN, LSGAN 及其他)
  • 条件 GAN,GAN 的各种应用领域探索

3.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=13

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

4.要点总结

本篇讲了三种目前最常用生成模型:

  • PixelCNN 和 PixelRNN 他们是显式密度模型,该模型优化的是一个显式的似然函数并产生良好的样本,但是效率很低,它是一个顺序的生成过程。
  • VAE 优化的是一个似然函数的下界,它会产生一个有用的隐式表征,可以用它来进行查询推断,生成的样本也不是特别好。
  • GAN 是目前能生成最好样本的模型,但是训练需要技巧且不稳定,查询推断上也有一些问题。
  • 还有一些将模型的优点结合起来做的研究。

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(17) | 深度强化学习 (马尔可夫决策过程,Q-Learning,DQN)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125248774

ShowMeAI 研究中心


Reinforcement Learning; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

监督学习(Supervised Learning)和无监督学习(Unsupervised Learning)之外,我们还有另外一类机器学习算法,叫做「强化学习」。

  • 监督学习:从外部监督者提供的带标注训练集中进行学习(任务驱动型)
  • 无监督学习:寻找未标注数据中隐含结构的过程(数据驱动型)。
  • 强化学习:「试探」与「开发」之间的折中权衡,智能体必须开发已有的经验来获取收益,同时也要进行试探,使得未来可以获得更好的动作选择空间(即从错误中学习)。强化学习也可以理解为有延迟标签(奖励)的学习方式。

目前强化学习在包括游戏广告和推荐对话系统机器人等多个领域有着非常广泛的应用。我们会在下面再展开介绍。

本篇重点

  • 强化学习概念与应用
  • 马尔可夫决策过程
  • Q-Learning 算法
  • DQN(Deep Q Network)算法

1.强化学习介绍与应用

1.1 强化学习介绍

强化学习是一类对目标导向的学习与决策问题进行理解和自动化处理的算法。它强调智能体通过与环境的直接互动来学习,无需像监督学习一样密集的样本级标签标注,通过奖励来学习合理的策略。

强化学习包含 2 个可以进行交互的对象:智能体(Agnet)环境(Environment) ,它们的定义与介绍如下:

  • 智能体(Agent) :可以感知环境的状态(State) ,并根据反馈的奖励(Reward) 学习选择一个合适的动作(Action) ,我们希望它能最大化长期总收益。
  • 环境(Environment) :环境会接收智能体执行的一系列动作,对这一系列动作进行评价并转换为一种可量化的信号反馈给智能体。环境对智能体来说是一套相对固定的规则。

强化学习系统; Agnet, Environment;

强化学习系统在智能体(Agnet)环境(Environment) 之外,还包含其他核心要素:策略(Policy)回报函数(Reward Function)价值函数(Value Function)环境模型(Environment Model)

上述核心要素中「环境模型」是可选的。

  • 策略(Policy) :智能体在环境中特定时间的行为方式,策略可以视作环境状态到动作的映射。
  • 回报函数(Reward Function) :智能体在环境中的每一步,环境向其发送的 1 个标量数值,指代「收益」。
  • 价值函数(Value Function) :表示了从长远的角度看什么是好的。一个状态的价值是一个智能体从这个状态开始,对将来累积的总收益的期望。
  • 环境模型(Environment Model) :是一种对环境的反应模式的模拟,它允许对外部环境的行为进行推断。

1.2 强化学习应用

1) 游戏

AlphaGo 是于 2014 年开始由 Google DeepMind 开发的人工智能围棋软件。AlphaGo 在围棋比赛中战胜人类顶级选手取得成功,它包含了大量强化学习的算法应用,核心过程是使用蒙特卡洛树搜索(Monte Carlo tree search),借助估值网络(value network)与走棋网络(policy network)这两种深度神经网络,通过估值网络来评估大量选点,并通过走棋网络选择落点。

强化学习应用; 游戏 AlphaGo

AlphaStar 是由 DeepMind 开发的玩 星际争霸 II 游戏的人工智能程序。它能够通过模仿学习星际争霸上玩家所使用的基本微观和宏观策略。这个初级智能体在 95% 的游戏中击败了内置的「精英」AI 关卡(相当于人类玩家的黄金级别)。

AlphaStar 神经网络结构将 Transformer 框架运用于模型单元(类似于关系深度强化学习),结合一个深度 LSTM 核心、一个带有 pointer network 的自回归策略前端和一个集中的值基线。超强的网络设计使得其适合长期序列建模和大输出空间(如翻译、语言建模和视觉表示)的挑战。它还还集成了多智能体学习算法。

强化学习应用; 游戏 AlphaStar

OpenAI Five 是一个由 OpenAI 开发的用于多人视频游戏 Dota 2 的人工智能程序。OpenAI Five 通过与自己进行超过 10,000 年时长的游戏进行优化学习,最终获得了专家级别的表现。

强化学习应用; 游戏 OpenAI Five

Pluribus 是由 Facebook 开发的第一个在六人无限注德州扑克中击败人类专家的 AI 智能程序,其首次在复杂游戏中击败两个人或两个团队。

强化学习应用; 游戏 Pluribus

2) 广告和推荐

在淘宝京东等电商领域与字节跳动等信息流产品里,也可以见到强化学习的有效应用,它使得广告投放与推荐更具智能化。

强化学习应用; 广告和推荐

3) 对话系统

Alphago 的成功使人们看到了强化学习在序列决策上的巨大进步,这些进步进而推动了深度强化学习算法在自动语音和自然语言理解领域的研究和应用,探索解决自然语言理解及响应等开展对话中存在的挑战。基于深度强化学习的 Bot 具有扩展到当前尚无法涉足领域的能力,适用于开放域聊天机器人的场景。

强化学习应用; 推荐系统

4) 机器人

复杂未知环境下智能感知与自动控制是目前机器人在控制领域的研究热点之一,在高维连续状态-动作空间中,运用深度强化学习进行机器人运动控制有很不错的效果,最终可以应用在自主导航、物体抓取、步态控制、人机协作以及群体协同等很多任务上。

强化学习应用; 机器人

2.从游戏说起强化学习

首先,让我们简单介绍一下 Breakout 这个游戏。在这个游戏中,你需要控制屏幕底端的一根横杆左右移动,将飞向底端的球反弹回去并清除屏幕上方的砖块。每次你击中并清除了砖块,你的分数都会增加——你获得了奖励。

强化学习; Atari Breakout 游戏

假设你想教神经网络模型玩这个游戏,模型的输入是游戏机屏幕像素所构成的图片,输出是三种动作:向左,向右以及开火(把球射出)。把这个问题建模为分类问题是很合理的——对于每个屏幕画面,你都需要进行决策:是左移,右移,还是开火。

分类的建模方法看起来很直接。不过,你需要大量训练数据训练你的分类模型。传统的做法是找一些专家让他们玩游戏并且记录他们的游戏过程。

但人类肯定不是这样玩游戏的,我们不需要有人站在我们背后告诉我们向左还是向右。我们只需要知道在某些情况下我们做了正确的动作并且得分了,其他的依靠我们自身的学习机制完成。这个问题就是强化学习尝试解决的问题。

强化学习处于监督学习与无监督学习的中间地带。在监督学习中,每个训练实例都有一个正确标签;在无监督学习中,训练实例并没有标签。在强化学习中,训练实例有稀疏并延时的标签——奖励。基于奖励,强化学习中的智能体可以学习如何对环境做出正确的反映。

上述的观点看起来很直观,但是实际存在很多挑战。举例来讲,在 Breakout 这个游戏中,击中砖块并且得分和前一时刻如何移动横杆没有直接关系。最相关的是前面如何将横杆移动到正确位置并反弹球。这个问题叫做信用分配问题(credit assignment problem),即:建模获得奖励之前的哪些动作对获得奖励产生贡献以及贡献的大小

如果你已经获得了某种策略并且通过它得了不少奖励,你应该继续坚持这种策略还是试试其他的可能获得更高分的策略?仍举 Breakout 这个游戏为例,在游戏开始时,你把横杆停在左边并把球射出去,如果你不移动横杆,你总是能得 10 分的(当然得分的下一秒,你就死了)。你满足于这个得分吗,或者你想不想再多得几分?这种现象有一个专门的名词——探索-利用困境(exploration-exploitation dilemma) 。决策时应该一直延用现有的策略还是试试其他更好的策略?

强化学习是人类(或者更一般的讲,动物)学习的一种重要模式。父母的鼓励,课程的分数,工作的薪水——这些都是我们生活中的奖励。功劳分配问题以及探索-利用困境在我们日常生活工作中经常发生。这就是我们研究强化学习的原因。而对于强化学习,游戏是尝试新方法的最佳的沙盒。

3. 马尔科夫决策过程

下面,我们的问题是如何形式化定义强化学习问题使其支持推断。最常用的表示方式是马尔科夫决策过程。

假想你是一个智能体(agent),面对某种场景(比如说 Breakout 游戏)。你所处的环境可以定义为状态(state)(比如横杆的位置,球的位置,球的方向,当前环境中的砖块等等)。

智能体能够在环境中采取一些动作(actions)(比如向左或向右移动横杆)。这些动作会导致一些奖励(reward)(比如分数的增加)。智能体采取动作将导致新的环境,也就是新的状态。在新的状态下,智能体能够继续采取动作,循环往复。你采取行动的原则叫做策略(policy)。

通常来讲,环境是很复杂的,智能体的下一状态可能带有一定的随机性(比如当你失去一个球发射另一个球时,它的方向是随机的)。

马尔可夫决策过程; Markov decision process

一系列的状态、动作、以及采取动作的规则构成了一个马尔科夫决策过程(Markov decision process)。一个马尔科夫决策过程(比如一局游戏)由一串有限个数的状态、动作、反馈组成,形式化地表示为:

s 0 , a 0 , r 1 , s 1 , a 1 , r 2 , s 2 , … , s n − 1 , a n − 1 , r n , s n s_0, a_0, r_1, s_1, a_1, r_2, s_2, …, s_{n-1}, a_{n-1}, r_n, s_n s0​,a0​,r1​,s1​,a1​,r2​,s2​,…,sn−1​,an−1​,rn​,sn​

其中 s i s_i si​ 代表状态, a i a_i ai​ 代表动作, r i + 1 r_{i+1} ri+1​ 代表进行动作后获得的奖励, s n s_n sn​ 是终止状态。一个马尔科夫决策过程建立在马尔科夫假设上,即下一时刻的状态 s i + 1 s_{i+1} si+1​ 只和当前状态 s i s_i si​ 和动作 a i a_i ai​ 有关,和之前的状态及动作无关。

4. 打折的未来奖励

为了在长期决策过程中表现的更好,我们不但要考虑采取一个动作后的即时奖励,也要考虑这个动作的未来奖励。那么问题来了,我们应该如何建模这个未来奖励?

给定一个马尔科夫决策过程,它对应的奖励总和很容易用如下方式计算:

R = r 1 + r 2 + r 3 + … + r n R=r_1+r_2+r_3+…+r_n R=r1​+r2​+r3​+…+rn​

而 t t t 时刻的未来奖励可以表示为:

R t = r t + r t + 1 + r t + 2 + … + r n R_t=r_t+r_{t+1}+r_{t+2}+…+r_n Rt​=rt​+rt+1​+rt+2​+…+rn​

由于智能体所处的环境非常复杂,我们甚至无法确定在两次采取相同动作,智能体能够获得相同的奖励。智能体在未来进行的动作越多,获得的奖励越不相同。所以,我们一般采用一种「打折的未来奖励」作为 t t t 时刻未来奖励的代替。

R t = r t + γ r t + 1 + γ 2 r t + 2 … + γ n − t r n R_t=r_t+\gamma r_{t+1}+\gamma² r_{t+2}…+\gamma^{n-t} r_n Rt​=rt​+γrt+1​+γ2rt+2​…+γn−trn​

其中 γ \gamma γ 是 0 0 0 到 1 1 1 之间的折扣因子。这个 γ \gamma γ 值使得我们更少地考虑哪些更长远未来的奖励。数学直觉好的读者可以很快地看出 R t R_t Rt​ 可以用 R t + 1 R_{t+1} Rt+1​ 来表示,从而将上式写成一种递推的形式,即:

R t = r t + γ ( r t + 1 + γ ( r t + 2 + … ) ) = r t + γ R t + 1 R_t=r_t+\gamma (r_{t+1}+\gamma (r_{t+2}+…))=r_t+\gamma R_{t+1} Rt​=rt​+γ(rt+1​+γ(rt+2​+…))=rt​+γRt+1​

如果 γ \gamma γ 是 0 0 0,我们将会采取一种短视的策略。也就是说,我们只考虑即刻奖励。如果我们希望在即刻奖励与未来奖励之间寻求一种平衡,我们应该使用像 0.9 0.9 0.9 这样的参数。如果我们所处的环境对于动作的奖励是固定不变的,也就是说相同的动作总会导致相同的奖励,那么 γ \gamma γ 应该等于 1 1 1。

好的策略应该是:智能体在各种环境下采用最大(打折的)未来奖励的策略。

5. Q-learning 算法

5.1 Q-Learning 算法讲解

在 Q-learning 中,我们定义一个 Q ( s , a ) Q(s, a) Q(s,a) 函数,用来表示智能体在 s s s 状态下采用 a a a 动作并在之后采取最优动作条件下的打折的未来奖励。

Q ( s t , a t ) = m a x π R t + 1 Q(s_t,a_t)=max_{\pi} R_{t+1} Q(st​,at​)=maxπ​Rt+1​

直观讲, Q ( s , a ) Q(s, a) Q(s,a) 是智能体「在 s s s 状态下采取 a a a 动作所能获得的最好的未来奖励」。由于这个函数反映了在 s s s 状态下采取 a a a 动作的质量(Quality),我们称之为 Q-函数。

这个定义看起来特别奇怪。我们怎么可能在游戏进行的过程中,只凭一个状态和动作估计出游戏结束时的分数呢?实际上我们并不能估计出这个分数。但我们可以从理论上假设这样函数的存在,并且把重要的事情说三遍,「Q-函数存在,Q-函数存在,Q-函数存在」。Q-函数是否存在呢?

如果你还不相信,我们可以在假设 Q-函数存在的情况下想一想会发生什么。假设智能体处在某个状态并且思考到底应该采用 a a a 动作还是 b b b 动作,你当然希望选取使游戏结束时分数最大的动作。如果你有那个神奇的 Q-函数,你只要选取 Q-函数值最大的动作。

π ( s ) = a r g m a x a Q ( s , a ) \pi(s) =argmax_a Q(s,a) π(s)=argmaxa​Q(s,a)

上式中, π \pi π 表示在 s s s 状态下选取动作的策略。

然后,我们应该如何获得 Q-函数呢?首先让我们考虑一个转移 <s, a, r, s’>。我们可以采用与打折的未来奖励相同的方式定义这一状态下的 Q 函数。

Q ( s , a ) = r + γ m a x a ’ Q ( s ’ , a ’ ) Q(s,a)=r + \gamma max_{a’}Q(s’,a’) Q(s,a)=r+γmaxa’​Q(s’,a’)

这个公式叫贝尔曼公式。如果你再想一想,这个公式实际非常合理。对于某个状态来讲,最大化未来奖励相当于最大化即刻奖励与下一状态最大未来奖励之和。

Q-learning 的核心思想是:我们能够通过贝尔曼公式迭代地近似 Q-函数。最简单的情况下,我们可以采用一种填表的方式学习 Q-函数。这个表包含状态空间大小的行,以及动作个数大小的列。填表的算法伪码如下所示:

initialize Q[numstates,numactions] arbitrarily
observe initial state s
repeat
    select and carry out an action a
    observe reward r and new state s'
    Q[s,a] = Q[s,a] + α(r + γmaxa' Q[s',a'] - Q[s,a])
    s = s'
until terminated 

Al gorithm 5 Q-learning 迭代算法

Algorithm 5 Q-learning; 迭代算法

其中 α \alpha α 是在更新 Q [ s , a ] Q[s, a] Q[s,a] 时,调节旧 Q [ s , a ] Q[s, a] Q[s,a] 与新 Q [ s , a ] Q[s, a] Q[s,a] 比例的学习速率。如果 α = 1 \alpha=1 α=1, Q [ s , a ] Q[s, a] Q[s,a] 就被消掉,而更新方式就完全与贝尔曼公式相同。

使用 m a x a ’ Q [ s ’ , a ’ ] max_{a’}Q[s’, a’] maxa’​Q[s’,a’] 作为未来奖励来更新 Q [ s , a ] Q[s, a] Q[s,a] 只是一种近似。在算法运行的初期,这个未来奖励可能是完全错误的。但是随着算法迭代, Q [ s , a ] Q[s, a] Q[s,a] 会越来越准(它的收敛性已经被证明)。我们只要不断迭代,终有一天它会收敛到真实的 Q 函数的。

5.2 Q-Learning 案例

我们来通过一个小案例理解一下 Q-Learning 算法是如何应用的。

1) 环境

假设我们有 5 个相互连接的房间,并且对每个房间编号,整个房间的外部视作房间 5。
Q-Learning 案例; 环境

以房间为节点,房门为边,则可以用图来描述房间之间的关系:

Q-Learning 案例; 环境

2) 奖励机制

这里设置一个 agent(在强化学习中, agent 意味着与环境交互、做出决策的智能体), 初始可以放置在任意一个房间, agent 最终的目标是走到房间 5(外部)。

为此, 为每扇门设置一个 reward(奖励), 一旦 agent 通过该门, 就能获得奖励:

Q-Learning 案例; 奖励机制

其中一个特别的地方是房间 5 可以循环回到自身节点, 并且同样有 100 点奖励。

在 Q-learning 中, agent 的目标是达成最高的奖励值, 如果 agent 到达目标, 就会一直停留在原地, 这称之为 absorbing goal。

对于 agent, 这是 i 一个可以通过经验进行学习的 robot, agent 可以从一个房间(节点)通过门(边)通往另一个房间(节点), 但是它不知道门会连接到哪个房间, 更不知道哪扇门能进入房间 5(外部)。

3) 学习过程

举个栗子,现在我们在房间 2 设置一个 agent,我们想让它学习如何走能走向房间 5。

Q-Learning 案例; 学习过程

在 Q-leanring 中,有两个术语 state(状态)和 action(行为)。

每个房间可以称之为 state,而通过门进行移动的行为称之为 action,在图中 state 代表节点,action 代表边。

现在代理处于 state2 ( 节点 2,房间 2),从 state2 可以通往 state3 ,但是无法直接通往 state1

state3 ,可以移动到 state1 或回到 state2

Q-Learning 案例; 学习过程

根据现在掌握的 state,reward,可以形成一张 reward table(奖励表),称之为矩阵 R R R:

Q-Learning 案例; 学习过程

只有矩阵 R R R 是不够的, agent 还需要一张表, 被称之为矩阵 Q Q Q , 矩阵 Q Q Q 表示 agent 通过经验学习到的记忆(可以理解为矩阵 Q Q Q 就是模型通过学习得到的权重)。

起初, 代理对环境一无所知, 因此矩阵 Q Q Q 初始化为 0 0 0。为了简单起见, 假设状态数已知为 6。如果状态数未知, 则 Q Q Q 仅初始化为单个元素 0 0 0, 每当发现新的状态就在矩阵中添加更多的行列。

Q-learning 的状态转移公式如下:

Q ( s t a t e , a c t i o n ) = R ( s t a t e , a c t i o n ) + G a m m a ∗ M a x [ Q ( n e x t − s t a t e , a l l a c t i o n s ) ] Q(state, action) = R(state, action) + Gamma \ast Max[Q(next-state, all actions)] Q(state,action)=R(state,action)+Gamma∗Max[Q(next−state,allactions)]

根据该公式, 可以对矩阵 Q Q Q 中的特定元素赋值。

agent 在没有老师的情况下通过经验进行学习(无监督), 从一个状态探索到另一个状态, 直到到达目标为止。每次完整的学习称之为 episode(其实也就相当于 epoch), 每个 episode 包括从初始状态移动到目标状态的过程, 一旦到达目标状态就可以进入下一个 episode。

4) 算法过程

设置 gamme 参数, 在矩阵 R 中设置 reward
初始化矩阵 Q
对于每一个 episode:
    选择随机的初始状态(随便放到一个房间里)
    如果目标状态没有达成, 则
        从当前所有可能的 action 中选择一个
        执行 action, 并准备进入下一个 state
        根据 action 得到 reward
        计算$Q(state, action) = R(state, action) + Gamma * Max[Q(next-state, all actions)]$
        将下一个 state 设置为当前的 state.
        进入下一个 state
结束 

在算法中, 训练 agent 在每一个 episode 中探索环境(矩阵 R R R ), 并获得 reward, 直到达到目标状态。训练的目的是不断更新 agent 的矩阵 Q Q Q :每个 episode 都在对矩阵 Q Q Q 进行优化。因此, 起初随意性的探索就会被通往目标状态的最快路径取代。

参数 gamma 取 0 0 0 到 1 1 1 之间。该参数主要体现在 agent 对于 reward 的贪心程度上, 具体的说, 如果 gamma 为 0 0 0, 那么 agent 仅会考虑立即能被得到的 reward, 而 gamma 为 1 1 1 时, agent 会放长眼光, 考虑将来的延迟奖励。

要使用矩阵 Q Q Q , agent 只需要查询矩阵 Q Q Q 中当前 state 具有最高 Q Q Q 值的 action:

  • ① 设置当前 state 为初始 state
  • ② 从当前 state 查询具有最高 Q Q Q 值的 action
  • ③ 设置当前 state 为执行 action 后的 state
  • ④ 重复 2,3 直到当前 state 为目标 state

5) Q-learning 模拟

现在假设 g a m m e = 0.8 gamme=0.8 gamme=0.8,初始 state 为房间 1 1 1。

初始化矩阵 Q Q Q :

Q-Learning 案例; 模拟

同时有矩阵 R R R :

Q-Learning 案例; 模拟

① episode 1

现在 agent 处于房间 1, 那么就检查矩阵 R R R 的第二行。agent 面临两个 action, 一个通往房间 3, 一个通往房间 5。通过随机选择, 假设 agent 选择了房间 5。

Q ( s t a t e , a c t i o n ) = R ( s t a t e , a c t i o n ) + G a m m a ∗ M a x [ Q ( n e x t s t a t e , a l l a c t i o n s ) ] Q(state, action) = R(state, action) + Gamma \ast Max[Q(next state, all actions)] Q(state,action)=R(state,action)+Gamma∗Max[Q(nextstate,allactions)]

Q ( 1 , 5 ) = R ( 1 , 5 ) + 0.8 ∗ M a x [ Q ( 5 , 1 ) , Q ( 5 , 4 ) , Q ( 5 , 5 ) ] = 100 + 0.8 ∗ 0 = 100 Q(1, 5) = R(1, 5) + 0.8 \ast Max[Q(5, 1), Q(5, 4), Q(5, 5)] = 100 + 0.8 \ast 0 = 100 Q(1,5)=R(1,5)+0.8∗Max[Q(5,1),Q(5,4),Q(5,5)]=100+0.8∗0=100

由于矩阵 Q Q Q 被初始化为 0 0 0, 因此 Q ( 5 , 1 ) Q(5,1) Q(5,1), Q ( 5 , 4 ) Q(5, 4) Q(5,4), Q ( 5 , 5 ) Q(5, 5) Q(5,5) 都是 0 0 0, 那么 Q ( 1 , 5 ) Q(1, 5) Q(1,5) 就是 100 100 100。

现在房间 5 变成了当前 state, 并且到达目标 state, 因此这一轮 episode 结束。

于是 agent 对矩阵 Q Q Q 进行更新。

Q-Learning 案例; 模拟 - episode 1

② episode 2

现在, 从新的随机 state 开始, 假设房间 3 为初始 state。

同样地, 查看矩阵 R R R 的第四行, 有 3 种可能的 action:进入房间 1、2 或者 4。通过随机选择, 进入房间 1 1 1。计算 Q Q Q 值:

Q ( s t a t e , a c t i o n ) = R ( s t a t e , a c t i o n ) + G a m m a ∗ M a x [ Q ( n e x t s t a t e , a l l a c t i o n s ) ] Q(state, action) = R(state, action) + Gamma \ast Max[Q(next state, all actions)] Q(state,action)=R(state,action)+Gamma∗Max[Q(nextstate,allactions)]

Q ( 3 , 1 ) = R ( 3 , 1 ) + 0.8 ∗ M a x [ Q ( 1 , 3 ) , Q ( 1 , 5 ) ] = 0 + 0.8 ∗ 100 = 80 Q(3, 1) = R(3, 1) + 0.8 \ast Max[Q(1, 3), Q(1, 5)] = 0+ 0.8 \ast 100 = 80 Q(3,1)=R(3,1)+0.8∗Max[Q(1,3),Q(1,5)]=0+0.8∗100=80

现在 agent 处于房间 1 1 1, 查看矩阵 R R R 的第二行。此时可以进入房间 3 或房间 5, 选择去 5, 计算 Q 值:

Q ( s t a t e , a c t i o n ) = R ( s t a t e , a c t i o n ) + G a m m a ∗ M a x [ Q ( n e x t s t a t e , a l l a c t i o n s ) ] Q(state, action) = R(state, action) + Gamma \ast Max[Q(next state, all actions)] Q(state,action)=R(state,action)+Gamma∗Max[Q(nextstate,allactions)]

Q ( 1 , 5 ) = R ( 1 , 5 ) + 0.8 ∗ M a x [ Q ( 5 , 1 ) , Q ( 5 , 4 ) , Q ( 5 , 5 ) ] = 100 + 0.8 ∗ 0 = 100 Q(1, 5) = R(1, 5) + 0.8 \ast Max[Q(5, 1), Q(5, 4), Q(5, 5)] = 100 + 0.8 \ast 0 = 100 Q(1,5)=R(1,5)+0.8∗Max[Q(5,1),Q(5,4),Q(5,5)]=100+0.8∗0=100

由于到达目标 state, 对矩阵 Q Q Q 进行更新, Q ( 3 , 1 ) = 80 Q(3, 1)=80 Q(3,1)=80, Q ( 1 , 5 ) = 100 Q(1, 5)=100 Q(1,5)=100。

Q-Learning 案例; 模拟 - episode 2

③ episode n

之后就是不断重复上面的过程,更新 Q Q Q 表,直到结束为止。

6) 推理

假设现在 Q Q Q 表被更新为:

Q-Learning 案例; 推理

对数据标准化处理( m a t r i x Q / m a x ( m a t r i x Q ) matrix_Q / max(matrix_Q) matrixQ​/max(matrixQ​) ),可以将 Q Q Q 值看作概率:

Q-Learning 案例; 推理

描绘成图:

Q-Learning 案例; 推理

到这里已经很清晰了,agent 已经总结出一条从任意房间通往房间 5(外部)的路径。

6.Deep Q Network 算法

6.1 算法介绍

Breakout 游戏的状态可以用横杆的位置,球的位置,球的方向或者每个砖块是否存在来进行定义。然而,这些表示游戏状态的直觉只能在一款游戏上发挥作用。我们要问:我们能不能设计一种通用的表示游戏状态的方法呢?最直接的方法是使用游戏机屏幕的像素作为游戏状态的表示。像素可以隐式地表示出球速以及球的方向之外的所有游戏状态的信息。而如果采用连续两个游戏机屏幕的像素,球速和球的方向也可以得到表示。

如果 DeepMind 的论文采用离散的游戏屏幕作为状态表示,让我们计算一下使用连续四步的屏幕作为状态,可能的状态空间。屏幕大小 84 ∗ 84 84 \ast 84 84∗84,每个像素点有 256 256 256 个灰度值,那么就有 25 6 84 ∗ 84 ∗ 4 ∼ 1 0 67970 256^{84 \ast 84 \ast 4} \sim10^{67970} 25684∗84∗4∼1067970 种可能的状态。也就是说,我们的 Q-表将要有 1 0 67970 10^{67970} 1067970 行。这个数字甚至多于宇宙中的原子个数!有人会说:有些屏幕状态永远不会出现,或者,能不能只用在学习过程中碰到过的状态作为 Q-表的状态呢?即便如此,这种稀疏表示的 Q-表里仍包含很多状态,并且需要很长时间收敛。理想的情况是,即便某些状态没有见过,我们的模型也能对它进行比较合理的估计。

在这种情况下,深度学习就进入了我们的视野。深度学习的一大优势是从结构数据中抽取特征(对数据进行很好的表示)。我们可以用一个神经网络对 Q-函数进行建模。这个神经网络接收一个状态(连续四步的屏幕)和一个动作,然后输出对应的 Q-函数的值。当然,这个网络也可以只接受一个状态作为输入,然后输出所有动作的分数(具体来讲是动作个数大小的向量)。这种做法有一个好处:我们只需要做一次前向过程就可以获得所有动作的分数。

朴素的 Q-函数网络; 在 DeepMind 论文中使用优化的 Q 网络

DeepMind 在论文中使用的网络结构如下:

Layer Input Filter size Stride Num filters Activation Output
conv1 84x84x4 8×8 4 32 ReLU 20x20x32
conv2 20x20x32 4×4 2 64 ReLU 9x9x64
conv3 9x9x64 3×3 1 64 ReLU 7x7x64
fc4 7x7x64 512 ReLU 512
fc5 512 18 Linear 18

这个网络是普通的神经网络:从输入开始,三个卷积层,接着两个全连接层。熟悉使用神经网络做物体识别的读者或许会意识到,这个网络没有池化层(pooling layer)。但是细想一下我们就知道,池化层带来位置不变性,会使我们的网络对于物体的位置不敏感,从而无法有效地识别游戏中球的位置。而我们都知道,球的位置对于决定游戏潜在的奖励来讲有非常大的意义,我们不应该丢掉这部分信息。

输入是 84 ∗ 84 84 \ast 84 84∗84 的灰度图片,输出的是每种可能的动作的 Q-值。这个神经网络解决的问题变成了一个典型的回归问题。简单的平方误差可以用作学习目标。

L = 1 2 [ r + γ m a x a ′ Q ( s ′ , a ′ ) ⏟ target − Q ( s , a ) ⏟ prediction ] 2 L=\frac{1}{2}[\underbrace{r + \gamma max_{a'}Q(s',a')}{\text{target}} - \underbrace{Q(s,a)}{\text{prediction}}]² L=21​[target r+γmaxa′​Q(s′,a′)​​−prediction Q(s,a)​​]2

给定一个转移<s, a, r, s’>,Q-表的更新算法只要替换成如下流程就可以学习 Q-网络。

  • 对于当前状态 s,通过前向过程获得所有动作的 Q-值
  • 对于下一个状态s’,通过前向过程计算 Q-值最大的动作 m a x a ’ Q ( s ’ , a ’ ) max_{a’} Q(s’, a’) maxa’​Q(s’,a’)
  • 将 r + γ m a x a ’ Q ( s ’ , a ’ ) r+\gamma max_{a’} Q(s’, a’) r+γmaxa’​Q(s’,a’) 作为学习目标,对于其他动作,设定第一步获得的 Q-值作为学习目标(也就是不会在反向过程中更新参数)
  • 使用反向传播算法更新参数。

6.2 经验回放

到现在,我们已经知道如何用 Q-learning 的算法估计未来奖励,并能够用一个卷积神经网络近似 Q-函数。但使用 Q 值近似非线性的 Q-函数可能非常不稳定。你需要很多小技巧才能使这个函数收敛。即使如此,在单 GPU 上也需要一个星期的时间训练模型。

这其中,最重要的技巧是经验回放(experience replay)。在玩游戏的过程中,所有经历的<s, a, r, s’>都被记录起来。当我们训练神经网络时,我们从这些记录的<s, a, r, s’>中随机选取一些 mini-batch 作为训练数据训练,而不是按照时序地选取一些连续的<s, a, r, s’>。在后一种做法中,训练实例之间相似性较大,网络很容易收敛到局部最小值。同时,经验回放也使 Q-learning 算法更像传统监督学习。我们可以收集一些人类玩家的记录,并从这些记录中学习。

6.3 探索-利用困境

Q-learning 算法尝试解决信用分配问题。通过 Q-learning ,奖励被回馈到关键的决策时刻。然而,我们还没有解决探索-利用困境。

我们第一个观察是:在游戏开始阶段,Q-表或 Q-网络是随机初始化的。它给出的 Q-值最高的动作是完全随机的,智能体表现出的是随机的「探索」。当 Q-函数收敛时,随机「探索」的情况减少。所以, Q-learning 中包含「探索」的成分。但是这种探索是「贪心」的,它只会探索当前模型认为的最好的策略。

对于这个问题,一个简单的修正技巧是使用 ϵ \epsilon ϵ- 贪心探索。在学习 Q-函数时,这种技巧以 ϵ \epsilon ϵ 的概率选取随机的动作做为下一步动作, 1 − ϵ 1-\epsilon 1−ϵ 的概率选取分数最高的动作。在 DeepMind 的系统中, ϵ \epsilon ϵ 随着时间从 1 1 1 减少到 0.1 0.1 0.1。这意味着开始时,系统完全随机地探索状态空间,最后以固定的概率探索。

6.4 Deep Q-learning 算法流程

最后给出使用经验回放的 Deep Q-learning 算法

initialize replay memory D
initialize action-value function Q with random weights
observe initial state s
repeat
    select an action a
        with probability ε select a random action
        otherwise select a = argmaxa’Q(s,a’)
    carry out action a
    observe reward r and new state s’
    store experience <s, a, r, s’> in replay memory D
    sample random transitions <ss, aa, rr, ss’> from replay memory D
    calculate target for each minibatch transition
        if ss’ is terminal state then tt = rr
        otherwise tt = rr + γmaxa’Q(ss’, aa’)
    train the Q network using (tt - Q(ss, aa))² as loss
    s = s'
until terminated 

中文版算法流程如下:

初始化回放存储 D
使用随机权重初始化动作价值函数 Q
观察初始状态 s
重复
    选择一个动作 s
        以概率ε选择一个随机动作
        否则选择 a=argmaxa'Q(s,a')
    执行动作 a
    观察奖励 r 和新状态 s'
    在回放存储 D 中保存经验<s,a,r,s′>
    从回放存储 D 中进行样本随机变换<ss,aa,rr,ss'>
    为每个微批数据变换计算目标
        如果 ss′是终点状态,那么 tt=rr
        否则 tt=rr+Ymax a' Q(ss',aa′)
    使用(tt-Q(ss,aa))2 作为损失训练 Q 网络 
    s=s' 

除了上述技巧,DeepMind 还使用了一系列其他的技巧,比如:目标网络、误差截断、回馈截断等等。但是这些已经超出本文的范畴了。

最令人惊喜的是这种算法可以应对各种学习问题。在算法的运行初期,Q-函数用来学习模型参数的数据几乎完全是(随机猜测的)垃圾数据,在运行过程中,也只能通过一些偶然的奖励学习参数。这种事情想想就很疯狂,它是怎么学到一个很好的模型呢?但事实上,它确实学到了一个很好的模型。

7.DQN 后续

自从 Deep Q-learning 提出之后,很多工作尝试对他进行提升,其中包括:Double Q-learning, Prioritized Experience Replay, Dueling Network Architecture, extension to continuous action space 等等。如果要跟进最新的研究成果,可以关注 NIPS 2015 deep reinforcement learning workshop 以及 ICLR 2016(用「reinforcement」作为关键词搜索)。有一点需要注意的是 Deep Q-learning 已经被谷歌申请专利了。

我们常说我们还没搞清楚什么是人工智能。一旦我们搞清其中的工作原理,它看起来就不那么智能。但是深度 Q-网络仍在不断地给我带来惊喜。观察 Q-learning 学习玩一个新游戏的过程就像观察野外的动物。通过不断地与环境交互获得奖励从而成为更强的物种。

8.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=14

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

9.要点总结

本篇介绍了强化学习:

  • 强化学习是不同于监督学习与无监督学习的另外一类算法,主要是「智能体」与「环境」交互学习。
  • 强化学习没去在「游戏」「广告与推荐」「智能对话」「机器人」等领域都有应用。
  • GAN 是目前能生成最好样本的模型,但是训练需要技巧且不稳定,查询推断上也有一些问题。
  • 马尔科夫决策过程
  • 打折未来奖励及其计算方式
  • Q-Learning 算法
  • Deep Q-Learning 算法

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

深度学习与计算机视觉教程(18) | 深度强化学习 (梯度策略,Actor-Critic,DDPG,A3C)(CV 通关指南·完结)

原文:blog.csdn.net/ShowMeAI/article/details/125249798

ShowMeAI 研究中心


Reinforcement Learning; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福 CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

前一篇 ShowMeAI 的文章 深度学习与计算机视觉教程(17) | 深度强化学习 (马尔可夫决策过程, Q-Learning, DQN) 中,我们学习了 Q-Learning 系列方法,它是基于价值(value-based)的方法, 也就是通过计算每一个状态动作的价值,然后选择价值最大的动作执行。

这是一种间接的强化学习建模做法,另外一类 policy-based 方法会直接更新策略。本篇我们将顺着这条主线给大家介绍 Policy Gradient 等方法,当然,强化学习中还有结合 policy-based 和 value-based 的 Actor-Critic 方法,以及在 Actor-Critic 基础上的 DDPG、A3C 方法。

本篇重点

  • Policy Gradient 算法
  • Actor-Critic 算法
  • DDPG 算法
  • A3C 算法

1.Policy Gradient 算法

1.1 算法介绍

Policy Gradient 是最基础的强化学习算法之一,它通过更新 策略网络/Policy Network 来直接更新策略的。Policy Network 是一个神经网络,输入是状态,输出直接就是动作(不是 Q 值) ,且一般输出有两种方式:

  • ① 概率的方式,即输出某一个动作的概率。
  • ② 确定性的方式,即输出具体的某一个动作。

如果要更新 Policy Network 策略网络,或者说要使用梯度下降的方法来更新网络,需要有一个目标函数,对于所有强化学习的任务来说,最终目标都是使所有累加 reward/奖励(带衰减)最大。如下公式所示:

L ( θ ) = E ( r 1 + γ r 2 + γ 2 r 3 + … ∣ π ( , θ ) ) L(\theta) = \mathbb E(r_1+\gamma r_2 + \gamma² r_3 + … \mid \pi(,\theta)) L(θ)=E(r1​+γr2​+γ2r3​+…∣π(,θ))

但上述损失函数和 Policy Network 策略网络无法直接关联:reward 是环境给出的,无法基于参数 θ \theta θ 计算得到。假如我们后续优化还是基于「梯度下降」等算法,那损失函数关于参数的梯度 ∇ θ L ( θ ) \nabla_{\theta} L(\theta) ∇θ​L(θ) 如何得到呢?

我们换一个思路来考虑:假如我们现在有一个策略网络,输入状态,输出动作的概率。然后执行完动作之后,我们可以得到 reward,或者 result。

我们可以采取 1 个非常直观简单的做法:增大得到 reward 多的动作的输出概率,减小得到 reward 少的动作的输出概率

但是大家可能会注意到:用 reward 或 result 来评判动作好坏可能是不准确的(因为任何一个 reward,result 都依赖于大量的动作才导致的,不能只将功劳或过错归于当前的动作上)。

我们的处理思路改为:我们构造一个判断动作好坏的「评判指标」,再通过改变动作的出现概率来优化策略!

假设这个评价指标是 f ( s , a ) f(s,a) f(s,a),我们的策略网络输出的 π ( a ∣ s , θ ) \pi(a|s,\theta) π(a∣s,θ) 是概率,那么可以通过极大似然估计的方法来优化这个目标。比如说我们可以构造如下目标函数:

L ( θ ) = ∑ l o g π ( a ∣ s , θ ) f ( s , a ) L(\theta) = \sum log\pi(a|s,\theta)f(s,a) L(θ)=∑logπ(a∣s,θ)f(s,a)

举例来说,对某场游戏来说,假如最终赢了,那么认为这局游戏中每一步都是好的,如果输了,那么认为都是不好的。好的 f ( s , a ) f(s,a) f(s,a) 就是 1 1 1,不好的就是 − 1 -1 −1,然后极大化上面的目标函数即可。

实际上,除了极大化上面的目标函数,还可以直接对 f ( s , a ) f(s,a) f(s,a) 进行极大化,如这篇博文 Deep Reinforcement Learning: Pong from Pixels 中直接最大化 f ( x ) f(x) f(x) 也就是 f ( s , a ) f(s, a) f(s,a) 的期望,可以看到,最后的结果跟上面的目标函数是一致的。

∇ θ E x [ f ( x ) ] = ∇ θ ∑ x p ( x ) f ( x )  定义期望  = ∑ x ∇ θ p ( x ) f ( x )  把「求和」挪至前方  = ∑ x p ( x ) ∇ θ p ( x ) p ( x ) f ( x )  公式变化,乘除  p ( x ) = ∑ x p ( x ) ∇ θ log ⁡ p ( x ) f ( x )  基于求导公式  ∇ θ log ⁡ ( z ) = 1 z ∇ θ z = E x [ f ( x ) ∇ θ log ⁡ p ( x ) ] 期望的定义 \begin{aligned} \nabla_{\theta} E_{x}[f(x)] & =\nabla_{\theta} \sum_{x} p(x) f(x) & \text { 定义期望 } \ & =\sum_{x} \nabla_{\theta} p(x) f(x) & \text { 把「求和」挪至前方 } \ & =\sum_{x} p(x) \frac{\nabla_{\theta} p(x)}{p(x)} f(x) & \text { 公式变化,乘除 } p(x) \ & =\sum_{x} p(x) \nabla_{\theta} \log p(x) f(x) & \text { 基于求导公式 } \nabla_{\theta} \log (z)=\frac{1}{z} \nabla_{\theta} z \ & =E_{x}\left[f(x) \nabla_{\theta} \log p(x)\right] & \text {期望的定义} \end{aligned} ∇θ​Ex​[f(x)]​=∇θ​x∑​p(x)f(x)=x∑​∇θ​p(x)f(x)=x∑​p(x)p(x)∇θ​p(x)​f(x)=x∑​p(x)∇θ​logp(x)f(x)=Ex​[f(x)∇θ​logp(x)]​ 定义期望  把「求和」挪至前方  公式变化,乘除 p(x) 基于求导公式 ∇θ​log(z)=z1​∇θ​z 期望的定义​

1.2 PG 评判指标的选择

从上文中可以看出来,Policy Gradient 中评价指标 f ( s , a ) f(s,a) f(s,a) 的定义是关键。 我们前面提到的「根据回合的输赢来判断这个回合中的每 1 步好坏」的方式比较粗糙简单。但其实我们更希望每走 1 步就能够获取到这一步的具体评价,因此出现了很多其他的直接给出某个时刻的评估的评价方式。如这篇论文 High-dimensional continuous control using generalized advantage estimation 里就对比了若干种 PG 评价指标。

1.3 梯度策略算法总结

借用 David Silver 老师讲解梯度策略算法时候的一页核心 PPT 内容,翻译作为梯度策略算法的总结。Policy gradient 通过不断重复估测梯度 g : = ∇ θ E [ ∑ t = 0 ∞ r t ] g:=\nabla_{\theta} \mathbb{E}\left[\sum_{t=0}^{\infty} r_{t}\right] g:=∇θ​E[∑t=0∞​rt​] 来最大化期望收益,有几种不同的梯度策略形式,我们可以统一写成:

g = E [ ∑ t = 0 ∞ Ψ t ∇ θ log ⁡ π θ ( a t ∣ s t ) ] g=\mathbb{E}\left[\sum_{t=0}^{\infty} \Psi_{t} \nabla_{\theta} \log \pi_{\theta}\left(a_{t} \mid s_{t}\right)\right] g=E[t=0∑∞​Ψt​∇θ​logπθ​(at​∣st​)]

上面公式中的 Ψ t \Psi_t Ψt​ 就是 t t t 时刻的评价指标。它可能是如下的形态:

  • ① ∑ t = 0 ∞ r t \sum_{t=0}^{\infty} r_{t} ∑t=0∞​rt​:整个过程总体 reward 收益

  • ② ∑ t ′ = t ∞ r t ′ \sum_{t{\prime}=t} r_{t^{\prime}} ∑t′=t∞​rt′​:动作 a t a_{t} at​ 之后的收益

  • ③ ∑ t ′ = t ∞ r t ′ − b ( s t ) \sum_{t{\prime}=t} r_{t^{\prime}}-b\left(s_{t}\right) ∑t′=t∞​rt′​−b(st​):前序公式的基线版本

  • ④ Q π ( s t , a t ) Q^{\pi}\left(s_{t}, a_{t}\right) Qπ(st​,at​):state-action 价值函数

  • ⑤ A π ( s t , a t ) A^{\pi}\left(s_{t}, a_{t}\right) Aπ(st​,at​):advantage function/优势函数

  • ⑥ r t + V π ( s t + 1 ) − V π ( s t ) r_{t}+V{\pi}\left(s_{t+1}\right)-V\left(s_{t}\right) rt​+Vπ(st+1​)−Vπ(st​) :TD residual/时序差分残差

更具体的一些公式如下:

V π ( s t ) : = E s t + 1 : ∞ , [ ∑ l = 0 ∞ r t + l ] V^{\pi}\left(s_{t}\right):=\mathbb{E} s_{t+1: \infty},\left[\sum_{l=0}^{\infty} r_{t+l}\right] Vπ(st​):=Est+1:∞​,[l=0∑∞​rt+l​]

Q π ( s t , a t ) : = E a t + 1 : ∞ a t + 1 : ∞ [ ∑ l = 0 ∞ r t + l ] Q^{\pi}\left(s_{t}, a_{t}\right):=\mathbb{E}{a{t+1: \infty}}^{a_{t+1: \infty}}\left[\sum_{l=0}^{\infty} r_{t+l}\right] Qπ(st​,at​):=Eat+1:∞​at+1:∞​​[l=0∑∞​rt+l​]

A π ( s t , a t ) : = Q π ( s t , a t ) − V π ( s t ) ( A d v a n t a g e f u n c t i o n ) A^{\pi}\left(s_{t}, a_{t}\right):=Q^{\pi}\left(s_{t}, a_{t}\right)-V^{\pi}\left(s_{t}\right)\quad (Advantage function) Aπ(st​,at​):=Qπ(st​,at​)−Vπ(st​)(Advantagefunction)

在梯度策略里我们可以使用 reward,使用 Q Q Q、 A A A 或者 T D TD TD 来作为动作的评价指标。这些方法的本质区别在于 variance 和 bias 的问题:

用 reward 来作为动作的评价

  • 这样做 bias 比较低,不过 variance 很大,reward 值太不稳定,训练可能不收敛。

采用 Q Q Q 值的方法

  • 因为 Q Q Q 值是对 reward 的期望值,使用 Q Q Q 值 variance 比较小,bias 比较大。一般我们会选择使用 A A A,Advantage。 A = Q − V A=Q-V A=Q−V,是一个动作相对当前状态的价值。本质上 V V V 可以看做是 baseline。对于上面 ③ 的公式,也可以直接用 V V V 来作为 baseline。
  • 但是还是一样的问题, A A A 的 variance 比较大。为了平衡 variance 和 bias 的问题,使用 T D TD TD 会是比较好的做法,既兼顾了实际值 reward,又使用了估计值 V V V。在 T D TD TD 中, T D ( l a m b d a ) TD(lambda) TD(lambda) 平衡不同长度的 T D TD TD 值,会是比较好的做法。

在实际使用中,需要根据具体的问题选择不同的方法。有的问题 reward 很容易得到,有的问题 reward 非常稀疏。reward 越稀疏,也就越需要采用估计值。

总结一下 Policy Gradient 的核心思想:

通过 policy network 输出的 Softmax 概率 和获取的 reward (通过评估指标获取)构造目标函数,然后对 policy network 进行更新。

梯度策略避免了原来的 reward 和 policy network 之间是不可微的问题。也因为它这个特点,目前的很多传统监督学习的问题因为输出都是 Softmax 的离散形式,都可以改造成 Policy Gradient 的方法来实现,调节得当效果会在监督学习的基础上进一步提升。

2.Actor-Critic 算法

在 policy gradient 中讲解到的多种评估指标已经涵盖了下面要介绍的 Actor-Critic 的思想,梯度策略算法往往采用回合更新的模式,即每轮结束后才能进行更新。

如某盘游戏,假如最后的结果是胜利了,那么可以认为其中的每一步都是好的,反之则认为其中的每一步都是不好的。

下图摘自 David Silver 老师的强化学习 Policy Gradient 讲解课件 ,这种方法也叫 Monte-Carlo Policy Gradient

Policy Gradient; Actor-Critic

上图中的 log ⁡ π θ ( s t , a t ) \log \pi_{\theta}(s_t, a_t) logπθ​(st​,at​) 是 policy network 输出的概率, v t v_t vt​ 是当前这一局的结果。这是 policy gradient 最基本的更新形式。

但我们前面也分析了:最后的结果好久并不能说明其中每一步都好。我们能不能抛弃回合更新的做法,加快到单步更新呢,Actor-Critic 算法就做了这个调整。

但要采用单步更新,我们就需要为每一步都即时做出评估。Actor-Critic 算法中的 Critic 负责的就是评估这部分工作,而 Actor 则是负责选择出要执行的动作。这就是 Actor-Critic 的思想。Critic 的输出有多种形式,可以采用 Q Q Q 值、 V V V 值 或 T D TD TD 等。

总结一下 Actor-Critic 算法核心思想:

在 Actor-Critic 算法中,Critic 是评判模块(多采用深度神经网络方法),它会对动作的好坏评价,然后反馈给 Actor(多采用深度神经网络方法),让 Actor 更新策略。

从具体的训练细节来说,Actor 和 Critic 分别采用不同的目标函数进行更新, 可以参考的代码实现如[这里](https://keras.io/examples/rl/actor_critic_cartpole/)。

3.DDPG 算法(Deep Deterministic Policy Gradient)

对于 action 动作个数是离散和有限的情况,我们前面提到的 Policy Gradient 梯度策略算法是 OK 的,但有些情况下输出的值是连续的,比如说「自动驾驶控制的速度」,「机器人控制移动的幅度」,「游戏中调整的角度」等,那梯度策略就不管用了。

Deterministic Policy Gradient Algorithms 这篇论文中提出了输出连续动作值的 DPG(Deterministic Policy Gradient),之后论文 Continuous control with deep reinforcement learning 基于 DPG 做了改进,提出了 DDPG(Deep Deterministic Policy Gradient)算法。

DPG 算法主要证明了 deterministic policy gradient 不仅存在,而且是 model-free 形式且是 action-value function 的梯度。因此 policy 不仅仅可以通过概率分布表示,动作空间可以无限大。

DDPG 相对于 DPG 的核心改进是引入了 Deep Learning,采用深度神经网络作为 DPG 中的 Policy 策略函数 μ \mu μ 和 Q Q Q 函数的模拟,即 Actor 网络和 Critic 网络;然后使用深度学习的方法来训练上述神经网络。DDPG 与 DPG 的关系类似于 DQN 与 Q-learning 的关系。

DDPG 算法中有 2 个网络:「Actor 网络」与 「Critic 网络」:

  • ① 对于状态 s s s,基于 Actor 网络获取动作 action a a a(这里的 a a a 是一个向量)
  • ② 将 a a a 输入 Critic 网络,得到 Q Q Q 值(输出),目标函数就是极大化 Q Q Q 值

具体的「Actor 网络」和 「Critic 网络」更新有一差异,DDPG 论文中算法流程如下图所示:

Deep Deterministic Policy Gradient; DDPG

如上图,Actor 网络和 Critic 网络是分开训练的,但两者的输入输出存在联系,Actor 网络输出的 action 是 Critic 网络的输入,同时 Critic 网络的输出会被用到 Actor 网络进行反向传播。

两个网络的参考示意图如下:Critic 跟之前提到的 DQN 有点类似,但是这里的输入是 state + action,输出是一个 Q Q Q 值而不是各个动作的 Q Q Q 值。

Deep Deterministic Policy Gradient; DDPG

在 DDPG 算法中,我们不再用单一的概率值表示某个动作,而是用向量表示某个动作,由于向量空间可以被认为是无限的,因此也能够跟无限的动作空间对应起来。DDPG 的代码实现可以参考 这里

4.A3C 算法(Asynchronous Advantage Actor-Critic)

DDPG 算法之后,DeepMind 对其改造,提出了效果更好的 Asynchronous Advantage Actor-Critic(A3C)算法(论文是 Asynchronous Methods for Deep Reinforcement Learning )。A3C 算法和 DDPG 类似,通过深度神经网络拟合 policy function 和 value function 的估计。改进点在于:

  • ① A3C 中有多个 agent 对网络进行异步更新,这样的做法使得样本间的相关性较低,A3C 中也无需采用 Experience Replay 的机制,且支持在线的训练模式。
  • ② A3C 有两个输出,其中一个 Softmax output 作为 policy π ( a t ∣ s t ; θ ) \pi(a_t|s_t;\theta) π(at​∣st​;θ),而另一个 linear output 为 value function V ( s t ; θ v ) V(s_t;\theta_v) V(st​;θv​) 。
  • ③ A3C 中的 Policy network 的评估指标采用的是上面比较了多种评估指标的论文中提到的 Advantage Function(即 A 值) 而不是 DDPG 中单纯的 Q Q Q 值。

下图(摘自 这篇文章 )展示了其网络结构:

Asynchronous Advantage Actor-Critic; A3C

从上图可以看出输出包含 2 个部分,value network 的部分可以用来作为连续动作值的输出,而 policy network 可以作为离散动作值的概率输出,因此能够同时解决前面提到的 2 类问题。

两个网络的更新公式如下:

  • Estimate state-value function

V ( s , v ) ≈ E [ r t + 1 + γ r t + 2 + … ∣ s ] V(s, \mathbf{v}) \approx \mathbb{E}\left[r_{t+1}+\gamma r_{t+2}+\ldots \mid s\right] V(s,v)≈E[rt+1​+γrt+2​+…∣s]

  • Q-value estimated by an n -step sample

q t = r t + 1 + γ r t + 2 ⋯ + γ n − 1 r t + n + γ n V ( s t + n , v ) q_{t}=r_{t+1}+\gamma r_{t+2 \cdots}+\gamma^{n-1} r_{t+n}+\gamma^{n} V\left(s_{t+n}, \mathbf{v}\right) qt​=rt+1​+γrt+2⋯​+γn−1rt+n​+γnV(st+n​,v)

  • Actor is updated towards target

∂ I u ∂ u = ∂ log ⁡ π ( a t ∣ s t , u ) ∂ u ( q t − V ( s t , v ) ) \frac{\partial I_{u}}{\partial \mathbf{u}}=\frac{\partial \log \pi\left(a_{t} \mid s_{t}, \mathbf{u}\right)}{\partial \mathbf{u}}\left(q_{t}-V\left(s_{t}, \mathbf{v}\right)\right) ∂u∂Iu​​=∂u∂logπ(at​∣st​,u)​(qt​−V(st​,v))

  • Critic is updated to minimise MSE w.r.t. Target

I v = ( q t − V ( s t , v ) ) 2 I_{v}=\left(q_{t}-V\left(s_{t}, \mathbf{v}\right)\right)^{2} Iv​=(qt​−V(st​,v))2

A3C 通过创建多个 agent,在多个环境实例中并行且异步的执行和学习,有个潜在的好处是不那么依赖于 GPU 或大型分布式系统,实际上 A3C 可以跑在一个多核 CPU 上,而工程上的设计和优化也是原始 paper 的一个重点。

A3C 的代码实现可以参考这里

5.算法总结

  • 我们从最基础的Policy Gradient 梯度策略方法开始介绍,最基础的版本是「回合更新」的。
  • 引入 Critic 后变成了「单步更新」,进而演变为Actor-Critic算法,其中 Critic 有多种可选的方法。
  • 输出动作为连续值的情形,无法通过输出动作概率分布方式解决,因此提出了 DPGDDPG 算法,DDPG 对 DPG 的改进在于引入深度神经网络去拟合 policy function 和 value function。
  • 在 DDPG 基础上又提出了效果更好的 A3C 算法,这个方法在 DDPG 上引入了多个 agent 对网络进行 异步更新,不仅取得了更好的效果,而且降低了训练的代价。

6.拓展学习

可以点击 B 站 查看视频的【双语字幕】版本

player.bilibili.com/player.html?aid=759478950&page=15

【字幕+资料下载】斯坦福 CS231n | 面向视觉识别的卷积神经网络 (2017·全 16 讲)

7.参考资料

ShowMeAI 斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

showmeai 用知识加速每一次技术成长

posted @ 2026-03-26 13:17  布客飞龙V  阅读(0)  评论(0)    收藏  举报