¿Cómo se calcula la pérdida total en múltiples clases en Keras?

Digamos que tengo red con los siguientes parámetros:

  1. Red totalmente convolucional para la segmentación semántica.
  2. pérdida = entropía cruzada binaria ponderada (pero podría ser cualquier función de pérdida, no importa)
  3. 5 clases: las entradas son imágenes y las verdades básicas son máscaras binarias
  4. Tamaño de lote = 16

Ahora, sé que la pérdida se calcula de la siguiente manera: la entropía cruzada binaria se aplica a cada píxel en la imagen con respecto a cada clase. Así que esencialmente, cada píxel tendrá 5 valores de pérdida.

¿Qué pasa después de este paso?

Cuando entreno mi red, imprime solo un único valor de pérdida para una época. Hay muchos niveles de acumulación de pérdidas que deben producirse para producir un solo valor y la forma en que ocurre no está clara en absoluto en los documentos / códigos.

  1. Lo que se combina primero: (1) los valores de pérdida de la clase (por ejemplo, 5 valores (uno para cada clase) se combinan por píxel) y luego todos los píxeles de la imagen o (2) todos los píxeles de la imagen para cada uno clase individual, entonces todas las perdidas de clase se combinan?
  2. ¿Cómo están sucediendo exactamente estas diferentes combinaciones de píxeles? ¿Dónde se está sumndo / dónde se está promediando?
  3. Los promedios binarios_crossentropía de Keras sobre el axis=-1 . Entonces, ¿esto es un promedio de todos los píxeles por clase o promedio de todas las clases o son ambas?

Para expresslo de una manera diferente: ¿cómo se combinan las pérdidas de diferentes clases para producir un solo valor de pérdida para una imagen?

Esto no se explica en los documentos en absoluto y sería muy útil para las personas que realizan predicciones de varias clases en keras, independientemente del tipo de red. Aquí está el enlace al inicio del código keras en el que uno pasa primero en la función de pérdida.

Lo más cercano que pude encontrar a una explicación es

pérdida: cadena (nombre de la función objective) o función objective. Ver pérdidas. Si el modelo tiene múltiples salidas, puede usar una pérdida diferente en cada salida al pasar un diccionario o una lista de pérdidas. El valor de pérdida que será minimizado por el modelo será la sum de todas las pérdidas individuales

de keras . Entonces, ¿esto significa que las pérdidas para cada clase en la imagen simplemente se sumn?

