[혼만딥] 1주차 합성곱 신경망 이해하기
본 게시글은 한빛미디어의 혼자 만들면서 공부하는 딥러닝을 바탕으로 작성되었습니다.
시작하기 앞서…
지난번에는 혼공학습단에서 혼자 공부하는 머신러닝+딥러닝 책을 가지고 활동을 했었는데 마무리가 조금 아쉬웠다. 그리고 엄청 간단한 내용들만 있을 줄 알고 가벼운 마음으로 시작해 많이 허술하게 한 감이 없잖아 있었다. 따라서 이번에는 새로 나온 혼자 만들면서 공부하는 딥러닝 책으로 새로운 마음으로 시작해 보려고 한다. 그리고 이번에는 또 원대한 계획을 세웠다. 기본적으로 혼공학습단은 총 6주 동안 책을 다루는데, 진도는 4장까지밖에 안 나간다. 앞부분도 충분히 의미있고, 다양한 내용이 있다. 그러나 뒷부분이 더 재미있어 보이고, 지난번 데이콘 컴페티션에서 T5 모델을 약간 다루긴 했었는데 T5 모델에 대해서 자세히 살펴보지는 않아서 뒷부분에 약간 더 흥미가 생겼다. 따라서 이번에는 하루에 한 장씩 끝내고, 최종적으로 한 권을 다 끝내는 것을 목표로 하기로 했다.
그동안 주로 LaTeX을 중심으로 쓰다가 다시 Markdown으로 돌아오니까 조금 불편한 것이 많다. 처음에는 Markdown이 따로 설정할 필요 없이 텍스트를 적으면 되어서 편했는데 LaTeX의 여러 패키지를 쓰다보니 이젠 패키지가 없는 것이 더 아쉬운 느낌이다. Markdown에서 수식을 쓰는 것도 너무 귀찮다. Jekyll 블로그에서 LaTeX 수식 뷰어랑 VS Code에서 사용하는 수식 뷰어가 내가 기억하기로는 달랐던 것으로 기억한다. 그래서 VS Code에서 수식이 이쁘게 보이면 Jekyll 블로그나 Github 뷰어로는 수식이 깨지고 그 반대로 하면 VS Code에서 수식이 안 보였던 것으로 기억한다. 그래서 PDF로 출력이 되던 LaTeX 문서 작성이 더 쓰고 싶어진다. 하지만 블로그를 여기서 쓰기로 한 이상 이건 어쩔 수 없긴 하다.
어쨌든 다시 혼공학습단을 시작해보자. 지난번에는 가벼운 마음으로 시작했다면 이번에는 엄청나게 부담을 갖고 빡세게 해볼 것이다. 하지만 이것도 얼마나 갈지는 자신이 없긴 해서 최종 목표는 일단 혼공학습단 진도까지는 완벽하게 끝내는 것이다.
MNIST 데이터
합성곱 신경망의 초기 형태는 Y. LeCun 연구팀의 LeNet-5에서 시작되었다고 할 수 있다[1]. LeNet은 손으로 쓴 숫자를 인식하기 위해서 만들어진 모델로, 미국 우편 서비스에서 우편물에 사용되는 우편 번호와 같은 것을 자동으로 인식하는 것이 그 목표였다. 그런데 손으로 쓴 숫자 데이터? 어디서 본 기억이 있을 것이다. 머신러닝이나 딥러닝을 배울 때 무조건 등장하게 되는 MNIST 데이터이다.
위 이미지를 보면 아마 익숙한 모습이 보일 것이다[1]. 위 이미지처럼 손으로 쓴 글씨들을 데이터로 활용하여 고정된 카테고리로 분류하는 multi class classification 문제이다. 아마 여기서 한 가지 궁금한 것이 생길 수도 있다. 분명 우리는 아까 우편물에 적은 글씨를 인식하는 것이 목표라고 했던 것을 기억할 것이다. 그리고 우리의 상식이라면 흰 종이에 검은 글씨를 쓰는 것이 맞을 것이다. 그런데 왜 MNIST 데이터는 검은 바탕에 흰 글씨를 썼을까? 당시에 우편물에 검은 종이에 수정액으로 주소를 썼던 것일까?
아쉽게도 그건 아니다. 이건 색을 표현하는 체계와 관련이 있는데 아마 빛의 삼원색이라고 들어본 적이 있을 것이다. 빨강색, 녹색, 초록색 빛을 사용해 여러 색을 만들어내는 것으로 흔히 RGB 값이라고도 한다. 그런데 여기서 색의 삼원색과 헷갈릴 수 있는데, 색의 삼원색은 물감을 생각하면 된다. 우리가 일반적으로 물감이나 프린터를 사용할 때, 흰색 도화지에 색을 칠하는데, 이때는 색을 칠하면 칠할수록 검은색에 가까워진다. 그런데 아무것도 안 그리면 흰 도화지이다. 포토샵이나 인디자인 같은 것을 했을 때 볼 수 있는 CMYK라고도 한다. 아래의 이미지들을 보면 어떻게 색이 구성되는지 알 수 있다[2][3].
우선, 오른쪽 이미지를 보면, 마젠타, 시안, 옐로우로 색의 삼원색을 볼 수 있다. 이것도 아마 프린터를 많이 사용했다면 익숙할텐데 매일 넣는 잉크의 색이 이렇게 3가지, 그리고 검은색이다. 하지만 이건 우리가 일상적으로 사용하는 종이에 넣는 색이다. 이제 MNIST 데이터로 다시 돌아가보자. 왼쪽의 이미지를 보면 배경이 검은색이고 모든 색을 다 더하면 흰 색이 되는 것을 확인할 수 있다. 사실 이건 당연하다. 아무 빛도 없으면 보이지 않는 미래처럼 캄캄한게 당연하다. 그리고 여기에 조금씩 빛을 더해서 엄청 밝게 해버리면 흰 빛이 된다. 이때, 여기서 사용되는 색이 빨강색, 녹색, 초록색인 것이다.
그럼 이제 이것과 MNIST 데이터에서의 배경과 글자 색이 어떤 관련이 있는지 생각해보자. 앞서 빛의 삼원색에서 아무것도 없으면 캄캄하다고 했었다. 즉, 아무것도 없는 상태, 0이라는 의미이다. 우리가 종이로 쓰는 색의 삼원색 체계에서는 아무것도 없는 상태, 흰 도화지의 상태는 흰색이기에 0이 흰색이 되지만 빛의 삼원색 체계에서는 0이 검은색이 되는 것이다. 따라서 배경 이미지가 0이 되는 것이다. 그리고 무언가를 명확하게 쓴 상태는 당연히 그 반대인 흰색이 되는 것이다. 따라서 MNIST 데이터에서 검은 배경에 흰 글씨로 데이터가 만들어지게 된 것이다.
사실 이 책을 다루는 단계라면 dataframe에 대한 개념은 이미 알고 있을 것이라고 생각한다. 그래도 간단히 살펴본다면 왼쪽과 같은 숫자 5에 대한 숫자 필기 이미지가 있을 때, 이게 사실 엑셀과 같이 하나의 표인 것이다. 오른쪽 이미지와 같이 각 픽셀들이 있고 그 픽셀에는 값들이 들어가 있는 것이다. 이때, 이미지의 해상도는 $28\times28$이므로 이만큼의 개수의 값들이 이 이미지를 그리는데 사용되는 것이다.
각 픽셀마다 실제 값들을 입력해서 살펴보면 아래와 같이 나타나는 것을 확인할 수 있다. 이전에 말했던 것과 같이 검은색 배경 부분은 0이고, 밝을수록 1에 가까워지는 것을 확인할 수 있다. 따라서 모델이 하얀색 글자 부분, 즉 큰 값을 가진 부분에 집중하도록 하는 것이 우리의 목표이다.
LeNet
지금까지 데이터에 대해서 살펴보았다. 이제 이 데이터를 가지고 어떤 것을 했는지 살펴보도록 하자. 책에서도 나왔듯이 합성곱 신경망의 초기 형태라고 할 수 있는 LeNet에 대해서 살펴볼 것인데, 논문에서 그린 모델 구조는 아래와 같다[4].
다른 건 이해 못할 수도 있어도 Input 정도는 이해할 수 있을 것이다. 그런데 여기서 또 하나 의문이 생길 수 있다. 방금 MNIST 데이터에 살펴본 바로는 이미지의 해상도가 $28\times28$였던 것으로 기억할 것이다. 그런데 여기서는 Input이 $32\times32$이다. 다른 데이터를 사용한 것일까? 아님 그림판에 이미지를 놓고 당겨서 크기를 늘린 것일까? 정답은 그냥 테두리에 아무 값도 없는 공간을 붙여넣은 것이다.
Y. LeCun는 이렇게 굳이 하는 이유를 친절하게 설명해준다. 모두가 알고 있듯 기본 이미지 크기는 $28\times28$이다. 그런데 사실 이 MNIST 데이터에서도 실제 숫자가 그려져 있는 공간은 중간의 $20\times20$ 정도의 공간으로 이미 한 번 테두리를 늘린 상태이다. 그런데 여기서 $28\times28$의 이미지를 $32\times32$의 이미지로 또 테두리를 늘려준 것이다. 굳이 왜 이렇게 했을까?
Padding
이것은 간단하게 말하면, 특정을 더욱 더 잘 학습하기 위함이다. 나중에 설명을 하겠지만, 모델이 학습할 때, $20\times20$의 공간만큼 모델이 포착해서 학습을 하게 된다. 아래 이미지와 같이 $20\times20$의 붉은 사각형 만큼의 공간을 학습하는 것이다. 그런데 우리가 어떤 것을 볼 때 시야 가운데에 있는 것에 집중하지 테두리에 있는 것을 집중하지는 않을 것이다. 여기서도 그것과 마찬가지로 가운데에 있는 것을 더 잘 학습하게 된다. 따라서 테두리를 더 만들어 이 숫자에 대한 정보를 가운데에 놓이게 하여 테두리에 가까운 데이터도 시야의 가운데에 놓고 더 잘 학습할 수 있게 하는 것이다. 이제 왼쪽 이미지를 보면 숫자 $5$의 획이나 글자 테두리 부분이 가운데에 있다고 보기는 조금 어려울 것이다. 이러한 경우 학습이 잘 이루어지지 않을 것이다. 그런데 오른쪽 이미지처럼 테두리를 늘려서 숫자 $5$의 아래 부분을 잘 학습할 수 있게 해주는 것이다.
결과적으로 간단한 예시를 생각해보면 아까와 같이 생각할 수 있다. 조금 전에 MNIST 데이터의 글자 부분은 사실 조금 더 적은 범위에 있고 MNIST 데이터가 이미 테두리를 늘려준 것이라고 말한 것을 기억할 것이다.그리고 여기에 추가로 테두리를 추가하는 것까지 한번 살펴보자. 그냥 아래의 과정을 보면 배경이 늘었구나 정도로만 생각이 들 것이다.
하지만, 만약 딥러닝 모델이 한번에 $3\times3$만큼 본다고 하고, 그 가운데 부분에 집중해서 학습을 한다고 할 때는 결과가 달라진다. 아래 이미지의 굵은 사각형은 모델이 보는 면적이고, 가운데 부분을 집중해서 보는 영역이라고 할 때, 모델이 전체 영역을 보면서 이동하며 집중해서 보게 되는 영역을 붉게 칠한 것이다. 이렇게 했을 때 배경이 있는 이미지와 없는 이미지에 차이가 발생하게 된다. 왼쪽의 배경이 더 적은 이미지는 3의 바깥 부분은 집중해서 학습하지 못했다는 것을 알 수 있다. 그에 반해 오른쪽 이미지를 보면 3 전체 부분을 모두 집중해서 학습할 수 있었다는 것을 확인할 수 있다. 이렇듯 테두리를 추가함으로써 주어진 데이터를 조금 더 잘 학습할 수 있게 만들 수 있다. 이렇게 테두리를 추가해주는 것을 padding이라고 한다.
그런데 사실 아까 모델 구조에는 이런 말이 없었던 것으로 기억한다. 학습하는 영역인 $20$이라는 숫자도 구경하지 못했다. 그리고 모델 구조도 어떤 것을 의미하는지 이해하기 어려울 수 있다. 이런 것들을 포함해서 다시 모델 구조를 차근차근 살펴보기로 하자.
우선, 입력 데이터 크기는 다음과 같다. 이때, 여기서는 padding을 포함한 수치이다. \(\rm{Input}=32\times32\)
Convolution
그다음 Convolutions이라는 표시가 나온 것을 확인할 수 있다. 이는 합성곱 연산으로, filter를 통해 입력 데이터에 원소 단위로 연산을 하는 것이다. 우선 간단한 예시를 통해 살펴보기로 하자. 만약 아래와 같이 $3\times3$의 input이 있고, $2\times2$의 필터가 있을 때, $2\times2$의 데이터가 나오게 된다. 이 데이터는 특성 맵, feature map으로 filter가 input에서 감지한 데이터의 특징, 패턴을 정리해둔 것이다. 이때, $*$는 합성곱 연산을 수행한다는 표시이다. 그런데 이렇게 수식으로 보면 직관적이지 않을 수 있다. 따라서 더 나은 예시를 살펴보자.
제일 왼쪽에는 $5\times5$의 숫자 3이 적힌 데이터가 있다. 그리고 $3\times3$의 filter가 있다고 가정해보자. 그럴 때 먼저 filter의 크기와 동일하게 왼쪽 위부터 데이터를 가져온다. 그리고 원소별 곱셈을 한다. 일반적인 행렬의 곱셈과 다르게 그냥 동일한 위치에 있는 것끼리 곱해주기만 하면 되는 간단한 것이다. 이렇게 하면 결과적으로 제일 오른쪽의 $3\times3$의 데이터가 나오는데 여기서 각 원소들을 모두 다 더하면 feature map의 한 데이터 값이 나오는 것이다.
아까는 왼쪽 위 부분에 대해서 값을 구했다면 다음으로는 아래 이미지처럼 filter가 차례차례 이동하면서 총 9개의 값을 구하게 된다. 이때, 한칸씩 이동하는 것을 확인할 수 있는데 이것이 바로 stride이다. 기본적으로 stride는 1로 설정하는데 상황에 따라 2 이상으로 설정할 수 있다.
결과적으로 아까 살펴봤던 수식과 동일한 방식으로 $5\times5$ input에 대한 $3\times3$ feature map을 구하게 된다.
Pooling
Dense
이제 이 내용을 논문에서 제시한 구조도를 바탕으로 더 자세하게 살펴보자. 먼저, $R_l$은 $l$번째 layer에서 receptive field의 크기이고 $J_l$이 $l$번째 layer에서의 jump size라고 할 때, 초기 $R_0, J_0$을 다음과 같이 설정한다. \(R_0=1, J_0=1\)
그리고 C1 layer, $5\times5$ convolution layer를 통과한다. \(R_1=R_0+(k_1-1)\cdot J_0=1+(5-1)\cdot1=5\) \(J_1=J_0\cdot s_1=1\cdot1=1\)
다음으로 S2 layer, $2\times2$ pooling layer를 통과한다. \(R_2=R_1+(k_2-1)\cdot J_1=5+(2-1)\cdots1=6\) \(J_2=J_1\cdot s_1=1\cdot2=2\)
이후, C3 layer, $5\times5$ convolution layer를 통과한다. \(R_3=R_2+(k_3-1)\cdot J_2=6+(5-1)\cdot2=14\) \(J_3=J_2\cdot s_3=2\cdot1=2\)
이제 C3 뉴런의 중심 위치를 계산해보자. C3 layer는 $10\times10$ 크기로, 뉴런의 인덱스는 $i=0, 1, \cdots, 9$이다. 여기서 각 뉴런의 receptive field 중심 좌표를 구해보면 \(x_i=\frac{R_3}{2}+i\cdot J_3=7+2i\)
\(x_0=7+2\cdot0=7, x_1=7+2\cdot1=9, x_2=7+2\cdot2=11, x_3=7+2\cdot3=13\) \(x_4=7+2\cdot4=15, x_5=7+2\cdot5=17, x_6=7+2\cdot6=19, x_7=7+2\cdot7=21\) \(x_8=7+2\cdot8=23, x_9=7+2\cdot9=25\)
따라서 중심 좌표는 ${7, 9, 11, 13, 15, 17, 19, 21, 23, 25}$이다.
중심 좌표의 범위는 $7$부터 $25$로 $25-7+1=19$로 중심의 19픽셀, 거의 20픽셀을 중심으로 할 수 있다는 것을 확인할 수 있다.
또한, 중심 위치가 커버하는 위치는 각 중심 좌표를 중심으로 좌우 $\pm7$ 범위 만큼 커버를 한다. 따라서 중심좌표를 기준으로 최소값 $7-7=0$이므로 $0$부터 $25+7=32$이므로 $32$까지 전범위를 커버하는 것을 확인할 수 있다.
인공 신경망
인공 신경망과 합성곱 신경망 모두 딥러닝의 기본 구조인 신경망의 한 형태이지만 구조에 차이가 있다. 인공 신경망은 흔히 우리가 알듯이 input layer과 output layer, 그리고 그 사이에 있는 hidden layer가 있다. 그런데 인공신경망에서 fully-connected layer를 사용할 때 이미지의 모든 픽셀을 일종의 sequence 형태로 펼쳐서 처리한다. 그러면 아까 사용한 $32\times32\times3$ 픽셀의 컬러 이미지를 입력 데이터로 사용하려면 3,072개의 노드, 즉 변수가 필요하다.
</br>
기본 과제
구글 코렙에 tensorflow/keras 설치하기
이전에도 말했듯이 난 클라우드 환경이나 온라인으로 연결해서 사용하는 것은 별로 안 좋아한다. 연결이 끊어질 경우 데이터가 날아가거나 돌리고 있던 코드가 중단될 수 있기 때문이다. 그리고 난 안정성을 매우 중시하기 때문에
그런데 사실 구글 코렙에 tensorflow와 keras는 이미 설치가 되어있다. 그래서 그냥 단순하게 아래와 같이 라이브러리를 import하면 바로 사용이 가능할 것이다.
1
2
import tensorflow as tf
from tensorflow.keras import layers, models
그래도 굳이 tensorflow를 설치하고자 한다면, 아래와 같이 사용할 수 있다. 이미 설치된 tensorflow를 굳이 설치하고자 하는 경우는 분명 호환성 문제로 다른 버전의 tensorflow를 설치하는 경우 말고는 거의 없을텐데 아래에 작성한 것과 같이 버전도 설정하면 된다.
1
!pip3 install tensorflow==2.15.0
</br>
LeNet 모델 시각화하기
summary()
tensorflow로 모델을 구현하고 시각화 하는 방법에는 여러가지가 있다. 일단 간단하게 tensorflow의 기본 기능인 summary()를 사용해서 시각화를 해보자. 먼저 아래와 같이 모델을 만든 다음, 간단하게 summary()만 붙이면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import tensorflow as tf
from tensorflow.keras import layers, models
model = models.Sequential([
layers.Input(shape=(32, 32, 1)),
layers.Conv2D(6, kernel_size=5, activation='tanh', padding='same'),
layers.AveragePooling2D(pool_size=2, strides=2),
layers.Conv2D(16, kernel_size=5, activation='tanh'),
layers.AveragePooling2D(pool_size=2, strides=2),
layers.Flatten(),
layers.Dense(120, activation='tanh'),
layers.Dense(84, activation='tanh'),
layers.Dense(10, activation='softmax')
])
model.summary()
Model: "sequential_1" ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ conv2d_2 (Conv2D) │ (None, 32, 32, 6) │ 156 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ average_pooling2d_2 │ (None, 16, 16, 6) │ 0 │ │ (AveragePooling2D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_3 (Conv2D) │ (None, 12, 12, 16) │ 2,416 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ average_pooling2d_3 │ (None, 6, 6, 16) │ 0 │ │ (AveragePooling2D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ flatten_1 (Flatten) │ (None, 576) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_3 (Dense) │ (None, 120) │ 69,240 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_4 (Dense) │ (None, 84) │ 10,164 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_5 (Dense) │ (None, 10) │ 850 │ └─────────────────────────────────┴────────────────────────┴───────────────┘ Total params: 82,826 (323.54 KB) Trainable params: 82,826 (323.54 KB) Non-trainable params: 0 (0.00 B)
plot_model
또 다른 방법으로는 plot_model이다. 이건 png 파일로 모델을 그려서 보여주는 것으로 위의 텍스트 기반으로 그리는 것보다 더 이쁘고 보기 좋게 그릴 수 있다. 다만, 이걸 사용하기 위해서는 Graphviz를 설치해야 제대로 작동한다. Google Colab에서는 아마 안 해도 됐던 것으로 기억하는데 모르겠다. 어쨌든 이걸 바탕으로 LeNet 모델을 아래와 같이 시각화할 수 있다.
1
2
3
from tensorflow.keras.utils import plot_model
plot_model(model, to_file='lenet.png', show_shapes=True, show_layer_names=True)
</br>
PlotNeuralNet
이것 말고도 LaTeX 기반의 PlotNeuralNet도 있다. 이건 직접 그리려고 했는데 Github에서 예시로 LeNet 모델 구조에 대한 그림 그리는 코드를 구현해놓은 게 있어서 한 번 가져와 봤다. 아래 이미지와 같이 이쁘게 나오는 것을 확인할 수 있다.
그런데 지난 학기 프로젝트에서 사용해봤는데, 생각보다 별로였다. 너무 수려하다고 해야 하려나? 그리고 솔직히 작성하는 것도 지나치게 오래 걸린다. 만약 시간이 남아돌아서 이쁜 그림 그리는 데에 시간을 더 많이 쏟고 싶다면 이걸 쓰겠지만 다시 쓰기에는 노력에 비해 결과물이 조금 쓸모가 없었다. 대신 발표용 PPT에 사용할 목적이라면 생각보다 괜찮은 것 같았다. 아니면 ChatGPT한데 만들어달라고 하는 것도 괜찮을 것 같기도 하다.
이것 말고도 draw.io나 그냥 포토샵, 워드나 한글의 도형을 사용해서 그리는 방법 등이 있을 텐데 그건 정말 수작업으로 해야 하는 것이라 생략했다. 사실 이전 프로젝트 때 만든 모델 구조도는 워드로 수작업으로 만들었었다. 수작업이 제일 내가 원하는 대로 만들 수 있어서 어쩔 수 없는 것 같다.
추가 과제
Convolutional Layer 설명하기
Pooling Layer 설명하기
Dense Layer 설명하기
</br>
참고문헌
[1] Wikimedia Commons, “MNIST dataset example.png,” https://commons.wikimedia.org/wiki/File:MNIST_dataset_example.png, Accessed: Jun. 29, 2025.
[2] Wikimedia Commons, “Synthese+.svg,” https://commons.wikimedia.org/wiki/File:Synthese%2B.svg, Accessed: Jun. 30, 2025.
[3] Wikimedia Commons, “CMYK color model.svg,” https://commons.wikimedia.org/wiki/File:CMYK_color_model.svg, Accessed: Jun. 30, 2025.
[4] Y. Lecun, L. Bottou, Y. Bengio, and P. Haffner, “Gradient-based learning applied to document recognition,” Proc. IEEE, vol. 86, no. 11, pp. 2278–2324, Nov. 1998, doi: 10.1109/5.726791.
[5] 박해선, 혼자 만들면서 공부하는 딥러닝. 서울: 한빛미디어, 2025.
[6] 이긍희, 김용대, 김기온, 딥러닝의 통계적 이해. 서울: 한국방송통신대학교출판문화원, 2020.