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

В этом блоге мы узнаем, как использовать CNN в реальном наборе данных гистопатологии. Реальные данные требуют гораздо большей предварительной обработки, чем стандартные наборы данных, такие как MNIST, и мы пройдем через процесс подготовки данных для классификации, а затем будем использовать CNN для классификации изображений. Я также расскажу об используемой архитектуре CNN и некоторых гиперпараметрах, настроенных при построении модели.

Различные разделы этого блога:

  • Вступление
  • Понимание набора данных
  • Загрузка набора данных
  • Предварительная обработка данных
  • Устранение дисбаланса данных с помощью случайной недостаточной выборки
  • Архитектура модели
  • Составление модели
  • Увеличение данных
  • Обучение модели
  • Делать прогнозы
  • Оценка производительности моделей

Я предполагаю, что читатель знает, что такое сверточные нейронные сети и их основной рабочий механизм.

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

Репозиторий Github для этого поста можно найти здесь. Я предлагаю вам изучить блокнот Jupyter во время изучения этого руководства.

Вступление

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

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

IDC - это тип рака груди, при котором рак распространился на окружающую ткань груди.

Раковые опухоли можно разделить на два типа: злокачественные и доброкачественные. Доброкачественная опухоль - это опухоль, которая не проникает в окружающие ткани, тогда как злокачественная опухоль может распространяться на окружающие ткани или другие части тела.

Понимание набора данных

Набор данных, который мы будем использовать, можно скачать здесь. Прокрутите страницу до раздела Описание набора данных и загрузите zip-файл размером 1,6 ГБ.

Набор данных состоит из 162 полных изображений слайдов образцов рака груди, сканированных с 40-кратным увеличением. Из них было извлечено 277 524 патча размером 50 x 50, из которых 198 738 являются IDC-отрицательными (доброкачественными) и 78 786 - положительными (злокачественными) IDC.

Имя файла каждого патча имеет формат:

u_xX_yY_classC.png → пример 10253_idx5_x1351_y1101_class0.png

Где u - идентификатор пациента (10253_idx5), X - координата x того места, откуда был вырезан этот патч, Y - координата y того места, откуда этот патч был вырезан, а C указывает класс, где 0 не-IDC ( доброкачественный) и 1 - IDC (злокачественный)

Загрузка набора данных:

imagePatches = glob('C:/Users/asus/Documents/Breast cancer classification/**/*.png', recursive=True)
patternZero = '*class0.png'
patternOne = '*class1.png'
#saves the image file location of all images with file name 'class0' classZero = fnmatch.filter(imagePatches, patternZero) 
#saves the image file location of all images with file name 'class1'
classOne = fnmatch.filter(imagePatches, patternOne)

Набор данных состоит из 279 папок с подпапками 0 и 1 внутри каждой из 279 папок. Сначала мы создаем две переменные classZero и classOne, которые сохраняют расположение изображений всех изображений класса 0 и класса 1 соответственно.

def process_images(lowerIndex,upperIndex):
    """
    Returns two arrays: 
        x is an array of resized images
        y is an array of labels
    """ 
    height = 50
    width = 50
    channels = 3
    x = [] #list to store image data
    y = [] #list to store corresponding class
    for img in imagePatches[lowerIndex:upperIndex]:
        full_size_image = cv2.imread(img)
        image = (cv2.resize(full_size_image, (width,height), interpolation=cv2.INTER_CUBIC))
        x.append(image)
        if img in classZero:
            y.append(0)
        elif img in classOne:
            y.append(1)
        else:
            return
    return x,y

Затем мы создаем функцию process_images, которая принимает в качестве входных данных начальный и конечный индексы изображений. Эта функция сначала считывает изображение с помощью cv2.imread () OpenCV, а также изменяет размер изображения. Изменение размера выполняется, потому что некоторые изображения в наборе данных имеют размер не 50x50x3. Функция возвращает два массива: X, который представляет собой массив данных изображения с измененным размером, и Y, который представляет собой массив соответствующих меток.

X, Y = process_images(0,100000)

В этом руководстве мы будем анализировать только изображения с индексом от 0 до 60 000. Данные изображения (значения пикселей) теперь хранятся в списке X, а соответствующие им классы - в списке Y.

Предварительная обработка данных:

X = np.array(X)
X = X.astype(np.float32)
X /= 255.

Список X сначала преобразуется в массив numpy, а затем приводится к типу float32 для экономии места.

