Files
2026-07-13 12:09:03 +08:00

175 lines
6.4 KiB
Python

"""Tests for token and positional embeddings."""
from __future__ import annotations
import math
import os
import sys
import unittest
import torch
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
from main import ( # noqa: E402
EmbeddingComposer,
LearnedPositionalEmbedding,
SinusoidalPositionalEmbedding,
TokenEmbedding,
count_parameters,
neighbour_cosine_curve,
)
class TestTokenEmbedding(unittest.TestCase):
def test_output_shape(self) -> None:
torch.manual_seed(0)
emb = TokenEmbedding(vocab_size=100, d_model=8)
ids = torch.randint(0, 100, (3, 7), dtype=torch.long)
out = emb(ids)
self.assertEqual(out.shape, (3, 7, 8))
def test_long_dtype_required(self) -> None:
emb = TokenEmbedding(vocab_size=10, d_model=4)
ids = torch.zeros(2, 3, dtype=torch.float32)
with self.assertRaises(TypeError):
emb(ids)
def test_rank_two_required(self) -> None:
emb = TokenEmbedding(vocab_size=10, d_model=4)
ids_1d = torch.zeros(5, dtype=torch.long)
with self.assertRaises(ValueError):
emb(ids_1d)
def test_parameter_count(self) -> None:
emb = TokenEmbedding(vocab_size=100, d_model=8)
self.assertEqual(count_parameters(emb), 100 * 8)
class TestLearnedPositionalEmbedding(unittest.TestCase):
def test_output_shape(self) -> None:
torch.manual_seed(0)
emb = LearnedPositionalEmbedding(max_context_length=64, d_model=16)
out = emb(seq_len=10)
self.assertEqual(out.shape, (10, 16))
def test_parameter_count(self) -> None:
emb = LearnedPositionalEmbedding(max_context_length=64, d_model=16)
self.assertEqual(count_parameters(emb), 64 * 16)
def test_rejects_seq_len_past_max(self) -> None:
emb = LearnedPositionalEmbedding(max_context_length=16, d_model=4)
with self.assertRaises(ValueError):
emb(seq_len=17)
def test_rejects_zero_seq_len(self) -> None:
emb = LearnedPositionalEmbedding(max_context_length=16, d_model=4)
with self.assertRaises(ValueError):
emb(seq_len=0)
class TestSinusoidalPositionalEmbedding(unittest.TestCase):
def test_output_shape(self) -> None:
emb = SinusoidalPositionalEmbedding(max_context_length=64, d_model=16)
out = emb(seq_len=10)
self.assertEqual(out.shape, (10, 16))
def test_zero_parameters(self) -> None:
emb = SinusoidalPositionalEmbedding(max_context_length=64, d_model=16)
self.assertEqual(count_parameters(emb), 0)
def test_odd_d_model_rejected(self) -> None:
with self.assertRaises(ValueError):
SinusoidalPositionalEmbedding(max_context_length=8, d_model=5)
def test_sin_cos_formula(self) -> None:
d_model = 8
base = 10000.0
emb = SinusoidalPositionalEmbedding(max_context_length=16, d_model=d_model, base=base)
table = emb(seq_len=4)
for p in range(4):
for k in range(d_model // 2):
denom = base ** (2 * k / d_model)
expected_sin = math.sin(p / denom)
expected_cos = math.cos(p / denom)
self.assertAlmostEqual(table[p, 2 * k].item(), expected_sin, places=5)
self.assertAlmostEqual(table[p, 2 * k + 1].item(), expected_cos, places=5)
def test_deterministic_across_constructions(self) -> None:
a = SinusoidalPositionalEmbedding(max_context_length=32, d_model=16)
b = SinusoidalPositionalEmbedding(max_context_length=32, d_model=16)
self.assertTrue(torch.equal(a(seq_len=32), b(seq_len=32)))
class TestComposer(unittest.TestCase):
def test_learned_composition_shape(self) -> None:
torch.manual_seed(0)
tok = TokenEmbedding(vocab_size=50, d_model=8)
pos = LearnedPositionalEmbedding(max_context_length=32, d_model=8)
c = EmbeddingComposer(tok, pos)
ids = torch.randint(0, 50, (2, 5), dtype=torch.long)
out = c(ids)
self.assertEqual(out.shape, (2, 5, 8))
def test_sinusoidal_composition_shape(self) -> None:
torch.manual_seed(0)
tok = TokenEmbedding(vocab_size=50, d_model=8)
pos = SinusoidalPositionalEmbedding(max_context_length=32, d_model=8)
c = EmbeddingComposer(tok, pos)
ids = torch.randint(0, 50, (2, 5), dtype=torch.long)
out = c(ids)
self.assertEqual(out.shape, (2, 5, 8))
def test_composer_sums_token_and_position(self) -> None:
torch.manual_seed(0)
tok = TokenEmbedding(vocab_size=10, d_model=4)
pos = SinusoidalPositionalEmbedding(max_context_length=8, d_model=4)
c = EmbeddingComposer(tok, pos)
ids = torch.tensor([[1, 2, 3]], dtype=torch.long)
expected = tok(ids) + pos(seq_len=3).unsqueeze(0)
self.assertTrue(torch.allclose(c(ids), expected))
def test_d_model_mismatch_rejected(self) -> None:
tok = TokenEmbedding(vocab_size=10, d_model=4)
pos = SinusoidalPositionalEmbedding(max_context_length=8, d_model=8)
with self.assertRaises(ValueError):
EmbeddingComposer(tok, pos)
class TestNeighbourCosine(unittest.TestCase):
def test_curve_length(self) -> None:
torch.manual_seed(0)
table = torch.randn(20, 8)
curve = neighbour_cosine_curve(table, max_offset=5)
self.assertEqual(len(curve), 5)
def test_sinusoidal_curve_decays(self) -> None:
pos = SinusoidalPositionalEmbedding(max_context_length=128, d_model=64)
curve = neighbour_cosine_curve(pos.pe, max_offset=6)
self.assertGreater(curve[0], curve[5])
class TestGradientFlow(unittest.TestCase):
def test_token_embedding_gradient_only_for_used_rows(self) -> None:
torch.manual_seed(0)
emb = TokenEmbedding(vocab_size=10, d_model=4)
ids = torch.tensor([[1, 2]], dtype=torch.long)
out = emb(ids).sum()
out.backward()
grads = emb.embedding.weight.grad
self.assertIsNotNone(grads)
self.assertGreater(grads[1].abs().sum().item(), 0.0)
self.assertGreater(grads[2].abs().sum().item(), 0.0)
self.assertEqual(grads[0].abs().sum().item(), 0.0)
self.assertEqual(grads[7].abs().sum().item(), 0.0)
def test_sinusoidal_has_no_grad(self) -> None:
pos = SinusoidalPositionalEmbedding(max_context_length=16, d_model=8)
params = list(pos.parameters())
self.assertEqual(params, [])
if __name__ == "__main__":
unittest.main()