Cómo construí la segmentación supervisada de lesiones cutáneas en el conjunto de datos HAM10000: hacia la IA

Estás leyendo la publicación: Cómo construí la segmentación supervisada de lesiones cutáneas en el conjunto de datos HAM10000: hacia la IA

Publicado originalmente en Hacia la IA.

El cáncer de piel es uno de los tipos de cáncer más comunes en el mundo. Su diagnóstico precoz es fundamental para eliminar los tumores malignos del cuerpo humano.

Hay muchas investigaciones en curso sobre la detección, localización y clasificación del cáncer de piel. La segmentación es un paso esencial en la localización del cáncer de piel.

La segmentación es una técnica utilizada en la visión por computadora para identificar objetos o límites para simplificar una imagen y analizarla de manera más eficiente.

Se realiza dividiendo una imagen en diferentes regiones en función de las características de los píxeles. La segmentación binaria se utiliza para dividir dos regiones principales de la imagen. La segmentación binaria, en particular, se utiliza en la localización del cáncer de piel.

# importando las bibliotecas requeridas

!pip instalar rayo
importar rayo como pl

importar numpy como np
importar pandas como pd
importar seaborn como sns

importar sistema operativo
antorcha de importación
importar matplotlib.pyplot como plt
importar antorcha visión
importar torchvision.transforms como transformaciones
!pip instalar modelos de segmentación-antorcha
importar segmentation_models_pytorch como smp
de imagen de importación PIL
desde pprint importar pprint
desde torch.utils.data importar DataLoader

Conjunto de datos

Para explorar la segmentación binaria, utilicé el popularmente disponible Conjunto de datos HAM10000 de 10015 imágenes de lesiones cutáneas con el tipo de cáncer y sus correspondientes máscaras.

Las máscaras son imágenes 2D con intensidades de píxeles de 1 (alta) o 0 (baja). Son esencialmente el producto de cualquier tarea de segmentación binaria, ya que nuestro objetivo es dividir la imagen en dos regiones principales.

Los tipos de cáncer en el conjunto de datos incluyen:

  • Queratosis actínica y carcinoma intraepitelial / enfermedad de Bowen (AKIEC),
  • carcinoma basocelular (CCB),
  • lesiones similares a la queratosis benigna (lentigos solares / queratosis seborreica y queratosis similar al liquen plano, BKL),
  • dermatofibroma (DF),
  • melanoma (MEL),
  • nevo melanocítico (NV)
  • lesiones vasculares (angiomas, angioqueratomas, granulomas piógenos y hemorragia, VASC).

Se realizó una exploración de datos en el conjunto de datos. Se encontró que el tipo de cáncer más común fue el nevus melanocítico con un 67% de participación.

GT = pd.read_csv(‘/content/GroundTruth.csv’) gt = GT.sum().to_frame().reset_index().drop(0) gt.columns = [‘toc’, ‘sum’]

plt.figure(figsize=(15,9))
explotar = [0, 0.1, 0, 0, 0, 0, 0]
plt.pie(gt[‘sum’]etiquetas=gt[‘toc’],explotar=explotar, autopct=’%.0f%%’)
plt.mostrar()

Dado que mi enfoque estaba en la segmentación, me concentré solo en las imágenes. El tipo de cáncer podría usarse en otras tareas posteriores, como la clasificación del cáncer.

Algo de EDA se hizo en las máscaras de verdad de tierra. Usando la fracción de intensidades de píxeles, encontré la distribución del tamaño de la lesión en comparación con el tamaño de la imagen.

Nota: Un punto a tener en cuenta aquí es que se desconoce el nivel de zoom de las imágenes cuando fueron capturadas. El nivel de zoom puede afectar enormemente el análisis realizado aquí. Dado que no tenemos información al respecto en los datos, podemos revisar algunos valores atípicos. Esto nos da una idea de si el motivo del cambio en la fracción cubierta es la gravedad o el nivel de zoom. Al monitorear algunos de los valores atípicos, se encontró que algunas imágenes parecen haber sido tomadas a través de un microscopio, lo que indica un alto nivel de zoom, mientras que otras parecen haber sido tomadas usando solo una cámara.

