To fit a model, we need a training set \({x_i,y_i}_{i=1}^{I}\) of input/output pairs. We seek parameters \(\boldsymbol\phi\) for the model \(f[x_i,\boldsymbol\phi]\) that map the inputs \(x_i\) to the outputs \(y_i\) as closely as possible.
To this end, we define a loss function \(L[\boldsymbol\phi]\) that returns a single number that quantifies the mismatch in this mapping. The goal of an optimization algorithm is to find parameters \(\hat{\boldsymbol\phi}\) that minimize the loss:
There are many families of optimization algorithms, but the standard methods for training neural networks are iterative. These algorithms initialize the parameters heuristically and then adjust them repeatedly in such a way that the loss decreases.The simplest method in this class is gradient descent.
This starts with initial parameters \(\boldsymbol\phi= [\phi_0,\phi_1,...,\phi_N]^T\) and iterates two steps:
Step 1. Compute the derivatives of the loss with respect to the parameters:
where the positive scalar \(\alpha\) determines the magnitude of the change.
The first step computes the gradient of the loss function at the current position. This determines the uphill direction of the loss function.
The second step moves a small distance \(\alpha\) downhill (hence the negative sign). The parameter \(\alpha\) may be fixed (in which case, we call it a learning rate), or we may perform a line search where we try several values of \(\alpha\) to find the one that most decreases the loss.
At the minimum of the loss function, the surface must be flat (or we could improve further by going downhill). Hence, the gradient will be zero, and the parameters will stop changing. In practice, we monitor the gradient magnitude and terminate the algorithm when it becomes too small.
Linear regression example
Consider applying gradient descent to the 1D linear regression model. The model \(f[x,\phi]\) maps a scalar input xto a scalar output yand has parameters \(ϕ= [\phi_0,\phi1_1]^T\), which represent the y-intercept and the slope:
\[\begin{eqnarray}
y &=& \mbox{f}[x,\boldsymbol\phi]\nonumber \\
&=& \phi_{0} + \phi_{1}x.
\end{eqnarray}\]
Given a dataset \(\{x_i,y_i\}_{i=1}^{I}\) containing \(I\) input/output pairs, we choose the least squares loss function:
The important difference is that the gradient for \(\phi_1\) contains an additional \(x_i\). This comes directly from differentiating \(-\phi_1x_i\) with respect to \(\phi_1\).
Finally, gradient descent updates both parameters in the direction opposite to their gradients:
Here, \(\eta\) is the learning rate, which determines how large a step we take during each gradient descent update.
Gradient descent for the linear regression model.
Training set of \(I = 12\) input/output pairs \(\{x_i, y_i\}\).
Loss function showing iterations of gradient descent. We start at point 0 and move in the steepest downhill direction until we can improve no further to arrive at point 1. We then repeat this procedure. We measure the gradient at point 1 and move downhill to point 2 and so on.
This can be visualized better as a heatmap, where the brightness represents the loss. After only four iterations, we are already close to the minimum.
The model with the parameters at point 0 (lightest line) describes the data very badly, but each successive iteration improves the fit. The model with the parameters at point 4 (darkest line) is already a reasonable description of the training data.
Figure above shows the progression of the gradient descent algorithm as we iteratively compute the derivatives according to equations above. In this case, we have used a line search procedure to find the value of \(\alpha\) that decreases the loss the most at each iteration.
Gabor model example
Loss functions for linear regression problems always have a single well-defined global minimum. More formally, they are convex. Convexity implies that wherever we initialize the parameters, we are bound to reach the minimum if we keep walking downhill; the training procedure can’t fail.
Unfortunately, loss functions for most nonlinear models, including both shallow and deep networks, are non-convex. Visualizing neural network loss functions is challenging due to the number of parameters. Hence, we first explore a simpler nonlinear model with two parameters to gain insight into the properties of non-convex loss functions:
This Gabor model maps scalar input \(x\) to scalar output \(y\) and consists of a sinusoidal component (creating an oscillatory function) multiplied by a negative exponential com- ponent (causing the amplitude to decrease as we move from the center).
It has two parameters \(\phi= [\phi_0,\phi_1]^T\), where \(\phi_0 \in \mathbb{R}\) determines the mean position of the function and \(\phi_1 \in \mathbb{R}^+\) stretches or squeezes it along the x-axis. Consider a training set of \(I\) examples \(\{x_i,y_i\}\)(figure 6.3). The least squares loss function for I training examples is defined as:
Once more, the goal is to find the parameters \(\hat{\phi}\)that minimize this loss.
Gabor model. This nonlinear model maps scalar input \(x\) to scalar output \(y\) and has parameters \(\phi = [\phi_0, \phi_1]^T\). It describes a sinusoidal function that decreases in amplitude with distance from its center. Parameter \(\phi_0 \in \mathbb{R}\) determines the position of the center. As \(\phi_0\) increases, the function moves left. Parameter \(\phi_1 \in \mathbb{R}^+\) squeezes the function along the x-axis relative to the center. As \(ϕ_1\) increases, the function narrows. a–c) Model with different parameters.
Training data for fitting the Gabor model.
The training dataset contains 28 input/output examples \(\{x_i, y_i\}\).
These data were created by uniformly sampling \(x_i \in [−15, 15]\), passing the samples through a Gabor model with parameters \(\phi = [0.0, 16.6]^T\) , and adding normally distributed noise.
Local minima and saddle points
Figure below depicts the loss function associated with the Gabor model for this dataset.
There are numerous local minima (red circles). Here the gradient is zero, and the loss increases if we move in any direction, but we are not at the overall minimum of the function.
The point with the lowest loss is known as the global minimum and is depicted by the gray circle. If we start in a random position and use gradient descent to go downhill, there is no guarantee that we will wind up at the global minimum and find the best parameters. It’s equally or even more likely that the algorithm will terminate in one of the local minima. Furthermore, there is no way of knowing whether there is a better solution elsewhere.
Loss function for the Gabor model. a) The loss function is non-convex, with multiple local minima (cyan circles) in addition to the global minimum (gray circle). It also contains saddle points where the gradient is locally zero, but the function increases in one direction and decreases in the other. The blue cross is an example of a saddle point; the function decreases as we move horizontally in either direction but increases as we move vertically. b–f) Models associated with the different minima. In each case, there is no small change that decreases the loss. Panel (c) shows the global minimum, which has a loss of 0.64. (Interactive figure)
In addition, the loss function contains saddle points (e.g., the beige cross in figure above). At saddle points, the gradient is zero, but the function increases in some directions and decreases in others. If the current parameters are not exactly at the saddle point, then gradient descent can escape by moving downhill. However, the surface near the saddle point is flat, so it’s hard to be sure that training hasn’t converged; if we terminate the algorithm when the gradient is small, we may erroneously stop near a saddle point.
# import librariesimport numpy as npimport matplotlib.pyplot as pltfrom matplotlib import cmfrom matplotlib.colors import ListedColormapplt.style.use('dark_background')# Let's create our training data of 30 pairs {x_i, y_i}# We'll try to fit the Gabor model to these datadata = np.array([[-1.920e+00,-1.422e+01,1.490e+00,-1.940e+00,-2.389e+00,-5.090e+00,-8.861e+00,3.578e+00,-6.010e+00,-6.995e+00,3.634e+00,8.743e-01,-1.096e+01,4.073e-01,-9.467e+00,8.560e+00,1.062e+01,-1.729e-01,1.040e+01,-1.261e+01,1.574e-01,-1.304e+01,-2.156e+00,-1.210e+01,-1.119e+01,2.902e+00,-8.220e+00,-1.179e+01,-8.391e+00,-4.505e+00], [-1.051e+00,-2.482e-02,8.896e-01,-4.943e-01,-9.371e-01,4.306e-01,9.577e-03,-7.944e-02 ,1.624e-01,-2.682e-01,-3.129e-01,8.303e-01,-2.365e-02,5.098e-01,-2.777e-01,3.367e-01,1.927e-01,-2.222e-01,6.352e-02,6.888e-03,3.224e-02,1.091e-02,-5.706e-01,-5.258e-02,-3.666e-02,1.709e-01,-4.805e-02,2.008e-01,-1.904e-01,5.952e-01]])# Let's define our modeldef model(phi,x): sin_component = np.sin(phi[0] +0.06* phi[1] * x) gauss_component = np.exp(-(phi[0] +0.06* phi[1] * x) * (phi[0] +0.06* phi[1] * x) /32) y_pred= sin_component * gauss_componentreturn y_pred# Draw modeldef draw_model(data,model,phi,title=None): x_model = np.arange(-15,15,0.1) y_model = model(phi,x_model) fix, ax = plt.subplots() ax.plot(data[0,:],data[1,:],'bo') ax.plot(x_model,y_model,'m-') ax.set_xlim([-15,15]);ax.set_ylim([-1,1]) ax.set_xlabel('x'); ax.set_ylabel('y')if title isnotNone: ax.set_title(title) plt.show()# Initialize the parameters and draw the modelphi = np.zeros((2,1))phi[0] =-5# Horizontal offsetphi[1] =25# Frequencydraw_model(data,model,phi, "Initial parameters")
def compute_loss(data_x, data_y, model, phi): y_pred = model(phi,data_x) loss = np.sum((y_pred - data_y)**2)return lossloss = compute_loss(data[0,:],data[1,:],model,np.array([[0.6],[-0.2]]))print('Your loss = %3.3f'%(loss))
# These came from writing out the expression for the sum of squares loss and taking the# derivative with respect to phi0 and phi1. It was a lot of hassle to get it right!def gabor_deriv_phi0(data_x,data_y,phi0, phi1): x =0.06* phi1 * data_x + phi0 y = data_y cos_component = np.cos(x) sin_component = np.sin(x) gauss_component = np.exp(-0.5* x *x /16) deriv = cos_component * gauss_component - sin_component * gauss_component * x /16 deriv =2* deriv * (sin_component * gauss_component - y)return np.sum(deriv)def gabor_deriv_phi1(data_x, data_y,phi0, phi1): x =0.06* phi1 * data_x + phi0 y = data_y cos_component = np.cos(x) sin_component = np.sin(x) gauss_component = np.exp(-0.5* x *x /16) deriv =0.06* data_x * cos_component * gauss_component -0.06* data_x*sin_component * gauss_component * x /16 deriv =2*deriv * (sin_component * gauss_component - y)return np.sum(deriv)def compute_gradient(data_x, data_y, phi): dl_dphi0 = gabor_deriv_phi0(data_x, data_y, phi[0],phi[1]) dl_dphi1 = gabor_deriv_phi1(data_x, data_y, phi[0],phi[1])# Return the gradientreturn np.array([[dl_dphi0],[dl_dphi1]])
We can check we got this right using a trick known as finite differences. If we evaluate the function and then change one of the parameters by a very small amount and normalize by that amount, we get an approximation to the gradient, so:
We can’t do this when there are many parameters; for a million parameters, we would have to evaluate the loss function two million times, and usually computing the gradients directly is much more efficient.
# Compute the gradient using your functiongradient = compute_gradient(data[0,:],data[1,:], phi)print("Your gradients: (%3.3f,%3.3f)"%(gradient[0],gradient[1]))# Approximate the gradients with finite differencesdelta =0.0001dl_dphi0_est = (compute_loss(data[0,:],data[1,:],model,phi+np.array([[delta],[0]])) -\ compute_loss(data[0,:],data[1,:],model,phi))/deltadl_dphi1_est = (compute_loss(data[0,:],data[1,:],model,phi+np.array([[0],[delta]])) -\ compute_loss(data[0,:],data[1,:],model,phi))/deltaprint("Approx gradients: (%3.3f,%3.3f)"%(dl_dphi0_est,dl_dphi1_est))
Your gradients: (3.344,0.519)
Approx gradients: (3.344,0.519)
Now we are ready to perform gradient descent. We’ll need to use our line search routine from Notebook 6.1, which I’ve reproduced here plus the helper function loss_function_1D that converts from a 2D problem to a 1D problem
def loss_function_1D(dist_prop, data, model, phi_start, gradient):# Return the loss after moving this farreturn compute_loss(data[0,:], data[1,:], model, phi_start+ gradient * dist_prop)def line_search(data, model, phi, gradient, thresh=.00001, max_dist =0.1, max_iter =15, verbose=False):# Initialize four points along the range we are going to search a =0 b =0.33* max_dist c =0.66* max_dist d =1.0* max_dist n_iter =0# While we haven't found the minimum closely enoughwhile np.abs(b-c) > thresh and n_iter < max_iter:# Increment iteration counter (just to prevent an infinite loop) n_iter = n_iter+1# Calculate all four points lossa = loss_function_1D(a, data, model, phi,gradient) lossb = loss_function_1D(b, data, model, phi,gradient) lossc = loss_function_1D(c, data, model, phi,gradient) lossd = loss_function_1D(d, data, model, phi,gradient)if verbose:print('Iter %d, a=%3.3f, b=%3.3f, c=%3.3f, d=%3.3f'%(n_iter, a,b,c,d))print('a %f, b%f, c%f, d%f'%(lossa,lossb,lossc,lossd))# Rule #1 If point A is less than points B, C, and D then change B,C,D so they are half their current distance from Aif np.argmin((lossa,lossb,lossc,lossd))==0: b = a+ (b-a)/2 c = a+ (c-a)/2 d = a+ (d-a)/2continue;# Rule #2 If point b is less than point c then# point d becomes point c, and# point b becomes 1/3 between a and new d# point c becomes 2/3 between a and new dif lossb < lossc: d = c b = a+ (d-a)/3 c = a+2*(d-a)/3continue# Rule #2 If point c is less than point b then# point a becomes point b, and# point b becomes 1/3 between new a and d# point c becomes 2/3 between new a and d a = b b = a+ (d-a)/3 c = a+2*(d-a)/3# Return average of two middle pointsreturn (b+c)/2.0def gradient_descent_step(phi, data, model):# Step 1: Compute the gradient gradient = compute_gradient(data[0,:],data[1,:], phi)# Step 2: Update the parameters -- note we want to search in the negative (downhill direction) alpha = line_search(data, model, phi, gradient*-1, max_dist =2.0) phi = phi - alpha * gradientreturn phi# Initialize the parametersn_steps =21phi_all = np.zeros((2,n_steps+1))phi_all[0,0] =-1.5phi_all[1,0] =8.5# Measure loss and draw initial modelloss = compute_loss(data[0,:], data[1,:], model, phi_all[:,0:1])draw_model(data,model,phi_all[:,0:1], "Initial parameters, Loss = %f"%(loss))for c_step inrange (n_steps):# Do gradient descent step phi_all[:,c_step+1:c_step+2] = gradient_descent_step(phi_all[:,c_step:c_step+1],data, model)# Measure loss and draw model every 4th stepif c_step %4==0: loss = compute_loss(data[0,:], data[1,:], model, phi_all[:,c_step+1:c_step+2]) draw_model(data,model,phi_all[:,c_step+1], "Iteration %d, loss = %f"%(c_step+1,loss))draw_loss_function(compute_loss, data, model,phi_all)