Post
Ch5 CNN 맛보기 | Gihun Son

Ch5 CNN 맛보기

fashion_mnist 데이터셋을 사용하여 CNN을 직접 구현해보자.

[Fashion_mnist Dataset]

fashion_mnist 데이터셋은 토치비전(torchvision)에 내장된 예제 데이터로 운동화, 셔츠, 샌들 같은 작은 이미지의 모음이며, 기본 MNIST 데이터셋처럼 열 가지로 분류될 수 있는 28×28 픽셀의 이미지 7만 개로 구성되어 있다.

데이터셋을 자세히 살펴보면 훈련 데이터(train_images)는 0에서 255 사이의 값을 갖는 28×28 크기의 넘파이(NumPy) 배열이고, 레이블(정답) 데이터(train_labels)는 0에서 9까지 정수 값을 갖는 배열이다.

0에서 9까지 정수 값은 이미지(운동화, 셔츠 등)의 클래스를 나타내는 레이블이다. 각 레이블과 클래스는 다음과 같다.

0 : T-Shirt

1 : Trouser

2 : Pullover

3 : Dress

4 : Coat

5 : Sandal

6 : Shirt

7 : Sneaker

8 : Bag

9 : Ankle Boot

아래는 예제 진행을 위해 필요한 library

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
import matplotlib.pyplot as plt

import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.nn.functional as F

import torchvision
import torchvision.transforms as transforms #data preprocessing에 사용
from torch.utils.data import Dataset, DataLoader

다음 코드는 파이토치를 사용할 환경을 구성하는 코드이다. GPU가 장착되어 있다면 사용하고, 아니면 CPU를 사용한다.

1
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

[GPU 사용]

일반적으로 하나의 GPU를 사용할 때는 다음과 같이 사용한다.

  • device = torch.device(“cuda:0” if torch.cuda.is_available() else “cpu”)
  • model = Net()
  • model.to(device)

하지만 다수의 GPU를 사용할 때에는 다음과 같이 nn.DataParallel을 사용한다.

  • device = torch.device(“cuda” if torch.cuda.is_available() else “cpu”)
  • model = Net()
  • if torch.cuda.device_count() > 1:
  •      model = nn.DataParallel(net)
  • model.to(device)

nn.DataParallel을 사용할 경우 batch size가 알아서 각 GPU로 분배되는 방식으로 동작한다. 따라서 GPU 수만큼 batch size도 늘려줘야 한다.

아래 코드를 통해 fashion_mnist 데이터셋을 다운받을 수 있다.

1
2
3
4
train_dataset=torchvision.datasets.FashionMNIST("../chap05/data",download=True,
                                                transform=transforms.Compose([transforms.ToTensor()]))
test_dataset=torchvision.datasets.FashionMNIST("../chap05/data",download=True,
                                                train=False,transform=transforms.Compose([transforms.ToTensor()]))

torchvision.datasets는 torch.utils.data.Dataset의 하위 클래스로 다양한 데이터셋(CIFAR, COCO, MNIST, ImageNet 등)을 포함한다. 사용되는 parameter의 역할은 다음과 같다.

  • ”../chap05/data”:FashionMNIST를 다운받을 위치
  • download=True: download파라미터를 True로 설정하면, 첫번째 파라미터의 위치에 해당 데이터셋이 있는지 확인 한 후 내려받는다.
  • transform=transforms.Compose([transforms.ToTensor()]): 이미지를 텐서(0~1)로 변환한다.

아래 코드는 다운받은 fashion_mnist데이터를 메모리로 불러오기 위해 DataLoader에 전달한다.

troch.utils.data.DataLoader를 사용하여 원하는 크기의 batch size로 데이터를 불러오거나, shuffle 파라미터를 통해 순서를 무작위로 설정할 수도 있다.

1
2
train_loader=torch.utils.data.DataLoader(train_dataset,batch_size=100)
test_loader=torch.utils.data.DataLoader(test_dataset,batch_size=100)

