home

Multilayer Perceptron

Creator: Petar Veličković (original)

A feedforward multilayer perceptron with one hidden neuron expanded into its affine transform and nonlinear activation.


Multilayer Perceptron

Download

PNG PDF SVG

Code

multilayer-perceptron.typ (84 lines)
#import "@preview/cetz:0.5.3": canvas, draw
#import draw: bezier, circle, content, line

#set page(width: auto, height: auto, margin: 8pt, fill: none)
#set text(size: 12pt)

#canvas({
  let arrow = (mark: (end: "stealth", fill: black, scale: .55), stroke: .8pt)
  let perceptron-radius = .48
  let unit(name, pos, label: none, radius: .28) = {
    circle(pos, radius: radius, fill: rgb("#cdd3da"), stroke: .8pt, name: name)
    if label != none { content(name, label) }
  }
  // point on the perceptron rim in the direction of pos
  let rim-from(pos) = {
    let length = calc.sqrt(pos.at(0) * pos.at(0) + pos.at(1) * pos.at(1))
    (pos.at(0) / length * perceptron-radius, pos.at(1) / length * perceptron-radius)
  }

  // Single perceptron: weighted inputs feeding an affine sum and activation
  unit("perceptron", (0, 0), label: $Sigma sigma$, radius: perceptron-radius)
  for (name, y, label, weight, ctrl-y) in (
    ("x0", 1.5, $+1$, $w_0$, 1.05),
    ("x1", .75, $x_1$, $w_1$, .72),
    ("x2", 0, $x_2$, $w_2$, 0),
    ("x3", -.75, $x_3$, $w_3$, -.72),
    ("xn", -1.75, $x_n$, $w_n$, -1.05),
  ) {
    let pos = (-2, y)
    unit(name, pos, label: label)
    bezier(name + ".east", rim-from((-.85, ctrl-y)), (-1.45, y), (-.85, ctrl-y), ..arrow)
    content((-1.25, (y + ctrl-y) / 2 + .12), weight, anchor: "south")
  }
  content((rel: (0, .53), to: "xn"), $dots.v$)
  content("x0.west", [bias], anchor: "east", padding: 4pt)
  content("x2.west", [inputs], anchor: "east", padding: 4pt)
  content((-1.25, 2.05), [weights], anchor: "south")
  line(
    (rel: (0, -.43), to: "perceptron"),
    (rel: (0, .43), to: "perceptron"),
    stroke: (dash: "dashed", paint: black, thickness: .5pt),
  )
  line("perceptron.east", (rel: (3.5, 0), to: "perceptron.east"), ..arrow, name: "perceptron-out")
  content(
    (rel: (0, .66), to: "perceptron-out.mid"),
    $sigma(w_0 + sum_(i=1)^n w_i x_i)$,
    fill: rgb("#cdd3da"),
    padding: 1pt,
  )
  content((rel: (0, 1.4), to: "perceptron-out.mid"), [$sigma$: activation function])

  // Fully connected network with 3 inputs, 5 hidden units and 2 outputs
  let inputs = (("i1", (6, .75), $I_1$), ("i2", (6, 0), $I_2$), ("i3", (6, -.75), $I_3$))
  let hidden = range(5).map(idx => ("h" + str(idx + 1), (8, 1.5 - idx * .75)))
  let outputs = (("o1", (10, .75), $O_1$), ("o2", (10, -.75), $O_2$))
  for (name, pos, label) in inputs {
    unit(name, pos, radius: .16)
    line((rel: (-.85, 0), to: name + ".west"), name + ".west", ..arrow, name: name + "-in")
    content((rel: (0, .28), to: name + "-in.mid"), label)
  }
  for (name, pos, ..) in hidden + outputs { unit(name, pos, radius: .16) }
  for (input-name, ..) in inputs {
    for (hidden-name, ..) in hidden { line(input-name, hidden-name, ..arrow) }
  }
  for (hidden-name, ..) in hidden {
    for (output-name, ..) in outputs { line(hidden-name, output-name, ..arrow) }
  }
  for (name, pos, label) in outputs {
    line(name + ".east", (rel: (.85, 0), to: name + ".east"), ..arrow, name: name + "-out")
    content((rel: (0, .28), to: name + "-out.mid"), label)
  }
  content((rel: (0, 1.55), to: "i1.north"), align(center)[Input\ layer])
  content((rel: (0, .8), to: "h1.north"), align(center)[Hidden\ layer])
  content((rel: (0, 1.55), to: "o1.north"), align(center)[Output\ layer])

  // Zoom lines linking the perceptron to the hidden unit it expands
  for sign in (1, -1) {
    line(
      (rel: (.12, sign * .43), to: "perceptron"),
      (rel: (0, sign * .16), to: "h5"),
      stroke: (paint: luma(45%), dash: "dashed", thickness: 1pt),
    )
  }
})