使用keras进行语义分割注意事项

Author Avatar
patrickcty 8月 22, 2019

总览

  • 载入数据
    • 图像和标签相对应
    • 数据预处理与增强
    • 标签处理
    • 标签还原与可视化预测结果
    • 多输入/出处理
  • 网络结构
    • 反卷积
    • 上采样
    • todo
  • 训练脚本
    • Warm-up
    • 多输入/出(合并到第一部分)

载入数据

图像和标签对应

语义分割中需要同时载入图像和标签,和分类问题不一样,语义分割的标签也是图像,因此要和载入的图像相对应。

如果使用 keras 自带的 ImageDataGenerator 来获取数据则需要分别为图像和标签初始化两个对象,并传入相同的随机化种子,具体操作如下:

# we create two instances with the same arguments
data_gen_args = dict(featurewise_center=True,
                     featurewise_std_normalization=True,
                     rotation_range=90,
                     width_shift_range=0.1,
                     height_shift_range=0.1,
                     zoom_range=0.2)
image_datagen = ImageDataGenerator(**data_gen_args)
mask_datagen = ImageDataGenerator(**data_gen_args)

# Provide the same seed and keyword arguments to the fit and flow methods
seed = 1
image_datagen.fit(images, augment=True, seed=seed)
mask_datagen.fit(masks, augment=True, seed=seed)

image_generator = image_datagen.flow_from_directory(
    directory='data',
    class_mode=None,
    classes=['图像文件夹的名字'],
    seed=seed)

mask_generator = mask_datagen.flow_from_directory(
    'data',
    class_mode=None,
    classes=['标签文件夹的名字'],
    seed=seed)

# combine generators into one which yields image and masks
train_generator = zip(image_generator, mask_generator)

model.fit_generator(
    directory=train_generator,
    # 要指定这个参数,因为 zip 之后无法知道
    # 每个 epoch 有多少次迭代
    steps_per_epoch=2000,
    epochs=50)