먼저 데이터를 살펴보자. fashion_mnist 데이터의 20개의 이미지를 label정보와 함께 출력해보자. 이 때 label은 ‘T-Shirt’, ‘Tensor’, ‘Pullover’, ‘Dress’, ‘Coat’, ‘Sandal’, ‘Shirt’, ‘Sneaker’, ‘Bag’, ‘Ankle Boot’의 10개의 class로 구성된다.

np.random은 무작위로 데이터를 생성할 때 사용한다. 또한 np.random.randint()는 discrete distribution(셀 수 있는 정수형 분포)을 갖는 데이터에서 값을 무작위로 추출할때 사용한다.

  • np.random.rand(8): 0~1사이의 standard normal distribution(표준정규분포) 난수를 (1x8)행렬로 출력
  • np.random.rand(4,2): 0~1사이의 standard normal distribution 난수를 (4x2)행렬로 출력
  • np.random.rand(1,10): 1~9의 임의의 숫자 출력
  • np.random.rand(10): 0~10의 임의의 숫자 출력
  • np.random.randn(4,2): 평균이 0이고, 표준편차가 1인 Gaussian normal distribution난수를 (4x2) 행렬로 출력

train_dataset을 이용하여 3차원 배열을 생성. 아래는 예시코드

1
2
3
4
5
6
7
import numpy as np
examp=np.arange(0,100,3) #1~99의 숫자에서 3씩 건너뛴 행렬을 생성
examp.resize(6,4) #행렬의 크기를 6x4로 조정
print(examp)
print(examp[3]) #3행에 해당하는 모든값 출력
print(examp[3,3]) #3행 3열의 값 출력
print(examp[3][3]) #동일하게 3행 3열 값 출력

따라서 train_dataset[img_xy][0][0,:,:]은 4차원의 배열중 해당하는 데이터를 나타내는 것

1
2
3
4
5
6
7
8
9
10
11
12
labels_map = {0 : 'T-Shirt', 1 : 'Trouser', 2 : 'Pullover', 3 : 'Dress', 4 : 'Coat',
              5 : 'Sandal', 6 : 'Shirt', 7 : 'Sneaker', 8 : 'Bag', 9 : 'Ankle Boot'}
fig=plt.figure(figsize=(8,8)); #출력할 이미지의 가로세로 길이(inch)
columns=4;
rows=5;
for i in range(1,columns*rows+1):
    img_xy=np.random.randint(len(train_dataset))
    img=train_dataset[img_xy][0][0,:,:]
    fig.add_subplot(rows,columns,i)
    plt.title(labels_map[train_dataset[img_xy][1]])
    plt.imshow(img,cmap='gray')
plt.show

CNN과 CNN이 아닌 DNN의 비교를 위해 먼저 DNN(ConvVet이 없음)을 생성하여 보겠다.


image.png

super()를 사용하는 이유는 부모 클래스의 instance속성 또한 상속받기 위함

위 예시에서는 부모 클래스의 super().init()을 통해 attribute를 불러온 적이 없기 때문에, 부모 클래스의 instance를 받아올 수 없다.

+) super(Class,self).init()과 super(self).init()은 같은 역할을 하지만 super(Class,self).init()은 python 2, 3에도 모두 동작하고 상속받는 부모클래스를 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A:
    def __init__(self):
        self.a = 10

    def get_a(self):
        return self.a

class B(A):
    def __init__(self):
        super(B,self).__init__()
        self.b = 20

    def get_b(self):
        return self.b

class C(B):
    def __init__(self):
        super(B,self).__init__()
        self.c = 30

    def get_c(self):
        return self.c

new_c = C()
print(new_c.get_a())
print(new_c.get_c())
#아래 코드에서 오류가 생기는 이유는 super(B,self).__init__()을 통해 C의 부모 class인 B를 상속받은 것이 아니라 B의 부모 class인 A를 상속받았기 때문이다.
#print(new_c.get_b())

F.xx()(또는 nn.functional.xx())와 nn.xx()는 사용하는 방법에 차이가 있다.

