Траектория «Нейросети» · конспект 7 из 12

Архитектуры CNN и перенос обучения

О чём эта тема
Разбираются классические архитектуры свёрточных сетей — AlexNet, VGGNet, GoogLeNet/Inception, ResNet, U-Net, MobileNet — и общие идеи, которые каждая из них добавила к тому, что было в конспекте 4; для каждой архитектуры приводятся схема и реализация на Keras. Отдельно — перенос обучения: как использовать уже обученную на ImageNet сеть для своей задачи с гораздо меньшим объёмом данных.
Аннотация
Конспект построен как хронологический обзор архитектур свёрточных сетей. После знакомства с соревнованием ImageNet и метриками Top-1/Top-5 рассматриваются AlexNet (2012) — первая по-настоящему глубокая свёрточная сеть, победившая в ImageNet; VGGNet (2014) с её идеей стека одинаковых блоков из свёрток 3×3; GoogLeNet/Inception (2014) с параллельными ветками и глобальным усредняющим пулингом; ResNet (2015), где остаточные связи решают проблему затухающего градиента в глубоких сетях; U-Net (2015) — кодировщик-декодировщик для семантической сегментации изображений, вместе с метриками качества сегментации IoU и Dice; и MobileNet (2017) со свёрткой, разделимой по глубине, для мобильных устройств. Каждая архитектура сопровождается схемой и реализацией на Keras. Завершается конспект переносом обучения — использованием сети, обученной на ImageNet, для новой задачи — на примерах классификации рентгеновских снимков и сегментации (TernausNet).
Пререквизиты
Конспект 4 (свёртка, пулинг, полносвязный слой, формула размера выхода (4.2), матрица ошибок); конспект 5 (переобучение, Dropout, аугментация); конспект 6 (повышение разрешения, resize-свёртка) — для раздела про U-Net.
Мотивация
Сеть на CIFAR-10 из конспекта 4 обучалась с нуля на 50 000 изображений. Для многих реальных задач данных на порядки меньше — например, размеченных рентгеновских снимков конкретного заболевания могут быть сотни, а не десятки тысяч. Обучить с нуля глубокую сеть на таком объёме данных, как правило, не получится: она либо не обучится (недообучение), либо переобучится (конспект 5). Перенос обучения решает эту проблему, переиспользуя сеть, уже обученную на ImageNet — 1,2 млн изображений, 1000 классов.

1. ImageNet и метрики Top-1/Top-5

ImageNet (ILSVRC, ImageNet Large Scale Visual Recognition Challenge) — ежегодное соревнование по классификации изображений на 1000 классов, ставшее опорной точкой развития архитектур CNN. Помимо обычной accuracy (конспект 4, формула (4.3)) для одной верной метки, для ImageNet использовались:

Top-5 мягче штрафует ошибки, когда на изображении несколько предметов или классы очень похожи между собой (разные породы собак и т. п.).

2. AlexNet (2012): начало

AlexNet (Krizhevsky, Sutskever, Hinton, 2012) — сеть, победившая в ImageNet 2012 года с большим отрывом от конкурентов и снизившая ошибку с 20% до 15,4%; с неё принято отсчитывать современный подъём интереса к нейросетям. Архитектура — пять свёрточных слоёв (с пулингом и нормализацией между ними) и три полносвязных слоя в конце, то есть в точности набор блоков из конспекта 4, без принципиально новых элементов.

Решения, которые на момент публикации были нестандартными и сейчас стали общепринятыми:

Схема AlexNet: пять свёрточных слоёв (числа под слоями — количество ядер), три из них с max-пулингом, и три полносвязных слоя. Запись «11×11/4» — ядро 11×11 с шагом 4.

Все элементы схемы уже знакомы по конспекту 4, поэтому AlexNet целиком записывается через Sequential:

from tensorflow.keras import layers, models