Código de ejemplo aquí para que alguien lo pruebe. Aquí hay una implementación básica tomada de Kaggle y modificada para la predicción de tags múltiples:

 # Build U-Net model num_classes = 5 IMG_DIM = 256 IMG_CHAN = 3 weights = {0: 1, 1: 1, 2: 1, 3: 1, 4: 1000} #chose an extreme value just to check for any reaction inputs = Input((IMG_DIM, IMG_DIM, IMG_CHAN)) s = Lambda(lambda x: x / 255) (inputs) c1 = Conv2D(8, (3, 3), activation='relu', padding='same') (s) c1 = Conv2D(8, (3, 3), activation='relu', padding='same') (c1) p1 = MaxPooling2D((2, 2)) (c1) c2 = Conv2D(16, (3, 3), activation='relu', padding='same') (p1) c2 = Conv2D(16, (3, 3), activation='relu', padding='same') (c2) p2 = MaxPooling2D((2, 2)) (c2) c3 = Conv2D(32, (3, 3), activation='relu', padding='same') (p2) c3 = Conv2D(32, (3, 3), activation='relu', padding='same') (c3) p3 = MaxPooling2D((2, 2)) (c3) c4 = Conv2D(64, (3, 3), activation='relu', padding='same') (p3) c4 = Conv2D(64, (3, 3), activation='relu', padding='same') (c4) p4 = MaxPooling2D(pool_size=(2, 2)) (c4) c5 = Conv2D(128, (3, 3), activation='relu', padding='same') (p4) c5 = Conv2D(128, (3, 3), activation='relu', padding='same') (c5) u6 = Conv2DTranspose(64, (2, 2), strides=(2, 2), padding='same') (c5) u6 = concatenate([u6, c4]) c6 = Conv2D(64, (3, 3), activation='relu', padding='same') (u6) c6 = Conv2D(64, (3, 3), activation='relu', padding='same') (c6) u7 = Conv2DTranspose(32, (2, 2), strides=(2, 2), padding='same') (c6) u7 = concatenate([u7, c3]) c7 = Conv2D(32, (3, 3), activation='relu', padding='same') (u7) c7 = Conv2D(32, (3, 3), activation='relu', padding='same') (c7) u8 = Conv2DTranspose(16, (2, 2), strides=(2, 2), padding='same') (c7) u8 = concatenate([u8, c2]) c8 = Conv2D(16, (3, 3), activation='relu', padding='same') (u8) c8 = Conv2D(16, (3, 3), activation='relu', padding='same') (c8) u9 = Conv2DTranspose(8, (2, 2), strides=(2, 2), padding='same') (c8) u9 = concatenate([u9, c1], axis=3) c9 = Conv2D(8, (3, 3), activation='relu', padding='same') (u9) c9 = Conv2D(8, (3, 3), activation='relu', padding='same') (c9) outputs = Conv2D(num_classes, (1, 1), activation='sigmoid') (c9) model = Model(inputs=[inputs], outputs=[outputs]) model.compile(optimizer='adam', loss=weighted_loss(weights), metrics=[mean_iou]) def weighted_loss(weightsList): def lossFunc(true, pred): axis = -1 #if channels last #axis= 1 #if channels first classSelectors = K.argmax(true, axis=axis) classSelectors = [K.equal(tf.cast(i, tf.int64), tf.cast(classSelectors, tf.int64)) for i in range(len(weightsList))] classSelectors = [K.cast(x, K.floatx()) for x in classSelectors] weights = [sel * w for sel,w in zip(classSelectors, weightsList)] weightMultiplier = weights[0] for i in range(1, len(weights)): weightMultiplier = weightMultiplier + weights[i] loss = BCE_loss(true, pred) - (1+dice_coef(true, pred)) loss = loss * weightMultiplier return loss return lossFunc model.summary() 

La función de pérdida real de BCE-DICE se puede encontrar aquí.

Motivación para la pregunta: Según el código anterior, la pérdida total de validación de la red después de 20 épocas es de ~ 1%; sin embargo, la intersección media sobre los puntajes de unión para las primeras 4 clases es superior al 95% cada una, pero para la última clase es del 23%. Indica claramente que a la 5ª clase no le está yendo nada bien. Sin embargo, esta pérdida de precisión no se refleja en absoluto en la pérdida. Por lo tanto, eso significa que las pérdidas individuales para la muestra se están combinando de una manera que niega completamente la enorme pérdida que vemos para la 5ta clase. Y, entonces, cuando las pérdidas por muestra se combinan en un lote, todavía es muy bajo. No estoy seguro de cómo conciliar esta información.

Aunque ya mencioné parte de esta respuesta en una respuesta relacionada , pero inspeccionemos el código fuente paso a paso con más detalles para encontrar la respuesta concreta.

Primero, avancemos (!): Hay una llamada a la función weighted_loss que toma y_true , y_pred , sample_weight y mask como entradas:

 weighted_loss = weighted_losses[i] # ... output_loss = weighted_loss(y_true, y_pred, sample_weight, mask) 

weighted_loss es en realidad un elemento de una lista que contiene todas las funciones de pérdida (aumentadas) que se pasan al método de fit :

 weighted_losses = [ weighted_masked_objective(fn) for fn in loss_functions] 

