Skip to content

llm/test_llm.py ​

配套源码,运行方法见同目录 README。返回实验总览。

python
"""Run: python3 -m unittest discover -s labs/llm -p 'test_*.py' -v"""
import math
from pathlib import Path
import tempfile
import unittest
import numpy as np
from bpe import ByteBPE, CharacterTokenizer, merge_pair
from model import Config, TinyTransformer, AdamW, cross_entropy, make_batch, generate
from algorithms import (online_attention, exact_deduplicate, fit_power_law,
                        group_advantages, masked_sft_loss, clipped_policy_objective, dpo_loss)


class BPETests(unittest.TestCase):
    def test_utf8_round_trip(self):
        tokenizer = ByteBPE.train(["blue blue 蓝色", "red red 红色"], 20)
        for text in ["", "蓝色 fox 🦊", "\n\t unseen café  ", "blue blue"]:
            self.assertEqual(tokenizer.decode(tokenizer.encode(text)), text)

    def test_non_overlapping_merges(self):
        self.assertEqual(merge_pair([1, 1, 1, 1, 1], (1, 1), 256), [256, 256, 1])

    def test_determinism_and_document_boundary(self):
        self.assertEqual(ByteBPE.train(["ab", "ac"], 1).merges, [(97, 98)])
        self.assertEqual(ByteBPE.train(["a", "b"], 1).merges, [])
        self.assertEqual(ByteBPE.train(["a b"], 1).merges, [])

    def test_serialization(self):
        tokenizer = ByteBPE.train(["abc abc abc"], 5)
        with tempfile.TemporaryDirectory() as directory:
            file = Path(directory) / "bpe.json"
            tokenizer.save(file)
            self.assertEqual(ByteBPE.load(file).encode("abc!"), tokenizer.encode("abc!"))


class TransformerTests(unittest.TestCase):
    def setUp(self):
        self.model = TinyTransformer(Config(7, dim=8, heads=2, hidden=12, layers=2, context=5))
        self.x = np.array([[1, 2, 1, 3, 4], [2, 1, 3, 2, 5]])
        self.y = np.array([[2, 1, 3, 4, 6], [1, 3, 2, 5, 6]])

    def test_future_cannot_change_past(self):
        changed = self.x.copy()
        changed[:, 3:] = 6
        original_logits = self.model.forward(self.x)[0]
        changed_logits = self.model.forward(changed)[0]
        np.testing.assert_allclose(original_logits[:, :3], changed_logits[:, :3], atol=1e-12)
        self.assertGreater(np.max(np.abs(original_logits[:, 3:] - changed_logits[:, 3:])), 1e-5)

    def test_shifted_batch(self):
        x, y = make_batch(np.arange(10), 20, 4, np.random.default_rng(1))
        np.testing.assert_array_equal(y, x + 1)
        np.testing.assert_array_equal(x[:, 1:], y[:, :-1])

    def test_uniform_and_extreme_loss(self):
        loss, grad = cross_entropy(np.zeros((2, 5, 7)), self.y)
        self.assertAlmostEqual(loss, math.log(7))
        np.testing.assert_allclose(grad.sum(axis=-1), 0, atol=1e-15)
        logits = np.array([[[1000., -1000.]]])
        value, derivative = cross_entropy(logits, np.array([[1]]))
        self.assertAlmostEqual(value, 2000.)
        self.assertTrue(np.all(np.isfinite(derivative)))

    def test_every_parameter_family_gradient(self):
        _, grads = self.model.loss(self.x, self.y, backward=True)
        rng = np.random.default_rng(8)
        eps = 1e-5
        # Four selected scalar entries in every parameter tensor, across two
        # blocks; repeated embeddings test accumulation and residual paths.
        for name, value in self.model.params.items():
            for flat_index in rng.choice(value.size, min(4, value.size), replace=False):
                index = np.unravel_index(flat_index, value.shape)
                old = value[index]
                value[index] = old + eps
                plus = self.model.loss(self.x, self.y)
                value[index] = old - eps
                minus = self.model.loss(self.x, self.y)
                value[index] = old
                numeric = (plus - minus) / (2 * eps)
                self.assertAlmostEqual(numeric, grads[name][index], delta=2e-5,
                                       msg=f"{name}{index}: numerical={numeric}, analytic={grads[name][index]}")

    def test_can_overfit_small_batch(self):
        optimizer = AdamW(self.model.params, lr=.005, decay=0)
        initial = self.model.loss(self.x, self.y)
        for _ in range(60):
            _, grads = self.model.loss(self.x, self.y, backward=True)
            optimizer.step(grads)
        self.assertLess(self.model.loss(self.x, self.y), initial * .3)

    def test_checkpoint_and_generation(self):
        tokenizer = CharacterTokenizer("abcdefg")
        with tempfile.TemporaryDirectory() as directory:
            file = Path(directory) / "model.npz"
            self.model.save(file, tokenizer)
            restored, tok = TinyTransformer.load(file)
            np.testing.assert_array_equal(self.model.forward(self.x)[0], restored.forward(self.x)[0])
            self.assertEqual(tok.encode("abc"), [0, 1, 2])
            first = generate(restored, [1, 2], 12, seed=4)
            second = generate(restored, [1, 2], 12, seed=4)
            self.assertEqual(first, second)
            self.assertEqual(len(first), 14)

    def test_invalid_config(self):
        with self.assertRaises(ValueError):
            Config(7, dim=9, heads=3)
        with self.assertRaises(ValueError):
            generate(self.model, [], 5)


