AI

[Deep Learning from Scratch] CHAPTER 7 합성곱 신경망(CNN)

위시리 2023. 3. 9. 21:35

합성곱 신경망(convolutional neural network, CNN)

- 이미지 인식, 음성 인식 등 다양한 곳에서 사용 (특히 이미지 인식분야에서 딥러닝을 활용한 기법은 거의 다 CNN을 기초로 함)

 

CNN의 매커니즘 설정 및 파이썬 구현

 

7.1 전체 구조

CNN 네트워크 구조

신경망과 같이 레고 블록처럼 계층을 조합하여 만들 수 있음

다만, 합성곱 계층(convolutional layer)과 풀링 계층(pooling layer)가 새롭게 등장

>> 계층들을 어떻게 조합하여 CNN을 만드는지

 

신경망은 인접하는 계층의 모든 뉴런과 결합 → 완전 결합(fully-connected, 전결합)

완전히 연결된 계층을 Affine 계층이라는 이름으로 구현

 

Affine 계층을 사용하면 예를 들어 아래와 같이 5층의 완전 연결 신경망을 구현할 수 있음

완전연결 계층(Affine 계층)으로 이뤄진 네트워크 예

 

위 그림과 같이 완전연결 신경망은 Affine 계층 뒤에 활성화 함수 ReLU 계층(or Sigmoid 계층)이 이어짐

위 그림에서는 Affine-ReLU 조합이 4개가 쌓였고,

마지막 5번째 계층은 Softmax 계층에서 최종 결과(확률) 출력

 

그렇다면 CNN의 구조는?

CNN으로 이뤄진 네트워크 예 : 합성곱 계층과 풀링 계층이 새로 추가(회색)

 

CNN에서는 새로운 '합성곱 계층(Conv)'과 '풀링 계층(Pooling)'이 추가

CNN의 계층은 'Conv-ReLU-(Pooling)'흐름으로 연결(풀링 계층은 생략하기도 함)

 

'Affine-ReLU' 연결이 'Conv-ReLU-(Pooling)'으로 바뀜

 

CNN에서 주목할 또 다른 점 : 출력에 가까운 층에서는 지금까지의 'Affine-ReLU'구성을 사용할 수 있다는 점

또 마지막 출력 계층에서는 'Affine-Softmax' 조합 그대로 사용

(일반적인 CNN에서 흔히 볼 수 있는 구성)

 


7.2 합성곱 계층

CNN에서는 패딩(padding), 스트라이드(stride) 등 고유의 용어 등장

또, 각 계층 사이에는 3차원 데이터같이 입체적인 데이터가 흐른다는 점에서 완전연결 신경망과 차이점이 있음

 

CNN에서 사용하는 합성곱 계층의 구조 살펴보기

 

7.2.1 완전연결 계층의 문제점

완전연결 신경망에서는 완전연결 계층(Affine 계층)을 사용

완전연결 계층에서는 인접하는 계층의 뉴런이 모두 연결되고 출력의 수 임의로 결정 가능

 

완전연결 계층의 문제점 : '데이터의 형상이 무시'됨

ex. 입력 데이터가 이미지인 경우

  이미지는 통상 세로·가로·채널(색상)로 구성된 3차원 데이터

그러나 완전연결 계층에 입력할 때는 3차원 데이터를 평평한 1차원 데이터로 평탄화 해줘야함

 

MNIST 데이터셋을 이용한 사례에서 형상이 (1,28,28)인 이미지(1채널, 세로 28픽셀, 가로 28픽셀)를 1줄로 세운 784개의 데이터를 첫 Affine 계층에 입력

 

이미지는 3차원 형상이며, 이 형상에는 공간적 정보가 담겨 있음

예를 들어 공간적으로 가까운 픽셀은 값이 비슷하거나, RGB의 각 채녈은 서로 밀접하게 관련되어 있거나, 거리가 먼 픽셀끼리는 별 연관이 없는 등, 3차원 속에서 의미를 갖는 본질적인 패턴이 숨어 있을 것

그러나 완전연결 계층은 형상을 무시하고 모든 입력 데이터를 동등한 뉴런(같은 차원의 뉴런)으로 취급하여 형상에 담긴 정보를 살릴 수 없음

 

한편, 합성곱 계층은 형상 유지

이미지도 3차원 데이터로 입력 받음

다음 계층에도 3차원 데이터로 전달

>> CNN에서는 이미지처럼 형상을 가진 데이터를 제대로 이해할 (가능성이 있는) 것

 

CNN에서는 합성곱 계층의 입출력 데이터를 특징 맵(feature map)이라고 함

