마음을 다스리는 글

욕심을 비우면 마음보다 너른 것이 없고, 탐욕을 채우면 마음보다 좁은 곳이 없다.
염려를 놓으면 마음보다 편한 곳이 없고, 걱정을 붙들면 마음보다 불편한 곳이 없다.
-공지사항: 육아일기 등 가족이야기는 비공개 블로그로 이사했습니다.

2025년 6월 23일 월요일

Deep Learning - 제6장 심층 학습

 Deep Learning을 공부하고자 하는 많은 입문자들을 위하여 내가 공부하면서 입문서로 가장 도움이 되었던 Michael Nielsen의 온라인 도서를 여기 장별로 정리하여 두고자 한다.

이 장에서 나오는 내용들은 그의 온라인 도서 6장에 대한 내용을 한국어로 얼기설기 번역한 것이다. 번역이 어색하지만, 개념을 이해하기에는 무리가 없다고 생각한다.


지난 장에서 우리는 심층 신경망이 얕은 신경망보다 훨씬 학습시키기 어렵다는 것을 알았습니다. 심층 신경망을 학습시킬 수 있다면 얕은 신경망보다 훨씬 더 강력할 것이라고 믿을 만한 충분한 이유가 있기 때문에 이는 안타까운 일입니다. 하지만 지난 장의 내용이 낙담스럽더라도 우리는 포기하지 않을 것입니다 이번 장에서는 심층 신경망을 학습시키는데 사용할 수 있는 기술을 개발하고 실제로 적용해볼 것입니다. 또한 이미지 인식, 음성 인식 및 기타 애플리케이션을 위한 심층 신경망 사용에 대한 최근 현황을 간략히 살펴보면서 더 넓은 그림을 조망해볼 것입니다. 그리고 신경망과 인공 지능의 미래에 대한 간략한 전망도 해볼 예정입니다.

이 장은 좀 긴 편입니다. 이 장에서 다루게 될 내용들에 대하여 간략하게 먼저 살펴보도록 하겠습니다. 이번 장의 하위 섹션들은 내용적 연관성이 크지 않으므로 신경망에 대한 기본적인 이해가 있다면, 관심있는 섹션으로 바로 이동해도 괜찮습니다.

이 장의 주요 부분은 가장 널리 사용되는 심층 신경망의 유형 중 하나인 심층 합성곱 신경망(deep convolutional network)에 대한 소개입니다. MNIST 학습 데이터에서 손으로 쓴 숫자 분류 문제를 해결하기 위하여 합성곱 신경망을 사용하는 방법에 대한 자세한 사례도 다루도록 하겠습니다.


합성곱 신경망에 대한 설명은 이 책의 앞 부분에서 이 문제를 다루는데 사용한 얕은 신경망부터 시작할 예정입니다. 그리고, 얕은 신경망을 반복적으로 수정해가면서 점점 더 강력한 신경망을 구축해가도록 하겠습니다. 이 장을 진행해가면서 convolution, pooling, 얕은 신경망으로 수행했던 것보다 더 많은 것을 수행하기 위하여 GPU를 사용하는 방법, 과적합을 억제하기 위하여 알고리즘을 이용하여 학습 데이터를 확장하고 드롭아웃 기술을 활용하는 방법, 그리고 신경망 앙상블(ensembles of networks)과 같은 기법들을 활용하는 것에 대해서도 다룰 것입니다. 이런 기법들을 활용하여 신경망을 거의 인간에 버금가는 수준으로 만들어 볼 예정입니다. 학습 중에 보지 못한 10,000개의 MNIST 테스트 이미지 중에서 우리의 신경망 시스템은 9,967개를 정확하게 분류할 수 있을 것입니다. 여기서 잘못 분류된 33개의 이미지를 살짝 미리 보여드리겠습니다. 아래 그림에서 오른쪽 상단에 표시된 숫자는 올바르게 분류했을 때의 답이며, 오른쪽 하단의 숫자는 우리 신경망 시스템이 분류한 결과를 표시합니다.


이러한 이미지 중 상당수는 사람이 분류하기도 어렵습니다. 예를 들어 맨 윗줄의 세 번째 이미지를 보시기 바랍니다. 이 이미지는 숫자 "8"을 쓴 것이지만, "9"로도 보입니다. 우리 신경망도 "9"라고 인식했습니다. 이러한 종류의 오류는 최소한 납득 가능하며, 어쩌면 칭찬할 만할 수도 있습니다. 이미지 인식에 대한 논의는 신경망(특히 합성곱 신경망)을 사용하여 이미지 인식을 수행하는 최근의 놀라운 발전에 대한 개요로 마무리하도록 하겠습니다.

이 장의 나머지 부분에서는 더 넓지만 덜 자세한 관점에서 심층 학습(deep learning)에 대해서 논의할 것입니다. 순환 신경망(recurrent neural nets) 및 장단기 메모리 장치(long short-term memory units)와 같은 다른 신경망 모델과 이러한 모델이 음성 인식, 자연어 처리 및 기타 분야에서 어떻게 응용될 수 있는지 간략하게 살펴볼 것입니다. 그리고 의도 기반 사용자 인터페이스와 같은 아이디어부터 인공 지능에서 심층 학습의 역할에 이르기까지 신경망과 심층 학습의 미래에 대해서도 살펴볼 것입니다.

이 장은 역전파, 정규화, 소프트맥스 함수 등과 같은 아이디어를 활용하고 통합하면서 이 책의 앞 장들에서 살펴본 내용들을 토대로 하고 있습니다. 그러나 이 장을 읽기 위해서 앞 장들을 자세히 공부할 필요는 없습니다.

이 장에서는 최신 신경망 라이브러리들에 대하여는 다루지 않을 예정입니다. 또 최첨단 문제를 해결하기 위해 수십 개의 계층을 가진 심층 신경망을 학습시키지도 않을 것입니다. 오히려 심층 신경망의 핵심 원리를 이해하고 간단하고 이해하기 쉬운 MNIST 문제에 이를 적용해보는 것에 초점을 맞출 것입니다. 다시 말해, 이 장에서는 현재 진행되고 있는 광범위한 심층 신경망과 심층 학습에 대한 내용들을 이해할 수 있도록 기본에 충실할 예정입니다.

합성곱 신경망 개요

이전 장들에서 우리는 신경망을 학습시켜 손으로쓴 숫자를 인식하는데 꽤 좋은 성과를 올렸습니다. 


우리는 인접한 신경망 계층이 서로 완전히 연결된 신경망을 사용하여 학습시켰습니다. 신경망 계층이 완전히 연결되었다는 것은 신경망 내의 모든 뉴런이 인접한 계층의 모든 뉴런과 연결되어 있음을 의미합니다.


특히, 입력 이미지의 각 픽셀에 대하여 해당 픽셀의 강도를 입력 계층의 해당 뉴런 값으로 대응되도록 신경망을 설계했습니다. 즉, 28×28 픽셀을 갖는 입력 이미지의 각 픽셀이 784개의 입력 뉴런에 하나씩 대응된다는 의미입니다. 그런 다음 신경망의 가중치와 편향을 학습시켜 신경망의 출력이 입력 이미지를 "1", "2", " ... , "8" 또는 "9"로 정확하게 식별하도록 만들었습니다.

이렇게 학습시킨 신경망은 꽤 잘 작동했습니다. MNIST 손글씨 숫자 데이터 세트의 학습 및 테스트 데이터를 사용하여 98% 이상의 분류정확도를 얻었었습니다. 그러나 다시 생각해보면 완전 연결 계층을 가진 신경망을 사용하여 이미지를 분류하는 것은 다소 이상합니다. 그 이유는 그러한 신경망 아키텍처가 이미지의 공간 구조를 고려하지 않기 때문입니다. 예를 들어 이런 신경망 아키텍처는 멀리 떨어져 있거나 가까이 있는 픽셀을 똑같이 취급합니다. 이러한 공간 구조의 개념은 학습 데이터의 특성을 반영하여야 합니다. 입력 데이터의 공간구조를 전혀 고려하지 않은 신경망 아키텍처를 사용하는 대신, 이를 고려한 신경망 아키텍처를 사용하면 어떻게 될까요? 이 섹션에서는 합성곱 신경망(convolutional neural network)에 대하여 다루도록 하겠습니다. 이러한 신경망은 특히 이미지 분류에 적합한 특별한 아키텍처를 사용합니다. 이 아키텍처를 사용하면 합성곱 신경망을 빠르게 학습시킬 수 있습니다. 이는 결과적으로 이미지 분류에 매우 뛰어난 심층 신경망을 학습시키는데 도움이 됩니다. 오늘날 심층 합성곱 신경망 또는 이를 변형한 신경망들이 대부분의 이미지 인식에 사용됩니다.

합성곱 신경망은 세 가지 기본 아이디어를 사용합니다. 지역 수용장(local receptive fields), 가중치 공유(shared weights)와 폴링(pooling)이 그것들입니다. 이들을 차례로 살펴보도록 하겠습니다.

지역 수용장: 앞서 언급한 완전 연결 계층에서는 입력이 뉴런의 수직적 배열로 표현되어 있습니다. 합성곱 신경망에서는 입력을 28×28 뉴런 정사각형으로 간주할 수 있습니다. 이 뉴런들의 값은 입력으로 사용되는 28×28 픽셀 강도에 해당합니다.


마찬가지로, 입력 픽셀은 은닉층 뉴런에 연결될 것입니다. 하지만 모든 입력 픽셀을 모든 은닉층 뉴런에 연결하지는 않을 것입니다. 대신 입력 이미지의 작고 국소적인 영역에서만 연결을 만들도록 하겠습니다.

더 정확히 말하면, 첫 번째 은닉층의 각 뉴런은 입력 뉴런의 작은 영역, 예를 들어 5×5영역 (25개 입력 픽셀에 해당)에 연결됩니다. 따라서 특정 은닉층 뉴런의 경우 다음과 같이 연결될 수 있습니다.



입력 이미지에서 은닉 뉴런과 연결된 영역을 은닉 뉴런의 지역 수용장이라고 부릅니다. 이것이 입력 픽셀에 대한 작은 창과 같습니다. 각 연결은 가중치를 학습합니다. 그리고 은닉 뉴런은 전체적인 편향 또한 학습합니다. 우리는 그 특정 은닉 뉴런이 자신의 특정 지역 수용장을 분석하는 것을 학습한다고 간주할 수 있습니다.

우리는 전체 입력 이미지에 대하여 지역 수용장을 슬라이딩 합니다.(주: 마치 창문을 옆으로 밀어서 움직이는 것처럼 지역 수용장이라는 작은 영역을 전체 이미지의 왼쪽에서 오른쪽으로, 위에서 아래로 차례로 움직이면서 각 위치에 특증일 추출해 나가는 것을 의미한다.) 각 지역 수용장에 대하여 첫 번째 은닉 계층에 다른 은닉 뉴런이 있습니다. 이것을 구체적으로 설명하기 위하여 왼쪽 상단 모서리에 있는 지역 수용장부터 시작해보겠습니다.


다음, 우리는 지역 수용장을 한 픽셀(다시말 해 한 뉴런) 오른쪽으로 이동(sliding)시켜 두번째 은닉 뉴런과 연결시킵니다.


이와 같은 방법으로 첫 번째 은닉 뉴런을 구성합니다. 만약 28×28 크기의 입력 이미지와 5×5 크기의 지역 수용장이 있다면, 은닉 계층에는 24×24개의 뉴런이 존재할 것이라는 점에 유의하시기 바랍니다. 이는 입력 이미지의 왼쪽 끝에서 시작해서 오른쪽 끝까지 지역 수용장을 23개의 뉴런만큼, 위에서 아래까지도 23개의 뉴런만큼 이동시킬 수 있기 때문입니다.

여기서는 지역 수용장이 한 번에 한 픽셀씩 이동하는 것으로 가정했습니다. 사실, 다른 스트라이드 길이(stride length)를 사용할 수도 있습니다. 예를 들어 지역 수용장을 오른쪽 (또는 아래)으로 2픽셀씩 이동할 수도 있습니다. 이 경우 스트라이드 길이가 2라고 이야기합니다. 이 장에서는 주로 스트라이드 길이를 1로 사용하지만, 때로는 다른 스트라이드 길이를 사용할 수도 있다는 점도 기억하시면 좋을 것 같습니다.

공유 가중치와 편향: 각 은닉 뉴런은 각각의 지역 수용장에 연결된 편향과 5×5 크기의 가중치를 가지고 있습니다. 아직 언급하지 않았던 것은 24×24개의 은닉 뉴런에 대해 동일한 가중치와 편향을 사용하려 한다는 것입니다. 다시 말해, j,k번째 은닉 뉴런의 출력은 다음과 같습니다.

σ(b+4l=04m=0wl,maj+l,k+m).

여기서 σ는 신경 활성화 함수이며, 이전 장에서 사용했던 시그모이드 함수를 사용할 수도 있습니다. b는 공유되는 값인 편향입니다. wl,m은 공유 가중치의 5×5 배열입니다. 마지막으로 ax,y는 위치 x, y에서의 입력 활성화를 나타냅니다.

이는 첫 번째 은닉 계층의 모든 뉴런이 입력 이미지의 서로 다른 위치에서 정확히 동일한 특징을 감지한다는 의미입니다. 이것이 왜 합리적인지 이해하기 위해, 가중치와 편향이 숩겨진 뉴런이 특정 지역 수용장에서 예를 들어 수직 모서리를 찾아낼 수 있도록 설정되어 있다고 가정해보겠습니다. 그 능력은 이미지의 다른 위치에서도 유용할 가능성이 높습니다. 따라서 이미지 전체에 동일한 감지기를 적용하는 것이 유용합니다. 좀 더 추상적으로 표현하자면 합성곱 신경망은 이미지의 특성이 위치가 바뀌어도 변하지 않는다는 병진 불변성에 잘 적응합니다. 예를 들어, 고양이 사진을 약간 이동시키더라도 여전히 고양이 사진입니다.

이러한 이유로 이렇게 입력 계층에서 은닉 계층으로 대응시키는 것을 때로는 특징 맵(feature map)이라고 부릅니다. 특징 맵을 정의하는 가중치를 공유 가중치라고 부릅니다. 그리고 이러한 방식으로 특징 맵을 정의하는 편향을 공유 편향이라고 부릅니다. 공유 가중치와 편향은 종종 커널(kernel) 또는 필터(filter)를 정의한다고 합니다. 여러 문헌에서 이러한 용어들이 약간 다른 방식으로 사용되기도 하므로 더 정확하게 정의하지는 않도록 하겠습니다. 대신 잠시 후에 좀 더 구체적인 예를 살펴보도록 하겠습니다.

지금까지 설명한 신경망의 구조는 한 종류의 지역화된 특징만 감지할 수 있습니다. 이미지 인식을 위해서는 하나 이상의 특징 맵이 필요합니다. 따라서 완전한 합성곱 계층은 여러 개의 서로 다른 특징 맵으로 구성되기도 합니다.


위 예에서는 3개의 특징 맵이 있습니다. 각 특징 맵은 5×5 크기의 공유 가중치 세트와 하나의 공유 편향으로 정의됩니다. 그 결과 네트워크는 3가지 다른 종류의 특징을 감지할 수 있으며, 각 특징은 전체 이미지에서 감지될 수 있습니다. 위 다이어그램에서 3개의 특징 맵만 표시하고 있지만, 실제로 합성곱 신경망에서는 더 많은 특징 맵이 사용되기도 합니다. 초기 합성곱 신경만 중 하나인 LeMet-5는 MNIST 숫자를 인식하기 위하여 각각 5×5 크기의 지역 수용장과 관련된 6개의 특징 맵을 사용했습니다. 따라서 위에 설명된 예는 실제로 LeNet-5와 매우 유사합니다. 이 장의 뒷부분에서 개발할 예에서는 20개 및 40개의 특징 맵을 가진 합성곱 계층을 사용할 것입니다. 학습된 특징 중 일부를 간단히 살펴보겠습니다.


위 20개의 이미지는 20개의 서로 다른 특징 맵(필터 또는 커널)을 표시합니다. 각 맵은 지역 수용장의 5×5 가중치에 해당하는 5×5 블록 이미지를 나타냅니다. 흰식 블록은 더 작은 (일반적으로 작은 음수) 가중치를 의미하므로 특징 맵은 해당 입력 픽셀에 덜 반응합니다. 어두운 불록은 더 큰 가중치를 의미하므로 특징 맵은 해당 입력 픽셀에 더 많이 반응합니다. 매우 간단히 말하자면, 위의 이미지는 합성곱 계층에 반응하는 특징의 유형을 보유준다고 볼 수 있습니다.

