Сверточные нейронные сети (CNN) для распознавания изображений

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

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

Внедрение ИИ-решений в этой области потенциально может способствовать решающему шагу вперед в развитии.

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

Этапы реализации проекта будут следующими:

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

Часть I: Предварительная обработка и исследовательский анализ данных обучения

Набор обучающих данных для этого проекта будет включать 800 изображений рентгеновских снимков грудной клетки: 662 из которых были предоставлены Народной больницей Шэньчжэня №3 в Китае, а остальные 138 - из Министерства здравоохранения и социальных служб округа Монтгомери. США.

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

Набор данных был первоначально опубликован Национальной медицинской библиотекой США с целью предоставить достаточное количество общедоступных данных по обучению для исследований в области компьютерной диагностики заболеваний легких. Данные, используемые в этом проекте, были получены через Kaggle любезно предоставленным пользователем K Scott Mader. Все соответствующие цитаты можно найти в разделе Ссылки в конце этого сообщения.

Кодирование изображений

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

Метод imread пакета по существу создаст массив numPy для каждого изображения с каждым элементом в массиве, представляющим закодированную шкалу серого отдельного пикселя, с тремя отдельными слоями для синего, зеленого и красного.

Размеры каждого массива будут равны высоте изображения (в пикселях), ширине (в пикселях) и количеству цветовых слоев (три).

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

Получение целевых меток

Описание в источнике обучающих данных объясняет, что метка наземной достоверности для каждого изображения (как определено медицинским экспертом) хранится как суффикс в его имени файла: с 1, обозначающим, что сканирование отображает отклонение от нормы и 0, означающее, что это не так.

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

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

def encode_images():
    
    X = []
    y = []
    directories = ['xray_images/ChinaSet_AllFiles/ChinaSet_AllFiles/CXR_png/', 
                  'xray_images/Montgomery/MontgomerySet/CXR_png/']
for directory in directories:
        for filename in os.listdir(directory):
            if filename.endswith('.png'):
                X.append(cv2.imread(directory + filename))
                y.append(int(filename[-5]))
    
    return np.array(X), np.array(y)

Оценка и изменение размеров изображений

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

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

Мы видим, что размеры изображений варьируются. Чтобы исправить это, мы можем переписать нашу функцию кодирования, чтобы реализовать метод resize из пакета Pillow в начале цикла For.

Но какой размер мы должны выбрать для наших входных изображений?

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

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

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

img = Image.open(directory + filename)
img = img.resize((256, 256))
img.save(directory + filename)

Нормализация масштаба изображений

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

Чтобы учесть это, прежде чем перейти к этапу моделирования, давайте разделим наш массив функций на максимум (255), чтобы нормализовать каждую запись так, чтобы она варьировалась от 0 до 1:

print((np.min(X), np.max(X)))
X = X.astype('float32') / 255
print((np.min(X), np.max(X)))
>>> (0, 255)
>>> (0.0, 1.0)

Анализ целевой переменной

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

Давайте посмотрим на разделение данных обучения на положительную и отрицательную:

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

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

Часть II: Построение модели CNN

Разделение обучающих данных

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

При работе с любой формой нейронной сети рекомендуется разбить данные на три отдельных набора (обучение, проверка и тестирование) вместо двух стандартных.

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

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

Мы можем выполнить это разделение, используя метод train_test_split scikit-learn, чтобы сначала разделить данные на наборы для обучения и тестирования, а затем снова разделить набор для тестирования на отдельные наборы для проверки и тестирования:

train_size = 0.6
val_size = 0.2
test_size = 0.2
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=(val_size + test_size), random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_test, y_test, test_size=(test_size / (val_size + test_size)), random_state=42)
print('Training: {}; Validation: {}; Testing: {}'.format((len(X_train), len(y_train)), (len(X_val), len(y_val)), (len(X_test), len(y_test))))
>>> Training: (480, 480); Validation: (160, 160); Testing: (160, 160)

Кодирование целевой переменной

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

y_train = keras.utils.to_categorical(y_train, len(set(y)))
y_val = keras.utils.to_categorical(y_val, len(set(y)))
y_test = keras.utils.to_categorical(y_test, len(set(y)))
print(y_train.shape, y_val.shape, y_test.shape)
>>> (480, 2) (160, 2) (160, 2)

