实训报告 2nd

最近主要按照师兄的指导,读论文了解ResNet,读TensorFlow官方的ResNet实现,简单修改网络以及了解DeepFashion数据集。

1. 残差网络

1.1. 理解残差网络

1.1.1. The deeper, the better

已经有大量实验以及实践证明,网络的层级结构越深,网络的表现就越好。

1.1.2. The deeper the network, the harder it is to train

既然网络深度如此重要,那么训练出更好的网络,岂不是就和不断堆叠网络深度一样简单?

当然不是!不断加深的网络会带来两个问题:

  • 梯度消失/爆炸 Vanishing/Exploding Gradients

    更深的网络遇到的第一个问题就是臭名昭著的梯度消失/爆炸问题,它使得网络一开始就难以收敛。不过现在这个问题很大程度上已经可以通过采用标准初始化 Normalized Initialization中间层标准化 Intermediate Normalization解决。

  • 退化 Degradation

    虽然通过上述的方法,我们可以使数十层的网络通过随机梯度下降 SGD开始收敛 ,但是退化现象接踵而至:随着网络深度的增加,模型的准确度会在某个深度达到饱和,之后迅速下降。我们暂时把准确度饱和时的深度称为适宜深度,退化现象表现为,当网络深度超过适宜深度时,训练误差会越来越高,这也会导致更高的测试误差。如下图:

    1524117726549

    这是20层和56层的简单网络再CIFAR-10数据集上的训练误差和测试误差。可以看出,由于退化现象,更深的网络反而表现更差。

1.1.3. 残差映射 Residual Mapping

通常情况下,我们正利用数个堆叠的层拟合一个目标映射$\mathcal{H}(x)$。但在深度残差学习框架 deep residual learning framework 中,我们不直接拟合目标映射$\mathcal{H}(x)$,而是这几个堆叠的层拟合另一个映射$\mathcal{F}(x) := \mathcal{H}(x) - x$。此时,目标映射被重写为$\mathcal{F}(x) + x$。

这里有一个假设:残差映射$\mathcal{H}(x)-x$比原映射$\mathcal{H}(x)$更容易优化。

那么为什么要使用残差学习?

引用theone的直觉描述:

假设这个网络的目标映射是输出5,输出5.1,即$\mathcal{H}(5)=5.1$。如果直接拟合这个映射,那么可以得到一个拟合结果,函数$\mathcal{F’}(5)=5.1$。引入残差后,拟合结果的函数变为$\mathcal{F}(5)=0.1$(其中$\mathcal{H}(5)=\mathcal{F}(5)+5$ )。

现在假设输出的要求从5.1变为了5.2,即$\mathcal{H}(5)=5.2$。那么直接拟合的函数$\mathcal{F’}(5)$从5.1到5.2变化了$1/51\approx0.2$,而引入残差的映射$\mathcal{F}(5)$从0.1到0.2变化了100%。可以看出,引入残差映射对于输出的变化更加敏感,对权重的调整作用更大

因此,残差学习的思想可以理解为:去掉相同的主体部分,从而突出微小的变化。

1.1.4. 快捷连接 Shortcut Connections

上述映射$\mathcal{F}(x)+x$由一个快捷连接实现,如下图:

1524120342408

在这里,快捷连接是一个恒等映射,直接被添加到整个堆叠层的输出$\mathcal{F}(x)$上,得到$\mathcal{F}(x)+x$。恒等快捷连接不仅不增加额外的参数,而且不增加计算复杂度。

公式表示为:

$$y=\mathcal{F}(x, {W_i}) + x$$

这里要求$x$与$\mathcal{F}(x)$具有相同的维度,如果维度不同,则使用简单的线性投影$W_s$来匹配维度:

$$y=\mathcal{F}(x,W_i)+W_sx$$

为什么要使用恒等映射?

实际上在ResNet原团队的另一篇论文[2]中解释了这个问题。在文章中,作者设计了六种快捷连接的映射:

1524134061037

实验证明,恒等映射的误差衰减最快,误差也最小,其他快捷连接的形式都产生了较大的误差(有的甚至超过了20%):

1524134192442

1.1.5. 构建块 Building Block

