Train the deep house model
Training means: for many epochs, call trainOne on every sold house — forward, backprop, update — until mean squared error falls.
Same five HOUSES as the machine-learning lessons. Same DeepHouseNet
(3 → 4 ReLU → 4 ReLU → 1 linear). Learning rate 1e-9; seed 37.
Square footage is thousands; we scale inputs so training does not explode.
- Epoch
- One full pass over all five houses — five
trainOnecalls. - MSE
- Mean of squared (prediction − target) over the training set. Lower is better.
- Loss history
trainreturns one MSE per epoch so you can watch the curve.
Data
The five sold houses
Every epoch visits each row once. Features are [sqft, beds, age]; the label is the sale price.
| sqft | beds | age | Sale price |
|---|---|---|---|
| 1200 | 2 | 15 | $245,000 |
| 1800 | 3 | 8 | $310,000 |
| 2200 | 4 | 3 | $420,000 |
| 900 | 2 | 40 | $180,000 |
| 1500 | 3 | 10 | $295,000 |
Loop
Epochs while loss crawls down
With lr = 1e-9 and scaled features, each epoch is a small downhill shuffle — the same dial from
Gradient Descent. Watch the shape of the loop as MSE falls from ~9.04×10¹⁰ toward ~1.5×10⁸.
- in play
Seed 37, fresh weights. Predictions are near zero. MSE on HOUSES is huge — about 9.04 × 10¹⁰.
1e-9 keeps updates stable through the two ReLU layers. Five thousand epochs is enough for this toy set to reach usable dollar guesses.Solution in TypeScript
mse averages squared error. train runs epochs, calls trainOne on every house, and returns the loss history. Then freeze the net and forward once for a price guess.
type Vector = number[];
type Matrix = number[][]; // rows = neurons, cols = inputs
function relu(x: number): number {
return Math.max(0, x);
}
function reluDeriv(x: number): number {
return x > 0 ? 1 : 0;
}
/** Square footage is thousands; we scale inputs so training does not explode. */
function scale(features: Vector): Vector {
return [features[0] / 1000, features[1], features[2] / 10];
}
/** Mulberry32 — fixed seed so this lesson’s numbers are reproducible. */
function mulberry32(seed: number): () => number {
return () => {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
class Layer {
weights: Matrix;
biases: Vector;
activation: "relu" | "linear";
constructor(
inputSize: number,
outputSize: number,
activation: "relu" | "linear",
rnd: () => number,
) {
this.activation = activation;
this.weights = Array.from({ length: outputSize }, () =>
Array.from({ length: inputSize }, () => rnd() * 0.5 - 0.25),
);
this.biases = Array(outputSize).fill(0);
}
forward(inputs: Vector): { outputs: Vector; preActivations: Vector } {
const preActivations: Vector = [];
const outputs: Vector = [];
for (let j = 0; j < this.weights.length; j++) {
let sum = this.biases[j];
for (let i = 0; i < inputs.length; i++) {
sum += inputs[i] * this.weights[j][i];
}
preActivations.push(sum);
outputs.push(this.activation === "relu" ? relu(sum) : sum);
}
return { outputs, preActivations };
}
}
/** DeepHouseNet: 3 → 4 ReLU → 4 ReLU → 1 linear. */
class DeepHouseNet {
h1: Layer;
h2: Layer;
out: Layer;
constructor(seed = 37) {
const rnd = mulberry32(seed);
this.h1 = new Layer(3, 4, "relu", rnd);
this.h2 = new Layer(4, 4, "relu", rnd);
this.out = new Layer(4, 1, "linear", rnd);
}
forward(features: Vector): number {
const x = scale(features);
const a = this.h1.forward(x);
const b = this.h2.forward(a.outputs);
const o = this.out.forward(b.outputs);
return o.outputs[0];
}
}
const HOUSES: [Vector, number][] = [
[[1200, 2, 15], 245_000],
[[1800, 3, 8], 310_000],
[[2200, 4, 3], 420_000],
[[ 900, 2, 40], 180_000],
[[1500, 3, 10], 295_000],
];
function trainOne(
net: DeepHouseNet,
features: Vector,
target: number,
lr: number,
): void {
const x = scale(features); // Square footage is thousands; we scale inputs so training does not explode.
const h1 = net.h1.forward(x);
const h2 = net.h2.forward(h1.outputs);
const o = net.out.forward(h2.outputs);
const pred = o.outputs[0];
const error = target - pred;
for (let j = 0; j < net.out.weights.length; j++) {
for (let i = 0; i < h2.outputs.length; i++) {
net.out.weights[j][i] += error * h2.outputs[i] * lr;
}
net.out.biases[j] += error * lr;
}
const h2Error: Vector = Array(h2.outputs.length).fill(0);
for (let i = 0; i < h2.outputs.length; i++) {
let sum = 0;
for (let j = 0; j < net.out.weights.length; j++) {
sum += net.out.weights[j][i] * error;
}
h2Error[i] = sum * reluDeriv(h2.preActivations[i]);
}
for (let j = 0; j < net.h2.weights.length; j++) {
for (let i = 0; i < h1.outputs.length; i++) {
net.h2.weights[j][i] += h2Error[j] * h1.outputs[i] * lr;
}
net.h2.biases[j] += h2Error[j] * lr;
}
const h1Error: Vector = Array(h1.outputs.length).fill(0);
for (let i = 0; i < h1.outputs.length; i++) {
let sum = 0;
for (let j = 0; j < net.h2.weights.length; j++) {
sum += net.h2.weights[j][i] * h2Error[j];
}
h1Error[i] = sum * reluDeriv(h1.preActivations[i]);
}
for (let j = 0; j < net.h1.weights.length; j++) {
for (let i = 0; i < x.length; i++) {
net.h1.weights[j][i] += h1Error[j] * x[i] * lr;
}
net.h1.biases[j] += h1Error[j] * lr;
}
}
function mse(net: DeepHouseNet, data: [Vector, number][]): number {
return (
data.reduce((sum, [x, y]) => sum + (net.forward(x) - y) ** 2, 0) /
data.length
);
}
/** Epochs over all HOUSES; returns loss history (one MSE per epoch). */
function train(
net: DeepHouseNet,
data: [Vector, number][],
epochs: number,
lr: number,
): number[] {
const history: number[] = [];
for (let epoch = 0; epoch < epochs; epoch++) {
for (const [x, y] of data) {
trainOne(net, x, y, lr);
}
history.push(mse(net, data));
}
return history;
}
const net = new DeepHouseNet();
const history = train(net, HOUSES, 5000, 1e-9);
console.log("epoch 0 MSE", history[0].toExponential(3));
console.log("epoch 999 MSE", history[999].toExponential(3));
console.log("epoch 4999 MSE", history[4999].toExponential(3));
for (const [x, y] of HOUSES) {
console.log(x, "→", net.forward(x).toFixed(0), "(actual", y + ")");
}
// Seed 37 + scale + 1e-9: loss ~9.04e10 → ~1.5e8; predictions land near sale prices.Results
What the run looks like (seed 37)
Verified numbers for seed 37, scale, and lr = 1e-9 after 5000 epochs:
| Checkpoint | MSE (approx) | forward([1500, 3, 10]) |
|---|---|---|
| After epoch 0 | ~9.04 × 10¹⁰ | ~0.006 |
| After epoch 1000 | ~9.04 × 10¹⁰ (slightly lower) | still tiny |
| After epoch 5000 | ~1.5 × 10⁸ | ≈ 303754 |
| Target for that house | — | $295,000 |
| Features | Prediction | Actual |
|---|---|---|
[1200, 2, 15] | ≈ 227611 | 245000 |
[1800, 3, 8] | ≈ 325667 | 310000 |
[2200, 4, 3] | ≈ 408375 | 420000 |
[900, 2, 40] | ≈ 180165 | 180000 |
[1500, 3, 10] | ≈ 303754 | 295000 |
Next
Freeze the weights
Training moves dials. Inference freezes them and only calls forward — price a new listing, then check a held-out house so train loss is not the whole story.