Мы обучим модель глубокого обучения с архитектурой, известной как U-Net, на наборе данных электронной микроскопии. Наша модель научится преобразовывать полутоновое ЭМ изображение нервных клеток (слева) в точную карту границ, разделяющую стены между ними (справа) на уровне пикселей, как показано выше. U-Net остается передовым оборудованием для выполнения семантической сегментации, и та же модель с незначительной настройкой гиперпараметров и с экспериментальной головкой может использоваться практически для любой задачи сегментации изображений.

Примечание. В этом руководстве предполагается, что читатель будет иметь представление о том, как работают сверточные нейронные сети, а моя реализация U-Net в основном будет использовать функциональный API Keras, поэтому, если вы новичок в глубоком обучении, я Предлагаю вам получить хорошее представление, прочитав и реализовав мою статью о том, как работают сверточные нейронные сети, нажав здесь.

  • Модель, которую мы будем строить в этом посте, была скомпилирована на видеокарте Nvidia GTX 1060, на обучение потребуется несколько часов, если вы скомпилируете ее на процессоре, чтобы добиться хорошей точности, я бы предложил запустить ее на Версия Keras для графического процессора, если у вас есть графический процессор.

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

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

Итак, самый простой из них - это классификация изображений (а), когда мы пытаемся получить информацию о том, что находится на изображении, но здесь проблема в том, что мы не знаем, где находится определенный класс объектов и сколько его экземпляров присутствует в изображение и так далее. И, следовательно, позже появилась локализация / обнаружение объекта (b), которая не только сообщает нам, что изображено на картинке, но и где она расположена, что очень помогает. Но тогда даже этот подход дает нам только граничные рамки, прямоугольники, отмеченные над объектом, находящимся на изображении. Более глубокий уровень локализации этого объекта - семантическая сегментация, которая является основной темой данной статьи. Семантическую сегментацию можно описать в соответствии с классификацией пикселей для изображений, здесь мы помечаем каждый пиксель его соответствующим классом, как показано ниже:

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

Но модель, которую мы будем строить сегодня, - это сегментирование биомедицинских изображений, и документ, который я реализую для этого, был опубликован в 2015 году и стал исключительным в победе в конкурсе ISBI 2015. Эту архитектуру можно применять там, где используются обучающие данные. очень меньше. В частности, в медицинских секторах доступных обучающих выборок очень мало, особенно потому, что опыт в этой области очень ограничен, и очень трудно получить действительно хорошо маркированные и высококачественные данные, но U-Net по-прежнему остается на современном уровне в решении таких проблем. задания.

Теория, лежащая в основе U-Net

Ключевые моменты:

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

Сетевая архитектура:

Изображение выше описывает архитектуру U-Net, взятую из основного документа. Итак, как упоминалось ранее, наша сеть будет иметь 2 пути: путь понижающей дискретизации и путь повышающей дискретизации.

Путь понижающей дискретизации:

Левая часть сети - это часть с понижающей дискретизацией, это путь, по которому мы пропускаем изображение через несколько сверточных слоев и добавляем максимальный пул между ними для понижения дискретизации и уменьшения размера изображения, одновременно увеличивая количество слоев. путем удвоения количества фильтров сверточных слоев в каждом блоке свертки.

  • Путь имеет 4 блока свертки (по 2 свертки), за которыми следуют слои максимального объединения размером 2x2 с шагом 2 для понижающей дискретизации.
  • За 5-м блоком свертки не следует максимальное объединение, а он связан с путем повышающей дискретизации.
  • Первый блок свертки содержит 64 фильтра на каждом сверточном слое.
  • Количество фильтров удваивается с каждым последовательным блоком свертки.
  • Разрешение уменьшается с увеличением глубины (количества слоев),
  • Заполнение не используется (допустимое заполнение »).
  • Сверточные фильтры имеют размер 3x3 с функцией активации ReLU.

Путь увеличения выборки:

