드디어 고대하던 딥러닝을 배우는 한 주가 시작되었다.
이곳 저곳 흩어진 비정형 데이터를 모아 의미있는 값을 추출해낸다는 딥러닝의 아이디어가 너무도 매력적으로 느껴져 열심히 배워보고 싶었지만,,,, 연휴를 지나며 두통을 동반한 감기에 걸리는 바람에 수업을 제대로 따라가지 못하였다.
그나마 다행인 점은 동기 분의 세세한 수업 필기를 받을 수 있어 혼자서라도 따라갈 길이 보였다는 점 ! 감사합니당
딥러닝의 라이브러리로는 세세한 터치가 가능한 pytorch 를 사용하기로 하였으며, pytorch의 기본 자료형인 tensor를 다루는 것부터 수업이 진행되었다.
비정형 데이터의 복잡하고 흩어진 특징을 뽑아내기 위해 이뤄지는 여러 겹의 Layer와 가중치들, 이들을 최적화하기 위해서는 초기 가중치가 여러 Layer를 거친 뒤 최종 결과에 미치는 영향력, 그 미분값에 대한 계산이 필수적인데 딥러닝 프레임워크인 pytorch는 이를 tensor 수준에서 제공한다.
import torch
input_data = torch.tensor([30.2]) # 입력값
output_data = torch.tensor([10.5]) # 결과값
weight = torch.tensor([0.2], requires_grad=True) # 가중치
output_pred = weight * input_data # 예측값
loss = (output_pred - output_data)**2 # 손실함수
위처럼 텐서간의 계산이 존재할 때 가중치가 최종 손실함수에 미치는 영향력(미분값)을 알아야 이를 토대로 weight 값을 고쳐나갈 수 있을 것이다.
loss.backward() # 역전파 계산
weight.grad # 계산된 미분값
--- tensor([-269.3840])
loss.grad_fn # 계산에 사용된 연산 그래프
--- <PowBackward0 at 0x1f4ec3a85e0>
초기 Tensor 생성 시 requires_grad=True 로 설정된 대상은 여러 Layer 를 거치는 과정에서 연산 그래프의 형태로 추적된다.
이를 통해 역전파 계산 요청이 들어왔을 때 연산 그래프를 거꾸로 타고 들어가 미분값을 계산할 수 있게 되는 것이다.
계산과정을 살펴보면, weight가 loss 값에 이르기까지 1) input_data 값을 곱하고 2) output_data 값을 빼고 3) 제곱 연산 을 하였다. 이 연산 그래프를 확인해보자.아래 코드에서 부가적인 내용은 제외하였다
loss.grad_fn
--- <PowBackward0 at 0x1f4ec3a85e0> # 제곱 연산
loss.grad_fn.next_functions
--- <SubBackward0 at 0x1f4ec817f10> # 뺄셈 연산
loss.grad_fn.next_functions.next_functions
--- <MulBackward0 at 0x1f4ec9e95a0> # 곱셈 연산
loss.grad_fn.next_functions.next_functions.next_functions
--- <AccumulateGrad at 0x1f4eae9e860> # 최종 grad 값 누적
이처럼 연산 그래프를 거꾸로 타고 들어가 마지막 weight 에 도착하였을 때 계산된 gradient 값을 누적시키고 끝나는 것을 확인할 수 있었다.
데이터를 불러와 이런저런 전처리 작업을 거친 뒤, 여러 모델과 파라미터를 최적화시키는 머신러닝과 사뭇 다른 딥러닝의 과정을 간략히? 풀어보자.
편의를 위해 torchvision에 존재하는 MNIST 데이터를 사용하겠다.
import os
import torch
from torch.utils.data import DataLoader
from torchvision import dataset, transforms
dataset_dir = 'datasets/MNIST'
os.makedirs(dataset_dir, exist_ok=True)
# 데이터 셋 받아오기
trainset = datasets.MNIST(
root=dataset_dir,
download=True,
transform=transforms.ToTensor(),
train=True)
# 데이터 로더 객체 생성
trainset_loader = DataLoader(
trainset,
batch_size=256,
shuffle=True,
drop_last=True)
확연히 무거워진 데이터 크기에 메모리에 바로 올리지 못하고 다운로드 받아 사용하는 모습이다....
MNIST 데이터는 img 형태로 저장되어 있으므로 이후 모델에 넣어주기 위해 ToTensor 전처리기를 이용해 Tensor 형태로 변환해주었다.
여러 Layer를 순차적으로 쌓는 Squential 방식보다 유연하게 구조를 짤 수 있는 Subclass 방식을 살펴보자. 어차피 순차적으로 쌓을거지만ㅎ
import torch.nn as nn
class MNISTmodel(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 128) # 28*28 픽셀의 MNIST 이미지의 각 픽셀값을 Feature화
self.layer2 = nn.Linear(128, 64)
self.layer3 = nn.Linear(64, 32)
self.layer4 = nn.Linear(32, 10) # 0~9 의 최종 클래스 예측
def forward(self, X):
'''
Args:
X(torch.FloatTensor) - (1, 28, 28)의 shape
'''
X = torch.flatten(X, start_dim=1) # 평탄화 작업
X = self.layer1(X)
X = nn.ReLU()(X) # 활성화 함수
X = self.layer2(X)
X = nn.ReLU()(X)
X = self.layer3(X)
X = nn.ReLU()(X)
return self.layer4(X)
본래 이미지 파일을 분석하는 것이기에 연결된 값들 간의 유사성을 분석하는데 유리한 nn.Conv2d 모델을 사용하고 싶었지만 흐름을 잡는데 초점을 맞추기 위해 Linear 모델만을 사용하였다.
또한, Layer를 이루는 세부 모델은 데이터로 하여금 [배치 크기, 특성 수] 꼴의 입력을 기대하기에 배치 크기를 살리는 평탄화 작업을 진행하였다.
전체 구조의 경우 가장 단순한 Linear 모델을 4개 연결하여 Feature Extraction을 진행했으며, 각각의 과정에서 활성화 함수로서 ReLU 함수를 이용해 Feature 간 비선형성을 도입해주었다.
그런데 문득 든 의문.
그저 음수 형태의 입력값을 0으로 바꾸는 단순한 구조의 ReLU 함수가 복잡한 딥러닝의 비선형성을 모델링하기에 적합한 활성화 함수일까?
Universal Approximation Theorem :
1개의 비선형 활성화 함수만을 포함한 모델이 Neural Network를 통해 어떠한 함수든 근사시킬 수 있다는 이론
활성화 함수 자체는 간단할지 몰라도 그 활성화 함수가 이용하는 Feature 가 굉장히 다양하므로 근사가 된다고 한다 ! 신기....
위에서 우리가 정의한 모델 클래스는 단지 forward method, 데이터를 가지고 하나의 prediction 을 만들어 낼 뿐이다.
다른 부가 기능이 없으므로 머신러닝에서와 다르게 최적화의 기준이 될 Loss Function과 이를 실행시켜줄 Optimizer 객체가 필요하다.
import torch.nn as nn
import torch.optim
LEARNING_RATE = 0.001
model = MNISTmodel() # 모델 객체 생성
loss_fn = nn.CrossEntropyLoss() # 손실 함수 객체 생성
optimizer = torch.optim.Adam( # Optimizer 객체 생성
model.parameters(),
lr=LEARNING_RATE)
다중 분류 문제에서 흔히 사용되는 CrossEntropy를 손실함수로 채택했으며, optimizer로는 여태까지의 gradient 누적 변화 방향을 이용해 방향성을, 최근 최적화 속도를 이용해 학습률을 개선한 Adam Optimizer를 채택하였다.
굉장히 큰 데이터 크기를 감안하여 우리는 전체 데이터를 batch로 나누어 관리하였다. 따라서 전체 데이터 학습 횟수, epoch와 그 내부에서 batch 별로 모델을 따로 학습시키고, 손실함수를 계산하고, 최적화를 시켜야 한다.
for epoch in range(5000):
model.train()
for X_train, y_train in train_loader:
pred = model(X_train) # 모델 학습
loss = loss_fn(pred, y_train) # 손실 함수 계산
loss.backward() # 역전파 계산
optimizer.step() # 가중치 최적화
optimizer.zero_grad() # gradient 초기화
머신러닝에서 모델을 학습시키기 위해 model.fit(X, y) 꼴의 지도학습을 했던 것에 반해 Feature 를 추출해내는 것이 핵심인 딥러닝 모델은 X 값만을 input으로 넣어주게 된다.
또한, 하나의 노드가 여러 가중치를 가지고 여러 Layer 를 거치며 최종값에 영향을 끼칠 수 있는 연산 그래프의 특성상 역전파 계산에서 gradient 는 해당하는 weight에 계속해서 누적되므로 항상 이를 초기화 해주어야한다 !!
추가로 각 epoch 별 학습 현황을 보고자 한다면 epoch 별로 loss의 평균을 계산하여 plotting 해볼 수 있겠다.
MNIST 데이터 셋이 다중분류임을 감안하여 맞춘 갯수를 가지고 모델을 평가해보자.
for epoch in range(5000):
with torch.no_grad():
loss_test = 0
acc_test = 0
for X_test, y_test in test_loader:
# pred : (batch_size, 최종 클래스 개수) 꼴
pred_test = model(X_test)
# loss를 통한 평가
loss_test += loss_fn(pred_test, y_test).item()
# accuracy를 통한 평가
acc_test += torch.sum(y_test == pred_test.argmax(dim=-1)).item()
print(f'{epoch}번 째 loss : {loss_test}/len(test_loader) / acc : {acc_test}/len(testset)')
평가의 경우 별도의 gradient 계산이 필요하지 않기에 with torch.no_grad() 구문을 이용해주었다.
평가방식의 경우 손실함수 자체를 이용하는 방법과 다중 분류의 정답 갯수를 카운팅하는 방법을 사용하였다.
위 예제를 통해 대중적인 MNIST 모델을 대상으로 모델을 설정해 학습하고 평가하는 일련의 과정을 맛보았다.
그런데.... 이걸 활용하려면 데이터 셋을 구성하는 것부터 우리가 손수해야하지 않겠나?
pytorch 는 torchvision , torchaudio , torchtext 등등 여러 패키지에 걸쳐 매우 다양한 데이터 셋을 제공해준다.
torchvision.datasets 의 일부분을 가져와봤다. 보다시피 하위 모듈이 더 존재하지만 편의성을 위해 본 모듈 호출 시 모두 불러와지는 것을 확인할 수 있었다.
from torchvision import datasets
from torch.utils.data import Dataset
dataset = datasets.MNIST( ~ )
type(dataset)
>>> torchvision.datasets.mnist.MNIST
isinstance(dataset, Dataset)
>>> True
그렇기에 datasets.MNIST 꼴로 접근할 수 있었으며 type을 찍어보면 .mnist가 하나 더 들어가 있는 것을 볼 수 있다.
이들은 모두
torch에 존재하는Dataset클래스를 계승한 클래스로 이루어져 있다.
Dataset의 클래스를 계승한 클래스로IterableDataset,TensorDataset,StackDataset등 많은 종류가 정의되어 있다.
그런데 torchvision, torchaudio 등은 데이터의 형식이 모두 다르지 않은가?
각 패키지마다 고유한 전처리기를 제공한다.
torchvision의 경우ToTensor,Normalize,Compose등
data_1 = dataset[0] # 첫 번째 데이터 선택
data_1
>>> (<PIL.Image.Image image mode=L size=28x28>, 5)
from torchvision import transforms
dataset = datasets.MNIST( ~ , transform=transforms.ToTensor()) # ToTensor 객체 전달
data_1 = dataset[0] # 다시 첫 번째 데이터 선택
data_1[0].shape, data_1[1]
>>> (torch.Size([1, 28, 28]), 5)
데이터셋 다운로드 시 이미지 전처리기인 ToTensor() 객체를 전달해주었다. 이미지의 픽셀값 (0~255) 의 범위를 0~1 로 줄여주며, 값을 torch.Tensor 로 변환시켜준다.
참고로 각 이미지의 첫 번째 차원은 채널(channel) 값으로 각 픽셀이 몇 개의 차원으로 이루어졌느냐를 알려주는 동시에 1 = 흑백 / 3 = RGB / 4 = RGBA 값을 나타내게 된다.
이 데이터들은 이제 학습을 위해 모델에 전달되어야 한다. 하지만 머신러닝과 다르게 굉장히 많은 양의 데이터를 필요로하는 딥러닝은 데이터를 한 번에 하나씩 넘기지 않고 지정된 batch size 만큼씩 전달해주어 병렬 연산이 가능하도록 한다.
이를 위한 것이 DataLoader 인 셈.
from torch.utils.data import DataLoader
dataloader = DataLoader(dataset, batch_size=1_000, shuffle=True) # 데이터 로더 객체
dataloader_iter = iter(dataloader) # Iterator 객체 뽑아내기
batch = next(dataloader_iter) # 첫 번째 batch
batch[0].shape, batch[1]
>>> (torch.Size([1000, 1, 28, 28]), torch.Size([1000]))
위처럼 Dataloader 는 iterator 로서 매 반복 batch 를 뱉어낸다...!
이렇게 torchvision 에서 다루는 데이터 셋의 형태와 데이터 셋 로더에 대해 알아보았다. 자체적으로 제공해주는 내용도 굉장히 많지만 원한다면 내가 가진 사진들로 Dataset 클래스를 상속받아 직접 데이터 셋을 만들 수도 있을 것이다 !!! 실제로 해보았지만 너무 길어서 패스
데이터셋이 데이터가 이미지냐 음성이냐에 따라 그 전처리가 달라졌다고 한다면 딥러닝 모델은 풀고자하는 문제에 따라 그 구조가 결정된다.
딥러닝 모델은 입력 레이어 - 히든 레이어 - 출력 레이어 와 그 과정에서 사용될 Loss 함수, 평가지표에 따라 갈리게 된다.
| 항목 | 회귀 모델 | 다중분류 모델 | 이진분류 모델 |
|---|---|---|---|
| Input feature 수 | 입력 데이터 feature 수 | 입력 데이터 feature 수 | 입력 데이터 feature 수 |
| Hidden layer 수 | 경험적(art), 보통 점차 feature 수 줄임 | 동일 | 동일 |
| Output feature 수 | 정답 개수 (예: 집값=1, 주택유형=3 등) | 클래스 수 | 1 (positive 확률) |
| Output Activation 함수 | 보통 없음 (특정 경우 Sigmoid, Tanh 등) | Softmax | Sigmoid (Logistic) |
| Loss 함수 | MSELoss | CrossEntropyLoss (NLLLoss + LogSoftmax) | BCELoss (Binary Crossentropy) |
| 평가지표 | MSE, RMSE, R² | 분류 정확도, F1-score 등 | 정확도, F1-score 등 |
딥러닝 모델은 최종 "값" 을 원하는 문제 유형에 맞춘다.
예를 들어 10개 중 하나로 분류하는 다중 분류 문제의 경우 총 10개의 실수값을 도출한 뒤, Softmax 를 활용하여 이를 각각에 대응하는 확률값으로 만들어버린다. 이진분류의 경우 이 역할을 Sigmoid 함수가 대신 해준다.
이렇게 모델 구조를 정의하였다면 이제 받고자 하는 데이터의 유형에 따라 Dense Layer 를 사용할 것인지, CNN (Convolutional Neural Network) 를 사용할 것인지 아니면 RNN (Recurrent Neural Network) 를 사용할 것인지 등을 통해 내부 레이어를 구성해주면 된다 !!
즉 딥러닝 모델은 풀고자하는 문제 유형에 따라 모델의 구조가 정해지며,
받고자하는 데이터의 유형에 따라 레이어의 종류가 정해진다.
이렇게 정의된 모델은 앞선 DataLoader, LossFunction, Optimizer 등에 의해 학습된다.
model # 학습이 완료된 모델
torch.save(model, "저장 경로") # 모델 전체 저장
new_model = torch.load("저장 경로") # 모델 전체 로드
torch.save(model.state_dict(), "저장 경로") # 모델 파라미터만 저장
new_model.load_state_dict("저장 경로") # 모델 파라미터만 로드
torch.save({ # 체크포인트 저장
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': train_loss
}, "저장경로")
checkpoint = torch.load("저장경로") # 체크포인트 로드
new_model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
epoch = checkpoint['epoch']
loss = checkpoint['loss']
학습까지 했다면 당연히 저장이 필수다 !!! 후에 HuggingFace 등을 이용해 올리는 방법도 있지만 여기서는 로컬에 저장하는 법만 적어봤다.
1) 모델 전체 저장 -> 딥러닝은 모델의 크기가 매우 커서 비효율적인 경우가 많다.
2) 파라미터만 저장 -> 모델의 구조를 다시 재현 후 불러와야한다는 단점이 존재하지만 가볍다.
3) 체크포인트 저장 -> 학습이 오래 걸리는 딥러닝 특성 상 학습 도중에 저장하는 방법이다.
지난 주에 이어 이론으로만 알던 내용을 코드로 직접 작성해보는 시간을 가지고 있는데 정말 너무 유익하고 너무 재밌다. 더 줘! 또 줘!
단점이라면,, 직접 손으로 써보며 이것저것 시도해봐야 머리에 남는 타입인지라 계속 써보며 공부하는 중인데 양이 점점 늘어난다....!!
난 이것말고도 하고 싶은게 천지인데 ㅜㅜ
당장 다음주는 목~금에 걸쳐 소형 프로젝트가 예정되어 있으며 한 달 전 덜컥 신청해버린 코딩 대회도 목요일로 예정되어 있다.
어,,,,, 공모전도 해야하는데,,,,,
아몰랑!@
딥러닝에 대해서 잘 보고 가요~! 화이팅!