딥러닝독학! (4) 합성곱 신경망 CNN - 밑바닥부터시작하는딥러닝

 

 

 

드디어 CNN을 다루는구나...... 겨울방학 프로젝트에 접하기만 하고 어떻게 돌아가는건지, 다루는건지 이해하지 못했었다. 가장 재밌..진 않고 파악하고 싶었던 딥러닝중에 하나이다. 참고로 이책에선 딥러닝프레임워크를 사용하지 않고 다룬다고하니 원리와 구조만 이해하고 스킵한뒤에 텐서플로로 복습해야겠다.

 

1. 합성곱 계층

1.1 전체 구조

CNN 네트워크 구조에는 합성곱 계층풀링 계층이 등장한다. 지금까지 신경망은 인접하는 계층의 모든 뉴런과 결합되어 있었다. 즉, 한 층의 모든 뉴런이 다음 층의 모든 뉴런과 연결된 상태이다. (완전연결) 이렇게 연결된 계층아 Affine 계층이다. 

Affine 계층으로 이뤄진 네트워크의 예

위 그림에서 완전연결 신경망은 Affine계층 뒤에 ReLU (or Sigmoid)계층이 이어진다. 마지막 5번째 층은 AFfine계층에 이어 소프트맥스계층에서 최종결과를 출력한다.

 

그렇다면 CNN은?

CNN으로 이뤄진 네트워크의 예

합성곱 계층과 풀링 계층이 추가되었다. Affine-ReLU연결이 Conv-ReLU-(Pooling)의 흐름으로 바뀌었다. 

다만, 출력에 가까운 층에서는 Affine-ReLU구성이 사용될 수 있다. 

 

1.2 완전연결 계층의 문제점

완전연결 계층 : 인접하는 계층의 뉴런이 모두 연결, 출력의 수 임의로 정할 수 있음

but '데이터의 형상이 무시'된다.  (이미지는 3차원 => 입력할땐 평평한 1차원 데이터로 바꿔야함)

더보기

앞에서 MNIST 데이터셋은 (1,28,28)인 이미지 형상을 1줄로 세운 782개의 데이터를 입력했음.

이렇게되면 공간적 정보가 손실됨 (공간적으로 가까운 픽셀은 값이 비슷하거나, 거리가 먼 픽셀낄리는 연관x)

한편, 합성곱 계층은 형상을 유지한다. 이미지도 3차원 데이터로 입력받고, 다음 계층에도 3차원 데이터로 전달한다.

CNN에서...
특징 맵(feature map) : 합성곱 계층의 입출력 데이터
입력 특징 맵(input feature map) : 합성곱 계층의 입력 데이터
출력 특징 맵(output feature map) : 합성곱 계층의 출력 데이터

 

1.3 합성곱 연산