Здесь, в пути повышающей дискретизации, мы заменяем слои пула операторами повышающей дискретизации, которые увеличивают разрешение вывода.

  • Путь повышающей дискретизации остается симметричным пути понижающей дискретизации, превращая сеть в U-образную нейронную сеть, отсюда и название «U-Net».
  • Есть 4 блока свертки с 2 слоями свертки в каждом, за которыми следует транспонированная свертка / слой свертки с повышением частоты. Когда транспонированная свертка увеличивает разрешение изображения с помощью изученного фильтра.
  • Количество фильтров для каждого последовательного блока свертки равно половине фильтров из предыдущего блока свертки.
  • Разрешение увеличивается с уменьшением глубины (количества слоев).
  • Здесь также не используется заполнение (допустимое заполнение »).
  • Сверточные фильтры имеют размер 3x3 с функцией активации ReLU.
  • Соответствующие карты характеристик из пути понижающей дискретизации объединяются с соответствующими слоями повышающей дискретизации для достижения точной локализации.
  • Последний слой свертки имеет фильтр размером 1x1 для сопоставления каждого из 64 векторных компонентов с желаемым количеством классов (в данном случае это ячейка и фон).

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

Внедрение U-Net

Набор данных, который мы будем использовать в этом руководстве, будет набором данных задачи отслеживания ячеек ISBI 2015 года. Он содержит 30 изображений электроскопа с соответствующими аннотированными изображениями (метками). Вы можете найти набор данных и код, описанные в этом руководстве, на github. Итак, давайте построим модель в Керасе.

# unet_model.py
from keras.models import Model
from keras.backend import int_shape
from keras.layers import BatchNormalization, Conv2D, Conv2DTranspose, MaxPooling2D, Dropout, UpSampling2D, Input, concatenate

Давайте посмотрим, что мы импортируем и почему:

'Модель' из Функционального API Keras, используемого для построения сложных моделей глубокого обучения, направленных ациклических графов и т. д., а 'int_shape' возвращает форму тензора или переменная как кортеж из записей типа int или None.

‘BatchNormalization’: нормализует вывод активаций из слоев.

‘Conv2D’: используется для создания сверточного слоя.

‘Conv2DTranspose’: для выполнения транспонированной свертки.

‘MaxPooling2D’: выполняет операцию максимального объединения пространственных данных.

‘Dropout’: используется для удаления единиц (скрытых и видимых) в нейронной сети.

‘Input’: используется для создания экземпляра тензора Кераса.

‘concatenate’: возвращает тензор, который представляет собой конкатенацию входных данных вдоль переданной оси.

def upsample_conv(filters, kernel_size, strides, padding):
    return Conv2DTranspose(filters, kernel_size, strides=strides, padding=padding)
def upsample_simple(filters, kernel_size, strides, padding):
    return UpSampling2D(strides)

Вышеупомянутые две функции выполняют два разных типа передискретизации.

Функция upsampling_conv выполняет операцию транспонированной свертки, что означает повышающую дискретизацию изображения на основе изученного фильтра. И для этого мы используем Conv2DTranspose. Переданные для этого параметры говорят сами за себя. Если не уверены, нажмите здесь.

Функция «upsample_simple» выполняет простую прямую операцию повышения дискретизации изображения с ядром указанного размера. Для этого мы используем «Upsampling2D».

def conv2d_block(
    inputs, 
    use_batch_norm=True, 
    dropout=0.3, 
    filters=16, 
    kernel_size=(3,3), 
    activation='relu', 
    kernel_initializer='he_normal', 
    padding='same'):
    
    c = Conv2D(filters, kernel_size, activation=activation, 
    kernel_initializer=kernel_initializer, padding=padding) (inputs)
    if use_batch_norm:
        c = BatchNormalization()(c)
    if dropout > 0.0:
        c = Dropout(dropout)(c)
    c = Conv2D(filters, kernel_size, activation=activation, 
    kernel_initializer=kernel_initializer, padding=padding) (c)
    if use_batch_norm:
        c = BatchNormalization()(c)
    return c