像上一小节中一个残差单元的结构,被称为一个构建块 Building Block。此外还有另一种瓶颈构建块 Bottleneck Building Block

1524204482795

这种构建块由三个卷积层堆叠而成,第一个和最后一个$1\times1$卷积层负责减小或恢复维度,中间的$3\times3$卷积层负责提取特征。这样的构建块通常用于更深的网络中。

1.2. 官方模型

以下代码来自于TensorFlow官方给出的ResNet实现,用于训练CIFAR-10数据集。接下来将通过全部使用默认配置 情况下,了解ResNet是如何被构建的。

1.2.1. 配置解释

在默认情况下,运行python cifar10_main.py,使用的是如下配置:

  • data_dir=/tmp/cifar10_data 决定数据集的位置,这个参数对模型没有影响,不讨论。
  • model_dir=/tmp/cifar10_model 决定模型存储的位置,这个参数对模型没有影响,不讨论。
  • resnet_size=32 模型中卷积层的数量
  • train_epochs=250 针对整个数据集的训练次数
  • epochs_between_evals=10 这个参数对模型没有影响,不讨论。
  • batch_size=128 每次从训练集中取出的样本数

卷积层的数量

第三个参数resnet_size=32表示卷积层的数量,在官方实现中,对这个数量有一个限制:

1
2
3
# cifar10_main.py
if resnet_size % 6 != 2:
raise ValueError('resnet_size must be 6n + 2:', resnet_size)

也就是说,卷积层的数量必须是6n+2。为什么有这样的限制呢?

首先解释6n。通过阅读ResNet的论文[1],我们可以知道,有两种构建块——普通的构建块具有两个卷积层、瓶颈构建块具有三个卷积层。

假设我们只使用具有两个卷积层的普通构建块,那么只需要要求卷积层满足2n即可,因为每个构建块有两个卷积层,构建块是不可分的,所以有n个构建块则表示有2n个卷积层,这很好理解。同样的道理,如果我们只使用有三个卷积层的瓶颈构建块,则要求卷积层有3n个。

但由于官方的ResNet同时支持使用两种构建块,为了实现这一点,就只能找2n3n的最小公倍数——6n

接着解释为什么要+2。通过上面的解释,可以得出一个结论:6n这个数量表示的是构建块中卷积层的总数。而+2自然得就可以看做是网络中其他部分的卷积层总数,查看源码,可以看到这两段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# resnet_model.py
# 构建块之前的卷积,用于维度转换
inputs = conv2d_fixed_padding(...)

...

# 构建块
for i, num_blocks in enumerate(self.block_sizes):
num_filters = self.num_filters * (2**i)
inputs = block_layer(...)

...

# 构建块之后的平均池化
# 平均池化是卷积的一种
inputs = tf.layers.average_pooling2d(...)

从代码可以看出,除了在构建块的前后分别有一次用于维度转换的卷积核一次平均池化(卷积的一种),这就是+2的来源。

因此可以得出,在这种实现中卷积层数数量一定满足6n+2

其他参数

另外另个值得一提的参数是train_epochsbatch_size。前者指的是对于整个训练集,模型进行训练的次数;后者则表示,每次从训练集中取出的样本数。

可以很简单的得到一个公式,计算总的训练步数:

$$step = \frac{train_epochs\times dataset_size}{batch_size}$$

将默认数据,train_epochs=250batch_size=128以及CIFAR-10的训练集大小dataset_size=50000带入可以算出,step=97656.25

实际的训练情况如下:

1524205891688

题外话

可以通过日志看到,在GeForce GTX 1080上,训练速度大概如下(大概20step/s):

1524206243857

所以总的训练时间大概是97656/20=4882秒,大概一个半小时

1.2.2. 网络结构

1) 残差结构之前

输入

先搞清楚整个网络的输入。输入来自CIFAR-10的训练集,在这个实现中,图片被进行了多次预处理(包括随机剪裁、随机镜像反转、标准化等),最后形成了$3\times32\times32$的矩阵,或者写成[3, 32, 32],对应[channels, width, height]。实现中还处理了channels_lastchannels_first的问题,这里为了方便就认为所有输入图片都是channels_first。此外,每次从数据集中取出的是batch_size=128张图,所以实际上的输入矩阵为[128, 3, 32, 32],对应[batch, channels, width, height]

