TypeScript: composing neural-network layers behind an interface

Build a small network from Dense and Sigmoid layers that share one Layer interface.

TypeScript
interface Layer {
  forward(input: number[]): number[];
}

class Dense implements Layer {
  private weights: number[][];
  private biases: number[];

  constructor(weights: number[][], biases: number[]) {
    this.weights = weights;
    this.biases = biases;
  }

  forward(input: number[]): number[] {
    return this.weights.map((row, j) => row.reduce((sum, w, i) => sum + w * input[i], this.biases[j]));
  }
}

class Sigmoid implements Layer {
  forward(input: number[]): number[] {
    return input.map((z) => 1 / (1 + Math.exp(-z)));
  }
}

class Sequential implements Layer {
  private layers: Layer[];

  constructor(layers: Layer[]) {
    this.layers = layers;
  }

  forward(input: number[]): number[] {
    return this.layers.reduce((value, layer) => layer.forward(value), input);
  }
}

const network = new Sequential([
  new Dense([[20, 20], [20, 20]], [-10, -30]),
  new Sigmoid(),
  new Dense([[20, -20]], [-10]),
  new Sigmoid(),
]);

for (const input of [[0, 0], [0, 1], [1, 0], [1, 1]]) {
  console.log(input[0] + " XOR " + input[1] + " = " + network.forward(input)[0].toFixed(3));
}
Output
0 XOR 0 = 0.000
0 XOR 1 = 1.000
1 XOR 0 = 1.000
1 XOR 1 = 0.000

Explanation

Deep-learning libraries describe a network as a stack of layers. Each layer takes a vector and returns a vector, so they can all implement one interface with a forward method. A Sequential network then just feeds the output of each layer to the next.

A Dense layer computes a weighted sum plus a bias for each output. A Sigmoid layer applies the sigmoid function to each element. With the weights below, the first Dense layer builds OR-like and AND-like features, and the second combines them into XOR. The structure is the same as in frameworks such as PyTorch or TensorFlow.js, with the weights fixed instead of learned.

Explore the dry run
Guided walkthroughReady
1interface Layer {
2 forward(input: number[]): number[];
3}
4
5class Dense implements Layer {
6 private weights: number[][];
7 private biases: number[];
8
9 constructor(weights: number[][], biases: number[]) {
10 this.weights = weights;
11 this.biases = biases;
12 }
13
14 forward(input: number[]): number[] {
15 return this.weights.map((row, j) => row.reduce((sum, w, i) => sum + w * input[i], this.biases[j]));
16 }
17}
18
19class Sigmoid implements Layer {
20 forward(input: number[]): number[] {
21 return input.map((z) => 1 / (1 + Math.exp(-z)));
22 }
23}
24
25class Sequential implements Layer {
26 private layers: Layer[];
27
28 constructor(layers: Layer[]) {
29 this.layers = layers;
30 }
31
32 forward(input: number[]): number[] {
33 return this.layers.reduce((value, layer) => layer.forward(value), input);
34 }
35}
36
37const network = new Sequential([
38 new Dense([[20, 20], [20, 20]], [-10, -30]),
39 new Sigmoid(),
40 new Dense([[20, -20]], [-10]),
41 new Sigmoid(),
42]);
43
44for (const input of [[0, 0], [0, 1], [1, 0], [1, 1]]) {
45 console.log(input[0] + " XOR " + input[1] + " = " + network.forward(input)[0].toFixed(3));
46}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
The contract every layer follows: a vector in, a vector out.—
—
Each output is a weighted sum of the inputs plus its bias.—
—
Feed the output of each layer into the next one.—
—
Two hidden units: the first is on for OR, the second only for AND.—
—
The network outputs about 0, 1, 1, 0.—
0 XOR 0 = 0.000
0 XOR 1 = 1.000
1 XOR 0 = 1.000
1 XOR 1 = 0.000

Cost and common mistakes

O(weights) per forward pass.

  • Dense expects input vectors of the right length. Frameworks check shapes and report errors early; this sketch does not.
  • Class fields are written out explicitly here instead of as constructor parameter properties, so the program also runs under type-stripping runtimes.
  • A Sequential network is itself a Layer, so networks can be nested.

Try it yourself

Why does the second Dense layer use the weights 20 and −20?

Reveal answer and reasoning

It fires when the OR feature is on (+20) and the AND feature is off (−20 would reduce the sum), which is exactly XOR.

Further reading