합성곱 신경망(Convolution Neural Network) 이란 무엇일까요? 먼저 들어가기 전 간단한 요약을 보고 들어가겠습니다. 해당 요약을 잘 숙지해주시고 본문의 내용을 관심있게 뵈주시길 부탁드립니다.
ⓐ 인간의 시신경을 모방(다중 퍼셉트론)하여 만든 딥러닝 구조 중 하나
ⓑ 합성곱을 이용하여 이미지의 공간적 특징을 유지
본 글을 시작하기 앞서 알아두면 좋은 내용을 추천한다.
합성곱 신경망(CNN)의 필요성
1998년 Yann LeCun, et. al.가 손 글씨 숫자를 인식하는 딥러닝 구조 LeNet-5를 발표하며 현재 합성곱 신경망의 초석이 되었습니다. 관련해서 자세한 내용은 논문을 참고 부탁드리며, 향후 본 블로그에서 자세히 설명하도록 하겠습니다.
오늘은 합성곱 신경망의 핵심이 되는 합성곱(convolution)과 풀링(pooling), 플래튼에 대해서 살펴보도록 하겠습니다. 마지막으로 keras MNIST(Modified National Institute of Standards and Technology) 이미지 데이터를 활용한 파이썬 예제 코드를 공부하도록 하겠습니다.
우리가 알고 있는 일반적인 신경망의 학습은 입력층에서 은닉층을 거쳐갈수록 점점 복잡한 특징들을 학습해나갑니다. 일반적인 신경망의 학습과 별반 다르지 않게 합성곱 신경망 또한 모델(함수) 계수의 오차를 계산하고 오차 만큼 역 전파(오차역전파)하여 가중치를 조정해가는 학습 과정입니다. 여기서 다른 점은 일반 신경망에서는 특정 학습을 전 결합층(MLP, Fully-Connected Layer)에서 했다면, 합성곱 신경망에서는 합성곱층(convolution layer)에서 오차를 계산하고 오차만큼 역 전파하여 가중치를 조정하는 점입니다.
![]() |
| 이미지를 벡터로 변환한 결과 (출처) |
그런데 이미지 학습에 있어서 전 결합층(MLP, fully-connected layer)의 단점은 입력시 1차원 형태로 벡터 변환을 하기 때문에 공간적 특징(Spatial Structure)을 읽어버리는 것입니다. 여기서 이미지 학습에 있어 하나의 아이디어는 공간적 특징을 학습하는 부분과 분류를 학습하는 부분의 역할을 분담하여 각각의 장점을 살리는 것 입니다. 즉 공간적 특징 추출(feature extraction)은 지역적 연결을 가진 합성곱층에 맡기고 기존에 잘 수행하고 있던 분류(classification)의 역할은 그대로 전 결합층에 맡겨보는 것입니다. 본 블로그에서는 합성곱 신경망의 공간적 특징을 추출하는 부분에 대해 설명하도록 하겠습니다.
채널(Channel)이란?
들어가기 앞서 이미지 처리에 필요한 간단한 언어를 소개하겠습니다. 기계는 글자나 이미지보다 숫자 즉, 텐서를 더 잘 처리할 수 있습니다. 이미지는 (높이, 너비, 채널)이라는 3차원 텐서로 정의할 수 있습니다. 여기서 높이는 이미지의 세로 방향 픽셀 수, 너비는 이미지의 가로 방향 픽셀 수, 채널은 색 성분을 의미합니다. 흑백 이미지는 채널 수가 1이며, 각 픽셀은 0부터 255 사이의 값을 가집니다. 아래는 28 × 28 픽셀의 손 글씨 데이터를 보여줍니다.
위 손글씨 데이터는 흑백 이미지므로 채널 수가 1임을 고려하면 (28 × 28 × 1)의 크기를 가지는 3차원 텐서입니다. 그렇다면 흑백이 아닌 컬러 이미지는 어떨까요? 컬러 이미지는 적색(Red), 녹색(Green), 청색(Blue) 채널 수가 3개입니다. 하나의 픽셀은 RGB 삼원색의 조합으로 이루어집니다. 아래 그림과 같이 높이가 500, 너비가 800인 이미지가 있다면 이 이미지의 텐서는 (500 x 800 x 3)의 크기를 가지는 3차원 텐서입니다. 여기서 채널은 때로 깊이(depth)라고도 하며, 이 경우 이미지는 (높이, 너비, 깊이)라는 3차원 텐서로 표현합니다.
합성곱(Convolution)
합성곱층은 합성곱 신경망의 핵심이며 주된 역할은 이미지의 공간적 특징을 추출해냅니다. 먼저 합성곱의 연산 부터 이해해볼까요? 합성곱은 커널(kernel) 혹은 필터(filter) 또는 윈도우(window)라는 \( n \times m \) 크기의 행렬로 높이(height) x 너비(width) 크기의 이미지를 처음부터 끝까지 겹치며 훑으면서 \( n \times m \) 크기의 겹쳐지는 부분의 각 이미지와 커널 내 원소의 값을 곱해서 모두 더한 값을 출력하는 것을 말합니다. 이때 순서는 이미지의 가장 왼쪽 위부터 가장 오른쪽 아래까지 순처적으로 훑습니다. 여기서 순차적으로 훑을 때의 이동 보폭을 스트라이드(stride)라고 합니다. 일반적으로 커널은 3 x 3 또는 5 x 5를 사용합니다.
예를 통해 알아보겠습니다. 아래 그림은 3 x 3 크기의 커널로 5 x 5의 이미지 행렬에 합성곱 연산을 수행하는 과정을 보여줍니다. 한 번의 연산을 1 스텝(Step)이라고 하였을 때, 합성곱 연산의 1 스텝까지 이미지와 식으로 표현해보았습니다.
□ 예제: 1 스텝 합성곱 연산
입력 이미지와 커널 내 원소의 값을 곱해서 모두 더한 결과 값:
\( (1 \times 1) + (2 \times 0) + (3 \times 1) + (2 \times 1) + (1 \times 0) + ( 0 \times 1) + (3 \times 0) + (0 \times 1) + (1 \times 0) = 6 \)
위 스텝을 총 9번까지 수행하여 연산을 마쳤을 때 최종 결과는 아래 그림과 같습니다. 스텝 연산과 같이 입력으로부터 커널을 사용하여 합성곱 연산을 통해 나온 결과를 특성 맵(feature map)이라고 합니다.
위의 1 스텝 예제에서는 커널의 크기가 3 × 3이고 이동 범위가 1 스트라이드 이였지만, 커널의 크기와 이동 범위는 사용자가 정할 수 있습니다. 여기서 커널의 이동 범위를 스트라이드(stride)라고 정의 합니다. 아래는 스트라이드가 2일 경우에 5 × 5 이미지에 합성곱 연산을 수행하는 3 × 3 커널의 움직임을 보여줍니다. 최종적으로 2 × 2의 크기의 특성 맵을 얻습니다.
예제로 작은 이미지에 대한 합성곱 연산을 살펴보았습니다. 단지 5 x 5 픽셀의 이미지와 단 1개의 커널를 사용하였으며, 모든 항목은 정수형 곱셈으로 이루어졌습니다. 그럼에도 프로세싱해야 하는 곱셈 연산이 상당히 많은 것을 알 수 있는데요, 요즘에는 보통 600 필셀 이상의 이미지를 사용하고 커널의 수도 많다면 연산 프로세스는 지금 보다 더 많겠죠? 또한 요즘 이미지 모델의 학습과 추론에는 32bit 실수가 사용되어, 연산의 복잡성은 CPU 혼자서 감당하기는 힘들 것입니다. 이럴떄 GPU를 활용하면 연산 속도를 좀더 효율적으로 제어 가능할 것입니다. GPU 관련해서는 본 블로그의 [CUDA] 딥러닝의 필수 GPU와 CUDA 참고 부탁드립니다.
패딩(Padding)은 왜 사용할까?
앞선 예제에서 5 × 5 이미지에 3 × 3의 커널로 합성곱 연산을 하였을 때, 스트라이드가 1일 경우에는 3 × 3의 특성 맵을 얻었습니다. 이와 같이 합성곱 연산의 결과로 얻은 특성 맵은 입력보다 크기가 작아진다는 특징이 있습니다. 만약, 합성곱 층을 여러개 쌓았다면 최종적으로 얻은 특성 맵은 초기 입력보다 매우 작아진 상태가 되버립니다. 합성곱 연산 이후에도 특성 맵의 크기가 입력의 크기와 동일하게 유지되도록 하고 싶다면 패딩(padding)을 사용하면 됩니다.
패딩은 합성곱 연산을 하기 전에 입력 이지미의 가장자리에 지정된 개수의 폭만큼 행과 열을 추가해주는 것을 말합니다. 주로 0 값을 채우는 제로 패딩(zero padding)을 사용합니다. 아래 그림은 5 x 5 이미지에 1폭짜리 제로 패딩을 사용한 예제입니다.
1 2 3 4 5 6 7 8 | from tensorflow.keras.layers import Conv1D, MaxPooling1D, Conv2D, MaxPooling2D # 임의 데이터 생성 X = tf.random.normal(shape=(128,28,28,1), mean=0., stddev=1.) # [batch size, W, H, channel] # 입력과 출력의 크기가 같게 패딩 설정하기 hiddens = Conv2D(filters=32, kernel_size=(3,3), strides=2, padding='same', activation="relu")(X) print('After Convolution: ', hiddens.shape) | cs |
지금까지 정리한 내용을 간단하게 수식으로 표현해볼까요? 한 층의 합성곱을 일반화한 식을 다음과 같이 정리할 수 있습니다. 여기서 아랫첨자 1, 2는 각각 입력과 출력입니다.
입력 데이터
\( H_{1} \times W_{1} \times D_{1} \)
\( H_{1} \)는 높이, \( W_{1} \)는 너비, \( C_{1} \)는 채널 또는 깊이
하이퍼 파라미터(Hyper-Parameters)
여기서 커널의 높이와 너비는 같다고 가정하였습니다. 다를 경우 높이와 너비 별로 각 각 계산하면 됩니다.
커널의 수: \( K \)
커널의 크기: \( F \)
스트라이드: \( S \)
패딩: \( P \)
출력 테이터
여기서 계산한 출력 데이터에서 소수점 이하는 버립니다.
\( H_{2} = \frac{H_{1} - F + 2P} {S} + 1 \)
\( W_{2} = \frac{ W_{1} - F + 2P} {S} + 1 \)
\( D_{2} = K \)
가중치의 수
\( \left [F_{2} \times D_{1} + D_{1} \right ] \times K \)
풀링(Poolling)
앞에서 소개한 그림 처럼 합성곱 층(합성곱 연산과 활성화 함수) 다음에는 풀링(pooling) 층을 추가하는 것이 일반적입니다. 풀링 층에서는 특성 맵을 다운샘플링(down-sampling)하여 특성 맵의 크기를 줄이는 풀링 연산이 이루어집니다. 풀링 연산에는 일반적으로 최대 풀링(max pooling)과 평균 풀링(average pooling)이 사용됩니다.풀링 연산에서도 합성곱 연산과 마찬가지로 커널과 스트라이드의 개념을 가집니다. 아래 그림과 같이 스트라이드가 2일 때, 2 x 2 크기 커널로 맥스 풀링 연산을 했을 때 특성맵이 절반의 크기로 다운샘플링되는 것을 보여줍니다. 맥스 풀링은 커널과 겹치는 영역 안에서 최대값을 추출하는 방식으로 다운샘플링합니다.
앞서 소개한 tensorflow 코드에 풀링까지 적용한 결과를 확인해 볼 까요?
1 2 3 4 5 6 7 8 9 10 11 | from tensorflow.keras.layers import Conv1D, MaxPooling1D, Conv2D, MaxPooling2D # 임의 데이터 생성 X = tf.random.normal(shape=(128,28,28,1), mean=0., stddev=1.) # [batch size, W, H, channel] # 입력과 출력의 크기가 같게 패딩 설정하기 hiddens = Conv2D(filters=32, kernel_size=(3,3), strides=2, padding='same', activation="relu")(X) hiddens_pool = MaxPooling2D(pool_size=(2,2), strides=(2,2))(hiddens) print('After Pooling: ', hiddens_pool.shape) //After Pooling: (128, 7, 7, 32) | cs |
합성곱 신경망의 가중치와 편향
합성곱 신경망에서의 가중치와 편향을 이해하기 위해서 다층 퍼셉트론(multi-layer perceptron)과 비교해보겠습니다. 3 x 3 크기의 이미지를 처리하여 2 x 2 특성맵을 계산하는 예로 비교하면 아래 그림과 같이 표현 할 수 있습니다. 인공 신경망으로 3 x 3 이미지를 배열로 펼처서(flattening) 각 픽셀에 가중치를 곱하여 은닉충에 전달하는 것과 같습니다. 이렇게 펼치게 되면 앞서 말한 바와 같이 공간적 특성을 유지하는 것이 어렵게 되겠죠?
합성곱 신경망에서는 특성 맵을 얻기 위해서 커널로 이미지 전체를 훑으며 합성곱 연산을 진행합니다. 이 때 사용되는 가중치는 \( w_{0}, w_{1}, w_{2}, w_{3} \) 4개 뿐입니다. 즉, 위 그림에서 이미지의 모든 픽셀 수에 대응하는 가중치를 사용하는 것이 아니라, 커널의 수에 대응하는 가중치를 사용하여 커널과 매핑되는 픽셀만을 입력으로 사용하는 것을 볼 수 있습니다. 하지만 다층 퍼셉트론의 경우 입력층에서 은닉층으로 통과시키기 위해 각 은닉층의 노드 값마다 입력층에 대응하는 가중치를 사용한 것을 알 수 있습니다. 결국 합성곱 신경망은 다층 퍼셉트론을 사용할 때보다 훨씬 적은 수의 가중치를 사용하여 공간적 구조 정보를 보존하는 특징을 알 수 있습니다.
다층 퍼셉트론의 은닉층에서는 가중치 연산 후에 비선형성을 추가하기 위해 활성화 함수를 통과시켰습니다. 이와 마찬가지로 합성곱 신경망의 은닉층에서도 활성화 함수를 통과시키며, 이때 렐루 함수나 렐루 함수의 변형들이 주로 사용됩니다. 이와 같이 합성곱 연산을 통해서 특성 맵을 얻고, 활성화 함수를 지나는 연산을 하는 신경망의 층을 합성곱 층(convolution layer)이라고 합니다.
합성곱 신경망에도 편향(bias)를 추가할 수 있습니다. 편향을 사용한다면 일반적으로 커널을 적용한 뒤에 더해집니다. 여기서 편향은 하나의 값만 존재하며, 커널이 적용된 결과의 모든 원소에 더해진 특성맵을 산출하게 됩니다.
채널 개수가 2이상인 합성곱 연산
지금까지는 채널의 개수를 고려하지 않은 2차원 텐서를 가정하여 설명하였습니다. 그렇다면 다수의 채널을 가질 경우, 즉 3차원 텐서인 경우에 합성곱 연산을 어떻게 하는지 예제를 통해 알아보도록 하겠습니다. 결과적으로 채널의 수에 대응하는 특성 맵을 산출하고, 그리고 그 결과를 모두 더하여 최종 특성 맵을 얻습니다.
위 그림은 3개의 채널을 가진 입력 데이터와 3개의 채널을 가진 커널의 합성곱 연산을 보여줍니다. 여기서 각 커널과 채널끼리의 크기는 같아야 합니다. 각 채널 간 합성곱 연산을 마치고, 그 결과를 모두 더해서 하나의 채널을 가지는 특성 맵을 만듭니다. 주의할 점은 위의 연산에서 사용되는 커널은 3개의 커널이 아니라 3개의 채널을 가진 1개의 커널 입니다.
필터 개수가 2이상인 합성곱 연산
그렇다면 이번에는 필터의 개수가 2 이상인 합성곱 연산에 대해 예제를 통해 알아보도록 하겠습니다. 결론적으로 필터 각각으로 특성맵을 얻게됩니다.
전결합층
합성곱 신경망의 마지막에서 분류(classification)를 결정하는 단계입니다. 여기서 플래튼(flatten)은 각 풀링 레이어(pooling layer)에서 얻은 특성맵을 1차원 벡터로 변환하는 것을 말합니다. 이 후 1차원 벡터로 변환된 레이어를 하나의 벡터로 연결하여 전결합층(fully-connected layer)만들고 softmax 등의 함수를 이용하여 가장 확률이 높은 class를 결과 값으로 분류합니다. 아래 그림은 플래튼과 전결합층 중간 단계를 보여주고 있습니다. (원래는 16행이여야 하지만, 시각적 표현을 위해 8행으로 축소한 점 양해 부탁드립니다)
다음은 keras MNIST(Modified National Institute of Standards and Technology) 이미지 데이터를 활용한 파이썬 예제 코드입니다. 단순 합성곱 신경망 모델을 구축하였습니다. 딥러닝 관련해서 개인적으로 감명 받았던 김경원 교수님의 github를 공유드립니다. CNN등 관련해서 더 공부가 필요하시면 깃허브 링크 참고 부탁드립니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | import warnings warnings.filterwarnings('ignore') import os import pandas as pd pd.options.display.float_format = '{:,.2f}'.format import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn import preprocessing import statsmodels.api as sm from scipy import stats import tensorflow as tf import tensorflow_addons as tfa from tensorflow import keras from tensorflow.keras import layers, regularizers, callbacks from tensorflow.keras.models import Sequential, Model, load_model from tensorflow.keras.layers import Input, Dense, Activation, Flatten, Dropout, Reshape from tensorflow.keras.layers import Conv1D, MaxPooling1D, Conv2D, MaxPooling2D from tensorflow.keras.applications import ResNet50 from tensorflow.python.keras.utils import np_utils from tensorflow.keras.utils import plot_model from sklearn.model_selection import train_test_split, GridSearchCV # Evaluation metrics from sklearn import metrics # for regression from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error # for classification from sklearn.metrics import confusion_matrix, classification_report from sklearn.metrics import accuracy_score, f1_score, roc_auc_score from sklearn.metrics import roc_curve, auc, precision_recall_curve # 하이퍼파라미터 tf.random.set_seed(1) KERNEL_SIZE = (3,3) STRIDE = 1 POOL_SIZE = (2,2) POOL_STRIDE = 2 PADDING = 'same' HIDDEN_ACTIVATION = 'relu' OUTPUT_ACTIVATION = 'softmax' LOSS = 'sparse_categorical_crossentropy' LEARNING_RATE = 0.01 OPTIMIZER = keras.optimizers.Adam(learning_rate=LEARNING_RATE) METRICS = ['accuracy'] BATCH_SIZE = 64 EPOCHS = 5 VERBOSE = 1 mnist = keras.datasets.mnist (X_train, Y_train), (X_test, Y_test) = mnist.load_data() X_train = X_train.astype("float32")/255 X_test = X_test.astype("float32")/255 print('normalized X: ', X_train.shape, X_train.min(), X_train.max()) print('normalized X: ', X_test.shape, X_test.min(), X_test.max()) X_train = X_train.reshape(-1, X_train.shape[1], X_train.shape[2], 1) X_test = X_test.reshape(-1, X_test.shape[1], X_train.shape[2], 1) print('X_train:', X_train.shape, 'Y_train:', Y_train.shape) print('X_test:', X_test.shape, 'Y_test:', Y_test.shape) # CNN 구축 inputs = Input(shape=(X_train.shape[1], X_train.shape[2], X_train.shape[3])) hiddens = Conv2D(128 , kernel_size=KERNEL_SIZE , strides=STRIDE , padding=PADDING , activation=HIDDEN_ACTIVATION)(inputs) hiddens = MaxPooling2D(pool_size=POOL_SIZE, strides=POOL_STRIDE)(hiddens) hiddens = Flatten()(hiddens) output = Dense(10, activation=OUTPUT_ACTIVATION)(hiddens) model = Model(inputs, output) model.summary() plot_model(model) # 학습하기 model.compile(loss=LOSS, optimizer=OPTIMIZER, metrics=METRICS) model_fit = model.fit(X_train, Y_train, validation_split=0.2, batch_size=BATCH_SIZE, epochs=EPOCHS, verbose=VERBOSE) plt.plot(pd.DataFrame(model_fit.history[METRICS[0]])) plt.plot(pd.DataFrame(model_fit.history['val_'+METRICS[0]])) plt.legend([METRICS[0], 'val_'+METRICS[0]]) plt.show() model.evaluate(X_train, Y_train) model.evaluate(X_test, Y_test) print('\nTest Confusion Maxtrix: ') pd.crosstab(Y_test, np.argmax(model.predict(X_test), axis=1), rownames=['True'], colnames=['Pred']) | cs |
참고자료
⒜ https://velog.io/@seongguk/AI-CNNConvolutional-Neural-Network-%ED%95%99%EC%8A%B5
⒝ https://wikidocs.net/64066
⒞ https://ieeexplore.ieee.org/document/6795724
⒟ https://proceedings.neurips.cc/paper_files/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf
















댓글
댓글 쓰기