합성곱 계층의 입력 데이터를 입력 특징 맵(input feature map), 출력 데이터를 출력 특징 맵(output feature map)이라 함

 

('입출력 데이터' & '특징 맵' 같은 의미로 사용)

 

7.2.2 합성곱 연산

합성곱 계층에서의 합성곱 연산 처리

합성곱 연산 : 이미지 처리에서 말하는 필터 연산에 해당

 

합성곱 연산은 입력 데이터에 필터로 적용

위 그림 7-3에서 입력 데이터는 가로·세로 방향의 형상을 가졌고,

필터 역시 가로·세로 방향의 차원을 가짐

 

데이터와 필터의 형상을 (높이(height), 너비(width))로 표기

 

그림 7-3에서 입력은 (4,4), 필터는 (3,3), 출력은 (2,2)가 됨

(문헌에 따라 필터 = 커널)

 

위 그림의 합성곱 연산 예에서 어떻게 계산이 이루어 지는가

합성곱 연산의 계산 순서

 

합성곱 연산은 필터의 윈도우(window)를 일정 간격으로 이동해가며 입력 데이터에 적용

- 윈도우 : 회색 3x3

입력과 필터에서 대응하는 원소끼리 곱한 후 총합을 구함 → 단일 곱셈-누산(fused multiply-add, FMA)

(첫번째 : 1*2 + 2*0 + 3*1 + 0*0 + 1*1 + 2*2 + 3*1 + 0*0 + 1*2 = 15)

그리고 그 결과를 해당 장소에 저장

이 과정을 모든 장소에서 수행하면 합성곱 연산의 출력 완성

 

완전연결 신경망에는 가중치 매개변수와 편향이 존재하는데,

CNN에서는 필터의 매개변수가 '가중치'에 해당

& CNN에도 편향 존재

 

위 그림 7-3은 필터를 적용하는 단계까지만 보여준 것

아래 그림은 편향을 포함하여 나타낸 흐름

 

합성곱 연산의 편향 : 필터를 적용한 원소에 고정값(편향)을 더함

 

편향은 필터를 적용한 후 데이터에 더해짐

그리고 편향은 항상 하나(1x1)만 존재

하나의 편향 값을 필터를 적용한 모든 원소에 더함

 

7.2.3 패딩

패딩(padding) : 합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값으로 채움

(합성곱 연산에서 자주 사용하는 기법)

 

합성곱 연산의 패딩 처리 : 입력 데이터 주위에 0을 채운다(패딩은 점선으로 표시했으며 그 안의 값 '0'은 생략)

 

위 그림과 같이 처음에 크기가 (4,4)인 입력 데이터에 패딩이 추가되어 (6,6)이 됨

이 입력에 (3,3) 크기의 필터를 걸면 (4,4) 크기의 출력 데이터 생성

(패딩은 원하는 정수로 설정 가능)

→ 만약 패딩을 2로 설정하면 입력 데이터의 크기는 (8,8)이 되고 3으로 설정하면 (10,10)이 됨

 

** 패딩은 주로 출력 크기를 조정할 목적으로 사용

예를 들어 (4,4) 입력 데이터에 (3,3) 필터를 적용하면 출력은 (2,2)가 되어, 입력보다 2만큼 줄어듦

이는 합성곱 연산을 몇 번이나 되풀이하는 심층 신경망에서는 문제가 될 수 있음

합성곱 연산을 거칠 때마다 크기가 작아지면 어느 시점에서는 출력 크기가 1이 되어버림

더 이상은 합성곱 연산을 적용 못한다는 뜻

이러한 사태를 막기 위해 패딩 사용

앞의 예에서 패딩의 폭을 1로 설정하니 (4,4) 입력에 대한 출력이 같은 크기인 (4,4)로 유지

>> 입력 데이터의 공간적 크기를 고정한 채로 다음 계층으로 전달 가능

 

7.2.4 스트라이드

스트라이드(stride) : 필터를 적용하는 위치의 간격

위의 예에서는 stride = 1

stride = 2로 설정하면

스트라이드가 2인 합성곱 연산

필터를 적용하는 윈도우가 두 칸씩 이동

 

위 그림과 같이 크기가 (7,7)인 입력 데이터에 스트라이드를 2로 설정한 필터를 적용

>> 스트라이드는 필터를 적용하는 간격 지정

 

그런데 스트라이드를 2로 하니 출력은 (3,3)이 됨

이처럼 스트라이드를 키우면 출력 크기는 작아짐

한편, 패딩을 크게 하면 출력 크기가 커짐

 

이러한 관계를 아래와 같이 수식화 할 수 있음 (패딩, 스트라이드, 출력 크기 계산)

