[혼만딥] 1주차 합성곱 신경망 이해하기
본 게시글은 한빛미디어의 혼자 만들면서 공부하는 딥러닝을 바탕으로 작성되었습니다.
시작하기 앞서…
지난번에는 혼공학습단에서 혼자 공부하는 머신러닝+딥러닝 책을 가지고 활동을 했었는데 마무리가 조금 아쉬웠다. 그리고 엄청 간단한 내용들만 있을 줄 알고 가벼운 마음으로 시작해 많이 허술하게 한 감이 없잖아 있었다. 따라서 이번에는 새로 나온 혼자 만들면서 공부하는 딥러닝 책으로 새로운 마음으로 시작해 보려고 한다. 그리고 이번에는 또 원대한 계획을 세웠다. 기본적으로 혼공학습단은 총 6주 동안 책을 다루는데, 진도는 4장까지밖에 안 나간다. 앞부분도 충분히 의미있고, 다양한 내용이 있다. 그러나 뒷부분이 더 재미있어 보이고, 지난번 데이콘 컴페티션에서 T5 모델을 약간 다루긴 했었는데 T5 모델에 대해서 자세히 살펴보지는 않아서 뒷부분에 약간 더 흥미가 생겼다. 따라서 이번에는 하루에 한 장씩 끝내고, 최종적으로 한 권을 다 끝내는 것을 1차 목표로 하기로 했다.
그동안 주로 LaTeX을 중심으로 쓰다가 다시 Markdown으로 돌아오니까 조금 불편한 것이 많다. 처음에는 Markdown이 따로 설정할 필요 없이 텍스트를 적으면 되어서 편했는데 LaTeX의 여러 패키지를 쓰다보니 이젠 패키지가 없는 것이 더 아쉬운 느낌이다. 물론 Html을 활용해서 쓸 수는 있긴 하겠지만 난 Html은 정말 찍먹만 한 수준이라 ChatGPT의 도움을 받아야 하는 수준이다. 그리고 Markdown에서 수식을 쓰는 것도 너무 귀찮다. Jekyll 블로그에서 LaTeX 수식 뷰어랑 VS Code에서 사용하는 수식 뷰어가 내가 기억하기로는 달랐던 것으로 기억한다. 그래서 VS Code에서 수식이 이쁘게 보이면 Jekyll 블로그나 Github 뷰어로는 수식이 깨지고 그 반대로 하면 VS Code에서 수식이 안 보였던 것으로 기억한다. 그래서 PDF로 출력이 되던 LaTeX 문서 작성이 더 쓰고 싶어진다. 하지만 블로그를 여기서 쓰기로 한 이상 이건 어쩔 수 없긴 하다.
그리고 지난번에는 노트북으로 코드를 돌렸다면 이번에는 PC를 따로 마련해서 엄청 빠르게 코드가 실행될 것이라고 기대하고 있다. 아무래도 딥러닝이나 머신러닝 모델을 돌리다보면 시간이 하루 이상 걸릴 때가 많았는데 답답해서 그냥 RTX 3090 그래픽 카드를 사버렸다. 새 PC를 사기에는 너무 돈이 많이 들 것 같아서 중고로 부품 하나하나 드래곤볼 모으듯이 조립해서 PC를 마련했다. 그리고 PC에 우분투를 설치하고 SSH를 사용해서 노트북에 연결해 사용할 수 있게 해 엄청나게 잘 활용하고 있다. 지금까지의 결과로는 돈 값을 하는 것 같아 대만족이다. 덕분에 Kaggle이나 Dacon도 부담없이 이것저것 다 해보는 중이다.
혼공학습단 이야기로 다시 돌아오자면 지난번에는 가벼운 마음으로 시작했다면 이번에는 엄청나게 부담을 갖고 빡세게 해볼 것이다. 하지만 이것도 얼마나 갈지는 자신이 없긴 해서 최종 목표는 일단 혼공학습단 진도까지는 완벽하게 끝내는 것이다. 그리고 시간이 된다면 일주일에 최소 하나 정도는 관련 논문 리뷰 글도 작성하는 게 이번 목표이다. 사실 교수님께서는 하루에 논문 한 편 정도는 읽으라고 하셨지만… 현재의 내 수준으로는 어림도 없는 것 같다. 아무튼 지금부터 혼공학습단을 새로 시작해보자!
ANN과 CNN
인공 신경망과 합성곱 신경망 모두 딥러닝의 기본 구조인 신경망의 한 형태이지만 구조에 차이가 있다. 인공 신경망은 흔히 우리가 알듯이 input layer과 output layer, 그리고 그 사이에 있는 hidden layer가 있다. 그런데 인공신경망에서 fully-connected layer를 사용할 때 이미지의 모든 픽셀을 일종의 sequence 형태로 펼쳐서 처리한다. 그러면 아까 사용한 $32\times32\times3$ 픽셀의 컬러 이미지를 입력 데이터로 사용하려면 3,072개의 노드, 즉 변수가 필요하다.
하지만 이렇게 처리하는 방식에는 한계가 존재하다. Fully-connected layer를 통과한 후, 일종의 sequence 형태로 만든다고 했는데, 이러한 경우 픽셀의 위치 관계가 없어지기 때문이다. 그냥 일렬로 나열된 픽셀들이 되어버린다. 또한, 앞서 살펴봤듯이 단순한 $32\times32$ 크기의 컬러 이미지를 사용하더라도 최소 3,072개의 변수가 필요하게 된다. 따라서 이러한 문제를 해결하기 위해 CNN이 등장하게 되었다.
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\]Convolutional Layer
그다음 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 Layer
다음으로는 pooling이 있다. Pooling은 이미지의 크기를 줄여주는 것으로, 합성곱층 뒤에 붙어 추출된 feature map을 압축해준다. 이렇게 하면 이미지를 더 단순하게 만들어 더 중요한 feature를 알 수 있고, 데이터가 줄어들어 연산량이 줄어든다는 장점이 있다. 또한, 세부적인 feature를 줄여 overfitting에 대한 우려도 줄일 수 있다. Pooling은 일반적으로 최댓값을 뽑는 Max Pooling과 평균값을 구하는 Average Pooling이 있다.
아래의 이미지를 보면 더 자세하게 볼 수 있는데, 8이라는 숫자에 대한 이미지가 있을 때, filter의 크기를 $2\times2$라고 가정하면 왼쪽 위의 0이 4개 있는 사각형 만큼 구역을 지정하는 것이다. 이후 그 영역 내에서 최댓값을 구하거나 평균을 구하는 것이다. 이때, 서로 겹치지 않게 구역을 설정하기에 여기서 stride는 2로 설정하게 된다. 따라서 이러한 방식으로 max pooling과 average pooling을 수행하면 아래의 이미지와 같이 되는 것을 확인할 수 있다. 현재 convolution layer 이후에는 max pooling을 주로 사용하는데 지금 다루고 있는 LeNet에서는 average pooling을 사용했다.
위의 이미지를 볼 때는 큰 차이가 있나? 라고 생각이 들 수 있는데 만약 0 부분을 정말 검정색으로 바꾸게 된다면 엄청 극단적으로 차이가 나는 것을 확인할 수 있을 것이다. Max pooling의 경우 feature를 매우 확실하게 포착하게 되고, average pooling은 조금 더 부드럽게 feature를 가져오게 된다. 이렇게 될 때, average pooling이 더 많은 정보를 수집할 것이라 생각하여 당시에는 그렇게 설정한 것으로 알고 있다. 하지만 지금은 일반적으로 max pooling이 더 성능이 좋아 max pooling을 많이 사용하고 있다.
Dense Layer
지금까지는 feature를 추출하는 과정이었다면 이제는 최종 classification을 하는 과정이다. 앞에서는 공간 정보를 보존해서 feature를 추출하는 과정이었다면 이번에는 이렇게 추출한 feature를 종합해서 최종 판단을 내리는 것이다.
앞의 모델을 보면, S4 layer에서 결국 16개의 $5\times5$ feature map이 나오는데 이를 하나의 vector로 펼치는 것이다. 따라서 400개의 1차원 벡터로 변환이 되게 된다.
\[16\cdot4\cdot4=400\] \[\mathbf{x}=[x_1, x_2, \cdots, x_{400}]^T\]이 vector $\mathbf{x}$를 이제 dense layer의 input으로 넣어볼 것이다. Dense layer에서는 input vector $\mathbf{x}$에 weight matrix $\mathbf{W}$와 bias vector $\mathbf{b}$를 적용한다.
\[\mathbf{z}=\mathbf{Wx}+\mathbf{b}\]이후 이 결과를 활성화 함수 $f(\cdot)$에 넣어 최종 output을 만들어내게 되는 것이다. 일반적으로 활성화 함수에 Softmax를 사용한다고 말을 하기는 했지만 사실 LeNet-5 논문에서는 거리 기반 방식인 RBF(Radial Basis Function)을 사용해서 분류를 했다. 식은 아래와 같이 적을 수 있는데, 자세한 건 조용히 넘어가고 나중에 Softmax에 대해서 살펴보기로 하자.
\[\hat{y}=\rm{arg}\it\min_{k}||a-c_k||^2\]LeNet 살펴보기
이제 이 내용을 논문에서 제시한 구조도를 바탕으로 더 자세하게 살펴보자. 그 전에 앞서 새로운 개념에 대해 살펴볼 것인데, receptive field와 jump size이다. 먼저, receptive field는 어떤 층의 뉴런이 원본 이미지에서 얼마나 넓은 곳을 보고 있는지를 말하는 것이다. 우리도 책을 볼 때 한 번에 책을 다 볼 수 없듯이 여기서도 동일하다. 그런데 여기서 조금 유의할 점은 각 층마다 receptive field와 jump size가 달라지는데, 이것은 점점 층에 따라서 모델이 보는 것이 더 많아지기 때문이다.
조금 간단하게 비유를 하자면 처음 책을 볼 때는 처음 배우는 언어라면 글자, 아니면 단어 하나하나 읽으면서 책을 조금씩 이해할 것이다. 그런데 한번 다 읽고 또 다시 공부하고 위해서 다시 책을 본다면 그때는 조금 더 휙휙 지나갈 것이다. 그때는 이제 문장마다 어떤 내용이었지 기억하면서 더 많은 내용을 한번에 보게 될 것이고 문장 단위로 빠르게 내용을 훑어볼 수도 있을 것이다. 마지막에는 전체 페이지를 크게 훑어보면서 여기는 어떤 주제의 페이지였지 하면서 기억하는 정도까지 오게 될 것이다. 이 과정이 바로 아래에서 수행하게 될 과정이다. 즉, receptive field가 책에서 얼마나 보는지, 그리고 jump size가 얼마나 내용을 띄어서 보는지를 의미한다.
먼저, $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를 통과한다. 이때의 filter는 6개이고, $k_1\times k_1=5\times5$의 크기를 가지고 있다. 여기서 stride $s_1$은 $1$로 설정했다. 이렇게 설정했을 때, 첫 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를 통과한다. 여기서는 average pooling을 사용하며 filter는 $k_2\times k_2=2\times2$의 크기를 가진다. average pooling을 수행하여 기존 특징을 가지면서 해상도를 축소시켜 연산량을 줄이는 역할을 수행한다. 여기서는 stride를 $s_2=2$로 설정한다.
\[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를 통과한다. Filter는 16개이며 $k_3\times k_3=5\times5$의 크기를 가진다. 여기서도 이전 C1과 마찬가지로 stride $s_3=1$이다.
\[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\]그리고 또 다시 S4 layer, $2\times2$ pooling layer를 통과한다. 여기서도 S2와 마찬가지로 average pooling을 사용한다. 다른 것도 동일하게 filter는 $2\times2$, stride는 $s_4=2$로 설정한다.
\[R_4=R_3+(k_4-1)\cdot J_3=14+(2-1)\cdot2=16\] \[J_4=J_3\cdot s_4=2\cdot2=4\]이후 세번째 convolution layer인데 여기서는 fully connected layer와 같이 작동하는 것이다. 필터는 120개이고 크기는 $k_5\times k_5=5\times5$로 설정한다. 그리고 stride는 $s_5=1$이다. 갑자기 뭔가 달라진 것을 확인할 수 있다. 이전까지의 convolution, pooling 과정은 특징을 찾는 과정이었다. 만약 숫자 5에 대해서 특징을 찾는다면 여기서는 꺾인 부분이 하나 있네? 아래 쪽에는 곡선이 있네? 위쪽에서는 직선이 있구나? 이런 특징들을 찾는 과정인 것이다.
그런데 언제까지나 계속 feature를 뽑아내기만 하면 의미가 없을 것이다. 결국 숫자 이미지의 feature를 찾아서 어떤 숫자의 feature인지 학습하고 결국 이미지가 주어졌을 때 그게 어떤 숫자인지 분류를 해야 한다. 따라서 뽑아낸 feature들을 보고 결과적으로 어떤 숫자의 feature들인지 전체적으로도 알아야 할 것이다. 따라서 이 layer에서는 $5\times5$ filter를 적용해 $1\times1$의 output을 만들어낸다. 따라서 이를 120차원의 vector 형태로 바꾸는 것이다.
다음으로 또 마지막으로 fully connected layer를 통과하는데, 여기서는 120차원의 벡터를 84차원으로 줄여 특징을 정제해 최종적으로 classification에 필요한 특성들을 정한다. 그리고 output layer에서는 Softmax activation function을 사용하여 이 정보가 0부터 9라는 숫자 category 중에 어떤 것인지 확률적으로 예측하게 된다.
Padding 더 살펴보기
그런데 이렇게 한 것은 알겠는데 $R$이랑 $J$는 갑자기 왜 보고 있는 것일까? 아까 padding 부분에서 padding을 하는 이유가 모델이 집중해서 보는 부분의 가운데에 글자를 위치하게 하고 싶어서 하는 것이라고 했었다. 그럼 LeNet에서 원래 $28\times28$의 MNIST 이미지를 $32\times32$로 padding해서 사용하고 실제 숫자는 가운데의 $20\times20$ 부분이라면 만약 receptive field의 중심이 지나가는 공간이 가운데의 $20\times20$이라면 제대로 padding을 한 것이라고 할 수 있을 것이다. 따라서 이게 실제로 맞는지 계산해보고자 한다.
그런데 특징을 추출하는 과정에서의 receptive field를 확인하면 되기에 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 픽셀을 중심으로 할 수 있다는 것을 확인할 수 있다. 따라서 실제 숫자가 작성되어 있는 중심의 20 픽셀 부분을 전부 모델의 receptive field의 가운데에 놓고 학습하게 된다는 것을 확인할 수 있다.
또한, 중심 위치가 커버하는 위치는 각 중심 좌표를 중심으로 대략적으로 $\pm7$ 범위 만큼 커버를 한다. 따라서 중심좌표를 기준으로 최소값 $7-7=0$이므로 $0$부터 $25+7=32$이므로 전체 이미지의 크기인 $32$까지 전범위를 커버하는 것을 확인할 수 있다. 따라서 이미지의 해상도를 $32\times32$로 padding하여 학습할 경우, 실제 숫자가 작성되어 있는 중심의 20 픽셀 부분을 receptive field의 가운데에 놓고 집중해서 학습할 수 있게 설계한 것을 확인할 수 있으며 $32\times32$의 모든 부분을 학습에 활용할 수 있게 되는 것을 확인할 수 있다. 물론 이렇게 한다고 해서 무조건 더 학습이 잘 되는 것은 아니지만, 설계상 $32\times32$로 설정한 것에 대해서 한 번 생각해본 것이다. 학습이 잘 되는지는 상황에 따라 살펴봐야 할 것이다. 지금과 같은 경우에는 숫자가 모두 가운데에 몰려 있는 상황이라 아마 더 잘 될 수도 있을 것 같긴 하다.
기본 과제
구글 코렙에 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
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)
PlotNeuralNet
이것 말고도 LaTeX 기반의 PlotNeuralNet도 있다. 이건 직접 그리려고 했는데 Github에서 예시로 LeNet 모델 구조에 대한 그림 그리는 코드를 구현해놓은 게 있어서 한 번 가져와 봤다. 아래 이미지와 같이 이쁘게 나오는 것을 확인할 수 있다.
그런데 지난 학기 프로젝트에서 사용해봤는데, 생각보다 별로였다. 너무 수려하다고 해야 하려나? 그리고 솔직히 작성하는 것도 지나치게 오래 걸린다. 만약 시간이 남아돌아서 이쁜 그림 그리는 데에 시간을 더 많이 쏟고 싶다면 이걸 쓰겠지만 다시 쓰기에는 노력에 비해 결과물이 조금 쓸모가 없었다. 대신 발표용 PPT에 사용할 목적이라면 생각보다 괜찮은 것 같았다. 아니면 ChatGPT한데 만들어달라고 하는 것도 괜찮을 것 같기도 하다.
이것 말고도 draw.io나 그냥 포토샵, 워드나 한글의 도형을 사용해서 그리는 방법 등이 있을 텐데 그건 정말 수작업으로 해야 하는 것이라 생략했다. 사실 이전 프로젝트 때 만든 모델 구조도는 워드로 수작업으로 만들었었다. 수작업이 제일 내가 원하는 대로 만들 수 있어서 어쩔 수 없는 것 같다.
추가 과제
Convolutional Layer 설명하기
Convolutional layer는 이미지의 feature을 찾는 layer로, feature map을 만들어 낸다.
Pooling Layer 설명하기
Convolutional layer에서 만든 feature map을 축소해서 연산에 사용되는 데이터의 양을 줄이고, 모델이 중요한 feature에 집중하도록 한다. 또한, 너무 세세한 feature를 줄임으로써 overfitting을 방지할 수도 있다.
Dense Layer 설명하기
이전 layer에서 추출한 feature를 통해 최종 output을 만드는 layer로, 최종 classification을 한다.
참고문헌
[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.