텐서의 차원 개념과 연산 후 차원 변화를 정확히 파악하는 것은 복잡한 신경망 구조를 이해하는 데 핵심입니다. 이 글에서는 PyTorch 텐서 생성과 브로드캐스팅 메커니을 심도 있게 다룹니다.
무작위 텐서 생성
딥러닝에서는 가중치 초기화나 모듈 통과 후 출력 형태 확인 등을 위해 무작위 텐서를 자주 생성합니다. 실제 이미지를 불러올 필요 없이 원하는 형태의 텐서를 빠르게 만들 수 있습니다.
torch.randn 활용
torch.randn()은 표준정규분포(평균 0, 표준편차 1)를 따르는 난수로 텐서를 채웁니다. 모델 파라미터 초기화나 테스트 데이터 생성에 유용합니다.
import torch
# 0차원 (스칼라)
s = torch.randn(())
print(f"스칼라: {s}, 형태: {s.shape}")
# 1차원 (벡터)
v = torch.randn(5)
print(f"벡터 형태: {v.shape}")
# 3차원 (채널, 높이, 너비)
img = torch.randn(3, 224, 224)
print(f"3D 텐서 형태: {img.shape}")
# 4차원 (배치, 채널, 높이, 너비)
batch = torch.randn(4, 3, 224, 224)
print(f"4D 텐서 형태: {batch.shape}")
기타 난수 생성 함수
분포에 따라 다른 함수들도 활용할 수 있습니다.
# 균등분포 [0, 1)
u = torch.rand(3, 2)
# 지정 범위 내 정수
i = torch.randint(low=0, high=10, size=(2, 3))
신경망 통과 시 차원 변화 추적
실제 신경망 모듈을 통과하며 텐서 형태가 어떻게 바뀌는지 추적해봅니다.
import torch.nn as nn
# 입력: (배치=2, 채널=3, 높이=32, 너비=32)
x = torch.randn(2, 3, 32, 32)
print(f"입력: {x.shape}")
# 합성곱 층
conv = nn.Conv2d(3, 16, kernel_size=3, padding=1)
x = conv(x)
print(f"합성곱 후: {x.shape}") # (2, 16, 32, 32)
# 풀링 층
x = nn.MaxPool2d(2, 2)(x)
print(f"풀링 후: {x.shape}") # (2, 16, 16, 16)
# 펼치기
x = x.view(x.size(0), -1)
print(f"펼친 후: {x.shape}") # (2, 4096)
# 전결합 층
x = nn.Linear(4096, 128)(x)
print(f"선형층 후: {x.shape}") # (2, 128)
# 분류기 출력
logits = nn.Linear(128, 10)(x)
print(f"최종 출력: {logits.shape}") # (2, 10)
# 확률 분포로 변환
probs = nn.Softmax(dim=-1)(logits)
print(f"확률 합: {probs.sum(dim=-1)}")
브로드캐스팅 메커니즘
브로드캐스팅은 형태가 다른 텐서 간 연산을 가능하게 하는 강력한 기능입니다. 명시적인 복사 없이 논리적으로 차원을 확장하여 효율적으로 계산합니다.
브로드캐스팅 규칙
- 차원 수가 다르면 왼쪽에 1을 패딩하여 맞춤
- 오른쪽부터 차원을 비교하며, 크기가 같거나 둘 중 하나가 1이면 가능
- 다 1보다 크고 서로 다르면 오류 발생
덧셈에서의 브로드캐스팅
import torch
# (3, 1) + (3,) → (3, 1) + (1, 3) → (3, 3)
a = torch.tensor([[10], [20], [30]])
b = torch.tensor([1, 2, 3])
print(f"a 형태: {a.shape}") # (3, 1)
print(f"b 형태: {b.shape}") # (3,)
result = a + b
print(f"결과 형태: {result.shape}")
print(result)
# tensor([[11, 12, 13],
# [21, 22, 23],
# [31, 32, 33]])
메모리에는 실제로 복사되지 않고 가상 확장되어 연산됩니다.
고차원 브로드캐스팅
# (1, 2, 2) + (1, 2) → (1, 2, 2) + (1, 1, 2) → (1, 2, 2)
a = torch.tensor([[[1, 2], [3, 4]]])
b = torch.tensor([[5, 6]])
print((a + b).shape) # torch.Size([1, 2, 2])
행렬 곱셈에서의 브로드캐스팅
행렬 곱셈(@)은 마지막 두 차원이 곱셈 조건을 만족해야 하며, 나머지 배치 차원은 브로드캐스팅 규칙을 따릅니다.
# 배치 행렬 × 단일 행렬
A = torch.randn(2, 3, 4) # 2개의 3×4 행렬
B = torch.randn(4, 5) # 단일 4×5 행렬 → (1, 4, 5)로 확장
result = A @ B
print(result.shape) # torch.Size([2, 3, 5])
# 부분 배치 브로드캐스팅
A = torch.randn(3, 2, 4) # 3개의 2×4
B = torch.randn(1, 4, 5) # 1개의 4×5 → 3개로 확장
result = A @ B
print(result.shape) # torch.Size([3, 2, 5])
# 고차원 브로드캐스팅
A = torch.randn(2, 3, 4, 5) # 다중 배치, 다중 채널
B = torch.randn(5, 6) # 단일 행렬 → (1, 1, 5, 6) 확장
result = A @ B
print(result.shape) # torch.Size([2, 3, 4, 6])
핵심 정리
| 요소 | 설명 |
|---|---|
| 형태 결정 | 각 차원의 최대값으로 결과 형태 결정 |
| 값 확장 | 크기 1인 차원이 대상 차원 크기로 논리 확장 |
| 메모리 효율 | 실제 데이터 복사 없이 뷰(View)로 처리 |
| 행렬 곱 조건 | A의 마지막 차원 == B의 뒤에서 두 번째 차원 |