입력 크기 : (H,W), 필터 크기 : (FH, FW), 출력 크기 : (OH, OW), 패딩 : P, 스트라이드 : S

 

출력 크기 : 

이때 각 식의 결과가 정수로 나눠떨어지는 값이여야 함

출력 크기가 정수가 아니면 오류를 내는 등의 대응 필요

+) 딥러닝 프레임워크 중 값이 딱 나눠떨어지지 않을 때는 가장 가까운 정수로 반올림하는 등, 특별히 에러를 내지 않고 진행하도록 구현하는 경우도 있음

 

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

위의 예 들은 2차원 형상을 다루는 합성곱 연산

BUT 이미지만 해도 세로x가로로 더해서 채널까지 고려한 3차원 데이터

 

채널까지 고려한 3차원 데이터를 다루는 합성곱 연산 살펴볼 것

 

 

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

아래는 계산 순서

 

2차원일 때와 비교하면, 길이 방향(채널 방향)으로 특징 맵이 늘어남

채널 쪽으로 특징 맵이 여러 개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 

그 결과를 더해서 하나의 출력을 얻음

 

3차원의 합성곱 연산에서 주의할 점 : 입력 데이터의 채널 수와 필터의 채널 수가 같아야 함

(위 예는 3개로 일치)

 

한편, 필터 자체의 크기는 원하는 값으로 설정 가능

(단, 모든 채널의 필터가 같은 크기여야 함)

위 예에서 필터의 크기가 (3,3)이지만, 원하면 (2,2)나 (1,1) 또는 (5,5)으로도 설정 가능

 

7.2.6 블록으로 생각하기

3차원의 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉬움

블록은 아래와 같은 3차원 직육면체

& 3차원 데이터를 다차원 배열로 나타낼 때 : 채널(channel), 높이(height), 너비(width) 순서로 씀 (C,H,W)

필터도 같은 순서로 씀 → 채널 수 C, 필터 높이 FH(Filter Height), 필터 너비(Filter Width)의 경우 (C, FH, FW)로 씀

합성곱 연산을 직육면체 블록으로 생각

위 예에서의 출력 데이터는 한 장의 특징 맵

한 장의 특징 맵 = 채널이 1개인 특징 맵

→ 합성곱 연산의 출력으로 다수의 채널을 내보내려면 어떻게 해야 하는가??

    A) 필터(가중치)를 다수로 사용

여러 필터를 사용한 합성곱 연산의 예

 

위 그림과 같이 필터를 FN개 적용하면 출력 맵도 FN개 생성

FN개의 을 모았을 때 형상 : (FN,OH,OW)인 블록 완성

이 완성된 블록을 다음 계층으로 넘기겠다는 것이 CNN의 처리 흐름

 

이상에서 보듯 합성곱 연산에서는 필터의 수도 고려해야 함

그런 이유로 필터의 가중치 데이터는 4차원 데이터이며 (출력 채널 수, 입력 채널 수, 높이, 너비) 순으로 씀

ex. 채널 수3, 크기 5x5 인 필터 20개 → (20, 3, 5, 5)

 

합성곱 연산에도 (완전연결 계층과 마찬가지로) 편향이 쓰임

합성곱 연산의 처리 흐름(편향 추가)

편향은 채널 하나에 값 하나씩 구성

>> 편향의 형상 : (FN, 1, 1)이고,

필터의 출력 결과의 형상 : (FN, OH, OW)

 

이 두 블록을 더하면 편향의 각 값이 필터의 출력인 (FN, OH, OW) 블록의 대응 채널의 원소 모두에 더해짐

참고로, 형상이 다른 블록의 덧셈은 넘파이 브로드캐스트 기능으로 쉽게 구현 가능

 

7.2.7 배치 처리

신경망 처리에서는 입력 데이터를 한 덩어리로 묶어 배치로 처리

완전연결 신경망을 구현하면서는 이 방식을 지원하여 처리 효율을 높이고, 미니배치 방식의 학습도 지원

 

합성곱 연산도 마찬가지로 배치 처리를 지원하고자 함

그래서 각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장

구체적으로는 데이터를 (데이터 수, 채널 수, 높이, 너비) 순으로 저장

 

합성곱 연산의 처리 흐름(배치 처리)

 

위는 데이터가 N개 일 때 배치처리했을 때의 데이터 형태

 

배치 처리 시의 데이터 흐름을 나타낸 위 그림을 보면 각 데이터의 선두에 배치용 차원 추가

이처럼 데이터는 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄짐

즉, N회 분의 처리를 한 번에 수행

 


7.3 풀링 계층

