처음부터 하는 딥러닝

[딥러닝 기초] 본인 컴퓨터에서 직접 딥러닝 코드를 작성하고 실행해보자

빈이름 2023. 10. 1. 15:09

지금까진 colab을 이용해서 코드를 작성하고 실행해 봤었습니다. 이번엔 본인 컴퓨터에서 직접 코드를 작성하고 실행시켜 보도록 하겠습니다. 그리고 어떤 식으로 코드를 구성하고 관리하면 좋을지에 대해서도 알아보겠습니다.

1. Anaconda
    1-1. Anaconda 설치하기
    1-2. 가상환경 만들어보기
2. 코드 작성하기 (jupyter notebook)
3. 코드 작성하기 (.py파일로 작성하기)
4. 설정값 관리하기
5. 실험 결과 관리하기
    5-1. tensorboard
    5-2. wandb
6. 마무리

1. Anaconda

파이썬으로 프로그램을 짜다 보면 여러가지 라이브러리를 사용하게 됩니다. 그런데 만약 2개 이상의 프로젝트를 동시에 진행하고 있는데 두 프로젝트에서 서로 같은 라이브러리지만 버전이 다른 것을 사용한다면 어떻게 될까요? 프로젝트를 할 때마다 버전에 맞춰서 재설치를 반복해야할 겁니다. Anaconda는 이런 일이 없도록 프로젝트마다 서로 다른 '가상환경'을 만들어서 버전 관리를 용이하도록 도와주는 프로그램입니다.

1-1. Anaconda 설치하기

우선 Anaconda를 설치해 봅시다.

https://www.anaconda.com/download

 

Free Download | Anaconda

Anaconda's open-source Distribution is the easiest way to perform Python/R data science and machine learning on a single machine.

www.anaconda.com

설치는 어렵지 않을 거라 생각하고, 다음으로 넘어가겠습니다.

설치가 완려되었다면 이제 실행해 보겠습니다. Window라면 anaconda prompt를 아래와 같이 찾을 수 있을 것입니다.

window에서 anaconda 실행

실행해 보면 아래와 같은 화면을 볼 수 있습니다.

Anaconda 실행 화면

Linux의 경우엔 환경변수 설정을 해준다면 원래 쓰던 prompt에서 위와 같은 화면을 볼 수 있게 될겁니다.

맨 앞의 (base)는 현재 실행 중인 가상환경의 이름을 나타냅니다. (base)는 가상환경을 실행하지 않은 기본상태를 뜻하며, 가상환경을 실행할 경우 괄호 안의 글자가 가상환경의 이름으로 바뀌게 됩니다.

그 오른쪽의 경로는 현재 anaconda prompt의 경로를 나타냅니다. cd 명령어를 이용해서 경로를 바꿀 수 있습니다.

cd downloads/project

1-2. 가상환경 만들어보기

설치를 무사히 마쳤다면 가상환경을 직접 만들어 보겠습니다. 콘솔창에 아래 명령어를 입력해 보겠습니다.

conda create -n deeplearning

conda : Anaconda를 사용할 땐 앞에 항상 'conda'를 붙입니다.

create -n deeplearning : create는 가상환경을 만드는 명령어이며, 뒤의 -n은 '이름'을 나타냅니다.

 

즉, 위의 명령어는 'deeplearning'이라는 이름의 가상환경을 만드는 명령어입니다.

중간에 Proceed ([y]/n)? 이라는 문구가 나오면 y를 누르고 엔터를 눌러 실행해 주세요.

잘 됐다면 아래 명령어를 입력해 가상환경을 실행할 수 있을 겁니다.

conda activate deeplearning

여기까지 잘됐다면 화면이 위와 같이 될 것입니다. 앞의 글자가 '(base)'에서 '(deeplearning)'으로 변한 것을 확인할 수 있습니다.

이제 python을 설치해 봅시다. 아래 명령어를 통해 python을 설치할 수 있습니다.

conda install python

이럴 경우 자동으로 최신 버전의 python을 설치할 수 있으며, 만약 특정 파이썬 버전이 필요하다면 아래와 같이 '=3.7'과 같이 명령어를 추가하여 원하는 버전을 설치할 수 있습니다.

conda install python=3.7

잘 설치되었다면 콘솔창에 'python'을 입력했을 때 python이 실행되는 것을 확인할 수 있습니다. (앞의 글자가  '>>>'으로 바뀜)

