본문 바로가기

책/밑바닥부터 시작하는 딥러닝

7장 CNN (1)

전체 구조

지금까지 본 신경망은 FFNN이었다.

CNN은 뭐가 다를까?

- Affine-ReLU 연결이 Conv-ReLU-(Pooling)으로 바뀌었다.

- 상위 층에는 여전히 Affine-ReLU 연결을 사용할 수도 있다.

 

합성곱 계층

FFNN의 문제점

이미지 데이터는 3차원 형상이며, 이 형상에는 공간적 정보가 담겨있다. FFNN으로 이 이미지를 처리하기 위해서는 Flatten을 수행하여 형상을 무시하고 모든 입력 데이터를 동등한 뉴런으로 취급하여 형상에 담긴 정보를 살릴 수가 없다. 한편 CNN은 이 형상을 유지하여 데이터를 제대로 이해할 가능성이 있다.

 

합성곱 연산

합성곱 연산은 필터의 위도우를 일정 간격으로 이동해가며 입력 데이터에 적용한다. 여기에 사용되는 연산은 단일 곱셈-누산으로 원소별 곱을 행한 뒤 이를 모두 더한다.

CNN에서는 필터의 매개변수가 그동안의 가중치 역할을 한다. 또한 CNN에도 편향이 존재한다.

편향은 항상 (1x1) 크기의 하나만 존재한다. 하나의 값을 필터를 적용한 원소에 모두 더해준다.

 

패딩

합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값(예를 들면 0)으로 채운다. 패딩은 주로 출력 크기를 조정할 목적으로 사용된다.

스트라이드

필터를 적용하는 위치의 간격을 스트라이드라고 한다. 지금까지 본 예는 스트라이드가 1이었고 만약 2를 적용한다면 다음과 같다.

출력 크기를 계산하는 방법을 알아보자

- 입력 크기 (H, W)

- 필터 크기 (FH, FW)

- 출력 크기 (OH, OW)

- 패딩 P

- 스트라이드 S

위의 두 식의 계산 결과가 정수로 떨어지도록 파라미터 값을 잘 설정해야 한다.

 

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

흑백 이미지가 아닌 3채널 이미지인 경우 합성곱 연산을 다음과 같이 수행한다.

길이 방향(채널 방향)으로 필터가 늘어났다. 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고 그 결과를 더해서 하나의 출력을 얻는다.

 

출력 데이터가 채널이 여러개 즉, 여러 특징맵을 갖도록 하고 싶으면 필터를 여러개 사용하면 된다.

 

풀링 계층

풀링은 가로, 세로 방향 공간을 줄이는 연산이다. 예를 들어 2x2 max pooling을 스트라이드 2로 처리하는 순서는 다음과 같다.

대응하는 영역 중 가장 큰 값만 취한다. 풀링은 가로, 세로 방향 공간을 줄이는 연산이다. (깊이 방향으로 줄이는 풀링도 있음 - 전역 평균 풀링)

풀링은 윈도우 크기와 스트라이드가 같은 값으로 설정하는 것이 보통이다. 3x3이면 스트라이드 3 이런식으로

 

풀링 계층의 특징

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

- 채널 수가 변하지 않는다. 채널마다 독릭접으로 계산하기 때문

- 입력의 변화에 영향을 적게 받는다. (근데 이게 단점이 되기도 함)

 

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

*보통의 딥러닝 프레임워크에서는 (B, H, W, C) 순서인데 이 책에서는 (B, C, H, W) 순서로 데이터를 표현함

 

4차원 데이터에 대한 합성곱 연산을 곧이곧대로 구현하려면 for 문을 겹겹이 써야한다. 이는 성능 저하를 야기하기 때문에 본 책에서는 im2col이라는 함수를 정의하여 사용한다.

 

im2col은 입력 데이터를 행렬 연산이 용이하도록 2차원으로 reshape해주는 함수이다.

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

그런 다음 필터도 펼쳐서 연산을 수행한다.

im2col의 사용 예시를 보자

im2col(input_data, FH, FW, stride=1, pad=0)

- input_data : (N, C, H, W)의 4차원 데이터

- FH : 필터의 높이

- FW : 필터의 너비

x1 = np.random.rand(1, 3, 7, 7)
col1 = im2col(x1, 5, 5)
print(col1.shape) # (9, 75)

x2 = np.random.rand(10, 3, 7, 7)
col2 = im2col(x1, 5, 5)
print(col2.shape) # (90, 75)

75라는 숫자는 필터의 모든 요소의 개수이다. 이 예시에서는 5x5 필터가 3채널 있으므로 3x3x5 = 75이다. 

9는 당연히 출력의 차원이 3x3이므로 9라는 값을 갖게된다.

 

 

이를 사용한 Convolution 클래스는 다음과 같다.

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad

        # 중간 데이터 (backward에서 사용)
        self.x = None
        self.col = None
        self.col_W = None

        # 가중치와 편향 매개변수의 기울기
        self.dW = None
        self.db = None
    
    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((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)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

- 앞의 예시의 데이터를 사용했다고 치면 col의 shape은 (90, 75)이다. 

- 필터의 차원은 (10, 3, 5, 5)인데 이를 self.W.reshape(FN, -1)을 하게되면 FN=10이므로 (10, 75)의 shape을 갖게된다. 전치를 해주는 이유는 당연히 (90, 75) x (75, 10) 연산을 수행해야하기 때문이다.

 

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

backward 부분은 im2col을 역으로 처리하는 col2im을 사용하고 affine 계층과 동일한 매커니즘이다.

dout의 shape은 W와 같다는 점만 생각하고 코드를 차근 차근 읽어보면 어렵지 않게 이해 가능하다.

 

풀링 계층 구현

마찬가지로 im2col을 사용한다. 단 풀링의 경우엔 채널 쪽이 독립적으로 계산된다.

이렇게 전개한뒤 np.max(x, axis=1)을 사용하여 행별 최대값을 구하고 적절한 shape으로 바꿔주기만 하면된다.

 

참고로 argmax나 max같이 axis를 지정하는 메서드의 경우 axis가 헷갈릴 수도 있는데 없애야 하는 차원을 지정해준다고 생각하면 쉽다. 위의 경우 (12, 4)인데 우리가 원하는 것은 (12, ) 이므로 axis 1에 해당하는 차원을 없애야 하므로 axis=1로 지정하는 것이다.

 

풀링 클래스는 다음과 같다.

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
        
        self.x = None
        self.arg_max = None

    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)

        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

 

' > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

8장 딥러닝  (0) 2023.09.22
7장 CNN (2)  (0) 2023.09.22
6장 학습 관련 기술들 (2)  (0) 2023.09.20
6장 학습 관련 기술들 (1)  (0) 2023.09.20
5장 오차역전파법 (2)  (0) 2023.09.19