풀링 : 세로·가로 방향의 공간을 줄이는 연산

 

아래 그림과 같이 2x2 영역을 원소 하나로 집약하여 공간의 크기를 줄임

최대 풀링의 처리 순서

위 그림은 2x2 최대 풀링(max pooling, 맥스 풀링)을 스트라이드 2로 처리하는 순서

최대 풀링은 최댓값 max를 구하는 연산으로, '2x2'는 대상 영역의 크기를 뜻함

즉, 2x2 최대 풀링은 그림과 같이 2x2 크기의 영역에서 가장 큰 원소 하나를 꺼냄

또, 스트라이드는 위 예에서 2로 설정했으므로 2x2 윈도우가 원소 2칸 간격으로 이동

참고로, 풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정하는 것이 보통

ex. 윈도우가 3x3 → 스트라이드 : 3

 

** 풀링은 최대 풀링 외에도 평균 풀링(average pooling) 등이 있음

최대 풀링은 대상 영역에서 최댓값을 취하는 연산인 반면, 평균 풀링은 대상 영역의 평균을 계산

이미지 인식 분야에서는 주로 최대 풀링 사용

 

7.3.1 풀링 계층 특징

  • 학습해야 할 매개변수가 없음

풀링 계층은 합성곱 계층과 달리 학습해야 할 매개변수가 없음

풀링은 대상 영역에서 최댓값이나 평균을 취하는 명확한 처리이므로 특별히 학습할 것이 없음

 

  • 채널 수가 변하지 않음

풀링 연산은 입력 데이터의 채널 수 그대로 출력 데이터로 보냄

아래 처럼 채널마다 독립적으로 계산하기 때문

 

  • 입력의 변화에 영향을 적게 받음(강건함)

입력 데이터가 조금 변해도 풀링의 결과는 잘 변하지 않음

ex. 아래 그림은 입력 데이터의 차이(데이터가 오른쪽으로 1칸씩 이동)를 흡수해 사라자게 하는 모습을 보여줌

입력 데이터가 가로로 1원소 만큼 어긋나도 출력은 같음

 


7.4 합성곱/풀링 계층 구현하기

forward와 backward 메서드를 추가하여 모듈로 이용 가능

 

합성곱 계층과 풀링 계층은 복잡해 보이지만, '트릭'을 사용하면 쉽게 구현 가능

그 트릭을 활용하여 간단히 하면서 합성곱 계층 구현

 

7.4.1 4차원 배열

CNN에서 계층 사이를 흐르는 데이터는 4차원 데이터

ex. 데이터의 형상 : (10,1,28,28) → 높이 28, 너비 28, 채널 1개인 데이터가 10개

 

7.4.2 im2col로 데이터 전개하기

합성곱 연산을 곧이곧대로 구현하려면 for문을 겹겹히 써야함

이는 생각만해도 귀찮고, 또 넘파이에 for문을 사용하면 성능이 떨어진다는 단점도 있음

(넘파이에서 원소에 접근할 때 for문을 사용하지 않는 것이 바람직)

 

이번 절에서는 for 문 대신 im2col이라는 편의 함수를 사용하여 간단하게 구현

 

im2col : 입력 데이터를 필터링(가중치 계산)하기 좋게 전개하는 (펼치는) 함수

다차원의 데이터를 행렬로 변환하여 행렬 연산을 하도록 해주는 함수

 

아래 그림과 이 3차원 입력 데이터에 im2col을 적용하면 2차원 행렬로 바뀜

(정확하게는 배치 안의 데이터 수까지 포함한 4차원 데이터를 2차원으로 변환)

 

 

im2col은 필터링 하기 좋게 입력 데이터를 전개

구체적으로는 아래 그림과 같이 입력 데이터에서 필터를 적용하는 영역(3차원 블록)을 한 줄로 늘여높음

이 전개를 필터를 적용하는 모든 영역에서 수행하는게 im2col

 

필터 적용 영역을 앞에서부터 순서대로 1줄로 펼침

 

위 그림에서는 보기에 좋게 스트라이드를 크게 잡아 필터의 적용 영역이 겹치지 않도록 했지만, 실제 상황에서는 겹치는 경우가 대부분

필터 적용 영역이 겹치게 되면 im2col로 전개한 후의 원소 수가 원래 블록의 원소 수보다 많아짐

그래서 im2col을 사용해 구현하면 메모리를 더 많이 소비하는 단점이 있음

하지만 컴퓨터는 큰 행렬을 묶어서 계산하는데 탁월

ex. 행렬 계산 라이브러리(선형 대수 라이브러리) 등은 행렬 계산에 고도로 최적화되어 큰 행렬의 곱셈을 빠르게 계산 가능

