본문 바로가기
파이썬 프로그래밍/Numpy 딥러닝

31. [CNN기초] 2차원 배열 합성곱 - image to column-2

by Majestyblue 2023. 1. 11.

이전 시간에는 image to coumn을 이용하여 합성곱 연산을 진행하였다. 하지만 아직도 갈 길이 많다. 이전 예제는 원리를 설명하느냐 정말 간단하게만 구현하였기 때문에 제대로 하려면 스트라이드 구현, 패딩 구현, coumn to image, 다중 채널 이미지 변환 4가지가 남아 있다. 

 

 Numpy 딥러닝 시리즈에서는 1채널 (gray)로 진행한다. 다중 채널은 현재 코드 진행으로 매우 비효율적이므로 코드를 다루지 않고 개념만 마지막에 다룰 것이다. 

 

원래 이미지는 (batch, channels, height, width) 또는 (batch, height, width, channels)로 구성되어 있는데 편의를 위해 1채널만 사용하므로 (batch, height, width)로 사용할 것이다.

 

또한 쉬운 전개를 위해 이미지, 특성맵, 필터 크기는 정사각형으로 진행하겠다.

 

1. 스트라이드(stride) 구현

 스트라이드를 구현해 보자. 이전 예제에서 필터를 한 칸씩 이동하였는데 두 칸씩 이동하여 합성곱하는 것은 어떻게 구현할 수 있을까? 아래 코드를 실행시켜 보자

 

import numpy as np
import matplotlib.pyplot as plt

X = np.reshape(np.arange(0.1, 1.7, 0.1), (-1, 4))
print(X)

W = np.array([[1, 1],
              [1, 1]])

B = np.array([[0.01]])

# 1개의 배치, 1개의 채널이 있는 이미지 출력
plt.imshow(X, cmap='gray')
plt.show()

 

 

아래와 같이 hyper parameter를 설정한다.

# 2 × 2 필터 사용
filter_size = W.shape[0]

# 두 칸씩 이동
stride = 2

# 패딩은 0
padding = 0

# 출력 특성맵 크기
output_size = int((4-2+2*0)/2 + 1) # 2

 

 

 

아래와 같이 스트라이드가 1일 때와 2일 때 입력 X의 슬라이싱은 어떤 규칙성을 갖는지 확인해 보자.

 

쉽게 이야기해서 stride = 1 일 때 index가 한 칸씩 옮겨지니, stride = 2일 땐 두 칸씩 움직인다.

이를 이용하여 index × stride라는 일반화된 식을 유도할 수 있다. 

 

 

아래 im2col 코드에서 stride*o_h , stride*o_w에 주목하길 바란다.

def im2col(input, stride, output_size, filter_size):
  count = 0
  for o_h in range(output_size):
    for o_w in range(output_size):
      a = input[stride*o_h : stride*o_h + filter_size, stride*o_w:stride*o_w + filter_size]
      out = np.reshape(a, (1, -1))
      if count == 0:
        outs = out.copy()
      else:
        outs = np.concatenate((outs, out), axis=0)
      count += 1

  return np.transpose(outs, (1, 0))


im2col_image = im2col(input=X, stride=2, output_size=2, filter_size=2)
print(im2col_image)

 

 

 

 

 

2. 패딩(padding) 구현

