DETECCIÓN DE NEUMONÍA EN RADIOGRAFÍAS: 6 – COMPARATIVA Y ENSEMBLADO MANUAL DE MODELOS

Publicado por @JoseMa_deCuenca el March 8, 2020, 9:28 p.m.
Tras cada una de las pruebas realizadas con los diferentes modelos (SVM, árbol de clasificación, redes neuronales) y estrategias (uso de todo el dataset original, resampling, segmentación de imágenes), se ha ido procediendo a evaluar su rendimiento con el dataset de validación (carpeta test). Esto nos permite en este punto compararlos para elegir los que mejor se comportan y seguir trabajando sobre ellos. La comparación se realizará obteniendo las matrices de confusión tras cada validación, y calculando las métricas correspondientes a cada una. Clasificación en 2 clases: sano y enfermo de neumonía: Al ejecutar los modelos, veremos que el conjunto remuestreado tiene más aciertos negativos, pero falla en los positivos, por lo que cuestiona esa estrategia. Porque el objetivo de la herramienta debe ser maximizar la exactitud de la clase positiva (recall), sin sacrificar como positivos aquellos que no lo son (precision). Y realizarlo en un tiempo de entrenamiento aceptable, utilizando medios limitados (domésticos). Vemos que la ventaja de las redes neuronales sobre los algoritmos clásicos está en su mejor balance entre precisión y recall, especialmente la red convolucional separable realizada como penúltima prueba, aunque muy próximo a SVM. Por otra parte, al ejecutar varias veces los algoritmos, donde mayores variaciones del rendimiento se han observado ha sido en los clásicos. Sobre los diferentes dataset, parece que tras las pruebas arroja mejores métricas el original. Al remuestrear reduciendo las imágenes tomadas se mejora mucho los tiempos y se incrementa la precisión, pero se suele reduce algo la recuperación. Aunque esta reducción se considera aceptable por la reducción de los tiempos y su en todo caso elevado valor. El segmentado de datos es especialmente efectivo con el algoritmo de Soporte de Máquina Vectorial, pero el trabajo de segmentación no compensa en el resto de pruebas realizadas. En términos absolutos, el algoritmo que mejor resultados arroja tras las pruebas parece ser la última red convolucional separable, trabajando con el conjunto remuestreado, ya que es capaz de mantener una elevada precisión para detectar la neumonía, con una elevada fiabilidad en el diagnóstico (recall), y lo consigue en un tiempo de cómputo más reducido que el resto. No obstante, incluye muchos falsos negativos, y más aún falsos positivos. Con todo, la solución de compromiso más aceptable tras las pruebas, parece ser un ensemble entre el algoritmo SVM y la red convolucional separable básica, con el dataset remuestreado. Clasificación en 3 clases: sano, neumonía causada por bacteria, neumonía causada por virus Como se ha comentado, los modelos utilizados con ligeras adaptaciones también son útiles para tratar de identificar el tipo de patógeno causante de la infección, realizándose las pruebas anteriores con un clasificador de 3 etiquetas: sin infección, baceriana y vírica. Para ello, el código utilizado es prácticamente el mismo, alterado solamente en la generación de los conjuntos de etiquetas para indicar la columna con el código del patógeno (patog_ct) en sustitución de la que se refería al código del diagnóstico (diag_ct), y algunos parámetros internos. Aunque los resultados obtenidos en las pruebas con los clasificadores de 3 categorías han sido bastante peores que en la separación binaria anterior. La mayor dificultad se ha encontrado en la identificación de los virus (tercera columna de las matrices de confusión). Sin embargo mantienen una buena diferenciación en la identificación de la ausencia de enfermedad (primera columna) y un resultado aceptable en según qué casos para el diagnóstico de pneumonías bacterianes (segunda columna). En resumen, vemos que los algoritmos no permiten identificar correctamente los patógenos causantes de la enfermedad. Esto sucede muy probablemente por la simplificación a la que se somete las imágenes durante el proceso previo, y probablemente pueda mejorarse con un mayor número de datos de aprendizaje y el uso de mayores resoluciones, aunque ello implicaría el uso de mayores tiempos de proceso o máquinas más potentes. Así mismo en este caso no se aplicará la estrategia de segmentación porque los datos parecen perder la capacidad de reflejar las diferencias entre los patógenos causantes de la infección. Con todo, la solución de compromiso más aceptable tras las pruebas, parece ser de nuevo un ensemble entre el algoritmo SVM y la red convolucional separable básica, con el dataset remuestreado; que mejorará los resultados individuales de cada modelo. Debido a la enorme diferencia entre ambos modelos, no podemos recurrir a una función de ensemble automática, sino que deberemos realizarlo de forma enteramente manual. Para ello, tras la ejecución de cada modelo finalmente escogido, hay que realizar una predicción con los datos del conjunto de prueba no utilizados (carpeta VAL), almacenándo esas predicciones en un dataframe, junto con las etiquetas reales de los datos:. # Creo un dataframe para los modelos de validacion y sus prediciones # Preparo dataframe de resultados resultados_val = pd.DataFrame(columns=['y_val2', 'y_val3', 'svm2']) # Incorporo la columna original resultados_val['y_val2'] = y_val2 resultados_val['y_val3'] = y_val3 # Preparo dataset de validacion y uso modelo svm para predicciones poniendolas en dataframe X_val_pca = pca2.transform(X_val2_svm) resultados_val['svm2'] = clf2.predict(X_val_pca) # Muestro resultados de la prediccion resultados_val.head(20) Tras ejecutar todos los modelos y realizar las predicciones con cada uno de ellos, tanto sobre la clasificación en 2 clases (diagnóstico) como en 3 (patógeno), se montarán todos en la tabla, obtenido un dataframe de predicciones completo. A partir de este dataframe de predicciones se calcula un ensemble tipo voting, asignando pesos a cada uno de los modelos para lograr un ajuste óptimo. Se realiza manualmente mediante código, dado que los modelos usan datos diferentes dimensiones que impiden el uso de la función al efecto. Se realiza por separado para obtener resultados sobre 2 y sobre 3 clases. Por ejemplo, para 2 clases: ### ENSEMBLE MANUAL TIPO VOTING para 2 clases # Creo lista vacia para clasificador por filas voting_2 =[] # Analisis por filas val0_fila = 0 val1_fila = 0 for indice_fila, fila in resultados_val.iterrows(): # SVM2 peso = 1 if fila[2] == 0: val0_fila = val0_fila + peso else: val1_fila = val1_fila + peso # SVM3 peso = 2 if fila[3] == 0: val0_fila = val0_fila + peso else: val1_fila = val1_fila + peso # ConvSep2 peso = 3 if fila[4] == 0: val0_fila = val0_fila + peso else: val1_fila = val1_fila + peso # ConvSep3 peso = 2 if fila[5] == 0: val0_fila = val0_fila + peso else: val1_fila = val1_fila + peso # Balance para poner el valor dominante en la lista de resultados if val0_fila > val1_fila: voting_2.append(0) else: voting_2.append(1) val0_fila = 0 val1_fila = 0 print (voting_2) # Tomo la segunda y hago redondeo simple resultados_val['voting2'] = voting_2 # Muestro resultados de la prediccion resultados_val.head(20) El resultado final de ambos ensembles se incorpora al dataframe: Para finalizar, a partir del dataframe con todas las predicciones, se calculan las métricas para los ensembles de voting manualmente obtenidos, también de forma manual, a través de sus matrices de confusión. # Metricas manuales para 2 clases (diagnostico) # Reindexo para eliminar errores de comparacion columnas = ['y_val2', 'y_val3', 'svm2', 'svm3', 'convsep2', 'convsep3', 'voting2', 'voting3'] resultados_val_id = resultados_val.reindex(columns=columnas) resultados_val_id = resultados_val_id[columnas].astype(int) resultados_val_id.head(20) # Total predicciones por tipo TotP_2 = resultados_val_id[resultados_val_id['voting2']==1]['voting2'].count() TotN_2 = resultados_val_id[resultados_val_id['voting2']==0]['voting2'].count() print('Total positivos TotP: ', TotP_2) print('Total negativos TotN:', TotN_2) # Solo verdaderas prediciones resultados_pos = resultados_val_id[resultados_val_id['y_val2']==1] TP_2 = resultados_pos[resultados_pos['y_val2']==resultados_pos['voting2']]['y_val2'].count() print('\nVerdaderos Positivos TP: ', TP_2) resultados_neg = resultados_val_id[resultados_val_id['y_val2']==0] TN_2 = resultados_neg[resultados_neg['y_val2']==resultados_neg['voting2']]['y_val2'].count() print('Verdaderos Negativos TN: ', TN_2) # Cuento falsos positivos y falsos negativos FP_2 = TotP_2 - TP_2 FN_2 = TotN_2 - TN_2 print('\nFalsos Positivos FP: ', FP_2) print('Falsos Negativos FN: ', FN_2) # Cuento los aciertos y los fallos TA_2 = resultados_val_id[resultados_val_id['y_val2']==resultados_val_id['voting2']]['y_val2'].count() print('\nTotal aciertos TA: ', TA_2) TF_2 = abs(TA_2 - resultados_val_id['y_val2'].count()) print('Total Fallos TF: ', TF_2) # Dibujo la matriz de confusion para el ensemble (creando un numpy array) CM = np.array([[TN_2, FP_2],[FN_2, TP_2]]) fig, ax = plot_confusion_matrix(conf_mat=CM , figsize=(5, 5)) plt.show() # METRICAS DEL ENSEMBLE print('\nMETRICAS:') # Exactitud print('Exactitud del ensemble (acc): ', round((TP_2+TN_2)/(TotP_2+TotN_2),3)) # Sensibilidad print('Sensibilidad (sens): ', round(TP_2 / (TP_2+TN_2),3)) # Especificidad print('Especificidad (spec): ', round(FP_2 / (FN_2+FP_2) ,3)) # Precision print('Precision (prec): ', round(TP_2/(FP_2+TP_2) ,3)) # Recuperacion print('Recuperacion (recall): ', round(TP_2/(FN_2+TP_2) ,3)) # F1 print('F1: ', round(2*(TP_2/(FN_2+TP_2))*(TP_2/(FP_2+TP_2))/((TP_2/(FN_2+TP_2))+(TP_2/(FP_2+TP_2))) ,3)) El resultado de la matriz de confusión verifica que se ha alcanzado el objetivo de no tener ningún fallo en detección de positivos, aunque la herramienta marca como enfermas dos personas que no lo están. Los códigos completos pueden descargarse el repositorio https://github.com/watershed-lab/pneumo-detector