1. 왜 파이토치냐?
- 이전에
왜 텐서냐
, 부터 시작해야 한다. - 왜 텐서일까?
- 텐서란 무엇일까?
- 텐서는 배열이나 행렬과 같은 특수 자료 구조. GPU에서 사용할 수 있도록 NumPy의 ndarray를 개량했다. 개념적으로는 배열(array)와 다를게 없다.
- 정말로 다를게 없나?
- 그렇다! 텐서와 넘파이배열은 종종 내부 메모리를 공유하며 서로 형태를 전환할 수 있기까지 하다.
- 텐서란 무엇일까?
- 그러므로 다른 자료형과 구분되는 텐서의 특징은
GPU 사용이 가능
하다는 점이다. 즉, 병렬연산에 최적화 되어있다는 것과 같다. - 그 빠른 병렬연산으로 하는 일이
automatic differentiation
즉, 자동 미분이다.
Tensor is optimized at automatic differentiation
2. 주요 내용
데이터셋 구축 코드와 학습 코드를 분리하는 것이 가독성 및 유지보수면에서 좋다.
2.1. 데이터는 어떻게?
PyTorch
의 데이터셋 관리 방식은 독특하지만 편리하다. 데이터 작업을 위한 기본 요소 두가지가 존재하는데 이는 각각 DataLoader
와 Dataset
이다. 1 데이터를 받아오는건 샘플(feature), 정답(label)으로 구성된 Dataset
이고 각각의 값을 iterable한 객체로 감싸 접근하게 쉽게 만든 객체가 DataLoader
다.
1 모두 torch.utils.data
하위의 모듈이다.
Dataset
: sample, label; 정답이 매칭된 데이터- Dataset을 직접 생성하는 경우도 있는데, 이후에
DataLoader
로 사용하기 위해 다음 세가지 magic method를 구현해야 한다:__init__
,__len__
,__getitem__
- Dataset을 직접 생성하는 경우도 있는데, 이후에
DataLoader
iterable data; data를 minibatch에 전달하는 역할을 하며, 에폭마다 섞는 shuffle을 수행한다. PyTorch의 장점인 multiprocessing으로 속도 향상을 꾀할 수 있다.
- Dataset 객체가 구축될 때 한 번 실행되는 초기화 함수.
- 예를 들어, 이미지 파일과 주석 파일이 포함된 디렉토리와 변형방법 (transform, target_transform) 을 초기화한다.
- len 으로 총 개수를 알았으니 인덱스로 값을 불러올 수 있다.
- 계속해서 이미지를 예로 들었을 때,
pd.readcsv
로self.img_labels
를 불러왔으니DataFrame
형태다.
- 디스크에서 이미지 위치를 식별한다.
torchvision
2 의read_image
method 를 이용해 이미지를 텐서로 변환 한다.__init__
에서 정의한self.img_labels
로 텐서로 변환된 이미지의 라벨 값을 호출한다.- (선택) 필요시 transform 절차를 거친다. 3
- 텐서 이미지와 라벨을 최종 형태인 dictionary 로 변환한다.
def __getitem__(self, idx) -> dict:
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = read_image(img_path)
label = self.img_labels.iloc[idx, 1]
if self.transform:
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label)
sample = {"image": image, "label": label}
return sample
3 왜 하필 이 단계에서 진행하는지는 target_transform
을 확인할 것
2.1.1. 활용은 이렇게 한다.
from torch.utils.data import DataLoader
로 데이터로더를 받아오고, 인자로 데이터셋과 배치사이즈를 전달한다.Dataset
마다 loader가 있어야 하니 train, test 모두DataLoader
로 받아와야한다.- 배치 자동화, 샘플링, 섞기 등 다양한 기능을 내부에서 제공한다.
- 배치사이즈에 맞는 개수의 feature와, label을 묶은 객체의 요소 (batch) 를 반환한다.
- iterable한 객체이므로 for문으로 간단하게 테스트 할 수 있다.
2.2. 모델은 어떻게?
PyTorch의 모델들은 nn.Module
을 상속받는 클래스를 생성해서 정의한다. 모델을 구성하는 기본 요소가 이미 세팅되어있어 __init__에서 세팅만 하면 되니 편리하다.
__init__
함수에서 계층들을 정의하고forward
메서드에서 데이터를 전달하는 방식을 정한다.- 어떤 하드웨어(cpu, gpu, mps)를 사용할지 결정하는 것도 이 단계다.
2.2.1. 예시 모델 확인
tutorials.pytorch.py
# 학습에 사용할 CPU나 GPU, MPS 장치를 얻습니다.
device = (
"cuda"
if torch.cuda.is_available()
else "mps"
if torch.backends.mps.is_available()
else "cpu"
)
print(f"Using {device} device")
# 모델을 정의합니다.
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10)
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
model = NeuralNetwork().to(device)
print(model)
선형 딥러닝 모델의 최종 값이다.
- log + odds
- 정규화 되지 않은 로그 확률로, 모든 실수가 될 수 있다.
- log-odds function은 \(0~1\) 사이의 값을 계산하는 시그모이드의 역함수다.
classification을 진행할 때 중간 레이어에 무엇을 넣든 마지막에 FC layer로 마무리하는데 이때 노드를 분류하는 클래스의 수만큼 만든다. 즉, 이 FC layer를 통과한 결과가 특정 클래스에 해당할 가능성을 의미한다고 볼 수 있다. FC layer에 들어가기 전단계인 활성화 함수로는 Sigmoid, Softmax, ReLU 등을 사용한다. 활성화 함수에 넣기 전의 로그 확률을 logits라고 말하며 PyTorch에서는 실질적으로 이 값을 다룬다.
역순으로 생각하면 쉽다.
1. 분류 문제에서 각 클래스에 해당할 확률을 알아낸다.
2. 그 확률은 활성화 함수라는 값을 통해 나온 값이다.4
3. 위의 활성화 함수에 넣는 값이logit
이다
왜 최종 확률이 아니라 logits을 남겨두는가?
활성화 함수를 통한 값, 확률을 보면 직관적인 이해도가 높아지지만 잠재적으로 값이 누수되거나 연산 과정에서 값이 누락될 위험이 잔존한다. 활성화 함수를 통해 확률을 구하는 과정은 어렵지 않으므로 그 원형인 logits
을 보존하는게 나은 선택이다. 그뿐 아니라 logit은 entropy 연산에서도 사용되므로 남겨두는 편이 활용도가 좋다.
즉, logit을 남겨두는 이유를 아래로 정리할 수 있다.
1. 정보의 손실을 막기 위해
2. cross entropy loss 등의 loss 계열에 사용하기 위해
4 활성화 함수는 확률을 연산하는 함수다.
backward
는?
순전파와 역전파는 역할이 분리된 함수 각각에 포함되어 있다.
- 순전파는 모델을 구성하는 방식이고
- 역전파는 학습에서의 파라미터 ‘최적화’ 과정이다.
.backward()
로 구현되어 있다. 다음 단계인 train 함수에서 사용한다.
model().to(device)
를 해줘야 하는 이유?
- 기본적으로 텐서의 생성 위치는 CPU인데 tensor.to(‘cuda’) 를 통해 GPU로 텐서를 이동 할 수 있다.
- 모든 텐서의 위치가 동일해야 연산을 할 수 있으니 코드 설계에 유의하자.
2.3. 학습은 어떻게?
텐서플로우에서 처럼 fit
으로 끝나는게 아니라 train
함수를 따로 정의해야했다. DataLoader는 학습에 필요한 데이터니 이때 모델이 학습할 수 있게 데이터를 넘겨주고 위에서 선언한 모델과 손실함수, 최적화 함수를 함께 전달한다. 실질적인 학습이 진행되는 곳이라 위에서 정의한 값들을 다 여기에 전달해주는게 맞다.
- 최적화 단계: 하이퍼파라미터를 정의하고 학습하며 파라미터를 조정한다. (train_loop)
- epoch, batch size5, learning rate
- 각 epoch마다 어떤 단계를 거칠 것인가, 모델 설계는 이쪽에 들어간다
5 size에 맞추어 batch를 넘겨줄 수 있는것도 DataLoader가 배치를 만들어주는 역할을 하기 때문이다.
학습 데이터의 분포를 정하는 방식, 학습 단계에서만 사용한다.
학습을 하면서 분포를 정하는 방식인 배치 정규화는 과적합 외에도 기울기 소실 및 폭주를 완화하여 학습을 안정적으로 할 수 있게 돕는데 가중치 \(w\) 가 커질 경우 다음 층에서 학습해야 하는 범위가 커진다. 따라서 학습 중 레이어 단위의 가중치 조절이 필요한데 이 역할이 배치 정규화다.
- 검증 단계 (test_loop)
- 손실함수: 모델 출력인 logit이 여기서 사용된다.
eval()
로 평가모드 전환 잊지 말 것
eval
이 무엇인가
- evaluation의 약자로 모델을 평가하는 과정이다.
- 모델을 평가모드로만 전환하는 단계다.
- dropout 비활성화
- 배치정규화(의 이동평균, 이동분산) 업데이트 정지
- 일관성 있는 결과를 얻을 수 있다. (모델 자체의 성능에 집중할 수 있다.)
tutorials.pytorch.py
def train_loop(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
for batch, (X, y) in enumerate(dataloader):
# 예측(prediction)과 손실(loss) 계산
pred = model(X)
loss = loss_fn(pred, y)
# 역전파
optimizer.zero_grad()
loss.backward()
optimizer.step()
if batch % 100 == 0:
loss, current = loss.item(), (batch + 1) * len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
def test_loop(dataloader, model, loss_fn):
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
pred = model(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
2.4. 모델 관리는 어떻게?
저장하고 불러오기
모델의 형태를 포함하여 저장하고 불러오는 것을 목표로 한다.
모델 전체를 불러오는
torch.save()
[권장] 매개변수만 저장하는
torch.save_dict()
- 문법이 직관적이다.
- 하지만 모델 저장시 사용한 클래스 밑 디렉토리 구조에 종속된다. (매개변수이니 어쩔 수 없다.)
- 불러올 때에도
.eval()
과정을 거친단걸 잊지 말자!
3. 전체 흐름
- 데이터셋 구축
- 모델 구축
- 하이퍼파라미터 정의 (학습)
- 최적화 단계는 어떻게? (학습)
- 모델 정의 방법?