Participe da Maratona Behind the Code! A competição de programação que mais te desafia! Inscreva-se aqui

Usando o deep learning para combater o vírus da COVID-19

Renúncia de responsabilidade: não sou um profissional das áreas médica, de radiologia nem de epidemiologia. Este artigo foi um experimento da perspectiva de um engenheiro e cientista de dados e deve ser considerado como tal.

Para aplicar o deep learning à COVID-19, é necessário contar com um bom conjunto de dados, que tenha muitas amostras, casos de borda, metadados e diferentes imagens. O modelo precisa generalizar com base em dados, para que possa fazer previsões precisas em relação a dados novos e desconhecidos. Infelizmente, não há muitos dados disponíveis. Há postagens em sites, como LinkedIn e Medium, que declaram >90%, ou em alguns casos 100%, de precisão na detecção de casos de COVID-19. No entanto, essas postagens geralmente contêm erros, que nem sempre são considerados. Este artigo tenta minimizar alguns dos erros comuns explorando soluções para os seguintes problemas:

  • Testar o desempenho do modelo com os mesmos dados usados para treiná-lo. A primeira regra do aprendizado de máquina é nunca testar o desempenho do modelo com os mesmos dados usados para treiná-lo. Não usar um conjunto de dados de teste, mas sim testar e medir a precisão do modelo no conjunto de dados de teste não fornece uma representação precisa de como o modelo generaliza em relação a dados novos e desconhecidos. Talvez você não acredite que isso é um problema, mas é uma falha típica para iniciantes no aprendizado de máquina e na ciência de dados.

  • Não usar técnicas de visão computacional para melhorar a generalização. O aumento é uma necessidade absoluta, principalmente quando há poucas amostras para o aprendizado do modelo.

  • Não analisar aquilo que o modelo aprende, ou seja, o modelo realmente aprende o padrão da aparência da COVID-19 na imagem de radiografia ou é provável que ele esteja aprendendo com outro padrão de ruído no conjunto de dados? Uma história divertida do início do aprendizado de máquina é chamada de Detectando tanques. Nessa história, fotos de tanques camuflados foram tiradas em dias nublados, enquanto fotos simples de florestas foram tiradas em dias de sol.

  • Não usar as métricas corretas. Se você usar a métrica de precisão em um conjunto de dados altamente desbalanceado, o modelo poderá aparentar um bom desempenho no caso geral, mesmo apresentando um desempenho ruim nos casos de COVID-19. 95% de precisão pode não ser tão impressionante se a precisão nos casos de COVID-19 é de apenas 30%.

Todo o trabalho deste artigo está disponível no GitHub.

Adquirindo dados

A lista a seguir mostra diferentes fontes de dados que acumulei durante o período do disseminação do coronavírus. Atualmente, isso é o melhor que pude obter na Internet porque os dados coletados em muitos países e hospitais são informações confidenciais. Mesmo se não fossem informações confidenciais, ainda seria necessário o consentimento de cada paciente.

Problema com fontes de dados mistos

Nem todos os dados disponíveis na Internet foram submetidos ao mesmo pré-processamento, e todas as imagens são claramente diferentes em relação à quantidade de barras pretas, como explicado abaixo. A maioria dos dados de COVID-19 positiva apresenta a imagem de radiografia inteira ocupando a maior parte da tela, com nenhuma barra nas laterais ou poucas delas (exceto algumas). No entanto, o conjunto de dados com os casos negativos de COVID-19 tem a maior parte das barras pretas na lateral de cada imagem.

Comparação de imagens

Isso se torna um problema porque o modelo pode aprender mais tarde que basta consultar as barras pretas na lateral para saber se uma nova amostra é um caso positivo ou negativo de COVID-19.

Após a inspeção manual do conjunto de dados, fica aparente que quase todos os casos negativos apresentam essas barras pretas, enquanto aproximadamente de 10% a 20% dos casos positivos apresentam as barras pretas. Não seria uma grande coincidência se, mais tarde, a precisão da previsão de positivo ou negativo ficasse em torno da marca dos 80% a 90%?

Marcadores de imagens cortados

Na imagem anterior, o lado esquerdo mostra uma imagem com um caso negativo de COVID-19 (barras pretas laterais e superiores). A imagem à direita mostra a mesma imagem, mas ela está cortada no sentido central e dimensionada para corresponder aos tamanhos.

