본 게시물에서는 실제로 제가 pytorch 공식 문서를 보고 이전에 강의 세션에서 공부했던 개념들로 신경망을 구축하고 학습하는 과정들을 정리해보겠습니다.
https://pytorch.org/vision/stable/datasets.html
Built-in DATASETS
모든 pytorch의 데이터 셋들은 torch.utils.data.Dataset 에 포함되어 있습니다. 그리고 pytorch의 데이터 셋은 __getitem__과 __len__메소드가 구현되어 있습니다. 그리고 torch.utils.data.DataLoader는 데이터를 배치로 불러오는 역할을 하는데, 이 클래스는 멀티프로세싱을 활용하여 데이터를 병렬로 불러올 수 있습니다.
여기서 저희가 쓸 dataset은 FASHIONMNIST데이터 입니다.
여기에서 인자들을 볼 수 있습니다. 먼저 transform: Optional[Callable] = None이라고 되어 있습니다. transform은 A function/transform that takes in a PIL image and returns a transformed version 이라고 되어 있는데, PIL은 강력한 이미지 처리와 그래픽 기능을 제공하는 이미지프로세싱 라이브러리의 한 종류라고 합니다. 여기에는 픽셀 단위의 조작, 마스킹 및 투명도 제어, 흐림, 윤곽 보정 다듬어 윤곽 검충 등의 이미지 필터,,,.. 등의 기능이 있습니다.
우리는 Optional[Callable]( 이는 null과 Callable의 합타입 ) 을 여기다가 집어 넣을 것인데, 우선 PyTorch에서 텐서(Tensor)는 기본적으로 다차원 배열을 나타내는 데 사용되는 데이터 구조입니다. 이는 다양한 차원과 크기를 가질 수 있으며, 단일 값(스칼라), 1D 배열(벡터), 2D 배열(행렬), 또는 그 이상의 차원을 가진 배열 등을 표현할 수 있습니다. 이는 Numpy의 ndarray와 매우 유사하며, 텐서 연산은 CPU또는 GPU에서 수행될 수 있습니다. 이는 GPU연산을 통해 병렬 연산이 가능해서 대규모 텐서 계산을 효율적으로 할 수 있습니다.
우리는 ToTensor()와 normalize(tensor, mean, std[,inplace])를 사용해서 일단 FASHIONMNIST datasets를 변형해서 우리가 지정한 경로에 다운로드까지 시켜보겠습니다. 그리고 Compose를 통해 이 transform을 묶을 수 있습니다. 아참 그리고 데이터를 정규화도 할 거기 때문에 Noramlize를 해서 mean=0.5, std=0.5 가우시안 분포를 따르도록 정규화하겠습니다.
compose의 인자로는 transforms즉 list of Transform objects가 들어간다고 되어있습니다.
data_root = os.path.join(os.getcwd(), "data")
# 전처리 부분 (preprocessing) & 데이터 셋 정의
transform = transforms.Compose(
[
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5]), # mean, # std
]
)
fashion_mnist_dataset = FashionMNIST(data_root, download=True, train=True, transform=transform)
이렇게 코드를 짜겠습니다.
그리고 __getitem__을 구현해 놓았기 때문에, [0], [1]로 image, target이 잘 접근되는지도 확인해보도록 하겠습니다.
잘 출력됩니다. 그리고 random_split을 사용해서 데이터셋을 분할해보도록 하겠습니다.
이 random_split함수는 주어진 길이 리스트에 따라 데이터 셋을 무작위로 분할합니다. 이것은 즉시 무작위성을 적용하기 때문에 데이터 셋의 원래 순서를 무시하고 데이터를 무작위로 섞습니다. 이것은 종종 원치 않은 결과를 초래할 수 있습니다. 그래서 우리는
from typing import List, Dict
import numpy as np
import torch
def dataset_split(
dataset: torch.utils.data.Dataset,
split: List[float] = [0.9, 0.1],
random_train_val_split: bool = False,
) -> Dict[str, torch.utils.data.dataset.Subset]:
"""split torch.utils.data.Dataset by given split ratio.
Written by Jungbae Park.
Args:
dataset (torch.utils.data.Dataset): input interaction dataset.
split (List[float], optional): split ratio.
len(split) should be in [2, 3] & sum(split) should be 1.0
if len(split) == 2:
return {
"train": train_dataset,
"val": val_dataset
}
elif len(split) == 3:
return {
"train": train_dataset,
"val": val_dataset,
"test": test_dataset
}
Defaults to [0.9, 0.1].
random_train_val_split (bool, optional):
if it's True, will randomly mix mix of train, val indices.
In that case, test_dataset will remain
as the last portion of all_dataset.
else, will keep order for splits (sequential split)
Defaults to False.
Returns:
Dict[str, torch.utils.data.dataset.Subset]:
return subset of datasets as dictionaries.
i.e. {
"train": train_dataset,
"val": val_dataset,
"test": test_dataset
}
"""
assert len(split) in [2, 3]
assert sum(split) == 1.0
for frac in split:
assert frac >= 0.0
indices = list(range(len(dataset)))
modes = ["train", "val", "test"][: len(split)]
sizes = np.array(np.cumsum([0] + list(split)) * len(dataset), dtype=int)
# sizes = [0, train_size, train_size+val_size, len(datasets)]
if random_train_val_split:
train_and_val_idx = indices[: sizes[2]]
random.shuffle(train_and_val_idx)
indices = train_and_val_idx + indices[sizes[2] :]
datasets = {
mode: torch.utils.data.Subset(
dataset, indices[sizes[i] : sizes[i + 1]]
)
for i, mode in enumerate(modes)
}
return datasets
위와같이 새로 작성된 dataset_split함수를 통해 분할 비율과 함께 선택적인 무작위 분할 기능을 제공하는 함수를 통해 분할해보겠습니다. 즉 랜덤성이 덜 보장되게 해서 값을 예측할 수 있게 해보겠다는 것입니다.
datasets = dataset_split(fashion_mnist_dataset, split=[0.9, 0.1])
train_dataset = datasets['train']
val_dataset = datasets['val']
train_batch_size = 100
val_batch_size = 10
위와같이 train_dataset과 val_dataset을 나누었습니다. 그리고 mini-batch size도 위와같이 설정해 주었습니다. 그리고 데이터를 배치 단위로 묶기 위해서 DataLoader를 통해 코드를 짜줍니다.
train_dataloader = torch.utils.data.DataLoader(
train_dataset, batch_size=train_batch_size, shuffle=True, num_workers=1
)
val_dataloader = torch.utils.data.DataLoader(
val_dataset, batch_size=val_batch_size, shuffle=False, num_workers=1
)
그리고 shuffle을 통해 데이터 셋을 섞을 수도 있고, num_workers를 통해 병렬 프로세싱도 할 수 있습니다.
for sample_batch in train_dataloader:
print(sample_batch)
print(sample_batch[0].shape, sample_batch[1].shape)
break
그리고 위와같은 코드로 for문을 돌면서 train batch를 확인하고 shape도 확인해보겠습니다.
그럼 결과가 잘 나오게 되는데, 첫번째는 image, 두번째는 label이 들어고게 되는데 image의 shape을 보면 [100, 1, 28, 28]입니다. 이는 각각 [batch_size, channel, width, height]입니다. 여기서 우리가 batch_size를 100으로 해줬고 channel은 fashion_mnist dataset이 흑백 사진이므로 1, 그리고 width*height는 28*28임을 확인할 수 있습니다. 그리고 label의 크기는 100이 나온 것을 볼 수 있습니다. 이는 당연히 우리가 정한 미니배치 사이즈와 일치해야 하는 것이겠죠
이제 모델을 정의해야 하는 일이 남았습니다. 여기서는 torch.nn을 사용해서 모델링해보겠습니다.
# Define Model.
class MLP(nn.Module):
def __init__(self, in_dim: int, hl_dim: int, h2_dim: int, out_dim: int):
super().__init__()
self.linear1 = nn.Linear(in_dim, hl_dim)
self.linear2 = nn.Linear(hl_dim, h2_dim)
self.linear3 = nn.Linear(h2_dim, out_dim)
self.relu = F.relu
pass
def forward(self, input):
x = torch.flatten(input, start_dim=1)
x = self.relu(self.linear1(x))
x = self.relu(self.linear2(x))
out = self.linear3(x)
# dout = F.softmax(out)
return out
우리는 모델을 정의할 때 무조건 nn.Module을 상속해서 해야합니다. 그 이유는 CPU -> GPU환경으로 마이그래이션이 가능하게 하려면 무조건 해야하는 것이죠. 위는 MLP의 전형적인 모습입니다.
생성자로 우리는 우선 input dimension, hidden-layer의 dimension, hidden2-layer의 dimension, output-dimension을 알아야 합니다. 그리고 당연히 nn.Module의 생성자를 통해 MLP class를 초기화 해야합니다. 우리는 MLP를 할 때 중간 hidden layer에서 linear function의 결과를 activation function을 거쳐서 다음 입력으로 넘어간다고 했습니다.
공식문서에서 nn.Linear는 위와같습니다. 이는 in_features, out_feature, bias를 받게 됩니다.
그리고 activation function으로는 ReLU를 사용해주도록 하겠습니다. 그리고 우리의 인풋은 미니배치 100을 빼면 [1, 28, 28]이라고 했습니다. 근데 이를 linear function에 집어넣어주려면 이를 flattening해주어야 합니다. 그래서 start_dim=1을 해줘서 1차원 배열로 flattening해주었습니다. 그리고 forward에다가 x값을 linear, activation function을 차례대로 적용해줍니다. 그리고 맨 마지막에 일단은 softmax를 감싸주어서 classification을 해주지 않겠습니다. 이로써 모델의 1차 정의가 끝났습니다.
그리고 우리는 모델 선언 및 손실함수, 최적화정의, Logger를 정의해주겠습니다.
# define model.
# 10가지 종류로 classification
model = MLP(28*28, 128, 64, 10)
# define loss
loss_function = nn.CrossEntropyLoss()
# define optimizer -> Adam의 learning rate를 10-3으로 설정
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
max_epoch = 15
# define tensorboard logger
writer = SummaryWriter()
log_interval = 100
우선 input dimension은 1*28*28이였습니다. 그리고 h1은 128, h2는 64, output은 10개로 classification할거니까 10으로 지정해주었습니다. 그리고 loss function은 이전에 살펴보았던 Cross entropy를 사용해주도록 하겠습니다. 그리고 optimizer로는 Adam을 사용해주었습니다. 이건 나중에 더 자세히 살펴보겠습니다. 그리고 epoch는 15개로 일단 돌아보도록 하겠습니다, 마지막으로 tensorboard logger를 설정해주겠습니다. 그리고 마지막으로 나중에 학습할때 배치마다 100개의 interval로 train_loss, train_acc를 찍기 위해 100을 interval로 주었습니다.
%load_ext tensorboard
%tensorboard --logdir runs/
# train_step을 초기화 해준다.
train_step = 0
# epoch를 돌면서 미니배치들을 학습해간다. -> train dataset shuffle=True를 해서 과적합 방지
for epoch in range(1, max_epoch+1):
# valid step을 통해 variance등을 체크하여 overfitting을 체킹할 수 있을 것이다.
with torch.no_grad():
val_loss = 0.0
val_corrects = 0
# train을 하면서 validate까지 해준다.
for val_batch_idx, (val_images, val_labels) in enumerate(
tqdm(val_dataloader, position=0, leave=True, desc="validation") # tqdm을 통해 가시화
):
# forward step -> model의 forward실행
val_outputs = model(val_images)
_, val_preds = torch.max(val_outputs, 1) # predict한 index를 추출한다.
# los & acc
val_loss += loss_function(val_outputs, val_labels) / val_outputs.shape[0] # batch size만큼 나눠서 평균을 내겠다
val_corrects += torch.sum(val_preds == val_labels.data) / val_outputs.shape[0]
# valid step logging -> 최종적으로 len(val_dataloader)만큼 나누어 주어야 한다.
val_epoch_loss = val_loss / len(val_dataloader)
val_epoch_acc = val_corrects / len(val_dataloader)
# epoch마다 validate셋의 loss, acc를 찍는다.
print(f"{epoch} epoch, {train_step} step: val_loss: {val_epoch_loss}, train_acc: {val_epoch_acc}")
# tensorboard에 가시화
writer.add_scalar("Loss/val", val_epoch_loss, train_step)
writer.add_scalar("Acc/val", val_epoch_acc, train_step)
writer.add_images("images/val", val_images, train_step)
val_loss = 0.0
val_corrects = 0
# train data set학습
for batch_idx, (images, labels) in enumerate(
tqdm(train_dataloader, position=0, leave=True, desc="train")):
current_loss = 0.0
current_corrects = 0
# get prediction -> 이 2개를 계속 체크 할것임
outputs = model(images) # model에 대해 forward를 수행
_, preds = torch.max(outputs, 1)
# get loss (Loss 계산)
loss = loss_function(outputs, labels) # plogQ 형태
# Backpropagation
# optimization 초기화
optimizer.zero_grad()
# Perform backward pass
loss.backward()
# Perform Optimization
optimizer.step()
current_loss += loss.item()
# 우리가 얼마나 맞추었는지도 알아야 한다. -> torch.sum을 통해 텐서연산 최적화
current_corrects += torch.sum(preds == labels.data)
if train_step % log_interval == 0:
train_loss = current_loss / log_interval
train_acc = current_corrects / log_interval
print(f"{train_step}: train_loss: {train_loss}, train_acc: {train_acc}")
writer.add_scalar("Loss/train", train_loss, train_step)
writer.add_scalar("Acc/train", train_acc, train_step)
writer.add_images("images/train", images, train_step)
writer.add_graph(model, images)
current_loss = 0
current_corrects = 0
train_step += 1
코드의 주석으로 충분히 이해할 수 있을 것입니다. 이를 실행하면 아래와 같이 됩니다.
우리는 이를 validation과정을 통해 실제 variance가 높아지고 있는지 확인할 수 있고 이를 통해 과적합도 피해서 early stopping을 할 수 있습니다. 보면 train_loss는 계속 줄고(무조건 계속 줌), train_acc는 과적합이 되진 않았지만 들쭉날쭉하게 미세한 차이로 증가하는 것을 볼 수 있습니다. 우선 6epoch 까지 학습한 모델을 tensorboard에서 확인도 하고 저장해보도록 하겠습니다.
실제로 그래프로 볼 수 있으며, 실제 val acc이 train acc보다 작음을 알 수 있습니다.
# save model
os.makedirs("./logs/models", exist_ok=True)
torch.save(model, "./logs/models/mlp.ckpt")
# load model
loaded_model = torch.load("./logs/models/mlp.ckpt")
loaded_model.eval()
print(loaded_model)
그 다음으로 모델을 저장해주었습니다. 이제 저장한 모델을 마지막으로 softmax를 씌워서 정확도와 ROC Curve를 그려보도록 하겠습니다.
def softmax(x, axis=0):
"numpy softmax"
max = np.max(x, axis=axis, keepdims=True)
e_x = np.exp(x - max)
sum = np.sum(e_x, axis=axis, keepdims=True)
f_x = e_x / sum
return f_x
l = np.array([[1.3, 5.1, 2.2, 0.7, 1.1], [7.3, 2.1, 3.1, 2.7, 1.6]])
softmax(l, axis=1)
여기서 각 값들을 정규화 하고 softmax를 진행해 주었습니다.
test_batch_size = 100
test_dataset = FashionMNIST(
data_root, download=True, train=False, transform=transforms.ToTensor())
test_dataloader = torch.utils.data.DataLoader(
test_dataset, batch_size=test_batch_size, shuffle=False, num_workers=1)
test_labels_list = []
test_preds_list = []
test_outputs_list = []
for i, (test_images, test_labels) in enumerate(
tqdm(test_dataloader, position=0, leave=True, desc="testing")):
# forward step
test_outputs = loaded_model(test_images)
_, test_preds = torch.max(test_outputs, 1)
# apply softmax activation function
final_outs = softmax(test_outputs.detach().numpy(), axis=1)
test_outputs_list.extend(final_outs)
test_preds_list.extend(test_preds.detach().numpy())
test_labels_list.extend(test_labels.detach().numpy())
test_preds_list = np.array(test_preds_list)
test_labels_list = np.array(test_labels_list)
print(f"\nacc: {np.mean(test_preds_list == test_labels_list)*100}%")
위와 동일하게 fashion mnist dataset의 test set을 가져온다음에 우리가 학습시킨 모델의 정확도를 측정하였습니다.
그리고 ROC Curve를 sklearn을 통해 그려보았는데, 각 class별로 fpr, tpr을 통해 성능을 확인해보았습니다.
# ROC Curve
from sklearn.metrics import roc_curve
from sklearn.metrics import roc_auc_score
fpr = {}
tpr = {}
thresh = {}
n_class = 10
for i in range(n_class):
fpr[i], tpr[i], thresh[i] = roc_curv = roc_curve(test_labels_list, np.array(test_outputs_list)[:, i], pos_label=i)
# plot.
for i in range(n_class):
plt.plot(fpr[i], tpr[i], linestyle="--", label=f"Class {i} vs Rest")
plt.title("Multi-class ROC Curve")
plt.xlabel("False Postiive Rate")
plt.ylabel("True Positive Rate")
plt.legend(loc="best")
plt.show()
print("auc_score", roc_auc_score(test_labels_list, test_outputs_list, multi_class="ovo", average="macro"))
'AIML > 딥러닝 최신 트렌드 알고리즘' 카테고리의 다른 글
[딥러닝 논문 리뷰 - PRMI Lab] - Deep Residual Learning for Image Recognition (CVPR 2016) + 실습 (0) | 2023.06.29 |
---|---|
[딥러닝 논문 리뷰 - PRMI lab] - 배치 정규화(Batch Normalization) + 보편적 근사 정리(Universal Approximation Theorem) (0) | 2023.06.29 |
[딥러닝 최신 트렌드 알고리즘] - Feedforward Network (0) | 2023.06.26 |
[딥러닝 최신 알고리즘] - 정보이론 (엔트로피, KL 발산, 크로스 엔트로피) (0) | 2023.06.25 |
[딥러닝 기초 알고리즘] - ML 기초 (0) | 2023.06.25 |