[밑바닥딥러닝] 16. 합성곱 신경망(CNN) (2)

2021. 10. 19. 20:06Deep Learning

본 게시글은 한빛미디어 『밑바닥부터 시작하는 딥러닝, 사이토 고키, 2020』의 내용을 참조하였음을 밝힙니다.

 

 

풀링(Pooling)


2x2 Max 풀링

풀링(Pooling)은 합성곱 연산을 마친 특징 맵에서 특정 윈도우마다의 최대값이나 평균값을 계산하여 

 

또 하나의 특징맵을 만들어내는 과정을 말한다. 위 그림에서는 2x2 구간마다의 최대값을 뽑아내는 

 

Max 풀링을 나타낸다. Max 풀링 외에도 해당 윈도우의 평균값을 계산하는 평균 풀링(average pooling)도 존재한다. 

 

보통 풀링의 윈도우와 스트라이드는 같은 값으로 설정한다. 

 

풀링 계층은 학습해야할 가중치가 없다는 특징이 있다. 

 

 

 

 

합성곱 계층 구현


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

합성곱 계층에서는 필터에 사용되는 가중치들과 편향, 그리고 필터에 적용될 스트라이드와 입력 데이터에 

 

적용되는 패딩 정보를 입력으로 받는다. 

class Convolution:
   .
   .
    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

Convolution의 순전파(forward)는 위와 같다.

        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

 먼저 입력 데이터의 크기, 필터의 크기, 패딩, 스트라이드를 근거로 출력 특징 맵의 형상(shape)를 계산한다. 

 

im2col 함수에서는 2차원 데이터를 column 형태로 reshape해주는 함수이다.

    x = np.random.rand(4,3,3,3)
    col = im2col(x, 2, 2, stride=1, pad=0)
    print(col.shape)
    
    결과>>>
    (16, 12)

 3개의 채널을 가진 3*3 크기의 데이터 4개의 배치 데이터에 im2col 함수를 적용한 결과이다. 

im2col

이렇게 필터가 적용되는 구간(윈도우)를 기준으로 데이터를 일렬로 펼치면 행렬 연산이 가능해져서 

 

다수의 데이터를 동시에 처리하기 수월해진다. 

        out = np.dot(col, col_W) + self.b
        out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

im2col이 적용된 입력 데이터와 필터를 행렬곱(np.dot)해준 뒤에 편향(b)을 각 인덱스에 더해준다. 

 

reshape 함수와 transpose 함수로 행렬 데이터를 원래의 형태로 되돌려준다.

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

        return out

역전파를 고려하여 계층에 머물렀던 데이터들을 객체 내부에 저장해둔다. 

 

마지막으로 연산된 결과(out)을 반환한다.

class Convolution:
    .
    .
    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) 구현 코드이다. 

 

흘러온 역전파값(dout)을 재형상(reshape)해주고, 편향(b)는 순전파 시 같은 값이 여러 번 반복적으로 더해졌으므로 

 

역전파 값의 0번째 축을 기준으로 더해진다. 필터 가중치(W)의 그레디언트는 역전파값과 순전파 시 입력데이터와

 

곱한 값이 된다.(입력 데이터(x)의 그레디언트(dx)도 같은 원리) 각 그레디언트를 객체 변수(self.)에 저장한다.

 

마지막으로 입력데이터의 그레디언트(dx)를 반환한다. 

 

 

 

 

풀링 계층 구현


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

풀링 계층은 풀링하려는 크기와 스트라이드 패딩 정보를 입력 받는다. 

 

class Pooling:
    .
    .
    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

im2col

im2col 함수에서는 위와 같이 한 행에 윈도우가 적용되는 한 이미지 데이터를 (전체 채널까지) 일렬로 펼친다. 

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

im2col 함수에 풀링에 대한 정보를 전달하고 입력 데이터(x)를 일렬로 펼치고 

        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)

1번째 축(열)을 기준으로 가장 큰 인덱스을 찾고, col에서 해당 값을 찾는다. (Max 풀링)

        self.x = x
        self.arg_max = arg_max

        return out

풀링 계층을 통과한 입력 데이터(x)와 가장 큰 값의 인덱스를 객체 내부에 저장하고, 최종 연산 결과(out)을 반환한다. 

 

class Pooling:
    .
    .
    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

풀링 계층의 역전파에서는 dout을 평탄화(flatten)한 값을 dmax에 저장하고, 이를 적절히 재형상(reshape)한 뒤 

 

col2im 함수를 통해 데이터를 이미지화한 뒤에 반환한다.