Сначала изображения нормализуются путем деления на 255. Это гарантирует, что все значения находятся между 0 и 1. Это помогает нам быстрее обучать модель, а также предотвращает попадание в проблему исчезающих и взрывающихся градиентов.

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,Y,test_size=0.15)

Набор данных разделен на обучающий и тестовый набор, при этом 15% всего набора данных зарезервировано для тестирования. Для набора данных из 60 000 это означает, что 51 000 изображений зарезервированы для обучения и 9000 для тестирования.

Устранение дисбаланса данных с помощью случайной недостаточной выборки

y_train.count(1)  #counting the number of 1
y_train.count(0)  #counting the number of 0

Подсчитав количество единиц и нулей в массиве Y, мы обнаружим, что имеется 44478 изображений класса 0 и 15522 изображения класса 1.

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

y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

Перед этим нам нужно в горячем режиме закодировать выходные переменные y_train и y_test.

X_trainShape = X_train.shape[1]*X_train.shape[2]*X_train.shape[3]
X_testShape = X_test.shape[1]*X_test.shape[2]*X_test.shape[3]
X_trainFlat = X_train.reshape(X_train.shape[0], X_trainShape)
X_testFlat = X_test.reshape(X_test.shape[0], X_testShape)

Нам также нужно изменить форму X_train и X_test, чтобы использовать Random Under Sampler.

from imblearn.under_sampling import RandomUnderSampler
random_under_sampler = RandomUnderSampler(ratio='majority')
X_trainRos, Y_trainRos = random_under_sampler.fit_sample(X_trainFlat, y_train)
X_testRos, Y_testRos = random_under_sampler.fit_sample(X_testFlat, y_test)

Параметр «соотношение = большинство» указывает, что случайная выборка не соответствует классу большинства.

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

Модель Архитектура

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

batch_size = 256
num_classes = 2
epochs = 80
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3),
                 activation='relu',
                 input_shape=(50,50,3)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, (3,3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(Conv2D(256, (3, 3), activation='relu'))
model.add(Flatten()) #this converts our 3D feature maps to 1D feature vectors for the dense layer below
model.add(Dropout(0.5))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(128, activation='relu'))
model.add(Dense(num_classes, activation='sigmoid'))

Модель является последовательной, что позволяет нам создавать модель слой за слоем. Архитектура состоит из сверточных слоев, слоев максимального объединения, слоев исключения и полностью связанных слоев.

Первый слой представляет собой сверточный слой с 32 фильтрами, каждый размером 3 x 3. Нам также необходимо указать входную форму в первом слое, который в нашем случае составляет 50 x 50 x 3.

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

Второй слой - это объединяющий слой. Слои объединения используются для уменьшения размера. Максимальный пул с окном 2x2 учитывает только максимальное значение в окне 2x2.

Третий уровень снова представляет собой сверточный слой из 64 фильтров, каждый размером 3 x 3, за которым следует еще один максимальный уровень объединения с окном 2x2. Обычно количество фильтров в сверточном слое увеличивается после каждого слоя. Первые слои с меньшим количеством фильтров изучают простые функции изображений, тогда как более глубокие слои изучают более сложные функции.

Следующие два слоя снова являются сверточными слоями с тем же размером фильтра, но с увеличивающимся числом фильтров; 128 и 256.

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

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

Значение 0,5 взято из оригинальной статьи Hinton (2012), которая оказалась очень эффективной. Эти выпадающие слои добавляются после каждого из полностью подключенных слоев перед выводом. Отсев также сокращает время обучения для каждой эпохи.

Следующий плотный слой (полностью связанный слой) состоит из 128 нейронов. Затем следует еще один слой отсева с коэффициентом отсева 0,5.

Следующий слой - еще один плотный слой со 128 нейронами.

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

Составление модели

model.compile(loss=keras.losses.binary_crossentropy,
              optimizer=keras.optimizers.Adam(lr=0.00001),
              metrics=['accuracy'])

Модель скомпилирована с двоичной функцией кросс-энтропии потерь и используется оптимизатор Адама. Метрика «точность» используется для оценки модели.

Adam - это алгоритм оптимизации, который итеративно обновляет веса сети.

Хотя начальную скорость обучения для Адама можно установить (в нашем случае мы установили ее на 0,00001), это начальная скорость обучения, и скорость обучения для каждого параметра адаптируется по мере начала обучения. Вот чем Адам (сокращение от оценки адаптивного момента) отличается от стохастического градиентного спуска, который поддерживает единую скорость обучения для всех обновлений веса. Подробное объяснение алгоритма оптимизации Адама можно найти здесь.

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

