Более подробное определение соревнования представлено на сайте 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