AI tech

밑바닥부터 시작하는 딥러닝 정리 Chapter 7 - 합성곱 신경망(CNN)

도리컴 2024. 2. 7. 02:18
반응형

Chapter 7 - 합성곱 신경망(CNN)

개요

  • CNN 메커니즘 자세히 설명하고 파이썬으로 구현할 예정
  • 사용 : 이미지/음성 인식

7.1 전체 구조

  • CNN도 지금까지 본 신경망과 같이 레고처럼 계층을 조합해서 만들 수 있음
  • 새로운 등장! → 합성곱 계층, 풀링 계층
  • 인접 계층의 모든 뉴런과 연결된 경우 → fully connected → 완전 연결된 계층을 Affine 계층이라는 이름으로 구현했음
  • 기존 Affine 계층

 

  • 그럼 CNN 구조는 어떻게 다르지?

  • 합성곱 계층과 풀링 계층이 추가됨
  • Conv - ReLU - (Pooling) 순으로 연결(풀링 계층은 생략하기도 함)
  • 또 다른 특징 : Affine-ReLU를 출력에 가까운 층에서 사용 가능 + 마지막에는 Affine-Softmax 사용 (일반적인 CNN에서 흔히 볼 수 있는 구성)

7.2 합성곱 계층

  • padding, stride 등장(CNN 고유의 용어)
  • 합성곱 계층의 구조를 살펴볼 예정

완전연결 계층(Affine)의 문제점

  • 데이터의 형상이 무시된다는 문제가 있음
  • 이미지는 통상 가로, 세로, 채널(색상)로 구성된 3차원 데이터임 → 이걸 Affine 계층에 입력 시 1차원 데이터로 평탄화를 해야 함
  • 3차원 형상에는 공간적 정보가 담겨 있는데(가까운 픽셀은 비슷하다는 등, 3차원 속에서 의미를 갖는 패턴들) → 모든 입력을 동등한 뉴런으로 취급하면 이 정보를 못살림

합성곱 계층은?

  • 형상을 유지한다~ → 이미지도 3차원 데이터로 입력 받고, 다음 계층에도 3차원으로 전달
  • 형상을 가진 데이터(like 이미지) 를 제대로 이해할 가능성이 있다!
  • 합성곱 계층의 입출력 데이터 → feature map이라고 부름 / ex: input feature map

합성곱 연산

  • 이미지 처리에서 말하는 그 유명한 필터 연산에 해당함!
    • 입력은? (4, 4) / 출력은? (2, 2) / 필터는? (3, ,3) → 필터를 ‘커널’이라고도 부름

 

  • 필터의 ‘윈도우’를 일정 간격으로 이동해가며 입력 데이터에 적용
  • 윈도우 무엇? - 그림에서 회색 부분 → 필터 크기이고, 한 칸씩 이동하면서 계산을 진행

  • 어떤 계산? → 원소끼리 곱한 후 총합(fused mutiply-add → FMA) → 그 결과를 해당 장소에 저장
  • 이렇게 모든 장소에서 수행 → 합성곱 연산의 출력 완성~

 

  • 핵심 : 요 필터의 매개변수가 CNN에서의 가중치에 해당하는 겁니다.
  • 편향도 존재(항상 하나만 존재, 그림 참고)

패딩

  • 입력값 주변을 0으로 채움(보통 폭 단위로 해결)
  • 왜? 왜 채워? - 출력 크기를 조정하기 위한 목적이 있음
    • 합성곱 연산을 몇 번이나 되풀이하는 심층 신경망? → 크기가 작아지면 문제가 됨
    • 즉, 공간적 크기를 고정한 채로 다음 계층에 전달할 수 있다~

  •  (4, 4) 입력 데이터에 패딩 1 추가해서 (6, 6)이 됨(패딩 원하는 값으로 설정 가능)

 

스트라이드

  • 정의 : 필터를 적용하는 위치의 간격(필터를 얼만 큼 띄엄띄엄 할 건지 → 지금까지 1이었음)
  • 2로 하면, 윈도우가 두 칸 씩 이동

 

패딩, 스트라이드, 출력 크기 계산?

  • 수식을 외운다기 보다 직관적으로 이해를 하면 좋겠음주의 : 각 값이 정수로 나누어 떨어져야 함

 

3차원 데이터의 합성곱 연산

  • 핵심 : 필터가 3차원이 되었다~ / 채널 방향으로 feature map이 늘어남
  • 주의 : 입력과 필터의 채널 수가 같아야 한다~
  • 필터 자체의 크기는 원하는 값으로 설정 가능하나, 모든 채널의 필터는 같은 크기여야 함

