본문 바로가기

7. ML | DL | NLP

6/19(월) IT K-DT(73일차) / 18. CNN 기초

 

 

18. CNN(Convolutional Neural Networks)

 

18-1. CNN의 개요

CNN(컨볼루셔널 신경망)은 이미지 처리와 인식에 주로 사용되는 딥러닝 알고리즘임.

이미지를 입력으로 받아들이고, 이미지 내의 특징을 추출하여 해당 이미지가 어떤 카테고리에 속하는지

분류하는 작업을 수행함.

 

 

일반적인 신경망과는 달리 CNN은 이미지의 공간적 구조를 활용하여 특징을 추출하고 학습함.

이를 가능하게 하는 핵심 개념으로는 합성곱 연산(convolution)과 풀링(pooling)이 있음.

합성곱 연산(convolution)은 입력 이미지와 일련의 작은 필터(filter)를 곱하여 특징 맵(feature map)을 생성함.

필터는 이미지에서 특정한 패턴이나 특징을 찾는 역할을 수행하며, 엣지(edge)를 검출하기 위한 필터,

직선/곡선의 특정 방향을 찾기 위한 필터 등 여러 종류가 있음.

이러한 필터를 여러 개 사용하여 이미지의 다양한 특징을 추출함.

풀링(pooling)은 특징 맵의 크기를 줄이는 역할을 수행함.

일반적으로 맥스 풀링(max pooling)이라는 특정 영역에서 최대값을 선택하여 다음 층으로 전달하는

개념이 가장 많이 사용됨. (= '최대값'이라는 '액기스'만 뽑는다는 개념)

이를 통해 공간적인 크기를 축소하면서도 중요한 특징을 보존할 수 있음.

CNN은 합성곱과 풀링 과정을 반복하여 이미지의 다양한 특징을 추출한 후, 

추출된 특징을 사용하여 분류나 객체 인식 등의 작업을 수행함.
이러한 구조는 이미지 처리 분야에서 매우 효과적으로 작동하며

컴퓨터 비전, 얼굴 인식, 자율 주행, 의료 영상 분석 등 다양한 응용 분야에서 널리 사용됨.


한편으로 '전통적인 뉴럴 네트워크에 컨볼루셔널 레이어를 붙인 형태' 라고도 표현함.

전통적인 뉴럴 네트워크는 완전 연결된 레이어(예: 다층 퍼셉트론)로 구성된 인공신경망을 의미하며 입력 데이터를 처리하고 출력을 생성하는 데 사용됨.

컨볼루셔널 레이어는 주로 이미지 처리와 관련된 작업에 사용되는 특별한 유형의 뉴럴 네트워크 레이어를 의미하고 입력 데이터의 지역적 패턴을 인식하고 강조하는 데 특화되어 있음.

이러한 레이어는 이미지에서 특징이나 패턴을 추출하는 데 매우 효과적이며, 주로 컴퓨터 비전 분야에서 많이 사용됨.

결론적으로, 컨볼루셔널 레이어를 사용하여 특징(예: 입력 데이터의 지역적 패턴)을 추출하고

그 결과를 전통적인 뉴럴 네트워크에 전달하여 더 효과적인 학습과 예측을 가능하게 한다는 것을 의미함.

 

18-2. CNN의 사용 이유

1) DNN(Deep Neural Network)을 사용한 이미지 분류에서의 문제점 해결:

일반적인 DNN은 1차원 형태의 데이터를 이용함.

→ 2차원 이상의 데이터가 입력되는 경우 Flatten을 통해 데이터를 변환한 후 입력해야 하며 이 과정에서 

    이미지의 공간적/지역적 정보가 손실됨
이러한 DNN의 문제점을 해결하고자 이미지를 그대로 입력하여(Raw input) 공간적/지역적 정보를 유지함.


DNN(Deep Neural Network):
여러 개의 은닉층(중간 층)을 가진 인공신경망 구조를 의미.

전통적인 뉴럴 네트워크(MLP)는 입력층, 은닉층, 출력층으로 구성되어 있으며 은닉층은 일반적으로 1개 이상의 층으로 구성되는데, DNN에서는 이 은닉층이 많이 추가되어 깊은 구조를 형성함.
이를 통해 DNN은 다양한 추상화 수준에서 더 복잡한 특징을 학습하고 표현할 수 있게 됨.