그렇다면 특징 맵에서 어떤 결론을 얻을 수 있을까요? 여기서는 무작위로 에상할 수 있는 것 이상의 공간 구조가 반영되어 있습니다. 많은 특징들이 밝고 어두운 명확한 하위 영역을 가지고 있습니다. 이는 우리의 신경망이 실제로 공간 구조와 관련된 것들을 학습하고 있다는 것을 나타냅니다. 그러나 그 이상으로 이러한 특징 감지기가 무엇을 학습하고 있는지 확인하기는 어렵습니다. 확실히 우리는 많은 전통적 이미지 인식 접근 방식에 사용되었던 가버 필터(Gabor filter)와 같은 것을 학습하고 있지는 않습니다. 사실, 현재 합성곱 신경망이 학습한 특징을 더 잘 이해하기 위한 많은 연구가 진행 중입니다. 이런 연구에 관심이 있으시면 Matthew Zeiler와 Rob Fergus의 논문, "Visualizing and Understanding Convolutional Networks" (2013)를 읽어보시기 바랍니다.

가중치와 편향을 공유하는 것의 가장 큰 장점은 합성곱 신경망에 관련된 매개변수의 수를 크게 줄여준다는 것입니다. 각 특징 맵에 대해서 25 (5×5)개의 공유 가중치와 하나의 공유 편향이 필요합니다. 따라서 각 특징 맵에는 26개의 매개변수가 필요합니다. 20개의 특징 맵이 있다면 합성곱 계층을 정의하는 총 매개변수는 520 (20×26)개 입니다. 이에 비해 책 앞 부분에서 사용했던 것처럼 784  (28×28)개의 입력 뉴런과 비교적 적은 30개의 은닉 뉴런을 가진 완전히 연결된 첫 번재 계층이 있다고 가정해보겠습니다. 이는 총 784×30개의 가중치에 30개의 편향이 추가되어 총 23,550개의 매개변수가 필요합니다. 즉, 완전히 연결된 계층은 합성곱 계층보다 40배 이상 많은 매개변수가 필요합니다.

물론 두 모델을 근본적으로 다르기 때문에 매개변수의 수를 직접적으로 비교할 수는 없습니다. 그러나 직관적으로 볼 때 합성곱 계층에서 병진 불변성을 활용하는 것이 완전히 연결된 모델을 가지고 동일한 성능을 얻는 데 필요한 매개변수 수 보다 훨씬 적은 매개변수 수가 필요할 가능성이 높습니다. 이는 결과적으로 합성곱 모델의 훈련 속도를 높이고 궁극적으로 합성곱 계층을 사용하여 심층 신경망을 구축하는데 도움이 될 것입니다.

덧붙여서 합성곱이라는 이름은 식 (125)의 연산이 때때로 합성곱(convolution)이라고 알려져 있기 때문에 유래했습니다. 좀 더 정확히 말하자면, 식 (125)는 때로 a1=σ(b+wa0)와 같이 표시되기도 합니다. 여기서 a1은 하나의 특징 맵에서 나오는 출력 활성화의 집합을 나타내고 a0은 입력 활성화의 집합이며, *는 합성곱 연산을 의미합니다. 여기서 합성곱 연산을 수학적으로 깊이있게 다루지 않을 것이므로 이 것에 대해서 너무 걱정할 필요는 없습니다. 하지만 이름의 유래에 대해서는 알아두는 것이 좋습니다.

폴링 계층: 방금 설명한 합성곱 계층 외에도 합성곱 신경망에서는 폴링 계층도 사용됩니다. 폴링 계층은 일반적으로 합성곱 계층 바로 뒤에 사용됩니다. 폴링 계층의 역할은 합성곱 계층의 출력에서 정보를 단순화하는 것입니다.

좀 더 상세히 말하자면, 폴링 계층은 합성곱 계층에서 출력된 각 특징 맵을 가져와 축약된 특징 맵을 만듭니다. 예를 들어 폴링 계층의 각 뉴런(unit)은 이전 계층에서 2×2개의 뉴런 영역에 대한 요약 작업을 할 수 있습니다. 구체적인 예로 폴링의 일반적인 절차 중 하나는 최대 폴링(max-pooling)이라고 하는 것입니다. 최대 폴링에서 폴링 뉴런은 단순히 다음 그림에서 설명하고 있는 것처럼 2×2 입력 영역에서 최대 활성화를 출력합니다.


합성곱 계층에서 24×24개의 뉴런이 출력되므로, 폴링 후에는 12×12개의 뉴런이 된다는 점에 유의하시기 바랍니다.

위에서 언급했듯이 합성곱 계층은 보통 하나 이상의 특징 맵을 포함합니다. 우리는 각 특징 맵에서 개별적으로 최대 폴링을 적용할 수 있습니다. 이러한 경우 아래 그림과 같은 형태가 될 것입니다.


최대 폴링은 신경망이 이미지 영역 내에 특정 특징이 있는지 여부를 묻는 방법으로 간주할 수 있습니다. 그런 다음 정확한 위치정보는 버립니다. 이에 대한 직관적 설명은 일단 특징이 발견되면 다른 특징과 비교했을 때 정확한 위치 보다는 대락적인 위치만 알면된다는 것입니다. 이러한 방식의 큰 장점은 폴링된 특징의 수가 훨씬 적다는 것입니다. 따라서 이후 계층에서 필요한 매개 변수 수를 줄일 수 있습니다.

최대 폴링만이 폴링에서 사용되는 유일한 기법이 아닙니다. 일반적으로 사용되는 또 다른 폴링으로는 L2 폴링이라는 것이 있습니다. 이 폴링 기법은 2×2 뉴런 영역의 최대 활성화 값을 추출하는 대신 2×2 영역 내에 있는 제곱합의 제곱근을 추출합니다. 세부 사항은 다르지만 직관적인 작동 방식은 최대 폴링과 유사합니다. L2 폴링은 합성곱 계층의 정보를 압축하는 방법입니다. 실제로 두 기술 모두 널리 사용되었습니다. 그리고 때로는 다른 유형의 폴링 연산을 하고도 합니다. 성능을 최대화하려는 경우 검증 데이터를 사용하여 여러 가지 다른 폴링 방식을 비교하고 가장 잘 작동하는 방식을 채택할 수 있습니다. 하지만 우리는 그런 세부적인 최적화에 대해서는 다루지 않을 예정입니다.

전체 구성: 지금까지 논의한 모든 아이디어들을 바탕으로 완전한 합성곱 신경망을 구성할 수 있습니다. 이는 우리가 방금 살펴본 아키텍처에 MNIST 숫자에 대한 10가지 가능한 값('0', '1', '2' 등)에 해당하는 10개의 출력 뉴런 계층을 추가한 것입니다.



위 신경망은 MNIST 이미지의 픽셀 강도를 나타내는데 사용되는 28×28 입력 뉴런으로 시작합니다. 그 다음에는 5×5 지역 수용장과 2개의 특징 맵을 사용하는 합성곱 계층이 이어집니다. 그 결과 3×24×24 크기의 은닉 특징 뉴런 계층이 생성됩니다.

신경망의 마지막 연결 계층은 완전 연결 계층입니다. 즉, 이 계층은 최대 폴링 계층의 모든 뉴런을 10개의 출력 뉴런으로 연결합니다. 이 완전 연결 아키텍처는 이전 장에서와 같습니다. 그러나 위 다이어그램에서 모든 연결을 보여 주는 대신 단순화를 위하여 하나의 화살표만 표시했습니다.

이 합성곱 아키텍처는 이전 장의 아키텍처와 상당히 다릅니다. 그러나 전체적인 그림은 유사합니다. 즉, 가중치와 편향에 의해 동작이 결정되는 많은 간단한 뉴런으로 구성된 신경망입니다. 그리고 전체적인 목표는 여전히 동일합니다. 즉, 학습 데이터를 사용하여 신경망의 가중치와 편향을 학습시켜 입력된 숫자를 잘 분류하도록 하는 것입니다.

특히, 이 책의 앞 부분과 마찬가지로 확률적 경사 하강법과 역전파를 사용하여 신경망을 훈련시킬 것입니다. 이는 이전 장과 거의 동일한 방식으로 진행됩니다. 그러나, 역전파 절차에 몇 가지 수정이 필요합니다. 그 이유는 이전의 역전파 유도가 완전 연결 계층을 가진 신경망을 위한 것이기 때문입니다. 다행히 합성곱 및 최대 폴링 계층에 맞도록 이를 수정하는 것은 그리 어렵지 않습니다. (이것은 숙제로 남기도록 하겠습니다.)

합성곱 신경망 실습

합성곱 신경망의 핵심 아이디어에 대해서 살펴 보았습니다. 몇 가지 합성곱 신경망을 구현해 보고 MNIST 숫자 분류 문제에 적용하여 실제 어떻게 작동하는지 살펴보도록 하겠습니다. 이를 위해 사용할 프로그램은 network3.py 이며, 이전 장에서 살펴변 network.py 나 network2.py 프로그램의 개선된 버전입니다. 직접 실행시켜 보고 싶다면 코드를 Github에서 받을 수 있습니다. 다음 섹션에서 network3.py 에 대하여 자세히 살펴보도록 하겠습니다. 이 섹션에서는 network3.py에서 라이브러리를 사용하여 합성곱 신경망을 구축해보도록 하겠습니다.

network.py 와 network2.py는 Python과 행렬 라이브러리인 Numpy를 사용하여 구현되어있습니다. 이 프로그램들은 기본 원리부터 시작하여 역전파, 확률적 경사 하강법 등 세부 사항까지 직접 구현하고 있습니다. 하지만 이제 이러한 세부 사항을 이해했으므로, network3.py 에서는 Theano 라는 기계학습 라이브러리를 사용하도록 하겠습니다. Theano를 사용하면 관련된 모든 매핑을 자동으로 계산하므로 합성곱 신경망에 대한 역전파를 쉽게 구현할 수 있습니다. 또한 Theano는 이해하기 쉽도록 개발되어 있으며, 속도가 빠르지 않았던 이전 코드들 보다 훨씬 빠르게 구동됩니다. 이 라이브러리를 사용하면 복잡한 신경망을 훨씬 쉽게 학습시킬 수 있습니다. 특히 Theano의 큰 특징 중 하나는 CPU 또는 사용 가능한 경우 GPU에서 코드를 쉽게 실행할 수 있다는 것입니다. GPU에서 실행하면 속도가 상당히 향상되며 더 복잡한 신경망을 더 용이하게 학습시킬 수 있습니다.

이번 장에서 다루는 프로그램을 실행시켜 보고 싶다면, Theano를 설치해야합니다. 프로젝트 홈페이지의 지침에 따라 Theano를 설치할 수 있습니다. 예제들은 Theano 0.6을 활용하여 개발되어 있습니다. 이 장의 실행 결과 중 일부는 GPU 없이 Mac OS X Yosemite, 일부는 NVIDIA GPU가 있는 Ubuntu 14.04에서 실행된 것입니다. 그리고 일부 실행 결과는 두 플랫폼 모두에서 실행된 것입니다. network3.py 를 실행하려면 소스에서 GPU 플래그를 True 또는 False로 적절히 설정해야합니다. 그 외에도 GPU에서 Theano를 설정하기 실행하기 위해서 일련의 작업이 필요할 수도 있습니다. 필요한 지침이나 튜토리얼들을 구글에서 검색해볼 수 있습니다. 로컬 컴퓨터로 GPU를 사용할 수 없다면 Amazon Webservice EC2 G2 Spot Instance를 사용하는 것도 대안이 될 수 있습니다. GPU를 사용하더라도 코드가 실행되는데는 시간이 어느 정도 걸립니다. CPU 환경에서 이 장에서 다루는 가장 복잡한 프로그램을 실행시키면 며칠이 걸리기도 합니다. 이 경우 epoch를 줄이거나 실행을 생략하시기 바랍니다.

(주: Theano 라이브러리는 현재 PyTensor 라이브러리로 대체되었습니다.)

성능 측정의 기준을 삼기 위하여 100개의 은닉 뉴런만 포함하는 단일 은닉 계층을 사용하는 얕은 신경망 아키텍처에서 부터 시작하도록 하겠습니다. 학습률 η=0.1, 미니 배치 크기 10, 정규화 없이 60 epoch 동안 학습을 학습시키도록 하겠습니다.

>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
        FullyConnectedLayer(n_in=784, n_out=100),
        SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1, 
            validation_data, test_data)
* 주석: PyTensor 라이브러리로 구현한 network3.py와 run6.py는 이 링크에서 받을 수 있다. (python 3.13 환경에서 구동 가능 / PyTensor 라이브러리 설치 필요)

위와 같이 실행한 결과 97.80%의 분류 정확도를 얻을 수 있습니다. 이는 검증 데이터를 가지고 최고 분류 정확도를 얻은 학습 에포트에서 평가된 테스트 데이터에 대한 분류 정확도입니다. 테스트 정확도를 평가할 시점을 결정하는 데 검증 데이터를 사용하는 것은 테스트 데이터에 대한 과적합을 피하는데 도움이 됩니다. (이전 장에서 논의한 바 있습니다.) 이후로도 이와 같은 관행을 따라 분류 정확도를 평가하도록 하겠습니다. 신경망의 가중치와 편향이 무작위로 초기화되기 때문에 결과는 약간씩 차이가 날 수 있습니다.

97.80%라는 정확도는 3장에서 비슷한 신경망 아키텍처와 학습 파라미터를 사용하여 얻었던 98.04% 정확도와 유사합니다. 특히 두 예 모두 100개의 은닉 뉴런을 포함하는 단일 은닉 계층이 있는 얕은 신경망을 사용한 것입니다. 뿐만 아니라 두 경우 모두 60 epoch 동안 학습을 진행했고, 미니 배치 크기도 10, 학습률 η=0.1을 사용했습니다.

그러나 이전 신경망에는 두 가지 차이점이 있습니다. 첫째, 과적합의 영향을 줄이기 위하여 이전 신경망에는 정규화 기술을 적용했었습니다. 이번 장의 신경망에 정규화를 적용하면 정확도가 향상되지만, 그 효과는 미미하므로 정규화를 적용하지 않았습니다. 둘째, 이전 장의 신경망의 마지막 계층은 시그모이드 활성화 함수와 교차 엔트로피 비용 함수를 사용한 반면, 이번 장의 신경망에는 소프트맥스 최종 계층과 로그-우도(log-likelihood) 비용 함수를 사용했습니다. 3장에서 언급한 바와 같이 이는 큰 변경은 아닙니다. 특별히 이유가 있어서 이렇게 변경한 것은 아닙니다. 주된 이유는 소프트맥스에 로그-우도 비용 함수를 사용하는 것이 이미지 분류에서 일반적이기 때문입니다.

심층 신경망 아키텍처를 사용하면 지금 얻은 결과보다 더 나은 성능을 얻을 수 있을까요?

신경망 시작 부분에 합성곱 계층을 삽입하는 것부터 시작해보도록 하겠습니다. 5×5 지역 수용장, 스트라이드 길이 1, 20개의 특징 맵을 사용해보도록 하겠습니다. 또한 2×2 폴링 윈도우를 사용하여 특징을 결합하는 최대 폴링 계층도 추가하도록 하겠습니다. 따라서 전체 신경망의 구조는 이전 섹션에서 논의한 것과 매우 유사하지만, 완전 연결 계층이 하나 더 추가된 모습입니다.


이 아키텍처에서 합성곱 및 풀링 계층은 입력되는 학습 이미지의 지역적인 공간 구조에 대하여 학습하고, 뒤쪽의 완전 연결 계층은 이미지 전체의 전역 정보를 통합하여 보다 추상적인 수준에서 학습을 하는 것으로 간주할 수 있습니다. 이러한 아키텍처는 합성곱 신경망에서 흔히 사용되고 있습니다.

이 신경망을 학습시키고 성능을 확인해보겠습니다.

>>> net = Network([
        ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28), 
                      filter_shape=(20, 1, 5, 5), 
                      poolsize=(2, 2)),
        FullyConnectedLayer(n_in=20*12*12, n_out=100),
        SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1, 
            validation_data, test_data)  

