La simplicidad y complejidad de las primitivas o cómo determinar el preprocesamiento innecesario para una red neuronal

Este es el tercer artículo sobre el análisis y estudio de elipses, triángulos y otras formas geométricas.
Los artículos anteriores plantearon algunas preguntas muy interesantes entre los lectores, en particular, sobre la complejidad o simplicidad de ciertas secuencias de entrenamiento. Las preguntas son realmente muy interesantes, por ejemplo, ¿cuánto más difícil es aprender un triángulo que un cuadrángulo u otro polígono?



Intentemos comparar, y para comparar tenemos una gran idea, probada por generaciones de estudiantes, la idea: cuanto más corta es la hoja de trucos, más fácil es el examen.

Este artículo también es simplemente el resultado de la curiosidad y el interés ocioso, nada de eso se encuentra en la práctica y para tareas prácticas hay un par de grandes ideas, pero no hay casi nada para copiar y pegar. Este es un pequeño estudio de la complejidad de las secuencias de entrenamiento: se presentan el razonamiento y el código del autor, puede verificar / complementar / cambiar todo usted mismo.

Entonces, intentemos averiguar qué figura geométrica es más complicada o más simple para la segmentación, qué curso de conferencias para IA es más comprensible y mejor absorbido.

Hay muchas formas geométricas diferentes, pero solo compararemos triángulos, cuadrángulos y estrellas de cinco puntas. Utilizaremos un método simple para construir una secuencia de tren: dividiremos las imágenes monocromas de 128x128 en cuatro partes y colocaremos al azar una elipse y, por ejemplo, un triángulo en estos cuartos. Detectaremos un triángulo del mismo color que la elipse. Es decir la tarea es entrenar la red para distinguir, por ejemplo, un polígono cuadrangular de una elipse pintada del mismo color. Aquí hay ejemplos de imágenes que estudiaremos.







No detectaremos un triángulo y un cuadrángulo en una imagen, los detectaremos por separado, en diferentes trenes, contra el fondo de interferencia en forma de elipse.

Tomemos la clásica red U y tres tipos de secuencias de entrenamiento con triángulos, cuadrángulos y estrellas para la investigación.

Entonces, dado:

  • tres secuencias de entrenamiento de pares de imágenes / máscaras;
  • la red Red U ordinaria, que se usa ampliamente para la segmentación.

Idea para probar:

  • determinar cuál de las secuencias de entrenamiento es "más difícil" de aprender;
  • cómo algunas técnicas de preprocesamiento afectan el aprendizaje

Comencemos, seleccione 10,000 pares de imágenes de cuadrángulos con elipses y máscaras y considérelos cuidadosamente. Estamos interesados ​​en lo corto que resultará la cuna y de qué longitud depende.

Cargamos bibliotecas, determinamos los tamaños de una serie de imágenes.
import numpy as np import matplotlib.pyplot as plt %matplotlib inline import math from tqdm import tqdm from skimage.draw import ellipse, polygon from keras import Model from keras.optimizers import Adam from keras.layers import Input,Conv2D,Conv2DTranspose,MaxPooling2D,concatenate from keras.layers import BatchNormalization,Activation,Add,Dropout from keras.losses import binary_crossentropy from keras import backend as K import tensorflow as tf import keras as keras w_size = 128 train_num = 10000 radius_min = 10 radius_max = 20 


