File size: 9,563 Bytes
5c6c847
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
"""框架自检:用数值差分验证每个算子 / 层的反向传播,并跑两个小模型确认能学会。
运行: python3 tests/test_zerocg.py
"""
import os
import sys

import numpy as np

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import zerocg as zc  # noqa: E402
from zerocg import functional as F  # noqa: E402
from zerocg import nn  # noqa: E402

zc.set_default_dtype(np.float64)   # 数值梯度检查需要 float64 的精度
rng = np.random.default_rng(0)


def grad_check(fn, *shapes, tol=1e-5, positive=False):
    """fn(*tensors) -> Tensor。比较自动求导和中心差分的梯度(float64)。"""
    arrays = [rng.standard_normal(s) for s in shapes]
    if positive:
        arrays = [np.abs(a) + 0.5 for a in arrays]
    ts = [zc.Tensor(a.copy(), requires_grad=True) for a in arrays]
    w = np.asarray(rng.standard_normal(fn(*ts).shape))  # 随机投影成标量,检查完整雅可比
    (fn(*ts) * zc.Tensor(w)).sum().backward()
    eps = 1e-6
    for t, a in zip(ts, arrays):
        num = np.zeros_like(a)
        for i in np.ndindex(a.shape):
            for sign in (1, -1):
                b = a.copy()
                b[i] += sign * eps
                inputs = [zc.Tensor(b) if x is t else zc.Tensor(x.data) for x in ts]
                with zc.no_grad():
                    num[i] += sign * (fn(*inputs).data * w).sum()
            num[i] /= 2 * eps
        err = np.abs(num - t.grad).max() / max(1.0, np.abs(num).max())
        assert err < tol, f"梯度误差 {err:.2e}"


def module_grad_check(module, x):
    for p in module.parameters():
        p.data = p.data.astype(np.float64)
    params = module.parameters()
    # 直接对参数逐个做差分
    xt = zc.Tensor(x)
    w = np.asarray(rng.standard_normal(module(xt).shape))
    for p in params:
        p.grad = None
    (module(xt) * zc.Tensor(w)).sum().backward()
    eps = 1e-6
    for p in params:
        num = np.zeros_like(p.data)
        for i in np.ndindex(p.shape):
            old = p.data[i]
            p.data[i] = old + eps
            with zc.no_grad():
                up = (module(xt).data * w).sum()
            p.data[i] = old - eps
            with zc.no_grad():
                down = (module(xt).data * w).sum()
            p.data[i] = old
            num[i] = (up - down) / (2 * eps)
        err = np.abs(num - p.grad).max() / max(1.0, np.abs(num).max())
        assert err < 1e-5, f"参数梯度误差 {err:.2e}"


# ---------------- 算子 ----------------
def test_ops():
    grad_check(lambda a, b: a + b, (3, 4), (4,))
    grad_check(lambda a, b: a - b, (3, 4), (3, 1))
    grad_check(lambda a, b: a * b, (2, 3), (2, 3))
    grad_check(lambda a, b: a / b, (2, 3), (3,), positive=True)
    grad_check(lambda a: a ** 3, (4,))
    grad_check(lambda a: a.sqrt(), (4,), positive=True)
    grad_check(lambda a: -a + 2.0 - a * 3.0, (4,))
    grad_check(lambda a: 1.0 / a, (4,), positive=True)
    grad_check(lambda a: a.exp(), (3, 2))
    grad_check(lambda a: a.log(), (3, 2), positive=True)
    grad_check(lambda a: a.tanh(), (5,))
    grad_check(lambda a: a.relu(), (5,))
    grad_check(lambda a: a.masked_fill(np.eye(3, dtype=bool), -5.0), (3, 3))


