Чтение документа FCN полной сверточной сети и реализация кода для семантической сегментации

Идентификация изображения

Сегодня давайте посмотрим на ретро-статью Full Convolutional Networks, которая является полной сверточной нейронной сетью, Это статья в направлении семантической сегментации в 2015 году, и это относительно долгая новаторская работа. Так как направление семантической сегментации я изучаю недавно, то решил начать именно с этого оригинатора.Ведь многие из следующих статей позаимствовали идеи из этой статьи.Только освоив основу можно взлететь выше. Эта статья разделена на две части: интерпретация статьи и реализация кода.

Адрес бумаги:Fully Convolutional Networks for Semantic Segmentation

Интерпретация диссертации

Введение в семантическую сегментацию

Цель семантической сегментации состоит в том, чтобыКлассифицировать каждый пиксель, а задача общей классификации выводит только определенную категорию, вывод задачи семантической сегментации такой же, какВведите изображение того же размера, что и изображение, каждый пиксель выходного изображения соответствует категории каждого пикселя входного изображения.

语义分割的预测结果

сетевая структура

Базовая структура FCN очень проста и представляет собой сеть, состоящую из всех сверточных слоев. Общая структура сети для классификации изображений:Свертка-пул-свертка-пул-полное соединение",вСверточные и полносвязные слоиЕсть параметры, а у пулинга нет параметров. Автор статьи считает, что полносвязный слой делает цельИнформация о местонахожденииисчез, только осталсясемантическая информация, поэтому замена полносвязной операции на операцию свертки может сохранить какИнформация о местоположении и семантическая информация, чтобы достичь цели классификации каждого пикселя. Базовая структура сети выглядит следующим образом:

fcn网络结构

После свертки и объединения входного изображения ширина и высота полученной карты признаков уменьшаются в несколько раз по сравнению с исходным изображением. Например, на следующем рисунке ширина и высота «кубоида признаков» после извлечения признаков равны 1/32 от исходного изображения.Выходной результат такого же размера исходного изображения нужно подвергнуть апсемплингу (апсемплингу), один из методов апсемплинга - деконволюция ("толщина" конечного вывода на рисунке равна 21, поскольку количество категорий равно 21, каждый слой можно рассматривать как вероятность принадлежности каждого пикселя исходного изображения к определенной категории, на что необходимо обращать внимание при кодировании).

网络结构细节

Деконволюция

Деконволюция — это способ апсэмплинга (дискретизации).После эксперимента авторы обнаружили, что деконволюция более эффективна, чем другие методы апсэмплинга, такие как билинейный апсэмплинг, поэтому этот метод принят на вооружение.

Объяснение деконволюции основано на этой статье:medium.com/activating-…, Англоязычные друзья рекомендуют читать оригинальный текст, он очень прозрачен.

Сначала посмотрим на прямую свертка, мы знаем, что суть операции свертки заключается в умножении матриц, а затем сложении. Теперь предположим, что у нас есть матрица 4x4, размер ядра свертки 3x3, шаг равен 1 и нет заполнения, тогда на выходе будет матрица 2x2, этот процесс на самом деле представляет собоймного к одномуОтображение:

卷积

Разделение всего шага свертки на четыре шага выглядит так:

卷积步骤

Из приведенного выше рисунка также видно, что операция свертки фактически сохраняет информацию о положении, например, число «122» в верхнем левом углу выходной матрицы соответствует 9 элементам в верхнем левом углу исходной матрицы. Итак, как «расширить» полученную матрицу 2x2 до матрицы 4x4 (один ко многимотображение). Давайте посмотрим на свертку с другой точки зрения.Во-первых, мы расширяем ядро ​​​​свертки в строку и исходную матрицу в столбец:

卷积核展开为一行
原矩阵展开为一列

Затем первый шаг процесса свертки:

卷积的第一个步骤

Это может быть выражено как умножение двух приведенных выше разложений,Всю операцию свертки также можно записать в виде следующего матричного умножения:

卷积表示为两矩阵相乘

Не волнуйтесь, вы можете испытать это в деталях, операция свертки действительно имеет место.Видно, что матрица ядра свертки 4x16 умножается на входную матрицу 16x1, чтобы получить выходную матрицу 4x1, которая достигает эффекта отображения «многие к одному», затем транспонирует матрицу ядра свертки в 16x4 и умножает выход матрица 4 x 1. Он может достичь эффекта отображения «один ко многим», поэтому деконволюцию также называют транспонированной сверткой. Конкретный процесс выглядит следующим образом:

一对多
转置卷积

Таким образом, основная структура полностью сверточной сети такова:Классификатор свертки-деконволюции", извлечение информации о местоположении и семантической информации посредством свертки, повышение дискретизации до исходного размера изображения посредством деконволюции и слияние глубоких и мелких признаков для достижения цели семантической сегментации.

Код

Я сам реализовал простую версию FCN и запустил ее на небольшом наборе данных.Кодовой адрес:GitHub.com/fr о йо ззз/CV…, вопросы и звезды приветствуются. Сетевой код VGG16, вперед, возвращает выходные данные каждого уровня в процессе свертки, чтобы их можно было позже объединить с функциями деконволюции.

