Мы хорошо знаем GAN за его успехи в создании реалистичных изображений. Мы не так хорошо знаем о формировании табличных данных. Однако их можно использовать при одновременной реализации табличных данных и изображений.

Зачем одновременно создавать табличные данные и изображение?

Я создал приложение coronarography.ai. На вход нейросети подаются структурированные данные (факторы риска развития заболеваний сердца) и изображение ЭКГ, а на выходе — патология магистральных артерий сердца. Мне стало интересно проверить точность предсказания обученной нейронной сети на синтетически сгенерированных данных. Почему бы и нет) Давайте увеличим выборку с помощью GAN и посмотрим на точность обученной нейросети на синтетических данных. Для этого необходимо получить эти синтетические данные.

Описание проблемы.

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

Нам необходимо одновременно формировать структурированные данные (факторы риска и мишени в виде поражения артерий сердца) и картинку — изображение ЭКГ. Я не встречал в литературе таких примеров, чтобы табличные данные и картинка генерировались одновременно. Хорошо, давайте сделаем это в первый раз.) Сгенерируем 1500000 синтетических наблюдаемых в виде табличных данных и изображений ЭКГ.

Блок-схема исследования.

Получение синтетических данных с помощью GAN.

На вход генератора подавались 100 случайных чисел с нормальным распределением. На выходе сгенерировано изображение (200, 200) и структурированные табличные данные размера (1, 35). (одна строка, 35 столбцов). Внутри генератора был уровень обобщения, чтобы поддерживать поток данных между строкой таблицы и изображением.

Входными данными дискриминатора были сгенерированное изображение размера (200, 200) вместе с реальными изображениями ЭКГ (200, 200) и сгенерированные табличные данные размера (1, 35) вместе с реальными табличными данными. На выходе дискриминатор выдавал бинарную классификацию, соответствующую реальным данным и синтетическим.

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

Реализация на TensorFlow.

Возьмем за основу структуру GAN, описанную в моей статье про генерацию ЭКГ.

Используемые библиотеки

import pandas as pd

import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
from PIL import Image
from tensorflow.keras import layers
import time
import tensorflow as tf
from IPython import display
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
import matplotlib
from matplotlib.pyplot import figure
from sklearn.preprocessing import MinMaxScaler
import joblib

import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model
print(tf.__version__)
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = (8,6)

Загрузка и подготовка набора данных

Здесь мы загружаем изображения ЭКГ из папки в массив. Преобразовать в одноканальное (черно-белое) изображение, нормализовать его. Проводим небольшую коррекцию и нормализацию табличных данных.

# table data
data = pd.read_csv('../AI_coronarography/DATA_WORK/DATA_WORK/DP_cor.csv', sep=';')
data.drop(['FIO', 'number_of_affected_coronary_artery'], axis = 1, inplace=True)
for i in [
    'trunk_st', 
    'LAD_st',
    'lcx_stenosis',
    'RCA_stenosis'
]:
    data[i] = data[i].apply(lambda x: 1 if x >= 50 else 0)

scaler = MinMaxScaler(feature_range=(0, 1))
scaler = scaler.fit(data)
data[data.columns] = scaler.transform(data[data.columns])

# image
data_image = []
for k in os.listdir('../AI_coronarography/DATA_WORK/DATA_WORK/ЭКГ'):
    if k.endswith('.jpg'):
        img = Image.open('../AI_coronarography/DATA_WORK/DATA_WORK/ЭКГ/'+k)
        img = img.convert('L')
        img = img.resize((200, 200))
        data_image += [(np.array(img) - 127.5) / 127.5]

Пример загруженного изображения ЭКГ

Давайте определим размер пакета и объединим изображения и структурированные данные для входа в GAN. Давайте смешаем объединенные изображения ЭКГ и табличные данные.