class AlgorithmTests(unittest.TestCase):
    def test_sft_response_only(self):
        logits = np.zeros((1, 4, 3))
        labels = np.array([[0, 1, 2, 0]])
        mask = np.array([[0, 0, 1, 1]])
        loss, grad = masked_sft_loss(logits, labels, mask)
        self.assertAlmostEqual(loss, math.log(3))
        np.testing.assert_array_equal(grad[:, :2], 0)
        changed = logits.copy()
        changed[:, :2, 0] = 999
        self.assertAlmostEqual(masked_sft_loss(changed, labels, mask)[0], loss)
        with self.assertRaises(ValueError):
            masked_sft_loss(logits, labels, np.zeros_like(mask))

    def test_online_attention_matches_dense(self):
        from model import causal_attention
        rng = np.random.default_rng(21)
        q, k, v = (rng.normal(size=(13, 8)) for _ in range(3))
        dense = causal_attention(q[None, None], k[None, None], v[None, None])[0][0, 0]
        for block in (1, 3, 8, 32):
            np.testing.assert_allclose(online_attention(q, k, v, block), dense, rtol=1e-12, atol=1e-12)

    def test_data_and_scaling(self):
        self.assertEqual(exact_deduplicate(["a b", " a\n b ", "", "c"]), ["a b", "c"])
        x = np.array([1., 2., 4., 8., 16.])
        a, alpha = fit_power_law(x, 1.2 + 3 * x**(-.4), 1.2)
        self.assertAlmostEqual(a, 3.)
        self.assertAlmostEqual(alpha, .4)
        with self.assertRaises(ValueError):
            fit_power_law(x, np.ones(5), 1.)

    def test_alignment_sign_and_mask(self):
        advantages = group_advantages([[1, 1, 1], [0, 1, 0]])
        np.testing.assert_array_equal(advantages[0], 0)
        self.assertGreater(advantages[1, 1], 0)
        self.assertAlmostEqual(dpo_loss(-5, -5, -5, -5), math.log(2))
        self.assertLess(dpo_loss(-4, -6, -5, -5), math.log(2))
        value = clipped_policy_objective([math.log(2), 9], [0, 0], [1, -100], [1, 0])
        self.assertAlmostEqual(value, 1.2)


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