본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.
위 gif에서 수행하는 3차원 합성곱 연산을 코드로 구현하고자 한다.
import numpy as np
input_data = np.array([
[[1, 2, 3, 0],
[0, 1, 2, 3],
[3, 0, 1, 2],
[2, 3, 0, 1]],
[[1, 3, 2, 0],
[0, 1, 3, 2],
[2, 0, 3, 1],
[3, 2, 1, 0]],
[[2, 3, 1, 0],
[0, 2, 3, 1],
[1, 0, 2, 3],
[3, 1, 0, 2]]
])
kernel = np.array([
[[1, -1, 1],
[0, 1, -1],
[-1, 0, 1]],
[[-1, 1, 0],
[1, -1, 0],
[0, 1, -1]],
[[0, -1, 1],
[1, 0, -1],
[-1, 1, 0]]
])
먼저 gif의 input_data
와 kernel
을 구현한 뒤
#합성곱 연산
for y in range(out_data.shape[0]): #높이
for x in range(out_data.shape[1]): #너비
roi = input_data[:, y:y+kernel.shape[1], x:x+kernel.shape[2]]
filtered = roi * kernel #3x3x3의 값이 나온 filtered
conv_value = np.sum(filtered) #3x3x3의 모든 값을 SUM
out_data[y, x] = np.int16(conv_value) #이게 높이x너비 만큼 반복
print("Feature Map:\n", out_data)
3차원 합성곱 연산을 위한 코드를 작성하면 된다.
이때 np.sum
메서드에 '곱'연산이 완료된 filtered
만을 인자로 넣으면 filtered
의 [3x3x3]에 해당하는 모든 원소합을 수행하기에
2중 for문으로 Feature Map 연산을 수행할 수 있다.
im2col(image to column)은 Conv연산을 수행할 때 위 3채널에 해당하는 이미지를 가지고 멀티 채널 Conv을 수행했지만,
딥러닝에서 데이터를 처리할 때는
아래의 그림처럼 Batch_size
라는 항목이 하나 붙어서 4차원 데이터인 Tensor자료형을 기반으로 Conv연산을 수행한다.
이때 위 멀티 채널 Conv에서 선보인 2중 for문을 넘어선 다중 for문 구현으로 [배치사이즈, 멀티채널 Conv]을 수행해야 하기에
코드 난이도도 높아지고, 다중 for문으로 인한 연산속도의 저하도 심해진다.
이를 해결하기 위해 아래의 그림처럼 4차원에 속하는 필터(Kernel)
과 입력 이미지
를 차원 축소+행렬 형상변환(Reshape)으로
2차원 행렬 간 곱셈으로 연산을 처리하는 과정을 의미한다.
이렇게 위 사진처럼
1) 4차원 Tensor ->
2) 2차원 행렬로 전환 ->
3) 행렬 곱 수행 ->
4) 4차원 Tensor 로 전환하는 연산 과정이
다중 for문을 사용한 [배치사이즈, 멀티채널 Conv]보다 훨씬 빠르게 연산을 수행하기에 위 함수를 구현하고 이를 Conv 연산 과정에 활용한다.
이를 python 코드로 구현하면 아래의 함수와 같다
def im2col(img, kh, kw, stride=1, pad=0): #이미지, 커널사이즈(높이/너비)
N, C, H, W = img.shape #배치크기, 채널, 이미지 높이너비
out_h = (H + 2*pad - kh)//stride + 1
out_w = (W + 2*pad - kw)//stride + 1
#stride, padding을 통해 얻은 output 이미지 크기
pad_img = np.pad(array=img,
pad_width=[(0,0), (0,0), (pad, pad), (pad, pad)])
#입력 이미지의 N, C는 건너뛰고 크기, 너비에 각각 padding을 입힌 pad_img를 만들기
col = np.zeros((N, C, kh, kw, out_h, out_w)) #6차원 데이터를 일단 만든다
for y in range(kh):
y_max = y + stride*out_h
for x in range(kw):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = pad_img[:, :, y:y_max:stride,
x:x_max:stride]
#6차원 빵 데이터에 pad_img를 입력시켜준다.
col = col.transpose(0,4,5, 1,2,3).reshape(N*out_h*out_w, -1)
#N, C, kh, kw, out_h, out_w 이거를
#N, out_h, out_w, C, kh, kw 로 순번 바꾸기
#그리고 순번 바꾼 데이터를 [N * out_h * out_w, C * kw * kh] 형태의 2차원으로 reshape
#[배치 * 출력이미지, 채널 * 커널이미지] 이게 나온다.
return col
입력 이미지의 im2col
진행 과정을 설명하자면
1) Stride, Padding 인자를 고려하여 Out Feature Map [H, W]계산
1-1) 입력 이미지에 Padding을 적용
2) 중간 연산 수행을 위해 6차원 np.array
자료형 초기화
3) 2중 for문을 통해 4차원 입력 이미지를 6차원 자료형에 붙여넣기
4) 6차원 자료형의 축 재배치
5) 6차원 자료형을 2차원으로 차원 축소 및 행렬 변환
[배치 크기 *
출력이미지(높이,너비), 채널수 *
커널사이즈(높이,너비]
순으로 2차원 데이터로 변환한다.
설계한 im2col 함수에 예시 이미지 x1, x2를 삽입해보자
x1 = np.random.rand(8, 3, 5, 5) #배치사이즈, 채널, 이미지 높이너비
col1 = im2col(x1, kh=3, kw=3, stride=1, pad=0) #x1 , 커널 사이즈, stride, pad
print(col1.shape) #[배치 * 출력이미지, 채널 * 커널이미지]
x1의 [8, 3, 5, 5] 4차원 데이터는 kernel_size = [3,3]
그리고 stride=1
, padding=0
일 시
출력 이미지 크기 = [3,3]
im2col결과 : [8X3X3 = 72, 3X3X3 = 27]
으로 차원축소가 진행된다.
그 다음 입력 이미지와 Conv 연산을 수행할 Kernel도 4차원 Tensor 자료형이니 이를 2차원으로 형상변환을 수행한다
def fil2col(kernel):
filter_num, channel_num, filter_height, filter_width = kernel.shape
#커널의 개수, 이미지 채널, 커널 사이즈(높이 너비)
col = kernel.reshape(filter_num,
channel_num * filter_height * filter_width)
#4차원 커널[N, C, H, W]를
#2차원 [N, C*H*W]로 차원축소 및 행렬 형상 변환
return col
위 함수에 4차원 커널을 넣어 차원 축소 과정을 알아보자
filters = np.random.rand(6, 3, 5, 5)
fil_col = fil2col(filters)
print(fil_col.shape) #[6, 3*5*5=75]
[6, 3, 5, 5] 4차원 커널이 [6, 75] 2차원으로 차원축소가 진행되었다.
입력이미지, 커널에 대한 im2col과정을 수행해주는 2개의 함수를 설계했으니 위 함수로 행렬 연산을 수행해보자
N, C, H, W = 4, 3, 5, 5
#[배치사이즈, 채널, 이미지 높이너비]
F, kh, kw, stride, pad = 6, 3, 3, 1, 1
#[커널 개수, 커널사이즈(높이너비), stride, padding]
input_data = np.random.rand(N, C, H, W)
filters = np.random.rand(F, C, kh, kw)
첫번째로 4차원의 입력이미지, 커널, 그리고 stride
, padding
인자값을 결정하고
img_col = im2col(input_data, kh, kw, stride, pad)
print("im2col 결과:\n", img_col.shape)
fil_col = fil2col(filters)
print("fil2col 결과:\n", fil_col.shape)
각각 im2col을 수행한다.
위 과정으로 얻은 입력이미지, 커널을 행렬 곱연산을 수행한다.
conv_out = np.dot(img_col, fil_col.T)
#필터를 행렬곱 해야 하니 Transpose -> (6,27)이 (27,6)이 됨
#(100, 27) X (27,6) = (100, 6)
print("합성곱연산결과:\n", conv_out.shape)
얻어낸 Conv의 연산결과(2차원) 데이터를
[배치 사이즈, 출력 이미지(높이, 너비), 커널 개수] 순으로 4차원 자료형 변환을 진행한다.
out_h = (H + 2*pad - kh) // stride + 1
out_w = (W + 2*pad - kw) // stride + 1
conv_out = conv_out.reshape(N, out_h, out_w, F)
#합성곱의 연산 결과를 [배치 사이즈, 출력이미지(높이너비), 커널의 개수] 로 다시 차원분해
print(conv_out.shape) #(4, 5, 5, 6)
#합성곱 연산 결관는
#[배치사이즈, 출력이미지(높이너비), 커널 개수]
이렇게 Conv의 연산에는 중간에 im2col함수가 도입되어
4차원 데이터 간 합성곱 연산을 원활하게 해주는 작업이 수행된다.
이 과정에 대해 개념을 알아가자는 차원에서 해당 코드를 작성하였다.