如何设计最优的 CNN?

How to design an optimal CNN?

我正在研究 Ph.D。 objective 旨在减少地球上 CO2 排放的项目。

我有一个数据集,我能够成功实现 CNN,它提供了 80% 的准确性(最坏情况)。然而,我工作的领域要求很高,我的印象是我可以通过优化良好的 CNN 获得更好的准确性。

专家是如何设计的CNN's?我如何在 Inception 模块、Dropout 正则化、Batch Normalization、卷积滤波器大小、卷积通道的大小和深度、全连接层数、激活神经元等之间进行选择?人们如何以科学的方式解决这个大型优化问题?组合是无穷无尽的。是否有任何现实生活中的例子解决了这个问题,解决了它的全部复杂性(不仅仅是优化一些超参数)?

希望我的数据集不会太大,所以我正在考虑的 CNN 模型应该只有很少的参数。

我认为您对所需参数数量的估计有很大偏差。更像是几百万,如果你使用迁移学习,你会得到什么。如果你愿意,你可以努力尝试制作自己的模型,但你可能不会比从迁移学习中获得的结果更好(而且更可能没有那么好)。我强烈推荐 MobileV2 模型。现在,如果您使用 ReduceLROnPlateau 的可调整学习率,您可以使该模型或任何其他模型表现更好。相关文档是 here. The other thing I recommend is to use the Keras callback EarlyStopping. Documentation is here. 。将其设置为监控验证损失并设置 restore_best_weights=True。将 epoch 的数量设置为较大的数字,以便触发此回调,并且 returns 模型的权重来自验证损失最低的 epoch。我推荐的代码如下

height=224
width=224
img_shape=(height, width, 3)
dropout=.3
lr=.001
class_count=156 # number of classes
img_shape=(height, width, 3)
base_model=tf.keras.applications.MobileNetV2( include_top=False, input_shape=img_shape, pooling='max', weights='imagenet') 
x=base_model.output
x=keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001 )(x)
x = Dense(512, kernel_regularizer = regularizers.l2(l = 0.016),activity_regularizer=regularizers.l1(0.006),
                bias_regularizer=regularizers.l1(0.006) ,activation='relu', kernel_initializer= tf.keras.initializers.GlorotUniform(seed=123))(x)
x=Dropout(rate=dropout, seed=123)(x)        
output=Dense(class_count, activation='softmax',kernel_initializer=tf.keras.initializers.GlorotUniform(seed=123))(x)
model=Model(inputs=base_model.input, outputs=output)
model.compile(Adamax(lr=lr), loss='categorical_crossentropy', metrics=['accuracy']) 
rlronp=tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=1, verbose=1, mode='auto', min_delta=0.0001, cooldown=0, min_lr=0)
estop=tf.keras.callbacks.EarlyStopping( monitor="val_loss", min_delta=0, patience=4,
                                       verbose=1,  mode="auto",  baseline=None,
                                        restore_best_weights=True)
callbacks=[rlronp, estop]

同时查看您的数据集中的余额。也就是说,比较每个 class 你有多少训练样本。如果大多数 samples/least 样本的比率 > 2 或 3,您可能需要采取措施来缓解这种情况。方法很多,最简单的就是使用model.fit中的class_weight参数。 o 这样做你需要创建一个 class_weights 字典。下面概述了执行此操作的过程

Lets say your class distribution is
 class0 - 500 samples
 class1- 2000 samples
 class2 - 1500 samples
 class3 - 200 samples
Then your dictionary would be
class_weights={0: 2000/500, 1:2000/2000, 2: 2000/1500, 3: 2000/200}
in model.fit set class_weight=class_weights

How do experts design CNN's? How could I choose between Inception Modules, Dropout Regularization, Batch Normalization, convolutional filter size, size and depth of convolutional channels, number of fully-connected layers, activations neurons, etc? How do people navigate this large optimization problem in a scientific manner? The combinations are endless.

