이 모듈의 도달 목표

Tensor와 자동 미분 직접 구현하기

Tensor의 데이터·shape·gradient 역할을 이해하고 계산 그래프와 연쇄 법칙으로 작은 자동 미분 엔진을 구현합니다.

결과물 · 미니 자동 미분 검사기
약 6~8시간
  • Tensor의 data·shape·grad 역할을 설명한다
  • 연산할 때 계산 그래프와 국소 미분을 기록한다
  • 위상 정렬의 역순으로 gradient를 전파한다
  • 자동 미분 결과를 수치 미분으로 검증한다
01

Tensor는 데이터와 학습 정보를 함께 담는다

Tensor는 스칼라·벡터·행렬 같은 숫자 배열을 담고 shape와 자료형을 유지합니다. 학습할 값에는 같은 shape의 gradient 저장 공간과 추적 여부도 함께 둡니다. 실제 프레임워크는 여기에 장치와 연산 기록까지 관리합니다.

일상에서 보면숫자 내용뿐 아니라 크기표와 수정 방향을 적는 칸까지 붙은 상자와 같습니다.
import numpy as np
data = np.array([[1.0, 2.0]])
grad = np.zeros_like(data)
print(data.shape, grad.tolist())
실행 결과
(1, 2) [[0.0, 0.0]]
한 줄씩 보기
  1. 한 행의 데이터를 만듭니다.
  2. 같은 shape의 gradient 공간을 0으로 만듭니다.
  3. 데이터와 gradient가 위치별로 대응합니다.
직접 값 바꾸기data를 (2, 2) 행렬로 바꾸고 grad shape도 함께 바뀌는지 확인하세요.
흔한 실수gradient는 매개변수 하나당 숫자 하나가 아니라 매개변수 data와 같은 shape를 가집니다.

짧은 확인(3, 4) 가중치 Tensor의 grad shape는 무엇일까요?

02

연산 결과는 계산 그래프의 새 노드가 된다

a+b나 a*b를 계산하면 결과값만 만들지 않고 어떤 부모 Tensor와 어떤 연산에서 왔는지 기록합니다. 이렇게 연결된 계산 그래프가 있어야 최종 손실에서 각 입력까지 변화의 경로를 거슬러 갈 수 있습니다.

일상에서 보면완성 요리에 어떤 재료와 조리 단계가 쓰였는지 이력표를 붙이는 것과 같습니다.
# out = a * b
# out._prev = {a, b}
# out._backward = 곱셈의 국소 미분 함수
실행 결과
결과 노드가 부모와 역전파 규칙을 기억합니다.
한 줄씩 보기
  1. 곱셈 결과를 새 노드로 만듭니다.
  2. 두 입력을 부모로 기록합니다.
  3. 나중에 gradient를 보낼 방법을 저장합니다.
직접 값 바꾸기덧셈 결과라면 부모와 국소 미분이 어떻게 달라지는지 적어 보세요.
흔한 실수data 값만 계산하고 부모 연결을 버리면 backward가 입력까지 갈 수 없습니다.

짧은 확인자동 미분을 위해 결과 Tensor가 값 외에 기억해야 할 두 가지는 무엇일까요?

03

연쇄 법칙은 경로의 국소 기울기를 이어 곱한다

최종 손실이 중간값을 거쳐 매개변수에 의존하면 각 구간의 국소 기울기를 곱해 전체 기울기를 구합니다. loss=(w*x+b-y)^2에서 loss→error, error→prediction, prediction→w의 기울기를 이어 곱하면 dw=2*error*x가 됩니다.

일상에서 보면여러 기어가 연결됐을 때 각 기어의 회전 비율을 곱해 처음과 끝의 변화량을 구하는 것과 같습니다.
x, error = 2.0, -1.5
dloss_derror = 2 * error
dprediction_dw = x
print(dloss_derror * dprediction_dw)
실행 결과
-6.0
한 줄씩 보기
  1. 제곱 손실의 error 기울기를 구합니다.
  2. 선형 예측의 weight 기울기는 x입니다.
  3. 두 국소 기울기를 곱해 dw를 얻습니다.
직접 값 바꾸기x를 -1로 바꾸면 dw의 방향이 어떻게 바뀌는지 확인하세요.
흔한 실수중간 경로 하나의 국소 기울기만 사용하면 최종 손실에 대한 기울기가 아닙니다.

