합성곱 신경망(Convolutional Neural Network, CNN)은 이미지와 같은 공간적 데이터의 특징을 효과적으로 학습하기 위한 신경망 아키텍처입니다.
CNN은 주로 이미지 처리 작업에서 사용되며, 이를 위해 입력 데이터의 지역적 특성을 추출하고, 이를 통해 객체 인식, 분류, 얼굴 인식 등의 다양한 컴퓨터 비전 작업을 수행합니다.
이 글에서는 합성곱 신경망(CNN)에 대해 알아보고 파이썬 코드로 예제로 실험해 보겠습니다.
목차
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
참고 문헌 및 참고자료: 밑바닥부터 시작하는 딥러닝
합성곱 신경망?
기존의 Affine 계층으로 이루어진 완전 연결 신경망은 모든 입력 데이터를 평탄화 (즉 flatten) 하여 신경망 학습을 하였습니다.
import numpy as np
x_input=np.random.rand(1,28,28,3)
x_input_flatten=x_input.flatten() #flatten하여 차원을 1차원 벡터로 변환
print(x_input_flatten)
하지만 이렇게 되면 이미지와 같은 경우 근접한 점과의 관련성이 무너져 학습이 어려워 지게 됩니다. 하지만 합성곱 신경망 은 이런 단점을 보완하여 입력 이미지의 지역적 특성을 추출할 수 있게 됩니다.
- 합성곱층: 작은 필터인 Kernel을 이용해 입력 이미지를 슬라이딩하면서 이미지의 경계, 질감, 색상 등의 특징을 추출
- 풀링층: 합성곱 층에서의 출력의 공간을 효율적으로 줄이고, 계산양을 줄이기 위해 사용
Max Pooling, Average Pooling 등을 이용 - 완전 연결층: CNN의 마지막 출력을 생성하는 층 (Affine 이나 퍼셉트론)
- 활성화 함수: 대표적으로 합성곱층에서 사용하는 활성화 함수는 Relu 입니다.
CNN은 주로 이미지 분류, 객체 검출, 얼굴 인식, 자율 주행 차량의 환경 인식 등 다양한 컴퓨터 비전 작업에서 사용되며, 이미지 데이터의 공간적인 특징을 효과적으로 학습하는 데 강점을 가지고 있습니다.
합성곱 신경망의 연산 (Conv)
입력 데이터를 아래와 같이 필터 사이즈와 동일한 크기만큼 각 요소에 곱하여 더해줍니다.
여기서 필터(Kernal)는 가중치의 역할을 하게 됩니다. (최종적으로 필터에서 나온 값에 편향을 더해줍니다.)
여기서 \bigodot 는 아다마르 곱 (Hadamard product) 이라고 하고, 각 행렬의 요소별 곱을 수행합니다. 그리고 전부 더해서 결과 행렬에 집어넣습니다.
즉 \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \\ \end{bmatrix} \bigodot \begin{bmatrix} b_{11} & b_{12} & b_{13} \\ b_{21} & b_{22} & b_{23} \\ b_{31} & b_{32} & b_{33} \\ \end{bmatrix} = \begin{bmatrix} (a_{11} \times b_{11}) & (a_{12} \times b_{12}) & (a_{13} \times b_{13}) \\ (a_{21} \times b_{21}) & (a_{22} \times b_{22}) & (a_{23} \times b_{23}) \\ (a_{31} \times b_{31}) & (a_{32} \times b_{32}) & (a_{33} \times b_{33}) \\ \end{bmatrix}
위의 그림에서 빨간색 부분을 계산해 보면 \begin{bmatrix} 1 & 2 & 3 \\ 0 & 1 & 2 \\ 3 & 0 & 1 \\ \end{bmatrix} \bigodot \begin{bmatrix} 2 & 0 & 1 \\ 0 & 1 & 2 \\ 1 & 0 & 2 \\ \end{bmatrix} = \begin{bmatrix} (1 \cdot 2) & (2 \cdot 0) & (3 \cdot 1) \\ (0 \cdot 0) & (1 \cdot 1) & (2 \cdot 2) \\ (3 \cdot 1) & (0 \cdot 0) & (1 \cdot 2) \\ \end{bmatrix} = \begin{bmatrix} 2 & 0 & 3 \\ 0 & 1 & 4 \\ 3 & 0 & 2 \\ \end{bmatrix}
(2+0+3+1+4+3+0+2) + 편향 =15 + 3 = 18
합성곱 신경망의 결과값의 (1,1) 부분이 이 계산되었고 이를 이동시키면서 반복하면 됩니다.
한 칸 씩 이동하는 부분의 반복문을 파이썬으로 작성하면 아래와 같습니다..
x_input = np.array([[1, 2, 3, 0],
[0, 1, 2, 3],
[3, 0, 1, 2],
[2, 3, 0, 1]])
kernel = np.array([[2, 0, 1],
[0, 1, 2],
[1, 0, 2]])
'''
우선 총 출력의 사이즈는
출력의 높이(행의 갯수) = 입력값의 높이 - 필터의 높이 + 1
출력의 너비(열의 갯) = 입력값의 너비 - 필터의 너비 + 1
반복문은 출력층 높이만큼, 너비만큼 반복시키면 되겠습니다. 즉 반복을 1회 할때마다 하나의 요소에 채워지게 됩니다.
0,0 -> 0,1 -> 1,0 -> 1,1 순으로 채워지게 됩니다.
즉
output[0,0] = np.sum(x_input[0:3,0:3] * kernel)
output[0,1] = np.sum(x_input[0:3,1:4] * kernel)
output[1,0] = np.sum(x_input[1:4,0:3] * kernel)
output[1,1] = np.sum(x_input[1:4,1:4] * kernel)
아래의 for문은 위의 코드를 반복하게 합니다.
'''
for i in range(0, input_height - kernel_height + 1):
for j in range(0, input_width - kernel_width + 1):
output[i, j] = np.sum(padded_input[i:i+kernel_height, j:j+kernel_width] * kernel)
이제 전체 코드를 살펴보면 아래와 같이 작성해 볼 수 있겠습니다.
import numpy as np
# 입력 행렬과 커널 정의
x_input = np.array([[1, 2, 3, 0],
[0, 1, 2, 3],
[3, 0, 1, 2],
[2, 3, 0, 1]])
kernel = np.array([[2, 0, 1],
[0, 1, 2],
[1, 0, 2]])
# 입력 행렬과 커널의 차원 가져오기
input_height, input_width = x_input.shape # 4,4
kernel_height, kernel_width = kernel.shape # 3,3
# 출력 행렬 초기화
output_height = input_height - kernel_height + 1 # 4-3+1 = 2
output_width = input_width - kernel_width + 1 # 4-3+1 = 2
output = np.zeros((output_height, output_width)) #2,2 인 0행렬 만들
# 합성곱 연산 수행
for i in range(output_height):
for j in range(output_width):
output[i, j] = np.sum(x_input[i:i+kernel_height, j:j+kernel_width] * kernel)
output += 3
# 결과 출력
print("\n합성곱 결과:")
print(output)
합성곱 결과: \begin{pmatrix} 18.&19. \\ 9. & 18. \end{pmatrix} 와 같이 나오게 됩니다.
합성곱 신경망: 패딩(Padding)과 스트라이드(Stride)
합성곱에 패딩과 스트라이드 개념이 있습니다.
- 패딩: 입력 데이터 끝에 0과 같은 특정 값으로 크기를 채웁니다. (폭이 1이고, 0인 패딩을 적용했습니다.)
- 스트라이드: 필터를 적용할 때 한 번에 이동하는 거리입니다.
출력의 크기: 출력의 크기는 입력의 크기, 패딩, 스트라이드, 필터의 크기에 따라 다릅니다.
출력의 크기(H) \text{} = \frac{\text{입력 높이} + 2 \times \text{패딩}-\text{필터의 높이}}{ \text{스트라이드}} +1
출력의 너비(W) = \frac{\text{입력 너비} + 2 \times \text{패딩}-\text{필터의 너비}}{ \text{스트라이드}} +1
패딩과 스트라이드를 추가한 합성곱 연산 코드는 다음과 같습니다.
padding을 0, stride를 1로 설정하면 이전과 동일한 결과가 나오며, 원하는대로 다른 값으로 수정하여 적용할 수 있습니다.
import numpy as np
# 입력 행렬과 커널 정의
x_input = np.array([[1, 2, 3, 0],
[0, 1, 2, 3],
[3, 0, 1, 2],
[2, 3, 0, 1]])
kernel = np.array([[2, 0, 1],
[0, 1, 2],
[1, 0, 2]])
# 패딩과 스트라이드 정의
padding = 0
stride = 1
# 입력 행렬과 커널의 차원 가져오기
input_height, input_width = x_input.shape
kernel_height, kernel_width = kernel.shape
# 출력 행렬 크기 계산
output_height = (input_height - kernel_height + 2 * padding) // stride + 1
output_width = (input_width - kernel_width + 2 * padding) // stride + 1
# 출력 행렬 초기화
output = np.zeros((output_height, output_width))
# 패딩 적용
padded_input = np.pad(x_input, ((padding, padding), (padding, padding)), mode='constant')
# 합성곱 연산 수행
for i in range(0, input_height - kernel_height + 2 * padding + 1, stride):
for j in range(0, input_width - kernel_width + 2 * padding + 1, stride):
output[i // stride, j // stride] = np.sum(padded_input[i:i+kernel_height, j:j+kernel_width] * kernel)
output += 3
# 결과 출력
print("\n합성곱 결과:")
print(output)
이제 이 결과를 Relu층을 통과시켜 다음 층으로 전달하면 됩니다.
\text{Relu}(x) = \text{max}(0,x)합성곱 신경망: 풀링층 (Pooling)
위의 과정으로 Conv 층과 Relu층을 거친 값에 대하여 Pooling층을 거쳐 다음 Conv에 보내게 됩니다.
합성곱 신경망: 풀링층이란
입력 데이터의 공간 차원을 줄이는 역할을 합니다. 이를 통해 계산 효율성을 높이고 특징 추출을 개선하는데 도움을 줍니다. Pooling 층은 특히 이미지 처리와 관련된 작업에서 널리 사용됩니다.
풀링층의 이점은 다음과 같습니다.
- 공간 차원 축소: 입력 데이터의 공간 차원(가로 및 세로 차원)을 줄여 메모리와 계산량을 절약합니다.
- 위치 불변성: 작은 변화에 대해 민감하지 않도록 만들어, 이미지나 특징에 대한 이동, 크기 조절, 회전 등의 변환에 강인합니다.
- 특징 강화: 중요한 정보를 강조하고, 노이즈나 불필요한 세부 정보를 제거하여 특징 추출을 개선합니다.
합성곱 신경망: 풀링층의 종류
합성곱 신경망: Max Pooling (최대 풀링):
- 각 풀링 영역에서 가장 큰 값을 선택하여 출력합니다.
- 주로 가장 많이 사용되며, 강력한 특징 추출 능력을 가집니다.
- 예시: 3×3 Max Pooling은 3×3 영역에서 가장 큰 값 하나를 선택하여 출력합니다.
import numpy as np
conved_input = np.array([[2, 1, 3, 1, 4],
[4, 5, 3, 2, 5],
[7, 5, 4, 3, 4],
[5, 2, 5, 6, 1],
[5, 1, 5, 7, 11]])
max_pooling_size = 3
max_pooling_stride = 2
# 입력 배열의 크기와 풀링 크기를 기반으로 결과 배열의 크기를 계산합니다.
input_height, input_width = conved_input.shape
pooling_height, pooling_width = max_pooling_size, max_pooling_size
stride = max_pooling_stride
output_height = ((input_height - pooling_height) // stride) + 1
output_width = ((input_width - pooling_width) // stride)+ 1
# 결과 배열 초기화
max_pooling_result = np.zeros((output_height, output_width))
# 최대 풀링 연산 수행
for i in range(output_height):
for j in range(output_width):
# 풀링 영역 선택
row_start = i * stride
row_end = row_start + pooling_height
col_start = j * stride
col_end = col_start + pooling_width
# 선택한 풀링 영역에서 최댓값을 찾아 결과 배열에 저장
max_pooling_result[i, j] = np.max(conved_input[row_start:row_end, col_start:col_end])
print("Max Pooling Result:")
print(max_pooling_result)
결과: Max Pooling Result: = \begin{pmatrix} 7 & 5 \\ 7 & 11 \end{pmatrix}
합성곱 신경망: Average Pooling (평균 풀링):
- 각 풀링 영역의 평균 값을 계산하여 출력합니다.
- Max Pooling보다는 정보를 약간 더 보존하며, 더 부드러운 특징을 추출합니다.
- 예시: 3×3 Average Pooling은 3×3 영역의 값을 평균하여 출력합니다.
import numpy as np
conved_input = np.array([[2, 1, 3, 1, 4],
[4, 5, 3, 2, 5],
[7, 5, 4, 3, 4],
[5, 2, 5, 6, 1],
[5, 1, 5, 7, 11]])
pooling_size = 3
pooling_stride = 2
# 입력 배열의 크기와 풀링 크기를 기반으로 결과 배열의 크기를 계산합니다.
input_height, input_width = conved_input.shape
pooling_height, pooling_width = pooling_size, pooling_size
stride = pooling_stride
output_height = ((input_height - pooling_height) // stride) + 1
output_width = ((input_width - pooling_width) // stride) + 1
# 결과 배열 초기화
average_pooling_result = np.zeros((output_height, output_width))
# 평균 풀링 연산 수행
for i in range(output_height):
for j in range(output_width):
# 풀링 영역 선택
row_start = i * stride
row_end = row_start + pooling_height
col_start = j * stride
col_end = col_start + pooling_width
# 선택한 풀링 영역에서 평균값을 결과 배열에 저장
average_pooling_result[i, j] = np.mean(conved_input[row_start:row_end, col_start:col_end])
print("Average Pooling Result:")
print(average_pooling_result)
결과: Average Pooling Result: = \begin{pmatrix} 3.77777778 & 3.22222222 \\ 4.33333333 & 5.11111111 \end{pmatrix}
이제 이 결과를 다음 Conv 층으로 보내면 됩니다.
위의 예제는 하나의 차원에서만 수행을 했지만 실제로는 3차원 (RGB) 차원에서 수행해 줍니다.
Im2Col
Im2Col은 이미지 처리와 합성곱 신경망(Convolutional Neural Network, CNN)과 관련된 연산 중 하나입니다. Im2Col (img to column) 의 약자 입니다.
Im2Col 작업을 도표로 표현하면 아래와 같이 표현할 수 있습니다. 그림을 보면 알 수 있듯이 4차원인 입력값을 2차원으로 줄여 계산 비용을 줄일 수 있습니다.
첫 번째 사진의 R 과 그 필터를 예시를 한번 들어 계산을 해 보자면
이미지 = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} 에서 적용할 필터가 2 x 2 = \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} 이고 이동거리(stride)가 1 라고 할때 im2col을 적용하면 아래와 같이 됩니다.
\begin{bmatrix} 1 & 2 & 4 & 5 \\ 2 & 3 & 5 & 6 \\ 4 & 5 & 7 & 8 \\ 5 & 6 & 8 &9 \end{bmatrix} \times \begin{bmatrix} 1 \\ 2 \\ 3 \\ 4 \end{bmatrix} = \begin{bmatrix} 37 \\ 47 \\ 67 \\ 77 \end{bmatrix} 모양을 다시 만들면 \begin{bmatrix} 37 & 47 \\ 67 & 77 \end{bmatrix} 의 결과가 나옵니다.
이제 위의 내용을 파이썬으로 구현을 해 보겠습니다. 우선 필요한 클래스와 메서드를 정의해 보겠습니다.
import numpy as np
class Im2ColConverter:
def __init__(self, filter_h, filter_w, stride=1, pad=0):
#필터의 높이, 너비 이동 패딩 초기화
self.filter_h = filter_h
self.filter_w = filter_w
self.stride = stride
self.pad = pad
def im2col(self, input_data):
'''
im2col의 과정
우선 갯수, 채널수, 높이, 너비의 모양을 불러옵니다. 이는 결괏값의 모양을 만듭니다.
패딩을 넣어줍니다. 결과의 모양을 고려해 0으로 채워줍니다. (초기화 과정)
'''
N, C, H, W = input_data.shape #
out_h = (H + 2 * self.pad - self.filter_h) // self.stride + 1
out_w = (W + 2 * self.pad - self.filter_w) // self.stride + 1
img = np.pad(input_data, [(0, 0), (0, 0), (self.pad, self.pad), (self.pad, self.pad)], 'constant')
col = np.zeros((N, C, out_h, out_w, self.filter_h, self.filter_w))
#반복문을 이용해 위에서 초기화된 0들을 각각 채워줍니다.
for y in range(self.filter_h):
y_max = y + self.stride * out_h
for x in range(self.filter_w):
x_max = x + self.stride * out_w
col[:, :, :, :, y, x] = img[:, :, y:y_max:self.stride, x:x_max:self.stride]
#이제 2차원으로 만들어줍니다. 또한 아웃풋의 높이와 너비를 최종적으로 모양을 만들기위해 같이 내보냅니다.
col = col.flatten().reshape(-1, self.filter_h * self.filter_w)
return col, out_h, out_w
def im2col_conv(col ,filter_matrix , out_h, out_w):
'''
합성곱 과정
입력값 col, filter_matrix를 받아 합성곱을 진행하고,
결과적으로 out_h 와 out_w모양으로 재배열하는 과정입니다.
'''
filter_index = 0
conv_result=np.zeros(col.shape[0])
for i in range(0, col.shape[0],4):
conv_result[i:i+filter_matrix[-1].shape[0]]=np.dot(col[i:i+filter_matrix[-1].shape[0]], filter_matrix[filter_index].T)
filter_index += 1
if filter_index == filter_matrix.shape[0]:
filter_index = 0
conv_result = conv_result.reshape(-1,filter_matrix.shape[0],out_h,out_w) #갯수, 채널, 행 ,열 로 재배열 하기
return conv_result
여기서 사진 입력값과 필터값은 실제 사진을 고려해 각각 4차원 (사진수, RGB, 높이, 너비) , 3차원 (RGB, 높이 너비) 를 맞춰줘야 합니다.
즉 파이썬으로 구현할때 위의 간단한 입력 R 과 그에 맞는 필터 값은 각각 입력값과 필터값은
x = np.array([[[[1,2,3],
[4,5,6],
[7,8,9]]]])
filter_matrix = np.array([[[1,2],
[3,4]]])
print('input shape: ',x.shape)
print('filter shape: ',filter_matrix.shape)
#결괏값
#input shape: (1, 1, 3, 3) (사진수, 채널수, 높이, 너비)
#filter shape: (1, 2, 2) (채널수, 높이, 너비)
위와 같이 [ ] 괄호를 넣 차원수를 맞춰줘야 합니다.
이후 아래코드 와 같이 진행을 시켜줍니다.
channel = x.shape[1]
numberof_pic= x.shape[0]
filter_height = filter_matrix.shape[1]
filter_width = filter_matrix.shape[2]
# Im2Col 변환기 생성
im2col_converter = Im2ColConverter(filter_h=filter_height, filter_w=filter_width, stride=stride)
# Im2Col 변환
col , out_h, out_w = im2col_converter.im2col(x)
# 필터를 (-1, 4)로
filter_matrix = filter_matrix.reshape(-1,filter_matrix.shape[1]*filter_matrix.shape[2])
result = im2col_conv(col, filter_matrix, out_h, out_w)
'''
최종결과
array([[[[37., 47.],
[67., 77.]]]])
'''
최종 결괏값으로 위와 같이 나왔고 아래와 같이 4차원 이미지와, 3차원 필터를 랜덤으로 지정해주어 사용해 테스트를 해 보겠습니다.
np.random.seed(0)
n, c, h, w = 1, 3, 6, 5
fh, fw = 5,4
stride = 1
x = np.random.randint(-2,3,n*c*h*w).reshape(n,c,h,w)
filter_matrix = np.random.randint(-2,3,c*fh*fw).reshape(c,fh,fw)
# x = np.array([[[[1,2,3],[4,5,6],[7,8,9]]]])
# filter_matrix = np.array([[[1,2],[3,4]]])
print('input shape: ',x.shape)
print('filter shape: ',filter_matrix.shape)
channel = x.shape[1]
numberof_pic= x.shape[0]
filter_height = filter_matrix.shape[1]
filter_width = filter_matrix.shape[2]
# Im2Col 변환기 생성
im2col_converter = Im2ColConverter(filter_h=filter_height, filter_w=filter_width, stride=stride)
# Im2Col 변환
col , out_h, out_w = im2col_converter.im2col(x)
# 필터를 (3, 4)로 리쉐입
filter_matrix = filter_matrix.reshape(-1,filter_matrix.shape[1]*filter_matrix.shape[2])
result = im2col_conv(col, filter_matrix, out_h, out_w)
result
'''
input shape: (1, 3, 6, 5)
filter shape: (3, 5, 4)
array([[[[ -7., 0.],
[ 3., -7.]],
[[ 1., -2.],
[ -8., 9.]],
[[-18., -9.],
[-10., 7.]]]])
'''
오류없이 잘 진행이 되는 것을 알 수 있습니다. 중요한 점은 채널 수는 동일하게 맞춰주고 필터의 크기는 사진의 크기보다 작아야 합니다.
결론:
“이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.”
합성곱 신경망에 대해 간략히 알아보고 코드로 실행해 보았습니다. 위의 내용인 합성곱 신경망을 결론적으로 요약하면 다음과 같습니다.
- 합성곱층(Convolutional Layer): 입력 데이터에 작은 필터(Kernel)를 슬라이딩하면서 이미지의 지역적 특성을 추출하는 역할을 합니다. 각 위치에서 필터와 입력 데이터의 아다마르 곱을 계산하고 편향을 더하여 출력을 생성합니다.
- 패딩(Padding)과 스트라이드(Stride): 패딩은 입력 데이터 주변에 0 또는 특정 값으로 패딩을 추가하여 출력 크기를 조절하는데 사용됩니다. 스트라이드는 필터를 이동시키는 거리를 나타내며, 출력 크기에 영향을 미칩니다.
- 풀링층(Pooling Layer): 입력 데이터의 공간 차원을 축소하여 메모리와 계산량을 줄이고 특징 추출을 개선합니다. Max Pooling과 Average Pooling이 주로 사용되며, 각각 최대값과 평균값을 사용하여 출력을 생성합니다.
- 활성화 함수(Activation Function): 주로 Relu 함수가 사용되며, 음수 입력을 0으로 만들어 신경망의 비선형성을 증가시킵니다.
- Im2Col(Img to Column): Im2Col은 입력 이미지와 필터를 사용하여 합성곱 연산을 효율적으로 수행할 수 있도록 데이터를 변환.
합성곱 신경망 (CNN)은 이미지 처리 작업뿐만 아니라 다양한 컴퓨터 비전 작업에 활용되며, 입력 데이터의 공간적 특성을 효과적으로 학습하여 높은 성능을 보입니다.
함께 참고하면 좋은 글
1 thought on “합성곱 신경망: 딥러닝 기초 시리즈 8”