* 주석: PyTensor 라이브러리로 구현한 network3.py와 run6.py는 이 링크에서 받을 수 있다. 
  (CPU 환경에서 실행한다면, 오랜 시간이 걸립니다.)

이를 실행시킨 결과 98.78%의 정확도를 얻을 수 있습니다. 이는 이전 어떤 결과보다도 상당히 향상된 수치입니다. 실제로 오류율을 1/3 이상 줄였으며, 이러한 개선은 상당한 성과입니다.

신경망 아키텍처를 설계하면서 합성곱 계층과 폴링 계층을 단일 계층으로 취급했습니다. 이를 별도 계층으로 간주할 것인지 단일 계층으로 간주할 것인지는 개인의 취향에 달려있습니다. network3.py 에서는 코드를 좀 더 간결하게 만들기 위해서 단일 계층으로 취급하도록 하겠습니다. 그러나, 별도 계층으로 취급하고 싶다면, network3.py 를 쉽게 수정할 수도 있을 것입니다.

98.78%의 분류 정확도를 더 개선할 수 있을까요?

두 번째 합성곱-폴링 계층을 추가해보도록 하겠습니다. 기존의 합성곱-폴링 계층과 완전 연결 은닉 계층 사이에 추가해보도록 하겠습니다. 새로 추가하는 계층도 5×5 지역 수용장과 2×2 영역에 대하여 폴링을 하도록 하겠습니다. 이전에 실행 것과 비슷한 하이퍼파라미터를 사용하여 어떤 학습 결과가 나오는지 살펴보겠습니다.

>>> net = Network([
        ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28), 
                      filter_shape=(20, 1, 5, 5), 
                      poolsize=(2, 2)),
        ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12), 
                      filter_shape=(40, 20, 5, 5), 
                      poolsize=(2, 2)),
        FullyConnectedLayer(n_in=40*4*4, n_out=100),
        SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1, 
            validation_data, test_data)
* 주석: PyTensor 라이브러리로 구현한 network3.py와 run6.py는 이 링크에서 받을 수 있다. 
  (run6.py의 주석을 변경해서 실행하도록 하였습니다. GPU 환경에서도 실행시간이 상당히 느립니다. NVIDIA GPU가 없다면 google colab에서 실행해보는 것을 권장합니다.)

새로 추가한 계층으로 분류 정확도는 99.06%까지 개선된 것을 확인할 수 있습니다.

이 시점에서 두 가지 자연스러운 질문을 던질 수 있습니다. 첫 번째 질문은 두 번째로 추가한 합성곱-폴링 계층은 도데체 어떤 의미를 갖는가 하는 것입니다. 두 번째 합성곱-폴링 계층은 입력으로 12×12 크기의 "이미지"를 입력으로 받는 것으로 생각할 수 있습니다. 여기서 "픽셀"은 원래 입력 이미지에서 특정 지역화된 특징의 존재(또는 부재)를 나타냅니다. 따라서 이 계층은 원래 입력 이미지의 변형된 버전을 입력으로 받는다고 생각할 수 있습니다. 그 버전은 추상화되고 압축되었지만 여전히 많은 공간 구조 정보를 가지고 있으므로 두 번째 합성곱-폴링 계층을 사용하는 것이 합리적이라 생각할 수 있습니다.

이는 그럭저럭 만족스러운 답변이지만, 두 번째 질문이 남습니다. 이전 계층의 출력은 20개의 개별 특징 맵을 포함하므로 두 번째 합성곱-폴링 계층에는 20×12×12개의 입력이 있습니다. 첫 번째 합성곱-폴링 계층의 경우와 달리 마치 합성곱-폴링 계층에 단일 이미지가 아닌 20개의 개별 이미지가 입력되는 것과 같습니다. 두 번째 합성곱-폴링 계층의 뉴런은 이러한 여러 입력 이미지에 어떻게 반응해야 할까요? 사실 이 계층의 각 뉴런이 자신의 지역 수용장 내의 모든 20×5×5 입력 뉴런으로 부터 학습하도록 허용할 것입니다. 좀 더 비공식적으로 말하면 두 번째 합성곱-폴링 계층의 특징 감지기는 이전 계층의 모든 특징에 접근할 수 있지만, 자신의 특정 지역 수용장 내에서만 가능합니다.

Rectified linear units (ReLU) 활용: 방금 우리가 구현한 신경망은 사실 1998년의 기념비 적인 논문에서 기술한 신경망의 변형입니다. 세부적인 차이는 많지만 넓게 보면 우리의 신경망도 이 논문에서 설명하고 있는 신경망과 매우 유사합니다.

MNIST 문제에서 소개된 LeNet-5라고 알려진 신경망입니다. 이 신경망은 추가적으로 활용할 수도 있으며, 이해와 직관을 쌓는데 좋은 기반이 됩니다. 특히 결과를 개선하기 위해 신경망을 다양하게 변형할 수 있는 많은 방법이 있습니다.

이에 대한 시작으로, 뉴런에 시그모이드 활성화 함수 대신, ReLU를 사용해보도록 하겠습니다. 즉 활성화 함수 f(z)max(0,z)를 사용해 보겠습니다. 학습율 η=0.03으로 60번의 epoch동안 학습시켜 보도록 하겠습니다. 또한 정규화 매개변수 λ=0.1로 L2 정규화 기술도 활용하도록 하겠습니다.