train_images = np.array(data_image).reshape(np.array(data_image).shape[0], 200, 200, 1).astype('float32')
train_data = np.array(data).reshape(np.array(data).shape[0], 35).astype('float32')
BUFFER_SIZE = 100
BATCH_SIZE = 10
train_dataset = tf.data.Dataset.from_tensor_slices((train_images, train_data)).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

Давайте создадим генератор

def make_generator_model():
    
    input_1 = Input(shape=(100, ), name = "InputRandomNoise")
    
    x = Dense(25*25*256, use_bias=False)(input_1)
    x = BatchNormalization()(x)
    conc = LeakyReLU()(x)
    
    x = Reshape((25, 25, 256))(conc)
    x = Conv2DTranspose(256, (5, 5), strides=(1, 1), padding='same', use_bias=False)(x)
    x = BatchNormalization()(x)
    x = LeakyReLU()(x)
    x = Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False)(x)
    x = BatchNormalization()(x)
    x = LeakyReLU()(x)
    x = Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False)(x)
    x = BatchNormalization()(x)
    x = LeakyReLU()(x)
    x = Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh', name = "OutputImage")(x)
    
    y = Dense(400)(conc)
    y = BatchNormalization()(y)
    y = LeakyReLU()(y)
    y = Dense(200)(y)
    y = BatchNormalization()(y)
    y = LeakyReLU()(y)
    y = Dense(128)(y)
    y = BatchNormalization()(y)
    y = LeakyReLU()(y)
    y = Dense(64)(y)
    y = BatchNormalization()(y)
    y = LeakyReLU()(y)
    y = Dense(35, activation='sigmoid', name = "OutputTableData")(y)

    
    model = Model(inputs=input_1, outputs=[x, y])
    
    return model

generator = make_generator_model()

noise = tf.random.normal([1, 100])
generated_image = generator(noise, training=False)

Структура генератора

Давайте создадим дискриминатор

def make_discriminator_model():
    input_1 = Input(shape=(200, 200, 1), name = "InputImage")
    input_2 = Input(shape=(35,), name = "InputTableData")
    
    x = Conv2D(64, (5, 5), strides=(2, 2), padding='same')(input_1)
    x = LeakyReLU()(x)
    x = Dropout(0.3)(x)
    x = Conv2D(128, (5, 5), strides=(2, 2), padding='same')(x)
    x = LeakyReLU()(x)
    x = Dropout(0.3)(x)
    x = Conv2D(256, (5, 5), strides=(2, 2), padding='same')(x)
    x = LeakyReLU()(x)
    x = Dropout(0.3)(x)
    x = Conv2D(256, (5, 5), strides=(1, 1), padding='same')(x)
    x = LeakyReLU()(x)
    x = Dropout(0.3)(x)
    x = Flatten()(x)
    
    y = Dense(400)(input_2)
    y = LeakyReLU()(y)
    y = Dropout(0.3)(y)
    y = Dense(200)(y)
    y = LeakyReLU()(y)
    y = Dropout(0.3)(y)
    y = Dense(128)(y)
    y = LeakyReLU()(y)
    y = Dropout(0.3)(y)
    y = Dense(64)(y)
    y = LeakyReLU()(y)
    y = Dropout(0.3)(y)
    z = concatenate([x, y])
    z = Dense(25)(z)
    z = Dropout(0.3)(z)
    z = Dense(1)(z)
    
    model = Model(inputs=[input_1, input_2], outputs=z)
    
    return model

Структура дискриминатора

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

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

def discriminator_loss(real_output, fake_output):
    real_loss = cross_entropy(tf.ones_like(real_output), real_output)
    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
    total_loss = real_loss + fake_loss
    return total_loss

def generator_loss(fake_output):
    return cross_entropy(tf.ones_like(fake_output), fake_output)

generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)

checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
                                 discriminator_optimizer=discriminator_optimizer,
                                 generator=generator,
                                 discriminator=discriminator)

Определение цикла обучения

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

EPOCHS = 5000
noise_dim = 100
num_examples_to_generate = 16

seed = tf.random.normal([num_examples_to_generate, noise_dim])

