Lucas Barbosa / Feedforward Neural Network for Regression

A research notebook · Oakhill College ·


Data & preparation

The full dataset contains eight rows, written directly into the Python program. Four are used to optimize the weights; four are passed to the training callback to track test cost. There is no separate validation set.

The complete dataset

The column order follows the original introduction: hours slept first, hours studied second. Some prose elsewhere in the manual reverses the verbal order; the numerical arrays are preserved here as written.

Table 1. All eight examples from the final Python implementation.
SplitRowSleep (hours)Study (hours)Score / 100
Training13575
Training25182
Training310293
Training461.570
Test145.570
Test24.5189
Test392.585
Test46275

The repository does not document sampling, student identities, collection methods, or measurement uncertainty. These are best read as illustrative observations for a programming assignment, rather than a documented empirical study of student performance.

Arrays and dimensions

The code represents each split as a floating-point matrix with one example per row. Inputs have shape (4,2)(4,2) and targets have shape (4,1)(4,1).

train_x = np.array(([3, 5], [5, 1], [10, 2], [6, 1.5]), dtype=float)
train_y = np.array(([75], [82], [93], [70]), dtype=float)

test_x = np.array(([4, 5.5], [4.5, 1], [9, 2.5], [6, 2]), dtype=float)
test_y = np.array(([70], [89], [85], [75]), dtype=float)

Floating-point arrays matter because the helper scales values in place. The target’s second dimension also matters: keeping a column vector avoids unintended broadcasting when subtracting predictions.

The original normalization

Each input column is divided by its largest value. Each score is divided by 100. Let HH contain the raw hours and let sis_i be a raw score. The normalized inputs and targets are XX and yy, the notation used in the network equations. For a split containing mm rows, the original helper computes:

Xij=Hijmax⁡1≤k≤mHkj,yi=si100.\begin{aligned} X_{ij} &= \frac{H_{ij}}{\max_{1\leq k\leq m} H_{kj}},\\[0.75em] y_i &= \frac{s_i}{100}. \end{aligned}
def scale_data(self, hours, test_score):
    MAX_SCORE = 100.
    hours /= np.amax(hours, axis=0)
    test_score /= MAX_SCORE
    return hours, test_score

The training maxima are 10 hours of sleep and 5 hours of study. The resulting input and target arrays are:

Xtrain=[0.31.00.50.21.00.40.60.3]\def\arraystretch{1.25} X_{\mathrm{train}}= \begin{bmatrix} 0.3 & 1.0\\ 0.5 & 0.2\\ 1.0 & 0.4\\ 0.6 & 0.3 \end{bmatrix} ytrain=[0.750.820.930.70]\def\arraystretch{1.25} y_{\mathrm{train}}= \begin{bmatrix} 0.75\\ 0.82\\ 0.93\\ 0.70 \end{bmatrix}

Scaling the inputs keeps their numerical values in a similar range. Scaling the targets also makes them compatible with the sigmoid output. A network can handle quantities with different physical units; normalization is a numerical and modeling choice, not a requirement that all original quantities share a unit.

The train–test scaling mismatch

The program calls the helper independently on each split:

train_x, train_y = Aux.scale_data(train_x, train_y)
test_x, test_y = Aux.scale_data(test_x, test_y)

For the test inputs, the maxima are 9 and 5.5 instead of 10 and 5. This changes the coordinate system seen by the network. For example, 6 hours of sleep becomes 6/10=0.66/10=0.6 during training but 6/9≈0.6676/9\approx0.667 during testing.

Xtest≈[0.44441.00000.50000.18181.00000.45450.66670.3636]\def\arraystretch{1.25} X_{\mathrm{test}}\approx \begin{bmatrix} 0.4444 & 1.0000\\ 0.5000 & 0.1818\\ 1.0000 & 0.4545\\ 0.6667 & 0.3636 \end{bmatrix}

This is an inconsistency in preprocessing, and the test inputs influence their own scaling. It means the archived test-cost curve does not evaluate the same transformation of raw hours that the network was trained on.

Retrospective correction: determine the input scale using the training split, then reuse it on the test split and future examples. The following is an illustrative correction, not the preprocessing used for the historical results.

input_scale = train_x.max(axis=0)
x_train_scaled = train_x / input_scale
x_test_scaled = test_x / input_scale
y_train_scaled = train_y / 100.0
y_test_scaled = test_y / 100.0

Here, the 5.5-hour test study value becomes 1.1. That is acceptable: a future observation can exceed the training maximum. It should not silently redefine the meaning of an input.

What the split does and does not provide

The BFGS objective uses only training inputs and training targets. Test cost is evaluated in a callback after optimizer iterations; it does not contribute to the supplied training gradients.

Keeping separate rows is a useful first step toward evaluating generalization. With only four test examples, however, the evaluation would remain fragile even after correcting normalization. The saved notebook does not include a test prediction array or a scalar test error.

The original helper also assumes positive, nonzero column maxima and divides arrays in place. Calling it on a single new example would normalize each nonzero feature to one, losing the actual hours. Reusing a stored training scale is necessary for meaningful inference.

Source: architecture notebook, training notebook, and the complete source.