Построение базовой модели

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

  1. Начальный входной слой, в котором мы указываем форму наших входных объектов.
  2. Ноль или более «скрытых» слоев, которые будут пытаться выявить закономерности в данных.
  3. Последний выходной слой, который будет классифицировать каждый экземпляр на основе его входных данных.

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

base_model = Sequential()
base_model.add(Conv2D(filters=16, kernel_size=2, padding='same', activation='relu', input_shape=X[0].shape))
base_model.add(MaxPooling2D(pool_size=2))
base_model.add(Flatten())
base_model.add(Dense(len(set(y)), activation='softmax'))

Несколько замечаний относительно базовой модели:

  1. Нам нужно указать количество фильтров, которые мы хотим использовать, а также размер квадратных ядер во входном слое.
  2. Нам также необходимо указать отступы на случай, если фильтр выйдет за край изображения, для чего мы будем использовать ‘same’
  3. Мы начнем с функции активации ReLu, которая оставляет без изменений положительные значения и устанавливает отрицательные значения на 0. Мы можем протестировать различные функции, когда настроим модель.
  4. Поскольку мы изменили размер всех входных данных функций, чтобы они не менялись, мы можем использовать первый экземпляр, чтобы указать форму входных данных.
  5. Слой максимального объединения был добавлен, чтобы уменьшить размерность данных, беря максимальное значение из каждого квадрата 2x2.
  6. Прежде чем мы дойдем до выходного слоя, нам нужно сгладить данные в 2D-массив, чтобы их можно было передать на полностью связанный слой.
  7. На уровне вывода нам нужно указать, для скольких классов модель должна делать прогнозы (равное количеству уникальных значений в нашем целевом массиве, которое в данном случае равно 2), и использовать функцию активации Softmax для нормализации вывода как распределение вероятностей

Нам также необходимо скомпилировать модель, которая включает определение метрики (показателей), которые мы будем использовать для измерения ее эффективности. Поскольку мы обнаружили, что целевая переменная была равномерно сбалансирована по классам при изучении данных, чистая точность должна быть подходящей метрикой для использования:

base_model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])

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

После того, как мы определили и скомпилировали архитектуру модели, мы можем подогнать ее к обучающим данным:

base_model.fit(X_train, y_train, epochs=10, batch_size=32, validation_data=(X_val, y_val), verbose=1, shuffle=True)

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

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

Количество эпох сообщает модели, сколько раз она должна протолкнуть данные через сеть и впоследствии распространить в обратном направлении веса и смещение. Вскоре мы обсудим оптимальное количество эпох, но для начала мы будем использовать 10.

Оценка базовой модели

После того, как мы подогнали модель к указанному количеству эпох, мы можем оценить ее истинную производительность на ранее невиданном тестовом наборе:

round(base_model.evaluate(X_test, y_test, verbose=0)[1], 4)
>>> 0.7812

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

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

Выбор количества эпох

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

epochs = [5, 10, 20, 50, 100, 200]
scores = []
for e in epochs:
    test_model = Sequential()
    test_model.add(Conv2D(filters=16, kernel_size=2, padding='same', activation='relu', input_shape=X[0].shape))
    test_model.add(MaxPooling2D(pool_size=2))
    test_model.add(Flatten())
    test_model.add(Dense(len(set(y)), activation='softmax'))
    test_model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
    test_model.fit(X_train, y_train, epochs=e, batch_size=32, validation_data=(X_val, y_val), verbose=False, 
                   shuffle=True)
    scores.append(test_model.evaluate(X_test, y_test, verbose=False)[1])

а затем нанесите на график результаты:

Из вышесказанного видно, что использование 20 эпох вместо 5 или 10 значительно улучшает производительность модели. Однако последующее увеличение числа эпох приводит к менее радикальным улучшениям, если таковые имеются.

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

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

Внедрение увеличения изображения

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

Увеличение изображения относительно просто выполнить в Keras. Сначала нам нужно создать и подогнать объекты генератора изображений для каждого из наборов обучения и проверки:

datagen_train = ImageDataGenerator(width_shift_range=0.1, height_shift_range=0.1, horizontal_flip=True)
datagen_val = ImageDataGenerator(width_shift_range=0.1, height_shift_range=0.1, horizontal_flip=True)
datagen_train.fit(X_train)
datagen_val.fit(X_val)

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

batch_size = 32
aug_base_model.fit(datagen_train.flow(X_train, y_train, batch_size=batch_size), 
                   steps_per_epoch=X_train.shape[0] / batch_size, epochs=20, verbose=1, callbacks=[checkpointer], 
                   validation_data=datagen_val.flow(X_val, y_val, batch_size=batch_size), 
                   validation_steps=X_val.shape[0] / batch_size)

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

Мы можем использовать тот же метод, который использовался ранее, для оценки производительности расширенной базовой модели:

round(aug_base_model.evaluate(X_test, y_test, verbose=0)[1]
>>> 0.6875

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

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

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

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

Настройка модели

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

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

  1. Реализация дополнительных сверточных слоев
  2. Добавление плотных слоев перед окончательным выходным слоем
  3. Реализация исключения для случайного «выключения» некоторых узлов в каждом слое.
  4. Экспериментируйте с различными функциями активации, такими как сигмовидная функция
  5. Использование больших шагов в сверточных слоях

Конечный продукт

После нескольких раундов настройки модель, получившая наивысший балл точности (84%), состояла из:

  • Три сверточных слоя с увеличивающимся количеством фильтров
  • Функция активации ReLu на каждом слое
  • Максимальный слой объединения размером два после каждого слоя.
  • Выпадающий слой с вероятностью 0,3 после третьего сверточного слоя

Часть III: Обсуждение подхода

Успехов

Используя набор данных рентгеновских изображений, предварительно помеченных опытными радиологами, мы смогли построить простую модель CNN, чтобы правильно идентифицировать легочные аномалии в 78% случаев.

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

Ограничения

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

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

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

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

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

Разработка модели в будущих версиях

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

Чтобы справиться с непрактичностью обучения сложной CNN на локальном ЦП, также было бы целесообразно использовать услуги облачного сервера с поддержкой графического процессора, такого как AWS.

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

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

Заключительное слово

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

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

Путь от набора данных необработанных изображений к окончательной рабочей модели включал:

  1. Изменение размера, кодирование и нормализация данных изображения
  2. Построение и оценка архитектуры базовой модели CNN в качестве отправной точки
  3. Экспериментирование и настройка ряда условий модели, таких как количество эпох, количество сверточных слоев и использование исключения, для повышения производительности.

Читатели, заинтересованные в изучении кода, включая различные этапы настройки, более подробно могут найти его в этом репозитории моего Github. Отзывы, вопросы и предложения по улучшению модели всегда приветствуются.

использованная литература

К. Скотт Мадер Рентгенологические аномалии легочной грудной клетки https://www.kaggle.com/kmader/pulmonary-chest-xray-abnormalities

Jaeger S, Candemir S, Antani S, Wáng YX, Lu PX, Thoma G. Два общедоступных набора данных рентгена грудной клетки для компьютерного скрининга легочных заболеваний. Quant Imaging Med Surg. 2014. 4 (6): 475–477. DOI: 10.3978 / j.issn.2223–4292.2014.11.20

Джагер С., Караргирис А., Кандемир С., Фолио L, Сигельман Дж., Каллаган Ф., Сюэ З., Паланиаппан К., Сингх Р.К., Антани С., Тома Дж., Ван YX, Лу П.Х., Макдональд С.Дж. Автоматический скрининг на туберкулез с использованием рентгенограмм грудной клетки. IEEE Trans Med Imaging. 2014 Февраль; 33 (2): 233–45. DOI: 10.1109 / TMI.2013.2284099. PMID: 24108713

Кандемир С., Джегер С., Паланиаппан К., Муско Дж. П., Сингх Р.К., Сюэ З., Караргирис А., Антани С., Тома Дж., Макдональд С.Дж. Сегментация легких на рентгенограммах грудной клетки с использованием анатомических атласов с нежесткой регистрацией. IEEE Trans Med Imaging. 2014 Февраль; 33 (2): 577–90. DOI: 10.1109 / TMI.2013.2290491. PMID: 24239990