합성곱 연산은 이미지 처리에서 말하는 필터연산에 해당한다. (filter => kernel이라 칭하기도 함

합성곱 연산의 예

위 예에서 입력은 (4,4), 필터는 (3,3), 출력은 (2,2)가 된다. 

연산은 어떻게 이뤄질까?

1. 필터의 윈도우를 일정 간격으로 이동해가며 입력 데이터에 적용한다.
2. 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구한다. (단일 곱셈 누산)
3. 출력의 해당 장소에 저장

위 사진의 경우, 회색 3*3 부분이 윈도우가 된다. 

첫번째 윈도우에선 (1*2 + 2*0 + 3*1) + (0*0 + 1*1 + 2*2) + (3*1 + 0*0 + 1*2) = 15가 된다.

그리고 이 과정을 모든 장소에서 수행하면 합성곱 연산의 출력이 완성된다.

 

완전연결 신경망에는 가중치 매개변수와 편향이 존재한다. CNN에서는 필터의 매개변수가 '가중치'에 해당한다.

 

그렇다면 편향은? 그 하나의 값을 필터를 적용한 모든 원소에 더한다.

필터를 적용한 원소에 고정값(필터)을 더함

 

1.4 패딩

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

폭 1짜리 패딩이라하면 입력 데이터 사방 1픽셀을 특정 값으로 채우는 것

패딩은 점선으로 표기, 0 입력됨

출력 크기를 조정할 목적으로 사용.
합성곱 연산을 거칠 때마다 크기가 작아지는데 어느 시점에서는 출력 크기가 1이 될 것.
이러한 사태를 막기 위해 패딩 사용.

 

1.5 스트라이드

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

스트라이드 크기만큼 필터를 적용하는 윈도우가 이동한다. (2이면 두 칸씩 이동)

스트라이드를 키우면 출력 크기는 작아진다. 패딩을 키우면 출력 크기가 커진다.

이러한 관계를 수식화하면 다음과 같다.

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

단, OH, OW가 정수로 나눠떨어지는 값이어야 함

 

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

채널까지 고려한 3차원 데이터. 채널쪽으로 특징 맵이 여러 개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻는다.

입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다.
필터 자체의 크기는 원하는 값으로 설정할 수 있다.
모든 채널의 필터가 같은 크기여야 한다. 

 

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

출력 데이터는 한 장의 특징맵(한 장의 채널)이다. 출력으로 다수의 채널을 보내려면? 필터를 다수 사용하면 된다.

 

위와 같이 필터를 FN개 적용하면 출력 맵도 FN개 적용됨.

합성곱 연산에서는 필터의 수도 고려해야 하기 때문에 (출력채널 수, 입력채널 수, 높이, 너비) 순으로 쓴다.

 

편향을 적용하면?

형상이 다른 블록의 덧셈은 넘파이의 브로드캐스팅으로 쉽게 구현 가능.

 

1.7 배치 처리

각 계층을 흐르는 데이트의 차원을 하나 늘려 4차원 데이터로 저장 ▶ (데이터수, 채널 수, 높이, 너비)

각 데이터의 선두에 배치용 차원 추가 → 데이터가 4차원 형상을 가진 채 각 계층을 타고 흐른다.

4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다. 즉 N회 분 처리를 한 번에!

2. 풀링 계층

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

위 그림에서는 2*2영역을 원소 하나로 집약하여 공간 크기를 줄인다. 최댓값을 구하는 최대풀링(Max Pooling, 맥스풀링) 사용

즉, '2*2'는 대상 영역의 크기를 뜻하고, 2*2 최대 풀링은 위 그림에서처럼 2*2 크기에서 가장 큰 원소 하나를 꺼낸다.

여기서 스트라이드를 2로 서정하여 두칸 간격으로 이동했음. (보통 풀링의 윈도우 크기와 스트라이드를 같은 크기로)

최대풀링 외에도 평균풀링(Average Pooling) 등이 있다. 평균 풀링은 대상 영역의 평균을 계산한다. 
이미지 인식 분야에서는 주로 최대풀링을 사용한다.

 

2.1 풀링 계층의 특징

1. 학습해야 할 매개변수가 없다.

어차피 최댓값이나 평균을 취하는 명확한 처리이므로

2. 채널 수가 변하지 않는다.

입력 데이터의 채널 수 그대로 출력 데이터로 내보낸다.

3. 입력의 변화에 영향을 적게 받는다.

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

 

위 그림에서처럼 데이터가 오른쪽으로 1칸씩 이동한 입력 데이터의 차이를 풀링이 흡수해 사라지게 한다.

 

3. 합성곱/ 풀링 계층 구현

합성곱/풀링 계층은 '트릭'을 사용하면 쉽게 구현할 수 있다.

 

3.1 4차원 배열

CNN 계층 사이를 흐르는 데이터는 4차원이다.

데이터의 형상을 (10,1,28,28) 이라 하자.

x = np.random.rand(10,1,28,28)

각 데이터에 접근 하기 위해서는 다음과 같이 쓴다.

x[0] #첫번째 데이터
x[0,0] # = x[0][0]첫번째 데이터의 첫 채널의 공간 데이터

여기서 im2col이라는 '트릭'을 사용하여 문제를 단순하게 만든다.

 

3.2 im2col로 데이터 전개

합성곱 연산을 곧이곧대로 구현하려면 for문을 겹겹이 써야 한다. 엄청 비효율적임.

(애초에 numpy연산에서는 for문을 사용하지 않는 것이 바람직함)

 

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

대략적인 im2col 동작

구체적으로는 다음 그림과 같이 입력 데이터에서 필터를 적용하는 영역을 한 줄로 늘어놓는다.

이 전개를 필터를 적용하는 모든 영역에서 수행함.필터 적용 영역을 앞에서부터 순서대로 1줄로 펼친다.

 

실제 상황에서는 영역이 겹치는 경우가 대부분이라 im2col로 전개한 후의 원소수가 원래보다 많아진다. 따라서, 평소보다 메모리를 많이 소비하게 됨. 

하지만, 행렬 꼐산 라이브러리는 큰 행렬을 묶어서 계산하는데 탁월하다.

 

im2col로 전개한 후에는 합성곱 계층의 필터를 1열로 전개하고, 두 행렬의 내적을 계산한다. 

 

3.3 합성곱 계층 구현

책에서는 im2col함수를 미리 만들어놓음. 코드 깃헙에서 common/util.py 참조

인터페이스는 다음과 같다.

iuput_data (데이터수, 채널수, 높이, 너비)의 4차원 배열 입력 데이터
filter_h 필터 높이
filter_w 필터 너비
stride 스트라이드
pad 패딩
스트라이드, 필터크기, 패딩을 고려하여 입력 데이터를 2차원 배열로 전개
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    N, C, H, W = input_data.shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    return col
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

reshape의 두 번째 인수는 -1인데, 이는 다차원 배열의 원소수가 변환 후에도 똑같이 유지되도록 적절히 묶어준다.

forward구현의 마지막에서는 transpose함수를 사용하는데 이는 다차원 배열의 축 순서를 바꿔준다.

인덱스 번호로 축 순서를 바꿔줌

합성곱 계층의 역전파에서는 im2col을 역으로 처리해야 한다. 

 

3.4 풀링 계층 구현

합성곱 계층과 마찬가지로 im2col을 사용해 입력 데이터를 전개

but, 채널 쪽이 독립적이라는 점이 다르다.

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

2*2풀링

이제 전개한 행렬에서 행별 최댓값을 구하고 적절한 형상으로 성형한다.

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

 

4. CNN 구현

아래와 같은 CNN을 구현한다. 'Convolution-ReLU-Pooling-AFfine-ReLU-Affine-Softmax' 순으로 흐른다.

SimpleConvNet라는 이름의 클래스로 구현하며 인터페이스는 아래와 같다.

input_dim 입력 데이터 (채널수, 높이, 너비)
conv_param 합성곱 계층의 하이퍼파라미터(딕셔너리)
- filter_num 필터 수
- filter_size 필터 크기
- stride 스트라이드
- pad 패딩
hidden_size 은닉층의 뉴런수
output_size 출력층의 뉴런수
weight_init_std 초기화 때의 가중치 표준편차

 

<초기화>

내용이 길어 세 부분으로 나눈다.

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

먼저, 초기화 인수로 주어진 합성곱 계층의 하이퍼파라미터를 딕셔너리에서 꺼냄

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

        # 가중치 초기화
        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번째 층의 합성곱 계층과 나머지 두 Affine계층의 가중치와 편향.

이들 매개변수를 params딕셔너리에 저장

        # 계층 생성
        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()

Orderdict순서가 있는 딕셔너리

layers에 계층들을 차례로 추가한다.

마지막 SoftmaxWithLoss는 lastLayer라는 별도 변수에 저장.

 

<추론/손실 함수>

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

 

<실제 학습>

코드는 생략.

SimpleConvNet을 MNIST 데이터셋으로 학습하면 훈련 데이터에 대한 정확도는 99.82%, 테스트 데이터에 대한 정확도는 무려 99%가 된다. 

 

5. 대표적인 CNN

5.1 LeNet

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

합성곱 계층과 풀링 계층을 반복하고, 마지막으로 완전연결 계층을 거치면서 결과를 출력한다.

 

현재의 CNN과의 차이점
- LeNet은 시그모이드 함수를 사용하는데 반해, 현재는 주로 ReLU를 사용한다.
- LeNet은 서브샘플링을 하여 중간 데이터의 크기가 작아지지만 현재는 최대 풀링이 주류이다.

 

5.2 AlexNet

딥러닝 열풍을 일으키는 데 큰 역할을 함. 구성은 크게 다르지 않다.

LeNet에 비해 AlexNet에서는 다음과 같은 변화를 주었다.

활성화 함수로 ReLU를 이용
LRN이라는 국소적 정규화를 실시하는 계층을 이용함
드롭아웃 사용

 

6.정리

CNN = 완전연결계층 + 합성곱계층 + 풀링계층

합성곱/풀링계층은 im2col 트릭을 이용하여 간단하고 효율적으로 구현

CNN은 계층이 깊어질수록 고급 정보가 추출됨

대표적인 CNN : LeNet, AlexNet