model = models.Sequential([
    layers.Input(shape=(227, 227, 3)),
    layers.Conv2D(96, (11, 11), strides=4, activation='relu'),
    layers.MaxPool2D((3, 3), strides=2),
    layers.Conv2D(256, (5, 5), padding='same', activation='relu'),
    layers.MaxPool2D((3, 3), strides=2),
    layers.Conv2D(384, (3, 3), padding='same', activation='relu'),
    layers.Conv2D(384, (3, 3), padding='same', activation='relu'),
    layers.Conv2D(256, (3, 3), padding='same', activation='relu'),
    layers.MaxPool2D((3, 3), strides=2),
    layers.Flatten(),
    layers.Dense(4096, activation='relu'), layers.Dropout(0.5),
    layers.Dense(4096, activation='relu'), layers.Dropout(0.5),
    layers.Dense(1000, activation='softmax'),
])
Типичная ошибка Пытаются напрямую применить архитектуру AlexNet (или похожую на неё сеть, рассчитанную на вход 224×224) к изображениям CIFAR-10 (32×32). При таком агрессивном уменьшении пространственного размера в первых же слоях карта признаков схлопывается до размера меньше ядра следующей свёртки. Архитектуру нужно адаптировать под размер входа, а не переносить как есть.

3. VGGNet (2014): стек одинаковых блоков

VGGNet (Simonyan, Zisserman, Оксфорд, 2014) ввела идею, ставшую с тех пор общим местом: строить сеть из повторяющихся стандартных блоков (несколько свёрток 3×3 подряд, затем max-пулинг), увеличивая глубину повторением этих блоков, а не изобретением новых. Число в названии (VGG16, VGG19) — количество слоёв с обучаемыми весами (свёрточных и полносвязных).

Все свёрточные слои VGGNet используют исключительно ядра 3×3. Причина — два слоя свёртки 3×3 подряд дают то же рецептивное поле, что и один слой 5×5, но с меньшим числом параметров и с двумя нелинейностями ReLU вместо одной. При 64 каналах на входе и выходе (без учёта смещений): два слоя 3×3 — это \(2\cdot3\cdot3\cdot64\cdot64 = 73\,728\) параметров, один слой 5×5 — \(5\cdot5\cdot64\cdot64 = 102\,400\).

Схема VGG16: 13 свёрточных слоёв, сгруппированных в пять одинаковых по устройству блоков, плюс 3 полносвязных слоя. После каждого блока — max-пулинг 2×2, уменьшающий карту признаков вдвое.

Повторяемость блоков видна и в коде — вся сеть строится циклом:

from tensorflow.keras import layers, models

def vgg_block(model, filters, convs):
    for _ in range(convs):
        model.add(layers.Conv2D(filters, (3, 3), padding='same', activation='relu'))
    model.add(layers.MaxPool2D((2, 2), strides=2))

model = models.Sequential([layers.Input(shape=(224, 224, 3))])
for filters, convs in [(64, 2), (128, 2), (256, 3), (512, 3), (512, 3)]:
    vgg_block(model, filters, convs)
model.add(layers.Flatten())
model.add(layers.Dense(4096, activation='relu'))
model.add(layers.Dense(4096, activation='relu'))
model.add(layers.Dense(1000, activation='softmax'))
Типичная ошибка Думают, что раз рецептивное поле совпадает, то стек из нескольких 3×3 и один большой фильтр — это одно и то же. Помимо меньшего числа параметров, у стека между свёртками стоит по одной функции активации ReLU на слой — то есть стек реализует нелинейную функцию, а один большой линейный фильтр — только линейное преобразование области входа.

4. GoogLeNet / Inception (2014): параллельные ветки

GoogLeNet (Szegedy et al., 2014) ввела Inception-модуль: вместо последовательного выбора одного размера ядра на слой несколько свёрток разного размера (обычно 1×1, 3×3, 5×5) и слой пулинга вычисляются параллельно на одном и том же входе, а их результаты объединяются по каналам (конкатенацией, а не сложением). Это позволяет слою одновременно улавливать признаки разного пространственного масштаба, не выбирая заранее один размер ядра.

Схема Inception-модуля. Свёртки 1×1 перед ветками 3×3 и 5×5 уменьшают число каналов, чтобы дорогие свёртки большего размера работали с меньшим объёмом данных.

Модуль удобно записывать через функциональный API Keras (тот же способ записи, что в коде переноса обучения в разделе 8 — слой применяется к тензору как функция):

from tensorflow.keras.layers import Conv2D, MaxPool2D, concatenate

