[AI] PyTorch : 8. Convolutional Neural Network

NewPlus·2022년 7월 5일
1

PyTorch

목록 보기
8/11

2022.07.05 연구실 공부(PyTorch). 'PyTorch로 시작하는 딥 러닝 입문'< 이 글은 이 책의 내용을 요약 정리한 것임.>(내 저작물이 아니고 저 위 링크에 있는 것이 원본임)
07 Convolutional Neural Network

Summary

  • Convolution and Pooling
  • MNIST Classification by CNN
  • MNIST Classification by Deep CNN

1. Convolution and Pooling

1-1. Convolutional Neural Network(CNN)

  • 위 두 손글씨는 사람에게는 같은 알파벳 Y이지만 컴퓨터에게는 각 픽셀의 값이 달라 완전히 다른 입력값임 -> 다층 퍼셉트론을 위해 1차원 텐서(벡터)로 변환하면 원래 어떤 이미지였는지 알아보기가 어려움 -> 공간적인 구조(spatial structure) 정보가 유실된 상태
  • 이미지의 공간적인 구조 정보를 보존하면서 학습할 수 있는 방법이 필요해졌고, 이를 위해 사용하는 것이 합성곱 신경망(CNN)임

1-2. Channel

  • 이미지 : (높이, 너비, 채널)인 3차원 텐서 -> 채널은 RGB 성분 -> 흑백 이미지의 경우 채널이 1임(각 픽셀이 0 ~ 255 사이 값임)
  • 흑백 28 × 28 픽셀의 손글씨 데이터 예시
  • 흑백 이미지므로 채널 수가 1임을 고려하면 (28 × 28 × 1)의 크기를 가지는 3차원 텐서임
  • 컬러 이미지는 적색(Red), 녹색(Green), 청색(Blue) 채널 수가 3개 -> 높이가 28, 너비가 28인 컬러 이미지이면 텐서는 (28 × 28 × 3)의 크기를 가지는 3차원 텐서임(Channel = Depth)

1-3. Convolution operation

  • 합성곱층은 합성곱 연산(Convolution operation)을 통해서 이미지의 특징을 추출하는 역할
  • 커널(kernel), 필터(filter) : n × m 크기의 행렬
  • 커널 혹은 필터로 입력(높이(height) × 너비(width) 크기의 이미지)을 처음부터 끝까지 겹치며 훑으면서 겹쳐지는 부분의 각 이미지와 커널의 원소의 값을 곱해서 모두 더한 값을 출력
  • 구체적인 과정
  • (1×1) + (2×0) + (3×1) + (2×1) + (1×0) + (0×1) + (3×0) + (0×1) + (1×0) = 6
  • (2×1) + (3×0) + (4×1) + (1×1) + (0×0) + (1×1) + (0×0) + (1×1) + (1×0) = 9
  • (3×1) + (4×0) + (5×1) + (0×1) + (1×0) + (2×1) + (1×0) + (1×1) + (0×0) = 11
  • (2×1) + (1×0) + (0×1) + (3×1) + (0×0) + (1×1) + (1×0) + (4×1) + (1×0) = 10
  • 총 9번의 스텝까지 마쳤다고 가정하면 그 결과는 아래와 같음
  • 특성 맵(feature map) : 입력으로부터 커널을 사용하여 합성곱 연산을 통해 나온 결과
  • 스트라이드(stride) : 커널의 이동 범위
  • 스트라이드가 2인 경우의 예시(입력은 5 × 5 이미지)

1-4. Padding

  • 다층 퍼셉트론으로 3 × 3 이미지를 처리한다고 가정
  • 3 × 3의 입력을 1차원 텐서(벡터)로 만들면 3 × 3 = 9가 되므로 입력층은 9개의 뉴런을 가짐
  • 4개의 뉴론을 가지는 은닉층을 추가
  • 연결선 = 가중치(9 × 4 = 36개), 합성곱 신경망(CNN)으로 3 × 3 이미지를 처리, 2 × 2 커널, 스트라이드는 1
  • 이를 인공 신경망의 형태로 표현하면 다음과 같음
  • 동일한 커널로 이미지 전체를 훑으며 합성곱 연산 진행 -> 가중치는 4개(w_0, w_1, w_2, w_3) -> 커널과 맵핑되는 픽셀만을 입력으로 사용 -> CNN은 다층 퍼셉트론보다 훨씬 적은 수의 가중치를 사용하여 공간적 구조 정보를 보존
  • CNN에서의 활성화 함수 : 비선형이면서 시그모이드가 아닌 렐루(ReLU)가 사용됨.
  • 합성곱 층(convolution layer) : 합성곱 연산을 통해서 특성 맵을 얻고, 활성화 함수를 지나는 연산을 하는 합성곱 신경망의 은닉층
  • 편향(bias)도 추가 가능, 커널 적용 후 추가, 하나의 값만 존재, 모든 원소에 더함.