하위 은닉층은 저수준의 특징(예: 에지, 직선 등)을 학습하고,
상위 은닉층은 더 복잡하고 추상적인 특징(예: 물체의 형태, 텍스처 등)을 학습할 수 있음.
이를 통해 DNN은 매우 복잡한 문제에 대한 표현력을 갖추고 있으며, 이미지 인식, 음성 인식, 자연어 처리 등 다양한 분야에서
높은 성능을 보여줌.

DNN은 깊은 구조 때문에 많은 데이터와 연산량을 요구하고, 과적합(overfitting) 문제를 주의해야 하는 등 일부 도전적인 측면도 가지고 있음.


2) 지역적인 특징 인식:

CNN은 이미지의 지역적 특징을 인식하는 데 특화되어 있음.
이미지에서 작은 부분들과 그들 간의 관계를 이해하여 특징을 추출하고,

전체 이미지를 고려하여 패턴을 파악할 수 있음.

이는 이미지 내의 객체 인식, 세분화, 분할 등에 유용함.

3) 공간적 구조 학습:

이미지는 공간적인 구조를 가지고 있음.

CNN은 이미지의 공간적 관계를 활용하여 특징을 학습하고 인식함.

이를 통해 이미지의 공간적 변형, 크기 변경, 회전 등에 상대적으로 강인한 성능을 보임.

4) 매개 변수 공유:

CNN은 합성곱 연산을 통해 필터를 사용하여 이미지 특징을 추출하는데,

이 과정에서 필터의 매개 변수를 이미지의 전체 영역에 동일하게 공유함.

이는 학습할 매개 변수 수를 대폭 감소시키고, 모델의 복잡도를 낮추면서도 일반화 성능을 향상시킴.

5) 데이터의 효율적인 처리:

CNN은 맥스 풀링과 같은 연산을 통해 이미지의 공간적 크기를 줄여나갈 수 있음.

이를 통해 처리해야 할 데이터 양을 줄이고, 계산 비용을 감소시킴.

따라서 대규모 이미지 데이터셋에서도 효율적인 처리가 가능함.

6) 전이 학습(Transfer Learning):

CNN은 사전에 대규모 데이터셋에서 사전 훈련된 모델을 활용하여 다른 작업에 적용할 수 있는

전이 학습의 이점을 가짐. 이미지 처리에 필요한 일반적인 특징을 학습한 모델을 사용하여,

작은 규모의 데이터셋에서도 효과적인 학습을 수행할 수 있음.

 

18-3. 이미지 데이터

컬러 이미지는 3개의 채널(RGB색;빨파녹)로 이루어진 Tensor.
컴퓨터는 이러한 이미지를 숫자로 인식하여 연산함.


이미지의 정보는 0~255까지 총 256개의 숫자로 표현됨.
(빨강255, 파랑255, 초록255: 흰색 / 빨강0, 파랑0, 초록0: 검정색)

 

18-4. Convolution 연산

 

Convolution(컨볼루션) 연산:

입력 신호(데이터)와 필터(커널 또는 커널 행렬) 간의 합성곱을 계산하는 과정.

 

필터(Filter):

일반적으로 작은 크기의 행렬로, 입력 신호와 겹쳐지면서 각 위치에서의 요소별 곱셈/덧셈을 수행함.

이러한 곱셈과 덧셈을 통해 입력 신호와 필터 사이에서 패턴이나 특징이 감지되고 강조됨.

입력 데이터를 지역적으로 조사하면서 특징을 추출하는 기능을 가지고 있음.

이러한 특징은 이미지에서는 에지, 질감, 모서리 등과 같은 시각적인 패턴이 될 수 있음.

CNN에서는 여러 개의 필터를 사용하여 다양한 패턴을 동시에 추출하고,

이를 통해 더 복잡한 특징을 학습할 수 있음.


출력 크기를 동일하게 하기 위해 패딩(padding)을 사용하기도 함.
입력값 주위로 0을 넣어 입력값의 크기를 인위적으로 키워 결과값이 작아지는 것을 방지함.




▲ 컬러 이미지에는 2D 컨볼루션 연산을 사용함.

 