짧은 확인prediction=w*x+b에서 prediction을 bias로 미분한 값은 무엇일까요?

04

backward는 출력에서 입력 방향으로 실행한다

forward는 부모를 먼저 계산하지만 backward는 최종 손실의 gradient를 1로 두고 결과에서 부모 방향으로 진행합니다. 모든 자식의 gradient가 모인 뒤 부모를 처리하도록 그래프를 위상 정렬하고 그 순서를 뒤집어 실행합니다.

일상에서 보면여러 지류가 합쳐진 강의 하구에서 출발해 물길을 거꾸로 추적하는 것과 같습니다.
topo = ['w', 'x', 'prediction', 'loss']
for node in reversed(topo):
    print(node)
실행 결과
loss
prediction
x
w
한 줄씩 보기
  1. 부모가 앞에 오도록 정렬된 목록을 준비합니다.
  2. 역순으로 최종 출력부터 방문합니다.
  3. 각 노드가 받은 gradient를 부모에게 보냅니다.
직접 값 바꾸기중간 error 노드를 목록에 넣어 실제 계산 순서를 적어 보세요.
흔한 실수forward와 같은 순서로 backward를 실행하면 아직 자식에게서 gradient를 받지 못한 부모를 먼저 처리할 수 있습니다.

짧은 확인최종 스칼라 손실의 grad를 1로 시작하는 이유는 무엇일까요?

05

gradient는 누적하고 수치 미분으로 검사한다

한 Tensor가 여러 경로에 사용되면 각 경로의 gradient 기여를 더해야 하므로 대입이 아니라 누적합니다. 구현 결과는 매개변수를 아주 조금 앞뒤로 움직여 손실 변화율을 구하는 중앙 차분과 비교해 검사할 수 있습니다.

일상에서 보면여러 지점에서 같은 통장으로 들어오는 금액을 마지막 입금액으로 덮지 않고 모두 합산하는 것과 같습니다.
def f(x): return x * x
x, eps = 3.0, 1e-5
numerical = (f(x + eps) - f(x - eps)) / (2 * eps)
print(round(numerical, 4))
실행 결과
6.0
한 줄씩 보기
  1. 검사할 함수 x²를 정의합니다.
  2. x를 아주 조금 앞뒤로 움직입니다.
  3. 두 함수값 차이로 기울기를 근사합니다.
  4. 자동 미분의 2x와 비교합니다.
직접 값 바꾸기x=-2에서 자동 미분값 -4와 수치 미분값이 일치하는지 확인하세요.
흔한 실수x*x처럼 같은 Tensor가 두 번 쓰일 때 gradient를 덮어쓰면 절반만 남습니다.

짧은 확인gradient 계산이 맞는지 독립적으로 확인하는 간단한 방법은 무엇일까요?

데이터와 gradient를 담는 Tensor

  1. Tensor가 보관할 세 속성을 구분합니다.
  2. 입력을 float64 배열로 통일합니다.
  3. zeros_like로 gradient 공간을 준비합니다.
  4. 추적하는 Tensor와 추적하지 않는 Tensor를 비교합니다.
import numpy as np

class Tensor:
    def __init__(self, data, requires_grad=False):
        self.data = np.array(data, dtype=np.float64)
        self.grad = np.zeros_like(self.data)
        self.requires_grad = requires_grad

rows, cols = map(int, input().split())
values = np.array(input().split(), dtype=np.float64).reshape(rows, cols)
requires_grad = input() == 'true'
tensor = Tensor(values, requires_grad)
print(f'data shape: {tensor.data.shape}')
print(f'grad shape: {tensor.grad.shape}')
print(f'gradient 추적: {tensor.requires_grad}')

핵심을 확인해 볼까요?

5문항 중 4문항 이상 정답이면 완료됩니다. 오답 후에도 다시 제출할 수 있습니다.

1Tensor의 grad shape는 보통 무엇과 같은가?
2계산 그래프의 노드가 기억해야 하는 것은?
3연쇄 법칙이 하는 일은?
4backward를 위상 정렬의 역순으로 실행하는 이유는?
5x*x에서 x.grad를 누적해야 하는 이유는?