def inception_module(x, f1, f3_in, f3, f5_in, f5, f_pool):
    b1 = Conv2D(f1, (1, 1), padding='same', activation='relu')(x)

    b2 = Conv2D(f3_in, (1, 1), padding='same', activation='relu')(x)
    b2 = Conv2D(f3, (3, 3), padding='same', activation='relu')(b2)

    b3 = Conv2D(f5_in, (1, 1), padding='same', activation='relu')(x)
    b3 = Conv2D(f5, (5, 5), padding='same', activation='relu')(b3)

    b4 = MaxPool2D((3, 3), strides=1, padding='same')(x)
    b4 = Conv2D(f_pool, (1, 1), padding='same', activation='relu')(b4)

    return concatenate([b1, b2, b3, b4])  # объединение по каналам

Ещё одно нововведение — глобальный усредняющий пулинг (англ. Global Average Pooling) вместо полносвязных слоёв на выходе: карта признаков произвольного пространственного размера \(H\times W\times C\) усредняется по \(H\) и \(W\) до размера \(1\times1\times C\). Например, карта \(7\times7\times1024\) в конце GoogLeNet, будучи развёрнута в вектор (Flatten, конспект 3), дала бы более 50 тысяч признаков на входе первого полносвязного слоя; глобальный усредняющий пулинг сразу сжимает её до 1024 чисел без единого обучаемого параметра. В Keras это слой GlobalAveragePooling2D().

5. ResNet (2015): остаточные связи

Опыт VGGNet и GoogLeNet показал, что более глубокая сеть не обязательно лучше: авторы ResNet привели сравнение 56-слойной и 20-слойной сетей одной архитектуры (в духе VGGNet), где у 56-слойной сети ошибка оказалась выше, чем у 20-слойной, причём на обучающих данных тоже — то есть дело не в переобучении (конспект 5), а в том, что глубокую сеть стало сложнее обучить в принципе: при обратном распространении ошибки (конспект 2) градиент, пройдя через много слоёв подряд, может стать исчезающе малым.

conv 3×3 BN, ReLU conv 3×3 BN + остаточная связь (тождественное отображение x) x
Остаточный блок: выход блока — F(x) + x, а не просто F(x)

Решение ResNet (He et al., 2015) — добавить в вычислительный граф сети остаточную связь (англ. skip connection): связь в обход пары свёрточных слоёв, складывающую их выход с неизменённым входом блока. Слои внутри блока при этом аппроксимируют не саму искомую зависимость \(H(x)\), а лишь остаток (англ. residual) между ней и тождественным отображением:

\[ y = F(x) + x \tag{7.1}\]

где \(F(x)\) — то, что вычисляют свёрточные слои блока. Смысл формулы (7.1) для обратного распространения ошибки: производная суммы равна сумме производных, поэтому

\[ \frac{\partial y}{\partial x} = \frac{\partial F(x)}{\partial x} + 1 \tag{7.2}\]

даже если \(\partial F(x)/\partial x\) станет очень малой (тот самый затухающий градиент), слагаемое \(+1\) не даёт полной производной обратиться в ноль — градиент проходит через блок по крайней мере по пути тождественного отображения. Сеть ResNet-152 (152 слоя) обучилась и превзошла по точности заметно более мелкую VGG19, при этом имея меньше обучаемых параметров (58 млн против 144 млн) — за счёт глобального усредняющего пулинга вместо полносвязных слоёв (как в GoogLeNet) и отсутствия избыточно широких слоёв VGG-подобной сети.

Остаточный блок в точности повторяет схему выше — сложение выполняет слой Add:

from tensorflow.keras.layers import Conv2D, BatchNormalization, ReLU, Add

def residual_block(x, filters):
    f = Conv2D(filters, (3, 3), padding='same')(x)
    f = BatchNormalization()(f)
    f = ReLU()(f)
    f = Conv2D(filters, (3, 3), padding='same')(f)
    f = BatchNormalization()(f)
    y = Add()([x, f])          # y = F(x) + x — формула (7.1)
    return ReLU()(y)
Типичная ошибка Считают остаточную связь способом бороться с переобучением, как Dropout. Она решает другую проблему — устойчивость обучения глубокой сети (распространение градиента), а не регуляризацию; сеть с остаточными связями может переобучиться точно так же, как и без них.

6. U-Net (2015): семантическая сегментация

Все архитектуры выше решают задачу классификации: одно изображение — одна метка класса. Но во многих задачах нужно знать не «что на изображении», а «где именно»: семантическая сегментация назначает метку класса каждому пикселю изображения.