▲ 중요한 특징을 추출하고 차원을 축소하기 위해 풀링(Pooling) 연산을 사용
풀링:
  - 중요한 특징을 추출하고 차원을 축소하기 위해 사용하는 연산
  - MaxPool: MaxPool2D / AvgPool: AvgPool2D

 

18-5. 스트라이드(Stride)

컨볼루셔널 신경망(CNN)에서 연산을 수행할 때 필터가 얼마나 이동할지를 결정하는 매개변수.

필터가 입력 데이터를 한 번에 얼마나 이동하는지를 나타내는 값.


Stride의 값은 일반적으로 1보다 큰 양수로 설정됨.

Stride가 1인 경우 필터가 한 칸씩 이동하면서 입력 데이터를 순차적으로 조사함.

 

Stride가 1보다 큰 경우, 필터가 여러 칸을 건너뛰며 이동하므로 출력 데이터의 크기가 줄어들게 되며
이는 공간 해상도를 줄이는 효과를 가지게 됨.

이는 다양한 장점을 제공하는데 출력 데이터의 크기를 줄이는 것으로써 계산량을 감소시키고

패턴을 조금 더 건너뛰며 탐지할 수 있어 더 큰 컨텍스트를 고려할 수 있게 되며

파라미터 수를 줄이고 추상화 수준을 조절하는 데 도움을 줌.
필터를 적용하여 얻어낸 결과를 Feature map 또는 Activation map이라고 부름.

 

18-6. 드롭아웃 레이어 (Dropout Layer)

드롭아웃(Dropout)은 신경망에서 과적합(overfitting)을 줄이고 일반화 성능을 향상시키기 위해 

사용되는 정규화(regularization) 기법 중 하나로,

서로 연결된 연결망(layer)에서 0부터 1 사이의 확률로 뉴런을 제거(drop)하는 기법임.

 

예를 들어, 위의 그림과 같이 drop-out rate가 0.5라고 가정할 경우,

Drop-out 이전에 4개의 뉴런끼리 모두 연결되어 있는 전결합 계층(Fully Connected Layer)에서

4개의 뉴런 각각은 0.5의 확률로 제거될지 말지 랜덤하게 결정됨.

위의 예시에서는 2개가 제거된 것을 알 수 있음.

즉, 제거되는 뉴런의 종류와 개수는 오로지 랜덤하게 drop-out rate에 따라 결정됨.

 

Drop-out Rate는 하이퍼파라미터이며 일반적으로 0.5로 설정함.
제거된 뉴런은 해당 훈련 배치에 대한 역전파 과정에서 그래디언트를 전달받지 않고,

그래디언트 업데이트에 영향을 주지 않음.

이로 인해 다른 뉴런들은 제거된 뉴런의 기능을 보완하기 위해 더 강력한 표현력을 갖게됨.

드롭아웃은 신경망 내부의 강한 상호 의존성을 해결하고, 과적합을 줄여 모델의 일반화 성능을 향상시킴.

또한 앙상블 효과를 가지기도 함. 훈련 과정에서 여러 번 드롭아웃을 적용하면 각각 다른 네트워크가 생성되고, 이들을 앙상블하여 예측을 수행함. 이는 모델의 안정성을 높이고 오버피팅을 줄이는 효과를 가져옴.

드롭아웃은 일반적으로 훈련 시에만 적용되고, 테스트나 예측 단계에서는 비활성화되어야 함.

이렇게 함으로써 모델이 더 다양한 특징을 학습하고 일반화 성능을 향상시킬 수 있음.

 

18-7. FC Layer(Fully Connected Layer)

'완전히 연결 되었다라는 뜻'으로, 한층의 모든 뉴런이 다음층이 모든 뉴런과 연결된 상태인 레이어.

이미지를 분류 또는 설명하기 위해 예측하는 레이어이며 2차원의 배열 형태 이미지를 

1차원의 평탄화 작업을 통해 분류하는데 사용되는 계층.
 

예를 들어, 위의 이미지와 같이 고양이 이미지를 이용할 경우,

수염이나 털과 같은 것을 나타내는 형상은 "cat" 라벨에 대한 높은 확률을 가져야 함.

위 이미지는 입력 값이 뉴런의 첫 번째 층으로 어떻게 흘러 들어가는지를 보여줌.

