이번에는 MNIST 숫자 손글씨 이미지 분류 실습을 진행해보려고 한다.
기본이 되는 데이터셋과 분류방법이므로, cnn 과정에 대해서 공부하는 느낌으로 읽으면 될 것 같다.
마지막에는 특성맵을 시각화 하는 과정도 포함한다.
CNN에서 특성 맵은 입력 이미지에 대한 특정한 특징을 감지하기 위해 합성곱 연산을 통과한 결과를 말한다. 각 특성 맵은 하나의 필터(커널)에 해당하며, 이 필터는 입력 데이터의 작은 부분에 대해 합성곱 연산을 수행하여 특징을 추출한다.
# PyTorch 불러오기
import torch
# 랜덤 이미지 생성
img_tensor = torch.rand(3, 28, 28)
# 랜덤 이미지에 컬러 입히기
img_tensor = torch.round(img_tensor * 255) # 이미지 값은 0부터 255를 갖습니다.
print(img_tensor)
PIL
의 Image
함수를 불러옴transform
함수도 불러옴from PIL import Image
from torchvision import transforms
tensor_to_image = transforms.ToPILImage()
img = tensor_to_image(img_tensor); img
# PyTorch 내의 nn.Conv2d 레이어에 제작한 이미지 통과 시키기
layer = torch.nn.Conv2d(in_channels = 3, out_channels = 6, kernel_size = 5, stride = 1, padding = 1)
output = layer(img_tensor)
output.size()
>>> torch.Size([6, 26, 26])
# 가상의 이미지 데이터 생성
img = torch.round(torch.rand(10,3,28,28) * 255)
# Padding 함수 구현
def add_padding(inputs):
inputs.to(device)
batch_size, channel, height, width = inputs.size()
zero_padding = torch.zeros(batch_size, channel, height+2, width+2)
zero_padding[:, :, 1:-1, 1:-1] = inputs
return zero_padding
# Padding 적용하기
padded_img = add_padding(img)
print(padded_img[0])
특성맵 = 합성곱층을 입력 이미지와 필터를 연산해서 얻은 결과임
# 특성맵의 사이즈 계산 함수
def cal_output_size(input_size, kernel_size, stride, padding = True):
return (input_size + 2 * padding - kernel_size) // stride + 1
# 임의의 커널 생성
out_features = 6
in_features = 3
kernel = torch.rand(out_features, in_features, kernel_size, kernel_size, requires_grad = True)
# 특성맵에 저장하기
feature_map = torch.Tensor(batch_size, out_features, feature_map_size, feature_map_size)
for i in range(feature_map_size):
for j in range(feature_map_size):
region = padded_img[:,:, i * stride: i * stride + kernel_size, j * stride : j * stride + kernel_size]
region = region.reshape(batch_size, -1)
feature_map[:, :, i, j] = torch.matmul(region, kernel.view(out_features, -1).t())
self.kernel
과 self.bias
를 PyTorch에서 제공하는 파라미터로 변경class CustomCNN(torch.nn.Module):
def __init__(self, in_features, out_features, kernel_size, stride, padding):
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.kernel_size = kernel_size
self.stride = stride
self.padding = padding
self.kernel = torch.nn.Parameter(torch.rand(self.out_features, self.in_features, self.kernel_size, self.kernel_size, requires_grad = True)).to(device)
self.bias = torch.nn.Parameter(torch.rand(self.out_features, requires_grad = True)).to(device)
def forward(self, inputs):
batch_size, in_channels, height, width = inputs.shape
out_channels, in_channels, kernel_height, kernel_width = self.kernel.shape
output_height = cal_output_size(height, kernel_height, self.stride, self.padding)
output_width = cal_output_size(width, kernel_width, self.stride, self.padding)
if self.padding:
inputs = add_padding(inputs)
else:
pass
inputs = inputs.to(device)
self.feature_map = torch.Tensor(batch_size, out_channels, output_height, output_width).to(device)
for i in range(output_height):
for j in range(output_width):
self.region = inputs[:,
:,
i * self.stride : i * self.stride + kernel_height,
j * self.stride : j * self.stride + kernel_width]
self.region = self.region.reshape(batch_size, -1)
self.feature_map[:, :, i, j] = torch.matmul(self.region, self.kernel.reshape(self.out_features, -1).t())
self.feature_map += self.bias.view(1,-1,1,1)
return self.feature_map
‘cuda’
로 설정함import torchvision.datasets as dsets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import random
random.seed(319)
torch.manual_seed(319)
if device == 'cuda':
torch.cuda.manual_seed_all(319)
# 하이퍼파라미터 설정
training_epochs = 3
batch_size = 100
learning_rate = 0.001
# MNIST dataset
mnist_train = dsets.MNIST(root='MNIST_data/',
train=True,
transform=transforms.ToTensor(),
download=True)
mnist_test = dsets.MNIST(root='MNIST_data/',
train=False,
transform=transforms.ToTensor(),
download=True)
# 데이터 로더
data_loader = DataLoader(dataset=mnist_train,
batch_size= batch_size, # 배치 크기는 100
shuffle=True,
drop_last=True)
ReLU
와 MacPooling
을 추가함class CNNwithMaxPooling(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer1 = CustomCNN(1, 32, kernel_size = 3, stride = 1, padding = True).to(device)
self.relu1 = torch.nn.ReLU()
self.maxpooling1 = torch.nn.MaxPool2d(2)
self.dropout1 = torch.nn.Dropout(0.2)
self.layer2 = CustomCNN(32, 64, kernel_size = 3, stride = 1, padding = True).to(device)
self.relu2 = torch.nn.ReLU()
self.maxpooling2 = torch.nn.MaxPool2d(2)
self.dropout2 = torch.nn.Dropout(0.2)
self.fc = torch.nn.Linear(7 * 7 * 64 , 10, bias = True)
torch.nn.init.xavier_uniform_(self.fc.weight)
def forward(self, x):
out = self.dropout1(self.maxpooling1(self.relu1(self.layer1(x))))
out = self.dropout2(self.maxpooling2(self.relu2(self.layer2(out))))
out = out.view(out.size(0), -1)
out = self.fc(out)
return out
model = CNNwithMaxPooling().to(device)
criterion = torch.nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
from tqdm.auto import tqdm
for epoch in range(training_epochs):
avg_cost = 0
for X, Y in tqdm(data_loader): # 미니 배치 단위로 꺼내옵니다. X는 미니 배치, Y는 레이블.
# image is already size of (28x28), no reshape
# label is not one-hot encoded
X = X.to(device)
Y = Y.to(device)
optimizer.zero_grad()
hypothesis = model(X)
cost = criterion(hypothesis, Y)
cost.backward()
optimizer.step()
avg_cost += cost / total_batch
print('[Epoch: {:>4}] cost = {:>.9}'.format(epoch + 1, avg_cost))
>>>
[Epoch: 1] cost = 114.13855
[Epoch: 2] cost = 67.8441467
[Epoch: 3] cost = 64.6813049
with torch.no_grad():
X_test = mnist_test.data.view(len(mnist_test), 1, 28, 28).float().to(device)
Y_test = mnist_test.targets.to(device)
prediction = model(X_test)
correct_prediction = torch.argmax(prediction, 1) == Y_test
accuracy = correct_prediction.float().mean()
print('Accuracy:', accuracy.item())
>>> Accuracy: 0.7749999761581421
image_set = model.layer1(X_test[:5])
image_set = model.relu1(image_set)
image_set = model.maxpooling1(image_set)
import matplotlib.pyplot as plt
fig = plt.figure()
rows = 4
cols = 8
for n in range(rows * cols):
img = image_set[1][n].cpu().detach()
img = tensor_to_image(img)
ax = fig.add_subplot(rows, cols, n + 1)
ax.grid(False)
ax.set_xticks([])
ax.set_yticks([])
ax.imshow(img)
n += 1
특성맵은 결국 이미지의 특성을 포착한 집합체입니다. 그래서 이 결괏값으로 XAI를 구현하기도 하고 다시 다른 모델의 입력값으로 넣어 다른 목적(생성, 탐지 등)으로 활용하기도 합니다. 단순히 모델을 구현하고 훈련시키고 테스트하는 것이 아니라 어떤 과정을 통해 구현됐는지 살펴보고 각각의 결괏값에 대한 이해와 활용 방법을 익혀 나가면서 컴퓨터 비전에 한 발 더 다가갈 수 있습니다.
[출처 | 딥다이브 Code.zip 매거진]