패딩 구현은 이전 (https://toyourlight.tistory.com/51)에서 구현한 방법을 토대로 간단하게 구현할 수 있다. 모든 방향으로 똑같이 패딩한다고 가정하자. 아래 코드처럼 쉽게 패딩을 할 수 있다.

 

padding = 1
X_copy = np.pad(X, ((padding, padding), (padding, padding)))
print(X_copy)
plt.imshow(X_copy, cmap='gray')
plt.show()

padding = 1, stride = 2로 합성곱을 한다면 출력은 ((4-2+2*1)2 + 1 =3이다. 즉 output size = 3이다. 

 

패딩 구현은 위 코드를 함수 안으로 집어넣고 매개변수 padding을 넣으면 된다.

def im2col(input, stride, padding, output_size, filter_size):
  count = 0
  input = np.pad(input, ((padding, padding), (padding, padding)))
  for o_h in range(output_size):
    for o_w in range(output_size):
      a = input[stride*o_h : stride*o_h + filter_size, stride*o_w:stride*o_w + filter_size]
      out = np.reshape(a, (1, -1))
      if count == 0:
        outs = out.copy()
      else:
        outs = np.concatenate((outs, out), axis=0)
      count += 1

  return np.transpose(outs, (1, 0))


im2col_image = im2col(input=X, stride=2, padding = 1, output_size=3, filter_size=2)
print(im2col_image)

 

 

 

 

 

 

3. column to image (col2im) 구현

column to image는 말 그대로 열을 이미지로 바꾸는 작업으로 역전파에 사용된다. 

역전파 작업 자체가 출력을 거꾸로 타고 들어가서 입력으로 가는 것이기 때문에 반대 작업이 반드시 필요하다.

(어떻게 쓰이는지는 추후 포스트에서 보여준다.)

 

아래 im2col과 col2im을 비교하며 어떤 변수를 설정하였는지 알아보자.

 

 

origin size를 추가한 이유는 원래 이미지 크기 배열을 선언하고 여기에 다시 원상복구를 시킬 것이기에 필요하다.

원래 이미지는 패딩이 없으므로 패딩을 제거하기 위해 padding 변수도 필요하다. 

 

생각보다 간단한데 im2col 연산을 반대로 하는 것이다. 어떻게 하는 것일까?

먼저 필터와 곱해지는 모양으로 바꾸어 주어야 한다.

 

 

 

그리고 필터와 곱해지는 모양으로 변형된 배열을, 원래 이미지 크기였던 배열로 다시 재지정해준다.

 

 

이를 아래와 같이 코드로 표현할 수 있다.

만약 패딩이 존재하면 np.delete를 이용하여 패딩을 제거할 수 있다.

# 열을 다시 이미지로 바꾼다. origin_size는 합성곱 전 입력된 이미지 크기로 패딩을 포함한 사이즈
# output_size는 합성곱 연산 결과 출력된 사이즈이다.
# 스트라이드까지 구현
# 패딩을 없애보자.
def col2im(input, stride, padding, filter_size, output_size, origin_size):
  input = np.transpose(input, (1, 0)) # col로 된 것을 다시 row로 바꿈
  input = np.reshape(input, (-1, filter_size, filter_size)) # row로 된 것을 다시 필터 크기로 바꾸어준다.
  output = np.zeros((origin_size, origin_size)) # 원래 이미지 크기의 0 행렬을 만들어 준다.
  index = 0
  for o_h in range(output_size):
    for o_w in range(output_size):
      output[stride*o_h : stride*o_h + filter_size, stride*o_w:stride*o_w + filter_size] = input[index] # input2row의 역연산을 수행
      index += 1
  if padding == 0:
    pass
  else:
    for i in range(padding): # 패딩 제거 작업
      output = np.delete(output, 0, axis=0) # 맨 윗 줄 없앰 
      output = np.delete(output, (output.shape[0]-1), axis=0) # 맨 아래줄 없앰 
      output = np.delete(output, 0, axis=1) # 맨 앞 줄 없앰 
      output = np.delete(output, (output.shape[1]-1), axis=1) # 맨 뒷줄 없앰 
  
  return output

 

 

 

실제로 잘 작동하는지 알아보자. 아래와 같이 이전에 실행했던 padding = 1, stride=2인 예시이다.

 

입력 X

 

 

 

 

이를 다시 역으로 하여 원래와 같은 입력 X로 만들고 싶다. 주의할 점은 padding = 1을 하였기 때문에 아래와 같은 이미지가 패딩을 붙인 이미지가 되며 origin_size = 6이 된다.

 

 

return_image = col2im(input=im2col_image, stride=2, padding = 1, filter_size=2, output_size=3, origin_size=6)
print(return_image)

오... 다시 원상복구가 되었다.

 

 

 

 

 

다른 예시를 살펴보자. 이번엔 stride = 2, padding = 2인 예시이다.

im2col_image = im2col(input=X, stride=2, padding = 2, output_size=4, filter_size=2)
print(im2col_image)

 

padding = 2 이므로 패딩까지 고려한 원래 이미지 사이즈는 8이 된다. (origin_size = 8)

return_image = col2im(input=im2col_image, stride=2, padding = 2, filter_size=2, output_size=4, origin_size=8)
print(return_image)

 

다시 원상복구가 되었다.

 

 

다음 시간부터 이를 이용하여 2차원 합성곱을 이용한 인공신경망 훈련을 어떻게 하는지 진행하겠다.