Data Science/FullStackDeepLearning

[FSDL] Pre-Lab 01: Deep Neural Networks in PyTorch

김 기승 2023. 6. 23. 11:52

Getting data and making Tensors

  • 모델을 구축하려면 먼저 데이터가 필요하다.
  • 아래 코드는 파이썬 표준 라이브러리를 사용하여 인터넷에서 손으로 쓴 숫자로 구성된 MNIST 데이터셋을 다운로드한다.
  • 데이터 너무 크기 때문에, 디스크에 저장하는 것이 아닌 네트워크를 통해 데이터를 가져오는 방식을 사용한다.
from pathlib import Path
import requests

def download_mnist(path):
    url = "<https://github.com/pytorch/tutorials/raw/main/_static/>"
    filename = "mnist.pkl.gz"

    if not (path / filename).exists():
        content = requests.get(url + filename).content
        (path / filename).open("wb").write(content)

    return path / filename

data_path = Path("data") if Path("data").exists() else Path("../data")
path = data_path / "downloaded" / "vector-mnist"
path.mkdir(parents=True, exist_ok=True)

datafile = download_mnist(path)
  • 압축된 형태의 데이터(e.g. .gz)를 사용한다.
  • 데이터셋의 각 부분(training 및 validation의 입력과 출력)은 단일 Python 객체(배열)이다.
  • 파이썬 객체를 디스크에 지속(”직렬화(serialization)”)하고 pickle 라이브러리를 사용하여 다시 로드(”역직렬화(deserialization)”)할 수 있다. (.pkl 확장자)
import gzip
import pickle