콘솔창에서 python을 직접 실행할 수 있다.

2. 코드 작성하기 (jupyter notebook)

이제 컴퓨터에서 코드를 직접 작성해 보겠습니다. 만약 colab과 같은 노트북 형태의 환경을 원한다면, jupyter notebook을 설치해서 사용하면 됩니다.

pip install notebook

설치가 완료되면 콘솔창에 'jupyter notebook'을 입력하면 인터넷이 켜지면서 다음과 같은 화면을 볼 수 있습니다.

jupyter notebook의 실행화면. 버전에 따라 보이는 화면이 다를 수 있습니다.

아래와 같은 창이 뜨면 'Select'를 누릅니다.

그러면 colab에서 보던 익숙한 화면을 볼 수 있게 됩니다. 다만 이젠 colab에서 제공해주는 서버에서 코드를 작성하는 것이 아니라 내 컴퓨터에서 직접 코드를 작성하고 실행할 수 있게 됩니다.

3. 코드 작성하기 (.py파일로 작성하기)

jupyter notebook은 실행 결과를 바로 확인할 수도 있고 매우 편리하지만, 다른 사람과 협업을 하거나 프로그램을 배포할 때 불편한 사항이 생깁니다. 따라서 notebook 파일이 아니라 '.py'형식의 파일로도 코드를 작성할 수 있어야 합니다. 보통 jupyter notebook에서 코드를 작성한 뒤, py 파일로 옮기는 방식이 편리합니다.

 

딥러닝 프로그램 코드를 작성할 때 정해진 틀은 없지만 그래도 대체로 아래와 같은 형식은 지킵니다.

project1
    ㄴdataset
        ㄴ__init__.py
        ㄴimage_data.py
    ㄴmodels
        ㄴ__init__.py
        ㄴvgg16.py
    ㄴmain.py

일반적으로 데이터셋을 처리하는 코드는 'dataset' 폴더 내에, 딥러닝 모델 코드는 'models' 폴더 내에 작성한 뒤, 최종 코드 실행은 'main.py'로 하는 식으로 많이 작성됩니다.

이런 식으로 폴더를 구분하는 것은 추후에 프로젝트를 관리하는데 도움을 줍니다. 지금은 단순한 프로젝트이기 때문에 이런 구조가 비효율적으로 보일 수도 있지만, 만약 모델을 10개씩 실험해보고 사용하는 데이터셋도 많아진다면 코드가 점점 복잡해질 겁니다. 그렇기 때문에 처음부터 이런 식으로 폴더를 구분해놓고 코드를 구분하는 식으로 시작을 한다면, 나중에 코드의 확장이나 수정이 용이합니다.

 

위 형식대로 한번 vgg16 모델을 이용해 cifar10 데이터셋을 학습하는 코드를 작성해 보겠습니다. 코드 작성은 본인이 편한 툴을 사용하시면 됩니다. 메모장을 써도 되고 pycharm을 써도 되고... 저 같은 경우엔 visual studio code를 주로 사용합니다.

 

지금부터 작성할 코드는 아래 github에서 볼 수 있습니다.

https://github.com/intrandom5/dl_basic_template

 

GitHub - intrandom5/dl_basic_template: 의 코드.

의 코드. Contribute to intrandom5/dl_basic_template development by creating an account on GitHub.

github.com

코드를 실행하기 위해선 pip를 이용해 pytorch와 tqdm을 설치해야 합니다.

# https://pytorch.org/
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117

pip install tqdm

A. 데이터셋 코드

("version1(기본)"의 코드)

우선 dataset 코드부터 작성해 보도록 하겠습니다.

# dataset/image_data.py
import torch
import numpy as np
from torch.utils.data import Dataset
from torchvision.datasets import CIFAR10


class cifar_dataset(Dataset):
    def __init__(self, train=True):
        super(cifar_dataset, self).__init__()
        data = CIFAR10(
            root="../CIFAR10/",
            train=train,
            download=True
        )

        self.images = [np.array(d[0]) for d in data]
        self.labels = [d[1] for d in data]

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        return {
            "images": torch.tensor(self.images[idx]).transpose(0, 2)/255,
            "labels": self.labels[idx]
        }

cifar10 데이터셋을 불러오는 데이터셋 코드("dataset/image_data.py")입니다. 다음으로 "dataset/__init__.py"에서 위의 'cifar_dataset을 불러올 수 있도록 import를 해줍시다.