网络中使用的卷积

网络中并不是直接使用TensorFlow的卷积实现,而是定义了一个名为conv2d_fixed_padding的卷积操作。逻辑如下:

  • 如果strides=1,则通过padding保证卷积前后size一致
  • 如果strides>=1,则通过padding使得卷积后的特征图的size变为$\lceil \frac{size}{strides} \rceil$

通过读源码可知,实际上是通过padding将kernel_size的影响抵消,过程比较繁琐就不写出来了。

举个简单的例子,如果输入为[128, 3, 32, 32],卷积核数量为16

  • strides=1,输出位[128, 16, 32, 32],size依旧为32*32
  • strides=3,输出位[128, 16, 11, 11],其中$11 = \lceil \frac{32}{3} \rceil$

在之后的描述中,所有的卷积都指的是conv2d_fixed_padding

第一次卷积

来自训练集的输入[128, 3, 32, 32]在这里经历了第一次卷积,参数如下:

  • strides = 1
  • filters = 16
  • kernel_size = 3

这次卷积的目的主要是进行维度的转换,卷积的的结果是[128, 16, 32, 32]。

最大池化

实现中实际上定义了最大池化的操作,但是由于默认配置下,这个操作并不会被执行,所以这里忽略它。

残差层之前总结

1524208974147

2) 残差结构

这是网络的重点,整个残差结构分为多个残差层,每个残差层分为多个构建块,每个构建块呢有多个卷积层,每个卷积层内有多个卷积核。下面将逐一进行介绍。

残差层

在一个残差结构中,有多个残差层。我们采用默认配置的情况下,有三个残差层。

实际上在官方实现的ResNet中,残差层被固定为3层:

1
2
3
4
5
6
# cifar10_main.py
super(Cifar10Model, self).__init__(
...,
block_sizes=[num_blocks] * 3,
block_strides=[1, 2, 2]
)

这是构建模型时的代码,其中block_sizesblock_strides的长度就是残差层的数量,被固定为了3层。

1524212942030

每个残差层内部,特征图的维度是一致的,但是在残差层之间特征图维度不同。

构建块

每个残差层内部有多个构建块,我们采用默认配置的情况下,每个残差层有5个构建块。

1
2
3
4
5
6
7
# cifar10_main.py
num_blocks = (resnet_size - 2) // 6
super(Cifar10Model, self).__init__(
...,
block_sizes=[num_blocks] * 3,
block_strides=[1, 2, 2]
)

同样是这段代码,其中num_blocks代表每一层中构建块的数量,可以看出是通过卷积层的数量resnet_size计算出来的。默认情况下resnet_size=32,所以num_blocks=5,每个残差层有5个构建块。

1524213398594

从上文我们可以知道,论文[1]中构建块共有两种实现,分别为普通构建块v1和瓶颈构建块v1,在另一篇论文[2]中,这两种构建块有不同的实现——普通构建块v2和瓶颈构建块v2。v1和v2两种实现的主要区别是:BN层是在卷积操作之前执行还是之后执行,具体可以见论文[2]。在这四中构建块中,根据默认配置:

1
2
3
4
5
6
# cifar10_main.py
DEFAULT_VERSION = 2
super(Cifar10Model, self).__init__(
...,
bottleneck=False
)

可以得出,使用的是普通构建块v2

1524215412114

卷积层和卷积核

由上图,每个构建块都由两次卷积操作构成,每次卷积操作之前都会进行BN和ReLU,最后叠加一个快捷连接的恒等映射,得出结果。也就是说,每个构建块中有两个卷积层。

第一个卷积层中的每个卷积核的参数如下:

  • 第一个残差层中:strides=1kernel_size=3filters=16
  • 第一个残差层中:strides=2kernel_size=3filters=32
  • 第一个残差层中:strides=2kernel_size=3filters=64

其中strides和kernel_size是在再如下代码中定义:

1
2
3
4
5
6
# cifar10_main.py
super(Cifar10Model, self).__init__(
...,
kernel_size=3,
block_strides=[1, 2, 2]
)