Три изображения: фотография велосипедиста, ручная разметка по классам и результат сегментации нейросетью
Задача сегментации: слева оригинал, в центре ручная разметка, справа результат сегментации сетью. Из практикума курса по сегментации аэрофотоснимков

Сеть с Flatten и полносвязными слоями на выходе здесь не подходит: выход должен быть не вектором из \(K\) вероятностей, а картой размера исходного изображения — по распределению вероятностей классов на каждый пиксель. Поэтому сегментационные сети делают полностью свёрточными (англ. fully convolutional network, FCN): полносвязная «голова» классификатора заменяется свёрточными слоями, повышающими разрешение обратно до входного.

Сравнение: свёрточная сеть с полносвязным выходом для классификации и полностью свёрточная сеть с разворачивающей частью для сегментации
Вверху — сеть для классификации (полносвязный выход, метка класса), внизу — полностью свёрточная сеть: вместо полносвязных слоёв — деконволюция и повышение разрешения до карты классов. Из практикума курса по сегментации

U-Net (Ronneberger, Fischer, Brox, 2015) — самая известная архитектура этого семейства, изначально созданная для сегментации биомедицинских изображений на небольших выборках. Она состоит из двух частей. Кодировщик (сжимающий путь) — обычная свёрточная сеть из блоков «две свёртки 3×3 + max-пулинг 2×2», как в VGGNet: разрешение падает, число каналов растёт, сеть извлекает всё более абстрактные признаки. Декодировщик (разжимающий путь) — зеркальное отражение кодировщика: повышение разрешения, свёртка, уменьшающая число каналов, и — ключевая особенность — конкатенация с картой признаков той же ступени кодировщика.

Схема U-Net: сжимающий путь из свёрток и пулинга, разжимающий путь из повышения разрешения и свёрток, горизонтальные связи-конкатенации между ступенями одного разрешения
Схема U-Net для входа 256×256: буква «U» образована кодировщиком (слева вниз) и декодировщиком (справа вверх); горизонтальные стрелки — передача карт признаков кодировщика в декодировщик. Из практикума курса по сегментации

Горизонтальные связи U-Net напоминают остаточные связи ResNet, но устроены иначе: в ResNet вход складывается с выходом блока (формула (7.1)), а в U-Net карта признаков кодировщика конкатенируется с картой декодировщика по каналам. Смысл тоже другой: связи переносят в декодировщик детали высокого разрешения (границы объектов), которые были потеряны при пулинге, — без них восстановленная карта классов получается размытой.

Реализация на Keras из практикума курса (вход 256×256×3, шесть классов; повышение разрешения — UpSampling2D + свёртка, то есть ровно resize-свёртка из конспекта 6, раздел 3, не создающая шахматных артефактов):

from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, Dropout, UpSampling2D, concatenate

input_img = Input(shape=(256, 256, 3))

# кодировщик: разрешение ↓, каналы ↑
conv1 = Conv2D(64, (3, 3), activation='relu', padding='same')(input_img)
conv1 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv1)
pool1 = MaxPooling2D((2, 2))(conv1)

conv2 = Conv2D(128, (3, 3), activation='relu', padding='same')(pool1)
conv2 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv2)
pool2 = MaxPooling2D((2, 2))(conv2)

conv3 = Conv2D(256, (3, 3), activation='relu', padding='same')(pool2)
conv3 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv3)
pool3 = MaxPooling2D((2, 2))(conv3)

conv4 = Conv2D(512, (3, 3), activation='relu', padding='same')(pool3)
conv4 = Conv2D(512, (3, 3), activation='relu', padding='same')(conv4)
drop4 = Dropout(0.5)(conv4)
pool4 = MaxPooling2D((2, 2))(drop4)

# «дно» U
conv5 = Conv2D(1024, (3, 3), activation='relu', padding='same')(pool4)
conv5 = Conv2D(1024, (3, 3), activation='relu', padding='same')(conv5)
drop5 = Dropout(0.5)(conv5)

# декодировщик: UpSampling2D + свёртка (resize-свёртка, конспект 6)
# и конкатенация с картой той же ступени кодировщика
up6 = Conv2D(512, (2, 2), activation='relu', padding='same')(UpSampling2D((2, 2))(drop5))
merge6 = concatenate([drop4, up6])
conv6 = Conv2D(512, (3, 3), activation='relu', padding='same')(merge6)
conv6 = Conv2D(512, (3, 3), activation='relu', padding='same')(conv6)

