Pytorch 기반 LeNet 구현 및 데이터 증식된 CIFAR10 학습해보기
본 글은 Pytorch 기반 LeNet 직접 구현하여 CIFAR10 학습해보는 내용입니다.
하나하나 자세히 분해해봅시다.
코드 : udemy - PyTorch for Deep Learning and Computer Vision
블로그 글 코드 : CIFAR 10_agumentation.ipynb
파이토치 튜토리얼 : pytorch.org
Introduction
이미지출처1
- LeNet-5은 input, 3개의 convolution layer, 2개의 subsampling layer, 1개의 fully-connected layer, output 으로 되어있다.
이미지출처2
- CIFAR10 Dataset은 10개 클래스 범주의 60000x32x32 컬러 이미지로 구성되며, 클래스당 6000개의 이미지로 구성되어 있다.
- 50000개의 training image와 10000개의 test image가 있다.
LeNet using CIFAR10 in Pytorch
- 이제 파이토치로 코드를 구현해본다.
라이브러리 불러오기
import torch
import matplotlib.pyplot as plt
import numpy as np
import torch.nn.functional as F
from torch import nn
from torchvision import datasets, transforms
GPU 설정하기
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(device)
데이터 증식하기
- 데이터의 수가 많지 않으면 모델 학습이 어려워질 수 있다.
- 따라서 데이터 증식(data augmentation)을 활용한다.
- 이미지의 색깔, 회전, 각도 등을 조금씩 변형하여 데이터의 수를 늘려간다.
- 다음은 pytorch의 함수 설명이다. 3
Compose(transforms)
: 여려 변환(transforms)을 함께 구성Resize(size)
: 입력 이미지의 크기를 지정된 크기로 조정RandomHorizontalFlip([p])
: 주어진 이미지를 주어진 확률(p)로 무작위로 수평으로 뒤짚는다.RandomAffine(, translate, scale)
: 이미지의 중심이 바뀌지 않는 것을 유지하는 random affine 변환ColorJitter(brightness, contrast,...)
: 이미지의 밝기(brightness), 채도(contrast), 채도(saturation) 및 색상(hue)을 임의로 변경ToTensor()
: PIL image 또는 numpy.ndarray 배열을 텐서로 변환normalize(tensor, mean, std[, inplace])
: 평균(mean), 표준편차(std)로 텐서 이미지 정규화
transform_train = transforms.Compose([transforms.Resize((32,32)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
transforms.RandomAffine(0, shear=10, scale=(0.8, 1.2)),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))])
transform = transforms.Compose([transforms.Resize((32,32)),
transforms.ToTensor(),
transforms.Normalize((0.5,),(0.5,))])
CIFAR10 데이터 불러오기
torch.utils.data.DataLoader
와torch.utils.data.Dataset
은 pre-loaded dataset과 개인 데이터를 사용할 수 있게 해준다.- Dataset은 샘플과 해당 레이블을 저장
- DataLoader는 샘플에 쉽게 액세스할 수 있도록 데이터 집합 주위에 반복 가능한 레이블을 감싼다.
training_dataset = datasets.CIFAR10(
root='./data',
train=True,
download=True,
transform=transform_train)
# transform = simply dictates any image manipulations that you wish to apply on your images
validation_dataset = datasets.CIFAR10(
root='./data',
train=False,
download=True,
transform=transform)
training_loader = torch.utils.data.DataLoader(
dataset=training_dataset,
batch_size=100,
shuffle=True)
validation_loader = torch.utils.data.DataLoader(
dataset=validation_dataset,
batch_size=100,
shuffle=False)
이미지 변환 및 클래스 지정하기
.detach()
: 연산 기록으로부터 분리한 tensor를 반환하는 함수- 연산 추적 방지
.cpu()
: GPU 메모리에 올려져 있는 tensor를 cpu 메모리로 복사하는 함수.numpy()
:tensor를 numpy로 변환하여 반환.- 이때 저장공간을 공유하기 때문에 하나를 변경하면 다른 하나도 변경된다. 4
- cpu().detach().numpy() 순서로!
numpy.clip(min, max)
: array 내의 요소들에 대해서 min 값보다 작은 값들을 min 값으로 바꿔주고, max값보다 큰 값들을 max 값으로 바꿔주는 함수 5
iter()
: iter() 함수로 만든 iterator 객체는 한 번에 하나씩 그 객츼의 요소를 순서대로 액세스 할 수 있는 객체로 만들어줌 6next()
: iterator 객체는 그 순서대로 next() 함수를 통해 가져올 수 있다.
# 이미지 전환 함수 정의
def im_convert(tensor):
image = tensor.cpu().clone().detach().numpy()
image = image.transpose(1, 2, 0) # transpose : 잔차 (행과 열을 바꿈)
image = image * np.array((0.5, 0.5, 0.5)) + np.array((0.5, 0.5, 0.5))
image = image.clip(0, 1) # numpy.clip(min, max) : ar
return image
# 클래서 범주 지정
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# 이미지 나타내기
dataiter = iter(training_loader)
images, labels = dataiter.next()
fig = plt.figure(figsize=(25, 4))
for idx in np.arange(20):
ax = fig.add_subplot(2, 10, idx+1, xticks=[], yticks=[])
plt.imshow(im_convert(images[idx]))
ax.set_title(classes[labels[idx].item()])
# 결과
LeNet 구조 정의
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding)
F.max_pool2d(input, kernel_size, stride)
class LeNet(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, 3, 1, padding=1)
self.conv2 = nn.Conv2d(16, 32, 3, 1, padding=1)
self.conv3 = nn.Conv2d(32, 64, 3, 1, padding=1)
self.fc1 = nn.Linear(4*4*64, 500) # 32x32 -> 28x28 -> 14x14 -> 10x10 -> 5x5
self.dropout1 = nn.Dropout(0.5)
self.fc2 = nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv3(x))
x = F.max_pool2d(x, 2, 2)
x = x.view(-1, 4*4*64)
x = F.relu(self.fc1(x))
x = self.dropout1(x)
x = self.fc2(x)
return x
model = LeNet().to(device) # GPU에 할당
model
# 결과
손실 및 옵티마이저 정의
torch.nn.Module.parameters()
: 신경망 파라미터를 optimizer에 전달 할 때 쓴다.- 모듈의 학습 가능한 파라미터를 iterator로 반환한다.
criterion = nn.CrossEntropyLoss() # 입력과 타겟 사이의 손실 계산을 위한 기준 정의
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
학습하기
opt.zero_grad()
: 마지막 단계에서 이전 그레이디언트를 지운다.loss.backward()
: 역전파를 사용하여 파라미터(또는 그레이디언트가 필요한 모든 것)에 대한 손실의 도함수를 계산- 모든 변수는 그레이디언트가 누적된
.grad
함수를 갖게 된다.
- 모든 변수는 그레이디언트가 누적된
opt.step()
: 파라미터의 그레이디언트를 기반으로 한 단계를 수행
torch.max(input)
: 입력 텐서에 있는 모든 요소의 최대값 반환loss.item()
손실이 갖고 있는 스칼라 값을 가져온다.
epochs = 20 # 에포크 설정
running_loss_history = []
running_corrects_history = []
val_running_loss_history = []
val_running_corrects_history = []
for e in range(epochs):
running_loss = 0.0
running_corrects = 0.0
val_running_loss = 0.0
val_running_corrects = 0.0
for inputs, labels in training_loader:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
_, preds = torch.max(outputs, 1)
running_loss += loss.item()
running_corrects += torch.sum(preds == labels.data)
else:
with torch.no_grad():
for val_inputs, val_labels in validation_loader:
val_inputs = val_inputs.to(device)
val_labels = val_labels.to(device)
val_outputs = model(val_inputs)
loss = criterion(val_outputs, val_labels)
_, val_preds = torch.max(val_outputs, 1)
val_running_loss += loss.item()
val_running_corrects += torch.sum(val_preds == val_labels.data)
epoch_loss = running_loss/len(training_loader)
epoch_acc = running_corrects.float() / len(training_loader)
running_loss_history.append(epoch_loss)
running_corrects_history.append(epoch_acc.cpu().detach().numpy())
val_epoch_loss = val_running_loss/len(validation_loader)
val_epoch_acc = val_running_corrects.float() / len(validation_loader)
val_running_loss_history.append(val_epoch_loss)
val_running_corrects_history.append(val_epoch_acc.cpu().detach().numpy())
print('epoch : ', (e+1))
print('training loss: {:.4f}, training acc : {:.4f}'.format(epoch_loss, epoch_acc))
print('validation loss: {:.4f}, validation acc : {:.4f}'.format(val_epoch_loss, val_epoch_acc))
결과 출력
plt.plot(running_loss_history, label='training loss')
plt.plot(val_running_loss_history, label='validation loss')
plt.legend()
plt.plot(running_corrects_history, label="training_acc")
plt.plot(val_running_corrects_history, label="validation_acc")
plt.legend()
댓글남기기