# dataset/__init__.py
from image_data import cifar_dataset

B. 모델 코드

모델 코드는 아래와 같습니다. 간단히 VGG16 모델을 구현해 보았습니다.

# models/vgg16.py
import torch.nn as nn


def calc_n(input_shape, channels):
    w, h = input_shape
    w /= 2**len(channels)
    h /= 2**len(channels)

    return int(w*h*channels[-1])

class VGG16(nn.Module):
    def __init__(self, 
                 input_shape: tuple, 
                 channels: list, 
                 dense_dims: list, 
                 n_class: int,
                 dropout_rate: int = 0.1,
                 ):
        super(VGG16, self).__init__()
        self.input_shape = input_shape
        self.channels = channels
        self.dense_dims = dense_dims
        self.n_class = n_class
        self.dropout_rate = dropout_rate

        self.conv_layers = self.constrcut_convs()
        self.flatten = nn.Flatten()

        self.dense = self.construct_denses()
        self.cls = nn.Linear(self.dense_dims[-1], n_class)
        self.dropout = nn.Dropout(self.dropout_rate)

    def construct_denses(self):
        in_dim = calc_n(self.input_shape, self.channels)
        dense_list = []
        for dense_dim in self.dense_dims:
            dense_list.append(
                nn.Linear(in_dim, dense_dim)
            )
            dense_list.append(
                nn.ReLU()
            )
            in_dim = dense_dim
        return nn.Sequential(*dense_list)

    def constrcut_convs(self):
        conv_list = []
        in_channel = 3
        for i, out_channel in enumerate(self.channels):
            if i == 0:
                n_conv = 2
            else:
                n_conv = 3
            conv_list.append(
                self.conv_layer(in_channel, out_channel, n_conv)
            )
            conv_list.append(
                nn.MaxPool2d(2, 2)
            )
            in_channel = out_channel
        return nn.Sequential(*conv_list)

    def conv_layer(self, in_channel, out_channel, n_conv=3):
        conv_list = []
        for _ in range(n_conv):
            conv_list.append(
                nn.Conv2d(in_channel, out_channel, (3, 3), stride=1, padding=1),
            )
            conv_list.append(
                nn.ReLU()
            )
            in_channel = out_channel
        return nn.Sequential(*conv_list)
    
    def forward(self, x):
        x = self.conv_layers(x)
        x = self.flatten(x)
        x = self.dropout(x)

        x = self.dense(x)
        x = self.dropout(x)
        
        x = self.cls(x)

        return x

VGG16을 구현한 코드로, VGG16의 구조는 아래와 같습니다.

VGG16의 구조

여기서 데이터셋의 종류에 따라 모델의 구조가 달라질 수 있기 때문에 '이미지 크기', '라벨 클래스의 수'를 파라미터로 추가하였고, 모델의 크기도 원하는 대로 수정할 수 있도록 convolution 레이어의 채널 수 리스트와 dense layer의 채널 수 리스트를 추가로 입력 받을 수 있도록 코드를 구현했습니다.

(예를 들어, 위 이미지와 똑같이 구현하고 싶다면 파라미터를 아래와 같이 설정하면 됩니다.)

model = VGG16(input_shape=(224, 224), channels=[64, 128, 256, 512, 512], dense_dims=[4096], n_class=1000)

'models/vgg16.py'에 위와 같은 코드를 작성하고, 'models/__init__.py'(아래)에 import를 했습니다.

# models/__init__.py
from vgg16 import VGG16

C. main.py

from dataset import cifar_dataset
from models import VGG16
from tqdm import tqdm
import os

import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import DataLoader


def main():
    train_dataset = cifar_dataset(train=True)
    test_dataset = cifar_dataset(train=False)
    train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

    model = VGG16(input_shape=(32, 32), channels=[64, 128, 256, 512], dense_dims=[1000], n_class=10)
    print(model)
    criterion = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-5)

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model_save_dir = "./saved_models"

    model.to(device)
    for epoch in range(5):
        print("epoch :", epoch)
        model.train()
        train_loss = 0
        for data in tqdm(train_loader):
            data = {k: v.to(device) for k, v in data.items()}
            pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            train_loss += loss.detach().cpu().item()

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        print("train loss :", train_loss/len(train_loader))

        model.eval()
        eval_loss = 0
        for data in tqdm(test_loader):
            data = {k: v.to(device) for k, v in data.items()}
            with torch.no_grad():
                pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            eval_loss += loss.detach().cpu().item()

        print("eval loss :", eval_loss/len(test_loader))
        torch.save(model.state_dict, os.path.join(model_save_dir, f"epoch{epoch}.pt"))
    

