Why plain Hebbian learning runs away, and two ways to stop it: hard bounds and Oja’s rule, drawn as trajectories in the plane of two synaptic weights.
“Cells that fire together wire together” is the shortest summary of Hebb’s (1949) rule, and it is a good one. Taken literally, though, the rule has a problem: nothing in it ever makes a weight smaller. Weights only grow, and they grow faster the larger they already are. Every textbook on learning spends a chapter on ways to tame this, and this page shows two of them side by side.
To keep things drawable, the neuron has only two inputs, so its two weights \(w_1\) and \(w_2\) form a plane and learning becomes a path through that plane. Everything runs in your browser. Pick a rule, click anywhere in the plane to choose a starting point, and watch where the weights go.
Code
exFont = ({size:getComputedStyle(document.body).fontSize,family:getComputedStyle(document.body).fontFamily,toString() { returnthis.size+" "+this.family; }})exFg = () =>getComputedStyle(document.body).colormkCanvas = (W, H) => {const c = DOM.context2d(W, H); c.canvas.style.width="100%"; c.canvas.style.maxWidth= W +"px"; c.canvas.style.height="auto";return c;}axisBox = (c, x, y, w, h) => { c.strokeStyle="#999"; c.lineWidth=1; c.strokeRect(x, y, w, h); }
The three rules
A single neuron receives two inputs with activities \(u_1\) and \(u_2\) through weights \(w = (w_1, w_2)\), and its output is \(v = w^{\mathsf T} u\). The basic Hebbian rule says that each weight changes in proportion to the product of its input and the output, \(\tau\, dw/dt = v\,u\). We are not interested in the jittery response to individual input patterns, only in the average drift of the weights over many patterns. Averaging replaces \(v\,u\) by \(Q\,w\), where \(Q = \langle u u^{\mathsf T} \rangle\) is the correlation matrix of the inputs. For two inputs of equal strength it is
and the single number \(q\), the correlation between the two inputs, is the slider you will use most. Positive \(q\) means the inputs tend to be active together, negative \(q\) means that one is active when the other is quiet. The three rules are then
The first is Hebb’s rule as it stands. The second is the same rule with a fence around it: each weight is clipped to the interval \([0, 1]\), which is the simplest thing an implementation can do and roughly what a synapse that cannot become negative or arbitrarily strong would do. The third is Oja’s (1982) rule, which subtracts a term proportional to the output’s variance times the weight itself. The subtracted term grows faster than the Hebbian term, so it acts as a brake that kicks in once the weights are large. The parameter \(\alpha\) sets how hard the brake is, and \(\tau\) only sets the overall speed.
hbCol = ({C0:"#1f77b4",C1:"#ff7f0e",C2:"#2ca02c",K:"#444",T1:"#9467bd",T2:"#17becf"})hbLay = ({W:760,H:400,S:340,m:80,top:16}) // plane occupies x in [m, m+S], y in [top, top+S]mutable hbClick =nullhbCanvas = {const c =mkCanvas(hbLay.W, hbLay.H); c.canvas.style.cursor="crosshair"; c.canvas.addEventListener("click", ev => {const r = c.canvas.getBoundingClientRect();const x = (ev.clientX- r.left) * hbLay.W/ r.width, y = (ev.clientY- r.top) * hbLay.H/ r.height;const {m, S, top} = hbLay;if (x < m || x > m + S || y < top || y > top + S) return; mutable hbClick = {fx: (x - m) / S,fy:1- (y - top) / S}; });return c;}hbLimits = ({hebb: [-2,2],bounded: [-0.1,1.1],oja: [-1.2,1.2]})hbStart = {const [lo, hi] = hbLimits[hbRule];if (!hbClick) return [0.01,0.05];return [lo + (hi - lo) * hbClick.fx, lo + (hi - lo) * hbClick.fy];}hbStep = (w, rule, q, alpha, dt =0.1) => {const Qw = [w[0] + q * w[1], q * w[0] + w[1]];let n;if (rule ==="oja") { const wQw = w[0] * Qw[0] + w[1] * Qw[1]; n = [w[0] + dt * (Qw[0] - alpha * wQw * w[0]), w[1] + dt * (Qw[1] - alpha * wQw * w[1])]; }else n = [w[0] + dt * Qw[0], w[1] + dt * Qw[1]];if (rule ==="bounded") n = [Math.min(1,Math.max(0, n[0])),Math.min(1,Math.max(0, n[1]))];return n;}hbEndColour = (w, rule) => {const near = (a, b, tol) =>Math.abs(w[0] - a) < tol &&Math.abs(w[1] - b) < tol;if (rule ==="bounded") { if (near(1,1,1e-3)) return hbCol.C0;if (near(0,1,1e-3)) return hbCol.C1;if (near(1,0,1e-3)) return hbCol.C2; }if (rule ==="oja") { const r =Math.SQRT1_2;if (near(r, r,0.02)) return hbCol.C0;if (near(-r, r,0.02)) return hbCol.C1;if (near(r,-r,0.02)) return hbCol.C2; }return hbCol.K;}hbField = {const N =20, steps =100, trajs = [];for (let i =0; i < N; i++) for (let j =0; j < N; j++) {let w = [i / (N -1), j / (N -1)];const pts = [w];for (let s =0; s < steps; s++) { w =hbStep(w, hbRule, hbQ, hbAlpha); pts.push(w); } trajs.push({pts,col:hbEndColour(w, hbRule)}); }return trajs;}hbTraj = { hbReplay;const steps =100;let w = hbStart;const pts = [w];let k =0;while (true) {if (!viewof hbPause.value&& k < steps) { w =hbStep(w, hbRule, hbQ, hbAlpha); pts.push(w); k++; }yield {pts,done: k >= steps,col: k >= steps ?hbEndColour(w, hbRule) : hbCol.K}; }}
Code
{const c = hbCanvas, fg =exFg(), {W, H, S, m, top} = hbLay;const [lo, hi] = hbLimits[hbRule];const px = w => m + S * (w - lo) / (hi - lo), py = w => top + S * (1- (w - lo) / (hi - lo)); c.clearRect(0,0, W, H); c.font= exFont;axisBox(c, m, top, S, S); c.fillStyle= fg; c.textAlign="center"; c.fillText("w₁", m + S /2, top + S +24); c.fillText(lo,px(lo), top + S +24); c.fillText(hi,px(hi), top + S +24); c.textAlign="right"; c.fillText("w₂", m -10, top + S /2+5); c.fillText(lo, m -8,py(lo) +5); c.fillText(hi, m -8,py(hi) +5); c.save(); c.beginPath(); c.rect(m, top, S, S); c.clip(); c.lineWidth=1;for (const {pts, col} of hbField) { c.strokeStyle= col; c.globalAlpha=0.25; c.beginPath(); pts.forEach((p, i) => i ? c.lineTo(px(p[0]),py(p[1])) : c.moveTo(px(p[0]),py(p[1]))); c.stroke(); } c.globalAlpha=1; c.lineWidth=2.5; c.strokeStyle= hbTraj.col; c.beginPath(); hbTraj.pts.forEach((p, i) => i ? c.lineTo(px(p[0]),py(p[1])) : c.moveTo(px(p[0]),py(p[1]))); c.stroke();const last = hbTraj.pts[hbTraj.pts.length-1], first = hbTraj.pts[0]; c.fillStyle= hbTraj.col; c.beginPath(); c.arc(px(last[0]),py(last[1]),6,0,2*Math.PI); c.fill(); c.strokeStyle= fg; c.lineWidth=1.5; c.beginPath(); c.arc(px(first[0]),py(first[1]),6,0,2*Math.PI); c.stroke(); c.restore(); c.fillStyle= fg; c.textAlign="left"; c.fillText(`start (${first[0].toFixed(2)}, ${first[1].toFixed(2)}) · click to move`, m +8, top +22);// time course, rightconst x0 = m + S +70, tW = W - x0 -16;const tx = i => x0 + tW * i /100, ty = w =>py(w);axisBox(c, x0, top, tW, S); c.fillStyle= fg; c.textAlign="center"; c.fillText("time steps", x0 + tW /2, top + S +24); c.fillText("0",tx(0), top + S +24); c.fillText("100",tx(100), top + S +24); c.textAlign="right"; c.fillText(lo, x0 -8,py(lo) +5); c.fillText(hi, x0 -8,py(hi) +5); c.save(); c.beginPath(); c.rect(x0, top, tW, S); c.clip();for (const [j, col] of [[0, hbCol.T1], [1, hbCol.T2]]) { c.strokeStyle= col; c.lineWidth=2; c.beginPath(); hbTraj.pts.forEach((p, i) => i ? c.lineTo(tx(i),ty(p[j])) : c.moveTo(tx(i),ty(p[j]))); c.stroke(); } c.restore(); c.textAlign="left"; c.fillStyle= hbCol.T1; c.fillText("w₁", x0 +8, top +22); c.fillStyle= hbCol.T2; c.fillText("w₂", x0 +44, top +22);return c.canvas;}
Reading the figure
The square on the left is the weight plane, \(w_1\) along the bottom and \(w_2\) up the side. The thick line is the trajectory from your starting point, which is marked by the open circle. It ends at the filled dot. Behind it, the faint lines are trajectories started from a grid of points, so you can see the overall flow of the rule at a glance. All of these are coloured by where they end up after 100 steps. For the bounded rule, blue means both weights saturated at 1, orange means \((0, 1)\) and green means \((1, 0)\). For Oja’s rule the end points are unit vectors instead, blue for \((1,1)/\sqrt2\), orange for \((-1,1)/\sqrt2\) and green for \((1,-1)/\sqrt2\). Grey means none of the above. Plain Hebbian learning is grey everywhere, since nothing ever stops.
The panel on the right shows the same trajectory as two curves against time, \(w_1\) in purple and \(w_2\) in teal. It is easier to see from this panel whether a weight has settled or is still moving.
Things to try
Begin with the plain Hebbian rule and the default negative correlation. From any starting point the weights head off along a diagonal and leave the plot. Which diagonal depends on \(q\): the weights grow fastest along the eigenvector of \(Q\) with the largest eigenvalue, and that is \((1, 1)\) when the inputs are positively correlated and \((1, -1)\) when they are anticorrelated. Move \(q\) through zero and watch the flow swing from one diagonal to the other. At \(q = 0\) both eigenvalues are equal and every direction grows at the same rate, so the trajectories are straight rays from the origin.
Switch to the bounded rule. The runaway is gone, but something else has appeared: the plane is divided into regions, each draining into a different corner. A starting point that favours input 1 ends at \((1, 0)\), one that favours input 2 at \((0, 1)\), and with positive correlation most of the plane ends at \((1, 1)\). These regions are the basins of attraction of the three stable corners, and the bounds have turned a rule with no stable state into one that stores a decision about which input wins. Change \(q\) and watch the boundaries between the basins move.
Switch to Oja’s rule. Now the trajectories bend towards a circle of radius \(1/\sqrt{\alpha}\) and then slide along it to an eigenvector of \(Q\). The rule does two things at once: it normalises the weight vector and it picks out the direction of largest input variance, which is the first principal component of the inputs. This is the main reason Oja’s rule is famous. Raise \(\alpha\) and the circle shrinks. Set \(q\) to zero and the trajectories still reach the circle, but stop wherever they land on it, since no direction is preferred.
This page is based on the Hebbian learning exercise in Dayan and Abbott (2001, chapter 8). Miller and MacKay (1994) give a careful comparison of different ways of constraining Hebbian learning, including why subtractive and multiplicative normalisation lead to different outcomes, which is exactly the difference between the bounded rule and Oja’s rule above.
References
Dayan P, Abbott LF (2001). Theoretical Neuroscience: Computational and Mathematical Modeling of Neural Systems. MIT Press.
Hebb DO (1949). The Organization of Behavior: A Neuropsychological Theory. Wiley.
Miller KD, MacKay DJC (1994). The role of constraints in Hebbian learning. Neural Computation 6: 100–126. DOI
Oja E (1982). A simplified neuron model as a principal component analyzer. Journal of Mathematical Biology 15: 267–273. DOI