ContentsFoundations to FrontierBezhig

ai-lab/m01_autograd/tests/test_nn.py

All lab code. Download this file.

"""AI01 lab tests for nn.py (step 7)."""
import math
import random
import unittest

from _impl import load

engine = load("engine")
nn = load("nn")
Value = engine.Value


def set_params(module, values):
    params = module.parameters()
    assert len(params) == len(values)
    for p, v in zip(params, values):
        p.data = v


class Step7NeuralNet(unittest.TestCase):
    def setUp(self):
        random.seed(7)

    def test_neuron_parameters_are_weights_then_bias(self):
        n = nn.Neuron(3)
        ps = n.parameters()
        self.assertEqual(len(ps), 4)
        self.assertTrue(all(isinstance(p, Value) for p in ps))
        self.assertTrue(all(-1.0 <= p.data <= 1.0 for p in ps[:3]))
        self.assertEqual(ps[3].data, 0.0, "the bias starts at 0.0")

    def test_weights_start_on_both_sides_of_zero(self):
        ws = [p.data for p in nn.Neuron(200).parameters()[:-1]]
        self.assertTrue(all(-1.0 <= w <= 1.0 for w in ws))
        self.assertLess(min(ws), -0.5)
        self.assertGreater(max(ws), 0.5)

    def test_neuron_forward_with_tanh(self):
        n = nn.Neuron(2)
        set_params(n, [0.5, -1.0, 0.25])            # w0, w1, b
        out = n([1.0, 2.0])                         # 0.5 - 2 + 0.25 = -1.25
        self.assertIsInstance(out, Value)
        self.assertAlmostEqual(out.data, math.tanh(-1.25))

    def test_linear_neuron(self):
        n = nn.Neuron(2, nonlin=False)
        set_params(n, [0.5, -1.0, 0.25])
        self.assertAlmostEqual(n([1.0, 2.0]).data, -1.25)

    def test_neuron_accepts_values_as_inputs(self):
        n = nn.Neuron(2, nonlin=False)
        set_params(n, [0.5, -1.0, 0.25])
        x0 = Value(1.0)
        out = n([x0, Value(2.0)])
        out.backward()
        self.assertAlmostEqual(out.data, -1.25)
        self.assertAlmostEqual(x0.grad, 0.5)

    def test_layer_returns_a_list_or_a_single_value(self):
        self.assertEqual(len(nn.Layer(2, 3)([1.0, 2.0])), 3)
        self.assertIsInstance(nn.Layer(2, 1)([1.0, 2.0]), Value)
        self.assertEqual(len(nn.Layer(2, 3).parameters()), 9)

    def test_mlp_parameter_counts(self):
        self.assertEqual(len(nn.MLP(3, [4, 4, 1]).parameters()), 41)
        self.assertEqual(len(nn.MLP(2, [8, 1]).parameters()), 33)

    def test_mlp_single_output_is_a_value(self):
        self.assertIsInstance(nn.MLP(2, [3, 1])([0.5, -1.0]), Value)

    def test_last_layer_is_linear(self):
        model = nn.MLP(1, [1])
        set_params(model, [3.0, 2.0])
        self.assertAlmostEqual(model([5.0]).data, 17.0)

    def test_hidden_layers_use_tanh(self):
        model = nn.MLP(1, [1, 1])
        set_params(model, [1.0, 0.0, 1.0, 0.0])    # hidden w, b, then output w, b
        self.assertAlmostEqual(model([2.0]).data, math.tanh(2.0))

    def test_parameters_are_the_live_objects(self):
        model = nn.MLP(2, [3, 1])
        before = model([0.5, -1.0]).data
        for p in model.parameters():
            p.data += 0.1
        self.assertNotAlmostEqual(model([0.5, -1.0]).data, before)

    def test_every_parameter_gets_the_right_gradient(self):
        model = nn.MLP(2, [3, 2, 1])
        x = [0.7, -0.4]
        out = model(x)
        out.backward()
        h = 1e-6
        for i, p in enumerate(model.parameters()):
            analytic = p.grad
            p.data += h
            up = model(x).data
            p.data -= 2 * h
            down = model(x).data
            p.data += h
            with self.subTest(parameter=i):
                self.assertAlmostEqual(analytic, (up - down) / (2 * h), places=6)

    def test_zero_grad_clears_every_parameter(self):
        model = nn.MLP(2, [3, 1])
        model([0.5, -1.0]).backward()
        model.zero_grad()
        self.assertTrue(all(p.grad == 0.0 for p in model.parameters()))


if __name__ == "__main__":
    unittest.main()