if __name__ == "__main__":
    main()

'main.py'에서는 데이터셋과 모델을 import한 뒤, 학습하는 코드를 작성합니다.

코드가 잘 작동되는지 anaconda에서 직접 실행하여 확인해 봅시다.

conda activate deeplearning
cd (프로젝트 위치 폴더)
python main.py

실행되면 위와 같이 학습이 진행되는 것을 확인할 수 있다.

4. 설정값 관리하기

코드가 잘 작동한다면 다음으로 넘어가 보겠습니다. 현재 코드에는 'main.py'에 여러 파라미터 값들이 작성되어 있습니다. 그러나 에포크 수, learning rate, 모델 파라미터 등의 값들은 실험마다 자주 바꾸는 값들입니다. 만약 이런 값들을 바꾸고자 한다면 'main.py'에서 직접 해당 파라미터 값들이 들어간 위치를 찾아서 수정을 하고, 또 따로 기록해야 합니다.

직접 파라미터의 위치를 찾고 수정하다 보면 실수하기도 쉽고, 따로 파라미터를 기록하는 일도 정말 번거로운 일입니다.

그렇기 때문에 딥러닝 실험을 효율적으로 진행하기 위해선 여러가지 설정값들을 한 곳에서 한번에 관리할 수 있도록 코드를 작성하는 것이 좋습니다.

 

여기서 활용하기 좋은 라이브러리로 argparse가 있습니다. argparse를 이용하면 'main.py'를 실행할 때 실험에 필요한 파라미터 값들을 함께 입력할 수 있도록 해줍니다.

("version2(argparse)"의 코드)

from dataset import cifar_dataset
from models import VGG16
from tqdm import tqdm
import argparse
import os

import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import DataLoader


def main(args):
    train_dataset = cifar_dataset(train=True)
    test_dataset = cifar_dataset(train=False)
    train_loader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=args.batch_size, shuffle=False)

    model = VGG16(input_shape=args.input_shape, channels=args.channels, dense_dims=args.dense_dims, n_class=args.n_class)
    print(model)
    criterion = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=args.lr)

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model_save_dir = args.model_save_dir
    if not os.path.exists(model_save_dir):
        os.mkdir(model_save_dir)

    model.to(device)
    for epoch in range(args.epochs):
        print("epoch :", epoch)
        model.train()
        train_loss = 0
        for data in tqdm(train_loader):
            data = {k: v.to(device) for k, v in data.items()}
            pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            train_loss += loss.detach().cpu().item()

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        print("train loss :", train_loss/len(train_loader))

        model.eval()
        eval_loss = 0
        for data in tqdm(test_loader):
            data = {k: v.to(device) for k, v in data.items()}
            with torch.no_grad():
                pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            eval_loss += loss.detach().cpu().item()

        print("eval loss :", eval_loss/len(test_loader))
        torch.save(model.state_dict, os.path.join(model_save_dir, f"epoch{epoch}.pt"))
    

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--channels", type=str, help="VGG16의 conv layer들의 채널 수 리스트")
    parser.add_argument("--dense_dims", type=str, help="VGG16의 dense layer들의 채널 수 리스트")
    parser.add_argument("--n_class", type=int, help="데이터셋의 라벨 클래스 수")
    parser.add_argument("--input_shape", type=str, help="모델에 입력되는 이미지 크기")
    parser.add_argument("--model_save_dir", type=str, help="모델 저장 경로")
    parser.add_argument("--batch_size", type=int, help="배치 크기")
    parser.add_argument("--lr", type=float, help="learning rate 설정")
    parser.add_argument("--epochs", type=int, help="학습 에포크 설정")
    args = parser.parse_args()

    args.channels = eval(args.channels)
    args.dense_dims = eval(args.dense_dims)
    args.input_shape = eval(args.input_shape)

    main(args)

argparse를 이용해 수정된 main.py입니다. 실험에서 관리가 필요한 파라미터 값들을 모두 argparse를 통해 입력받도록 코드를 수정하였습니다. 이제 main.py를 실행할 땐 아래와 같이 파라미터값들을 직접 입력해야 합니다.

