Machine Learning for CybersecuritySource on GitHub

Unit 5 · Exercises

Unit 5 exercises

These exercises practice backpropagation and the training choices of Unit 5, ask you to write a gradient check, and use the results of Lab 5b. Try each one before opening the solution.

Exercise 1. Backpropagation with an inactive unit

A network has two inputs, two hidden ReLU units and a sigmoid output, as in Lesson 5.1. Its parameters are \(W = \begin{pmatrix} 1 & 1 \\ -1 & 1 \end{pmatrix}\), \(b = (0, -1)\), \(v = (1, -2)\) and \(c = 0\). One record has \(x = (2, -1)\) and label \(y = 0\). Compute the forward pass, the cross-entropy loss and the gradients with respect to \(v\), \(c\), \(W\) and \(b\).

Show the answer

Forward pass. \(z_1 = 2 - 1 + 0 = 1\) and \(z_2 = -2 - 1 - 1 = -4\), so \(h = (1, 0)\). The logit is \(a = 1 \cdot 1 - 2 \cdot 0 + 0 = 1\), \(p = \sigma(1) \approx 0.7311\), and since \(y = 0\) the loss is \(-\ln(1 - p) \approx 1.3133\).

Backward pass. \(\partial \ell/\partial a = p - y \approx 0.7311\). Then \(\partial \ell/\partial v = (p - y) h \approx (0.7311, 0)\) and \(\partial \ell/\partial c \approx 0.7311\). Next \(\partial \ell/\partial h = (p - y) v\), which is approximately \((0.7311, -1.4621)\). The second unit is inactive (\(z_2 < 0\)), so ReLU passes no gradient there, and \(\partial \ell/\partial z \approx (0.7311, 0)\). Finally \(\partial \ell/\partial b \approx (0.7311, 0)\), and the gradient with respect to the hidden weights is

\[ \begin{aligned} \partial \ell/\partial W &= (\partial \ell/\partial z)\, x^\top \\ &\approx \begin{pmatrix} 1.4621 & -0.7311 \\ 0 & 0 \end{pmatrix}. \end{aligned} \]

The second row of \(W\), the second hidden bias and the second output weight receive zero gradient from this record, even though \(\partial \ell/\partial h_2\) is not zero. If \(z_2\) were negative for every training record, the unit would never learn.

Exercise 2. Collapsing linear layers

Two layers without an activation between them have \(W_1 = \begin{pmatrix} 1 & 2 \\ 0 & 1 \end{pmatrix}\), \(b_1 = (1, -1)\), \(W_2 = (3, -1)\) and \(b_2 = 0.5\). Find the single affine map \(x \mapsto A x + c\) that they compute, and check it on \(x = (1, 1)\).

Show the answer

The second layer applied to the first gives \(W_2 (W_1 x + b_1) + b_2\), so the weight row and the offset are

\[ \begin{aligned} A &= W_2 W_1 \\ &= (3 \cdot 1 - 1 \cdot 0,\; 3 \cdot 2 - 1 \cdot 1) \\ &= (3, 5), \\[4pt] c &= W_2 b_1 + b_2 \\ &= 3 \cdot 1 + (-1)(-1) + 0.5 \\ &= 4.5. \end{aligned} \]

For \(x = (1, 1)\), the first layer gives \(W_1 x + b_1 = (3 + 1, 1 - 1)\), which is \((4, 0)\), and then the second gives \(3 \cdot 4 - 1 \cdot 0 + 0.5 = 12.5\), and the single map gives \(3 + 5 + 4.5 = 12.5\). The map is affine (it has an offset), and adding more linear layers would still give an affine map.

Exercise 3. Epochs, iterations and MLPClassifier

A training set has 50,000 records.

  1. With mini-batches of 128, how many iterations does one epoch have, and how many weight updates do 20 epochs perform?
  2. You train scikit-learn's MLPClassifier with the default adam solver, batch_size=128 and max_iter=20. How many values does loss_curve_ contain if training runs to the end, and what is the horizontal axis of a plot of it?
  3. A colleague says the model "has no dropout, so it cannot be regularized". Is that right?
