Более подробное определение соревнования представлено на сайте Kaggle RSNA Pneumonia Detection Challenge: https://www.kaggle.com/c/rsna-pneumonia-detection-challenge
Цель состоит в том, чтобы создать алгоритм, который может обнаруживать визуальные сигналы пневмонии на медицинских изображениях. В частности, алгоритм должен автоматически обнаруживать помутнения легких на рентгенограммах грудной клетки, но только помутнения, которые выглядят как пневмония, и отбрасывать другие типы помутнений, например, вызванные перегрузкой жидкостью (отек легких), кровотечением, потерей объема (ателектаз или коллапс). , рак легких, пострадиационные или хирургические изменения. За пределами легких жидкость в плевральной полости (плевральный выпот) также проявляется в виде повышенной непрозрачности на рентгенограмме. Помутнение от пневмонии - это часть легких, которая на рентгенограмме выглядит темнее и имеет форму, указывающую на наличие (или возможность наличия) пневмонии.
Поскольку цель состоит в том, чтобы обнаружить и нарисовать ограничивающую рамку для каждой непрозрачности пневмонии, где каждое изображение может иметь 0 или несколько непрозрачностей, а обучающая выборка уже классифицирована, ее можно проанализировать как статистическую многоабонентскую классификацию с контролируемым обучением.
Некоторые алгоритмы / модели классификации были адаптированы для задачи с несколькими метками, наиболее распространенными являются: повышение, k-ближайшие соседи, деревья решений, методы ядра для векторного вывода и нейронные сети. Необходимо провести некоторые тесты, чтобы определить, какой из них даст наилучшие результаты в этом соревновании, но нейронные сети, кажется, являются лучшим выбором, особенно DCN (Deep Convolutional Network).
Ядро с лучшим результатом на момент написания этого документа использует сверточную нейронную сеть ResNet для сегментации изображения.
Шаги по обучению модели для задачи обнаружения пневмонии RSNA:
Путем копирования и вставки каждого из описанных ниже фрагментов кода в Google Colaboratory будет сгенерирован полный файл .csv, готовый для отправки на конкурс Kaggle, так что это хороший способ понять, как работает конкурс и какие части составить модель машинного обучения, которую можно изменять и настраивать для получения лучших результатов. В некоторых разделах есть некоторые изменения и параметры, которые можно попробовать улучшить ядро.
1 Установите и импортируйте требования:
# install dependencies not included by Colab # use pip3 to ensure compatibility w/ Google Deep Learning Images !pip3 install -q pydicom !pip3 install -q tqdm !pip3 install -q imgaug import os import sys import random import math import numpy as np import cv2 import matplotlib.pyplot as plt import json import pydicom from imgaug import augmenters as iaa from tqdm import tqdm import pandas as pd import glob # Install Kaggle API to download competition data !pip3 install -q kaggle
2 Загрузите набор данных:
Конкурс проходит в два этапа, поэтому для текущего этапа (этап 1) файлы:
Образы поезда: stage_1_train_images.zip
Тестовые изображения: stage_1_test_images.zip
Данные тренировки: «stage_1_train_labels.csv»
Пример файла для отправки: «stage_1_sample_submission.csv,»
Файл с подробной информацией о положительных и отрицательных классах обучающей выборки: «stage_1_detailed_class_info.csv»
Как указано в этом начальном ядре: https://github.com/mdai/ml-lessons/blob/master/lesson3-rsna-pneumonia-detection-kaggle.ipynb
Есть два способа загрузки файлов: один - с использованием формата данных Kaggle, а другой - с использованием клиентской библиотеки Python MD.ai. В следующем примере подробно рассматривается первый вариант с использованием Google Colaboratory.
Изменения и варианты, которые стоит попробовать: загрузка файлов на диск Google для прямого доступа к ним или загрузка их с помощью библиотеки MD.ai, как указано здесь https://github.com/mdai/ml -lessons / blob / master / lesson3-rsna-pneumonia-detection-mdai-client-lib.ipynb
Это не варианты, которые помогли бы улучшить окончательный результат в соревновании, так как любой из них получит точно такие же файлы, просто хорошо знать, что они существуют, на случай, если есть один, лучше подходящий для конкретного человека / платформы / программы. .
3 Вы должны принять пользовательское соглашение на веб-сайте конкурса:
Затем следуйте инструкциям, чтобы получить учетные данные Kaggle.
Если вам не удается загрузить набор данных о конкурсе, проверьте, приняли ли вы пользовательское соглашение на веб-сайте конкурса.
После того, как у вас есть имя пользователя и ключ, назначьте их соответствующим переменным в следующем коде:
# enter your Kaggle credentials here os.environ['KAGGLE_USERNAME']="" os.environ['KAGGLE_KEY']="" # Root directory of the project ROOT_DIR = os.path.abspath('./lesson3-data') # Directory to save logs and trained model MODEL_DIR = os.path.join(ROOT_DIR, 'logs') if not os.path.exists(ROOT_DIR): os.makedirs(ROOT_DIR) os.chdir(ROOT_DIR) # If you are unable to download the competition dataset, check to see if you have # accepted the user agreement on the competition website. !kaggle competitions download -c rsna-pneumonia-detection-challenge # unzipping takes a few minutes !unzip -q -o stage_1_test_images.zip -d stage_1_test_images !unzip -q -o stage_1_train_images.zip -d stage_1_train_images !unzip -q -o stage_1_train_labels.csv.zip
4 Реализуйте конкретную модель:
В этом случае модель Matterport Mask-RCNN будет установлена с github.
os.chdir(ROOT_DIR) !git clone https://github.com/matterport/Mask_RCNN.git os.chdir('Mask_RCNN') !python setup.py -q install # Import Mask RCNN sys.path.append(os.path.join(ROOT_DIR, 'Mask_RCNN')) # To find local version of the library from mrcnn.config import Config from mrcnn import utils import mrcnn.model as modellib from mrcnn import visualize from mrcnn.model import log train_dicom_dir = os.path.join(ROOT_DIR, 'stage_1_train_images') test_dicom_dir = os.path.join(ROOT_DIR, 'stage_1_test_images')
Изменения и варианты, которые стоит попробовать. Это может быть наиболее важной частью ядра, так как тип модели и ее форма будут определять, как будет происходить обучение, и будут напрямую влиять на предсказательную способность модели. отсюда и оценка конкурса.
В данном примере используется модель Mask RCNN, а именно https://github.com/matterport/Mask_RCNN.git.
Но есть много других моделей, которые можно использовать здесь, наиболее подходящими будут модели, разработанные для классификации по нескольким меткам и использующие DCN (Deep Convolutional Network). Также существует возможность загрузки предварительно обученной модели, поэтому нет необходимости обучать ее с нуля.
5 Определите некоторые функции и классы:
В этом случае для использования в модели Mask-RCNN.
def get_dicom_fps(dicom_dir): dicom_fps = glob.glob(dicom_dir+'/'+'*.dcm') return list(set(dicom_fps)) def parse_dataset(dicom_dir, anns): image_fps = get_dicom_fps(dicom_dir) image_annotations = {fp: [] for fp in image_fps} for index, row in anns.iterrows(): fp = os.path.join(dicom_dir, row['patientId']+'.dcm') image_annotations[fp].append(row) return image_fps, image_annotations # The following parameters have been selected to reduce running time for demonstration purposes # These are not optimal class DetectorConfig(Config): """Configuration for training pneumonia detection on the RSNA pneumonia dataset. Overrides values in the base Config class. """ # Give the configuration a recognizable name NAME = 'pneumonia' # Train on 1 GPU and 8 images per GPU. We can put multiple images on each # GPU because the images are small. Batch size is 8 (GPUs * images/GPU). GPU_COUNT = 1 IMAGES_PER_GPU = 8 BACKBONE = 'resnet50' NUM_CLASSES = 2 # background + 1 pneumonia classes # Use small images for faster training. Set the limits of the small side # the large side, and that determines the image shape. IMAGE_MIN_DIM = 64 IMAGE_MAX_DIM = 64 RPN_ANCHOR_SCALES = (32, 64) TRAIN_ROIS_PER_IMAGE = 16 MAX_GT_INSTANCES = 3 DETECTION_MAX_INSTANCES = 3 DETECTION_MIN_CONFIDENCE = 0.9 DETECTION_NMS_THRESHOLD = 0.1 RPN_TRAIN_ANCHORS_PER_IMAGE = 16 STEPS_PER_EPOCH = 100 TOP_DOWN_PYRAMID_SIZE = 32 STEPS_PER_EPOCH = 100 config = DetectorConfig() config.display() class DetectorDataset(utils.Dataset): """Dataset class for training pneumonia detection on the RSNA pneumonia dataset. """ def __init__(self, image_fps, image_annotations, orig_height, orig_width): super().__init__(self) # Add classes self.add_class('pneumonia', 1, 'Lung Opacity') # add images for i, fp in enumerate(image_fps): annotations = image_annotations[fp] self.add_image('pneumonia', image_id=i, path=fp, annotations=annotations, orig_height=orig_height, orig_width=orig_width) def image_reference(self, image_id): info = self.image_info[image_id] return info['path'] def load_image(self, image_id): info = self.image_info[image_id] fp = info['path'] ds = pydicom.read_file(fp) image = ds.pixel_array # If grayscale. Convert to RGB for consistency. if len(image.shape) != 3 or image.shape[2] != 3: image = np.stack((image,) * 3, -1) return image def load_mask(self, image_id): info = self.image_info[image_id] annotations = info['annotations'] count = len(annotations) if count == 0: mask = np.zeros((info['orig_height'], info['orig_width'], 1), dtype=np.uint8) class_ids = np.zeros((1,), dtype=np.int32) else: mask = np.zeros((info['orig_height'], info['orig_width'], count), dtype=np.uint8) class_ids = np.zeros((count,), dtype=np.int32) for i, a in enumerate(annotations): if a['Target'] == 1: x = int(a['x']) y = int(a['y']) w = int(a['width']) h = int(a['height']) mask_instance = mask[:, :, i].copy() cv2.rectangle(mask_instance, (x, y), (x+w, y+h), 255, -1) mask[:, :, i] = mask_instance class_ids[i] = 1 return mask.astype(np.bool), class_ids.astype(np.int32)
Изменения и варианты, которые стоит попробовать. В этой части есть код, специфичный для RSNA Pneumonia Detection Challenge, поэтому для запуска единственными необходимыми изменениями, которые немедленно повлияют на подсчет баллов, являются следующие:
GPU_COUNT: зависит от количества графических процессоров, доступных для запуска модели.
IMAGES_PER_GPU: размер Баха / количество графических процессоров, в данном случае 8/1 = 8.
IMAGE_MIN_DIM, IMAGE_MAX_DIM: чем ближе к исходному размеру изображения, тем лучше, но это замедлит обучение.
TRAIN_ROIS_PER_IMAGE: указывает, сколько (хороших) положительных + отрицательных ROI от RPN использовать для обучения сети ODN.
MAX_GT_INSTANCES: определяет верхний предел количества экземпляров достоверных объектов на изображение.
DETECTION_MAX_INSTANCES: принимает предложения с наивысшей оценкой DETECTION_MAX_INSTANCES после NMS для классификации объектов и регрессии BB, чтобы ускорить время вывода. Таким образом, это только для вывода, а не для обучения.
DETECTION_MIN_CONFIDENCE: устанавливает предел для рассмотрения обнаружения в зависимости от его достоверности. Рентабельность инвестиций ниже этого порога пропускается.
DETECTION_NMS_THRESHOLD: NMS (Non Max Suppression) используется для устранения повторяющихся ограничивающих рамок, которые могут быть обнаружены для одной и той же непрозрачности, в идеале должна быть только одна непрозрачность для каждого обнаружения. Подробнее о NMS здесь: https://www.pyimagesearch.com/2014/11/17/non-maximum-suppression-object-detection-python
А здесь: https://www.coursera.org/lecture/convolutional-neural-networks/non-max-suppression-dvrjH
RPN_TRAIN_ANCHORS_PER_IMAGE: сколько якорей на изображение использовать для обучения RPN.
STEPS_PER_EPOCH: общее количество шагов (пакетов выборок), которые нужно выдать генератору перед объявлением одной эпохи завершенной и началом следующей эпохи.
Обычно он должен быть равен количеству уникальных выборок набора данных, деленному на размер пакета.
TOP_DOWN_PYRAMID_SIZE: размер нисходящих слоев, используемых для построения пирамиды функций.
6 Изучите данные аннотации, проанализируйте набор данных и просмотрите поля изображения dicom:
# training dataset anns = pd.read_csv(os.path.join(ROOT_DIR, ‘stage_1_train_labels.csv’)) anns.head(6)
Распечатайте поля dicom, содержащие метаданные:
image_fps, image_annotations = parse_dataset(train_dicom_dir, anns=anns) ds = pydicom.read_file(image_fps[0]) # read dicom image from filepath image = ds.pixel_array # get image array # show dicom fields ds
7 Разделите данные на наборы данных для обучения и проверки
Примечание. Мы использовали только часть изображений в демонстрационных целях. См. Комментарии ниже.
- Чтобы использовать все изображения, выполните: image_fps_list = list (image_fps)
- Или измените количество изображений со 100 на произвольное.
# Original DICOM image size: 1024 x 1024 ORIG_SIZE = 1024 ###################################################################### # Modify this line to use more or fewer images for training/validation. # To use all images, do: image_fps_list = list(image_fps) image_fps_list = list(image_fps[:1000]) ##################################################################### # split dataset into training vs. validation dataset # split ratio is set to 0.9 vs. 0.1 (train vs. validation, respectively) sorted(image_fps_list) random.seed(42) random.shuffle(image_fps_list) validation_split = 0.1 split_index = int((1 - validation_split) * len(image_fps_list)) image_fps_train = image_fps_list[:split_index] image_fps_val = image_fps_list[split_index:] print(len(image_fps_train), len(image_fps_val)) # prepare the training dataset using the DetectorDataset class dataset_train = DetectorDataset(image_fps_train, image_annotations, ORIG_SIZE, ORIG_SIZE) dataset_train.prepare() # prepare the validation dataset dataset_val = DetectorDataset(image_fps_val, image_annotations, ORIG_SIZE, ORIG_SIZE) dataset_val.prepare()
Изменения и варианты, которые стоит попробовать: Как упоминалось ранее, чтобы использовать все изображения, выполните: image_fps_list = list (image_fps) Или измените количество изображений со 100 на произвольное.
8 Составьте графики, чтобы лучше понимать данные:
Распечатайте аннотацию из случайной выборки. Аннотации регистрируются как координаты (x, y) верхнего левого угла ограничивающей рамки, а также ширина, высота и цель.
# Show annotation(s) for a DICOM image test_fp = random.choice(image_fps_train) image_annotations[test_fp]
Отображение случайного изображения с ограничивающими рамками:
# Load and display random samples and their bounding boxes # Suggestion: Run this a few times to see different examples. image_id = random.choice(dataset_train.image_ids)#choose a random image image_fp = dataset_train.image_reference(image_id)#image file path image = dataset_train.load_image(image_id)#load the chosen image mask, class_ids = dataset_train.load_mask(image_id)#load mask of the chosen img print(image.shape) plt.figure(figsize=(10, 10)) plt.subplot(1, 2, 1) plt.imshow(image[:, :, 0], cmap='gray') plt.axis('off') plt.subplot(1, 2, 2) masked = np.zeros(image.shape[:2]) for i in range(mask.shape[2]): masked += image[:, :, 0] * mask[:, :, i] plt.imshow(masked, cmap='gray') plt.axis('off') print(image_fp) print(class_ids)
Изменения и варианты, которые стоит попробовать. Графики полезны для понимания данных, поэтому многие другие типы графиков могут помочь улучшить модель, например, гистограмма высот ограничивающих прямоугольников и другой их ширины можно установить максимальная ширина и высота для прогнозируемого ограничивающего прямоугольника, поэтому любой прогноз, который создает ограничивающий прямоугольник за пределами указанных пределов, может быть отброшен.
Такая же проверка может быть сделана для местоположения на изображении прогнозируемых затемненных участков, которые все должны находиться в определенном диапазоне около центра изображения (места, где расположены легкие).
9 Настройте некоторые переменные на пользовательские значения:
#finetuning some image augmentation variables to custom values augmentation = iaa.SomeOf((0, 1), [ iaa.Fliplr(0.5), iaa.Affine( scale={"x": (0.8, 1.2), "y": (0.8, 1.2)}, translate_percent={"x": (-0.2, 0.2), "y": (-0.2, 0.2)}, rotate=(-25, 25), shear=(-8, 8) ), iaa.Multiply((0.9, 1.1)) ])
10 Обучите модель:
Примечание: следующая модель предназначена только для демонстрационных целей. Для обучения задана одна эпоха, а для конфигурации детектора установлены номинальные значения для сокращения времени выполнения.
- dataset_train и dataset_val являются производными от DetectorDataset
- DetectorDataset загружает изображения из имен файлов изображений и масок из данных аннотации
- модель - Mask-RCNN
model = modellib.MaskRCNN(mode='training', config=config, model_dir=MODEL_DIR) NUM_EPOCHS = 1 # Train Mask-RCNN Model import warnings warnings.filterwarnings("ignore") model.train(dataset_train, dataset_val, learning_rate=config.LEARNING_RATE, epochs=NUM_EPOCHS, layers='all', augmentation=augmentation)
Изменения и варианты, которые стоит попробовать: NUM_EPOCHS: например, для целей установлено значение 1, но по мере того, как модель работает с большим количеством эпох, она будет изучать больше, что потребует больше времени, поэтому это следует увеличить до возможно, в идеале около 80 эпох, обратная сторона - это потребует большой мощности компьютера для работы за достаточно короткое время, так как в Google Colaboratory требуется около 12 минут на эпоху.
11 Выберите обученную модель и сделайте прогнозы:
# select trained model dir_names = next(os.walk(model.model_dir))[1] key = config.NAME.lower() dir_names = filter(lambda f: f.startswith(key), dir_names) dir_names = sorted(dir_names) if not dir_names: import errno raise FileNotFoundError( errno.ENOENT, "Could not find model directory under {}".format(self.model_dir)) fps = [] # Pick last directory for d in dir_names: dir_name = os.path.join(model.model_dir, d) # Find the last checkpoint checkpoints = next(os.walk(dir_name))[2] checkpoints = filter(lambda f: f.startswith("mask_rcnn"), checkpoints) checkpoints = sorted(checkpoints) if not checkpoints: print('No weight files in {}'.format(dir_name)) else: checkpoint = os.path.join(dir_name, checkpoints[-1]) fps.append(checkpoint) model_path = sorted(fps)[-1] print('Found model {}'.format(model_path)) class InferenceConfig(DetectorConfig): GPU_COUNT = 1 IMAGES_PER_GPU = 1 inference_config = InferenceConfig() # Recreate the model in inference mode model = modellib.MaskRCNN(mode='inference', config=inference_config, model_dir=MODEL_DIR) # Load trained weights (fill in path to trained weights here) assert model_path != "", "Provide path to trained weights" print("Loading weights from ", model_path) model.load_weights(model_path, by_name=True)
Распечатайте сравнения между предсказанными ограничивающими прямоугольниками и наземными прямоугольниками с использованием набора данных проверки:
# set color for class def get_colors_for_class_ids(class_ids): colors = [] for class_id in class_ids: if class_id == 1: colors.append((.941, .204, .204)) return colors # Show few example of ground truth vs. predictions on the validation dataset dataset = dataset_val fig = plt.figure(figsize=(10, 30)) for i in range(4): image_id = random.choice(dataset.image_ids) original_image, image_meta, gt_class_id, gt_bbox, gt_mask =\ modellib.load_image_gt(dataset_val, inference_config, image_id, use_mini_mask=False) plt.subplot(6, 2, 2*i + 1) visualize.display_instances(original_image, gt_bbox, gt_mask, gt_class_id, dataset.class_names, colors=get_colors_for_class_ids(gt_class_id), ax=fig.axes[-1]) plt.subplot(6, 2, 2*i + 2) results = model.detect([original_image]) #, verbose=1) r = results[0] visualize.display_instances(original_image, r['rois'], r['masks'], r['class_ids'], dataset.class_names, r['scores'], colors=get_colors_for_class_ids(r['class_ids']), ax=fig.axes[-1])
12 Создайте отправляемый файл .csv с прогнозами, которые будет оценивать Kaggle:
# Get filenames of test dataset DICOM images test_image_fps = get_dicom_fps(test_dicom_dir) # Make predictions on test images, write out sample submission def predict(image_fps, filepath='sample_submission.csv', min_conf=0.98): # assume square image with open(filepath, 'w') as file: for image_id in tqdm(image_fps): ds = pydicom.read_file(image_id) image = ds.pixel_array # If grayscale. Convert to RGB for consistency. if len(image.shape) != 3 or image.shape[2] != 3: image = np.stack((image,) * 3, -1) patient_id = os.path.splitext(os.path.basename(image_id))[0] results = model.detect([image]) r = results[0] out_str = "" out_str += patient_id assert( len(r['rois']) == len(r['class_ids']) == len(r['scores']) ) if len(r['rois']) == 0: pass else: num_instances = len(r['rois']) out_str += "," for i in range(num_instances): if r['scores'][i] > min_conf: out_str += ' ' out_str += str(round(r['scores'][i], 2)) out_str += ' ' # x1, y1, width, height x1 = r['rois'][i][1] y1 = r['rois'][i][0] width = r['rois'][i][3] - x1 height = r['rois'][i][2] - y1 bboxes_str = "{} {} {} {}".format(x1, y1, \ width, height) out_str += bboxes_str file.write(out_str+"\n") # predict only the first 50 entries for testing sample_submission_fp = 'sample_submission.csv' predict(test_image_fps[:50], filepath=sample_submission_fp) output = pd.read_csv(sample_submission_fp, names=['id', 'pred_string']) output.head(50)
Весь код этого сообщения также доступен в Google Colaboratory по этой ссылке: https://colab.research.google.com/drive/1VJhrrChdGNFHiffWBKAk9bUYbFwhptoq
После выполнения приведенных выше инструкций процесс участия в RSNA Pneumonia Detection Challenge должен быть ясным, и будут получены некоторые знания о том, какие части нужно изменить, чтобы улучшить окончательный результат. Стартовое ядро выше было основано на этом стартовом ядре, опубликованном на Kaggle: https://github.com/mdai/ml-lessons/blob/master/lesson3-rsna-pneumonia-detection-kaggle.ipynb