Para evitar este tipo de desviación en el conjunto de datos, se debe seguir una forma más estructurada de recopilación de datos, como metadatos adicionales sobre cómo se capturó la imagen y con qué nivel de zoom. Esto mejorará la calidad del conjunto de datos y cualquier tarea posterior realizada en él.

Este análisis se realiza bajo el supuesto de que todas las imágenes tenían el mismo nivel de zoom o bajo el supuesto de que el nivel de zoom no afecta significativamente nuestro enfoque.

lesion_fracs = []
para i en el rango (len (máscaras redimensionadas)): únicos, vc = np.unique (máscaras redimensionadas)[i].numpy(), return_counts=True) if len(vc) == 1: lesion_fracs.append(0) else: lesion_fracs.append(vc[1]/(vc[0]+vc[1]))

lfs = pd.DataFrame(lesion_fracs)

plt.figure(figsize=(8,5))
sns.set_style(‘rejilla oscura’)
sns.distplot(lfs, bins = 20)
plt.title(‘Distribución del tamaño de la lesión cutánea’, fontdict={‘tamaño’: 20})
plt.xlabel(‘Fracción de imagen cubierta por lesión cutánea’, fontdict={‘size’: 10})
plt.ylabel(‘Frecuencia’, fontdict={‘tamaño’: 10})
plt.mostrar()

🔥 Recomendado:  Cómo convertirte en diseñador gráfico en 8 simples pasos

La distribución del tamaño de las lesiones cutáneas parece sesgada hacia el lado izquierdo, con la “fracción de imagen cubierta por lesión cutánea” más frecuente de ~10 a 20%. Dado que nuestro modelo se construirá en base a estos datos, si vamos a probar un conjunto de datos diferente en nuestro modelo, tendremos que garantizar una distribución similar de los tamaños de las lesiones.

Acercarse

Una idea de alto nivel para el enfoque utilizado es la siguiente. Los detalles y las descripciones detalladas se proporcionan más adelante.

  • Modelo: para realizar la tarea de segmentación binaria, se utiliza una red neuronal de convolución.
  • Entrada: la entrada a nuestra CNN sería una imagen RGB del conjunto de datos redimensionado a 128 × 128.
  • Salida: la salida sería una imagen de máscara 2D del tamaño 128 × 128.
  • Función de pérdida: la pérdida de dados se utiliza como función de pérdida para este modelo. Es una métrica común utilizada en la segmentación binaria.
  • Métrica de seguimiento — Jaccard Index/Intersection over Union es otra métrica popular en la segmentación binaria.

Modelo

Los modelos de segmentación de la biblioteca de Python antorcha‘ se usó para crear y manejar fácilmente el modelo CNN. ‘modelos de segmentación pytorch’ nos permite crear un modelo combinando arquitecturas de codificador y decodificador y pesos previamente entrenados de arquitecturas populares de CNN como ResNet, VGGNet, UNet, etc. El modelo que se utilizó para la tarea tenía:

  • Arquitectura de la red U: Para explotar la fuerza del marco codificador-decodificador y omitir las conexiones del codificador al decodificador. El codificador convierte la imagen en una forma concisa de menor dimensión mediante el uso de una serie de capas convolucionales y capas de agrupación máxima.
    Luego, el decodificador convierte la forma concisa en la forma de salida requerida a través de una serie de capas convolucionales y de aumento de escala.
    Se ha encontrado que este marco es efectivo porque convertir la información a un vector dimensional más bajo elimina el ruido y ayuda a mejorar el entrenamiento en características importantes.
    Las conexiones de salto adicionales se encuentran para aumentar la calidad de la salida.
  • Codificador y decodificador Resnet34 con pesos preentrenados en ImageNet: Para ayudar con una buena inicialización. ImageNet es un conjunto de datos de imágenes popular que se utiliza en competiciones y para comparar modelos.
    Mediante el uso de pesos previamente entrenados, aseguramos la capacidad del modelo para identificar características importantes para comenzar.
    Esto nos ayuda a usar menos datos y menos tiempo de capacitación, ya que la mayor parte del trabajo está hecho y solo se necesita ajustar nuestra tarea.