그래서 문제를 행렬 계산으로 만들면 선형 대수 라이브러리를 활용해 효율을 높일 수 있음

 

** im2col = image to column, 이미지에서 행렬로

 

im2col로 입력 데이터를 전개한 다음에는 합성곱 계층의 필터(가중치)를 1열로 전개하고, 두 행렬의 곱을 계산하면 됨

이는 완전연결 계층의 Affine 계층에서 한 것과 거의 같음

 

합성곱 연산의 필터 처리 상세 과정 : 필터를 세로로 1열로 전개하고, im2col이 전개한 데이터와 행렬 곱을 계산, 마지막으로 출력 데이터를 변형(reshape)

위 그림과 같이 im2col 방식으로 출력한 결과는 2차원 행렬

CNN은 데이터를 4차원 배열로 저장하므로 2차원인 출력 데이터를 4차원으로 변형(reshape)

이상이 합성곱 계층의 구현 흐름

 

7.4.3 합성곱 계층 구현하기

코드 출처 : https://github.com/WegraLee/deep-learning-from-scratch

 

GitHub - WegraLee/deep-learning-from-scratch: 『밑바닥부터 시작하는 딥러닝』(한빛미디어, 2017)

『밑바닥부터 시작하는 딥러닝』(한빛미디어, 2017). Contribute to WegraLee/deep-learning-from-scratch development by creating an account on GitHub.

github.com

 

im2col 함수의 인터페이스

im2col(input_data, filter_h, filter_w, stride=1, pad=0)

input_data : (데이터 수, 채널 수, 높이, 너비)의 4차원 데이터로 이뤄진 입력 데이터

filter_h : 필터의 높이

filter_w : 필터의 너비

stride : 스트라이드

pad : 패딩

 

im2col은 '필터 크기', '스트라이드', '패딩'을 고려하여 입력 데이터를 2차원 배열로 전개

 

im2col 실제 사용

import sys, os
import numpy as np
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)

[결과]

x1 = (9, 75) # 채널 3, 5x5 데이터

x2 = (90, 75)

 

위에서 알 수 있는 것 2가지

1) 배치 크기가 1 (데이터 1개), 채널 3개, 높이 · 너비가 7x7의 데이터

2) 배치 크기만 10이고 나머지는 첫 번째와 같음

im2col 함수를 적용한 두 경우 모두 2번째 차원의 원소는 75개, 이 값은 필터의 원소 수와 같음(채널 3개, 5x5 데이터)

또한 배치 크기가 1일 때는 im2col의 결과의 크기가 (9, 25) 이고,

10일 때는 그 10배인 (90, 75) 크기의 데이터 저장

 

이제 im2col을 사용하여 합성곱 계층을 구현

합성곱 계층을 Convolution이라는 클래스로 구현

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.stride)
       
        col = im2col(x, FH, FW, self.stride, self.pad)
        col_W = self.W.reshape(FN, -1).T # 필터 전개
        out = np.dot(col, col_W) + self.b
       
        out = out.reshape(N, out_h, out_w, -1).transpose(0,3,1,2)

        return out

합성곱 계층은 필터(가중치), 편향, 스트라이드, 패딩을 인수로 받아 초기화

필터는 (FN, C, FH, FW)의 4차원 형상

FN : 필터 개수

C : 채널

FH : 필터 높이

FW : 필터 너비

 

col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T # 필터 전개
out = np.dot(col, col_W) + self.b

위 코드의 해당 부분에서 입력 데이터를 im2col로 전개하고 필터도 reshape을 사용해 2차원 배열로 전개

그리고 이렇게 전개한 두 행렬의 곱을 구함

 

필터를 전개하는 부분 위 코드 부분은 각 필터 블록을 1줄로 펼쳐 세움

이때 reshape 의 두 번째 인수를 -1로 지정했는데, 이는 reshape이 제공하는 편의 기능

reshape에 -1을 지정하면 다차원 배열의 원소 수가 변환 후에도 똑같이 유지되도록 적절히 묶어줌

 

forward 구현의 마지막에서는 출력 데이터를 적절한 형상으로 바꿈

이때 넘파이의 transpose 함수를 사용하는데, 이는 다차원 배열의 축 순서를 바꿔주는 함수

→ 인덱스(번호)로 축의 순서를 변경

 

im2col로 전개한 덕분에 완전연결 계층의 Affine 계층과 거의 똑같이 구현 가능(5.6 Affine / Softmax 계층 구현하기)

 

합성곱의 계층의 역전파는 Affine 계층의 구현과 공통점이 많아 생략