Увеличение данных

datagen = ImageDataGenerator(
    featurewise_center=True,
    featurewise_std_normalization=True,
    rotation_range=180,
    horizontal_flip=True,vertical_flip = True)

Как правило, чем больше у нас данных, тем лучше работает глубокое обучение. Keras ImageDataGenerator генерирует изображения в реальном времени во время обучения с использованием увеличения данных. Преобразования выполняются на мини-пакетах «на лету». Увеличение данных помогает обобщить модель за счет уменьшения способности сети соответствовать обучающим данным. Вращение, вертикальное и горизонтальное переворачивание - некоторые из распространенных методов увеличения данных.

Keras ImageDataGenerator предоставляет различные методы увеличения данных. Однако мы будем использовать лишь некоторые из них.

Гистопатологический слайд, помеченный как злокачественный, остается злокачественным, если его повернуть на 20 градусов и перевернуть вертикально.

Обучение модели

Обучение модели с помощью графического процессора ускоряет процесс обучения. Для этого вам понадобится графический процессор NVIDIA. Я последовал этому руководству, чтобы включить обучение GPU.

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

early_stopping_monitor = EarlyStopping(monitor='val_loss', patience=3, mode='min')

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

В нашем случае мы говорим EarlyStopping отслеживать val_loss, и если он не улучшается в течение 3 эпох непрерывно, останавливаем процесс обучения ».

Размер пакета обычно устанавливается в степень 2, потому что он более эффективен с точки зрения вычислений. Мы установили 256.

Мы используем еще один обратный вызов Keras, называемый ModelCheckpoint.

model_checkpoint = ModelCheckpoint('best_model.h5', monitor='val_loss', mode='min', verbose=1, save_best_only=True)

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

Комбинируя оба этих обратных вызова, сохраняется лучшая модель (которая имеет минимальные потери при проверке), и после этого, если потери при проверке не улучшаются (уменьшаются) в течение следующих 3 эпох (заданных EarlyStopping), обучение модели прекращается.

training = model.fit_generator(datagen.flow(X_trainRosReshaped,Y_trainRosHot,batch_size=batch_size),steps_per_epoch=len(X_trainRosReshaped) / batch_size, epochs=epochs,validation_data=(X_testRosReshaped, Y_testRosHot), verbose=1, callbacks=[early_stopping_monitor, model_checkpoint])

Поскольку мы используем ImageDataGenerator на лету, мы используем model.fit_generator для обучения модели. Мы устанавливаем для него переменную «обучение», так как позже мы построим график потерь при обучении и проверке. Это помогает нам получить представление о дисперсии, то есть о разнице между ошибкой обучения и ошибкой набора проверки.

Для проверки мы будем использовать X_testRosReshaped и Y_testRosHot, которые мы получили после недовыборки наборов x_test и y_test.

Обучение прекращается через 37 эпох из-за ранней остановки. Следовательно, лучшая сохраненная модель - это модель эпохи 34 с точностью проверки 79,10%.

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

Делать прогнозы

from keras.models import load_model
from sklearn import metrics
model = load_model('best_model.h5')
y_pred_one_hot = model.predict(X_testRosReshaped)
y_pred_labels = np.argmax(y_pred_one_hot, axis = 1)
y_true_labels = np.argmax(Y_testRosHot,axis=1)

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

Оценка производительности модели

confusion_matrix = metrics.confusion_matrix(y_true=y_true_labels, y_pred=y_pred_labels)

Мы используем матрицу неточностей для оценки производительности моделей. Матрица неточностей в матрице двоичной классификации имеет четыре квадранта; ложные срабатывания, ложные отрицания, истинные положительные и истинные отрицательные результаты.

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

Мы получаем матрицу путаницы:

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

Мы видим, что наша модель работает хорошо с точностью 79,10% на тестовой выборке. Только матрица путаницы также подходит для нас, и у нас есть модель с низкой дисперсией.

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

Спасибо за внимание. Если у вас есть какие-либо вопросы, прокомментируйте их ниже. Я буду регулярно писать на такие темы, как глубокое обучение, поэтому подписывайтесь на меня здесь, на Medium. Я также доступен в LinkedIn! :) Удачного кодирования.