clase Modelo de segmentación (pl. LightningModule):

def __init__(self, arch, codificador_nombre, en_canales, fuera_clases, **kwargs):
super().__init__()
self.modelo = smp.create_model(
arch, nombre_codificador=nombre_codificador, in_channels=in_channels, classes=out_classes, **kwargs
)

# parámetros de preprocesamiento para la imagen
params = smp.encoders.get_preprocessing_params(nombre_codificador)
self.register_buffer(“std”, torch.tensor(parámetros[“std”]).vista(1, 3, 1, 1))
self.register_buffer(“media”, torch.tensor(parámetros[“mean”]).vista(1, 3, 1, 1))

# pérdida de dados como función de pérdida para la segmentación de imágenes binarias
self.loss_fn = smp.losses.DiceLoss(modo=’binario’, from_logits=True)

def adelante(auto, imagen):
imagen = (imagen – self.mean) / self.std
máscara = self.modelo(imagen)
máscara de retorno

def _step(self, lote, etapa):

imagen = lote[0] # Forma de la imagen: (batch_size, num_channels, height, width)
máscara = lote[1]

logits_mask = self.forward(imagen)
pérdida = self.loss_fn(logits_mask, máscara)

prob_mask = logits_mask.sigmoid()
pred_mask = (prob_mask > 0.5).float()

tp, fp, fn, tn = smp.metrics.get_stats(pred_mask.long(), mask.long(), mode=”binary”)
volver {“pérdida”: pérdida, “tp”: tp, “fp”: fp, “fn”: fn, “tn”: tn,}

def _epoch_end(self, salidas, etapa):

tp = antorcha.cat([x[“tp”] para x en salidas])
fp = antorcha.cat([x[“fp”] para x en salidas])
fn = antorcha.cat([x[“fn”] para x en salidas])
tn = antorcha.cat([x[“tn”] para x en salidas])

# micro-imagewise: primero calcule el puntaje IoU para cada imagen y luego calcule la media sobre estos puntajes
per_image_iou = smp.metrics.iou_score(tp, fp, fn, tn,duction=”micro-imagewise”)

métricas = {
f”{escenario}_per_image_iou”: per_image_iou,
}

self.log_dict(métricas, prog_bar=Verdadero)

def training_step(self, lote, lote_idx):
return self._step(lote, “entrenar”)

def training_epoch_end(self, salidas):
return self._epoch_end(salidas, “entrenar”)

def validación_paso(auto, lote, lote_idx):
return self._step(lote, “válido”)

def validation_epoch_end(self, salidas):
return self._epoch_end(salidas, “válido”)

def test_step(self, lote, lote_idx):
return self._step(lote, “prueba”)

def test_epoch_end(self, salidas):
volver self._epoch_end(salidas, “prueba”)

def configure_optimizers(self):
devolver torch.optim.Adam(self.parameters(), lr=0.0001)

modelo = SegmentationModel(“Unet”, “resnet34”, in_channels=3, out_classes=1)

Entrada y salida

Las imágenes en el conjunto de datos tenían forma (3 450 600), lo que indica que son imágenes en color con 3 canales: RGB, y tienen una altura de 450 píxeles y un ancho de 600 píxeles. Estas imágenes fueron redimensionadas a (3,128,128).

🔥 Recomendado:  Cómo iniciar un negocio de Airbnb: la guía definitiva

Esto se debe a que la arquitectura de codificador-decodificador de las CNN maneja fácilmente tamaños de potencia de 2. Las imágenes de mayor calidad son computacionalmente costosas, pero reducir demasiado el muestreo resultará en la pérdida de características importantes.

Así que se eligió 128 para equilibrar el equilibrio entre la calidad de la imagen y el tiempo computacional. Las máscaras de salida 2D y las máscaras de verdad en tierra también se configuraron en el mismo tamaño (128,128).

# Se usaron las siguientes transformaciones en las imágenes