def read_mnist(path):
    with gzip.open(path, "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
    return x_train, y_train, x_valid, y_valid

x_train, y_train, x_valid, y_valid = read_mnist(datafile)
  • 파이토치는 자체 배열 유형인 torch.Tensor를 제공한다.
  • '텐서'라는 용어는 ML분야에서 2차원 이상의 배열을 의미한다.
  • 텐서의 가장 중요한 meatdata는 ‘dimension’과 ‘shape’

Building a DNN using only torch.Tensor methods and Python

Defining the model

import math

import torch

weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
def linear(x: torch.Tensor) -> torch.Tensor:
    return x @ weights + bias
def log_softmax(x: torch.Tensor) -> torch.Tensor:
    return x - torch.log(torch.sum(torch.exp(x), axis=1))[:, None]

def model(xb: torch.Tensor) -> torch.Tensor:
    return log_softmax(linear(xb))
  • sofmax는 너무 큰 값이 계산될 수 있기 때문에, log softmax를 사용한다.

Defining the loss and metrics

def accuracy(out: torch.Tensor, yb: torch.Tensor) -> torch.Tensor:
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()
def cross_entropy(output: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
    return -output[range(target.shape[0]), target].mean()

loss_func = cross_entropy
loss = loss_func(outs, yb)
loss.backward()
  • But, argmax는 backward가 계산되지 않기 때문이다.
    • Cross-Entropy

Defining and running the fitting loop

  • 신경망 학습에 필요한 요소들은 다음과 같다.
    • 데이터 (x_train, y_train)
    • 네트워크 아키텍쳐 + 파라미터 (model, weights, and bias)
    • cross_entropy를 최적화하기 위한 loss function(손실 함수)
      • cross_entropy는 gradients의 .backward(역전파) 연산을 지원한다.
lr = 0.5  # learning rate hyperparameter
epochs = 2  # how many epochs to train for

for epoch in range(epochs):  # loop over the data repeatedly
    for ii in range((n - 1) // bs + 1):  # in batches of size bs, so roughly n / bs of them
        start_idx = ii * bs  # we are ii batches in, each of size bs
        end_idx = start_idx + bs  # and we want the next bs entires

        # pull batches from x and from y
        xb = x_train[start_idx:end_idx]
        yb = y_train[start_idx:end_idx]

        # run model
        pred = model(xb)

        # get loss
        loss = loss_func(pred, yb)

        # calculate the gradients with a backwards pass
        loss.backward()

        # update the parameters
        with torch.no_grad():  # we don't want to track gradients through this part!
            # SGD learning rule: update with negative gradient scaled by lr
            weights -= weights.grad * lr
            bias -= bias.grad * lr

            # ACHTUNG: PyTorch doesn't assume you're done with gradients
            #          until you say so -- by explicitly "deleting" them,
            #          i.e. setting the gradients to 0.
            weights.grad.zero_()
            bias.grad.zero_()

Refactoring with core torch.nn components

Using torch.nn.functional for stateless computation

  • stateless한 계산이 있는 torch.nn.functional에서 구현을 찾을 수 있다.
    • e.g. cross_entropy & log_softmax
  • 이 함수들은 전역 변수를 참조하지 않고, 입력에 대해 작동하므로 stateless한 계산이 가능하다.
    import torch.nn.functional as F
    
    loss_func = F.cross_entropy
    
    def model(xb):
        return xb @ weights + bias
    ​

 

Using torch.nn.Module to define functions whose state is given by torch.nn.Parameters

  • 함수처럼 호출할 수 있지만, 객체처럼 상태를 추적하는 Python 클래스인 nn.Module 클래스
  • 모델 내에서 PyTorch가 추적하기를 원하는 상태를 나타내는 텐서는 무엇이든 nn.Parameters로 정의되고 모델에 속성으로 저장된다.
  • .forward 메서드에서 해당 상태를 사용하는 계산을 정의한다.
    • 이 메서드는 인스턴스화된 nn.Module을 함수처럼 취급하여 인수를 전달하면 호출된다.
from torch import nn

class MNISTLogistic(nn.Module):
    def __init__(self):
        super().__init__()  # the nn.Module.__init__ method does import setup, so this is mandatory
        self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
        self.bias = nn.Parameter(torch.zeros(10))

		def forward(self, xb: torch.Tensor) -> torch.Tensor:
		    return xb @ self.weights + self.bias
  • .parameters 메서드를 통해 모델의 모든 torch.nn.Parameters 를 반복할 수 있다.
def fit():
    for epoch in range(epochs):
        for ii in range((n - 1) // bs + 1):
            start_idx = ii * bs
            end_idx = start_idx + bs
            xb = x_train[start_idx:end_idx]
            yb = y_train[start_idx:end_idx]
            pred = model(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            with torch.no_grad():
                for p in model.parameters():  # finds params automatically
                    p -= p.grad * lr
                model.zero_grad()

Refactoring intermediate torch.nn components: network layers, optimizers, and data handling

Using torch.nn.Linear for the model definition

  • 원시 텐서인 nn.Parameters를 nn.Module의 attribute로 정의하는 대신, 다른 nn.Modules를 attribute로 정의할 수 있다.
  • PyTorch는 자식 nn.Modules의 nn.Parameters를 재귀적으로 부모에 할당한다.
  • 이러한 nn.Module은 재사용이 가능하다.
    • e.g. 동일한 유형의 여러 레이어로 네트워크를 만들고자 하는 경우
  • Already defined
    • torch.nn.Modules: Module, Identity, Linear, Conv1d, Conv2d, Conv3d, ConvTranspose1d, ConvTranspose2d, ConvTranspose3d, Threshold, ReLU, Hardtanh, ReLU6, Sigmoid, Tanh, Softmax, Softmax2d, LogSoftmax, ELU, SELU, CELU, GLU, GELU, Hardshrink, LeakyReLU, LogSigmoid, Softplus, Softshrink, MultiheadAttention, PReLU, Softsign, Softmin, Tanhshrink, RReLU, L1Loss, NLLLoss, KLDivLoss, ...
class MNISTLogistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)  # pytorch finds the nn.Parameters inside this nn.Module

    def forward(self, xb):
        return self.lin(xb)  # call nn.Linear.forward here

Applying gradients with torch.optim.Optimizer

from torch import optim

def configure_optimizer(model: nn.Module) -> optim.Optimizer:
    return optim.Adam(model.parameters(), lr=3e-4)
model = MNISTLogistic()
opt = configure_optimizer(model)

print("before training:", loss_func(model(xb), yb), sep="\\n\\t")

for epoch in range(epochs):
	############################################
    for ii in range((n - 1) // bs + 1):
        start_idx = ii * bs
        end_idx = start_idx + bs
        xb = x_train[start_idx:end_idx]
        yb = y_train[start_idx:end_idx]
	############################################
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print("after training:", loss_func(model(xb), yb), sep="\\n\\t")

Organizing data with torch.utils.data.Dataset

  • 여러 데이터 소스를 결합하여 동시에 인덱싱할 수 있는 방법
  • 이 작업은
    1. torch.utils.data.Dataset을 상속하고,
    2. 인덱싱을 지원하는 두 가지 메서드(__getitem__과 len)를 구현해야 한다.
  • 실습에서는 BaseDataset 클래스를 사용
from text_recognizer.data.util import BaseDataset

train_ds = BaseDataset(x_train, y_train)
model = MNISTLogistic()
opt = configure_optimizer(model)

for epoch in range(epochs):
	############################################
    for ii in range((n - 1) // bs + 1):
        xb, yb = train_ds[ii * bs: ii * bs + bs]  # xb and yb in one line!
	############################################
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

Batching up data with torch.utils.data.DataLoader

  • 여전히 수동으로 배치를 구축하고 있다.
  • 데이터 세트에서 배치를 만드는 것은 최신 딥러닝 training workflow의 핵심 구성 요소
  • Dataset을 DataLoader에 전달하고 batch_size를 선택하기만 하면 된다.
  • 파라미터와 다른 DataLoader 인수(num_workers, pin_memory)를 조정하여 훈련 루프의 성능을 개선할 수 있다.
  • DataLoader 파라미터의 영향에 관해서는 다음 링크에서 더 확인할 수 있다. Blog post and Colab.
from torch.utils.data import DataLoader

train_ds = BaseDataset(x_train, y_train)
train_dataloader = DataLoader(train_ds, batch_size=bs)
def fit(self: nn.Module, train_dataloader: DataLoader):
    opt = configure_optimizer(self)

    for epoch in range(epochs):
		############################################
        for xb, yb in train_dataloader:
		############################################
            pred = self(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            opt.step()
            opt.zero_grad()

MNISTLogistic.fit = fit

Swapping in another model

  • 코드베이스의 text_recognizer 라이브러리에서 MLP(다층 퍼셉트론) 모델을 사용한다.
from text_recognizer.models.mlp import MLP

MLP.fit = fit  # attach our fitting loop
  • MLP의 .forward 메서드를 살펴보면 nn.Dropout 및 F.relu와 같이 우리가 보지 못한 몇 가지 모듈과 함수를 사용한다.
def forward(self, x):
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout(x)
        x = self.fc2(x)
        x = F.relu(x)
        x = self.dropout(x)
        x = self.fc3(x)
        return x
  • def forward(self, x): x = torch.flatten(x, 1) x = self.fc1(x) x = F.relu(x) x = self.dropout(x) x = self.fc2(x) x = F.relu(x) x = self.dropout(x) x = self.fc3(x) return x
  • MLP는 호출 가능하며 x를 받아 y 레이블에 대한 추측을 반환한다.
  • 생성자 __init__을 보면 nn.Modules(fc 및 드롭아웃)가 초기화되고 attributes으로 첨부되는 것을 볼 수 있다.
def __init__(
        self,
        data_config: Dict[str, Any],
        args: argparse.Namespace = None,
    ) -> None:

        super().__init__()
        self.args = vars(args) if args is not None else {}
        self.data_config = data_config

        input_dim = np.prod(self.data_config["input_dims"])
        num_classes = len(self.data_config["mapping"])

        fc1_dim = self.args.get("fc1", FC1_DIM)
        fc2_dim = self.args.get("fc2", FC2_DIM)
        dropout_p = self.args.get("fc_dropout", FC_DROPOUT)

        self.fc1 = nn.Linear(input_dim, fc1_dim)
        self.dropout = nn.Dropout(dropout_p)
        self.fc2 = nn.Linear(fc1_dim, fc2_dim)
        self.fc3 = nn.Linear(fc2_dim, num_classes)

 

  • data_config 딕셔너리를 제공해야 하며 선택적으로 args로 모듈을 구성할 수 있다.
    • init 메서드에서 사용되는 x에 대한 input_dims와, y의 클래스 인덱스에서 클래스 레이블로의 매핑인 data_config의 내용만 지정함.
    digits_to_9 = list(range(10))
    data_config = {"input_dims": (784,), 
                   "mapping": {digit: str(digit) for digit in digits_to_9}}
    model = MLP(data_config)

Extra goodies: data organization, validation, and acceleration

  • DNN fitting loop를 위해서는 다음 세 가지 기능이 더 필요하다.
    • 체계적인 데이터 로딩 코드
    • 유효성 검사
    • GPU 가속
  • 지금까지 우리가 해온 모든 작업은 컴퓨터의 중앙처리장치(CPU)에서 이루어짐.
    • 중소규모의 신경망에는 괜찮지만, 계산이 빠르게 병목현상이 발생하여 좋은 성능을 달성하는 것이 불가능
  • 그래픽 처리 장치(GPU)는 대규모 행렬 곱셈을 병렬로 수행하도록 설계되어 딥러닝을 가속화하는 데 매우 뛰어남.
  • PyTorch는 CPU와 GPU에서 동시에 계산을 수행할 수 있도록 설계되었으며, 이는 고성능을 위해 매우 중요
  • 따라서 가속을 사용하기 시작하면 텐서 내부의 데이터가 어디에 있는지, 즉 어떤 물리적 torch.device에서 찾을 수 있는지에 대해 더 정확하게 파악해야 한다.
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")

model.to(device)

loss_func(model(xb.to(device)), yb.to(device))
  • 구체적으로, 입력에 대해 호출되는 transform과 레이블이 반환되기 전에 호출되는 target_transform을 제공할 수 있다.
  • FSDL 코드베이스에서 이 기능은 모양 변경, 크기 조정 및 정규화와 같은 데이터 준비에 사용된다.
def push_to_device(tensor):
    return tensor.to(device)

train_ds = BaseDataset(x_train, y_train, transform=push_to_device, target_transform=push_to_device)
train_dataloader = DataLoader(train_ds, batch_size=bs)
  • 이 모델과 데이터의 크기가 작기 때문에 여기서는 속도가 2배 정도만 빨라질 수 있다.
  • 더 큰 모델의 경우 GPU 가속을 통해 50~100배 빠른 반복을 쉽게 구현할 수 있다.
model = MLP(data_config)
model.to(device)

model.fit(train_dataloader)
  • 고성능 GPU 가속 신경망 코드를 작성하는 것과 관련된 몇 가지 핵심 원칙에 대한 간략한 소개는 Horace He의 블로그 게시물을 참조

Adding validation data and organizing data code with a DataModule

  • 네트워크가 각 입력 숫자에 대한 레이블을 외울 수 있기 때문에, 이전에 본 데이터에서 잘 작동하는 것만으로는 그다지 인상적이지 않다.
  • 모델을 최적화하는 데 직접 사용되지 않은 데이터 포인트 집합, 일반적으로 validation set이라고 하는 데이터 집합에 대한 성능을 확인해야 한다.
  • 제대로 된 DataModule은 머신에서 데이터를 준비하는 데 필요한 모든 코드를 수집하여 데이터 집합으로 설정하고, 아래와 같이 해당 데이터 집합을 데이터 로더로 전환한다.
class MNISTDataModule:
    url = "<https://github.com/pytorch/tutorials/raw/master/_static/>"
    filename = "mnist.pkl.gz"

    def __init__(self, dir, bs=32):
        self.dir = dir
        self.bs = bs
        self.path = self.dir / self.filename

    def prepare_data(self):
        if not (self.path).exists():
            content = requests.get(self.url + self.filename).content
            self.path.open("wb").write(content)

    def setup(self):
        with gzip.open(self.path, "rb") as f:
            ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")

        x_train, y_train, x_valid, y_valid = map(
            torch.tensor, (x_train, y_train, x_valid, y_valid)
            )

        self.train_ds = BaseDataset(x_train, y_train, transform=push_to_device, target_transform=push_to_device)
        self.valid_ds = BaseDataset(x_valid, y_valid, transform=push_to_device, target_transform=push_to_device)

    def train_dataloader(self):
        return torch.utils.data.DataLoader(self.train_ds, batch_size=self.bs, shuffle=True)

    def val_dataloader(self):
        return torch.utils.data.DataLoader(self.valid_ds, batch_size=2 * self.bs, shuffle=False)
  • 이제 필요에 따라 메서드를 호출하여 DataModule을 피팅 파이프라인에 통합할 수 있다.
def fit(self: nn.Module, datamodule):
    datamodule.prepare_data()
    datamodule.setup()

    val_dataloader = datamodule.val_dataloader()

    self.eval()
    with torch.no_grad():
        valid_loss = sum(loss_func(self(xb), yb) for xb, yb in val_dataloader)

    print("before start of training:", valid_loss / len(val_dataloader))

    opt = configure_optimizer(self)
    train_dataloader = datamodule.train_dataloader()
    for epoch in range(epochs):
        self.train()
        for xb, yb in train_dataloader:
            pred = self(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            opt.step()
            opt.zero_grad()

        self.eval()
        with torch.no_grad():
            valid_loss = sum(loss_func(self(xb), yb) for xb, yb in val_dataloader)

        print(epoch, valid_loss / len(val_dataloader))

MNISTLogistic.fit = fit
MLP.fit = fit
  • self.eval과 self.train
    • 신경망의 기능이 프로덕션이나 평가에서와 다르게 훈련할 때 다르게 작동하는 것이 유용하다.
    • e.g. Dropout, BatchNorm
    • 이제 validation loop를 고려해야 한다
  • torch.no_grad의 반환
    • 처음 몇 번의 구현에서는 파라미터를 업데이트하는 동안 기울기를 추적하지 않기 위해 torch.no_grad를 사용해야 했다.
    • 이제 validation 중에 기울기 추적을 피하기 위해 이 함수를 사용해야 한다.
  • MNISTLogistic 및 MNISTDataModule 클래스를 정의했다면 아래 셀만으로 네트워크를 훈련할 수 있다.
model = MLP(data_config)
model.to(device)

datamodule = MNISTDataModule(dir=path, bs=32)

model.fit(datamodule=datamodule)
  • torch.nn이 신경망 정의, 배치 반복, 기울기 계산을 위한 유용한 도구와 인터페이스를 제공하는 것과 마찬가지로,
  • PyTorch Lightning과 같은 파이토치 기반 프레임워크는 신경망 훈련을 훨씬 더 높은 수준으로 추상화할 수 있는 유용한 도구와 인터페이스를 제공한다.
  • 이러한 프레임워크의 대부분은 최소한 모델을 정의하고 데이터 파이프라인을 정의하기 위해 핵심 PyTorch 기능이 필요하다.

 

  • MNLISTLogistic Final Code
class MNISTLogistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)  # pytorch finds the nn.Parameters inside this nn.Module

    def forward(self, xb):
        return self.lin(xb)  # call nn.Linear.forward here

		def fit(self: nn.Module, datamodule):
		    datamodule.prepare_data()
		    datamodule.setup()
		
		    val_dataloader = datamodule.val_dataloader()
		
		    self.eval()
		    with torch.no_grad():
		        valid_loss = sum(loss_func(self(xb), yb) for xb, yb in val_dataloader)
		
		    print("before start of training:", valid_loss / len(val_dataloader))
		
		    opt = configure_optimizer(self)
		    train_dataloader = datamodule.train_dataloader()
		    for epoch in range(epochs):
		        self.train()
		        for xb, yb in train_dataloader:
		            pred = self(xb)
		            loss = loss_func(pred, yb)
		
		            loss.backward()
		            opt.step()
		            opt.zero_grad()
		
		        self.eval()
		        with torch.no_grad():
		            valid_loss = sum(loss_func(self(xb), yb) for xb, yb in val_dataloader)
		
		        print(epoch, valid_loss / len(val_dataloader))
  • MLP Final Code
class MLP(nn.Module):

    def __init__(
        self,
        data_config: Dict[str, Any],
        args: argparse.Namespace = None,
    ) -> None:

        super().__init__()
        self.args = vars(args) if args is not None else {}
        self.data_config = data_config

        input_dim = np.prod(self.data_config["input_dims"])
        num_classes = len(self.data_config["mapping"])

        fc1_dim = self.args.get("fc1", FC1_DIM)
        fc2_dim = self.args.get("fc2", FC2_DIM)
        dropout_p = self.args.get("fc_dropout", FC_DROPOUT)

        self.fc1 = nn.Linear(input_dim, fc1_dim)
        self.dropout = nn.Dropout(dropout_p)
        self.fc2 = nn.Linear(fc1_dim, fc2_dim)
        self.fc3 = nn.Linear(fc2_dim, num_classes)

    def forward(self, x):
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout(x)
        x = self.fc2(x)
        x = F.relu(x)
        x = self.dropout(x)
        x = self.fc3(x)
        return x

		def fit(self: nn.Module, datamodule):
		    datamodule.prepare_data()
		    datamodule.setup()
		
		    val_dataloader = datamodule.val_dataloader()
		
		    self.eval()
		    with torch.no_grad():
		        valid_loss = sum(loss_func(self(xb), yb) for xb, yb in val_dataloader)
		
		    print("before start of training:", valid_loss / len(val_dataloader))
		
		    opt = configure_optimizer(self)
		    train_dataloader = datamodule.train_dataloader()
		    for epoch in range(epochs):
		        self.train()
		        for xb, yb in train_dataloader:
		            pred = self(xb)
		            loss = loss_func(pred, yb)
		
		            loss.backward()
		            opt.step()
		            opt.zero_grad()
		
		        self.eval()
		        with torch.no_grad():
		            valid_loss = sum(loss_func(self(xb), yb) for xb, yb in val_dataloader)
		
		        print(epoch, valid_loss / len(val_dataloader))

    @staticmethod
    def add_to_argparse(parser):
        parser.add_argument("--fc1", type=int, default=FC1_DIM)
        parser.add_argument("--fc2", type=int, default=FC2_DIM)
        parser.add_argument("--fc_dropout", type=float, default=FC_DROPOUT)
        return parser

 

[출처] https://fullstackdeeplearning.com/