간단히 말하면 nn.functional은 함수이므로 instance를 생성하지 않고, 바로 사용할 수 있고(파라미터값은 항상 전달해야함), nn은 Class이므로 instance를 생성하여 사용해야 한다.

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# nn.functional.xx()
import torch
import torch.nn as nn
inputs=torch.randn(64,3,244,244)
conv=nn.Conv2d(in_channels=3,out_channels=64,kernel_size=3,padding=1)
outputs=conv(inputs)
layer=nn.Conv2d(1,1,3)

# nn.xx()
import torch.nn.functional as F
inputs=torch.randn(64,3,244,244)
weight=torch.randn(64,3,3,3)
bias=torch.randn(64)
outputs=F.conv2d(inputs,weight,bias,padding=1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FashionDNN(nn.Module):
    def __init__(self):
        super(FashionDNN,self).__init__()
        #in_features는 입력의 크기, out_features는 출력의 크기
        self.fc1=nn.Linear(in_features=784,out_features=256)
        # nn.Dropout(p)는 p의 확률로 텐서의 값이 0이 되고, 0이 되지 않은 값은 (1/(1-p))배 되어 커진다.
        self.drop=nn.Dropout(0.25)
        self.fc2=nn.Linear(in_features=256,out_features=128)
        self.fc3=nn.Linear(in_features=128,out_features=10)

    def forward(self,input_data): #학습데이터를 입력받아 forward propagation을 진행(반드시 forward()라는 이름이어야 함)
    #pytorch view()는 numpy의 reshape과 같은 역할로 텐서의 크기를 변경
    #view(-1,784)는 2차원의 (?,784) 텐서로 변경하라는 것
        out=input_data.view(-1,784)
        out=F.relu(self.fc1(out))
        out=self.drop(out)
        out=F.relu(self.fc2(out))
        out=self.fc3(out)
        return out

이제 Loss function, Learning rate, Optimizer를 정의하자.

1
2
3
4
5
6
7
learning_rate=0.001
model=FashionDNN()
model.to(device)

criterion=nn.CrossEntropyLoss()
optimizer=torch.optim.Adam(model.parameters(),lr=learning_rate)
print(model)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
num_epochs = 5
count = 0
loss_list = [] #------ ① 비어있는 행렬을 만들고, append를 사용하여 값을 입력한다.
iteration_list = []
accuracy_list = []

predictions_list = []
labels_list = []

for epoch in range(num_epochs):
    for images, labels in train_loader: #------ ②
    #모델이 데이터를 처리하기 윟새 모델과 데이터가 동일한 장치에 있어야 한다. model.to(device)로 GPU에 모델이 있으므로 데이터에도 동일하게 적용
        images, labels = images.to(device), labels.to(device) #------ ③

        #torch.autograd패키지 안에 있는 Variable은  backpropagation을 위한 미분값을 자동으로 계산해 준다.
        train = Variable(images.view(100, 1, 28, 28)) #------ ④
        labels = Variable(labels)

        outputs = model(train) #------ 학습 데이터를 모델에 적용
        loss = criterion(outputs, labels)
        #loss.backward()를 호출하면 각 파라미터들의 .grad 값에 변화도가 저장
        #만약 zero_grad()를 하여 .grad를 초기화해주지 않으면 이전 loop의 값도 영향을 주게 된다.
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        count += 1

        if not (count % 50): #------ count를 50으로 나누었을 때 나머지가 0이 아니라면 실행
            total = 0
            correct = 0
            for images, labels in test_loader:
                images, labels = images.to(device), labels.to(device)
                labels_list.append(labels)
                test = Variable(images.view(100, 1, 28, 28))
                outputs = model(test)
                predictions = torch.max(outputs, 1)[1].to(device)
                predictions_list.append(predictions)
                correct += (predictions == labels).sum()
                total += len(labels)

            accuracy = correct * 100 / total #------ ⑤ 백분율로 나타낼 때
            loss_list.append(loss.data)# ------ ①′
            iteration_list.append(count)
            accuracy_list.append(accuracy)

        if not (count % 500):
            print("Iteration: {}, Loss: {}, Accuracy: {}%".format(count, loss.data, accuracy))

②for images, labels in train_loader: image.png

위와 같이 86%의 정확도를 볼 수 있었다. 이제 CNN을 적용해보자

nn.Sequential: nn.Sequential을 통해 forward()함수에서 구현될 forward propagation을 가독성 좋게 코드로 작성할 수 있다. 즉 여러개의 layer를 하나의 layer로 선언할 수 있다.

Conv2d: convolutional layer는 convolution을 통해 이미지의 특징을 추출한다. kernel(filter)이 이미지를 처음부터 끝까지 훑으면서 각 원소 값끼리 곱한 후 모두 더한 값을 출력한다. kernel은 일반적으로 3x3 또는 5x5를 사용한다. 파라미터의 의미는 다음과 같다.

  • in_channels: 입력 채널의 수를 의미(예시에서는 Grayscale이므로 1)
  • out_channels: 출력 채널의 수
  • kernel_size: kernel의 크기, (3,5)의 형태로 지정해도 된다.
  • padding: 패딩의 크기를 의미(클수록 출력크기도 커진다.

BatchNorm2d: training과정에서 각 batch별로 데이터가 다양한 분포를 가지더라도 평균과 분산을 이용하여 정규화한다. 예시 그림은 아래와 같다.

image.png

MaxPool2d: 이미지 크기를 축소시키는 용도로 사용한다. Convolutional layer의 출력을 입력데이터로 받아 크기를 축소한다. 파라미터의 설명은 다음과 같다.

  • kernel_size:pooling kernel의 크기
  • stride: kernel의 간격

fully connected layer: 클래스를 분류하기 위해서 이미지 형태의 데이터를 배열 형태로 변환하여야 한다. 이때 Conv2d에서 사용하는 하이퍼파라미터 값들에 따라 출력 크기가 달라진다. 이 출력값을 fc layer에 전달한다.

  • in_features: 입력 데이터의 크기 (이전 Convolutional layer의 출력 크기를 계산해보아야 한다.)
  • out_features: 출력 데이터의 크기 image.pngimage.png

out = out.view(out.size(0), -1): out.size(0)은 100을 의미하고, (100,?) 크기의 텐서로 변경하겠다는 의미

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FashionCNN(nn.Module):
    def __init__(self):
        super(FashionCNN, self).__init__()
        self.layer1 = nn.Sequential( #------ ①
            nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1),# ------ ②
            nn.BatchNorm2d(32),# ------ ③
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2) #------ ④
        )
        self.layer2 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        self.fc1 = nn.Linear(in_features=64*6*6, out_features=600)# ------ ⑤
        self.drop = nn.Dropout2d(0.25)
        self.fc2 = nn.Linear(in_features=600, out_features=120)
        self.fc3 = nn.Linear(in_features=120, out_features=10) #------ 마지막 계층의 out_features는 클래스 개수를 의미

    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = out.view(out.size(0), -1) #------ ⑥
        out = self.fc1(out)
        out = self.drop(out)
        out = self.fc2(out)
        out = self.fc3(out)
        return out
1
2
3
4
5
6
7
learning_rate = 0.001;
model = FashionCNN();
model.to(device)

criterion = nn.CrossEntropyLoss();
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate);
print(model)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
num_epochs = 5
count = 0
loss_list = []
iteration_list = []
accuracy_list = []

predictions_list = []
labels_list = []

for epoch in range(num_epochs):
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)

        train = Variable(images.view(100, 1, 28, 28))
        labels = Variable(labels)

        outputs = model(train)
        loss = criterion(outputs, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        count += 1

        if not (count % 50):
            total = 0
            correct = 0
            for images, labels in test_loader:
                images, labels = images.to(device), labels.to(device)
                labels_list.append(labels)
                test = Variable(images.view(100, 1, 28, 28))
                outputs = model(test)
                predictions = torch.max(outputs, 1)[1].to(device)
                predictions_list.append(predictions)
                correct += (predictions == labels).sum()
                total += len(labels)

            accuracy = correct * 100 / total
            loss_list.append(loss.data)
            iteration_list.append(count)
            accuracy_list.append(accuracy)

        if not (count % 500):
            print("Iteration: {}, Loss: {}, Accuracy: {}%".format(count, loss.data, accuracy))
This post is licensed under CC BY 4.0 by the author.