python main.py \
--channels [64,128,256,512] \
--dense_dims [500] \
--n_class 10 \
--input_shape (32,32) \
--model_save_dir ./saved_model \
--batch_size 64 \
--lr 1e-5 \
--epochs 5 \

argparse를 이용해 파라미터들을 한번에 입력받을 수 있게 되었습니다. 하지만 argparse는 파라미터를 기록하는 기능은 없습니다. 그렇기 때문에 json이나 yaml 파일을 이용해 설정값을 관리하면 더 좋습니다.

이번엔 argparse를 이용해 config 파일 경로를 입력 받으면, 해당 config를 불러와서 사용하는 식으로 main.py를 바꿔봤습니다.

("version3(yaml)"의 코드)

from dataset import cifar_dataset
from models import VGG16
from tqdm import tqdm
import argparse
import yaml
import os

import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import DataLoader


def main(config):
    train_dataset = cifar_dataset(train=True)
    test_dataset = cifar_dataset(train=False)
    train_loader = DataLoader(train_dataset, batch_size=config["batch_size"], shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=config["batch_size"], shuffle=False)

    model = VGG16(input_shape=config["input_shape"], channels=config["channels"], dense_dims=config["dense_dims"], n_class=config["n_class"])
    print(model)
    criterion = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=config["lr"])

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model_save_dir = config["model_save_dir"]
    if not os.path.exists(model_save_dir):
        os.mkdir(model_save_dir)

    model.to(device)
    for epoch in range(config["epochs"]):
        print("epoch :", epoch)
        model.train()
        train_loss = 0
        for data in tqdm(train_loader):
            data = {k: v.to(device) for k, v in data.items()}
            pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            train_loss += loss.detach().cpu().item()

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        print("train loss :", train_loss/len(train_loader))

        model.eval()
        eval_loss = 0
        for data in tqdm(test_loader):
            data = {k: v.to(device) for k, v in data.items()}
            with torch.no_grad():
                pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            eval_loss += loss.detach().cpu().item()

        print("eval loss :", eval_loss/len(test_loader))
        torch.save(model.state_dict, os.path.join(model_save_dir, f"epoch{epoch}.pt"))
    

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--conf", type=str, help="실험 config 경로")
    config = parser.parse_args()
    with open(config.conf, "r") as f:
        config = yaml.load(f, Loader=yaml.Loader)
    config["input_shape"] = eval(config["input_shape"])
    main(config)

yaml 파일을 이용해 config 설정을 따로 받도록 코드를 수정했습니다. configs 폴더를 만들어서 안에 'conf1.yaml'이라는 이름으로 아래와 같이 설정 파일을 작성했습니다.

channels: [64, 128, 256, 512]
dense_dims: [500]
n_class: 10
input_shape: (32, 32)
model_save_dir: "./saved_model/exp1"
batch_size: 64
lr: 1.e-3
epochs: 5

이제 아래 커맨드를 실행하면 앞에 argparse로 했던 것과 똑같이 코드가 실행되는 것을 확인할 수 있습니다.

python main.py --conf configs/conf1.yaml

5. 실험 결과 관리하기

지금까지 실험 설정값들을 관리하여 실험을 효율적으로 진행하는 방법에 대해 알아봤습니다. 이번에는 실험 결과들을 효율적으로 정리하는 툴들을 알아보겠습니다.

5-1. tensorboard

tensorboard는 실험 결과를 기록하는 툴입니다. 조금의 코드를 추가하는 것만으로도 실험 결과들을 보기 좋게 기록할 수 있습니다.

pip install tensorboard

tensorboard를 사용하기 위해선 설치가 필요합니다. 위 커맨드를 입력해 설치해 줍시다.

tensorboard의 대략적인 사용방법을 한눈에 보자면 아래와 같습니다.

from torch.utils.tensorboard import SummaryWriter


# log를 남길 폴더를 선택합니다.
writer = SummaryWriter("./logs")

# 모델 구조를 기록합니다.
writer.add_graph(model, data["images"])

# training loss를 기록합니다.
writer.add_scalar("training loss", train_loss, epoch)
writer.add_scalar("eval loss", eval_loss, epoch)

더 자세한 것은 홈페이지를 참고하면 좋을 것 같습니다.

https://tutorials.pytorch.kr/intermediate/tensorboard_tutorial.html

 

