2 분 소요

PyTorch는 실행 시 정의되는 계산 그래프를 기반으로 만든 동적·명령형 딥러닝 프레임워크다. 이것이 기존 정적 그래프 프레임워크와 구별되는 가장 근본적인 차이다. PyTorch에서는 Python 코드가 실행되는 동안 그래프가 생성되므로 Python의 디버깅 기능과 유연성을 그대로 활용할 수 있다.

1.1 텐서

텐서는 PyTorch의 핵심 데이터 구조다. 연속된 메모리를 기반으로 하는 n차원 배열이며, 선택적으로 쿠다 또는 애플 메탈 성능 셰이더 장치를 사용할 수 있다. 모든 텐서는 데이터형, 형태, 장치 정보를 가진다.

import torch

# 텐서 생성
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])  # Python 목록에서 생성
z = torch.zeros(3, 4)                        # 0으로 이루어진 3x4 텐서
o = torch.ones(2, 2, dtype=torch.float32)
r = torch.randn(5, 5)                        # 정규분포
a = torch.arange(0, 10, step=2)              # [0, 2, 4, 6, 8]

print(x.shape)   # torch.Size([2, 2])
print(x.dtype)   # torch.float32
print(x.device)  # cpu
생성 함수 기능
torch.tensor(data) Python 목록이나 배열에서 텐서를 생성하며 데이터를 복사한다.
torch.zeros(shape) 0으로 채운 텐서를 생성한다. 1로 채울 때는torch.ones()를 사용한다.
torch.randn(*shape) 정규분포 N(0,1)을 따르는 값을 생성한다. 균등분포 [0,1)는torch.rand()를 사용한다.
torch.arange(start, end, step) Python의 범위와 유사한 1차원 텐서를 생성한다.
torch.linspace(start, end, steps) 일정한 간격의 값을 생성한다.
torch.eye(n) n×n 단위행렬을 생성한다.
torch.empty(shape) 초기화하지 않은 텐서를 생성한다. 속도는 빠르지만 임의의 값이 들어 있다.

1.2 텐서 연산

# 산술 연산(요소별)
a = torch.tensor([1.0, 2.0, 3.0])
b = torch.tensor([4.0, 5.0, 6.0])
print(a + b)  # tensor([5., 7., 9.])
print(a * b)  # 요소별 곱셈
print(torch.matmul(a.unsqueeze(0), b.unsqueeze(1)))  # 내적 -> [[32.]]

# 제자리 연산(이름 끝에 밑줄 사용)
a.add_(1)  # a를 제자리에서 수정하며 결과는 [2., 3., 4.]다.

# 형태 변경
x = torch.randn(4, 6)
y = x.view(2, 12)     # 메모리를 공유하며 연속 메모리가 필요하다.
z = x.reshape(3, 8)   # 필요하면 데이터를 복사한다.
w = x.squeeze()       # 크기가 1인 차원을 제거한다.
v = x.unsqueeze(0)    # 0번 위치에 차원을 추가한다.

# 배열 방식 인덱싱
print(x[0, :])  # 첫 번째 행
print(x[:, 2])  # 세 번째 열

중요: view()는 연속 메모리가 필요하며 저장 공간을 공유한다. 오류가 발생하면 먼저 .contiguous()를 호출하거나 reshape()를 사용한다. 이름이 _로 끝나는 제자리 연산은 기울기 계산이 필요한 텐서에 사용할 수 없다.

1.3 자동 미분

자동 미분은 기울기를 자동으로 계산하는 PyTorch의 엔진이다. 텐서에 requires_grad=True를 설정하면 PyTorch가 해당 텐서에 수행된 연산을 계산 그래프, 즉 grad_fn 연결 구조에 기록한다. 이후 스칼라 손실에서 .backward()를 호출하면 이 그래프를 역방향으로 탐색해 기울기를 계산한다.

x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x + 1  # y = x² + 3x + 1
y.backward()             # dy/dx를 계산한다.
print(x.grad)            # 2x + 3 = 7.0

# 그래프에서 분리
z = x.detach()  # 기울기를 추적하지 않으며 데이터를 공유한다.
w = x.data      # 결과는 같지만 자동 미분의 안전 검사를 우회한다.

# 기울기 추적 비활성화(추론 시 메모리 부담 없음)
with torch.no_grad():
    out = model(inputs)  # 기울기를 추적하지 않는다.

# 누적: 기울기는 기본적으로 누적된다.
optimizer.zero_grad()  # 역전파 전에 항상 호출한다.

중요: 기울기는 기본적으로 누적된다. backward() 전에 optimizer.zero_grad()를 호출하지 않는 실수는 PyTorch 학습 루프에서 가장 흔한 오류 중 하나다.

1.4 동적 계산 그래프와 정적 계산 그래프

PyTorch는 실행 시 정의되는 동적 그래프를 사용한다. 그래프는 순전파 과정에서 즉시 생성되고 backward() 이후 폐기된다. 따라서 모델 내부에서 Python의 표준 제어 흐름인 if, for, while을 사용할 수 있으며 일반 Python 코드를 다루는 방식과 동일하게 디버깅할 수 있다. 텐서플로 1.x는 먼저 정의하고 나중에 실행하는 정적 그래프를 사용했다. 이러한 차이 때문에 PyTorch가 연구자들에게 널리 채택됐다.

개념 의미
실행 시 정의 순전파가 실행될 때마다 그래프를 생성한다. Python의 실행 흐름이 곧 모델의 실행 흐름이다.
requires_grad 텐서별로 기울기 추적을 활성화한다.
grad_fn 텐서를 생성한 함수를 가리킨다. 함수의 연결 구조가 그래프를 이룬다.
.backward() 호출한 텐서에서 역방향 모드 자동 미분을 시작한다.
.grad backward() 이후 리프 텐서에 누적된 기울기를 담는다.
retain_graph=True 여러 차례 역전파할 수 있도록backward() 이후에도 그래프를 유지한다.

댓글남기기