La palabra “aumentada” que mencioné es importante aquí. Esto se debe a que, como puede ver arriba, la función de pérdida real está envuelta por otra función llamada weighted_masked_objective que se ha definido de la siguiente manera:

 def weighted_masked_objective(fn): """Adds support for masking and sample-weighting to an objective function. It transforms an objective function `fn(y_true, y_pred)` into a sample-weighted, cost-masked objective function `fn(y_true, y_pred, weights, mask)`. # Arguments fn: The objective function to wrap, with signature `fn(y_true, y_pred)`. # Returns A function with signature `fn(y_true, y_pred, weights, mask)`. """ if fn is None: return None def weighted(y_true, y_pred, weights, mask=None): """Wrapper function. # Arguments y_true: `y_true` argument of `fn`. y_pred: `y_pred` argument of `fn`. weights: Weights tensor. mask: Mask tensor. # Returns Scalar tensor. """ # score_array has ndim >= 2 score_array = fn(y_true, y_pred) if mask is not None: # Cast the mask to floatX to avoid float64 upcasting in Theano mask = K.cast(mask, K.floatx()) # mask should have the same shape as score_array score_array *= mask # the loss per batch should be proportional # to the number of unmasked samples. score_array /= K.mean(mask) # apply sample weighting if weights is not None: # reduce score_array to same ndim as weight array ndim = K.ndim(score_array) weight_ndim = K.ndim(weights) score_array = K.mean(score_array, axis=list(range(weight_ndim, ndim))) score_array *= weights score_array /= K.mean(K.cast(K.not_equal(weights, 0), K.floatx())) return K.mean(score_array) return weighted 

Entonces, hay una función anidada, weighted , que en realidad llama a la función de pérdida real fn en la línea score_array = fn(y_true, y_pred) . Ahora, para ser concretos, en el caso del ejemplo proporcionado por el OP, el fn (es decir, la función de pérdida) es binary_crossentropy . Por lo tanto, necesitamos echar un vistazo a la definición de binary_crossentropy() en Keras:

 def binary_crossentropy(y_true, y_pred): return K.mean(K.binary_crossentropy(y_true, y_pred), axis=-1) 

que a su vez, llama a la función de backend K.binary_crossentropy() . En caso de utilizar Tensorflow como backend, la definición de K.binary_crossentropy() es la siguiente:

 def binary_crossentropy(target, output, from_logits=False): """Binary crossentropy between an output tensor and a target tensor. # Arguments target: A tensor with the same shape as `output`. output: A tensor. from_logits: Whether `output` is expected to be a logits tensor. By default, we consider that `output` encodes a probability distribution. # Returns A tensor. """ # Note: tf.nn.sigmoid_cross_entropy_with_logits # expects logits, Keras expects probabilities. if not from_logits: # transform back to logits _epsilon = _to_tensor(epsilon(), output.dtype.base_dtype) output = tf.clip_by_value(output, _epsilon, 1 - _epsilon) output = tf.log(output / (1 - output)) return tf.nn.sigmoid_cross_entropy_with_logits(labels=target, logits=output) 

El tf.nn.sigmoid_cross_entropy_with_logits devuelve:

Un tensor de la misma forma que los logits con las pérdidas logísticas por componentes.

Ahora, volvamos a propagar (!): Considerando la nota anterior, la forma de salida de K.binray_crossentropy sería la misma que y_pred (o y_true ). Como mencionó el OP, y_true tiene una forma de (batch_size, img_dim, img_dim, num_classes) . Por lo tanto, K.mean(..., axis=-1) se aplica sobre un tensor de forma (batch_size, img_dim, img_dim, num_classes) que da como resultado un tensor de salida de forma (batch_size, img_dim, img_dim) . Por lo tanto, los valores de pérdida de todas las clases se promedian para cada píxel en la imagen. Por lo tanto, la forma de score_array en la función weighted mencionada anteriormente sería (batch_size, img_dim, img_dim) . Hay un paso más: la statement de retorno en la función weighted toma la media nuevamente, es decir, return K.mean(score_array) . Entonces, ¿cómo se calcula la media? Si echa un vistazo a la definición de la función backend mean , descubrirá que el argumento del axis es None por defecto:

 def mean(x, axis=None, keepdims=False): """Mean of a tensor, alongside the specified axis. # Arguments x: A tensor or variable. axis: A list of integer. Axes to compute the mean. keepdims: A boolean, whether to keep the dimensions or not. If `keepdims` is `False`, the rank of the tensor is reduced by 1 for each entry in `axis`. If `keepdims` is `True`, the reduced dimensions are retained with length 1. # Returns A tensor with the mean of elements of `x`. """ if x.dtype.base_dtype == tf.bool: x = tf.cast(x, floatx()) return tf.reduce_mean(x, axis, keepdims) 