up7 = Conv2D(256, (2, 2), activation='relu', padding='same')(UpSampling2D((2, 2))(conv6))
merge7 = concatenate([conv3, up7])
conv7 = Conv2D(256, (3, 3), activation='relu', padding='same')(merge7)
conv7 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv7)

up8 = Conv2D(128, (2, 2), activation='relu', padding='same')(UpSampling2D((2, 2))(conv7))
merge8 = concatenate([conv2, up8])
conv8 = Conv2D(128, (3, 3), activation='relu', padding='same')(merge8)
conv8 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv8)

up9 = Conv2D(64, (2, 2), activation='relu', padding='same')(UpSampling2D((2, 2))(conv8))
merge9 = concatenate([conv1, up9])
conv9 = Conv2D(64, (3, 3), activation='relu', padding='same')(merge9)
conv9 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv9)

# выход: распределение по 6 классам для каждого пикселя
output = Conv2D(6, (1, 1), activation='softmax')(conv9)

unet = Model(inputs=input_img, outputs=output)
unet.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

6.1. Метрики качества сегментации: IoU и Dice

Попиксельная точность (доля верно классифицированных пикселей — та же accuracy из конспекта 4, формула (4.3), применённая к пикселям) для сегментации обманчива: если объект занимает 5% кадра, сеть, отвечающая «фон» на каждый пиксель, получит точность 95%, не найдя объект вовсе. Поэтому качество сегментации измеряют по перекрытию предсказанной маски \(P\) и истинной маски \(T\). Метрика IoU (Intersection over Union, индекс Жаккара):

\[ \mathrm{IoU} = \frac{|P \cap T|}{|P \cup T|} = \frac{TP}{TP + FP + FN} \tag{7.3}\]

Индекс Дайса (эквивалентен F1-мере):

\[ \mathrm{Dice} = \frac{2\,|P \cap T|}{|P| + |T|} = \frac{2\,TP}{2\,TP + FP + FN} \tag{7.4}\]

Здесь \(TP\), \(FP\), \(FN\) — элементы матрицы ошибок из конспекта 4, подсчитанные по пикселям. Обе метрики монотонно связаны друг с другом (\(\mathrm{Dice} = 2\,\mathrm{IoU}/(1+\mathrm{IoU})\)), поэтому достаточно вычислять одну из них. В Keras средний IoU по классам — метрика keras.metrics.MeanIoU.

Тренажёр: IoU и Dice на сетке пикселей
TP — пиксель есть в обеих масках FP — только в предсказанной маске FN — только в истинной маске

Истинная маска — квадрат 6×6 (данные иллюстративные). Сдвигайте и масштабируйте предсказанную маску и следите, как меняются IoU (7.3) и Dice (7.4): при точном совпадении обе равны 1, при отсутствии перекрытия — 0.

Типичная ошибка Оценивают сегментацию попиксельной точностью при сильно несбалансированных классах. Модель, выдающая один фон, получает высокую точность при нулевом IoU по всем содержательным классам — качество сегментации нужно измерять метриками перекрытия (IoU, Dice), а не долей верных пикселей.

7. MobileNet (2017): свёртка для мобильных устройств

MobileNet (Howard et al., 2017) — архитектура, специально спроектированная для работы на CPU мобильных устройств. Скорость достигается заменой обычной свёртки на свёртку, разделимую по глубине (depthwise separable convolution): вместо одной свёртки, одновременно комбинирующей каналы и пространственные соседние пиксели, используются две последовательные операции — свёртка отдельно по каждому каналу (без смешивания каналов между собой), а затем свёртка \(1\times1\), которая уже комбинирует каналы между собой. Это заметно уменьшает число операций и параметров по сравнению с обычной свёрткой того же рецептивного поля — та же идея разложения на более простые шаги, что и замена свёртки 5×5 на две свёртки 3×3 у VGGNet.

Разделимая по глубине свёртка: обычная свёртка выполняла бы обе работы одним дорогим слоем 3×3×C×C′, здесь они разнесены на два дешёвых шага.