Pré-processamento – melhorando os dados

O problema dos dados anterior é claramente algo que precisa ser resolvido. Podemos de alguma forma detectar quando há barras pretas superiores, inferiores ou laterais? E, podemos corrigir isso automaticamente de alguma forma?

As etapas a seguir são o segredo para resolver isso:

  1. Uma métrica simples: há um número x ou maior de pixels pretos em uma determinada linha ou coluna?
  2. Sim? Remova essa linha ou coluna. Repita isso para todas as linhas e conlunas.
  3. Concluída a verificação de pixels pretos? Então, corte a imagem.
  4. Passe para a próxima imagem e repita o processo.

Ok, não tão rapidamente. Aqui está um novo problema: é necessário manter a mesma proporção de aspecto. E não é possível aumentar ou diminuir a escala quando a proporção de aspecto não é a mesma. O motivo é as arquiteturas existentes que precisam de um tamanho de entrada igual a 224 x 224, que é uma proporção de aspecto de 1 x 1.

Em outras palavras, é possível verificar se estamos removendo o mesmo número de colunas que o de linhas? É possível compensar isso removendo linhas específicas caso haja muitas colunas com pixels pretos, mas nenhuma linha de pixels pretos ou poucas delas?

Removendo as barras pretas e corrigindo a proporção de aspecto

A imagem a seguir demonstra a entrada e a saída da remoção de barras pretas, com um exemplo especificamente bem-sucedido. Esse exemplo é muito eficiente, mas ainda não estamos seguindo a proporção de aspecto de 1 x 1.

Barras pretas removidas de imagens

A solução mais fácil e eficiente que encontrei para corrigir a proporção de aspecto foi calcular a diferença entre o número de linhas removidas e o número de colunas removidas. Divida a diferença por dois e remova as linhas superiores e inferiores ou remova as colunas esquerdas e direitas.

Imagens com a proporção de aspecto ajustada

A última imagem foi salva como uma imagem de 224 x 224 porque é possível diminuir a escala, agora que a proporção de aspecto é 1 x 1. Isso poupa espaço e tempo ao carregar a imagem novamente em outro script.

Executando o script

Demora cerca de 14 segundos por imagem para encontrar as linhas e colunas a serem removidas. Eu uso uma abordagem simples, que poderia ser executada com mais eficiência, embora funcione para o experimento. Eu tinha 8851 imagens no conjunto de dados Kaggle 1, o que gerou 39 horas de processamento. Todos os dados foram liberados e estão disponíveis no repositório do GitHub para este artigo.

Executei o código nos conjuntos de dados Kaggle 1 e do GitHub e o resultado é que todas essas imagens agora estão salvas no formato de imagem de 224 x 224. O código a seguir é indicado apenas para execução no conjunto de dados Kaggle 1, mas é possível substituir a primeira parte do código para carregar qualquer conjunto de dados de imagens.

import pandas as pd
import matplotlib.pyplot as plt
import cv2, time, os
import numpy as np

df = pd.read_csv('kaggle-pneumonia-jpg/stage_2_detailed_class_info.csv')
image_folder = 'kaggle-pneumonia-jpg/stage_2_train_images_jpg/'

df = df.loc[df['class'] == 'Normal']

O código a seguir mostra como carreguei o conjunto de dados da COVID-19.Observe que o restante do código é o mesmo. Ele está apenas carregando um conjunto de dados diferente.

columns = ['finding', 'modality', 'filename', 'view']

# only use COVID-19 samples done with X-ray and PA view
df = pd.read_csv('metadata.csv', usecols=columns)
df = df.loc[df['finding'] == 'COVID-19']
df = df.loc[df['modality'] == 'X-ray']
df = df.loc[df['view'] == 'PA']

Depois de carregar o quadro de dados, carreguei as imagens, os rótulos e os nomes de arquivos em arrays.

images = []
labels = []
filenames = []

for index, row in df.iterrows():
    image = cv2.imread(image_folder + row.patientId + '.jpg')
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    images.append(image)

    label = row['class']
    labels.append(label)

    filename = row.patientId
    filenames.append(filename)

images = np.array(images)

Em seguida, temos a função get_black_pixel_indices() que faz um loop entre as linhas e colunas de pixels. É possível imaginar uma imagem como uma tabela com linhas e colunas de pixels que tenha valores de pixel.