주의할 점 하나 : 합성곱 계층의 역전파에서 im2col을 역으로 처리해야 함

>> col2im 함수 사용

col2im을 사용한다는 점을 제외하면 합성곱 계층의 역전파는 Affine 계층과 똑같음

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    """(im2col과 반대) 2차원 배열을 입력받아 다수의 이미지 묶음으로 변환한다.
   
    Parameters
    ----------
    col : 2차원 배열(입력 데이터)
    input_shape : 원래 이미지 데이터의 형상(예:(10, 1, 28, 28))
    filter_h : 필터의 높이
    filter_w : 필터의 너비
    stride : 스트라이드
    pad : 패딩
   
    Returns
    -------
    img : 변환된 이미지들
    """
    N, C, H, W = input_shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)

    img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    return img[:, :, pad:H + pad, pad:W + pad]

 

7.4.4 풀링 계층 구현하기

풀링 계층 구현도 합성곱 계층과 마찬가지로 im2col을 사용하여 입력 데이터 전개

단, 풀링의 경우에는 채널 쪽이 독립적이라는 점이 합성곱 계층 때와는 다름

 

구체적으로는 아래와 같이 풀링 적용 영역을 채널마다 독립적으로 전개

입력 데이터에 풀링 적용 영역을 전개(2x2 풀링의 예)

일단 위와 같이 전개 후

전개한 행렬에서 최댓값을 구하고

적절한 형상으로 성형

풀링 계층 구현의 흐름 : 풀링 적용 영역에서 가장 큰 원소는 회색으로 표시

 

이상이 풀링 계층의 forward 처리 흐름

아래는 이를 파이썬으로 구현한 코드

import sys, os
import numpy as np
sys.path.append(os.pardir)
from common.util import im2col

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, axis=1)

        # 성형(3)
        out = out.reshape(N, out_h, out_w, C).transpose(0,3,1,2)
       
        return out

 

풀링 계층 구현은 아래 3단계로 진행

1. 입력 데이터를 전개

2. 행별 최댓값 구하기

3. 적절한 모양으로 성형

(각 단계는 한두 줄 정도로 간단히 구현)

 

이상 풀링 계층의 forward 처리

이 절에서 선택한 전략을 따라 입력 데이터를 풀링하기 쉬운 형태로 전개해버리면 그 후 구현은 간단

 

풀링 계층의 backward 처리는 관련 사항을 이미 설명했으니 생략

ReLU 계층을 구현할 때 사용한 max의 역전파 참고(5.5.1 ReLU 계층)

 


7.5 CNN 구현하기

합성곱 계층과 풀링 계층을 구현했으니, 이 계층들을 조합하여 손글씨 숫자를 인식하는 CNN 조립

 

CNN 네트워크 구성 : Conv-ReLU-Pooling --- Affine-ReLU --- Affine-Softmax ---> 순으로 흐름

이를 SimpleConvNet이라는 이름의 클래스로 구현

 

SimpleConvNet의 초기화(__init__) 살펴보기

초기화때 받는 인수들

º input_dim : 입력 데이터(채널 수, 높이, 너비)의 차원

º conv_param : 합성곱 계층의 하이퍼파라미터(딕셔너리). 딕셔너리의 키는 아래와 같음

     - filter_num : 필터 수

     - filter_size : 필터 크기

     - stride : 스트라이드

     - pad : 패딩

º hidden_size : 은닉층(완전연결)의 뉴런 수

º output_size : 출력층(완전연결)의 뉴런 수

º weight_init_std : 초기화 때의 가중치 표준편차

 

여기에서 합성곱 계층의 하이퍼파라미터는 딕셔너리 형태로 주어짐(conv_param)

