Convergence issues in 2D RBF Neuron implemented as a Keras layer

Viewed 547

We implemented a 2D Gaussian radial basis layer (RBF) in Keras and are running into convergence issues with batch sizes larger than 1. The Neuron should implement the following function:

f(x,y)=exp(-a((x-x_0)²+(y-y_0)²)

Here x_0, y_0 and a are fit parameters.

Testcase

Currently we are doing correctness tests and are trying to fit just a single Neuron on the 2D function above. The Neuron should be (and is in case of batch_size 1) able to approximate this function exactly. The optimal loss is 0.

Problem

If we choose a batch size of 1 in this code, the prediction with Keras will converge very often and will be nearly independent of the starting parameters. If we increase the batch size, the fit might produce a random walk, freeze or not converge at all. In all of these cases (even batch_size 2) convergence is a lot worse than in the batch_size 1 case. If we choose the batch_size as the size of the trainingset (i.e. 1296, our desired batch size), the fit will freeze most of the time mostly independent of learning rate.

Code

We implemented this layer in the following code:

# 2D RBF Layer
# In case anybody wants to use this code afterwards:
# Licenses: Apache, MIT, BSD, LGPLv2 and v3 and Public Domain
# Input: x,y Pairs, shape: (2,)
# Output: exp(a* ((x-x_0)**2 + (y-y_0)**2)), shape: (1,)
# Parameters: x_0, y_0, a - called: mean_x, mean_y and opening in the following code:
# x and y should both lie in [0,1] - only [0,infinity] is enforced currently
class RBFLayer2D(Layer):
    def __init__(self,  **kwargs):
        super(RBFLayer2D, self).__init__(**kwargs)

    def build(self, input_shape):
        # Create a trainable weight variable for this layer.
        self.mean_x = K.variable(0.35)
        self.constraints[self.mean_x] = NonNeg()
        self.mean_y = K.variable(0.35)
        self.constraints[self.mean_y] = NonNeg()
        self.opening = K.variable(2.0)
        self.constraints[self.opening] = NonNeg()
        self.trainable_weights = [self.mean_x,self.mean_y,self.opening]
        super(RBFLayer2D, self).build(input_shape)  # Be sure to call this somewhere!

    def call(self, x):
        x_m = x[:,0] - self.mean_x
        y_m = x[:,1] - self.mean_y
        out = x_m*x_m + y_m*y_m
        outexp = 50.0*K.exp(-64.8*self.opening*out)
        # Output: exp(-a* ((x-x_0)**2 + (y-y_0)**2))
        return outexp

    def compute_output_shape(self, input_shape):
        # If Inputshape is (None, N) Outputshape is (None,N/2)
        # In our example we only look at (None, 2), which outputs (None,1)
        output_shape = (input_shape[0], input_shape[1]//2)
        return output_shape

Reproduction

To reproduce set a batch_size of 1 in the (not-so) minimal example after this section. When you run it, the code will display the target distribution (a circle in the lower left corner), the starting guess for our RBF ANN (a smaller circler in the middle) and then after each iteration the current guess (a circle getting bigger and moving to the lower left corner).
Afterwards set a batch_size of 12 and restart the code and you will not observe convergence anymore.

Minimal Example

from __future__ import print_function
from __future__ import division
import numpy as np
np.random.seed(1234)
import matplotlib.pyplot as plt
from keras.engine import Layer
from keras.optimizers import SGD
from keras.models import Sequential
from keras.constraints import NonNeg
from keras import backend as K

# 2D RBF Layer
# Input: x,y Pairs, shape: (2,)
# Output: exp(a* ((x-x_0)**2 + (y-y_0)**2)), shape: (1,)
# Parameters: x_0, y_0, a - called: mean_x, mean_y and opening in the following code:
# x and y should both lie in [0,1] - only [0,infinity] is enforced currently
class RBFLayer2D(Layer):
    def __init__(self,  **kwargs):
        super(RBFLayer2D, self).__init__(**kwargs)

    def build(self, input_shape):
        # Create a trainable weight variable for this layer.
        self.mean_x = K.variable(0.35)
        self.constraints[self.mean_x] = NonNeg()
        self.mean_y = K.variable(0.35)
        self.constraints[self.mean_y] = NonNeg()
        self.opening = K.variable(2.0)
        self.constraints[self.opening] = NonNeg()
        self.trainable_weights = [self.mean_x,self.mean_y,self.opening]
        super(RBFLayer2D, self).build(input_shape)

    def call(self, x):
        x_m = x[:,0] - self.mean_x
        y_m = x[:,1] - self.mean_y
        out = x_m*x_m + y_m*y_m
        outexp = 50.0*K.exp(-64.8*self.opening*out)
        # Output: exp(-a* ((x-x_0)**2 + (y-y_0)**2))
        return outexp

    def compute_output_shape(self, input_shape):
        # If Inputshape is (None, N) Outputshape is (None,N/2)
        # In our example we only look at (None, 2), which outputs (None,1)
        output_shape = (input_shape[0], input_shape[1]//2)
        return output_shape

# The function we want to train.
# It can be exactly represented using a single Neuron.
def twodenergy(phi, psi):
    r0 = np.array([-180, -180])
    b = 0.00005
    return 50.0 * np.exp(- b * ((phi - r0[0]) ** 2 + (psi - r0[1]) ** 2))

# One of two plotting helper functions to show the results
def make_plot(y,numsteps,numbins,minangle,maxangle,plotnum, batch_size):
    evaluation = np.zeros((numsteps, numsteps))
    for i in range(0, numbins):
        mx = i % numsteps
        my = int(i / numsteps)
        evaluation[mx,my]=y[i]

    plt.imshow(evaluation.T, origin='lower',extent=[minangle, maxangle, minangle, maxangle])
    plt.xlabel("x")
    plt.ylabel("y")

    if plotnum == 0:       
        plt.title("Startconfiguration")
    else:
        plt.title("RBF for batch_size %i at frame %03d" % (batch_size, plotnum))

    plt.show()

# One of two plotting helper functions to show the target function
def plot_target_function(phi, psi, minangle, maxangle, delta_angle_half, numbins, numsteps ):
    eval_matrix_corr = np.zeros((numsteps, numsteps))
    for i in range(0, numbins):
        mx = i % numsteps
        my = int(i / numsteps)
        ph = phi[mx] +delta_angle_half
        ps = psi[my] +delta_angle_half
        eval_matrix_corr[mx,my] = twodenergy(ph,ps)

    plt.imshow(eval_matrix_corr.T, origin='lower', extent=[minangle, maxangle, minangle, maxangle])
    plt.title("Target Function")
    plt.xlabel("phi")
    plt.ylabel("psi")
    plt.show()


if __name__ == "__main__":
    # batch_size ==    1:     converges very often nearly independent of input parameters
    # batch_size ==    2:     no to slow convergence, but distribution stays in the right place more or less
    # batch_size == 3-12:     random walk
    # batch_size == 1296:     no movement in case of low learning_rate, random_walk in case of high learning_rate
    #                         (this is the case where the whole map is evaluated in every step.
    #                         1296 is our desired testcase, because it evaluates the whole map we want to fit.
    batch_size = 1
    learning_rate = 1E-5

    ### Here we generate the target function ###
    ### f(phi,psi)
    ### phi is [-180,180]
    ### psi is [-180,180]
    anglestep = 10.0
    minangle = -180.0
    maxangle = 180.0
    numsteps = int((maxangle - minangle)/anglestep)
    anglerange = maxangle - minangle
    numbins = numsteps*numsteps
    delta_angle_half = anglerange /(2.0* numsteps)

    phi = np.arange(minangle, maxangle, anglestep)
    psi = np.arange(minangle, maxangle, anglestep)

    #Target Function Plot, Gaussian in lower left
    plot_target_function(phi, psi, minangle, maxangle, delta_angle_half, numbins, numsteps )


    # Input Parameter Regularization
    # we map -180..180 to 0..1
    # we also calculate the training parameters for our x,y pairs:
    x_train = np.zeros((numbins, 2))
    y_train = np.zeros((numbins, 1))
    for x,ph in enumerate(phi):
        for y,ps in enumerate(psi):
            myphi = (ph + delta_angle_half - minangle)/(anglerange)
            mypsi = (ps + delta_angle_half- minangle)/(anglerange)

            x_train[x * numsteps + y, 0] = (ph +delta_angle_half - minangle)/(anglerange)
            x_train[x * numsteps + y, 1] = (ps + delta_angle_half- minangle)/(anglerange)
            y_train[x * numsteps + y] = twodenergy(ph +delta_angle_half,ps +delta_angle_half)

    # Prediction with Keras
    model = Sequential()
    # Single RBF Layer, only one node
    model.add(RBFLayer2D(input_shape=(2,)))
    sgd = SGD(lr=learning_rate)
    model.compile(loss="mean_squared_error", optimizer=sgd)

    # We plot the starting configuration.
    y = model.predict(x_train, batch_size=batch_size)
    make_plot(y, numsteps, numbins, minangle, maxangle, 0, batch_size)

    #Plot the first 15 iterations:
    for i in range(0,15):
        # For demonstration purposes, we fit 1 epoch and plot the output.  
        model.fit(x_train,y_train, epochs=1, batch_size=batch_size)
        y = model.predict(x_train, batch_size=batch_size)
        make_plot(y, numsteps, numbins, minangle, maxangle, 1 + i, batch_size)
0 Answers
Related