-
[기계학습]합성곱 신경망(CNN : Convolutional Nerual Network) (Part 2/2)코딩(Coding)/기계학습 2022. 2. 18. 12:03728x90
합성곱 신경망(CNN :Convolutional Nerual Network) (Part 2/3)
- 기존의 Fully-Connected 모델은 1차원의 데이터 말고 2차원 이상의 데이터를 사용하게 된다면, 해당 입력 데이터를 Flatten시켜 한 줄의 데이터로 만들어야 한다.
- 이 과정에서 데이터의 손상이 발생하게 된다.
- 이미지의 경우에는 상하좌우 이웃 픽셀의 정보가 손실된다.
- 위 문제를 해결하기 위해 고안한 해결책이 바로 CNN이다.
CNN 장점
- 단순 Fully-connected 보다 학습시킬 weight가 적다.
- 학습과 연산에 속도가 빠르며, 효율적이다.
- 이미지나 영상데이터를 처리할 때 사용한다.
CNN의 접근
이미지 표현 => Matrix
해당 실습에서 사용된 데이터와 코드(.ipynb)는 아래 링크에서 확인할 수 있습니다.
- 데이터 : https://github.com/JoSangYeon/Machine_Learning_Project/tree/master/Data
- 코 드 : https://github.com/JoSangYeon/Machine_Learning_Project/blob/master/04.%20CNN.ipynb
패키지 Import
import pandas as pd import numpy as np import matplotlib.pyplot as plt import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.utils.data import Dataset, DataLoader from torchsummary import summary as summary_ from tqdm import tqdm, notebook
데이터 살펴보기
Fashion MNIST Dataset
# label_tags = ["티셔츠/탑", "트루저", "풀오버", "드레스", "코트", "샌들", "셔츠", "스니커", "가방", "앵클부츠"] label_tags = ["T-shirt/top", "Trouser", "Pullover", "Dress", "Coat", "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"] train_dataset = pd.read_csv("Data/fashion-mnist_train.csv") test_dataset = pd.read_csv("Data/fashion-mnist_test.csv") # Split to Image & Label train_images = (train_dataset.iloc[:, 1:].values).astype("float32") train_labels = train_dataset["label"].values test_images = (test_dataset.iloc[:, 1:].values).astype("float32") test_labels = test_dataset["label"].values
Pandas 패키지를 통해서 Fashion_Mnist CSV 파일을 읽어온다.
- Train 60000개, Test 10000개
그 다음 Feature와 Label로 구분지어준다.
# Split into Train, Valid Dataset from sklearn.model_selection import train_test_split train_images, valid_images, train_labels, valid_labels = train_test_split(train_images, train_labels, stratify = train_labels, random_state = 42, test_size = 0.2)
SciKit-Learn 패키지의 Train_Test_Split 함수를 통해서 Train데이터 60000개를 48000개의 학습셋과 12000개의 검증셋으로 나눈다.
# Reshape image's size to check for ours # (size, 784) => (size, 28, 28) train_images = train_images.reshape(train_images.shape[0], 28, 28) valid_images = valid_images.reshape(valid_images.shape[0], 28, 28) test_images = test_images.reshape(test_images.shape[0], 28, 28)
이미지를 2차원 데이터로 차원 변환해준다.(784 -> 28x28)
# Check Train, Valid, Test Image's Shape print("The Shape of Train Images: ", train_images.shape) print("The Shape of Valid Images: ", valid_images.shape) print("The Shape of Test Images: ", test_images.shape) # Check Train, Valid Label's Shape print("The Shape of Train Labels: ", train_labels.shape) print("The Shape of Valid Labels: ", valid_labels.shape) print("The Shape of Valid Labels: ", test_labels.shape)
The Shape of Train Images: (48000, 28, 28) The Shape of Valid Images: (12000, 28, 28) The Shape of Test Images: (10000, 28, 28) The Shape of Train Labels: (48000,) The Shape of Valid Labels: (12000,) The Shape of Valid Labels: (10000,)
데이터를 시각화 해보자
# 데이터 시각화 img = train_images[20] label = train_labels[20] print("Label :",label_tags[label]) plt.imshow(img, cmap='gray'); plt.show()
Label : Pullover
Dataset 정의
class MyDataset(Dataset): def __init__(self, feature_data, label_data, num_classes = 10): self.x_data = feature_data self.y_data = label_data self.num_classes = num_classes def __len__(self): return len(self.x_data) def __getitem__(self, idx): # image img = self.x_data[idx] / 255. # 명암값 정규화 img = torch.FloatTensor(img) # Tensor로 변환 img = img.view(1, 28, 28) # (channel, width, height) # label label = torch.tensor(self.y_data[idx]) label = F.one_hot(label, num_classes = self.num_classes) # one-hot 인코딩 label = label.float() return img, label
label_tags = ['T-Shirt', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt','Sneaker', 'Bag', 'Ankle Boot'] train_dataset = MyDataset(train_images, train_labels) valid_dataset = MyDataset(valid_images, valid_labels) test_dataset = MyDataset(test_images, test_labels) train_loader = DataLoader(train_dataset, batch_size = 32, shuffle = True) valid_loader = DataLoader(valid_dataset, batch_size = 32) test_loader = DataLoader(test_dataset, batch_size = 32) print(train_loader) print(valid_loader) print(test_loader)
TASK에 맞게 Dataset을 정의하고 DataLoader로 생성해준다.
- Image는 기존에 28x28 Shape에서 1x28x28으로 변환해준다.(Channel, Width, Height)
- Label은 0~9까지 10개의 Label을 One-Hot Encoding 해준다.
- Batch_size = 32이다.필자 GPU가 좋지 못하다..
훈련 & 검증 함수 정의
loss_fn = nn.CrossEntropyLoss() def calc_acc(X, Y): x_val, x_idx = torch.max(X, dim=1) y_val, y_idx = torch.max(Y, dim=1) return (x_idx == y_idx).sum().item() def train(EPOCHS, model, train_loader, opt): train_loss_history = [] valid_loss_history = [] train_acc_history = [] valid_acc_history = [] for epoch in range(1, EPOCHS+1): model.train() train_acc = 0 print("<<< EPOCH {} >>>".format(epoch)) for batch_idx, (img,label) in enumerate(notebook.tqdm(train_loader)): img, label = img.to(DEVICE), label.to(DEVICE) output = model(img) # 순전파 loss = loss_fn(output, label) # 오차 계산 opt.zero_grad() # opt내부 값 초기화 loss.backward() # 오차 역전파 opt.step() # 가중치 갱신 train_acc += calc_acc(output, label) if batch_idx % 100 == 0 and batch_idx != 0: print("Training : [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t Acc : {:.3f}".format( batch_idx * len(img), len(train_loader.dataset), 100. * batch_idx / len(train_loader), loss.item(), train_acc / len(train_loader.dataset))) t_loss, t_acc = evaluate(model, valid_loader) print("[{}] valid Loss : {:.4f}\t accuracy: {:.2f}%\n\n".format(epoch, t_loss, t_acc*100.)) train_loss_history.append(loss.item()) train_acc_history.append(train_acc / len(train_loader.dataset)) valid_loss_history.append(t_loss.item()) valid_acc_history.append(t_acc) return train_loss_history, train_acc_history, valid_loss_history, valid_acc_history def evaluate(model, valid_loader): model.eval() t_loss = 0 correct = 0 with torch.no_grad(): for img, label in notebook.tqdm(valid_loader): img, label = img.to(DEVICE), label.to(DEVICE) output = model(img) t_loss += loss_fn(output, label) correct += calc_acc(output, label) t_loss /= len(valid_loader) t_acc = correct / len(valid_loader.dataset) return t_loss, t_acc def predict(model, lower=0, upper=10): model.eval() for idx in range(lower, upper): img, _ = test_dataset.__getitem__(idx) output = model(img.view(1, 1, 28, 28)) o_val, o_idx = torch.max(output, dim=1) print("Label :", label_tags[o_idx.item()]) plt.imshow(img.view(28, 28), cmap='gray') plt.show() print()
학습을 위한 함수를 정의해 준다.
- 훈련을 위한 Train() 함수 => Train_Loader
- 검증을 위한 Evaluate()함수 => Valid_Loader or Test_Loader
- 추론을 위한 Predict()함수 => Test_Loader(본 포스팅에선 사용하지 않는다.)
모델 정의
모델은 2가지를 정의하였다.
- 단순히 선형변환을 사용하는 LinearNet
- CNN을 이용한 분류기 CNN
Linear Net
3계층의 선형변환을 수행한다.
class LinearNet(nn.Module): def __init__(self): super(LinearNet, self).__init__() self.fc1 = nn.Linear(784, 256) self.fc2 = nn.Linear(256, 64) self.fc3 = nn.Linear(64, 10) self.act_fn = nn.LeakyReLU() def forward(self, x): x = x.view(-1, 1*28*28) x = self.fc1(x) x = self.act_fn(x) x = self.fc2(x) x = self.act_fn(x) x = self.fc3(x) return x
CNN
Convolutional Dot + Pooling을 4계층으로 쌓고 2계층의 선형분류기를 쌓은 CNN 클래스이다.
class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1 = nn.Conv2d(1, 8, kernel_size = 3, padding = 1) self.conv2 = nn.Conv2d(8 , 16, kernel_size = 3, padding = 1) self.conv3 = nn.Conv2d(16, 32, kernel_size = 3, padding = 1) self.conv4 = nn.Conv2d(32, 64, kernel_size = 3, padding = 1) self.pooling = nn.MaxPool2d(2, 2) self.flatten = nn.AdaptiveAvgPool2d(1) self.fc1 = nn.Linear(64, 24) self.fc2 = nn.Linear(24, 10) self.act_fn = nn.LeakyReLU() def forward(self, x): x = self.conv1(x) # (batch, 1, 28, 28) -> (batch, 8, 28, 28) x = self.pooling(x) # (batch, 8, 28, 28) -> (batch, 8, 14, 14) x = self.act_fn(x) x = self.conv2(x) # (batch, 8, 14, 14) -> (batch, 16, 14, 14) x = self.pooling(x) # (batch, 16, 14, 14) -> (batch, 16, 7, 7) x = self.act_fn(x) x = self.conv3(x) # (batch, 16, 7, 7) -> (batch, 32, 7, 7) x = self.pooling(x) # (batch, 32, 7, 7) -> (batch, 32, 3, 3) x = self.act_fn(x) x = self.conv4(x) # (batch, 32, 3, 3) -> (batch, 64, 3, 3) x = self.pooling(x) # (batch, 64, 3, 3) -> (batch, 64, 1, 1) x = self.act_fn(x) x = self.flatten(x) # # (batch, 64, 3, 3) -> (batch, 64, 1, 1) x = x.view(-1, 64*1*1) x = self.fc1(x) x = self.act_fn(x) x = self.fc2(x) return x
훈련 및 검증
이제 모든 준비는 끝났다.
LinearNet부터 훈련/검증을 시작해보자Linear Net
USE_CUDA = torch.cuda.is_available() DEVICE = "cuda" if USE_CUDA else "cpu" model = LinearNet().to(DEVICE) opt = optim.Adam(model.parameters()) print("Device :", DEVICE) summary_(model,(1,28,28), device=DEVICE)
Device : cuda ---------------------------------------------------------------- Layer (type) Output Shape Param # ================================================================ Linear-1 [-1, 256] 200,960 LeakyReLU-2 [-1, 256] 0 Linear-3 [-1, 64] 16,448 LeakyReLU-4 [-1, 64] 0 Linear-5 [-1, 10] 650 ================================================================ Total params: 218,058 Trainable params: 218,058 Non-trainable params: 0 ---------------------------------------------------------------- Input size (MB): 0.00 Forward/backward pass size (MB): 0.00 Params size (MB): 0.83 Estimated Total Size (MB): 0.84 ----------------------------------------------------------------
LinearNet은 단순한 3계층이다.
사용하는 가중치 개수는 218,058이다.
# 학습 시작 # t_loss_his, t_acc_his, v_loss_his, v_acc_his = train(EPOCHS = 10, model = model, train_loader = train_loader, opt = opt)
<<< EPOCH 1 >>> Training : [3200/48000 (7%)] Loss: 0.735302 Acc : 0.041 ... 중간 생략 ... Training : [44800/48000 (93%)] Loss: 0.350932 Acc : 0.752 [1] valid Loss : 0.3948 accuracy: 84.87% <<< EPOCH 2 >>> Training : [3200/48000 (7%)] Loss: 0.448148 Acc : 0.057 ... 중간 생략 ... Training : [44800/48000 (93%)] Loss: 0.507022 Acc : 0.803 [2] valid Loss : 0.3696 accuracy: 86.38% ... 중간 생략 ... <<< EPOCH 10 >>> Training : [3200/48000 (7%)] Loss: 0.036901 Acc : 0.061 ... 중간 생략 ... Training : [44800/48000 (93%)] Loss: 0.253712 Acc : 0.851 [10] valid Loss : 0.3202 accuracy: 88.84%
10번의 학습 후 검증데이터에 대한 Loss와 정확도는 각각 0.3202와 88.84%이다.
plt.plot(t_loss_his, label="train") plt.plot(v_loss_his, label="valid") plt.legend() plt.show()
plt.plot(t_acc_his, label="train") plt.plot(v_acc_his, label="valid") plt.legend() plt.show()
Test셋에 대한 검증 결과는 아래와 같다.
v_loss, v_acc = evaluate(model, test_loader) print("Test Loss : {:.4f}\t accuracy: {:.2f}%\n".format(v_loss, v_acc*100.))
Test Loss : 0.3141 accuracy: 88.88%
CNN
USE_CUDA = torch.cuda.is_available() DEVICE = "cuda" if USE_CUDA else "cpu" model = CNN().to(DEVICE) opt = optim.Adam(model.parameters()) print("Device :", DEVICE) summary_(model,(1,28,28), device=DEVICE)
Device : cuda ---------------------------------------------------------------- Layer (type) Output Shape Param # ================================================================ Conv2d-1 [-1, 8, 28, 28] 80 MaxPool2d-2 [-1, 8, 14, 14] 0 LeakyReLU-3 [-1, 8, 14, 14] 0 Conv2d-4 [-1, 16, 14, 14] 1,168 MaxPool2d-5 [-1, 16, 7, 7] 0 LeakyReLU-6 [-1, 16, 7, 7] 0 Conv2d-7 [-1, 32, 7, 7] 4,640 MaxPool2d-8 [-1, 32, 3, 3] 0 LeakyReLU-9 [-1, 32, 3, 3] 0 Conv2d-10 [-1, 64, 3, 3] 18,496 MaxPool2d-11 [-1, 64, 1, 1] 0 LeakyReLU-12 [-1, 64, 1, 1] 0 AdaptiveAvgPool2d-13 [-1, 64, 1, 1] 0 Linear-14 [-1, 24] 1,560 LeakyReLU-15 [-1, 24] 0 Linear-16 [-1, 10] 250 ================================================================ Total params: 26,194 Trainable params: 26,194 Non-trainable params: 0 ---------------------------------------------------------------- Input size (MB): 0.00 Forward/backward pass size (MB): 0.13 Params size (MB): 0.10 Estimated Total Size (MB): 0.23 ----------------------------------------------------------------
CNN은 앞선 LinearNet보다는 복잡한 구조를 가졌지만,
사용하는 가중치 개수는 26,194이다.
LinearNet보다 대략 9~10배 정도 더 적은 가중치를 사용한다.
과연, CNN은 적은 가중치로 어떤 성능을 보여줄까?# 학습 시작 # t_loss_his, t_acc_his, v_loss_his, v_acc_his = train(EPOCHS = 10, model = model, train_loader = train_loader, opt = opt)
<<< EPOCH 1 >>> Training : [3200/48000 (7%)] Loss: 1.139337 Acc : 0.024 ... 중간 생략 ... Training : [44800/48000 (93%)] Loss: 0.460232 Acc : 0.673 [1] valid Loss : 0.5410 accuracy: 79.25% <<< EPOCH 2 >>> Training : [3200/48000 (7%)] Loss: 0.342676 Acc : 0.055 ... 중간 생략 ... Training : [44800/48000 (93%)] Loss: 0.312424 Acc : 0.781 ... 중간 생략 ... <<< EPOCH 10 >>> Training : [3200/48000 (7%)] Loss: 0.082751 Acc : 0.061 ... 중간 생략 ... Training : [44800/48000 (93%)] Loss: 0.157439 Acc : 0.847 [10] valid Loss : 0.2796 accuracy: 89.85%
10번의 학습 후 검증데이터에 대한 Loss와 정확도는 각각 0.2796와 89.85%이다.
plt.plot(t_loss_his, label="train") plt.plot(v_loss_his, label="valid") plt.legend() plt.show()
plt.plot(t_acc_his, label="train") plt.plot(v_acc_his, label="valid") plt.legend() plt.show()
v_loss, v_acc = evaluate(model, test_loader) print("Test Loss : {:.4f}\t accuracy: {:.2f}%\n".format(v_loss, v_acc*100.))
Test Loss : 0.2746 accuracy: 89.97%
Test 데이터에 대해서는 Linear Net에 비해 대략 1%정도 더 높은 성능을 보인다.
과연 이 1%의 성능이 더 좋은 것일까?필자는 9~10배 정도의 적은 가중치를 사용하고도 LinearNet보다 더 좋은 성능을 보인 CNN의 손을 들어주고 싶다.
LinearNet과 CNN의 진정한 성능차이는 컬러이미지에서 들어난다.
다음 포스팅은 Cifar-10 데이터셋을 통해서 코드 실습을 진행해보고자 한다.
728x90'코딩(Coding) > 기계학습' 카테고리의 다른 글
[기계학습] CIFAR-10 데이터를 분류하는 분류기를 만들어보자(with CNN) (코드실습) (0) 2022.02.25 [기계학습] 합성곱 신경망(CNN : Convolutional Nerual Network) (Part 1/2) (0) 2022.02.09 [기계학습] IRIS 데이터를 분류하는 분류기를 만들어보자(코드 실습) (0) 2022.02.08 [기계학습] 로지스틱 회귀(분류 Classification)(Logistic Regression) (Part 2/2) (0) 2022.01.25 [기계학습] 로지스틱 회귀(분류 Classification)(Logistic Regression) (Part 1/2) (0) 2022.01.21