Устранение дисбаланса классов и эффективное использование предварительно обученной модели

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

  • Двоичная классификация с использованием глубокой нейронной сети (DNN).
  • Использование набора данных TensorFlow для создания более быстрого конвейера анализа данных.
  • Улучшенные методы (например, стандартизация) для предварительной обработки данных.
  • Аугментация, изменение масштаба и т. д. в виде лямбда-слоев в модели TensorFlow.
  • Дисбаланс классов и построение пользовательских взвешенных кросс-энтропийных потерь.

Без промедления приступим! [Все используемые здесь коды доступны в Kaggle Notebook].

Знакомство со структурой данных:

Поскольку мы будем напрямую обращаться к данным из входного каталога Kaggle, давайте просто посмотрим на распределение меток «Нормальный» и «Пневмония» в папках Train, Validation и Test. Ниже приведен блок кода, который я использовал для проверки количества файлов в каждой папке —

Во-первых, мы видим, что тренировочные изображения несбалансированы по классам и содержат гораздо больше изображений с пометкой «Пневмония», чем «Нормальное». Кроме того, папка проверки содержит очень мало примеров (точнее, 8 изображений «Нормальное» и 8 изображений «Пневмония»). .

tot_normal_train = len(train_im_n) + len(valid_im_n) 
tot_pneumonia_train = len(train_im_p) + len(valid_im_p)
print ('total normal xray images: ', tot_normal_train)
print ('total pneumonia xray images: ', tot_pneumonia_train)
>>> total normal xray images:  1349
total pneumonia xray images:  3883

Мы также можем визуализировать некоторые из примеров изображений «Нормальный» и «Пневмония», как показано ниже:

Точно так же мы можем просматривать изображения «пневмонии» —

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

предварительная обработка; Стандартизация:

Мы настроим данные нашего изображения так, чтобы новое среднее значение данных было равно нулю, а стандартное отклонение данных было равно 1. Позже мы будем использовать набор данных TensorFlow и определим функцию, в которой каждое значение пикселя в изображение будет заменено новым значением, которое рассчитывается путем вычитания среднего значения и деления на стандартное отклонение ( x-μ)/σ. Давайте посмотрим, как стандартизация помогает нам распределять значения пикселей в некоторых случайных примерах —

Чтобы включить этот тип стандартизации, мы создаем функцию, которая будет использоваться в качестве лямбда-слоя при построении модели. Таким образом, мощность графического процессора также будет использоваться для этого процесса —

#### define a function that will be added as lambda layer later
def standardize_layer(tensor):
    tensor_mean = tf.math.reduce_mean(tensor)
    tensor_std = tf.math.reduce_std(tensor)
    new_tensor = (tensor-tensor_mean)/tensor_std
    return new_tensor

Создайте входной конвейер с набором данных TensorFlow:

В предыдущем посте я подробно описал, как построение конвейера ввода, включая аугментацию с использованием TensorFlow Dataset API, может ускорить обучение DNN. Я буду следовать аналогичным шагам. Поскольку данные изображения доступны в папках train, test, validation, мы можем начать с функции image_dataset_from_directory

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

num_elements = tf.data.experimental.cardinality(train_dir).numpy()
print (num_elements)
num_elements_val = tf.data.experimental.cardinality(val_dir).numpy()
print (num_elements_val)
>>> 82
1

Мы видим, что есть 82 обучающих пакета и 1 проверочный пакет. Чтобы увеличить пакет проверки, сначала давайте объединим наборы данных «обучение» и «проверка». Затем мы назначаем 20% от общего набора данных для проверки и используем dataset.take и dataset.skip для создания новых наборов данных, используя блок кода ниже —

new_train_ds = train_dir.concatenate(val_dir)

print (new_train_ds, train_dir)

train_size = int(0.8 * 83) # 83 is the elements in dataset (train + valid)
val_size = int(0.2 * 83)
    
train_ds = new_train_ds.take(train_size)
val_ds = new_train_ds.skip(train_size).take(val_size)


#### check the dataset size back again 
num_elements_train = tf.data.experimental.cardinality(train_ds).numpy()
print (num_elements_train)
num_elements_val_ds = tf.data.experimental.cardinality(val_ds).numpy()
print (num_elements_val_ds)
>>> <ConcatenateDataset shapes: ((None, 300, 300, 1), (None, 1)), types: (tf.float32, tf.float32)> <BatchDataset shapes: ((None, 300, 300, 1), (None, 1)), types: (tf.float32, tf.float32)>
66
16

Я уже описывал технику Prefetching и насколько она быстрее, чем ImageDataGenerator. Добавим это -

autotune = tf.data.AUTOTUNE ### most important function for speed up training


train_data_batches = train_ds.cache().prefetch(buffer_size=autotune)
valid_data_batches = val_ds.cache().prefetch(buffer_size=autotune)
test_data_batches = test_dir.cache().prefetch(buffer_size=autotune)

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

from tensorflow.keras import layers

rescale_layer = tf.keras.Sequential([layers.experimental.preprocessing.Rescaling(1./255)])

data_augmentation = tf.keras.Sequential([
  layers.experimental.preprocessing.RandomFlip(),
  layers.experimental.preprocessing.RandomRotation(10), 
  layers.experimental.preprocessing.RandomZoom(0.1) ])

Взвешенная бинарная кросс-энтропийная потеря:

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

Построение модели DNN, включая дополнения:

После определения соответствующей пользовательской функции потерь нам остается построить модель, включающую масштабирование и увеличение в виде лямбда-слоев. Для данной работы я использовал предварительно обученную модель InceptionResNetV2, и вы можете проверить модуль Keras здесь. В соревнованиях Kaggle нам не разрешено использовать Интернет, поэтому мне нужно было загрузить предварительно обученные веса, что объясняет аргумент weights внутри функции InceptionResNetV2. Масштабирование, стандартизация и увеличение добавляются в виде лямбда-слоев перед передачей пакетов изображений в модель. Давайте посмотрим на блок кода ниже —

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

y_pred = model.predict(test_data_batches)
true_categories = tf.concat([y for x, y in test_data_batches], axis=0)

Давайте установим порог 0,75 для присвоения метки «1» и всего, что ниже, для метки «0».

y_pred_th = (y_pred > 0.75).astype(np.float32)
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_pred_th, true_categories)
class_names = train_dir.class_names
plt.figure(figsize=(8,8))
plt.title('CM for threshold 0.75')
sns_hmp = sns.heatmap(cm, annot=True, xticklabels = [class_names[i] for i in range(len(class_names))], 
                      yticklabels = [class_names[i] for i in range(len(class_names))], fmt="d")
fig = sns_hmp.get_figure()

Приведенные выше блоки кода привели к приведенной ниже матрице путаницы:

Точно так же мы можем построить кривую ROC, и результаты будут показаны —

Заключительные примечания:

Все коды, используемые выше, доступны на моем GitHub, и вы также можете проверить Kaggle Notebook. Используемый набор данных доступен в Kaggle по лицензии Creative Commons, поэтому мы можем свободно использовать и адаптировать его. Наконец, в заключение мы прошли конвейер анализа данных, включая набор данных о дисбалансе классов, и научились эффективно использовать набор данных TensorFlow.

Будьте сильными и будьте здоровы!