Eu conto o número de pixels pretos, definido pelo número de valores de pixel inferiores a 40. Um valor de pixel consiste em um valor para cada canal de cor, ou seja, vermelho, verde ou azul (RGB). Eu poderia ter um array de [39, 67, 45], e o valor de pixel médio seria a média entre esses números, ou seja 50,33.

Ao concluir a contagem do número de pixels pretos de uma determinada linha ou coluna, verifico se há mais pixels pretos do que pixels que não são pretos. Em caso afirmativo, escolho remover essa linha ou coluna.

def get_black_pixel_indices(img):
    rows_remove = []
    cols_remove = []

    for j, counter in enumerate(range(img.shape[0])):
        row = img[counter]
        # shift column dim to first dim, row dim to second dim
        # acts like a 90 degree counterclock-wise rotation of image
        col = img.transpose(1, 0, 2)[counter]

        row_black_pixel_count = 0
        col_black_pixel_count = 0
        for selector in range(img.shape[1]):
            row_pixel = row[selector]
            col_pixel = col[selector]

            if np.average(row_pixel) < 40:
                row_black_pixel_count += 1
            if np.average(col_pixel) < 40:
                col_black_pixel_count += 1

        if row_black_pixel_count > len(row)/2:
            rows_remove.append(j)
        if col_black_pixel_count > len(col)/2:
            cols_remove.append(j)

    return rows_remove, cols_remove

Antes de remover as linhas e colunas com uma contagem maior de pixels pretos do que de pixels que não são pretos, preciso verificar se há linhas e colunas que possam obstruir a imagem em caso de remoção. A maneira mais simples que encontrei é que nenhuma linha e coluna deve ser removida entre a coluna e a linha 200 e 800. Depois de filtrar esses índices de obstrução, removo as linhas e colunas.

def remove_obstructing_indices(rows_remove, cols_remove):
    for i, value in enumerate(rows_remove):
        if 200 <= value <= 800:
            del rows_remove[i]
    for i, value in enumerate(cols_remove):
        if 200 <= value <= 800:
            del cols_remove[i]

    return rows_remove, cols_remove

def remove_black_pixels(img, rows_remove, cols_remove):
    img = np.delete(img, rows_remove, axis=0)
    img = np.delete(img, cols_remove, axis=1)

    return img

A última etapa do pré-processamento é ajustar a proporção de aspecto. Eu sei que na maioria dos casos, um tronco humano é mais alto do que largo, portanto, escolho cortar aleatoriamente a imagem de baixo e de cima. Também há casos em que o tronco é mais largo, mas isso foi considerado raro durante a inspeção do conjunto de dados.

Começo calculando a diferença de coluna para linha e de linha para coluna. Com base nisso, descubro quantas linhas ou colunas preciso remover para corrigir a proporção de aspecto. Descubro que é possível dividir a diferença por dois e, em seguida, remover metade da diferença da parte superior e inferior ou da parte esquerda e direita. Depois disso, pode haver uma linha ou coluna restante quando a diferença é um número impar. Eu compenso isso removendo uma linha da parte inferior ou uma coluna da direita.

def adjust_aspect_ratio(img, rows_remove, cols_remove):
    col_row_diff = len(cols_remove) - len(rows_remove)
    row_col_diff = len(rows_remove) - len(cols_remove)

    if col_row_diff > 0:
        slice_size = int(col_row_diff/2)

        img = img[:-slice_size]
        img = img[slice_size:]

        if img.shape[0] != img.shape[1]:
            img = img[:-1]

    elif row_col_diff > 0:
        slice_size = int(row_col_diff/2)

        img = img[:,:-slice_size,:]
        img = img[:,slice_size:,:]

        if img.shape[0] != img.shape[1]:
            img = img[:,:-1,:]

    if img.shape[0] == img.shape[1]:
        return img, True
    else:
        return img, False

Finalmente, executo o loop que chama as quatro funções já descritas. Isso resulta em imagens com as barras pretas removidas das partes esquerda, direita, superior e inferior. Reduzi a escala da imagem para 224 x 224 e, em seguida, salvei a imagem juntamente com o rótulo e o nome do arquivo em um arquivo .csv, que acumula todos os caminhos e nomes de arquivo de todas as imagens pré-processadas.

save_folder = 'normal_dataset/'