transform = transforma.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
])

resize_images = []
para imagen en imágenes[:4000]:
resized_image = transformar (imagen)
imágenes_redimensionadas.append(imagen_redimensionada)

máscaras_redimensionadas = []
para máscara en máscaras[:]:
resize_mask = transformar (máscara)
si resize_mask.squeeze()[0][0]*resize_mask.squeeze()[0][-1]*resize_mask.squeeze()[-1][0]*resize_mask.squeeze()[-1][-1] == 1:
máscara_redimensionada = -máscara_redimensionada.ronda()+1
máscaras_redimensionadas.append(máscara_redimensionada.ronda())

Función de pérdida y métricas

La calidad de los resultados de un modelo de aprendizaje automático está muy influenciada por la métrica que usamos para evaluar y evaluar el modelo. Se pueden utilizar diferentes métricas de evaluación y funciones de pérdida para probar el rendimiento de nuestro modelo.

En cualquier tarea de predicción, para cada punto de datos, tenemos una verdad fundamental que es el resultado correcto esperado y una predicción correspondiente que genera el modelo. El propósito de una métrica de evaluación es darnos una idea de qué tan cerca está la predicción de la realidad básica.

Ahora que tenemos una intuición cualitativa detrás de cuál es el propósito de una métrica, usamos la predicción y la verdad fundamental para crear una fórmula cuantitativa. Supongamos una tarea de clasificación de 2 salidas 2 clases. Entonces, para cada punto de datos, la salida puede ser cualquiera de las 2 clases, digamos 0 (clase negativa) y 1 (clase positiva).

Ahora, para cada punto de datos, tenemos una verdad básica correspondiente y una predicción. Podemos dividir el conjunto de datos en 4 partes:

  • Verdaderos positivos (TP): los puntos de datos en los que tanto la verdad básica como la predicción fueron de clase 1.
  • Negativos verdaderos (TN): los puntos de datos en los que tanto la realidad como la predicción eran de clase 0.
  • Falsos positivos (FP): los puntos de datos en los que la realidad básica era de clase 0 y la predicción era de clase 1.
  • Falsos negativos (FN): los puntos de datos en los que la verdad fundamental era de clase 1 y la predicción era de clase 0.

Al observar estas medidas, vemos que nuestro objetivo sería maximizar TP y TN y minimizar FP y FN.

La “precisión” es una métrica muy conocida y se utiliza en muchas aplicaciones de predicción. La precisión se puede definir simplemente como el porcentaje de predicciones correctas de todas las predicciones. Usando los términos mencionados anteriormente, la fórmula para la precisión es:

¡Parece todo simple y directo hasta ahora! Pero ahora nos enfrentamos a 2 problemas que presentamos y resolvemos secuencialmente.

El primer problema es que la tarea en cuestión es la segmentación binaria y no la clasificación binaria. ¡Para cada punto de datos, la salida no es una clase binaria sino una imagen! Tenemos la tarea de comparar una imagen real del terreno con una imagen predicha.

Una imagen es solo una matriz de valores de intensidad de píxeles. En nuestro caso, las imágenes de salida son máscaras en las que las intensidades de los píxeles pueden ser 0 o 1. Así que ahora solo tenemos valores reales y predichos de una matriz de píxeles cuyos valores son 0 o 1. Esto se parece mucho a la clasificación binaria indicada anteriormente. Ahora solo podemos usar TP, TN, FP y FN sobre todos los valores de píxeles y calcular la precisión.

El segundo problema al que nos enfrentamos es el de la elección de la métrica. La precisión puede resultar ineficaz en algunas aplicaciones. Para nuestra tarea, nos interesa predecir el área de la piel afectada, es decir, nos enfocamos más en acertar en las predicciones de una de las clases, siendo la clase 1, digamos. En este caso, algunas métricas brindan mejores perspectivas.

El índice de Jaccard o la intersección sobre la unión (IoU) es simplemente la relación del número de píxeles activados en la intersección de la verdad del terreno G y la imagen predicha S y la unión.

🔥 Recomendado:  21 artículos caros que valen la pena