TensorBoard로 모델, 데이터, 학습 시각화하기

PyTorch로 딥러닝하기: 60분만에 끝장내기 에서는 데이터를 불러오고, nn.Module 의 서브클래스(subclass)로 정의한 모델에 데이터를 공급(feed)하고, 학습 데이터로 모델을 학습하고 테스트 데이터로 테

tutorials.pytorch.kr

tensorboard를 main.py에 추가하여 실험 결과를 기록하고 확인해 보도록 하겠습니다.

("version4(tensorboard)"의 코드)

from dataset import cifar_dataset
from models import VGG16
from tqdm import tqdm
import argparse
import yaml
import os

import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter


def main(config):
    train_dataset = cifar_dataset(train=True)
    test_dataset = cifar_dataset(train=False)
    train_loader = DataLoader(train_dataset, batch_size=config["batch_size"], shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=config["batch_size"], shuffle=False)

    model = VGG16(input_shape=config["input_shape"], channels=config["channels"], dense_dims=config["dense_dims"], n_class=config["n_class"])
    print(model)
    criterion = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=config["lr"])

    writer = SummaryWriter(config["log_dir"])
    data = next(iter(train_loader))
    writer.add_graph(model, data["images"])

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model_save_dir = config["model_save_dir"]
    if not os.path.exists(model_save_dir):
        os.mkdir(model_save_dir)

    model.to(device)
    for epoch in range(config["epochs"]):
        print("epoch :", epoch)
        model.train()
        train_loss = 0
        for data in tqdm(train_loader):
            data = {k: v.to(device) for k, v in data.items()}
            pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            train_loss += loss.detach().cpu().item()

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        train_loss /= len(train_loader)
        print("train loss :", train_loss)
        writer.add_scalar('training loss', train_loss, epoch)

        model.eval()
        eval_loss = 0
        for data in tqdm(test_loader):
            data = {k: v.to(device) for k, v in data.items()}
            with torch.no_grad():
                pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            eval_loss += loss.detach().cpu().item()

        print("eval loss :", eval_loss/len(test_loader))
        writer.add_scalar('eval loss', eval_loss, epoch)
        torch.save(model.state_dict, os.path.join(model_save_dir, f"epoch{epoch}.pt"))
    

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--conf", type=str, help="실험 config 경로")
    config = parser.parse_args()
    with open(config.conf, "r") as f:
        config = yaml.load(f, Loader=yaml.Loader)
    config["input_shape"] = eval(config["input_shape"])
    main(config)

config에 tensorboard의 기록을 남길 폴더와 관련된 파라미터 'log_dir'를 추가하였습니다.


channels: [64, 128, 256, 512]
dense_dims: [500]
n_class: 10
input_shape: (32, 32)
model_save_dir: "./saved_model/exp1"
log_dir: "./logs/exp1"
batch_size: 64
lr: 1.e-5
epochs: 5

이제 main.py를 실행하면 'logs/exp1' 경로에 실험 결과가 'events. ...'과 같은 이름으로 기록되는 것을 확인할 수 있습니다.

이 log 파일은 아래 커맨드를 입력하면 시각적으로 확인할 수 있습니다.

tensorboard --logdir ./logs

'--logdir'에 events 파일이 존재하는 경로를 입력하면 결과를 확인할 수 있습니다.