Show the answer
  1. \(50{,}000/128 = 390.625\), so an epoch has \(\lceil 390.625 \rceil = 391\) iterations (390 full batches and one of 80 records), and 20 epochs perform \(20 \times 391 = 7{,}820\) updates.

  2. For the stochastic solvers, max_iter counts epochs, and loss_curve_ gets one value per epoch, the training loss averaged over that epoch (including the L2 penalty). It has 20 values, and its horizontal axis is the epoch, not the iteration.

  3. MLPClassifier indeed has no dropout. It can still be regularized, through the L2 penalty alpha and through early_stopping=True, which holds out a random, stratified validation fraction of the training data. For time-ordered security data that random fraction is not a good validation set, so training for a fixed number of epochs and choosing among models on a later validation period is safer.

Exercise 4. Two steps of Adam

A single parameter starts at \(\theta_0 = 0\). Its gradients at the first two steps are \(g_1 = 0.2\) and \(g_2 = -0.1\). Apply two steps of Adam with \(\alpha = 0.001\), \(\beta_1 = 0.9\), \(\beta_2 = 0.999\) and \(\epsilon = 10^{-8}\). Then explain why \(\theta\) keeps decreasing at the second step although the gradient has changed sign.

Show the answer

Write \(m_t\) and \(v_t\) for Adam's first and second moment estimates after step \(t\), as in Lesson 5.2, with \(m_0 = v_0 = 0\). This \(v_t\) is unrelated to the output weights \(v\) of Exercise 1.

Step 1. \(m_1 = 0.1 \times 0.2 = 0.02\) and \(v_1 = 0.001 \times 0.04 = 0.00004\). The bias-corrected values are \(0.02/0.1 = 0.2\) and \(0.00004/0.001 = 0.04\), so the step is \(0.001 \times 0.2/\sqrt{0.04} = 0.001\), and \(\theta_1 = -0.001\).

Step 2. The moment estimates become

\[ \begin{aligned} m_2 &= 0.9 \times 0.02 + 0.1 \times (-0.1) \\ &= 0.008, \\[4pt] v_2 &= 0.999 \times 0.00004 \\ &\quad + 0.001 \times 0.01 \\ &= 0.00004996. \end{aligned} \]

The corrections divide by \(1 - 0.9^2 = 0.19\) and \(1 - 0.999^2 = 0.001999\), giving \(0.04211\) and \(0.02499\). The step is

\[ 0.001 \times \frac{0.04211}{\sqrt{0.02499}} \approx 0.000266, \]

so \(\theta_2 \approx -0.001266\).

The first moment \(m\) is a running average of gradients, and after one positive and one smaller negative gradient it is still positive, so the parameter continues in the same direction, more slowly. This momentum-like behavior smooths noisy mini-batch gradients; it also means Adam reacts to a genuine change of direction with a delay.

Exercise 5. Writing a gradient check

Write a function that takes a PyTorch model, a batch \((X, y)\) and a loss function, and compares the autograd gradient of randomly chosen parameters with a central-difference estimate. Use double precision. Test it on a small network.

Show the answer

The function below perturbs one parameter entry at a time by \(\pm\epsilon\), where \(\epsilon\) is the finite-difference step (the argument eps, \(10^{-6}\) by default, unrelated to the Adam constant of Exercise 4), recomputes the loss without tracking gradients, and compares the central difference with the gradient from backward().

import torch
from torch import nn