В Keras оба шага объединены в слой SeparableConv2D; их можно записать и явно:

from tensorflow.keras.layers import DepthwiseConv2D, Conv2D, SeparableConv2D

# два шага явно:
def depthwise_separable(x, filters):
    x = DepthwiseConv2D((3, 3), padding='same', activation='relu')(x)  # по каждому каналу отдельно
    return Conv2D(filters, (1, 1), activation='relu')(x)              # смешивание каналов

# или одним слоем:
# SeparableConv2D(filters, (3, 3), padding='same', activation='relu')

8. Перенос обучения

Перенос обучения (англ. transfer learning; встречается также калька «трансферное обучение») — использование сети, обученной на одной задаче с большим объёмом данных, как основы для другой задачи с малым объёмом данных. Keras предоставляет перечисленные выше архитектуры уже обученными на ImageNet:

from tensorflow.keras.applications import vgg16, resnet50, mobilenet, inception_v3

vgg_model = vgg16.VGG16(weights='imagenet')
resnet_model = resnet50.ResNet50(weights='imagenet')
mobilenet_model = mobilenet.MobileNet(weights='imagenet')
inception_model = inception_v3.InceptionV3(weights='imagenet')

Так загруженные сети сразу готовы предсказывать один из 1000 классов ImageNet. Но чаще нужна не готовая сеть, а перенос уже выученных признаков на свою задачу с другими классами — например, различение рентгеновских снимков здоровых лёгких и лёгких, поражённых covid-19 (всего два класса, вместо тысячи классов ImageNet). Для этого у предобученной сети берут только часть, извлекающую признаки (без завершающих полносвязных слоёв — include_top=False), замораживают её веса, и поверх строят свой небольшой классификатор:

from tensorflow.keras.applications import VGG16
from tensorflow.keras.layers import AveragePooling2D, Dropout, Flatten, Dense, Input
from tensorflow.keras.models import Model

baseModel = VGG16(weights="imagenet", include_top=False,
                    input_tensor=Input(shape=(224, 224, 3)))

# новый классификатор поверх признаков VGG16
headModel = baseModel.output
headModel = AveragePooling2D(pool_size=(4, 4))(headModel)
headModel = Flatten()(headModel)
headModel = Dense(64, activation="relu")(headModel)
headModel = Dropout(0.5)(headModel)
headModel = Dense(2, activation="softmax")(headModel)

model = Model(inputs=baseModel.input, outputs=headModel)

# замораживаем веса VGG16 — обучаться будет только новый классификатор сверху
for layer in baseModel.layers:
    layer.trainable = False

Дальше сеть компилируется и обучается как обычно (конспект 3), только с малым числом изображений (в этом примере — рентгеновских снимков лёгких, разделённых на обучающую и тестовую выборки) и с аугментацией (конспект 5) для дополнительного разнообразия обучающих данных:

from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.optimizers import Adam

trainAug = ImageDataGenerator(rotation_range=15, fill_mode="nearest")

opt = Adam(learning_rate=1e-3)
model.compile(loss="binary_crossentropy", optimizer=opt, metrics=["accuracy"])

H = model.fit(trainAug.flow(trainX, trainY, batch_size=10),
              validation_data=(testX, testY), epochs=25)

Обучаются только веса нового классификатора (несколько тысяч параметров), а не все веса VGG16 (14+ млн) — это и позволяет получить рабочую модель на выборке из сотен, а не десятков тысяч изображений.

Перенос обучения работает не только в классификации. В U-Net кодировщик — обычная свёрточная сеть, и его тоже можно заменить сетью, предобученной на ImageNet: архитектура TernausNet (Игловиков, Швец, 2018) использует в качестве кодировщика VGG16 с готовыми весами, обучая с нуля только декодировщик:

Схема TernausNet-16: U-Net, в которой сжимающий путь заменён предобученной сетью VGG16, с остаточными связями-конкатенациями к декодировщику
TernausNet-16: U-Net с кодировщиком VGG16, предобученным на ImageNet. Из практикума курса по сегментации
Типичная ошибка Забывают выставить layer.trainable = False для слоёв базовой сети. Без этого при fit будут обучаться все веса, включая предобученные — на маленькой выборке это, как правило, быстро приводит к переобучению (конспект 5) и разрушает уже выученные на ImageNet признаки.

Контрольные вопросы