filters则通过以下代码定义:

1
2
3
4
5
6
7
8
9
# cifar10_main.py
super(Cifar10Model, self).__init__(
...,
num_filters=16,
block_sizes=[num_blocks] * 3
)
# resnet_model.py
for i, num_blocks in enumerate(self.block_sizes):
num_filters = self.num_filters * (2**i)

可以看出,在每个残差层,filters数量都会加倍。

卷积代码:

1
2
3
4
inputs = conv2d_fixed_padding(
inputs=inputs, filters=filters, kernel_size=3, strides=strides,
data_format=data_format
)

第二个卷积层中的卷积核,参数与第一层中的基本一致,唯一的区别是,所有的卷积核的strides=1

1
2
3
4
inputs = conv2d_fixed_padding(
inputs=inputs, filters=filters, kernel_size=3, strides=1,
data_format=data_format
)
残差层之间的投影

由于残差层之间,输入和输出存在维度不同的问题,所以每个残差层的第一个构建块都需要进行投影。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 投影函数
def projection_shortcut(inputs):
return conv2d_fixed_padding(
inputs=inputs, filters=filters_out, kernel_size=1, strides=strides,
data_format=data_format)

# 每个残差层的第一个构建块进行投影
inputs = block_fn(inputs, filters, training, projection_shortcut, strides,
data_format)

# 其余的构建块不需要投影
for _ in range(1, blocks):
inputs = block_fn(inputs, filters, training, None, 1, data_format)
结果

1524241808617

经过残差结构之后,输入和输出情况如上图。

3) 残差结构之后

残差结构之后就是简单的BN层、ReLU层、平均池化层和全连接层:

1524241923499

4) 总体网络结构

1524241957064

题外话

PPT是世界上最好用的作图工具23333,上图的PPT源文件链接

1.3. 修改官方模型

得益于官方模型高可用的实现,对这个模型进行简单修改非常简单:

  • 增减卷积层/残差结构:在运行时加入参数--resnet_size=m,这里m就是卷积层的数量,必须满足m % 6 == 2。可以得出整个网络中构建块的数量为block_num = (m - 2) // 2。最终卷积层的数量实际上的与你采用什么构建块有关,如果使用普通构建块,则卷积层数量为block_num * 2,如果采用瓶颈构建块则为block_num * 3

  • 使用不同的构建块:默认使用的是普通构建块v2,可以通过修改resnet_model.py中的DEFAULT_VERSION使用不同的版本;通过修改cifar10_main.py中的bottleneck=True使用不同的构建块。

  • 训练不同的数据集:网络的结构不需要过多的修改,主要修改的是cifar10_main.py数据输入的处理函数:

    • get_filenames
    • parse_record
    • preprocess_image
    • input_fn
    • get_synth_input_fn

    以及修改一些全局变量:

    • _NUM_CLASSES

2. DeepFashion

2.1. 简介

DeepFashion是一个关于服饰的超大规模数据集,具有以下特点:

  • DeepFashion拥有超过800,000张时尚服饰图片,包括良好的店铺图片和消费者拍摄的不受约束的图片。
  • DeepFashion拥有50个分类(categories)、1000中属性(attributes)以及服饰关键点(clothing landmarks)。
  • DeepFashion拥有超过300,000对交叉/跨域的服饰图,例如同一个服饰来自商家和消费者的一对图片。

此外,DeepFashion还有五个基准测试问题(Benchmark),包括:类别和属性预测、店铺服饰检索、消费者对店铺服饰检索、服饰关键点检测和综合测试。

2.2. 基准测试问题

DeepFashion目前有五个基准测试问题:

  • 类别和属性预测 Category and Attribute Prediction Benchmark,这项任务是对50个类别和1,000个属性进行分类。
  • 店铺服饰检索 In-shop Clothes Retrieval Benchmark,这个任务是确定源自店铺拍摄的两幅图片是否属于相同的服饰。
  • 消费者对店铺服饰检索 Consumer-to-shop Clothes Retrieval Benchmark,这项任务是将来自消费者图片与来自店铺的相同服饰的图片进行匹配。
  • 服饰关键点检测 Fashion Landmark Detection Benchmark,这项任务是预测服饰上关键点的位置,例如领口,下摆和袖口的位置。
  • 综合测试 Fashion Synthesis Benchmark,这项任务是利用已有的图片生成新的图片。