determinar las funciones de pérdida y precisión
 def dice_coef(y_true, y_pred): y_true_f = K.flatten(y_true) y_pred = K.cast(y_pred, 'float32') y_pred_f = K.cast(K.greater(K.flatten(y_pred), 0.5), 'float32') intersection = y_true_f * y_pred_f score = 2. * K.sum(intersection) / (K.sum(y_true_f) + K.sum(y_pred_f)) return score def dice_loss(y_true, y_pred): smooth = 1. y_true_f = K.flatten(y_true) y_pred_f = K.flatten(y_pred) intersection = y_true_f * y_pred_f score = (2. * K.sum(intersection) + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth) return 1. - score def bce_dice_loss(y_true, y_pred): return binary_crossentropy(y_true, y_pred) + dice_loss(y_true, y_pred) def get_iou_vector(A, B): # Numpy version batch_size = A.shape[0] metric = 0.0 for batch in range(batch_size): t, p = A[batch], B[batch] true = np.sum(t) pred = np.sum(p) # deal with empty mask first if true == 0: metric += (pred == 0) continue # non empty mask case. Union is never empty # hence it is safe to divide by its number of pixels intersection = np.sum(t * p) union = true + pred - intersection iou = intersection / union # iou metrric is a stepwise approximation of the real iou over 0.5 iou = np.floor(max(0, (iou - 0.45)*20)) / 10 metric += iou # teake the average over all images in batch metric /= batch_size return metric def my_iou_metric(label, pred): # Tensorflow version return tf.py_func(get_iou_vector, [label, pred > 0.5], tf.float64) from keras.utils.generic_utils import get_custom_objects get_custom_objects().update({'bce_dice_loss': bce_dice_loss }) get_custom_objects().update({'dice_loss': dice_loss }) get_custom_objects().update({'dice_coef': dice_coef }) get_custom_objects().update({'my_iou_metric': my_iou_metric }) 


Usaremos la métrica del primer artículo . Permítanme recordarles a los lectores que vamos a predecir la máscara del píxel: este es el "fondo" o "cuadrángulo" y evaluar la verdad o la falsedad de la predicción. Es decir Son posibles las siguientes cuatro opciones: predijimos correctamente que un píxel es un fondo, predijimos correctamente que un píxel es un cuadrángulo o cometimos un error al predecir un "fondo" o "cuadrángulo". Entonces, para todas las imágenes y todos los píxeles, estimamos el número de las cuatro opciones y calculamos el resultado; este será el resultado de la red. Y cuanto menos predicciones erróneas y más verdaderas, cuanto más preciso sea el resultado y mejor será la red.

Examinamos la red como un "recuadro negro", no veremos lo que está sucediendo con la red interna, cómo cambian los pesos y cómo se eligen los gradientes; más adelante analizaremos las entrañas de la red cuando comparemos las redes.

U-net simple
 def build_model(input_layer, start_neurons): # 128 -> 64 conv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(input_layer) conv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(conv1) pool1 = MaxPooling2D((2, 2))(conv1) pool1 = Dropout(0.25)(pool1) # 64 -> 32 conv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(pool1) conv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(conv2) pool2 = MaxPooling2D((2, 2))(conv2) pool2 = Dropout(0.5)(pool2) # 32 -> 16 conv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(pool2) conv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(conv3) pool3 = MaxPooling2D((2, 2))(conv3) pool3 = Dropout(0.5)(pool3) # 16 -> 8 conv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(pool3) conv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(conv4) pool4 = MaxPooling2D((2, 2))(conv4) pool4 = Dropout(0.5)(pool4) # Middle convm = Conv2D(start_neurons * 16, (3, 3), activation="relu", padding="same")(pool4) convm = Conv2D(start_neurons * 16, (3, 3), activation="relu", padding="same")(convm) # 8 -> 16 deconv4 = Conv2DTranspose(start_neurons * 8, (3, 3), strides=(2, 2), padding="same")(convm) uconv4 = concatenate([deconv4, conv4]) uconv4 = Dropout(0.5)(uconv4) uconv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(uconv4) uconv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(uconv4) # 16 -> 32 deconv3 = Conv2DTranspose(start_neurons * 4, (3, 3), strides=(2, 2), padding="same")(uconv4) uconv3 = concatenate([deconv3, conv3]) uconv3 = Dropout(0.5)(uconv3) uconv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(uconv3) uconv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(uconv3) # 32 -> 64 deconv2 = Conv2DTranspose(start_neurons * 2, (3, 3), strides=(2, 2), padding="same")(uconv3) uconv2 = concatenate([deconv2, conv2]) uconv2 = Dropout(0.5)(uconv2) uconv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(uconv2) uconv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(uconv2) # 64 -> 128 deconv1 = Conv2DTranspose(start_neurons * 1, (3, 3), strides=(2, 2), padding="same")(uconv2) uconv1 = concatenate([deconv1, conv1]) uconv1 = Dropout(0.5)(uconv1) uconv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(uconv1) uconv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(uconv1) uncov1 = Dropout(0.5)(uconv1) output_layer = Conv2D(1, (1,1), padding="same", activation="sigmoid")(uconv1) return output_layer # model input_layer = Input((w_size, w_size, 1)) output_layer = build_model(input_layer, 26) model = Model(input_layer, output_layer) model.compile(loss=bce_dice_loss, optimizer=Adam(lr=1e-4), metrics=[my_iou_metric]) model.summary() 


