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
- 여러 데이터 소스를 결합하여 동시에 인덱싱할 수 있는 방법
- 이 작업은
- torch.utils.data.Dataset을 상속하고,
- 인덱싱을 지원하는 두 가지 메서드(__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
'Data Science > FullStackDeepLearning' 카테고리의 다른 글
[FSDL] Pre-Lab 03: Transformers and Paragraphs (0) | 2023.07.06 |
---|---|
[FSDL] Pre-Lab 02b: Training a CNN on Synthetic Handwriting Data (0) | 2023.06.26 |
[FSDL] Pre-Lab 02a: PyTorch Lightning (0) | 2023.06.23 |