def test_reductions_and_shapes():
    grad_check(lambda a: a.sum(), (3, 4))
    grad_check(lambda a: a.sum(axis=1), (3, 4))
    grad_check(lambda a: a.mean(axis=0, keepdims=True), (3, 4))
    grad_check(lambda a: a.max(axis=-1), (3, 4))
    grad_check(lambda a: a.reshape(4, 3), (3, 4))
    grad_check(lambda a: a.transpose(2, 0, 1), (2, 3, 4))
    grad_check(lambda a: a[1:, ::2], (3, 4))
    grad_check(lambda a: a[np.array([0, 2, 0])], (3, 4))  # 重复索引
    grad_check(lambda a, b: zc.concat([a, b, a], axis=1), (2, 3), (2, 1))


def test_matmul():
    grad_check(lambda a, b: a @ b, (3, 4), (4, 5))
    grad_check(lambda a, b: a @ b, (2, 3, 4), (4, 5))      # 批量 @ 共享权重
    grad_check(lambda a, b: a @ b, (2, 2, 3, 4), (2, 2, 4, 3))


def test_functional():
    grad_check(lambda a: F.softmax(a, axis=-1), (3, 5))
    grad_check(lambda a: F.log_softmax(a, axis=-1), (3, 5))
    grad_check(lambda a: F.gelu(a), (6,))
    grad_check(lambda a: F.sigmoid(a), (6,))
    grad_check(lambda a: F.cross_entropy(a, np.array([1, 0, 4])), (3, 5))
    # 和直接的数学定义对比数值
    x = rng.standard_normal((4, 6))
    t = np.array([0, 5, 2, 2])
    ref = -np.log(np.exp(x) / np.exp(x).sum(1, keepdims=True))[np.arange(4), t].mean()
    assert abs(F.cross_entropy(zc.Tensor(x), t).item() - ref) < 1e-10


def test_layers():
    module_grad_check(nn.Linear(4, 3), rng.standard_normal((2, 4)))
    module_grad_check(nn.LayerNorm(5), rng.standard_normal((3, 5)))
    module_grad_check(nn.Sequential(nn.Linear(3, 4), nn.GELU(), nn.Linear(4, 2)), rng.standard_normal((2, 3)))
    module_grad_check(nn.CausalSelfAttention(8, 2), rng.standard_normal((2, 4, 8)))

    emb = nn.Embedding(5, 3)
    out = emb(np.array([[1, 1, 4]]))
    out.sum().backward()
    assert emb.weight.grad[1].tolist() == [2, 2, 2] and emb.weight.grad[0].tolist() == [0, 0, 0]


def test_attention_is_causal():
    attn = nn.CausalSelfAttention(8, 2).eval()
    x = rng.standard_normal((1, 5, 8)).astype(np.float32)
    y1 = attn(zc.Tensor(x)).data
    x[0, 4] += 10  # 改最后一个位置,前面的输出不应变化
    y2 = attn(zc.Tensor(x)).data
    assert np.allclose(y1[0, :4], y2[0, :4]) and not np.allclose(y1[0, 4], y2[0, 4])


def test_count_flops():
    with zc.count_flops() as c:
        zc.randn(2, 3, 4) @ zc.randn(4, 5)
    assert c.flops == 2 * (2 * 3 * 5) * 4


def test_default_dtype():
    zc.set_default_dtype(np.float32)
    try:
        x = zc.Tensor(np.ones(3))                       # float64 数组进来也变成 float32
        assert x.dtype == np.float32 and (x * 0.5).dtype == np.float32
        assert nn.Linear(2, 2).weight.dtype == np.float32
        assert zc.Tensor(np.arange(3)).dtype.kind == "i"   # 整数下标不动
        assert np.isfinite(F.sigmoid(zc.Tensor([-1000.0, 0.0, 1000.0])).data).all()
    finally:
        zc.set_default_dtype(np.float64)


