PyTorch dtype 정리
ssafy ai 강의 실습중 궁금증 해결 2
라벨을 long으로 바꾸라고?
오늘 실습을 하다가 재밌는 일이 있었다.
요구사항에 이렇게 적혀 있었다.
Feature는 float 자료형으로, 라벨은 long 자료형으로 변환할 것
float는 알겠는데... long? 파이썬 쓰면서 long을 써본 적이 한 번도 없었다. 애초에 파이썬에 long이라는 타입이 있긴 한가? 잠깐 멍하니 화면을 봤다.
>>> long(3)
NameError: name 'long' is not defined
역시 없다. Python 2에는 있었지만 PEP 237로 int에 통합되면서 사라졌다. 그러니까 파이썬 관점에서 "long으로 변환하라"는 말은 성립하지 않는다.
알고 보니 파이썬 얘기가 아니라 PyTorch 텐서의 dtype 얘기였다.
먼저 결론부터
torch.long은torch.int64의 별칭이다. PyTorch가 C++ 뿌리라서 C 스타일 이름을 쓴다- 분류 라벨은 long이어야 한다. 클래스 인덱스로 쓰이기 때문이고, int32도 안 되고 정확히 int64여야 한다
- Feature는 float32여야 한다. 모델 가중치가 float32인데 numpy 데이터는 float64라서 안 맞는다
- ⚠️
float과torch.float은 다르다. 앞은 파이썬 내장 타입이라 float64, 뒤는 torch 별칭이라 float32다 - 헷갈릴 바엔
torch.float32/torch.int64처럼 비트 수를 숫자로 명시하면 고민이 사라진다 - 단 회귀나 이진 분류에서는 라벨도 float이다. 무지성
.long()금지 아래는 각각이 왜 그런지에 대한 이야기다.
torch.long은 int64의 별칭이다
둘은 완전히 같은 것이다. 이름만 두 개다.
import torch
y = torch.tensor([0, 1, 2])
print(y.dtype) # torch.int64
print(y.long().dtype) # torch.int64 ← 똑같다
print(torch.long is torch.int64) # True
PyTorch가 이런 이름을 쓰는 이유는 뿌리가 C++이기 때문이다. libtorch(ATen) 레벨에서 타입 이름을 C 계열 관습대로 지었고, 파이썬 바인딩에도 그 이름이 그대로 올라왔다. 그래서 C 스타일 별칭이 한 세트 존재한다.
torch.half is torch.float16 # True → .half()
torch.float is torch.float32 # True → .float()
torch.double is torch.float64 # True → .double()
torch.short is torch.int16 # True → .short()
torch.int is torch.int32 # True → .int()
torch.long is torch.int64 # True → .long()
즉 요구사항을 번역하면 **"feature는 float32로, 라벨은 int64로 만들어라"**였다. 이름만 낯설었지 내용은 평범했다.
기본 dtype 추론 규칙
정수는 int64, 실수는 float32로 추론된다. 실수만 64가 아닌 게 좀 비대칭이다.
torch.tensor([1, 2]) # torch.int64 ← 정수 리터럴
torch.tensor([1., 2.]) # torch.float32 ← 실수 리터럴
torch.get_default_dtype()이 float32이기 때문이다. 딥러닝에서 float64를 쓸 일이 거의 없으니 기본값을 32로 잡아둔 것이다.
그리고 대문자 Tensor와 소문자 tensor가 다르다. 소문자만 쓰면 된다.
torch.Tensor([1, 2]).dtype # torch.float32 ← 항상 float32로 강제
torch.tensor([1, 2]).dtype # torch.int64 ← 데이터를 보고 추론
torch.Tensor는 옛날 API라 dtype 인자도 못 받고 무조건 float32다.
numpy에서 가져올 때는 또 다르다. from_numpy는 원본 dtype을 그대로 물고 온다.
import numpy as np
arr = np.random.rand(3) # numpy 기본은 float64
torch.from_numpy(arr).dtype # torch.float64
메모리를 공유하기 때문에 dtype을 바꿀 수 없다. 이게 나중에 문제를 일으킨다.
왜 하필 float + long 조합일까
이건 그냥 관습이 아니라 안 지키면 에러가 나는 규칙이다.
라벨이 long이어야 하는 이유
CrossEntropyLoss가 라벨을 "몇 번째 클래스인지"를 가리키는 인덱스로 쓰기 때문이다. 내부적으로 정답 클래스의 확률만 뽑아오는 인덱싱 연산을 하는데, 인덱스는 당연히 정수여야 하고 PyTorch는 이 인덱스 타입으로 int64를 요구한다.
라벨이 float이면 이런 에러가 난다.
RuntimeError: expected scalar type Long but found Float
재밌는 건 int32(torch.int)여도 안 된다는 점이다.
RuntimeError: "nll_loss_forward_reduce_cuda_kernel_2d_index"
not implemented for 'Int'
"정수면 되겠지"가 아니라 정확히 int64여야 한다. .int()가 아니라 .long()인 이유다.
Feature가 float이어야 하는 이유
모델 가중치가 float32인데, numpy에서 온 데이터는 float64라 안 맞기 때문이다. 이쪽은 더 흔하게 당한다.
X = np.random.rand(10, 3) # float64
t = torch.from_numpy(X) # torch.float64 ← 여기서 어긋난다
model(t)
RuntimeError: expected scalar type Double but found Float
에러 메시지가 은근히 헷갈린다. "Double을 기대했는데 Float이 나왔다"고 하니 내 데이터가 Float인 줄 알기 쉬운데, 실제로는 입력이 Double(float64)이라서 가중치(Float32)와 안 맞는다는 뜻이다. 읽는 방향이 반대다.
float64를 안 쓰는 이유는 단순하다. 메모리가 두 배로 들고, GPU는 float32(혹은 float16) 연산에 최적화돼 있어서 훨씬 빠르다. 딥러닝에선 정밀도보다 속도가 훨씬 중요해서 float32가 사실상 표준이다.
진짜 함정: float과 torch.float은 다르다
torch. 하나 붙었냐 안 붙었냐로 비트 수가 두 배 차이 난다. 여기서 한 번 더 걸렸다.
X = torch.tensor(data, dtype=float)
print(X.dtype) # torch.float64 ← 엥?
torch.float이 float32라고 방금 배웠는데, 앞에 torch.를 안 붙였더니 float64가 나왔다.
이유는 PyTorch가 dtype 인자로 두 종류의 물건을 받기 때문이다.
# 파이썬 내장 타입을 넘긴 경우 → numpy 관습을 따름
torch.tensor([1, 2], dtype=float) # torch.float64
torch.tensor([1, 2], dtype=int) # torch.int64
torch.tensor([1, 2], dtype=bool) # torch.bool
torch.tensor([1, 2], dtype=complex) # torch.complex128
# torch dtype 객체를 넘긴 경우 → C 관습을 따름
torch.tensor([1, 2], dtype=torch.float) # torch.float32
torch.tensor([1, 2], dtype=torch.int) # torch.int32
torch.tensor([1, 2], dtype=torch.long) # torch.int64
파이썬 내장 float는 실제로 C의 double(64비트)이고, 내장 int는 임의 정밀도다. "가장 가까운 대응"으로 매핑하면 float64/int64가 맞다. numpy가 오래전부터 그렇게 해왔고(np.array(x, dtype=float)도 float64다) PyTorch가 호환성 때문에 따라간 것이다.
반면 torch.float은 C++ 쪽 이름이라 C 관습대로 32비트다. 같은 단어인데 출신이 다르니 크기도 다르다.
왜 이게 문제가 되나
라벨 쪽은 운이 좋다. dtype=int나 dtype=torch.long이나 둘 다 int64라 결과가 같다. 그래서 "int 써도 잘 되네?" 하고 넘어가기 쉽다.
그런데 feature 쪽에서 똑같이 하면 터진다.
X = torch.tensor(data, dtype=float).to('mps')
# TypeError: Cannot convert a MPS Tensor to float64 dtype
# as the MPS framework doesn't support float64.
Apple Silicon의 MPS 백엔드는 float64를 아예 지원하지 않는다. Metal 셰이딩 언어에 double 타입 자체가 없기 때문이다. CUDA에서는 에러 대신 앞서 본 expected scalar type Double but found Float가 뜨거나, 운 나쁘면 조용히 느려진다.
메서드는 안 헷갈린다
.float(), .int() 같은 메서드에는 파이썬 내장 타입이 낄 자리가 없어서 항상 torch 관습이다.
t.float() # torch.float32
t.int() # torch.int32
t.long() # torch.int64
t.double() # torch.float64
실제로는 이렇게 쓴다
비트 수를 숫자로 박아버리면 float이냐 torch.float이냐 고민할 일이 사라진다. 요구사항에 "float, long"이라고 적혀 있어도 코드에는 이렇게 쓰는 게 안전하다.
import numpy as np
import torch
X = np.random.rand(100, 4) # float64
y = np.random.randint(0, 3, size=100) # int64
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.int64)
# 또는 메서드 체이닝
X = torch.from_numpy(X).float()
y = torch.from_numpy(y).long()
print(X.dtype, y.dtype) # torch.float32 torch.int64
Dataset 안에서 처리하면 더 깔끔하다.
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, X, y):
self.X = torch.tensor(X, dtype=torch.float32)
self.y = torch.tensor(y, dtype=torch.int64)
def __len__(self):
return len(self.y)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
또 하나의 함정: 회귀에서는 반대다
라벨이 범주면 long, 수치면 float이다. 무조건 .long() 하는 습관이 들면 회귀 문제에서 터진다.
- 다중 분류 —
CrossEntropyLoss→ 라벨은 long (클래스 인덱스) - 이진 분류 —
BCEWithLogitsLoss→ 라벨은 float (0.0 / 1.0) - 회귀 —
MSELoss→ 라벨은 float (연속값)long은 어디까지나 **"이건 값이 아니라 인덱스다"**라는 신호다. 집값을 예측하는데 라벨을 long으로 바꾸면 소수점이 잘려나가고, 이진 분류에서BCELoss에 long을 넣으면 타입 에러가 난다.