블록으로 생각하기

  • 여기서, 합성곱 연산의 출력으로 다수의 채널을 내보내려면? → 필터(가중치)를 여러 개 쓰자!
  • 필터를 a 쓰면, 출력 맵도 a개 생성
  • 편향도 2차원과 마찬가지로 쓰임~

  • 합성곱 연산에서는 필터의 수도 고려해야 하기에, 필터의 가중치 데이터는 4차원 데이터임
  • 무슨 말? → (출력 채널 수, 입력 채널 수, 높이, 너비) 순으로 나타냄

 

배치 처리

  • 합성곱 연산도 신경망 처리 처럼 배치 처리를 지원하고자 함
  • 각 계층의 데이터 차원을 하나 늘려서 4차원으로 저장 → (데이터 수, 채널 수, 높이, 너비) 순주의 : 신경망에 4차원 데이터 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이루어짐(N회 분의 처리를 한 번에 수행)

7.3 풀링 계층(Pooling)

  • 풀링? - 세로, 가로 방향의 공간을 줄이는 연산
  • 최대 풀링, 평균 풀링 등이 있음
    • 최대 풀링 : 대상 영역의 최댓값을 구함 (평균 풀링은 평균)
  • 풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정하는 것이 보통임(겹치지 않게 하려고)

풀링 계층의 특징

  • 학습해야 할 매개변수가 없음
    • 영역의 최댓값/평균 구하는 것이므로, 학습할 게 없음
  • 채널 수 불변
    • 채널마다 독립적으로 계산하므로, 채널 수 변화 x
  • 입력의 변화에 영향이 적다
    • 입력 데이터가 바뀌어도 풀링의 결과는 잘 안변함

7.4 합성곱/풀링 계층 구현

4차원 데이터 → 배열로 생성

x = np.random.rand(10, 1, 28 ,28) # 무작위로 데이터 생성
x.shape
# (10, 1, 28, 28)

    x[0].shape # (1, 28, 28) # x[0], x[1] 이렇게 데이터 접근

x[0, 0]

im2col로 전개하기(image to column)

  • 이게 뭔데 : 합성곱 연산의 문제를 단순하게 만들어주는 함수
  • → 입력 데이터를 필터링(가중치 계산)하기 좋게 펼치는 함수
    • 펼치면 뭐해? → 그냥 행렬곱 연산으로 3차원 데이터 합성곱 연산 계산할 수 있는거
  • for문 쓰면 복잡해지므로 im2col 등장

  • 실제로는 필터 적용 영역이 겹치게 되는 경우가 대부분임 / 하지만, 컴퓨터는 큰 행렬곱에 속도가 탁월하기 때문에 빠르게 계산 가능 → 효율 상승
  • 입력데이터 전개 끝나면? → 필터(가중치)도 1열로 전개하고, 두 행렬곱 계산하면 됨 → 완전 연결 계층의 Affine 계층에서 한 것과 거의 동일

- 위 그림의 출력데이터는 2차원 행렬인데, CNN은 데이터를 4차원 배열로 저장함 → reshape까지 해주면, 합성곱 게층 구현 흐름 끝!

합성곱 계층 구현하기

  • 코드로 구현 / im2col 함수를 책에서 만들어서 제공해줌→ 필터 크기, 스트라이드, 패딩을 고려하여 입력 데이터를 2차원 배열로 전개

  • import sys, os sys.path.append(os.pardir) from common.util import im2col x1 = np.random.rand(1, 3, 7, 7) # (데이터 수, 채널 수, 높이, 너비) col1 = im2col(x1, 5, 5, stride=1, pad=0) print(col1.shape) # (9, 75) x2 = np.random.rand(10, 3, 7, 7) # 데이터 10개 col2 = im2col(x2, 5, 5, stride=1, pad=0) print(col2.shape) # (90, 75)
  • 첫번째 : 배치 1 / 두번째 : 배치 10
  • 합성곱 계층 클래스
  • class Convolution: def __init__(self, W, b, stride=1, pad=0): self.W = W self.b = b self.stride = stride self.pad = pad def forward(self, x): FN, C, FH, FW = self.W.shape N, C, H, W = x.shape out_h = int(1 + (H + 2*self.pad - FH) / self.stride) out_w = int(1 + (W = 2*self.pad - FW) / self.srtride) col = im2col(x, FH, FW, self.stride, self.pad) col_W = self.W.reshape(FN, -1).T # 필터 전개 / reshape에서 -1은 원소 수가 똑같이 유지되도록 자동설정 out = np.dot(col, col_W) + self.b out = out.reshape(N, out_h, out_w, -1).transpose(0, 1, 3, 2) # 적절한 형상으로 바꿈 return out
  • 초기화 시 인수 : 필터(가중치), 편향, 스트라이드, 패딩
  • 하는 짓 : 입력 데이터를 im2col로 전개 + 필터 전개(reshape사용) → 두 개 행렬곱 구함
  • transpose로 형상 변경하기 - 아래 그림 참조

  • 합성곱 계층의 역전파는 어떻게?
    • Affine계층과 똑같다
    • 다만, im2col을 역으로 처리하려면 col2im을 쓰기(책 코드 찾아보면 나옴)

