TheAnig

Back

First project for CSE 5526: Introduction to Neural Networks (Autumn 2018). After the homeworks covered M-P neurons and the perceptron learning rule, this project had us implement a full multi-layer perceptron (MLP) from scratch and train it with backpropagation.

The N-Bit Parity Problem#

The N-bit parity function outputs 1 if the number of 1-bits in the input is odd, and 0 otherwise. For example, with N=4N = 4:

  • 01010101 (two 1-bits) \rightarrow 0 (even parity)
  • 01110111 (three 1-bits) \rightarrow 1 (odd parity)

What makes this hard for a neural network is that every input bit matters, and flipping any single bit changes the output. There’s no local structure to exploit. The network has to learn a truly global function over all inputs, which is why parity is a standard benchmark for training algorithms.

For N=4N = 4, the training set is all 24=162^4 = 16 binary numbers (0 through 15).

Network Architecture#

The MLP uses a two-layer feedforward architecture with sigmoid activation:

Layer 1 (hidden): Takes the NN-bit input (plus a bias term) and produces NN hidden units.

  • Weight matrix: (N+1)×N(N+1) \times N

Layer 2 (output): Takes the hidden layer output (plus a bias term) and produces a single output.

  • Weight matrix: (N+1)×1(N+1) \times 1

Forward propagation:

h=σ(W0Tx)(hidden layer)h = \sigma(W_0^T \cdot x) \quad \text{(hidden layer)} y=σ(W1T[1;h])(output layer)y = \sigma(W_1^T \cdot [1; h]) \quad \text{(output layer)}

where σ(z)=11+ez\sigma(z) = \frac{1}{1 + e^{-z}} is the sigmoid activation function, and [1;h][1; h] denotes prepending a bias term.

Backward propagation computes the error gradients using the sigmoid derivative σ(z)=σ(z)(1σ(z))\sigma'(z) = \sigma(z)(1 - \sigma(z)), which simplifies nicely when expressed in terms of the output: y=y(1y)y' = y(1 - y).

For the output layer:

δk=yk(1yk)(dkyk)\delta_k = y_k(1 - y_k)(d_k - y_k)

For the hidden layer:

δj=hj(1hj)W1[1:]δk\delta_j = h_j(1 - h_j) \cdot W_1[1:] \cdot \delta_k

Weight updates incorporate optional momentum (α\alpha):

ΔW(t)=ηδxT+αΔW(t1)\Delta W(t) = \eta \cdot \delta \cdot x^T + \alpha \cdot \Delta W(t-1)

Convergence criterion: All 16 outputs must be within 0.05 of their desired values.

Implementation#

The implementation is a self-contained MultiLayerPerceptron class in Python/NumPy:

I also parallelized the training using Python’s multiprocessing module, since each configuration (learning rate + momentum combination) is independent and can run on its own core.

Experimental Results#

I swept learning rates from 0.05 to 0.50 (in steps of 0.05), both without momentum (α=0\alpha = 0) and with momentum (α=0.9\alpha = 0.9).

Without Momentum (α=0\alpha = 0)#

η\etaEpochs
0.0535,869
0.1016,477
0.1512,681
0.209,541
0.258,719
0.306,180
0.355,319
0.405,121
0.456,683
0.50120,389

Epochs vs learning rate without momentum

Epochs to convergence as a function of learning rate (no momentum). The optimal learning rate is 0.40, with a sharp blowup at 0.50.

There’s a clear sweet spot. Increasing η\eta from 0.05 to 0.40 steadily reduces the number of epochs needed. But past 0.40, the step size gets too large and the network overcompensates for each error, causing weight oscillations that slow convergence way down. At η=0.50\eta = 0.50, it takes over 120,000 epochs, which is a 23x regression from the optimal.

With Momentum (α=0.9\alpha = 0.9)#

η\etaEpochs
0.053,206
0.101,791
0.151,139
0.20975
0.25878
0.30623
0.35547
0.40514
0.455,197
0.509,878

Epochs vs learning rate with momentum

Epochs to convergence with momentum (alpha = 0.9). Same optimal learning rate, but roughly 10x fewer epochs across the board.

Momentum gives a consistent and big speedup without changing the overall shape of the curve. The optimal η\eta is still 0.40, but the network converges in 514 epochs instead of 5,121.

Momentum Speedup Analysis#

η\etaWithout α\alphaWith α=0.9\alpha = 0.9Speedup
0.0535,8693,20611.2x
0.1016,4771,7919.2x
0.1512,6811,13911.1x
0.209,5419759.8x
0.258,7198789.9x
0.306,1806239.9x
0.355,3195479.7x
0.405,12151410.0x
0.456,6835,1971.3x
0.50120,3899,87812.2x

Speedup from momentum

Speedup factor from momentum as a function of learning rate. Consistent ~10x improvement, with a weird dip at eta = 0.45.

The speedup is pretty consistent at around 10x across most learning rates. The weird outlier is η=0.45\eta = 0.45, where momentum only gives a 1.3x speedup. My guess is that this is a transitional zone: the learning rate is just large enough to cause instability on its own, and momentum (which increases the effective step size) tips it over into oscillatory behavior. What’s interesting is that at the even higher η=0.50\eta = 0.50, momentum gets its 12x speedup back, which suggests a different dynamical regime.

What I Took Away#

Learning rate turned out to be the most important hyperparameter by far. The difference between a good and bad choice was orders of magnitude in convergence time, and the transition from “working fine” to “taking forever” was pretty abrupt (0.40 to 0.50).

Momentum gave a solid ~10x speedup most of the time, but it’s not a free parameter to crank up. The η=0.45\eta = 0.45 case showed that momentum and learning rate can interact badly: the combined effective step size is what actually matters, not either parameter in isolation.

The parity problem itself was a good choice for this project. Because every input bit affects the output, every neuron has to participate in every decision. You can’t get away with some neurons being “lazy.” I also appreciated having to implement backpropagation by hand, computing δk\delta_k for the output layer, propagating back through the hidden layer, wiring in momentum. It’s not hard to use a framework for this stuff, but doing it manually once made the algorithm feel a lot more concrete.

Backpropagation and the N-Bit Parity Problem
https://theanig.dev/blog/intro-to-nn-project-1
Author Anirudh Ganesh
Published at September 27, 2018