start = time.time()
for i, img in enumerate(images):
    rows_remove, cols_remove = get_black_pixel_indices(img)
    rows_remove, cols_remove = remove_obstructing_indices(rows_remove, cols_remove)

    new_img = remove_black_pixels(img, rows_remove, cols_remove)
    adj_img, shape_match = adjust_aspect_ratio(new_img, rows_remove, cols_remove)

    if shape_match:
        resized_image = cv2.resize(adj_img, (224, 224))

        label = labels[i]
        name = filenames[i] + '.jpg'

        cv2.imwrite(save_folder + name, resized_image)
        new_df = pd.DataFrame({'filename': [name], 'finding': [label]})

        if i == 0:
            new_df.to_csv('normal_xray_dataset.csv')
        else:
            new_df.to_csv('normal_xray_dataset.csv', mode='a', header=False)

    print('image number {0}, time spent {1:2f}s'.format(i+1, time.time() - start))

Para obter uma visão geral final das outras 40 imagens de casos negativos e positivos, vejo claramente que a abordagem de pré-processamento ajudou.

Comparação de imagem 2

Aumento das amostras de COVID-19

Para aproveitar ao máximo o que tenho (99 imagens), preciso usar o aumento dos dados de treinamento. O aumento é uma manipulação da imagem, que parecerá novo para a CNN. Alguns exemplos comuns de aumento são rotações, cortes transversais e inversões de imagens.

Tenha em mente que o aumento é usado para melhorar a generalização do modelo e evitar ajuste excessivo. Basicamente, sempre que uso uma forma de aumento, incluo outra imagem similar, mas um pouco diferente, ao treinamento do conjunto de dados. Observe que isso também é uma demonstração de como seria o aumento das imagens e não o aumento que usaremos mais tarde.

Contraste ajustado

Eu uso a função image.adjust_contrast() do TensorFlow para ajustar o contraste de todas as imagens.

X_train_contrast = []

for x in X_train:
    contrast = tf.image.adjust_contrast( x, 2 )
    X_train_contrast.append(contrast.numpy())

plot_images(X_train_contrast, 'Adjusted Contrast')

Isso resulta nas seguintes imagens atualizadas.

Contraste ajustado na imagem

Saturação ajustada

Eu uso a função image.adjust_saturation() do TensorFlow para ajustar a saturação de todas as imagens.

X_train_saturation = []

for x in X_train:
    saturation = tf.image.adjust_saturation( x, 3 )
    X_train_saturation.append(saturation.numpy())

plot_images(X_train_saturation, 'Adjusted Saturation')

Isso resulta nas seguintes imagens atualizadas.

Saturação ajustada na imagem

Inversão esquerda direita

Eu uso a função image.flip_left_right() do TensorFlow para inverter todas as imagens da esquerda para a direita.

X_train_flipped = []

for x in X_train:
    flipped = tf.image.flip_left_right(x)
    X_train_flipped.append(flipped.numpy())

plot_images(X_train_flipped, 'Flipped Left Right')

Isso resulta nas seguintes imagens atualizadas.

Imagem invertida da esquerda para a direita

Invertendo para baixo

Eu uso a função image.flip_up_down() do TensorFlow para inverter todas as imagens de cima para baixo.

X_train_flipped_up_down = []

for x in X_train:
    flipped = tf.image.flip_up_down(x)
    X_train_flipped_up_down.append(flipped.numpy())

plot_images(X_train_flipped_up_down, 'Flipped Up Down')

Isso resulta nas seguintes imagens atualizadas.

alt

Invertendo de cima para baixo e da esquerda para direita

Eu uso a função image.flip_left_right() do TensorFlow para inverter todas as imagens que já estão invertidas de cima para baixo, o que resulta na inversão das imagens de cima para baixo e, em seguida, da esquerda para a direita.

X_train_flipped_up_down_left_right = []

for x in X_train_flipped_up_down:
    flipped = tf.image.flip_left_right(x)
    X_train_flipped_up_down_left_right.append(flipped.numpy())

plot_images(X_train_flipped_up_down_left_right, 'Flipped Up Down Left Right')

Isso resulta nas seguintes imagens atualizadas.

Invertida

Rotações

O TensorFlow possui uma extensão chamada TensorFlow Addons que pode ser instalada pelo comando pip pip install tensorflow-addons. O pacote tem uma função image.rotate() que permite girar qualquer imagem em um número qualquer de graus. A função usa os radianos como entrada, portanto, eu uso a função radians() da importação matemática do Python.

