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

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

В этой статье я кратко объясню проблему, которую мне пришлось решить (обнаружение опухоли мозга с помощью МРТ), и стек, который я использовал (Colab, Drive и потрясающее сообщество с открытым исходным кодом Kaggle), чтобы получить окончательный результат (который в конечном итоге был разочаровывающим). . Для меня это был не первый хакатон, но это был мой первый конкурс Kaggle, в котором я был полон решимости пройти до конца. И я сделал.

Что решать?

Введение в проблему

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

Проблема, как описывают авторы:

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

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

…. В этом соревновании вы будете предсказывать генетический подтип глиобластомы с помощью сканирования МРТ (магнитно-резонансная томография), чтобы обучить и протестировать вашу модель на наличие метилирования промотора MGMT.

Соревнование проводилось с 13 июля по 15 октября 2021 года. В нем участвовало более 1555 команд, включая 1958 участников и 27 466 заявок (частично из-за запутанных правил подачи заявок, но все же впечатляющих).

Введение в данные

Нам сделали МРТ около 685 человек. Данные в необработанном виде представляли собой объекты Dicom объемом 128 ГБ (DCM, по-видимому, общий формат для обмена данными в мире медицины).

Training/Validation/Testing
│
└─── 00000
│   │
│   └─── FLAIR
│   │   │ Image-1.dcm
│   │   │ Image-2.dcm
│   │   │ ...
│   │   
│   └─── T1w
│   │   │ Image-1.dcm
│   │   │ Image-2.dcm
│   │   │ ...
│   │   
│   └─── T1wCE
│   │   │ Image-1.dcm
│   │   │ Image-2.dcm
│   │   │ ...
│   │   
│   └─── T2w
│   │   │ Image-1.dcm
│   │   │ Image-2.dcm
│   │   │ .....
│   
└─── 00001
│   │ ...
│   
│ ...   
│   
└─── 00002
│   │ ...

Для каждого пациента (или записи, например, «00000») у нас есть 4 типа сканирований (или каналов, если хотите), каждый из которых представляет определенное направление (тип МРТ), в котором мозг был разрезан сканером МРТ. Включены точные типы МРТ:

  • Восстановление инверсии с затуханием жидкости (FLAIR)
  • Т1-взвешенный предварительный контраст (T1w)
  • Постконтрастный T1-взвешенный (T1wCE)
  • T2-взвешенный (T2w)

Здесь мы создадим набор, чтобы вместе называть их N = {FLAIR, T1W, T1wCE, T2W}

train_labels.csv файл содержит MGMT_value непосредственно для каждого предмета в обучении. Это становится нашей целью (наличие метилирования промотора MGMT).

Эта ссылка прекрасно визуализирует данные - обязательно посмотрите! Здесь Аюш создал GIF-файл каждого набора сканированных изображений для каждой записи, которая у нас есть.

У каждого пациента есть серия изображений (в некоторых случаях более 100 изображений для каждого типа МРТ). Таким образом, мы можем рассматривать это как видеоданные для каждого типа МРТ для каждого пациента. У нас есть серия поперечных срезов головного мозга (m-й тип МРТ), начиная с t = 0 , в X[i, m, 0] для i-го пациента. ; который продолжается до временного шага t = T , где t ≥ 0 , в X[i, m, T].

С 4 типами МРТ и произвольным выбором 64 образцов из T временных шагов: мы получаем «воксель» из 256 изображений (4 изображения на временной шаг × 64 временных шага), организованный как 1 x 4 x 64 x 256 x 256, где 1 представляет 1 пациента, 4 представляет 4 типа МРТ-сканирования (N), 64 представляет количество выборок из T временных шагов, а 256 соответствуют высоте и ширине изображения (также обычно 256).

Авторы конкурса коварно решили немного смешать подтипы МРТ между собой, тем самым испортив данные. Итак, у нас было несколько образцов T2w, скрывающихся под FLAIR (как мы можем видеть для пациента «00002» на изображении 1). Я считал, что сохранение данных в том виде, как они есть, может впоследствии помочь модели упорядочить себя и, таким образом, избежать переобучения, поэтому я оставил ее как есть.

Загрузка данных

0. Преобразование формата из .dcm в .png (128 ГБ - ›5,2 ГБ)

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

Например, Джонатан Бесоми любезно поделился своей записной книжкой, где он преобразовал все файлы .dcm размером 128 ГБ в более легко читаемые файлы .png размером всего 5,2 ГБ. Спасибо, Джонатан. Миру нужно больше героев, подобных вам.

