Thursday: Repetion and summary of Stochastic Gradient descent with examples and automatic differentiation and begin Neural Networks.
-
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model.
+
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model. Presentation of project 2.
Reading suggestions for both days: Aurelien Geron's chapter 10 and Hastie et al chapter 11.
For Stochastic Gradient Descent, we recommend chapter 4 of Geron's text.
@@ -251,7 +336,7 @@ For neural networks we recommend Goodfellow et al chapters 6 and 7 and Bishop 5.
A different variant of FFNNs are convolutional neural networks
-(CNNs), which have a connectivity pattern inspired by the animal
-visual cortex. Individual neurons in the visual cortex only respond to
-stimuli from small sub-regions of the visual field, called a receptive
-field. This makes the neurons well-suited to exploit the strong
-spatially local correlation present in natural images. The response of
-each neuron can be approximated mathematically as a convolution
-operation. (figure to come)
+
The four equations above provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm.
+
+
+
+
+
First, we set up the input data \( \boldsymbol{x} \) and the activations
+\( \boldsymbol{z}_1 \) of the input layer and compute the activation function and
+the pertinent outputs \( \boldsymbol{a}^1 \).
+
+
-
Convolutional neural networks emulate the behaviour of neurons in the
-visual cortex by enforcing a local connectivity pattern between
-nodes of adjacent layers: Each node in a convolutional layer is
-connected only to a subset of the nodes in the previous layer, in
-contrast to the fully-connected FFNN. Often, CNNs consist of several
-convolutional layers that learn local features of the input, with a
-fully-connected layer at the end, which gathers all the local data and
-produces the outputs. They have wide applications in image and video
-recognition.
+
+
+
+
+
Secondly, we perform then the feed forward till we reach the output
+layer and compute all \( \boldsymbol{z}_l \) of the input layer and compute the
+activation function and the pertinent outputs \( \boldsymbol{a}^l \) for
+\( l=2,3,\dots,L \).
+
+
+
+
+
+
+
+
+
Thereafter we compute the ouput error \( \boldsymbol{\delta}^L \) by computing all
Finally, we update the weights and the biases using gradient descent for each \( l=L-1,L-2,\dots,2 \) and update the weights and biases according to the rules
The parameter \( \eta \) is the learning parameter discussed in connection with the gradient descent methods.
+Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training.
Setting up a Multi-layer perceptron model for classification
-
So far we have only mentioned ANNs where information flows in one
-direction: forward. Recurrent neural networks on the other hand,
-have connections between nodes that form directed cycles. This
-creates a form of internal memory which are able to capture
-information on what has been calculated before; the output is
-dependent on the previous computations. Recurrent NNs make use of
-sequential information by performing the same task for every element
-in a sequence, where each element depends on previous elements. An
-example of such information is sentences, making recurrent NNs
-especially well-suited for handwriting and speech recognition.
+
We are now gong to develop an example based on the MNIST data
+base. This is a classification problem and we need to use our
+cross-entropy function we discussed in connection with logistic
+regression. The cross-entropy defines our cost function for the
+classificaton problems with neural networks.
+
In binary classification with two classes \( (0, 1) \) we define the
+logistic/sigmoid function as the probability that a particular input
+is in class \( 0 \) or \( 1 \). This is possible because the logistic
+function takes any input from the real numbers and inputs a number
+between 0 and 1, and can therefore be interpreted as a probability. It
+also has other nice properties, such as a derivative that is simple to
+calculate.
+
+
+
For an input \( \boldsymbol{a} \) from the hidden layer, the probability that the input \( \boldsymbol{x} \)
+is in class 0 or 1 is just. We let \( \theta \) represent the unknown weights and biases to be adjusted by our equations). The variable \( x \)
+represents our activation values \( z \). We have
+
There are many other kinds of ANNs that have been developed. One type
-that is specifically designed for interpolation in multidimensional
-space is the radial basis function (RBF) network. RBFs are typically
-made up of three layers: an input layer, a hidden layer with
-non-linear radial symmetric activation functions and a linear output
-layer (''linear'' here means that each node in the output layer has a
-linear activation function). The layers are normally fully-connected
-and there are no cycles, thus RBFs can be viewed as a type of
-fully-connected FFNN. They are however usually treated as a separate
-type of NN due the unusual activation functions.
+
Our cost function is given as (see the Logistic regression lectures)
This last equality means that we can interpret our cost function as a sum over the loss function
+for each point in the dataset \( \mathcal{L}_i(\boldsymbol{\theta}) \).
+The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather
+than maximizing a negative number.
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset (numbers from \( 0 \) to \( 9 \))..
+
+
If \( \boldsymbol{x}_i \) is the \( i \)-th input (image), \( y_{ic} \) refers to the \( c \)-th component of the \( i \)-th
+output vector \( \boldsymbol{y}_i \).
+The probability of \( \boldsymbol{x}_i \) being in class \( c \) will be given by the softmax function:
+
See the logistic regression lectures for a full definition of the cost function.
+
+
The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before!
+
@@ -357,7 +385,7 @@ type of NN due the unusual activation functions.
One uses often so-called fully-connected feed-forward neural networks
-with three or more layers (an input layer, one or more hidden layers
-and an output layer) consisting of neurons that have non-linear
-activation functions.
+
As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters \( \beta \) as
The parameters \( \boldsymbol{\beta} \) were defined using a minimization method like gradient descent or Newton-Raphson's method.
+
+
Now we replace \( x_i \) with the activation \( z_i^l \) for a given layer \( l \) and the outputs as \( y_i=a_i^l=f(z_i^l) \), with \( z_i^l \) now being a function of the weights \( w_{ij}^l \) and biases \( b_i^l \).
+We have then
where we have defined the targets \( t_i \). The derivatives of the cost function with respect to the output \( a_i^L \) are then easily calculated and we get
In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation \( z_i^l \), that is we need
According to the Universal approximation theorem, a feed-forward
-neural network with just a single hidden layer containing a finite
-number of neurons can approximate a continuous multidimensional
-function to arbitrary accuracy, assuming the activation function for
-the hidden layer is a non-constant, bounded and
-monotonically-increasing continuous function.
-
Note that the requirements on the activation function only applies to
-the hidden layer, the output nodes are always assumed to be linear, so
-as to not restrict the range of output values.
-
Let us first try to fit various gates using standard linear
-regression. The gates we are thinking of are the classical XOR, OR and
-AND gates, well-known elements in computer science. The tables here
-show how we can set up the inputs \( x_1 \) and \( x_2 \) in order to yield a
-specific target \( y_i \).
+
Here we will be using the MNIST dataset, which is readily available through the scikit-learn
+package. You may also find it for example here.
+The MNIST (Modified National Institute of Standards and Technology) database is a large database
+of handwritten digits that is commonly used for training various image processing systems.
+The MNIST dataset consists of 70 000 images of size \( 28\times 28 \) pixels, each labeled from 0 to 9.
+The scikit-learn dataset we will use consists of a selection of 1797 images of size \( 8\times 8 \) collected and processed from this database.
+
+
+
To feed data into a feed-forward neural network we need to represent
+the inputs as a design/feature matrix \( X = (n_{inputs}, n_{features}) \). Each
+row represents an input, in this case a handwritten digit, and
+each column represents a feature, in this case a pixel. The
+correct answers, also known as labels or targets are
+represented as a 1D array of integers
+\( Y = (n_{inputs}) = (5, 3, 1, 8,...) \).
+
+
+
As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from
+measurements of height (in m)
+and weight (in kg). If we have measurements of 5 people the design/feature matrix could be for example:
+
Since each input image is a 2D matrix, we need to flatten the image
+(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a
+design/feature matrix. This means we lose all spatial information in the
+image, such as locality and translational invariance. More complicated
+architectures such as Convolutional Neural Networks can take advantage
+of such information, and are most commonly applied when analyzing
+images.
@@ -334,36 +362,48 @@ specific target \( y_i \).
-
"""
-Simple code that tests XOR, OR and AND gates with linear regression
-"""
-
+
# import necessary packagesimportnumpyasnp
-# Design matrix
-X = np.array([ [1, 0, 0], [1, 0, 1], [1, 1, 0],[1, 1, 1]],dtype=np.float64)
-print(f"The X.TX matrix:{X.T @ X}")
-Xinv = np.linalg.pinv(X.T @ X)
-print(f"The invers of X.TX matrix:{Xinv}")
-
-# The XOR gate
-yXOR = np.array( [ 0, 1 ,1, 0])
-ThetaXOR = Xinv @ X.T @ yXOR
-print(f"The values of theta for the XOR gate:{ThetaXOR}")
-print(f"The linear regression prediction for the XOR gate:{X @ ThetaXOR}")
+importmatplotlib.pyplotasplt
+fromsklearnimport datasets
-# The OR gate
-yOR = np.array( [ 0, 1 ,1, 1])
-ThetaOR = Xinv @ X.T @ yOR
-print(f"The values of theta for the OR gate:{ThetaOR}")
-print(f"The linear regression prediction for the OR gate:{X @ ThetaOR}")
+# ensure the same random numbers appear every time
+np.random.seed(0)
+
+# display images in notebook
+%matplotlib inline
+plt.rcParams['figure.figsize'] = (12,12)
-# The OR gate
-yAND = np.array( [ 0, 0 ,0, 1])
-ThetaAND = Xinv @ X.T @ yAND
-print(f"The values of theta for the AND gate:{ThetaAND}")
-print(f"The linear regression prediction for the AND gate:{X @ ThetaAND}")
+# download MNIST dataset
+digits = datasets.load_digits()
+
+# define inputs and labels
+inputs = digits.images
+labels = digits.target
+
+print("inputs = (n_inputs, pixel_width, pixel_height) = "+str(inputs.shape))
+print("labels = (n_inputs) = "+str(labels.shape))
+
+
+# flatten the image
+# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
+n_inputs =len(inputs)
+inputs = inputs.reshape(n_inputs, -1)
+print("X = (n_inputs, n_features) = "+str(inputs.shape))
+
+
+# choose some random images to display
+indices = np.arange(n_inputs)
+random_indices = np.random.choice(indices, size=5)
+
+for i, image inenumerate(digits.images[random_indices]):
+ plt.subplot(1, 5, i+1)
+ plt.axis('off')
+ plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
+ plt.title("Label: %d"% digits.target[random_indices[i]])
+plt.show()
Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions.
+
+
We will reserve \( 80 \% \) of our dataset for training and \( 20 \% \) for testing.
+
+
It is important that the train and test datasets are drawn randomly from our dataset, to ensure
+no bias in the sampling.
+Say you are taking measurements of weather data to predict the weather in the coming 5 days.
+You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data
+collected from 12.00 to 24.00.
+
@@ -327,55 +329,33 @@ MathJax.Hub.Config({
-
"""
-Simple code that tests XOR and OR gates with linear regression
-and logistic regression
-"""
+
fromsklearn.model_selectionimport train_test_split
-importmatplotlib.pyplotasplt
-fromsklearn.linear_modelimport LogisticRegression
-importnumpyasnp
+# one-liner from scikit-learn library
+train_size =0.8
+test_size =1- train_size
+X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
+ test_size=test_size)
-# Design matrix
-X = np.array([ [1, 0, 0], [1, 0, 1], [1, 1, 0],[1, 1, 1]],dtype=np.float64)
-print(f"The X.TX matrix:{X.T @ X}")
-Xinv = np.linalg.pinv(X.T @ X)
-print(f"The invers of X.TX matrix:{Xinv}")
+# equivalently in numpy
+deftrain_test_split_numpy(inputs, labels, train_size, test_size):
+ n_inputs =len(inputs)
+ inputs_shuffled = inputs.copy()
+ labels_shuffled = labels.copy()
+
+ np.random.shuffle(inputs_shuffled)
+ np.random.shuffle(labels_shuffled)
+
+ train_end =int(n_inputs*train_size)
+ X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:]
+ Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:]
+
+ return X_train, X_test, Y_train, Y_test
-# The XOR gate
-yXOR = np.array( [ 0, 1 ,1, 0])
-ThetaXOR = Xinv @ X.T @ yXOR
-print(f"The values of theta for the XOR gate:{ThetaXOR}")
-print(f"The linear regression prediction for the XOR gate:{X @ ThetaXOR}")
+#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size)
-
-# The OR gate
-yOR = np.array( [ 0, 1 ,1, 1])
-ThetaOR = Xinv @ X.T @ yOR
-print(f"The values of theta for the OR gate:{ThetaOR}")
-print(f"The linear regression prediction for the OR gate:{X @ ThetaOR}")
-
-
-# The OR gate
-yAND = np.array( [ 0, 0 ,0, 1])
-ThetaAND = Xinv @ X.T @ yAND
-print(f"The values of theta for the AND gate:{ThetaAND}")
-print(f"The linear regression prediction for the AND gate:{X @ ThetaAND}")
-
-# Now we change to logistic regression
-
-
-# Logistic Regression
-logreg = LogisticRegression()
-logreg.fit(X, yOR)
-print("Test set accuracy with Logistic Regression for OR gate: {:.2f}".format(logreg.score(X,yOR)))
-
-logreg.fit(X, yXOR)
-print("Test set accuracy with Logistic Regression for XOR gate: {:.2f}".format(logreg.score(X,yXOR)))
-
-
-logreg.fit(X, yAND)
-print("Test set accuracy with Logistic Regression for AND gate: {:.2f}".format(logreg.score(X,yAND)))
+print("Number of training images: "+str(len(X_train)))
+print("Number of test images: "+str(len(X_test)))
Our simple feed-forward neural network will consist of an input layer, a single hidden layer and an output layer. The activation \( y \) of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have
-
-
-
-
-
-
-
# and now neural networks with Scikit-Learn and the XOR
+
$$ z = \sum_{i=1}^n w_i a_i ,$$
-fromsklearn.neural_networkimport MLPClassifier
-fromsklearn.datasetsimport make_classification
-X, yXOR = make_classification(n_samples=100, random_state=1)
-FFNN = MLPClassifier(random_state=1, max_iter=300).fit(X, yXOR)
-FFNN.predict_proba(X)
-print(f"Test set accuracy with Feed Forward Neural Network for XOR gate:{FFNN.score(X, yXOR)}")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
$$ y = f(z) ,$$
+
where \( f \) is the activation function, \( a_i \) represents input from neuron \( i \) in the preceding layer
+and \( w_i \) is the weight to input \( i \).
+The activation of the neurons in the input layer is just the features (e.g. a pixel value).
+
+
+
The simplest activation function for a neuron is the Heaviside function:
A feed-forward neural network with this activation is known as a perceptron.
+For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer.
+This activation can be generalized to \( k \) classes (using e.g. the one-against-all strategy),
+and we call these architectures multiclass perceptrons.
+
+
+
However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and
+Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function.
+
+
+
Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU).
+We will be using the sigmoid function \( \sigma(x) \):
+
+
+
$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$
+
+
which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions.
Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons.
-
This function receives \( x_i \) as inputs.
-Here the activation \( z=(\sum_{i=1}^n w_ix_i+b_i) \).
-In an FFNN of such neurons, the inputs \( x_i \) are the outputs of
-the neurons in the preceding layer. Furthermore, an MLP is
-fully-connected, which means that each neuron receives a weighted sum
-of the outputs of all neurons in the previous layer.
+
+
Hidden layer
+
+
We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer.
+Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer.
+
+
+
+
Output
+
+
If we were building a binary classifier, it would be sufficient with a single neuron in the output layer,
+which could output 0 or 1 according to the Heaviside function. This would be an example of a hard classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a soft classifier, which outputs the probability of being in class 0 or 1.
+
+
+
For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class.
+
+
Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons \( j = 0,1,...,9 \). The activation of each output neuron \( j \) will be according to the softmax function:
i.e. each neuron \( j \) outputs the probability of being in class \( j \) given an input from the hidden layer \( \boldsymbol{a} \), with \( \boldsymbol{w}_j \) the weights of neuron \( j \) to the inputs.
+The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1.
+The exponent is just the weighted sum of inputs as before:
+
+
+
$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$
+
+
Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500
+weights to the output layer.
@@ -358,7 +374,7 @@ of the outputs of all neurons in the previous layer.
Here \( b_i \) is the so-called bias which is normally needed in
-case of zero activation weights or inputs. How to fix the biases and
-the weights will be discussed below. The value of \( z_i^1 \) is the
-argument to the activation function \( f_i \) of each node \( i \), The
-variable \( M \) stands for all possible inputs to a given node \( i \) in the
-first layer. We define the output \( y_i^1 \) of all neurons in layer 1 as
+
Typically weights are initialized with small values distributed around zero, drawn from a uniform
+or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless.
where we assume that all nodes in the same layer have identical
-activation functions, hence the notation \( f \). In general, we could assume in the more general case that different layers have different activation functions.
-In this case we would identify these functions with a superscript \( l \) for the \( l \)-th layer,
+
Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range
+of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron \( j \), \( b_j \):
The bias weights \( \boldsymbol{b} \) are often initialized to zero, but a small value like \( 0.01 \) ensures all neurons have some output which can be backpropagated in the first training cycle.
+
+
+
+
+
+
+
+
# building our neural network
+
+n_inputs, n_features = X_train.shape
+n_hidden_neurons =50
+n_categories =10
+
+# we make the weights normally distributed using numpy.random.randn
+
+# weights and bias in the hidden layer
+hidden_weights = np.random.randn(n_features, n_hidden_neurons)
+hidden_bias = np.zeros(n_hidden_neurons) +0.01
+
+# weights and bias in the output layer
+output_weights = np.random.randn(n_hidden_neurons, n_categories)
+output_bias = np.zeros(n_categories) +0.01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
where \( N_l \) is the number of nodes in layer \( l \). When the output of
-all the nodes in the first hidden layer are computed, the values of
-the subsequent layer can be calculated and so forth until the output
-is obtained.
-
Denote \( F \) the number of features, \( H \) the number of hidden neurons and \( C \) the number of categories.
+For each input image we calculate a weighted sum of input features (pixel values) to each neuron \( j \) in the hidden layer \( l \):
+
We can generalize this expression to an MLP with \( l \) hidden
-layers. The complete functional form is,
+
Since our data has the dimensions \( X = (n_{inputs}, n_{features}) \) and our weights to the hidden
+layer have the dimensions
+\( W_{hidden} = (n_{features}, n_{hidden}) \),
+we can easily feed the network all our training data in one go by taking the matrix product
which illustrates a basic property of MLPs: The only independent
-variables are the input values \( x_n \).
+
and obtain a matrix that holds the weighted sum of inputs to the hidden layer
+for each input image and each hidden neuron.
+We also add the bias to obtain a matrix of weighted sums to the hidden layer \( Z^{h} \):
This confirms that an MLP, despite its quite convoluted mathematical
-form, is nothing more than an analytic function, specifically a
-mapping of real-valued vectors \( \hat{x} \in \mathbb{R}^n \rightarrow
-\hat{y} \in \mathbb{R}^m \).
+
To measure how well our neural network is doing we need to introduce a cost function.
+We will call the function that gives the error of a single sample output the loss function, and the function
+that gives the total error of our network across all samples the cost function.
+A typical choice for multiclass classification is the cross-entropy loss, also known as the negative log likelihood.
-
Furthermore, the flexibility and universality of an MLP can be
-illustrated by realizing that the expression is essentially a nested
-sum of scaled activation functions of the form
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset.
+
+
Let \( y_{ic} \) denote the \( c \)-th component of the \( i \)-th one-hot vector.
+We define the cost function \( \mathcal{C} \) as a sum over the cross-entropy loss for each point \( \boldsymbol{x}_i \) in the dataset.
where the parameters \( c_i \) are weights and biases. By adjusting these
-parameters, the activation functions can be shifted up and down or
-left and right, change slope or be rescaled which is the key to the
-flexibility of a neural network.
+
In the one-hot representation only one of the terms in the loss function is non-zero, namely the
+probability of the correct category \( c' \)
+(i.e. the category \( c' \) such that \( y_{ic'} = 1 \)). This means that the cross entropy loss only punishes you for how wrong
+you got the correct label. The probability of category \( c \) is given by the softmax function. The vector \( \boldsymbol{\theta} \) represents the parameters of our network, i.e. all the weights and biases.
+
@@ -369,7 +361,7 @@ flexibility of a neural network.
We can introduce a more convenient notation for the activations in an A NN.
-
-
Additionally, we can represent the biases and activations
-as layer-wise column vectors \( \hat{b}_l \) and \( \hat{y}_l \), so that the \( i \)-th element of each vector
-is the bias \( b_i^l \) and activation \( y_i^l \) of node \( i \) in layer \( l \) respectively.
+
The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is gradient descent and its generalizations. The idea behind gradient descent
+is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a local minimum of the cost function.
+Each parameter \( \theta \) is iteratively adjusted according to the rule
-
We have that \( \mathrm{W}_l \) is an \( N_{l-1} \times N_l \) matrix, while \( \hat{b}_l \) and \( \hat{y}_l \) are \( N_l \times 1 \) column vectors.
-With this notation, the sum becomes a matrix-vector multiplication, and we can write
-the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as
-
where \( \eta \) is known as the learning rate, which controls how big a step we take towards the minimum.
+This update can be repeated for any number of iterations, or until we are satisfied with the result.
+
+
+
A simple and effective improvement is a variant called Batch Gradient Descent.
+Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient
+on a subset of the data called a minibatch.
+If there are \( N \) data points and we have a minibatch size of \( M \), the total number of batches
+is \( N/M \).
+We denote each minibatch \( B_k \), with \( k = 1, 2,...,N/M \). The gradient then becomes:
+
It is common to add an extra term to the cost function, proportional
+to the size of the weights. This is equivalent to constraining the
+size of the weights, so that they do not grow out of control.
+Constraining the size of the weights means that the weights cannot
+grow arbitrarily large to fit the training data, and in this way
+reduces overfitting.
+
We will measure the size of the weights using the so called L2-norm, meaning our cost function becomes:
-
This is not just a convenient and compact notation, but also a useful
-and intuitive way to think about MLPs: The output is calculated by a
-series of matrix-vector multiplications and vector additions that are
-used as input to the activation functions. For each operation
-\( \mathrm{W}_l \hat{y}_{l-1} \) we move forward one layer.
+
i.e. we sum up all the weights squared. The factor \( \lambda \) is known as a regularization parameter.
+
+
In order to train the model, we need to calculate the derivative of
+the cost function with respect to every bias and weight in the
+network. In total our network has \( (64 + 1)\times 50=3250 \) weights in
+the hidden layer and \( (50 + 1)\times 10=510 \) weights to the output
+layer (\( +1 \) for the bias), and the gradient must be calculated for
+every parameter. We use the backpropagation algorithm discussed
+above. This is a clever use of the chain rule that allows us to
+calculate the gradient efficently.
@@ -360,9 +361,6 @@ used as input to the activation functions. For each operation
A property that characterizes a neural network, other than its
-connectivity, is the choice of activation function(s). As described
-in, the following restrictions are imposed on an activation function
-for a FFNN to fulfill the universal approximation theorem
+
To more efficently train our network these equations are implemented using matrix operations.
+The error in the output layer is calculated simply as, with \( \boldsymbol{t} \) being our targets,
where \( \boldsymbol{a} = (n_{inputs}, n_{hidden}) \). This simply means that we are summing up the gradients for each input.
+Since we are going backwards we have to transpose the activation matrix.
+
+
+
The gradient with respect to the output bias is then
where \( f'(a_{h}) \) is the derivative of the activation in the hidden layer. The matrix products mean
+that we are summing up the products for each neuron in the output layer. The symbol \( \circ \) denotes
+the Hadamard product, meaning element-wise multiplication.
+
+
+
This again gives us the gradients in the hidden layer:
Activation functions, Logistic and Hyperbolic ones
+
Improving performance
-
The second requirement excludes all linear functions. Furthermore, in
-a MLP with only linear activation functions, each layer simply
-performs a linear transformation of its inputs.
+
As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image.
+In order to obtain a network that does something useful, we will have to do a bit more work.
-
Regardless of the number of layers, the output of the NN will be
-nothing but a linear function of the inputs. Thus we need to introduce
-some kind of non-linearity to the NN to be able to fit non-linear
-functions Typical examples are the logistic Sigmoid
+
The choice of hyperparameters such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a grid-search is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates \( \eta = 10^{-6}, 10^{-5},...,10^{-1} \) with different regularization parameters \( \lambda = 10^{-6},...,10^{-0} \).
+
+
Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an iteration, and a full training period
+going through the entire dataset (\( n/M \) batches) an epoch.
-$$
- f(x) = \frac{1}{1 + e^{-x}},
-$$
-
-
and the hyperbolic tangent function
-$$
- f(x) = \tanh(x)
-$$
-
+
If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers.
+Andrew Ng goes through some of these considerations in this video. You can find a summary of the video here.
+
The sigmoid function are more biologically plausible because the
-output of inactive neurons are zero. Such activation function are
-called one-sided. However, it has been shown that the hyperbolic
-tangent performs better than the sigmoid for training MLPs. has
-become the most popular for deep neural networks
+
It is very natural to think of the network as an object, with specific instances of the network
+being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below.
@@ -334,77 +322,105 @@ become the most popular for deep neural networks
-
"""The sigmoid function (or the logistic curve) is a
-function that takes any real number, z, and outputs a number (0,1).
-It is useful in neural networks for assigning weights on a relative scale.
-The value z is the weighted sum of parameters involved in the learning algorithm."""
+
The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of
-
-
A neural network with one or more layers of nodes between the input and the output nodes.
-
The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer.
-
The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer.
-
-
As a convention it is normal to call a network with one layer of input units, one layer of hidden
-units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc.
+
To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data.
+We measure the performance of the network using the accuracy score.
+The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of \( 1 \).
-
For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units.
-Hereafter we will call the various entities of a layer for nodes.
-There are also no connections within a single layer.
-
The number of input nodes does not need to equal the number of output
-nodes. This applies also to the hidden layers. Each layer may have its
-own number of nodes and activation functions.
-
+
where \( I \) is the indicator function, \( 1 \) if \( \tilde{y}_i = y_i \) and \( 0 \) otherwise.
+
+
+
+
+
+
+
+
+
epochs =100
+batch_size =100
+
+dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
+ n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
+dnn.train()
+test_predict = dnn.predict(X_test)
+
+# accuracy score from scikit library
+print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
+
+# equivalent in numpy
+defaccuracy_score_numpy(Y_test, Y_pred):
+ return np.sum(Y_test == Y_pred) /len(Y_test)
+
+#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
The hidden layers have their name from the fact that they are not
-linked to observables and as we will see below when we define the
-so-called activation \( \hat{z} \), we can think of this as a basis
-expansion of the original inputs \( \hat{x} \). The difference however
-between neural networks and say linear regression is that now these
-basis functions (which will correspond to the weights in the network)
-are learned from data. This results in an important difference between
-neural networks and deep learning approaches on one side and methods
-like logistic regression or linear regression and their modifications on the other side.
-
@@ -370,13 +378,6 @@ like logistic regression or linear regression and their modifications on the oth
From one to many layers, the universal approximation theorem
+
Adjust hyperparameters
-
A neural network with only one layer, what we called the simple
-perceptron, is best suited if we have a standard binary model with
-clear (linear) boundaries between the outcomes. As such it could
-equally well be replaced by standard linear regression or logistic
-regression. Networks with one or more hidden layers approximate
-systems with more complex boundaries.
+
We now perform a grid search to find the optimal hyperparameters for the network.
+Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around \( 98\% \) (\( 2\% \) error rate).
-
As stated earlier,
-an important theorem in studies of neural networks, restated without
-proof here, is the universal approximation
-theorem.
-
-
It states that a feed-forward network with a single hidden layer
-containing a finite number of neurons can approximate continuous
-functions on compact subsets of real functions. The theorem thus
-states that simple neural networks can represent a wide variety of
-interesting functions when given appropriate parameters. It is the
-multilayer feedforward architecture itself which gives neural networks
-the potential of being universal approximators.
-
+
+
+
+
+
+
+
eta_vals = np.logspace(-5, 1, 7)
+lmbd_vals = np.logspace(-5, 1, 7)
+# store the models for later use
+DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
+
+# grid search
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
+ n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
+ dnn.train()
+
+ DNN_numpy[i][j] = dnn
+
+ test_predict = dnn.predict(X_test)
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
+ print()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -361,12 +376,6 @@ the potential of being universal approximators.
Deriving the back propagation code for a multilayer perceptron model
+
Visualization
-
As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications.
-The unknowwn quantities are our weights \( w_{ij} \) and we need to find an algorithm for changing them so that our errors are as small as possible.
-This leads us to the famous back propagation algorithm.
-
-
The questions we want to ask are how do changes in the biases and the
-weights in our network change the cost function and how can we use the
-final output to modify the weights?
-
+
+
+
+
+
+
+
# visual representation of grid search
+# uses seaborn heatmap, you can also do this with matplotlib imshow
+importseabornassns
-
To derive these equations let us start with a plain regression problem
-and define our cost function as
-
where the $t_i$s are our \( n \) targets (the values we want to
-reproduce), while the outputs of the network after having propagated
-all inputs \( \hat{x} \) are given by \( y_i \). Below we will demonstrate
-how the basic equations arising from the back propagation algorithm
-can be modified in order to study classification problems with \( K \)
-classes.
-
With our definition of the targets \( \hat{t} \), the outputs of the
-network \( \hat{y} \) and the inputs \( \hat{x} \) we
-define now the activation \( z_j^l \) of node/neuron/unit \( j \) of the
-\( l \)-th layer as a function of the bias, the weights which add up from
-the previous layer \( l-1 \) and the forward passes/outputs
-\( \hat{a}^{l-1} \) from the previous layer as
+
scikit-learn focuses more
+on traditional machine learning methods, such as regression,
+clustering, decision trees, etc. As such, it has only two types of
+neural networks: Multi Layer Perceptron outputting continuous values,
+MPLRegressor, and Multi Layer Perceptron outputting labels,
+MLPClassifier. We will see how simple it is to use these classes.
where \( b_k^l \) are the biases from layer \( l \). Here \( M_{l-1} \)
-represents the total number of nodes/neurons/units of layer \( l-1 \). The
-figure here illustrates this equation. We can rewrite this in a more
-compact form as the matrix-vector products we discussed earlier,
+
scikit-learn implements a few improvements from our neural network,
+such as early stopping, a varying learning rate, different
+optimization methods, etc. We would therefore expect a better
+performance overall.
With the activation values \( \hat{z}^l \) we can in turn define the
-output of layer \( l \) as \( \hat{a}^l = f(\hat{z}^l) \) where \( f \) is our
-activation function. In the examples here we will use the sigmoid
-function discussed in our logistic regression lectures. We will also use the same activation function \( f \) for all layers
-and their nodes. It means we have
-
+
+
+
+
+
+
+
fromsklearn.neural_networkimport MLPClassifier
+# store models for later use
+DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-$$
-a_j^l = f(z_j^l) = \frac{1}{1+\exp{-(z_j^l)}}.
-$$
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',
+ alpha=lmbd, learning_rate_init=eta, max_iter=epochs)
+ dnn.fit(X_train, Y_train)
+
+ DNN_scikit[i][j] = dnn
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", dnn.score(X_test, Y_test))
+ print()
+
Thursday: Repetion and summary of Stochastic Gradient descent with examples and automatic differentiation and begin Neural Networks.
-
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model.
+
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model. Presentation of project 2.
Reading suggestions for both days: Aurelien Geron's chapter 10 and Hastie et al chapter 11.
@@ -1468,6 +1468,1424 @@ Here it is convenient to use stochastic gradient descent (see the examples below
+
+
Setting up the Back propagation algorithm
+
+
The four equations above provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm.
+
+
+
+
+
First, we set up the input data \( \boldsymbol{x} \) and the activations
+\( \boldsymbol{z}_1 \) of the input layer and compute the activation function and
+the pertinent outputs \( \boldsymbol{a}^1 \).
+
+
+
+
+
+
+
+
Secondly, we perform then the feed forward till we reach the output
+layer and compute all \( \boldsymbol{z}_l \) of the input layer and compute the
+activation function and the pertinent outputs \( \boldsymbol{a}^l \) for
+\( l=2,3,\dots,L \).
+
+
+
+
+
+
+
+
Thereafter we compute the ouput error \( \boldsymbol{\delta}^L \) by computing all
Finally, we update the weights and the biases using gradient descent for each \( l=L-1,L-2,\dots,2 \) and update the weights and biases according to the rules
The parameter \( \eta \) is the learning parameter discussed in connection with the gradient descent methods.
+Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training.
+
+
+
+
+
Setting up a Multi-layer perceptron model for classification
+
+
We are now gong to develop an example based on the MNIST data
+base. This is a classification problem and we need to use our
+cross-entropy function we discussed in connection with logistic
+regression. The cross-entropy defines our cost function for the
+classificaton problems with neural networks.
+
+
+
In binary classification with two classes \( (0, 1) \) we define the
+logistic/sigmoid function as the probability that a particular input
+is in class \( 0 \) or \( 1 \). This is possible because the logistic
+function takes any input from the real numbers and inputs a number
+between 0 and 1, and can therefore be interpreted as a probability. It
+also has other nice properties, such as a derivative that is simple to
+calculate.
+
+
+
For an input \( \boldsymbol{a} \) from the hidden layer, the probability that the input \( \boldsymbol{x} \)
+is in class 0 or 1 is just. We let \( \theta \) represent the unknown weights and biases to be adjusted by our equations). The variable \( x \)
+represents our activation values \( z \). We have
+
This last equality means that we can interpret our cost function as a sum over the loss function
+for each point in the dataset \( \mathcal{L}_i(\boldsymbol{\theta}) \).
+The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather
+than maximizing a negative number.
+
+
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset (numbers from \( 0 \) to \( 9 \))..
+
+
If \( \boldsymbol{x}_i \) is the \( i \)-th input (image), \( y_{ic} \) refers to the \( c \)-th component of the \( i \)-th
+output vector \( \boldsymbol{y}_i \).
+The probability of \( \boldsymbol{x}_i \) being in class \( c \) will be given by the softmax function:
+
See the logistic regression lectures for a full definition of the cost function.
+
+
The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before!
+
+
+
+
Example: binary classification problem
+
+
As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters \( \beta \) as
The parameters \( \boldsymbol{\beta} \) were defined using a minimization method like gradient descent or Newton-Raphson's method.
+
+
Now we replace \( x_i \) with the activation \( z_i^l \) for a given layer \( l \) and the outputs as \( y_i=a_i^l=f(z_i^l) \), with \( z_i^l \) now being a function of the weights \( w_{ij}^l \) and biases \( b_i^l \).
+We have then
+
where we have defined the targets \( t_i \). The derivatives of the cost function with respect to the output \( a_i^L \) are then easily calculated and we get
In case we use another activation function than the logistic one, we need to evaluate other derivatives.
+
+
+
+
The Softmax function
+
In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation \( z_i^l \), that is we need
Here we will be using the MNIST dataset, which is readily available through the scikit-learn
+package. You may also find it for example here.
+The MNIST (Modified National Institute of Standards and Technology) database is a large database
+of handwritten digits that is commonly used for training various image processing systems.
+The MNIST dataset consists of 70 000 images of size \( 28\times 28 \) pixels, each labeled from 0 to 9.
+The scikit-learn dataset we will use consists of a selection of 1797 images of size \( 8\times 8 \) collected and processed from this database.
+
+
+
To feed data into a feed-forward neural network we need to represent
+the inputs as a design/feature matrix \( X = (n_{inputs}, n_{features}) \). Each
+row represents an input, in this case a handwritten digit, and
+each column represents a feature, in this case a pixel. The
+correct answers, also known as labels or targets are
+represented as a 1D array of integers
+\( Y = (n_{inputs}) = (5, 3, 1, 8,...) \).
+
+
+
As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from
+measurements of height (in m)
+and weight (in kg). If we have measurements of 5 people the design/feature matrix could be for example:
+
Since each input image is a 2D matrix, we need to flatten the image
+(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a
+design/feature matrix. This means we lose all spatial information in the
+image, such as locality and translational invariance. More complicated
+architectures such as Convolutional Neural Networks can take advantage
+of such information, and are most commonly applied when analyzing
+images.
+
+
+
+
+
+
+
+
+
+
# import necessary packages
+importnumpyasnp
+importmatplotlib.pyplotasplt
+fromsklearnimport datasets
+
+
+# ensure the same random numbers appear every time
+np.random.seed(0)
+
+# display images in notebook
+%matplotlib inline
+plt.rcParams['figure.figsize'] = (12,12)
+
+
+# download MNIST dataset
+digits = datasets.load_digits()
+
+# define inputs and labels
+inputs = digits.images
+labels = digits.target
+
+print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
+print("labels = (n_inputs) = " + str(labels.shape))
+
+
+# flatten the image
+# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
+n_inputs = len(inputs)
+inputs = inputs.reshape(n_inputs, -1)
+print("X = (n_inputs, n_features) = " + str(inputs.shape))
+
+
+# choose some random images to display
+indices = np.arange(n_inputs)
+random_indices = np.random.choice(indices, size=5)
+
+for i, image inenumerate(digits.images[random_indices]):
+ plt.subplot(1, 5, i+1)
+ plt.axis('off')
+ plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
+ plt.title("Label: %d" % digits.target[random_indices[i]])
+plt.show()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Train and test datasets
+
+
Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions.
+
+
We will reserve \( 80 \% \) of our dataset for training and \( 20 \% \) for testing.
+
+
It is important that the train and test datasets are drawn randomly from our dataset, to ensure
+no bias in the sampling.
+Say you are taking measurements of weather data to predict the weather in the coming 5 days.
+You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data
+collected from 12.00 to 24.00.
+
Our simple feed-forward neural network will consist of an input layer, a single hidden layer and an output layer. The activation \( y \) of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have
+
+
+$$ z = \sum_{i=1}^n w_i a_i ,$$
+
+
+
+$$ y = f(z) ,$$
+
+
+
where \( f \) is the activation function, \( a_i \) represents input from neuron \( i \) in the preceding layer
+and \( w_i \) is the weight to input \( i \).
+The activation of the neurons in the input layer is just the features (e.g. a pixel value).
+
+
+
The simplest activation function for a neuron is the Heaviside function:
A feed-forward neural network with this activation is known as a perceptron.
+For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer.
+This activation can be generalized to \( k \) classes (using e.g. the one-against-all strategy),
+and we call these architectures multiclass perceptrons.
+
+
+
However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and
+Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function.
+
+
+
Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU).
+We will be using the sigmoid function \( \sigma(x) \):
+
+
+
+$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$
+
+
+
which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions.
+
+
+
+
Layers
+
+
+
Input
+
+
+
Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons.
+
+
+
Hidden layer
+
+
+
We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer.
+Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer.
+
+
+
+
Output
+
+
+
If we were building a binary classifier, it would be sufficient with a single neuron in the output layer,
+which could output 0 or 1 according to the Heaviside function. This would be an example of a hard classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a soft classifier, which outputs the probability of being in class 0 or 1.
+
+
+
For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class.
+
+
Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons \( j = 0,1,...,9 \). The activation of each output neuron \( j \) will be according to the softmax function:
i.e. each neuron \( j \) outputs the probability of being in class \( j \) given an input from the hidden layer \( \boldsymbol{a} \), with \( \boldsymbol{w}_j \) the weights of neuron \( j \) to the inputs.
+The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1.
+The exponent is just the weighted sum of inputs as before:
+
+
+
+$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$
+
+
+
Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500
+weights to the output layer.
+
+
+
+
+
Weights and biases
+
+
Typically weights are initialized with small values distributed around zero, drawn from a uniform
+or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless.
+
+
+
Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range
+of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron \( j \), \( b_j \):
+
+
+
+$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$
+
+
+
The bias weights \( \boldsymbol{b} \) are often initialized to zero, but a small value like \( 0.01 \) ensures all neurons have some output which can be backpropagated in the first training cycle.
+
+
+
+
+
+
+
+
# building our neural network
+
+n_inputs, n_features = X_train.shape
+n_hidden_neurons = 50
+n_categories = 10
+
+# we make the weights normally distributed using numpy.random.randn
+
+# weights and bias in the hidden layer
+hidden_weights = np.random.randn(n_features, n_hidden_neurons)
+hidden_bias = np.zeros(n_hidden_neurons) + 0.01
+
+# weights and bias in the output layer
+output_weights = np.random.randn(n_hidden_neurons, n_categories)
+output_bias = np.zeros(n_categories) + 0.01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Feed-forward pass
+
+
Denote \( F \) the number of features, \( H \) the number of hidden neurons and \( C \) the number of categories.
+For each input image we calculate a weighted sum of input features (pixel values) to each neuron \( j \) in the hidden layer \( l \):
+
Since our data has the dimensions \( X = (n_{inputs}, n_{features}) \) and our weights to the hidden
+layer have the dimensions
+\( W_{hidden} = (n_{features}, n_{hidden}) \),
+we can easily feed the network all our training data in one go by taking the matrix product
+
+
+
+$$ X W^{h} = (n_{inputs}, n_{hidden}),$$
+
+
+
and obtain a matrix that holds the weighted sum of inputs to the hidden layer
+for each input image and each hidden neuron.
+We also add the bias to obtain a matrix of weighted sums to the hidden layer \( Z^{h} \):
+
# setup the feed-forward pass, subscript h = hidden layer
+
+defsigmoid(x):
+ return1/(1 + np.exp(-x))
+
+deffeed_forward(X):
+ # weighted sum of inputs to the hidden layer
+ z_h = np.matmul(X, hidden_weights) + hidden_bias
+ # activation in the hidden layer
+ a_h = sigmoid(z_h)
+
+ # weighted sum of inputs to the output layer
+ z_o = np.matmul(a_h, output_weights) + output_bias
+ # softmax output
+ # axis 0 holds each input and axis 1 the probabilities of each category
+ exp_term = np.exp(z_o)
+ probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
+
+ return probabilities
+
+probabilities = feed_forward(X_train)
+print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape))
+print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0]))
+print("probabilities sum up to: " + str(probabilities[0].sum()))
+print()
+
+# we obtain a prediction by taking the class with the highest likelihood
+defpredict(X):
+ probabilities = feed_forward(X)
+ return np.argmax(probabilities, axis=1)
+
+predictions = predict(X_train)
+print("predictions = (n_inputs) = " + str(predictions.shape))
+print("prediction for image 0: " + str(predictions[0]))
+print("correct label for image 0: " + str(Y_train[0]))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose cost function and optimizer
+
+
To measure how well our neural network is doing we need to introduce a cost function.
+We will call the function that gives the error of a single sample output the loss function, and the function
+that gives the total error of our network across all samples the cost function.
+A typical choice for multiclass classification is the cross-entropy loss, also known as the negative log likelihood.
+
+
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset.
+
+
Let \( y_{ic} \) denote the \( c \)-th component of the \( i \)-th one-hot vector.
+We define the cost function \( \mathcal{C} \) as a sum over the cross-entropy loss for each point \( \boldsymbol{x}_i \) in the dataset.
+
+
+
In the one-hot representation only one of the terms in the loss function is non-zero, namely the
+probability of the correct category \( c' \)
+(i.e. the category \( c' \) such that \( y_{ic'} = 1 \)). This means that the cross entropy loss only punishes you for how wrong
+you got the correct label. The probability of category \( c \) is given by the softmax function. The vector \( \boldsymbol{\theta} \) represents the parameters of our network, i.e. all the weights and biases.
+
+
+
+
+
Optimizing the cost function
+
+
The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is gradient descent and its generalizations. The idea behind gradient descent
+is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a local minimum of the cost function.
+Each parameter \( \theta \) is iteratively adjusted according to the rule
+
where \( \eta \) is known as the learning rate, which controls how big a step we take towards the minimum.
+This update can be repeated for any number of iterations, or until we are satisfied with the result.
+
+
+
A simple and effective improvement is a variant called Batch Gradient Descent.
+Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient
+on a subset of the data called a minibatch.
+If there are \( N \) data points and we have a minibatch size of \( M \), the total number of batches
+is \( N/M \).
+We denote each minibatch \( B_k \), with \( k = 1, 2,...,N/M \). The gradient then becomes:
+
i.e. instead of averaging the loss over the entire dataset, we average over a minibatch.
+
+
This has two important benefits:
+
+
Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima.
+
+
It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient.
+
+
+
The various optmization methods, with codes and algorithms, are discussed in our lectures on Gradient descent approaches.
+
+
+
+
Regularization
+
+
It is common to add an extra term to the cost function, proportional
+to the size of the weights. This is equivalent to constraining the
+size of the weights, so that they do not grow out of control.
+Constraining the size of the weights means that the weights cannot
+grow arbitrarily large to fit the training data, and in this way
+reduces overfitting.
+
+
+
We will measure the size of the weights using the so called L2-norm, meaning our cost function becomes:
i.e. we sum up all the weights squared. The factor \( \lambda \) is known as a regularization parameter.
+
+
In order to train the model, we need to calculate the derivative of
+the cost function with respect to every bias and weight in the
+network. In total our network has \( (64 + 1)\times 50=3250 \) weights in
+the hidden layer and \( (50 + 1)\times 10=510 \) weights to the output
+layer (\( +1 \) for the bias), and the gradient must be calculated for
+every parameter. We use the backpropagation algorithm discussed
+above. This is a clever use of the chain rule that allows us to
+calculate the gradient efficently.
+
+
+
+
+
Matrix multiplication
+
+
To more efficently train our network these equations are implemented using matrix operations.
+The error in the output layer is calculated simply as, with \( \boldsymbol{t} \) being our targets,
+
where \( \boldsymbol{a} = (n_{inputs}, n_{hidden}) \). This simply means that we are summing up the gradients for each input.
+Since we are going backwards we have to transpose the activation matrix.
+
+
+
The gradient with respect to the output bias is then
where \( f'(a_{h}) \) is the derivative of the activation in the hidden layer. The matrix products mean
+that we are summing up the products for each neuron in the output layer. The symbol \( \circ \) denotes
+the Hadamard product, meaning element-wise multiplication.
+
+
+
This again gives us the gradients in the hidden layer:
# to categorical turns our integer vector into a onehot representation
+fromsklearn.metricsimport accuracy_score
+
+# one-hot in numpy
+defto_categorical_numpy(integer_vector):
+ n_inputs = len(integer_vector)
+ n_categories = np.max(integer_vector) + 1
+ onehot_vector = np.zeros((n_inputs, n_categories))
+ onehot_vector[range(n_inputs), integer_vector] = 1
+
+ return onehot_vector
+
+#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test)
+Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test)
+
+deffeed_forward_train(X):
+ # weighted sum of inputs to the hidden layer
+ z_h = np.matmul(X, hidden_weights) + hidden_bias
+ # activation in the hidden layer
+ a_h = sigmoid(z_h)
+
+ # weighted sum of inputs to the output layer
+ z_o = np.matmul(a_h, output_weights) + output_bias
+ # softmax output
+ # axis 0 holds each input and axis 1 the probabilities of each category
+ exp_term = np.exp(z_o)
+ probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
+
+ # for backpropagation need activations in hidden and output layers
+ return a_h, probabilities
+
+defbackpropagation(X, Y):
+ a_h, probabilities = feed_forward_train(X)
+
+ # error in the output layer
+ error_output = probabilities - Y
+ # error in the hidden layer
+ error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h)
+
+ # gradients for the output layer
+ output_weights_gradient = np.matmul(a_h.T, error_output)
+ output_bias_gradient = np.sum(error_output, axis=0)
+
+ # gradient for the hidden layer
+ hidden_weights_gradient = np.matmul(X.T, error_hidden)
+ hidden_bias_gradient = np.sum(error_hidden, axis=0)
+
+ return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient
+
+print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
+
+eta = 0.01
+lmbd = 0.01
+for i inrange(1000):
+ # calculate gradients
+ dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot)
+
+ # regularization term gradients
+ dWo += lmbd * output_weights
+ dWh += lmbd * hidden_weights
+
+ # update weights and biases
+ output_weights -= eta * dWo
+ output_bias -= eta * dBo
+ hidden_weights -= eta * dWh
+ hidden_bias -= eta * dBh
+
+print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Improving performance
+
+
As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image.
+In order to obtain a network that does something useful, we will have to do a bit more work.
+
+
+
The choice of hyperparameters such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a grid-search is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates \( \eta = 10^{-6}, 10^{-5},...,10^{-1} \) with different regularization parameters \( \lambda = 10^{-6},...,10^{-0} \).
+
+
Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an iteration, and a full training period
+going through the entire dataset (\( n/M \) batches) an epoch.
+
+
+
If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers.
+Andrew Ng goes through some of these considerations in this video. You can find a summary of the video here.
+
+
+
+
+
Full object-oriented implementation
+
+
It is very natural to think of the network as an object, with specific instances of the network
+being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below.
+
To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data.
+We measure the performance of the network using the accuracy score.
+The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of \( 1 \).
+
We now perform a grid search to find the optimal hyperparameters for the network.
+Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around \( 98\% \) (\( 2\% \) error rate).
+
+
+
+
+
+
+
+
+
+
eta_vals = np.logspace(-5, 1, 7)
+lmbd_vals = np.logspace(-5, 1, 7)
+# store the models for later use
+DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
+
+# grid search
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
+ n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
+ dnn.train()
+
+ DNN_numpy[i][j] = dnn
+
+ test_predict = dnn.predict(X_test)
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
+ print()
+
scikit-learn focuses more
+on traditional machine learning methods, such as regression,
+clustering, decision trees, etc. As such, it has only two types of
+neural networks: Multi Layer Perceptron outputting continuous values,
+MPLRegressor, and Multi Layer Perceptron outputting labels,
+MLPClassifier. We will see how simple it is to use these classes.
+
+
+
scikit-learn implements a few improvements from our neural network,
+such as early stopping, a varying learning rate, different
+optimization methods, etc. We would therefore expect a better
+performance overall.
+
+
+
+
+
+
+
+
+
+
fromsklearn.neural_networkimport MLPClassifier
+# store models for later use
+DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
+
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',
+ alpha=lmbd, learning_rate_init=eta, max_iter=epochs)
+ dnn.fit(X_train, Y_train)
+
+ DNN_scikit[i][j] = dnn
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", dnn.score(X_test, Y_test))
+ print()
+
diff --git a/doc/pub/week40/html/week40-solarized.html b/doc/pub/week40/html/week40-solarized.html
index 406df8935..106f7957f 100644
--- a/doc/pub/week40/html/week40-solarized.html
+++ b/doc/pub/week40/html/week40-solarized.html
@@ -166,7 +166,68 @@ div.toc p,a {
('Setting up the Back propagation algorithm',
2,
None,
- 'setting-up-the-back-propagation-algorithm')]}
+ 'setting-up-the-back-propagation-algorithm'),
+ ('Setting up the Back propagation algorithm',
+ 2,
+ None,
+ 'setting-up-the-back-propagation-algorithm'),
+ ('Setting up a Multi-layer perceptron model for classification',
+ 2,
+ None,
+ 'setting-up-a-multi-layer-perceptron-model-for-classification'),
+ ('Defining the cost function',
+ 2,
+ None,
+ 'defining-the-cost-function'),
+ ('Example: binary classification problem',
+ 2,
+ None,
+ 'example-binary-classification-problem'),
+ ('The Softmax function', 2, None, 'the-softmax-function'),
+ ('Developing a code for doing neural networks with back '
+ 'propagation',
+ 2,
+ None,
+ 'developing-a-code-for-doing-neural-networks-with-back-propagation'),
+ ('Collect and pre-process data',
+ 2,
+ None,
+ 'collect-and-pre-process-data'),
+ ('Train and test datasets', 2, None, 'train-and-test-datasets'),
+ ('Define model and architecture',
+ 2,
+ None,
+ 'define-model-and-architecture'),
+ ('Layers', 2, None, 'layers'),
+ ('Weights and biases', 2, None, 'weights-and-biases'),
+ ('Feed-forward pass', 2, None, 'feed-forward-pass'),
+ ('Matrix multiplications', 2, None, 'matrix-multiplications'),
+ ('Choose cost function and optimizer',
+ 2,
+ None,
+ 'choose-cost-function-and-optimizer'),
+ ('Optimizing the cost function',
+ 2,
+ None,
+ 'optimizing-the-cost-function'),
+ ('Regularization', 2, None, 'regularization'),
+ ('Matrix multiplication', 2, None, 'matrix-multiplication'),
+ ('Improving performance', 2, None, 'improving-performance'),
+ ('Full object-oriented implementation',
+ 2,
+ None,
+ 'full-object-oriented-implementation'),
+ ('Evaluate model performance on test data',
+ 2,
+ None,
+ 'evaluate-model-performance-on-test-data'),
+ ('Adjust hyperparameters', 2, None, 'adjust-hyperparameters'),
+ ('Visualization', 2, None, 'visualization'),
+ ('scikit-learn implementation',
+ 2,
+ None,
+ 'scikit-learn-implementation'),
+ ('Visualization', 2, None, 'visualization')]}
end of tocinfo -->
@@ -204,7 +265,7 @@ MathJax.Hub.Config({
-
Oct 2, 2022
+
Oct 3, 2022
@@ -213,7 +274,7 @@ MathJax.Hub.Config({
Thursday: Repetion and summary of Stochastic Gradient descent with examples and automatic differentiation and begin Neural Networks.
-
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model.
+
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model. Presentation of project 2.
Reading suggestions for both days: Aurelien Geron's chapter 10 and Hastie et al chapter 11.
For Stochastic Gradient Descent, we recommend chapter 4 of Geron's text.
@@ -1362,6 +1423,1303 @@ $$
Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training.
+
+
Setting up the Back propagation algorithm
+
+
The four equations above provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm.
+
+
+
+
+
First, we set up the input data \( \boldsymbol{x} \) and the activations
+\( \boldsymbol{z}_1 \) of the input layer and compute the activation function and
+the pertinent outputs \( \boldsymbol{a}^1 \).
+
+
+
+
+
+
+
+
Secondly, we perform then the feed forward till we reach the output
+layer and compute all \( \boldsymbol{z}_l \) of the input layer and compute the
+activation function and the pertinent outputs \( \boldsymbol{a}^l \) for
+\( l=2,3,\dots,L \).
+
+
+
+
+
+
+
+
Thereafter we compute the ouput error \( \boldsymbol{\delta}^L \) by computing all
Finally, we update the weights and the biases using gradient descent for each \( l=L-1,L-2,\dots,2 \) and update the weights and biases according to the rules
The parameter \( \eta \) is the learning parameter discussed in connection with the gradient descent methods.
+Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training.
+
+
+
+
Setting up a Multi-layer perceptron model for classification
+
+
We are now gong to develop an example based on the MNIST data
+base. This is a classification problem and we need to use our
+cross-entropy function we discussed in connection with logistic
+regression. The cross-entropy defines our cost function for the
+classificaton problems with neural networks.
+
+
+
In binary classification with two classes \( (0, 1) \) we define the
+logistic/sigmoid function as the probability that a particular input
+is in class \( 0 \) or \( 1 \). This is possible because the logistic
+function takes any input from the real numbers and inputs a number
+between 0 and 1, and can therefore be interpreted as a probability. It
+also has other nice properties, such as a derivative that is simple to
+calculate.
+
+
+
For an input \( \boldsymbol{a} \) from the hidden layer, the probability that the input \( \boldsymbol{x} \)
+is in class 0 or 1 is just. We let \( \theta \) represent the unknown weights and biases to be adjusted by our equations). The variable \( x \)
+represents our activation values \( z \). We have
+
This last equality means that we can interpret our cost function as a sum over the loss function
+for each point in the dataset \( \mathcal{L}_i(\boldsymbol{\theta}) \).
+The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather
+than maximizing a negative number.
+
+
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset (numbers from \( 0 \) to \( 9 \))..
+
+
If \( \boldsymbol{x}_i \) is the \( i \)-th input (image), \( y_{ic} \) refers to the \( c \)-th component of the \( i \)-th
+output vector \( \boldsymbol{y}_i \).
+The probability of \( \boldsymbol{x}_i \) being in class \( c \) will be given by the softmax function:
+
See the logistic regression lectures for a full definition of the cost function.
+
+
The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before!
+
+
+
Example: binary classification problem
+
+
As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters \( \beta \) as
The parameters \( \boldsymbol{\beta} \) were defined using a minimization method like gradient descent or Newton-Raphson's method.
+
+
Now we replace \( x_i \) with the activation \( z_i^l \) for a given layer \( l \) and the outputs as \( y_i=a_i^l=f(z_i^l) \), with \( z_i^l \) now being a function of the weights \( w_{ij}^l \) and biases \( b_i^l \).
+We have then
+
where we have defined the targets \( t_i \). The derivatives of the cost function with respect to the output \( a_i^L \) are then easily calculated and we get
In case we use another activation function than the logistic one, we need to evaluate other derivatives.
+
+
+
The Softmax function
+
In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation \( z_i^l \), that is we need
Here we will be using the MNIST dataset, which is readily available through the scikit-learn
+package. You may also find it for example here.
+The MNIST (Modified National Institute of Standards and Technology) database is a large database
+of handwritten digits that is commonly used for training various image processing systems.
+The MNIST dataset consists of 70 000 images of size \( 28\times 28 \) pixels, each labeled from 0 to 9.
+The scikit-learn dataset we will use consists of a selection of 1797 images of size \( 8\times 8 \) collected and processed from this database.
+
+
+
To feed data into a feed-forward neural network we need to represent
+the inputs as a design/feature matrix \( X = (n_{inputs}, n_{features}) \). Each
+row represents an input, in this case a handwritten digit, and
+each column represents a feature, in this case a pixel. The
+correct answers, also known as labels or targets are
+represented as a 1D array of integers
+\( Y = (n_{inputs}) = (5, 3, 1, 8,...) \).
+
+
+
As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from
+measurements of height (in m)
+and weight (in kg). If we have measurements of 5 people the design/feature matrix could be for example:
+
Since each input image is a 2D matrix, we need to flatten the image
+(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a
+design/feature matrix. This means we lose all spatial information in the
+image, such as locality and translational invariance. More complicated
+architectures such as Convolutional Neural Networks can take advantage
+of such information, and are most commonly applied when analyzing
+images.
+
+
+
+
+
+
+
+
+
+
# import necessary packages
+importnumpyasnp
+importmatplotlib.pyplotasplt
+fromsklearnimport datasets
+
+
+# ensure the same random numbers appear every time
+np.random.seed(0)
+
+# display images in notebook
+%matplotlib inline
+plt.rcParams['figure.figsize'] = (12,12)
+
+
+# download MNIST dataset
+digits = datasets.load_digits()
+
+# define inputs and labels
+inputs = digits.images
+labels = digits.target
+
+print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
+print("labels = (n_inputs) = " + str(labels.shape))
+
+
+# flatten the image
+# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
+n_inputs = len(inputs)
+inputs = inputs.reshape(n_inputs, -1)
+print("X = (n_inputs, n_features) = " + str(inputs.shape))
+
+
+# choose some random images to display
+indices = np.arange(n_inputs)
+random_indices = np.random.choice(indices, size=5)
+
+for i, image inenumerate(digits.images[random_indices]):
+ plt.subplot(1, 5, i+1)
+ plt.axis('off')
+ plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
+ plt.title("Label: %d" % digits.target[random_indices[i]])
+plt.show()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Train and test datasets
+
+
Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions.
+
+
We will reserve \( 80 \% \) of our dataset for training and \( 20 \% \) for testing.
+
+
It is important that the train and test datasets are drawn randomly from our dataset, to ensure
+no bias in the sampling.
+Say you are taking measurements of weather data to predict the weather in the coming 5 days.
+You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data
+collected from 12.00 to 24.00.
+
Our simple feed-forward neural network will consist of an input layer, a single hidden layer and an output layer. The activation \( y \) of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have
+
+
$$ z = \sum_{i=1}^n w_i a_i ,$$
+
+
$$ y = f(z) ,$$
+
+
where \( f \) is the activation function, \( a_i \) represents input from neuron \( i \) in the preceding layer
+and \( w_i \) is the weight to input \( i \).
+The activation of the neurons in the input layer is just the features (e.g. a pixel value).
+
+
+
The simplest activation function for a neuron is the Heaviside function:
A feed-forward neural network with this activation is known as a perceptron.
+For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer.
+This activation can be generalized to \( k \) classes (using e.g. the one-against-all strategy),
+and we call these architectures multiclass perceptrons.
+
+
+
However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and
+Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function.
+
+
+
Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU).
+We will be using the sigmoid function \( \sigma(x) \):
+
+
+
$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$
+
+
which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions.
+
+
+
Layers
+
+
+
Input
+
+
Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons.
+
+
+
Hidden layer
+
+
We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer.
+Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer.
+
+
+
+
Output
+
+
If we were building a binary classifier, it would be sufficient with a single neuron in the output layer,
+which could output 0 or 1 according to the Heaviside function. This would be an example of a hard classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a soft classifier, which outputs the probability of being in class 0 or 1.
+
+
+
For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class.
+
+
Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons \( j = 0,1,...,9 \). The activation of each output neuron \( j \) will be according to the softmax function:
i.e. each neuron \( j \) outputs the probability of being in class \( j \) given an input from the hidden layer \( \boldsymbol{a} \), with \( \boldsymbol{w}_j \) the weights of neuron \( j \) to the inputs.
+The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1.
+The exponent is just the weighted sum of inputs as before:
+
+
+
$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$
+
+
Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500
+weights to the output layer.
+
+
+
+
Weights and biases
+
+
Typically weights are initialized with small values distributed around zero, drawn from a uniform
+or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless.
+
+
+
Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range
+of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron \( j \), \( b_j \):
+
+
+
$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$
+
+
The bias weights \( \boldsymbol{b} \) are often initialized to zero, but a small value like \( 0.01 \) ensures all neurons have some output which can be backpropagated in the first training cycle.
+
+
+
+
+
+
+
+
# building our neural network
+
+n_inputs, n_features = X_train.shape
+n_hidden_neurons = 50
+n_categories = 10
+
+# we make the weights normally distributed using numpy.random.randn
+
+# weights and bias in the hidden layer
+hidden_weights = np.random.randn(n_features, n_hidden_neurons)
+hidden_bias = np.zeros(n_hidden_neurons) + 0.01
+
+# weights and bias in the output layer
+output_weights = np.random.randn(n_hidden_neurons, n_categories)
+output_bias = np.zeros(n_categories) + 0.01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Feed-forward pass
+
+
Denote \( F \) the number of features, \( H \) the number of hidden neurons and \( C \) the number of categories.
+For each input image we calculate a weighted sum of input features (pixel values) to each neuron \( j \) in the hidden layer \( l \):
+
Since our data has the dimensions \( X = (n_{inputs}, n_{features}) \) and our weights to the hidden
+layer have the dimensions
+\( W_{hidden} = (n_{features}, n_{hidden}) \),
+we can easily feed the network all our training data in one go by taking the matrix product
+
+
+
$$ X W^{h} = (n_{inputs}, n_{hidden}),$$
+
+
and obtain a matrix that holds the weighted sum of inputs to the hidden layer
+for each input image and each hidden neuron.
+We also add the bias to obtain a matrix of weighted sums to the hidden layer \( Z^{h} \):
+
# setup the feed-forward pass, subscript h = hidden layer
+
+defsigmoid(x):
+ return1/(1 + np.exp(-x))
+
+deffeed_forward(X):
+ # weighted sum of inputs to the hidden layer
+ z_h = np.matmul(X, hidden_weights) + hidden_bias
+ # activation in the hidden layer
+ a_h = sigmoid(z_h)
+
+ # weighted sum of inputs to the output layer
+ z_o = np.matmul(a_h, output_weights) + output_bias
+ # softmax output
+ # axis 0 holds each input and axis 1 the probabilities of each category
+ exp_term = np.exp(z_o)
+ probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
+
+ return probabilities
+
+probabilities = feed_forward(X_train)
+print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape))
+print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0]))
+print("probabilities sum up to: " + str(probabilities[0].sum()))
+print()
+
+# we obtain a prediction by taking the class with the highest likelihood
+defpredict(X):
+ probabilities = feed_forward(X)
+ return np.argmax(probabilities, axis=1)
+
+predictions = predict(X_train)
+print("predictions = (n_inputs) = " + str(predictions.shape))
+print("prediction for image 0: " + str(predictions[0]))
+print("correct label for image 0: " + str(Y_train[0]))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose cost function and optimizer
+
+
To measure how well our neural network is doing we need to introduce a cost function.
+We will call the function that gives the error of a single sample output the loss function, and the function
+that gives the total error of our network across all samples the cost function.
+A typical choice for multiclass classification is the cross-entropy loss, also known as the negative log likelihood.
+
+
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset.
+
+
Let \( y_{ic} \) denote the \( c \)-th component of the \( i \)-th one-hot vector.
+We define the cost function \( \mathcal{C} \) as a sum over the cross-entropy loss for each point \( \boldsymbol{x}_i \) in the dataset.
+
+
+
In the one-hot representation only one of the terms in the loss function is non-zero, namely the
+probability of the correct category \( c' \)
+(i.e. the category \( c' \) such that \( y_{ic'} = 1 \)). This means that the cross entropy loss only punishes you for how wrong
+you got the correct label. The probability of category \( c \) is given by the softmax function. The vector \( \boldsymbol{\theta} \) represents the parameters of our network, i.e. all the weights and biases.
+
+
+
+
+
Optimizing the cost function
+
+
The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is gradient descent and its generalizations. The idea behind gradient descent
+is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a local minimum of the cost function.
+Each parameter \( \theta \) is iteratively adjusted according to the rule
+
where \( \eta \) is known as the learning rate, which controls how big a step we take towards the minimum.
+This update can be repeated for any number of iterations, or until we are satisfied with the result.
+
+
+
A simple and effective improvement is a variant called Batch Gradient Descent.
+Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient
+on a subset of the data called a minibatch.
+If there are \( N \) data points and we have a minibatch size of \( M \), the total number of batches
+is \( N/M \).
+We denote each minibatch \( B_k \), with \( k = 1, 2,...,N/M \). The gradient then becomes:
+
i.e. instead of averaging the loss over the entire dataset, we average over a minibatch.
+
+
This has two important benefits:
+
+
Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima.
+
It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient.
+
+
The various optmization methods, with codes and algorithms, are discussed in our lectures on Gradient descent approaches.
+
+
+
Regularization
+
+
It is common to add an extra term to the cost function, proportional
+to the size of the weights. This is equivalent to constraining the
+size of the weights, so that they do not grow out of control.
+Constraining the size of the weights means that the weights cannot
+grow arbitrarily large to fit the training data, and in this way
+reduces overfitting.
+
+
+
We will measure the size of the weights using the so called L2-norm, meaning our cost function becomes:
i.e. we sum up all the weights squared. The factor \( \lambda \) is known as a regularization parameter.
+
+
In order to train the model, we need to calculate the derivative of
+the cost function with respect to every bias and weight in the
+network. In total our network has \( (64 + 1)\times 50=3250 \) weights in
+the hidden layer and \( (50 + 1)\times 10=510 \) weights to the output
+layer (\( +1 \) for the bias), and the gradient must be calculated for
+every parameter. We use the backpropagation algorithm discussed
+above. This is a clever use of the chain rule that allows us to
+calculate the gradient efficently.
+
+
+
+
Matrix multiplication
+
+
To more efficently train our network these equations are implemented using matrix operations.
+The error in the output layer is calculated simply as, with \( \boldsymbol{t} \) being our targets,
+
where \( \boldsymbol{a} = (n_{inputs}, n_{hidden}) \). This simply means that we are summing up the gradients for each input.
+Since we are going backwards we have to transpose the activation matrix.
+
+
+
The gradient with respect to the output bias is then
where \( f'(a_{h}) \) is the derivative of the activation in the hidden layer. The matrix products mean
+that we are summing up the products for each neuron in the output layer. The symbol \( \circ \) denotes
+the Hadamard product, meaning element-wise multiplication.
+
+
+
This again gives us the gradients in the hidden layer:
# to categorical turns our integer vector into a onehot representation
+fromsklearn.metricsimport accuracy_score
+
+# one-hot in numpy
+defto_categorical_numpy(integer_vector):
+ n_inputs = len(integer_vector)
+ n_categories = np.max(integer_vector) + 1
+ onehot_vector = np.zeros((n_inputs, n_categories))
+ onehot_vector[range(n_inputs), integer_vector] = 1
+
+ return onehot_vector
+
+#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test)
+Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test)
+
+deffeed_forward_train(X):
+ # weighted sum of inputs to the hidden layer
+ z_h = np.matmul(X, hidden_weights) + hidden_bias
+ # activation in the hidden layer
+ a_h = sigmoid(z_h)
+
+ # weighted sum of inputs to the output layer
+ z_o = np.matmul(a_h, output_weights) + output_bias
+ # softmax output
+ # axis 0 holds each input and axis 1 the probabilities of each category
+ exp_term = np.exp(z_o)
+ probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
+
+ # for backpropagation need activations in hidden and output layers
+ return a_h, probabilities
+
+defbackpropagation(X, Y):
+ a_h, probabilities = feed_forward_train(X)
+
+ # error in the output layer
+ error_output = probabilities - Y
+ # error in the hidden layer
+ error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h)
+
+ # gradients for the output layer
+ output_weights_gradient = np.matmul(a_h.T, error_output)
+ output_bias_gradient = np.sum(error_output, axis=0)
+
+ # gradient for the hidden layer
+ hidden_weights_gradient = np.matmul(X.T, error_hidden)
+ hidden_bias_gradient = np.sum(error_hidden, axis=0)
+
+ return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient
+
+print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
+
+eta = 0.01
+lmbd = 0.01
+for i inrange(1000):
+ # calculate gradients
+ dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot)
+
+ # regularization term gradients
+ dWo += lmbd * output_weights
+ dWh += lmbd * hidden_weights
+
+ # update weights and biases
+ output_weights -= eta * dWo
+ output_bias -= eta * dBo
+ hidden_weights -= eta * dWh
+ hidden_bias -= eta * dBh
+
+print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Improving performance
+
+
As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image.
+In order to obtain a network that does something useful, we will have to do a bit more work.
+
+
+
The choice of hyperparameters such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a grid-search is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates \( \eta = 10^{-6}, 10^{-5},...,10^{-1} \) with different regularization parameters \( \lambda = 10^{-6},...,10^{-0} \).
+
+
Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an iteration, and a full training period
+going through the entire dataset (\( n/M \) batches) an epoch.
+
+
+
If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers.
+Andrew Ng goes through some of these considerations in this video. You can find a summary of the video here.
+
+
+
+
Full object-oriented implementation
+
+
It is very natural to think of the network as an object, with specific instances of the network
+being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below.
+
To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data.
+We measure the performance of the network using the accuracy score.
+The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of \( 1 \).
+
We now perform a grid search to find the optimal hyperparameters for the network.
+Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around \( 98\% \) (\( 2\% \) error rate).
+
+
+
+
+
+
+
+
+
+
eta_vals = np.logspace(-5, 1, 7)
+lmbd_vals = np.logspace(-5, 1, 7)
+# store the models for later use
+DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
+
+# grid search
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
+ n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
+ dnn.train()
+
+ DNN_numpy[i][j] = dnn
+
+ test_predict = dnn.predict(X_test)
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
+ print()
+
scikit-learn focuses more
+on traditional machine learning methods, such as regression,
+clustering, decision trees, etc. As such, it has only two types of
+neural networks: Multi Layer Perceptron outputting continuous values,
+MPLRegressor, and Multi Layer Perceptron outputting labels,
+MLPClassifier. We will see how simple it is to use these classes.
+
+
+
scikit-learn implements a few improvements from our neural network,
+such as early stopping, a varying learning rate, different
+optimization methods, etc. We would therefore expect a better
+performance overall.
+
+
+
+
+
+
+
+
+
+
fromsklearn.neural_networkimport MLPClassifier
+# store models for later use
+DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
+
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',
+ alpha=lmbd, learning_rate_init=eta, max_iter=epochs)
+ dnn.fit(X_train, Y_train)
+
+ DNN_scikit[i][j] = dnn
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", dnn.score(X_test, Y_test))
+ print()
+
Thursday: Repetion and summary of Stochastic Gradient descent with examples and automatic differentiation and begin Neural Networks.
-
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model.
+
Friday: Neural Networks, setting up the basic steps, from the simple perceptron model to the multi-layer perceptron model. Presentation of project 2.
Reading suggestions for both days: Aurelien Geron's chapter 10 and Hastie et al chapter 11.
For Stochastic Gradient Descent, we recommend chapter 4 of Geron's text.
@@ -1439,6 +1500,1303 @@ $$
Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training.
+
+
Setting up the Back propagation algorithm
+
+
The four equations above provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm.
+
+
+
+
+
First, we set up the input data \( \boldsymbol{x} \) and the activations
+\( \boldsymbol{z}_1 \) of the input layer and compute the activation function and
+the pertinent outputs \( \boldsymbol{a}^1 \).
+
+
+
+
+
+
+
+
Secondly, we perform then the feed forward till we reach the output
+layer and compute all \( \boldsymbol{z}_l \) of the input layer and compute the
+activation function and the pertinent outputs \( \boldsymbol{a}^l \) for
+\( l=2,3,\dots,L \).
+
+
+
+
+
+
+
+
Thereafter we compute the ouput error \( \boldsymbol{\delta}^L \) by computing all
Finally, we update the weights and the biases using gradient descent for each \( l=L-1,L-2,\dots,2 \) and update the weights and biases according to the rules
The parameter \( \eta \) is the learning parameter discussed in connection with the gradient descent methods.
+Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training.
+
+
+
+
Setting up a Multi-layer perceptron model for classification
+
+
We are now gong to develop an example based on the MNIST data
+base. This is a classification problem and we need to use our
+cross-entropy function we discussed in connection with logistic
+regression. The cross-entropy defines our cost function for the
+classificaton problems with neural networks.
+
+
+
In binary classification with two classes \( (0, 1) \) we define the
+logistic/sigmoid function as the probability that a particular input
+is in class \( 0 \) or \( 1 \). This is possible because the logistic
+function takes any input from the real numbers and inputs a number
+between 0 and 1, and can therefore be interpreted as a probability. It
+also has other nice properties, such as a derivative that is simple to
+calculate.
+
+
+
For an input \( \boldsymbol{a} \) from the hidden layer, the probability that the input \( \boldsymbol{x} \)
+is in class 0 or 1 is just. We let \( \theta \) represent the unknown weights and biases to be adjusted by our equations). The variable \( x \)
+represents our activation values \( z \). We have
+
This last equality means that we can interpret our cost function as a sum over the loss function
+for each point in the dataset \( \mathcal{L}_i(\boldsymbol{\theta}) \).
+The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather
+than maximizing a negative number.
+
+
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset (numbers from \( 0 \) to \( 9 \))..
+
+
If \( \boldsymbol{x}_i \) is the \( i \)-th input (image), \( y_{ic} \) refers to the \( c \)-th component of the \( i \)-th
+output vector \( \boldsymbol{y}_i \).
+The probability of \( \boldsymbol{x}_i \) being in class \( c \) will be given by the softmax function:
+
See the logistic regression lectures for a full definition of the cost function.
+
+
The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before!
+
+
+
Example: binary classification problem
+
+
As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters \( \beta \) as
The parameters \( \boldsymbol{\beta} \) were defined using a minimization method like gradient descent or Newton-Raphson's method.
+
+
Now we replace \( x_i \) with the activation \( z_i^l \) for a given layer \( l \) and the outputs as \( y_i=a_i^l=f(z_i^l) \), with \( z_i^l \) now being a function of the weights \( w_{ij}^l \) and biases \( b_i^l \).
+We have then
+
where we have defined the targets \( t_i \). The derivatives of the cost function with respect to the output \( a_i^L \) are then easily calculated and we get
In case we use another activation function than the logistic one, we need to evaluate other derivatives.
+
+
+
The Softmax function
+
In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation \( z_i^l \), that is we need
Here we will be using the MNIST dataset, which is readily available through the scikit-learn
+package. You may also find it for example here.
+The MNIST (Modified National Institute of Standards and Technology) database is a large database
+of handwritten digits that is commonly used for training various image processing systems.
+The MNIST dataset consists of 70 000 images of size \( 28\times 28 \) pixels, each labeled from 0 to 9.
+The scikit-learn dataset we will use consists of a selection of 1797 images of size \( 8\times 8 \) collected and processed from this database.
+
+
+
To feed data into a feed-forward neural network we need to represent
+the inputs as a design/feature matrix \( X = (n_{inputs}, n_{features}) \). Each
+row represents an input, in this case a handwritten digit, and
+each column represents a feature, in this case a pixel. The
+correct answers, also known as labels or targets are
+represented as a 1D array of integers
+\( Y = (n_{inputs}) = (5, 3, 1, 8,...) \).
+
+
+
As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from
+measurements of height (in m)
+and weight (in kg). If we have measurements of 5 people the design/feature matrix could be for example:
+
Since each input image is a 2D matrix, we need to flatten the image
+(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a
+design/feature matrix. This means we lose all spatial information in the
+image, such as locality and translational invariance. More complicated
+architectures such as Convolutional Neural Networks can take advantage
+of such information, and are most commonly applied when analyzing
+images.
+
+
+
+
+
+
+
+
+
+
# import necessary packages
+importnumpyasnp
+importmatplotlib.pyplotasplt
+fromsklearnimport datasets
+
+
+# ensure the same random numbers appear every time
+np.random.seed(0)
+
+# display images in notebook
+%matplotlib inline
+plt.rcParams['figure.figsize'] = (12,12)
+
+
+# download MNIST dataset
+digits = datasets.load_digits()
+
+# define inputs and labels
+inputs = digits.images
+labels = digits.target
+
+print("inputs = (n_inputs, pixel_width, pixel_height) = "+str(inputs.shape))
+print("labels = (n_inputs) = "+str(labels.shape))
+
+
+# flatten the image
+# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
+n_inputs =len(inputs)
+inputs = inputs.reshape(n_inputs, -1)
+print("X = (n_inputs, n_features) = "+str(inputs.shape))
+
+
+# choose some random images to display
+indices = np.arange(n_inputs)
+random_indices = np.random.choice(indices, size=5)
+
+for i, image inenumerate(digits.images[random_indices]):
+ plt.subplot(1, 5, i+1)
+ plt.axis('off')
+ plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
+ plt.title("Label: %d"% digits.target[random_indices[i]])
+plt.show()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Train and test datasets
+
+
Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions.
+
+
We will reserve \( 80 \% \) of our dataset for training and \( 20 \% \) for testing.
+
+
It is important that the train and test datasets are drawn randomly from our dataset, to ensure
+no bias in the sampling.
+Say you are taking measurements of weather data to predict the weather in the coming 5 days.
+You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data
+collected from 12.00 to 24.00.
+
Our simple feed-forward neural network will consist of an input layer, a single hidden layer and an output layer. The activation \( y \) of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have
+
+
$$ z = \sum_{i=1}^n w_i a_i ,$$
+
+
$$ y = f(z) ,$$
+
+
where \( f \) is the activation function, \( a_i \) represents input from neuron \( i \) in the preceding layer
+and \( w_i \) is the weight to input \( i \).
+The activation of the neurons in the input layer is just the features (e.g. a pixel value).
+
+
+
The simplest activation function for a neuron is the Heaviside function:
A feed-forward neural network with this activation is known as a perceptron.
+For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer.
+This activation can be generalized to \( k \) classes (using e.g. the one-against-all strategy),
+and we call these architectures multiclass perceptrons.
+
+
+
However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and
+Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function.
+
+
+
Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU).
+We will be using the sigmoid function \( \sigma(x) \):
+
+
+
$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$
+
+
which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions.
+
+
+
Layers
+
+
+
Input
+
+
Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons.
+
+
+
Hidden layer
+
+
We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer.
+Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer.
+
+
+
+
Output
+
+
If we were building a binary classifier, it would be sufficient with a single neuron in the output layer,
+which could output 0 or 1 according to the Heaviside function. This would be an example of a hard classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a soft classifier, which outputs the probability of being in class 0 or 1.
+
+
+
For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class.
+
+
Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons \( j = 0,1,...,9 \). The activation of each output neuron \( j \) will be according to the softmax function:
i.e. each neuron \( j \) outputs the probability of being in class \( j \) given an input from the hidden layer \( \boldsymbol{a} \), with \( \boldsymbol{w}_j \) the weights of neuron \( j \) to the inputs.
+The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1.
+The exponent is just the weighted sum of inputs as before:
+
+
+
$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$
+
+
Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500
+weights to the output layer.
+
+
+
+
Weights and biases
+
+
Typically weights are initialized with small values distributed around zero, drawn from a uniform
+or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless.
+
+
+
Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range
+of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron \( j \), \( b_j \):
+
+
+
$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$
+
+
The bias weights \( \boldsymbol{b} \) are often initialized to zero, but a small value like \( 0.01 \) ensures all neurons have some output which can be backpropagated in the first training cycle.
+
+
+
+
+
+
+
+
# building our neural network
+
+n_inputs, n_features = X_train.shape
+n_hidden_neurons =50
+n_categories =10
+
+# we make the weights normally distributed using numpy.random.randn
+
+# weights and bias in the hidden layer
+hidden_weights = np.random.randn(n_features, n_hidden_neurons)
+hidden_bias = np.zeros(n_hidden_neurons) +0.01
+
+# weights and bias in the output layer
+output_weights = np.random.randn(n_hidden_neurons, n_categories)
+output_bias = np.zeros(n_categories) +0.01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Feed-forward pass
+
+
Denote \( F \) the number of features, \( H \) the number of hidden neurons and \( C \) the number of categories.
+For each input image we calculate a weighted sum of input features (pixel values) to each neuron \( j \) in the hidden layer \( l \):
+
Since our data has the dimensions \( X = (n_{inputs}, n_{features}) \) and our weights to the hidden
+layer have the dimensions
+\( W_{hidden} = (n_{features}, n_{hidden}) \),
+we can easily feed the network all our training data in one go by taking the matrix product
+
+
+
$$ X W^{h} = (n_{inputs}, n_{hidden}),$$
+
+
and obtain a matrix that holds the weighted sum of inputs to the hidden layer
+for each input image and each hidden neuron.
+We also add the bias to obtain a matrix of weighted sums to the hidden layer \( Z^{h} \):
+
# setup the feed-forward pass, subscript h = hidden layer
+
+defsigmoid(x):
+ return1/(1+ np.exp(-x))
+
+deffeed_forward(X):
+ # weighted sum of inputs to the hidden layer
+ z_h = np.matmul(X, hidden_weights) + hidden_bias
+ # activation in the hidden layer
+ a_h = sigmoid(z_h)
+
+ # weighted sum of inputs to the output layer
+ z_o = np.matmul(a_h, output_weights) + output_bias
+ # softmax output
+ # axis 0 holds each input and axis 1 the probabilities of each category
+ exp_term = np.exp(z_o)
+ probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
+
+ return probabilities
+
+probabilities = feed_forward(X_train)
+print("probabilities = (n_inputs, n_categories) = "+str(probabilities.shape))
+print("probability that image 0 is in category 0,1,2,...,9 = \n"+str(probabilities[0]))
+print("probabilities sum up to: "+str(probabilities[0].sum()))
+print()
+
+# we obtain a prediction by taking the class with the highest likelihood
+defpredict(X):
+ probabilities = feed_forward(X)
+ return np.argmax(probabilities, axis=1)
+
+predictions = predict(X_train)
+print("predictions = (n_inputs) = "+str(predictions.shape))
+print("prediction for image 0: "+str(predictions[0]))
+print("correct label for image 0: "+str(Y_train[0]))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose cost function and optimizer
+
+
To measure how well our neural network is doing we need to introduce a cost function.
+We will call the function that gives the error of a single sample output the loss function, and the function
+that gives the total error of our network across all samples the cost function.
+A typical choice for multiclass classification is the cross-entropy loss, also known as the negative log likelihood.
+
+
+
In multiclass classification it is common to treat each integer label as a so called one-hot vector:
i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset.
+
+
Let \( y_{ic} \) denote the \( c \)-th component of the \( i \)-th one-hot vector.
+We define the cost function \( \mathcal{C} \) as a sum over the cross-entropy loss for each point \( \boldsymbol{x}_i \) in the dataset.
+
+
+
In the one-hot representation only one of the terms in the loss function is non-zero, namely the
+probability of the correct category \( c' \)
+(i.e. the category \( c' \) such that \( y_{ic'} = 1 \)). This means that the cross entropy loss only punishes you for how wrong
+you got the correct label. The probability of category \( c \) is given by the softmax function. The vector \( \boldsymbol{\theta} \) represents the parameters of our network, i.e. all the weights and biases.
+
+
+
+
+
Optimizing the cost function
+
+
The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is gradient descent and its generalizations. The idea behind gradient descent
+is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a local minimum of the cost function.
+Each parameter \( \theta \) is iteratively adjusted according to the rule
+
where \( \eta \) is known as the learning rate, which controls how big a step we take towards the minimum.
+This update can be repeated for any number of iterations, or until we are satisfied with the result.
+
+
+
A simple and effective improvement is a variant called Batch Gradient Descent.
+Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient
+on a subset of the data called a minibatch.
+If there are \( N \) data points and we have a minibatch size of \( M \), the total number of batches
+is \( N/M \).
+We denote each minibatch \( B_k \), with \( k = 1, 2,...,N/M \). The gradient then becomes:
+
i.e. instead of averaging the loss over the entire dataset, we average over a minibatch.
+
+
This has two important benefits:
+
+
Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima.
+
It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient.
+
+
The various optmization methods, with codes and algorithms, are discussed in our lectures on Gradient descent approaches.
+
+
+
Regularization
+
+
It is common to add an extra term to the cost function, proportional
+to the size of the weights. This is equivalent to constraining the
+size of the weights, so that they do not grow out of control.
+Constraining the size of the weights means that the weights cannot
+grow arbitrarily large to fit the training data, and in this way
+reduces overfitting.
+
+
+
We will measure the size of the weights using the so called L2-norm, meaning our cost function becomes:
i.e. we sum up all the weights squared. The factor \( \lambda \) is known as a regularization parameter.
+
+
In order to train the model, we need to calculate the derivative of
+the cost function with respect to every bias and weight in the
+network. In total our network has \( (64 + 1)\times 50=3250 \) weights in
+the hidden layer and \( (50 + 1)\times 10=510 \) weights to the output
+layer (\( +1 \) for the bias), and the gradient must be calculated for
+every parameter. We use the backpropagation algorithm discussed
+above. This is a clever use of the chain rule that allows us to
+calculate the gradient efficently.
+
+
+
+
Matrix multiplication
+
+
To more efficently train our network these equations are implemented using matrix operations.
+The error in the output layer is calculated simply as, with \( \boldsymbol{t} \) being our targets,
+
where \( \boldsymbol{a} = (n_{inputs}, n_{hidden}) \). This simply means that we are summing up the gradients for each input.
+Since we are going backwards we have to transpose the activation matrix.
+
+
+
The gradient with respect to the output bias is then
where \( f'(a_{h}) \) is the derivative of the activation in the hidden layer. The matrix products mean
+that we are summing up the products for each neuron in the output layer. The symbol \( \circ \) denotes
+the Hadamard product, meaning element-wise multiplication.
+
+
+
This again gives us the gradients in the hidden layer:
# to categorical turns our integer vector into a onehot representation
+fromsklearn.metricsimport accuracy_score
+
+# one-hot in numpy
+defto_categorical_numpy(integer_vector):
+ n_inputs =len(integer_vector)
+ n_categories = np.max(integer_vector) +1
+ onehot_vector = np.zeros((n_inputs, n_categories))
+ onehot_vector[range(n_inputs), integer_vector] =1
+
+ return onehot_vector
+
+#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test)
+Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test)
+
+deffeed_forward_train(X):
+ # weighted sum of inputs to the hidden layer
+ z_h = np.matmul(X, hidden_weights) + hidden_bias
+ # activation in the hidden layer
+ a_h = sigmoid(z_h)
+
+ # weighted sum of inputs to the output layer
+ z_o = np.matmul(a_h, output_weights) + output_bias
+ # softmax output
+ # axis 0 holds each input and axis 1 the probabilities of each category
+ exp_term = np.exp(z_o)
+ probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
+
+ # for backpropagation need activations in hidden and output layers
+ return a_h, probabilities
+
+defbackpropagation(X, Y):
+ a_h, probabilities = feed_forward_train(X)
+
+ # error in the output layer
+ error_output = probabilities - Y
+ # error in the hidden layer
+ error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1- a_h)
+
+ # gradients for the output layer
+ output_weights_gradient = np.matmul(a_h.T, error_output)
+ output_bias_gradient = np.sum(error_output, axis=0)
+
+ # gradient for the hidden layer
+ hidden_weights_gradient = np.matmul(X.T, error_hidden)
+ hidden_bias_gradient = np.sum(error_hidden, axis=0)
+
+ return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient
+
+print("Old accuracy on training data: "+str(accuracy_score(predict(X_train), Y_train)))
+
+eta =0.01
+lmbd =0.01
+for i inrange(1000):
+ # calculate gradients
+ dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot)
+
+ # regularization term gradients
+ dWo += lmbd * output_weights
+ dWh += lmbd * hidden_weights
+
+ # update weights and biases
+ output_weights -= eta * dWo
+ output_bias -= eta * dBo
+ hidden_weights -= eta * dWh
+ hidden_bias -= eta * dBh
+
+print("New accuracy on training data: "+str(accuracy_score(predict(X_train), Y_train)))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Improving performance
+
+
As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image.
+In order to obtain a network that does something useful, we will have to do a bit more work.
+
+
+
The choice of hyperparameters such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a grid-search is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates \( \eta = 10^{-6}, 10^{-5},...,10^{-1} \) with different regularization parameters \( \lambda = 10^{-6},...,10^{-0} \).
+
+
Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an iteration, and a full training period
+going through the entire dataset (\( n/M \) batches) an epoch.
+
+
+
If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers.
+Andrew Ng goes through some of these considerations in this video. You can find a summary of the video here.
+
+
+
+
Full object-oriented implementation
+
+
It is very natural to think of the network as an object, with specific instances of the network
+being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below.
+
To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data.
+We measure the performance of the network using the accuracy score.
+The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of \( 1 \).
+
where \( I \) is the indicator function, \( 1 \) if \( \tilde{y}_i = y_i \) and \( 0 \) otherwise.
+
+
+
+
+
+
+
+
+
epochs =100
+batch_size =100
+
+dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
+ n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
+dnn.train()
+test_predict = dnn.predict(X_test)
+
+# accuracy score from scikit library
+print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
+
+# equivalent in numpy
+defaccuracy_score_numpy(Y_test, Y_pred):
+ return np.sum(Y_test == Y_pred) /len(Y_test)
+
+#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Adjust hyperparameters
+
+
We now perform a grid search to find the optimal hyperparameters for the network.
+Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around \( 98\% \) (\( 2\% \) error rate).
+
+
+
+
+
+
+
+
+
+
eta_vals = np.logspace(-5, 1, 7)
+lmbd_vals = np.logspace(-5, 1, 7)
+# store the models for later use
+DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
+
+# grid search
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
+ n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
+ dnn.train()
+
+ DNN_numpy[i][j] = dnn
+
+ test_predict = dnn.predict(X_test)
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
+ print()
+
scikit-learn focuses more
+on traditional machine learning methods, such as regression,
+clustering, decision trees, etc. As such, it has only two types of
+neural networks: Multi Layer Perceptron outputting continuous values,
+MPLRegressor, and Multi Layer Perceptron outputting labels,
+MLPClassifier. We will see how simple it is to use these classes.
+
+
+
scikit-learn implements a few improvements from our neural network,
+such as early stopping, a varying learning rate, different
+optimization methods, etc. We would therefore expect a better
+performance overall.
+
+
+
+
+
+
+
+
+
+
fromsklearn.neural_networkimport MLPClassifier
+# store models for later use
+DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
+
+for i, eta inenumerate(eta_vals):
+ for j, lmbd inenumerate(lmbd_vals):
+ dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',
+ alpha=lmbd, learning_rate_init=eta, max_iter=epochs)
+ dnn.fit(X_train, Y_train)
+
+ DNN_scikit[i][j] = dnn
+
+ print("Learning rate = ", eta)
+ print("Lambda = ", lmbd)
+ print("Accuracy score on test set: ", dnn.score(X_test, Y_test))
+ print()
+