3. 이미지 분류 · 05 CNN
CNN

합성곱/교차상관/패딩/풀링을 작은 배열로 직접 확인하기

무엇을 배울까요?

  • 합성곱이 ‘필터를 움직이며 곱해서 더하는 것’이라는 감
  • valid/full/same이 출력 크기를 어떻게 바꾸는지
  • conv2d / max pooling이 왜 CNN에 자주 나오는지

이해하기 쉬운 예시

  • 사진에서 ‘모서리’만 진하게 만들고 싶다면, 작은 필터로 주변 픽셀을 섞어 점수를 만들 수 있어요. 이게 합성곱의 출발점이에요.
01

1차원 합성곱(직접 출력)

이 섹션에서 하는 일

먼저 1차원 배열로 ‘필터를 뒤집고(dot) 곱해서 더하는’ 계산을 직접 찍어보면서 감을 잡아요.

코드(원문 그대로)
#합성곱
import numpy as np
w = np.array([2, 1, 5, 3])
x = np.array([2, 8, 3, 7, 1, 2, 0, 4, 5])
#w 배열을 뒤집어서 출력
w_r = np.flip(w)
print(w_r)
#합성곱 계산
# w_r을 x의 왼쪽 자리에 맞추고 각 인덱스마다 곱한 후 더함
# 2x3 + 8x5 + 3x1 + 7x2 = 63
# w_r을 오른쪽으로 한자리 shift하여 곱셈
for i in range(6):
print(np.dot(x[i:i + 4], w_r))
예상 출력
[3 5 1 2]
63
48
49
28
21
20
[63 48 49 28 21 20]
[48 57 24 25 16 39]
[ 6 34 51 48 57 24 25 16 39 29 13 10]
[34 51 48 57 24 25 16 39 29]
[[ 2  4]
 [ 8 10]]
[[ 2  4  6]
 [ 8 10 12]
 [14 16 18]]
[[ 2.  4.  6.]
 [ 8. 10. 12.]
 [14. 16. 18.]]
[[ 6.  8.]
 [14. 16.]]
출력 해설(왜 이게 나오나요?)

처음에는 w를 뒤집어서(w_r) x의 4개 구간씩 곱해서 더해요. 그래서 print(w_r)로 뒤집힌 필터가 맞는지 확인하고, 그 다음 숫자 63,48,...은 각 위치에서 만든 ‘점수(특징)’예요. 마지막 [63 48 ...]은 scipy convolve(valid)로 같은 결과가 한 번에 나온 거예요.

02

scipy 합성곱/교차상관

이 섹션에서 하는 일

이제 scipy 함수로 같은 계산을 한 번에 해봐요. valid/full/same은 패딩 처리 방식 차이예요.