Следующим шагом было сделать данные легко доступными для использования в среде Colab. Раньше я выполнял несколько проектов с помощью Colab и Deep Learning, поэтому я использовал Drive и Colab вместе, однако данные изображений пришли со своими проблемами, которые я медленно обнаружил.

1. Наивный метод (запрос необработанных данных из Colab)

from google.colab import drive
drive.mount('/content/drive')

Во-первых, 5,2 ГБ данных - это еще немного, когда дело доходит до загрузки каждый раз, когда я запускаю свой экземпляр Colab. Изначально я загрузил все 5,2 ГБ данных на свой Google Диск. Затем мне пришлось подключить свой Google Диск к моей среде Colab, чтобы я мог удаленно получить доступ к файлам, хранящимся на моем Диске (после авторизации экземпляра).

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

2. Загрузите все данные в Colab Local и разархивируйте

! unzip path/in/drive/data.zip  -d path/in/colab/data

Идея здесь в том, что мы загружаем zip-папку (вместо необработанных данных .png) на Google Диск. А затем, когда мы запускаем экземпляр Colab, мы подключаемся к Диску и распаковываем папку данных на экземпляр Colab. Теперь у нас есть данные, доступные непосредственно в Colab, поэтому их можно запрашивать намного быстрее, сохраняя при этом сжатую и удобную для переноса версию данных на Диск.

3. Применяйте кеширование для ускорения __getitem__ во время обучения и тестирования

Любой, кто раньше работал с какой-либо структурой PyTorch, знает, что для загрузки данных для обучения мы создаем класс Dataset, который используется с DataLoader для обучения, проверки и прогнозирования. Функция __getitem__ определена в Dataset для конкретной загрузки объекта и его возврата; поэтому каждый раз, когда мы вызываем data[0], мы неявно вызываем data.__getitem__(0). Это распространено в приложениях DL из-за огромного количества данных, которые необходимо потребить, и поскольку мы не можем загрузить все данные в память сразу, мы создаем функцию __getitem__, которая читает с диска и выгружает их в память.

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

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

Теперь, чтобы кэшировать данные, сохранив их в self.cache, мы хотим преобразовать тензор в объект scipy.sparse, чтобы сохранить нашу оперативную память. Если numpy берет 112 единиц, тогда torch.tensor берет 72 единиц, а scipy.sparse объект занимает 60 единиц. И scipy.sparse матричных операций ожидает двумерная матрица. Итак, мы преобразовываем данные из 4 измерений 4 x 64 x 256 x 256 в 2 измерения, а затем кэшируем их как разреженную матрицу. Это хорошо работает, потому что для данных изображений, особенно данных МРТ, многие значения равны 0.

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

Это сократило время тренировки с примерно 15 минут на эпоху до менее 2 минут на эпоху.

Дополнительное примечание: я попытался сохранить только массивы NumPy, но моя оперативная память продолжала не хватать памяти. Сначала я подумал, что это из-за того, что мои num_workers в DataLoader были высокими, поэтому я установил его на 0. Нам нужно установить его на 0 , иначе он создаст несколько копий кеша, что снова приведет к перегрузке вашей оперативной памяти. Затем, с NumPy, даже с num_workers = 0 мой кеш все еще перегружал мою оперативную память, поэтому я решил использовать разреженную матрицу SciPy как наиболее эффективный способ решения данной проблемы.

Моделирование

Google Colab имеет бесплатный доступ к графическому процессору и около 13 ГБ оперативной памяти. Для этого соревнования, поскольку я продолжал использовать графический процессор постоянно и часто (иногда в ночное время), мне стало труднее получить хороший графический процессор (если вообще). Со временем это стало препятствием на пути к хорошему прогрессу, поэтому я решил просто инвестировать 10 долларов в месяц с подпиской на Google Colab Pro. И после этого мои заботы о графическом процессоре прекратились (по большей части). Я все еще был ограничен оперативной памятью графического процессора с точки зрения размера модели, которую я мог обучить.

Подход 1. Комбинированная модель CNN-RNN

Я начал соревнование, полагая, что он созрел для модели CNN-RNN. У нас есть CNN для пространственных зависимостей, RNN для временных зависимостей, а затем с несколькими полностью связанными слоями выводится окончательная вероятность. Со временем, когда он все время переоснащался моей модели, я понял, что у нас почти не хватает образцов для обучения двух отдельных пользовательских модулей. И я был в невыгодном положении по сравнению с другими Kagglers, которые использовали предварительно обученные модели; в течение недели я отказался от своей работы с CNN-RNN и начал все сначала с семейства предварительно обученных моделей EfficientNet.

