뉴런의 연산 모형 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""뉴런의 연산 모형 검증.

문서: 07.뉴런의 연산 모형 (예시로 보기, 정의, 바이어스의 크기, 카드 C2·C3)
주장:
  1. A가 R x D, x가 D x 1이면 Ax는 R x 1이다. 그래서 바이어스 b는 R x 1이어야 한다.
     b를 입력 수에 맞춰 D x 1로 두면 R != D일 때 더할 수 없다.
  2. 활성 함수 없이 선형(아핀) 층을 두 번 쌓으면 한 층과 같다:
     A2 (A1 x + b1) + b2 = (A2 A1) x + (A2 b1 + b2).
  3. 예시: 가중치 (1, 1, -1), b = 0인 뉴런에 x = (3, 2, 4)면 o = 1, x = (1, 0, 4)면 o = -3.
     발화율은 음수가 될 수 없으므로 a(o) = max(0, o)를 거치면 0이다.
  4. 활성 함수 a(o) = min(max(o, 0), r_max)의 출력은 늘 [0, r_max] 안에 있다.
방법: 분수(정확 계산)로 행렬 연산을 직접 구현해 무작위 입력으로 확인한다.
"""
import random
from fractions import Fraction as F

Matrix = list  # 행 목록. 열벡터는 R x 1 행렬로 쓴다.


def shape(m: Matrix) -> tuple[int, int]:
    return len(m), len(m[0])


def matmul(a: Matrix, b: Matrix) -> Matrix:
    ra, ca = shape(a)
    rb, cb = shape(b)
    if ca != rb:
        raise ValueError(f"곱할 수 없음: {ra}x{ca} @ {rb}x{cb}")
    return [[sum(a[i][k] * b[k][j] for k in range(ca)) for j in range(cb)] for i in range(ra)]


def matadd(a: Matrix, b: Matrix) -> Matrix:
    if shape(a) != shape(b):
        raise ValueError(f"더할 수 없음: {shape(a)} + {shape(b)}")
    return [[x + y for x, y in zip(ra, rb)] for ra, rb in zip(a, b)]


def rand_matrix(r: int, c: int, rng: random.Random) -> Matrix:
    return [[F(rng.randint(-5, 5)) for _ in range(c)] for _ in range(r)]


def activation(o: F, r_max: F) -> F:
    return min(max(o, F(0)), r_max)


def main() -> None:
    rng = random.Random(0)

    # 주장 1: 차원
    R, D = 2, 3
    A = rand_matrix(R, D, rng)
    x = rand_matrix(D, 1, rng)
    Ax = matmul(A, x)
    assert shape(Ax) == (R, 1)
    try:
        matadd(Ax, rand_matrix(D, 1, rng))  # 틀리기 쉬운 D x 1 바이어스
        raise AssertionError("D x 1 바이어스가 더해지면 안 된다")
    except ValueError:
        pass
    o = matadd(Ax, rand_matrix(R, 1, rng))
    assert shape(o) == (R, 1)
    print("[OK] R=2, D=3: Ax는 2x1. b가 3x1이면 더할 수 없고 2x1이면 된다")

    # R = D일 때만 D x 1도 우연히 맞는다
    Asq = rand_matrix(3, 3, rng)
    matadd(matmul(Asq, x), rand_matrix(3, 1, rng))
    print("[OK] R = D일 때만 D x 1 바이어스가 우연히 맞는다")

    # 주장 2: 선형 층 두 개 = 선형 층 하나
    for _ in range(200):
        d, h, r = rng.randint(1, 5), rng.randint(1, 5), rng.randint(1, 5)
        A1, b1 = rand_matrix(h, d, rng), rand_matrix(h, 1, rng)
        A2, b2 = rand_matrix(r, h, rng), rand_matrix(r, 1, rng)
        xx = rand_matrix(d, 1, rng)
        two_layers = matadd(matmul(A2, matadd(matmul(A1, xx), b1)), b2)
        one_layer = matadd(matmul(matmul(A2, A1), xx), matadd(matmul(A2, b1), b2))
        assert two_layers == one_layer
    print("[OK] 무작위 200회: A2(A1x+b1)+b2 = (A2A1)x + (A2b1+b2)")

    # 주장 3: 예시
    w = [[F(1), F(1), F(-1)]]
    for xs, expect_o, expect_a in (((3, 2, 4), 1, 1), ((1, 0, 4), -3, 0)):
        xv = [[F(v)] for v in xs]
        o1 = matmul(w, xv)[0][0]
        assert o1 == expect_o and activation(o1, F(800)) == expect_a
        print(f"[OK] 예시 x={xs}: o={o1}, a(o)={activation(o1, F(800))}")

    # 주장 4: 활성 함수의 출력 범위
    r_max = F(800)
    for _ in range(1000):
        val = F(rng.randint(-5000, 5000), rng.randint(1, 7))
        out = activation(val, r_max)
        assert 0 <= out <= r_max
    print("[OK] 무작위 1,000회: a(o)는 늘 [0, 800] 안")

    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()