커맨드 창에 나타난 주소 (http://localhost:6006/)으로 접속하면 실험 결과를 아래와 같이 확인할 수 있습니다.

실험 그래프를 그림과 같이 직접 비교할 수 있다.

5-2. wandb

wandb도 실험 기록을 도와주는 툴입니다. 차이점은 tensorboard는 로컬 서버에서 실험 기록을 남기지만 wandb는 자체 서버를 제공해준다는 것입니다. 그렇기 때문에 다른 사람과 실험을 함께 공유할 때 좋지만 유료라는 단점이 있긴합니다...

개인용으로는 100GB 용량까지 무료로 사용할 수 있으며, 이 정도면 개인용으로는 넘치게 사용할 수 있습니다. 개인적으로 많이 사용하는 툴이기도 합니다. wandb 사용법을 알아보겠습니다.

https://wandb.ai/quickstart?utm_source=app-resource-center&utm_medium=app&utm_term=quickstart  

 

quickstart

Weights & Biases, developer tools for machine learning

wandb.ai

우선 wandb 홈페이지에 들어가서 회원가입을 해야 합니다. 구글 계정이나 github 계정으로도 가능합니다. 그 다음 wandb를 설치해야 합니다. 아나콘다 콘솔창에서 가상환경을 켜 준 다음에 아래 커맨드로 설치합시다.

pip install wandb

wandb를 사용하기 위해선 로그인이 필요합니다. 콘솔창에 'wandb login'이라고 치면 아래와 같이 api키를 입력하라고 합니다. wandb 홈페이지에 로그인해서 개인 설정에서 api 키를 찾아 복사 붙여넣기 해줍시다.

저는 이미 로그인을 해놨기 때문에 뒤에 '--relogin'을 붙였습니다. 처음 하실 땐 'wandb login'만 쳐도 됩니다.

이제 main.py를 수정해서 wandb에 기록하도록 해봅시다.

("version5(wandb)"의 코드)

from dataset import cifar_dataset
from models import VGG16
from tqdm import tqdm
import argparse
import yaml
import os

import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import DataLoader
import wandb


def main(config):
    train_dataset = cifar_dataset(train=True)
    test_dataset = cifar_dataset(train=False)
    train_loader = DataLoader(train_dataset, batch_size=config["batch_size"], shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=config["batch_size"], shuffle=False)

    model = VGG16(input_shape=config["input_shape"], channels=config["channels"], dense_dims=config["dense_dims"], n_class=config["n_class"])
    print(model)
    criterion = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=config["lr"])

    wandb.init(
        project="cifar-10",
        config=config
    )
    data = next(iter(train_loader))

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model_save_dir = config["model_save_dir"]
    if not os.path.exists(model_save_dir):
        os.mkdir(model_save_dir)

    model.to(device)
    for epoch in range(config["epochs"]):
        print("epoch :", epoch)
        model.train()
        train_loss = 0
        for data in tqdm(train_loader):
            data = {k: v.to(device) for k, v in data.items()}
            pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            train_loss += loss.detach().cpu().item()

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        train_loss /= len(train_loader)
        print("train loss :", train_loss)
        wandb.log({"train loss": train_loss})

        model.eval()
        eval_loss = 0
        for data in tqdm(test_loader):
            data = {k: v.to(device) for k, v in data.items()}
            with torch.no_grad():
                pred = model(data["images"])
            loss = criterion(pred, data["labels"])
            eval_loss += loss.detach().cpu().item()

        print("eval loss :", eval_loss/len(test_loader))
        wandb.log({"eval loss": eval_loss})
        torch.save(model.state_dict, os.path.join(model_save_dir, f"epoch{epoch}.pt"))
    

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--conf", type=str, help="실험 config 경로")
    config = parser.parse_args()
    with open(config.conf, "r") as f:
        config = yaml.load(f, Loader=yaml.Loader)
    config["input_shape"] = eval(config["input_shape"])
    main(config)

wandb.init()의 경우 훨씬 더 많은 것들이 기록 가능하니 홈페이지 참고해서 필요한 기능들을 사용하면 좋을 것 같습니다. 이제 main.py를 실행하면 wandb에 학습이 기록되는 것을 확인할 수 있습니다.

train loss와 eval loss를 기록한 결과
wandb.init에서 config를 기록하면 이런 식으로 각 실험별 config 값들도 확인할 수 있다.

6. 마무리

이런 식으로 코드를 .py 파일에 직접 작성하며 프로젝트를 관리하는 방법에 대해 알아봤습니다. 여기에 나온 방법이 정석은 아니지만, 맨 처음 시작하는 입장에서 이런 template이 도움이 되었으면 하는 마음에 글을 작성해 봤습니다.

딥러닝 공부를 하다 보면 github의 여러가지 오픈 소스들을 활용하게 될 건데 이런 오픈 소스의 코드들을 참고하는 것도 편리한 코드 작성 방법을 익히는데 도움이 될 겁니다.

딥러닝은 워낙 실험을 많이 진행하기 때문에 계속 하다 보면 실험 할 때마다 이런 부분이 불편하다, 이런 부분이 자동화 됐으면 좋겠다 하는 부분들이 계속 생길 겁니다. 이런 부분들을 어떻게 해결할지를 고민해 보는 것도 중요하다고 생각합니다. 여기서 그치지 않고, 더 편리한 방법, 더 편리한 툴 등을 찾아보면서 자신만의 코드 작성 방법을 고민해 봅시다.