코드(원문 그대로)
##############################
#사이파이에서 제공하는 합성곱 함수
#w를 뒤집어서 곱하는 방식
from scipy.signal import convolve
# valid - 원본 배열에 패딩을 추가하지 않는 방식
# 원본 이미지가 4x4인 경우 결과물이 3x3으로 줄어드는 방식
convolve(x, w, mode='valid')
예상 출력
(위 출력 중에서 convolve/correlate 결과가 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

여기서는 같은 계산을 ‘함수로’ 확인해요. convolve(valid)는 패딩 없이 딱 맞는 구간만 계산해서 길이가 줄고, correlate는 필터를 뒤집지 않는 방식(교차상관)이라 값이 달라져요. full은 양쪽에 0을 붙였다고 생각하면 더 길게 나오고, same은 원본 길이를 맞추려고 길이가 x와 같게 나와요.

03

2차원 교차상관

이 섹션에서 하는 일

2차원에서도 같은 아이디어가 적용돼요. correlate2d로 결과 모양을 확인해요.

코드(원문 그대로)
##############################
#합성곱 신경망에서는 w를 뒤집지 않고 그대로 곱하는 교차상관 방식을 사용함
#초기 가중치값은 랜덤으로 만들어지므로 뒤집어서 곱하는 것과 뒤집지 않고 곱하는 것이 큰 의미가 없음
#정확히 표현하면 교차상관이지만 합성곱 신경망이라는 이름을 관례적으로 사용하고 있음
#교차상관 - w를 뒤집지 않고 곱하는 방식
from scipy.signal import correlate
correlate(x, w, mode='valid')
예상 출력
(위 출력 중에서 correlate2d 결과가 2x2, 3x3 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

2차원에서는 2x2 필터를 3x3 입력 위로 움직이며 곱해서 더해요. valid는 겹치는 곳만 계산해서 2x2가 나오고, same은 가장자리까지 계산하려고 패딩을 넣어서 3x3이 나와요. 출력 배열의 각 숫자는 ‘그 위치에서 필터가 본 특징 점수’예요.

04

TensorFlow conv2d

이 섹션에서 하는 일

TensorFlow conv2d는 입력/필터를 4차원으로 맞춰야 해요. SAME 패딩 결과를 확인해요.

코드(원문 그대로)
##############################
#full 패딩 - 제로패딩을 한 후 연산을 하게 되면 원본 배열의 모든 원소가 연산에 동일하게 참여하게 됨
correlate(x, w, mode='full')
예상 출력
(위 출력 중에서 conv2d 결과가 3x3 실수 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

TensorFlow는 (배치,높이,너비,채널) 모양을 요구해서 reshape를 해요. padding='SAME'이라 출력이 입력(3x3)과 같은 크기로 나오고, 값이 실수로 보이는 건 dtype을 float64로 넣었기 때문이에요.

05

MaxPooling

이 섹션에서 하는 일

마지막으로 max_pool2d로 2x2 영역에서 ‘가장 큰 값만 남겨서’ 줄이는 걸 확인해요.

코드(원문 그대로)
##############################
#출력 배열의 길이가 원본 배열의 길이와 같아지도록 제로 패딩을 추가하는 방식
#합성곱 신경망에서 많이 사용하는 방식
correlate(x, w, mode='same')
예상 출력
(위 출력 중에서 max_pool2d 결과가 2x2 실수 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

MaxPooling은 2x2 칸마다 가장 큰 값만 남겨서 정보를 요약해요. 그래서 4x4 입력이 2x2로 줄고, 각 칸의 출력은 그 2x2 구역의 ‘최댓값’이 돼요.

06

MaxPooling

이 섹션에서 하는 일

마지막으로 max_pool2d로 2x2 영역에서 ‘가장 큰 값만 남겨서’ 줄이는 걸 확인해요.

코드(원문 그대로)
##############################
#2차원 배열에 대한 합성곱 계산
from scipy.signal import correlate2d
x = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
w = np.array([[2, 0],
[0, 0]])
correlate2d(x, w, mode='valid')
예상 출력
(위 출력 중에서 max_pool2d 결과가 2x2 실수 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

MaxPooling은 2x2 칸마다 가장 큰 값만 남겨서 정보를 요약해요. 그래서 4x4 입력이 2x2로 줄고, 각 칸의 출력은 그 2x2 구역의 ‘최댓값’이 돼요.

07

MaxPooling

이 섹션에서 하는 일

마지막으로 max_pool2d로 2x2 영역에서 ‘가장 큰 값만 남겨서’ 줄이는 걸 확인해요.

코드(원문 그대로)
##############################
#제로패딩을 하여 원본과 같은 사이즈로 출력되도록 함
x = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
w = np.array([[2, 0],
[0, 0]])
correlate2d(x, w, mode='same')
예상 출력
(위 출력 중에서 max_pool2d 결과가 2x2 실수 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

MaxPooling은 2x2 칸마다 가장 큰 값만 남겨서 정보를 요약해요. 그래서 4x4 입력이 2x2로 줄고, 각 칸의 출력은 그 2x2 구역의 ‘최댓값’이 돼요.

08

MaxPooling

이 섹션에서 하는 일

마지막으로 max_pool2d로 2x2 영역에서 ‘가장 큰 값만 남겨서’ 줄이는 걸 확인해요.

코드(원문 그대로)
##############################
#텐서플로에서 지원하는 합성곱 함수
import tensorflow as tf
#4차원 배열을 사용해야 함
x = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 입력값: reshape(batch, height, width, channel)
x_4d = x.astype(np.float64).reshape(1, 3, 3, 1) #실수형으로 입력해야 함
# 필터(가중치) reshape(height,width,channel,가중치의개수)
w_4d = w.reshape(2, 2, 1, 1)
#SAME 대문자로 작성해야 함
c_out = tf.nn.conv2d(x_4d, w_4d, strides=1, padding='SAME')
# 텐서를 넘파이 배열로 변환
print(c_out.numpy().reshape(3, 3))
예상 출력
(위 출력 중에서 max_pool2d 결과가 2x2 실수 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

MaxPooling은 2x2 칸마다 가장 큰 값만 남겨서 정보를 요약해요. 그래서 4x4 입력이 2x2로 줄고, 각 칸의 출력은 그 2x2 구역의 ‘최댓값’이 돼요.

09

MaxPooling

이 섹션에서 하는 일

마지막으로 max_pool2d로 2x2 영역에서 ‘가장 큰 값만 남겨서’ 줄이는 걸 확인해요.

코드(원문 그대로)
##############################
#맥스풀링
# 입력값: reshape(샘플수, height, width, channel)
x = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]])
x = x.reshape(1, 4, 4, 1)
# ksize 커널사이즈 2x2, strides 이동간격
p_out = tf.nn.max_pool2d(x, ksize=2, strides=2, padding='SAME')
print(p_out.numpy().reshape(2, 2))
예상 출력
(위 출력 중에서 max_pool2d 결과가 2x2 실수 배열로 출력돼요)
출력 해설(왜 이게 나오나요?)

MaxPooling은 2x2 칸마다 가장 큰 값만 남겨서 정보를 요약해요. 그래서 4x4 입력이 2x2로 줄고, 각 칸의 출력은 그 2x2 구역의 ‘최댓값’이 돼요.

핵심 한 줄