모양은 계약의 일부다
텐서는 수치를 여러 축으로 배치한 자료다. shape는 축마다 원소가 몇 개인지, dtype은 수치 저장 형식, device는 CPU 등 어느 장치에 있는지를 나타낸다. shape=(3,2)는 세 표본에 특징 둘일 수도 있고 세 행/두 열 이미지일 수도 있다. 텐서 자체가 첫 축을 표본이라고 알아내지는 않으므로 각 축의 의미를 문서화한다. 같은 숫자/shape라도 단위와 정규화가 다르면 다른 입력이다.
합성 특징 세 쌍을 [[1,2],[3,4],[5,6]]으로 두자. 첫 축은 표본, 둘째는 [left,right] 순서의 무차원 정규화 특징이다. float32는 원소당 4 byte이므로 원소 6개의 수치 저장량은 24 byte다. Python 객체/allocator·모형·중간 결과 등의 메모리는 포함하지 않았다. 이것만 보고 전체 추론 메모리를 24 byte라고 보고하면 틀리다.
reshape는 원소 수가 맞는 새 모양으로 재배치하는 연산이지 축 의미를 자동 교환하는 연산이 아니다. 독립된 예로 [[1,2,3],[4,5,6]]을 (3,2)로 reshape하면 [[1,2],[3,4],[5,6]]이지만 전치는 [[1,4],[2,5],[3,6]]이다. 원소 수와 최종 shape가 같아도 대응이 다르다. 영상의 HWC를 CHW로 바꿀 때도 단순 reshape만으로 채널/위치가 바르게 옮겨졌다고 보지 않는다. H/W는 행/열, C는 채널이라는 약속이다.
브로드캐스팅도 조용한 오류를 만들 수 있다. 회귀 출력 (B,1)과 목표 (B,)를 바로 빼면 예상한 B개 오차 대신 축 정렬 규칙에 따라 (B,B)가 될 수 있다. B는 배치 표본 수다. shape를 눈으로 확인하고 의미에 맞게 맞춘다. dtype/device도 연산과 손실이 요구하는 계약을 따른다. 분류의 클래스 번호와 회귀의 실수 목표를 같은 형식이라고 가정하지 않는다. 공식 텐서 설명이 기본 속성을 보충한다.
표본을 읽는 책임과 묶는 책임
이번 map-style Dataset은 인덱스에 해당하는 입력/목표 쌍을 반환하고 전체 길이를 제공한다. 사용자 정의 구현에서는 __getitem__과 __len__이 이 역할을 맡는다. TensorDataset은 같은 첫 축의 텐서들을 묶어 이 계약을 제공하는 편리한 구현이다. 입력과 목표를 따로 섞지 말고 같은 인덱스로 함께 읽는다. 무작위 shuffle은 틀린 라벨 대응이나 시간 정렬을 고치는 기능이 아니다.
DataLoader는 표본을 어떤 순서로 읽고 batch로 묶을지 정한다. 자료 세 개에 batch_size=2, shuffle=False, drop_last=False라면 첫 배치는 입력(2,2)/목표(2,1), 마지막은 입력(1,2)/목표(1,1)이다. 마지막 크기가 2라고 하드코딩한 reshape는 실패할 수 있다. drop_last=True이면 마지막 표본이 빠지므로 학습/평가에서 그 의미를 확인한다. num_workers=0은 이 작은 선택 예제에서 현재 프로세스로 읽게 한 설정이며 모든 작업의 최적 성능 설정은 아니다.
평가 손실을 합칠 때도 표본 수를 고려한다. 표본별 손실이 [1,1,9]이면 두 배치 평균은 1/9다. 이를 단순 평균하면 5지만 전체 표본 평균은(1+1+9)/3=11/3≈3.6667이다. 배치 평균에 각 배치 표본 수를 곱해 더한 뒤 총 표본 수로 나누어야 한다. 출력 차원과 손실 reduction이 다르면 무엇을 평균하는지도 명시한다. 이 예는 표본당 스칼라 손실 하나다.
저장된 숫자가 전체 모형은 아니다
state_dict는 파라미터와 등록된 buffer 등 모듈 상태의 이름→텐서 대응이다. 그것을 저장했다고 클래스의 구조·입력 특징 순서·단위·정규화·클래스 이름까지 자동 보존되는 것은 아니다. 동일한 구조를 먼저 만들고 호환되는 상태를 로드해야 한다. 키/shape가 맞아 로드가 성공해도 의미가 맞는지는 별도다.
가상 모형 y=1×left+2×right에 첫 표본[1,2]를 넣으면 5다. 특징 순서를[right,left]로 바꾸고 같은 가중치를 쓰면 4다. shape=(1,2)는 같아도 의미가 바뀌었다. 정규화 평균/표준편차, 영상 색 순서/크기, 목표의 역정규화와 단위를 함께 보존한다. 정규화 통계는 M11.03의 분할 정책에 맞춰 정하고 시험 자료로 다시 맞추지 않는다.
추론에는 eval 모드와 경사 계산 비활성화를 구별한다. eval은 Dropout/BatchNorm 같은 층의 동작을 평가 방식으로 바꾸며 경사 추적을 자동으로 끄지는 않는다. inference_mode/no_grad는 경사 추적과 관련된 별도 설정이다. 이 단순 Linear에는 해당 층이 없지만 실제 모형의 습관을 위해 두 책임을 구분한다. 공식 저장/로드 설명과 함께 구조/입력 계약을 확인한다.
학습 재개는 추론용 가중치 로드보다 더 많은 상태가 필요할 수 있다. optimizer/scheduler와 진행 단계, 데이터 분할/순서·무작위 상태·버전/설정을 기록해야 같은 단계에서 이어 갔다고 비교할 수 있다. 이번 코드는 학습 재개 구현이 아니며 외부 파일을 읽지 않고 직접 만든 상태를 메모리에서 왕복한다. weights_only=True는 가중치 로딩 범위를 제한하지만 임의 외부 파일의 의미/출처 검증까지 대신하지 않는다.
선택 CPU 코드로 형식과 왕복을 확인한다
다음 완전 예제는 작성 시 Darwin/PyTorch 2.13.0 CPU에서 실제 실행했다. 가중치를 손으로[1,2]로 설정하며 학습으로 성능을 얻었다는 예가 아니다. BytesIO는 메모리의 바이트 버퍼다. 디스크 체크포인트 파일이나 모델 구조/전처리 manifest를 생성했다고 주장하지 않는다. 기본 활동은 아래 출력에 해당하는 종이 계산으로 수행할 수 있다.
import io
import torch
from torch.utils.data import TensorDataset, DataLoader
x = torch.tensor([[1., 2.], [3., 4.], [5., 6.]], dtype=torch.float32)
y = torch.tensor([[5.], [11.], [17.]], dtype=torch.float32)
loader = DataLoader(TensorDataset(x, y), batch_size=2,
shuffle=False, drop_last=False, num_workers=0)
model = torch.nn.Linear(2, 1, bias=False)
with torch.no_grad():
model.weight.copy_(torch.tensor([[1., 2.]]))
for xb, yb in loader:
with torch.no_grad():
print(tuple(xb.shape), tuple(yb.shape), model(xb).flatten().tolist())
buffer = io.BytesIO()
torch.save(model.state_dict(), buffer)
buffer.seek(0)
restored = torch.nn.Linear(2, 1, bias=False)
restored.load_state_dict(torch.load(buffer, map_location="cpu", weights_only=True))
restored.eval()
with torch.inference_mode():
print("reload_equal", torch.allclose(model(x), restored(x)))
print("x_bytes", x.numel() * x.element_size())
(2, 2) (2, 1) [5.0, 11.0]
(1, 2) (1, 1) [17.0]
reload_equal True
x_bytes 24
reload_equal은 이 입력/허용치의 수치 비교일 뿐 원시 바이트 전체가 같거나 다른 환경에서 작업이 성공했다는 뜻은 아니다. 기존 5.5 정규화·행동 표현의 state/action 통계·표현도 가중치와 함께 관리한다.