import tensorflow_addons as tfa
from math import radians

X_train_rot_45_deg = []
X_train_rot_135_deg = []
X_train_rot_225_deg = []
X_train_rot_315_deg = []

for x in X_train:
    deg_45 = tfa.image.rotate(image, radians(45))
    deg_135 = tfa.image.rotate(image, radians(135))
    deg_225 = tfa.image.rotate(image, radians(225))
    deg_315 = tfa.image.rotate(image, radians(315))

    X_train_rot_45_deg.append(deg_45)
    X_train_rot_135_deg.append(deg_135)
    X_train_rot_225_deg.append(deg_225)
    X_train_rot_315_deg.append(deg_315)

plot_images(X_train_rot_45_deg, 'Rotated 45 Degrees')
plot_images(X_train_rot_135_deg, 'Rotated 135 Degrees')
plot_images(X_train_rot_225_deg, 'Rotated 225 Degrees')
plot_images(X_train_rot_315_deg, 'Rotated 315 Degrees')

Isso resulta nas seguintes imagens atualizadas.

Comparação, parte 3

Visão geral da arquitetura

Os modelos VGG e ResNet são especificamente populares no momento porque apresentam o melhor desempenho, de acordo com alguns documentos liberados sobre a COVID-19.

  1. Hemdan, E., Shouman M., & Karar M. (2020). COVIDX-Net: uma estrutura de classificadores de deep learning para diagnosticar a COVID-19 em imagens de radiografia. arXiv:2003.11055.
  2. Wang, L., & Wong, A. (2020). COVID-Net: um design de rede neural convolucional altamente customizada para detecção de casos de COVID-19 em imagens de radiografia de tórax. arXiv:2003.09871.

Fiz uma visão geral das duas arquiteturas, a VGG19 e a ResNet50, usando um guia de estilo adaptado deste artigo. O R amarelo representa ReLU, enquanto o S amarelo representa Softmax. Os colchetes representam blocos de camadas repetidos. Observe que eu eliminei a etapa de normalização do lote.

Imagem do ResNet50

Executando um modelo de deep learning

Usarei a transferência de aprendizado com o modelo VGG19, em que inicializarei os pesos para serem os do ImageNet, embora esse seja um conjunto de dados muito diferente do meu. O ImageNet consiste em imagens de objetos aleatórios, enquanto meu conjunto de dados possui apenas imagens de radiografia. Estou transferindo as camadas ocultas que encontram padrões coloridos dos objetos já vistos ou seus formatos geométricos diferentes em outro conjunto de dados.

Executei um experimento curto no qual tentei não usar a transferência de aprendizado, e o resultado foi uma precisão e um F1 de cerca de 0,5 – 0,6. Escolhi continuar com a abordagem que apresentou o melhor desempenho, como você já verá. Se você deseja experimentar, defina um modelo VGG19 com weights=None.

Importações, carregamento de dados e divisão de dados

Para todas as importações para carregar dados e modelagem, eu uso diversos pacotes: pandas, PyPlot, NumPy, cv2, TensorFlow e scikit-learn.

import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import cv2, time
import tensorflow as tf

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelBinarizer
from tensorflow.keras.utils import to_categorical

from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.applications import VGG19
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Dense

Começo carregando os dois conjuntos de dados (observe que o dataset/ nas variáveis de caminho é o nome da pasta do repositório do GitHub). Coloco uma restrição no número de casos normais que uso para a modelagem porque o uso de todas as imagens, ou até mesmo de 20% das imagens normais, leva a um super ajuste dessas amostras.

covid_path = 'dataset/covid_dataset.csv'
covid_image_path = 'dataset/covid_adjusted/'

normal_path = 'dataset/normal_xray_dataset.csv'
normal_image_path = 'dataset/normal_dataset/'

covid_df = pd.read_csv(covid_path, usecols=['filename', 'finding'])
normal_df = pd.read_csv(normal_path, usecols=['filename', 'finding'])

normal_df = normal_df.head(99)

covid_df.head()

O código a seguir carrega as imagens em arrays Python. Finalmente, carrego as imagens em arrays NumPy e normalizo-as para que todos os valores de pixel estejam entre zero e um.

covid_images = []
covid_labels = []