2.2. 店铺服饰检索

2.2.1. 图片

In-shop Clothes Retrieval Benchmark的图片格式为:

  • 居中的256*256 JPG格式图片
  • 原始图像的纵横比保持不变

2.2.2. 标注

In-shop Clothes Retrieval Benchmark的数据集中标注如下:

  • 图片中服饰的边界盒 list_bbox_inshop.txt,第一行为数量,第二行为列名

    • 图片名,如img/WOMEN/Blouses_Shirts/id_00000001/02_1_front.jpg

    • 服饰类型

      | 值 | 意义 |
      | —- | ———- |
      | 1 | upper-body |
      | 2 | lower-body |
      | 3 | full-body |

    • 姿势类型

      | 值 | 意义 |
      | —- | —————- |
      | 1 | frontal view |
      | 2 | side view |
      | 3 | back view |
      | 4 | zoom-out view |
      | 5 | zoom-in view |
      | 6 | stand-alone view |

    • 边界盒位置

      | 符号 | 意义 |
      | ——– | —————– |
      | x_1, y_1 | upper left point |
      | x_2, y_2 | lower right point |

  • 图片中服饰的关键点 list_landmarks_inshop.txt,第一行为数量,第二行为列名

    • 图片名,如img/WOMEN/Blouses_Shirts/id_00000001/02_1_front.jpg

    • 服饰类型

      | 值 | 意义 |
      | —- | ——————— |
      | 1 | upper-body,6个关键点 |
      | 2 | lower-body,4个关键点 |
      | 3 | full-body,8个关键点 |

    • 变化类型

      | 值 | 意义 |
      | —- | ————– |
      | 1 | normal pose |
      | 2 | medium pose |
      | 3 | large pose |
      | 4 | medium zoom-in |
      | 5 | large zoom-in |

    • 关键点可见性

      | 值 | 意义 |
      | —- | —————— |
      | 1 | visible |
      | 2 | invisible/occluded |
      | 3 | truncated/cut-off |

    • 关键点

      | 服饰类型 | 数量 | 意义 |
      | ——– | —- | ———————————————————— |
      | 1 | 6 | left collar, right collar, left sleeve, right sleeve, left hem, right hem |
      | 2 | 4 | left waistline, right waistline, left hem, right hem |
      | 3 | 8 | left collar, right collar, left sleeve, right sleeve, left waistline, right waistline, left hem, right hem |

  • 服饰ID集合 list_item_inshop.txt,第一行为数量

    • ID,如id_00000001
  • 服饰描述 list_description_inshop.json

    • ID,如id_00000001
    • 颜色,如Cream
    • 描述,如Style Deals - When temps start to rise...
  • 属性集合 list_attr_cloth.txt 第一行位属性数量,第二行为列名

    • 属性名,如lightweight
  • 服饰属性 list_attr_items.txt

    • 还有若干个值,数量等于属性集合中属性的数量

      | 值 | 意义 |
      | —- | —————— |
      | 1 | 服饰有对应的属性 |
      | 2 | 服饰没有对应的属性 |

  • 映射关系 list_eval_partition.txt,第一行为数量,第二行为列名

    • 图片名,如img/WOMEN/Dresses/id_00000002/02_1_front.jpg

    • 物品ID,如id_00000002

    • 状态

      | 值 | 意义 |
      | ——- | ————– |
      | train | training image |
      | query | query image |
      | gallery | gallery image |

  • 图片中服饰的颜色 list_color_cloth.txt,第一行为数量,第二行为列名

    • 图片名,如img/WOMEN/Blouses_Shirts/id_00000001/02_1_front.jpg
    • 颜色,如Cream

2.2.3. 对标注的理解

1524298234121

3. 参考

[1]Deep Residual Learning for Image Recognition

[2]Identity Mappings in Deep Residual Networks

[3]知乎|theone的回答

[4]DeepFashion: Powering Robust Clothes Recognition and Retrieval with Rich Annotations

[5]Fashion Landmark Detection in the Wild