La función de generar pares de imagen / máscara. En una imagen en blanco y negro de 128x128 llena de ruido aleatorio con una selección aleatoria de dos rangos, o 0.0 ... 0.75 o 0.25..1.0. Seleccione aleatoriamente un cuarto en la imagen y coloque una elipse orientada al azar y en el otro cuarto colocamos un cuadrilátero e igualmente color con ruido aleatorio.

 def next_pair(): img_l = (np.random.sample((w_size, w_size, 1))* 0.75).astype('float32') img_h = (np.random.sample((w_size, w_size, 1))* 0.75 + 0.25).astype('float32') img = np.zeros((w_size, w_size, 2), dtype='float') i0_qua = math.trunc(np.random.sample()*4.) i1_qua = math.trunc(np.random.sample()*4.) while i0_qua == i1_qua: i1_qua = math.trunc(np.random.sample()*4.) _qua = np.int(w_size/4) qua = np.array([[_qua,_qua],[_qua,_qua*3],[_qua*3,_qua*3],[_qua*3,_qua]]) p = np.random.sample() - 0.5 r = qua[i0_qua,0] c = qua[i0_qua,1] r_radius = np.random.sample()*(radius_max-radius_min) + radius_min c_radius = np.random.sample()*(radius_max-radius_min) + radius_min rot = np.random.sample()*360 rr, cc = ellipse( r, c, r_radius, c_radius, rotation=np.deg2rad(rot), shape=img_l.shape ) p0 = np.rint(np.random.sample()*(radius_max-radius_min) + radius_min) p1 = qua[i1_qua,0] - (radius_max-radius_min) p2 = qua[i1_qua,1] - (radius_max-radius_min) p3 = np.rint(np.random.sample()*radius_min) p4 = np.rint(np.random.sample()*radius_min) p5 = np.rint(np.random.sample()*radius_min) p6 = np.rint(np.random.sample()*radius_min) p7 = np.rint(np.random.sample()*radius_min) p8 = np.rint(np.random.sample()*radius_min) poly = np.array(( (p1, p2), (p1+p3, p2+p4+p0), (p1+p5+p0, p2+p6+p0), (p1+p7+p0, p2+p8), (p1, p2), )) rr_p, cc_p = polygon(poly[:, 0], poly[:, 1], img_l.shape) if p > 0: img[:,:,:1] = img_l.copy() img[rr, cc,:1] = img_h[rr, cc] img[rr_p, cc_p,:1] = img_h[rr_p, cc_p] else: img[:,:,:1] = img_h.copy() img[rr, cc,:1] = img_l[rr, cc] img[rr_p, cc_p,:1] = img_l[rr_p, cc_p] img[:,:,1] = 0. img[rr_p, cc_p,1] = 1. return img 

Creemos una secuencia de entrenamiento de pares, ver al azar 10. Permítanme recordarles que las imágenes son monocromas, en escala de grises.

 _txy = [next_pair() for idx in range(train_num)] f_imgs = np.array(_txy)[:,:,:,:1].reshape(-1,w_size ,w_size ,1) f_msks = np.array(_txy)[:,:,:,1:].reshape(-1,w_size ,w_size ,1) del(_txy) #    10   fig, axes = plt.subplots(2, 10, figsize=(20, 5)) for k in range(10): kk = np.random.randint(train_num) axes[0,k].set_axis_off() axes[0,k].imshow(f_imgs[kk]) axes[1,k].set_axis_off() axes[1,k].imshow(f_msks[kk].squeeze()) 



