DETECCIÓN DE NEUMONÍA EN RADIOGRAFÍAS: 5 – MODELOS DE APRENDIZAJE PROFUNDO (II)

Publicado por @JoseMa_deCuenca el March 8, 2020, 9:26 p.m.
Una red convolucional, pese a tener un número de parámetros a entrenar mucho más reducido que una red completamente conectada, requiere unos tiempos de cálculo por época de entrenamiento similares, debido a las mayor cantidad de operaciones que requieren las convoluciones. Una alternativa para reducir este coste de cálculo son las convoluciones sepables, que consisten en reemplazar las convoluciones por una convolución profunda (deepthwise separable convolution) independiente en cada canal seguida de una convolución puntual (pointwise convolution) entre los canales (con un tamaño de kernel 1x1). Esta estrategia fue implementada en la red Xception y ha sido adoptada por las redes MobileNet para permitir su adaptación a todo tipo de dispositivos, incluidos móviles La capa convolucional separable tiene una computación más eficiente que una única operación de convolución 2D, ya que primero busca correlaciones en un espacio bidimensional (2D), y a continuación en un espacio unidimensional (1D), dando como resultado la convolución 3D (ancho x alto x canal). Nada más introducir los datos con una red convolucional con kernel más elevado, reduzco el número de datos con MaxPooling para mantener el número de parámetros bajo. A continuación aplico bloques compuestos de una capa convolucional separable en profundidad de 2D (se denomina así porque trabaja por canales). Esta capa aprende detalles por separado en cada canal y luego los recompone. Después añado una de normalización interna BatchNormalization salvo en el primer bloque, y otra de MaxPooling. La capa convolucional separable en profundidad divide el núcleo en dos más pequeños (que requieren menos operaciones) y acelera el proceso de cálculo. La normalización mejora la capacidad de detección, y el MaxPooling permite reducir el número de parámetros y también acelera los cálculos al simplificar la arquitectura, a la vez que reduce el riesgo de sobreajuste. Como en la primera red neuronal, los bloques de capas se repiten incrementando el número de filtros cada vez para concentrar detalles. Terminan con un formateador con Flatten y un clasificador con una capa totalmente conectada Dense y otra softmax. El número de parámetros de este modelo es aproximadamente la mitad del primero basado en redes convolucionales, y aunque el tiempo requerido es muy similar, sus resultados son algo mejores, incluso con un muy limitado número de iteraciones, que como en el caso anterior se adopta para acelerar las pruebas. Posteriormente, tras seleccionar los mejores modelos, se usará un algoritmo de autotuning con GridSearchCV para su optimización. Esto permite reducir el tiempo de las pruebas como mínimo a la mitad. # Definicion de MODELO DE RN CONVOLUCIONAL SECUENCIAL model_basico = Sequential() model_basico.add(Conv2D(64, (3, 3), activation='relu', padding="same", input_shape=(3,150,150))) model_basico.add(MaxPooling2D(pool_size=(2, 2))) model_basico.add(SeparableConv2D(64, (3,3), activation='relu', padding='same')) model_basico.add(MaxPooling2D(pool_size=(2, 2))) model_basico.add(SeparableConv2D(64, (3,3), activation='relu', padding='same')) model_basico.add(BatchNormalization()) model_basico.add(MaxPooling2D(pool_size=(2, 2))) model_basico.add(SeparableConv2D(128, (3,3), activation='relu', padding='same')) model_basico.add(BatchNormalization()) model_basico.add(MaxPooling2D(pool_size=(2, 2))) # Formateo de datos con Flatten model_basico.add(Flatten()) # Clasificador con capas totalmente conectadas model_basico.add(Dense(64, activation='relu')) # Capa con funcion de activacion tuneada model_basico.add(Dropout(0.4)) model_basico.add(Dense(2 , activation='softmax')) # Compilacion model_basico.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0001, decay=1e-5), metrics=['accuracy']) # Resumen del modelo print(model_basico.summary()) ## ENTRENAMIENTO MODELO RN CONVOLUCIONAL SEPARABLE BASICO # Parametros de entrenamiento batch_size = 256 epochs = 4 # Habilito una ruta y un archivo para guardar los pesos de entrenamiento de la red ## ESPECIFICO PARA ESTA RED filepath="./checkpoints/chest_xray_weights_basico.hdf5" checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max') # Entrenamiento del modelo # Inicia cronometro start_time = time.time() history_basico = model_basico.fit(X_train, y_train, validation_data = (X_test , y_test), callbacks=[lr_reduce,checkpoint], epochs=epochs) # Detiene el cronometro end_time = time.time() # Informa cronometro dt = round(end_time - start_time, 2) print("El entrenamiento de este modelo tardó: ", dt, "segundos (", round(dt/60,1) , "minutos)") # Definimos variables para guardar los resultados del modelo de referencia entrenado acc_bas = history_basico.history['acc'] val_acc_bas = history_basico.history['val_acc'] loss_bas = history_basico.history['loss'] val_loss_bas = history_basico.history['val_loss'] # Representacion de metricas para el modelo de referencia durante el entrenamiento # Precision plt.plot(acc_bas, label='Precis. entrenamiento', color='Red') plt.plot(val_acc_bas, label='Precis. Prueba', color='Green') plt.title('Precision durante entrenamiento y prueba') plt.ylabel('accuracy') plt.xlabel('epoch-1') plt.legend(fontsize=10, loc='upper left') plt.figure() # Perdida plt.plot(loss_bas, label='Pérd. entrenamiento', color='Red') plt.plot(val_loss_bas, label='Pérd. prueba', color='Green') plt.title('Pérdida durante entrenamiento y prueba') plt.ylabel('loss') plt.xlabel('epoch-1') plt.legend(fontsize=10, loc='upper right') plt.figure() # Represento plt.show() pred = model_basico.predict(X_test) pred = np.argmax(pred,axis = 1) y_true = np.argmax(y_test,axis = 1) # Libreria mlxtend para dibujo rapido de la matriz CM = confusion_matrix(y_true, pred) fig, ax = plot_confusion_matrix(conf_mat=CM , figsize=(5, 5)) plt.show() Para optimizar los parámetros de este modelo podemos utilizar un estimador GridSearchCV, que pruebe diferente número de épocas y tamaños de bloque, dejando finalmente solo una muestra de tales pruebas, que contiene el óptimo. Se evidencia que a menor tamaño de lote más número de iteraciones son necesarias, y que cuando este crece, antes se produce el overfitting. Por ejemplo, el código de optimización para el análisis de clasificación binaria, según diagnóstico, quedará: ## ENTRENAMIENTO RN CONV SEPARABLE BASICO 2 CLASES - TUNEADO ECHOCH / BATCHSIZE # Fijo una semilla para tener reproductibilidad seed = 7 np.random.seed(seed) # Defino funcion para crear un modelo def creador_modelo(): model_auto = Sequential() model_auto.add(Conv2D(64, (3, 3), activation='relu', padding="same", input_shape=(3,150,150))) model_auto.add(MaxPooling2D(pool_size=(2, 2), data_format='channels_first')) model_auto.add(SeparableConv2D(64, (3,3), activation='relu', padding='same')) model_auto.add(MaxPooling2D(pool_size=(2, 2), data_format='channels_first')) model_auto.add(SeparableConv2D(64, (3,3), activation='relu', padding='same')) model_auto.add(BatchNormalization()) model_auto.add(MaxPooling2D(pool_size=(2, 2), data_format='channels_first')) model_auto.add(SeparableConv2D(128, (3,3), activation='relu', padding='same')) model_auto.add(BatchNormalization()) model_auto.add(MaxPooling2D(pool_size=(2, 2), data_format='channels_first')) model_auto.add(Flatten()) model_auto.add(Dense(64, activation='relu')) # Capa con funcion de activacion tuneada model_auto.add(Dropout(0.4)) model_auto.add(Dense(2 , activation='softmax')) model_auto.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0001, decay=2e-5), metrics=['accuracy']) return model_auto # Recreo el modelo cada iteracion para evitar sobre entrenamientos model_basico_auto2 = KerasClassifier(build_fn=creador_modelo, verbose=1) # Defino parametros a probar batch_size = [64, 128] epochs = [4, 6] param_grid = dict(batch_size=batch_size, epochs=epochs) # Inicia cronometro start_time = time.time() # grid2 = GridSearchCV(estimator=model_basico_auto2, param_grid=param_grid, n_jobs=-1) grid2 = GridSearchCV(estimator=model_basico_auto2, param_grid=param_grid, n_jobs=8, pre_dispatch=16) grid_result2 = grid2.fit(X_train2, y_train2) # Detiene el cronometro end_time = time.time() # Informa cronometro dt = round(end_time - start_time, 2) print("\nEl entrenamiento de este modelo tardó: ", dt, "segundos (", round(dt/60,1) , "minutos)") # summarize results print("\nMejor: %f usando %s" % (grid_result2.best_score_, grid_result2.best_params_)) means2 = grid_result2.cv_results_['mean_test_score'] stds2 = grid_result2.cv_results_['std_test_score'] params2 = grid_result2.cv_results_['params'] for mean, stdev, param in zip(means2, stds2, params2): print("%f (%f) with: %r" % (mean, stdev, param)) Una vez analizado el comportamiento de las redes neuronales seleccionadas, ajustado al óptimo el tamaño de lote y el número de épocas de entrenamiento, se procede a entrenar el modelo utilizando las funciones callbacks de Keras para almacenar los resultados, modificar la tasa de aprendizaje durante el entrenamiento y habilitar la monitorización con Tensor Board. De nuevo como ejemplo se incluye el código para el modelo de 2 clases, aunque se puede proceder igualmente para el modelo de 3 clases (identificación del patógeno). ## ENTRENAMIENTO MODELO RN CONVOLUCIONAL SEPARABLE BASICO 2 CLASES OPTIMIZADO ## ANTES DE EJECUTAR EN LA CONSOLA DE CONDA HACER tensorboard --logdir <<pathlogdir>> ## por ejmplo: tensorboard --logdir c:\Users\Jose\logs\tensorboard # Parametros de entrenamiento con los resultados prueba tunning anterior batch_size = 64 epochs = 6 # DEFINO CALLBACKS # Habilito una ruta y un archivo para guardar los pesos de entrenamiento de la red ## ESPECIFICO PARA ESTA RED filepath="./checkpoints/chest_xray_res2_weights_basico.hdf5" checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max') # Definimos reducción de la tasa de aprendizaje ReduceLROnPlateau cuando cerca de minimo lr_reduce = ReduceLROnPlateau(monitor='val_acc', factor=0.1, min_delta=0.001, patience=1, verbose=1) # inicializar tensorboardcolab tensorboard = TensorBoard(log_dir='./log/tensorboard', histogram_freq=0, write_graph=True, write_images=False) # Entrenamiento del modelo # Inicia cronometro start_time = time.time() # Entreno el modelo # Sin TensorBoard # history_basico2 = model_basico2.fit(X_train2, y_train2, validation_data = (X_test2 , y_test2), callbacks=[lr_reduce,checkpoint], epochs=epochs) history_basico2 = model_basico2.fit(X_train2, y_train2, validation_data = (X_test2 , y_test2), callbacks=[lr_reduce, checkpoint, tensorboard], epochs=epochs) # Detiene el cronometro end_time = time.time() # Informa cronometro dt = round(end_time - start_time, 2) print("El entrenamiento de este modelo tardó: ", dt, "segundos (", round(dt/60,1) , "minutos)") Los códigos completos pueden descargarse el repositorio https://github.com/watershed-lab/pneumo-detector Para finalizar, en la última entrega realizaremos un ensamblaje de los métodos más efectivos vistos, que nos permita obtener la mejor clasificación posible.