Y llama al tf.reduce_mean() que, dado un argumento axis=None , toma la media sobre todos los ejes del tensor de entrada y devuelve un solo valor. Por lo tanto, se calcula la media de todo el tensor de forma (batch_size, img_dim, img_dim) , que se traduce en tomar el promedio de todas las tags en el lote y en todos sus píxeles, y se devuelve como un único valor escalar que representa el pérdida de valor. Luego, este valor de pérdida es informado por Keras y se utiliza para la optimización.


Bonificación: ¿qué sucede si nuestro modelo tiene múltiples capas de salida y, por lo tanto, se utilizan múltiples funciones de pérdida?

Recuerda el primer código que mencioné en esta respuesta:

 weighted_loss = weighted_losses[i] # ... output_loss = weighted_loss(y_true, y_pred, sample_weight, mask) 

Como puede ver, hay una variable i que se utiliza para indexar la matriz. Es posible que haya adivinado correctamente: en realidad es parte de un bucle que calcula el valor de pérdida para cada capa de salida utilizando su función de pérdida designada y luego toma la sum (ponderada) de todos estos valores de pérdida para calcular la pérdida total :

 # Compute total loss. total_loss = None with K.name_scope('loss'): for i in range(len(self.outputs)): if i in skip_target_indices: continue y_true = self.targets[i] y_pred = self.outputs[i] weighted_loss = weighted_losses[i] sample_weight = sample_weights[i] mask = masks[i] loss_weight = loss_weights_list[i] with K.name_scope(self.output_names[i] + '_loss'): output_loss = weighted_loss(y_true, y_pred, sample_weight, mask) if len(self.outputs) > 1: self.metrics_tensors.append(output_loss) self.metrics_names.append(self.output_names[i] + '_loss') if total_loss is None: total_loss = loss_weight * output_loss else: total_loss += loss_weight * output_loss if total_loss is None: if not self.losses: raise ValueError('The model cannot be compiled ' 'because it has no loss to optimize.') else: total_loss = 0. # Add regularization penalties # and other layer-specific losses. for loss_tensor in self.losses: total_loss += loss_tensor 
 1) What gets combined first - (1) the loss values of the class(for instance 10 values(one for each class) get combined per pixel) and 

¿Entonces todos los píxeles en la imagen o (2) todos los píxeles en la imagen para cada clase individual, entonces todas las pérdidas de la clase se combinan? 2) ¿Cómo están sucediendo exactamente estas diferentes combinaciones de píxeles? ¿Dónde se está sumndo / dónde se promedia?

Mi respuesta para (1): cuando se entrena un lote de imágenes, se entrena una matriz que consta de valores de píxeles calculando la función no lineal, la pérdida y la optimización (actualizando los pesos). La pérdida no se calcula para cada valor de píxel; más bien, se hace para cada imagen .

Los valores de píxel (X_train), pesos y sesgo (b) se utilizan en un sigmoide (para el ejemplo más simple de no linealidad) para calcular el valor de y predicho. Esto, junto con el y_train (un lote a la vez) se usa para calcular la pérdida, que se optimiza utilizando uno de los métodos de optimización como SGD, momentum, Adam, etc. para actualizar los pesos y sesgos.

Mi respuesta para (2): Durante la operación de no linealidad, los valores de píxeles (X_train) se combinan con los pesos (a través de un producto de puntos) y se agregan al sesgo para formar un valor objective predicho.

En un lote, puede haber ejemplos de entrenamiento pertenecientes a diferentes clases. Los valores objective correspondientes (para cada clase) se comparan con los valores pronosticados correspondientes para calcular la pérdida. Estos son, por lo tanto, está perfectamente bien para sumr todas las pérdidas.

Realmente no importa si pertenecen a una clase o varias clases, siempre y cuando lo compares con un objective correspondiente de la clase correcta. ¿Tener sentido?