미리 학습된 Weight에 의해 계산되어지고 인공신경망에서처럼 활성화 함수(일반적으로 ReLu)를 통과함.

그런 다음 그것들은 출력층으로 전달되는데, 그 안에서 모든 뉴런은 이미 정의된 분류 라벨을 나타냄.

 

18-8. CNN 구성 레이어

1) Conv2D: 주어진 입력 이미지에서 특징(feature)을 추출하는 역할.

2) ReLU: 활성화 함수. 비선형성을 도입하고, 모델이 더 복잡한 패턴과 비선형 관계를 학습함.
3) MaxPool2D: 공간적인 차원을 축소하는 역할.

1) Conv2D(반복): 주어진 입력 이미지에서 특징(feature)을 추출하는 역할.

2) ReLU(반복): 활성화 함수. 비선형성을 도입하고, 모델이 더 복잡한 패턴과 비선형 관계를 학습함.
3) MaxPool2D(반복): 공간적인 차원을 축소하는 역할.
 ....
4) Flatten: 다차원의 입력을 1차원으로 변경.FC Layer 입력 전처리단계.
5) Linear: 입력과 가중치 사이의 선형 연산을 수행.
6) ReLU(반복): 활성화 함수. 비선형성을 도입하고, 모델이 더 복잡한 패턴과 비선형 관계를 학습함.
 ...
7) Sigmoid(or Softmax): 마지막으로 모델의 출력을 확률로 해석하기 위해 사용.

 

 

18-9. CNN 모델 제작 실습

import torch
import torch.nn as nn
import torch.optim as optim

 

# 임의의 28*28 탠서를 생성하여 inputs라는 변수로 담음

inputs = torch.Tensor(1,1,28,28) # torch.Tensor( 배치크기 * 채널 * 높이 * 너비 )
print(inputs.shape)

 

 

# 합성곱 신경망의 첫 번째 합성곱 층 conv1을 정의

conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding='same')
# kernal_size=3 :3*3 size의 커널 사용 # padding=same : 입력과 동일한 크기의 padding 사용
out = conv1(inputs) # conv1에 inputs를 넣어 out으로 출력
print(out.shape)

 

 

# 최대 풀링 층 pool을 정의

pool = nn.MaxPool2d(kernel_size=2) # 2x2 크기의 풀링 영역을 사용
out = pool(out)
print(out.shape) # 1개의 샘플에 대해 32개의 채널을 가지는 14x14 크기의 출력

 

 

# 두 번째 합성곱 층 conv2를 정의

conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding='same')
out = conv2(out)
print(out.shape) # 1개의 샘플에 대해 64개의 채널을 가지는 14x14 크기의 출력

 

 

# 2D 최대 풀링 층 pool을 정의

pool = nn.MaxPool2d(kernel_size=2)
out = pool(out)
print(out.shape) # 1개의 샘플에 대해 64개의 채널을 가지는 7x7 크기의 출력

 

 

# 2D 텐서를 1D로 평탄화(flatten)

flatten = nn.Flatten()
out = flatten(out)
print(out.shape) # 1개의 샘플에 대해 3136개(64*7*7=3136)의 요소를 가진 1차원 벡터로 출력
# 평탄화를 통해 2D 텐서가 1D 벡터로 최대 풀링 이후의 출력인 (1, 64, 7, 7)가 (1, 3136)로 변환
# 1차원으로 변경해야 nn.Linear() 레이어에 삽입 가능

 

 

# 선형 레이어(Linear layer)를 사용하여 분류를 수행

fc = nn.Linear(3136, 10)
# 이전 평탄화 층 이후의 출력 크기가 3136이므로, fc의 입력 크기는 3136
# 10개의 클래스로 분류를 수행하고자 하므로, fc의 출력 크기는 10
out = fc(out)
print(out.shape)

 

 

18-10. MNIST 데이터셋 분류

import torch
import torchvision.datasets as datasets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader

 

# 학습의 신속성을 위해 device를 GPU로 변경

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(device) # 실행 후 런타임에서 변경 후 재시작

 

 

# 데이터셋 안의 데이터 다운로드

# train_data 다운로드
train_data = datasets.MNIST(
    root='data',
    train=True, # 변수로 train data가 출력
    transform = transforms.ToTensor(), # transforms가 Tensor로 변경되어 출력.
    download=True
)