풀링 계층 구현하기

  • 어떻게? - 합성곱 계층처럼 im2col 써서 입력 데이터 전개
  • 단, 합성곱 계층과 달리, 채널 쪽이 독립적이다!
  • 각 부분을 행으로 나열 후, 행 별 최댓값을 적절한 형상으로 변환(성형?)하면 됨(그림)

  • 코드
    • 3 단계 알면 끝
      1. 입력데이터 전개
      2. 행 별 최댓값 구하기
      3. 적절한 모양으로 성형하기
  • class Pooling: def __init__(self, pool_h, pool_w, stride=1, pad=0): self.pool_h = pool_h self.pool_w = pool_w self.stride = stride self.pad = pad def forward(self, x): N, C, H, W = x.shape out_h = int(1 + (H - self.pool_h) / self.stride) out_w = int(1 + (W - self.pool_w) / self.stride) # 전개 (1단계) col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) col = col.reshape(-1, self.pool_h * self.pool_w) # 최댓값 (2단계) out = np.max(col, axix=1) # axis=1 : 입력 x의 1번째 차원의 축마다 최댓값 구함 # 성형 (3단계) out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2) return out

7.5 CNN 구현하기

  • 손글씨 숫자 인식 CNN 조립할 예정(SimpleConvNet)

  • hparams가 딕셔너리로 주어짐을 알아야 함
  • 코드(초기화 부터)
  • class SimpleConvNet: def __init__(self, input_dim=(1, 28, 28), conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size=100, output_size=10, weight_init_std=0.01): # 1. hparams 꺼내서 합성곱 계층의 출력 크기 계산 filter_num = conv_param['filter_num'] filter_size = conv_parmam['filter_size'] filter_pad = conv_parmam['pad'] filter_stride = conv_parmam['stride'] input_size = input_dim[1] conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1 pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
        # 2. 가중치 매개변수 초기화 - 각 완전연결 계층의 가중치와 편향 저장
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0],
                                                                                                                    filter_size, filter_size)
        self.params['b1'] = np.zeros(filter_num)
        self.params['W2'] = weight_init_std * np.random.randn(pool_outout_size, hidden_size)
        self.params['b2'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b3'] = np.zeros(output_size)


        # 3. CNN 구성 계층 생성
        self.layers = OrderedDict()
        self.layers['Conv1'] = Convolution(self.params['W1'],
                                                                            self.params['b1'],
                                                                            conv_param['stride'],
                                                                            conv_param['pad'])
        self.layers['Relu1'] = Relu()
        self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
        self.layers['Affine1'] = Affine(self.params['W2'],
                                                                        self.params['b2'])
        self.layers['Relu2'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W3'],
                                                                        self.params['b3'])

        self.last_layer = SoftmaxWithLoss()
```
  • 추론 및 손실 함수 값 구하기(predict, loss)
  • def predict(self, x): for layer in self.layers.values(): x = layer.forward() # layers의 계층을 맨 앞에서부터 차례로 forward 후 결과 전달 return x def loss(self, x, t): y = self.predict(x) return self.last_layer.forward(y, t) # 마지막 층의 forward 호출해서 처리
  • x는 입력 데이터, t는 정답 레이블
  • 기울기 구하기(오차역전파법)
    • 순전파와 역전파를 반복함 - 적절한 순서로 호출만 해주면 됨
    • grads(딕셔너리 변수)에 각 가중치 매개변수 기울기 저장~ → 끝
  • def gradient(self, x, t): # 순전파 self.loss(x, t) # 역전파 dout = 1 dout = self.last_layer.backward(dout) layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) # 결과 저장 grads = {} grads['W1'] = self.layers['Conv1'].dW grads['b1'] = self.layers['Conv1'].db grads['W2'] = self.layers['Affine1'].dW grads['b2'] = self.layers['Affine1'].db grads['W3'] = self.layers['Affine2'].dW grads['b3'] = self.layers['Affine2'].db return grads
  • SimpleConvNet으로 MNIST 데이터셋 학습해보면? (train_convnet.py 코드 참조)
    • 정확도는 - 훈련데이터 99.82%, 시험데이터 98.96%
    • 작은 네트워크로서 훌륭한 성능이라 할 수 있다~
    • 다음 장에서 계층을 더 깊게 해볼 예정(정확도 향상)

결론

  • 합성곱 계층, 풀링 계층은 이미지 인식에 필수적인 모듈이다~
  • 이미지라는 공간적인 형상에 담긴 특징을 CNN이 잘 파악해서 높은 정확도를 보여준다~

7.6 CNN 시각화하기

  • CNN은 이미지 데이터에서 ‘무엇을 보고 있는’ 것일까?

1번째 층의 가중치 시각화

  • 합성곱 계층 필터를 이미지로 나타내보자!
  • 학습 전과 후 가중치 비교(아래 그림 참조)
    • 학습 전 : 무작위 초기화 - 규칙성 없음
    • 학습 후 : 뭔가 덩어리(블롭)가 진 것(?) 같은 규칙성이 생겼다~

  • 오른쪽 ‘학습 후’ 처럼 규칙성 있는 필터는 ‘무엇을 보고 있는 걸까?
  • → 바로 에지(색상이 바뀐 경계선)와 블롭(국소적으로 덩어리진 영역)을 봄

  • 결론 : 합성곱 계층의 필터는 에지나 블롭 등의 원시적 정보를 추출할 수 있다~
  • → 이런 원시적인 정보가 CNN의 뒷단 계층에 전달됨

층 깊이에 따른 추출 정보 변화

  • 1번째 합성곱 계층에선 에지나 블롭 등의 저수준 정보가 추출됨
  • 겹겹이 쌓인 CNN 각 계층에선 어떤 정보가 추출될까?
    • 더 깊어질수록, 추출 정보(강하게 반응하는 뉴런)는 더 추상화 된다고 함
  • AlexNet에 대해 - 일반 사물 인식(자동차나 개)을 수행한 8층 CNN
    • 합성곱 계층, 풀링 계층을 여러 겹 쌓고, 마지막에 완전연결 계층 거쳐 결과 출력
      • 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 ‘고급’정보로 변화함
      • → 즉, 사물의 ‘의미’를 이해하도록 변화하는 것!

7.7 대표적인 CNN

  • 다양한 CNN 네트워크 중 특히 중요한 네트워크 두 개
  • CNN의 원조인 LeNet
  • 딥러닝이 주목받도록 이끈 AlexNet

LeNet

  • 손글씨 숫자 인식하는 네트워크(1998년 제안)
  • 단순히 합성곱 계층과 풀링 계층을 반복, 마지막엔 완전연결 계층 거쳐 결과 출력
  • 현재의 CNN과 비교하면?
    • 활성화 함수 - LeNet은 시그모이드, 현재는 주로 ReLU
    • LeNet은 서브샘플링으로 중간 데이터 크기를 줄이지만, 현재는 최대 풀링이 주류임
    • LeNet이 20년 전 꺼인데, 현재의 CNN과 큰 차이가 없다는 게 놀랍다고 말함!

AlexNet

  • 딥러닝 열풍을 일으키는 데 큰 역할(2012년 발표)
  • 구성은 기본적으로 LeNet과 비슷
  • LeNet에서 다음과 같은 변화를 줌
    • 활성화 함수로 ReLU
    • LRN(Local Response Normalization)이라는 국소적 정규화를 실시하는 계층 이용
    • 드롭아웃 사용
  • 네트워크 구성 면에서는 LeNet과 AlexNet에 큰 차이는 없음
  • → 이를 둘러싼 환경과 컴퓨팅 기술이 큰 진보를 이룬 것!(병렬 GPU, 빅데이터 등)

7.8 정리

  • CNN - 기본 구성 모듈 : ‘합성곱 계층’, ‘풀링 계층’
  • 이 두 계층을 구현 수준으로 이해할 수 있도록 설명함
  • CNN은 이미지를 다루는 분야에서는 거의 예외 없이 쓰인다~

반응형