>> 필요한 하이퍼파라미터의 값이 {'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}처럼 저장

 

#1 초기화 인수로 주어진 합성곱 계층의 하이퍼파라미터를 딕셔너리에서 꺼냄(나중에 쓰기 쉽도록)

그리고 합성곱 계층의 출력 크기 계산

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):
        filter_num = conv_param['filter_num']
        filter_size = conv_param['filter_size']
        filter_pad = conv_param['pad']
        filter_stride = conv_param['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_output_size, hidden_size)
        self.params['b2'] = np.zeros(hidden_size)
        self.params['W3'] = weight_init_std * \
                            np.random.randn(hidden_size, output_size)
        self.params['b3'] = np.zeros(output_size)

학습에 필요한 매개변수는 1번째 층의 합성곱 계층과 나머지 두 완전연결 계층의 가중치와 편향

이 매개변수들을 인스턴스 변수 params 딕셔너리에 저장

1번째 층의 합성 곱 계층의 가중치를 W1, 편향을 b1이라는 키로 저장

마찬가지로 2번째 층의 완전 연결 계층의 가중치와 편향을 W2, b2

마지막 3번째 층의 완전연결 계층의 가중치와 편향을 W3, b3라는 키로 각각 저장

 

#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()

순서가 있는 딕셔너리(OrderedDict)인 layers에 계층들을 차례로 추가

마지막 SoftmaxWithLoss 계층 만큼은 last_layer라는 별도 변수에 저장해 둠

 

이상 SimpleConvNet의 초기화

이렇게 초기화를 마친 다음에는 추론을 수행하는 predict 메소드와 손실 함수의 값을 구하는 loss 메서드를 다음과 같이 구현할 수 있음

def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    def loss(self, x, t):
        y = self.predict(x)
        return self.last_layer.forward(y, t)

 

이 코드에서 인수 x는 입력 데이터, t는 정답 레이블

추론을 수행하는 predict 메소드는 초기화 때 layers에 추가한 계층을 맨 앞부터 차례로 forward 메소드를 호출하며 그 결과 다음 계층으로 전달

 

손실 함수를 구하는 loss 메소드는 predict 메소드의 결과를 인수로 마지막 층의 forward 메소드를 호출

즉, 첫 계층부터 마지막 계층까지 forward 처리

 

오차역전파법 기울기 구현 코드

def gradient(self, x, t):
        """기울기를 구한다(오차역전파법).
        Parameters
        ----------
        x : 입력 데이터
        t : 정답 레이블
        Returns
        -------
        각 층의 기울기를 담은 사전(dictionary) 변수
            grads['W1']、grads['W2']、... 각 층의 가중치
            grads['b1']、grads['b2']、... 각 층의 편향
        """
        # forward
        self.loss(x, t)

        # backward
        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'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
        grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

 

매개변수의 기울기는 오차역전파법으로 구함

이 과정은 순전파와 역전파를 반복

지금까지 각 계층의 순전파와 역전파 기능을 제대로 구현했다면, 여기에서는 단지 그것들을 적절한 순서로 호출 해주면 됨

 

마지막으로 grads라는 딕셔너리 변수에 각 가중치 매개변수 기울기를 저장

이상이 SimpleConvNet의 구현

 


7.6 CNN 시각화하기

CNN을 구성하는 합성곱 계층은 입력으로 받은 이미지 데이터에서 '무엇을 보고 있는가?'

 

7.6.1 1번째 층의 가중치 시각하기

MNIST 데이터셋으로 간단한 CNN 학습을 해보았는데, 그때 1번째 층의 합성곱 계층의 가중치는 그 형상이 (30,1,5,5) 

: 필터 30개, 채널 1개, 5x5 크기

 

필터의 크기가 5x5이고 채널이 1개라는 것 → 이 필터를 1채널의 회색조 이미지로 시각화 가능하다는 것

 

합성곱 계층(1층째) 필터를 이미지로 나타내기

 

구현코드

import numpy as np
import matplotlib.pyplot as plt
from simple_convnet import SimpleConvNet

def filter_show(filters, nx=8, margin=3, scale=10):
    """
    """
    FN, C, FH, FW = filters.shape
    ny = int(np.ceil(FN / nx))

    fig = plt.figure()
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)

    for i in range(FN):
        ax = fig.add_subplot(ny, nx, i+1, xticks=[], yticks=[])
        ax.imshow(filters[i, 0], cmap=plt.cm.gray_r, interpolation='nearest')
    plt.show()


network = SimpleConvNet()
# 무작위(랜덤) 초기화 후의 가중치
filter_show(network.params['W1'])

# 학습된 가중치
network.load_params("params.pkl")
filter_show(network.params['W1'])

 

학습 전 1번째 층의 합성곱 계층의 가중치

 

학습 후 1번째 층의 합성곱 계층의 가중치

 

가중치 원소는 실수이지만, 이미지에서는 가장 작은 값(0)은 검은색, 가장 큰 값(255)는 흰색으로 정규화하여 표시

위 그림과 같이 학습 전 필터는 무작위로 초기화되고 있어 흑백의 정도에 규칙성이 없음

한편, 학습을 마친 필터는 규칙성 있는 이미지가 됨

흰색이 검은색으로 점차 변화하는 필터와 덩어리(블롭, blob)가 진 필터 등 규칙을 띄는 필터로 바뀜

 

학습 후 그림과 같이 규칙성 있는 필터는 무엇을 보고 있는가?

>> 에지(색상이 바뀐 경계선)과 블롭(국소적으로 덩어리진 영역) 등을 보고 있음

 