def grad_check(model, X, y, loss_fn, eps=1e-6, n_checks=20, seed=0):
    """Compare autograd gradients with central differences
    at randomly chosen parameter entries."""
    model = model.double()
    X, y = X.double(), y.double()
    model.zero_grad()
    loss_fn(model(X).squeeze(1), y).backward()
    params = [p for p in model.parameters()]
    g = torch.Generator().manual_seed(seed)
    worst = 0.0
    for _ in range(n_checks):
        p = params[torch.randint(len(params), (1,), generator=g).item()]
        idx = tuple(torch.randint(s, (1,), generator=g).item()
                    for s in p.shape)
        analytic = p.grad[idx].item()
        with torch.no_grad():
            old = p[idx].item()
            p[idx] = old + eps
            plus = loss_fn(model(X).squeeze(1), y).item()
            p[idx] = old - eps
            minus = loss_fn(model(X).squeeze(1), y).item()
            p[idx] = old
        worst = max(worst, abs(analytic - (plus - minus) / (2 * eps)))
    return worst

torch.manual_seed(0)
net = nn.Sequential(nn.Linear(4, 8), nn.Tanh(), nn.Linear(8, 1))
X = torch.randn(32, 4)
y = (X[:, 0] > 0).float()
loss_fn = nn.BCEWithLogitsLoss()
print(f"largest difference: {grad_check(net, X, y, loss_fn):.2e}")

It prints a largest difference of about \(10^{-10}\). The central difference has an error of order \(\epsilon^2\), and double precision keeps rounding error small, which is why the check is done in float64. With ReLU, avoid points where a pre-activation is so close to zero that the perturbation moves it across zero, since the central difference then straddles the kink (where the derivative is not defined) and the two estimates can legitimately disagree. A large discrepancy usually means a bug in a hand-written backward pass, or a model whose dropout layers are active (call model.eval() first).

Exercise 6. The autoencoder at the alert budget

In Lab 5b the autoencoder's 100 alerts over four test days have precision 0.80, and only 0.20% of normal test records exceed the threshold.

  1. How many of the 100 alerts are false, and how many false alerts per day does an analyst see?
  2. How many normal test records are there, approximately, if that small percentage produces this number of false alerts? Compare with the number of normal test records reported by Lab 4.
  3. The team wants to halve the false alerts without reducing the budget. Suggest two approaches that do not involve a different algorithm.
Show the answer
  1. The 100 alerts contain 80 attacks and 20 normal records, about a quarter of that number per day.

  2. Dividing the number of false alerts by the share of normal records above the threshold gives the number of normal records, about ten thousand. Lab 4 reports 10,063 normal records in the same test period (both labs use the same deduplicated records and days). A rate that rounds to a fraction of a percent still produces a large share of the alerts, because the normal class is more than 20 times larger than the attack class.

  3. First, examine the false alerts. In Lab 4 the isolation forest's false alarms were dominated by a few benign behaviors (backups, a misconfigured service, administrator connections), while those of LOF followed the ordinary traffic mix, so the composition depends on the detector. Where the false alerts are concentrated in such behaviors, an allow-list or a separate model for those hosts and time windows removes them at the source. Second, add features that separate the benign behavior from the attacks it resembles, for example the hour of day for backups or the destination for scanner traffic. A third option, retraining on more recent normal data, also helps with drift, provided the data has been reviewed so that undetected attacks are not learned as normal.

Exercise 7. Choosing a regularizer

In the overfitting experiment of Lab 5b, weight decay gives the lowest validation cross-entropy (0.0529) but the early-stopped network without regularization gives a higher test AP (0.526 against 0.517). You must choose one configuration for deployment, and you are not allowed to look at the test period. What should you do, and what does this example teach about selection criteria?

Show the answer

Choose on the validation period, with the metric that matters in deployment. If the detector will be operated at an alert budget, compute AP or precision at the budget on the validation days for each configuration and pick the best, then report its test performance once. Validation cross-entropy measures both ranking and calibration, so it can prefer a configuration that is better calibrated but ranks attacks slightly worse, which is what the lab shows on the test period. The test AP must not be used for the choice, since that would turn the test period into a validation set and bias the reported number upward (Lesson 2.2). With only 93 attacks in the validation days, differences of a few hundredths in AP may well be within the noise, so a simpler or better-calibrated configuration is a defensible tie-breaker.