1-5. Feature Map Size


  • floor()는 소수점 이하 내림
  • 5 × 5 크기의 이미지에 3 × 3 커널을 사용하고 스트라이드 1로 합성곱 연산한 경우
  • 특성 맵의 높이 : floor((5 - 3) / 1 + 1) = 3
  • 특성 맵의 너비 : floor((5 - 3) / 1 + 1) = 3
  • 필요한 스텝 : 3 × 3 = 9

1-6. Convolution operation in Multiple Channels(3D Tensor)

  • 3개의 채널을 가진 입력 데이터와 3개의 채널을 가진 커널의 합성곱 연산
  • 커널의 각 채널끼리의 크기는 같아야 함, 각 채널 간 합성곱 연산 -> 그 결과를 모두 더해서 하나의 채널을 가지는 특성 맵 생성
  • 위 예제는 1개의 입력이 3개의 채널을 가짐 = 3개의 채널을 가진 1개의 커널
  • 3차원 텐서의 합성곱 연산의 예시
  • 3차원 텐서의 합성곱 연상에서 다수의 커널의 사용할 경우
  • 사용한 커널의 수는 합성곱 연산의 결과로 나오는 특성 맵의 채널 수가 됨
  • 하나의 커널 속의 하나의 채널은 커널의 높이 × 커널의 너비 만큼 매개 변수를 가짐 -> 커널의 채널 수는 입력 데이터의 채널 수와 같아야 함(커널의 높이 × 커널의 너비 × 채널 수) -> 이런 채널이 커널의 개수만큼 있으므로 -> 최종적으로 가중치 매개변수의 총 수는 (커널의 높이 × 커널의 너비 × 채널 수 × 커널의 수) 이다.

1-7. Pooling

  • 일반적으로 합성곱 층(합성곱 연산 + 활성화 함수) 다음엔 풀링 층 추가
  • 풀링 층 : 특성 맵을 다운샘플링하여 특성 맵의 크기를 줄이는 풀링 연산을 수행
  • Max Pooling(최대 풀링) : 커널의 크기와 겹치는 입력 데이터의 영역 안에서 최대값(max)을 추출하는 방식
  • Average Pooling(평균 풀링) : 커널의 크기와 겹치는 입력 데이터의 영역 안에서 평균값(average)을 추출하는 방식
  • 학습해야 할 가중치가 없으며 연산 후에 채널 수가 변하지 않음.

2. MNIST Classification by CNN

2-1. Model Architecture

  • 표기 1 : 합성곱(nn.Cov2d) + 활성화 함수(nn.ReLU)를 하나의 합성곱 층으로 보고, 맥스풀링(nn.MaxPoold2d)은 풀링 층으로 명명
  • 표기 2 : 합성곱(nn.Conv2d) + 활성화 함수(nn.ReLU) + 맥스풀링(nn.MaxPoold2d)을 하나의 합성곱 층으로 명명
  • 여기선 2번으로 정함
  • 모델의 아키텍처(3개의 층) 구성도
# 1번 레이어 : 합성곱층(Convolutional layer)
합성곱(in_channel = 1, out_channel = 32, kernel_size=3, stride=1, padding=1) + 활성화 함수 ReLU
맥스풀링(kernel_size=2, stride=2))

# 2번 레이어 : 합성곱층(Convolutional layer)
합성곱(in_channel = 32, out_channel = 64, kernel_size=3, stride=1, padding=1) + 활성화 함수 ReLU
맥스풀링(kernel_size=2, stride=2))

# 3번 레이어 : 전결합층(Fully-Connected layer)
특성맵을 펼친다. # batch_size × 7 × 7 × 64 → batch_size × 3136
전결합층(뉴런 10) + 활성화 함수 Softmax

2-2. 모델 구현

  • 필요한 도구 임포트
import torch
import torch.nn as nn
  • 임의의 텐서(1 × 1 × 28 × 28) 생성
# 배치 크기 × 채널 × 높이(height) × 너비(widht)의 크기의 텐서를 선언
inputs = torch.Tensor(1, 1, 28, 28)
print('텐서의 크기 : {}'.format(inputs.shape))
  • 1번 합성곱 층 구현
conv1 = nn.Conv2d(1, 32, 3, padding=1)
print(conv1)
  • 2번 합성곱 층 구현
conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
print(conv2)
  • 맥스풀링 구현
pool = nn.MaxPool2d(2)
print(pool)
  • 첫 번째 합성곱 층 통과 후 텐서의 크기
out = conv1(inputs)
print(out.shape)
torch.Size([1, 32, 28, 28])
  • 맥스풀링을 통과한 후의 텐서의 크기
out = pool(out)
print(out.shape)
torch.Size([1, 32, 14, 14])
  • 두 번째 합성곱 층 통과한 후의 텐서의 크기
out = conv2(out)
print(out.shape)
torch.Size([1, 64, 14, 14])
  • 맥스풀링을 통과한 후의 텐서의 크기
out = pool(out)
print(out.shape)
torch.Size([1, 64, 7, 7])
  • .view()로 텐서 펼치기
# 첫번째 차원인 배치 차원은 그대로 두고 나머지는 펼쳐라
out = out.view(out.size(0), -1) 
print(out.shape)
torch.Size([1, 3136])
  • 전결합층(Fully-Connteced layer)를 통과
fc = nn.Linear(3136, 10) # input_dim = 3,136, output_dim = 10
out = fc(out)
print(out.shape)
torch.Size([1, 10])

2-3. MNIST Classification by CNN

import torch
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import torch.nn.init

device = 'cuda' if torch.cuda.is_available() else 'cpu'

# 랜덤 시드 고정
torch.manual_seed(777)

# GPU 사용 가능일 경우 랜덤 시드 고정
if device == 'cuda':
    torch.cuda.manual_seed_all(777)

learning_rate = 0.001
training_epochs = 15
batch_size = 100

mnist_train = dsets.MNIST(root='MNIST_data/', # 다운로드 경로 지정
                          train=True, # True를 지정하면 훈련 데이터로 다운로드
                          transform=transforms.ToTensor(), # 텐서로 변환
                          download=True)

mnist_test = dsets.MNIST(root='MNIST_data/', # 다운로드 경로 지정
                         train=False, # False를 지정하면 테스트 데이터로 다운로드
                         transform=transforms.ToTensor(), # 텐서로 변환
                         download=True)

data_loader = torch.utils.data.DataLoader(dataset=mnist_train,
                                          batch_size=batch_size,
                                          shuffle=True,
                                          drop_last=True)

class CNN(torch.nn.Module):

    def __init__(self):
        super(CNN, self).__init__()
        # 첫번째층
        # ImgIn shape=(?, 28, 28, 1)
        #    Conv     -> (?, 28, 28, 32)
        #    Pool     -> (?, 14, 14, 32)
        self.layer1 = torch.nn.Sequential(
            torch.nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2))

        # 두번째층
        # ImgIn shape=(?, 14, 14, 32)
        #    Conv      ->(?, 14, 14, 64)
        #    Pool      ->(?, 7, 7, 64)
        self.layer2 = torch.nn.Sequential(
            torch.nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2))

        # 전결합층 7x7x64 inputs -> 10 outputs
        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.layer1(x)
        out = self.layer2(out)
        out = out.view(out.size(0), -1)   # 전결합층을 위해서 Flatten
        out = self.fc(out)
        return out

# CNN 모델 정의
model = CNN().to(device)

criterion = torch.nn.CrossEntropyLoss().to(device)    # 비용 함수에 소프트맥스 함수 포함되어져 있음.
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
total_batch = len(data_loader)

for epoch in range(training_epochs):
    avg_cost = 0

    for X, Y in 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))

# 학습을 진행하지 않을 것이므로 torch.no_grad()
with torch.no_grad():
    X_test = mnist_test.test_data.view(len(mnist_test), 1, 28, 28).float().to(device)
    Y_test = mnist_test.test_labels.to(device)

    prediction = model(X_test)
    correct_prediction = torch.argmax(prediction, 1) == Y_test
    accuracy = correct_prediction.float().mean()
    print('Accuracy:', accuracy.item())
  • 결과

  • 정확도가 대략 98%이다.

3. MNIST Classification by Deep CNN

  • 모델 아키텍처(3번과 4번 레이어를 더 추가)
# 1번 레이어 : 합성곱층(Convolutional layer)
합성곱(in_channel = 1, out_channel = 32, kernel_size=3, stride=1, padding=1) + 활성화 함수 ReLU
맥스풀링(kernel_size=2, stride=2))

# 2번 레이어 : 합성곱층(Convolutional layer)
합성곱(in_channel = 32, out_channel = 64, kernel_size=3, stride=1, padding=1) + 활성화 함수 ReLU
맥스풀링(kernel_size=2, stride=2))