Primer paso Entrenamos en el set inicial mínimo


El primer paso de nuestro experimento es simple, estamos tratando de entrenar a la red para predecir solo 11 primeras imágenes.

 batch_size = 10 val_len = 11 precision = 0.85 m0_select = np.zeros((f_imgs.shape[0]), dtype='int') for k in range(val_len): m0_select[k] = 1 t = tqdm() while True: fit = model.fit(f_imgs[m0_select>0], f_msks[m0_select>0], batch_size=batch_size, epochs=1, verbose=0 ) current_accu = fit.history['my_iou_metric'][0] current_loss = fit.history['loss'][0] t.set_description("accuracy {0:6.4f} loss {1:6.4f} ".\ format(current_accu, current_loss)) t.update(1) if current_accu > precision: break t.close() 

accuracy 0.8545 loss 0.0674 lenght 11 : : 793it [00:58, 14.79it/s]

Seleccionamos los primeros 11 de la secuencia inicial y capacitamos a la red en ellos. Ahora no importa si la red memoriza estas imágenes específicamente o resume, lo principal es que puede reconocer estas 11 imágenes de la manera que necesitamos. Dependiendo del conjunto de datos y la precisión seleccionados, la capacitación en red puede durar mucho, mucho tiempo. Pero solo tenemos unas pocas iteraciones. Repito que ahora no es importante para nosotros cómo y qué aprendió o aprendió la red, lo principal es que ha alcanzado la precisión establecida de predicción.

Ahora comienza el experimento principal


Construiremos la hoja de trucos, construiremos dichas hojas de trucos por separado para las tres secuencias de entrenamiento y compararemos su longitud. Tomaremos nuevos pares de imagen / máscara de la secuencia construida e intentaremos predecirlos por la red entrenada en la secuencia ya seleccionada. Al principio, son solo 11 pares de imagen / máscara y la red está entrenada, quizás no muy correctamente. Si en un nuevo par se predice la máscara de la imagen con una precisión aceptable, entonces descartamos este par, no tiene información nueva para la red, ya lo sabe y puede calcular la máscara a partir de esta imagen. Si la precisión de la predicción es insuficiente, entonces agregamos esta imagen con una máscara a nuestra secuencia y comenzamos a entrenar la red hasta que se obtenga un resultado de precisión aceptable en la secuencia seleccionada. Es decir Esta imagen contiene información nueva y la agregamos a nuestra secuencia de entrenamiento y extraemos la información contenida en ella mediante entrenamiento.

 batch_size = 50 t_batch_size = 1024 raw_len = val_len t = tqdm(-1) id_train = 0 #id_select = 1 while True: t.set_description("Accuracy {0:6.4f} loss {1:6.4f}\ selected img {2:5d} tested img {3:5d} ". format(current_accu, current_loss, val_len, raw_len)) t.update(1) if id_train == 1: fit = model.fit(f_imgs[m0_select>0], f_msks[m0_select>0], batch_size=batch_size, epochs=1, verbose=0 ) current_accu = fit.history['my_iou_metric'][0] current_loss = fit.history['loss'][0] if current_accu > precision: id_train = 0 else: t_pred = model.predict( f_imgs[raw_len: min(raw_len+t_batch_size,f_imgs.shape[0])], batch_size=batch_size ) for kk in range(t_pred.shape[0]): val_iou = get_iou_vector( f_msks[raw_len+kk].reshape(1,w_size,w_size,1), t_pred[kk].reshape(1,w_size,w_size,1) > 0.5) if val_iou < precision*0.95: new_img_test = 1 m0_select[raw_len+kk] = 1 val_len += 1 break raw_len += (kk+1) id_train = 1 if raw_len >= train_num: break t.close() 

 Accuracy 0.9338 loss 0.0266 selected img 1007 tested img 9985 : : 4291it [49:52, 1.73s/it] 

Aquí la precisión se usa en el sentido de "precisión", y no como la métrica estándar de keras, y la subrutina "my_iou_metric" se usa para calcular la precisión.