Примечание. Оказалось, что я ошибался. В пятерке лучших решений использовалась специальная модель CNN-RNN, получившая оценку 0,61. Великолепно.

Подход 2. Семейство предварительно обученных моделей EfficientNet

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

Я нашел в Интернете очень хорошую статью Николаса Адалоглоу, в которой подробно описаны различия между различными предварительно обученными архитектурами CNN, такими как EfficientNet, ResNet, DenseNet и т. Д. Вы можете получить к ней доступ здесь.

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

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

Это был возможный конвейер EfficientNet3D, который я мог создать.

  • Для каждого пациента мы рассматриваем 4 последовательности (FLAIR, T1w, T1Gd, T2), и для каждой из этих последовательностей мы берем 64 среза из середины. Мы изменяем размер фрагмента изображения до (256 x 256)
  • Создайте объект EfficientNet3d в PyTorch с формой ввода (256, 256, 256) или (4, 64, 256, 256)
  • Настройте полностью подключенный компонент по желанию
  • Выполнить двоичную классификацию

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

Поэтому я решил использовать другой подход с (4, 64, 256, 256), где мы рассматриваем каждый тип МРТ как отдельный канал. И для каждого канала у нас есть трехмерный тензор, представляющий 64 фрагмента (256 x 256) изображений.

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

Теперь это была просто игра с экспериментами. Я много играл со следующими аспектами конвейера моделирования:

  • Скорость обучения
  • Оптимизатор
  • Планировщик
  • Нормализация
  • Отключение обучения сверточных слоев после 75% точности обучения (только полносвязный слой на нем)
  • Полностью связанный слой - количество блоков и количество слоев

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

Как бороться с переобучением

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

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

Нормализация с помощью MCLAHE

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

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

Наивные способы, которые я продолжал пробовать, были основаны на стандартизации w.r.t. среднее значение и стандартное отклонение канала для конкретного образца, а затем нормализация между 0–1. Это не сработало. Именно тогда я натолкнулся на новую технику с учетом времени, основанную на CLAHE под названием MCLAHE (M для многомерного) в ветке обсуждения Kaggle. Мне это было интересно из-за теоретических возможностей этой техники, как описано в их репозитории на GitHub.

Проблему переобучения было нелегко решить только этим.

Я часами гуглил и пробовал разные вещи - более глубокие и широкие fc слои, меньшую и большую частоту ошибок (и все, что между ними) и различные параметры нормализации. Я пробовал все версии EfficientNet3D от b0 - самой маленькой и простой с 5,3 млн параметров до b7 - более мудрого дедушки с 66 млн параметров.

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

Однако, что бы я ни пробовал, модель либо превосходила обучающие данные, либо вообще ничего не учила - точность проверки редко превышала ~ 62%. В те дни моя интуиция подверглась серьезному испытанию, когда я подверг сомнению все, что знал о CNN.

Окончательная отправка

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

В рамках самого соревнования мой результат в публичной таблице лидеров составил 0,54 балла по бинарной кросс-энтропии, а в частной таблице лидеров (последняя со скрытыми тестовыми данными) - 0,45, что соответствует примерно 1200-му месту из 1600. Победитель конкурса получил 0,63 балла, так что это был довольно низкий балл. Вот целая ветка обсуждения того, была ли эта задача вообще возможной - оказывается, вероятно, нет; по крайней мере, не с этими данными.

Заключение

Это был не тот результат, на который я надеялся. За последние 2 дня до даты окончания конкурса я подумал, может, мне лучше просто обучить EfficientNet3D для каждого типа МРТ (то есть по одной модели на канал), но я отказался от этого. Для меня это было исследование чего-то нового, изучение этого и тщательное изучение этого.

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

Это позволило мне проверить свое понимание архитектур CNN, впервые исчерпать ОЗУ, посмотреть, как код, который я пишу, «узнать» что-то, что делают врачи, посмотреть на мои значения потерь в ожидании, когда они приближаются к историческим минимумам - это было весь опыт. И, наконец, общение с очень интересным, знающим и находчивым сообществом Kaggle. Я вернусь снова для этого приключения.

Если вы дошли до этого места, спасибо, что нашли время прочитать это! В качестве награды ссылка: https://github.com/tg1482/mri-brain-tumor-detection

Больше контента на plainenglish.io