@tf.function
def train_step(images):
    noise = tf.random.normal([BATCH_SIZE, noise_dim])

    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
      generated_images = generator(noise, training=True)

      real_output = discriminator(images, training=True)
      fake_output = discriminator(generated_images, training=True)

      gen_loss = generator_loss(fake_output)
      disc_loss = discriminator_loss(real_output, fake_output)

    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)

    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))

def train(dataset, epochs):
  for epoch in range(epochs):
    start = time.time()

    for image_batch in dataset:
      train_step(image_batch)

    display.clear_output(wait=True)
    generate_and_save_images(generator,
                             epoch + 1,
                             seed)

    if (epoch + 1) % 500 == 0:
      checkpoint.save(file_prefix = checkpoint_prefix)

    print ('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))

  # Generate after the final epoch
  display.clear_output(wait=True)
  generate_and_save_images(generator,
                           epochs,
                           seed)

def generate_and_save_images(model, epoch, test_input):
  
  predictions = model(test_input, training=False)

  fig = plt.figure(figsize=(7, 7))

  for i in range(predictions[0].shape[0]):
      plt.subplot(4, 4, i+1)
      plt.imshow(predictions[0][i, :, :, 0] * 127.5 + 127.5, cmap='gray')
      plt.axis('off')

  plt.savefig('image_at_epoch.png'.format(epoch))
  plt.show()

Обучение моделей

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

train(train_dataset, EPOCHS)

Мы можем контролировать обучение визуально по изображениям ЭКГ.

В начале обучения сгенерированные изображения выглядят как случайный шум. По мере обучения нейронных сетей сгенерированные изображения ЭКГ будут выглядеть все более и более реальными.

Давайте создадим 1 500 000 синтетических наблюдаемых и сохраним их.

dataset_pred = []

for i in tqdm(range(0, 1500000)):
    noise = tf.random.normal([1, 100])
    generated_image = generator(noise, training=False)
    
    Image.fromarray(np.array(generated_image[0] * 127.5 + 127.5, dtype='uint8').reshape(200, 200)).save(f"./new_GANECG/{i}.jpg")
    
    dataset_pred.append(generated_image[1].numpy())

with open('dataset_pred.pickle', 'wb') as f:
    pickle.dump(dataset_pred, f)

Сравним изображения ЭКГ. Внешне изображения ЭКГ практически неотличимы от реальных.

Более сложная проблема — табличные данные, насколько они близки к реальным?

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

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

Тепловая карта была создана для сравнения основных описательных статистик (медиана, среднее 25 квантилей, 75 квантилей, минимальное и максимальное значения). Достоверные различия получены по половине признаков.

Создана тепловая карта разницы корреляционных матриц реальных и синтетических наборов данных. Основные компоненты корреляции сохраняются.

Выполнен расчет и визуализация главных компонент (PCA) реального и сгенерированного наборов данных.

Визуализировано t-распределенное стохастическое встраивание соседей (t-SNE).

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

Сравнение результатов точности предсказания coronarography.ai на входных синтетических данных.

Собственно поэтому и вся эта суета. Какая точность у Карла?????

Проведен прогноз поражения магистральных коронарных артерий и транзиторной ишемии миокарда на 1 500 000 синтетических наблюдений.

Показатель AUC составил 0,79. Точность достигла 88 %, «прецизионная» точность (точность) — 73 %, полнота (recall) — 63 %, оценка f1 — 67 %.

+-------------------------------------------------------------------------+-----+----------+-----------+--------+----------+
| Predicting damage to the main coronary arteries and myocardial ischemia | AUC | Accuracy | Precision | Recall | F1 score |
+-------------------------------------------------------------------------+-----+----------+-----------+--------+----------+
| Non-invasive predictive AI coronary angiography                         |  79 |       88 |        73 |     63 |       67 |
+-------------------------------------------------------------------------+-----+----------+-----------+--------+----------+

Задание выполнено!