>>> from network3 import ReLU
>>> net = Network([
        ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28), 
                      filter_shape=(20, 1, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12), 
                      filter_shape=(40, 20, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
        SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.03, 
            validation_data, test_data, lmbda=0.1)

위와 같이 실행하면 99.23%의 분류 정확도를 얻을 수 있습니다. 이는 시그모이드를 활용한 결과인 99.06%보다 약간 향상된 수치입니다. 하지만, 여러 가지 실험을 해보면, ReLU 기반의 신경망이 시그모이드 활성화 함수를 기반으로 한 신경망보다 일관적으로 우수한 성능을 보인다는 것을 발견할 수 있습니다. 우리의 문제(MNIST 손글씨 인식)에 ReLU를 사용하는 것은 확실한 이점이 있는 것 같습니다.

ReLU 함수의 어떤 점들이 시그모이드 함수나 tanh 함수보다 나은 성능을 보이는 것일까요? 현재로서는 이 질문에 대한 명확한 이해가 부족합니다. 실제로 ReLU는 최근 몇 년 동안에야 널리 사용되기 시작했습니다. 이러한 최근의 추세는 경험적인 것입니다. 몇몇 사람들은 어쩌면 직감이나 경험적인 주장에 근거하여 ReLU를 사용하기 시작했고, 벤치마크 데이터 세트 분류에서 긍정적인 결과를 얻었으며, ReLU를 사용하는 경향이 확산되기 시작했습니다. 이상적인 상황을 가정하면, 어떤 응용 분야에 어떤 활성화 함수를 선택해야하는지를 알려주는 이론이 있어야 합니다. 하지만 현재 우리는 이러한 이상적인 상황과 동떨어져 있습니다. 더 나은 활성화 함수를 선택함으로써 훨씬 더 큰 개선이 이루어냈다고 해서 놀라운 일은 아닐 것입니다. 그리고 앞으로 수십 년 안에 활성화 함수에 대한 강력한 이론이 개발될 것이라고 기대합니다. 오늘날 우리는 여전히 제대로 이해되지 않은 경험과 이를 기반으로 한 법칙들에 의존할 수 밖에 없습니다.

학습데이터 확장: 결과를 개선할 수 있는 또 다른 방법은 알고리즘을 활용하여 학습 데이터를 확장하는 것입니다. 학습 데이터를 확장하는 간단한 방법은 각 훈련 이미지를 한 픽셀씩 위, 아래, 왼쪽 또는 오른쪽으로 이동시키는 것입니다. shell prompt에서 expand_mnist.py 프로그램을 실행하여 이를 수행할 수 있습니다.

$ python expand_mnist.py

이 프로그램을 실행하면 50,000개의 MNIST 학습 이미지를 사용하여 250,000개의 학습 이미지가 포함된 확장된 학습 데이터 세트를 생성합니다. 그런 다음 이 학습 이미지를 사용하여 신경망을 훈련시킬 수 있습니다. 위에서 사용한 것과 동일한 신경망을 ReLU 함수를 이용하여 학습시킬 것입니다. 초기 실험에서 학습 epoch를 줄였는데 이는 5배나 많은 데이터로 학습을 진행하기 때문입니다. 하지만 실제로 데이터를 확장하는 것은 과적합의 영향을 상당히 줄이는 것으로 나타났습니다. 그래서 몇 번의 실험 끝에 결국 60 epoch 동안 훈련하는 것으로 되돌아 갔습니다. 어쨌든 학습을 시작해봅시다.

>>> expanded_training_data, _, _ = network3.load_data_shared(
        "../data/mnist_expanded.pkl.gz")
>>> net = Network([
        ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28), 
                      filter_shape=(20, 1, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12), 
                      filter_shape=(40, 20, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
        SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03, 
            validation_data, test_data, lmbda=0.1)

확장된 학습 데이터를 사용하여 위와 같이 실행하면 99.37%의 정확도를 얻을 수 있습니다. 이처럼 사소한 변경으로도 훈련 정확도를 크게 향상시킬 수 있습니다. 실제로 앞서 논의한 바와 같이 알고리즘적으로 데이터를 확장하는 아이디어는 다른 방식으로도 적용할 수 있습니다. 이전 논의에서 얻은 일부 결과의 특징을 다시 한번 언급하자면, 2003년에 Simard, Steinkraus, Platt은 우리와 매우 유사한 신경망(두개의 학성곱-폴링 계층과 그 뒤에 100개의 뉴런을 가진 완전 연결 은닉 계층을 사용)을 사용하여 MNIST 손글씨 분류하는 문제의 정확도를 99.6%로 향상시켰습니다. 그들의 아키텍처에는 몇가지 세부적인 차이점이 있었습니다. 예를 들어 ReLU를 사용하지는 않았지만, 그들의 성능을 향상시킨 핵심은 학습 데이터를 확장한 것이었습니다. 그들은 MNIST 학습 이미지를 회전하고 이동시키고 기울여서 이를 달성했습니다. 또한 사람이 글을 쓸 때 손 근육이 겪는 무작위적인 떨림을 모방하는 "탄성 왜곡"이라는 프로세스도 개발했습니다. 이러한 모든 프로세스를 결합하여 학습 데이터의 실제 크기를 크게 늘렸고, 이렇게 함으로써 그들은 99.6%라는 분류 정확도를 달성했습니다.

완전 연결 계층 추가하기: 분류 정확도를 훨씬 더 개선할 수는 업을까요? 한 가지 가능성은 위와 정확히 동일한 절차를 활용하되, 완전 연결 계층의 크기를 늘리는 것입니다. 300개와 1,000개의 뉴런으로 시도해 본 결과 각각 99.46%와 99.43%의 정확도를 얻었습니다. 흥비롭지만 이는 이전 결과 99.37%에 비해 확실한 개선이라고 보기는 어렵습니다.

완전 연결 계층을 하나 더 추가하는 것은 어떨까요? 완전 연결 계층을 하나 더 추가하여 100개의 은닉 뉴런을 가진 완전 연결 계층이 두 개가 되도록 해보겠습니다.

>>> net = Network([
        ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28), 
                      filter_shape=(20, 1, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12), 
                      filter_shape=(40, 20, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
        FullyConnectedLayer(n_in=100, n_out=100, activation_fn=ReLU),
        SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03, 
            validation_data, test_data, lmbda=0.1)

이렇게 하면 분류 정확도 99.43%를 얻을 수 있습니다. 다시 말하지만, 확장된 신경망은 크게 도움이 되지 않습니다. 300개와 1,000개의 뉴런을 완전 연결 계층에 추가해도 분류 정확도는 99.48%와 99.47%입니다. 고무적이긴 하지만 여전히 결정적인 개선이라고 보기에는 무리가 있습니다.

여기서 무슨 일이 일어나고 있는 걸까요? 완전 연결 게층을 추가하거나 확장해도 MNIST 손글씨 분류 문제에는 정말로 도움이 되지 않는 걸까요? 아니면 우리 신경망이 더 잘할 수 있는 능력이 있지만, 우리의 학습 방식이 잘못된 것일까요? 예를 들어 과적합 경향을 줄이기 위해 더 강력한 정규화 기술을 사용할 수는 없을까요? 한 가지 가능성은 3장에 소개한 드롭아웃 기술입니다. 드롭아웃의 기본 아이디어는 신경망을 학습시키는 동안 개별 활성화를 무작위로 제거하는 것입니다. 이렇게 하면 모델이 개별 증거의 손실에 더 강인해지고 따라서 학습 데이터의 특정 특성에 덜 의존하게 됩니다. 마지막 완전 연결 계층에 드롭아웃을 적용해보도록 하겠습니다.

>>> net = Network([
        ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28), 
                      filter_shape=(20, 1, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12), 
                      filter_shape=(40, 20, 5, 5), 
                      poolsize=(2, 2), 
                      activation_fn=ReLU),
        FullyConnectedLayer(
            n_in=40*4*4, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
        FullyConnectedLayer(
            n_in=1000, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
        SoftmaxLayer(n_in=1000, n_out=10, p_dropout=0.5)], 
        mini_batch_size)
>>> net.SGD(expanded_training_data, 40, mini_batch_size, 0.03, 
            validation_data, test_data)
주: 확장된 학습 데이터와 dropout이 적용된 구글 coloab에서 실행 가능한 파일은 여기에서 받을 수 있습니다.

이렇게 실행하면 99.60%의 분류 정확도를 얻을 수 있습니다. 이는 이전 결과 특히, 100개의 은닉 뉴런을 가지는 신경망에서 얻었던 99.37%보다 상당히 개선된 수치입니다.

여기서 주목할 만한 두 가지 변화가 있습니다.
첫째, 훈련 epoch 수를 40으로 줄였습니다. 드롭아웃을 적용하여 과적합을 줄여 학습 속도를 개선했습니다. 둘째, 완전 연결 은닉 계층의 뉴런 수는 이전의 100개가 아닌 1,000개입니다. 물론 드롭아웃은 학습 중에 많은 뉴런을 효과적으로 제거하므로 어느 정도의 확장하는 것은 예상할 수 있습니다. 실제로 300개와 1,000개의 은닉 뉴런으로 실험을 해본 결과 1,000개의 은닉 뉴런을 사용했을 때, 검증 선응이 (아주 약간) 더 좋았습니다.

신경망 앙상블 사용: 성능을 더욱 향상시킬수 있는 쉬운 방법은 여러 개의 신경망을 만들고 이들이 투표를 통해 최적의 분류를 결정하도록 하는 것입니다. 예를 들어 위에서 설명한 방식대로 5개의 다른 신경망을 훈련시켜 각각 약 99.6%의 분류 정확도를 달성했다고 가정해 보겠습니다. 비록 신경망들이 모두 비슷한 정확도를 갖겠지만, 서로 무작위로 초기화하면 다른 오류를 범할 가능성이 높습니다. 따라서 5개의 신경망이 투표를 하면 어떤 개별 신경망보다 더 나은 분류 결과를 얻을 수 있을 것입니다.

믿기 어려울 정도로 좋게 들리겠지만, 이러한 종류의 앙상블은 신경망과 다른 기계 학습 기술에서 모두 흔히 사용되는 기술입니다. 그리고 실제로 결과가 추가적으로 개선됩니다. 신경망 앙상블 기술을 활용하면 99.67%의 분류 정확도를 얻을 수 있습니다. 다시 말해 우리의 신경망 앙상블은 10,000개의 테스트 이미지 중 33개를 제외한 모든 이미지를 정확하게 분류할 수 있습니다.

테스트 세트에서 33개의 분류하기 어려운 이미지는 아래와 같습니다. 오른 쪽 상단의 레이블은 MNIST 데이터를 올바르게 분류한 답이며, 오른쪽 하단의 레이블은 우리 신경망 앙상블이 분류한 결과입니다.

이들을 잠시 자세히 살펴보도록 하겠습니다. 처음 두 자리, 6과 5를 신경망 앙상블이 0과 3으로 분류한 것은 명백한 오류입니다. 그러나 인간도 충분히 범할 수 있는 이해 가능한 오류이기도 합니다. 6은 0과 매우 비슷하게 보이고, 5도 3과 매우 비슷하게 보입니다. 세 번째 이미지 8도 9로 보이기도 합니다. 신경망 앙상블의 편을 좀 들면, 원래 숫자를 그린 사람보다더 더 잘 분류한 것이 아닌가 합니다. 반면에 네 번째 이미지은 6은 실제로 신경망 앙상블이 잘못 분류한 것처럼 보입니다.

나머지 이미지들도 이런 식입니다. 대부분의 경우 우리 신경망의 선택은 적어도 실수를 저지를 법한 것들이며, 어떤 경우에는 원래 숫자를 쓴 사람보다 더 잘 분류했습니다. 전반적으로 우리 신경망은 뛰어난 성능을 보입니다. 특히 보여드리지 않은 9,967개의 이미지를 정확하게 분류했다는 점을 고려하면 더욱 그러합니다. 그러한 맥락에서 볼 때, 여기 몇몇 명백한 오류는 꽤 이해할 만합니다. 주의 깊은 사람조차 때로는 실수를 합니다. 따라서 매우 꼼꼼하고 체계적인 사람만이 훨씬 더 잘할 수 있을 것입니다. 우리의 신경망의 성능은 거의 인간 수준에 근접한다할 수 있습니다.

완전 연결 계층에만 드롭아웃을 적용한 이유: 위의 코드를 자세히 살펴보면, 신경망의 완전 연결 계층에만 드롭아웃을 적용했고, 합성곱 계층에는 적용하지 않았다는 것을 알 수 있습니다. 원칙적으로는 합성곱 계층에도 비슷한 절차를 적용할 수 있습니다. 하지만 사실 그럴 필요가 없습니다. 합성곱 계층은 과적합에 대한 상당한 내성이 있기 때문입니다. 그 이유는 공유된 가중치는 합성곱 필터가 전체 이미지를 통해 학습하도록 강제하는 것을 의미하기 때문입니다. 이는 학습 데이터의 지역적인 특성을 덜 포착하게 만듭니다. 따라서 드롭아웃과 같은 다른 정규화 기술을 적용할 필요성이 크지 않습니다.

더 나아가기: MNIST 손글씨 분류 문제에서 성능을 더욱 향상시키는 것이 가능합니다. Rodrigo Benenson은 관련 논문들의 링크와 함께 수년간의 발전을 보여주는 유익한 요약 페이지를 만들어 게시했습니다. 이 논문들의 대부분은 우리가 사용했던 신경망과 유사한 방식으로 심층 합성곱 신경망을 사용합니다. 논문들을 읽어보면 흥미로운 기술들을 많이 발견할 수 있으며, 그중 일부를 구현해볼 수도 있습니다. 만약 해보고 싶다면, 간단한 신경망부터 시작하는 것을 추천합니다. 이렇게 해보면 무슨 일들이 일어나고 있는지를 더 빠르게 이해할 수 있습니다.

최근 연구를 하나 살펴보도록 하겠습니다. Ciresan, Meier, Gambardella, Schmidhuber는 2010년 논문을 발표했습니다. 이 논문의 장점은 매우 간단하다는 것입니다. 그들은 완전 연결 계층만 사용하는 (합성곱 계층이 없는) 다층 신경망을 사용했습니다. 가장 우수한 결과를 보인 신경망들은 각각 2,500, 2,000, 1,500, 1,000, 500개의 뉴런을 포함하는 은닉 계층을 가진 신경망들이었습니다. 그들은 Simard 등과 유사한 아이디어를 사용하여 학습 데이터를 확장했습니다. 하지만 그 외에는 합성곱 계층을 포함하지 않은 몇 가지 간단한 방법만 사용했습니다. 이는 충분한 인내심과 컴퓨팅 파워가 있다면 1980년대에도 학습시킬 수 있는 평범한 신경망이었습니다. 그들은 99.65%의 분류 정확도를 달성했는데, 이는 우리의 결과와 거의 같습니다. 핵심은 큰 심층 신경망을 사용하고 GPU를 사용하여 학습 속도를 높이는 것이었습니다. 이를 통해 많은 epoch 동안 학습을 진행할 수 있습니다. 또한 긴 하습 시간을 활용하여 학습률을 103에서 106으로 점진적으로 감소시켰습니다. 그들의 아키텍처와 유사한 방식으로 이러한 결과를 재현해보는 것도 재미있는 연습이 될 것입니다.

학습이 가능한 이유: 이전 장에서 깊고 많은 계층으로 이루어진 신경망을 학습시키는데 근본적인 장애가 있다는 것에 대하여 다루었습니다. 출력 계층에서 입력 계층으로 이동할 수록 기울기가 매우 불안정해지는 경향이 있다는 것을 확인했습니다. 기울기가 소멸하거나 폭증하는 경향이 있었습니다. 기울기는 우리가 학습시키는데 사용하는 신호이기 때문에 이것이 문제가 되었습니다.

우리는 이러한 문제를 어떻게 피했을까요?

물론, 우리가 이 문제를 피한 것이 아닙니다. 대신, 그럼에도 불구하고 진행하는데 도움이 되는 몇 가지 작업을 수행했습니다. 특히 (1) 합성곱 계층을 사용하여 해당 계층의 매개변수의 수를 크게 줄여 학습 문제를 훨씬 쉽게 만들었습니다. (2) 더 강략한 정규화 기술(특히 드롭아웃과 합성곱 계층)을 사용하여 과적합 문제를 완화시켰습니다. 과적합은 더 복잡한 신경망에서 더 큰 문제가 됩니다. (3) 시그모이드 뉴런 대신 ReLU를 사용하여 학습 속도를 높였습니다. 경험적으로 학습 속도가 3~5배 정도 빨라집니다. (4) GPU를 사용하여 오랜 시간 동안 학습시켰습니다. 특히, 가장 최근에 실행한 결과는 원래 MNIST 학습 데이터보다 5배 더 많은 학습 데이터를 사용하여 40 epoch 동안 학습을 진행했습니다. 책 앞 부분에서는 주로 원 MNIST 학습 데이터만 사용하여 30 epoch 동안 학습을 진행했었스빈다. (3)과 (4)의 요인을 결합하면 이전보다 약 30배 더 오래 학습을 진행한 것과 같습니다.

아마 "그게 다인가요? 심층 신경망을 학습시키기 위해 우리가 했던 것이 이것들이 전부인가요? 이것들이 그렇게 대단한 일인가요?"라는 생각이 들 수도 있습니다.

물론, 우리는 다른 아이디어들도 활용했습니다. (과적합을 피하는데 도움이 되는) 충분히 큰 학습 데이터 세트 활용, (학습 속도 저하를 피하기 위한) 적절한 비용 함수 선택, (뉴런 포화로 인한 학습 속도 저하를 피하기 위한) 적절한 가중치 초기화 기법 적용, 알고리즘을 활용한 데이터 확장 등이 있습니다. 우리는 이전 장에서 이러한 아이디어와 다른 아이디어들을 논의했으며, 이 장에서는 대부분 별다른 언급 없이 이러한 아이디어를 재활용할 수 있었습니다.

그렇긴 하지만, 이것들은 정말로 꽤 간단한 아이디어들이었습니다. 함께 사용하면 간단하지만 그 효과는 적지 않습니다. 심층 학습을 시작하는 것은 그리 어렵지 않습니다.

심층 신경망은 얼마나 깊은가요? 합성곱-폴링 계층을 하나의 계층으로 간주한다면 우리의 최종 신경망 아키텍처는 4개의 은닉 계층을 가지고 있습니다. 이러한 신경망을 정말로 심층 신경망이라고 부를만 할까요? 물론, 4개의 은닉 계층을 가진 신경망은 우리가 이전에 다루었던 얕은 신경망보다 훨씬 많은 은닉 계층을 갖고 있다고 할 수 있습니다. 그 신경망들의 대부분은 하나의 은닉 계층만 가지졌거나 간혹 2개의 은닉 계층을 가지고 있었습니다. 반면에 2015년 현재 최첨단 심층 신경망은 때때로 수십 개의 은닉 계층을 가지고 있습니다. 가끔 사람들이 은닉 계층의 수가 적거나 하면 진정한 심층 학습을 하는 것이 아니라는 식의 우월감을 드러내곤 합니다. 그러나 이것은 올바르지 않습니다. 심층 학습의 부분적인 정의는 순간적인 결과에 의존하게 만들기 때문입니다. 심층 학습의 진정한 획기적인 발전은 2000년대 중반까지 주류를 이루었던 1개 또는 2개의 은닉 계층을 가진 얕은 신경망을 넘어설 수 있다는 것을 깨달은 것이었습니다. 이것은 정말로 중요한 돌파구였으며, 훨씬 더 표현력이 풍부한 모델을 탐구할 수 있는 길을 열었습니다. 하지만 그 이상으로, 계층 수는 주요한 근본적인 관심사는 아닙니다. 오히려 더 깊은 신경망을 사용하는 것은 더 나은 분류 정확도와 같은 다른 목표를 달성하는 데 도움이 되는 도구입니다.

절차에 대한 설명: 이 섹션에서는 하나의 은닉 계층을 가진 얕은 신경망에서 다층 합성곱 신경망으로 우리의 관심을 옮겼습니다. 모든 것이 너무 쉬워보일 수 있습니다. 우리는 우리의 모델을 수정했고, 대부분의 경우 이전 보다 더 나은 결과를 얻었습니다. 하지만 실제 실험을 시작하면 일이 항상 그렇게 순탄하지 않을 것이라고 장담할 수 있습니다. 그 이유는 여기까지 오는데 거쳐야했던 많은 시행착오들을 생략하고 성공한 주요 설명들만 했기 때문입니다. 이러한 정리된 설명은 기본적인 아이디어를 명확히 하는데 도움이 됩니다. 하지만 불완전한 인상을 줄 위험도 있습니다. 좋은 결과를 내는 작동하는 신경망을 얻기 까지 꽤 많은 시행착오와 좌절을 겪어야할 수도 있습니다. 실제로 상당한 실험을 수행해야 합니다. 이 과정을 올바르게 진행하기 위해 3장에서 신경망의 하이퍼파라미터를 선택하는 방법에 대한 논의를 다시 검토하거나, 해당 섹선에서 다룬 추가 자료들을 살펴볼 필요가 있을 수도 있습니다.

합성곱 신경망의 코드

자, 이제 부터 프로그램 코드인 network3.py를 살펴보도록 하겠습니다. 구조적으로 3장에서 논의한 프로그램인 network2.py 와 유사하지만, Theano를 사용했기 때문에, 세부적으로 다릅니다. 이 책의 앞부분에서 공부했던 계층과 유사한 FullyConnectedLayer 클래스부터 살펴보겠습니다. 코드는 다음과 같습니다.

class FullyConnectedLayer(object):

    def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):
        self.n_in = n_in
        self.n_out = n_out
        self.activation_fn = activation_fn
        self.p_dropout = p_dropout
        # Initialize weights and biases
        self.w = theano.shared(
            np.asarray(
                np.random.normal(
                    loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),
                dtype=theano.config.floatX),
            name='w', borrow=True)
        self.b = theano.shared(
            np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),
                       dtype=theano.config.floatX),
            name='b', borrow=True)
        self.params = [self.w, self.b]

    def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
        self.inpt = inpt.reshape((mini_batch_size, self.n_in))
        self.output = self.activation_fn(
            (1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
        self.y_out = T.argmax(self.output, axis=1)
        self.inpt_dropout = dropout_layer(
            inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
        self.output_dropout = self.activation_fn(
            T.dot(self.inpt_dropout, self.w) + self.b)

    def accuracy(self, y):
        "Return the accuracy for the mini-batch."
        return T.mean(T.eq(y, self.y_out))
__init__ 메서드는 대부분 자명하지만, 코드를 더 잘 이해할 수 있도록 몇 가지를 설명하고자 합니다. 이전과 마찬가지로 가중치와 편향은 적절한 표준 편차를 가진 정규 분포 난수 함수로 초기화합니다. 이작업을 하는 코드는 약간 복잡해 보입니다. 하지만 이 복잡성의 대부분은 가중치와 편향을 Theano 라이브러리에서 공유 변수라 불리우는 것으로 로드하는 것 뿐입니다. 이는 GPU를  사용할 수 있는 환경에서 이러한 변수가 GPU에서 처리될 수 있도록 합니다. 이에 대한 자세한 내용은 다루지 않겠습니다. 관심이 있다면 Theano 라이브러리의 문서를 참조하시기 바랍니다. 또한 이 가중치와 편향의 초기화 방법은 시그모이드 활성화 함수를 위해 설계되었다는 점도 유의하시기 바랍니다. 이상적으로는 tanh나 ReLU 같은 활성화 함수를 사용할 경우 가중치와 편향을 약간 다르게 초기화해야 합니다. 이에 대해서는 이후 좀 더 자세히 논의하도록 하겠습니다. __init__ 메서드는 self.params = [self.w, self.b]로 끝납니다. 이는 해당 계층과 관련된 모든 학습 가능한 매개벼수를 묶는 간편한 방법입니다. 나중에 Network.SGD 메서드는 params 속성을 사용하여 Network 인스턴스에서 학습 가능한 변수를 인식합니다.

set_input 메서드는 계층들의 입력을 설정하고 해당 출력을 계산하는데 사용됩니다. input 이라는 이름 대신 inpt 라는 이름을 사용하는 것은 input 이 Python의 내장 함수이기 때문입니다. 입력을 실제로 두 가지 개별적인 방식, self.inpt 와 self.inpt_dropout으로 설정하는 점을 유의하시기 바랍니다. 이는 학습 중에 드롭아웃 기법을 적용할 수 있기 때문에 그러합니다. 만약 드롭아웃을 적용하고자 한다면, 뉴런들 중 self.p_dropout 이 지정하는 만큼을 제거합니다. 이것이 set_inpt 메서드의 마지막에서 두 번째 라인에 있는 dropout_layer 함수가 하는 일입니다. 따라서, self.inpt_dropout 과 self.output_dropout 이 학습 중에 사용되고, self.inpt 와 self.output 은 요효성 검사와 테스트 데이터에 대한 정확도 평가와 같은 다른 모든 목적에 사용됩니다.

ConvPoolLayer 와 SoftmaxLayer 클래스 정의는 FullyConnectedLayer 와 유사합니다. 실제로 매우 유사하여 여기서는 코드를 발췌하여 살펴보지는 않을 것입니다. 관심 있다면, network3.py 의 전체 코드에서 살펴보시기 바랍니다.

하지만 몇 가지 세부적인 차이점은 언급하고자 합니다. 가장 큰 차이는 CovPoolLayer 와 SoftmaxLayer 모두에서 해당 계층 유형에 적합한 방식으로 출력 활성화를 계산한다는 것입니다. 다행히 Theano 라이브러리는 합성곱, 최대 폴링과 소프트맥스 함수를 계산하기 위한 내장 연산을 제공합니다.

덜 명백한 것은 소프트맥스 계층을 소개했을 때, 가중치와 편향을 초기화하는 방법에 대하여 논의한 적이 없다는 것입니다. 시그모이드 계층의 경우 적절한 매개변수가 설정된 정규 분포 난수 함수를 사용하여 가중치를 초기화해야한다고 언급했습니다. 하지만 이 것은 시그모이드 뉴런(그리고 약간 수정하면 tanh 뉴런)에 한정된 것입니다. 이것이 소프트맥스 계층에도 적용되어야할 특별한 이유는 없습니다. 따라서 이렇게 초기화하는 것을 정당화할 수 있는 선험적인 이유는 없습니다. 대신, 모든 가중치와 편향을 0으로 초기화하겠습니다. 이는 다소 임시방편적인 것이지만 실제로 충분히 잘 작동합니다.

지금까지 계층과 관련된 클래스들을 살펴보았고, 지금부터는 Network 클래스를 살펴보겠습니다. __init__ 메서드 부터 살펴보겠습니다.

class Network(object):
    
    def __init__(self, layers, mini_batch_size):
        """Takes a list of `layers`, describing the network architecture, and
        a value for the `mini_batch_size` to be used during training
        by stochastic gradient descent.

        """
        self.layers = layers
        self.mini_batch_size = mini_batch_size
        self.params = [param for layer in self.layers for param in layer.params]
        self.x = T.matrix("x")  
        self.y = T.ivector("y")
        init_layer = self.layers[0]
        init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
        for j in xrange(1, len(self.layers)):
            prev_layer, layer  = self.layers[j-1], self.layers[j]
            layer.set_inpt(
                prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)
        self.output = self.layers[-1].output
        self.output_dropout = self.layers[-1].output_dropout
이 부분의 코드들도 대부분 자명합니다. self.params = [param for layer in ...] 줄은 각 계층의 매개변수를 하나의 list 로 묶습니다. 위에서 예상한 바와 같이 Network.SGD 메서드는 self.params 를 사용하여 Network 에ㅓ 학습 가능한 매개변수를 인식합니다. self.x = T.matrix("x") 와 self.y = T.ivector("y") 줄은 Theano 심볼 변수 x와 y를 정의하는 것입니다. 이들은 신경망의 입력과 원하는 출력을 나타내는데 사용됩니다.

여기서 우리가 Theano 라이브러리의 모든 세부사항을 다룰 수 없으므로 이러한 심볼릭 변수가 정확히 무엇을 의미하는지에 대해서는 깊이 다루지 않겠습니다. 대략적으로 설명하자면 이들이 명시적인 값이 아닌 수학적 변수를 나타내는 것이라는 정도만 언급하도록 하겠습니다. 우리는 이러한 변수로 덧셈, 뺄셈, 곱셈, 함수 적용 등과 같은 일반적인 작업을 모두 수행할 수 있습니다. 실제로 Theano는 합성곱, 최대 폴링 등과 같은 작업을 수행하는 이러한 심볼릭 변수를 조작하는 많은 방법을 제공합니다. 그러나 가장 큰 이점은 매우 일반적인 형태의 역전파 알고리즘을 사용하여 빠른 심볼 미분을 수행할 수 있다는 것입니다. 이는 당연한 신경망 아키테처에 확률적 경사 하강법을 적용하는데 매우 유용합니다. 특히 다음의 몇 줄의 코드는 신경망의 심볼 출력을 정의하는 것입니다. 우리는 먼저 다음 코드로 입력 계층의 입력을 설정합니다.

        init_layer.set_inpt(self.x, self.x, self.mini_batch_size)

입력은 한 번에 하나의 미니 배치씩 설정되므로 미니 배치의 크기가 매개변수로 지정되어 있습니다. 또한 입력을 나타내는 sellf.x 를 중복해서 매개변수로 전달한다는 점에 유의하십시오. 이는 드롭아웃을 사용하거나 사용하지 않는 두가지 다른 방식으로 신경망을 학습시킬 수 있기 때문입니다. 그런 다음 for 루프는 심볼 변수 self.x를 Network 계층을 통해 순방향으로 전파합니다. 이를 통해 신경망의 출력을 심볼로 나타내는 최종 output과 output_dropout 속성을 정의할 수 있습니다.

이제 Network 가 초기화되는 방식을 이해했으므로 SGD 메서드를 사용하여 신경망을 학습시키는 방법을 살펴보겠습니다. 코드가 매우 길어보이지만 매우 간단합니다. 코드 뒤에는 설명을 위한 주석이 있습니다.

    def SGD(self, training_data, epochs, mini_batch_size, eta, 
            validation_data, test_data, lmbda=0.0):
        """Train the network using mini-batch stochastic gradient descent."""
        training_x, training_y = training_data
        validation_x, validation_y = validation_data
        test_x, test_y = test_data

        # compute number of minibatches for training, validation and testing
        num_training_batches = size(training_data)/mini_batch_size
        num_validation_batches = size(validation_data)/mini_batch_size
        num_test_batches = size(test_data)/mini_batch_size

        # define the (regularized) cost function, symbolic gradients, and updates
        l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
        cost = self.layers[-1].cost(self)+\
               0.5*lmbda*l2_norm_squared/num_training_batches
        grads = T.grad(cost, self.params)
        updates = [(param, param-eta*grad) 
                   for param, grad in zip(self.params, grads)]

        # define functions to train a mini-batch, and to compute the
        # accuracy in validation and test mini-batches.
        i = T.lscalar() # mini-batch index
        train_mb = theano.function(
            [i], cost, updates=updates,
            givens={
                self.x:
                training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
                self.y: 
                training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        validate_mb_accuracy = theano.function(
            [i], self.layers[-1].accuracy(self.y),
            givens={
                self.x: 
                validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
                self.y: 
                validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        test_mb_accuracy = theano.function(
            [i], self.layers[-1].accuracy(self.y),
            givens={
                self.x: 
                test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
                self.y: 
                test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        self.test_mb_predictions = theano.function(
            [i], self.layers[-1].y_out,
            givens={
                self.x: 
                test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        # Do the actual training
        best_validation_accuracy = 0.0
        for epoch in xrange(epochs):
            for minibatch_index in xrange(num_training_batches):
                iteration = num_training_batches*epoch+minibatch_index
                if iteration 
                    print("Training mini-batch number {0}".format(iteration))
                cost_ij = train_mb(minibatch_index)
                if (iteration+1) 
                    validation_accuracy = np.mean(
                        [validate_mb_accuracy(j) for j in xrange(num_validation_batches)])
                    print("Epoch {0}: validation accuracy {1:.2
                        epoch, validation_accuracy))
                    if validation_accuracy >= best_validation_accuracy:
                        print("This is the best validation accuracy to date.")
                        best_validation_accuracy = validation_accuracy
                        best_iteration = iteration
                        if test_data:
                            test_accuracy = np.mean(
                                [test_mb_accuracy(j) for j in xrange(num_test_batches)])
                            print('The corresponding test accuracy is {0:.2
                                test_accuracy))
        print("Finished training network.")
        print("Best validation accuracy of {0:.2
            best_validation_accuracy, best_iteration))
        print("Corresponding test accuracy of {0:.2
처음 몇 줄은 데이터 셋을 x와 y 구성요소로 분리하고 각 데이터셋에 사용된 미니 배치의 수를 계산하는 간단한 내용입니다. 이후 몇 줄은 더 흥미롭게 Theano를 사용하는 것이 재미있는 이유 중 일부를 보여줍니다. 다음 줄들을 보시기 바랍니다.

        # define the (regularized) cost function, symbolic gradients, and updates
        l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
        cost = self.layers[-1].cost(self)+\
               0.5*lmbda*l2_norm_squared/num_training_batches
        grads = T.grad(cost, self.params)
        updates = [(param, param-eta*grad) 
                   for param, grad in zip(self.params, grads)]
이 줄에서 우리는 정규화된 로그-우도 (log-likelihood) 비용 함수를 심볼로 설정하고 경사 함수에서 해당 도함수와 매개변수들에 대한 업데이트도 계합니다.  Theano를 사용하면 이 모든 것을 몇 줄의 코드만으로 해낼 수 있습니다. 여기서 명시적으로 드러나지 않은 유일한 것은 비용을 계산하는 데 출력 계층의 cost 메서드를 호출해야 한다는 것입니다. 이 코드는 network3.py 의 다른 부분에 있습니다. 하지만 그 코드 역시 짧고 간단합니다. 이러한 것들과 함께, 미니 배치 인덱스가 주어지면 updates를 사용하는 Theano 라이브러리의 심볼 함수인 train_mb 함수를 정의할 준비가 완료됩니다. 마찬가지로 validate_mb_accuracy 와 test_mb_accuracy 는 주어진 유효성 검사나 테스트 데이터의 미니 배치에 대한 Network 의 정확도를 계산합니다. 이러한 함수들의 평균을 내면 전체 유효성 검사와 테스트 데이터 세트에 대한 정확도를 계산할 수 있습니다.

SGD 메서드의 나머지 부분은 자명합니다. 단순히 epoch를 반복하며 학습 데이터의 미니 배치에 대해 신경망을 반복적으로 학습시키고 유효성 검사와 테스트 정확도를 계산합니다.

자, 이제 network3.py 의 가장 중요한 부분들을 이해했습니다. 프로그램 전체를 간략하게 살펴보겠습니다. 자세히 읽을 필요는 없지만 훑어 보고 마음에 드는 부분을 자세히 살펴보는 것도 좋을 것입니다. 물론 실제로 이해하는 가장 좋은 방법은 코드를 수정하고 기능을 추가하거나 더 우아하게 실행될 수 있도록 고쳐보는 것입니다.

"""network3.py
~~~~~~~~~~~~~~

A Theano-based program for training and running simple neural
networks.

Supports several layer types (fully connected, convolutional, max
pooling, softmax), and activation functions (sigmoid, tanh, and
rectified linear units, with more easily added).

When run on a CPU, this program is much faster than network.py and
network2.py.  However, unlike network.py and network2.py it can also
be run on a GPU, which makes it faster still.

Because the code is based on Theano, the code is different in many
ways from network.py and network2.py.  However, where possible I have
tried to maintain consistency with the earlier programs.  In
particular, the API is similar to network2.py.  Note that I have
focused on making the code simple, easily readable, and easily
modifiable.  It is not optimized, and omits many desirable features.

This program incorporates ideas from the Theano documentation on
convolutional neural nets (notably,
http://deeplearning.net/tutorial/lenet.html ), from Misha Denil's
implementation of dropout (https://github.com/mdenil/dropout ), and
from Chris Olah (http://colah.github.io ).

Written for Theano 0.6 and 0.7, needs some changes for more recent
versions of Theano.

"""

#### Libraries
# Standard library
import cPickle
import gzip

# Third-party libraries
import numpy as np
import theano
import theano.tensor as T
from theano.tensor.nnet import conv
from theano.tensor.nnet import softmax
from theano.tensor import shared_randomstreams
from theano.tensor.signal import downsample

# Activation functions for neurons
def linear(z): return z
def ReLU(z): return T.maximum(0.0, z)
from theano.tensor.nnet import sigmoid
from theano.tensor import tanh


#### Constants
GPU = True
if GPU:
    print "Trying to run under a GPU.  If this is not desired, then modify "+\
        "network3.py\nto set the GPU flag to False."
    try: theano.config.device = 'gpu'
    except: pass # it's already set
    theano.config.floatX = 'float32'
else:
    print "Running with a CPU.  If this is not desired, then the modify "+\
        "network3.py to set\nthe GPU flag to True."

#### Load the MNIST data
def load_data_shared(filename="../data/mnist.pkl.gz"):
    f = gzip.open(filename, 'rb')
    training_data, validation_data, test_data = cPickle.load(f)
    f.close()
    def shared(data):
        """Place the data into shared variables.  This allows Theano to copy
        the data to the GPU, if one is available.

        """
        shared_x = theano.shared(
            np.asarray(data[0], dtype=theano.config.floatX), borrow=True)
        shared_y = theano.shared(
            np.asarray(data[1], dtype=theano.config.floatX), borrow=True)
        return shared_x, T.cast(shared_y, "int32")
    return [shared(training_data), shared(validation_data), shared(test_data)]

#### Main class used to construct and train networks
class Network(object):

    def __init__(self, layers, mini_batch_size):
        """Takes a list of `layers`, describing the network architecture, and
        a value for the `mini_batch_size` to be used during training
        by stochastic gradient descent.

        """
        self.layers = layers
        self.mini_batch_size = mini_batch_size
        self.params = [param for layer in self.layers for param in layer.params]
        self.x = T.matrix("x")
        self.y = T.ivector("y")
        init_layer = self.layers[0]
        init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
        for j in xrange(1, len(self.layers)):
            prev_layer, layer  = self.layers[j-1], self.layers[j]
            layer.set_inpt(
                prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)
        self.output = self.layers[-1].output
        self.output_dropout = self.layers[-1].output_dropout

    def SGD(self, training_data, epochs, mini_batch_size, eta,
            validation_data, test_data, lmbda=0.0):
        """Train the network using mini-batch stochastic gradient descent."""
        training_x, training_y = training_data
        validation_x, validation_y = validation_data
        test_x, test_y = test_data

        # compute number of minibatches for training, validation and testing
        num_training_batches = size(training_data)/mini_batch_size
        num_validation_batches = size(validation_data)/mini_batch_size
        num_test_batches = size(test_data)/mini_batch_size

        # define the (regularized) cost function, symbolic gradients, and updates
        l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
        cost = self.layers[-1].cost(self)+\
               0.5*lmbda*l2_norm_squared/num_training_batches
        grads = T.grad(cost, self.params)
        updates = [(param, param-eta*grad)
                   for param, grad in zip(self.params, grads)]

        # define functions to train a mini-batch, and to compute the
        # accuracy in validation and test mini-batches.
        i = T.lscalar() # mini-batch index
        train_mb = theano.function(
            [i], cost, updates=updates,
            givens={
                self.x:
                training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
                self.y:
                training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        validate_mb_accuracy = theano.function(
            [i], self.layers[-1].accuracy(self.y),
            givens={
                self.x:
                validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
                self.y:
                validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        test_mb_accuracy = theano.function(
            [i], self.layers[-1].accuracy(self.y),
            givens={
                self.x:
                test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
                self.y:
                test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        self.test_mb_predictions = theano.function(
            [i], self.layers[-1].y_out,
            givens={
                self.x:
                test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
            })
        # Do the actual training
        best_validation_accuracy = 0.0
        for epoch in xrange(epochs):
            for minibatch_index in xrange(num_training_batches):
                iteration = num_training_batches*epoch+minibatch_index
                if iteration % 1000 == 0:
                    print("Training mini-batch number {0}".format(iteration))
                cost_ij = train_mb(minibatch_index)
                if (iteration+1) % num_training_batches == 0:
                    validation_accuracy = np.mean(
                        [validate_mb_accuracy(j) for j in xrange(num_validation_batches)])
                    print("Epoch {0}: validation accuracy {1:.2%}".format(
                        epoch, validation_accuracy))
                    if validation_accuracy >= best_validation_accuracy:
                        print("This is the best validation accuracy to date.")
                        best_validation_accuracy = validation_accuracy
                        best_iteration = iteration
                        if test_data:
                            test_accuracy = np.mean(
                                [test_mb_accuracy(j) for j in xrange(num_test_batches)])
                            print('The corresponding test accuracy is {0:.2%}'.format(
                                test_accuracy))
        print("Finished training network.")
        print("Best validation accuracy of {0:.2%} obtained at iteration {1}".format(
            best_validation_accuracy, best_iteration))
        print("Corresponding test accuracy of {0:.2%}".format(test_accuracy))

#### Define layer types

class ConvPoolLayer(object):
    """Used to create a combination of a convolutional and a max-pooling
    layer.  A more sophisticated implementation would separate the
    two, but for our purposes we'll always use them together, and it
    simplifies the code, so it makes sense to combine them.

    """

    def __init__(self, filter_shape, image_shape, poolsize=(2, 2),
                 activation_fn=sigmoid):
        """`filter_shape` is a tuple of length 4, whose entries are the number
        of filters, the number of input feature maps, the filter height, and the
        filter width.

        `image_shape` is a tuple of length 4, whose entries are the
        mini-batch size, the number of input feature maps, the image
        height, and the image width.

        `poolsize` is a tuple of length 2, whose entries are the y and
        x pooling sizes.

        """
        self.filter_shape = filter_shape
        self.image_shape = image_shape
        self.poolsize = poolsize
        self.activation_fn=activation_fn
        # initialize weights and biases
        n_out = (filter_shape[0]*np.prod(filter_shape[2:])/np.prod(poolsize))
        self.w = theano.shared(
            np.asarray(
                np.random.normal(loc=0, scale=np.sqrt(1.0/n_out), size=filter_shape),
                dtype=theano.config.floatX),
            borrow=True)
        self.b = theano.shared(
            np.asarray(
                np.random.normal(loc=0, scale=1.0, size=(filter_shape[0],)),
                dtype=theano.config.floatX),
            borrow=True)
        self.params = [self.w, self.b]

    def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
        self.inpt = inpt.reshape(self.image_shape)
        conv_out = conv.conv2d(
            input=self.inpt, filters=self.w, filter_shape=self.filter_shape,
            image_shape=self.image_shape)
        pooled_out = downsample.max_pool_2d(
            input=conv_out, ds=self.poolsize, ignore_border=True)
        self.output = self.activation_fn(
            pooled_out + self.b.dimshuffle('x', 0, 'x', 'x'))
        self.output_dropout = self.output # no dropout in the convolutional layers

class FullyConnectedLayer(object):

    def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):
        self.n_in = n_in
        self.n_out = n_out
        self.activation_fn = activation_fn
        self.p_dropout = p_dropout
        # Initialize weights and biases
        self.w = theano.shared(
            np.asarray(
                np.random.normal(
                    loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),
                dtype=theano.config.floatX),
            name='w', borrow=True)
        self.b = theano.shared(
            np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),
                       dtype=theano.config.floatX),
            name='b', borrow=True)
        self.params = [self.w, self.b]

    def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
        self.inpt = inpt.reshape((mini_batch_size, self.n_in))
        self.output = self.activation_fn(
            (1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
        self.y_out = T.argmax(self.output, axis=1)
        self.inpt_dropout = dropout_layer(
            inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
        self.output_dropout = self.activation_fn(
            T.dot(self.inpt_dropout, self.w) + self.b)

    def accuracy(self, y):
        "Return the accuracy for the mini-batch."
        return T.mean(T.eq(y, self.y_out))

class SoftmaxLayer(object):

    def __init__(self, n_in, n_out, p_dropout=0.0):
        self.n_in = n_in
        self.n_out = n_out
        self.p_dropout = p_dropout
        # Initialize weights and biases
        self.w = theano.shared(
            np.zeros((n_in, n_out), dtype=theano.config.floatX),
            name='w', borrow=True)
        self.b = theano.shared(
            np.zeros((n_out,), dtype=theano.config.floatX),
            name='b', borrow=True)
        self.params = [self.w, self.b]

    def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
        self.inpt = inpt.reshape((mini_batch_size, self.n_in))
        self.output = softmax((1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
        self.y_out = T.argmax(self.output, axis=1)
        self.inpt_dropout = dropout_layer(
            inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
        self.output_dropout = softmax(T.dot(self.inpt_dropout, self.w) + self.b)

    def cost(self, net):
        "Return the log-likelihood cost."
        return -T.mean(T.log(self.output_dropout)[T.arange(net.y.shape[0]), net.y])

    def accuracy(self, y):
        "Return the accuracy for the mini-batch."
        return T.mean(T.eq(y, self.y_out))


#### Miscellanea
def size(data):
    "Return the size of the dataset `data`."
    return data[0].get_value(borrow=True).shape[0]

def dropout_layer(layer, p_dropout):
    srng = shared_randomstreams.RandomStreams(
        np.random.RandomState(0).randint(999999))
    mask = srng.binomial(n=1, p=1-p_dropout, size=layer.shape)
    return layer*T.cast(mask, theano.config.floatX)

이미지 인식 분야의 최근 현황

1998년 MNIST가 처음 소개되었을 때, 최첨단 워크스테이션으로 학습시키는데 몇 주가 걸렸습니다. 그 결과는 우리가 GPU를 사용하여 한 시간도 안되는 시간동안 학습시킨 것으로 달성한 것보다 훨씬 낮은 정확도를 기록했습니다. 따라서 MNIST는 더 이상 가용한 기술의 한계를 뛰어넘는 문제가 아니라 학습 속도가 빠르기 때문에 교육 이나 학습 목적으로 좋은 문제입니다. 한편, 그 동안 연구의 초점들도 변화하여, 최근에는 더 어려운 이미지 인식 문제들에 대한 연구가 진행되고 있습니다. 이 섹션에서는 신경망을 사용한 이미지 인식에 대한 최근 연구를 간략하게 설명하고자 합니다.

이 섹션은 책의 대부분에서 다른 내용들과는 동떨어져 있습니다. 이 책의 전반적인 부분은 역전파, 정규화, 합성곱 신경망과 같이 지속적인 관심을 끌 가능성이 높은 아이디어들에 초점을 맞추었습니다. 이 책을 집필하면서 당시 유행하고 있지만 장기적인 가치가 알려지지 않은 연구들은 피하려고 노력했습니다. 과학에서 그러한 결과는 대부분 사라지고 지속적인 영향력이 거의 없는 일시적인 현상들이기 때문입니다. 이를 감안할 때 회의론자는 이렇게 말할 수도 있습니다. "글쎄, 이미지 인식의 최근 발전이 그러한 일시적인 현상의 예는 아닐까? 2~3년 후에는 상황이 바뀔 수 있다. 그렇다면 이러한 결과는 절대적인 최전선에서 경쟁하려는 소수 전문가들에게만 흥미로운 것이 아닌가? 굳이 논의할 필요가 있는가?"

이러한 회의론자는 최근 논문의 세부 사항 중 일부의 중요성이 점차 감소할 것이라는 점에서는 옳습니다. 그럼에도 불구하고 지난 몇 년 동안 심층 신경망을 사용하여 매우 어려운 이미지 인식 작업을 해결하는 데 놀라운 발전이 있었습니다. 2100년 과학사학자가 컴퓨터 비전에 대하여 글을 쓴다고 상상해 보세요. 그들은 2011년부터 2015년을 심층 합성곱 신경망에 의해 주도된 엄청난 돌파구가 열렸던 시기로 인식할 것입니다. 그렇다고 해서 심층 합성곱 신경망이 2100년에도 여전히 사용될 것이라는 것을 의미하지는 않습니다. 드롭아웃이나 ReLU 등과 같은 세부적인 아이디어는 말할 것도 없습니다. 그러나 지금 바로 아이디어의 역사에서 중요한 전환이 일어나고 있다는 것을 의미합니다. 이는 원자의 발견이나 항생제의 발명과 다소 유사합니다. 역사적인 규모의 발명과 발견인 것입니다. 따라서 세부 사항을 깊이 파고들지는 않겠지만, 현재 이루어지고 있는 흥무로운 발견에 대해 어느 정도 이해하는 것은 가치가 있습니다.

2012 LRMD 논문: 스탠퍼드와 구글 연구진 그룹의 2012년 논문을 살펴보겠습니다. 이 논문의 첫 네 저자의 성을 따서 LRMD라고 부르겠습니다. LRMD는 매우 어려운 이미지 인식 문제인 ImageNet의 이미지를 분류하기 위해 신경망을 사용했습니다. 그들이 사용한 2011년 ImageNet 데이터에는 2만 개 카테고리의 1600만 장의 풀 컬러 이미지가 포함되어 있었습니다. 이 이미지들은 공개된 인터넷에서 수집되었고, 아마존의 Mecanical Turk 서비스 노동자들에 의해 분류된 것들이었습니다. 다음은 ImageNet의 몇 가지 예들 입니다.


이들은 각각 구슬깎기 대패, 갈색 뿌리썩음병 균, 데운 우유, 그리고 일반 회충 범주에 속합니다. ImageNet의 수동 공구 목록을 방문하여 구슬깎기 대패, 블록 대패, 모따기 대패 그리고 약 12가지 다른 유형의 대패를 포함한 여러 범주들을 확인해보시기 바랍니다. 여러분은 어떠실지 모르지만, 이 모든 도구 유형을 자신있게 구별하기는 쉽지 않습니다. 이것은 분명 MNIST 보다 훨씬 어려운 이미지 인식 과제입니다. LRMD의 신경망은 ImageNet 이미지를 정확하게 분류하는데 15.8%라는 존경할 만한 정확도를 얻었습니다. 이는 인상적으로 들리지 않을 수도 있지만, 이전 최고 결과인 9.3% 정확도에 비하면 훨씬 큰 진척이었습니다. 이렇나 도약은 신경망이 ImageNet과 같이 매우 어려운 이미지 인식 작업에 강력한 접근 방식을 제공할 수 있음을 시사했습니다.

2014 ILSVRC 대회: 2012년 이후 빠른 발전이 계속되고 있습니다. 2014년 ILSVRC 대회에 대하여 알아보겠습니다. 2012년과 마찬가지로 1,000개의 카테고리에 120만 개의 이미지로 구성된 학습 데이터 세트를 사용했으며, 성능 측정 기준은 상위 5개 예측에 정답 카테고리가 포함되었는지 여부였습니다. 주로 Google에 기반을 둔 우승팀은 22개의 뉴런 계층을 가진 심층 합성곱 신경망을 사용했습니다. 그들은 LeNet-5에 대한 경의의 표시로 신경망을 GoogLeNet이라고 명명했습니다. GoogLeNet은 93.33%의 top-5 정확도를 달성했는데, 이는 2013년 우승팀(Clarifai, 88.3%)과 2012년 우승팀(KSH, 84.7%)보다 엄청나게 향상된 수치입니다.

GoogLeNet의 93.33% 정확도는 얼마나 높은 것일까요? 2014년에 한 연구팀이 ILSVRC 대회에 대한 조사 논문을 발표했습니다. 그들이 다룬 질문 중 하나는 인간이 ILSVRC에서 얼마나 잘 수행할 수 있는가 였습니다. 이를 위해 그들은 인간이 ILSVRC 이미지를 분류할 수 있는 시스템을 구축했습니다. 저자 중 한 명인 Andrej Karpathy가 블로그에서 설명했듯이, 인간이 GoogLeNet의 성능에 도달하도록 하는 것은 매우 어려웠습니다.

... 1000개 카테고리 중 5개를 선택하여 이미지에 레이블을 지정하는 작업은 ILSVRC와 그 분류에 대해 오랫동안 연구해온 연구실의 일부 동료들에게 도차 매우 어려운 것으로 밝혀졌습니다. 처음에는 Amazon Mechanical Turk에 올릴 생각도 했습니다. 그러다가 유료 학부생을 모집할까 생각했습니다. 그러고 나서 저는 연구실의 전문 레이블러들만 참여하는 강도 높은 레이블링 파티를 조직했습니다. 그런 다음 GoogLeNet 예측을 사용하여 캍고리 수를 1000개에서 약 100개로 줄이는 수정된 인터페이스를 개발했습니다. 여전히 너무 어려워서 사람들은 계속 카테고리를 놓치고 오류율이 13~15%까지 올라갔습니다. 결국 GoogLeNet에 경쟁력 있게 가까워지려면 제가 직접 앉아서 고통스러울 정도로 긴 학습 과정과 그에 따른 신중한 주석 과정을 거치는 것이 가장 효율적이라는 것을 깨달았습니다. ... 레이블링은 분당 약 1개의 속도로 진행되었지만, 시간이 지남에 따라 감소했습니다. ... 일부 이미지는 쉽게 인식할 수 있는 반면 일부 이미지(예: 미세한 품종의 개, 새 또는 원숭이)는 몇 분 동안의 집중적인 노력이 필요한 경우도 있었습니다. 저는 개 품종 식별에 매우 능숙해졌습니다. ... 제가 작업한 이미지 샘플을 기준으로 GoogLeNet 분류 오류는 6.8%로 나타냈습니다. .. 제 자신의 최종 오류율은 5.1%로 약 1.7% 더 나았습니다.

다시 말해, 꼼꼼하게 작업하는 숙련된 인간 전문가도 엄청난 노력을 기울여 겨우 심층 신경망을 근소한 차이로 이길 수 있었습니다. 실제로 Karpathy는 더 작은 이미지 샘플로 훈련된 두 번째 인간 전문가는 12.0%의 top-5 오류율만 달성하여 GoogLeNet의 성능보다 훨씬 낮았다고 보고했습니다. 오류의 약 절반은 전문가가 "정답 레이블을 옵션으로 인식하고 고려하지 못해기" 때문이었습니다.

이것은 놀라운 결과입니다. 실제로 이 연구 이후 여러 팀에서 top-5 오류율이 실제로 5.1%보다 나은 시스템을 보고한 바도 있습니다. 이는 때때로 언론에서 시스템이 인간 시력보다 더 나은 것으로 보도되었습니다. 결과가 정말로 흥미롭지만 시스템을 인간 시력보다 더 나은 것으로 생각하게 만드는 많은 주의 사항이 있습니다. ILSVRC 대회는 여러 면에서 다소 제한적인 문제입니다. 공개 웹 크롤링이 반드시 애플리케이션에서 발견되는 이미지를 대표하는 것이 아닙니다. 그리고 물론 top-5 기준은 다소 인위적입니다. 우리는 이미지 인식 또는 더 넓게는 컴퓨터 비전 문제를 해결하는 데 여전히 먼 길을 가야합니다. 그럼에도 불구하고 이렇게 어려운 문제에 불과 몇 년 만에 엄청난 진전이 이루어지는 것을 보는 것은 매우 고무적입니다.

기타: 지금까지 ImageNet에 대해서 이야기했지만, 신경망을 이용한 이미지 인식 분야에는 다른 흥미로운 활동들도 많이 있습니다. 최근의 몇 가지 주목할만한 연구 결과들에 대해서 간략하게 알아보도록하겠습니다.

실용적인 측면에서 고무적인 결과는 구글 팀으로 부터 나왔습니다. 이들은 심층 합성곱 신경망응ㄹ 구글 스트리트 뷰 이미지에서 주소지의 숫자를 인식하는 문제에 적용했습니다. 그들의 논문에 따르면 인간 운영자와 비슷한 정확도로 거의 1억 개의 주소지 숫자를 감지하고 자동으로 판독했다고 합니다. 이 시스템은 매우 빨랐습니다. 프랑스에 있는 스트리트 뷰의 모든 주소지 숫자 이미지를 판독하는데 채 1시간도 걸리지 않았습니다. 그들은 다음과 같이 이야기했습니다. "이 새로운 데이터 셋 덕분에 여러 국가에서 구글 지도의 지오코딩(geocoding) 품질이 크게 향상되었으며, 특기 기존에 좋은 지오코딩 소스가 없던 국가에서 더욱 그러했습니다." 그리고 더 나아가 다음과 같은 포괄적인 주장을 하기도 했습니다. "우리는 이 모델로 많은 애플리케이션에서 짧은 문자에 대한 광학 문자 인식(optical character recognition, OCR) 문제를 해결했다고 믿습니다."

지금까지 우리는 고무적인 결과들만 다루었습니다. 물론 우리가 아직 이해하지 못하는 근본적인 문제에 대한 흥미로운 연구들도 있습니다. 예를 들어 2013년에는 심층 신경망이 사각지대(blind spot)와 같은 문제로 어려움을 겪을 수 있음을 보여주는 논문이 발표되기도 했습니다. 아래 이미지들을 보시기 바랍니다. 왼쪽에 있는 이미지는 신경망이 ImageNet 이미지로 올바르게 분류한 것입니다. 오른쪽에 있는 이미지는 신경망이 잘못 분류한 약간 변형된 이미지입니다. (가운데 부분이 변형된 부분입니다.) 이 논문의 저자들은 이러한 '적대적(adversarial)' 이미지가 몇몇 특별한 경우뿐만 아니라 모든 샘플 이미지에 대해 존재했다는 것을 발견했습니다.


이것은 꽤 충격적인 결과입니다. 이 논문에서 사용된 신경망은 KSH의 신경망과 동일한 코드를 기반으로 하고 있으며, 이는 현재 널리 사용되고 있는 유형의 신경망입니다. 이러한 신경망은 원칙적으로 연속적인 함수를 계산하지만, 이와 같은 결과는 실제로 거의 불연속적인 함수를 계산할 가능성이 있음을 시사합니다. 더 나쁜 것은 이러한 불연속성이 우리가 생각하는 합리적인 행동과는 거리가 먼 방식으로 나타난다는 점입니다. 이는 매우 우려스러운 일입니다. 게다가 무엇이 이러한 불연속성을 야기하는지 아직 제대로 이해하지 못하고 있습니다. 손실 함수(loss function) 때문일까요? 아니면 활성화 함수(activation function) 때문일까요? 신경망의 아키텍처 때문일까요? 아니면 다른 무엇 때문일 까요? 우리는 아직 정확히 모릅니다.

하지만 이러한 결과가 들리는 것만큼 완전히 나쁜 것은 아닙니다. 이러한 적대적 이미지는 흔하지만 실제로는 발생할 가능성이 매우 낮습니다. 논문에서 다음과 같이 언급했듯이:

"적대적 음성(adversarial negatives)의 존재는 신경망이 높은 일반화 성능을 달성하는 능력과 모순되는 것처럼 보입니다. 실제로 신경망이 잘 일반화할 수 있다면 일반적인 예시와 구별할 수 없는 이 적대적 음성들에 의해 어떻게 혼란스러워질 수 있을까요? 이에 대한 설명은 적대적 음성의 집합은 확율이 극도로 낮아서 테스트 세트에서 전혀(또는 거의) 관찰되지 않지만, 조밀하게 분포되어 있어서(유리수와 유사함) 사실상 모든 테스트 사례들에서 발견된다는 것입니다."

그럼에도 불구하고 우리가 신경망을 너무나도 제대로 이해하지 못해서 이러한 종류의 결과가 최근에야 발견되었다는 사실은 안타깝습니다. 물론, 이 결과의 큰 장점은 후속 연구를 활발하게 촉진했다는 점입니다. 예를 들어 최근의 한 논문에서는 훈련된 신경망에 대해 인간에게는 "백색 소음(white noise)"처럼 보이는 이미지를 생성할 수 있지만, 신경망은 이를 특정 범주로 매우 높은 확신을 가지고 분류하는 것을 보여주었습니다. 이것은 우리가 신경망과 이미지 인식에서의 그 활용법을 이해하는 데 아직 갈 길이 멀다는 것을 보여주는 또 다른 증거입니다.

이러한 결과들에도 불구하고 전반적인 상황은 긍정적입니다. 우리는 ImageNet과 같은 매우 어려운 벤치마크에서 빠른 진전을 보여주고 있습니다. 또한, 스트리트 뷰에서 주소지 숫자를 인식하는 것과 같은 실제 문제에서도 빠른 진전을 경험하고 있습니다. 하지만 이러한 진전이 고무적이긴 해도, 벤치마크나 심지어 실제 응용 분야에서 개선을 보는 것만으로는 충분하지 않습니다. 우리는 적대적 이미지의 존재와 같은 근본적인 현상을 여전히 제대로 이해하지 못하고 있습니다. 이러한 근본적인 문제들이 여전히 발견되고 있는 상황(해결은 고사하고)에서 이미지 인식 문제를 거의 해결했다고 이야기하는 것은 시기상조입니다. 동시에 이러한 문제들은 추가적인 연구를 위한 흥미로운 자극제가 됩니다.

심층 신경망에 대한 다른 접근법들

이 책을 통해 우리는 MNIST 숫자 분류라는 한 가지 문제에 집중해왔습니다. 이 문제는 확률적 경사 하강법, 역전파, 합성곱 신경망, 정규화 등과 같은 강력한 아이디어들을 이해하게 해준 아주 좋은 문제였습니다. 하지만 동시에 협소한 문제이기도 합니다. 신경망 관련 논문들을 읽다보면, 우리가 아직 다루지 않은 만흥ㄴ 개념들, 예를 들어 순환 신경망(recurrent neural networks), 볼츠만 머신(Boltzmann machines), 생성 모델(generative models), 전이 학습(transfer learning), 강화 학습(reinforcement learning) 등등 ... 끝없이 계속해서 등장하는 아이디어들을 접하게 됩니다. 신경망은 정말 방대한 분야입니다. 하지만 많은 중요한 아이디어들이 우리가 이미 논의했던 개념들의 변형이며, 약간의 노력만으로 이해할 수 있습니다. 이 섹션에서는 아직 보지 못한 이 넓은 세계를 살짝 엿보도록 하겠습니다. 다만, 자세하거나 포괄적이지는 않습니다. 그렇게 하기에는 너무 많은 분량이 필요하기 때문입니다. 대신, 이 분야의 개념적인 풍부함을 접해보고 그 풍요로움 중 일부는 우리가 이미 다룬 내용과 연결될 수 있음을 보여주려는 인상적인 시도로 이해해주시기 바랍니다. 더 자세히 알고자 하는 분들을 위하여 외부 자료 링크도 제공하도록 하겠습니다. 물론 이 링크들 중 다수는 곧 새로운 자료로 대체될 수 있으니, 더 최신 문헌을 찾아보는 것이 좋을 수도 있습니다. 그럼에도 불구하고 많은 기본 아이디어들은 지속적으로 중요성을 잃지 않을 것이라고 생각합니다.

순환 신경망(Recurrent Neural Networks, RNNs)

우리가 다룬 순방향 신경망(Feedforward net)에서는 하나의 입력이 이후 모든 층의 뉴런의 활성화에 완전히 영향을 미칩니다. 이것은 매우 정적인 그림입니다. 신경망의 모든 것이 고정되어 있고, 얼어붙은 결정체와 같은 특성을 가집니다. 하지만 신경망의 구성 요소들이 동적인 방식으로 계속 변하도록 허용된다면 어떨까요? 예를 들어 은닉 뉴런의 행동이 이전 은닉층의 활성화 뿐만 아니라 이전 시간대의 활성화에 의해서도 영향을 받는다고 가정해봅시다. 심지어 한 뉴런의 활성화가 이전 시간대의 자기 자신의 활성화에 의해 부분적으로 결정될 수도 있습니다. 이는 순방향 신경망에서는 확실히 일어나지 않는 일입니다. 또는 은닉 뉴런이나 출력 뉴런의 활성화가 신경망 현재 입력뿐만 아니라 이전의 입력에도 영향을 받을 수 있습니다.

이러한 종류의 시간 변화적 동작을 가진 신경망을 순환 신경망(RNNs)이라고 합니다. 지난 섹션에서 비공식적으로 설명한 순환 신경망을 수학적으로 형식화하는 방법은 여러 가지가 있습니다. RNN에 대한 위키피디아 페이지를 보면 이러한 수학적 모델의 일부를 엿볼 수 있습니다. 십여가지가 넘는 다양한 모델들에 대해서 위키피디아에서는 다루고 있습니다. 하지만 수학적 세부 사항은 차지하고, 넓은 의미에서 RNN은 시간에 따른 동적인 변화라는 개념이 포함된 신경망입니다. 따라서 당연하게도, 시간에 따라 변하는 데이터나 프로세스를 분석하는 데 특히 유용합니다. 예를 들어, 음성이나 자연어와 같은 문제에서 이러한 데이터와 프로세스가 자연스럽게 필요합니다.

현재 RNN이 사용되는 한 가지 방법은 신경망을 "튜링 머신(Turning machine)"이나 기존의 프로그래밍 언어와 같은 전통적인 알고리즘적 사고방식과 더 밀접하게 연결하는 것입니다. 2014년 논문에서는 (매우, 매우 간단한) 파이선 프로그램의 문자별 설명을 입력으로 받아 출력을 예측할 수 있는 RNN을 개발했습니다. 간단히 말해, 이 신경망은 특정 파이선 프로그램을 "이해하는 법"을 배우는 것입니다. 또 다른 논문, 역시 2014년에 발표된 것인데, RNN을 출발점으로 삼아 "신경 뉴링 머신(neural Turning machine, NTM)"이라고 부르는 것을 개발했습니다. 이는 전체 구조가 경사 하강법을 이용해 훈련될 수 있는 범용 컴퓨터입니다. 이들은 NTM을 훈련시켜 정렬(sorting)이나 복사(copying)와 같은 몇 가지 간단한 문제에 대한 알고리즘을 추록하도록 했습니다.

현시점에서 이들은 극도로 간단한 장난감 모델에 불과합니다. print(398345+42598) 와 같은 파이선 프로그램을 실행하도록 배우는 것이 신경망을 완전한 파이선 인터프리터로 만든느 것은 아닙니다. 이러한 아이디어를 얼마나 더 발전시킬 수 있을지는 불분명합니다. 그럼에도 불구하고 그 결과는 흥미롭습니다. 역사적으로 신경망은 기존의 알고리즘적 접근 방식이 어려움을 겪는 패턴 인식 문제에서 좋은 성과를 냈습니다. 반대로 기존의 알고리즘적 접근 방식은 신경망이 잘하지 못하는 문제를 해결하는데 능숙합니다. 오늘날 웹 서버나 데이터베이스 프로그램을 신경망으로 구현하는 사람은 아무도 없습니다. 신경망과 기존 알고리즘 접근 방식의 강점을 통합하는 통합 모델을 개발하는 것은 매우 중요합니다. RNN과 RNN에서 영감을 얻은 아이디어들이 이를 돕는데 기여할 수 있습니다.

RNN은 최근 몇 년 동안 다른 많은 문제들을 해결하는 데도 사용되었습니다. 특히 음성 인식에서 유용성이 입증되었습니다. RNN 기반 접근법은 예를 들어 음소 인식 정확도에서 기록을 세우기도 했습니다. 또한 사람들이 말할 때 사용하는 언어에 대한 개선된 모델을 개발하는 데에도 사용되었습니다. 더 나은 언어 모델은 그렇지 않으면 똑같이 들리는 발화를 구별하는 데 도움이 됩니다. 예를 들어 좋은 언어 모델은 "to infinity and beyond"가 "two infinity and beyond"보다 훠린 더 가능성이 높다는 것을 알려줄 것입니다. 두 구문이 똑같이 들리더라도 말이죠. RNN은 툭정 언어 벤치마크에서 새로운 기록을 세우기도 했습니다.

이러한 연구는 우연히도 RNN뿐만 아니라 모든 종류의 심층 신경망이 음성 인식에 광범위하게 사용되는 추세의 일부입니다. 예를 들어, 심층 신경망 기반 접근법은 대규모 어휘 연속 음성 인식에서 탁월한 결과를 보였습니다. 그리고 신층 신경망 기반의 또 다른 시스템은 구글 안드로이드 은영체제에 포함되기도 했습니다.

지금까지 RNN이 무엇을 할 수 있는지에 대해서 다루었지만 어떻게 작동하는지에 대해서는 많이 다루지 않았습니다. 순방향 신경망에서 사용되는 많은 아이디어들이 RNN에서도 사용될 수 있다는 것을 알게 되더라도 놀라지 않을 것입니다. 특히, 경사 하강법과 역전파를 간단하게 수정하여 RNN을 학습시킬 수도 있습니다. 정규화 기법부터 합성곱, 활성화 및 비용 함수에 이르기까지 순방향 신경망에서 사용되는 많은 아이디어들도 순환 신경망에서 유용합니다. 따라서 우리가 다룬 많은 기법들을 RNN에 맞춰서 적용할 수 있습니다.

심층 신뢰 신경망(Deep Belief Netwrok, DBN), 생성 모델(generative model) 및 볼츠만 머신(Bolzmann machine): 심층 학습에 대한 현대적 관심은 2006년에 심층 신뢰 신경망(Deep Belief Network, DBN)이라는 신경망 학습 방법을 설명하는 논문이 발표되면서 시작되었습니다. DBN은 몇 년 동안 영향력이 컸지만, 이후 순방향 신경망이나 순환 신경망과 같은 모델들이 유행하면서 인기가 시들해졌습니다. 그럼에도 불구하고 DBN은 흥미로운 여러가지 특징을 가지고 있습니다.

DBN이 흥비로운 한가지 이유는 생성 모델(generative model)이라고 불리는 것의 한 예이기 때문입니다. 우리가 사용해온 순방향 신경망에서는 입력 활성화가 일어나면, 이것이 신경망의 다음 계층에 있는 특징 뉴런들의 활성화에 영향을 미칩니다. DBN과 같은 생선 모델도 비슷한 방식으로 사용될 수 있지만 일부 특징 뉴런의 값을 지정한 다음 신경망을 "거꾸로 실행"하여 입력 활성화에 대한 값을 생성하는 것도 가능합니다. 좀 더 구체적으로 말하면, 손글씨 숫자 이미지로 훈련된 DBN은 (잠재적으로, 그리고 약간의 주의를 기울이면) 손글씨 숫자처럼 보이는 이미지를 생성하는데도 사용될 수 있습니다. 다시 말해, DBN이 어떤 의미에서는 글씨 쓰는 법을 배우는 것입니다. 이런 점에서 생성 모델은 인간의 뇌와 매우 유사합니다. 숫자를 읽을 수 있을 뿐만 아니라 쓸 수도 있기 때문입니다. 제프리 힌턴(Geoffrey Hinton)의 인상적인 표현을 빌리자면, "모양을 인식하려면, 먼저 이미지를 생성하는 법을 배워라"라고 할 수 있습니다.

DBN이 흥미로운 두 번째 이유는 비지도 및 준지도 학습을 수행할 수 있기 때문입니다. 예를 들어 이미지 데이터로 훈련할 때, DBN은 학습 이미지가 레이블이 없더라도 다른 이미지를 이해하는 데 유용한 특징들을 학습할 수 있습니다. 그리고 비지도 학습을 수행하는 능력은 근본적인 과학적 이유뿐만 아니라 충분히 잘 작동하게 만들 수 있다면 실용적인 응용 분야에서도 매우 흥비롭습니다.

이러한 매력적인 특정에도 불구하고 왜 DBN은 신층 학습 모델로서 인기가 시들해졌을까요? 부분적인 이유는 순방향 신경망이나 순환 신경망과 같은 모델들이 이미지나 음석 인식 벤치마크에서 획기적인 성과를 거두는 등 여러 가지 놀라운 결과를 달성했기 때문입니다. 현재 이 모델들에 많은 관심이 쏠리는 것은 당연하고 타당합니다. 그러나 안타까운 결과도 있습니다. 아이디어 시장은 종종 승자 독식 방식으로 작동하여 어떤 분야에서든 현재 유행하는 것에 거의 모든 관심이 쏠리는 경향이 있습니다. 비록 장기적으로 분명히 매우 흥비로운 아이디어라 하더라도 잠시 유행에서 벗어난 아이디어를 연구하는 것은 극도로 어려워질 수 있습니다. DBN과 다른 생성형 모델들이 현재 받고 있는 관심보다 더 많은 관심을 받을 자격이 있다고 생각합니다. 그리고 언젠가 DBN이나 관련 모델이 현재 유행하는 모델들을 능가할 수도 있습니다. DBN에 대한 입문자료로 다음 자료들을 참고하시길 바랍니다.

  • DBN 개요
  • 볼츠만 머신 훈련 가이드 (DBN 자체에 대한 내용은 아니지만, DBN의 핵심 구성요소인 제한된 볼츠만 머신(restricted Boltzmann machine)에 대한 유용한 정보가 많음)
다른 아이디어들: 신경망과 심층 학습 분야에서는 또 어떤 일들이 일어나고 있을까요? 정말 엄청나게 많은 흥비로운 연구들이 진행되고 있습니다. 활발한 연구 분야로는 신경망을 이용한 자연어 처리(이 리뷰 논문 참조하세요), 기계 번역, 그리고 음악 정보학(music informatics)과 같은 다소 놀라운 응용 분야도 있습니다. 물론 이 외에도 많은 분야가 있습니다. 이 책을 읽으신 분이라면 대부분의 경우 최근 연구들을 따라갈 수 있을 것입니다. 물론, 배경지식의 공백을 채워야할 수도 있습니다.

이 섹션을 마무리하면서 특히 재미있는 논문 하나를 말씀드리고자 합니다. 이 논문은 심층 합성곱 신경망과 강화 학습(reinforcement learning)이라는 기술을 결합하여 비디오 게임을 하는 방법을 학습시킨 것에 대하여 다루고 있습니다. (후속 연구도 참조하세요.) 이 논문의 아이디어는 합성곱 신경망을 사용하여 게임 화면의 픽셀을 단순화하고 이를 더 간단한 특징 세트로 변환하여 "왼쪽으로 가기", "아래로 가기", "발사하기" 등 어떤 행동을 취할지를 결정하는 것이었습니다. 특히 흥미로운 점은 하나의 신경망이 7개의 다른 고전 비디오 게임을 꽤 잘 하도록 훈련시켰고, 그 중 3개 게임에서는 인간 전문가를 능가했다는 것입니다. 이 모든 것이 묘기처럼 들리고 "강화 학습으리 아타리 게임하기"라는 제목으로 논문을 마케팅하는데 성공했습니다. 하지만 겉치레를 넘어서, 이 시스템이 게임 규칙조차 모르는 순수한 픽셀 데이터를 입력으로 받아 매우 복잡한 규칙을 가진 매우 다른 적대적 환경 여러 곳에서 고품질의 의사결정을 하는 법을 배웠다는 점을 생각해보세요. 정말 대단한 일이 아니라 할 수 없습니다.

신경망의 미래

"내가 하는 말을 듣지 말고, 내가 의미하는 바를 들어라"라는 성질 급한 교수가 혼란스러워하는 학생에게 이야기한 오래된 농담이 있습니다. 역사적으로 컴퓨터는 혼란스러워하는 학생처럼 사용자가 무엇을 의미하는지에 대해 알지 못하는 경우가 많았습니다. 하지만 이것이 바뀌고 있습니다. 요즘 구글은 검색어를 잘못 임력하면, "혹시 [수정된 검색어]를 말씀하신 건가요?"라고 물으며 해당 검색 결과를 보여줍니다. 놀랍지 않은가요? 구글의 CEO인 래리 페이지(Larry Page)는 한때 완벽한 검색 인진을 "[사용자의 쿼리가] 무엇을 의미하는지 정확히 이해하고 사용자가 원하는 것을 정확하게 되돌려주는 것"이라고 묘사했습니다.

이것이 바로 의도 중심 사용자 인터페이스의 비전입니다. 이 비전에서는 검색 엔진이 사용자의 문자 그대로의 쿼리에 응답하는 대신, 기계 학습을 사용하여 모호한 사용자 입력을 받아들이고 사용자가 정확히 무엇을 위도했는지 파악하며, 그 통찰을 바탕으로 행동을 취합니다.

의도 중심의 인터페이스의 개념은 검색 외에도 훨씬 더 광범위하게 적용될 수 있습니다. 앞으로 수십년 동안 수천 개의 기업들이 기계 학습을 사용하여 정확하지 않은 입력도 허용하면서 사용자의 진정한 의도를 파악하고 그에 따라 행동하는 사용자 인터페이스를 갖춘 제품을 만들어 낼 것입니다. 우리는 이미 이러한 의도 중심 인터페이스의 초기 사례들을 보고 있습니다. 애플의 Siri, Wolfram Alpha, IBM의 Watson, 사진과 동영상에 주석을 달 수 있는 시스템 등이 그 예시들입니다.

이러한 제품의 대부분은 실패할 것입니다. 영감을 주는 사용자 인터페이스 디자인은 어렵고, 많은 기업들이 강력한 기계 학습 기술을 사용해 형편없는 사용자 인터페이스를 만들 것이라고 예상합니다. 사용자 인터페이스 개념이 형편없다면 세계 최고의 기계 학습 기술도 소용이 없을 것입니다. 하지만 성공하는 소수의 제품은 살아 남게 될 것입니다. 시간이 지나면서 이는 우리가 컴퓨터와 관계를 맺는 방식에 엄청난 변화를 가져올 것입니다. 얼마 전까지만 해도, 사용자들은 컴퓨터와의 대부분의 상호작용에서 정확성이 필요하다는 것을 당연하게 여겼습니다. 실제로 컴퓨터 활용 능력은 '컴퓨터는 극도로 문자적 이다'라는 생각을 내면화하는 것을 의미했습니다. 세미콜론 하나만 잘못 입력해도 컴퓨터와의 상호작용은 완전히 달라질 수 있었습니다. 하지만 앞으로 수십 년 동안 우리는 많은 성공적인 의도 중심 사용자 인터페이스를 경험하게 될 것이며, 이는 컴퓨터와 상호작용할 때 우리가 기대하는 바를 극적으로 변화시킬 것입니다.

기계 학습, 데이터 과학, 그리고 혁신의 선순환

물론, 기계 학습은 의도 중심 인터페이스를 만다는 데만 사용되는 것이 아닙니다. 또 다른 주목할 만한 응용 분야는 데이터 과학입니다. 여기서 기계 학습은 데이터에 숨겨진 "알려진 미지의 것(known unknown)"을 찾아내는 데 사용될 수 있습니다. 이 분야는 이미 큰 인기를 얻고 있으며 많은 글이 쓰여졌기 때문에 길게 설명하지는 않겠습니다. 하지만 이 유행의 한 가지 결과에 대해서 언급하고자 합니다. 흔히 주목받지 않은 점인데, 장기적으로 기계 학습의 가장 큰 혁신은 어떤 단일한 개념적 돌파구가 아닐 수도 있습니다. 오히려, 기계 학습 연구가 데이터 과학 및 다른 분야에 대한 응용을 통해 수익성을 갖게 되는 것이 가장 큰 돌파구가 될 수 있습니다. 만약 한 기업이 기계 학습 연구에 1달러를 투자하여 1.1달러를 합리적인 속도로 회수할 수 있다면, 엄청난 돈이 기계 학습 연구에 투입될 것입니다. 다시 말해, 기계 학습은 기술 분야에서 여러 주요 신규 시장과 성장 영역을 창출하는 엔진 역할을 하고 있습니다. 그 결과, 심층적인 전문 지식과 엄청난 자원에 접근할 수 있는 대규모 팀들이 생겨날 것입니다. 이는 기계 학습을 더욱 발전시켜 더 많은 시장과 기회를 창출하는 혁신의 선순환을 만들어 낼 것입니다.

신경망과 심층 학습이 인공지능으로 이어질까?

이 책엥서 우리는 이미지 문류와 같은 특정 작업을 수행하기 위해 신경망을 사용하는데 집중했습니다. 이제 우리의 야망을 넓혀서 범용적인 사고 컴퓨터는 어떨지 질문해 봅시다. 신경망과 심층 학습이 범용 인공지능 문제를 해결하는데 도움이 될까요? 그리고 그렇다면 최근 심층 학습의 빠른 발전을 고려할 때 범용 인공지능을 곧 기대할 수 있을까요?

이 질문들에 포괄적으로 답하려면 별도의 책이 필요할 것입니다. 대신, 한 가지 관찰을 제시하고자 합니다. 이 관찰은 "콘웨이의 법칙(Conway's law)"라고 알려져 있습니다.

"시스템을 설계하는 모든 조직은 ... 필연적으로 그 조직의 소통 구조를 복제한 구조를 가진 디자인을 만들어 낸다."

예를 들어 콘웨이의 법칙에 따르면 보잉 747 항공기의 디자인은 747이 설계될 당시 보잉과 계약업체들의 확장된 조직 구조를 반영합니다. 더 간단하고 구체적인 예로 복잡한 소프트웨어 애플리케이션을 만드는 기업을 생각해 봅시다. 만약 애플리케이션의 대시보드가 특정 기계 학습 알고리즘과 통합되어야 한다면, 대시보드를 만다는 사람은 당연히 그 기업의 기계학습 전문가와 대화해야합니다. 콘웨이 법칙은 이러한 관찰을 확대해서 표현한 것일 뿐입니다.

콘웨이의 법칙을 처음 들었을 때 사람들은 "그거 너무 진부하거 뻔한 거 아닌가요?" 또는 "그거 틀린 거 아닌가요?"라고 반응합니다. 먼저, 틀렸다는 반론부터 다루어보도록 하겠습니다. 이 반론의 예로 보잉의 회계 부서가 747 디자인에 어떻게 반영될까요? 청소 부서는요? 사내 마케팅 부서는요? 그 대답은 이 조직의 부서들은 747에 명시적으로 나타나지 않을 가능성이 높다는 것입니다. 따라서 우리는 콘웨이의 법칙을 명시적으로 설계 및 엔지니어링에 관련된 조직 부분에만 적용되는 것으로 이해해야 합니다.

그렇다면 다른 반론, 즉 진부하고 뻔하다는 반론은 어떨까요? 이 말도 맞을 수 있지만, 반드시 그렇지는 않습니다. 왜냐하면 조직들은 너무나 자주 콘웨이의 법칙을 무시하고 행동하기 때문입니다. 신제품을 만드는 팀은 종종 오래된 인력으로 부풀려지거나, 반대로 핵심적인 전문성을 가진 사람이 부족한 경우가 있습니다. 쓸모없고 복잡한 기능이 있는 모든 제품들을 생각해 보세요. 또는 명백한 주요 결함이 있는 모든 제품들을 생각해보세요. 두 가지 종류의 문제 모두, 좋은 제품을 만드는 데 필요했던 팀과 실제로 구성된 팀 사이의 불일치가 있을 때 종종 발생합니다. 콘웨이의 법칙은 뻔해 보일 수 있지만 사람들이 일상적으로 이를 무시하지 않는다는 뜻은 아닙니다.

콘웨이의 법칙은 우리가 구성 요소와 그것을 만드는 방법을 꽤 잘 이해하고 있는 시스템의 설계 및 엔지니어링에 적용됩니다. 하지만 인공지능 개발에는 직접적으로 적용할 수 없습니다. 왜나하면 인공지능은 아직 그런 문제가 아니기 때문입니다. 우리는 구성 요소가 무엇인지 모릅니다. 심지어 어떤 기본적인 질문을 해야하는지도 확신하지 못합니다. 다시 말해, 이 시점에서 인공지능은 엔지니어링 문제라기 보다는 과학의 문제에 가깝습니다. 제트 엔진이나 공기역학의 원리를 모르는 상태에서 747을 설계한다고 생각해보세요. 어떤 종류의 전문가를 고용해야 할지조차 모를 것입니다. 베르너 폰 브라운(Wernher von Braun)이 말했듯이, "기초 연구란 내가 무엇을 하고 있는지 모를 때 하는 것이다." 엔지니어링보다 과학에 가까운 문제에 적용되는 콘웨이의 법칙 버전이 있을까요?

이 질문에 대한 통찰력을 얻기 위해 의학의 역사를 생각해보도록 합시다. 초창기 의학은 갈레노스와 히포크라테스 같은 의사들이 신체 전체를 연구하던 영역이었습니다. 하지만 지식이 커지면서 사람들은 전문화할 수밖에 없었습니다. 우리는 질병의 세균설, 항체의 작동방식에 대한 이해, 또는 심장, 폐, 정맥, 동맥이 완전한 심혈관계를 형성한다는 이해와 같은 여러 심오하고 새로운 사실들을 발견했습니다. 이러한 깊은 통찰력은 역학, 면역학, 그리고 심혈관계 관련 분야들의 기초가 되었습니다. 이처럼 지식의 구조가 의학의 사회적 구조를 형성했습니다. 특히 면역학의 경우가 두드러집니다. 면역 시스템이 존재하고 연구할 가치가 있는 시스템이라는 것을 깨닫는 것은 매우 비범한 통찰입니다. 그래서 우리는 전문가, 학회, 심지어 상까지 있는 의학 분야 전체가 보이지 않을 뿐만 아니라 어쩌면 실체가 아닐 수도 있는 무언가를 중심으로 조직되어 있는 것을 보게 됩니다.

이것은 의학뿐만 아니라 물리학, 수학, 화학 등 여러 확립된 과학 분야에서 반복되어 온 흔한 패턴입니다. 과학 분야는 몇 가지 깊은 아이디어만 가진 단일체로 시작됩니다. 초기 전문가들은 그 모든 아이디어를 마스터할 수 있습니다. 그러나 시간이 지나면서 그 단일체적 특성은 변합니다. 한 사람이 진정으로 마스터하기에는 너무 많은 심오하고 새로운 아이디어들을 발견하게 됩니다. 그 결과, 분야의 사회적 구조가 재조직되어 그 아이디어들을 중심으로 분화됩니다. 단일체 대신, 우리는 분야 속의 분야 속의 분야라는 복잡하고 재귀적이며 자기 참조적인 사회적 구조를 가지고 되며, 그 조직은 우리의 가장 깊은 통찰 사이의 연결을 반영합니다. 이처럼 지식의 구조가 과학의 사회적 조직을 형성합니다. 그러나 그 사회적 형태는 다시 우리가 무엇을 발견할 수 있는지를 제약하고 결정하는 데 도움이 됩니다. 이것이 콘웨이 법칙에 대한 과학적 비유입니다.

그렇다면 이것이 심층 학습이나 인공지능과 무슨 관련이 있을까요?

인공지능의 초창기부터 한쪽에서는 "이것이 그렇게 어렵지 않을 겁니다. 우리에게는 초특급 무기가 있으니까요"라고 주장하고, 다른 한쪽에서는 "초특급 무기만으로 충분하지 않을 겁니다."라고 반박하는 논쟁이 있었습니다. 심층 학습은 이러한 논쟁에서 가장 최신의 초특급 무기입니다. 초기 버전의 논쟁에서는 논리, 프롤로그, 전문가 시스템 등 그 시대의 가장 강력한 기술이 사용되었습니다. 이러한 논쟁의 문제는 주어진 초특급 무기가 얼마나 강력한지 제대로 말해줄 수 있는 방법이 없다는 것입니다. 물론 우리는 심층 학습이 극도로 어려운 문제들을 해결할 수 있다는 증거를 검토하는데 적지 않은 노력을 들였습니다. 확실히 매우 흥미롭고 유망해 보입니다. 하지만 프롤로그, 유리스코, 전문가 시스템 같은 기술들도 그 당시에는 마찬가지로 유망해 보였습니다. 따라서 단순히 어떤 아이디어들이 매우 유망해 보인다는 사실만으로는 큰 의미가 없습니다. 어떻게 심층 학습이 이러한 이전 아이디어들과 진정으로 다른지 알 수 있을까요? 아이디어의 집합이 얼마나 강력하고 유망한지 측정하는 방법이 있을까요? 콘웨이 법칙은 거칠과 경험적인 대리 지표로서, 그 아이디어와 관련된 사회적 구조의 복잡성을 평가할 수 있다고 주장합니다.

그래서 두 가지 질문을 던져봐야 합니다. 첫째, 사회적 복잡성이라는 이 척도에 따르면 심층 학습과 관련된 아이디어의 집합은 얼마나 강력할까요? 둘째, 범용 인공지능을 구축하기 위하여 얼마나 강력한 이론이 필요할까요?

첫 번째 질문에 대해: 오늘날 심층 학습을 보면 흥미롭고 매우 빠르게 발전하고 있지만 여전히 상대적으로 단일체적인 분야입니다. 몇 가지 깊은 아이디어와 몇 개의 주요 학회가 있으며 이들 학회 간에는 상당한 중복이 있습니다. 그리고 확률적 경사 하강법을 사용하여 비용 함수를 최적화하는 동일한 기본 아이디어 세트를 활용하는 논문들이 계속해서 나오고 있습니다. 이 아디어들이 매우 성공적이라는 것은 놀라운 일입니다. 하지만 우리가 아직 보지 못하는 것은 각자의 깊은 아이디어 세트를 탐구하며 심층 학습을 여러 방향으로 밀고 나가는 잘 발달된 하위 분야들입니다. 따라서 사회적 복잡성이라는 척도에 따르면 심층 학습은 말장난처럼 들릴지 모르지만, 여전히 얕은 분야입니다. 한 사람이 이 분야의 가장 깊은 아이디어 대부분을 마스터하는 것이 여전히 가능합니다.

두 번째 질문에 대해: 인공지능을 얻기 위해 얼마나 복잡하고 강력한 아이디어 세트가 필요할까요? 물론, 이 질문에 답은 아무도 확실히 모른다는 것입니다. 이 질문에 대한 기존 증거들을 검토한 결과를 부록에 담았습니다. 꽤 낙관적으로 본다하더라도 인공지능을 구축하기 위해서는 매우, 매우 많은 깊은 아이디어가 필요할 것이라고 판단됩니다. 따라서 콘웨이의 법칙은 그러한 지점에 도달하기 위해서는 우리의 가장 깊은 통찰력 속의 구조를 반영하는 복잡하고 놀라운 구조를 가진 많은 상호 연관된 학문 분야의 출현을 필연적으로 보게 될 것이라고 시사합니다. 우리는 신경망과 심층 학습의 사용에 아직 이 풍부한 사회적 구조를 보지 못하고 있습니다. 따라서 심층 학습을 사용하여 범용 인공지능을 개발하기까지는 적어도 수십 년이 더 걸릴 것이라 믿습니다.

여기서 우리는 잠정적이고, 다소 뻔해 보이며, 불확실한 결론을 내리기 위해 많은 노력을 들여 논증을 구성했습니다. 이는 획실성을 갈망하는 사람들을 좌절시킬 수도 있습니다. 인터넷에는 비약한 추론과 존재하지 않는 증거들을 바탕으로 인공지능에 대해 매우 단정적이고 강한 주장을 펴는 많은 사람들을 볼 수 있습니다. 이에 대한 솔직한 의견은 이렇습니다. 아직 말하기에는 너무 이르다는 것입니다. 오래된 농담처럼, 과학자에게 어떤 발견이 얼마나 남았는지 물었을 때 "10년"(또는 그 이상)이라고 대답하면, 그들이 의미하는 바는 "전혀 모른다"입니다. 인공지능은 통제된 핵융합과 같은 몇몇 다른 기술처럼 60년 이상 동안 "10년 후"에 머물러 있었습니다. 반면에 우리가 심층 학습에서 확실한 것은 아직 한계가 발견되지 않은 강력한 기술이며, 해결해야할 근본적인 문제도 많다는 것입니다. 이것이야 말로 흥미진진한 창조적 기회입니다.

댓글 없음:

Lee, Jeong Ho

Lee, Jeong Ho
Biography: Bachelor: Computer Science in Korea Univ. Master: Computer Science in KAIST Carrier: 1. Junior Researcher at Korea Telecom (2006 ~ 2010) 2. Researcher at Korea Institute of Nuclear Nonproliferation and Control (2010~)