你说的对,组合的数量是巨大的。如果方法不正确,您可能会一事无成。一个伟大的人说机器学习是一门艺术,而不是科学。结果取决于数据。关于您的上述问题,这里有一些提示。

  • Log Everything:在训练时间内,保存每个实验的必要日志,如训练损失、验证损失、权重文件、执行时间、可视化、等。其中一些可以用 CSVLoggerModelCheckpoint 等保存。TensorBoard 是检查训练日志和可视化等的好工具。

  • 强验证策略:这很重要。要构建稳定的 交叉验证 (CV),我们必须对数据和面临的挑战有很好的理解。我们将检查并确保验证集训练集具有相似分布测试集。我们将努力确保我们的模型在我们的 CV 测试集 both (如果 gt 可用于测试集)。基本上,随机分区数据通常不足以满足这一要求。了解数据以及我们如何在不在我们的 CV 中引入 数据泄漏 的情况下对其进行分区是避免 过度拟合 .

    的关键
  • 只改变一个:在实验过程中,一次改变一件事并保存这些变化的观察结果(logs)。例如:将图像大小从 224(例如)逐渐变大并观察结果。我们应该从一个小组合开始。在试验图像大小时,修复其他像 model 体系结构、learning rate 等。learning rate 部分或 model 体系结构也是如此。然而,稍后当我们得到一些有希望的组合时,我们也可能需要改变不止一个。在 kaggle 比赛中,这些是人们会遵循的非常常见的方法。下面是一个非常简单的例子。但它不受任何限制。


不过,正如你所说,你的Ph.D。项目是减少地球上的二氧化碳排放量。据我了解,这些更多是特定于应用程序的问题,而不是算法-特定 问题。因此,我们认为最好利用公认的预训练模型。

万一我们想自己写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 等顺序增加通道深度。如果我们想在我们的网络中估算 InceptionResidual Block,我认为,我们应该先做一些基本的数学运算,了解由此产生的特征属性等。按照这样的概念,我们可能还希望看看 SENetResNeSt 等方法。关于 Dropout,如果我们观察到我们的模型在训练过程中过度拟合,那么我们应该添加一些。在最后一层,我们可能想要选择 GlobalAveragePooling 而不是 Flatten 层 (FCC)。我们现在大概可以理解,需要进行大量的消融研究才能获得令人满意的 CNN 模型。

在这方面,我们建议您探索两个最重要的事情:(1)。阅读其中一个预训练模型 papers/blogs/videos,了解他们构建算法的策略。例如:查看此 EfficientNet Explained。 (2)。接下来,探索它的源代码。这会给你更多的感觉,并鼓励你建立自己的巨人。


我们想用最后一个工作示例来结束这个。见下面的模型图,它是一个小型初始网络source。仔细观察就会发现,它由以下三个模块组成。

  • 转换模块
  • 初始模块
  • 下采样模块

仔细看看filter sizestrides等每个模块的配置,让我们试着理解和实现这个模块.在此之前,这里有两个很好的参考(1, 2)对于Inception概念的刷新概念。

转换模块

从图中我们可以看出,它由一个卷积网络,一个批量归一化,和一个relu 激活。此外,它还生成 C 倍的特征图,其中包含 K x K 个过滤器和 S x S 个步幅。为此,我们将创建一个 class 对象,它将继承 tf.keras.layers.Layer classes

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 模块。根据上图,它由两个卷积模块组成,然后合并在一起。现在我们知道要合并,这里我们需要确保输出特征映射维度(heightwidth)需要 相同.

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 中,我们已经将 padding 设置为 same,因此特征图的维度对于两者(self.conv1self.conv2)都是相同的;这是将它们连接到最后所必需的。

同样,在此模块中,两个变量作为占位符执行,kernel_size1x1kernel_size3x3。这当然是为了目的。因为我们会需要不同数量的特征映射到整个模型的不同阶段。如果我们查看模型图,我们会发现 InceptionModule 在模型的不同阶段采用不同数量的过滤器。

下采样模块

最后是 下采样模块 。下采样的主要直觉是我们希望获得更多相关的特征信息,这些信息高度代表模型的输入。因为它倾向于删除不需要的特征,以便模型可以专注于最相关的特征。我们可以通过多种方式降低特征图(或输入)的维度。例如:使用strides 2 或使用常规的pooling 操作。池化操作有很多种,分别是:MaxPoolingAveragePoolingGlobalAveragePooling.

从图中我们可以看到下采样模块包含一个卷积层和一个最大池化层,它们后来合并在一起.现在,如果我们仔细观察图表(右上),我们会看到卷积层采用 3 x 3 大小的过滤器和 strides 2 x 2。池化层(此处 MaxPooling)采用池化大小 3 x 3strides 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函数中)。