ND Convolución Backprogation

8

Para mi educación, estoy tratando de implementar una capa convolucional N-dimensional en una red neuronal convolucional.

Me gustaría implementar una función de retropropagación. Sin embargo, no estoy seguro de la forma más eficiente de hacerlo.

Actualmente, estoy usando signal.fftconvolvepara:

  • En el paso hacia adelante, convolucione el filtro y el kernel hacia adelante sobre todos los filtros;

  • En el paso Backpropagation, convolucione los derivados (invertidos en todas las dimensiones con la función FlipAllAxes) con la matriz ( https://jefkine.com/general/2016/09/05/backpropagation-in-convolutional-neural-networks/ ) sobre todos los filtros y sumarlos. La salida que considero es la suma de cada imagen convolucionada con cada derivada para cada filtro.

Estoy particularmente confundido acerca de cómo convolucionar los derivados . El uso de la clase a continuación para propagar hacia atrás da como resultado una explosión en el tamaño de los pesos.

¿Cuál es la forma correcta de programar la convolución de la derivada con la salida y los filtros?

EDITAR:

De acuerdo con este documento ( Capacitación rápida de redes convolucionales a través de FFT ), que busca hacer exactamente lo que deseo hacer:

  • Las derivadas para la capa anterior están dadas por la convolución de las derivadas de la capa actual con los pesos:

    dL / dy_f = dL / dx * w_f ^ T

  • La derivada para los pesos es la suma por partes de la convolución de las derivadas con la entrada original:

    dL / dy = dL / dx * x

He implementado, lo mejor que sé cómo, esto a continuación. Sin embargo, esto no parece dar el resultado deseado, ya que la red que he escrito usando esta capa exhibe fluctuaciones salvajes durante el entrenamiento.

    import numpy as np
    from scipy import signal

    class ConvNDLayer:
        def __init__(self,channels, kernel_size, dim):

            self.channels = channels
            self.kernel_size = kernel_size;
            self.dim = dim

            self.last_input = None

            self.filt_dims = np.ones(dim+1).astype(int)
            self.filt_dims[1:] =  self.filt_dims[1:]*kernel_size
            self.filt_dims[0]= self.filt_dims[0]*channels 
            self.filters = np.random.randn(*self.filt_dims)/(kernel_size)**dim


        def FlipAllAxes(self, array):

            sl = slice(None,None,-1)
            return array[tuple([sl]*array.ndim)] 

        def ViewAsWindows(self, array, window_shape, step=1):
             # -- basic checks on arguments
             if not isinstance(array, cp.ndarray):
                 raise TypeError("`array` must be a Cupy ndarray")
             ndim = array.ndim
             if isinstance(window_shape, numbers.Number):
                  window_shape = (window_shape,) * ndim
             if not (len(window_shape) == ndim):
                   raise ValueError("`window_shape` is incompatible with `arr_in.shape`")

             if isinstance(step, numbers.Number):
                  if step < 1:
                  raise ValueError("`step` must be >= 1")
                  step = (step,) * ndim
             if len(step) != ndim:
                   raise ValueError("`step` is incompatible with `arr_in.shape`")

              arr_shape = array.shape
              window_shape = np.asarray(window_shape, dtype=arr_shape.dtype))

              if ((arr_shape - window_shape) < 0).any():
                   raise ValueError("`window_shape` is too large")

              if ((window_shape - 1) < 0).any():
                    raise ValueError("`window_shape` is too small")

               # -- build rolling window view
                    slices = tuple(slice(None, None, st) for st in step)
                    window_strides = array.strides
                    indexing_strides = array[slices].strides
                    win_indices_shape = (((array.shape -window_shape)
                    // step) + 1)

                 new_shape = tuple(list(win_indices_shape) + list(window_shape))
                 strides = tuple(list(indexing_strides) + list(window_strides))

                  arr_out = as_strided(array, shape=new_shape, strides=strides)

                  return arr_out

        def UnrollAxis(self, array, axis):
             # This so it works with a single dimension or a sequence of them
             axis = cp.asnumpy(cp.atleast_1d(axis))
             axis2 = cp.asnumpy(range(len(axis)))

             # Put unrolled axes at the beginning
             array = cp.moveaxis(array, axis,axis2)
             # Unroll
             return array.reshape((-1,) + array.shape[len(axis):])

        def Forward(self, array):

             output_shape =cp.zeros(array.ndim + 1)    
             output_shape[1:] =  cp.asarray(array.shape)
             output_shape[0]= self.channels 
             output_shape = output_shape.astype(int)
             output = cp.zeros(cp.asnumpy(output_shape))

             self.last_input = array

             for i, kernel in enumerate(self.filters):
                    conv = self.Convolve(array, kernel)
                    output[i] = conv

             return output


        def Backprop(self, d_L_d_out, learn_rate):

            d_A= cp.zeros_like(self.last_input)
            d_W = cp.zeros_like(self.filters)


           for i, (kernel, d_L_d_out_f) in enumerate(zip(self.filters, d_L_d_out)):

                d_A += signal.fftconvolve(d_L_d_out_f, kernel.T, "same")
                conv = signal.fftconvolve(d_L_d_out_f, self.last_input, "same")
                conv = self.ViewAsWindows(conv, kernel.shape)
                axes = np.arange(kernel.ndim)
                conv = self.UnrollAxis(conv, axes)  
                d_W[i] = np.sum(conv, axis=0)


           output = d_A*learn_rate
           self.filters =  self.filters - d_W*learn_rate
           return output
Jack Rolph
fuente

Respuestas:

0

Multiplicar gradientes con learn_rate no suele ser suficiente.

Para un mejor rendimiento y reducir las grandes fluctuaciones, los gradientes se escalan utilizando optimizadores por métodos como dividir entre los últimos gradientes (RMSprop).

Las actualizaciones también dependen del error, si pasa el error para cada muestra individualmente, eso generalmente crea ruido, por lo que se considera mejor promediar sobre múltiples muestras (mini lotes).

SajanGohil
fuente