# 3번 레이어 : 합성곱층(Convolutional layer)
합성곱(in_channel = 64, out_channel = 128, kernel_size=3, stride=1, padding=1) + 활성화 함수 ReLU
맥스풀링(kernel_size=2, stride=2, padding=1))

# 4번 레이어 : 전결합층(Fully-Connected layer)
특성맵을 펼친다. # batch_size × 4 × 4 × 128 → batch_size × 2048
전결합층(뉴런 625) + 활성화 함수 ReLU

# 5번 레이어 : 전결합층(Fully-Connected layer)
전결합층(뉴런 10) + 활성화 함수 Softmax
  • 깊은 CNN 모델 구현
import torch
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import torch.nn.init

device = 'cuda' if torch.cuda.is_available() else 'cpu'

# 랜덤 시드 고정
torch.manual_seed(777)

# GPU 사용 가능일 경우 랜덤 시드 고정
if device == 'cuda':
    torch.cuda.manual_seed_all(777)

learning_rate = 0.001
training_epochs = 15
batch_size = 100

mnist_train = dsets.MNIST(root='MNIST_data/', # 다운로드 경로 지정
                          train=True, # True를 지정하면 훈련 데이터로 다운로드
                          transform=transforms.ToTensor(), # 텐서로 변환
                          download=True)

mnist_test = dsets.MNIST(root='MNIST_data/', # 다운로드 경로 지정
                         train=False, # False를 지정하면 테스트 데이터로 다운로드
                         transform=transforms.ToTensor(), # 텐서로 변환
                         download=True)

data_loader = torch.utils.data.DataLoader(dataset=mnist_train,
                                          batch_size=batch_size,
                                          shuffle=True,
                                          drop_last=True)
                                        
class CNN(torch.nn.Module):

    def __init__(self):
        super(CNN, self).__init__()
        self.keep_prob = 0.5
        # L1 ImgIn shape=(?, 28, 28, 1)
        #    Conv     -> (?, 28, 28, 32)
        #    Pool     -> (?, 14, 14, 32)
        self.layer1 = torch.nn.Sequential(
            torch.nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2))
        # L2 ImgIn shape=(?, 14, 14, 32)
        #    Conv      ->(?, 14, 14, 64)
        #    Pool      ->(?, 7, 7, 64)
        self.layer2 = torch.nn.Sequential(
            torch.nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2))
        # L3 ImgIn shape=(?, 7, 7, 64)
        #    Conv      ->(?, 7, 7, 128)
        #    Pool      ->(?, 4, 4, 128)
        self.layer3 = torch.nn.Sequential(
            torch.nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2, padding=1))

        # L4 FC 4x4x128 inputs -> 625 outputs
        self.fc1 = torch.nn.Linear(4 * 4 * 128, 625, bias=True)
        torch.nn.init.xavier_uniform_(self.fc1.weight)
        self.layer4 = torch.nn.Sequential(
            self.fc1,
            torch.nn.ReLU(),
            torch.nn.Dropout(p=1 - self.keep_prob))
        # L5 Final FC 625 inputs -> 10 outputs
        self.fc2 = torch.nn.Linear(625, 10, bias=True)
        torch.nn.init.xavier_uniform_(self.fc2.weight)

    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)
        out = out.view(out.size(0), -1)   # Flatten them for FC
        out = self.layer4(out)
        out = self.fc2(out)
        return out

# CNN 모델 정의
model = CNN().to(device)

criterion = torch.nn.CrossEntropyLoss().to(device)    # 비용 함수에 소프트맥스 함수 포함되어져 있음.
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
total_batch = len(data_loader)

for epoch in range(training_epochs):
    avg_cost = 0

    for X, Y in 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))

# 학습을 진행하지 않을 것이므로 torch.no_grad()
with torch.no_grad():
    X_test = mnist_test.test_data.view(len(mnist_test), 1, 28, 28).float().to(device)
    Y_test = mnist_test.test_labels.to(device)

    prediction = model(X_test)
    correct_prediction = torch.argmax(prediction, 1) == Y_test
    accuracy = correct_prediction.float().mean()
    print('Accuracy:', accuracy.item())
  • 결과

  • 정확도(98.2%)가 2.의 CNN(98.6%)보다 살짝 낮은데 이로부터 층을 단순히 많이 추가하는 것보다 효율적으로 구성하는 것이 더 중요하다는 것을 알 수 있다.

출처 : 'PyTorch로 시작하는 딥 러닝 입문' <이 책의 내용을 요약 정리한 것임.>

profile
매일 매일 새로워지는 나 자신을 꿈꾸며

0개의 댓글