donde |.| indica el número de píxeles activados.

Podemos reescribir lo mismo en términos de TP, TN, FP y FN como:

Dado que solo TP está presente en el numerador y no TN, solo las predicciones correctas de la clase 1 aumentarán la puntuación. Esta métrica es muy intuitiva porque, idealmente, queremos tener la mayor intersección posible entre la realidad básica y la predicción, y queremos que la unión esté lo más cerca posible de la intersección.

Una métrica estrechamente relacionada con el índice Jaccard es la puntuación de dados. La puntuación de dados es una métrica de superposición espacial que se calcula de la siguiente manera para la imagen predicha S y la verdad del terreno G.

Esto se puede reescribir como:

Cualquiera de estas métricas se puede utilizar para la segmentación binaria. Hemos utilizado la pérdida de dados para entrenar el modelo. El modelo minimiza la pérdida de dados, maximizando así la puntuación de dados. Hemos utilizado el Índice Jaccard para la evaluación final.

Capacitación

Se utilizó la biblioteca de Python ‘PyTorch lightning’ para entrenar el modelo. Se utilizaron 4000 imágenes del conjunto de datos completo. De estos, el 95 % (3800) se utilizó para capacitación y el 2,5 % (100) para validación y prueba.

de sklearn.model_selection import train_test_split

tren_imágenes,prueba_imágenes, tren_máscaras,prueba_máscaras = tren_prueba_división(imágenes_dimensionadas,máscaras_dimensionadas,tamaño_prueba=0.05)
test_images,val_images, test_masks,val_masks = train_test_split(test_images,test_masks,test_size=0.5)

tren_conjunto de datos = []
para i en el rango (len (train_images)):
conjunto_de_datos_del_tren.append([train_images[i]máscaras_de_tren[i]])
val_dataset = []
para i en el rango (len (val_images)):
val_dataset.append([val_images[i]val_masks[i]])
test_dataset = []
para i en el rango (len (test_images)):
test_dataset.append([test_images[i]test_masks[i]])

n_cpu = os.cpu_count()
train_dataloader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=n_cpu)
val_dataloader = DataLoader(val_dataset, batch_size=16, shuffle=False, num_workers=n_cpu)
test_dataloader = DataLoader(test_dataset, batch_size=16, shuffle=False, num_workers=n_cpu)

Los siguientes hiperparámetros fueron elegidos después de sintonizar por ensayo y error:

  • Tamaño del lote: en el entrenamiento por lotes, el modelo se actualiza después de cada lote de puntos de datos en función de la pérdida calculada utilizando los puntos de datos del lote. El tamaño del lote se seleccionó para ser 16.
  • Optimizador: Adam se utilizó como optimizador con una tasa de aprendizaje inicial de 0,0001. Adam Optimizer ayuda a lograr una mejor convergencia mediante el uso de un descenso de gradiente basado en impulso y una tasa de aprendizaje adaptable. Puede manejar gradientes escasos en problemas ruidosos.
  • Épocas: este es el número de iteraciones que el modelo entrena a través de todo el conjunto de datos. Cuanto mayor sea el número de épocas, mejor se ajusta a los datos de entrenamiento. Debe ser tal que el modelo no se ajuste ni sobreajuste. El número de épocas se fijó en 20.

entrenador = pl.Entrenador (gpus = 1, max_epochs = 20)

trainer.fit(modelo, train_dataloaders=train_dataloader, val_dataloaders=val_dataloader)

Resultados

Después del entrenamiento, el conjunto de prueba se evaluó utilizando el modelo entrenado. Se calculó el índice de Jaccard para cada muestra y se promedió sobre todo el conjunto de pruebas.

Se encontró que el índice de Jaccard medio era 0.8828. Algunos resultados se visualizan en las figuras 2–5.

Casos de falla y deficiencias.

Aunque el modelo logró un índice de Jaccard alto en los datos de prueba, el puntaje promedio se redujo debido a algunos escenarios específicos. Algunos de ellos se mencionan en las figuras 6–9.

Referencias

Publicado a través de Hacia la IA