# test_data 다운로드
test_data = datasets.MNIST(
    root='data',
    train=False, # 변수로 test tata가 출력
    transform = transforms.ToTensor(), # transforms가 Tensor로 변경되어 출력.
    download=True
)

 

# 출력

print(train_data)
print(test_data)

 

 

#  데이터 로딩을 위해 DataLoader를 사용하는 예시

loader = DataLoader(
    dataset = train_data,
    batch_size = 64,
    shuffle=True
)
# loader 객체를 사용하여 데이터를 미니배치 단위로 반복하면서 모델에 입력으로 제공이 가능
# 이를 통해 모델은 미니배치 단위로 데이터를 학습하거나 예측 가능

 

# 시각화

imgs, labels = next(iter(loader)) # iterator형식으로 생성
fig, axes=plt.subplots(8, 8, figsize=(16,16))
for ax, img, label in zip(axes.flatten(), imgs, labels):
  ax.imshow(img.reshape((28,28)), cmap='gray')
  ax.set_title(label.item())
  ax.axis('off')

 

 

# model 생성

model = nn.Sequential(
    nn.Conv2d(1,32, kernel_size=3, padding='same'),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=2),

    nn.Conv2d(32,64,kernel_size=3, padding='same'),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=2),

    nn.Flatten(),
    nn.Linear(7*7*64, 10) # 28*28에서 2번의 2*2 풀링 영역을 사용하여 7*7이 된 상태
).to(device)

print(model)

 

 

# 학습

optimizer = optim.Adam(model.parameters(), lr=0.001)

epochs = 10

for epoch in range(epochs):
  sum_losses = 10
  sum_accs = 0

  for x_batch, y_batch in loader:
    x_batch = x_batch.to(device)
    y_batch = y_batch.to(device)

    y_pred = model(x_batch)
    loss = nn.CrossEntropyLoss()(y_pred, y_batch) # loss를 뽑는 함수.
    optimizer.zero_grad()
    loss.backward() # 역전파
    optimizer.step() # 기울기

    # 배치단위 loss 저장
    sum_losses = sum_losses + loss.item()
    # 배치단위 정확도 저장
    y_prob = nn.Softmax(1)(y_pred)
    y_pred_index = torch.argmax(y_prob, axis=1)
    acc = (y_batch == y_pred_index).float().sum() / len(y_batch) * 100 # 백분율
    sum_accs = sum_accs + acc.item()
  avg_loss = sum_losses / len(loader)
  avg_acc = sum_accs / len(loader)

  print(f'Epoch {epoch+1:4d}/{epochs} Loss:{avg_loss:6f} Accuracy:{avg_acc:.2f}%')

 

 

모델의 training 이후, test_data에 대한 성능을 평가하기 위해 test_loaer라는 DataLoader를 정의

 

# test loader 정의

test_loader = DataLoader(
    dataset=test_data,
    batch_size=64,
    shuffle=True
)

 

test_data의 시각화

 

# 시각화
imgs, labels = next(iter(test_loader)) # iterator형식으로 생성
fig, axes=plt.subplots(8, 8, figsize=(16,16))
for ax, img, label in zip(axes.flatten(), imgs, labels):
  ax.imshow(img.reshape((28,28)), cmap='gray')
  ax.set_title(label.item())
  ax.axis('off')

 

 

 # 모델을 테스트모드로 전환
 
model.eval()


모델을 테스트모드로 전환하는 이유: 배치 정규화와 드롭아웃의 비활성화로 일관된 예측결과의 획득이 필요

 

# 테스트 데이터셋에 대한 정확도를 계산하는 예시

sum_accs = 0

for x_batch, y_batch in test_loader:
  x_batch = x_batch.to(device)
  y_batch = y_batch.to(device)

  y_pred = model(x_batch)
  
  # 배치단위 정확도 저장
  y_prob = nn.Softmax(1)(y_pred)
  y_pred_index = torch.argmax(y_prob, axis=1)
  acc = (y_batch == y_pred_index).float().sum() / len(y_batch) * 100 # 백분율

  sum_accs = sum_accs + acc

avg_acc = sum_accs / len(test_loader)

print(f'테스트 정확도는 {avg_acc:2f}% 입니다.')