Вышеупомянутая функция «conv2d_block» будет обрабатывать операции свертки в сети. Мы используем классическую функцию «Conv2D» от Keras для выполнения операций свертки. Аргументы, которые могут быть переданы, - это размер ввода, выбор использования пакетной нормализации внутри слоев, частота отсева, количество фильтров, размер ядра, функция активации для использования, инициализатор ядра 'he_normal' (для установите начальные веса сети полностью случайными) и, наконец, заполнение (в нашем случае 'same', т.е. выходы слоя будут иметь те же пространственные размеры, что и его входные данные).

def unet_model(
    input_shape,
    num_classes=1,
    use_batch_norm=True, 
    upsample_mode='deconv', # 'de-convolution' or 'simple upsampling' 
    use_dropout_on_upsampling=False, 
    dropout=0.3, 
    dropout_change_per_layer=0.0,
    filters=16,
    num_layers=4,
    output_activation='sigmoid'): # 'sigmoid' or 'softmax'
    
    if upsample_mode=='deconv':
        upsample=upsample_conv
    else:
        upsample=upsample_simple
    # Build U-Net model
    inputs = Input(input_shape)
    x = inputs   
    down_layers = []
    for l in range(num_layers):
        x = conv2d_block(inputs=x, filters=filters, use_batch_norm=use_batch_norm, 
    dropout=dropout)
        down_layers.append(x)
        x = MaxPooling2D((2, 2)) (x)
        dropout += dropout_change_per_layer
        filters = filters*2 # double the number of filters with each layer
    x = conv2d_block(inputs=x, filters=filters, use_batch_norm=use_batch_norm, 
    dropout=dropout)
    if not use_dropout_on_upsampling:
        dropout = 0.0
        dropout_change_per_layer = 0.0
    for conv in reversed(down_layers):        
        filters //= 2 # decrease the number of filters with each layer 
        dropout -= dropout_change_per_layer
        x = upsample(filters, (2, 2), strides=(2, 2), padding='same') (x)
        x = concatenate([x, conv])
        x = conv2d_block(inputs=x, filters=filters,      use_batch_norm=use_batch_norm, 
    dropout=dropout)
    
    outputs = Conv2D(num_classes, (1, 1), activation=output_activation) (x)    
    
    model = Model(inputs=[inputs], outputs=[outputs])
    return model

Вышеупомянутая функция unet_model завершает всю модель u-net. Щелкните здесь, чтобы увидеть графическую структуру указанной модели.

# train.py
import numpy as np
import matplotlib.pyplot as plt
import glob
import os
import sys
from PIL import Image
masks = glob.glob("./dataset/isbi2015/train/label/*.png")
orgs = glob.glob("./dataset/isbi2015/train/image/*.png")

Вышеупомянутая функция unet_model завершает всю модель u-net. Щелкните здесь, чтобы увидеть графическую структуру указанной модели.

# train.py
import numpy as np
import matplotlib.pyplot as plt
import glob
import os
import sys
from PIL import Image
masks = glob.glob("./dataset/isbi2015/train/label/*.png")
orgs = glob.glob("./dataset/isbi2015/train/image/*.png")

Мы импортируем набор данных в приведенном выше коде с помощью glob. Не забудьте загрузить или клонировать мой репозиторий github, чтобы найти набор данных.

imgs_list = []
masks_list = []
for image, mask in zip(orgs, masks):
    imgs_list.append(np.array(Image.open(image).resize((512,512))))
    
    im = Image.open(mask).resize((512,512))
    masks_list.append(np.array(im))
imgs_np = np.asarray(imgs_list)
masks_np = np.asarray(masks_list)

Здесь мы инициализировали два списка, преобразовав необработанные изображения и изображения с аннотациями (метками) в разрешение 512x512 и добавив их в «imgs_list» и «masks_list» соответственно.

from sklearn.model_selection import train_test_split
x_train, x_val, y_train, y_val = train_test_split(x, y, test_size=0.5, random_state=0)

Здесь мы разделяем наш импортированный набор данных на обучающий набор и набор проверки, используя функцию «train_test_split» из sklearn. Мы выбрали 15 изображений для обучающей выборки и еще 15 изображений в качестве тестовой.

