专家如何设计 CNN?如何在初始模块、Dropout 正则化、批量标准化、卷积滤波器大小、卷积通道的大小和深度、全连接层数、激活神经元等之间进行选择?人们如何以科学的方式驾驭这个大型优化问题?组合无穷无尽。
您确实说过,组合的数量很大。如果没有正确接近,您可能会一事无成。一位伟大的人说机器学习是一门艺术,而不是科学。结果取决于数据。以下是关于您上述问题的一些提示。
-
Log Everything:在训练时间,保存每个实验的必要日志,例如训练损失、验证损失、权重文件、执行时间、可视化等。其中一些可以用@保存987654329@、ModelCheckpoint 等。TensorBoard 是检查训练日志和可视化等的绝佳工具。
-
强大的验证策略:这非常重要。要构建稳定的交叉验证 (CV),我们必须对数据和面临的挑战有充分的了解。我们将检查并确保验证集与训练集和测试集具有相似分布。我们将努力确保我们的模型在我们的 CV 和 测试集 上both(如果 gt 可用于测试集)。基本上,随机划分数据通常不足以满足这一点。了解数据以及我们如何在不引入数据泄漏的情况下对数据进行分区是避免过度拟合的关键。
-
仅更改一项:在实验期间,一次更改一件事并保存这些更改的观察结果 (logs)。例如:将图像大小从224(例如)逐渐更改为更高并观察结果。我们应该从一个小的组合开始。在尝试图像大小时,修复其他的,如model 架构、learning rate 等。learning rate 部分或model 架构也是如此。但是,稍后当我们得到一些有希望的组合时,我们可能还需要更改多个。在 kaggle 比赛中,这些是人们会遵循的非常常见的方法。下面是一个非常简单的例子。但它不受任何限制。
但是,正如您所说,您的博士学位。项目是减少地球上的二氧化碳排放量。据我了解,这些问题更多的是特定于应用程序的问题,而不是特定于算法的问题。因此,我们认为最好从公认的预训练模型中获益。
如果我们想自己写CNN,我们应该花点时间写。从一个非常简单的开始,例如:
Conv2D (16, 3, 'relu') - > MaxPool (2)
Conv2D (32, 3, 'relu') - > MaxPool (2)
Conv2D (64, 3, 'relu') - > MaxPool (2)
Conv2D (128, 3, 'relu') - > MaxPool (2)
这里我们逐渐增加深度但减少特征维度。到最后一层,会出现更多的语义信息。在堆叠Conv2D 层时,通常的做法是按16, 32, 64, 128 等顺序增加通道深度。如果我们想在我们的网络中估算Inception 或Residual Block,我认为,我们应该先做一些基本的数学运算什么特征属性会由此产生,等等。按照这样的概念,我们可能还希望看看SENet、ResNeSt 等方法。关于Dropout,如果我们观察到我们的模型在训练,那么我们应该添加一些。在最后一层,我们可能希望在Flatten 层(FCC)上选择GlobalAveragePooling。我们现在大概可以理解,要获得令人满意的CNN 模型,需要进行大量的消融研究。
在这方面,我们建议您探索两个最重要的事情:(1)。阅读其中一篇关于他们构建算法的策略的预训练模型论文/博客/视频。例如:查看此EfficientNet Explained。 (2)。接下来,探索它的源代码。这会让你更有感觉,并鼓励你建立自己的巨人。
我们想以最后一个工作示例来结束这一切。请看下面的模型图,它是一个小型初始网络,source。如果我们仔细观察,我们会发现,它由以下三个模块组成。
仔细看看每个模块的配置,如filter size、strides等。让我们试着理解和实现这个模块。在此之前,这里有两个很好的参考资料(1、2),供 Inception 概念刷新概念。
转化模块
从图中我们可以看出,它由一个卷积网络、一个batch normalization和一个relu激活组成。此外,它生成C 次特征图,带有K x K 过滤器和S x S 步幅。为此,我们将创建一个继承 tf.keras.layers.Layer 类的类对象
class ConvModule(tf.keras.layers.Layer):
def __init__(self, kernel_num, kernel_size, strides, padding='same'):
super(ConvModule, self).__init__()
# conv layer
self.conv = tf.keras.layers.Conv2D(kernel_num,
kernel_size=kernel_size,
strides=strides, padding=padding)
# batch norm layer
self.bn = tf.keras.layers.BatchNormalization()
def call(self, input_tensor, training=False):
x = self.conv(input_tensor)
x = self.bn(x, training=training)
x = tf.nn.relu(x)
return x
初始模块
接下来是 Inception 模块。根据上图,它由两个卷积模块组成,然后合并在一起。现在我们知道要合并,这里我们需要确保输出特征映射维度(height 和 width)需要相同。
class InceptionModule(tf.keras.layers.Layer):
def __init__(self, kernel_size1x1, kernel_size3x3):
super(InceptionModule, self).__init__()
# two conv modules: they will take same input tensor
self.conv1 = ConvModule(kernel_size1x1, kernel_size=(1,1), strides=(1,1))
self.conv2 = ConvModule(kernel_size3x3, kernel_size=(3,3), strides=(1,1))
self.cat = tf.keras.layers.Concatenate()
def call(self, input_tensor, training=False):
x_1x1 = self.conv1(input_tensor)
x_3x3 = self.conv2(input_tensor)
x = self.cat([x_1x1, x_3x3])
return x
您可能会注意到,我们现在根据网络(图表)对两个卷积层的确切内核大小和步幅数进行了硬编码。同样在ConvModule 中,我们已经为same 设置了填充,以便两者的特征图的维度相同(self.conv1 和self.conv2);这是将它们连接到末尾所必需的。
同样,在此模块中,两个变量充当占位符,kernel_size1x1 和 kernel_size3x3。这当然是为了目的。因为我们将需要不同数量的特征映射到整个模型的不同阶段。如果我们查看模型图,我们会看到InceptionModule 在模型的不同阶段采用不同数量的过滤器。
下采样模块
最后是下采样模块。下采样的主要直觉是我们希望获得更多相关的特征信息,这些信息高度代表模型的输入。因为它倾向于删除不需要的功能,以便模型可以专注于最相关的功能。有很多方法可以减少特征图(或输入)的维度。例如:使用strides 2或使用常规的pooling操作。池化操作有很多种,分别是:MaxPooling、AveragePooling、GlobalAveragePooling。
从图中,我们可以看到下采样模块包含一个卷积层和一个最大池化层,它们随后合并在一起。现在,如果我们仔细查看图表(右上角),我们会看到卷积层采用3 x 3 大小的过滤器和strides 2 x 2。池化层(此处为MaxPooling)采用池化大小3 x 3 和strides 2 x 2。然而,公平地说,我们还确保来自它们中的每一个的维度应该相同,以便最终合并。现在,如果我们记得当我们设计ConvModule 时,我们故意将padding 参数的值设置为same。但是在这种情况下,我们需要将其设置为valid。
class DownsampleModule(tf.keras.layers.Layer):
def __init__(self, kernel_size):
super(DownsampleModule, self).__init__()
# conv layer
self.conv3 = ConvModule(kernel_size, kernel_size=(3,3),
strides=(2,2), padding="valid")
# pooling layer
self.pool = tf.keras.layers.MaxPooling2D(pool_size=(3, 3),
strides=(2,2))
self.cat = tf.keras.layers.Concatenate()
def call(self, input_tensor, training=False):
# forward pass
conv_x = self.conv3(input_tensor, training=training)
pool_x = self.pool(input_tensor)
# merged
return self.cat([conv_x, pool_x])
好的,现在我们已经构建了所有三个模块,即:ConvModule InceptionModule DownsampleModule。让我们根据图表初始化它们的参数。
class MiniInception(tf.keras.Model):
def __init__(self, num_classes=10):
super(MiniInception, self).__init__()
# the first conv module
self.conv_block = ConvModule(96, (3,3), (1,1))
# 2 inception module and 1 downsample module
self.inception_block1 = InceptionModule(32, 32)
self.inception_block2 = InceptionModule(32, 48)
self.downsample_block1 = DownsampleModule(80)
# 4 inception module and 1 downsample module
self.inception_block3 = InceptionModule(112, 48)
self.inception_block4 = InceptionModule(96, 64)
self.inception_block5 = InceptionModule(80, 80)
self.inception_block6 = InceptionModule(48, 96)
self.downsample_block2 = DownsampleModule(96)
# 2 inception module
self.inception_block7 = InceptionModule(176, 160)
self.inception_block8 = InceptionModule(176, 160)
# average pooling
self.avg_pool = tf.keras.layers.AveragePooling2D((7,7))
# model tail
self.flat = tf.keras.layers.Flatten()
self.classfier = tf.keras.layers.Dense(num_classes, activation='softmax')
def call(self, input_tensor, training=True, **kwargs):
# forward pass
x = self.conv_block(input_tensor)
x = self.inception_block1(x)
x = self.inception_block2(x)
x = self.downsample_block1(x)
x = self.inception_block3(x)
x = self.inception_block4(x)
x = self.inception_block5(x)
x = self.inception_block6(x)
x = self.downsample_block2(x)
x = self.inception_block7(x)
x = self.inception_block8(x)
x = self.avg_pool(x)
x = self.flat(x)
return self.classfier(x)
每个计算块的filter个数根据模型的设计来设定(见图)。初始化所有块后(在__init__函数中),我们按照设计将它们连接起来(在call函数中)。