for index, row in covid_df.iterrows():
    filename = row['filename']
    label = row['finding']
    path = covid_image_path + filename

    image = cv2.imread(path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    covid_images.append(image)
    covid_labels.append(label)

normal_images = []
normal_labels = []

for index, row in normal_df.iterrows():
    filename = row['filename']
    label = row['finding']
    path = normal_image_path + filename

    image = cv2.imread(path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    normal_images.append(image)
    normal_labels.append(label)

# normalize to interval of [0,1]
covid_images = np.array(covid_images) / 255

# normalize to interval of [0,1]
normal_images = np.array(normal_images) / 255

Desejo garantir que tanto os casos normais quanto os infectados sejam divididos corretamente e, como eu os carreguei separadamente, também os divido separadamente. Se, mais tarde, eu decidir carregar mais casos normais do que casos infectados, ou seja, carregar um conjunto de dados desbalanceado, ainda obterei uma boa divisão em termos do número de casos infectados nos conjuntos de dados de treinamento e teste.

Após a divisão do conjunto de dados, faço a concatenação dele em entrada X e saída y, juntamente com um rótulo para os dados de treinamento e teste. Também converto os rótulos COVID-19 e Normal em números usando as funções LabelBinarizer() e to_categorical().

# split into training and testing
covid_x_train, covid_x_test, covid_y_train, covid_y_test = train_test_split(
    covid_images, covid_labels, test_size=0.2)

normal_x_train, normal_x_test, normal_y_train, normal_y_test = train_test_split(
    normal_images, normal_labels, test_size=0.2)

X_train = np.concatenate((normal_x_train, covid_x_train), axis=0)
X_test = np.concatenate((normal_x_test, covid_x_test), axis=0)
y_train = np.concatenate((normal_y_train, covid_y_train), axis=0)
y_test = np.concatenate((normal_y_test, covid_y_test), axis=0)

# make labels into categories - either 0 or 1
y_train = LabelBinarizer().fit_transform(y_train)
y_train = to_categorical(y_train)

y_test = LabelBinarizer().fit_transform(y_test)
y_test = to_categorical(y_test)

Definindo o modelo

Usei um modelo VGG19 pré-treinado. Configurei include_top=False, que significa que não há nenhuma camada totalmente conectada no final da arquitetura VGG19. Isso permite usar as saídas das convoluções, comprimi-las e analisá-las em minha própria camada de saída.

Também incluí uma regularização usando uma camada de Dropout porque eu estava fazendo um super ajuste nos dados de treinamento. Ainda estou fazendo um super ajuste nos dados de treinamento, mas nem tanto. Passei de uma precisão de 0,97 para 0,88.

vggModel = VGG19(weights="imagenet", include_top=False,
    input_tensor=Input(shape=(224, 224, 3)))

outputs = vggModel.output
outputs = Flatten(name="flatten")(outputs)
outputs = Dropout(0.5)(outputs)
outputs = Dense(2, activation="softmax")(outputs)

model = Model(inputs=vggModel.input, outputs=outputs)

for layer in vggModel.layers:
    layer.trainable = False

model.compile(
        loss='categorical_crossentropy',
        optimizer='adam',
        metrics=['accuracy']
)

Definindo o aumento dos dados de treinamento

A maneira que eu escolhi aumentar os dados foi generalizando os lotes de aumentos em cada amostra. Para cada amostra, obtenho um novo lote de amostras do ImageDataGenerator definido no código a seguir. Todas essas novas amostras aumentadas são usadas para o treinamento, em vez da amostra original.

train_aug = ImageDataGenerator(
    rotation_range=20,
    width_shift_range=0.2,
    height_shift_range=0.2,
    horizontal_flip=True
)

Escolhi configurar rotation_range=20, que é o intervalo de grau das rotações aleatórias da imagem original. Isso significa que o Keras escolherá aleatoriamente um grau de 1 a 20 e girará a imagem nesse grau no sentido horário ou no sentido anti-horário.

Enquanto isso, também tenho width_shift_range=0.2 e height_shift_range=0.2, que inverte aleatoriamente a imagem pela largura (ou seja, para a esquerda e para a direita) e pela altura (ou seja, para cima e para baixo) conforme especificado.

Finalmente, tenho horizontal_flip=True, que inverte aleatoriamente a imagem horizontalmente (ou seja, invertendo a imagem da esquerda para a direita) como você viu anteriormente.

Uma boa visualização da função ImageDataGenerator pode ser encontrada neste blog.

Ajustando, prevendo e pontuando

Eu defini o modelo, o aumento e os dados. Agora, preciso iniciar o treinamento do modelo VGG19. Posso treinar o modelo com os dados aumentados alimentando os lotes do aumento pela função train_aug.flow().

history = model.fit(train_aug.flow(X_train, y_train, batch_size=32),
                    steps_per_epoch=len(X_train) / 32,
                    epochs=500)

Como desejo ver o grau de desempenho do modelo em diferentes classes existentes, uso as métricas corretas para medir esse desempenho (que se chama I F1 e não precisão). David Ernst explica por que eu poderia usar tanto AUC quanto F1.

“O ROC AUC [ou F1] reconhece o desbalanceamento de classe no sentido de que quando há uma classe minoritária, geralmente ela é definida como uma classe positiva e causará um forte impacto no valor de AUC [ou F1]. Esse é basicamente o comportamento desejado. A precisão não faz esse tipo de reconhecimento, por exemplo. Ela pode ser muito alta, mesmo quando a classe minoritária não é bem prevista.”

Após o treinamento do modelo, ele pode ser usado para fazer previsões sobre os dados novos e desconhecidos. Uso argmax para encontrar a probabilidade prevista mais alta e, em seguida, posso gerar um relatório de classificação usando scikit-learn.

from sklearn.metrics import classification_report

y_pred = model.predict(X_test, batch_size=32)
print(classification_report(np.argmax(y_test, axis=1), np.argmax(y_pred, axis=1)))

O LabelBinarizer usado anteriormente fez com que ‘0’ fosse a classe da COVID-19 e ‘1’ a classe normal. Posso ver claramente que a precisão é de 82%, enquanto a pontuação de F1 da COVID-19 é de 0,84. Isso é bom considerando-se os dados limitados. Talvez isso possa sugerir que há mais ruído nas imagens não filtradas ou não corrigidas.

              precision    recall  f1-score   support

           0       0.75      0.95      0.84        19
           1       0.93      0.70      0.80        20

    accuracy                           0.82        39
   macro avg       0.84      0.82      0.82        39
weighted avg       0.84      0.82      0.82        39

Agora, posso criar um gráfico da precisão do treinamento e das perdas do treinamento usando o PyPlot.

plt.figure(figsize=(10,10))
plt.style.use('dark_background')

plt.plot(history.history['accuracy'])
plt.plot(history.history['loss'])

plt.title('Model Accuracy & Loss')
plt.ylabel('Value')
plt.xlabel('Epoch')

plt.legend(['Accuracy', 'Loss'])

plt.show()

Isso gera um gráfico simples que mostra como a precisão do modelo aumentou ao longo do tempo no conjunto de dados de treinamento, bem como a perda no conjunto de dados de treinamento também aumentou.

Perda de ACC do modelo

Conclusão

Embora minha abordagem tenha tentado minimizar os erros cometidos em outras abordagens, acredito que ainda posso melhorá-la bastante. Como você viu na pontuação de F1, provavelmente ainda há alguns problemas com os dados que não posso ver imediatamente.

A lista a seguir oferece algumas das abordagens que poderiam ser usadas para obter um modelo melhor e resultados mais confiáveis.

  1. Usando imagens de radiografia de SARS, MERS, ARDS e outros tipos de infecções em vez de apenas a categoria normal. Isso permitiria distinguir entre diferentes infecções.
  2. Dados de mais qualidade dos casos de COVID-19 da visualização posteroanterior (PA).
  3. Dados em diferentes estágios da COVID-19: infectado, sem sintomas; infectado, sintomas leves; infectado, sintomas fortes.
  4. Tipos mais avançados de dados de imagiologia, ou seja, imagens de CT que incluem mais informações.
  5. A validação cruzada ou a validação cruzada aninhada para melhorar a estimativa do erro de generalização verdadeira do modelo.

Os resultados atingidos neste artigo não devem ser interpretados como apropriados para serem usados em cenários reais. Você somente deve considerar uma abordagem de deep learning da COVID-19 após testes rigorosos realizados por engenheiros, médicos e outros profissionais, seguidos por um estudo clínico.

Aviso

O conteúdo aqui presente foi traduzido da página IBM Developer US. Caso haja qualquer divergência de texto e/ou versões, consulte o conteúdo original.