其中要注意的有:

  • flow_from_directory 方法是通过 directory + class name 来寻找图片的,也就是说如果在这里让 directory='data/images' 并且不设置 classes 则无法读取到图片;另外,classes 应该传入一个列表;还有则是 class_mode 要设置为 None,不然就会根据子文件夹名返回标签
  • fit_generator 中要指定 steps_per_epoch,在这里可以通过 ceil(len(image_generator / batch_size) 来计算
  • fitflow 传入相同的随机化种子以保证生成相对应的图片

图像预处理与增强

  • 标准化:
    • img = (img - img_mean) / img_std
    • 简化起见也可以 img = img / 255 - 0.5
  • 数据增强:
    • 语义分割中最主要的增强是通过随机裁剪来进行的 ,具体的操作是把一张很大的图片随机裁剪出若干张(256, 256) 大小的图片,这样既增加了数据大小,也避免缩放出现的信息丢失
    • todo:还需要进一步查看文档

标签处理

由于语义分割的标签是和原图像同样大小的彩色图片,并且为了标注的方便,标签图像用一种颜色,也就是一组 RGB 的值来表示一种类别。

在进行损失函数的计算时,我们需要将 RGB 值转换成一组 one-hot 的标签,并且把图片拉成一个向量(以便于计算交叉熵损失)。比如对于 (256, 256, 3) 的标签,我们要将其转变成 (256 2, 3) 的向量,然后转换为 (256 2, num_classes) 的 one-hot 形式。具体的操作如下:

# VOC 中的列表表示
VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
                [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
                [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
                [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
                [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
                [0, 64, 128]]

VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
               'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
               'diningtable', 'dog', 'horse', 'motorbike', 'person',
               'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']

# 完成 RGB 与类别数值的映射
colormap2label = np.zeros(256 ** 3)
label2colormap = np.zeros_like(VOC_CLASSES)
for i, colormap in enumerate(VOC_COLORMAP):
    idx = (colormap[0] * 256 + colormap[1]) * 256 + colormap[2]
    colormap2label[idx] = i
    label2colormap[i] = idx

def mask_preprocessing(mask):
    mask = mask.astype('int32')
    if len(mask.shape) == 3:  # 输入为单个图片
        w, h, _ = mask.shape
        # 将 RGB 的类别值转换成单一数字表示的的类别值
        idx = ((mask[:, :, 0] * 256 + mask[:, :, 1]) * 256 + mask[:, :, 2])
        # 将图片拉成向量
        new_mask = colormap2label[idx].reshape((w * h, -1))
    else:  # len == 4  # 输入为一个 batch 的图片
        b, w, h, _ = mask.shape
        idx = ((mask[:, :, :, 0] * 256 + mask[:, :, :, 1]) * 256 + mask[:, :, :, 2])
        new_mask = colormap2label[idx].reshape((b, w * h, -1))
    # 将标签转换为 one-hot 的形式
    r_mask = to_categorical(new_mask, num_classes=self.config.num_classes)
    return r_mask

标签还原与可视化预测结果

预测结果的标签和输入的一样都是一个 (w * h, num_classes) 的 one-hot 矩阵,我们要先将其变成非 one-hot 的类别形式,然后再还原成 RGB 的形式,然后将其变成图片的形状,之后输出的标签就和标注好的标签比较类似了。操作方法如下:

# 将图片从单个数字表示的类别映射回 RGB
def labelVisualize(self, num_class, img):
    img = img[:, :, 0] if len(img.shape) == 3 else img
    img_out = np.zeros(img.shape + (3,))
    for i in range(num_class):
        img_out[img == i, :] = VOC_COLORMAP[i]
    return img_out / 255

def mask_propressing(predict_mask_list, save_to=None):
    # 这里是因为有多个输出因此要用循环来处理
    for i in range(len(predict_mask_list)):
        # 将 one-hot 还原
        num_classes = predict_mask_list[0].shape[-1]
        predict_mask = predict_mask_list[i]
        num_classes = predict_mask.shape[-1]
        mkdir_if_not_exist(os.path.join(save_to, str(i)))
        if len(predict_mask.shape) == 3:  # (?, 65536, 21)
            temp_mask = predict_mask.reshape((predict_mask.shape[0], self.config.input_shape,
                                              self.config.input_shape, num_classes))
            for item in temp_mask:
                img = self.labelVisualize(num_classes, item)
                if save_to:
                    io.imsave(os.path.join(save_to, str(i), '%s_pred.png' % str(uuid.uuid4())[:4]), img)

        else:  # (65536, 21)
            temp_mask = predict_mask.reshape((self.config.input_shape, self.config.input_shape, num_classes))
            img = self.labelVisualize(num_classes, temp_mask)
            if save_to:
                io.imsave(os.path.join(save_to, str(i), '%s_pred.png' % str(uuid.uuid4())[:4]), img)

多输出处理

如果神经网络有多输入/出,则对于每个输入和输出都要有相应的标签进行对应:

# 输入到 fit_generator 的形式
# tuple 的第一个第二个分别是输入和输入
# 多个不同的通过名字和数据的字典来对应
({'input1': img1, 'input2', img2}, 
 {'pred1': mask1, 'pred2': mask2, 'pred3': mask3})
 
# 输入 comple 的形式
# 这里主要是对输入的内容进行指定
model.compile(optimizer='rmsprop',
              loss={'pred1': 'binary_crossentropy', 'pred2': 'binary_crossentropy'},
              'pred3': 'binary_crossentropy'},
              loss_weights={'pred1': 1., 'pred2': 0.2, 'pred3': 0.2})

网络结构

todo

训练脚本

Warm-up

在开始的时候使用比较小的学习率训练,再使用正常的学习率训练,这种方法可以防止神经网络在一开始跑向了错误的方向而取得不理想的效果。

假设初始的学习率为 lr_init,warm-up 的 epoch 数为 num,当前 epoch 数为 i(从 1 开始计数),则当前学习率为:lr = lr_init * i / num (i < num)

在 keras 里面可以用 LeaningRateScheduler 的回调来实现:

def lr_scheduler_warm_up(epoch_idx, cur_lr):
    if epoch_idx == 0:
        return cur_lr / 5
    if epoch_idx < 5:  # 前五个 epoch
        return cur_lr * (epoch_idx + 1) / epoch_idx
    return cur_lr

lr_warm_up = LearningRateScheduler(lr_scheduler_warm_up)