가령 왼쪽 절반이 흰색이고 오른쪽 절반이 검은색인 필터는 아래와 같이 세로 방향의 에지에 반응하는 필터

가로 에지와 세로 에지에 반응하는 필터 : 출력 이미지 1은 세로 에지에 흰 픽셀이 나타나고, 출력 이미지 2는 가로 에지에 흰 픽셀이 많이 나옴

위 그림은 학습된 필터 2개를 선택하여 입력 이미지에 합성곱 처리를 한결과로, '필터 1'은 세로 에지에 반응하며 '필터 2'는 가로 에지에 반응

 

이처럼 합성곱 계층의 필터는 에지나 블롭 등의 원시적인 정보를 추출할 수 있음

이런 원시적인 정보가 뒷단 계층에 전달된다는 것이 앞에서 구현한 CNN에서 일어나는 일 

 

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

앞 절의 결과는 1번째 층의 합성곱 계층을 대상으로 한 것

1번째 층의 합성곱 계층에서는 에지나 블롭 등의 저수준 정보가 추출된다 치고,

그럼 겹겹이 쌓인 CNN의 각 계층에서는 어떤 정보가 추출 되는가?

 

딥러닝 시각화에 관한 연구에 따르면, 

계층이 깊어질수록 추출되는 정보(정확히는 강하게 반응하는 뉴런)는 더 추상화 된다는 것을 알 수 있음

 

CNN의 합성곱 계층에서 추출되는 정보, 1번째 층은 에지와 블롭, 3번째 층은 텍스처, 5번째 층은 사물의 일부, 마지막 완전 연결 계층은 사물의 클래스(개, 자동차 등)에 뉴런이 반응

 

딥러닝의 흥미로운 점은 위 그림과 같이 합성곱 계층을 여러 겹 쌓으면, 층이 깊어지면서 더 복잡하고 추상화된 정보가 추출된다는 것

 

처음 층은 단순한 에지에 반으앟고, 이어서 텍스처에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화

즉, 층이 깊어지면서 뉴런이 반응하는대상이 단순한 모양에서 '고급' 정보로 변화해 감

다시말하면 사물의 '의미'를 이해하도록 변화하는 것

 

 

7.7 대표적인 CNN

지금까지 제안된 CNN 네트워크의 구성은 다양함

이번 절에서는 그중에서도 특히 중요한 네트워크 두 개 소개

1) CNN의 원조인 LeNet

2) 딥러닝이 주목받도록 이끈 AlexNet

 

7.7.1 LeNet

손글씨 숫자를 인식하는 네트워크

1998년 제안

아래와 같이 합성곱 계층과 풀링 계층(정확히는 단순히 '원소를 줄이기'만 하는 서브 샘플링 계층)을 반복하고,

마지막으로 완전연결 계층을 거치면서 결과 출력

LeNet의 구성

 

LeNet과 현재의 CNN을 비교하면 몇 가지 면에서 차이가 있음

1) 활성화 함수

- LeNet은 시그모이드 함수를 사용하는 데 반해, 현재는 주로 ReLU 사용

2) 원래의 LeNet은 서브 샘플링을 하여 중간 데이터의 크기를 줄이지만 현재는 최대 풀링이 주류

 

7.7.2 AlexNet

2012년 발표

딥러닝 열풍을 일으키는 데 큰 역할

 

AlexNet의 구성

 

AlexNet은 합성곱 계층과 풀링 계층을 거듭하며 마지막으로 완전연결 계층을 거쳐 결과 출력

LeNet에서 큰 구조는 바뀌지 않았지만 AlexNet에서는 다음과 같은 변화를 줌

- 활성화 함수로 ReLU 이용

- LRN(Local Response Normalization)이라는 국소적 정규화를 실시하는 계층 이용

- 드롭 아웃 사용

 

네트워크 구성면에서는 LeNet과 큰 차이는 x

그러나 이를 둘러싼 환경과 컴퓨터 기술이 큰 진보를 이룸

대량의 데이터를 누구나 얻을 수 있게 되었고, 병렬 계산에 특화된 GPU가 보급되면서 대량의 연산을 고속으로 수행할 수 있게 됨

 

빅데이터 & GPU : 딥러닝 발전의 큰 원동력

 

** 딥러닝(심층 신경망)에는 대부분 수많은 매개변수가 쓰임

그래서 학습하려면 엄청난 양의 계산을 해야함

또한 그 매개변수를 '피팅(fitting,적합)시키는 데이터도 대량으로 필요

GPU와 빅데이터는 이런 문제에 해결책을 던졌다고 할 수 있음