Stage 01 — 역전파 전에 필요한 것: 계산을 기억하는 스칼라
순수 Python으로 Value 객체를 만들고, 덧셈과 곱셈이 순전파 값과 계산 그래프를 동시에 만드는 과정을 직접 실행해 확인한다.
GPT를 공부할 때 가장 먼저 마주치는 벽은 모델 크기가 아니다. 수식 속의 값 하나가 코드 안에서 무엇이어야 하는지 이해하는 일이다.
보통 Python의 float는 현재 숫자만 기억한다. 2.0 * -3.0의 결과가 -6.0이라는 사실은 알지만, 그 결과가 어떤 두 값의 곱으로 만들어졌는지는 남기지 않는다. 역전파를 하려면 그 정보가 필요하다. Stage 01에서는 숫자와 계산 이력을 함께 보존하는 가장 작은 Value 객체를 만든다.
이번 단계가 답할 질문
이번 Stage의 질문은 하나다.
덧셈과 곱셈을 평범한 산술식처럼 쓰면서, 결과를 만든 부모 노드와 연산도 함께 보존할 수 있는가?
구현 범위도 의도적으로 작게 잡았다.
- 유한한 스칼라 값
- 덧셈과 곱셈
- Python 실수의
Value승격 - 부모 노드와 연산자 기록
- 계산 그래프의 위상 순서 출력 gradient, chain rule, tensor, neural network, training은 아직 넣지 않는다. 역전파까지 한 번에 구현하면 계산 그래프라는 첫 번째 위험 경계가 보이지 않게 된다.
가장 작은 계산 그래프
실행 예제는 다음 식을 만든다.
product = x * y
z = product + bias
x = 2
y = -3
bias = 10
순전파 계산은 단순하다.
product = 2 * -3 = -6
z = -6 + 10 = 4
하지만 필요한 결과는 z.data == 4 하나가 아니다. 그래프는 다음 관계까지 보존해야 한다.
x -----\
(*) product -----\
y -----/ (+) z
bias --------------------/
product.parents는 (x, y), z.parents는 (product, bias)다. 복사된 숫자가 아니라 원래 Value 객체의 참조가 저장된다. 다음 Stage에서 gradient를 원래 leaf까지 돌려보낼 수 있는 이유가 여기서 생긴다.
Value가 책임지는 것
생성자는 외부에서 들어오는 숫자의 경계다.
class Value:
__slots__ = ("data", "label", "_parents", "_operation")
def __init__(self, data: Real, label: str = "") -> None:
if not isinstance(data, Real):
raise TypeError(...)
numeric_data = float(data)
if not math.isfinite(numeric_data):
raise ValueError(...)Value는 숫자를 받자마자 실수인지, 유한한 값인지 검사한다. 이 시점에 NaN을 허용하면 뒤의 모든 노드가 정상처럼 만들어진 뒤 학습 업데이트 단계에서야 문제가 드러날 수 있다. 입력 경계에서 거부하면 실패 범위는 노드 하나로 끝난다.
평범한 산술식이 그래프를 만드는 방법
__mul__과 __add__은 두 가지 일을 동시에 한다.
def __mul__(self, other: Value | Real) -> Value:
right = self._coerce(other)
return self._from_operation(
self.data * right.data,
(self, right),
"*",
)- 현재 순전파 값을 계산한다.
- 피연산자를 부모로 가진 새 노드를 만든다. 그래서 사용자 코드는 여전히 수식처럼 읽힌다.
product = (x * y).named("product")
z = (product + bias).named("z")복잡함을 숨긴 것이 아니라, 수학 표현과 그래프 생성의 경계를 한 연산자 안에 정렬한 것이다. 부모와 연산자는 property로 그대로 읽을 수 있으므로 내부 상태를 감추지도 않는다.
직접 실행한 결과
다음 명령을 Windows PowerShell에서 실행했다.
python .\stages\stage_01_value_graph.py
python -m unittest tests.test_stage_01_value_graph -v관찰된 결과는 다음과 같다.
product = -6z = 4- 그래프 순서:
x → y → product → bias → z - sanity check:
PASS - Stage 01 테스트: 6개 통과, 0.002초

Stage 01 순전파와 테스트
위상 순서에서 부모가 소비자보다 먼저 나타나는 것이 핵심이다. 역전파는 이 순서를 뒤집어 z에서 leaf 방향으로 이동하게 된다.
값을 바꾼 통제 실험
데모에 하드코딩된 결과만 맞는지 확인하는 것으로는 부족하다. 코드는 그대로 두고 입력만 다음처럼 바꿨다.
x = 5
product = x * -2
z = product + 3
결과는 z = -7.0이었다. 그래프에는 Python 리터럴 -2와 3도 leaf Value로 들어갔다.
이 실험은 두 가설을 분리한다.
- 산술 결과가 맞다.
- 명시적인
Value와 일반 숫자가 섞여도 그래프가 끊기지 않는다. 둘 다 실제 출력으로 확인됐다.
실패해야 하는 실험
이번에는 정상 입력이 아니라 NaN을 넣었다.
python -c "from stages.stage_01_value_graph import Value; Value(float('nan'))"프로그램은 의도한 생성 경계에서 즉시 멈췄다.
ValueError: Value data must be finite, got nan

에러가 발생하지 않는 것이 성공이 아니다. 잘못된 값이 들어왔을 때 정확한 경계에서 실패하는 것도 구현해야 할 동작이다.
그래프 순회와 비용
계산 그래프를 출력하려면 모든 부모가 소비자보다 먼저 오는 위상 순서가 필요하다. 구현은 재귀 호출 대신 명시적인 stack을 사용한다. 그래프가 깊어져도 Python call stack을 소모하지 않는다.
비용은 다음과 같다.
- 덧셈 또는 곱셈 한 번: 상수 시간, 결과 노드 하나 할당
V개 노드와E개 부모 간선의 메모리:O(V + E)- 위상 순회 시간:
O(V + E) - 순회 보조 메모리:
O(V)GPU나 NPU 연산은 없다. 모든 연산은 Python scalar 계산과 객체 할당이다. 따라서 이 구현은 빠른 학습기가 아니라 연산 구조를 눈으로 확인하기 위한 화이트박스다.
그래프 검사에는 기본 1,000노드 제한을 뒀다. 다만 이 제한은 순회를 막을 뿐 그래프 생성 전체를 제한하지 않는다. 하나의 guard가 시스템 전체 비용을 통제한다고 과장하지 않고, 이후 sequence 길이, model 크기, training step 경계에 각각 별도의 상한을 둘 것이다.
이번 Stage가 증명한 것
Stage 01은 평범한 스칼라 산술식이 순전파 결과를 계산하면서 정확한 방향성 계산 그래프를 만들 수 있다는 것을 증명했다.
아직 증명하지 않은 것도 분명하다.
- gradient가 올바른가?
- local derivative가 chain rule로 합성되는가?
- 같은 노드가 여러 경로에서 사용될 때 gradient가 합산되는가?
Stage 02에서는
grad와 local derivative를 추가하고 그래프를 역순으로 순회한다. 그 결과를 그럴듯하다고 받아들이지 않고, 유한차분으로 계산한 수치 미분과 비교해 chain rule 구현을 검증한다.