0%

经典网络解读

4.1 LeNet-5

4.1.1 模型介绍

​ LeNet-5是由 $LeCun$ 提出的一种用于识别手写数字和机器印刷字符的卷积神经网络(Convolutional Neural Network,CNN) $^{[1]}$ ,其命名来源于作者 $LeCun$ 的名字,5则是其研究成果的代号,在LeNet-5之前还有LeNet-4和LeNet-1鲜为人知。LeNet-5阐述了图像中像素特征之间的相关性能够由参数共享的卷积操作所提取,同时使用卷积、下采样(池化)和非线性映射这样的组合结构,是当前流行的大多数深度图像识别网络的基础。

4.1.2 模型结构

​ 图4.1 LeNet-5网络结构图

​ 如图4.1所示,LeNet-5一共包含7层(输入层不作为网络结构),分别由2个卷积层、2个下采样层和3个连接层组成,网络的参数配置如表4.1所示,其中下采样层和全连接层的核尺寸分别代表采样范围和连接矩阵的尺寸(如卷积核尺寸中的 $“5\times5\times1/1,6”$ 表示核大小为 $5\times5\times1$ 、步长为 $1​$ 且核个数为6的卷积核)。

​ 表4.1 LeNet-5网络参数配置

网络层 输入尺寸 核尺寸 输出尺寸 可训练参数量
卷积层 $C_1$ $32\times32\times1$ $5\times5\times1/1,6$ $28\times28\times6$ $(5\times5\times1+1)\times6$
下采样层 $S_2$ $28\times28\times6$ $2\times2/2$ $14\times14\times6$ $(1+1)\times6$ $^*$
卷积层 $C_3$ $14\times14\times6$ $5\times5\times6/1,16$ $10\times10\times16$ $1516^*$
下采样层 $S_4$ $10\times10\times16$ $2\times2/2$ $5\times5\times16$ $(1+1)\times16$
卷积层 $C_5 {% raw%}$$^*${% endraw %} {% raw%}$5\times5\times16${% endraw %} {% raw%}$5\times5\times16/1,120${% endraw %} {% raw%}$1\times1\times120${% endraw %} {% raw%}$(5\times5\times16+1)\times120${% endraw %}
全连接层 {% raw%}$F_6${% endraw %} {% raw%}$1\times1\times120${% endraw %} {% raw%}$120\times84${% endraw %} {% raw%}$1\times1\times84${% endraw %} {% raw%}$(120+1)\times84${% endraw %}
输出层 {% raw%}$1\times1\times84${% endraw %} {% raw%}$84\times10${% endraw %} {% raw%}$1\times1\times10${% endraw %} {% raw%}$(84+1)\times10${% endraw %}

​ {% raw%}$^*${% endraw %} 在LeNet中,下采样操作和池化操作类似,但是在得到采样结果后会乘以一个系数和加上一个偏置项,所以下采样的参数个数是 {% raw%}$(1+1)\times6​${% endraw %} 而不是零。

​ {% raw%}$^*${% endraw %} {% raw%}$C_3${% endraw %} 卷积层可训练参数并未直接连接 {% raw%}$S_2${% endraw %} 中所有的特征图(Feature Map),而是采用如图4.2所示的采样特征方式进行连接(稀疏连接),生成的16个通道特征图中分别按照相邻3个特征图、相邻4个特征图、非相邻4个特征图和全部6个特征图进行映射,得到的参数个数计算公式为 {% raw%}$6\times(25\times3+1)+6\times(25\times4+1)+3\times(25\times4+1)+1\times(25\times6+1)=1516${% endraw %} ,在原论文中解释了使用这种采样方式原因包含两点:限制了连接数不至于过大(当年的计算能力比较弱);强制限定不同特征图的组合可以使映射得到的特征图学习到不同的特征模式。

FeatureMap

​ 图4.2 {% raw%}$S_2${% endraw %} 与 {% raw%}$C_3${% endraw %} 之间的特征图稀疏连接

​ {% raw%}$^*${% endraw %} {% raw%}$C_5${% endraw %} 卷积层在图4.1中显示为全连接层,原论文中解释这里实际采用的是卷积操作,只是刚好在 {% raw%}$5\times5${% endraw %} 卷积后尺寸被压缩为 {% raw%}$1\times1​${% endraw %} ,输出结果看起来和全连接很相似。

4.1.3 模型特性

  • 卷积网络使用一个3层的序列组合:卷积、下采样(池化)、非线性映射(LeNet-5最重要的特性,奠定了目前深层卷积网络的基础)
  • 使用卷积提取空间特征
  • 使用映射的空间均值进行下采样
  • 使用 {% raw%}$tanh${% endraw %} 或 {% raw%}$sigmoid${% endraw %} 进行非线性映射
  • 多层神经网络(MLP)作为最终的分类器
  • 层间的稀疏连接矩阵以避免巨大的计算开销

4.2 AlexNet

4.2.1 模型介绍

​ AlexNet是由 {% raw%}$Alex${% endraw %} {% raw%}$Krizhevsky ${% endraw %} 提出的首个应用于图像分类的深层卷积神经网络,该网络在2012年ILSVRC(ImageNet Large Scale Visual Recognition Competition)图像分类竞赛中以15.3%的top-5测试错误率赢得第一名 {% raw%}$^{[2]}${% endraw %} 。AlexNet使用GPU代替CPU进行运算,使得在可接受的时间范围内模型结构能够更加复杂,它的出现证明了深层卷积神经网络在复杂模型下的有效性,使CNN在计算机视觉中流行开来,直接或间接地引发了深度学习的热潮。

4.2.2 模型结构

​ 图4.3 AlexNet网络结构图

​ 如图4.3所示,除去下采样(池化层)和局部响应规范化操作(Local Responsible Normalization, LRN),AlexNet一共包含8层,前5层由卷积层组成,而剩下的3层为全连接层。网络结构分为上下两层,分别对应两个GPU的操作过程,除了中间某些层( {% raw%}$C_3${% endraw %} 卷积层和 {% raw%}$F_{6-8}${% endraw %} 全连接层会有GPU间的交互),其他层两个GPU分别计算结 果。最后一层全连接层的输出作为 {% raw%}$softmax${% endraw %} 的输入,得到1000个图像分类标签对应的概率值。除去GPU并行结构的设计,AlexNet网络结构与LeNet十分相似,其网络的参数配置如表4.2所示。

​ 表4.2 AlexNet网络参数配置

网络层 输入尺寸 核尺寸 输出尺寸 可训练参数量
卷积层 {% raw%}$C_1${% endraw %} {% raw%}$^*${% endraw %} {% raw%}$224\times224\times3${% endraw %} {% raw%}$11\times11\times3/4,48(\times2_{GPU})${% endraw %} {% raw%}$55\times55\times48(\times2_{GPU})${% endraw %} {% raw%}$(11\times11\times3+1)\times48\times2${% endraw %}
下采样层 {% raw%}$S_{max}$${% endraw %} ^*$ $55\times55\times48(\times2_{GPU})$ $3\times3/2(\times2_{GPU})$ $27\times27\times48(\times2_{GPU})$ 0
卷积层 $C_2$ $27\times27\times48(\times2_{GPU})$ $5\times5\times48/1,128(\times2_{GPU})$ $27\times27\times128(\times2_{GPU})$ $(5\times5\times48+1)\times128\times2$
下采样层 $S_{max}$ $27\times27\times128(\times2_{GPU})$ $3\times3/2(\times2_{GPU})$ $13\times13\times128(\times2_{GPU})$ 0
卷积层 $C_3$ $^*$ $13\times13\times128\times2_{GPU}$ $3\times3\times256/1,192(\times2_{GPU})$ $13\times13\times192(\times2_{GPU})$ $(3\times3\times256+1)\times192\times2$
卷积层 $C_4$ $13\times13\times192(\times2_{GPU})$ $3\times3\times192/1,192(\times2_{GPU})$ $13\times13\times192(\times2_{GPU})$ $(3\times3\times192+1)\times192\times2$
卷积层 $C_5$ $13\times13\times192(\times2_{GPU})$ $3\times3\times192/1,128(\times2_{GPU})$ $13\times13\times128(\times2_{GPU})$ $(3\times3\times192+1)\times128\times2$
下采样层 $S_{max}$ $13\times13\times128(\times2_{GPU})$ $3\times3/2(\times2_{GPU})$ $6\times6\times128(\times2_{GPU})$ 0
全连接层 $F_6$ $^*$ $6\times6\times128\times2_{GPU}$ $9216\times2048(\times2_{GPU})$ $1\times1\times2048(\times2_{GPU})$ $(9216+1)\times2048\times2$
全连接层 $F_7$ $1\times1\times2048\times2_{GPU}$ $4096\times2048(\times2_{GPU})$ $1\times1\times2048(\times2_{GPU})$ $(4096+1)\times2048\times2$
全连接层 $F_8$ $1\times1\times2048\times2_{GPU}$ $4096\times1000$ $1\times1\times1000$ $(4096+1)\times1000\times2$

卷积层 $C_1$ 输入为 $224\times224\times3$ 的图片数据,分别在两个GPU中经过核为 $11\times11\times3$ 、步长(stride)为4的卷积卷积后,分别得到两条独立的 $55\times55\times48$ 的输出数据。

下采样层 $S_{max}$ 实际上是嵌套在卷积中的最大池化操作,但是为了区分没有采用最大池化的卷积层单独列出来。在 $C_{1-2}$ 卷积层中的池化操作之后(ReLU激活操作之前),还有一个LRN操作,用作对相邻特征点的归一化处理。

卷积层 $C_3$ 的输入与其他卷积层不同, $13\times13\times192\times2_{GPU}$ 表示汇聚了上一层网络在两个GPU上的输出结果作为输入,所以在进行卷积操作时通道上的卷积核维度为384。

全连接层 $F_{6-8}$ 中输入数据尺寸也和 $C_3$ 类似,都是融合了两个GPU流向的输出结果作为输入。

4.2.3 模型特性

  • 所有卷积层都使用ReLU作为非线性映射函数,使模型收敛速度更快
  • 在多个GPU上进行模型的训练,不但可以提高模型的训练速度,还能提升数据的使用规模
  • 使用LRN对局部的特征进行归一化,结果作为ReLU激活函数的输入能有效降低错误率
  • 重叠最大池化(overlapping max pooling),即池化范围z与步长s存在关系 $z>s$ (如 $S_{max}$ 中核尺度为 $3\times3/2$ ),避免平均池化(average pooling)的平均效应
  • 使用随机丢弃技术(dropout)选择性地忽略训练中的单个神经元,避免模型的过拟合

4.3 ZFNet

4.3.1 模型介绍

​ ZFNet是由 $Matthew$ $D. Zeiler$ 和 $Rob$ $Fergus$ 在AlexNet基础上提出的大型卷积网络,在2013年ILSVRC图像分类竞赛中以11.19%的错误率获得冠军(实际上原ZFNet所在的队伍并不是真正的冠军,原ZFNet以13.51%错误率排在第8,真正的冠军是 $Clarifai$ 这个队伍,而 $Clarifai$ 这个队伍所对应的一家初创公司的CEO又是 $Zeiler$ ,而且 $Clarifai$ 对ZFNet的改动比较小,所以通常认为是ZFNet获得了冠军) $^{[3-4]}​$ 。ZFNet实际上是微调(fine-tuning)了的AlexNet,并通过反卷积(Deconvolution)的方式可视化各层的输出特征图,进一步解释了卷积操作在大型网络中效果显著的原因。

4.3.2 模型结构

​ 图4.4 ZFNet网络结构图(原始结构图与AlexNet风格结构图)

​ 如图4.4所示,ZFNet与AlexNet类似,都是由8层网络组成的卷积神经网络,其中包含5层卷积层和3层全连接层。两个网络结构最大的不同在于,ZFNet第一层卷积采用了 $7\times7\times3/2$ 的卷积核替代了AlexNet中第一层卷积核 $11\times11\times3/4$ 的卷积核。图4.5中ZFNet相比于AlexNet在第一层输出的特征图中包含更多中间频率的信息,而AlexNet第一层输出的特征图大多是低频或高频的信息,对中间频率特征的缺失导致后续网络层次如图4.5(c)能够学习到的特征不够细致,而导致这个问题的根本原因在于AlexNet在第一层中采用的卷积核和步长过大。

​ 图4.5 (a)ZFNet第一层输出的特征图(b)AlexNet第一层输出的特征图(c)AlexNet第二层输出的特征图(d)ZFNet第二层输出的特征图

​ 表4.3 ZFNet网络参数配置

网络层 输入尺寸 核尺寸 输出尺寸 可训练参数量
卷积层 $C_1$ $^*$ $224\times224\times3$ $7\times7\times3/2,96$ $110\times110\times96$ $(7\times7\times3+1)\times96$
下采样层 $S_{max}$ $110\times110\times96$ $3\times3/2$ $55\times55\times96$ 0
卷积层 $C_2$ $^*$ $55\times55\times96$ $5\times5\times96/2,256$ $26\times26\times256$ $(5\times5\times96+1)\times256$
下采样层 $S_{max}$ $26\times26\times256$ $3\times3/2$ $13\times13\times256$ 0
卷积层 $C_3$ $13\times13\times256$ $3\times3\times256/1,384$ $13\times13\times384$ $(3\times3\times256+1)\times384$
卷积层 $C_4$ $13\times13\times384$ $3\times3\times384/1,384$ $13\times13\times384$ $(3\times3\times384+1)\times384$
卷积层 $C_5$ $13\times13\times384$ $3\times3\times384/1,256$ $13\times13\times256$ $(3\times3\times384+1)\times256$
下采样层 $S_{max}$ $13\times13\times256$ $3\times3/2$ $6\times6\times256$ 0
全连接层 $F_6$ $6\times6\times256$ $9216\times4096$ $1\times1\times4096$ $(9216+1)\times4096$
全连接层 $F_7$ $1\times1\times4096$ $4096\times4096$ $1\times1\times4096$ $(4096+1)\times4096$
全连接层 $F_8$ $1\times1\times4096$ $4096\times1000$ $1\times1\times1000$ $(4096+1)\times1000$

卷积层 $C_1$ 与AlexNet中的 $C_1$ 有所不同,采用 $7\times7\times3/2$ 的卷积核代替 $11\times11\times3/4​$ ,使第一层卷积输出的结果可以包含更多的中频率特征,对后续网络层中多样化的特征组合提供更多选择,有利于捕捉更细致的特征。

卷积层 $C_2$ 采用了步长2的卷积核,区别于AlexNet中 $C_2$ 的卷积核步长,所以输出的维度有所差异。

4.3.3 模型特性

​ ZFNet与AlexNet在结构上几乎相同,此部分虽属于模型特性,但准确地说应该是ZFNet原论文中可视化技术的贡献。

  • 可视化技术揭露了激发模型中每层单独的特征图。
  • 可视化技术允许观察在训练阶段特征的演变过程且诊断出模型的潜在问题。
  • 可视化技术用到了多层解卷积网络,即由特征激活返回到输入像素空间。
  • 可视化技术进行了分类器输出的敏感性分析,即通过阻止部分输入图像来揭示那部分对于分类是重要的。
  • 可视化技术提供了一个非参数的不变性来展示来自训练集的哪一块激活哪个特征图,不仅需要裁剪输入图片,而且自上而下的投影来揭露来自每块的结构激活一个特征图。
  • 可视化技术依赖于解卷积操作,即卷积操作的逆过程,将特征映射到像素上。

4.4 Network in Network

4.4.1 模型介绍

​ Network In Network (NIN)是由 $Min Lin$ 等人提出,在CIFAR-10和CIFAR-100分类任务中达到当时的最好水平,因其网络结构是由三个多层感知机堆叠而被成为NIN $^{[5]}$ 。NIN以一种全新的角度审视了卷积神经网络中的卷积核设计,通过引入子网络结构代替纯卷积中的线性映射部分,这种形式的网络结构激发了更复杂的卷积神经网络的结构设计,其中下一节中介绍的GoogLeNet的Inception结构就是来源于这个思想。

4.4.2 模型结构


​ 图 4.6 NIN网络结构图

​ NIN由三层的多层感知卷积层(MLPConv Layer)构成,每一层多层感知卷积层内部由若干层的局部全连接层和非线性激活函数组成,代替了传统卷积层中采用的线性卷积核。在网络推理(inference)时,这个多层感知器会对输入特征图的局部特征进行划窗计算,并且每个划窗的局部特征图对应的乘积的权重是共享的,这两点是和传统卷积操作完全一致的,最大的不同在于多层感知器对局部特征进行了非线性的映射,而传统卷积的方式是线性的。NIN的网络参数配置表4.4所示(原论文并未给出网络参数,表中参数为编者结合网络结构图和CIFAR-100数据集以 $3\times3$ 卷积为例给出)。

​ 表4.4 NIN网络参数配置(结合原论文NIN结构和CIFAR-100数据给出)

网络层 输入尺寸 核尺寸 输出尺寸 参数个数
局部全连接层 $L_{11}$ $^*$ $32\times32\times3$ $(3\times3)\times16/1$ $30\times30\times16$ $(3\times3\times3+1)\times16$
全连接层 $L_{12}$ $^*$ $30\times30\times16$ $16\times16$ $30\times30\times16$ $((16+1)\times16)$
局部全连接层 $L_{21}$ $30\times30\times16$ $(3\times3)\times64/1$ $28\times28\times64$ $(3\times3\times16+1)\times64$
全连接层 $L_{22}$ $28\times28\times64$ $64\times64$ $28\times28\times64$ $((64+1)\times64)$
局部全连接层 $L_{31}$ $28\times28\times64$ $(3\times3)\times100/1$ $26\times26\times100$ $(3\times3\times64+1)\times100$
全连接层 $L_{32}$ $26\times26\times100$ $100\times100$ $26\times26\times100$ $((100+1)\times100)$
全局平均采样 $GAP$ $^*$ $26\times26\times100$ $26\times26\times100/1$ $1\times1\times100$ $0$

局部全连接层 $L_{11}$ 实际上是对原始输入图像进行划窗式的全连接操作,因此划窗得到的输出特征尺寸为 $30\times30$ ( $\frac{32-3_k+1}{1_{stride}}=30$ )
全连接层 $L_{12}$ 是紧跟 $L_{11}$ 后的全连接操作,输入的特征是划窗后经过激活的局部响应特征,因此仅需连接 $L_{11}$ 和 $L_{12}$ 的节点即可,而每个局部全连接层和紧接的全连接层构成代替卷积操作的多层感知卷积层(MLPConv)。
全局平均采样层或全局平均池化层 $GAP$ (Global Average Pooling)将 $L_{32}$ 输出的每一个特征图进行全局的平均池化操作,直接得到最后的类别数,可以有效地减少参数量。

4.4.3 模型特点

  • 使用多层感知机结构来代替卷积的滤波操作,不但有效减少卷积核数过多而导致的参数量暴涨问题,还能通过引入非线性的映射来提高模型对特征的抽象能力。
  • 使用全局平均池化来代替最后一个全连接层,能够有效地减少参数量(没有可训练参数),同时池化用到了整个特征图的信息,对空间信息的转换更加鲁棒,最后得到的输出结果可直接作为对应类别的置信度。

4.5 VGGNet

4.5.1 模型介绍

​ VGGNet是由牛津大学视觉几何小组(Visual Geometry Group, VGG)提出的一种深层卷积网络结构,他们以7.32%的错误率赢得了2014年ILSVRC分类任务的亚军(冠军由GoogLeNet以6.65%的错误率夺得)和25.32%的错误率夺得定位任务(Localization)的第一名(GoogLeNet错误率为26.44%) $^{[5]}$ ,网络名称VGGNet取自该小组名缩写。VGGNet是首批把图像分类的错误率降低到10%以内模型,同时该网络所采用的 $3\times3$ 卷积核的思想是后来许多模型的基础,该模型发表在2015年国际学习表征会议(International Conference On Learning Representations, ICLR)后至今被引用的次数已经超过1万4千余次。

4.5.2 模型结构

​ 图 4.7 VGG16网络结构图

​ 在原论文中的VGGNet包含了6个版本的演进,分别对应VGG11、VGG11-LRN、VGG13、VGG16-1、VGG16-3和VGG19,不同的后缀数值表示不同的网络层数(VGG11-LRN表示在第一层中采用了LRN的VGG11,VGG16-1表示后三组卷积块中最后一层卷积采用卷积核尺寸为 $1\times1$ ,相应的VGG16-3表示卷积核尺寸为 $3\times3$ ),本节介绍的VGG16为VGG16-3。图4.7中的VGG16体现了VGGNet的核心思路,使用 $3\times3$ 的卷积组合代替大尺寸的卷积(2个 $3\times3卷积即可与 {% raw%}$$5\times5${% endraw %} 卷积拥有相同的感受视野),网络参数设置如表4.5所示。

​ 表4.5 VGG16网络参数配置

网络层 输入尺寸 核尺寸 输出尺寸 参数个数
卷积层 {% raw%}$C_{11}${% endraw %} {% raw%}$224\times224\times3${% endraw %} {% raw%}$3\times3\times64/1${% endraw %} {% raw%}$224\times224\times64${% endraw %} {% raw%}$(3\times3\times3+1)\times64${% endraw %}
卷积层 {% raw%}$C_{12}${% endraw %} {% raw%}$224\times224\times64${% endraw %} {% raw%}$3\times3\times64/1${% endraw %} {% raw%}$224\times224\times64${% endraw %} {% raw%}$(3\times3\times64+1)\times64${% endraw %}
下采样层 {% raw%}$S_{max1}${% endraw %} {% raw%}$224\times224\times64${% endraw %} {% raw%}$2\times2/2${% endraw %} {% raw%}$112\times112\times64${% endraw %} {% raw%}$0${% endraw %}
卷积层 {% raw%}$C_{21}${% endraw %} {% raw%}$112\times112\times64${% endraw %} {% raw%}$3\times3\times128/1${% endraw %} {% raw%}$112\times112\times128${% endraw %} {% raw%}$(3\times3\times64+1)\times128${% endraw %}
卷积层 {% raw%}$C_{22}${% endraw %} {% raw%}$112\times112\times128${% endraw %} {% raw%}$3\times3\times128/1${% endraw %} {% raw%}$112\times112\times128${% endraw %} {% raw%}$(3\times3\times128+1)\times128${% endraw %}
下采样层 {% raw%}$S_{max2}${% endraw %} {% raw%}$112\times112\times128${% endraw %} {% raw%}$2\times2/2${% endraw %} {% raw%}$56\times56\times128${% endraw %} {% raw%}$0${% endraw %}
卷积层 {% raw%}$C_{31}${% endraw %} {% raw%}$56\times56\times128${% endraw %} {% raw%}$3\times3\times256/1${% endraw %} {% raw%}$56\times56\times256${% endraw %} {% raw%}$(3\times3\times128+1)\times256${% endraw %}
卷积层 {% raw%}$C_{32}${% endraw %} {% raw%}$56\times56\times256${% endraw %} {% raw%}$3\times3\times256/1${% endraw %} {% raw%}$56\times56\times256${% endraw %} {% raw%}$(3\times3\times256+1)\times256${% endraw %}
卷积层 {% raw%}$C_{33}${% endraw %} {% raw%}$56\times56\times256${% endraw %} {% raw%}$3\times3\times256/1${% endraw %} {% raw%}$56\times56\times256${% endraw %} {% raw%}$(3\times3\times256+1)\times256${% endraw %}
下采样层 {% raw%}$S_{max3}${% endraw %} {% raw%}$56\times56\times256${% endraw %} {% raw%}$2\times2/2${% endraw %} {% raw%}$28\times28\times256${% endraw %} {% raw%}$0${% endraw %}
卷积层 {% raw%}$C_{41}${% endraw %} {% raw%}$28\times28\times256${% endraw %} {% raw%}$3\times3\times512/1${% endraw %} {% raw%}$28\times28\times512${% endraw %} {% raw%}$(3\times3\times256+1)\times512${% endraw %}
卷积层 {% raw%}$C_{42}${% endraw %} {% raw%}$28\times28\times512${% endraw %} {% raw%}$3\times3\times512/1${% endraw %} {% raw%}$28\times28\times512${% endraw %} {% raw%}$(3\times3\times512+1)\times512${% endraw %}
卷积层 {% raw%}$C_{43}${% endraw %} {% raw%}$28\times28\times512${% endraw %} {% raw%}$3\times3\times512/1${% endraw %} {% raw%}$28\times28\times512${% endraw %} {% raw%}$(3\times3\times512+1)\times512${% endraw %}
下采样层 {% raw%}$S_{max4}${% endraw %} {% raw%}$28\times28\times512${% endraw %} {% raw%}$2\times2/2${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$0${% endraw %}
卷积层 {% raw%}$C_{51}${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$3\times3\times512/1${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$(3\times3\times512+1)\times512${% endraw %}
卷积层 {% raw%}$C_{52}${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$3\times3\times512/1${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$(3\times3\times512+1)\times512${% endraw %}
卷积层 {% raw%}$C_{53}${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$3\times3\times512/1${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$(3\times3\times512+1)\times512${% endraw %}
下采样层 {% raw%}$S_{max5}${% endraw %} {% raw%}$14\times14\times512${% endraw %} {% raw%}$2\times2/2${% endraw %} {% raw%}$7\times7\times512${% endraw %} {% raw%}$0${% endraw %}
全连接层 {% raw%}$FC_{1}${% endraw %} {% raw%}$7\times7\times512${% endraw %} {% raw%}$(7\times7\times512)\times4096${% endraw %} {% raw%}$1\times4096${% endraw %} {% raw%}$(7\times7\times512+1)\times4096${% endraw %}
全连接层 {% raw%}$FC_{2}${% endraw %} {% raw%}$1\times4096${% endraw %} {% raw%}$4096\times4096${% endraw %} {% raw%}$1\times4096${% endraw %} {% raw%}$(4096+1)\times4096${% endraw %}
全连接层 {% raw%}$FC_{3}${% endraw %} {% raw%}$1\times4096${% endraw %} {% raw%}$4096\times1000${% endraw %} {% raw%}$1\times1000${% endraw %} {% raw%}$(4096+1)\times1000${% endraw %}

4.5.3 模型特性

  • 整个网络都使用了同样大小的卷积核尺寸 {% raw%}$3\times3${% endraw %} 和最大池化尺寸 {% raw%}$2\times2${% endraw %} 。
  • {% raw%}$1\times1${% endraw %} 卷积的意义主要在于线性变换,而输入通道数和输出通道数不变,没有发生降维。
  • 两个 {% raw%}$3\times3${% endraw %} 的卷积层串联相当于1个 {% raw%}$5\times5${% endraw %} 的卷积层,感受野大小为 {% raw%}$5\times5${% endraw %} 。同样地,3个 {% raw%}$3\times3${% endraw %} 的卷积层串联的效果则相当于1个 {% raw%}$7\times7${% endraw %} 的卷积层。这样的连接方式使得网络参数量更小,而且多层的激活函数令网络对特征的学习能力更强。
  • VGGNet在训练时有一个小技巧,先训练浅层的的简单网络VGG11,再复用VGG11的权重来初始化VGG13,如此反复训练并初始化VGG19,能够使训练时收敛的速度更快。
  • 在训练过程中使用多尺度的变换对原始数据做数据增强,使得模型不易过拟合。

4.6 GoogLeNet

4.6.1 模型介绍

​ GoogLeNet作为2014年ILSVRC在分类任务上的冠军,以6.65%的错误率力压VGGNet等模型,在分类的准确率上面相比过去两届冠军ZFNet和AlexNet都有很大的提升。从名字GoogLeNet可以知道这是来自谷歌工程师所设计的网络结构,而名字中GoogLeNet更是致敬了LeNet {% raw%}$^{[0]}${% endraw %} 。GoogLeNet中最核心的部分是其内部子网络结构Inception,该结构灵感来源于NIN,至今已经经历了四次版本迭代(Inception {% raw%}$_{v1-4}${% endraw %} )。


​ 图 4.8 Inception性能比较图

4.6.2 模型结构


​ 图 4.9 GoogLeNet网络结构图
​ 如图4.9中所示,GoogLeNet相比于以前的卷积神经网络结构,除了在深度上进行了延伸,还对网络的宽度进行了扩展,整个网络由许多块状子网络的堆叠而成,这个子网络构成了Inception结构。图4.9为Inception的四个版本: {% raw%}$Inception_{v1}​${% endraw %} 在同一层中采用不同的卷积核,并对卷积结果进行合并; {% raw%}$Inception_{v2}​${% endraw %} 组合不同卷积核的堆叠形式,并对卷积结果进行合并; {% raw%}$Inception_{v3}​${% endraw %} 则在 {% raw%}$v_2​${% endraw %} 基础上进行深度组合的尝试; {% raw%}$Inception_{v4}​${% endraw %} 结构相比于前面的版本更加复杂,子网络中嵌套着子网络。

{% raw%}$Inception_{v1}${% endraw %}

{% raw%}$Inception_{v2}${% endraw %}

{% raw%}$Inception_{v3}${% endraw %}

{% raw%}$Inception_{v4}${% endraw %}

​ 图 4.10 Inception {% raw%}$_{v1-4}${% endraw %} 结构图

​ 表 4.6 GoogLeNet中Inception {% raw%}$_{v1}${% endraw %} 网络参数配置

网络层 输入尺寸 核尺寸 输出尺寸 参数个数
卷积层 {% raw%}$C_{11}${% endraw %} {% raw%}$H\times{W}\times{C_1}${% endraw %} {% raw%}$1\times1\times{C_2}/2${% endraw %} {% raw%}$\frac{H}{2}\times\frac{W}{2}\times{C_2}${% endraw %} {% raw%}$(1\times1\times{C_1}+1)\times{C_2}${% endraw %}
卷积层 {% raw%}$C_{21}${% endraw %} {% raw%}$H\times{W}\times{C_2}${% endraw %} {% raw%}$1\times1\times{C_2}/2${% endraw %} {% raw%}$\frac{H}{2}\times\frac{W}{2}\times{C_2}${% endraw %} {% raw%}$(1\times1\times{C_2}+1)\times{C_2}${% endraw %}
卷积层 {% raw%}$C_{22}${% endraw %} {% raw%}$H\times{W}\times{C_2}${% endraw %} {% raw%}$3\times3\times{C_2}/1${% endraw %} {% raw%}$H\times{W}\times{C_2}/1${% endraw %} {% raw%}$(3\times3\times{C_2}+1)\times{C_2}${% endraw %}
卷积层 {% raw%}$C_{31}${% endraw %} {% raw%}$H\times{W}\times{C_1}${% endraw %} {% raw%}$1\times1\times{C_2}/2${% endraw %} {% raw%}$\frac{H}{2}\times\frac{W}{2}\times{C_2}${% endraw %} {% raw%}$(1\times1\times{C_1}+1)\times{C_2}${% endraw %}
卷积层 {% raw%}$C_{32}${% endraw %} {% raw%}$H\times{W}\times{C_2}${% endraw %} {% raw%}$5\times5\times{C_2}/1${% endraw %} {% raw%}$H\times{W}\times{C_2}/1${% endraw %} {% raw%}$(5\times5\times{C_2}+1)\times{C_2}${% endraw %}
下采样层 {% raw%}$S_{41}${% endraw %} {% raw%}$H\times{W}\times{C_1}${% endraw %} {% raw%}$3\times3/2${% endraw %} {% raw%}$\frac{H}{2}\times\frac{W}{2}\times{C_2}${% endraw %} {% raw%}$0${% endraw %}
卷积层 {% raw%}$C_{42}${% endraw %} {% raw%}$\frac{H}{2}\times\frac{W}{2}\times{C_2}${% endraw %} {% raw%}$1\times1\times{C_2}/1${% endraw %} {% raw%}$\frac{H}{2}\times\frac{W}{2}\times{C_2}${% endraw %} {% raw%}$(3\times3\times{C_2}+1)\times{C_2}${% endraw %}
合并层 {% raw%}$M${% endraw %} {% raw%}$\frac{H}{2}\times\frac{W}{2}\times{C_2}(\times4)${% endraw %} 拼接 {% raw%}$\frac{H}{2}\times\frac{W}{2}\times({C_2}\times4)${% endraw %} {% raw%}$0${% endraw %}

4.6.3 模型特性

  • 采用不同大小的卷积核意味着不同大小的感受野,最后拼接意味着不同尺度特征的融合;

  • 之所以卷积核大小采用1、3和5,主要是为了方便对齐。设定卷积步长stride=1之后,只要分别设定pad=0、1、2,那么卷积之后便可以得到相同维度的特征,然后这些特征就可以直接拼接在一起了;

  • 网络越到后面,特征越抽象,而且每个特征所涉及的感受野也更大了,因此随着层数的增加,3x3和5x5卷积的比例也要增加。但是,使用5x5的卷积核仍然会带来巨大的计算量。 为此,文章借鉴NIN2,采用1x1卷积核来进行降维。

Restnet

Densenet

4.7 为什么现在的CNN模型都是在GoogleNet、VGGNet或者AlexNet上调整的?

  • 评测对比:为了让自己的结果更有说服力,在发表自己成果的时候会同一个标准的baseline及在baseline上改进而进行比较,常见的比如各种检测分割的问题都会基于VGG或者Resnet101这样的基础网络。
  • 时间和精力有限:在科研压力和工作压力中,时间和精力只允许大家在有限的范围探索。
  • 模型创新难度大:进行基本模型的改进需要大量的实验和尝试,并且需要大量的实验积累和强大灵感,很有可能投入产出比比较小。
  • 资源限制:创造一个新的模型需要大量的时间和计算资源,往往在学校和小型商业团队不可行。
  • 在实际的应用场景中,其实是有大量的非标准模型的配置。

参考文献

[1] Y. LeCun, L. Bottou, Y. Bengio, and P. Haffner. Gradient-based learning applied to document recognition. Proceedings of the IEEE, november 1998.

[2] A. Krizhevsky, I. Sutskever and G. E. Hinton. ImageNet Classification with Deep Convolutional Neural Networks. Advances in Neural Information Processing Systems 25. Curran Associates, Inc. 1097–1105.

[3] LSVRC-2013. http://www.image-net.org/challenges/LSVRC/2013/results.php

[4] M. D. Zeiler and R. Fergus. Visualizing and Understanding Convolutional Networks. European Conference on Computer Vision.

[5] M. Lin, Q. Chen, and S. Yan. Network in network. Computing Research Repository, abs/1312.4400, 2013.

[6] K. Simonyan and A. Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition. International Conference on Machine Learning, 2015.

[7] Bharath Raj. a-simple-guide-to-the-versions-of-the-inception-network, 2018.

[8] Christian Szegedy, Sergey Ioffe, Vincent Vanhoucke, Alex Alemi. Inception-v4, Inception-ResNet and
the Impact of Residual Connections on Learning
, 2016.

[9] Sik-Ho Tsang. review-inception-v4-evolved-from-googlenet-merged-with-resnet-idea-image-classification, 2018.

[10] Zbigniew Wojna, Christian Szegedy, Vincent Vanhoucke, Sergey Ioffe, Jonathon Shlens. Rethinking the Inception Architecture for Computer Vision, 2015.

[11] Christian Szegedy, Wei Liu, Yangqing Jia, Pierre Sermanet, Scott Reed, Dragomir Anguelov, Dumitru Erhan, Vincent Vanhoucke, Andrew Rabinovich. Going deeper with convolutions, 2014.

循环神经网络(RNN)

6.1 为什么需要RNN?

​ 时间序列数据是指在不同时间点上收集到的数据,这类数据反映了某一事物、现象等随时间的变化状态或程度。一般的神经网络,在训练数据足够、算法模型优越的情况下,给定特定的x,就能得到期望y。其一般处理单个的输入,前一个输入和后一个输入完全无关,但实际应用中,某些任务需要能够更好的处理序列的信息,即前面的输入和后面的输入是有关系的。比如:

​ 当我们在理解一句话意思时,孤立的理解这句话的每个词不足以理解整体意思,我们通常需要处理这些词连接起来的整个序列; 当我们处理视频的时候,我们也不能只单独的去分析每一帧,而要分析这些帧连接起来的整个序列。为了解决一些这样类似的问题,能够更好的处理序列的信息,RNN就由此诞生了。

6.2 图解RNN基本结构

6.2.1 基本的单层网络结构

​ 在进一步了解RNN之前,先给出最基本的单层网络结构,输入是$x$,经过变换Wx+b和激活函数f得到输出y

6.2.2 图解经典RNN结构

​ 在实际应用中,我们还会遇到很多序列形的数据,如:

  • 自然语言处理问题。x1可以看做是第一个单词,x2可以看做是第二个单词,依次类推。

  • 语音处理。此时,x1、x2、x3……是每帧的声音信号。

  • 时间序列问题。例如每天的股票价格等等。

    其单个序列如下图所示:

    前面介绍了诸如此类的序列数据用原始的神经网络难以建模,基于此,RNN引入了隐状态 $h$ (hidden state), $h​$ 可对序列数据提取特征,接着再转换为输出。

    为了便于理解,先计算 $h_1​$ :

    注:图中的圆圈表示向量,箭头表示对向量做变换。

    RNN中,每个步骤使用的参数$U,W,b$​相同,$h_2$的计算方式和$h_1​$类似,其计算结果如下:

    计算 $h_3$ , $h_4​$ 也相似,可得:

    接下来,计算RNN的输出 $y_1$ ,采用 $Softmax$ 作为激活函数,根据 $y_n=f(Wx+b)$ ,得 $y_1​$ :

    使用和 $y_1​$ 相同的参数 $V,c​$ ,得到 $y_1,y_2,y_3,y_4​$ 的输出结构:

    以上即为最经典的RNN结构,其输入为 $x_1,x_2,x_3,x_4$ ,输出为 $y_1,y_2,y_3,y_4$ ,当然实际中最大值为 $y_n$ ,这里为了便于理解和展示,只计算4个输入和输出。从以上结构可看出,RNN结构的输入和输出等长。

6.2.3 vector-to-sequence结构

​ 有时我们要处理的问题输入是一个单独的值,输出是一个序列。此时,有两种主要建模方式:

​ 方式一:可只在其中的某一个序列进行计算,比如序列第一个进行输入计算,其建模方式如下:

​ 方式二:把输入信息X作为每个阶段的输入,其建模方式如下:

6.2.4 sequence-to-vector结构

​ 有时我们要处理的问题输入是一个序列,输出是一个单独的值,此时通常在最后的一个序列上进行输出变换,其建模如下所示:

6.2.5 Encoder-Decoder结构

​ 原始的sequence-to-sequence结构的RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。

​ 其建模步骤如下:

步骤一:将输入数据编码成一个上下文向量 $c$ ,这部分称为Encoder,得到 $c$ 有多种方式,最简单的方法就是把Encoder的最后一个隐状态赋值给 $c$ ,还可以对最后的隐状态做一个变换得到 $c$ ,也可以对所有的隐状态做变换。其示意如下所示:

步骤二:用另一个RNN网络(我们将其称为Decoder)对其进行编码,方法一是将步骤一中的 $c​$ 作为初始状态输入到Decoder,示意图如下所示:

方法二是将 $c$ 作为Decoder的每一步输入,示意图如下所示:

6.2.6 以上三种结构各有怎样的应用场景

网络结构 结构图示 应用场景举例
1 vs N 1、从图像生成文字,输入为图像的特征,输出为一段句子
2、根据图像生成语音或音乐,输入为图像特征,输出为一段语音或音乐
N vs 1 1、输出一段文字,判断其所属类别
2、输入一个句子,判断其情感倾向
3、输入一段视频,判断其所属类别
N vs M 1、机器翻译,输入一种语言文本序列,输出另外一种语言的文本序列
2、文本摘要,输入文本序列,输出这段文本序列摘要
3、阅读理解,输入文章,输出问题答案
4、语音识别,输入语音序列信息,输出文字序列

6.2.7 图解RNN中的Attention机制

​ 在上述通用的Encoder-Decoder结构中,Encoder把所有的输入序列都编码成一个统一的语义特征 $c​$ 再解码,因此, $c​$ 中必须包含原始序列中的所有信息,它的长度就成了限制模型性能的瓶颈。如机器翻译问题,当要翻译的句子较长时,一个 $c​$ 可能存不下那么多信息,就会造成翻译精度的下降。Attention机制通过在每个时间输入不同的 $c​$ 来解决此问题。

​ 引入了Attention机制的Decoder中,有不同的 $c$ ,每个 $c​$ 会自动选择与当前输出最匹配的上下文信息,其示意图如下所示:

举例,比如输入序列是“我爱中国”,要将此输入翻译成英文:

​ 假如用 $a_{ij}$ 衡量Encoder中第 $j$ 阶段的 $h_j$ 和解码时第 $i$ 阶段的相关性, $a_{ij}$ 从模型中学习得到,和Decoder的第 $i-1$ 阶段的隐状态、Encoder 第 $j$ 个阶段的隐状态有关,比如 $a_{3j}​$ 的计算示意如下所示:

最终Decoder中第 $i$ 阶段的输入的上下文信息 $c_i$ 来自于所有 $h_j$ 对 $a_{ij}$ 的加权和。

其示意图如下图所示:

​ 在Encoder中, $h_1,h_2,h_3,h_4$ 分别代表“我”,“爱”,“中”,“国”所代表信息。翻译的过程中, $c_1$ 会选择和“我”最相关的上下午信息,如上图所示,会优先选择 $a_{11}$ ,以此类推, $c_2$ 会优先选择相关性较大的 $a_{22}$ , $c_3$ 会优先选择相关性较大的 $a_{33},a_{34}$ ,这就是attention机制。

6.3 RNNs典型特点?

  1. RNNs主要用于处理序列数据。对于传统神经网络模型,从输入层到隐含层再到输出层,层与层之间一般为全连接,每层之间神经元是无连接的。但是传统神经网络无法处理数据间的前后关联问题。例如,为了预测句子的下一个单词,一般需要该词之前的语义信息。这是因为一个句子中前后单词是存在语义联系的。
  2. RNNs中当前单元的输出与之前步骤输出也有关,因此称之为循环神经网络。具体的表现形式为当前单元会对之前步骤信息进行储存并应用于当前输出的计算中。隐藏层之间的节点连接起来,隐藏层当前输出由当前时刻输入向量和之前时刻隐藏层状态共同决定。
  3. 标准的RNNs结构图,图中每个箭头代表做一次变换,也就是说箭头连接带有权值。
  4. 在标准的RNN结构中,隐层的神经元之间也是带有权值的,且权值共享。
  5. 理论上,RNNs能够对任何长度序列数据进行处理。但是在实践中,为了降低复杂度往往假设当前的状态只与之前某几个时刻状态相关,下图便是一个典型的RNNs

输入单元(Input units):输入集 $\bigr\{x_0,x_1,...,x_t,x_{t+1},...\bigr\}$ ,

输出单元(Output units):输出集 $\bigr\{y_0,y_1,...,y_t,y_{y+1},...\bigr\}$ ,

隐藏单元(Hidden units):输出集 $\bigr\{s_0,s_1,...,s_t,s_{t+1},...\bigr\}$ 。

图中信息传递特点:

  1. 一条单向流动的信息流是从输入单元到隐藏单元。
  2. 一条单向流动的信息流从隐藏单元到输出单元。
  3. 在某些情况下,RNNs会打破后者的限制,引导信息从输出单元返回隐藏单元,这些被称为“Back Projections”。
  4. 在某些情况下,隐藏层的输入还包括上一时刻隐藏层的状态,即隐藏层内的节点可以自连也可以互连。
  5. 当前单元(cell)输出是由当前时刻输入和上一时刻隐藏层状态共同决定。

6.4 CNN和RNN的区别 ?

类别 特点描述
相同点 1、传统神经网络的扩展。
2、前向计算产生结果,反向计算模型更新。
3、每层神经网络横向可以多个神经元共存,纵向可以有多层神经网络连接。
不同点 1、CNN空间扩展,神经元与特征卷积;RNN时间扩展,神经元与多个时间输出计算
2、RNN可以用于描述时间上连续状态的输出,有记忆功能,CNN用于静态输出

6.5 RNNs和FNNs有什么区别?

  1. 不同于传统的前馈神经网络(FNNs),RNNs引入了定向循环,能够处理输入之间前后关联问题。
  2. RNNs可以记忆之前步骤的训练信息。
    定向循环结构如下图所示

6.6 RNNs训练和传统ANN训练异同点?

相同点

  1. RNNs与传统ANN都使用BP(Back Propagation)误差反向传播算法。

不同点

  1. RNNs网络参数W,U,V是共享的(具体在本章6.2节中已介绍),而传统神经网络各层参数间没有直接联系。
  2. 对于RNNs,在使用梯度下降算法中,每一步的输出不仅依赖当前步的网络,还依赖于之前若干步的网络状态。

6.7 为什么RNN 训练的时候Loss波动很大

​ 由于RNN特有的memory会影响后期其他的RNN的特点,梯度时大时小,learning rate没法个性化的调整,导致RNN在train的过程中,Loss会震荡起伏,为了解决RNN的这个问题,在训练的时候,可以设置临界值,当梯度大于某个临界值,直接截断,用这个临界值作为梯度的大小,防止大幅震荡。

6.8 标准RNN前向输出流程

​ 以 $x$ 表示输入, $h$ 是隐层单元, $o$ 是输出, $L$ 为损失函数, $y$ 为训练集标签。 $t$ 表示 $t$ 时刻的状态, $V,U,W$ 是权值,同一类型的连接权值相同。以下图为例进行说明标准RNN的前向传播算法:

对于 $t$ 时刻:

$$ h^{(t)}=\phi(Ux^{(t)}+Wh^{(t-1)}+b) $$

其中 $\phi()$ 为激活函数,一般会选择tanh函数, $b$ 为偏置。

$t$ 时刻的输出为: $$ o^{(t)}=Vh^{(t)}+c $$

模型的预测输出为:

$$ \widehat{y}^{(t)}=\sigma(o^{(t)}) $$

其中 $\sigma​$ 为激活函数,通常RNN用于分类,故这里一般用softmax函数。

6.9 BPTT算法推导

​ BPTT(back-propagation through time)算法是常用的训练RNN的方法,其本质还是BP算法,只不过RNN处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT的中心思想和BP算法相同,沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛。需要寻优的参数有三个,分别是U、V、W。与BP算法不同的是,其中W和U两个参数的寻优过程需要追溯之前的历史数据,参数V相对简单只需关注目前,那么我们就来先求解参数V的偏导数。

$$ \frac{\partial L^{(t)}}{\partial V}=\frac{\partial L^{(t)}}{\partial o^{(t)}}\cdot \frac{\partial o^{(t)}}{\partial V} $$

RNN的损失也是会随着时间累加的,所以不能只求t时刻的偏导。

$$ L=\sum_{t=1}^{n}L^{(t)} $$ $$ \frac{\partial L}{\partial V}=\sum_{t=1}^{n}\frac{\partial L^{(t)}}{\partial o^{(t)}}\cdot \frac{\partial o^{(t)}}{\partial V} $$

​ W和U的偏导的求解由于需要涉及到历史数据,其偏导求起来相对复杂。为了简化推导过程,我们假设只有三个时刻,那么在第三个时刻 L对W,L对U的偏导数分别为:

$$ \frac{\partial L^{(3)}}{\partial W}=\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial W}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial W}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial h^{(1)}}\frac{\partial h^{(1)}}{\partial W} $$ $$ \frac{\partial L^{(3)}}{\partial U}=\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial U}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial U}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial h^{(1)}}\frac{\partial h^{(1)}}{\partial U} $$

可以观察到,在某个时刻的对W或是U的偏导数,需要追溯这个时刻之前所有时刻的信息。根据上面两个式子得出L在t时刻对W和U偏导数的通式:

$$ \frac{\partial L^{(t)}}{\partial W}=\sum_{k=0}^{t}\frac{\partial L^{(t)}}{\partial o^{(t)}}\frac{\partial o^{(t)}}{\partial h^{(t)}}(\prod_{j=k+1}^{t}\frac{\partial h^{(j)}}{\partial h^{(j-1)}})\frac{\partial h^{(k)}}{\partial W} $$ $$ \frac{\partial L^{(t)}}{\partial U}=\sum_{k=0}^{t}\frac{\partial L^{(t)}}{\partial o^{(t)}}\frac{\partial o^{(t)}}{\partial h^{(t)}}(\prod_{j=k+1}^{t}\frac{\partial h^{(j)}}{\partial h^{(j-1)}})\frac{\partial h^{(k)}}{\partial U} $$

整体的偏导公式就是将其按时刻再一一加起来。

6.9 RNN中为什么会出现梯度消失?

首先来看tanh函数的函数及导数图如下所示:

sigmoid函数的函数及导数图如下所示:

从上图观察可知,sigmoid函数的导数范围是(0,0.25],tanh函数的导数范围是(0,1],他们的导数最大都不大于1。

​ 基于6.8中式(9-10)中的推导,RNN的激活函数是嵌套在里面的,如果选择激活函数为 $tanh$ 或 $sigmoid$ ,把激活函数放进去,拿出中间累乘的那部分可得:

$$ \prod_{j=k+1}^{t}{\frac{\partial{h^{j}}}{\partial{h^{j-1}}}} = \prod_{j=k+1}^{t}{tanh^{'}}\cdot W_{s} $$ $$ \prod_{j=k+1}^{t}{\frac{\partial{h^{j}}}{\partial{h^{j-1}}}} = \prod_{j=k+1}^{t}{sigmoid^{'}}\cdot W_{s} $$

梯度消失现象:基于上式,会发现累乘会导致激活函数导数的累乘,如果取tanh或sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象。

​ 实际使用中,会优先选择tanh函数,原因是tanh函数相对于sigmoid函数来说梯度较大,收敛速度更快且引起梯度消失更慢。

6.10 如何解决RNN中的梯度消失问题?

​ 上节描述的梯度消失是在无限的利用历史数据而造成,但是RNN的特点本来就是能利用历史数据获取更多的可利用信息,解决RNN中的梯度消失方法主要有:

​ 1、选取更好的激活函数,如Relu激活函数。ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了“梯度消失“的发生。但恒为1的导数容易导致“梯度爆炸“,但设定合适的阈值可以解决这个问题。

​ 2、加入BN层,其优点包括可加速收敛、控制过拟合,可以少用或不用Dropout和正则、降低网络对初始化权重不敏感,且能允许使用较大的学习率等。

​ 2、改变传播结构,LSTM结构可以有效解决这个问题。下面将介绍LSTM相关内容。

6.11 LSTM

6.11.1 LSTM的产生原因

​ RNN在处理长期依赖(时间序列上距离较远的节点)时会遇到巨大的困难,因为计算距离较远的节点之间的联系时会涉及雅可比矩阵的多次相乘,会造成梯度消失或者梯度膨胀的现象。为了解决该问题,研究人员提出了许多解决办法,例如ESN(Echo State Network),增加有漏单元(Leaky Units)等等。其中最成功应用最广泛的就是门限RNN(Gated RNN),而LSTM就是门限RNN中最著名的一种。有漏单元通过设计连接间的权重系数,从而允许RNN累积距离较远节点间的长期联系;而门限RNN则泛化了这样的思想,允许在不同时刻改变该系数,且允许网络忘记当前已经累积的信息。

6.11.2 图解标准RNN和LSTM的区别

​ 所有 RNN 都具有一种重复神经网络模块的链式的形式。在标准的 RNN 中,这个重复的模块只有一个非常简单的结构,例如一个 tanh 层,如下图所示:

​ LSTM 同样是这样的结构,但是重复的模块拥有一个不同的结构。不同于单一神经网络层,这里是有四个,以一种非常特殊的方式进行交互。

注:上图图标具体含义如下所示:

​ 上图中,每一条黑线传输着一整个向量,从一个节点的输出到其他节点的输入。粉色的圈代表 pointwise 的操作,诸如向量的和,而黄色的矩阵就是学习到的神经网络层。合在一起的线表示向量的连接,分开的线表示内容被复制,然后分发到不同的位置。

6.11.3 LSTM核心思想图解

​ LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。示意图如下所示:

LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个 pointwise 乘法操作。示意图如下:

LSTM 拥有三个门,分别是忘记层门,输入层门和输出层门,来保护和控制细胞状态。

忘记层门

​ 作用对象:细胞状态 。

​ 作用:将细胞状态中的信息选择性的遗忘。

​ 操作步骤:该门会读取 $h_{t-1}$ 和 $x_t$ ,输出一个在 0 到 1 之间的数值给每个在细胞状态 $C_{t-1}​$ 中的数字。1 表示“完全保留”,0 表示“完全舍弃”。示意图如下:

输入层门

​ 作用对象:细胞状态

​ 作用:将新的信息选择性的记录到细胞状态中。

​ 操作步骤:

​ 步骤一,sigmoid 层称 “输入门层” 决定什么值我们将要更新。

​ 步骤二,tanh 层创建一个新的候选值向量 $\tilde{C}_t$ 加入到状态中。其示意图如下:

​ 步骤三:将 $c_{t-1}$ 更新为 $c_{t}$ 。将旧状态与 $f_t$ 相乘,丢弃掉我们确定需要丢弃的信息。接着加上 $i_t * \tilde{C}_t$ 得到新的候选值,根据我们决定更新每个状态的程度进行变化。其示意图如下:

输出层门
作用对象:隐层 $h_t$

​ 作用:确定输出什么值。

​ 操作步骤:

​ 步骤一:通过sigmoid 层来确定细胞状态的哪个部分将输出。

​ 步骤二:把细胞状态通过 tanh 进行处理,并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。

其示意图如下所示:

6.11.4 LSTM流行的变体

增加peephole 连接

​ 在正常的LSTM结构中,Gers F A 等人提出增加peephole 连接,可以门层接受细胞状态的输入。示意图如下所示:

对忘记门和输入门进行同时确定

​ 不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。示意图如下所示:

Gated Recurrent Unit

​ 由Kyunghyun Cho等人提出的Gated Recurrent Unit (GRU),其将忘记门和输入门合成了一个单一的更新门,同样还混合了细胞状态和隐藏状态,和其他一些改动。其示意图如下:

最终的模型比标准的 LSTM 模型要简单,也是非常流行的变体。

6.12 LSTMs与GRUs的区别

LSTMs与GRUs的区别如图所示:

从上图可以看出,二者结构十分相似,不同在于

  1. new memory都是根据之前state及input进行计算,但是GRUs中有一个reset gate控制之前state的进入量,而在LSTMs里没有类似gate;
  2. 产生新的state的方式不同,LSTMs有两个不同的gate,分别是forget gate (f gate)和input gate(i gate),而GRUs只有一种update gate(z gate);
  3. LSTMs对新产生的state可以通过output gate(o gate)进行调节,而GRUs对输出无任何调节。

6.13 RNNs在NLP中典型应用?

(1)语言模型与文本生成(Language Modeling and Generating Text)

​ 给定一组单词序列,需要根据前面单词预测每个单词出现的可能性。语言模型能够评估某个语句正确的可能性,可能性越大,语句越正确。另一种应用便是使用生成模型预测下一个单词的出现概率,从而利用输出概率的采样生成新的文本。

(2)机器翻译(Machine Translation)

​ 机器翻译是将一种源语言语句变成意思相同的另一种源语言语句,如将英语语句变成同样意思的中文语句。与语言模型关键的区别在于,需要将源语言语句序列输入后,才进行输出,即输出第一个单词时,便需要从完整的输入序列中进行获取。

(3)语音识别(Speech Recognition)

​ 语音识别是指给定一段声波的声音信号,预测该声波对应的某种指定源语言语句以及计算该语句的概率值。

(4)图像描述生成 (Generating Image Descriptions)

​ 同卷积神经网络一样,RNNs已经在对无标图像描述自动生成中得到应用。CNNs与RNNs结合也被应用于图像描述自动生成。

6.13 常见的RNNs扩展和改进模型

6.13.1 Simple RNNs(SRNs)

  1. SRNs是一个三层网络,其在隐藏层增加了上下文单元。下图中的y是隐藏层,u是上下文单元。上下文单元节点与隐藏层中节点的连接是固定的,并且权值也是固定的。上下文节点与隐藏层节点一一对应,并且值是确定的。
  2. 在每一步中,使用标准的前向反馈进行传播,然后使用学习算法进行学习。上下文每一个节点保存其连接隐藏层节点上一步输出,即保存上文,并作用于当前步对应的隐藏层节点状态,即隐藏层的输入由输入层的输出与上一步的自身状态所决定。因此SRNs能够解决标准多层感知机(MLP)无法解决的对序列数据进行预测的问题。
    SRNs网络结构如下图所示:

6.13.2 Bidirectional RNNs

​ Bidirectional RNNs(双向网络)将两层RNNs叠加在一起,当前时刻输出(第t步的输出)不仅仅与之前序列有关,还与之后序列有关。例如:为了预测一个语句中的缺失词语,就需要该词汇的上下文信息。Bidirectional RNNs是一个相对较简单的RNNs,是由两个RNNs上下叠加在一起组成的。输出由前向RNNs和后向RNNs共同决定。如下图所示:

6.13.3 Deep RNNs

​ Deep RNNs与Bidirectional RNNs相似,其也是又多层RNNs叠加,因此每一步的输入有了多层网络。该网络具有更强大的表达与学习能力,但是复杂性也随之提高,同时需要更多的训练数据。Deep RNNs的结构如下图所示:

6.13.4 Echo State Networks(ESNs)

ESNs特点

  1. 它的核心结构为一个随机生成、且保持不变的储备池(Reservoir)。储备池是大规模随机生成稀疏连接(SD通常保持1%~5%,SD表示储备池中互相连接的神经元占总神经元个数N的比例)的循环结构;
  2. 从储备池到输出层的权值矩阵是唯一需要调整的部分;
  3. 简单的线性回归便能够完成网络训练;

ESNs基本思想

​ 使用大规模随机连接的循环网络取代经典神经网络中的中间层,从而简化网络的训练过程。
网络中的参数包括:
(1)W - 储备池中节点间连接权值矩阵;
(2)Win - 输入层到储备池之间连接权值矩阵,表明储备池中的神经元之间是相互连接;
(3)Wback - 输出层到储备池之间的反馈连接权值矩阵,表明储备池会有输出层来的反馈;
(4)Wout - 输入层、储备池、输出层到输出层的连接权值矩阵,表明输出层不仅与储备池连接,还与输入层和自己连接。
(5)Woutbias - 输出层的偏置项。

​ ESNs的结构如下图所示:

6.13.4 Gated Recurrent Unit Recurrent Neural Networks

GRUs是一般的RNNs的变型版本,其主要是从以下两个方面进行改进。

  1. 以语句为例,序列中不同单词处的数据对当前隐藏层状态的影响不同,越前面的影响越小,即每个之前状态对当前的影响进行了距离加权,距离越远,权值越小。

  2. 在产生误差error时,其可能是由之前某一个或者几个单词共同造成,所以应当对对应的单词weight进行更新。GRUs的结构如下图所示。GRUs首先根据当前输入单词向量word vector以及前一个隐藏层状态hidden state计算出update gate和reset gate。再根据reset gate、当前word vector以及前一个hidden state计算新的记忆单元内容(new memory content)。当reset gate为1的时候,new memory content忽略之前所有memory content,最终的memory是由之前的hidden state与new memory content一起决定。

6.13.5 Bidirectional LSTMs

  1. 与bidirectional RNNs 类似,bidirectional LSTMs有两层LSTMs。一层处理过去的训练信息,另一层处理将来的训练信息。
  2. 在bidirectional LSTMs中,通过前向LSTMs获得前向隐藏状态,后向LSTMs获得后向隐藏状态,当前隐藏状态是前向隐藏状态与后向隐藏状态的组合。

6.13.6 Stacked LSTMs

  1. 与deep rnns 类似,stacked LSTMs 通过将多层LSTMs叠加起来得到一个更加复杂的模型。
  2. 不同于bidirectional LSTMs,stacked LSTMs只利用之前步骤的训练信息。

6.13.7 Clockwork RNNs(CW-RNNs)

​ CW-RNNs是RNNs的改良版本,其使用时钟频率来驱动。它将隐藏层分为几个块(组,Group/Module),每一组按照自己规定的时钟频率对输入进行处理。为了降低RNNs的复杂度,CW-RNNs减少了参数数量,并且提高了网络性能,加速网络训练。CW-RNNs通过不同隐藏层模块在不同时钟频率下工作来解决长时依赖问题。将时钟时间进行离散化,不同的隐藏层组将在不同时刻进行工作。因此,所有的隐藏层组在每一步不会全部同时工作,这样便会加快网络的训练。并且,时钟周期小组的神经元不会连接到时钟周期大组的神经元,只允许周期大的神经元连接到周期小的(组与组之间的连接以及信息传递是有向的)。周期大的速度慢,周期小的速度快,因此是速度慢的神经元连速度快的神经元,反之则不成立。

​ CW-RNNs与SRNs网络结构类似,也包括输入层(Input)、隐藏层(Hidden)、输出层(Output),它们之间存在前向连接,输入层到隐藏层连接,隐藏层到输出层连接。但是与SRN不同的是,隐藏层中的神经元会被划分为若干个组,设为 $g​$ ,每一组中的神经元个数相同,设为 $k​$ ,并为每一个组分配一个时钟周期 $T_i\epsilon\{T_1,T_2,...,T_g\}​$ ,每一组中的所有神经元都是全连接,但是组 $j​$ 到组 $i​$ 的循环连接则需要满足 $T_j​$ 大于 $T_i​$ 。如下图所示,将这些组按照时钟周期递增从左到右进行排序,即 $T_1

CW-RNNs的网络结构如下图所示

6.13.8 CNN-LSTMs

  1. 为了同时利用CNN以及LSTMs的优点,CNN-LSTMs被提出。在该模型中,CNN用于提取对象特征,LSTMs用于预测。CNN由于卷积特性,其能够快速而且准确地捕捉对象特征。LSTMs的优点在于能够捕捉数据间的长时依赖性。

参考文献

[1] 何之源.https://zhuanlan.zhihu.com/p/28054589.

[2] http://colah.github.io/posts/2015-08-Understanding-LSTMs/

[3] https://blog.csdn.net/zhaojc1995/article/details/80572098

[4] Graves A. Supervised Sequence Labelling with Recurrent Neural Networks[J]. Studies in Computational Intelligence, 2008, 385.

[5] Graves A. Generating Sequences With Recurrent Neural Networks[J]. Computer Science, 2013.

[6] Greff K , Srivastava R K , Koutník, Jan, et al. LSTM: A Search Space Odyssey[J]. IEEE Transactions on Neural Networks & Learning Systems, 2015, 28(10):2222-2232.

[7] Lanchantin J, Singh R, Wang B, et al. DEEP MOTIF DASHBOARD: VISUALIZING AND UNDERSTANDING GENOMIC SEQUENCES USING DEEP NEURAL NETWORKS.[J]. Pacific Symposium on Biocomputing Pacific Symposium on Biocomputing, 2016, 22:254.

[8] Pascanu R , Mikolov T , Bengio Y . On the difficulty of training Recurrent Neural Networks[J]. 2012.

[9] Hochreiter S. The Vanishing Gradient Problem During Learning Recurrent Neural Nets and Problem Solutions[J]. International Journal of Uncertainty, Fuzziness and Knowledge-Based Systems, 1998, 06(02):-.

[10] Dyer C, Kuncoro A, Ballesteros M, et al. Recurrent Neural Network Grammars[C]// Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies. 2016.

[11] Mulder W D , Bethard S , Moens M F . A survey on the application of recurrent neural networks to statistical language modeling.[M]. Academic Press Ltd. 2015.

[12] Graves A. Generating Sequences With Recurrent Neural Networks[J]. Computer Science, 2013.

[13] Zhang B, Xiong D, Su J. Neural Machine Translation with Deep Attention[J]. IEEE Transactions on Pattern Analysis and Machine Intelligence, 2018, PP(99):1-1.

[14] https://github.com/xuanyuansen/scalaLSTM

[15] Deep Learning,Ian Goodfellow Yoshua Bengio and Aaron Courville,Book in preparation for MIT Press,2016;

[16] http://colah.github.io/posts/2015-08-Understanding-LSTMs/

[17] Greff K, Srivastava R K, Koutník J, et al. LSTM: A Search Space Odyssey[J]. IEEE Transactions on Neural Networks & Learning Systems, 2016, 28(10):2222-2232.

[18] Yao K , Cohn T , Vylomova K , et al. Depth-Gated Recurrent Neural Networks[J]. 2015.

[19] Koutník J, Greff K, Gomez F, et al. A Clockwork RNN[J]. Computer Science, 2014:1863-1871.

[20] Gers F A , Schmidhuber J . Recurrent nets that time and count[C]// Neural Networks, 2000. IJCNN 2000, Proceedings of the IEEE-INNS-ENNS International Joint Conference on. IEEE, 2000.

[21] Li S, Wu C, Hai L, et al. FPGA Acceleration of Recurrent Neural Network Based Language Model[C]// IEEE International Symposium on Field-programmable Custom Computing Machines. 2015.

[22] Mikolov T , Kombrink S , Burget L , et al. Extensions of recurrent neural network language model[C]// Acoustics, Speech and Signal Processing (ICASSP), 2011 IEEE International Conference on. IEEE, 2011.

[23] Graves A . Generating Sequences With Recurrent Neural Networks[J]. Computer Science, 2013.

[24] Sutskever I , Vinyals O , Le Q V . Sequence to Sequence Learning with Neural Networks[J]. 2014.

[25] Liu B, Lane I. Joint Online Spoken Language Understanding and Language Modeling with Recurrent Neural Networks[J]. 2016.

[26] Graves A, Mohamed A R, Hinton G. Speech recognition with deep recurrent neural networks[C]// IEEE International Conference on Acoustics. 2013.

[27] https://cs.stanford.edu/people/karpathy/deepimagesent/

[28] Cho K, Van Merriënboer B, Gulcehre C, et al. Learning phrase representations using RNN encoder-decoder for statistical machine translation[J]. arXiv preprint arXiv:1406.1078, 2014.

卷积神经网络(CNN)

​ 卷积神经网络是一种用来处理局部和整体相关性的计算网络结构,被应用在图像识别、自然语言处理甚至是语音识别领域,因为图像数据具有显著的局部与整体关系,其在图像识别领域的应用获得了巨大的成功。

5.1 卷积神经网络的组成层

​ 以图像分类任务为例,在表5.1所示卷积神经网络中,一般包含5种类型的网络层次结构:

​ 表5.1 卷积神经网络的组成

CNN层次结构 输出尺寸 作用
输入层 $W_1\times H_1\times 3$ 卷积网络的原始输入,可以是原始或预处理后的像素矩阵
卷积层 $W_1\times H_1\times K$ 参数共享、局部连接,利用平移不变性从全局特征图提取局部特征
激活层 $W_1\times H_1\times K$ 将卷积层的输出结果进行非线性映射
池化层 $W_2\times H_2\times K$ 进一步筛选特征,可以有效减少后续网络层次所需的参数量
全连接层 $(W_2 \cdot H_2 \cdot K)\times C$ 将多维特征展平为2维特征,通常低维度特征对应任务的学习目标(类别或回归值)
$W_1\times H_1\times 3$ 对应原始图像或经过预处理的像素值矩阵,3对应RGB图像的通道; $K$ 表示卷积层中卷积核(滤波器)的个数; $W_2\times H_2$ 为池化后特征图的尺度,在全局池化中尺度对应 $1\times 1$ ; $(W_2 \cdot H_2 \cdot K)$ 是将多维特征压缩到1维之后的大小, $C$ 对应的则是图像类别个数。

5.1.1 输入层

​ 输入层(Input Layer)通常是输入卷积神经网络的原始数据或经过预处理的数据,可以是图像识别领域中原始三维的多彩图像,也可以是音频识别领域中经过傅利叶变换的二维波形数据,甚至是自然语言处理中一维表示的句子向量。以图像分类任务为例,输入层输入的图像一般包含RGB三个通道,是一个由长宽分别为 $H$ 和 $W$ 组成的3维像素值矩阵 $H\times W \times 3$ ,卷积网络会将输入层的数据传递到一系列卷积、池化等操作进行特征提取和转化,最终由全连接层对特征进行汇总和结果输出。根据计算能力、存储大小和模型结构的不同,卷积神经网络每次可以批量处理的图像个数不尽相同,若指定输入层接收到的图像个数为 $N$ ,则输入层的输出数据为 $N\times H\times W\times 3$ 。

5.1.2 卷积层

​ 卷积层(Convolution Layer)通常用作对输入层输入数据进行特征提取,通过卷积核矩阵对原始数据中隐含关联性的一种抽象。卷积操作原理上其实是对两张像素矩阵进行点乘求和的数学操作,其中一个矩阵为输入的数据矩阵,另一个矩阵则为卷积核(滤波器或特征矩阵),求得的结果表示为原始图像中提取的特定局部特征。图5.1表示卷积操作过程中的不同填充策略,上半部分采用零填充,下半部分采用有效卷积(舍弃不能完整运算的边缘部分)。
conv-same
​ 图5.1 卷积操作示意图

5.1.3 激活层

​ 激活层(Activation Layer)负责对卷积层抽取的特征进行激活,由于卷积操作是由输入矩阵与卷积核矩阵进行相差的线性变化关系,需要激活层对其进行非线性的映射。激活层主要由激活函数组成,即在卷积层输出结果的基础上嵌套一个非线性函数,让输出的特征图具有非线性关系。卷积网络中通常采用ReLU来充当激活函数(还包括tanh和sigmoid等)ReLU的函数形式如公式(5-1)所示,能够限制小于0的值为0,同时大于等于0的值保持不变。

$$ f(x)=\begin{cases} 0 &\text{if } x<0 0 \\ x &\text{if } x\ge \end{cases} \tag{5-1} $$

5.1.4 池化层

​ 池化层又称为降采样层(Downsampling Layer),作用是对感受域内的特征进行筛选,提取区域内最具代表性的特征,能够有效地降低输出特征尺度,进而减少模型所需要的参数量。按操作类型通常分为最大池化(Max Pooling)、平均池化(Average Pooling)和求和池化(Sum Pooling),它们分别提取感受域内最大、平均与总和的特征值作为输出,最常用的是最大池化。

5.1.5 全连接层

​ 全连接层(Full Connected Layer)负责对卷积神经网络学习提取到的特征进行汇总,将多维的特征输入映射为二维的特征输出,高维表示样本批次,低位常常对应任务目标。

5.2 卷积在图像中有什么直观作用

​ 在卷积神经网络中,卷积常用来提取图像的特征,但不同层次的卷积操作提取到的特征类型是不相同的,特征类型粗分如表5.2所示。
​ 表5.2 卷积提取的特征类型

卷积层次 特征类型
浅层卷积 边缘特征
中层卷积 局部特征
深层卷积 全局特征

图像与不同卷积核的卷积可以用来执行边缘检测、锐化和模糊等操作。表5.3显示了应用不同类型的卷积核(滤波器)后的各种卷积图像。
​ 表5.3 一些常见卷积核的作用

卷积作用 卷积核 卷积后图像
输出原图 $\begin{bmatrix} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 0 \end{bmatrix}$ origin_img
边缘检测(突出边缘差异) $\begin{bmatrix} 1 & 0 & -1 \\ 0 & 0 & 0 \\ -1 & 0 & 1 \end{bmatrix}$ edgeDetect-1
边缘检测(突出中间值) $\begin{bmatrix} -1 & -1 & -1 \\ -1 & 8 & -1 \\ -1 & -1 & -1 \end{bmatrix}$ edgeDetect-2
图像锐化 $\begin{bmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{bmatrix}$ sharpen_img
方块模糊 $\begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix} \times \frac{1}{9}$ box_blur
高斯模糊 $\begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix} \times \frac{1}{16}$ gaussian_blur

5.3 卷积层有哪些基本参数?

​ 卷积层中需要用到卷积核(滤波器或特征检测器)与图像特征矩阵进行点乘运算,利用卷积核与对应的特征感受域进行划窗式运算时,需要设定卷积核对应的大小、步长、个数以及填充的方式,如表5.4所示。

​ 表5.4 卷积层的基本参数

参数名 作用 常见设置
卷积核大小 (Kernel Size) 卷积核的大小定义了卷积的感受野 在过去常设为5,如LeNet-5;现在多设为3,通过堆叠 $3\times3$ 的卷积核来达到更大的感受域
卷积核步长 (Stride) 定义了卷积核在卷积过程中的步长 常见设置为1,表示滑窗距离为1,可以覆盖所有相邻位置特征的组合;当设置为更大值时相当于对特征组合降采样
填充方式 (Padding) 在卷积核尺寸不能完美匹配输入的图像矩阵时需要进行一定的填充策略 设置为’SAME’表示对不足卷积核大小的边界位置进行某种填充(通常零填充)以保证卷积输出维度与与输入维度一致;当设置为’VALID’时则对不足卷积尺寸的部分进行舍弃,输出维度就无法保证与输入维度一致
输入通道数 (In Channels) 指定卷积操作时卷积核的深度 默认与输入的特征矩阵通道数(深度)一致;在某些压缩模型中会采用通道分离的卷积方式
输出通道数 (Out Channels) 指定卷积核的个数 若设置为与输入通道数一样的大小,可以保持输入输出维度的一致性;若采用比输入通道数更小的值,则可以减少整体网络的参数量

卷积操作维度变换公式:

$O_d =\begin{cases} \lceil \frac{(I_d - k_{size})+ 1)}{s}\rceil ,& \text{padding=VALID}\\ \lceil \frac{I_d}{s}\rceil,&\text{padding=SAME} \end{cases}$

其中, $I_d$ 为输入维度, $O_d$ 为输出维度, $k_{size}$ 为卷积核大小, $s$ 为步长

5.4 卷积核有什么类型?

​ 常见的卷积主要是由连续紧密的卷积核对输入的图像特征进行滑窗式点乘求和操作,除此之外还有其他类型的卷积核在不同的任务中会用到,具体分类如表5.5所示。
​ 表5.5 卷积核分类

卷积类别 示意图 作用
标准卷积 image 最常用的卷积核,连续紧密的矩阵形式可以提取图像区域中的相邻像素之间的关联关系, $3\times3$ 的卷积核可以获得 $3\times3$ 像素范围的感受视野
扩张卷积(带孔卷积或空洞卷积) image 引入一个称作扩张率(Dilation Rate)的参数,使同样尺寸的卷积核可以获得更大的感受视野,相应的在相同感受视野的前提下比普通卷积采用更少的参数。同样是 $3\times3$ 的卷积核尺寸,扩张卷积可以提取 $5\times5$ 范围的区域特征,在实时图像分割领域广泛应用
转置卷积 image 先对原始特征矩阵进行填充使其维度扩大到适配卷积目标输出维度,然后进行普通的卷积操作的一个过程,其输入到输出的维度变换关系恰好与普通卷积的变换关系相反,但这个变换并不是真正的逆变换操作,通常称为转置卷积(Transpose Convolution)而不是反卷积(Deconvolution)。转置卷积常见于目标检测领域中对小目标的检测和图像分割领域还原输入图像尺度。
可分离卷积 image 标准的卷积操作是同时对原始图像 $H\times W\times C$ 三个方向的卷积运算,假设有 $K$ 个相同尺寸的卷积核,这样的卷积操作需要用到的参数为 $H\times W\times C\times K$ 个;若将长宽与深度方向的卷积操作分离出变为 $H\times W$ 与 $C$ 的两步卷积操作,则同样的卷积核个数 $K$ ,只需要 $(H\times W + C)\times K$ 个参数,便可得到同样的输出尺度。可分离卷积(Seperable Convolution)通常应用在模型压缩或一些轻量的卷积神经网络中,如MobileNet $^{[1]}$ 、Xception $^{[2]}$ 等

5.5 二维卷积与三维卷积有什么区别?

  • 二维卷积
    二维卷积操作如图5.3所示,为了更直观的说明,分别展示在单通道和多通道输入中,对单个通道输出的卷积操作。在单通道输入的情况下,若输入卷积核尺寸为 $(k_h, k_w, 1)​$ ,卷积核在输入图像的空间维度上进行滑窗操作,每次滑窗和 $(k_h, k_w)​$ 窗口内的值进行卷积操作,得到输出图像中的一个值。在多通道输入的情况下,假定输入图像特征通道数为3,卷积核尺寸则为 $(k_h, k_w, 3)​$ ,每次滑窗与3个通道上的 $(k_h, k_w)​$ 窗口内的所有值进行卷积操作,得到输出图像中的一个值。

image

  • 三维卷积
    3D卷积操作如图所示,同样分为单通道和多通道,且假定只使用1个卷积核,即输出图像仅有一个通道。对于单通道输入,与2D卷积不同之处在于,输入图像多了一个深度(depth)维度,卷积核也多了一个 $k_d​$ 维度,因此3D卷积核的尺寸为 $(k_h, k_w, k_d)​$ ,每次滑窗与 $(k_h, k_w, k_d)​$ 窗口内的值进行相关操作,得到输出3D图像中的一个值。对于多通道输入,则与2D卷积的操作一样,每次滑窗与3个channels上的 $(k_h, k_w, k_d)​$ 窗口内的所有值进行相关操作,得到输出3D图像中的一个值。

image

5.7 有哪些池化方法?

​ 池化操作通常也叫做子采样(Subsampling)或降采样(Downsampling),在构建卷积神经网络时,往往会用在卷积层之后,通过池化来降低卷积层输出的特征维度,有效减少网络参数的同时还可以防止过拟合现象。池化操作可以降低图像维度的原因,本质上是因为图像具有一种“静态性”的属性,这个意思是说在一个图像区域有用的特征极有可能在另一个区域同样有用。因此,为了描述一个大的图像,很直观的想法就是对不同位置的特征进行聚合统计。例如,可以计算图像在固定区域上特征的平均值 (或最大值)来代表这个区域的特征。
​ 表5.6 池化分类

池化类型 示意图 作用
一般池化(General Pooling) max_pooling 通常包括最大池化(Max Pooling)和平均池化(Mean Pooling)。以最大池化为例,池化范围 $(2\times2)$ 和滑窗步长 $(stride=2)$ 相同,仅提取一次相同区域的范化特征。
重叠池化(Overlapping Pooling) overlap_pooling 与一般池化操作相同,但是池化范围 $P_{size}$ 与滑窗步长 $stride$ 关系为 $P_{size}>stride$ ,同一区域内的像素特征可以参与多次滑窗提取,得到的特征表达能力更强,但计算量更大。
空间金字塔池化 $^*$ (Spatial Pyramid Pooling) spatial_pooling 在进行多尺度目标的训练时,卷积层允许输入的图像特征尺度是可变的,紧接的池化层若采用一般的池化方法会使得不同的输入特征输出相应变化尺度的特征,而卷积神经网络中最后的全连接层则无法对可变尺度进行运算,因此需要对不同尺度的输出特征采样到相同输出尺度。

SPPNet $^{[3]}$ 就引入了空间池化的组合,对不同输出尺度采用不同的滑窗大小和步长以确保输出尺度相同 $(win_{size}=\lceil \frac{in}{out}\rceil; stride=\lfloor \frac{in}{out}\rfloor; )$ ,同时用如金字塔式叠加的多种池化尺度组合,以提取更加丰富的图像特征。常用于多尺度训练和目标检测中的区域提议网络(Region Proposal Network)的兴趣区域(Region of Interest)提取

5.8 $1\times1$ 卷积作用?

​ NIN(Network in Network) $^{[4]}​$ 是第一篇探索 $1\times1​$ 卷积核的论文,这篇论文通过在卷积层中使用MLP替代传统线性的卷积核,使单层卷积层内具有非线性映射的能力,也因其网络结构中嵌套MLP子网络而得名NIN。NIN对不同通道的特征整合到MLP自网络中,让不同通道的特征能够交互整合,使通道之间的信息得以流通,其中的MLP子网络恰恰可以用 $1\times1​$ 的卷积进行代替。

​ GoogLeNet $^{[5]}​$ 则采用 $1\times1​$ 卷积核来减少模型的参数量。在原始版本的Inception模块中,由于每一层网络采用了更多的卷积核,大大增加了模型的参数量。此时在每一个较大卷积核的卷积层前引入 $1\times1​$ 卷积,可以通过分离通道与宽高卷积来减少模型参数量。以图5.2为例,在不考虑参数偏置项的情况下,若输入和输出的通道数为 $C_1=16​$ ,则左半边网络模块所需的参数为 $(1\times1+3\times3+5\times5+0)\times C_1\times C_1=8960​$ ;假定右半边网络模块采用的 $1\times1​$ 卷积通道数为 $C_2=8​ {% raw%}$$(满足C_1>C_2)​${% endraw %} ,则右半部分的网络结构所需参数量为 {% raw%}$(1\times1\times (3C_1+C_2)+3\times3\times C_2 +5\times5\times C_2)\times C_1=5248​${% endraw %} ,可以在不改变模型表达能力的前提下大大减少所使用的参数量。

image

​ 图5.2 Inception模块

综上所述, {% raw%}$1\times 1​${% endraw %} 卷积的作用主要为以下两点:

  • 实现信息的跨通道交互和整合。
  • 对卷积核通道数进行降维和升维,减小参数量。

5.9 卷积层和池化层有什么区别?

​ 卷积层核池化层在结构上具有一定的相似性,都是对感受域内的特征进行提取,并且根据步长设置获取到不同维度的输出,但是其内在操作是有本质区别的,如表5.7所示。

卷积层 池化层
结构 零填充时输出维度不变,而通道数改变 通常特征维度会降低,通道数不变
稳定性 输入特征发生细微改变时,输出结果会改变 感受域内的细微变化不影响输出结果
作用 感受域内提取局部关联特征 感受域内提取泛化特征,降低维度
参数量 与卷积核尺寸、卷积核个数相关 不引入额外参数

5.10 卷积核是否一定越大越好?

​ 在早期的卷积神经网络中(如LeNet-5、AlexNet),用到了一些较大的卷积核( {% raw%}$11\times11${% endraw %} 和 {% raw%}$5\times 5${% endraw %} ),受限于当时的计算能力和模型结构的设计,无法将网络叠加得很深,因此卷积网络中的卷积层需要设置较大的卷积核以获取更大的感受域。但是这种大卷积核反而会导致计算量大幅增加,不利于训练更深层的模型,相应的计算性能也会降低。后来的卷积神经网络(VGG、GoogLeNet等),发现通过堆叠2个 {% raw%}$3\times 3${% endraw %} 卷积核可以获得与 {% raw%}$5\times 5${% endraw %} 卷积核相同的感受视野,同时参数量会更少( {% raw%}$3×3×2+1${% endraw %} < {% raw%}$ 5×5×1+1${% endraw %} ), {% raw%}$3\times 3${% endraw %} 卷积核被广泛应用在许多卷积神经网络中。因此可以认为,在大多数情况下通过堆叠较小的卷积核比直接采用单个更大的卷积核会更加有效。

​ 但是,这并不是表示更大的卷积核就没有作用,在某些领域应用卷积神经网络时仍然可以采用较大的卷积核。譬如在自然语言处理领域,由于文本内容不像图像数据可以对特征进行很深层的抽象,往往在该领域的特征提取只需要较浅层的神经网络即可。在将卷积神经网络应用在自然语言处理领域时,通常都是较为浅层的卷积层组成,但是文本特征有时又需要有较广的感受域让模型能够组合更多的特征(如词组和字符),此时直接采用较大的卷积核将是更好的选择。

​ 综上所述,卷积核的大小并没有绝对的优劣,需要视具体的应用场景而定,但是极大和极小的卷积核都是不合适的,单独的 {% raw%}$1\times 1${% endraw %} 极小卷积核只能用作分离卷积而不能对输入的原始特征进行有效的组合,极大的卷积核通常会组合过多的无意义特征从而浪费了大量的计算资源。

5.11 每层卷积是否只能用一种尺寸的卷积核?

​ 经典的神经网络一般都属于层叠式网络,每层仅用一个尺寸的卷积核,如VGG结构中使用了大量的 {% raw%}$3×3${% endraw %} 卷积层。事实上,同一层特征图可以分别使用多个不同尺寸的卷积核,以获得不同尺度的特征,再把这些特征结合起来,得到的特征往往比使用单一卷积核的要好,如GoogLeNet、Inception系列的网络,均是每层使用了多个卷积核结构。如图5.3所示,输入的特征在同一层分别经过 {% raw%}$1×1${% endraw %} 、 {% raw%}$3×3${% endraw %} 和 {% raw%}$5×5${% endraw %} 三种不同尺寸的卷积核,再将分别得到的特征进行整合,得到的新特征可以看作不同感受域提取的特征组合,相比于单一卷积核会有更强的表达能力。

image

​ 图5.3 Inception模块结构

5.12 怎样才能减少卷积层参数量?

减少卷积层参数量的方法可以简要地归为以下几点:

  • 使用堆叠小卷积核代替大卷积核:VGG网络中2个 {% raw%}$3\times 3${% endraw %} 的卷积核可以代替1个 {% raw%}$5\times 5${% endraw %} 的卷积核
  • 使用分离卷积操作:将原本 {% raw%}$K\times K\times C${% endraw %} 的卷积操作分离为 {% raw%}$K\times K\times 1${% endraw %} 和 {% raw%}$1\times1\times C${% endraw %} 的两部分操作
  • 添加 {% raw%}$1\times 1${% endraw %} 的卷积操作:与分离卷积类似,但是通道数可变,在 {% raw%}$K\times K\times C_1${% endraw %} 卷积前添加 {% raw%}$1\times1\times C_2${% endraw %} 的卷积核(满足 {% raw%}$C_2
  • 在卷积层前使用池化操作:池化可以降低卷积层的输入特征维度

5.13 在进行卷积操作时,必须同时考虑通道和区域吗?

​ 标准卷积中,采用区域与通道同时处理的操作,如下图所示:

image

​ 这样做可以简化卷积层内部的结构,每一个输出的特征像素都由所有通道的同一个区域提取而来。

​ 但是这种方式缺乏灵活性,并且在深层的网络结构中使得运算变得相对低效,更为灵活的方式是使区域和通道的卷积分离开来,通道分离(深度分离)卷积网络由此诞生。如下图所示,Xception网络可解决上述问题。

image

​ 我们首先对每一个通道进行各自的卷积操作,有多少个通道就有多少个过滤器。得到新的通道特征矩阵之后,再对这批新通道特征进行标准的 {% raw%}$1×1​${% endraw %} 跨通道卷积操作。

5.14 采用宽卷积的好处有什么?

​ 宽卷积对应的是窄卷积,实际上并不是卷积操作的类型,指的是卷积过程中的填充方法,对应的是’SAME’填充和’VALID’填充。'SAME’填充通常采用零填充的方式对卷积核不满足整除条件的输入特征进行补全,以使卷积层的输出维度保持与输入特征维度一致;'VALID’填充的方式则相反,实际并不进行任何填充,在输入特征边缘位置若不足以进行卷积操作,则对边缘信息进行舍弃,因此在步长为1的情况下该填充方式的卷积层输出特征维度可能会略小于输入特征的维度。此外,由于前一种方式通过补零来进行完整的卷积操作,可以有效地保留原始的输入特征信息。

​ 比如下图左部分为窄卷积。注意到越在边缘的位置被卷积的次数越少。宽卷积可以看作在卷积之前在边缘用0补充,常见有两种情况,一个是全补充,如下图右部分,这样输出大于输入的维度。另一种常用的方法是补充一一部分0值,使得输出和输入的维度一致。

image

5.15 理解转置卷积与棋盘效应

5.15.1 标准卷积

在理解转置卷积之前,需要先理解标准卷积的运算方式。

首先给出一个输入输出结果

image

那是怎样计算的呢?

卷积的时候需要对卷积核进行180的旋转,同时卷积核中心与需计算的图像像素对齐,输出结构为中心对齐像素的一个新的像素值,计算例子如下:

image

这样计算出左上角(即第一行第一列)像素的卷积后像素值。

给出一个更直观的例子,从左到右看,原像素经过卷积由1变成-8。

image

通过滑动卷积核,就可以得到整张图片的卷积结果。

5.15.2 转置卷积

图像的deconvolution过程如下:

image

输入:2x2, 卷积核:4x4, 滑动步长:3, 输出:7x7

过程如下:

  1. 输入图片每个像素进行一次full卷积,根据full卷积大小计算可以知道每个像素的卷积后大小为 1+4-1=4, 即4x4大小的特征图,输入有4个像素所以4个4x4的特征图

  2. 将4个特征图进行步长为3的相加; 输出的位置和输入的位置相同。步长为3是指每隔3个像素进行相加,重叠部分进行相加,即输出的第1行第4列是由红色特阵图的第一行第四列与绿色特征图的第一行第一列相加得到,其他如此类推。

    可以看出翻卷积的大小是由卷积核大小与滑动步长决定, in是输入大小, k是卷积核大小, s是滑动步长, out是输出大小 得到 out = (in - 1) * s + k 上图过程就是, (2 - 1) * 3 + 4 = 7。

5.15.3 棋盘效应

5.16 卷积神经网络的参数设置

​ 卷积神经网络中常见的参数在其他类型的神经网络中也是类似的,但是参数的设置还得结合具体的任务才能设置在合理的范围,具体的参数列表如表XX所示。
​ 表XX 卷积神经网络常见参数

参数名 常见设置 参数说明
学习率(Learning Rate) {% raw%}$0-1${% endraw %} 反向传播网络中更新权值矩阵的步长,在一些常见的网络中会在固定迭代次数或模型不再收敛后对学习率进行指数下降(如 {% raw%}$lr=lr\times 0.1${% endraw %} )。当学习率越大计算误差对权值矩阵的影响越大,容易在某个局部最优解附近震荡;越小的学习率对网络权值的更新越精细,但是需要花费更多的时间去迭代
批次大小(Batch Size) {% raw%}$1-N${% endraw %} 批次大小指定一次性流入模型的数据样本个数,根据任务和计算性能限制判断实际取值,在一些图像任务中往往由于计算性能和存储容量限制只能选取较小的值。在相同迭代次数的前提下,数值越大模型越稳定,泛化能力越强,损失值曲线越平滑,模型也更快地收敛,但是每次迭代需要花费更多的时间
数据轮次(Epoch) {% raw%}$1-N${% endraw %} 数据轮次指定所有训练数据在模型中训练的次数,根据数据集规模和分布情况会设置为不同的值。当模型较为简单或训练数据规模较小时,通常轮次不宜过高,否则模型容易过拟合;模型较为复杂或训练数据规模足够大时,可适当提高数据的训练轮次。
权重衰减系数(Weight Decay) {% raw%}$0-0.001${% endraw %} 模型训练过程中反向传播权值更新的权重衰减值

5.17 提高卷积神经网络的泛化能力

​ 卷积神经网络与其他类型的神经网络类似,在采用反向传播进行训练的过程中比较依赖输入的数据分布,当数据分布较为极端的情况下容易导致模型欠拟合或过拟合,表XX记录了提高卷积网络泛化能力的方法。
​ 表XX 提高卷积网络化能力的方法

方法 说明
使用更多数据 在有条件的前提下,尽可能多地获取训练数据是最理想的方法,更多的数据可以让模型得到充分的学习,也更容易提高泛化能力
使用更大批次 在相同迭代次数和学习率的条件下,每批次采用更多的数据将有助于模型更好的学习到正确的模式,模型输出结果也会更加稳定
调整数据分布 大多数场景下的数据分布是不均匀的,模型过多地学习某类数据容易导致其输出结果偏向于该类型的数据,此时通过调整输入的数据分布可以一定程度提高泛化能力
调整目标函数 在某些情况下,目标函数的选择会影响模型的泛化能力,如目标函数 {% raw%}$f(y,y')=|y-y'|${% endraw %} 在某类样本已经识别较为准确而其他样本误差较大的侵害概况下,不同类别在计算损失结果的时候距离权重是相同的,若将目标函数改成 {% raw%}$f(y,y')=(y-y')^2${% endraw %} 则可以使误差小的样本计算损失的梯度比误差大的样本更小,进而有效地平衡样本作用,提高模型泛化能力
调整网络结构 在浅层卷积神经网络中,参数量较少往往使模型的泛化能力不足而导致欠拟合,此时通过叠加卷积层可以有效地增加网络参数,提高模型表达能力;在深层卷积网络中,若没有充足的训练数据则容易导致模型过拟合,此时通过简化网络结构减少卷积层数可以起到提高模型泛化能力的作用
数据增强 数据增强又叫数据增广,在有限数据的前提下通过平移、旋转、加噪声等一些列变换来增加训练数据,同类数据的表现形式也变得更多样,有助于模型提高泛化能力,需要注意的是数据变化应尽可能不破坏元数数据的主体特征(如在图像分类任务中对图像进行裁剪时不能将分类主体目标裁出边界)。
权值正则化 权值正则化就是通常意义上的正则化,一般是在损失函数中添加一项权重矩阵的正则项作为惩罚项,用来惩罚损失值较小时网络权重过大的情况,此时往往是网络权值过拟合了数据样本(如 {% raw%}$Loss=f(WX+b,y')+\frac{\lambda}{\eta}\sum{|W|}${% endraw %} )。
屏蔽网络节点 该方法可以认为是网络结构上的正则化,通过随机性地屏蔽某些神经元的输出让剩余激活的神经元作用,可以使模型的容错性更强。

对大多数神经网络模型同样通用

5.18 卷积神经网络在不同领域的应用

​ 卷积神经网络中的卷积操作是其关键组成,而卷积操作只是一种数学运算方式,实际上对不同类型的数值表示数据都是通用的,尽管这些数值可能表示的是图像像素值、文本序列中单个字符或是语音片段中单字的音频。只要使原始数据能够得到有效地数值化表示,卷积神经网络能够在不同的领域中得到应用,要关注的是如何将卷积的特性更好地在不同领域中应用,如表XX所示。
​ 表XX 卷积神经网络不同领域的应用

应用领域 输入数据图示 说明
图像处理 image_process 卷积神经网络在图像处理领域有非常广泛的应用,这是因为图像数据本身具有的局部完整性非常
自然语言处理 NLP
语音处理 audio_process

5.18.1 联系

​ 自然语言处理是对一维信号(词序列)做操作。
​ 计算机视觉是对二维(图像)或三维(视频流)信号做操作。

5.18.2 区别

​ 自然语言处理的输入数据通常是离散取值(例如表示一个单词或字母通常表示为词典中的one hot向量),计算机视觉则是连续取值(比如归一化到0,1之间的灰度值)。CNN有两个主要特点,区域不变性(location invariance)和组合性(Compositionality)。

  1. 区域不变性:滤波器在每层的输入向量(图像)上滑动,检测的是局部信息,然后通过pooling取最大值或均值。pooling这步综合了局部特征,失去了每个特征的位置信息。这很适合基于图像的任务,比如要判断一幅图里有没有猫这种生物,你可能不会去关心这只猫出现在图像的哪个区域。但是在NLP里,词语在句子或是段落里出现的位置,顺序,都是很重要的信息。
  2. 局部组合性:CNN中,每个滤波器都把较低层的局部特征组合生成较高层的更全局化的特征。这在CV里很好理解,像素组合成边缘,边缘生成形状,最后把各种形状组合起来得到复杂的物体表达。在语言里,当然也有类似的组合关系,但是远不如图像来的直接。而且在图像里,相邻像素必须是相关的,相邻的词语却未必相关。

5.19 卷积神经网络凸显共性的方法?

5.19.1 局部连接

​ 我们首先了解一个概念,感受野,即每个神经元仅与输入神经元相连接的一块区域。
在图像卷积操作中,神经元在空间维度上是局部连接,但在深度上是全连接。局部连接的思想,是受启发于生物学里的视觉系统结构,视觉皮层的神经元就是仅用局部接受信息。对于二维图像,局部像素关联性较强。这种局部连接保证了训练后的滤波器能够对局部特征有最强的响应,使神经网络可以提取数据的局部特征;
下图是一个很经典的图示,左边是全连接,右边是局部连接。

image

对于一个1000 × 1000的输入图像而言,如果下一个隐藏层的神经元数目为10^6个,采用全连接则有1000 × 1000 × 10^6 = 10^12个权值参数,如此巨大的参数量几乎难以训练;而采用局部连接,隐藏层的每个神经元仅与图像中10 × 10的局部图像相连接,那么此时的权值参数数量为10 × 10 × 10^6 = 10^8,将直接减少4个数量级。

5.19.2 权值共享

​ 权值共享,即计算同一深度的神经元时采用的卷积核参数是共享的。权值共享在一定程度上讲是有意义的,是由于在神经网络中,提取的底层边缘特征与其在图中的位置无关。但是在另一些场景中是无意的,如在人脸识别任务,我们期望在不同的位置学到不同的特征。
需要注意的是,权重只是对于同一深度切片的神经元是共享的。在卷积层中,通常采用多组卷积核提取不同的特征,即对应的是不同深度切片的特征,而不同深度切片的神经元权重是不共享。相反,偏置这一权值对于同一深度切片的所有神经元都是共享的。
权值共享带来的好处是大大降低了网络的训练难度。如下图,假设在局部连接中隐藏层的每一个神经元连接的是一个10 × 10的局部图像,因此有10 × 10个权值参数,将这10 × 10个权值参数共享给剩下的神经元,也就是说隐藏层中10^6个神经元的权值参数相同,那么此时不管隐藏层神经元的数目是多少,需要训练的参数就是这 10 × 10个权值参数(也就是卷积核的大小)。

image

这里就体现了卷积神经网络的奇妙之处,使用少量的参数,却依然能有非常出色的性能。上述仅仅是提取图像一种特征的过程。如果要多提取出一些特征,可以增加多个卷积核,不同的卷积核能够得到图像不同尺度下的特征,称之为特征图(feature map)。

5.19.3 池化操作

池化操作与多层次结构一起,实现了数据的降维,将低层次的局部特征组合成为较高层次的特征,从而对整个图片进行表示。如下图:

image

5.20 全连接、局部连接、全卷积与局部卷积

​ 大多数神经网络中高层网络通常会采用全连接层(Global Connected Layer),通过多对多的连接方式对特征进行全局汇总,可以有效地提取全局信息。但是全连接的方式需要大量的参数,是神经网络中最占资源的部分之一,因此就需要由局部连接(Local Connected Layer),仅在局部区域范围内产生神经元连接,能够有效地减少参数量。根据卷积操作的作用范围可以分为全卷积(Global Convolution)和局部卷积(Local Convolution)。实际上这里所说的全卷积就是标准卷积,即在整个输入特征维度范围内采用相同的卷积核参数进行运算,全局共享参数的连接方式可以使神经元之间的连接参数大大减少;局部卷积又叫平铺卷积(Tiled Convolution)或非共享卷积(Unshared Convolution),是局部连接与全卷积的折衷。四者的比较如表XX所示。
​ 表XX 卷积网络中连接方式的对比

连接方式 示意图 说明
全连接 full-connected 层间神经元完全连接,每个输出神经元可以获取到所有输入神经元的信息,有利于信息汇总,常置于网络末层;连接与连接之间独立参数,大量的连接大大增加模型的参数规模。
局部连接 local-connected 层间神经元只有局部范围内的连接,在这个范围内采用全连接的方式,超过这个范围的神经元则没有连接;连接与连接之间独立参数,相比于全连接减少了感受域外的连接,有效减少参数规模
全卷积 convolution 层间神经元只有局部范围内的连接,在这个范围内采用全连接的方式,连接所采用的参数在不同感受域之间共享,有利于提取特定模式的特征;相比于局部连接,共用感受域之间的参数可以进一步减少参数量。
局部卷积 local-conv 层间神经元只有局部范围内的连接,感受域内采用全连接的方式,而感受域之间间隔采用局部连接与全卷积的连接方式;相比与全卷积成倍引入额外参数,但有更强的灵活性和表达能力;相比于局部连接,可以有效控制参数量

5.21 局部卷积的应用

并不是所有的卷积都会进行权重共享,在某些特定任务中,会使用不权重共享的卷积。下面通过人脸这一任务来进行讲解。在读人脸方向的一些paper时,会发现很多都会在最后加入一个Local Connected Conv,也就是不进行权重共享的卷积层。总的来说,这一步的作用就是使用3D模型来将人脸对齐,从而使CNN发挥最大的效果。
image

截取论文中的一部分图,经过3D对齐以后,形成的图像均是152×152,输入到上述的网络结构中。该结构的参数如下:

Conv:32个11×11×3的卷积核,

Max-pooling: 3×3,stride=2,

Conv: 16个9×9的卷积核,

Local-Conv: 16个9×9的卷积核,

Local-Conv: 16个7×7的卷积核,

Local-Conv: 16个5×5的卷积核,

Fully-connected: 4096维,

Softmax: 4030维。

前三层的目的在于提取低层次的特征,比如简单的边和纹理。其中Max-pooling层使得卷积的输出对微小的偏移情况更加鲁棒。但不能使用更多的Max-pooling层,因为太多的Max-pooling层会使得网络损失图像信息。全连接层将上一层的每个单元和本层的所有单元相连,用来捕捉人脸图像不同位置特征之间的相关性。最后使用softmax层用于人脸分类。
中间三层都是使用参数不共享的卷积核,之所以使用参数不共享,有如下原因:

(1)对齐的人脸图片中,不同的区域会有不同的统计特征,因此并不存在特征的局部稳定性,所以使用相同的卷积核会导致信息的丢失。

(2)不共享的卷积核并不增加inference时特征的计算量,仅会增加训练时的计算量。
使用不共享的卷积核,由于需要训练的参数量大大增加,因此往往需要通过其他方法增加数据量。

5.22 NetVLAD池化 (贡献者:熊楚原-中国人民大学)

NetVLAD是论文[15]提出的一个局部特征聚合的方法。

在传统的网络里面,例如VGG啊,最后一层卷积层输出的特征都是类似于Batchsize x 3 x 3 x 512的这种东西,然后会经过FC聚合,或者进行一个Global Average Pooling(NIN里的做法),或者怎么样,变成一个向量型的特征,然后进行Softmax or 其他的Loss。

这种方法说简单点也就是输入一个图片或者什么的结构性数据,然后经过特征提取得到一个长度固定的向量,之后可以用度量的方法去进行后续的操作,比如分类啊,检索啊,相似度对比等等。

那么NetVLAD考虑的主要是最后一层卷积层输出的特征这里,我们不想直接进行欠采样或者全局映射得到特征,对于最后一层输出的W x H x D,设计一个新的池化,去聚合一个“局部特征“,这即是NetVLAD的作用。

NetVLAD的一个输入是一个W x H x D的图像特征,例如VGG-Net最后的3 x 3 x 512这样的矩阵,在网络中还需加一个维度为Batchsize。

NetVLAD还需要另输入一个标量K即表示VLAD的聚类中心数量,它主要是来构成一个矩阵C,是通过原数据算出来的每一个 {% raw%}$W \times H${% endraw %} 特征的聚类中心,C的shape即 {% raw%}$C: K \times D${% endraw %} ,然后根据三个输入,VLAD是计算下式的V:

$$ V(j, k) = \sum_{i=1}^{N}{a_k(x_i)(x_i(j) - c_k(j))} $$ 其中j表示维度,从1到D,可以看到V的j是和输入与c对应的,对每个类别k,都对所有的x进行了计算,如果 {% raw%}$x_i${% endraw %} 属于当前类别k, {% raw%}$a_k=1${% endraw %} ,否则 {% raw%}$a_k=0${% endraw %} ,计算每一个x和它聚类中心的残差,然后把残差加起来,即是每个类别k的结果,最后分别L2正则后拉成一个长向量后再做L2正则,正则非常的重要,因为这样才能统一所有聚类算出来的值,而残差和的目的主要是消减不同聚类上的分布不均,两者共同作用才能得到最后正常的输出。

输入与输出如下图所示:

image

中间得到的K个D维向量即是对D个x都进行了与聚类中心计算残差和的过程,最终把K个D维向量合起来后进行即得到最终输出的 {% raw%}$K \times D${% endraw %} 长度的一维向量。

而VLAD本身是不可微的,因为上面的a要么是0要么是1,表示要么当前描述x是当前聚类,要么不是,是个离散的,NetVLAD为了能够在深度卷积网络里使用反向传播进行训练,对a进行了修正。

那么问题就是如何重构一个a,使其能够评估当前的这个x和各个聚类的关联程度?用softmax来得到:

$$ a_k = \frac{e^{W_k^T x_i + b_k}}{e^{W_{k’}^T x_i + b_{k’}}} $$ 将这个把上面的a替换后,即是NetVLAD的公式,可以进行反向传播更新参数。 所以一共有三个可训练参数,上式a中的 {% raw%}$W: K \times D${% endraw %} ,上式a中的 {% raw%}$b: K \times 1${% endraw %} ,聚类中心 {% raw%}$c: K \times D${% endraw %} ,而原始VLAD只有一个参数c。

最终池化得到的输出是一个恒定的K x D的一维向量(经过了L2正则),如果带Batchsize,输出即为Batchsize x (K x D)的二维矩阵。

NetVLAD作为池化层嵌入CNN网络即如下图所示,

image

原论文中采用将传统图像检索方法VLAD进行改进后应用在CNN的池化部分作为一种另类的局部特征池化,在场景检索上取得了很好的效果。

后续相继又提出了ActionVLAD、ghostVLAD等改进。

参考文献

[1] 卷积神经网络研究综述[J]. 计算机学报, 2017, 40(6):1229-1251.

[2] 常亮, 邓小明, 周明全,等. 图像理解中的卷积神经网络[J]. 自动化学报, 2016, 42(9):1300-1312.

[3] Chua L O. CNN: A Paradigm for Complexity[M]// CNN a paradigm for complexity /. 1998.

[4] He K, Gkioxari G, Dollar P, et al. Mask R-CNN[J]. IEEE Transactions on Pattern Analysis & Machine Intelligence, 2017, PP(99):1-1.

[5] Hoochang S, Roth H R, Gao M, et al. Deep Convolutional Neural Networks for Computer-Aided Detection: CNN Architectures, Dataset Characteristics and Transfer Learning[J]. IEEE Transactions on Medical Imaging, 2016, 35(5):1285-1298.

[6] 许可. 卷积神经网络在图像识别上的应用的研究[D]. 浙江大学, 2012.

[7] 陈先昌. 基于卷积神经网络的深度学习算法与应用研究[D]. 浙江工商大学, 2014.

[8] CS231n Convolutional Neural Networks for Visual Recognition, Stanford

[9] Machine Learning is Fun! Part 3: Deep Learning and Convolutional Neural Networks

[10] cs231n 动态卷积图:http://cs231n.github.io/assets/conv-demo/index.html

[11] Krizhevsky A, Sutskever I, Hinton G E. Imagenet classification with deep convolutional neural networks[C]//Advances in neural information processing systems. 2012: 1097-1105.

[12] Sun Y, Wang X, Tang X. Deep learning face representation from predicting 10,000 classes[C]//Computer Vision and Pattern Recognition (CVPR), 2014 IEEE Conference on. IEEE, 2014: 1891-1898.

[13] 魏秀参.解析深度学习——卷积神经网络原理与视觉实践[M].电子工业出版社, 2018

[14] Jianxin W U , Gao B B , Wei X S , et al. Resource-constrained deep learning: challenges and practices[J]. Scientia Sinica(Informationis), 2018.

[15] Arandjelovic R , Gronat P , Torii A , et al. [IEEE 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) - Las Vegas, NV, USA (2016.6.27-2016.6.30)] 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) - NetVLAD: CNN Architecture for Weakly Supervised Place Recognition[C]// 2016:5297-5307.

生成对抗网络

7.1 GAN基本概念

7.1.1 如何通俗理解GAN?

​ 生成对抗网络(GAN, Generative adversarial network)自从2014年被Ian Goodfellow提出以来,掀起来了一股研究热潮。GAN由生成器和判别器组成,生成器负责生成样本,判别器负责判断生成器生成的样本是否为真。生成器要尽可能迷惑判别器,而判别器要尽可能区分生成器生成的样本和真实样本。

​ 在GAN的原作[1]中,作者将生成器比喻为印假钞票的犯罪分子,判别器则类比为警察。犯罪分子努力让钞票看起来逼真,警察则不断提升对于假钞的辨识能力。二者互相博弈,随着时间的进行,都会越来越强。那么类比于图像生成任务,生成器不断生成尽可能逼真的假图像。判别器则判断图像是否是真实的图像,还是生成的图像,二者不断博弈优化。最终生成器生成的图像使得判别器完全无法判别真假。

7.1.2 GAN的形式化表达

​ 上述例子只是简要介绍了一下GAN的思想,下面对于GAN做一个形式化的,更加具体的定义。通常情况下,无论是生成器还是判别器,我们都可以用神经网络来实现。那么,我们可以把通俗化的定义用下面这个模型来表示:
GAN网络结构

​ 上述模型左边是生成器G,其输入是 $z$ ,对于原始的GAN, $z$ 是由高斯分布随机采样得到的噪声。噪声 $z$ 通过生成器得到了生成的假样本。

​ 生成的假样本与真实样本放到一起,被随机抽取送入到判别器D,由判别器去区分输入的样本是生成的假样本还是真实的样本。整个过程简单明了,生成对抗网络中的“生成对抗”主要体现在生成器和判别器之间的对抗。

7.1.3 GAN的目标函数是什么?

​ 对于上述神经网络模型,如果想要学习其参数,首先需要一个目标函数。GAN的目标函数定义如下:

$$ \mathop {\min }\limits_G \mathop {\max }\limits_D V(D,G) = {\rm E}_{x\sim{p_{data}(x)}}[\log D(x)] + {\rm E}_{z\sim{p_z}(z)}[\log (1 - D(G(z)))] $$

​ 这个目标函数可以分为两个部分来理解:

​ 第一部分:判别器的优化通过 $\mathop {\max}\limits_D V(D,G)$ 实现, $V(D,G)$ 为判别器的目标函数,其第一项 ${\rm E}_{x\sim{p_{data}(x)}}[\log D(x)]$ 表示对于从真实数据分布 中采用的样本 ,其被判别器判定为真实样本概率的数学期望。对于真实数据分布 中采样的样本,其预测为正样本的概率当然是越接近1越好。因此希望最大化这一项。第二项 ${\rm E}_{z\sim{p_z}(z)}[\log (1 - D(G(z)))]$ 表示:对于从噪声 $P_z(z)​$ 分布当中采样得到的样本,经过生成器生成之后得到的生成图片,然后送入判别器,其预测概率的负对数的期望,这个值自然是越大越好,这个值越大, 越接近0,也就代表判别器越好。

​ 第二部分:生成器的优化通过 $\mathop {\min }\limits_G({\mathop {\max }\limits_D V(D,G)})$ 来实现。注意,生成器的目标不是 $\mathop {\min }\limits_GV(D,G)$ ,即生成器不是最小化判别器的目标函数,二是最小化判别器目标函数的最大值,判别器目标函数的最大值代表的是真实数据分布与生成数据分布的JS散度(详情可以参阅附录的推导),JS散度可以度量分布的相似性,两个分布越接近,JS散度越小。

7.1.4 GAN的目标函数和交叉熵有什么区别?

​ 判别器目标函数写成离散形式即为:

$$ V(D,G)=-\frac{1}{m}\sum_{i=1}^{i=m}logD(x^i)-\frac{1}{m}\sum_{i=1}^{i=m}log(1-D(\tilde{x}^i)) $$

​ 可以看出,这个目标函数和交叉熵是一致的,即判别器的目标是最小化交叉熵损失,生成器的目标是最小化生成数据分布和真实数据分布的JS散度


[1]: Goodfellow, Ian, et al. “Generative adversarial nets.” Advances in neural information processing systems. 2014.

7.1.5 GAN的Loss为什么降不下去?

​ 对于很多GAN的初学者在实践过程中可能会纳闷,为什么GAN的Loss一直降不下去。GAN到底什么时候才算收敛?其实,作为一个训练良好的GAN,其Loss就是降不下去的。衡量GAN是否训练好了,只能由人肉眼去看生成的图片质量是否好。不过,对于没有一个很好的评价是否收敛指标的问题,也有许多学者做了一些研究,后文提及的WGAN就提出了一种新的Loss设计方式,较好的解决了难以判断收敛性的问题。下面我们分析一下GAN的Loss为什么降不下去?
​ 对于判别器而言,GAN的Loss如下:

$$ \mathop {\min }\limits_G \mathop {\max }\limits_D V(D,G) = {\rm E}_{x\sim{p_{data}(x)}}[\log D(x)] + {\rm E}_{z\sim{p_z}(z)}[\log (1 - D(G(z)))] $$

​ 从 $\mathop {\min }\limits_G \mathop {\max }\limits_D V(D,G)​$ 可以看出,生成器和判别器的目的相反,也就是说两个生成器网络和判别器网络互为对抗,此消彼长。不可能Loss一直降到一个收敛的状态。

  • 对于生成器,其Loss下降快,很有可能是判别器太弱,导致生成器很轻易的就"愚弄"了判别器。
  • 对于判别器,其Loss下降快,意味着判别器很强,判别器很强则说明生成器生成的图像不够逼真,才使得判别器轻易判别,导致Loss下降很快。

​ 也就是说,无论是判别器,还是生成器。loss的高低不能代表生成器的好坏。一个好的GAN网络,其GAN Loss往往是不断波动的。

​ 看到这里可能有点让人绝望,似乎判断模型是否收敛就只能看生成的图像质量了。实际上,后文探讨的WGAN,提出了一种新的loss度量方式,让我们可以通过一定的手段来判断模型是否收敛。

7.1.6 生成式模型、判别式模型的区别?

​ 对于机器学习模型,我们可以根据模型对数据的建模方式将模型分为两大类,生成式模型和判别式模型。如果我们要训练一个关于猫狗分类的模型,对于判别式模型,只需要学习二者差异即可。比如说猫的体型会比狗小一点。而生成式模型则不一样,需要学习猫张什么样,狗张什么样。有了二者的长相以后,再根据长相去区分。具体而言:

  • 生成式模型:由数据学习联合概率分布P(X,Y), 然后由P(Y|X)=P(X,Y)/P(X)求出概率分布P(Y|X)作为预测的模型。该方法表示了给定输入X与产生输出Y的生成关系

  • 判别式模型:由数据直接学习决策函数Y=f(X)或条件概率分布P(Y|X)作为预测模型,即判别模型。判别方法关心的是对于给定的输入X,应该预测什么样的输出Y。

​ 对于上述两种模型,从文字上理解起来似乎不太直观。我们举个例子来阐述一下,对于性别分类问题,分别用不同的模型来做:

​ 1)如果用生成式模型:可以训练一个模型,学习输入人的特征X和性别Y的关系。比如现在有下面一批数据:

Y(性别) 0 1
X(特征) 0 1/4 3/4
1 3/4 1/4

​ 这个数据可以统计得到,即统计人的特征X=0,1….的时候,其类别为Y=0,1的概率。统计得到上述联合概率分布P(X, Y)后,可以学习一个模型,比如让二维高斯分布去拟合上述数据,这样就学习到了X,Y的联合分布。在预测时,如果我们希望给一个输入特征X,预测其类别,则需要通过贝叶斯公式得到条件概率分布才能进行推断:

$$ P(Y|X)={\frac{P(X,Y)}{P(X)}}={\frac{P(X,Y)}{P(X|Y)P(Y)}} $$

​ 2)如果用判别式模型:可以训练一个模型,输入人的特征X,这些特征包括人的五官,穿衣风格,发型等。输出则是对于性别的判断概率,这个概率服从一个分布,分布的取值只有两个,要么男,要么女,记这个分布为Y。这个过程学习了一个条件概率分布P(Y|X),即输入特征X的分布已知条件下,Y的概率分布。

​ 显然,从上面的分析可以看出。判别式模型似乎要方便很多,因为生成式模型要学习一个X,Y的联合分布往往需要很多数据,而判别式模型需要的数据则相对少,因为判别式模型更关注输入特征的差异性。不过生成式既然使用了更多数据来生成联合分布,自然也能够提供更多的信息,现在有一个样本(X,Y),其联合概率P(X,Y)经过计算特别小,那么可以认为这个样本是异常样本。这种模型可以用来做outlier detection。

7.1.7 什么是mode collapsing?

​ 某个模式(mode)出现大量重复样本,例如:
model collapsing
​ 上图左侧的蓝色五角星表示真实样本空间,黄色的是生成的。生成样本缺乏多样性,存在大量重复。比如上图右侧中,红框里面人物反复出现。

7.1.8 如何解决mode collapsing?

方法一:针对目标函数的改进方法

​ 为了避免前面提到的由于优化maxmin导致mode跳来跳去的问题,UnrolledGAN采用修改生成器loss来解决。具体而言,UnrolledGAN在更新生成器时更新k次生成器,参考的Loss不是某一次的loss,是判别器后面k次迭代的loss。注意,判别器后面k次迭代不更新自己的参数,只计算loss用于更新生成器。这种方式使得生成器考虑到了后面k次判别器的变化情况,避免在不同mode之间切换导致的模式崩溃问题。此处务必和迭代k次生成器,然后迭代1次判别器区分开[8]。DRAGAN则引入博弈论中的无后悔算法,改造其loss以解决mode collapse问题[9]。前文所述的EBGAN则是加入VAE的重构误差以解决mode collapse。

方法二:针对网络结构的改进方法

​ Multi agent diverse GAN(MAD-GAN)采用多个生成器,一个判别器以保障样本生成的多样性。具体结构如下:

​ 相比于普通GAN,多了几个生成器,且在loss设计的时候,加入一个正则项。正则项使用余弦距离惩罚三个生成器生成样本的一致性。

​ MRGAN则添加了一个判别器来惩罚生成样本的mode collapse问题。具体结构如下:

​ 输入样本 $x​$ 通过一个Encoder编码为隐变量 $E(x)​$ ,然后隐变量被Generator重构,训练时,Loss有三个。 $D_M​$ 和 $R​$ (重构误差)用于指导生成real-like的样本。而 $D_D​$ 则对 $E(x)​$ 和 $z​$ 生成的样本进行判别,显然二者生成样本都是fake samples,所以这个判别器主要用于判断生成的样本是否具有多样性,即是否出现mode collapse。

方法三:Mini-batch Discrimination

​ Mini-batch discrimination在判别器的中间层建立一个mini-batch layer用于计算基于L1距离的样本统计量,通过建立该统计量,实现了一个batch内某个样本与其他样本有多接近。这个信息可以被判别器利用到,从而甄别出哪些缺乏多样性的样本。对生成器而言,则要试图生成具有多样性的样本。

7.2 GAN的生成能力评价

7.2.1 如何客观评价GAN的生成能力?

​ 最常见评价GAN的方法就是主观评价。主观评价需要花费大量人力物力,且存在以下问题:

  • 评价带有主管色彩,有些bad case没看到很容易造成误判

  • 如果一个GAN过拟合了,那么生成的样本会非常真实,人类主观评价得分会非常高,可是这并不是一个好的GAN。

因此,就有许多学者提出了GAN的客观评价方法。

7.2.2 Inception Score

​ 对于一个在ImageNet训练良好的GAN,其生成的样本丢给Inception网络进行测试的时候,得到的判别概率应该具有如下特性:

  • 对于同一个类别的图片,其输出的概率分布应该趋向于一个脉冲分布。可以保证生成样本的准确性。
  • 对于所有类别,其输出的概率分布应该趋向于一个均匀分布,这样才不会出现mode dropping等,可以保证生成样本的多样性。

​ 因此,可以设计如下指标:

$$ IS(P_g)=e^{E_{x\sim P_g}[KL(p_M(y|x)\Vert{p_M(y)})]} 根据前面分析,如果是一个训练良好的GAN, {% raw%}$p_M(y|x)​${% endraw %} 趋近于脉冲分布, {% raw%}$p_M(y)​${% endraw %} 趋近于均匀分布。二者KL散度会很大。Inception Score自然就高。实际实验表明,Inception Score和人的主观判别趋向一致。IS的计算没有用到真实数据,具体值取决于模型M的选择

$$
​ 根据前面分析,如果是一个训练良好的GAN, $p_M(y|x)$ 趋近于脉冲分布, $p_M(y)$ 趋近于均匀分布。二者KL散度会很大。Inception Score自然就高。实际实验表明,Inception Score和人的主观判别趋向一致。IS的计算没有用到真实数据,具体值取决于模型M的选择。

特点:可以一定程度上衡量生成样本的多样性和准确性,但是无法检测过拟合。Mode Score也是如此。不推荐在和ImageNet数据集差别比较大的数据上使用。

7.2.3 Mode Score

​ Mode Score作为Inception Score的改进版本,添加了关于生成样本和真实样本预测的概率分布相似性度量一项。具体公式如下:

$$ MS(P_g)=e^{E_{x\sim P_g}[KL(p_M(y|x)\Vert{p_M(y)})-KL(p_M(y)\Vert p_M(y^*))]} $$

7.2.4 Kernel MMD (Maximum Mean Discrepancy)

计算公式如下:

$$ MMD^2(P_r,P_g)=E_{x_r\sim{P_r},x_g\sim{P_g}}[\lVert\Sigma_{i=1}^{n1}k(x_r)-\Sigma_{i=1}^{n2}k(x_g)\rVert] $$

​ 对于Kernel MMD值的计算,首先需要选择一个核函数 $k$ ,这个核函数把样本映射到再生希尔伯特空间(Reproducing Kernel Hilbert Space, RKHS) ,RKHS相比于欧几里得空间有许多优点,对于函数内积的计算是完备的。将上述公式展开即可得到下面的计算公式:

$$ MMD^2(P_r,P_g)=E_{x_r,x_r{'}\sim{P_r},x_g,x_g{'}\sim{P_g}}[k(x_r,x_r{'})-2k(x_r,x_g)+k(x_g,x_g{'})] $$

MMD值越小,两个分布越接近。

特点:可以一定程度上衡量模型生成图像的优劣性,计算代价小。推荐使用。

7.2.5 Wasserstein distance

​ Wasserstein distance在最优传输问题中通常也叫做推土机距离。这个距离的介绍在WGAN中有详细讨论。公式如下:

$$ WD(P_r,P_g)=min_{\omega\in\mathbb{R}^{m\times n}}\Sigma_{i=1}^n\Sigma_{i=1}^m\omega_{ij}d(x_i^r,x_j^g) $$ $$ s.t. \Sigma_{i=1}^mw_{i,j}=p_r(x_i^r), \forall i;\Sigma_{j=1}^nw_{i,j}=p_g(x_j^g), \forall j $$

​ Wasserstein distance可以衡量两个分布之间的相似性。距离越小,分布越相似。

特点:如果特征空间选择合适,会有一定的效果。但是计算复杂度为 $O(n^3)​$ 太高

7.2.6 Fréchet Inception Distance (FID)

​ FID距离计算真实样本,生成样本在特征空间之间的距离。首先利用Inception网络来提取特征,然后使用高斯模型对特征空间进行建模。根据高斯模型的均值和协方差来进行距离计算。具体公式如下:

$$ FID(\mathbb P_r,\mathbb P_g)=\lVert\mu_r-\mu_g\rVert+Tr(C_r+C_g-2(C_rC_g)^{1/2}) {% raw%}$\mu,C${% endraw %} 分别代表协方差和均值。

$$

$\mu,C​$ 分别代表协方差和均值。

特点:尽管只计算了特征空间的前两阶矩,但是鲁棒,且计算高效。

7.2.7 1-Nearest Neighbor classifier

​ 使用留一法,结合1-NN分类器(别的也行)计算真实图片,生成图像的精度。如果二者接近,则精度接近50%,否则接近0%。对于GAN的评价问题,作者分别用正样本的分类精度,生成样本的分类精度去衡量生成样本的真实性,多样性。

  • 对于真实样本 $x_r$ ,进行1-NN分类的时候,如果生成的样本越真实。则真实样本空间 $\mathbb R$ 将被生成的样本 $x_g$ 包围。那么 $x_r$ 的精度会很低。
  • 对于生成的样本 $x_g​$ ,进行1-NN分类的时候,如果生成的样本多样性不足。由于生成的样本聚在几个mode,则 $x_g​$ 很容易就和 $x_r​$ 区分,导致精度会很高。

特点:理想的度量指标,且可以检测过拟合。

7.2.8 其他评价方法

​ AIS,KDE方法也可以用于评价GAN,但这些方法不是model agnostic metrics。也就是说,这些评价指标的计算无法只利用:生成的样本,真实样本来计算。

7.3 其他常见的生成式模型有哪些?

7.3.1 什么是自回归模型:pixelRNN与pixelCNN?

​ 自回归模型通过对图像数据的概率分布 $p_{data}(x)$ 进行显式建模,并利用极大似然估计优化模型。具体如下:

$$ p_{data}(x)=\prod_{i=1}^np(x_i|x_1,x_2,...,x_{i-1}) $$

​ 上述公式很好理解,给定 $x_1,x_2,...,x_{i-1}$ 条件下,所有 $p(x_i)$ 的概率乘起来就是图像数据的分布。如果使用RNN对上述依然关系建模,就是pixelRNN。如果使用CNN,则是pixelCNN。具体如下[5]:

​ 显然,不论是对于pixelCNN还是pixelRNN,由于其像素值是一个个生成的,速度会很慢。语音领域大火的WaveNet就是一个典型的自回归模型。

7.3.2 什么是VAE?

​ PixelCNN/RNN定义了一个易于处理的密度函数,我们可以直接优化训练数据的似然;对于变分自编码器我们将定义一个不易处理的密度函数,通过附加的隐变量 $z$ 对密度函数进行建模。 VAE原理图如下[6]:

​ 在VAE中,真实样本 $X$ 通过神经网络计算出均值方差(假设隐变量服从正太分布),然后通过采样得到采样变量 $Z$ 并进行重构。VAE和GAN均是学习了隐变量 $z$ 到真实数据分布的映射。但是和GAN不同的是:

  • GAN的思路比较粗暴,使用一个判别器去度量分布转换模块(即生成器)生成分布与真实数据分布的距离。
  • VAE则没有那么直观,VAE通过约束隐变量 $z$ 服从标准正太分布以及重构数据实现了分布转换映射 $X=G(z)$

生成式模型对比

  • 自回归模型通过对概率分布显式建模来生成数据
  • VAE和GAN均是:假设隐变量 $z$ 服从某种分布,并学习一个映射 $X=G(z)$ ,实现隐变量分布 $z$ 与真实数据分布 $p_{data}(x)$ 的转换。
  • GAN使用判别器去度量映射 $X=G(z)$ 的优劣,而VAE通过隐变量 $z$ 与标准正太分布的KL散度和重构误差去度量。

7.4 GAN的改进与优化

7.4.1 如何生成指定类型的图像——条件GAN

​ 条件生成对抗网络(CGAN, Conditional Generative Adversarial Networks)作为一个GAN的改进,其一定程度上解决了GAN生成结果的不确定性。如果在Mnist数据集上训练原始GAN,GAN生成的图像是完全不确定的,具体生成的是数字1,还是2,还是几,根本不可控。为了让生成的数字可控,我们可以把数据集做一个切分,把数字0~9的数据集分别拆分开训练9个模型,不过这样太麻烦了,也不现实。因为数据集拆分不仅仅是分类麻烦,更主要在于,每一个类别的样本少,拿去训练GAN很有可能导致欠拟合。因此,CGAN就应运而生了。我们先看一下CGAN的网络结构:
CGAN网络结构
​ 从网络结构图可以看到,对于生成器Generator,其输入不仅仅是随机噪声的采样z,还有欲生成图像的标签信息。比如对于mnist数据生成,就是一个one-hot向量,某一维度为1则表示生成某个数字的图片。同样地,判别器的输入也包括样本的标签。这样就使得判别器和生成器可以学习到样本和标签之间的联系。Loss如下:

$$ \mathop {\min }\limits_G \mathop {\max }\limits_D V(D,G) = {\rm E}_{x\sim{p_{data}(x)}}[\log D(x|y)] + {\rm E}_{z\sim{p_z}(z)}[\log (1 - D(G(z|y)))] $$

​ Loss设计和原始GAN基本一致,只不过生成器,判别器的输入数据是一个条件分布。在具体编程实现时只需要对随机噪声采样z和输入条件y做一个级联即可。

7.4.2 CNN与GAN——DCGAN

​ 前面我们聊的GAN都是基于简单的神经网络构建的。可是对于视觉问题,如果使用原始的基于DNN的GAN,则会出现许多问题。如果输入GAN的随机噪声为100维的随机噪声,输出图像为256x256大小。也就是说,要将100维的信息映射为65536维。如果单纯用DNN来实现,那么整个模型参数会非常巨大,而且学习难度很大(低维度映射到高维度需要添加许多信息)。因此,DCGAN就出现了。具体而言,DCGAN将传统GAN的生成器,判别器均采用GAN实现,且使用了一下tricks:

  • 将pooling层convolutions替代,其中,在discriminator上用strided convolutions替代,在generator上用fractional-strided convolutions替代。
  • 在generator和discriminator上都使用batchnorm。
  • 移除全连接层,global pooling增加了模型的稳定性,但伤害了收敛速度。
  • 在generator的除了输出层外的所有层使用ReLU,输出层采用tanh。
  • 在discriminator的所有层上使用LeakyReLU。

网络结构图如下:
CGAN网络结构图

7.4.3 如何理解GAN中的输入随机噪声?

​ 为了了解输入随机噪声每一个维度代表的含义,作者做了一个非常有趣的工作。即在隐空间上,假设知道哪几个变量控制着某个物体,那么僵这几个变量挡住是不是就可以将生成图片中的某个物体消失?论文中的实验是这样的:首先,生成150张图片,包括有窗户的和没有窗户的,然后使用一个逻辑斯底回归函数来进行分类,对于权重不为0的特征,认为它和窗户有关。将其挡住,得到新的生成图片,结果如下:
DCGAN输入噪声理解
此外,将几个输入噪声进行算数运算,可以得到语义上进行算数运算的非常有趣的结果。类似于word2vec。
DCGAN输入噪声算术运算

7.4.4 GAN为什么容易训练崩溃?

​ 所谓GAN的训练崩溃,指的是训练过程中,生成器和判别器存在一方压倒另一方的情况。
GAN原始判别器的Loss在判别器达到最优的时候,等价于最小化生成分布与真实分布之间的JS散度,由于随机生成分布很难与真实分布有不可忽略的重叠以及JS散度的突变特性,使得生成器面临梯度消失的问题;可是如果不把判别器训练到最优,那么生成器优化的目标就失去了意义。因此需要我们小心的平衡二者,要把判别器训练的不好也不坏才行。否则就会出现训练崩溃,得不到想要的结果

7.4.5 WGAN如何解决训练崩溃问题?

​ WGAN作者提出了使用Wasserstein距离,以解决GAN网络训练过程难以判断收敛性的问题。Wasserstein距离定义如下:

$$ L={\rm E}_{x\sim{p_{data}}(x)}[f_w(x)] - {\rm E}_{x\sim{p_g}(x)}[f_w(x)] $$

通过最小化Wasserstein距离,得到了WGAN的Loss:

  • WGAN生成器Loss: $- {\rm E}_{x\sim{p_g}(x)}[f_w(x)]​$
  • WGAN判别器Loss: $L=-{\rm E}_{x\sim{p_{data}}(x)}[f_w(x)] + {\rm E}_{x\sim{p_g}(x)}[f_w(x)]$

从公式上GAN似乎总是让人摸不着头脑,在代码实现上来说,其实就以下几点:

  • 判别器最后一层去掉sigmoid
  • 生成器和判别器的loss不取log
  • 每次更新判别器的参数之后把它们的绝对值截断到不超过一个固定常数c

7.4.6 WGAN-GP:带有梯度正则的WGAN

​ 实际实验过程发现,WGAN没有那么好用,主要原因在于WAGN进行梯度截断。梯度截断将导致判别网络趋向于一个二值网络,造成模型容量的下降。
于是作者提出使用梯度惩罚来替代梯度裁剪。公式如下:

$$ L=-{\rm E}_{x\sim{p_{data}}(x)}[f_w(x)] + {\rm E}_{x\sim{p_g}(x)}[f_w(x)]+\lambda{\rm E}_{x\sim{p_x}(x)}[\lVert\nabla_x(D(x))\rVert_p-1]^2 由于上式是对每一个梯度进行惩罚,所以不适合使用BN,因为它会引入同个batch中不同样本的相互依赖关系。如果需要的话,可以选择Layer Normalization。实际训练过程中,就可以通过Wasserstein距离来度量模型收敛程度了: ![Wass距离随迭代次数变化](ch7/Wass%E8%B7%9D%E7%A6%BB%E9%9A%8F%E8%BF%AD%E4%BB%A3%E6%AC%A1%E6%95%B0%E5%8F%98%E5%8C%96.png) 上图纵坐标是Wasserstein距离,横坐标是迭代次数。可以看出,随着迭代的进行,Wasserstein距离趋于收敛,生成图像也趋于稳定。 $$

​ 由于上式是对每一个梯度进行惩罚,所以不适合使用BN,因为它会引入同个batch中不同样本的相互依赖关系。如果需要的话,可以选择Layer Normalization。实际训练过程中,就可以通过Wasserstein距离来度量模型收敛程度了:
Wass距离随迭代次数变化
​ 上图纵坐标是Wasserstein距离,横坐标是迭代次数。可以看出,随着迭代的进行,Wasserstein距离趋于收敛,生成图像也趋于稳定。

7.4.7 LSGAN

​ LSGAN(Least Squares GAN)这篇文章主要针对标准GAN的稳定性和图片生成质量不高做了一个改进。作者将原始GAN的交叉熵损失采用最小二乘损失替代。LSGAN的Loss:

$$ \mathop{\min }\limits_DJ(D)=\mathop{\min}\limits_D[{\frac{1}{2}}{\rm E}_{x\sim{p_{data}}(x)}[D(x)-a]^2 + {\frac{1}{2}}{\rm E}_{z\sim{p_z}(z)}[D(G(z))-b]^2] $$ $$ \mathop{\min }\limits_GJ(G)=\mathop{\min}\limits_G{\frac{1}{2}}{\rm E}_{z\sim{p_z}(z)}[D(G(z))-c]^2 $$

​ 实际实现的时候非常简单,最后一层去掉sigmoid,并且计算Loss的时候用平方误差即可。之所以这么做,作者在原文给出了一张图:
![LSGAN交叉熵与最小二乘损失对比图](ch7/lsgan loss compare.png)
​ 上面是作者给出的基于交叉熵损失以及最小二乘损失的Loss函数。横坐标代表Loss函数的输入,纵坐标代表输出的Loss值。可以看出,随着输入的增大,sigmoid交叉熵损失很快趋于0,容易导致梯度饱和问题。如果使用右边的Loss设计,则只在x=0点处饱和。因此使用LSGAN可以很好的解决交叉熵损失的问题。

7.4.8 如何尽量避免GAN的训练崩溃问题?

  • 归一化图像输入到(-1,1)之间;Generator最后一层使用tanh激活函数
  • 生成器的Loss采用:min (log 1-D)。因为原始的生成器Loss存在梯度消失问题;训练生成器的时候,考虑反转标签,real=fake, fake=real
  • 不要在均匀分布上采样,应该在高斯分布上采样
  • 一个Mini-batch里面必须只有正样本,或者负样本。不要混在一起;如果用不了Batch Norm,可以用Instance Norm
  • 避免稀疏梯度,即少用ReLU,MaxPool。可以用LeakyReLU替代ReLU,下采样可以用Average Pooling或者Convolution + stride替代。上采样可以用PixelShuffle, ConvTranspose2d + stride
  • 平滑标签或者给标签加噪声;平滑标签,即对于正样本,可以使用0.7-1.2的随机数替代;对于负样本,可以使用0-0.3的随机数替代。 给标签加噪声:即训练判别器的时候,随机翻转部分样本的标签。
  • 如果可以,请用DCGAN或者混合模型:KL+GAN,VAE+GAN。
  • 使用LSGAN,WGAN-GP
  • Generator使用Adam,Discriminator使用SGD
  • 尽快发现错误;比如:判别器Loss为0,说明训练失败了;如果生成器Loss稳步下降,说明判别器没发挥作用
  • 不要试着通过比较生成器,判别器Loss的大小来解决训练过程中的模型坍塌问题。比如:
    While Loss D > Loss A:
    Train D
    While Loss A > Loss D:
    Train A
  • 如果有标签,请尽量利用标签信息来训练
  • 给判别器的输入加一些噪声,给G的每一层加一些人工噪声。
  • 多训练判别器,尤其是加了噪声的时候
  • 对于生成器,在训练,测试的时候使用Dropout

7.3 GAN的应用(图像翻译)

7.3.1 什么是图像翻译?

​ GAN作为一种强有力的生成模型,其应用十分广泛。最为常见的应用就是图像翻译。所谓图像翻译,指从一副图像到另一副图像的转换。可以类比机器翻译,一种语言转换为另一种语言。常见的图像翻译任务有:

  • 图像去噪

  • 图像超分辨

  • 图像补全

  • 风格迁移

  • 本节将介绍一个经典的图像翻译网络及其改进。图像翻译可以分为有监督图像翻译和无监督图像翻译:

  • 有监督图像翻译:原始域与目标域存在一一对应数据

  • 无监督图像翻译:原始域与目标域不存在一一对应数据

7.3.2 有监督图像翻译:pix2pix

​ 在这篇paper里面,作者提出的框架十分简洁优雅(好用的算法总是简洁优雅的)。相比以往算法的大量专家知识,手工复杂的loss。这篇paper非常粗暴,使用CGAN处理了一系列的转换问题。下面是一些转换示例:
pix2pix结果示例

​ 上面展示了许多有趣的结果,比如分割图 $\longrightarrow$ 街景图,边缘图 $\longrightarrow$ 真实图。对于第一次看到的时候还是很惊艳的,那么这个是怎么做到的呢?我们可以设想一下,如果是我们,我们自己会如何设计这个网络?

直观的想法

​ 最直接的想法就是,设计一个CNN网络,直接建立输入-输出的映射,就像图像去噪问题一样。可是对于上面的问题,这样做会带来一个问题。生成图像质量不清晰。

​ 拿左上角的分割图 $\longrightarrow$ 街景图为例,语义分割图的每个标签比如“汽车”可能对应不同样式,颜色的汽车。那么模型学习到的会是所有不同汽车的评均,这样会造成模糊。pix2pix语义地图L1loss结果

如何解决生成图像的模糊问题

​ 这里作者想了一个办法,即加入GAN的Loss去惩罚模型。GAN相比于传统生成式模型可以较好的生成高分辨率图片。思路也很简单,在上述直观想法的基础上加入一个判别器,判断输入图片是否是真实样本。模型示意图如下:
pix2pix模型示意图

​ 上图模型和CGAN有所不同,但它是一个CGAN,只不过输入只有一个,这个输入就是条件信息。原始的CGAN需要输入随机噪声,以及条件。这里之所有没有输入噪声信息,是因为在实际实验中,如果输入噪声和条件,噪声往往被淹没在条件C当中,所以这里直接省去了。

7.3.3 其他图像翻译的tricks

从上面两点可以得到最终的Loss由两部分构成:

  • 输出和标签信息的L1 Loss。

  • GAN Loss

  • 测试也使用Dropout,以使输出多样化

    $$ G^*=arg\mathop {\min }\limits_G \mathop {\max }\limits_D \Gamma_{cGAN}(G,D)+\lambda\Gamma_{L1}(G) $$

​ 采用L1 Loss而不是L2 Loss的理由很简单,L1 Loss相比于L2 Loss保边缘(L2 Loss基于高斯先验,L1 Loss基于拉普拉斯先验)。

​ GAN Loss为LSGAN的最小二乘Loss,并使用PatchGAN(进一步保证生成图像的清晰度)。PatchGAN将图像换分成很多个Patch,并对每一个Patch使用判别器进行判别(实际代码实现有更取巧的办法),将所有Patch的Loss求平均作为最终的Loss。

7.3.4 如何生成高分辨率图像和高分辨率视频?

​ pix2pix提出了一个通用的图像翻译框架。对于高分辨率的图像生成以及高分辨率的视频生成,则需要利用更好的网络结构以及更多的先验只是。pix2pixHD提出了一种多尺度的生成器以及判别器等方式从而生成高分辨率图像。Vid2Vid则在pix2pixHD的基础上利用光流,时序约束生成了高分辨率视频。

7.3.5 有监督的图像翻译的缺点?

​ 许多图像翻译算法如前面提及的pix2pix系列,需要一一对应的图像。可是在许多应用场景下,往往没有这种一一对应的强监督信息。比如说以下一些应用场景:
CycleGAN结果例子
以第一排第一幅图为例,要找到这种一一配对的数据是不现实的。因此,无监督图像翻译算法就被引入了。

7.3.6 无监督图像翻译:CycleGAN

模型结构

​ 总体思路如下,假设有两个域的数据,记为A,B。对于上图第一排第一幅图A域就是普通的马,B域就是斑马。由于A->B的转换缺乏监督信息,于是,作者提出采用如下方法进行转换:

a. A->fake_B->rec_A
b. B->fake_A->rec_B

​ 对于A域的所有图像,学习一个网络G_B,该网络可以生成B。对于B域的所有图像,也学习一个网络G_A,该网络可以生成G_B。

​ 训练过程分成两步,首先对于A域的某张图像,送入G_B生成fake_B,然后对fake_B送入G_A,得到重构后的A图像rec_A。对于B域的某一张图像也是类似。重构后的图像rec_A/rec_B可以和原图A/B做均方误差,实现了有监督的训练。此处值得注意的是A->fake_B(B->fake_A)和fake_A->rec_B(fake_B->rec_A)的网络是一模一样的。下图是形象化的网络结构图:
CycleGAN模型示意图
​ cycleGAN的生成器采用U-Net,判别器采用LS-GAN。

Loss设计

​ 总的Loss就是X域和Y域的GAN Loss,以及Cycle consistency loss:

$$ L(G,F,D_X,D_Y)=L_{GAN}(G,D_Y,X,Y)+L_{GAN}(F,D_X,Y,X)+\lambda L_{cycle}(G,F) $$

整个过程End to end训练,效果非常惊艳,利用这一框架可以完成非常多有趣的任务

7.3.7 多领域的无监督图像翻译:StarGAN

cycleGAN模型较好的解决了无监督图像转换问题,可是这种单一域的图像转换还存在一些问题:

  • 要针对每一个域训练一个模型,效率太低。举例来说,我希望可以将橘子转换为红苹果和青苹果。对于cycleGAN而言,需要针对红苹果,青苹果分别训练一个模型。

  • 对于每一个域都需要搜集大量数据,太麻烦。还是以橘子转换为红苹果和青苹果为例。不管是红苹果还是青苹果,都是苹果,只是颜色不一样而已。这两个任务信息是可以共享的,没必要分别训练两个模型。而且针对红苹果,青苹果分别取搜集大量数据太费事。

    starGAN则提出了一个多领域的无监督图像翻译框架,实现了多个领域的图像转换,且对于不同领域的数据可以混合在一起训练,提高了数据利用率

7.4 GAN的应用(文本生成)

7.4.1 GAN为什么不适合文本任务?

​ GAN在2014年被提出之后,在图像生成领域取得了广泛的研究应用。然后在文本领域却一直没有很惊艳的效果。主要在于文本数据是离散数据,而GAN在应用于离散数据时存在以下几个问题:

  • GAN的生成器梯度来源于判别器对于正负样本的判别。然而,对于文本生成问题,RNN输出的是一个概率序列,然后取argmax。这会导致生成器Loss不可导。还可以站在另一个角度理解,由于是argmax,所以参数更新一点点并不会改变argmax的结果,这也使得GAN不适合离散数据。
  • GAN只能评估整个序列的loss,但是无法评估半句话,或者是当前生成单词对后续结果好坏的影响。
  • 如果不加argmax,那么由于生成器生成的都是浮点数值,而ground truth都是one-hot encoding,那么判别器只要判别生成的结果是不是0/1序列组成的就可以了。这容易导致训练崩溃。

7.4.2 seqGAN用于文本生成

​ seqGAN在GAN的框架下,结合强化学习来做文本生成。 模型示意图如下:

seqGAN模型
在文本生成任务,seqGAN相比较于普通GAN区别在以下几点:

  • 生成器不取argmax。
  • 每生成一个单词,则根据当前的词语序列进行蒙特卡洛采样生成完成的句子。然后将句子送入判别器计算reward。
  • 根据得到的reward进行策略梯度下降优化模型。

7.5 GAN在其他领域的应用

7.5.1 数据增广

​ GAN的良好生成特性近年来也开始被用于数据增广。以行人重识别为例,有许多GAN用于数据增广的工作[1-4]。行人重识别问题一个难点在于不同摄像头下拍摄的人物环境,角度差别非常大,导致存在较大的Domain gap。因此,可以考虑使用GAN来产生不同摄像头下的数据进行数据增广。以论文[1]为例,本篇paper提出了一个cycleGAN用于数据增广的方法。具体模型结构如下:

cycleGAN数据增广

​ 对于每一对摄像头都训练一个cycleGAN,这样就可以实现将一个摄像头下的数据转换成另一个摄像头下的数据,但是内容(人物)保持不变。
在CVPR19中,[9]进一步提升了图像的生成质量,进行了“淘宝换衣”式的高质量图像生成(如下图),提供了更高质量的行人训练数据。

DG-Net数据增广

7.5.2 图像超分辨与图像补全

​ 图像超分辨与补全均可以作为图像翻译问题,该类问题的处理办法也大都是训练一个端到端的网络,输入是原始图片,输出是超分辨率后的图片,或者是补全后的图片。文献[5]利用GAN作为判别器,使得超分辨率模型输出的图片更加清晰,更符合人眼主管感受。日本早稻田大学研究人员[6]提出一种全局+局部一致性的GAN实现图像补全,使得修复后的图像不仅细节清晰,且具有整体一致性。

7.5.3 语音领域

​ 相比于图像领域遍地开花,GAN在语音领域则应用相对少了很多。这里零碎的找一些GAN在语音领域进行应用的例子作为介绍。文献[7]提出了一种音频去噪的SEGAN,缓解了传统方法支持噪声种类稀少,泛化能力不强的问题。Donahue利用GAN进行语音增强,提升了ASR系统的识别率。

参考文献

[1] Zheng Z , Zheng L , Yang Y . Unlabeled Samples Generated by GAN Improve the Person Re-identification Baseline in Vitro[C]// 2017 IEEE International Conference on Computer Vision (ICCV). IEEE Computer Society, 2017.

[2] Zhong Z , Zheng L , Zheng Z , et al. Camera Style Adaptation for Person Re-identification[J]. 2017.

[3] Deng W , Zheng L , Ye Q , et al. Image-Image Domain Adaptation with Preserved Self-Similarity and Domain-Dissimilarity for Person Re-identification[J]. 2017.

[4] Wei L , Zhang S , Gao W , et al. Person Transfer GAN to Bridge Domain Gap for Person Re-Identification[J]. CVPR, 2017.

[5] Ledig C , Theis L , Huszar F , et al. Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network[J]. CVPR, 2016.

[6] Iizuka S , Simo-Serra E , Ishikawa H . Globally and locally consistent image completion[J]. ACM Transactions on Graphics, 2017, 36(4):1-14.

[7] Pascual S , Bonafonte A , Serrà, Joan. SEGAN: Speech Enhancement Generative Adversarial Network[J]. 2017.

[8] Donahue C , Li B , Prabhavalkar R . Exploring Speech Enhancement with Generative Adversarial Networks for Robust Speech Recognition[J]. 2017.

[9] Zheng, Z., Yang, X., Yu, Z., Zheng, L., Yang, Y., & Kautz, J. Joint discriminative and generative learning for person re-identification. IEEE Conference on Computer Vision and Pattern Recognition (CVPR)[C]. 2019.

目标检测

8.1 基本概念

8.1.1 什么是目标检测?

​ 目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的类别和位置,是计算机视觉领域的核心问题之一。由于各类物体有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具有挑战性的问题。

​ 计算机视觉中关于图像识别有四大类任务:

分类-Classification:解决“是什么?”的问题,即给定一张图片或一段视频判断里面包含什么类别的目标。

定位-Location:解决“在哪里?”的问题,即定位出这个目标的的位置。

检测-Detection:解决“是什么?在哪里?”的问题,即定位出这个目标的的位置并且知道目标物是什么。

分割-Segmentation:分为实例的分割(Instance-level)和场景分割(Scene-level),解决“每一个像素属于哪个目标物或场景”的问题。

图像识别四大类任务,图像来源于cs231n 2016课件Lecture 8

8.1.2 目标检测要解决的核心问题?

除了图像分类之外,目标检测要解决的核心问题是:

1.目标可能出现在图像的任何位置。

2.目标有各种不同的大小。

3.目标可能有各种不同的形状。

8.1.3 目标检测算法分类?

基于深度学习的目标检测算法主要分为两类:

1.Two stage目标检测算法

​ 先进行区域生成(region proposal,RP)(一个有可能包含待检物体的预选框),再通过卷积神经网络进行样本分类。

​ 任务:特征提取—>生成RP—>分类/定位回归。

​ 常见的two stage目标检测算法有:R-CNN、SPP-Net、Fast R-CNN、Faster R-CNN和R-FCN等。

2.One stage目标检测算法

​ 不用RP,直接在网络中提取特征来预测物体分类和位置。

​ 任务:特征提取—>分类/定位回归。

​ 常见的one stage目标检测算法有:OverFeat、YOLOv1、YOLOv2、YOLOv3、SSD和RetinaNet等。

8.1.4 目标检测有哪些应用?

​ 目标检测具有巨大的实用价值和应用前景。应用领域包括人脸检测、行人检测、车辆检测、飞机航拍或卫星图像中道路的检测、车载摄像机图像中的障碍物检测、医学影像在的病灶检测等。还有在安防领域中,可以实现比如安全帽、安全带等动态检测,移动侦测、区域入侵检测、物品看护等功能。

8.2 Two Stage目标检测算法

8.2.1 R-CNN

R-CNN有哪些创新点?

  1. 使用CNN(ConvNet)对 region proposals 计算 feature vectors。从经验驱动特征(SIFT、HOG)到数据驱动特征(CNN feature map),提高特征对样本的表示能力。
  2. 采用大样本下(ILSVRC)有监督预训练和小样本(PASCAL)微调(fine-tuning)的方法解决小样本难以训练甚至过拟合等问题。

注:ILSVRC其实就是众所周知的ImageNet的挑战赛,数据量极大;PASCAL数据集(包含目标检测和图像分割等),相对较小。

R-CNN 介绍

​ R-CNN作为R-CNN系列的第一代算法,其实没有过多的使用“深度学习”思想,而是将“深度学习”和传统的“计算机视觉”的知识相结合。比如R-CNN pipeline中的第二步和第四步其实就属于传统的“计算机视觉”技术。使用selective search提取region proposals,使用SVM实现分类。

原论文中R-CNN pipeline只有4个步骤,光看上图无法深刻理解R-CNN处理机制,下面结合图示补充相应文字

  1. 预训练模型。选择一个预训练 (pre-trained)神经网络(如AlexNet、VGG)。

  2. 重新训练全连接层。使用需要检测的目标重新训练(re-train)最后全连接层(connected layer)。

  3. 提取 proposals并计算CNN 特征。利用选择性搜索(Selective Search)算法提取所有proposals(大约2000幅images),调整(resize/warp)它们成固定大小,以满足 CNN输入要求(因为全连接层的限制),然后将feature map 保存到本地磁盘。

  4. 训练SVM。利用feature map 训练SVM来对目标和背景进行分类(每个类一个二进制SVM)

  5. 边界框回归(Bounding boxes Regression)。训练将输出一些校正因子的线性回归分类器

R-CNN 实验结果

R-CNN在VOC 2007测试集上mAP达到58.5%,打败当时所有的目标检测算法。

8.2.2 Fast R-CNN

Fast R-CNN有哪些创新点?

  1. 只对整幅图像进行一次特征提取,避免R-CNN中的冗余特征提取
  2. 用RoI pooling层替换最后一层的max pooling层,同时引入建议框数据,提取相应建议框特征
  3. Fast R-CNN网络末尾采用并行的不同的全连接层,可同时输出分类结果和窗口回归结果,实现了end-to-end的多任务训练【建议框提取除外】,也不需要额外的特征存储空间【R-CNN中的特征需要保持到本地,来供SVM和Bounding-box regression进行训练】
  4. 采用SVD对Fast R-CNN网络末尾并行的全连接层进行分解,减少计算复杂度,加快检测速度。

Fast R-CNN 介绍

​ Fast R-CNN是基于R-CNN和SPPnets进行的改进。SPPnets,其创新点在于计算整幅图像的the shared feature map,然后根据object proposal在shared feature map上映射到对应的feature vector(就是不用重复计算feature map了)。当然,SPPnets也有缺点:和R-CNN一样,训练是多阶段(multiple-stage pipeline)的,速度还是不够"快",特征还要保存到本地磁盘中。

将候选区域直接应用于特征图,并使用RoI池化将其转化为固定大小的特征图块。以下是Fast R-CNN的流程图

RoI Pooling层详解

因为Fast R-CNN使用全连接层,所以应用RoI Pooling将不同大小的ROI转换为固定大小。

RoI Pooling 是Pooling层的一种,而且是针对RoI的Pooling,其特点是输入特征图尺寸不固定,但是输出特征图尺寸固定(如7x7)。

什么是RoI呢?

RoI是Region of Interest的简写,一般是指图像上的区域框,但这里指的是由Selective Search提取的候选框。

往往经过RPN后输出的不止一个矩形框,所以这里我们是对多个RoI进行Pooling。

RoI Pooling的输入

输入有两部分组成:

  1. 特征图(feature map):指的是上面所示的特征图,在Fast RCNN中,它位于RoI Pooling之前,在Faster RCNN中,它是与RPN共享那个特征图,通常我们常常称之为“share_conv”;
  2. RoIs,其表示所有RoI的N*5的矩阵。其中N表示RoI的数量,第一列表示图像index,其余四列表示其余的左上角和右下角坐标。

在Fast RCNN中,指的是Selective Search的输出;在Faster RCNN中指的是RPN的输出,一堆矩形候选框,形状为1x5x1x1(4个坐标+索引index),其中值得注意的是:坐标的参考系不是针对feature map这张图的,而是针对原图的(神经网络最开始的输入)。其实关于ROI的坐标理解一直很混乱,到底是根据谁的坐标来。其实很好理解,我们已知原图的大小和由Selective Search算法提取的候选框坐标,那么根据"映射关系"可以得出特征图(featurwe map)的大小和候选框在feature map上的映射坐标。至于如何计算,其实就是比值问题,下面会介绍。所以这里把ROI理解为原图上各个候选框(region proposals),也是可以的。

注:说句题外话,由Selective Search算法提取的一系列可能含有object的bounding box,这些通常称为region proposals或者region of interest(ROI)。

RoI的具体操作

  1. 根据输入image,将ROI映射到feature map对应位置

    注:映射规则比较简单,就是把各个坐标除以“输入图片与feature map的大小的比值”,得到了feature map上的box坐标

  2. 将映射后的区域划分为相同大小的sections(sections数量与输出的维度相同)

  3. 对每个sections进行max pooling操作

这样我们就可以从不同大小的方框得到固定大小的相应 的feature maps。值得一提的是,输出的feature maps的大小不取决于ROI和卷积feature maps大小。RoI Pooling 最大的好处就在于极大地提高了处理速度。

RoI Pooling的输出

输出是batch个vector,其中batch的值等于RoI的个数,vector的大小为channel * w * h;RoI Pooling的过程就是将一个个大小不同的box矩形框,都映射成大小固定(w * h)的矩形框。

RoI Pooling示例

8.2.3 Faster R-CNN

Faster R-CNN有哪些创新点?

Fast R-CNN依赖于外部候选区域方法,如选择性搜索。但这些算法在CPU上运行且速度很慢。在测试中,Fast R-CNN需要2.3秒来进行预测,其中2秒用于生成2000个ROI。Faster R-CNN采用与Fast R-CNN相同的设计,只是它用内部深层网络代替了候选区域方法。新的候选区域网络(RPN)在生成ROI时效率更高,并且以每幅图像10毫秒的速度运行。

图8.1.13 Faster R-CNN的流程图
Faster R-CNN的流程图与Fast R-CNN相同,采用外部候选区域方法代替了内部深层网络。

图8.1.14
候选区域网络

候选区域网络(RPN)将第一个卷积网络的输出特征图作为输入。它在特征图上滑动一个3×3的卷积核,以使用卷积网络(如下所示的ZF网络)构建与类别无关的候选区域。其他深度网络(如VGG或ResNet)可用于更全面的特征提取,但这需要以速度为代价。ZF网络最后会输出256个值,它们将馈送到两个独立的全连接层,以预测边界框和两个objectness分数,这两个objectness分数度量了边界框是否包含目标。我们其实可以使用回归器计算单个objectness分数,但为简洁起见,Faster R-CNN使用只有两个类别的分类器:即带有目标的类别和不带有目标的类别。

图8.1.15
对于特征图中的每一个位置,RPN会做k次预测。因此,RPN将输出4×k个坐标和每个位置上2×k个得分。下图展示了8×8的特征图,且有一个3×3的卷积核执行运算,它最后输出8×8×3个ROI(其中k=3)。下图(右)展示了单个位置的3个候选区域。

图8.1.16
假设最好涵盖不同的形状和大小。因此,Faster R-CNN不会创建随机边界框。相反,它会预测一些与左上角名为锚点的参考框相关的偏移量(如x, y)。我们限制这些偏移量的值,因此我们的猜想仍然类似于锚点。

图8.1.17
要对每个位置进行k个预测,我们需要以每个位置为中心的k个锚点。每个预测与特定锚点相关联,但不同位置共享相同形状的锚点。

图8.1.18
这些锚点是精心挑选的,因此它们是多样的,且覆盖具有不同比例和宽高比的现实目标。这使得我们可以用更好的猜想来指导初始训练,并允许每个预测专门用于特定的形状。该策略使早期训练更加稳定和简便。

图8.1.19
Faster R-CNN使用更多的锚点。它部署9个锚点框:3个不同宽高比的3个不同大小的锚点(Anchor)框。每一个位置使用9个锚点,每个位置会生成2×9个objectness分数和4×9个坐标。

8.2.4 R-FCN

R-FCN有哪些创新点?

R-FCN 仍属于two-stage 目标检测算法:RPN+R-FCN

  1. Fully convolutional
  2. 位置敏感得分图(position-sentive score maps)

our region-based detector is fully convolutional with almost all computation shared on the entire image. To achieve this goal, we propose position-sensitive score maps to address a dilemma between translation-invariance in image classification and translation-variance in object detection.

R-FCN backbone:ResNet

ResNet-101+R-FCN:83.6% in PASCAL VOC 2007 test datasets

既提高了mAP,又加快了检测速度

假设我们只有一个特征图用来检测右眼。那么我们可以使用它定位人脸吗?应该可以。因为右眼应该在人脸图像的左上角,所以我们可以利用这一点定位整个人脸。如果我们还有其他用来检测左眼、鼻子或嘴巴的特征图,那么我们可以将检测结果结合起来,更好地定位人脸。现在我们回顾一下所有问题。在Faster R-CNN中,检测器使用了多个全连接层进行预测。如果有2000个ROI,那么成本非常高。R-FCN通过减少每个ROI所需的工作量实现加速。上面基于区域的特征图与ROI是独立的,可以在每个ROI之外单独计算。剩下的工作就比较简单了,因此R-FCN的速度比Faster R-CNN快。

图8.2.1 人脸检测
现在我们来看一下5×5的特征图M,内部包含一个蓝色方块。我们将方块平均分成3×3个区域。现在,我们在M中创建了一个新的特征图,来检测方块的左上角(TL)。这个新的特征图如下图(右)所示。只有黄色的网格单元[2,2]处于激活状态。在左侧创建一个新的特征图,用于检测目标的左上角。

图8.2.2 检测示例
我们将方块分成9个部分,由此创建了9个特征图,每个用来检测对应的目标区域。这些特征图叫做位置敏感得分图(position-sensitive score map),因为每个图检测目标的子区域(计算其得分)。

图8.2.3生成9个得分图
下图中红色虚线矩形是建议的ROI。我们将其分割成3×3个区域,并询问每个区域包含目标对应部分的概率是多少。例如,左上角ROI区域包含左眼的概率。我们将结果存储成3×3 vote数组,如下图(右)所示。例如,vote_array[0][0]包含左上角区域是否包含目标对应部分的得分。

图8.2.4
将ROI应用到特征图上,输出一个3x3数组。将得分图和ROI映射到vote数组的过程叫做位置敏感ROI池化(position-sensitive ROI-pool)。该过程与前面讨论过的ROI池化非常接近。

图8.2.5
将ROI的一部分叠加到对应的得分图上,计算V[i][j]。在计算出位置敏感ROI池化的所有值后,类别得分是其所有元素得分的平均值。

图8.2.6 ROI池化
假如我们有C个类别要检测。我们将其扩展为C+1个类别,这样就为背景(非目标)增加了一个新的类别。每个类别有3×3个得分图,因此一共有(C+1)×3×3个得分图。使用每个类别的得分图可以预测出该类别的类别得分。然后我们对这些得分应用 softmax 函数,计算出每个类别的概率。以下是数据流图,在本案例中,k=3。

图8.2.7

8.2.5 FPN

FPN有哪些创新点?

  1. 多层特征
  2. 特征融合

解决目标检测中的多尺度问题,通过简单的网络连接改变,在基本不增加原有模型计算量的情况下,大幅度提升小物体(small object)检测的性能。

在物体检测里面,有限计算量情况下,网络的深度(对应到感受野)与 stride 通常是一对矛盾的东西,常用的网络结构对应的 stride 一般会比较大(如 32),而图像中的小物体甚至会小于 stride 的大小,造成的结果就是小物体的检测性能急剧下降。传统解决这个问题的思路包括:

  1. 图像金字塔(image pyramid),即多尺度训练和测试。但该方法计算量大,耗时较久。
  2. 特征分层,即每层分别预测对应的scale分辨率的检测结果,如SSD算法。该方法强行让不同层学习同样的语义信息,但实际上不同深度对应于不同层次的语义特征,浅层网络分辨率高,学到更多是细节特征,深层网络分辨率低,学到更多是语义特征。

因而,目前多尺度的物体检测主要面临的挑战为:

  1. 如何学习具有强语义信息的多尺度特征表示?
  2. 如何设计通用的特征表示来解决物体检测中的多个子问题?如 object proposal, box localization, instance segmentation.
  3. 如何高效计算多尺度的特征表示?

FPN网络直接在Faster R-CNN单网络上做修改,每个分辨率的 feature map 引入后一分辨率缩放两倍的 feature map 做 element-wise 相加的操作。通过这样的连接,每一层预测所用的 feature map 都融合了不同分辨率、不同语义强度的特征,融合的不同分辨率的 feature map 分别做对应分辨率大小的物体检测。这样保证了每一层都有合适的分辨率以及强语义(rich semantic)特征。同时,由于此方法只是在原网络基础上加上了额外的跨层连接,在实际应用中几乎不增加额外的时间和计算量。作者接下来实验了将 FPN 应用在 Faster RCNN 上的性能,在 COCO 上达到了 state-of-the-art 的单模型精度。在RPN上,FPN增加了8.0个点的平均召回率(average recall,AR);在后面目标检测上,对于COCO数据集,FPN增加了2.3个点的平均精确率(average precision,AP),对于VOC数据集,FPN增加了3.8个点的AP。

FPN算法主要由三个模块组成,分别是:

  1. Bottom-up pathway(自底向上线路)
  2. Lareral connections(横向链接)
  3. Top-down path(自顶向下线路)

Bottom-up pathway

FPN是基于Faster R-CNN进行改进,其backbone是ResNet-101,FPN主要应用在Faster R-CNN中的RPN(用于bouding box proposal generation)和Fast R-CNN(用于object detection)两个模块中。

其中 RPN 和 Fast RCNN 分别关注的是召回率(recall)和精确率(precision),在这里对比的指标分别为 Average Recall(AR) 和 Average Precision(AP)。

注:Bottom-up可以理解为自底向上,Top-down可以理解为自顶向下。这里的下是指low-level,上是指high-level,分别对应于提取的低级(浅层)特征和高级语义(高层)特征。

Bottom-up pathway 是卷积网络的前向传播过程。在前向传播过程中,feature map的大小可以在某些层发生改变。一些尺度(scale)因子为2,所以后一层feature map的大小是前一层feature map大小的二分之一,根据此关系进而构成了feature pyramid(hierarchy)。

然而还有很多层输出的feature map是一样的大小(即不进行缩放的卷积),作者将这些层归为同一 stage。对于feature pyramid,作者为每个stage定义一个pyramid level。

作者将每个stage的最后一层的输出作为feature map,然后不同stage进行同一操作,便构成了feature pyramid。

具体来说,对于ResNets-101,作者使用了每个stage的最后一个残差结构的特征激活输出。将这些残差模块输出表示为{C2, C3, C4, C5},对应于conv2,conv3,conv4和conv5的输出,并且注意它们相对于输入图像具有{4, 8, 16, 32}像素的步长。考虑到内存占用,没有将conv1包含在金字塔中。

Top-down pathway and lateral connections

Top-town pathway是上采样(upsampling)过程。而later connection(横向连接)是将上采样的结果和bottom-up pathway生成的相同大小的feature map进行融合(merge)。

注:上采样尺度因子为2,因为为了和之前下采样卷积的尺度因子=2一样。上采样是放大,下采样是缩小。

具体操作如下图所示,上采样(2x up)feature map与相同大小的bottom-up feature map进行逐像素相加融合(element-wise addition),其中bottom-up feature先要经过1x1卷积层,目的是为了减少通道维度(reduce channel dimensions)。

注:减少通道维度是为了将bottom-up feature map的通道数量与top-down feature map的通道数量保持一致,又因为两者feature map大小一致,所以可以进行对应位置像素的叠加(element-wise addition)。

8.2.6 Mask R-CNN

Mask R-CNN有哪些创新点?

  1. Backbone:ResNeXt-101+FPN
  2. RoI Align替换RoI Pooling

Mask R-CNN是一个实例分割(Instance segmentation)算法,主要是在目标检测的基础上再进行分割。Mask R-CNN算法主要是Faster R-CNN+FCN,更具体一点就是ResNeXt+RPN+RoI Align+Fast R-CNN+FCN。

![](ch8/Mask R-CNN-01.png)

Mask R-CNN算法步骤

  1. 输入一幅你想处理的图片,然后进行对应的预处理操作,或者预处理后的图片;
  2. 将其输入到一个预训练好的神经网络中(ResNeXt等)获得对应的feature map;
  3. 对这个feature map中的每一点设定预定个的RoI,从而获得多个候选RoI;
  4. 将这些候选的RoI送入RPN网络进行二值分类(前景或背景)和BB回归,过滤掉一部分候选的RoI;
  5. 对这些剩下的RoI进行RoI Align操作(即先将原图和feature map的pixel对应起来,然后将feature map和固定的feature对应起来);
  6. 对这些RoI进行分类(N类别分类)、BB回归和MASK生成(在每一个RoI里面进行FCN操作)。

RoI Pooling和RoI Align有哪些不同?

ROI Align 是在Mask-RCNN中提出的一种区域特征聚集方式,很好地解决了RoI Pooling操作中两次量化造成的区域不匹配(mis-alignment)的问题。实验显示,在检测测任务中将 RoI Pooling 替换为 RoI Align 可以提升检测模型的准确性。

在常见的两级检测框架(比如Fast-RCNN,Faster-RCNN,RFCN)中,RoI Pooling 的作用是根据预选框的位置坐标在特征图中将相应区域池化为固定尺寸的特征图,以便进行后续的分类和包围框回归操作。由于预选框的位置通常是由模型回归得到的,一般来讲是浮点数,而池化后的特征图要求尺寸固定。故RoI Pooling这一操作存在两次量化的过程。

  • 将候选框边界量化为整数点坐标值。
  • 将量化后的边界区域平均分割成 $k\times k$ 个单元(bin),对每一个单元的边界进行量化。

事实上,经过上述两次量化,此时的候选框已经和最开始回归出来的位置有一定的偏差,这个偏差会影响检测或者分割的准确度。在论文里,作者把它总结为“不匹配问题(misalignment)”。

下面我们用直观的例子具体分析一下上述区域不匹配问题。如下图所示,这是一个Faster-RCNN检测框架。输入一张 $800\times 800$ 的图片,图片上有一个 $665\times 665$ 的包围框(框着一只狗)。图片经过主干网络提取特征后,特征图缩放步长(stride)为32。因此,图像和包围框的边长都是输入时的1/32。800正好可以被32整除变为25。但665除以32以后得到20.78,带有小数,于是RoI Pooling 直接将它量化成20。接下来需要把框内的特征池化 $7\times 7$ 的大小,因此将上述包围框平均分割成 $7\times 7$ 个矩形区域。显然,每个矩形区域的边长为2.86,又含有小数。于是ROI Pooling 再次把它量化到2。经过这两次量化,候选区域已经出现了较明显的偏差(如图中绿色部分所示)。更重要的是,该层特征图上0.1个像素的偏差,缩放到原图就是3.2个像素。那么0.8的偏差,在原图上就是接近30个像素点的差别,这一差别不容小觑。

![](ch8/Mask R-CNN-02.png)

为了解决RoI Pooling的上述缺点,作者提出了RoI Align这一改进的方法(如图2)。

![](ch8/Mask R-CNN-03.png)

RoI Align的思路很简单:取消量化操作,使用双线性内插的方法获得坐标为浮点数的像素点上的图像数值,从而将整个特征聚集过程转化为一个连续的操作。值得注意的是,在具体的算法操作上,RoI Align并不是简单地补充出候选区域边界上的坐标点,然后将这些坐标点进行池化,而是重新设计了一套比较优雅的流程,如下图所示:

  1. 遍历每一个候选区域,保持浮点数边界不做量化。

  2. 将候选区域分割成 $k\times k$ 个单元,每个单元的边界也不做量化。

  3. 在每个单元中计算固定四个坐标位置,用双线性内插的方法计算出这四个位置的值,然后进行最大池化操作。

这里对上述步骤的第三点作一些说明:这个固定位置是指在每一个矩形单元(bin)中按照固定规则确定的位置。比如,如果采样点数是1,那么就是这个单元的中心点。如果采样点数是4,那么就是把这个单元平均分割成四个小方块以后它们分别的中心点。显然这些采样点的坐标通常是浮点数,所以需要使用插值的方法得到它的像素值。在相关实验中,作者发现将采样点设为4会获得最佳性能,甚至直接设为1在性能上也相差无几。事实上,RoI Align 在遍历取样点的数量上没有RoI Pooling那么多,但却可以获得更好的性能,这主要归功于解决了mis alignment的问题。值得一提的是,我在实验时发现,RoI Align在VOC 2007数据集上的提升效果并不如在COCO上明显。经过分析,造成这种区别的原因是COCO上小目标的数量更多,而小目标受mis alignment问题的影响更大(比如,同样是0.5个像素点的偏差,对于较大的目标而言显得微不足道,但是对于小目标,误差的影响就要高很多)。

![](ch8/Mask R-CNN-04.png)

8.2.7 DetNet(贡献者:北京理工大学–明奇)

DetNet是发表在ECCV2018的论文,比较新,出发点是现有的检测任务backbone都是从分类任务衍生而来的,因此作者想针对检测专用的backbone做一些讨论和研究而设计了DetNet,思路比较新奇。

  1. Introduction
      很多backbone的提出都是用于挑战ImageNet分类任务后被应用到检测上来,而鲜有单独针对检测任务设计的backbone

  检测和分类有明显的区别:(1)不仅需要分类,还需要精确的定位 (2)最近的检测器都是基于类似FPN结构,在分类网络基础上加额外多尺度特征进行检测,应对不同尺度变化的目标。这两点又是相互补充,共同协助网络完成分类到检测任务的转变。例如分类任务是检测的一环所以必不可少,但是传统分类采用的最高级特征定位细节不够,因此很多最近网络设法用类似FPN的结构去处理尺度变化的问题,就将分类较好地过渡到检测任务上了。

  1. DetNet

2.1 Motivation
  主要着眼点是分辨率,从大目标和小目标分别阐述保持分辨率的重要性。所以DetNet也是从分辨率的保持着手,解决多尺度物体的识别问题。

  • Weak visibility of large objects
      网络在较深层如P6(FPN)P7(RetinaNet)大目标的边界不明确使精确定位困难。

  • Invisibility of small objects
      小目标就很惨了,降采样容易丢。这个就不赘述了,所以只要避开降采样就能防止目标丢失,但是这种方法又会导致抽象能力不够

​ 2.2 DetNet Design
  保持分辨率有两个麻烦的问题:(1)内存消耗大,计算大 (2)降采样减少导致高层的抽象特征不足以很好地进行分类任务。下面设计时会同时考虑时间和高层抽象信息两点。
  先放出DetNet的多尺度各stage的尺寸如下图, 可以看到,相比前两种方式,DetNet在P4之后就不再进一步降采样了,进行分辨率的保持。

  实现细节如下图:

  • 采用的backbone是ResNet-50,改进设计了DetNet-59。
  • 对bottlenecks进行了改进,传统的其实不止C,也包含两种,即将AB的膨胀卷积换成普通卷积。AB是新的基础模块。
  • 为了减少分辨率保持带来的时间和内存成本消耗,通道数固定为256(思考:降采样和膨胀卷积都会有信息丢失,这里可以想想)。
  • DetNet也可以加FPN结构,方法类似。
  1. Experiments
      检测和训练的细节配置就不看了。

3.1 Main Results

  • 在FPN基础上明显有大物体涨点,同时由于高分辨率,小物体也有不错的提升。
  • 膨胀卷积提供的大感受野使得分类也不逊色

​ 3.2 Results analysis

  • 从AP50看出,高好1.7;从AP80看出,高了3.7。由此可以看出确实提高了检测性能。(

  • 从定位性能来看,大物体的提升比小物体更多。作者认为是高分辨率解决了大物体边界模糊的问题。其实有一种解释:小目标没有大目标明显,因为膨胀卷积核降采样都会丢失小目标,只是膨胀卷积可能离散采样不至于像降采样直接给到后面没了,但是没有根本性的解决,所以小目标不大。

  • AR指标也有类似结论

  • AR50体现了小目标的查全率更好,这也印证上面分析的:相对降采样,膨胀卷积丢失会好点。此下大目标效果虽然提升不大但是也很高了,作者表示DetNet擅长找到更精确的定位目标,在AR85的高指标就能看出。

  • AR85看大目标丢失少,说明能够像 VGG一样对大目标效果优良。关于小目标的效果平平,作者认为没有必要太高,因为FPN结构对小目标已经利用地很充分了,这里即使不高也没事。

3.3 Discussion

  • 关于stage
      为了研究backbone对检测的影响,首先研究stage的作用。前4个还好说,和ResNet一样,但是P5 P6就不同,没有尺度的变化,和传统意义的stage不一样了,需要重新定义。这里DetNet也是类似ResNet的方法,虽然没有尺度变化,但是AB模块的位置还是保持了,B开启一个stage(听上去有点牵强)。如下图,认为新加的仍属于P5。

  验证方法是做了实验,将P6开始的block换成上图所示的A模块对比效果如下图。 发现还是加了B效果更好。(但是这个stage和传统意义很不一样,所以很多性质不能相提并论,只是B模块的改变也不好判定什么)

8.2.8 CBNet

本部分介绍一篇在COCO数据集达到最高单模型性能——mAP 53.3的网络,论文于2019.9.3发布在ArXiv,全名是CBNet: A Novel Composite Backbone Network Architecture for Object Detection

  1. Introduction

  名义上是单模型,实际是多模型的特征融合,只是和真正的多模型策略略有不同。作者的起点是,设计新的模型往往需要在ImageNet上进行预训练,比较麻烦。因而提出的Composite Backbone Network (CBNet),采用经典网络的多重组合的方式构建网络,一方面可以提取到更有效的特征,另一方面也能够直接用现成的预训练参数(如ResNet,ResNeXt等)比较简单高效。

  1. Proposed method

    2.1 Architecture of CBNet

  如上图,模型中采用K个(K>1)相同的结构进行紧密联结。其中两个相同backbone的叫Dual-Backbone (DB),三个叫Triple- Backbone (TB);L代表backbone的stage数目,这里统一设置为L=5。其中,和前任工作不同的地方在于,这里将不同的stage信息进行复用回传,以便获取更好的特征(为什么work不好说)。

2.2 Other possible composite styles

  相关工作的其他类似结构,大同小异。要么是前面backbone的stage往后传播,要么是往前一个传播,每个都有一篇论文,应该都会给出不同的解释;第四个结构不太一样,是类似densnet的结构,但是密集连接+多backbone assemble的内存消耗不出意外会非常大。但是脱离这些体系来看,多backbone的结构类似多模型的assemble,和单模型有点不公平。

  1. Experiment
  • result

COCO数据集上的结果。看来提升还是有的。但是也能看出,大趋势上,三阶级联效果不如两阶的提升大,也是这部分的特征提升空间有限的缘故,到底哪部分在work不好说。下图的研究就更说明这一点了,斜率逐渐减小。

  • Comparisons of different composite styles

他的级联网络相比,作者的阐述点只落脚于特征的利用情况,但是这个东西本身就很玄乎,不好说到底怎么算利用得好。硬要说这种做法的解释性,大概就是将backbone方向的后面高级语义特征传播回前面进行加强,相当于横向的FPN传播。

  • Number of backbones in CBNet

速度慢是必然的,FPN+ResNeXt为8fps,加上两个backboen后为5.5FPS;如果减去backbone的前两个stage,可以节省部分参数达到6.9FPS,而精度下降不大(整体速度太低,这个实验意义不大)

  • Sharing weights for CBNet

从中可以看出其实权重是否share区别不大, 不到一个点的降幅,参数量减少。

  • Effectiveness of basic feature enhancement by CBNet

从中可以看出激活响应效果更好,确实是能够提取到更为有效的特征,对物体的响应更加敏感。

8.3 One Stage目标检测算法

我们将对单次目标检测器(包括SSD系列和YOLO系列等算法)进行综述。我们将分析FPN以理解多尺度特征图如何提高准确率,特别是小目标的检测,其在单次检测器中的检测效果通常很差。然后我们将分析Focal loss和RetinaNet,看看它们是如何解决训练过程中的类别不平衡问题的。

8.3.1 SSD

SSD有哪些创新点?

  1. 基于Faster R-CNN中的Anchor,提出了相似的先验框(Prior box)
  2. 从不同比例的特征图(多尺度特征)中产生不同比例的预测,并明确地按长宽比分离预测。

不同于前面的R-CNN系列,SSD属于one-stage方法。SSD使用 VGG16 网络作为特征提取器(和 Faster R-CNN 中使用的 CNN 一样),将后面的全连接层替换成卷积层,并在之后添加自定义卷积层,并在最后直接采用卷积进行检测。在多个特征图上设置不同缩放比例和不同宽高比的先验框以融合多尺度特征图进行检测,靠前的大尺度特征图可以捕捉到小物体的信息,而靠后的小尺度特征图能捕捉到大物体的信息,从而提高检测的准确性和定位的准确性。如下图是SSD的网络结构图。

1. 怎样设置default boxes?
SSD中default box的概念有点类似于Faster R-CNN中的anchor。不同于Faster R-CNN只在最后一个特征层取anchor, SSD在多个特征层上取default box,可以得到不同尺度的default box。在特征图的每个单元上取不同宽高比的default box,一般宽高比在{1,2,3,1/2,1/3}中选取,有时还会额外增加一个宽高比为1但具有特殊尺度的box。如下图所示,在8x8的feature map和4x4的feature map上的每个单元取4个不同的default box。原文对于300x300的输入,分别在conv4_3, conv7,conv8_2,conv9_2,conv10_2,conv11_2的特征图上的每个单元取4,6,6,6,4,4个default box. 由于以上特征图的大小分别是38x38,19x19,10x10,5x5,3x3,1x1,所以一共得到38x38x4+19x19x6+10x10x6+5x5x6+
3x3x4+1x1x4=8732个default box.对一张300x300的图片输入网络将会针对这8732个default box预测8732个边界框。

2. 怎样对先验框进行匹配?
SSD在训练的时候只需要输入图像和图像中每个目标对应的ground truth. 先验框与ground truth 的匹配遵循两个原则:

(1)对图片中的每个ground truth, 在先验框中找到与其IOU最大的先验框,则该先验框对应的预测边界框与ground truth 匹配。

(2)对于(1)中每个剩下的没有与任何ground truth匹配到的先验框,找到与其IOU最大的ground truth,若其与该ground truth的IOU值大于某个阈值(一般设为0.5),则该先验框对应的预测边界框与该ground truth匹配。

按照这两个原则进行匹配,匹配到ground truth的先验框对应的预测边界框作为正样本,没有匹配到ground truth的先验框对应的预测边界框作为负样本。尽管一个ground truth可以与多个先验框匹配,但是ground truth的数量相对先验框还是很少,按照上面的原则进行匹配还是会造成负样本远多于正样本的情况。为了使正负样本尽量均衡(一般保证正负样本比例约为1:3),SSD采用hard negative mining, 即对负样本按照其预测背景类的置信度进行降序排列,选取置信度较小的top-k作为训练的负样本。

3. 怎样得到预测的检测结果?

最后分别在所选的特征层上使用3x3卷积核预测不同default boxes所属的类别分数及其预测的边界框location。由于对于每个box需要预测该box属于每个类别的置信度(假设有c类,包括背景,例如20class的数据集合,c=21)和该box对应的预测边界框的location(包含4个值,即该box的中心坐标和宽高),则每个box需要预测c+4个值。所以对于某个所选的特征层,该层的卷积核个数为(c+4)x 该层的default box个数.最后将每个层得到的卷积结果进行拼接。对于得到的每个预测框,取其类别置信度的最大值,若该最大值大于置信度阈值,则最大值所对应的类别即为该预测框的类别,否则过滤掉此框。对于保留的预测框根据它对应的先验框进行解码得到其真实的位置参数(这里还需注意要防止预测框位置超出图片),然后根据所属类别置信度进行降序排列,取top-k个预测框,最后进行NMS,过滤掉重叠度较大的预测框,最后得到检测结果。

SSD优势是速度比较快,整个过程只需要一步,首先在图片不同位置按照不同尺度和宽高比进行密集抽样,然后利用CNN提取特征后直接进行分类与回归,所以速度比较快,但均匀密集采样会造成正负样本不均衡的情况使得训练比较困难,导致模型准确度有所降低。另外,SSD对小目标的检测没有大目标好,因为随着网络的加深,在高层特征图中小目标的信息丢失掉了,适当增大输入图片的尺寸可以提升小目标的检测效果。

8.3.2 DSSD

DSSD有哪些创新点?

  1. Backbone:将ResNet替换SSD中的VGG网络,增强了特征提取能力
  2. 添加了Deconvolution层,增加了大量上下文信息

为了解决SSD算法检测小目标困难的问题,DSSD算法将SSD算法基础网络从VGG-16更改为ResNet-101,增强网络特征提取能力,其次参考FPN算法思路利用去Deconvolution结构将图像深层特征从高维空间传递出来,与浅层信息融合,联系不同层级之间的图像语义关系,设计预测模块结构,通过不同层级特征之间融合特征输出预测物体类别信息。

DSSD算法中有两个特殊的结构:Prediction模块;Deconvolution模块。前者利用提升每个子任务的表现来提高准确性,并且防止梯度直接流入ResNet主网络。后者则增加了三个Batch Normalization层和三个3×3卷积层,其中卷积层起到了缓冲的作用,防止梯度对主网络影响太剧烈,保证网络的稳定性。

SSD和DSSD的网络模型如下图所示:

Prediction Module

SSD直接从多个卷积层中单独引出预测函数,预测量多达7000多,梯度计算量也很大。MS-CNN方法指出,改进每个任务的子网可以提高准确性。根据这一思想,DSSD在每一个预测层后增加残差模块,并且对于多种方案进行了对比,如下图所示。结果表明,增加残差预测模块后,高分辨率图片的检测精度比原始SSD提升明显。

Deconvolution模块

为了整合浅层特征图和deconvolution层的信息,作者引入deconvolution模块,如下图所示。作者受到论文Learning to Refine Object Segments的启发,认为用于精细网络的deconvolution模块的分解结构达到的精度可以和复杂网络一样,并且更有效率。作者对其进行了一定的修改:其一,在每个卷积层后添加批归一化(batch normalization)层;其二,使用基于学习的deconvolution层而不是简单地双线性上采样;其三,作者测试了不同的结合方式,元素求和(element-wise sum)与元素点积(element-wise product)方式,实验证明元素点积计算能得到更好的精度。

8.3.3 YOLOv1

YOLOv1有哪些创新点?

  1. 将整张图作为网络的输入,直接在输出层回归bounding box的位置和所属的类别
  2. 速度快,one stage detection的开山之作

YOLOv1介绍

YOLO(You Only Look Once: Unified, Real-Time Object Detection)是one-stage detection的开山之作。之前的物体检测方法首先需要产生大量可能包含待检测物体的先验框, 然后用分类器判断每个先验框对应的边界框里是否包含待检测物体,以及物体所属类别的概率或者置信度,同时需要后处理修正边界框,最后基于一些准则过滤掉置信度不高和重叠度较高的边界框,进而得到检测结果。这种基于先产生候选区再检测的方法虽然有相对较高的检测准确率,但运行速度较慢。

YOLO创造性的将物体检测任务直接当作回归问题(regression problem)来处理,将候选区和检测两个阶段合二为一。只需一眼就能知道每张图像中有哪些物体以及物体的位置。下图展示了各物体检测系统的流程图。

事实上,YOLO也并没有真正的去掉候选区,而是直接将输入图片划分成7x7=49个网格,每个网格预测两个边界框,一共预测49x2=98个边界框。可以近似理解为在输入图片上粗略的选取98个候选区,这98个候选区覆盖了图片的整个区域,进而用回归预测这98个候选框对应的边界框。

1. 网络结构是怎样的?

YOLO网络借鉴了GoogLeNet分类网络结构,不同的是YOLO使用1x1卷积层和3x3卷积层替代inception module。如下图所示,整个检测网络包括24个卷积层和2个全连接层。其中,卷积层用来提取图像特征,全连接层用来预测图像位置和类别概率值。

2. YOLO的输入、输出、损失函数分别是什么?

前面说到YOLO将输入图像分成7x7的网格,最后输出是7x7xk的张量。YOLO网络最后接了两个全连接层,全连接层要求输入是固定大小的,所以YOLO要求输入图像有固定大小,论文中作者设计的输入尺寸是448x448。

YOLO将输入图像分成7x7的网格,每个网格预测2个边界框。若某物体的ground truth的中心落在该网格,则该网格中与这个ground truth IOU最大的边界框负责预测该物体。对每个边界框会预测5个值,分别是边界框的中心x,y(相对于所属网格的边界),边界框的宽高w,h(相对于原始输入图像的宽高的比例),以及这些边界框的confidencescores(边界框与ground truth box的IOU值)。同时每个网格还需要预测c个类条件概率 (是一个c维向量,表示某个物体object在这个网格中,且该object分别属于各个类别的概率,这里的c类物体不包含背景)。论文中的c=20,则每个网格需要预测2x5+20=30个值,这些值被映射到一个30维的向量。
为了让边界框坐标损失、分类损失达到很好的平衡,损失函数设计如下图所示。

如上图所示,损失函数分为坐标预测(蓝色框)、含有物体的边界框的confidence预测(红色框)、不含有物体的边界框的confidence预测(黄色框)、分类预测(紫色框)四个部分。

由于不同大小的边界框对预测偏差的敏感度不同,小的边界框对预测偏差的敏感度更大。为了均衡不同尺寸边界框对预测偏差的敏感度的差异。作者巧妙的对边界框的w,h取均值再求L2 loss. YOLO中更重视坐标预测,赋予坐标损失更大的权重,记为 coord,在pascal voc训练中coodd=5 ,classification error部分的权重取1。

某边界框的置信度定义为:某边界框的confidence = 该边界框存在某类对象的概率pr(object)*该边界框与该对象的ground truth的IOU值 ,若该边界框存在某个对象pr(object)=1 ,否则pr(object)=0 。由于一幅图中大部分网格中是没有物体的,这些网格中的边界框的confidence置为0,相比于有物体的网格,这些不包含物体的网格更多,对梯度更新的贡献更大,会导致网络不稳定。为了平衡上述问题,YOLO损失函数中对没有物体的边界框的confidence error赋予较小的权重,记为 noobj,对有物体的边界框的confidence error赋予较大的权重。在pascal VOC训练中noobj=0.5 ,有物体的边界框的confidence error的权重设为1.

3. YOLO怎样预测?

YOLO最后采用非极大值抑制(NMS)算法从输出结果中提取最有可能的对象和其对应的边界框。

输入一张图片到YOLO网络将输出一个7730的张量表示图片中每个网格对应的可能的两个边界框以及每个边界框的置信度和包含的对象属于各个类别的概率。由此可以计算某对象i属于类别 同时在第j个边界框中的得分:

每个网格有20个类条件概率,2个边界框置信度,相当于每个网格有40个得分,7x7个网格有1960个得分,每类对象有1960/20=98个得分,即98个候选框。

NMS步骤如下:

1.设置一个Score的阈值,一个IOU的阈值;

2.对于每类对象,遍历属于该类的所有候选框,

①过滤掉Score低于Score阈值的候选框;

②找到剩下的候选框中最大Score对应的候选框,添加到输出列表;

③进一步计算剩下的候选框与②中输出列表中每个候选框的IOU,若该IOU大于设置的IOU阈值,将该候选框过滤掉,否则加入输出列表中;

④最后输出列表中的候选框即为图片中该类对象预测的所有边界框

3.返回步骤2继续处理下一类对象。

YOLO将识别与定位合二为一,结构简便,检测速度快,更快的Fast YOLO可以达到155FPS。相对于R-CNN系列, YOLO的整个流程中都能看到整张图像的信息,因此它在检测物体时能很好的利用上下文信息,从而不容易在背景上预测出错误的物体信息。同时YOLO可以学习到高度泛化的特征,能将一个域上学到的特征迁移到不同但相关的域上,如在自然图像上做训练的YOLO,在艺术图片上可以得到较好的测试结果。

由于YOLO网格设置比较稀疏,且每个网格只预测2个边界框,其总体预测精度不高,略低于Fast RCNN。其对小物体的检测效果较差,尤其是对密集的小物体表现比较差。

8.3.4 YOLOv2

YOLOv2 有哪些创新点?

YOLOv1虽然检测速度快,但在定位方面不够准确,并且召回率较低。为了提升定位准确度,改善召回率,YOLOv2在YOLOv1的基础上提出了几种改进策略,如下图所示,可以看到,一些改进方法能有效提高模型的mAP。

  1. 大尺度预训练分类
  2. New Network:Darknet-19
  3. 加入anchor

YOLOv2 介绍

(1)Batch Normalization

YOLOv2中在每个卷积层后加Batch Normalization(BN)层,去掉dropout. BN层可以起到一定的正则化效果,能提升模型收敛速度,防止模型过拟合。YOLOv2通过使用BN层使得mAP提高了2%。
(2)High Resolution Classifier

目前的大部分检测模型都会使用主流分类网络(如vgg、resnet)在ImageNet上的预训练模型作为特征提取器,
而这些分类网络大部分都是以小于256x256的图片作为输入进行训练的,低分辨率会影响模型检测能力。YOLOv2将输入图片的分辨率提升至448x448,为了使网络适应新的分辨率,YOLOv2先在ImageNet上以448x448的分辨率对网络进行10个epoch的微调,让网络适应高分辨率的输入。通过使用高分辨率的输入,YOLOv2的mAP提升了约4%。

(3)Convolutional With Anchor Boxes

YOLOv1利用全连接层直接对边界框进行预测,导致丢失较多空间信息,定位不准。YOLOv2去掉了YOLOv1中的全连接层,使用Anchor Boxes预测边界框,同时为了得到更高分辨率的特征图,YOLOv2还去掉了一个池化层。由于图片中的物体都倾向于出现在图片的中心位置,若特征图恰好有一个中心位置,利用这个中心位置预测中心点落入该位置的物体,对这些物体的检测会更容易。所以总希望得到的特征图的宽高都为奇数。YOLOv2通过缩减网络,使用416x416的输入,模型下采样的总步长为32,最后得到13x13的特征图,然后对13x13的特征图的每个cell预测5个anchor boxes,对每个anchor box预测边界框的位置信息、置信度和一套分类概率值。使用anchor
boxes之后,YOLOv2可以预测13x13x5=845个边界框,模型的召回率由原来的81%提升到88%,mAP由原来的69.5%降低到69.2%.召回率提升了7%,准确率下降了0.3%。

(4)Dimension Clusters

在Faster R-CNN和SSD中,先验框都是手动设定的,带有一定的主观性。YOLOv2采用k-means聚类算法对训练集中的边界框做了聚类分析,选用boxes之间的IOU值作为聚类指标。综合考虑模型复杂度和召回率,最终选择5个聚类中心,得到5个先验框,发现其中中扁长的框较少,而瘦高的框更多,更符合行人特征。通过对比实验,发现用聚类分析得到的先验框比手动选择的先验框有更高的平均IOU值,这使得模型更容易训练学习。

(5)New Network:Darknet-19

YOLOv2采用Darknet-19,其网络结构如下图所示,包括19个卷积层和5个max pooling层,主要采用3x3卷积和1x1卷积,这里1x1卷积可以压缩特征图通道数以降低模型计算量和参数,每个卷积层后使用BN层以加快模型收敛同时防止过拟合。最终采用global avg pool 做预测。采用YOLOv2,模型的mAP值没有显著提升,但计算量减少了。

(6)Direct location prediction

Faster R-CNN使用anchor boxes预测边界框相对先验框的偏移量,由于没有对偏移量进行约束,每个位置预测的边界框可以落在图片任何位置,会导致模型不稳定,加长训练时间。YOLOv2沿用YOLOv1的方法,根据所在网格单元的位置来预测坐标,则Ground Truth的值介于0到1之间。网络中将得到的网络预测结果再输入sigmoid函数中,让输出结果介于0到1之间。设一个网格相对于图片左上角的偏移量是cx,cy。先验框的宽度和高度分别是pw和ph,则预测的边界框相对于特征图的中心坐标(bx,by)和宽高bw、bh的计算公式如下图所示。

YOLOv2结合Dimention Clusters, 通过对边界框的位置预测进行约束,使模型更容易稳定训练,这种方式使得模型的mAP值提升了约5%。

(7)Fine-Grained Features

YOLOv2借鉴SSD使用多尺度的特征图做检测,提出pass through层将高分辨率的特征图与低分辨率的特征图联系在一起,从而实现多尺度检测。YOLOv2提取Darknet-19最后一个max pool层的输入,得到26x26x512的特征图。经过1x1x64的卷积以降低特征图的维度,得到26x26x64的特征图,然后经过pass through层的处理变成13x13x256的特征图(抽取原特征图每个2x2的局部区域组成新的channel,即原特征图大小降低4倍,channel增加4倍),再与13x13x1024大小的特征图连接,变成13x13x1280的特征图,最后在这些特征图上做预测。使用Fine-Grained Features,YOLOv2的性能提升了1%.

(8)Multi-Scale Training

YOLOv2中使用的Darknet-19网络结构中只有卷积层和池化层,所以其对输入图片的大小没有限制。YOLOv2采用多尺度输入的方式训练,在训练过程中每隔10个batches,重新随机选择输入图片的尺寸,由于Darknet-19下采样总步长为32,输入图片的尺寸一般选择32的倍数{320,352,…,608}。采用Multi-Scale Training, 可以适应不同大小的图片输入,当采用低分辨率的图片输入时,mAP值略有下降,但速度更快,当采用高分辨率的图片输入时,能得到较高mAP值,但速度有所下降。

YOLOv2借鉴了很多其它目标检测方法的一些技巧,如Faster R-CNN的anchor boxes, SSD中的多尺度检测。除此之外,YOLOv2在网络设计上做了很多tricks,使它能在保证速度的同时提高检测准确率,Multi-Scale Training更使得同一个模型适应不同大小的输入,从而可以在速度和精度上进行自由权衡。

YOLOv2的训练

YOLOv2的训练主要包括三个阶段。
第一阶段:先在ImageNet分类数据集上预训练Darknet-19,此时模型输入为 $224\times 224$ ,共训练160个epochs。
第二阶段:将网络的输入调整为 $448\times 448$ ,继续在ImageNet数据集上finetune分类模型,训练10个epochs,此时分类模型的top-1准确度为76.5%,而top-5准确度为93.3%。
第三个阶段:修改Darknet-19分类模型为检测模型,并在检测数据集上继续finetune网络。
网络修改包括(网路结构可视化):移除最后一个卷积层、global avgpooling层以及softmax层,并且新增了三个 $3\times 3 \times 2014$ 卷积层,同时增加了一个passthrough层,最后使用 $1\times 1$ 卷积层输出预测结果。

8.3.5 YOLO9000

github:http://pjreddie.com/yolo9000/

YOLO9000是在YOLOv2的基础上提出的一种联合训练方法,可以检测超过9000个类别的模型。YOLOv2混合目标检测数据集和分类数据集,用目标检测数据集及其类别标记信息和位置标注信息训练模型学习预测目标定位和分类,用分类数据集及其类别标记信息进一步扩充模型所能识别的物体类别同时能增强模型鲁棒性。

1. YOLO9000是怎么组织数据的?

YOLO9000根据各个类别之间的从属关系建立一种树结WordTree, 将COCO数据集和ImageNet数据集组织起来。

WordTree的生成方式如下:

①首先遍历ImageNet中的类别名词。

②对每个名词,在WordNet(一种结构化概念及概念之间关系的语言数据库)上找到从它所在位置到根节点(设根节点为实体对象physical object)的最短路径,由于在WordNet中大多数同义词只有一个路径,所以先把将该路径上的词全都加到树中。

③迭代地检查剩下的名词,取它到根节点的最短路径,将该最短路径上的还没出现在层次树中的词加入到树中。
混合后的数据集形成一个有9418类的WordTree.生成的WordTree模型如下图所示。另外考虑到COCO数据集相对于ImageNet数据集数据量太少了,为了平衡两个数据集,作者进一步对COCO数据集过采样,使COCO数据集与ImageNet数据集的数据量比例接近1:4。

对于物体的标签,采用one-hot编码的形式,数据集中的每个物体的类别标签被组织成1个长度为9418的向量,向量中除在WordTree中从该物体对应的名词到根节点的路径上出现的词对应的类别标号处为1,其余位置为0。

2. YOLO9000是怎么进行联合训练的?

YOLO9000采用YOLOv2的结构,anchorbox由原来的5调整到3,对每个anchorbox预测其对应的边界框的位置信息x,y,w,h和置信度以及所包含的物体分别属于9418类的概率,所以每个anchorbox需要预测4+1+9418=9423个值。每个网格需要预测3x9423=28269个值。在训练的过程中,当网络遇到来自检测数据集的图片时,用完整的YOLOv2loss进行反向传播计算,当网络遇到来自分类数据集的图片时,只用分类部分的loss进行反向传播。

3. YOLO9000是怎么预测的?

WordTree中每个节点的子节点都属于同一个子类,分层次的对每个子类中的节点进行一次softmax处理,以得到同义词集合中的每个词的下义词的概率。当需要预测属于某个类别的概率时,需要预测该类别节点的条件概率。即在WordTree上找到该类别名词到根节点的路径,计算路径上每个节点的概率之积。预测时,YOLOv2得到置信度,同时会给出边界框位置以及一个树状概率图,沿着根节点向下,沿着置信度最高的分支向下,直到达到某个阈值,最后到达的节点类别即为预测物体的类别。

YOLO9000使用WordTree混合目标检测数据集和分类数据集,并在其上进行联合训练,使之能实时检测出超过9000个类别的物体,其强大令人赞叹不已。YOLO9000尤其对动物的识别效果很好,但是对衣服或者设备等类别的识别效果不是很好,可能的原因是与目标检测数据集中的数据偏向有关。

8.3.6 YOLOv3

YOLOv3总结了自己在YOLOv2的基础上做的一些尝试性改进,有的尝试取得了成功,而有的尝试并没有提升模型性能。其中有两个值得一提的亮点,一个是使用残差模型,进一步加深了网络结构;另一个是使用FPN架构实现多尺度检测。

YOLOv3有哪些创新点?

  1. 新网络结构:DarkNet-53
  2. 融合FPN
  3. 用逻辑回归替代softmax作为分类器

1. YOLOv3对网络结构做了哪些改进?

YOLOv3在之前Darknet-19的基础上引入了残差块,并进一步加深了网络,改进后的网络有53个卷积层,取名为Darknet-53,网络结构如下图所示(以256*256的输入为例)。

为了比较Darknet-53与其它网络结构的性能,作者在TitanX上,采用相同的实验设置,将256x256的图片分别输入以Darknet-19,ResNet-101,ResNet-152和Darknet-53为基础网络的分类模型中,实验得到的结果如下图所示。可以看到Darknet-53比ResNet-101的性能更好,而且速度是其1.5倍,Darknet-53与ResNet-152性能相似但速度几乎是其2倍。注意到,Darknet-53相比于其它网络结构实现了每秒最高的浮点计算量,说明其网络结构能更好的利用GPU。

2.YOLOv3中怎样实现多尺度检测?

YOLOv3借鉴了FPN的思想,从不同尺度提取特征。相比YOLOv2,YOLOv3提取最后3层特征图,不仅在每个特征图上分别独立做预测,同时通过将小特征图上采样到与大的特征图相同大小,然后与大的特征图拼接做进一步预测。用维度聚类的思想聚类出9种尺度的anchor box,将9种尺度的anchor box均匀的分配给3种尺度的特征图.如下图是在网络结构图的基础上加上多尺度特征提取部分的示意图(以在COCO数据集(80类)上256x256的输入为例):

从YOLOv1到YOLOv2再到YOLO9000、YOLOv3, YOLO经历三代变革,在保持速度优势的同时,不断改进网络结构,同时汲取其它优秀的目标检测算法的各种trick,先后引入anchor box机制、引入FPN实现多尺度检测等。

8.3.7 RetinaNet

研究背景

  • Two-Stage检测器(如Faster R-CNN、FPN)效果好,但速度相对慢
  • One-Stage检测器(如YOLO、SSD)速度快,但效果一般

作者对one-stage检测器准确率不高的问题进行探究,发现主要问题在于正负类别不均衡(简单-难分类别不均衡)。

We discover that the extreme foreground-background class imbalance encountered during training of dense detectors is the central cause.

作者建议通过重新设计标准的交叉熵损失(cross entropy loss)来解决这种类别不平衡(class inbalance)问题,即提出Focal Loss。

We propose to address this class imbalance by reshaping the standard cross entropy loss such that it down-weights the loss assigned to well-classified examples. Our novel Focal Loss focuses training on a sparse set of hard examples and prevents the vast number of easy negatives from overwhelming the detector during training.

结合Focal Loss的one-stage检测器称为RetinaNet,该检测器在COCO上mAP可以和特征金字塔网络(feature pyramid network,FPN)或者Mask R-CNN接近,

问:什么是类别不均衡(class imbalance)?

答:负样本的数量极大于正样本的数量,比如包含物体的区域(正样本)很少,而不包含物体的区域(负样本)很多。比如检测算法在早期会生成一大波的bbox。而一幅常规的图片中,顶多就那么几个object。这意味着,绝大多数的bbox属于background。

问:样本的类别不均衡会带来什么问题?

答:由于大多数都是简单易分的负样本(属于背景的样本),使得训练过程不能充分学习到属于那些有类别样本的信息;其次简单易分的负样本太多,可能掩盖了其他有类别样本的作用(这些简单易分的负样本仍产生一定幅度的loss,见下图蓝色曲线,数量多会对loss起主要贡献作用,因此就主导了梯度的更新方向,掩盖了重要的信息)

This imbalance causes two problems: (1) training is inefficient as most locations are easy negatives that contribute no useful learning signal; (2) en masse, the easy negatives can overwhelm training and lead to degenerate models.

简单来说,因为bbox数量爆炸。 正是因为bbox中属于background的bbox太多了,所以如果分类器无脑地把所有bbox统一归类为background,accuracy也可以刷得很高。于是乎,分类器的训练就失败了。分类器训练失败,检测精度自然就低了。

问:为什么在two-stage检测器中,没有出现类别不均衡(class imbalamce)问题呢?

答:因为通过RPN阶段可以减少候选目标区域,而在分类阶段,可以固定前景与背景比值(foreground-to-background ratio)为1:3,或者使用OHEM(online hard example mining)使得前景和背景的数量达到均衡。

RetinaNet有哪些创新点?

概述:

  • New loss:提出Focal Loss函数解决class imbalance
$$ FL(p_t) = -(1-p_t)^\gamma \log(p_t)FL(pt)=−(1−pt)γlog(pt) $$
  • New detector:RetinaNet = ResNet + FPN + Two sub-networks + Focal Loss

Focal Loss更加聚焦在困难样本(hard examples)上的训练。

将Focal Loss与ResNet-101-FPN backbone结合提出RetinaNet(one-stage检测器),RetinaNet在COCO test-dev上达到39.1mAP,速度为5FPS。

RetinaNet检测器与当时最佳的其它检测器进行比较,无论是速度上还是准确率上都是最佳:

详解:

作者提出一种新的损失函数,思路是希望那些hard examples对损失的贡献变大,使网络更倾向于从这些样本上学习。

作者以二分类为例进行说明:

交叉熵函数CE

首先是我们常使用的交叉熵损失函数:

上式中,y=+1或者y=-1。p∈[0,1]是y=+1的估计概率。作者定义pt为:

注:对交叉熵函数不了解的,可以参考理解交叉熵作为损失函数在神经网络中的作用

均衡交叉熵函数

要对类别不均衡问题对loss的贡献进行一个控制,即加上一个控制权重即可,最初作者的想法即如下这样,对于属于少数类别的样本,增大α即可

但这样有一个问题,它仅仅解决了正负样本之间的平衡问题,并没有区分易分/难分样本,按作者的话说:

While α balances the importance of positive/negative examples, it does not differentiate between easy/hard examples. Instead, we propose to reshape the loss function to down-weight easy examples and thus focus training on hard negatives.

问:为什么公式(3)只解决正负样本不均衡问题?

答:增加了一个系数αt,跟pt的定义类似,当label=1的时候,αt=a;当label=-1的时候,αt=1-a,a的范围也是0到1。因此可以通过设定a的值(一般而言假如1这个类的样本数比-1这个类的样本数多很多,那么a会取0到0.5来增加-1这个类的样本的权重)来控制正负样本对总的loss的共享权重。

Focal Loss

作者一开始给交叉熵损失函数添加modulating factor:

$$ (1-pt)^γ(1−pt)γ $$

显然,样本越易分,pt就越大(pt—>1),modulating factor趋近于0,则贡献的loss就越小,同样地,样本越难分,其pt就越小,modulating factor接近于1,则贡献的loss不受影响。

问:为什么pt越大,FL值越小?

答:根据公式(4)可知,FL与log(pt)中的pt成反比,与1-pt成正比,因此FL与pt的关系成反比。这是交叉熵函数的基本性质。当pt很大时(接近于1),FL值很小;而当pt很小时(接近于0),FL值会很大。

注:这里有个超参数—focusing parameter γ。

γ 放大了modulating factor的作用。

举原文中的一个例子,当pt=0.9时,带有modulating factor的focal loss是CE loss的100分之一,即进一步减小了正确分类的损失。

For instance, with γ = 2, an example classified with pt = 0.9 would have 100× lower loss compared with CE and with pt ≈ 0.968 it would have 1000× lower loss. This in turn increases the importance of correcting misclassified examples (whose loss is scaled down by at most 4× for pt ≤ .5 and γ = 2).

在实际中,作者采用如下公式,即综合了公式(3)和公式(4)的形式,这样机能调整正负样本的权重,又能控制难易分类样本的权重:

这里的两个参数 α和γ 来控制,在实验中a的选择范围也很广,一般而言当γ增加的时候,a需要减小一点,本文作者采用α=0.25,γ=2效果最好。

RetinaNet Detector

RetinaNet是由backbone网络和两个特殊任务的子网络(subnet)组成(属于one-stage检测器)。Backbone用来计算feature map;第一个子网络用来object classification,第二个子网络用来bounding box regression。

Feature Pyramid Network Backbone

Anchor

Classification Subnet

Box Regression Subnet

RetinaNet结构注意内容:

  1. 训练时FPN每一级的所有example都被用于计算Focal Loss,loss值加到一起用来训练;
  2. 测试时FPN每一级只选取score最大的1000个example来做nms;
  3. 整个结构不同层的head部分(上图中的c和d部分)共享参数,但分类和回归分支间的参数不共享;
  4. 分类分支的最后一级卷积的bias初始化成前面提到的-log((1-π)/π);

作者:张磊_0503 链接:https://www.jianshu.com/p/204d9ad9507f 來源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

实验结果

Table1是关于RetinaNet和Focal Loss的一些实验结果。(a)是在交叉熵的基础上加上参数a,a=0.5就表示传统的交叉熵,可以看出当a=0.75的时候效果最好,AP值提升了0.9。(b)是对比不同的参数γ和a的实验结果,可以看出随着γ的增加,AP提升比较明显。(d)通过和OHEM的对比可以看出最好的Focal Loss比最好的OHEM提高了3.2AP。这里OHEM1:3表示在通过OHEM得到的minibatch上强制positive和negative样本的比例为1:3,通过对比可以看出这种强制的操作并没有提升AP。(e)加入了运算时间的对比,可以和前面的Figure2结合起来看,速度方面也有优势!注意这里RetinaNet-101-800的AP是37.8,当把训练时间扩大1.5倍同时采用scale jitter,AP可以提高到39.1,这就是全文和table2中的最高的39.1AP的由来。

8.3.8 RFBNet

RFBNet有哪些创新点?

  1. 提出RF block(RFB)模块

RFBNet主要想利用一些技巧使得轻量级模型在速度和精度上达到很好的trade-off的检测器。灵感来自人类视觉的感受野结构Receptive Fields (RFs) ,提出了新奇的RF block(RFB)模块,来验证感受野尺寸和方向性的对提高有鉴别鲁棒特征的关系。RFBNet是以主干网络(backbone)为VGG16的SSD来构建的,主要是在Inception的基础上加入了dilated卷积层(dilated convolution),从而有效增大了感受野(receptive field)。整体上因为是基于SSD网络进行改进,所以检测速度还是比较快,同时精度也有一定的保证。

RFB介绍

RFB是一个类似Inception模块的多分支卷积模块,它的内部结构可分为两个组件:多分支卷积层和dilated卷积层。如下图:

1.多分支卷积层
​ 根据RF的定义,用多种尺寸的卷积核来实现比固定尺寸更好。具体设计:1.瓶颈结构,1x1-s2卷积减少通道特征,然后加上一个nxn卷积。2.替换5x5卷积为两个3x3卷积去减少参数,然后是更深的非线性层。有些例子,使用1xn和nx1代替nxn卷积;shortcut直连设计来自于ResNet和Inception ResNet V2。3.为了输出,卷积经常有stride=2或者是减少通道,所以直连层用一个不带非线性激活的1x1卷积层。

2.Dilated 卷积层

设计灵感来自Deeplab,在保持参数量和同样感受野的情况下,用来获取更高分辨率的特征。下图展示两种RFB结构:RFB和RFB-s。每个分支都是一个正常卷积后面加一个dilated卷积,主要是尺寸和dilated因子不同。(a)RFB。整体结构上借鉴了Inception的思想,主要不同点在于引入3个dilated卷积层(比如3x3conv,rate=1),这也是RFBNet增大感受野的主要方式之一;(b)RFB-s。RFB-s和RFB相比主要有两个改进,一方面用3x3卷积层代替5x5卷积层,另一方面用1x3和3x1卷积层代替3x3卷积层,主要目的应该是为了减少计算量,类似Inception后期版本对Inception结构的改进。

RFBNet300的整体结构如下图所示,基本上和SSD类似。RFBNet和SSD不同的是:1、主干网上用两个RFB结构替换原来新增的两层。2、conv4_3和conv7_fc在接预测层之前分别接RFB-s和RFB结构。

8.3.9 M2Det

M2Det有哪些创新点?

  1. 提出了多层次特征金字塔网络(MLFPN)来构建更有效的特征金字塔,用于检测不同尺度的对象。

M2Det的整体架构如下所示。M2Det使用backbone和多级特征金字塔网络(MLFPN)从输入图像中提取特征,然后类似于SSD,根据学习的特征生成密集的边界框和类别分数,最后是非最大抑制(NMS)操作以产生最终结果。 MLFPN由三个模块组成:特征融合模块(FFM),简化的U形模块(TUM)和按基于尺度的特征聚合模块(SFAM)。 FFMv1通过融合骨干网络的特征图,将语义信息丰富为基本特征。每个TUM生成一组多尺度特征,然后交替连接的TUM和FFMv2提取多级多尺度特征。此外,SFAM通过按比例缩放的特征连接操作和自适应注意机制将特征聚合到多级特征金字塔中。下面介绍有关M2Det中三个核心模块和网络配置的更多详细信息。

FFMs

FFM融合了M2Det中不同层次的特征,这对于构建最终的多级特征金字塔至关重要。它们使用1x1卷积层来压缩输入特征的通道,并使用连接操作来聚合这些特征图。特别是,由于FFMv1以backbone中不同比例的两个特征图作为输入,因此它采用一个上采样操作,在连接操作之前将深度特征重新缩放到相同的尺度。同时,FFMv2采用基本特征和前一个TUM的最大输出特征图 - 这两个具有相同的比例 - 作为输入,并产生下一个TUM的融合特征。 FFMv1和FFMv2的结构细节分别如下图(a)和(b)所示。

TUMs

TUM不同于FPN和RetinaNet,TUM采用简化的U形结构,如上图(c)所示。编码器是一系列3x3,步长为2的卷积层.并且解码器将这些层的输出作为其参考特征集,而原始FPN选择ResNet主干网络中每个阶段的最后一层的输出。此外,在解码器分支的上采样层后添加1x1卷积层和按元素求和的操作,以增强学习能力并保持特征的平滑性。每个TUM的解码器中的所有输出形成当前级别的多尺度特征。整体而言,堆叠TUM的输出形成多层次多尺度特征,而前TUM主要提供浅层特征,中间TUM提供中等特征,后TUM提供深层特征。

SFAM

SFAM旨在将由TUM生成的多级多尺度特征聚合成多级特征金字塔,如下图所示。SFAM的第一阶段是沿着信道维度将等效尺度的特征连接在一起。聚合特征金字塔可以表示为 $X = [X_1,X_2,...,X_i,...,X_L]$ ,其中

$$X_i = Concat(X_{1i}, X_{2i}, ...., X_{Li}) \in R^{W_i \times H_i \times C}$$

指的是尺度第i个最大的特征。这里,聚合金字塔中的每个比例都包含来自多级深度的特征。但是,简单的连接操作不太适合。在第二阶段,引入了通道注意模块,以促使特征集中在最有益的通道。在SE区块之后,使用全局平均池化来在挤压步骤中生成通道统计z∈RC。

8.4 人脸检测

在目标检测领域可以划分为了人脸检测与通用目标检测,往往人脸这方面会有专门的算法(包括人脸检测、人脸识别、人脸其他属性的识别等等),并且和通用目标检测(识别)会有一定的差别,着主要来源于人脸的特殊性(有时候目标比较小、人脸之间特征不明显、遮挡问题等),下面将从人脸检测和通用目标检测两个方面来讲解目标检测。

8.4.1 目前主要有人脸检测方法分类?

目前人脸检测方法主要包含两个区域:传统人脸检测算法和基于深度学习的人脸检测算法。传统人脸检测算法主要可以分为4类:

(1)基于知识的人脸检测方法;

(2)基于模型的人脸检测方法;

(3)基于特征的人脸检测方法;

(4)基于外观的人脸检测方法。

由于本书着重关注深度学习,下面会着重介绍基于深度学习的人脸检测方法。

2006年Hinton首次提出深度学习(Deep Learning)的概念,它是通过组合低层的特征形成更高层的抽象特征。随后研究者将深度学习应用在人脸检测领域,主要集中在基于卷积神经网络(CNN)的人脸检测研究,如基于级联卷积神经网络的人脸检测(cascade cnn)、 基于多任务卷积神经网络的人脸检测(MTCNN)、Facebox等,很大程度上提高了人脸检测的鲁棒性。当然通用目标检测算法像Faster-rcnn、yolo、ssd等也有用在人脸检测领域,也可以实现比较不错的结果,但是和专门人脸检测算法比还是有差别。下面部分主要介绍基于深度学习的的人脸检测算法,基于深度学习的通用目标检测算法将在第二大节介绍。

8.4.2 如何检测图片中不同大小的人脸?

传统人脸检测算法中针对不同大小人脸主要有两个策略:

(1)缩放图片的大小(图像金字塔如图8.4.1所示);

(2)缩放滑动窗的大小(如图8.4.2所示)。

图 8.1 图像金字塔

图 8.2 缩放滑动窗口

​ 基于深度学习的人脸检测算法中针对不同大小人脸主要也有两个策略,但和传统人脸检测算法有点区别,主要包括:

(1)缩放图片大小。(不过也可以通过缩放滑动窗的方式,基于深度学习的滑动窗人脸检测方式效率会很慢存在多次重复卷积,所以要采用全卷积神经网络(FCN),用FCN将不能用滑动窗的方法。)

(2)通过anchor box的方法(如图8.3所示,不要和图8.2混淆,这里是通过特征图预测原图的anchor box区域,具体在facebox中有描述)。

图 8.3 anchor box

8.4.3 如何设定算法检测最小人脸尺寸?

主要是看滑动窗的最小窗口和anchorbox的最小窗口。

(1)滑动窗的方法

假设通过12×12的滑动窗,不对原图做缩放的话,就可以检测原图中12×12的最小人脸。但是往往通常给定最小人脸a=40、或者a=80,以这么大的输入训练CNN进行人脸检测不太现实,速度会很慢,并且下一次需求最小人脸a=30*30又要去重新训练,通常还会是12×12的输入,为满足最小人脸框a,只需要在检测的时候对原图进行缩放即可:w=w×12/a。

(2)anchorbox的方法

原理类似,这里主要看anchorbox的最小box,通过可以通过缩放输入图片实现最小人脸的设定。

8.4.4 如何定位人脸的位置?

(1)滑动窗的方式:

滑动窗的方式是基于分类器识别为人脸的框的位置确定最终的人脸,

图 8.4 滑动窗

(2)FCN的方式:

​ FCN的方式通过特征图映射到原图的方式确定最终识别为人脸的位置,特征图映射到原图人脸框是要看特征图相比较于原图有多少次缩放(缩放主要查看卷积的步长和池化层),假设特征图上(2,3)的点,可粗略计算缩放比例为8倍,原图中的点应该是(16,24);如果训练的FCN为12*12的输入,对于原图框位置应该是(16,24,12,12),当然这只是估计位置,具体的再构建网络时要加入回归框的预测,主要是相对于原图框的一个平移与缩放。

(3)通过anchor box的方式:

​ 通过特征图映射到图的窗口,通过特征图映射到原图到多个框的方式确定最终识别为人脸的位置。

8.4.5 如何通过一个人脸的多个框确定最终人脸框位置?

图 8.5 通过NMS得到最终的人脸位置

NMS改进版本有很多,最原始的NMS就是判断两个框的交集,如果交集大于设定的阈值,将删除其中一个框,那么两个框应该怎么选择删除哪一个呢? 因为模型输出有概率值,一般会优选选择概率小的框删除。

8.4.6 基于级联卷积神经网络的人脸检测(Cascade CNN)

  1. cascade cnn的框架结构是什么?

级联结构中有6个CNN,3个CNN用于人脸非人脸二分类,另外3个CNN用于人脸区域的边框校正。给定一幅图像,12-net密集扫描整幅图片,拒绝90%以上的窗口。剩余的窗口输入到12-calibration-net中调整大小和位置,以接近真实目标。接着输入到NMS中,消除高度重叠窗口。下面网络与上面类似。

  1. cascade cnn人脸校验模块原理是什么?

该网络用于窗口校正,使用三个偏移变量:Xn:水平平移量,Yn:垂直平移量,Sn:宽高比缩放。候选框口(x,y,w,h)中,(x,y)表示左上点坐标,(w,h)表示宽和高。

我们要将窗口的控制坐标调整为:

$$ (x-{x_nw}/{s_n},y-{y_nh}/{s_n},{w}/{s_n},{h}/{s_n}) $$

这项工作中,我们有 $N=5×3×3=45$ 种模式。偏移向量三个参数包括以下值:

$$ Sn:(0.83,0.91,1.0,1.10,1.21) $$ $$ Xn:(-0.17,0,0.17) $$ $$ Yn:(-0.17,0,0.17) $$

同时对偏移向量三个参数进行校正。

3、训练样本应该如何准备?

人脸样本:

非人脸样本:

  1. 级联的好处

级联的工作原理和好处:

  • 最初阶段的网络可以比较简单,判别阈值可以设得宽松一点,这样就可以在保持较高召回率的同时排除掉大量的非人脸窗口;
  • 最后阶段网络为了保证足够的性能,因此一般设计的比较复杂,但由于只需要处理前面剩下的窗口,因此可以保证足够的效率;
  • 级联的思想可以帮助我们去组合利用性能较差的分类器,同时又可以获得一定的效率保证。

8.4.7 基于多任务卷积神经网络的人脸检测(MTCNN)

1.MTCNN模型有三个子网络。分别是P-Net,R-Net,O-Net.我想问一下,1.模型中的三个input size是指的是同一张图resize到不同尺度下喂给不同模型,还是同一张图,依次经过三个模型,然后是不同的输入尺寸?(这部分能给我讲一下吗)2.每个模型它都有对应三个结果(face classification;bounding box;facial landmark)这三个在网络上是如何对应的呢?

为了检测不同大小的人脸,开始需要构建图像金字塔,先经过pNet模型,输出人脸类别和边界框(边界框的预测为了对特征图映射到原图的框平移和缩放得到更准确的框),将识别为人脸的框映射到原图框位置可以获取patch,之后每一个patch通过resize的方式输入到rNet,识别为人脸的框并且预测更准确的人脸框,最后rNet识别为人脸的的每一个patch通过resize的方式输入到oNet,跟rNet类似,关键点是为了在训练集有限情况下使模型更鲁棒。

还要注意一点构建图像金字塔的的缩放比例要保留,为了将边界框映射到最开始原图上的

还要注意一点:如何从featureMap映射回原图

8.4.8 Facebox

(1)Rapidly Digested Convolutional Layers(RDCL)

在网络前期,使用RDCL快速的缩小feature map的大小。 主要设计原则如下:

  • Conv1, Pool1, Conv2 和 Pool2 的stride分别是4, 2, 2 和 2。这样整个RDCL的stride就是32,可以很快把feature map的尺寸变小。
  • 卷积(或pooling)核太大速度就慢,太小覆盖信息又不足。文章权衡之后,将Conv1, Pool1, Conv2 和 Pool2 的核大小分别设为7x7,3x3,5x5,3x3
  • 使用CReLU来保证输出维度不变的情况下,减少卷积核数量。

(2)Multiple Scale Convolutional Layers(MSCL)

在网络后期,使用MSCL更好地检测不同尺度的人脸。 主要设计原则有:

  • 类似于SSD,在网络的不同层进行检测;
  • 采用Inception模块。由于Inception包含多个不同的卷积分支,因此可以进一步使得感受野多样化。

(3)Anchor densification strategy

为了anchor密度均衡,可以对密度不足的anchor以中心进行偏移加倍,如下图所示:

8.5 目标检测的技巧汇总

8.5.1 Data Augmentation(贡献者:北京理工大学–明奇)

介绍一篇发表在Big Data上的数据增强相关的文献综述。

  1. Introduction
  • 数据增强与过拟合
    验证是否过拟合的方法:画出loss曲线,如果训练集loss持续减小但是验证集loss增大,就说明是过拟合了。

  • 数据增强目的
    通过数据增强实现数据更复杂的表征,从而减小验证集和训练集以及最终测试集的差距,让网络更好地学习迁移数据集上的数据分布。这也说明网络不是真正地理解数据,而是记忆数据分布。

  • 数据增强的方法
    (1)数据变换增强
    包括几何变换、色彩空间变换,随机擦除,对抗训练,神经风格迁移等
    (2)重采样增强
    主要侧重于新的实例合成。如图像混合(mixup),特征空间的增强,GAN生成图片。一张图看明白:

  1. Image Data Augmentation techniques

2.1 Data Augmentations based on basic image manipulations

  • Geometric transformations
      如果数据集潜在的表征能够被观察和分离,那么简单的几何变换就能取得很好的效果。对于复杂的数据集如医学影像,数据小而且训练集和测试集的偏差大,几何变换等增强的合理运用就很关键。

    • Flipping
      作者提到了要衡量普遍性的观点。但是这种变换对于数字数据集不具有安全性。

    • Color space
      主要提及的识别RGB通道上的变换,将三通道图进行分离,以及直方图变换增强等。(颜色空间更多增强方式可以参考A Preliminary Study on Data Augmentation of Deep Learning for Image Classification)

    • Cropping
      通常在输入图片的尺寸不一时会进行按中心的裁剪操作。裁剪某种程度上和平移操作有相似性。根据裁剪幅度变化,该操作具有一定的不安全性。

    • Rotation
      大幅度的旋转对数字集会有不安全性的考虑。

    • Translation
      平移也需要合理设计。如车站人脸检测,只需要中心检测时,就可以加合适的平移增强。平移后空出部分填0或者255,或用高斯分布噪声。

    • Noise injection
      在像素上叠加高斯分布的随机噪声。

  • Color space transformations
      由于实际图像中一定存在光线偏差,所以光线的增强十分有必要(但是IJCV的光流文章指出,3D建模的灯光增强实在是很难学习到,所以对于光线增强的效果不如几何也可能因为光线的复杂度更高,数据样本远远不够)。色彩变换十分多样,如像素限制、像素矩阵变换、像素值颠倒等;灰度图和彩图相比,计算时间成本大大较少,但是据实验效果会下降一些,很明显因为特征的维度被降维了;还有尝试将RGB映射到其他的色彩空间进行学习,YUV,CMY.HSV等。
      除了计算大内存消耗和时间长等缺点,色彩变换也面临不安全性,比如识别人脸的关键信息是黄白黑,但是大量增强出红绿蓝,会丢信息。颜色变换的增强方法是从色彩空间角度拟合偏置,效果有限的可能性是多样的:1. 真实几何多样性比颜色更简单 2. 色彩的变化多样性更多,导致增强不够反而学不好,颜色空间的欠拟合 3. 变换不安全

  • Experiment

随机裁剪效果最好。

2.2 Geometric versus photometric transformations

  • Kernel filter
    滤波器核在图像处理用的比较广,这里提到用这种方法来增强。还提到了一种正则化增强方法PatchShuffle,在一个patch内随机交换像素值,使得对噪声的抵抗更强以及避免过拟合。
    文章指出关于应用滤波器增强的工作尚且不多,因为这种方法其实和CNN的机制是一样的,这么做也许还不如直接在原始CNN上加层加深网络。

  • Mixing images
    就是那篇被ICLR拒稿的采样方法直接均值相加混合。

  还有非线性的mixup裁剪如下:

  以及随机裁剪的图像混合:

  这些混合方式是十分反人类直觉的,因此可解释性不强。只能说是可能增强了对底层低级特征如线条边缘等的鲁棒性。其实有点没有抓住关键点。

  • Random erasing
    随机擦除就是类似cutout的思想,通过mask的遮挡使得网络能够提高遮挡情况的鲁棒性。需要手工设计的部分包括mask的大小以及生成方式。是一种比较有效的方法。这种方式也需要考量增强的安全性,比如MNIST数据集8cutout后可能出问题。

  • A note on combining augmentations
    组合的增强方式往往是连续变化的,导致数据集的容量会迅速扩大,这对于小数据集领域来说容易发生过拟合 ,所以需要设计合理的搜索算法设计恰当的训练数据集。

2.3 Data Augmentations based on Deep Learning

  • Feature space augmentation
    之前刚看的基于SMOTE类别不平衡的过采样法来进行特征空间的插值操作进行数据增强,就实验效果而言不算特别出众。

  • Adversarial training
    对抗样本训练可以提高鲁棒性,但是实际应用中其实提高不一定明显,因为自然对抗样本的数目没有那么多。而NIPS的对抗攻击大赛很多从神经网络的学习策略下手,进行梯度攻击,更加偏向于人为的攻击了,对于普适的检测性能提高意义反而不大,更强调安全需求高的场合。

  • GAN‑based Data Augmentation

  • Neural Style Transfer

不觉得这个效果会普遍很好,应该来说是针对特定域会有效(如白天黑夜),实际效果应该有限。

  • Meta learning Data Augmentations
    • Neural augmentation
    • Smart Augmentation
      两个东西差不多,就是上次看到SmartAugment方法。随机采样类内图片进行通道叠加然后输出融合图像,学通过梯度下降使得输出图像的类内差距减小(没考虑类间关系,可能也不便处理)。

  • AutoAugment
    谷歌最早做的自学习增强方法,走的NAS的思路RL+RNN搜索增强空间,还有后来最近发的检测增强也是大同小异,基本就是换汤不换药,问题在于搜索空间太大,复现搜索过于依赖硬件条件(普通实验室玩不起
  1. Design considerations for image Data Augmentation

3.1 Test-time augmentation
  许多都论文指出在检测阶段进行同等的数据增强能够获得较好的效果。归结可以认为是训练检测阶段的一致性。当然,这种手段时间成本太高,只在如医学影像等追求精度的关键领域可以使用。

3.2 Curriculum learning
  Bengio团队早年在ICML提出的观点,确实合理,一开始就进行大量的增强容易导致网络不收敛。
从一个数据集学习到的数据增强也可以迁移到其他数据集。

3.3 Resolution impact
高清(1920×1080×3)或4K(3840×2160×3)等高分辨率图像需要更多的处理和内存来训练深度CNN。然而下一代模型更倾向于使用这样更高分辨率的图像。因为模型中常用的下采样会造成图像中信息的丢失,使图像识别更困难。
研究人员发现,高分辨率图像和低分辨率图像一起训练的模型集合,比单独的任何一个模型都要好。
某个实验(这里就不注明引用了)在256×256图像和512×512图像上训练的模型分别获得7.96%和7.42%的top-5 error。汇总后,他们的top-5 error变低,为6.97%。
随着超分辨率网络的发展,将图像放大到更高的分辨率后训练模型,能够得到更好更健壮的图像分类器。

3.4 Final dataset size
  数据增强的形式可以分为在线和离线增强。前者是在加载数据时增强,可能造成额外的内存消耗(现在都是数据容量不变的随机增强)。
  此外作者提到了一个比较有意思的点:当前数据集尤其是进行增广后是十分庞大的,明显能够在一定程度上缩小数据集但是保持性能下降不多的子集效率会高得多。

3.5 Alleviating class imbalance with Data Augmentation
  这也是值得借鉴的一点。通过增强在一定程度上解决类别不平衡问题。但增强需要仔细设计,否则会面对已经学习较好的类别或者场景造成过拟合等问题。

8.5.2 OHEM

8.5.3 NMS:Soft NMS/ Polygon NMS/ Inclined NMS/ ConvNMS/ Yes-Net NMS/ Softer NMS

8.5.4 Multi Scale Training/Testing

8.5.5 建立小物体与context的关系

8.5.6 参考relation network

8.5.7 结合GAN

8.5.8 结合attention

8.5.9 训练tricks(贡献者:北京理工大学–明奇)

介绍一篇2019.2.4亚马逊挂在ArXiv的目标检测训练tricks的文章(之前亚马逊发了篇分类的tricks在CVPR上)

  1. Introduction

  上次亚马逊发了个分类的训练trick在CVPR上,这次是检测的,还没发表。就没什么多说的了,下面直接介绍。先看效果如下,其实摘要声称的5%是单阶段的yolov3的提升,说明:单阶段没有RoIPooling阶段很多性质确实不如两阶段,因此采用trick很有必要;相反,两阶段本身结构优于单阶段所以外加的trick提供的如不变性等网络自身能够学习和适应就不起作用了。

  1. Bag of Freebies

  提出了一种基于mixup的视觉联系图像混合方法,以及一些数据处理和训练策略。

2.1 Visually Coherent Image Mixup for Object Detection
  先介绍图像分类中的mixup方法,作用是提供了训练的正则化,应用到图像上如下图,将图像作简单的像素值输入mixup的凸函数中得到合成图;然后将one-hot编码类似处理得到新的label。

  技术细节:

  • 相比于分类的resize,为了保证检测图像不畸变影响效果,作者选择直接叠加,取最大的宽高,空白进行灰度填充,不进行缩放。
  • 选择ab较大(如1.5,1.5)的Beta分布作为系数来混合图像,作者说是相干性视觉图像的更强;loss是两张图像物体的loss之和,loss计算权重分别是beta分布的系数

2.2 Classification Head Label Smoothing
  标签平滑在检测的分类任务常有用到,最早是Inceptionv2中提出。
  如果标签中有的是错的,或者不准,会导致网络过分信任标签而一起错下去。为了提高网络泛化能力,避免这种错误,在one-hot的label进行计算loss时,真实类别位置乘以一个系数(1-e),e很小如0.05,以0.95的概率送进去;非标注的类别原来为0,现在改为e=0.05送进去计算loss。网络的优化方向不变,但是相比0-1label会更加平滑。
(标签平滑这个讲的不错:https://juejin.im/post/5a29fd4051882534af25dc92)

  这里进一步改进了一下label smooth的公式而已,在原来基础上除了个类别数。

2.3 Data Preprocessing
  就是数据增强,没什么其他的。至于分类也是几何变换和色彩变换。这么分区别其实是是否变换label。但是将真实世界就这么简单地分解过于粗糙了。好不容易谷歌的增强考虑到了如何学习一下检测任务的增强,但是也只是加了bbox_only的增强,就效果而言,一般;而且就实际来说,合理性和有效性有待商榷。
  作者认为,两阶段网络的RPN生成就是对输入的任意裁剪,所以这个增强就够了;这老哥膨胀了,two-stage就不用裁剪的增强,虽然两阶段能提供一些不变性,但是用了一般来说都是更好的。

2.4 Training Schedule Revamping
训练策略上:余弦学习率调整+warmup

2.5 Synchronized Batch Normalization
跨多卡同步正则化,土豪专区,穷人退避

2.6 Random shapes training for single-stage object detection networks
多尺度训练,每经过一定的iteration更换一种尺度。举例是yolov3的尺度范围。

8.6 目标检测的常用数据集

8.6.1 PASCAL VOC

​ VOC数据集是目标检测经常用的一个数据集,自2005年起每年举办一次比赛,最开始只有4类,到2007年扩充为20个类,共有两个常用的版本:2007和2012。学术界常用5k的train/val 2007和16k的train/val 2012作为训练集,test 2007作为测试集,用10k的train/val 2007+test 2007和16k的train/val 2012作为训练集,test2012作为测试集,分别汇报结果。

8.6.2 MS COCO

​ COCO数据集是微软团队发布的一个可以用来图像recognition+segmentation+captioning 数据集,该数据集收集了大量包含常见物体的日常场景图片,并提供像素级的实例标注以更精确地评估检测和分割算法的效果,致力于推动场景理解的研究进展。依托这一数据集,每年举办一次比赛,现已涵盖检测、分割、关键点识别、注释等机器视觉的中心任务,是继ImageNet Chanllenge以来最有影响力的学术竞赛之一。

相比ImageNet,COCO更加偏好目标与其场景共同出现的图片,即non-iconic images。这样的图片能够反映视觉上的语义,更符合图像理解的任务要求。而相对的iconic images则更适合浅语义的图像分类等任务。

​ COCO的检测任务共含有80个类,在2014年发布的数据规模分train/val/test分别为80k/40k/40k,学术界较为通用的划分是使用train和35k的val子集作为训练集(trainval35k),使用剩余的val作为测试集(minival),同时向官方的evaluation server提交结果(test-dev)。除此之外,COCO官方也保留一部分test数据作为比赛的评测集。

8.6.3 Google Open Image

​ Open Image是谷歌团队发布的数据集。最新发布的Open Images V4包含190万图像、600个种类,1540万个bounding-box标注,是当前最大的带物体位置标注信息的数据集。这些边界框大部分都是由专业注释人员手动绘制的,确保了它们的准确性和一致性。另外,这些图像是非常多样化的,并且通常包含有多个对象的复杂场景(平均每个图像 8 个)。

8.6.4 ImageNet

​ ImageNet是一个计算机视觉系统识别项目, 是目前世界上图像识别最大的数据库。ImageNet是美国斯坦福的计算机科学家,模拟人类的识别系统建立的。能够从图片识别物体。Imagenet数据集文档详细,有专门的团队维护,使用非常方便,在计算机视觉领域研究论文中应用非常广,几乎成为了目前深度学习图像领域算法性能检验的“标准”数据集。Imagenet数据集有1400多万幅图片,涵盖2万多个类别;其中有超过百万的图片有明确的类别标注和图像中物体位置的标注。

8.6.5 DOTA

​ DOTA是遥感航空图像检测的常用数据集,包含2806张航空图像,尺寸大约为4kx4k,包含15个类别共计188282个实例,其中14个主类,small vehicle 和 large vehicle都是vehicle的子类。其标注方式为四点确定的任意形状和方向的四边形。航空图像区别于传统数据集,有其自己的特点,如:尺度变化性更大;密集的小物体检测;检测目标的不确定性。数据划分为1/6验证集,1/3测试集,1/2训练集。目前发布了训练集和验证集,图像尺寸从800x800到4000x4000不等。

8.7 目标检测常用标注工具

8.7.1 LabelImg

​ LabelImg 是一款开源的图像标注工具,标签可用于分类和目标检测,它是用 Python 编写的,并使用Qt作为其图形界面,简单好用。注释以 PASCAL VOC 格式保存为 XML 文件,这是 ImageNet 使用的格式。 此外,它还支持 COCO 数据集格式。

8.7.2 labelme

​ labelme 是一款开源的图像/视频标注工具,标签可用于目标检测、分割和分类。灵感是来自于 MIT 开源的一款标注工具 LabelMe。labelme 具有的特点是:

  • 支持图像的标注的组件有:矩形框,多边形,圆,线,点(rectangle, polygons, circle, lines, points)
  • 支持视频标注
  • GUI 自定义
  • 支持导出 VOC 格式用于 semantic/instance segmentation
  • 支出导出 COCO 格式用于 instance segmentation

8.7.3 Labelbox

​ Labelbox 是一家为机器学习应用程序创建、管理和维护数据集的服务提供商,其中包含一款部分免费的数据标签工具,包含图像分类和分割,文本,音频和视频注释的接口,其中图像视频标注具有的功能如下:

  • 可用于标注的组件有:矩形框,多边形,线,点,画笔,超像素等(bounding box, polygons, lines, points,brush, subpixels)
  • 标签可用于分类,分割,目标检测等
  • 以 JSON / CSV / WKT / COCO / Pascal VOC 等格式导出数据
  • 支持 Tiled Imagery (Maps)
  • 支持视频标注 (快要更新)

8.7.4 RectLabel

​ RectLabel 是一款在线免费图像标注工具,标签可用于目标检测、分割和分类。具有的功能或特点:

  • 可用的组件:矩形框,多边形,三次贝塞尔曲线,直线和点,画笔,超像素
  • 可只标记整张图像而不绘制
  • 可使用画笔和超像素
  • 导出为YOLO,KITTI,COCO JSON和CSV格式
  • 以PASCAL VOC XML格式读写
  • 使用Core ML模型自动标记图像
  • 将视频转换为图像帧

8.7.5 CVAT

​ CVAT 是一款开源的基于网络的交互式视频/图像标注工具,是对加州视频标注工具(Video Annotation Tool) 项目的重新设计和实现。OpenCV团队正在使用该工具来标注不同属性的数百万个对象,许多 UI 和 UX 的决策都基于专业数据标注团队的反馈。具有的功能

  • 关键帧之间的边界框插值
  • 自动标注(使用TensorFlow OD API 和 Intel OpenVINO IR格式的深度学习模型)

8.7.6 VIA

​ VGG Image Annotator(VIA)是一款简单独立的手动注释软件,适用于图像,音频和视频。 VIA 在 Web 浏览器中运行,不需要任何安装或设置。 页面可在大多数现代Web浏览器中作为离线应用程序运行。

  • 支持标注的区域组件有:矩形,圆形,椭圆形,多边形,点和折线

8.7.6 其他标注工具

​ liblabel,一个用 MATLAB 写的轻量级 语义/示例(semantic/instance) 标注工具。
ImageTagger:一个开源的图像标注平台。
Anno-Mage:一个利用深度学习模型半自动图像标注工具,预训练模型是基于MS COCO数据集,用 RetinaNet 训练的。


​ 当然还有一些数据标注公司,可能包含更多标注功能,例如对三维目标检测的标注(3D Bounding box Labelling),激光雷达点云的标注(LIDAR 3D Point Cloud Labeling)等。

8.8 目标检测工具和框架(贡献者:北京理工大学–明奇)

各种不同的算法虽然部分官方会有公布代码,或者github上有人复现,但是囿于安装环境不一,实现的框架(pytorch、C++、Caffee、tensorflow、MXNet等)不同,每次更换算法都需要重新安装环境,并且代码之间的迁移性差,十分不方便。所以为了方便将不同的算法统一在一个代码库中,不同的大厂都提出了自己的解决方案。如facebook的Detectron、商汤科技的mmdetection、SimpleDet等。其中Detectron最早,所以用户量最大,其次是国内近段时间崛起的mmdetection,下面介绍该目标检测工具箱。

  1. Introduction
    MMdetection的特点:
  • 模块化设计:将不同网络的部分进行切割,模块之间具有很高的复用性和独立性(十分便利,可以任意组合)
  • 高效的内存使用
  • SOTA
  1. Support Frameworks
  • 单阶段检测器
    SSD、RetinaNet、FCOS、FSAF

  • 两阶段检测器
    Faster R-CNN、R-FCN、Mask R-CNN、Mask Scoring R-CNN、Grid R-CNN

  • 多阶段检测器
    Cascade R-CNN、Hybrid Task Cascade

  • 通用模块和方法
    soft-NMS、DCN、OHEN、Train from Scratch 、M2Det 、GN 、HRNet 、Libra R-CNN

  1. Architecture

模型表征:划分为以下几个模块:
Backbone(ResNet等)、Neck(FPN)、DenseHead(AnchorHead)、RoIExtractor、RoIHead(BBoxHead/MaskHead)
结构图如下:

  1. Notice
  • 1x代表12epoch的COCO训练,2x类似推导
  • 由于batch-size一般比较小(1/2这样的量级),所以大多数地方默认冻结BN层。可以使用GN代替。
  1. 参考链接
    mmdetection代码高度模块化,十分好用和便利,更详细的文档直接参见官方文档:
    https://github.com/open-mmlab/mmdetection

注释版的mmdetection代码(更新至v1.0.0):https://github.com/ming71/mmdetection-annotated

使用方法简介:
安装记录(可能过时,以官方文档为准):https://ming71.github.io/mmdetection-memo.html
使用方法(截止更新日期,如果过时以官方为准):https://ming71.github.io/mmdetection-instruction.html

TODO

  • [ ] 目标检测基础知识:mAP、IoU和NMS等
  • [ ] 目标检测评测指标
  • [ ] 目标检测常见标注工具
  • [ ] 完善目标检测的技巧汇总
  • [ ] 目标检测的现在难点和未来发展

参考文献

https://github.com/amusi/awesome-object-detection

https://github.com/hoya012/deep_learning_object_detection

https://handong1587.github.io/deep_learning/2015/10/09/object-detection.html

https://www.zhihu.com/question/272322209/answer/482922713

http://blog.leanote.com/post/afanti.deng@gmail.com/b5f4f526490b

https://blog.csdn.net/hw5226349/article/details/78987385

[1] Girshick R, Donahue J, Darrell T, et al. Rich feature hierarchies for accurate object detection and semantic segmentation[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2014: 580-587.

[2] Girshick R. Fast r-cnn[C]//Proceedings of the IEEE international conference on computer vision. 2015: 1440-1448.

[3] He K, Zhang X, Ren S, et al. Spatial pyramid pooling in deep convolutional networks for visual recognition[J]. IEEE transactions on pattern analysis and machine intelligence, 2015, 37(9): 1904-1916.

[4] Ren S, He K, Girshick R, et al. Faster r-cnn: Towards real-time object detection with region proposal networks[C]//Advances in neural information processing systems. 2015: 91-99.

[5] Lin T Y, Dollár P, Girshick R, et al. Feature pyramid networks for object detection[C]//Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017: 2117-2125.

[6] He K, Gkioxari G, Dollár P, et al. Mask r-cnn[C]//Proceedings of the IEEE international conference on computer vision. 2017: 2961-2969.

[7] Liu W, Anguelov D, Erhan D, et al. Ssd: Single shot multibox detector[C]//European conference on computer vision. Springer, Cham, 2016: 21-37.

[8] Fu C Y, Liu W, Ranga A, et al. Dssd: Deconvolutional single shot detector[J]. arXiv preprint arXiv:1701.06659, 2017.

[9] Redmon J, Divvala S, Girshick R, et al. You only look once: Unified, real-time object detection[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2016: 779-788.

[10] Redmon J, Farhadi A. YOLO9000: better, faster, stronger[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2017: 7263-7271.

[11] Redmon J, Farhadi A. Yolov3: An incremental improvement[J]. arXiv preprint arXiv:1804.02767, 2018.

[12] Lin T Y, Goyal P, Girshick R, et al. Focal loss for dense object detection[C]//Proceedings of the IEEE international conference on computer vision. 2017: 2980-2988.

[13] Liu S, Huang D. Receptive field block net for accurate and fast object detection[C]//Proceedings of the European Conference on Computer Vision (ECCV). 2018: 385-400.

[14] Zhao Q, Sheng T, Wang Y, et al. M2Det: A Single-Shot Object Detector based on Multi-Level Feature Pyramid Network[J]. arXiv preprint arXiv:1811.04533, 2018

图像分割

9.1 图像分割算法分类?

图像分割是预测图像中每一个像素所属的类别或者物体。基于深度学习的图像分割算法主要分为两类:

1.语义分割

为图像中的每个像素分配一个类别,如把画面中的所有物体都指出它们各自的类别。

2.实例分割

与语义分割不同,实例分割只对特定物体进行类别分配,这一点与目标检测有点相似,但目标检测输出的是边界框和类别,而实例分割输出的是掩膜(mask)和类别。

9.2 传统的基于CNN的分割方法缺点?

传统的基于CNN的分割方法:为了对一个像素分类,使用该像素周围的一个图像块作为CNN的输入,用于训练与预测,这种方法主要有几个缺点:
1)存储开销大,例如,对每个像素使用15 * 15的图像块,然后不断滑动窗口,将图像块输入到CNN中进行类别判断,因此,需要的存储空间随滑动窗口的次数和大小急剧上升;
2)效率低下,相邻像素块基本上是重复的,针对每个像素块逐个计算卷积,这种计算有很大程度上的重复;
3)像素块的大小限制了感受区域的大小,通常像素块的大小比整幅图像的大小小很多,只能提取一些局部特征,从而导致分类性能受到限制。
而全卷积网络(FCN)则是从抽象的特征中恢复出每个像素所属的类别。即从图像级别的分类进一步延伸到像素级别的分类。

9.3 FCN

9.3.1 FCN改变了什么?

​ 对于一般的分类CNN网络,如VGG和Resnet,都会在网络的最后加入一些全连接层,经过softmax后就可以获得类别概率信息。但是这个概率信息是1维的,即只能标识整个图片的类别,不能标识每个像素点的类别,所以这种全连接方法不适用于图像分割。
​ 而FCN提出可以把后面几个全连接都换成卷积,这样就可以获得一张2维的feature map,后接softmax层获得每个像素点的分类信息,从而解决了分割问题,如图4。

图 4
### 9.3.2 FCN网络结构?

​ FCN对图像进行像素级的分类,从而解决了语义级别的图像分割(semantic segmentation)问题。与经典的CNN在卷积层之后使用全连接层得到固定长度的特征向量进行分类(全联接层+softmax输出)不同,FCN可以接受任意尺寸的输入图像,采用反卷积层对最后一个卷积层的feature map进行上采样, 使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时保留了原始输入图像中的空间信息, 最后在上采样的特征图上进行逐像素分类。
​ 下图是语义分割所采用的全卷积网络(FCN)的结构示意图:

9.3.3 全卷积网络举例?

​ 通常CNN网络在卷积层之后会接上若干个全连接层, 将卷积层产生的特征图(feature map)映射成一个固定长度的特征向量。以AlexNet为代表的经典CNN结构适合于图像级的分类和回归任务,因为它们最后都得到整个输入图像的一个概率向量。

  
如上图所示:
  
(1)在CNN中, 猫的图片输入到AlexNet, 得到一个长为1000的输出向量, 表示输入图像属于每一类的概率, 其中在“tabby cat”这一类统计概率最高, 用来做分类任务。
  
(2)FCN与CNN的区别在于把CNN最后的全连接层转换成卷积层,输出的是一张已经带有标签的图片, 而这个图片就可以做语义分割。
  
(3)CNN的强大之处在于它的多层结构能自动学习特征,并且可以学习到多个层次的特征: 较浅的卷积层感知域较小,学习到一些局部区域的特征;较深的卷积层具有较大的感知域,能够学习到更加抽象一些的特征。高层的抽象特征对物体的大小、位置和方向等敏感性更低,从而有助于识别性能的提高, 所以我们常常可以将卷积层看作是特征提取器。

9.2.4 全连接层和卷积层如何相互转化?

  
两者相互转换的可能性:
  
全连接层和卷积层之间唯一的不同就是卷积层中的神经元只与输入数据中的一个局部区域连接,并且在卷积列中的神经元共享参数。然而在两类层中,神经元都是计算点积,所以它们的函数形式是一样的。因此,将此两者相互转化是可能的:
  
(1)对于任一个卷积层,都存在一个能实现和它一样的前向传播函数的全连接层。权重矩阵是一个巨大的矩阵,除了某些特定块,其余部分都是零。而在其中大部分块中,元素都是相等的。
  
(2)任何全连接层都可以被转化为卷积层。比如VGG16中第一个全连接层是25088 * 4096的数据尺寸,将它转化为512 * 7 * 7 * 4096的数据尺寸,即一个K=4096的全连接层,输入数据体的尺寸是7 * 7 * 512,这个全连接层可以被等效地看做一个F=7, P=0, S=1, K=4096 的卷积层。换句话说,就是将滤波器的尺寸设置为和输入数据体的尺寸一致7 * 7, 这样输出就变为1 * 1 * 4096, 本质上和全连接层的输出是一样的。
  
输出激活数据体深度是由卷积核的数目决定的(K=4096)。
  
在两种变换中,将全连接层转化为卷积层在实际运用中更加有用。假设一个卷积神经网络的输入是227x227x3的图像,一系列的卷积层和下采样层将图像数据变为尺寸为7x7x512的激活数据体, AlexNet的处理方式为使用了两个尺寸为4096的全连接层,最后一个有1000个神经元的全连接层用于计算分类评分。我们可以将这3个全连接层中的任意一个转化为卷积层:
  
(1)第一个连接区域是[7x7x512]的全连接层,令其滤波器尺寸为F=7,K=4096,这样输出数据体就为[1x1x4096]。
  
(2)第二个全连接层,令其滤波器尺寸为F=1,K=4096,这样输出数据体为[1x1x4096]。
  
(3)最后一个全连接层也做类似的,令其F=1,K=1000,最终输出为[1x1x1000]。

9.2.5 为什么传统CNN的输入图片是固定大小?

  
对于CNN,一幅输入图片在经过卷积和pooling层时,这些层是不关心图片大小的。比如对于一个卷积层,outputsize = (inputsize - kernelsize) / stride + 1,它并不关心inputsize多大,对于一个inputsize大小的输入feature map,滑窗卷积,输出outputsize大小的feature map即可。pooling层同理。但是在进入全连接层时,feature map(假设大小为n×n)要拉成一条向量,而向量中每个元素(共n×n个)作为一个结点都要与下一个层的所有结点(假设4096个)全连接,这里的权值个数是4096×n×n,而我们知道神经网络结构一旦确定,它的权值个数都是固定的,所以这个n不能变化,n是conv5的outputsize,所以层层向回看,每个outputsize都要固定,那每个inputsize都要固定,因此输入图片大小要固定。

9.2.6 把全连接层的权重W重塑成卷积层的滤波器有什么好处?

  
这样的转化可以在单个向前传播的过程中, 使得卷积网络在一张更大的输入图片上滑动,从而得到多个输出(可以理解为一个label map)。
  
比如: 我们想让224×224尺寸的浮窗,以步长为32在384×384的图片上滑动,把每个经停的位置都带入卷积网络,最后得到6×6个位置的类别得分, 那么通过将全连接层转化为卷积层之后的运算过程为:
  
如果224×224的输入图片经过卷积层和下采样层之后得到了[7x7x512]的数组,那么,384×384的大图片直接经过同样的卷积层和下采样层之后会得到[12x12x512]的数组, 然后再经过上面由3个全连接层转化得到的3个卷积层,最终得到[6x6x1000]的输出((12 – 7)/1 + 1 = 6), 这个结果正是浮窗在原图经停的6×6个位置的得分。
  
一个确定的CNN网络结构之所以要固定输入图片大小,是因为全连接层权值数固定,而该权值数和feature map大小有关, 但是FCN在CNN的基础上把1000个结点的全连接层改为含有1000个1×1卷积核的卷积层,经过这一层,还是得到二维的feature map,同样我们也不关心这个feature map大小, 所以对于输入图片的size并没有限制。
  
如下图所示,FCN将传统CNN中的全连接层转化成卷积层,对应CNN网络FCN把最后三层全连接层转换成为三层卷积层:

一个分类网络
![](ch9/figure_9.1.7_2.png)
变为全卷积网络
![](ch9/figure_9.1.7_3.png)
End-to-end, pixels-to pixels网络
![](ch9/figure_9.1.7_4.jpg)

(1)全连接层转化为全卷积层 : 在传统的CNN结构中,前5层是卷积层,第6层和第7层分别是一个长度为4096的一维向量,第8层是长度为1000的一维向量,分别对应1000个不同类别的概率。FCN将这3层表示为卷积层,卷积核的大小 (通道数,宽,高) 分别为 (4096,1,1)、(4096,1,1)、(1000,1,1)。看上去数字上并没有什么差别,但是卷积跟全连接是不一样的概念和计算过程,使用的是之前CNN已经训练好的权值和偏置,但是不一样的在于权值和偏置是有自己的范围,属于自己的一个卷积核。
  
(2)CNN中输入的图像大小是统一固定成227x227大小的图像,第一层pooling后为55x55,第二层pooling后图像大小为27x27,第五层pooling后的图像大小为13x13, 而FCN输入的图像是H * W大小,第一层pooling后变为原图大小的1/2,第二层变为原图大小的1/4,第五层变为原图大小的1/8,第八层变为原图大小的1/16。
  
(3)经过多次卷积和pooling以后,得到的图像越来越小,分辨率越来越低。其中图像到H/32 * W/32的时候图片是最小的一层时,所产生图叫做heatmap热图,热图就是我们最重要的高维特征图,得到高维特征的heatmap之后就是最重要的一步也是最后的一步对原图像进行upsampling,把图像进行放大几次到原图像的大小。
  
相较于使用被转化前的原始卷积神经网络对所有36个位置进行迭代计算优化模型,然后再对36个位置做预测,使用转化后的卷积神经网络进行一次前向传播计算要高效得多,因为36次计算都在共享计算资源。这一技巧在实践中经常使用,通常将一张图像尺寸变得更大,然后使用变换后的卷积神经网络来对空间上很多不同位置进行评价得到分类评分,然后在求这些分值的平均值。

9.2.7 反卷积层理解

  
Upsampling的操作可以看成是反卷积(deconvolutional),卷积运算的参数和CNN的参数一样是在训练FCN模型的过程中通过bp算法学习得到。反卷积层也是卷积层,不关心input大小,滑窗卷积后输出output。deconv并不是真正的deconvolution(卷积的逆变换),最近比较公认的叫法应该是transposed convolution,deconv的前向传播就是conv的反向传播。
  
反卷积参数: 利用卷积过程filter的转置(实际上就是水平和竖直方向上翻转filter)作为计算卷积前的特征图。
  
反卷积的运算如下所示:
  
蓝色是反卷积层的input,绿色是反卷积层的outputFull padding, transposed Full padding, transposed。

上图中的反卷积,input是2×2, output是4×4。 Zero padding, non-unit strides, transposed。
![](ch9/figure_9.1.8_2.png)
上图中的反卷积,input feature map是3×3, 转化后是5×5, output是5×5
### 9.2.8 跳级(skip)结构

  
对CNN的结果做处理,得到了dense prediction,而作者在试验中发现,得到的分割结果比较粗糙,所以考虑加入更多前层的细节信息,也就是把倒数第几层的输出和最后的输出做一个fusion,实际上也就是加和:


  
实验表明,这样的分割结果更细致更准确。在逐层fusion的过程中,做到第三行再往下,结果又会变差,所以作者做到这里就停了。

9.2.9 模型训练

  
(1)用AlexNet,VGG16或者GoogleNet训练好的模型做初始化,在这个基础上做fine-tuning,全部都fine-tuning,只需在末尾加上upsampling,参数的学习还是利用CNN本身的反向传播原理。
  
(2)采用whole image做训练,不进行patchwise sampling。实验证明直接用全图已经很effective and efficient。
  
(3)对class score的卷积层做全零初始化。随机初始化在性能和收敛上没有优势。
举例:
  
FCN例子: 输入可为任意尺寸图像彩色图像;输出与输入尺寸相同,深度为:20类目标+背景=21,模型基于AlexNet。
  
蓝色:卷积层。
  
绿色:Max Pooling层。
  
黄色: 求和运算, 使用逐数据相加,把三个不同深度的预测结果进行融合:较浅的结果更为精细,较深的结果更为鲁棒。
  
灰色: 裁剪, 在融合之前,使用裁剪层统一两者大小, 最后裁剪成和输入相同尺寸输出。
  
对于不同尺寸的输入图像,各层数据的尺寸(height,width)相应变化,深度(channel)不变。


  
(1)全卷积层部分进行特征提取, 提取卷积层(3个蓝色层)的输出来作为预测21个类别的特征。

  
(2)图中虚线内是反卷积层的运算, 反卷积层(3个橙色层)可以把输入数据尺寸放大。和卷积层一样,升采样的具体参数经过训练确定。

    

  1. 以经典的AlexNet分类网络为初始化。最后两级是全连接(红色),参数弃去不用。


    
2) 从特征小图()预测分割小图(),之后直接升采样为大图。

反卷积(橙色)的步长为32,这个网络称为FCN-32s
     3) 升采样分为两次完成(橙色×2), 在第二次升采样前,把第4个pooling层(绿色)的预测结果(蓝色)融合进来。使用跳级结构提升精确性。

第二次反卷积步长为16,这个网络称为FCN-16s
     4) 升采样分为三次完成(橙色×3), 进一步融合了第3个pooling层的预测结果。

第三次反卷积步长为8,记为FCN-8s
其他参数:    minibatch:20张图片。    learning rate:0.001。    初始化:分类网络之外的卷积层参数初始化为0。    反卷积参数初始化为bilinear插值。    最后一层反卷积固定位bilinear插值不做学习。

9.2.10 FCN缺点

  
(1)得到的结果还是不够精细。进行8倍上采样虽然比32倍的效果好了很多,但是上采样的结果还是比较模糊和平滑,对图像中的细节不敏感。
  
(2)对各个像素进行分类,没有充分考虑像素与像素之间的关系。忽略了在通常的基于像素分类的分割方法中使用的空间规整(spatial regularization)步骤,缺乏空间一致性。

9.3 U-Net

  
卷积网络被大规模应用在分类任务中,输出的结果是整个图像的类标签。然而,在许多视觉任务,尤其是生物医学图像处理领域,目标输出应该包括目标类别的位置,并且每个像素都应该有类标签。另外,在生物医学图像往往缺少训练图片。所以,Ciresan等人训练了一个卷积神经网络,用滑动窗口提供像素的周围区域(patch)作为输入来预测每个像素的类标签。这个网络有两个优点:
第一,输出结果可以定位出目标类别的位置;
第二,由于输入的训练数据是patches,这样就相当于进行了数据增广,解决了生物医学图像数量少的问题。
  
但是,这个方法也有两个很明显缺点。
  
第一,它很慢,因为这个网络必须训练每个patch,并且因为patch间的重叠有很多的冗余(冗余会造成什么影响呢?卷积核里面的W,就是提取特征的权重,两个块如果重叠的部分太多,这个权重会被同一些特征训练两次,造成资源的浪费,减慢训练时间和效率,虽然说会有一些冗余,训练集大了,准确率不就高了吗?可是你这个是相同的图片啊,重叠的东西都是相同的,举个例子,我用一张相同的图片训练20次,按照这个意思也是增大了训练集啊,可是会出现什么结果呢,很显然,会导致过拟合,也就是对你这个图片识别很准,别的图片就不一定了)。
  
第二,定位准确性和获取上下文信息不可兼得。大的patches需要更多的max-pooling层这样减小了定位准确性(为什么?因为你是对以这个像素为中心的点进行分类,如果patch太大,最后经过全连接层的前一层大小肯定是不变的,如果你patch大就需要更多的pooling达到这个大小,而pooling层越多,丢失信息的信息也越多;小的patches只能看到很小的局部信息,包含的背景信息不够。
  
这篇论文建立了一个更好全卷积方法。我们定义和扩展了这个方法它使用更少的训练图片但产生更精确的分割。

  
(1) 使用全卷积神经网络。(全卷积神经网络就是卷积取代了全连接层,全连接层必须固定图像大小而卷积不用,所以这个策略使得,你可以输入任意尺寸的图片,而且输出也是图片,所以这是一个端到端的网络。)
  
(2) 左边的网络是收缩路径:使用卷积和maxpooling。
  
(3) 右边的网络是扩张路径:使用上采样产生的特征图与左侧收缩路径对应层产生的特征图进行concatenate操作。(pooling层会丢失图像信息和降低图像分辨率且是不可逆的操作,对图像分割任务有一些影响,对图像分类任务的影响不大,为什么要做上采样?因为上采样可以补足一些图片的信息,但是信息补充的肯定不完全,所以还需要与左边的分辨率比较高的图片相连接起来(直接复制过来再裁剪到与上采样图片一样大小),这就相当于在高分辨率和更抽象特征当中做一个折衷,因为随着卷积次数增多,提取的特征也更加有效,更加抽象,上采样的图片是经历多次卷积后的图片,肯定是比较高效和抽象的图片,然后把它与左边不怎么抽象但更高分辨率的特征图片进行连接)。
  
(4) 最后再经过两次反卷积操作,生成特征图,再用两个1X1的卷积做分类得到最后的两张heatmap,例如第一张表示的是第一类的得分,第二张表示第二类的得分heatmap,然后作为softmax函数的输入,算出概率比较大的softmax类,选择它作为输入给交叉熵进行反向传播训练。

下面是U-Net模型的代码实现:(贡献者:黄钦建-华南理工大学)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def get_unet():
inputs = Input((img_rows, img_cols, 1))
conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(inputs)
conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
# pool1 = Dropout(0.25)(pool1)
# pool1 = BatchNormalization()(pool1)

conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(pool1)
conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
# pool2 = Dropout(0.5)(pool2)
# pool2 = BatchNormalization()(pool2)

conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(pool2)
conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv3)
pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)
# pool3 = Dropout(0.5)(pool3)
# pool3 = BatchNormalization()(pool3)

conv4 = Conv2D(256, (3, 3), activation='relu', padding='same')(pool3)
conv4 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv4)
pool4 = MaxPooling2D(pool_size=(2, 2))(conv4)
# pool4 = Dropout(0.5)(pool4)
# pool4 = BatchNormalization()(pool4)

conv5 = Conv2D(512, (3, 3), activation='relu', padding='same')(pool4)
conv5 = Conv2D(512, (3, 3), activation='relu', padding='same')(conv5)

up6 = concatenate([Conv2DTranspose(256, (2, 2), strides=(
2, 2), padding='same')(conv5), conv4], axis=3)
# up6 = Dropout(0.5)(up6)
# up6 = BatchNormalization()(up6)
conv6 = Conv2D(256, (3, 3), activation='relu', padding='same')(up6)
conv6 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv6)

up7 = concatenate([Conv2DTranspose(128, (2, 2), strides=(
2, 2), padding='same')(conv6), conv3], axis=3)
# up7 = Dropout(0.5)(up7)
# up7 = BatchNormalization()(up7)
conv7 = Conv2D(128, (3, 3), activation='relu', padding='same')(up7)
conv7 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv7)

up8 = concatenate([Conv2DTranspose(64, (2, 2), strides=(
2, 2), padding='same')(conv7), conv2], axis=3)
# up8 = Dropout(0.5)(up8)
# up8 = BatchNormalization()(up8)
conv8 = Conv2D(64, (3, 3), activation='relu', padding='same')(up8)
conv8 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv8)

up9 = concatenate([Conv2DTranspose(32, (2, 2), strides=(
2, 2), padding='same')(conv8), conv1], axis=3)
# up9 = Dropout(0.5)(up9)
# up9 = BatchNormalization()(up9)
conv9 = Conv2D(32, (3, 3), activation='relu', padding='same')(up9)
conv9 = Conv2D(32, (3, 3), activation='relu', padding='same')(conv9)

# conv9 = Dropout(0.5)(conv9)

conv10 = Conv2D(1, (1, 1), activation='sigmoid')(conv9)

model = Model(inputs=[inputs], outputs=[conv10])

model.compile(optimizer=Adam(lr=1e-5),
loss=dice_coef_loss, metrics=[dice_coef])

return model

9.4 SegNet

  
可训练的图像分割引擎,包含一个encoder网络,一个对应的decoder网络,衔接像素级分类层,解码网络与VGG16的13层卷积层相同。解码网络是将低分辨率的编码特征图映射到全分辨率的特征图。解码网络使用最大池化层的池化索引进行非线性上采样,上采样过程就不需要学习。上采样得到的稀疏图与可训练的滤波器卷积得到致密的特征图。
  
使用池化层索引进行上采样的优势:
  
1)提升边缘刻画度;
  
2)减少训练的参数;
  
3)这种上采样模式可以包含到任何编码-解码网络中。
  
SegNet网络的结构如下图所示:

  
SegNet网络结构如图1所示,Input为输入图片,Output为输出分割的图像,不同颜色代表不同的分类。语义分割的重要性就在于不仅告诉你图片中某个东西是什么,而且告知你他在图片的位置。我们可以看到是一个对称网络,由中间绿色pooling层与红色upsampling层作为分割,左边是卷积提取高维特征,并通过pooling使图片变小,SegNet作者称为Encoder,右边是反卷积(在这里反卷积与卷积没有区别)与upsampling,通过反卷积使得图像分类后特征得以重现,upsampling使图像变大,SegNet作者称为Decoder,最后通过Softmax,输出不同分类的最大值。这就是大致的SegNet过程,下面对这个过程里面使用到的方法进行介绍。
  
编码网络与滤波器族卷积得到特征图,进行BN,ReLU,最大池化。最大池化是为了获得空间小位移的平移不变。最大池化和下采样损失了边缘细节,因此,在编码过程中保存边缘信息很重要。考虑到内存原因,只保存最大池化索引,如最大特征值的位置。
  
SegNet解码技术如下图所示:

  
解码网络使用保存的最大池化索引上采样,得到稀疏的特征图,将特征图与可训练的解码滤波器族卷积得到致密的特征图。之后进行BN。高维的特征图输入soft-max层,对每个像素进行分类,得到每个像素属于K类的概率。 图3中右边是FCN的解码技术,FCN对编码的特征图进行降维,降维后输入到解码网络,解码网络中,上采样使用反卷积实现,上采样的特征图与降维的编码图进行element-wise add得到最终的解码特征图。FCN解码模型需要存储编码特征图,在嵌入式设备中内存紧张。
  
SegNet的Encoder过程中,卷积的作用是提取特征,SegNet使用的卷积为same卷积(详见卷积神经网络CNN(1)),即卷积后不改变图片大小;在Decoder过程中,同样使用same卷积,不过卷积的作用是为upsampling变大的图像丰富信息,使得在Pooling过程丢失的信息可以通过学习在Decoder得到。SegNet中的卷积与传统CNN的卷积并没有区别。

9.5 空洞卷积(Dilated Convolutions)

  
在图像分割领域,图像输入到CNN(典型的网络比如FCN[3])中,FCN先像传统的CNN那样对图像做卷积再pooling,降低图像尺寸的同时增大感受野,但是由于图像分割预测是pixel-wise的输出,所以要将pooling后较小的图像尺寸upsampling到原始的图像尺寸进行预测(upsampling一般采用deconv反卷积操作,deconv可参见知乎答案如何理解深度学习中的deconvolution networks?),之前的pooling操作使得每个pixel预测都能看到较大感受野信息。因此图像分割FCN中有两个关键,一个是pooling减小图像尺寸增大感受野,另一个是upsampling扩大图像尺寸。在先减小再增大尺寸的过程中,肯定有一些信息损失掉了,那么能不能设计一种新的操作,不通过pooling也能有较大的感受野看到更多的信息呢?答案就是dilated conv。
  
以前的CNN主要问题总结:
  
(1)Up-sampling / pooling layer
  
(2)内部数据结构丢失;空间层级化信息丢失。
  
(3)小物体信息无法重建 (假设有四个pooling layer 则 任何小于 2^4 = 16 pixel 的物体信息将理论上无法重建。)
  
举例如下:

Dilated Convolution with a 3 x 3 kernel and dilation rate 2
   下面看一下dilated conv原始论文[4]中的示意图

  
(a) 图对应3x3的1-dilated conv,和普通的卷积操作一样,(b)图对应3x3的2-dilated conv,实际的卷积kernel size还是3x3,但是空洞为1,也就是对于一个7x7的图像patch,只有9个红色的点和3x3的kernel发生卷积操作,其余的点略过。也可以理解为kernel的size为7x7,但是只有图中的9个点的权重不为0,其余都为0。 可以看到虽然kernel size只有3x3,但是这个卷积的感受野已经增大到了7x7(如果考虑到这个2-dilated conv的前一层是一个1-dilated conv的话,那么每个红点就是1-dilated的卷积输出,所以感受野为3x3,所以1-dilated和2-dilated合起来就能达到7x7的conv),©图是4-dilated conv操作,同理跟在两个1-dilated和2-dilated conv的后面,能达到15x15的感受野。对比传统的conv操作,3层3x3的卷积加起来,stride为1的话,只能达到(kernel-1) * layer+1=7的感受野,也就是和层数layer成线性关系,而dilated conv的感受野是指数级的增长。
  
dilated的好处是不做pooling损失信息的情况下,加大了感受野,让每个卷积输出都包含较大范围的信息。在图像需要全局信息或者语音文本需要较长的sequence信息依赖的问题中,都能很好的应用dilated conv,比如图像分割、语音合成WaveNet、机器翻译ByteNet中。

9.6 RefineNet

  
网络结构:
  
RefineNet block的作用就是把不同resolution level的feature map进行融合。网络结构如下:


  
最左边一栏就是FCN的encoder部分(文中是用的ResNet),先把pretrained ResNet按feature map的分辨率分成四个ResNet blocks,然后向右把四个blocks分别作为4个path通过RefineNet block进行融合refine,最后得到一个refined feature map(接softmax再双线性插值输出)。
注意除了RefineNet-4,所有的RefineNet block都是二输入的,用于融合不同level做refine,而单输入的RefineNet-4可以看作是先对ResNet的一个task adaptation。

  
RefineNet Block
  
接下来仔细看一下RefineNet block,可以看到主要组成部分是Residual convolution unit, Multi-resolution fusion, Chained residual pooling, Output convolutions. 切记这个block作用是融合多个level的feature map输出单个level的feature map,但具体的实现应该是和输入个数、shape无关的。

  
Residual convolution unit就是普通的去除了BN的residual unit;

  
Multi-resolution fusion是先对多输入的feature map都用一个卷积层进行adaptation(都化到最小的feature map的shape),再上采样再做element-wise的相加。注意如果是像RefineNet-4那样的单输入block这一部分就直接pass了;

  
Chained residual pooling中的ReLU对接下来池化的有效性很重要,还可以使模型对学习率的变化没这么敏感。这个链式结构能从很大范围区域上获取背景context。另外,这个结构中大量使用了identity mapping这样的连接,无论长距离或者短距离的,这样的结构允许梯度从一个block直接向其他任一block传播。

  
Output convolutions就是输出前再加一个RCU。

9.7 PSPNet

  
场景解析对于无限制的开放词汇和不同场景来说是具有挑战性的.本文使用文中的pyramid pooling module实现基于不同区域的上下文集成,提出了PSPNet,实现利用上下文信息的能力进行场景解析。
  
作者认为,FCN存在的主要问题是没有采取合适的策略来用全局的信息,本文的做法就是借鉴SPPNet来设计了PSPNet解决这个问题。
  
很多State-of-the-art的场景解析框架都是基于FCN的.基于CNN的方法能够增强动态物体的理解,但是在无限制词汇和不同场景中仍然面临挑战.举个例子,如下图.


  
FCN认为右侧框中是汽车,但是实际上是船,如果参考上下文的先验知识,就会发现左边是一个船屋,进而推断是框中是船.FCN存在的主要问题就是不能利用好全局的场景线索。

  
对于尤其复杂的场景理解,之前都是采用空间金字塔池化来做的,和之前方法不同(为什么不同,需要参考一下经典的金字塔算法),本文提出了pyramid scene parsing network(PSPNet)。
  
本文的主要贡献如下:
  
(1) 提出了PSPNet在基于FCN的框架中集成困难的上下文特征
  
(2) 通过基于深度监督误差开发了针对ResNet的高效优化策略
  
(3) 构建了一个用于state-of-the-art的场景解析和语义分割的实践系统(具体是什么?)
  
通过观察FCN的结果,发现了如下问题:
  
(1) 关系不匹配(Mismatched Relationship)
  
(2) 易混淆的类别(Confusion Categories)
  
(3) 不显眼的类别(Inconspicuous Classes)
  
总结以上结果发现,以上问题部分或者全部与上下文关系和全局信息有关系,因此本文提出了PSPNet.框架如下:


  
并且加入额外的深度监督 Loss

9.8 DeepLab系列

9.8.1 DeepLabv1

  
DeepLab 是结合了深度卷积神经网络(DCNNs)和概率图模型(DenseCRFs)的方法。
  
在实验中发现 DCNNs 做语义分割时精准度不够的问题,根本原因是 DCNNs 的高级特征的平移不变性,即高层次特征映射,根源于重复的池化和下采样。
  
针对信号下采样或池化降低分辨率,DeepLab 是采用的 atrous(带孔)算法扩展感受野,获取更多的上下文信息。
  
分类器获取以对象中心的决策是需要空间变换的不变性,这天然地限制了 DCNN 的定位精度,DeepLab 采用完全连接的条件随机场(CRF)提高模型捕获细节的能力。
  
除空洞卷积和 CRFs 之外,论文使用的 tricks 还有 Multi-Scale features。其实就是 U-Net 和 FPN 的思想,在输入图像和前四个最大池化层的输出上附加了两层的 MLP,第一层是 128 个 3×3 卷积,第二层是 128 个 1×1 卷积。最终输出的特征与主干网的最后一层特征图融合,特征图增加 5×128=640 个通道。
  
实验表示多尺度有助于提升预测结果,但是效果不如 CRF 明显。
  
论文模型基于 VGG16,在 Titan GPU 上运行速度达到了 8FPS,全连接 CRF 平均推断需要 0.5s ,在 PASCAL VOC-2012 达到 71.6% IOU accuracy。

9.8.2 DeepLabv2

  
DeepLabv2 是相对于 DeepLabv1 基础上的优化。DeepLabv1 在三个方向努力解决,但是问题依然存在:特征分辨率的降低、物体存在多尺度,DCNN 的平移不变性。
  
因 DCNN 连续池化和下采样造成分辨率降低,DeepLabv2 在最后几个最大池化层中去除下采样,取而代之的是使用空洞卷积,以更高的采样密度计算特征映射。
  
物体存在多尺度的问题,DeepLabv1 中是用多个 MLP 结合多尺度特征解决,虽然可以提供系统的性能,但是增加特征计算量和存储空间。
  
论文受到 Spatial Pyramid Pooling (SPP) 的启发,提出了一个类似的结构,在给定的输入上以不同采样率的空洞卷积并行采样,相当于以多个比例捕捉图像的上下文,称为 ASPP (atrous spatial pyramid pooling) 模块。
  
DCNN 的分类不变形影响空间精度。DeepLabv2 是采样全连接的 CRF 在增强模型捕捉细节的能力。
  
论文模型基于 ResNet,在 NVidia Titan X GPU 上运行速度达到了 8FPS,全连接 CRF 平均推断需要 0.5s ,在耗时方面和 DeepLabv1 无差异,但在 PASCAL VOC-2012 达到 79.7 mIOU。

9.8.3 DeepLabv3

  
好的论文不止说明怎么做,还告诉为什么。DeepLab 延续到 DeepLabv3 系列,依然是在空洞卷积做文章,但是探讨不同结构的方向。
  
DeepLabv3 论文比较了多种捕获多尺度信息的方式:

  
1.Image Pyramid:将输入图片放缩成不同比例,分别应用在 DCNN 上,将预测结果融合得到最终输出。
  
2.Encoder-Decoder:利用 Encoder 阶段的多尺度特征,运用到 Decoder 阶段上恢复空间分辨率,代表工作有 FCN、SegNet、PSPNet 等工。
  
3.Deeper w. Atrous Convolution:在原始模型的顶端增加额外的模块,例如 DenseCRF,捕捉像素间长距离信息。
  
4.Spatial Pyramid Pooling:空间金字塔池化具有不同采样率和多种视野的卷积核,能够以多尺度捕捉对象。
  
DeepLabv1-v2 都是使用带孔卷积提取密集特征来进行语义分割。但是为了解决分割对象的多尺度问题,DeepLabv3 设计采用多比例的带孔卷积级联或并行来捕获多尺度背景。
  
此外,DeepLabv3 将修改之前提出的带孔空间金字塔池化模块,该模块用于探索多尺度卷积特征,将全局背景基于图像层次进行编码获得特征,取得 state-of-art 性能,在 PASCAL VOC-2012 达到 86.9 mIOU。

9.8.4 DeepLabv3+

  
语义分割关注的问题:
  
1、 实例对象多尺度问题。
  
2、 因为深度网络存在stride=2的层,会导致feature分辨率下降,从而导致预测精度降低,而造成的边界信息丢失问题。
  
deeplab V3新设计的aspp结构解决了问题1,deeplab v3+主要目的在于解决问题2。
  
问题2 可以使用空洞卷积替代更多的pooling层来获取分辨率更高的feature。但是feature分辨率更高会极大增加运算量。以deeplab v3使用的resnet101为例,stride=16将造成后面9层feature变大,后面9层的计算量变为原来的2*2=4倍大。stride=8则更为恐怖,后面78层的计算量都会变大很多。
  
解决方案:1、编解码器结构;2 Modified Aligned Xception

  
在deeplabv3基础上加入解码器。A是aspp结构,其中8x的上采样可以看做是一个解码器。B是编解码结构,它集合了高层和底层的特征。C就是本文采取的结构。
  
方法:
  
(1)Encoder-Decoder with Atrous Convolution

  
编码器采用deeplabv3。
  
解码器部分:先从低层级选一个feature,将低层级的feature用1 * 1的卷积进行通道压缩(原本为256通道,或者512通道),目的在于减少低层级的比重。作者认为编码器得到的feature具有更丰富的信息,所以编码器的feature应该有更高的比重。 这样做有利于训练。
  
再将编码器的输出上采样,使其分辨率与低层级feature一致。举个例子,如果采用resnet conv2 输出的feature,则这里要* 4上采样。将两种feature连接后,再进行一次3 * 3的卷积(细化作用),然后再次上采样就得到了像素级的预测。后面的实验结果表明这种结构在 stride=16 时既有很高的精度速度又很快。stride=8相对来说只获得了一点点精度的提升,但增加了很多的计算量。
  
(2)Modified Aligned Xception
  
Xception主要采用了deepwish seperable convolution来替换原来的卷积层。简单的说就是这种结构能在更少参数更少计算量的情况下学到同样的信息。这边则是考虑将原来的resnet-101骨架网换成xception。

  
红色部分为修改
  
更多层:重复8次改为16次(基于MSRA目标检测的工作)。
  
将原来简单的pool层改成了stride为2的deepwish seperable convolution。
  
额外的RELU层和归一化操作添加在每个 3 × 3 depthwise convolution之后(原来只在1 * 1卷积之后)

9.9 Mask-R-CNN

9.9.1 Mask-RCNN 的网络结构示意图

  
其中黑色部分为原来的Faster-RCNN,红色部分为在Faster网络上的修改:
  
1)将ROI Pooling层替换成了ROIAlign;
  
2)添加并列的FCN层(Mask层);
  
先来概述一下Mask-RCNN的几个特点(来自于PaperMask R-CNN的Abstract):
  
1)在边框识别的基础上添加分支网络,用于语义Mask识别;
  
2)训练简单,相对于Faster仅增加一个小的Overhead,可以跑到5FPS;
  
3)可以方便的扩展到其他任务,比如人的姿态估计等;
  
4)不借助Trick,在每个任务上,效果优于目前所有的 single-model entries;包括 COCO 2016 的Winners。

9.9.2 RCNN行人检测框架

  
来看下后面两种RCNN方法与Mask结合的示意图:


  
图中灰色部分是原来的RCNN结合ResNet or FPN的网络,下面黑色部分为新添加的并联Mask层,这个图本身与上面的图也没有什么区别,旨在说明作者所提出的Mask RCNN方法的泛化适应能力:可以和多种RCNN框架结合,表现都不错。

9.9.3 Mask-RCNN 技术要点

  
1.技术要点1 - 强化的基础网络
  
通过ResNeXt-101+FPN用作特征提取网络,达到state-of-the-art的效果。
  
2.技术要点2 - ROIAlign
  
采用ROIAlign替代RoiPooling(改进池化操作)。引入了一个插值过程,先通过双线性插值到1414,再pooling到77,很大程度上解决了仅通过Pooling直接采样带来的Misalignment对齐问题。
  
PS: 虽然 Misalignment 在分类问题上影响并不大,但在 Pixel 级别的 Mask 上会存在较大误差。
  
后面我们把结果对比贴出来(Table2 c & d),能够看到 ROIAlign 带来较大的改进,可以看到,Stride 越大改进越明显。
  
3.技术要点3 - Loss Function
  
每个ROIAlign对应K * m^2维度的输出。K对应类别个数,即输出K个mask,m对应池化分辨率(7 * 7)。Loss函数定义:

$$ Lmask(Cls_k)=Sigmoid(Cls_k) $$

  

$Lmask(Cls_k) = Sigmoid (Cls_k)$ ,平均二值交叉熵 (average binary cross-entropy)Loss,通过逐像素的 Sigmoid 计算得到。

  
Why K个mask?通过对每个 Class 对应一个Mask可以有效避免类间竞争(其他Class不贡献Loss)。


  
通过结果对比来看(Table2 b),也就是作者所说的 Decouple 解耦,要比多分类的Softmax效果好很多。
  
另外,作者给出了很多实验分割效果,就不都列了,只贴一张和FCIS的对比图(FCIS出现了Overlap的问题)

9.10 CNN在基于弱监督学习的图像分割中的应用

  
答案来源:CNN在基于弱监督学习的图像分割中的应用

  
最近基于深度学习的图像分割技术一般依赖于卷积神经网络CNN的训练,训练过程中需要非常大量的标记图像,即一般要求训练图像中都要有精确的分割结果。
  
对于图像分割而言,要得到大量的完整标记过的图像非常困难,比如在ImageNet数据集上,有1400万张图有类别标记,有50万张图给出了bounding box,但是只有4460张图像有像素级别的分割结果。对训练图像中的每个像素做标记非常耗时,特别是对医学图像而言,完成对一个三维的CT或者MRI图像中各组织的标记过程需要数小时。
  
如果学习算法能通过对一些初略标记过的数据集的学习就能完成好的分割结果,那么对训练数据的标记过程就很简单,这可以大大降低花在训练数据标记上的时间。这些初略标记可以是:
  
1、只给出一张图像里面包含哪些物体,
  
2、给出某个物体的边界框,
  
3、对图像中的物体区域做部分像素的标记,例如画一些线条、涂鸦等(scribbles)。

9.10.1 Scribble标记

  
论文地址:ScribbleSup: Scribble-Supervised Convolutional Networks for Semantic Segmentation (CVPR 2016)
  
香港中文大学的Di Lin提出了一个基于Scribble标记的弱监督学习方法。Scribble是一个很方便使用的标记方法,因此被用得比较广泛。如下图,只需要画五条线就能完成对一副图像的标记工作。


  
ScribbleSup分为两步,第一步将像素的类别信息从scribbles传播到其他未标记的像素,自动完成所有的训练图像的标记工作; 第二步使用这些标记图像训练CNN。在第一步中,该方法先生成super-pxels, 然后基于graph cut的方法对所有的super-pixel进行标记。

  
Graph Cut的能量函数为:

$$ \sum_{i}\psi _i\left(y_i|X,S\right)+\sum_{i,j}\psi_{ij}\left(y_i,y_j,X\right) $$

  
在这个graph中,每个super-pixel是graph中的一个节点,相接壤的super-pixel之间有一条连接的边。这个能量函数中的一元项包括两种情况,一个是来自于scribble的,一个是来自CNN对该super-pixel预测的概率。整个最优化过程实际上是求graph cut能量函数和CNN参数联合最优值的过程:

$$ \sum_{i}\psi _i^{scr}\left(y_i|X,S\right)+\sum _i-logP\left(y_i| X,\theta\right)+\sum_{i,j}\psi _{ij}\left(y_i,y_j|X\right) $$

  
上式的最优化是通过交替求 $Y$ 和 $\theta$ 的最优值来实现的。文章中发现通过三次迭代就能得到比较好的结果。

9.10.2 图像级别标记

  
论文地址:Constrained Convolutional Neural Networks for Weakly Supervised Segmentation (ICCV 2015)
  
UC Berkeley的Deepak Pathak使用了一个具有图像级别标记的训练数据来做弱监督学习。训练数据中只给出图像中包含某种物体,但是没有其位置信息和所包含的像素信息。该文章的方法将image tags转化为对CNN输出的label分布的限制条件,因此称为 Constrained convolutional neural network (CCNN).


  

该方法把训练过程看作是有线性限制条件的最优化过程:

$$ \underset{\theta ,P}{minimize}\qquad D(P(X)||Q(X|\theta ))\\ subject\to\qquad A\overrightarrow{P} \geqslant \overrightarrow{b},\sum_{X}^{ }P(X)=1 $$

  

其中的线性限制条件来自于训练数据上的标记,例如一幅图像中前景类别像素个数期望值的上界或者下界(物体大小)、某个类别的像素个数在某图像中为0,或者至少为1等。该目标函数可以转化为为一个loss function,然后通过SGD进行训练。


  
实验中发现单纯使用Image tags作为限制条件得到的分割结果还比较差,在PASCAL VOC 2012 test数据集上得到的mIoU为35.6%,加上物体大小的限制条件后能达到45.1%,如果再使用bounding box做限制,可以达到54%。FCN-8s可以达到62.2%,可见弱监督学习要取得好的结果还是比较难。

9.10.3 DeepLab+bounding box+image-level labels**

  
论文地址:Weakly-and Semi-Supervised Learning of a DCNN for Semantic Image Segmentation
  
Google的George Papandreou 和UCLA的Liang-Chieh Chen等在DeepLab的基础上进一步研究了使用bounding box和image-level labels作为标记的训练数据。使用了期望值最大化算法(EM)来估计未标记的像素的类别和CNN的参数。


  
对于image-level标记的数据,我们可以观测到图像的像素值和图像级别的标记 ,但是不知道每个像素的标号,因此把 $y$ 当做隐变量。使用如下的概率图模式:

$$ P\left ( x,y,z;\theta \right ) = P\left ( x \right )\left (\prod_{m=1}^{M} P\left ( y_m|x;\theta \right )\right )P\left ( z|y \right ) $$

  
这篇论文是通过EM算法来学习模型的参数 $\theta$ ,具体推导过程可参考原论文。


  
对于给出bounding box标记的训练图像,该方法先使用CRF对该训练图像做自动分割,然后在分割的基础上做全监督学习。通过实验发现,单纯使用图像级别的标记得到的分割效果较差,但是使用bounding box的训练数据可以得到较好的结果,在VOC2012 test数据集上得到mIoU 62.2%。另外如果使用少量的全标记图像和大量的弱标记图像进行结合,可以得到与全监督学习(70.3%)接近的分割结果(69.0%)。

9.10.4 统一的框架

  
论文地址:Learning to Segment Under Various Forms of Weak Supervision (CVPR 2015)

  
Wisconsin-Madison大学的Jia Xu提出了一个统一的框架来处理各种不同类型的弱标记:图像级别的标记、bounding box和部分像素标记如scribbles。该方法把所有的训练图像分成共计 $n$ 个super-pixel,对每个super-pixel提取一个 $d$ 维特征向量。因为不知道每个super-pixel所属的类别,相当于无监督学习,因此该方法对所有的super-pixel做聚类,使用的是最大间隔聚类方法(max-margin clustering, MMC),该过程的最优化目标函数是:

$$ \underset{W,H}{min} \qquad \frac{1}{2}tr\left ( W^TW \right ) + \lambda\sum_{p=1}^{n}\sum_{c=1}^{C}\xi \left ( w_c;x_p;h_p^c \right) $$

  
在这个目标函数的基础上,根据不同的弱标记方式,可以给出不同的限制条件,因此该方法就是在相应的限制条件下求最大间隔聚类。

  
该方法在Siftflow数据集上得到了比较好的结果,比state-of-the-art的结果提高了10%以上。

  
小结:在弱标记的数据集上训练图像分割算法可以减少对大量全标记数据的依赖,在大多数应用中会更加贴合实际情况。弱标记可以是图像级别的标记、边框和部分像素的标记等。训练的方法一般看做是限制条件下的最优化方法。另外EM算法可以用于CNN参数和像素类别的联合求优。

9.10.5 弱监督分割最新进展(贡献者:明奇-北京理工大学)

  • bbox监督
  1. Learning to Segment via Cut-and-Paste(ECCV 2018)

    利用GAN对抗学习的思想,在cut-paste思想指导下利用bbox弱监督进行实例分割。

    采用对抗学习的思想,网络主体分为两大部分:mask生成器和合成图像判别器。具体过程为:(1)在图像上截取gt,经过特征提取后预测一个bbox内gt的mask;(2)在原图上随机cut一个背景图像,将bbox内按照生成的mask提取出物体分割结果,然后paste到原图裁剪的背景上去;(3)合成的图像经过判别器进行真假判断。
    通过生成器生成更好mask来使得判别器更难判别,在对抗学习中提升两者的性能,逐渐获得更好的结果 .

  2. Simple Does It: Weakly Supervised Instance and Semantic Segmentation(CVPR2017)
    本文做的是bbox弱监督语义/实例分割任务,能达到全监督分割效果(DeepLabv1)的95%。主要工作为:讨论了使用弱监督语义标签进行迭代训练的方法,以及其限制和不足之处;证明了通过类似GrabCut的算法能通过bbox生成分割训练标签方法的可行性,可以避免像上面的迭代方法重新调整网络训练策略;在VOC数据集上逼近监督学习的分割任务效果。
    作者的启发是:将bbox level的mask送入网络训练后得到分割mask的比输入的bbox mask要好(这是很好的insight)。因此启发的操作是:将bbox level标注作为初始mask输入优化,每次得到的标注作为gt进行下一轮的迭代,从而不断获得更好的效果。效果图如下:

    在此基础上,再加上优化的GrabCut+算法,以及部分区域的筛选,以及BSDS500的边界预测信息整合到一起,能够达到很好的弱监督迭代分割效果。

  • 分类监督
  1. Weakly Supervised Learning of Instance Segmentation with Inter-pixel Relations(CVPR2019)
    使用分类标注作为弱监督信息,在CAM提取到特征的基础上,进一步设计IRNet学习额外的特征约束,从而到达更好的弱监督实例分割效果。为了解决CAM应用到实例分割的上述局限,设计IRNet。其组成为两部分:(1)不分类别的实例响应图 (2)pairwise semantic affinitie。其中通过不分类别的实例响应图和CAM结合,约束后得到instance-wise CAMS;另一个分支预先预测物体的边界然后得到pairwise semantic affinitie(关于这个的论文参考Related Work的对应部分,有相应的方法,暂时不深究)进行融合和处理得到最终的分割。整体流程如下:

  2. Weakly Supervised Instance Segmentation using Class Peak Response(CVPR2018)
    本文使用图像级的类别标注监督信息,通过探索类别响应峰值使分类网络能够很好地提取实例分割mask。本工作是使用图像级标注进行弱监督实例分割的首个工作。
    在分类监督信息之下,CNN网络会产生一个类别响应图,每个位置是类别置信度分数。其局部极大值往往具有实例很强视觉语义线索。首先将类别峰值响应图的信息进行整合,然后反向传播将其映射到物体实例信息量较大的区域如边界。上述从类别极值响应图产生的映射图称为Peak Response Maps (PRMs),该图提供了实例物体的详细表征,可以很好地用作分割监督信息。
    具体流程如图:

    首先将图片经过正常的分类网络训练,其中在类别预测响应图上提取出局部响应极值点,进行增强卷积后预测出PRM。然后结合多种信息进行推断生成mask。

  3. Weakly Supervised Semantic Segmentation Using Superpixel Pooling Network(AAAI 2017)
    本文介绍通过类别标注的标签实现弱监督语义分割的方法。该方法在语义分割mask生成和使用生成mask学习分割生成网络之间反复交替。要实现这种交替迭代学习,关键点就是如何利用类别标注得到较准确的初始分割。为了解决这一问题,提出了Superpixel Pooling Network (SPN),将输入图像的超像素分割结果作为低阶结构的表征,辅助语义分割的推断。

    首先是SPN生成初始mask,然后用另一个网络DecoupledNet来学习每个像素的mask标注。其中,该分割网络将语义分割任务解耦为分类和分割两个子任务,并且能够从类别标注中学习形状先验知识用于辅助分割。

9.11 DenseNet(贡献者:黄钦建-华南理工大学)

  
这篇论文是CVPR2017年的最佳论文。

  
卷积神经网络结构的设计主要朝着两个方向发展,一个是更宽的网络(代表:GoogleNet、VGG),一个是更深的网络(代表:ResNet)。但是随着层数的加深会出现一个问题——梯度消失,这将会导致网络停止训练。到目前为止解决这个问题的思路基本都是在前后层之间加一个identity connections(short path)。

  
由上图中可知Resnet是做值的相加(也就是add操作),通道数是不变的。而DenseNet是做通道的合并(也就是Concatenation操作),就像Inception那样。从这两个公式就可以看出这两个网络的本质不同。此外DensetNet的前面一层输出也是后面所有层的输入,这也不同于ResNet残差网络。

  
DenseNet的Block结构如上图所示。

  
1*1卷积核的目的:减少输入的特征图数量,这样既能降维减少计算量,又能融合各个通道的特征。我们将使用BottleNeck Layers的DenseNet表示为DenseNet-B。(在论文的实验里,将1×1×n小卷积里的n设置为4k,k为每个H产生的特征图数量)

  
上图是DenseNet网络的整体网络结构示意图。其中1*1卷积核的目的是进一步压缩参数,并且在Transition Layer层有个参数Reduction(范围是0到1),表示将这些输出缩小到原来的多少倍,默认是0.5,这样传给下一个Dense Block的时候channel数量就会减少一半。当Reduction的值小于1的时候,我们就把带有这种层的网络称为DenseNet-C。

  
DenseNet网络的优点包括:

  • 减轻了梯度消失
  • 加强了feature的传递
  • 更有效地利用了feature
  • 一定程度上较少了参数数量
  • 一定程度上减轻了过拟合

9.12 图像分割的常用数据集

9.12.1 PASCAL VOC

VOC 数据集分为20类,包括背景为21类,分别如下:

  • Person: person
  • Animal: bird, cat, cow, dog, horse, sheep
  • Vehicle: aeroplane, bicycle, boat, bus, car, motorbike, train
  • Indoor: bottle, chair, dining table, potted plant, sofa, tv/monitor

VOC 数据集中用于分割比赛的图片实例如下,包含原图以及图像分类分割和图像物体分割两种图(PNG格式)。图像分类分割是在20种物体中,ground-turth图片上每个物体的轮廓填充都有一个特定的颜色,一共20种颜色。

9.12.2 MS COCO

MS COCO 是最大图像分割数据集,提供的类别有 80 类,有超过 33 万张图片,其中 20 万张有标注,整个数据集中个体的数目超过 150 万个。MS COCO是目前难度最大,挑战最高的图像分割数据集。

9.12.3 Cityscapes

Cityscapes 是驾驶领域进行效果和性能测试的图像分割数据集,它包含了5000张精细标注的图像和20000张粗略标注的图像,这些图像包含50个城市的不同场景、不同背景、不同街景,以及30类涵盖地面、建筑、交通标志、自然、天空、人和车辆等的物体标注。Cityscapes评测集有两项任务:像素级(Pixel-level)图像场景分割(以下简称语义分割)与实例级(Instance-level)图像场景分割(以下简称实例分割)。

9.13 全景分割(贡献者:北京理工大学–明奇)

全景分割的开山之作:何恺明的Panoptic Segmentation

  1. Introduction

  语义分割通过带孔全卷积网络,根据不同的stuff进行划分;实例分割则是在目标检测的基础上基于检测框进行物体的分割。缺少一种框架可以将两者进行融合实现既能分割背景又能分割实例,而这在自动驾驶和AR技术中大有作为。由此提出的全景分割任务能将两者进行结合。

  全景分割的思路很直观:为图像的每个像素分配语义label和类内实例id,前者用于区分语义信息,后者用于分割实例(因此stuff不具有实例id)。提出全景分割时,只是启发式地将语意分割和实例分割两种任务的输出进行后处理的融合(如NMS),并以此建立PS任务的baseline。为了评价全景分割的质量,提出panoptic quality (PQ) 标准,将背景和物体的评价纳入一个完整的框架下。示意图如下:

  1. Panoptic Segmentation
  • Task format
    全景分割的标注方法:
    像素级的标注,标出类别label和类内实例id。如果某像素的这两个信息都能匹配,则可以将该像素匹配到某个类别和实例中去;类外的像素可以分配空标签,即并不是所有的像素都要有语义类别。

  • Stuff and thing labels
    对于stuff和thing(背景填充和物体实例)的标签,交集是空集,并集是所有可能的label空间。这两者是互相独立不相关的(很好理解,像素属于那个类和它属于哪个物体不具有相关性)。

  • Relationship
    都是像素级的label,需要为每个像素分配对应的标签。但是实例分割基于region的,允许重叠的segmentation,而全景分割和语义分割一样是像素级的label,不允许重叠标签的出现。

  • Confidence scores
    这一点上更像语义分割而不是实例分割,对于PS不需要置信分数评价分割质量。提到这个,作者认为语义分割和全景分割可以直接利用人工标注的label进行对比从而评价当前mask的质量;而实例分割在选择mask时评价的是分类置信度,这个并没有人工标注进行参考,因此难以把握。

  1. Panoptic Segmentation Metric
      用于衡量全景分割效果的指标应具有:完备性;可解释性;简洁性。由是提出了PQ指标,可分为两步:分割匹配、在匹配上进行计算PQ。

3.1 Segment Matching
  定义match:预测的segmentation和gt的iou大于0.5,说明两者can match。再结合全景分割的不可重叠性,不难得到:最多只有一个预测的segmentation可以match gt。

3.2 PQ Computation
  PQ的计算类似mAP,也是类内求取,然后求类间的平均值,以便不敏感类别不平衡。对于每一类,可以根据gt与预测的segmentation分为三类(下图描述):

TP: 预测为正,实际为正,描述match较好的
FP: 预测为正,实际为负,描述match错的
FN: 预测为负,实际为正,描述没match出来的gt
  通过上述三类可以计算得到PQ值公式:

式中出去FP与FN后,剩下的式子描述的是match的segmentation的平均IoU,加上FP与FN是为了惩罚match失败的分割实例。
有意思的是,对上述式子进行简单的恒等变化:

第一项评价的是match分割的质量,第二项类似于F1得分。因此可以PQ分解为:

$$PQ=SQ*RQ$$
  • Void labels
    gt中可能出现两种像素标注为空的情况:超出类别的像素和模糊不清的像素(难以分类)。在评估结果时,这些空的标签不予以评估。具体而言:
    (1)在matching部分,预测出为void的像素会被移出prediction并不参与IoU计算;
    (2)matching后,unmatched prediction按照一般情况会计算FP FN,但是对于空标签情况,如果该prediction含有的void像素块超过一定匹配阈值就会被移除,并不算作FP计算得分。

  • Group labels
    有时区分相同语义类别的实例个体标注比较困难,因此有提出组标签的标注方法。但对于PQ计算而言:
    (1)matching部分不使用组标签,而是严格区分实例
    (2)matching后,对于包含一部分相同类别像素点的unmatched predicted segments,这一部分将被去除并不视作false positives

3.3 Comparison to Existing Metrics

  • Semantic segmentation metrics
    衡量语义分割的标准有像素级精度,平均精度,IoU。但是其只专注于像素级的划分,不能反映物体实例级别的分割性能。

  • Instance segmentation metrics
    度量为AP,主要是引入了置信度分数confidence score对检测目标进行打分。(两者不是完全的隔绝,实例分割也有用IoU监督的,而confidence score是否能够反映mask的真实质量也有存疑过,这个标准也不是固定的)

  • Panoptic quality
    PQ的度量可以分解成SQ和RQ,SQ反映了语义分割的像素级IoU性能,RQ专注于检测识别的效果,因此将两者统一到一个框架下。

分割效果:




TODO
  • [ ] 图像分割数据集标注工具
  • [ ] 图像分割评价标准
  • [x] 全景分割
  • [ ] UNet++

螺旋矩阵系列

螺旋矩阵

题号为54,位于https://leetcode.cn/problems/spiral-matrix/description/ 题解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if len(matrix)==1:
return matrix[0]
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
row, col, direction_index = 0, 0, 0
res = []
m = len(matrix)
n = len(matrix[1])
for i in range(m * n):
res.append(matrix[row][col])
matrix[row][col] = -999999999
next_row = row + directions[direction_index % 4][0]
next_col = col + directions[direction_index % 4][1]
if next_row < 0 or next_row >= m or next_col < 0 or next_col >= n or matrix[next_row][next_col] == -999999999:
direction_index = direction_index + 1
next_row = row + directions[direction_index % 4][0]
next_col = col + directions[direction_index % 4][1]
row, col = next_row, next_col
return res

上下两道题用的是同样的思路,这样比较好统一

螺旋矩阵II

题号为59,位于 https://leetcode.cn/problems/spiral-matrix-ii/description/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
directions = [(0,1),(1,0),(0,-1),(-1,0)]
matrix = [[-1]*n for _ in range(n)]
row, col, direc_index = 0,0,0
for i in range(n*n):
matrix[row][col] = i + 1
next_row = row + directions[direc_index%4][0]
next_col = row + directions[direc_index%4][1]
if next_row < 0 or next_row >= n or next_col < 0 or next_row >= n or matrix[next_row][next_col] > -1:
direc_index = direc_index + 1
next_row = row + directions[direc_index%4][0]
next_col = row + directions[direc_index%4][1]
row, col = next_row, next_col
return matrix

注意具体的思路是先便利,然后换方向

螺旋矩阵III

题号为885,位于 https://leetcode.cn/problems/spiral-matrix-iii/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution:
def spiralMatrixIII(self, rows: int, cols: int, rStart: int, cStart: int) -> List[List[int]]:
visited_flag = [[False] * cols for _ in range(rows)]
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
direction_index = -1
cnt = 0
init_lens = 0
res = []
res.append([rStart, cStart])
while cnt + 1 < rows * cols:
init_lens = init_lens + 1

direction_index = direction_index + 1
for j1 in range(init_lens):
next_r = rStart + directions[direction_index % 4][0]
next_c = cStart + directions[direction_index % 4][1]
if next_r < 0 or next_r >= rows or next_c < 0 or next_c >= cols or visited_flag[next_r][next_c]:
rStart = next_r
cStart = next_c
else:
res.append([next_r, next_c])
rStart = next_r
cStart = next_c
visited_flag[next_r][next_c] = True
cnt = cnt + 1

direction_index = direction_index + 1
for j2 in range(init_lens):
next_r = rStart + directions[direction_index % 4][0]
next_c = cStart + directions[direction_index % 4][1]
if next_r < 0 or next_r >= rows or next_c < 0 or next_c >= cols or visited_flag[next_r][next_c]:
rStart = next_r
cStart = next_c
else:
res.append([next_r, next_c])
rStart = next_r
cStart = next_c
visited_flag[next_r][next_c] = True
cnt = cnt + 1
return res

主要思路就是分析题目,其实每个固定的长度比如走1格子,其实是分为两个角度来走的,那么整体上就是1步往右,1步往下,2步往左,2步往上。依次这样来再结合判断条件。

螺旋矩阵 IV

和,2一样,题目2326,位于 https://leetcode.cn/problems/spiral-matrix-iv/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def spiralMatrix(self, m: int, n: int, head: Optional[ListNode]) -> List[List[int]]:
res = []
while head:
res.append(head.val)
head = head.next
matrix = [[-1]*n for _ in range(m)]
visited = [[False]*n for _ in range(m)]
x, y, dirrection_index = 0,0,0
directions = [(0,1),(1,0),(0,-1),(-1,0)]
for i in range(m*n):
if i < len(res):
matrix[x][y] = res[i]
visited[x][y] = True
next_x = x + directions[dirrection_index%4][0]
next_y = y + directions[dirrection_index%4][1]
if next_x<0 or next_x>=m or next_y<0 or next_y>=n or visited[next_x][next_y]:
dirrection_index = dirrection_index + 1
next_x = x + directions[dirrection_index%4][0]
next_y = y + directions[dirrection_index%4][1]
x, y = next_x, next_y
return matrix

数组类

摆动排序II[324]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 自己解法
class Solution:
def wiggleSort(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
mid = (len(nums) + 1) // 2
nums.sort()
left = nums[0:mid][::-1]
right = nums[mid:][::-1]
all_combine = []
n1 = mid
n2 = len(nums) - n1
index1 = 0
index2 = 0
while index1 < n1 and index2 < n2:
all_combine.append(left[index1])
all_combine.append(right[index2])
index1 += 1
index2 += 1
all_combine.extend(left[index1:])
for i in range(len(nums)):
nums[i] = all_combine[i]
# 简单解法
class Solution:
def wiggleSort(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
nums.sort()
temp = [0] * len(nums)
j = len(nums) - 1
for i in range(1, len(nums), 2):
temp[i] = nums[j]
j = j - 1
for i in range(0, len(nums), 2):
temp[i] = nums[j]
j = j - 1
for i in range(len(nums)):
nums[i] = temp[i]

奇数索引上规律是从大到小,偶数索引上规律也是从大到小,再次发现先对奇数索引进行填充,再对偶数索引填充

数学类

幂类

获取2进制

如何获取一个数的2进制表示

1
2
3
4
5
6
7
8
n = 10
res = []
if n < 0:
n = n + pow(2,32)
while n > 0:
n, m = divmod(n, 2)
res.append(m)
print("".join([str(i) for i in res[::-1]))

小数的二进制结果如下:
题目在这里 https://leetcode-cn.com/problems/bianry-number-to-string-lcci/

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def printBin(self, num: float) -> str:
res = []
for i in range(1,50):
if num - 1/(2**i) >= 0:
num = num - 1/(2**i)
res.append(1)
else:
res.append(0)
index2 = len(res) - res[::-1].index(1)
if index2>=32:
return "ERROR"
return '0.' + "".join([str(x) for x in res[:index2]])

获取16进制

题目见 https://leetcode-cn.com/problems/convert-a-number-to-hexadecimal/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def toHex(self, num: int) -> str:
num_lst = list("0123456789abcdef")
res = []
if num < 0:
num = num + 2**32
while num > 0:
num, n = divmod(num, 16)
res.append(n)
res2 = [num_lst[i] for i in res[::-1]]
return "".join(res2)

计算pow

这道题有两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# v1 使用递归
class Solution:
def myPow(self, x: float, n: int) -> float:
def quickMul(N):
if N == 0:
return 1.0
y = quickMul(N // 2)
return y * y if N % 2 == 0 else y * y * x

return quickMul(n) if n >= 0 else 1.0 / quickMul(-n)


# v2 使用快读幂指数算法
class Solution:
def myPow(self, x: float, n: int) -> float:
if x == 0.0: return 0.0
res = 1
if n < 0: x, n = 1 / x, -n
while n:
if n & 1: res *= x
x *= x
n >>= 1
return res

数值的整数次方

题目见 https://leetcode-cn.com/problems/shu-zhi-de-zheng-shu-ci-fang-lcof/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def myPow(self, x: float, n: int) -> float:
res = 1
flag = 0
if n < 0:
n = -n
flag = 1
while n:
if n&1:
res *= x
n >>= 1
x = x * x
return res if not flag else 1/res

计算sqrt

题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 通过log
class Solution:
def mySqrt(self, x: int) -> int:
if x == 0:
return 0
ans = int(math.exp(0.5 * math.log(x)))
return ans + 1 if (ans + 1) ** 2 <= x else ans
# 通过二分
class Solution:
def mySqrt(self, x: int) -> int:
l, r, ans = 0, x, -1
while l <= r:
mid = (l + r) // 2
if mid * mid <= x:
ans = mid
l = mid + 1
else:
r = mid - 1
return ans
# 通过牛顿
class Solution:
def mySqrt(self, x: int) -> int:
if x == 0:
return 0

C, x0 = float(x), float(x)
while True:
xi = 0.5 * (x0 + C / x0)
if abs(x0 - xi) < 1e-7:
break
x0 = xi

return int(x0)


计算是否是2的幂

题目见 https://leetcode-cn.com/problems/power-of-two/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 普通方法
class Solution:
def isPowerOfTwo(self, n: int) -> bool:
if n==0:
return False
num = n
while num!=1:
ys = num%2
if ys==1:
return False
num = num//2
return True
# 技巧
class Solution:
def isPowerOfTwo(self, n: int) -> bool:
return n > 0 and (n & (n - 1)) == 0
# 框架
class Solution:
def isPowerOfTwo(self, n: int) -> bool:
while n and n % 2 == 0:
n //= 2
return n == 1

重新排序得到 2 的幂

题目见 https://leetcode-cn.com/problems/reordered-power-of-2/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# TL了
class Solution:
def reorderedPowerOf2(self, n: int) -> bool:
if n==1:
return True
res = []
def back(state, s):
if len(s)==0:
res.append(state[:])
else:
for i in range(len(s)):
state.append(s[i])
back(state, s[0:i]+s[i+1:])
state.pop()
back([],str(n))

res2 = []
for i in res:
if not int(i[-1])%2:
res2.append(int("".join(i)))

def isPow(n):
return n > 0 and (n & (n - 1)) == 0

for i in res2:
if isPow(i):
return True
return False

# 首先算好每个结果,然后去查一下是不是在其中
class Solution:
def reorderedPowerOf2(self, n: int) -> bool:
def check(targ: int, num: int) -> bool:
a = [0 for _ in range(10)]
b = [0 for _ in range(10)]
while targ:
x = targ % 10
a[x] += 1
targ //= 10
while num:
x = num % 10
b[x] += 1
num //= 10
return a == b


for i in range(31):
targ = 2 ** i
if check(targ, n) == True:
return True
return False

计算是否是3的幂

题目见 https://leetcode-cn.com/problems/power-of-three/ 题解如下:

1
2
3
4
5
class Solution:
def isPowerOfThree(self, n: int) -> bool:
while n and n % 3 == 0:
n //= 3
return n == 1

计算是否是4的幂

题目见 https://leetcode-cn.com/problems/power-of-four/,题解如下:

1
2
3
4
5
class Solution:
def isPowerOfFour(self, n: int) -> bool:
while n and n % 4 == 0:
n //= 4
return n == 1

幂集

题目在 https://leetcode-cn.com/problems/power-set-lcci/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 错误
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
def back(state,s):
res.append(state[:])
for i in range(len(s)):
state.append(s[i])
back(state, s[i:])
state.pop()
back([],nums)
return res
# 正确
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
def back(state,s):
res.append(state[:])
for i in range(len(s)):
state.append(s[i])
back(state, s[i+1:])
state.pop()
back([],nums)
return res

判断一个数字是否可以表示成三的幂的和

题目见 https://leetcode-cn.com/problems/check-if-number-is-a-sum-of-powers-of-three/ 题解如下:

1
2
3
4
5
6
7
8
class Solution:
def checkPowersOfThree(self, n: int) -> bool:
for i in range(31,-1,-1):
if n - 3**i >= 0:
n = n - 3**i
if n==0:
return True
return False

大餐计数

题目见 https://leetcode-cn.com/problems/count-good-meals/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 时间不够
class Solution:
def countPairs(self, deliciousness: List[int]) -> int:
nums = deliciousness

def ispow(x):
return x>0 and x & (x-1)==0

cnt = 0
for i in range(len(nums)):
for j in range(i+1, len(nums)):
v = nums[i] + nums[j]
if ispow(v):
cnt += 1
return cnt
# 错误
class Solution:
def countPairs(self, deliciousness: List[int]) -> int:
cnt = Counter(deliciousness)
res = 0
print(cnt.keys())
for key in cnt.keys():
for i in range(32):
if key == 2 **i:
res = res + cnt[key] * (cnt[key] - 1)
else:
res = res + cnt[key] * (cnt[2 ** i - key])
return res//2
# 好
class Solution:
def countPairs(self, deliciousness: List[int]) -> int:
cnt = Counter(deliciousness)
res = 0
print(cnt.keys())
for key in cnt.keys():
for i in range(32):
if key == 2 ** (i-1): # 改动
res = res + cnt[key] * (cnt[key] - 1)
else:
res = res + cnt[key] * (cnt[2 ** i - key])
return res//2

注意这里需要考虑 pow(2,i) 和key的关系,避免1+1=2这种,或者你在else里面加上 pow(2,i)-key!=key的判断才可以的.

除法类

快速求余

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def remainder(x, a, p):
rem = 1
for _ in range(a):
rem = (rem * x) % p
return rem

# 求 (x^a) % p —— 快速幂求余
def remainder(x, a, p):
rem = 1
while a > 0:
if a % 2: rem = (rem * x) % p
x = x ** 2 % p
a //= 2
return rem

参考 https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof/solution/mian-shi-ti-14-ii-jian-sheng-zi-iitan-xin-er-fen-f/

两数相除

题目见 https://leetcode-cn.com/problems/divide-two-integers/, 这里我做了很多次代码,每次都是在边界条件那里出了问题,看下详细的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 提交次数1
class Solution:
def divide(self, dividend: int, divisor: int) -> int:
flag = 1 if dividend * divisor > 0 else -1
dividend = abs(dividend)
divisor = abs(divisor)

if divisor == 0:
return 0
if divisor == 1:
return dividend
i = 1
while divisor * i < dividend:
i *= 2

l = i // 2
r = i

while r - l > 1:
mid = (r + l) / 2
if mid * divisor > dividend:
r = mid
else:
l = mid
return int(l * flag)

错误原因,对于1 -1这样的输入是会有问题的,接下来第二版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 提交次数2
class Solution:
def divide(self, dividend: int, divisor: int) -> int:
flag = 1 if dividend * divisor > 0 else -1
dividend = abs(dividend)
divisor = abs(divisor)
if divisor == 0:
return 0
if divisor == 1:
return dividend*flag
if divisor==dividend:
return 1*flag

i = 1
while divisor * i < dividend:
i *= 2

l = i // 2
r = i

while r - l > 1:
mid = (r + l) / 2
if mid * divisor > dividend:
r = mid
else:
l = mid
return int(l * flag)

错误的原因在于没有考虑到2^31这种,接下来第三版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution:
def divide(self, dividend: int, divisor: int) -> int:
flag = 1 if dividend * divisor > 0 else -1
dividend = abs(dividend)
divisor = abs(divisor)
if divisor == 0:
c = 0
elif divisor == 1:
c = dividend
elif divisor == dividend:
c = 1
else:
i = 1
while divisor * i < dividend:
i *= 2

l = i // 2
r = i

while r - l > 1:
mid = (r + l) / 2
if mid * divisor > dividend:
r = mid
else:
l = mid
c = l
c = c * flag
if c >= 2147483647:
c = 2147483647
if c <= -1 * math.pow(2, 31):
c = -1 * math.pow(2, 31)
return int(c)

这里改变了之前的逻辑结构,把所有的if改变了。但是还是会标错,原因在于while那里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution:
def divide(self, dividend: int, divisor: int) -> int:
flag = 1 if dividend * divisor > 0 else -1
dividend = abs(dividend)
divisor = abs(divisor)
if divisor == 0:
c = 0
elif divisor == 1:
c = dividend
elif divisor == dividend:
c = 1
else:
i = 1
while divisor * i <= dividend:
i *= 2

l = i // 2
r = i

while r - l > 1:
mid = (r + l) / 2
if mid * divisor > dividend:
r = mid
else:
l = mid
c = l
c = c * flag
if c >= 2147483647:
c = 2147483647
if c <= -1 * math.pow(2, 31):
c = -1 * math.pow(2, 31)
return int(c)

这是可以通过的代码。

高级的结算结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def divide(a, b):
INT_MIN, INT_MAX = -2 ** 31, 2 ** 31 - 1
if a == INT_MIN and b == -1:
return INT_MAX

sign = -1 if (a > 0) ^ (b > 0) else 1

a, b = abs(a), abs(b)
ans = 0
for i in range(31, -1, -1):
if (a >> i) - b >= 0:
a = a - (b << i)
ans += 1 << i

# bug 修复:因为不能使用乘号,所以将乘号换成三目运算符
return ans if sign == 1 else -ans

我模仿写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def divide(self, a: int, b: int) -> int:
INT_MIN, INT_MAX = -2 ** 31, 2 ** 31 - 1
if a == INT_MIN and b == -1:
return INT_MAX
if b == 0 or a == 0:
return 0
flag = 1 if a*b>0 else -1
a,b = abs(a),abs(b)
ans = 0
for i in range(31,-1,-1):
if (a>>i) - b >= 0:
a = a - (b<<i)
ans+= 1<<i
return ans*flag

剑指 Offer II 001. 整数除法

题目见 https://leetcode-cn.com/problems/xoh6Oh/, 和上面是一样的,解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def divide(a, b):
INT_MIN, INT_MAX = -2 ** 31, 2 ** 31 - 1
if a == INT_MIN and b == -1:
return INT_MAX

sign = -1 if (a > 0) ^ (b > 0) else 1

a, b = abs(a), abs(b)
ans = 0
for i in range(31, -1, -1):
if (a >> i) - b >= 0:
a = a - (b << i)
ans += 1 << i

# bug 修复:因为不能使用乘号,所以将乘号换成三目运算符
return ans if sign == 1 else -ans

Excel表列名称

题目见 https://leetcode-cn.com/problems/excel-sheet-column-title/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 错误
class Solution:
def convertToTitle(self, columnNumber: int) -> str:
res = []
while columnNumber > 0:
a0 = (columnNumber-1)%26 + 1
res.append(chr(a0 - 1 + ord("A")))
columnNumber = columnNumber - a0
return "".join(res[::-1] )
# 通过
class Solution:
def convertToTitle(self, columnNumber: int) -> str:
res = []
while columnNumber > 0:
a0 = (columnNumber-1)%26 + 1
res.append(chr(a0 - 1 + ord("A")))
columnNumber = (columnNumber - a0)//26
return "".join(res[::-1] )

这道题的做法有点饶的,还是需要结合题解进一步理解
image

Excel 表列序号

题目见 https://leetcode-cn.com/problems/excel-sheet-column-number/ 题解如下:

1
2
3
4
5
6
7
8
9
class Solution:
def titleToNumber(self, columnTitle: str) -> int:
num = 0
multipy = 1
for i in columnTitle[::-1]:
v = ord(i) - ord('A') + 1
num += v * multipy
multipy *= 26
return num

这道题中的最后的累乘和 数字序列中某一位的数字 这道题有点类似,可以看下

水壶问题(最大公约数)

题目见 https://leetcode-cn.com/problems/water-and-jug-problem/ 解法如下:

1
2
3
4
5
6
7
class Solution:
def canMeasureWater(self, x: int, y: int, z: int) -> bool:
if x + y < z:
return False
if x == 0 or y == 0:
return z == 0 or x + y == z
return z % math.gcd(x, y) == 0

其实就是求最大公约数的问题

完美数

题目见 https://leetcode-cn.com/problems/perfect-number/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 超时
直接循环来做的话,会超时,因此需要熊2到sqrt进行遍历,这样会少了一部分的计算量
# 通过
class Solution:
def checkPerfectNumber(self, num: int) -> bool:
if num==1:
return False
res = [1]
for i in range(2,int(num**0.5)+1):
if num % i == 0:
res.append(i)
res.append(num//i)
if sum(res)==num:
return True
else:
return False

分数到小数

题目见 https://leetcode-cn.com/problems/fraction-to-recurring-decimal/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution:
def fractionToDecimal(self, numerator: int, denominator: int) -> str:
if numerator == 0: return "0"
res = []
# 首先判断结果正负, 异或作用就是 两个数不同 为 True 即 1 ^ 0 = 1 或者 0 ^ 1 = 1
if (numerator > 0) ^ (denominator > 0):
res.append("-")
numerator, denominator = abs(numerator), abs(denominator)
# 判读到底有没有小数
a, b = divmod(numerator, denominator)
res.append(str(a))
# 无小数
if b == 0:
return "".join(res)
res.append(".")
# 处理余数
# 把所有出现过的余数记录下来
loc = {b: len(res)}
while b:
b *= 10
a, b = divmod(b, denominator)
res.append(str(a))
# 余数前面出现过,说明开始循环了,加括号
if b in loc:
res.insert(loc[b], "(")
res.append(")")
break
# 在把该位置的记录下来
loc[b] = len(res)
return "".join(res)

参考如下:https://leetcode-cn.com/problems/fraction-to-recurring-decimal/solution/ji-lu-yu-shu-by-powcai/

其实主要玩的思路是这样的,比如2/3这个数,逻辑如下所示:

1
2
3
4
5
6
x = 2
y = 3
a, b = divmod(x, y)
while b:
b *= 10
a, b = divmod(b, y)

计算器

实现计算器的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
代码如下
pri = {"(": 1, "+": 2, "-": 2, "*": 3, "/": 3}
ops_stack = []
post_stack = []
s = "(1+2)/3-4*2-(3-4)"
for i in s:
if i == ' ':
continue
if i.isdigit():
post_stack.append(i)
elif i == "(":
ops_stack.append(i)
elif i == ")":
while ops_stack[-1] != "(":
post_stack.append(ops_stack.pop())
else:
while ops_stack and pri[ops_stack[-1]] >= pri[i]:
post_stack.append(ops_stack.pop())
ops_stack.append(i)
post_stack.extend([i for i in ops_stack[::-1] if i != "("])

s1 = []
for i in post_stack:
if not i.isdigit():
a = int(s1.pop())
b = int(s1.pop())
s1.append(do_math(i, a, b))
else:
s1.append(i)
sum(s1)

基本计算器

题目见 https://leetcode-cn.com/problems/basic-calculator/, 题解如下:

更一般的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
a = "1+(2+6/1+2)"
# a = "2+9/3-5"
# 可能出现的符号
symbol_1 = ['+', '-', '*', '/']
symbol_2 = ['(']
symbol_3 = [')']
# 符号的优先级
priority = {'#': -1, '(': 1, '+': 2, '-': 2, '*': 3, '/': 3}
match_2 = {')': '('}
# 存储符号的栈
stack = []
stack.append("#")
# 结果
result = []


# 下面通过将中缀表达式转换为后缀表达式,并进行运算
def my_operation(symbol, a, b):
a, b = int(a), int(b)
if symbol == '+':
return a + b
elif symbol == '-':
return a - b
elif symbol == '*':
return a * b
elif symbol == '/':
return a / b


def to_operation(result, stack):
two = result.pop()
one = result.pop()
symbol = stack.pop()
ret = my_operation(symbol, one, two)
print(f"{one}{symbol}{two} = {ret}")
result.append(ret)


### 在表达式转换的时候就一边进行了运算
for i in a:
# 如果是数字直接添加到结果
if i.isdigit():
result.append(i)
# 如果是 + - * / 运算,则先出栈更低优先级的,然后入栈
elif i in symbol_1:
# 如果优先级低,则出栈所有优先级>=的符号
while priority[i] <= priority[stack[-1]]:
to_operation(result, stack)
# 压入符号
stack.append(i)
# 如果是左括号,直接压入
elif i in symbol_2:
stack.append(i)
# 如果是右括号,则出栈,直到遇到了匹配的左括号,然后吧左括号也出栈
elif i in symbol_3:
while stack[-1] != match_2[i]:
to_operation(result, stack)
stack.pop()

to_operation(result, stack)
print(result)

基本计算器 II

题目见 https://leetcode-cn.com/problems/basic-calculator-ii/,题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def calculate(self, s: str) -> int:
stack = []
nums = 0
pre_flag = "+" # 注意
s = s + "$" # 注意

for i in s:
if i.isdigit():
nums = nums*10 + int(i)
elif i==' ':
continue
else:
if pre_flag == '+':
stack.append(nums)
if pre_flag == '-':
stack.append(-nums)
if pre_flag == '*':
stack.append(stack.pop()*nums)
if pre_flag == '/':
stack.append(int(stack.pop()/nums))
pre_flag = i #注意
nums = 0
return sum(stack)

字典序类

数字序列中某一位的数字

题目见 https://leetcode-cn.com/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof/ 和之前的字典序的题目不太一样,和数学是有关系的,主要是要自减。
解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 简洁做法
def findNthDigit(n):
digit, start, count = 1, 1, 9
while n > count: # 1.
n -= count
start *= 10
digit += 1
count = 9 * start * digit
num = start + (n - 1) // digit # 2.获取数字
return int(str(num)[(n - 1) % digit]) # 3. 获取对应的位数
# 我的解法
def findNthDigit(n):
"""
:type n: int
:rtype: int
"""
digit = 1
while n > 0:
start = digit * 9 * (10**(digit-1))
n -= start
digit += 1

last_nums = n + start
digit -= 1
begin = 10 **(digit-1)
word = begin + last_nums//digit - 1
index = last_nums % digit
if index==0:
return int(str(word)[-1])
else:
return int(str(word+1)[index-1])

加法类

二进制求和

题解见 https://leetcode-cn.com/problems/add-binary/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def addBinary(self, a: str, b: str) -> str:
n1 = len(a)
n2 = len(b)
if n1 < n2:
a = '0'*(n2-n1)+a
if n2 < n1:
b = '0'*(n1-n2)+b

res = ''
div = 0
for i in range(len(a)-1,-1,-1):
h = int(a[i]) + int(b[i]) + div
div, remain = divmod(h + div, 2)
res+=str(remain)
if div:
res+=str(div) # 注意
return res[::-1]

字符串相加

题目见 https://leetcode-cn.com/problems/add-strings/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def addStrings(self, num1: str, num2: str) -> str:
n1 = len(num1)
n2 = len(num2)
if n1 < n2:
num1 = '0'*(n2-n1)+num1
if n2 < n1:
num2 = '0'*(n1-n2)+num2

res = ''
div = 0
for i in range(len(num1)-1,-1,-1):
h = int(num1[i])+int(num2[i])+div
div, remind = divmod(h, 10)
res+=str(zhi)
if div:
res+=str(div) # 注意
return res[::-1]

加一

题目见 https://leetcode-cn.com/problems/plus-one/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 错误
class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
res = []
y = 0
y = 0
digits2 = digits[::-1]
for i in range(len(digits2)):
if i == 0:
v = (digits2[i] + y + 1) % 10
else:
v = (digits2[i] + y) % 10
y = (digits2[i] + 1) // 10
res.append(v)
if y > 0:
res.append(y)
return res[::-1]
#正确
class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
res = []
y = 0
y = 0
digits2 = digits[::-1]
for i in range(len(digits2)):
if i == 0:
v = (digits2[i] + y + 1) % 10
y = (digits2[i] + y + 1) // 10
else:
v = (digits2[i] + y) % 10
y = (digits2[i] + y) // 10
res.append(v)
if y > 0:
res.append(y)
return res[::-1]

乘法类

递归乘法

题目见 https://leetcode-cn.com/problems/recursive-mulitply-lcci/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# v1
class Solution:
def multiply(self, A: int, B: int) -> int:
return A*B
# v2
class Solution:
def multiply(self, A: int, B: int) -> int:
res = 0
while B:
if B&1: res = res + A
A = A + A
B>>=1
return res
# v3
class Solution:
def multiply(self, A: int, B: int) -> int:
def dfs(A,B):
if B==1:
return A
s = dfs(A,B//2)
return s+s if B%2==0 else s+s+A
return dfs(A,B)

位运算

不用加减乘除做加法

题目见 https://leetcode-cn.com/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/ 题解如下:

1
2
3
4
5
6
7
class Solution:
def add(self, a: int, b: int) -> int:
x = 0xffffffff
a,b = a&x,b&x
while b!=0:
a,b = a^b, (a&b)<<1&x
return a if a <= 0x7fffffff else ~(a ^ x)

消失的数字

题目见 https://leetcode-cn.com/problems/missing-number-lcci/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def missingNumber(self, nums: List[int]) -> int:
n = len(nums)
res = 0
for i in range(n):
res = res ^ i ^ nums[i]
res ^= n
return res
#
class Solution:
def missingNumber(self, nums: List[int]) -> int:
r = 0
for i in nums:
r ^= i
for i in range(len(nums)+1):
r ^= i
return r

数组中数字出现的次数

题目见 https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def singleNumbers(self, nums: List[int]) -> List[int]:
res = 0
for i in nums:
res ^= i
m = 1
while res & m==0:
m <<=1
x = 0
y = 0
for i in nums:
if i & m:
x^=i
else:
y^=i
return [x,y]

单数字操作

最大交换

题目见 https://leetcode-cn.com/problems/maximum-swap/, 题解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from collections import Counter


class Solution:
def maximumSwap(self, num: int) -> int:
if not num:
return 0

# 倒序之后的数字列表
sorted_num = sorted(list(str(num)), reverse=True)
# 原数字列表
num_list = list(str(num))
if sorted_num == num_list:
# 本来就是降序的,直接返回
return num

index = 0
change_num = -1
# 一一对比原列表和排序列表
while index < len(num_list):
if num_list[index] == sorted_num[index]:
# 如果相同位置的数字相同,继续
index += 1
continue
# 找到不同的数字了,此时index就是需要交换的左边索引
# change_num就是需要交换的数字
change_num = sorted_num[index]
break

# 需要将最右边的大数跟最左边的小数交换
num_list.reverse()
# 找到最右边的大数索引
change_index = len(num_list) - 1 - num_list.index(change_num)
num_list.reverse()

# 交换
num_list[index], num_list[change_index] = num_list[change_index], \
num_list[index]
return int(''.join(num_list))

下一个排列

下一个更大元素 I

下一个更大元素 II

下一个更大元素 III

其他

剪绳子

题目见 https://leetcode-cn.com/problems/jian-sheng-zi-lcof/ 题解如下:

1
2
3
4
5
6
7
8
9
class Solution:
def cuttingRope(self, n: int) -> int:
if n < 4:
return n - 1
res = 1
while n > 4:
res *=3
n -= 3
return res * n

分糖果 II

题目见 https://leetcode-cn.com/problems/distribute-candies-to-people/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 复杂点的做法
class Solution:
def distributeCandies(self, candies: int, num_people: int) -> List[int]:
res = [0] * num_people
start = 1
index = 0
while candies - start> 0:
candies -= start
res[index] = res[index] + start
start += 1
index = (index + 1)%num_people
if candies > 0:
res[index] = res[index] + candies
return res
# 或者
class Solution:
def distributeCandies(self, candies: int, num_people: int) -> List[int]:
res = [0] * num_people
start = 1
index = 0
while candies - start> 0:
candies -= start
res[index%num_people] = res[index%num_people] + start
start += 1
index = index + 1
if candies > 0:
res[index%num_people] = res[index%num_people] + candies
return res
# 简洁做法
class Solution:
def distributeCandies(self, candies: int, num_people: int) -> List[int]:
res = [0]*num_people
i = 0
while candies!=0:
res[i%num_people] += min(i+1, candies)
candies = candies - min(i+1, candies) # 保证不为负数
i+=1
return res

排列序列

题目见 https://leetcode-cn.com/problems/permutation-sequence/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def getPermutation(self, n: int, k: int) -> str:
import math
toeken = [str(i) for i in range(1,n+1)]
k -= 1
res = ""
while n > 0:
n -= 1
a, k = divmod(k,math.factorial(n))
res += toeken.pop(a)
return res

三个数的最大乘积

题目见 https://leetcode-cn.com/problems/maximum-product-of-three-numbers/ 题解如下:

1
2
3
4
5
6
class Solution:
def maximumProduct(self, nums: List[int]) -> int:
nums.sort()
a = nums[-1]*nums[-2]*nums[-3]
b = nums[-1]*nums[0]*nums[1]
return max(a,b)

其实很简单的,只是我们在写的时候弄的复杂了,主要判断最后一个数字和倒数两个以及前两个的大小。

最小操作次数使数组元素相等

题目见 https://leetcode-cn.com/problems/minimum-moves-to-equal-array-elements/ 题解如下:

1
2
3
4
5
6
7
class Solution:
def minMoves(self, nums: List[int]) -> int:
min_num = min(nums)
res = 0
for num in nums:
res += num - min_num
return res

换酒问题

题目见 https://leetcode-cn.com/problems/water-bottles/ 题解如下:

1
2
3
4
5
6
7
8
class Solution:
def numWaterBottles(self, numBottles: int, numExchange: int) -> int:
res = numBottles
while numBottles >= numExchange:
bear, numBottles = divmod(numBottles, numExchange)
res += bear
numBottles = numBottles + bear
return res

注意上面的while的条件哈

矩形重叠

题目见 https://leetcode-cn.com/problems/rectangle-overlap/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 错误
class Solution:
def isRectangleOverlap(self, rec1: List[int], rec2: List[int]) -> bool:
x1, y1 = rec2[0], rec2[1]
x3, y3 = rec2[2], rec2[3]
x2, y2 = x1, y3
x4, y4 = x3, y1

x, y = rec1[0], rec1[1]
x5, y5 = rec1[2], rec1[3]

for k,f in [(x1,y1),(x2,y2),(x3,y3),(x4,y4)]:
if k>x and k<x5 and f>y and f<y5:
return True
return False
# 可以
class Solution:
def isRectangleOverlap(self, rec1: List[int], rec2: List[int]) -> bool:
x_overlap = not(rec1[2]<=rec2[0] or rec2[2]<=rec1[0])
y_overlap = not(rec1[3]<=rec2[1] or rec2[3]<=rec1[1])
return x_overlap and y_overlap

矩阵面积

题目见

单调递增的数字

题目见 https://leetcode-cn.com/problems/monotone-increasing-digits/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 自己写的
class Solution:
def monotoneIncreasingDigits(self, n: int) -> int:
def get_p_n(n):
res = []
while n > 0:
n, m = divmod(n, 10)
res.append(m)
return res[::-1]

def get_index(res):
for i in range(len(res)-1):
if res[i+1] < res[i]:
return i
else:
return -1

def dfs(res):
idx = get_index(res)
if idx==-1:
return res
else:
font = res[0:idx+1]
font[-1] = font[-1]-1 if font[-1]-1>=0 else 9
return dfs(font) + [9]*(len(res)-idx-1)

res = get_p_n(n)
res2 = dfs(res)
res3 = 0
for i in res2:
res3 = res3*10 + i
return res3
# 贪心做法
lass Solution:
def monotoneIncreasingDigits(self, N):
n = str(N)
num = []
for i in n:
num.append(i)

nine = len(num)
for i in range(len(num) - 1, 0, -1): # 注意之类的操作
if num[i - 1] > num[i]:
num[i - 1] = str(int(num[i - 1]) - 1)
nine = i

num = ''.join(num)
num = int(num[:nine] + '9' * (len(num) - nine))
return num

注意上面的操作,如果换成下面的代码会出错的,因此塔一边从后往前,一边会修改值。

1
2
3
4
for i in range(len(num) - 1):
if num[i + 1] > num[i]:
num[i] = str(int(num[i]) - 1)
nine = i

1~n 整数中 1 出现的次数

题目见 https://leetcode-cn.com/problems/1nzheng-shu-zhong-1chu-xian-de-ci-shu-lcof/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def countDigitOne(self, n: int) -> int:
a, b, one_count = 1, 10, 0
while n >= a:
x, y = divmod(n, b)

if y >= a * 2:
one_count += (x + 1) * a
elif y >= a:
one_count += y + 1 + (x - 1) * a
else:
one_count += x * a

a, b = b, b*10

return one_count

三角形的最大周长

题目见 https://leetcode-cn.com/problems/largest-perimeter-triangle/, 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 超时
class Solution:
def largestPerimeter(self, nums: List[int]) -> int:
max_triangle_length = 0
nums.sort(reverse=True)
for i in range(len(nums)):
for j in range(i+1, len(nums)):
p = j + 1
while p < len(nums):
if abs(nums[i] - nums[j]) < nums[p]:
return nums[i]+nums[j]+nums[p]
return 0
# 通过
class Solution:
def largestPerimeter(self, nums: List[int]) -> int:
nums.sort()
for i in range(len(nums)-1,1,-1):
for j in range(i-1,0,-1):
diff = abs(nums[i] - nums[j])
index = bisect.bisect_left(nums, diff)
if index < j and nums[j-1]>diff: # 注意条件哈
return nums[i]+nums[j]+nums[j-1]
else:
break
return 0

计算各个位数不同的数字个数

题目见 https://leetcode-cn.com/problems/count-numbers-with-unique-digits/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def countNumbersWithUniqueDigits(self, n: int) -> int:
def pending_sum(x):
res = 1
j = 10
for i in range(x):
res = res * min(9, j)
j = j - 1
return res
if n==0:
return 1
if n==1:
return 10

dp = [0] * (n+1)
dp[0] = 1
dp[1] = 10
for i in range(2,n+1):
dp[i] = dp[i-1] + pending_sum(i)
return dp[n]

圆圈中最后剩下的数字

题目见 https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/ 题解如下:

1
2
3
4
5
6
7
class Solution:
def lastRemaining(self, n: int, m: int) -> int:
x = 1
for i in range(2, n+1): # 注意起始不是1
x = (x+m)%i
return x
# https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/huan-ge-jiao-du-ju-li-jie-jue-yue-se-fu-huan-by-as/

image

类似的题目还有 找出游戏的获胜者,题目在https://leetcode-cn.com/problems/find-the-winner-of-the-circular-game/, 但是注意这个不是从0开始了,是从1开始的,因此解法会有些不一样

1
2
3
4
5
6
class Solution:
def findTheWinner(self, n: int, k: int) -> int:
x = 0
for i in range(2, n + 1):
x = (x + k) % i
return x + 1 # 不一样

消除游戏

题目见 https://leetcode-cn.com/problems/elimination-game/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 方法一:迭代做法
class Solution:
def lastRemaining(self, n: int) -> int:
head = 1
step = 1
left = True

while n > 1:
# 从左边开始移除 or(从右边开始移除,数列总数为奇数)
if left or n % 2 != 0:
head += step

step <<= 1 # 步长 * 2
n >>= 1 # 总数 / 2
left = not left #取反移除方向

return head
# 方法二:递归做法
class Solution:
def lastRemaining(self, n: int) -> int:
def dfs(n,direction):
if n==1:
return 1
else:
if direction:
return 2*dfs(n//2, not direction)
else:
if n&1:
return 2*dfs(n//2, not direction)
else:
return 2*dfs(n//2,not direction) - 1
return dfs(n, True)

用递归来做的话,是比较快的

最大数值

题目见 https://leetcode-cn.com/problems/maximum-lcci/ 题解如下:

1
2
3
class Solution:
def maximum(self, a: int, b: int) -> int:
return (a+b+abs(a-b))//2

求解方程

题目见 https://leetcode-cn.com/problems/solve-the-equation/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution:
def solveEquation(self, equation: str) -> str:
low, high = equation.replace("-", "+-").split("=")
a, b = 0, 0
# 处理左边
for i in low.split("+"):
if i=='':continue
if i.endswith('x'):
if i[:-1] == '':
a = a + 1
elif i[:-1] == '-':
a = a - 1
else:
a = a + int(i[:-1])
else:
b = b + int(i)
# 处理右边
for i in high.split("+"):
if i=='':continue
if i.endswith('x'):
if i[:-1] == '':
a = a - 1
elif i[:-1] == '-':
a = a + 1
else:
a = a - int(i[:-1])
else:
b = b - int(i)
if a == 0:
return 'Infinite solutions' if b == 0 else 'No solution'
else:
return 'x=%d' % (-b / a)

质数和因数

计算质数

题目见 https://leetcode-cn.com/problems/count-primes/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def count_primes_py(n):
"""
求n以内的所有质数个数(纯python代码)
"""
# 最小的质数是 2
if n < 2:
return 0

isPrime = [1] * n
isPrime[0] = isPrime[1] = 0 # 0和1不是质数,先排除掉

# 埃式筛,把不大于根号 n 的所有质数的倍数剔除
for i in range(2, int(n ** 0.5) + 1):
if isPrime[i]:
isPrime[i * i:n:i] = [0] * ((n - 1 - i * i) // i + 1)

return sum(isPrime)

注意上面的快速间隔的选中数值,也就是[1:100:3]每隔3个数

丑数

就是查看是否是2 3 5的乘积

1
2
3
4
5
6
7
8
class Solution:
def isUgly(self, n: int) -> bool:
if n==0:
return False
for i in [2,3,5]:
while n %i == 0:
n = n // i
return n==1

丑数 II

题目见 https://leetcode-cn.com/problems/ugly-number-ii/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def nthUglyNumber(self, n: int) -> int:
import heapq
seen = set()
res = [1]
for i in range(n-1):
v = heapq.heappop(res)
for j in [2,3,5]:
if v*j not in seen:
seen.add(v*j)
heapq.heappush(res,v*j)
return res[0]

超级丑数

题目见 https://leetcode-cn.com/problems/super-ugly-number/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def nthSuperUglyNumber(self, n: int, primes: List[int]) -> int:
dp = [0] * (n + 1)
m = len(primes)
pointers = [0] * m
nums = [1] * m

for i in range(1, n + 1):
min_num = min(nums)
dp[i] = min_num
for j in range(m):
if nums[j] == min_num:
pointers[j] += 1 # 把之前最小的dp的位置的存下来,后面* 然后得到新的数值
nums[j] = dp[pointers[j]] * primes[j]
return dp[n]

好子集的数目

题目见 https://leetcode-cn.com/problems/the-number-of-good-subsets/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution:
def numberOfGoodSubsets(self, nums):
num_map = collections.Counter(nums)
res = collections.defaultdict(int)
primes = set(map(lambda x: x*x,[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]))
# 初始化 res[1] 的个数,但是后续结果需要减去
res[1] = 2**num_map[1]

for num in num_map:
# 去除1
if num == 1:
continue

for key in res.copy():
# 暴力循环 构建数字
good_num = key * num

# 判断是否为好子集
if not all( good_num % p for p in primes):
continue

# 计算good_num的好子集的个数,初始 res[good_num] = 0
res[good_num] += res[key] * num_map[num]

return (sum(res.values()) - res[1]) % (10 ** 9 + 7)

只有两个键的键盘

题目见 https://leetcode-cn.com/problems/2-keys-keyboard/ 这道题就是分解质因素的思路,一毛一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 递归
class Solution:
def __init__(self):
self.cnt = float("inf")
def minSteps(self, n: int) -> int:
def dfs(n,c,s,t):
if s>=n:
if s==n:
self.cnt = min(self.cnt, t)
return
dfs(n, c, c+s, t+1)
dfs(n, s, s+s, t+2)
if n==1:
return 0
dfs(n,1,1,1)
return self.cnt
# 分解
class Solution:
def minSteps(self, n: int) -> int:
if n==1: # 注意
return 0
res = 0
for i in range(2,n):
while n%i ==0:
res+=i
n//=i
return res if res else n # 如果为0说明为质数,直接返回n即可
# 动态
class Solution:
def minSteps(self, n: int) -> int:
dp = [0] * (n+1)
dp[0] = 0
for i in range(2,n+1):
j = 1
dp[i] = float("inf")
while j * j <= n:
if i%j==0:
dp[i] = min(dp[i], dp[i//j]+j)
dp[i] = min(dp[i], dp[j]+i//j)
j = j + 1
return dp[n]

数组类

翻转类

轮转数组

题目见 https://leetcode-cn.com/problems/rotate-array/ ,题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 方法1
使用了额外的空间
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
yushu = k % n
start = n - yushu
print(nums[start:])
print(nums[:start])
v = nums[start:] + nums[:start]
nums[:] = v[:]
# 方法2
直接进行翻转即可
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
def reverse(nums,start,end):
while start < end:
nums[start], nums[end] = nums[end], nums[start]
start+=1
end-=1
yushu = k % len(nums)
reverse(nums, 0, len(nums)-1)
reverse(nums, 0, yushu-1)
reverse(nums,yushu,len(nums)-1)

原地哈希

缺失的第一个正数

考点:交换
因为需要找到值,而不是索引,所以需要交换,不然会丢失值。

建议先看下面的题目,从 数组中重复的数据开始看起,这样会加速理解,这道题其实按照我们在 找到所有数组中消失的数字 中的结论来看,不太好做的,主要是没有对数组中的值有范围,比如对于[7,8,-1,10]这种,数组的长度是4,但是里面的值都大于的,因此没法直接做value作为index的这样映射,这么一想其实很难做的。
不过,如果找不到索引的话,我们就不去动不就可以了吗,对于7这个值,对应的索引的地址是6,找不到6的地址的话,我们就不交换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from typing import List
class Solution:
# 3 应该放在索引为 2 的地方
# 4 应该放在索引为 3 的地方
def firstMissingPositive(self, nums: List[int]) -> int:
size = len(nums)
for i in range(size):
# 先判断这个数字是不是索引,然后判断这个数字是不是放在了正确的地方
while 1 <= nums[i] <= size and nums[i] != nums[nums[i] - 1]:
self.__swap(nums, i, nums[i] - 1)
for i in range(size):
if i + 1 != nums[i]:
return i + 1
return size + 1

def __swap(self, nums, index1, index2):
nums[index1], nums[index2] = nums[index2], nums[index1]

这里需要注意的是,程序的中的while不能换成if,不然会报错的。上面的代码可以换成如下所示:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
size = len(nums)
for i in range(size):
# 先判断这个数字是不是索引,然后判断这个数字是不是放在了正确的地方
while 1 <= nums[i] <= size and i != nums[i] - 1:
nums[nums[i]-1],nums[i] = nums[i], nums[nums[i]-1]
for i in range(size):
if i + 1 != nums[i]:
return i + 1
return size + 1

会报错的,不可以写成i!=nums[i]-1。还有一点
nums[nums[i]-1],nums[i] = nums[i], nums[nums[i]-1]不能写成nums[i], nums[nums[i]-1]=nums[nums[i]-1],nums[i]这种格式。
如果不这么写的话,就得像上面的答案一样,定义一个swap函数,这样就可以了。

最后真确的如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
size = len(nums)
for i in range(size):
# 先判断这个数字是不是索引,然后判断这个数字是不是放在了正确的地方
while 1 <= nums[i] <= size and nums[i] != nums[nums[i] - 1]:
nums[nums[i]-1],nums[i] = nums[i], nums[nums[i]-1]
for i in range(size):
if i + 1 != nums[i]:
return i + 1
return size + 1

剑指 Offer 03. 数组中重复的数字

考点:交换
因为需要找到旧数组值,而不是索引,所以需要交换,不然会丢失值。

题目见 https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/, 题解如下

1
2
3
4
5
6
7
8
9
10
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
i = 0
while i < len(nums):
if nums[i] == i:
i = i + 1
continue
if nums[nums[i]] == nums[i]:
return nums[i]
nums[nums[i]], nums[i] = nums[i], nums[nums[i]]

这个解法不是很好理解,且不是很好的能够和上面的进行对应起来,下面的代码就可以和上面对应起来,建议用下面的代码,这样统一下,会好点便于后面进行快速写出来

1
2
3
4
5
6
7
8
9
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
size = len(nums)
for i in range(size):
while 0<=nums[i]<=size-1 and nums[i]!=nums[nums[i]]:
nums[nums[i]],nums[i] = nums[i],nums[nums[i]]
for i in range(size):
if i!=nums[i]:
return nums[i]

还是要注意的是代码中的

1
2
3
4
5
6
7
for i in range(size):
while 0<=nums[i]<=size-1 and nums[i]!=nums[nums[i]]:
nums[nums[i]],nums[i] = nums[i],nums[nums[i]]
#不能写成如下的形式
for i in range(size):
while 0<=nums[i]<=size-1 and nums[i]!=nums[nums[i]]:
nums[i],nums[nums[i]] = nums[nums[i]],nums[i]

如果要写的话也是可以的,只要在外面定义一个swap函数,就可以了。

丢失的数字

这道题可以使用^来做,解法如下:

1
2
3
4
5
6
7
8
class Solution:
def missingNumber(self, nums: List[int]) -> int:
r = 0
for i in nums:
r ^= i
for i in range(len(nums)+1):
r ^= i
return r

使用交换来做得话,结果如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def missingNumber(self, nums: List[int]) -> int:
size = len(nums)
for i in range(size):
while nums[i] <= size - 1 and nums[i]!=nums[nums[i]]:
nums[nums[i]], nums[i] = nums[i], nums[nums[i]]

for i in range(size):
if i != nums[i]:
return i
return size

数组中重复的数据

考点:不是交换,而是直接赋值
因为需要找到数组中的值,其实也就是新数组的索引,直接在新数组上得到的索引就是原来的值。

题目见 https://leetcode-cn.com/problems/find-all-duplicates-in-an-array/, 题解如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
res = []
n = len(nums)
for i in nums:
index = (i - 1)%n
nums[index] += n
for i in range(len(nums)):
if nums[i] > 2*n:
res.append(i+1)
return res

其实就是将这个数组中的值,作为新的list的index, 这个数组的值是从1开始的,但是index的值是从0开始的,因此需要-1才可以。
如果题目改成至少出现3次,那就要将2改成3。

找到所有数组中消失的数字

考点:不是交换,而是直接赋值
理由和上面类似。

题目见 https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/, 这道题和上面的 数组中重复的元素基本上算是一样的,思路还是那个思路,就是把当前数组的值作为新数组的index,这里的做法和上面的类似,首先得到每个数组中的值,然后作为index,然后+n,那么那些缺失的值,如果以他们作为索引的话,对应的地方的值肯定是 < n的。这样就算出来的。

1
2
3
4
5
6
7
8
9
10
11
12
# 第一版-错的
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
n = len(nums)
for i in range(n):
index = nums[i] - 1
nums[index] += n
res = []
for i in range(n):
if nums[i] < n:
res.append(i)
return res

错误原因在于

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
n = len(nums)
for i in range(n):
index = (nums[i] - 1)%n # 改动1
nums[index] += n
res = []
for i in range(n):
if nums[i] <= n: # 改动2
res.append(i+1) # 改动3
return res
1
2
3
4
5
6
7
8
9
10
11
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
n = len(nums)
for i in range(n):
index = (nums[i] - 1)%n
nums[index] += n
res = []
for i in range(n):
if nums[i] <= n:
res.append(i+1)
return res

为什么要用%这个,主要是为了防止index超出了数组的大小,这类题目中数字额值都会在数组的长度的范围,[4,3,2,7,8,2,3,1]这个数组,其中的值都是小于数组长度8的,不这么做的话,没法算啊,不然index也会超的。

重复数

寻找重复数

数组中重复的数字

下一个排列

题目见 https://leetcode-cn.com/problems/next-permutation/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
i = len(nums) -2
while i>=0 and nums[i]>=nums[i+1]:
i = i - 1
if i >= 0:
j = len(nums) - 1
while j>=0 and nums[i] >= nums[j]:
j = j -1
nums[i], nums[j] = nums[j], nums[i]
left = i + 1
right = len(nums) - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left = left + 1
right = right - 1

二分和查找类

基础思路

在二分查找中,需要注意的是边界的问题,其中很多小的点,很容易出现问题,一般的解法如下所示:注意哈,写的时候,要hihg = len(nums) - 1, 不要忘了写成len(nums)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# 查找具体的数值
# 方案1
def binary_search(nums, target):
low = 0
high = len(nums) - 1
while low <= high: #注意
mid = low + (high - low) // 2 # 注意
# 不要使用else,要使用elif,不然有些情况会报错
if nums[mid] < target:
low = mid + 1 # 注意
elif nums[mid] > target:
high = mid - 1 # 这里
elif nums[mid] == target:
return mid
# 方案2
def binary_search(nums, target):
low = 0
high = len(nums) # 这里不能改成-1,不然有些值查不到
while low < high: #注意
mid = low + (high - low) // 2 # 注意
# 不要使用else,要使用elif,不然有些情况会报错
if nums[mid] < target:
low = mid + 1 # 注意
elif nums[mid] > target:
high = mid # 这里
elif nums[mid] == target:
return mid


# 查找左边界
# 方案1
def left_bound(nums, target):
low = 0
high = len(nums) - 1
ans = -1
while low <= high:
mid = low + (high - low) // 2
if nums[mid] < target:
low = mid + 1
elif nums[mid] > target: # 右边界往里
high = mid - 1
elif nums[mid]==target:
ans = mid
high = mid - 1
return ans

# 方案2
def left_bound(nums, target):
low = 0
high = len(nums)
while low < high:
mid = low + (high - low) // 2
if nums[mid] < target:
low = mid + 1
elif nums[mid] > target: # 右边界往里
high = mid
elif nums[mid]==target:
ans = mid
high = mid
return ans


# 查找右边界
# 方案1
def right_bound(nums, target):
low = 0
high = len(nums) - 1
ans = -1
while low <= high:
mid = low + (high - low) // 2
if nums[mid] < target: # 左边界往里
low = mid + 1
elif nums[mid] > target:
high = mid - 1
elif nums[mid] == target:
low = mid + 1
ans = mid
return ans
# 方案2
def right_bound(nums, target):
low = 0
high = len(nums)
ans = -1
while low < high:
mid = low + (high - low) // 2
if nums[mid] < target: # 左边界往里
low = mid + 1
elif nums[mid] > target:
high = mid
elif nums[mid] == target:
low = mid + 1
ans = mid
return ans


nums = [1, 2, 3, 4, 4, 5, 6, 7, 8]

target = 4
print(binary_search(nums, 5))
print(left_bound(nums, target))
print(left_bound2(nums, target))
print(right_bound(nums, target))
print(right_bound2(nums, target))

建议方案1,另外看如下的两个例子,明确下为啥low不能=mid,而需要=mid+1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def search_correct(nums, target):
low, high = 0, len(nums)
while low < high:
mid = low + (high - low) // 2
if nums[mid] < target:
low = mid + 1
else:
high = mid


def search_error(nums, target):
low, high = 0, len(nums)
while low < high:
mid = low + (high - low) // 2
if nums[mid] < target:
low = mid # 这里不更新会导致mid在经过(low+high)取值后,一直停留在mid
else:
high = mid - 1


nums = [1, 3, 4, 5, 6]
target = 8
print(search_correct(nums, target)) # None
print(search_error(nums, target)) # 循环
target = -1
print(search_correct(nums, target)) # None
print(search_error(nums, target)) # None

可以看到不同的参数设置,会导致不同的运行结果。

为什么二分查找中用一些特别的+1或者<=,看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
def binary_search(nums, target):
low = 0
high = len(nums)
while low <= high: #注意
mid = low + (high - low) // 2 # 注意
# 不要使用else,要使用elif,不然有些情况会报错
if nums[mid] < target:
low = mid # 注意
elif nums[mid] > target:
high = mid # 这里
elif nums[mid] == target:
return mid
binary_search(【1,2,3,4,5,6,7,8】, 9)

上面自己调一下就知道了, 从参考的文献来看的话,建议使用左闭右闭的的方式,也就是《=的基础方式的。但是对于一些需要旋转数组这些题,因为不知道要找的数最终是什么,所以一般用low<high, 然后结合low=mid+1, high=mid来 实现。对于一些寻找旋转数组中值的情况,因为是确切找值的,所以的话,一般用while low<=high, 然后结合 low=mid+1, high=mid-1 来实现。

参考如下:
[1] https://blog.csdn.net/qq_38235017/article/details/115177238?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~default-1.pc_relevant_paycolumn_v2&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~default-1.pc_relevant_paycolumn_v2&utm_relevant_index=1
[2] https://www.cnblogs.com/mxj961116/p/11945444.html
[3] https://leetcode.cn/circle/discuss/ooxfo8/ 这里说的很好,说道了上面讲述的为何要<问题,以及循环的问题。

单数组

排序数组中查找元素的第一个和最后一个位置[34]

位于 https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def lower_bound(nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 闭区间 [left, right]
while left <= right: # 区间不为空
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1 # 范围缩小到 [mid+1, right]
else:
right = mid - 1 # 范围缩小到 [left, mid-1]
return left # 或者 right+1

start = lower_bound(nums, target) # 选择其中一种写法即可
if start == len(nums) or nums[start] != target:
return [-1, -1]
end = lower_bound(nums, target + 1) - 1 # 如果 start 存在,那么 end 必定存在
return [start, end]

H 指数[274]

位于 https://leetcode.cn/problems/h-index/description/?envType=study-plan-v2&envId=top-interview-150

1
2
3
4
5
6
7
8
9
10
class Solution:
def hIndex(self, citations: List[int]) -> int:
import bisect
citations.sort()
res = 0
for i in range(1, len(citations)+1):
index = bisect.bisect_left(citations, i)
if len(citations) - index >= i:
res = max(res, i)
return res

山脉数组的峰顶索引

题目见 https://leetcode-cn.com/problems/peak-index-in-a-mountain-array/, 题解入下面的那道题

寻找峰值

题目见 https://leetcode-cn.com/problems/find-peak-element/ ,题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 不知道确切的值是啥,所以用low<high,当然这里也可以用都闭合的方式。
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
nums = [-float("inf")] + nums + [-float("inf")] # 这里加一下,方便操作
low = 0
high = len(nums) - 1 # attention
while low < high:
mid = low + (high - low)//2
if nums[mid+1] >= nums[mid]: # attention
low = mid + 1
else:
high = mid
return low - 1

有序数组中的单一元素[540]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 不知道这个数是啥,用low<high
def singleNonDuplicate(nums):
low = 0
high = len(nums) - 1
while low < high:
mid = (high + low) >> 1
halvesAreEven = (high - mid) % 2 == 0
if nums[mid] == nums[mid + 1]:
if halvesAreEven:
low = low + 2
else:
high = mid - 1 # 注意不是high=mid
elif nums[mid] == nums[mid - 1]:
if halvesAreEven:
high = mid - 2
else:
low = mid + 1
else:
return nums[mid]
return nums[low]

寻找旋转排序数组中的最小值[153]

1
2
3
4
5
6
7
8
9
10
11
#  不知道最小数是啥,用low<high
class Solution:
def findMin(self, nums: List[int]) -> int:
low, high = 0, len(nums) - 1
while low < high:
pivot = low + (high - low) // 2
if nums[pivot] < nums[high]:# 这里high改为len(nums)-1也可以
high = pivot
else:
low = pivot + 1
return nums[low]

寻找旋转排序数组中的最小值 II[154]

1
2
3
4
5
6
7
8
9
10
11
12
13
#  不知道最小数是啥,用low<high
class Solution:
def findMin(self, nums: List[int]) -> int:
while len(nums)>1 and nums[0]==nums[-1]: # 注意
nums.pop()
low, high = 0, len(nums) - 1
while low < high:
pivot = low + (high - low) // 2
if nums[pivot] < nums[high]:
high = pivot
else:
low = pivot + 1
return nums[low]

搜索旋转排序数组[33]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 知道搜的是啥,用low<=high
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
l, r = 0, len(nums) - 1
while l <= r:
mid = (l + r) // 2
if nums[mid] == target:
return mid
if nums[0] <= nums[mid]: # 这里是>= 不是>
if nums[0] <= target < nums[mid]: # 注意:是小于不是小于等于
r = mid - 1
else:
l = mid + 1
else:
if nums[mid] < target <= nums[len(nums) - 1]: # 左边是开,右边是闭
l = mid + 1
else:
r = mid -1
return -1

搜索旋转排序数组II[81]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 知道搜的是啥,用low<=high
class Solution:
def search(self, nums: List[int], target: int) -> bool:
while len(nums)>1 and nums[0]==nums[-1]: # 加这段代码处理一下重复的问题
nums.pop()
low = 0
high = len(nums) - 1
while low <= high:
mid = low + (high -low)//2
if nums[mid]==target:
return True
if nums[mid] >= nums[0]:
if nums[0]<=target<nums[mid]:
high = mid - 1
else:
low = mid + 1
elif nums[mid]<nums[0]:
if nums[mid]<target<=nums[-1]:
low =mid+1
else:
high=mid-1
return False

数组中的k-diff数对[532]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def findPairs(self, nums: List[int], k: int) -> int:
import bisect
from collections import Counter

nums_set = Counter(nums)
if k == 0:
return sum([i>1 for i in nums_set.values()]) # 注意这里

nums.sort()
diff_min, diff_max = nums[0], nums[-1]
cnt = 0

for j in nums_set.keys():
index = bisect.bisect_left(nums, j+k)
if index < len(nums):
if nums[index] == j+k:
cnt += 1

return cnt

注意,对于k=0是需要单独计算的,因此需要分开。

数组中的逆序对[LCR 170]

题目见 https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def reversePairs(self, record: List[int]) -> int:
import bisect
record = record[::-1] # 这步很重要
res = []
s = 0
for i in record:
index = bisect.bisect_left(res, i)
res[index:index] = [i] # bisect.insort(res, i)也可以
s = s + index
return s

通过二分查找排序的方法来做的话,更快。看下面这道题是一样的

翻转对[493]

题目见 https://leetcode-cn.com/problems/reverse-pairs/ 和上面的题目是一样的,题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def reversePairs(self, nums: List[int]) -> int:
import bisect
nums = nums[::-1]
res = []
sums = 0
for i in nums:
index = bisect.bisect_left(res, i) # 查看插入的位置,就是数量
sums = sums + index # 累加
index2 = bisect.bisect_left(res, 2*i) # 实际要对res进行处理,加入2*i这个数
res[index2:index2] = [2*i] # 加入数
return sums

计算右侧小于当前元素的个数[315]

题目见 https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/ 一看题目是没法使用二分查找的,但是只要转换下思路就可以了。
题解如下:

1
2
3
4
5
6
7
8
9
10
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
import bisect
data = []
res = []
for i in nums[::-1]:
index = bisect.bisect_left(data, i)
data[index:index] = [i] #bisect.insort(data, i)也行
res.append(index)
return res[::-1]

马戏团人塔[面试题 17.08]

这道题其实就是求最长上升子序列而已,题目见 https://leetcode-cn.com/problems/circus-tower-lcci/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 解法1
class Solution:
import bisect
def bestSeqAtIndex(self, height: List[int], weight: List[int]) -> int:
dp=[]
for a,b in sorted(zip(height,weight),key = lambda x:[x[0],-x[1]]):
pos = bisect.bisect_left(dp,b)
dp[pos:pos+1] = [b]
return len(dp)
# 解法2
class Solution:
def bestSeqAtIndex(self, height: List[int], weight: List[int]) -> int:
hw = list(zip(height, weight))
hw.sort(key=lambda x:(x[0],-x[1]))
v = [j[1] for j in hw]
stk = []
for x in v:
if stk and x <= stk[-1]:
idx = bisect_left(stk, x)
stk[idx] = x
else:
stk.append(x)
return len(stk)
# 解法3,DP
class Solution:
import bisect
def bestSeqAtIndex(self, height: List[int], weight: List[int]) -> int:
t = sorted(zip(height, weight),key=lambda x:(x[0],-x[1]))
new_height = [x[0] for x in t]
new_weight = [x[1] for x in t]
dp = [1] * len(new_weight)
for i in range(1,len(dp)):
for j in range(i):
if new_weight[i] > new_weight[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)

注意这里是[pos:pos+1]和上面的是不一样的

绝对差值和[1818]

位于 https://leetcode.cn/problems/minimum-absolute-sum-difference/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def minAbsoluteSumDiff(self, nums1: List[int], nums2: List[int]) -> int:
diff = sum(abs(nums1[i] - nums2[i]) for i in range(len(nums1)))
if not diff: return 0
ans = float("inf")
nums1_sort = sorted(nums1)
import bisect
for i, num in enumerate(nums2):
idx = bisect.bisect_left(nums1_sort, num)
if idx == len(nums1):
ans = min(ans, diff - abs(nums1[i] - nums2[i]) + abs(nums1_sort[idx-1] - nums2[i]))
if idx == 0:
ans = min(ans, diff - abs(nums1[i] - nums2[i]) + abs(nums1_sort[idx] - nums2[i]))
if idx<len(nums1) and idx>0:
ans = min(ans, diff - abs(nums1[i] - nums2[i]) + abs(nums1_sort[idx-1] - nums2[i]))
ans = min(ans, diff - abs(nums1[i] - nums2[i]) + abs(nums1_sort[idx] - nums2[i]))
return ans%(10**9+7)

简化为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def minAbsoluteSumDiff(self, nums1: List[int], nums2: List[int]) -> int:
diff = sum(abs(nums1[i] - nums2[i]) for i in range(len(nums1)))
if not diff: return 0
ans = float("inf")
nums1_sort = sorted(nums1)
import bisect
for i, num in enumerate(nums2):
idx = bisect.bisect_left(nums1_sort, num)
if idx:
ans = min(ans, diff - abs(nums1[i] - nums2[i]) + abs(nums1_sort[idx-1] - nums2[i]))
if idx<len(nums1):
ans = min(ans, diff - abs(nums1[i] - nums2[i]) + abs(nums1_sort[idx] - nums2[i]))
return ans%(10**9+7)

最小差[面试题 16.06]

题目见 https://leetcode-cn.com/problems/smallest-difference-lcci/ 这道题和
绝对差值和 一样,需要判断插入点的位置,然后再进行判断。
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 自己解法
class Solution(object):
def smallestDifference(self, a, b):
"""
:type a: List[int]
:type b: List[int]
:rtype: int
"""
import bisect
a.sort()
b.sort()

res = float("inf")
for i in a:
index = bisect.bisect_left(b, i)
if index==len(b):
diff = abs(i-b[index-1])
elif index == 0:
diff = abs(i - b[0])
elif index>0:
diff = min(abs(i-b[index-1]), abs(i-b[index]))
res = min(diff, res)
return res

两球之间的磁力[1552][SKIP]

这道题看起来没啥意思,但是却考差了基本的问题的分析能力,以及对二分的应用的能力,题目见 https://leetcode-cn.com/problems/magnetic-force-between-two-balls/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 错误
class Solution:
def maxDistance(self, position: List[int], m: int) -> int:
position.sort()
low, high = 1, position[-1]- position[0]
ans = -1
def check(mid):
pre = position[0]
cnt = 1
for i in range(1, len(position)):
if position[i] - pre >= mid:
pre = position[i]
cnt = cnt + 1
return cnt >= m
while low < high:
mid = low + (high - low) // 2
if check(mid):
ans = mid
low = mid + 1
else:
high = mid
return ans
# 正确
class Solution:
def maxDistance(self, position: List[int], m: int) -> int:
position.sort()
low, high = 1, position[-1]- position[0]
ans = -1
def check(mid):
pre = position[0]
cnt = 1
for i in range(1, len(position)):
if position[i] - pre >= mid:
pre = position[i]
cnt = cnt + 1
return cnt >= m
while low <= high: #这里
mid = low + (high - low) // 2
if check(mid):
ans = mid
low = mid + 1
else:
high = mid - 1 # 这类
return ans
# 或者将错误的那个地方换一下
class Solution:
def maxDistance(self, position: List[int], m: int) -> int:
position.sort()
low, high = 1, position[-1]- position[0] + 1 # 这里

ans = -1

def check(mid):
pre = position[0]
cnt = 1
for i in range(1, len(position)):
if position[i] - pre >= mid:
pre = position[i]
cnt = cnt + 1
return cnt >= m


while low < high:
mid = low + (high - low) // 2
if check(mid):
ans = mid
low = mid + 1
else:
high = mid
return ans

主要的思路就是一个一个试试,看看哪个间隔比较适合,最大的间隔就是position[-1] -position[0],最小的是0,我们一个一个来试一下就可以了。

单数组前缀和

在 D 天内送达包裹的能力[1011]

这道题相当给一个数组划分为k份,每份的和加起来最小数,题目见 https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/ ,题目是很好理解的,题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def shipWithinDays(self, weights: List[int], days: int) -> int:
def get_shop_times(weights, v):
need = 1
cur = 0
for i in weights:
if cur + i > v:
need += 1
cur = 0
cur += i
return need

low = max(weights)
high = sum(weights)

while low < high: # 注意的
mid = (high+low)//2
t = get_shop_times(weights, mid)
if t <= days: # 压缩右边的
high = mid # 注意的
else:
low = mid + 1
return low
或者 low<=high 然后 high=mid-1也可以的

最高频元素的频数[1838]

题目在这里 https://leetcode-cn.com/problems/frequency-of-the-most-frequent-element/ 这里我自己做了一种基于二分的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def maxFrequency(self, nums: List[int], k: int) -> int:
nums.sort()
low = 0
high = len(nums)
res = 0
prefix = [0]
for i in nums:
prefix.append(prefix[-1] + i)
for i in range(len(nums)):
low = 0
high = i
while low < high:
mid = (low + high) >> 1
if nums[i] * (i - mid + 1) - prefix[i + 1] + prefix[mid] <= k:
high = mid
else:
low = mid + 1
res = max(res, i - low + 1)
return res

还有双指针的做法,具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def maxFrequency(self, nums: List[int], k: int) -> int:
nums.sort()
i = ans = 0
# 问题转化一下,排序后 nums[j] * (j - i + 1) <= k + presum[j + 1] - presum[i]
for j, num in enumerate(nums):
k += num
while k < num * (j - i + 1):
k -= nums[i]
i += 1
# 对于当前j最远的i
ans = max(ans, j - i + 1)
return ans

其中就是老的思路而已,没有新的变化的,还是注意端点移动的条件。

转变数组后最接近目标值的数组和[1300]

题目见 https://leetcode-cn.com/problems/sum-of-mutated-array-closest-to-target/ 主要的思路是用二分来做,其实主要是找到一个值,让插入后后值全部变成这个值,比如[1,2,3,4,8,9], 我们设置为7,那么在后面的8和9就变成7。还需要注意的是,这里的prefix开始的值要为0,然后进行append才可以。
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def findBestValue(self, arr: List[int], target: int) -> int:
import bisect
n = len(arr)
arr.sort()
prefix = [0]
for i in arr:
prefix.append(prefix[-1] + i)
res = 0
min_diff = float("inf")
for i in range(arr[-1]+1):
index = bisect.bisect_left(arr, i)
sums = prefix[index] + (n-index)*i
if abs(sums - target) < min_diff:
min_diff = abs(sums - target)
res = i
return res

区间和的个数[327][SKIP]

题目见 https://leetcode-cn.com/problems/count-of-range-sum/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 暴力
class Solution:
def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
import bisect
prefix = [0]
for i in nums:
prefix.append(i + prefix[-1])
cnt = 0
for i in range(len(nums)):
for j in range(i, len(nums)):
if (prefix[j + 1] - prefix[i]) >= lower and (prefix[j + 1] - prefix[i]) <= upper:
cnt += 1
return cnt
# 通过
class Solution:
def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
res, pre, now = 0, [0], 0
for n in nums:
now += n
res += bisect.bisect_right(pre, now - lower) - bisect.bisect_left(pre, now - upper)
bisect.insort(pre, now)
return res
# AC [通俗版]
class Solution:
def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
sl = []
res = 0
pre_sum = [0]
for i in nums:
pre_sum.append(pre_sum[-1] + i)
for x in pre_sum:
res += bisect.bisect_right(sl, x - lower) - bisect.bisect_left(sl, x - upper)
bisect.insort(sl, x)
return res

乍一想前缀和不是单调的,没法进行插入排序,但这里的思路在于每个循环考虑以该index为结尾的符合条件的数量。这里的解法非常 https://leetcode.cn/problems/count-of-range-sum/solutions/2417725/sortedlist-da-fa-hao-a-bu-dao-shi-xing-d-7yyh/

将 x 减到 0 的最小操作数[1658]

题目见 https://leetcode-cn.com/problems/minimum-operations-to-reduce-x-to-zero/ ,解法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 我的解法
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
def get_prefix_sum(nums):
prefix = []
for i in nums:
val = prefix[-1] if prefix else 0
prefix.append(i+val)
return prefix

res = float("inf")
for i in range(len(nums)):
nums2 = nums[0:i][::-1] + nums[i:][::-1]
prefix = get_prefix_sum(nums2)
print(prefix)
if x in prefix:
res = min(res, prefix.index(x)+1)
return -1 if res==float("inf") else res
# 正确的
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
diff = sum(nums) - x
if diff < 0:
return -1
left = 0
right = 0
sm = 0
res = -1
while right < len(nums):
sm += nums[right]
while sm > diff:
sm -= nums[left]
left += 1
if sm == diff:
res = max(res,right - left + 1)
right += 1
return -1 if res==-1 else len(nums)-res

和大于等于 target 的最短子数组[LCR 008]

链接为:https://leetcode.cn/problems/2VG8Kg/description/ 题解如下,注意一下边界的条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
import bisect
prefix = [0]
for i in nums:
prefix.append(prefix[-1] + i)
if prefix[-1]<target:
return 0
res = float("inf")
for i in range(len(prefix)):
index = bisect.bisect_left(prefix, prefix[i] + target)
if index != len(prefix):
res = min(index - i, res)
return 0 if res==len(prefix) else res

也可以使用滑窗解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
ans = n + 1
start, end = 0, 0
total = 0
while end < n:
total += nums[end]
while total >= s:
ans = min(ans, end - start + 1)
total -= nums[start]
start += 1
end += 1

return 0 if ans == n + 1 else ans

和至少为k的最短子数组[862]

题目见 https://leetcode-cn.com/problems/shortest-subarray-with-sum-at-least-k/ 题解用滑窗如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 超时
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
n = len(nums)
res = float("inf")
for left in range(len(nums)):
right = left
sm =0
while right < n:
sm += nums[right]
while sm >= k:
res = min(res, right - left + 1)
sm -= nums[left]
left += 1
right += 1
return -1 if res==float("inf") else res
# 超时2:这个思路要弄懂 模仿区间和的个数区间和的个数来的
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
import bisect
res = float("inf")
cnt = 0
pre_sum = [[0, cnt]]
for i in range(1, len(nums) + 1):
pre_sum.append([pre_sum[-1][0] + nums[i - 1], i])
pre_sum.sort()
for i in pre_sum:
cur_val = i[0]
index = bisect.bisect_left(pre_sum, [cur_val + k, 0])
for j in range(index, len(pre_sum)):
if j <= len(nums) and pre_sum[j][0] >= cur_val + k and pre_sum[j][1] - i[1] >= 0:
res = min(res, pre_sum[j][1] - i[1])
if res==float("inf"):
return -1
return res
# 超时的原因在于有多个循环,其实看下面的结果也是有多个循环, 也就是1个for里面加了两个while, 不过计算的时候,是通过单调队列来做的,减少了滑窗计算的时间而已。这里和滑窗窗口的最大值是一样的。
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
import collections
prefix = [0]
for i in nums:
prefix.append(prefix[-1] + i)
stack = collections.deque()
ans = len(nums) + 1
for x, y in enumerate(prefix):
while stack and y <= prefix[stack[-1]]:
stack.pop()
while stack and y - prefix[stack[0]] >= k:
ans = min(ans, x - stack.popleft())
stack.append(x)
return ans if ans < len(nums) + 1 else -1

这道题和上面的那道题不一样,在于这道题有负数,导致前缀和非单调,无法用滑窗以及直接用二分来做。

双数组

寻找两个正序数组的中位数[4]

题目见 https://leetcode-cn.com/problems/median-of-two-sorted-arrays/ ,题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 解法1,使用双指针
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
m = len(nums1)
n = len(nums2)
lens = m + n
prev = -1
now = -1
num1_index = 0
num2_index = 0
for i in range(lens//2+1):
prev = now
if num1_index<m and (num2_index>=n or nums1[num1_index]<nums2[num2_index]):
now = nums1[num1_index]
num1_index += 1
else:
now = nums2[num2_index]
num2_index += 1
if lens & 1 == 0:
return (prev + now)/2
else:
return now
# 解法2:使用二分查找
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
n1 = len(nums1)
n2 = len(nums2)
if n1 > n2:
return self.findMedianSortedArrays(nums2, nums1)

k = (n1 + n2 + 1) // 2
left = 0
right = n1
while left < right:
m1 = left + (right - left) // 2
m2 = k - m1 #
if nums1[m1] < nums2[m2 - 1]: # 注意
left = m1 + 1
else:
right = m1

m1 = left # 算出的m是第2个数,如果是&1=1的话,直接取m-1,不是的话,取m-1和m的均值
m2 = k - m1

c1 = max(float('-inf') if m1 <= 0 else nums1[m1 - 1], float('-inf') if m2 <= 0 else nums2[m2 - 1]) # 情况1,容易写错为m1
if (n1 + n2) % 2 == 1:
return c1

c2 = min(float('inf') if m1 >= n1 else nums1[m1], float('inf') if m2 >= n2 else nums2[m2]) # 情况2

return (c1 + c2) / 2
# 自己做法
A = [1]
B = [2]
nums = []
left, right = 0, 0
while left < len(A) and right < len(B):
if A[left] < B[right]:
nums.append(A[left])
left += 1
else:
nums.append(B[right])
right += 1
if left >= len(A):
nums.extend(B[right:])
if right >= len(B):
nums.extend(A[left:])
print(nums)

可以从 https://leetcode.cn/problems/median-of-two-sorted-arrays/solutions/8999/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-2/ 查看。二分的思路在 https://leetcode.cn/problems/median-of-two-sorted-arrays/solutions/3983/shuang-zhi-zhen-by-powcai/

尽可能使字符串相等[1208]

题目见 https://leetcode-cn.com/problems/get-equal-substrings-within-budget/, 题解如下,建议使用双指针啊,比较快的。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
diff = [abs(ord(i)-ord(j)) for i,j in zip(s,t)]
start = end = res = 0
ds = 0
while end < len(diff):
ds += diff[end]
while ds > maxCost:
ds -= diff[start]
start += 1
res = max(res, end - start + 1)
end += 1
return res

二分查找的思路也是比较简单的,主要如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
n = len(s)
accDiff = [0] + list(accumulate(abs(ord(sc) - ord(tc)) for sc, tc in zip(s, t)))
maxLength = 0

for i in range(1, n + 1):
start = bisect.bisect_left(accDiff, accDiff[i] - maxCost)
maxLength = max(maxLength, i - start)

return maxLength

水位上升的泳池中游泳[778]

题目见 https://leetcode-cn.com/problems/swim-in-rising-water/ 主要的思路就是找一个值,小于这个值的地方可以连通起来,最后能连通到最后的点的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution:
def swimInWater(self, grid: List[List[int]]) -> int:

def dfs(temp, x,y):
if x<0 or x>=len(temp) or y<0 or y>=len(temp[0]):
return False
if temp[x][y]==0:
return False
if x==len(temp)-1 and y==len(temp[0])-1 and temp[x][y]==1:
return True
temp[x][y] = 0
for i,j in [[x+1,y],[x,y+1],[x-1,y],[x,y-1]]:
if dfs(temp,i,j):
return True
# dfs(temp,i,j): 直接这么写的话是错误的
return False

grid_list = sum(grid, [])
grid_list = sum(grid, [])
low = min(grid_list)
high = max(grid_list)
while low < high:
mid = (low + high) >> 1
temp = [[1 if j <= mid else 0 for j in i] for i in grid]
if dfs(temp, 0, 0):
high = mid
else:
low = mid + 1
return low

绝对差值和

题目见 https://leetcode-cn.com/problems/minimum-absolute-sum-difference/submissions/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution(object):
def minAbsoluteSumDiff(self, nums1, nums2):
"""
:type nums1: List[int]
:type nums2: List[int]
:rtype: int
"""
import bisect
temp = [abs(nums1[i] - nums2[i]) for i in range(len(nums1))]
abs_sum = sum(temp)

nums11 = sorted(nums1)

res = abs_sum

for i, j in enumerate(nums2):
index = bisect.bisect(nums11, j)
if index < len(nums11):
res = min(res, abs_sum - abs(nums1[i]-nums2[i]) + abs(nums11[index]-nums2[i]))
if index > 0:
res = min(res, abs_sum - abs(nums1[i]-nums2[i]) + abs(nums11[index-1]-nums2[i]))
return res% (10**9+7)

这里需要注意的是,要查看需要插入数值的位置,查看其前后的位置,是不是有让绝对值更小的值。

交换和[面试题 16.21]

这道题和上面的是类似的,在得到index后需要判断位置,是不行越界了。题目在 https://leetcode-cn.com/problems/sum-swap-lcci/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution(object):
def findSwapValues(self, array1, array2):
"""
:type array1: List[int]
:type array2: List[int]
:rtype: List[int]
"""
import bisect
array1.sort()
array2.sort()

sum1 = sum(array1)
sum2 = sum(array2)

diff = sum2 - sum1

if diff % 2 !=0:
return []

for i in array1:
index = bisect.bisect_left(array2, i + diff // 2)
index = min(len(array2)-1, index) #dasdsdsa
if array2[index] == i + diff // 2:
return [i, i + diff // 2]
return []

得到子序列的最少操作次数[1713]

题目见 https://leetcode-cn.com/problems/minimum-operations-to-make-a-subsequence/ 这里的题目和最长上升子序列的思路基本上是一样的,可以看 https://leetcode-cn.com/problems/minimum-operations-to-make-a-subsequence/solution/mo-gu-qie-cha-cong-lcswen-ti-dao-liswen-xist8/ 的讲解,说的很清楚,总结来说就是一个如下的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def minOperations(self, target: List[int], arr: List[int]) -> int:
posTa = {}
for i, t in enumerate(target):
posTa[t] = i
posAr = []
for i, a in enumerate(arr):
if a in posTa:
posAr.append(posTa[a])
# 算出来出现的索引就可以了,然后就是求解了
stk = []
for x in posAr:
if stk and x <= stk[-1]:
idx = bisect_left(stk, x)
stk[idx] = x
else:
stk.append(x)
return len(target) - len(stk)

矩阵

二维数组中的查找[LCR.121]

题目见 https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/ 题解如下:

1
2
3
4
5
6
7
8
class Solution:
def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool:
i, j = len(matrix) - 1, 0
while i >= 0 and j < len(matrix[0]):
if matrix[i][j] > target: i -= 1
elif matrix[i][j] < target: j += 1
else: return True
return False

有个人总结的比较好,题解在 https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/solution/yu-niang-niang-04er-wei-shu-zu-zhong-de-vpcs9/ 讲解了多个方法

搜索二维矩阵[74]

题目见 https://leetcode-cn.com/problems/search-a-2d-matrix/ 和上面的不一样,这个矩阵的展开收拾递增的,因此可以使用查找的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
low = 0
m = len(matrix)
n = len(matrix[0])
high = m * n - 1 # 如果这里改成了m*n的话,那么后面的最好用low<high
while low <= high:
mid = low + (high-low)//2
row = mid//n
col = mid%n
if matrix[row][col] > target:
high = mid - 1
elif matrix[row][col] < target:
low = mid + 1
else:
return True
return False

统计有序矩阵中的负数[1351]

题目见 https://leetcode-cn.com/problems/count-negative-numbers-in-a-sorted-matrix/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def countNegatives(self, grid: List[List[int]]) -> int:
end = len(grid[0])
i = 0
res = 0
while i < len(grid):
if grid[i][-1]<0:
index = bisect.bisect_right([-j for j in grid[i]],0)
res += len(grid[0]) - index
i = i + 1
return res

螺旋矩阵[54]

题号为54,位于https://leetcode.cn/problems/spiral-matrix/description/ 题解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if len(matrix)==1:
return matrix[0]
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
row, col, direction_index = 0, 0, 0
res = []
m = len(matrix)
n = len(matrix[1])
for i in range(m * n):
res.append(matrix[row][col])
matrix[row][col] = -999999999
next_row = row + directions[direction_index % 4][0]
next_col = col + directions[direction_index % 4][1]
if next_row < 0 or next_row >= m or next_col < 0 or next_col >= n or matrix[next_row][next_col] == -999999999:
direction_index = direction_index + 1
next_row = row + directions[direction_index % 4][0]
next_col = col + directions[direction_index % 4][1]
row, col = next_row, next_col
return res

上下两道题用的是同样的思路,这样比较好统一

螺旋矩阵II[59]

题号为59,位于 https://leetcode.cn/problems/spiral-matrix-ii/description/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
directions = [(0,1),(1,0),(0,-1),(-1,0)]
matrix = [[-1]*n for _ in range(n)]
row, col, direc_index = 0,0,0
for i in range(n*n):
matrix[row][col] = i + 1
next_row = row + directions[direc_index%4][0]
next_col = row + directions[direc_index%4][1]
if next_row < 0 or next_row >= n or next_col < 0 or next_row >= n or matrix[next_row][next_col] > -1:
direc_index = direc_index + 1
next_row = row + directions[direc_index%4][0]
next_col = row + directions[direc_index%4][1]
row, col = next_row, next_col
return matrix

注意具体的思路是先便利,然后换方向

螺旋矩阵III[885]

题号为885,位于 https://leetcode.cn/problems/spiral-matrix-iii/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution:
def spiralMatrixIII(self, rows: int, cols: int, rStart: int, cStart: int) -> List[List[int]]:
visited_flag = [[False] * cols for _ in range(rows)]
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
direction_index = -1
cnt = 0
init_lens = 0
res = []
res.append([rStart, cStart])
while cnt + 1 < rows * cols:
init_lens = init_lens + 1

direction_index = direction_index + 1
for j1 in range(init_lens):
next_r = rStart + directions[direction_index % 4][0]
next_c = cStart + directions[direction_index % 4][1]
if next_r < 0 or next_r >= rows or next_c < 0 or next_c >= cols or visited_flag[next_r][next_c]:
rStart = next_r
cStart = next_c
else:
res.append([next_r, next_c])
rStart = next_r
cStart = next_c
visited_flag[next_r][next_c] = True
cnt = cnt + 1

direction_index = direction_index + 1
for j2 in range(init_lens):
next_r = rStart + directions[direction_index % 4][0]
next_c = cStart + directions[direction_index % 4][1]
if next_r < 0 or next_r >= rows or next_c < 0 or next_c >= cols or visited_flag[next_r][next_c]:
rStart = next_r
cStart = next_c
else:
res.append([next_r, next_c])
rStart = next_r
cStart = next_c
visited_flag[next_r][next_c] = True
cnt = cnt + 1
return res

主要思路就是分析题目,其实每个固定的长度比如走1格子,其实是分为两个角度来走的,那么整体上就是1步往右,1步往下,2步往左,2步往上。依次这样来再结合判断条件。

总结

大纲

微信截图_20231210121917.png

相关细节

  1. 夹在state.append和dfs(root, state)中间的判断是否target_sum正确的时候,不需要加return,不然会导致运行结果的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
res = []
def dfs(root, state):
if not root:
return
state.append(root.val)
if not root.left and not root.right:
res.append(state[:]) # 这里不要return
dfs(root.left, state)
dfs(root.right, state)
state.pop()
dfs(root, [])
return ["->".join([str(i) for i in s]) for s in res]

遍历操作

介绍

树结构如下

先序:1 2 4 6 7 8 3 5
中序:4 7 6 8 2 1 3 5
后序:7 8 6 4 2 5 3 1

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right


root = TreeNode(1, left=TreeNode(2, left=TreeNode(3), right=TreeNode(4)),
right=TreeNode(5, left=TreeNode(6), right=TreeNode(7)))

# 前序遍历
res = []
def preOrder(root):
if not root:
return
res.append(root.val)
preOrder(root.left)
preOrder(root.right)
preOrder(root)
print("前序结果是:", res)


# 中序遍历
res = []
def inOrder(root):
if not root:
return
inOrder(root.left)
res.append(root.val)
inOrder(root.right)
inOrder(root)
print("中序结果是:", res)


# 后序遍历
res = []
def postOrder(root):
if not root:
return
postOrder(root.left)
postOrder(root.right)
res.append(root.val)
postOrder(root)
print("后序结果是:", res)

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# Definition for a binary tree node.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right


root = TreeNode(1, left=TreeNode(2, left=TreeNode(3), right=TreeNode(4)),
right=TreeNode(5, left=TreeNode(6), right=TreeNode(7)))


# 前序
stack = [root]
res = []
while stack:
node = stack.pop()
res.append(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
print("前序结果是:", res)

# 后序
stack = [root]
res = []
while stack:
node = stack.pop()
res.append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
print("后序结果是:", res)

# 中序,注意这里不能直接将root加入进去
stack = []
cur = root
res = []
while stack or cur:
if cur:
stack.append(cur)
cur = cur.left
else:
cur = stack.pop()
res.append(cur.val)
cur = cur.right
print("中序结果是:", res)

# 层序遍历
from _collections import deque
queue = deque([root])
while queue:
temp = []
for i in range(len(queue)):
node = queue.popleft()
temp.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
print(temp)
# 层序遍历2
from _collections import deque
queue = deque([root])
while queue:
temp = []
for i in range(len(queue)):
node = queue.popleft()
if node:
temp.append(node.val)
queue.append(node.left)
queue.append(node.right)
print(temp)

需要注意层序遍历1和层序遍历2的区别,层序遍历1中的queue只添加一些非None的节点,而层序遍历2中的话,连一些为None的节点也会添加,这点在对称二叉树中会用到第二种方法。

结构操作

翻转二叉树[226]

迭代题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return root
from collections import deque
queue = deque([root])
while queue:
for i in range(len(queue)):
node = queue.popleft()
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
node.left, node.right = node.right, node.left
return root

递归解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return root
root.left, root.right = root.right, root.left
self.invertTree(root.left)
self.invertTree(root.right)
return root

对称二叉树[101]

迭代解法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
from _collections import deque
queue = deque([root])
while queue:
res = []
for i in range(len(queue)):
node = queue.popleft()
if node:
res.append(node.val)
queue.append(node.left)
queue.append(node.right)
else:
res.append(None)
if res != res[::-1]:
return False
return True

递归解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
def check(p,q):
if p and not q:
return False
if not p and q:
return False
if not p and not q:
return True
if p.val != q.val:
return False
return check(p.left, q.right) and check(p.right, q.left)

if not root:
return True
return check(root, root)

这题主要要单独开一个sub-function出来操作。

平衡二叉树[110]

递归解法

1
2
3
4
5
6
7
8
9
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def get_height(root):
if not root:
return 0
return 1 + max(get_height(root.left), get_height(root.right))
if not root:
return True
return abs(get_height(root.left) - get_height(root.right)) <=1 and self.isBalanced(root.left) and self.isBalanced(root.right)

优化完后的递归的解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def recu(root):
if not root:
return 0
left = recu(root.left)
if left==-1:
return -1
right = recu(root.right)
if right==-1:
return -1
return max(left, right) + 1 if abs(left-right)<=1 else -1
return recu(root)!=-1

深度和高度

完全二叉树的节点个数[222]

迭代解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
from collections import deque
queue = deque([root])
cnt = 0
while queue:
for i in range(len(queue)):
node = queue.popleft()
cnt += 1
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return cnt

递归解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def __init__(self):
self.cnt = 0

def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
self.cnt += 1
self.countNodes(root.left)
self.countNodes(root.right)
return self.cnt

二叉树最大高度[104]

迭代解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
from collections import deque
queue = deque([root])
res = []
while queue:
temp = []
for i in range(len(queue)):
node = queue.popleft()
temp.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
res.append(temp)
return len(res)

递归法如下所示:

1
2
3
4
5
6
7
8
9
10
11
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))

二叉树最小深度[111]

迭代法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
from collections import deque
queue = deque([root])
min_depth = 0
while queue:
min_depth += 1
for i in range(len(queue)):
node = queue.popleft()
if node.left==None and node.right==None:
return min_depth
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return minDepth

递归法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
if not root.left and root.right:
return 1 + self.minDepth(root.right) # 容易写成不加1
if not root.right and root.left:
return 1 + self.minDepth(root.left)
return 1 + min(self.minDepth(root.left), self.minDepth(root.right))

路径问题

二叉树的所有路径[257]

迭代解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
if not root:
return False
if not root.left and not root.right:
return [str(root.val)]
from collections import deque
queue = deque([root])
queue2 = [str(root.val)]
res = []
while queue:
for i in range(len(queue)):
node = queue.popleft()
node_value = queue2.pop(0)
if not node.left and not node.right:
res.append(node_value)
if node.left:
queue.append(node.left)
queue2.append(str(node_value) + "->" + str(node.left.val))
if node.right:
queue.append(node.right)
queue2.append(str(node_value) + "->" + str(node.right.val))
return res

递归解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 使用回溯1
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
res = []
def dfs(root, state):
if root:
state.append(root.val)
if not root.left and not root.right:
res.append("->".join([str(i) for i in state[:]]))
return
if root.left:
dfs(root.left, state)
state.pop()
if root.right:
dfs(root.right, state)
state.pop()
dfs(root, [])
return res

# [推荐写法] 回溯2
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
res = []
def dfs(root, state):
if not root:
return
state.append(root.val)
if not root.left and not root.right:
res.append(state[:])
dfs(root.left, state)
dfs(root.right, state)
state.pop()
dfs(root, [])
return ["->".join([str(i) for i in s]) for s in res]

# 还有一种回溯的写法,不需要显示的调用pop函数
class Solution:
def binaryTreePaths(self, root):
"""
:type root: TreeNode
:rtype: List[str]
"""
def construct_paths(root, path):
if root:
path += str(root.val)
if not root.left and not root.right: # 当前节点是叶子节点
paths.append(path) # 把路径加入到答案中
else:
path += '->' # 当前节点不是叶子节点,继续递归遍历
construct_paths(root.left, path)
construct_paths(root.right, path)

paths = []
construct_paths(root, '')
return paths

路径总和[112]

迭代解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
from collections import deque
queue = deque([root]) # 使用了两个deque
queue2 = deque([root.val])
while queue:
for i in range(len(queue)):
node = queue.popleft()
node_value = queue2.popleft()
if node.left==None and node.right==None and node_value==targetSum:
return True
if node.left:
queue.append(node.left)
queue2.append(node_value+node.left.val)
if node.right:
queue.append(node.right)
queue2.append(node_value+node.right.val)
return False

递归解法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 正确解答1
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
if not root.left and not root.right:
return targetSum == root.val
return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val)
# [推荐写法]正确解答2【为了和后面的112保持一致,使用该解法】
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
targetSum = targetSum - root.val
if not root.left and not root.right and targetSum==0:
return True
return self.hasPathSum(root.left, targetSum) or self.hasPathSum(root.right, targetSum)
# 错写
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
if not root.left and not root.right and targetSum==0: # 注意
return False # 注意
return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val)

路径总和 II[113]

递归解法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 回溯解法1,按照标准的回溯来写的
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
res = []
def dfs(root, state):
if not root:
return
state.append(root.val)
if not root.left and not root.right:
if sum(state[:])==targetSum:
res.append(state[:])
return
if root.left:
dfs(root.left, state)
state.pop()
if root.right:
dfs(root.right, state)
state.pop()
dfs(root, [])
return res

# [推荐写法]回溯解法2, 按照标准回溯来
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
ret = list()
def dfs(root, state):
if not root:
return
state.append(root.val)
if not root.left and not root.right and sum(state[:]) == targetSum:
ret.append(state[:])
dfs(root.left, state)
dfs(root.right, state)
state.pop()
dfs(root, [])
return ret

# 回溯解法2,使用非标准的回溯来写的
class Solution:
def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]:
ret = list()
path = list()

def dfs(root: TreeNode, targetSum: int):
if not root:
return
path.append(root.val)
targetSum -= root.val
if not root.left and not root.right and targetSum == 0:
ret.append(path[:])
dfs(root.left, targetSum)
dfs(root.right, targetSum)
path.pop()

dfs(root, targetSum)
return ret

# 回溯解法3,使用非标准回溯来
class Solution:
def pathSum(self, root, targetSum):
ret = list()
path = list()

def dfs(root: TreeNode):
if not root:
return
path.append(root.val)
if not root.left and not root.right and sum(path[:]) == targetSum:
ret.append(path[:])
dfs(root.left)
dfs(root.right)
path.pop()

dfs(root)
return ret

非标准的意思是,这里的path是一个不在dfs中传进去的值

路径总和III[437]

我的解法如下,超时了,妈的不打算改了就这样吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
res = []
def dfs(root, state):
if not root:
return
state.append(root.val)
if sum(state[:])==targetSum:
res.append(state[:])
dfs(root.left, state)
dfs(root.right, state)
state.pop()

def dfs2(root):
if not root:
return
dfs(root,[])
dfs2(root.left)
dfs2(root.right)

dfs2(root)
return len(res)

二叉树中的最大路径和[124]

位于 https://leetcode.cn/problems/binary-tree-maximum-path-sum/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def __init__(self):
self.max_sum = -88

def maxPathSum(self, root: Optional[TreeNode]) -> int:
def maxGain(root):
if not root:
return 0
leftGain = max(maxGain(root.left), 0)
rightGrain = max(maxGain(root.right), 0)
cur_root_gain = root.val + leftGain + rightGrain
if cur_root_gain > self.max_sum:
self.max_sum = cur_root_gain
return root.val + max(rightGrain,leftGain) #写错为root.val + rightGrain + leftGain
maxGain(root)
return self.max_sum

注意这里的maxGain是需要定义好的,返回的值是以当前节点为开始或者结束的收益值,因此需要写成root.val+max(rightGain, leftGain)

二叉树的最近公共祖先[236]

递归法如下,最主要的点在代码中已经说了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 错误解法
class Solution(object):
def lowestCommonAncestor(self, root, p, q):
"""
:type root: TreeNode
:type p: TreeNode
:type q: TreeNode
:rtype: TreeNode
"""
if not root:
return root
if not root.left and root.right:
return self.lowestCommonAncestor(root.right, p, q)
if not root.right and root.left:
return self.lowestCommonAncestor(root.left, p, q)
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if not left:
return right
if not right:
return left
if left and right:
return root
# 正确解法
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root:
return root
if root==p or root==q: # 最重要的点
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if not left:
return right
if not right:
return left
return root

构造二叉树

最大二叉树[654]

迭代法

1
2
3
4
5
6
7
8
9
class Solution:
def constructMaximumBinaryTree(self, nums: List[int]) -> Optional[TreeNode]:
if not nums:
return
max_index = nums.index(max(nums))
root = TreeNode(val=nums[max_index])
root.left = self.constructMaximumBinaryTree(nums[:max_index])
root.right = self.constructMaximumBinaryTree(nums[max_index+1:])
return root

合并二叉树[617]

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
if not root1 and not root2:
return
if not root1 and root2:
return root2
if not root2 and root1:
return root1
root = TreeNode(root1.val + root2.val)
root.left = self.mergeTrees(root1.left, root2.left)
root.right = self.mergeTrees(root1.right, root2.right)
return root

从前序与中序遍历序列构造二叉树[105]

递归方法如下

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder:
return
if not inorder:
return
root = TreeNode(preorder[0])
inorder_index = inorder.index(preorder[0])
root.left = self.buildTree(preorder[1:inorder_index+1], inorder[:inorder_index])
root.right = self.buildTree(preorder[inorder_index+1:], inorder[inorder_index+1:])
return root

从中序与后序遍历序列构造二叉树[106]

递归解法如下

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
if not inorder:
return
if not postorder:
return
root = TreeNode(postorder[-1])
inorder_index = inorder.index(postorder[-1])
root.left = self.buildTree(inorder[:inorder_index], postorder[:inorder_index])
root.right = self.buildTree(inorder[inorder_index+1:], postorder[inorder_index:-1])
return root

二叉搜索树

将有序数组转换为二叉搜索树[108]

1
2
3
4
5
6
7
8
9
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
if len(nums)==0:
return None
mid = int(len(nums)/2)
root = TreeNode(nums[mid])
root.left = self.sortedArrayToBST(nums[:mid])
root.right = self.sortedArrayToBST(nums[mid+1:])
return root

二叉搜索树中的搜索[700]

迭代法,就是类似链表

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
cur = root
while cur:
f = cur.val
if f < val:
cur = cur.right
elif f > val:
cur = cur.left
else:
return cur
return None

递归法

1
2
3
4
5
6
7
8
9
10
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root:
return None
if root.val == val:
return root
elif root.val > val:
return self.searchBST(root.left, val)
elif root.val < val:
return self.searchBST(root.right, val)

验证二叉搜索树[98]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
res = []
def inOrder(root):
if not root:
return
inOrder(root.left)
res.append(root.val)
inOrder(root.right)
inOrder(root)
for i in range(len(res)-1):
if res[i+1] <= res[i]:
return False
return True

二叉搜索树的最小绝对差[530]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution(object):    
def getMinimumDifference(self, root):
"""
:type root: TreeNode
:rtype: int
"""
res = []
def inorder(root):
if not root:
return
inorder(root.left)
res.append(root.val)
inorder(root.right)
inorder(root)
mins = float("inf")
for i in range(len(res)-1):
mins = min(mins, abs(res[i+1] - res[i]))
return mins

二叉搜索树中的众数[501]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution(object):
def findMode(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
res = []
def inorder(root):
if not root:
return
inorder(root.left)
res.append(root.val)
inorder(root.right)
inorder(root)
from collections import Counter
cnt = Counter(res)
from collections import defaultdict
new_d = defaultdict(list)
for k, v in cnt.items():
new_d[v].append(k)
new_d = sorted(new_d.items(),key=lambda x:-x[0])
return new_d[0][1]

二叉搜索树的最近公共祖先[235]

递归解法如下

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root:
return root
if root == p or root == q:
return root
if root.val < min(p.val, q.val):
return self.lowestCommonAncestor(root.right, p, q)
if root.val > max(p.val, q.val):
return self.lowestCommonAncestor(root.left, p, q)
else:
return root

二叉树的最近公共祖先[236]

递归法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 正确解法
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root:
return root
if root==p or root==q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if not left:
return right
if not right:
return left
return root

二叉搜索树中的插入操作[701]

迭代法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution:
def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
res_trees = []
def inorder(root):
if not root:
return
inorder(root.left)
res_trees.append(root)
inorder(root.right)
if not root:
return TreeNode(val)
inorder(root)

if val < res_trees[0].val:
res_trees[0].left = TreeNode(val)

if val > res_trees[-1].val:
res_trees[-1].right = TreeNode(val)

for i in range(len(res_trees)-1):
if val>res_trees[i].val and val<res_trees[i+1].val:
left = res_trees[i]
right = res_trees[i+1]
if left.right==None:
left.right = TreeNode(val)
elif right.left==None:
right.left = TreeNode(val)
return root

官方迭代法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
if not root:
return TreeNode(val)

pos = root
while pos:
if val < pos.val:
if not pos.left:
pos.left = TreeNode(val)
break
else:
pos = pos.left
else:
if not pos.right:
pos.right = TreeNode(val)
break
else:
pos = pos.right

return root

删除二叉搜索树中的节点[450]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
if not root:
return None
if root.val > key:
root.left = self.deleteNode(root.left, key)
elif root.val < key:
root.right = self.deleteNode(root.right, key)
else:
# 判断root点的情况
if not root.left and root.right:
return root.right
if not root.right and root.left:
return root.left
if not root.left and not root.right:
return None
if root.left and root.right:
# 找到最左边的节点
t = node = root.right
while node.left:
node = node.left
node.left = root.left
return t
return root

修剪二叉搜索树[669]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 错误解法
class Solution:
def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
if not root:
return root
if root and root.val < low:
root = root.right
if root and root.val > high:
root = root.left
if root and root.left and root.left.val < low:
root.left = root.left.right
if root and root.right and root.right.val > high:
root.right = root.right.left
if root and root.left:
self.trimBST(root.left, low, high)
if root and root.right:
self.trimBST(root.right, low, high)
return root
# 【推荐解法】
class Solution:
def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
if not root:
return root
if root.val < low:
return self.trimBST(root.right, low, high)
elif root.val > high:
return self.trimBST(root.left, low, high)
root.left = self.trimBST(root.left, low, high)
root.right = self.trimBST(root.right, low, high)
return root

把二叉搜索树转换为累加树[538]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
res = []
def inorder(root):
if not root:
return
inorder(root.left)
res.append(root)
inorder(root.right)
inorder(root)
res=res[::-1]
for i in range(1,len(res)):
res[i].val = res[i].val + res[i-1].val
return root

把二叉搜索树转换为累加树[538]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
res = []
def inorder(root):
if not root:
return
inorder(root.left)
res.append(root)
inorder(root.right)
inorder(root)
res=res[::-1]
for i in range(1,len(res)):
res[i].val = res[i].val + res[i-1].val
return root

总结

大纲

相关细节

  1. 如果题目求最大值,则dp初始化为[-float(“inf”)]*n的数据
  2. 循环比较一般使用dp[i] = max(dp[i], dp[j] + nums[i]),而不是dp[i] = max(dp[i], dp[j]) + nums[i]

序列

单序列

模板

  • 一般初始化的时候,都会dp = [0] * len(str),而不是dp = [0] * (len(str)+1)
  • 注意有的时候不是输出dp[n-1]而是max(dp)
  • 其他点

最长递增子序列[300]

位于 https://leetcode.cn/problems/longest-increasing-subsequence
dp表达式如下

1
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

代码如下

1
2
3
4
5
6
7
8
class Solution(object):
def lengthOfLIS(self, nums):
dp = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)

最长连续递增序列[674]

位于 https://leetcode.cn/problems/longest-continuous-increasing-subsequence
dp表达式如下

1
if (nums[i] > nums[i-1]) dp[i] = max(dp[i], dp[i-1] + 1);

代码如下

1
2
3
4
5
6
7
class Solution(object):
def findLengthOfLCIS(self, nums):
dp = [1] * len(nums)
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
dp[i] = max(dp[i], dp[i-1] + 1)
return max(dp)

最大子数组和[53]

位于 https://leetcode.cn/problems/maximum-subarray

1
2
3
4
5
6
7
class Solution(object):
def maxSubArray(self, nums):
dp = [0] * len(nums)
dp[0] = nums[0]
for i in range(1, len(nums)):
dp[i] = max(dp[i-1]+nums[i], nums[i])
return max(dp)

回文子串[647]

位于 https://leetcode.cn/problems/palindromic-substrings/description/
动规表达式如下

1
2
3
dp[i][i] = 1
if s[i]==s[j] and j-i==1: dp[i][j]=1
if j-i>1 and dp[i+1][j-1] and s[i]==s[j]:dp[i][j]=1

题解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution(object):
def countSubstrings(self, s):
n = len(s)
dp = [[0]*n for _ in range(n)]
cnt = 0
for i in range(n):
dp[i][i] = 1
cnt += 1
for j in range(n):
for i in range(j):
if j-i==1 and s[i]==s[j]:
dp[i][j] = 1
cnt += 1
elif j -i > 1 and dp[i+1][j-1] and s[i]==s[j]:
dp[i][j] = 1
cnt += 1
return cnt

遍历顺序是这样的
image

最长回文子串[5]

位于 https://leetcode.cn/problems/longest-palindromic-substring
子串是连续的,子序列不是,这里要注意,和上面解法一样,代码没变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution(object):
def longestPalindrome(self, s):
n = len(s)
dp = [[0]*n for _ in range(n)]
m = 0
res = None
for i in range(n):
dp[i][i] = 1
if i-i+1>m:
m = i-i+1
res = [i,i]
for j in range(n):
for i in range(j):
if j-i==1 and s[i]==s[j]:
dp[i][j] = 1
if j-i+1>m:
m = j-i+1
res = [i,j]
elif j-i>1 and s[i]==s[j] and dp[i+1][j-1]==1:
dp[i][j] = 1
if j-i+1>m: # 要在里面写不能算完dp后再比较
m = j-i+1
res = [i,j]
return s[res[0]:res[1]+1]
# 推荐代码,如果按照上面写法做最长回文子序列会报错,因为循环的时候方向不对的,可以推导一下
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False] * n for _ in range(n)]
max_flag = 0
ind = 0
for i in range(n):
dp[i][i] = True
ind = (i, i)
for l in range(1, n):
for i in range(n - l):
j = i + l
if j-i==1 and s[i]==s[j]:
dp[i][j] = True
if j - i > max_flag:
max_flag = j - i
ind = (i, j)
if j-i>1 and s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1]
if dp[i][j] and j - i > max_flag:
max_flag = j - i
ind = (i, j)
return (s[ind[0]:ind[1] + 1])

环形子数组的最大和[918]

位于 https://leetcode.cn/problems/maximum-sum-circular-subarray/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def maxSubarraySumCircular(self, nums: List[int]) -> int:
n = len(nums)

max_ = -float("inf")
dp_max = [0] * n
for i in range(n):
dp_max[i] = max(0, dp_max[i-1]) + nums[i]
if dp_max[i] > max_:
max_ = dp_max[i]
if max(dp_max) < 0:
return max(nums)

min_ = float("inf")
dp_min = [0] * n
for i in range(n):
dp_min[i] = min(0, dp_min[i-1]) + nums[i]
if dp_min[i] < min_:
min_ = dp_min[i]

return max(max_, sum(nums) - min_)

最长回文子序列[516]

位于 https://leetcode.cn/problems/longest-palindromic-subsequence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
dp = [[0] * len(s) for _ in range(len(s))]
for i in range(len(s)):
dp[i][i] = 1
n = len(s)
for l in range(1, n):
for i in range(n - l):
j = i + l
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][n-1]

戳气球[312]

位于 https://leetcode.cn/problems/burst-balloons

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 递归
class Solution:
def maxCoins(self, nums: List[int]) -> int:
n = len(nums)
val = [1] + nums + [1]

@lru_cache(None)
def solve(left: int, right: int) -> int:
if left >= right - 1:
return 0

best = 0
for i in range(left + 1, right):
total = val[left] * val[i] * val[right]
total += solve(left, i) + solve(i, right)
best = max(best, total)

return best

return solve(0, n + 1)
# 动规
class Solution:
def maxCoins(self, nums: List[int]) -> int:
n = len(nums)
rec = [[0] * (n + 2) for _ in range(n + 2)]
val = [1] + nums + [1]

for i in range(n - 1, -1, -1):
for j in range(i + 2, n + 2):
for k in range(i + 1, j):
total = val[i] * val[k] * val[j]
total += rec[i][k] + rec[k][j]
rec[i][j] = max(rec[i][j], total)

return rec[0][n + 1]

https://leetcode.cn/problems/burst-balloons/solutions/337630/zhe-ge-cai-pu-zi-ji-zai-jia-ye-neng-zuo-guan-jian-/

鸡蛋掉落[887]

位于 https://leetcode.cn/problems/super-egg-drop 本题需要反过来想,如果我们可以做 t 次操作,而且有 k 个鸡蛋,那么我们能找到答案的最高的 n 是多少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def superEggDrop(self, k: int, n: int) -> int:
# k鸡蛋,n是楼层
if n==1:
return 1
dp = [[0]*(k+1) for _ in range(n+1)] # t次操作,k个鸡蛋,最高的n
for i in range(1,k+1):
dp[1][i] = 1
ans = -1
for i in range(2,n+1):
for j in range(1,k+1):
dp[i][j] = 1 + dp[i-1][j-1] + dp[i-1][j] # 鸡蛋碎和没碎
if dp[i][k] >=n: # 注意这里是k
ans = i
break
return ans

建议还是记下来吧,不然很容易忘

双序列

模板

  • 初始化dp为dp=[[0]*(n+1) for _ in range(m+1)],要多一位
  • 在判断的时候要少一位,比如num[i-1]就是对应到dp[i]这里来
  • 初始化一般都是0
  • 注意判断好条件

简单代码如下:

1
2
3
4
5
6
7
8
9
10
n1 = len(text1)
n2 = len(text2)
dp = [[0]*(n2+1) for _ in range(n1+1)] # 注意点1,都为0,且行列为n1+1啊
result = -1
for i in range(1, n1+1): # 注意点2,是从1开始
for j in range(1, n2+1):
if text1[i-1] == text2[j-1]: # 注意点3,这里判断i-1是否对的
dp[i][j] = func(dp[i][j], dp[i-1][j-1] + 1)
else:
dp[i][j] = func(..)

最长重复子数组[718]

位于 https://leetcode.cn/problems/maximum-length-of-repeated-subarray
注意题目中说的子数组,其实就是连续子序列。和最长连续递增序列[674]有点类似,下面的最长公共子序列和最长递增子序列[300]有点像。

dp表达式如下

1
A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;;

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
class Solution(object):
def findLength(self, nums1, nums2):
n1 = len(nums1)
n2 = len(nums2)
dp = [[0]*(n2+1) for _ in range(n1+1)] # 注意1:这里多了一位
max_res = -1
for i in range(1, n1+1):
for j in range(1, n2+1):
if nums1[i-1] == nums2[j-1]: # 这里判断i-1,就是 对应dp的i,主要是为了判断方便
dp[i][j] = max(dp[i][j], dp[i-1][j-1] + 1)
max_res = max(max_res, dp[i][j])
return max_res

最长公共子序列[1143]

位于 https://leetcode.cn/problems/longest-common-subsequence
和 最长重复子数组[718] 不一样,这道题不需要连续的数组。

p表达式如下

1
2
A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
不等的时候dp[i][j] = max(dp[i-1][j], dp[i][j-1])

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution(object):
def longestCommonSubsequence(self, text1, text2):
n1 = len(text1)
n2 = len(text2)
dp = [[0]*(n2+1) for _ in range(n1+1)]
result = -1
for i in range(1, n1+1):
for j in range(1, n2+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = max(dp[i][j], dp[i-1][j-1] + 1)
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
result = max(result, dp[i][j])
return result

不相交的线[1035]

位于 https://leetcode.cn/problems/uncrossed-lines
和最长公共子序列一样,代码也是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution(object):
def maxUncrossedLines(self, nums1, nums2):
n1 = len(nums1)
n2 = len(nums2)
dp = [[0]*(n2+1) for _ in range(n1+1)]
result = -1
for i in range(1, n1+1):
for j in range(1, n2+1):
if nums1[i-1] == nums2[j-1]:
dp[i][j] = max(dp[i][j], dp[i-1][j-1] + 1)
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
result = max(result, dp[i][j])
return result

判断子序列[392]

位于 https://leetcode.cn/problems/is-subsequence
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 编辑距离的基础题衍生的
class Solution(object):
def isSubsequence(self, s, t):
n1 = len(s)
n2 = len(t)
dp = [[0]*(n2+1) for _ in range(n1+1)]
for i in range(1, n1+1):
for j in range(1, n2+1):
if s[i-1]==t[j-1]:
dp[i][j] = max(dp[i][j],dp[i-1][j-1] + 1)
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
if dp[-1][-1]==len(s):
return True
return False

编辑距离[72]

位于 https://leetcode.cn/problems/edit-distance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n1 = len(word1)
n2 = len(word2)
dp = [[0]*(n2+1) for _ in range(n1+1)]
for i in range(n1+1):
dp[i][0] = i
for j in range(n2+1):
dp[0][j] = j
for i in range(1,n1+1):
for j in range(1, n2+1):
if word1[i-1]==word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j-1],dp[i-1][j],dp[i][j-1]) + 1
return dp[n1][n2]

不同的子序列[115]

位于 https://leetcode.cn/problems/distinct-subsequences
困难题动态规划方程不易想到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def numDistinct(self, s: str, t: str) -> int:
n1 = len(s)
n2 = len(t)
dp = [[0]*(n2+1) for _ in range(n1+1)]
for i in range(n1+1):
dp[i][0] = 1
for j in range(n2+1):
dp[0][j] = 0
dp[0][0] = 1
for i in range(1, n1+1):
for j in range(1, n2+1):
if s[i-1]==t[j-1]:
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
else:
dp[i][j] = dp[i-1][j]
return dp[-1][-1]

两个字符串的删除操作[583]

位于 https://leetcode.cn/problems/delete-operation-for-two-strings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1)]
for i in range(len(word1)+1):
dp[i][0] = i
for j in range(len(word2)+1):
dp[0][j] = j
for i in range(1, len(word1)+1):
for j in range(1, len(word2)+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j-1] + 2, dp[i-1][j] + 1, dp[i][j-1] + 1)
return dp[-1][-1]

爬楼梯类

斐波那契数[509]

位于 https://leetcode.cn/problems/fibonacci-number

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def fib(self, n: int) -> int:
if n==0:
return 0
if n==1:
return 1
dp = [0]*(n+1)
dp[0] = 0
dp[1]= 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]

爬楼梯[70]

位于 https://leetcode.cn/problems/climbing-stairs

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def climbStairs(self, n: int) -> int:
if n==1:
return 1
if n==2:
return 2
dp = [0] * (n+1)
dp[0] = 1
dp[1] = 2
for i in range(2,n+1):
dp[i] = dp[i-2] + dp[i-1]
return dp[n-1]

爬楼梯2[三步问题]

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def waysToStep(self, n: int) -> int:
run_maps = {1:1,2:2,3:4}
if n in run_maps:
return run_maps[n]
dp = [0] * (n+1)
dp[1] = 1
dp[2] = 2
dp[3] = 4
for i in range(4, n+1):
dp[i] = (dp[i-1] + dp[i-2] + dp[i-3])%1000000007
return dp[n]

爬楼梯3使用最小花费爬楼梯[LCR 088]

位于 https://leetcode.cn/problems/min-cost-climbing-stairs

1
2
3
4
5
6
7
8
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
dp = [0] * len(cost)
dp[0] = cost[0]
dp[1] = cost[1]
for i in range(2, len(cost)):
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
return min(dp[len(cost)-1],dp[len(cost)-2])

完全平方数[279]

位于 https://leetcode.cn/problems/perfect-squares

1
2
3
4
5
6
7
8
class Solution:
def numSquares(self, n: int) -> int:
dp = [float("inf")] * (n+1)
dp[0] = 0 # 注意
for i in range(1,n+1):
for j in range(int(i**0.5)+1):
dp[i] = min(dp[i], dp[i-j*j]+1)
return dp[-1]

跳跃游戏[55]

位于 https://leetcode.cn/problems/jump-game

1
2
3
4
5
6
7
8
9
10
class Solution:
def canJump(self, nums: List[int]) -> bool:
dp = [0] * len(nums)
dp[0] = 1
for i in range(1, len(nums)):
for j in range(i-1,-1,-1):
if dp[j] and nums[j] + j >= i:
dp[i] = 1
break
return True if dp[len(nums)-1] else False

跳跃游戏II[45]

位于 https://leetcode.cn/problems/jump-game-ii

1
2
3
4
5
6
7
8
9
class Solution:
def jump(self, nums: List[int]) -> int:
dp = [float("inf")] * len(nums)
dp[0] = 0
for i in range(1, len(nums)):
for j in range(i):
if nums[j] + j >= i:
dp[i] = min(dp[i], dp[j] + 1)
return dp[-1]

跳跃游戏 III[1306]

位于 https://leetcode.cn/problems/jump-game-iii
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# BFS
class Solution:
def canReach(self, arr: List[int], start: int) -> bool:
from collections import deque
used = {}
queue = deque([start])
while queue:
node = queue.popleft()
if arr[node]==0: # 容易写到2出
return True
if node in used:
continue
used[node] = 1
for v in [node-arr[node], node+arr[node]]:
if v >= 0 and v<len(arr):
if v in used:
continue # 2
queue.append(v)
return False
# DFS
class Solution:
def canReach(self, arr: List[int], start: int) -> bool:
s = {}
def dfs(arr, st):
if st not in s and arr[st]==0:
return True
s[st] = 1
current_step = arr[st]
for step in [st+current_step, st-current_step]:
if step>=0 and step< len(arr) and step not in s and dfs(arr, step):
return True
return False
return dfs(arr, start)

跳跃游戏 VI[1696]

位于 https://leetcode.cn/problems/jump-game-vi

1
2
3
4
5
6
7
8
9
class Solution:
def maxResult(self, nums: List[int], k: int) -> int:
dp = [-float("inf")] * len(nums) # 容易写错
dp[0] = nums[0]
for i in range(1, len(nums)):
for j in range(max(0, i - k), i):
dp[i] = max(dp[i], dp[j] + nums[i])
# 这里错写为max(dp[i], dp[j] + nums[i])
return dp[-1]

青蛙过河[403]

位于 https://leetcode.cn/problems/frog-jump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 递归
class Solution:
def canCross(self, stones: List[int]) -> bool:
for i in range(1, len(stones)):
if stones[i] > i + stones[i-1]:
return False
end = stones[-1]
stones = set(stones)

@functools.lru_cache(None)
def dfs(location, step):
if location == end: return True
for step in [step-1, step, step+1]:
if step > 0 and location+step in stones and dfs(location+step, step):
return True
return False
return dfs(0, 0)
# DP
class Solution:
def canCross(self, stones: List[int]) -> bool:
n = len(stones)
dp = [[False]* n for _ in range(n)]
dp[0][0] = True
for i in range(1,n):
for j in range(i-1,-1,-1):
k = stones[i] - stones[j]
if k>j+1:
break
dp[i][k] = dp[j][k] or dp[j][k-1] or dp[j][k+1]
for i in dp[n-1]:
if i:
return True
return False

不懂看这里 https://www.ai2news.com/blog/2980406/

打家劫舍[198]

位于 https://leetcode.cn/problems/house-robber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums)==1:
return nums[0]
if len(nums)==2:
return max(nums)
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = nums[1]
for i in range(2,len(nums)):
for j in range(i-1):
dp[i] = max(dp[i], dp[j]+nums[i])
# 容易写错为dp[i] = max(dp[i], dp[j]+nums[j])
return max(dp)

打家劫舍 II[213]

位于 https://leetcode.cn/problems/house-robber-ii

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def rob(self, nums: List[int]) -> int:
def dp_f(nums):
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = nums[1]
for i in range(2,len(nums)):
for j in range(i-1):
dp[i] = max(dp[i], dp[j]+nums[i])
return max(dp)
if len(nums)==1:
return nums[0]
if len(nums)==2:
return max(nums)
max_dp1 = dp_f(nums[0:len(nums)-1])
max_dp2 = dp_f(nums[1:len(nums)])
return max(max_dp1, max_dp2)

分开来做就好

打家劫舍 III[337]

位于 https://leetcode.cn/problems/house-robber-iii
递归法,实现思路:用root点和不用root点

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
# 用root点
money = root.val
if root.left:
money = money + self.rob(root.left.left) + self.rob(root.left.right)
if root.right:
money = money + self.rob(root.right.left) + self.rob(root.right.right)
return max(money, self.rob(root.left)+self.rob(root.right)) #不用root点

递归优化法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def __init__(self):
self.d = {}

def rob(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
if root in self.d:
return self.d[root]
money = root.val
if root.left:
money = money + self.rob(root.left.left) + self.rob(root.left.right)
if root.right:
money = money + self.rob(root.right.left) + self.rob(root.right.right)
result = max(money, self.rob(root.left)+self.rob(root.right))
self.d[root] = result
return result

背包零钱

背包问题介绍

可以看如下几个连接,增加对某些点的理解

整体上,背包问题的动态规划写成如下的形式:

0-1背包

1
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w_i]+v_i, 0<=w_i<=j)

完全背包

1
dp[i][j]=max(dp[i-1][j],dp[i][j-w_i]+v_i, 0<=w_i<=j)

注意:在使用的时候,大部分组合问题,因此for两层循环的话,外层是N个物件或者N种币,内层是背包的容量W或者是要凑的零钱大小W。关于组合还有排序的问题可以看 https://leetcode.cn/problems/coin-change-ii/solutions/143948/ling-qian-dui-huan-iihe-pa-lou-ti-wen-ti-dao-di-yo/

逻辑分开

做题的逻辑哈
图片1.png

模板

1
2
3
4
5
6
7
8
9
10
11
12
# 二维
dp[0][0] = x
for i in range(1, n+1):#注意加1
for j in range(amount+1): #注意加1
if xx:
dp[i][j] = min(dp[i-1][j] , dp[i][j-coins[i-1]]+1) # 注意i-1
else:
...
# 一维
for i in coins: # 1. 遍历硬币
for j in range(amount,i-1, -1): # 2.遍历金额,逆序,注意最小值
dp[j] = min,max,..., dp[j], dp[j-i] # 3. 写动态函数

0-1背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 二维
N = 3 # 物品数量
W = 4 # 背包容量
Wt = [2, 1, 3] # 所占用的容量
val = [4, 2, 3] # 价值
dp = [[0] * (W + 1) for _ in range(N + 1)]
for i in range(1, N + 1):
for w in range(1, W + 1):
if w - Wt[i - 1] < 0:
dp[i][w] = dp[i - 1][w]
else:
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - Wt[i - 1]] + val[i - 1])
# 【推荐写法】一维
def test_1_wei_bag_problem():
weight = [1, 3, 4]
value = [15, 20, 30]
bagWeight = 4

# 初始化
dp = [0] * (bagWeight + 1)
for i in range(len(weight)): # 遍历物品
for j in range(bagWeight, weight[i] - 1, -1): # 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

print(dp[bagWeight])

目标和[494]

位于 https://leetcode.cn/problems/target-sum

分析:0-1背包,外循环硬币,内循环金额,内循环逆序,注意内循环最小值

dp累加dp[i] = dp[i] + dp[j-i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 二维
class Solution(object):
def findTargetSumWays(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
total = sum(nums)
if abs(target) > total:
return 0
if (total+target)%2==1:
return 0
pos = (total + target) // 2
neg = (total - target) // 2
C = min(pos, neg) # 金额
n = len(nums) # 硬币数量

dp = [[0]*(C+1) for _ in range(n+1)]
dp[0][0] = 1
for i in range(1, n+1):
for j in range(C+1):
if j >= nums[i-1]:
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]] # 0-1背包
else:
dp[i][j] = dp[i-1][j]
return dp[n][C]

# 【推荐写法】一维
class Solution(object):
def findTargetSumWays(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
total = sum(nums)
if abs(target) > total:
return 0
if (total+target)%2==1:
return 0
pos = (total + target) // 2
neg = (total - target) // 2
C = min(pos, neg) # 金额
n = len(nums) # 硬币数量

dp = [0] * (C+1)
dp[0] = 1
for i in nums: # 1. 外循环:遍历硬币
for j in range(C, i-1, -1): # 2. 内循环 逆序:遍历目标金额,最小为i-1一定
dp[j] = dp[j] + dp[j-i]
return dp[C]

分割等和子集[416]

位于 https://leetcode.cn/problems/partition-equal-subset-sum

分析:0-1背包,外循环硬币,内循环金额,内循环逆序,注意内循环最小值

dp累求|即可,为dp[i] = dp[i] | dp[j-i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 一维动态规划
class Solution(object):
def canPartition(self, nums):
# 边界
total = sum(nums)
if total % 2==1:
return False
target = total // 2
if max(nums) > target:
return False
# dp
dp = [False] * (target+1)
dp[0] = True

for num in nums: # 1.遍历物品即硬币
for j in range(target, num-1, -1): # 2. 遍历目标(金额),逆序,最小为num-1
dp[j] |= dp[j-num] # 3. 动规表达式
return dp[target]

最后一块石头的重量 II[1049]

位于 https://leetcode.cn/problems/last-stone-weight-ii

分析:0-1背包,外循环硬币,内循环金额,内循环逆序,注意内循环最小值

dp为 dp[j] = max(dp[j], dp[j - stone] + stone)

分析:本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。分析到物品的重量为stones[i],物品的价值也为stones[i]。

dp数组含义,表示前i个stone得到不超过taget的最大重量,这里是最大,不是刚刚好。

1
2
3
4
5
6
7
8
9
10
11
12
# 一维动态规划
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
dp = [0] * 15001
total_sum = sum(stones)
target = total_sum // 2

for stone in stones: # 1. 遍历物品
for j in range(target, stone - 1, -1): # 2. 遍历背包,逆序,最小为stone-1
dp[j] = max(dp[j], dp[j - stone] + stone)

return total_sum - dp[target] - dp[target]

一和零[474]

位于 https://leetcode.cn/problems/ones-and-zeroes
分析:0-1背包,外循环硬币,内循环金额,内循环逆序,注意内循环最小值

dp表达式为 dp[m][n] = max(dp[m][n], dp[m-neg][n-pos]+1)

分析:从一个list的字符串中选择一些数,使得1的数不超过m,0的数不超过n.
dp[i][j]表示不超过i和j的最大集合的长度,和上面类似,这是这里二维。

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def findMaxForm(self, strs, m, n):
dp = [[0]*(n+1) for _ in range(m+1)]
for s in strs: # 1. 遍历硬币
pos = s.count('1')
neg = len(s) - pos
for m1 in range(m,neg-1,-1): # 2. 遍历金额,逆序,最小值
for n1 in range(n,pos-1,-1):# 3. 遍历金额,逆序,最小值
dp[m1][n1] = max(dp[m1][n1], dp[m1-neg][n1-pos]+1)
return dp[m][n]

完全背包

1
2
3
4
5
6
7
8
9
10
11
N = 3  # 物品数量
W = 4 # 背包容量
Wt = [2, 1, 3] # 所占用的容量
val = [4, 2, 3] # 价值
dp = [[0] * (W + 1) for _ in range(N + 1)]
for i in range(1, N + 1):
for w in range(1, W + 1):
if w - Wt[i - 1] < 0:
dp[i][w] = dp[i - 1][w]
else:
dp[i][w] = max(dp[i - 1][w], dp[i][w - Wt[i - 1]] + val[i - 1])

零钱兑换[322]

位于 https://leetcode.cn/problems/coin-change

分析:完全背包,外循环硬币,内循环金额,内循环顺序

dp公式dp[j] = min(dp[j],dp[j-i]+1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 二维解法
class Solution(object):
def coinChange(self, coins, amount):
# 1. 初始化
N = len(coins)
dp = [[float("inf")]*(amount+1) for _ in range(N+1)]
dp[0][0] = 0
# 2. 循环
for i in range(1,N+1):
for j in range(amount+1):
if j >= coins[i-1]:
dp[i][j] = min(dp[i-1][j] , dp[i][j-coins[i-1]]+1)
else:
dp[i][j] = dp[i-1][j]
ans = dp[N][amount]
return ans if ans!=float("inf") else -1
# 一维解法I
class Solution(object):
def coinChange(self, coins, amount):
# 1. 初始化
N = len(coins)
dp = [float("inf")]*(amount+1)
dp[0] = 0
# 2. 循环
for i in range(N):
for j in range(amount+1):
if j >= coins[i]:
dp[j] = min(dp[j], dp[j-coins[i]]+1)
return dp[amount] if dp[amount]!=float("inf") else -1
# 一维解法II
class Solution(object):
def coinChange(self, coins, amount):
if amount==0:
return 0
dp = [float('inf')] * (amount+1)
dp[0] = 0
for i in coins:
for j in range(i, amount+1):
dp[j] = min(dp[j],dp[j-i]+1)

return dp[amount] if dp[amount]!=float('inf') else -1

注意一维和二维中,在二维初始化的时候长度是amount+1, 一维也是,但是在N这个地方,二维是N+1,一维是N。

解法 https://leetcode.cn/problems/coin-change/solutions/1412324/by-flix-su7s/

零钱兑换II[518]

位于 https://leetcode.cn/problems/coin-change-ii

分析:完全背包,外循环硬币,内循环金额,内循环顺序

dp公式dp[j] = dp[j] + dp[j-i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 二维
class Solution(object):
def change(self, amount, coins):
# 1. 初始化
N = len(coins)
dp = [[0]*(amount+1) for _ in range(N+1)] # 方便后面取数
dp[0][0] = 1
# 2. 循环
for i in range(1, N+1):
for j in range(amount+1):
if coins[i-1] > j:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]
return dp[N][amount]

当然,官方简单的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1. 压缩为1维
class Solution(object):
def change(self, amount, coins):
if not amount:
return 1
# 1. 初始化
dp = [0] * (amount+1)
dp[0] = 1
# 2. 循环
N = len(coins)
for i in range(N):
for j in range(amount+1):
if j >= coins[i]:
dp[j] = dp[j] + dp[j-coins[i]]
return dp[amount]
# 2.再简化
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
if not amount:
return 1
dp = [0] * (amount+1)
dp[0] = 1
for i in coins:
for j in range(i, amount+1):
dp[j] = dp[j] + dp[j-i]
return dp[amount]

硬币问题[面试题 08.11]

题目见 https://leetcode-cn.com/problems/coin-lcci 题解如下:

1
2
3
4
5
6
7
8
9
10
class Solution:
def waysToChange(self, n: int) -> int:
mod = 10**9 + 7
coins = [25, 10, 5, 1]

f = [1] + [0] * n
for coin in coins:
for i in range(coin, n + 1):
f[i] += f[i - coin]
return f[n] % mod

分割数组的最大值[410]

位于 https://leetcode.cn/problems/split-array-largest-sum
分析:完全背包,外循环硬币,内循环金额,内循环顺序

dp公式f[i][j] = min(f[i][j], max(f[k][j - 1], sub[i] - sub[k]))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
n = len(nums)
f = [[10**18] * (m + 1) for _ in range(n + 1)]
sub = [0]
for elem in nums:
sub.append(sub[-1] + elem)

f[0][0] = 0
for i in range(1, n + 1):
for j in range(1, min(i, m) + 1):
for k in range(i):
f[i][j] = min(f[i][j], max(f[k][j - 1], sub[i] - sub[k]))

return f[n][m]

组数总和Ⅳ

位于 https://leetcode.cn/problems/combination-sum-iv
分析:完全背包,外循环金额【排列问题】,内循环硬币,内循环顺序

dp公式dp[i] = dp[i] + dp[i-j]

题目见 https://leetcode-cn.com/problems/combination-sum-iv/ 这道题和爬楼梯的问题有点类似,但是解法不太一样。动态规划的方程是
dp[i] = sum(dp[i-j]) j<=i.解法如下

1
2
3
4
5
6
7
8
9
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [0] * (target+1)
dp[0] = 1
for i in range(1,target+1):
for j in nums:
if i >= j:
dp[i] = dp[i] + dp[i-j]
return dp[-1]

爬楼梯[70]

位于 https://leetcode.cn/problems/climbing-stairs
分析:完全背包,外循环金额【排列问题】,内循环硬币,内循环顺序

dp公式dp[i] = dp[i] + dp[i-j]

1
2
3
4
5
6
7
8
target = 10 # 爬到阶梯数
w = [1, 2, 3] # 每次可以爬多少
dp = [0] * (target + 1)
for i in range(target + 1):
for j in range(len(w)):
if i - j >= 0:
dp[i] = dp[i] + dp[i - j]
print(dp[10])

剪绳子

题目见

股票买卖问题

模板

这里主要说一下DP的思路,按照labuladong的做法,基本上上可以在一个模板上丝毫不动,就可以得到结果

统一的动态规划方程如下

1
2
3
4
5
6
7
8
9
10
11
# 初始化
dp = [[0]*2 for _ in range(len(prics))]
for i in range(len(prices)):
dp[i][0] = 0
dp[i][1] = -float("inf")
if i==0:
dp[i][0] = 0
dp[i][1] = -prics[0] - 手续费
# for一下
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i] - 手续费)

i表示第几天,k表示最多交易次数,0表示手里没股票了,1表示手里还有股票。

https://labuladong.github.io/algo/di-er-zhan-a01c6/yong-dong--63ceb/yi-ge-fang-3b01b/

买卖股票的最佳时机[121]

题目:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 模板解法
class Solution(object):
def maxProfit(self, prices):
# dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
# dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
# k=1简化为
# dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
# dp[i][1] = max(dp[i-1][1], -prices[i])
dp = [[0]*2 for i in range(len(prices))]
for i in range(len(prices)):
if i == 0:
dp[i][0] = 0
dp[i][1] = -prices[0]
continue
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
return dp[len(prices)-1][0]
# 更简单做法
class Solution(object):
def maxProfit(self, prices):
mins = float("inf")
profit = 0
for i in prices:
if i < mins:
mins = i
profit = max(profit, i - mins)
return profit

买卖股票的最佳时机II[122]

题目:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 模板做法
class Solution(object):
def maxProfit(self, prices):
# dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
# dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
# k=1简化为
# dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
# dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]) #改动点1
dp = [[0]*2 for i in range(len(prices))]
for i in range(len(prices)):
if i == 0:
dp[i][0] = 0
dp[i][1] = -prices[0]
continue
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])#改动点2
return dp[len(prices)-1][0]
# 更简单做法
class Solution(object):
def maxProfit(self, prices):
profit = 0
for i in range(1, len(prices)):
if prices[i] - prices[i-1] > 0:
profit += prices[i] - prices[i-1]
return profit

买卖股票的最佳时机含冷冻期[309]

题目:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 模板做法
class Solution(object):
def maxProfit(self, prices):
# dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
# dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
# k=1简化为
# dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
# dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
dp = [[0]*2 for i in range(len(prices))]
for i in range(len(prices)):
if i == 0:
dp[i][0] = 0
dp[i][1] = -prices[0]
continue
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0]-prices[i]) # 改动点
return dp[len(prices)-1][0]

买卖股票的最佳时机含手续费[714]

题目:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution(object):
def maxProfit(self, prices, fee):
# dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
# dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
# k=1简化为
# dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
# dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
dp = [[0]*2 for i in range(len(prices))]
for i in range(len(prices)):
if i == 0:
dp[i][0] = 0
dp[i][1] = -prices[0] - fee # 改动点
continue
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]-fee) # 改动点
return dp[len(prices)-1][0]

买卖股票的最佳时机 III[123]

题目:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution(object):
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
# dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
# dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
# k=1简化为
# dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
# dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
max_k = 2
dp = [[[0] * 2 for _ in range(max_k + 1)] for _ in range(len(prices))]
for i in range(len(prices)):
for k in range(1, max_k+1): # 改动点
if i == 0:
dp[i][k][0] = 0
dp[i][k][1] = -prices[0]
continue
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])
return dp[len(prices)-1][max_k][0]

博弈问题

https://labuladong.online/algo/dynamic-programming/game-theory/

预测赢家[486]

题目:https://leetcode.cn/problems/predict-the-winner/description/

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def predictTheWinner(self, nums: List[int]) -> bool:
length = len(nums)
dp = [[0] * length for _ in range(length)]
for i, num in enumerate(nums):
dp[i][i] = num
for l in range(1,length):
for i in range(length-l):
j = i + l
dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])
return dp[0][length - 1] >= 0

石子游戏[877]

题目:https://leetcode.cn/problems/stone-game/description/

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
length = len(piles)
dp = [[0] * len(piles) for _ in range(len(piles))]
for i in range(len(piles)):
dp[i][i] = piles[i]
for l in range(1,length):
for i in range(length-l):
j = i + l
dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1])
return dp[0][length - 1] > 0

和上面一样,严格意义上,不算双序列问题,所以这里不用dp=[[0]*(n+1) for i in range(n+1)]