from utils import get_augmented
train_gen = get_augmented(
    x_train, y_train, batch_size=2,
    data_gen_args = dict(
        rotation_range=15.,
        width_shift_range=0.05,
        height_shift_range=0.05,
        shear_range=50,
        zoom_range=0.2,
        horizontal_flip=True,
        vertical_flip=True,
        fill_mode='constant'
    ))

Вышеупомянутая функция используется для выполнения увеличения данных в нашем наборе данных. Он использует файл utils.py, включенный в мой github, для импорта функции get_augmented, которая использует ImageDataGenerator из keras.preprocessing.image внутри. Имена параметров, переданных в вышеупомянутой функции, описывают типы выполняемых дополнений.

sample_batch = next(train_gen)
xx, yy = sample_batch
print(xx.shape, yy.shape)
from keras_unet.utils import plot_imgs
plot_imgs(org_imgs=xx, mask_imgs=yy, nm_img_to_plot=2, figsize=6)

Вы можете построить и изучить расширенные изображения, запустив приведенный выше фрагмент кода.

from unet_model import unet_model
input_shape = x_train[0].shape
model = unet_model(
    input_shape,
    num_classes=1,
    filters=64,
    dropout=0.2,
    num_layers=4,
    output_activation='sigmoid'
)
print(model.summary())

Инициализация сети и печать резюме реализованной модели.

from keras.callbacks import ModelCheckpoint

model_filename = 'segm_model_v0.h5'
callback_checkpoint = ModelCheckpoint(
    model_filename, 
    verbose=1, 
    monitor='val_loss', 
    save_best_only=True,
)
from keras.optimizers import Adam, SGD
from metrics import iou, iou_thresholded
model.compile(
    optimizer=SGD(lr=0.01, momentum=0.99),
    loss='binary_crossentropy',
    metrics=[iou, iou_thresholded]
)

Здесь мы компилируем вышеуказанную модель, используя стохастический градиентный спуск в качестве нашего оптимизатора со скоростью обучения 0,01. И binary_crossentropy »в качестве нашей функции потерь.

history = model.fit_generator(
    train_gen,
    steps_per_epoch=100,
    epochs=10,
    
    validation_data=(x_val, y_val),
    callbacks=[callback_checkpoint]
)

Приведенный выше код обучит модель, а на рисунке ниже показан график потерь и точности обучения:

После завершения обучения веса нашей обученной сети будут сохранены в том же каталоге, что и файл с расширением «.h5».

# test.py
from PIL import Image
import numpy as np
import glob
masks = glob.glob("./dataset/isbi2015/train/label/*.png")
orgs = glob.glob("./dataset/isbi2015/train/image/*.png")
imgs_list = []
masks_list = []
for image, mask in zip(orgs, masks):
    imgs_list.append(np.array(Image.open(image).resize((512,512))))
    
    im = Image.open(mask).resize((512,512))
    masks_list.append(np.array(im))

imgs_np = np.asarray(imgs_list)
masks_np = np.asarray(masks_list)
x = np.asarray(imgs_np, dtype=np.float32)/255
y = np.asarray(masks_np, dtype=np.float32)/255
y = y.reshape(y.shape[0], y.shape[1], y.shape[2], 1)
x = x.reshape(x.shape[0], x.shape[1], x.shape[2], 1)

from sklearn.model_selection import train_test_split
x_train, x_val, y_train, y_val = train_test_split(x, y, test_size=0.1, random_state=0)

from unet_model import unet_model
input_shape = x_train[0].shape
model = unet_model(
    input_shape,
    num_classes=1,
    filters=64,
    dropout=0.2,
    num_layers=4,
    output_activation='sigmoid'
)

model_filename = 'segm_model_v0.h5'
model.load_weights(model_filename)
y_pred = model.predict(x_val)
from utils import plot_imgs
plot_imgs(org_imgs=x_val, mask_imgs=y_val, pred_imgs=y_pred, nm_img_to_plot=3)

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

На этом заканчивается мое руководство по семантической сегментации, и то, что мы здесь увидели, является лишь верхушкой айсберга, учитывая широкий спектр приложений семантической сегментации, от медицинских изображений до беспилотных автомобилей. Спасибо.