class VGG(nn.Module):
    def __init__(self, pretrained=True):
        super(VGG, self).__init__()

        # conv1 1/2
        self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
        self.relu1_1 = nn.ReLU(inplace=True)
        self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.relu1_2 = nn.ReLU(inplace=True)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv2 1/4
        self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.relu2_1 = nn.ReLU(inplace=True)
        self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.relu2_2 = nn.ReLU(inplace=True)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv3 1/8
        self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
        self.relu3_1 = nn.ReLU(inplace=True)
        self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.relu3_2 = nn.ReLU(inplace=True)
        self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.relu3_3 = nn.ReLU(inplace=True)
        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv4 1/16
        self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
        self.relu4_1 = nn.ReLU(inplace=True)
        self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu4_2 = nn.ReLU(inplace=True)
        self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu4_3 = nn.ReLU(inplace=True)
        self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv5 1/32
        self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu5_1 = nn.ReLU(inplace=True)
        self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu5_2 = nn.ReLU(inplace=True)
        self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu5_3 = nn.ReLU(inplace=True)
        self.pool5 = nn.MaxPool2d(kernel_size=2, stride=2)
        
        # load pretrained params from torchvision.models.vgg16(pretrained=True)
        if pretrained:
            pretrained_model = vgg16(pretrained=pretrained)
            pretrained_params = pretrained_model.state_dict()
            keys = list(pretrained_params.keys())
            new_dict = {}
            for index, key in enumerate(self.state_dict().keys()):
                new_dict[key] = pretrained_params[keys[index]]
            self.load_state_dict(new_dict)

    def forward(self, x):
        x = self.relu1_1(self.conv1_1(x))
        x = self.relu1_2(self.conv1_2(x))
        x = self.pool1(x)
        pool1 = x

        x = self.relu2_1(self.conv2_1(x))
        x = self.relu2_2(self.conv2_2(x))
        x = self.pool2(x)
        pool2 = x

        x = self.relu3_1(self.conv3_1(x))
        x = self.relu3_2(self.conv3_2(x))
        x = self.relu3_3(self.conv3_3(x))
        x = self.pool3(x)
        pool3 = x

        x = self.relu4_1(self.conv4_1(x))
        x = self.relu4_2(self.conv4_2(x))
        x = self.relu4_3(self.conv4_3(x))
        x = self.pool4(x)
        pool4 = x

        x = self.relu5_1(self.conv5_1(x))
        x = self.relu5_2(self.conv5_2(x))
        x = self.relu5_3(self.conv5_3(x))
        x = self.pool5(x)
        pool5 = x

        return pool1, pool2, pool3, pool4, pool5

Сеть FCN, структура также очень проста, включая деконволюцию и слияние с поверхностной информацией.

class FCNs(nn.Module):
    def __init__(self, num_classes, backbone="vgg"):
        super(FCNs, self).__init__()
        self.num_classes = num_classes
        if backbone == "vgg":
            self.features = VGG()

        # deconv1 1/16
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn1 = nn.BatchNorm2d(512)
        self.relu1 = nn.ReLU()

        # deconv1 1/8
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn2 = nn.BatchNorm2d(256)
        self.relu2 = nn.ReLU()

        # deconv1 1/4
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.relu3 = nn.ReLU()

        # deconv1 1/2
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn4 = nn.BatchNorm2d(64)
        self.relu4 = nn.ReLU()

        # deconv1 1/1
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn5 = nn.BatchNorm2d(32)
        self.relu5 = nn.ReLU()

        self.classifier = nn.Conv2d(32, num_classes, kernel_size=1)

    def forward(self, x):
        features = self.features(x)

        y = self.bn1(self.relu1(self.deconv1(features[4])) + features[3])

        y = self.bn2(self.relu2(self.deconv2(y)) + features[2])

        y = self.bn3(self.relu3(self.deconv3(y)) + features[1])

        y = self.bn4(self.relu4(self.deconv4(y)) + features[0])

        y = self.bn5(self.relu5(self.deconv5(y)))

        y = self.classifier(y)

        return y

Обучение, каждая эпоха будет сохранять модель, которая по умолчанию сохраняется в./modelsВниз:

python train.py

Мой результат обучения, верхняя строка — прогнозируемое значение, а нижняя строка — целевое значение: На 5 эпохе:

5 个 epoch
В 10 эпох:
10 个 epoch
В 20 эпох:
20 个 epoch

Поскольку обучаемая модель относительно большая, я не стал загружать ее в гит. Если набор данных относительно небольшой, то обучение можно выполнить примерно за полчаса. Предпосылкой является наличие видеокарты. Если вам нужна обученная модель, вы можете оставить сообщение, и я отправлю его вам.

PS: Добро пожаловать, чтобы обратить внимание на мой личный публичный аккаунт WeChat [Путь обучения машинному обучению], изучайте Python, глубокое обучение, компьютерное зрение со мной!