def test_state_dict_roundtrip(tmp="/tmp"):
    path = os.path.join(os.environ.get("TMPDIR", tmp), "zerocg_test.npz")
    a = nn.Sequential(nn.Linear(3, 4), nn.LayerNorm(4))
    a.save(path)
    b = nn.Sequential(nn.Linear(3, 4), nn.LayerNorm(4)).load(path)
    x = zc.randn(2, 3)
    assert np.allclose(a(x).data, b(x).data)
    assert [n for n, _ in a.named_parameters()] == ["layers.0.weight", "layers.0.bias", "layers.1.weight", "layers.1.bias"]


def test_optimizer_resume():
    """训练 10 步 == 训练 5 步、存档、新建模型和优化器、读档、再训练 5 步。"""
    X, y = rng.standard_normal((8, 3)), np.array([0, 1, 1, 0, 1, 0, 0, 1])

    def make():
        np.random.seed(7)
        m = nn.Sequential(nn.Linear(3, 4), nn.Tanh(), nn.Linear(4, 2))
        return m, zc.optim.AdamW(m.parameters(), lr=0.05, weight_decay=0.01)

    def train(m, opt, n):
        for _ in range(n):
            loss = F.cross_entropy(m(zc.Tensor(X)), y)
            opt.zero_grad()
            loss.backward()
            opt.step()

    a, opt_a = make()
    train(a, opt_a, 10)
    b, opt_b = make()
    train(b, opt_b, 5)
    model_state, opt_state = b.state_dict(), opt_b.state_dict()
    c, opt_c = make()
    c.load_state_dict(model_state)
    opt_c.load_state_dict(opt_state)
    train(c, opt_c, 5)
    assert all(np.array_equal(p, q) for p, q in zip(a.state_dict().values(), c.state_dict().values()))


# ---------------- 端到端学习 ----------------
def test_learns_xor():
    np.random.seed(0)
    X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=np.float32)
    y = np.array([0, 1, 1, 0])
    model = nn.Sequential(nn.Linear(2, 16), nn.Tanh(), nn.Linear(16, 2))
    opt = zc.optim.AdamW(model.parameters(), lr=0.05)
    for _ in range(300):
        loss = F.cross_entropy(model(zc.Tensor(X)), y)
        opt.zero_grad()
        loss.backward()
        opt.step()
    assert (model(zc.Tensor(X)).data.argmax(1) == y).all(), f"XOR 没学会, loss={loss.item()}"


def test_attention_memorizes_sequence():
    """一层注意力 + 嵌入,记住一个短序列的下一个字符。"""
    np.random.seed(0)
    seq = np.array([0, 1, 2, 3, 2, 1, 0, 3])
    x, y = seq[None, :-1], seq[1:]

    class Tiny(nn.Module):
        def __init__(self):
            self.tok = nn.Embedding(4, 16)
            self.pos = nn.Embedding(8, 16)
            self.attn = nn.CausalSelfAttention(16, 2)
            self.ln = nn.LayerNorm(16)
            self.head = nn.Linear(16, 4)

        def forward(self, idx):
            T = idx.shape[1]
            h = self.tok(idx) + self.pos(np.arange(T))
            h = h + self.attn(self.ln(h))
            return self.head(h)

    model = Tiny()
    opt = zc.optim.AdamW(model.parameters(), lr=0.02)
    for _ in range(200):
        loss = F.cross_entropy(model(x).reshape(-1, 4), y)
        opt.zero_grad()
        loss.backward()
        zc.optim.clip_grad_norm(model.parameters(), 1.0)
        opt.step()
    assert loss.item() < 0.05, f"loss={loss.item()}"


if __name__ == "__main__":
    tests = [(k, v) for k, v in list(globals().items()) if k.startswith("test_")]
    failed = 0
    for name, fn in tests:
        try:
            fn()
            print(f"  ok   {name}")
        except Exception as e:  # noqa: BLE001
            failed += 1
            print(f"  FAIL {name}: {type(e).__name__}: {e}")
    print(f"\n{len(tests) - failed}/{len(tests)} 通过")
    sys.exit(1 if failed else 0)