Ahora compare el funcionamiento de la misma red con los mismos parámetros en una secuencia diferente, en triángulos



Y obtenemos un resultado completamente diferente

 Accuracy 0.9823 loss 0.0108 selected img 1913 tested img 9995 : : 6343it [2:11:36, 3.03s/it] 

La red seleccionó imágenes de 1913 con información "nueva", es decir ¡El contenido de las imágenes con triángulos es la mitad que con los cuadrángulos!

Verifiquemos lo mismo en las estrellas y ejecutemos la red en la tercera secuencia



tenemos

 Accuracy 0.8985 loss 0.0478 selected img 476 tested img 9985 : : 2188it [16:13, 1.16it/s] 

Como puede ver, las estrellas resultaron ser las más informativas, solo 476 imágenes en una hoja de trucos.

Teníamos razones para juzgar la complejidad de las formas geométricas para la percepción por su red neuronal. La más simple es la estrella, con solo 476 imágenes en la hoja de trucos, luego el cuadrángulo con su 1007 y el más complejo resultó ser un triángulo: para el entrenamiento necesitas 1913 imágenes.

Tenga en cuenta que esto es para nosotros, para las personas es una imagen, pero para la red es un curso de lectura sobre reconocimiento y el curso sobre triángulos resultó ser el más difícil.

Ahora sobre lo serio


A primera vista, todas estas elipses y triángulos parecen mimos, tortas de arena y lego. Pero aquí hay una pregunta específica y seria: si aplicamos algún tipo de preprocesamiento, filtro a la secuencia inicial, ¿cómo cambiará la complejidad de la secuencia? Por ejemplo, tomamos las mismas elipses y cuadrángulos y les aplicamos dicho preprocesamiento

 from scipy.ndimage import gaussian_filter _tmp = [gaussian_filter(idx, sigma = 1) for idx in f_imgs] f1_imgs = np.array(_tmp)[:,:,:,:1].reshape(-1,w_size ,w_size ,1) del(_tmp) fig, axes = plt.subplots(2, 5, figsize=(20, 7)) for k in range(5): kk = np.random.randint(train_num) axes[0,k].set_axis_off() axes[0,k].imshow(f1_imgs[kk].squeeze(), cmap="gray") axes[1,k].set_axis_off() axes[1,k].imshow(f_msks[kk].squeeze(), cmap="gray") 



A primera vista, todo es lo mismo, las mismas elipses, los mismos polígonos, pero la red comenzó a funcionar de una manera completamente diferente:

 Accuracy 1.0575 loss 0.0011 selected img 7963 tested img 9999 : : 17765it [29:02:00, 12.40s/it] 

Aquí se necesita una pequeña explicación, no utilizamos el aumento, porque La forma del polígono y la forma de la elipse se seleccionan inicialmente al azar. Por lo tanto, el aumento no dará nueva información y no tiene sentido en este caso.

Pero, como se puede ver en el resultado del trabajo, un simple gaussian_filter creó muchos problemas para la red, generó mucha información nueva y probablemente superflua.

Bueno, para los amantes de la simplicidad en su forma más pura, tomamos las mismas elipses con polígonos, pero sin ninguna aleatoriedad en el color.



El resultado sugiere que el color aleatorio no es una simple adición.

 Accuracy 0.9004 loss 0.0315 selected img 251 tested img 9832 : : 1000it [06:46, 1.33it/s] 

La red valió completamente la información extraída de 251 imágenes, casi cuatro veces menos que la de muchas imágenes pintadas con ruido.

El propósito del artículo es mostrar algunas herramientas y ejemplos de su trabajo en ejemplos frívolos, el lego en el sandbox. Tenemos una herramienta para comparar dos secuencias de entrenamiento, podemos evaluar cuánto complica nuestro preprocesamiento o simplifica la secuencia de entrenamiento, cómo esta o aquella primitiva en la secuencia de entrenamiento es fácil de detectar.

La posibilidad de aplicar este ejemplo de Lego en casos reales es obvia, pero los entrenamientos reales y las redes de lectores dependen de los propios lectores.

Source: https://habr.com/ru/post/439122/


All Articles