인공지능 딥러닝 개론 - 딥러닝이란 무엇인가 (상세)

2026년 4월 9일 2026년 4월 10일 22분

이 글은 “인공지능 딥러닝 개론” 강의의 상세 내용입니다. 강의 개요는 여기에서 확인할 수 있습니다.

이 내용을 슬라이드로 보려면 프레젠테이션 모드를 이용하세요.

인공지능 딥러닝 개론 - 딥러닝이란 무엇인가 시연
https://drive.google.com/drive/folders/1KebEH3Kf6zPiMobqsWpRSoERJaLe7XMi

Part 1 — 알고리즘과 함수

함수란

함수는 입력을 받아 출력을 내보내는 처리 단위입니다.
어떤 입력이 들어왔을 때 어떤 출력이 나오는지 정의되어 있다면, 그것을 함수라고 부를 수 있습니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR I["입력\n2"] --> F["함수"] --> O["출력\n초록색"] style I fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style F fill:#f8bbd0,stroke:#c2185b,stroke-width:2px,rx:10,ry:10 style O fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10

예를 들어 다음과 같은 입출력 관계가 있다고 합시다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR subgraph 입력["입력"] direction TB I2["2"] I4["4"] I5["5"] end subgraph 출력["출력"] direction TB O2["초록색"] O4["파란색"] O5["파란색"] end I2 --> O2 I4 --> O4 I5 --> O5 style I2 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style I4 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style I5 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style O2 fill:#a5d6a7,stroke:#388e3c,stroke-width:2px,rx:10,ry:10 style O4 fill:#90caf9,stroke:#1565c0,stroke-width:2px,rx:10,ry:10 style O5 fill:#90caf9,stroke:#1565c0,stroke-width:2px,rx:10,ry:10 style 입력 rx:15,ry:15 style 출력 rx:15,ry:15

이 관계를 만족하는 어떤 처리 단위가 있다면, 그것을 함수라고 할 수 있습니다.

알고리즘이란

알고리즘은 위 함수의 내부를 사람이 직접 설계한 것입니다.

위의 입출력 관계를 코드로 나타내면 다음과 같습니다.

1
2
3
4
5
if (input < 4) {
  return green;
} else if (input >= 4) {
  return blue;
}

알고리즘을 정리하면 이렇게 표현할 수 있습니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR Who(["사람이"]) --> What(["어떤 문제에 대한\n풀이 방법을"]) What --> How(["고민하여,\n컴퓨터 프로그램으로\n만듭니다"]) style Who fill:#ffcdd2,stroke:#c62828,stroke-width:3px style What fill:#fff9c4,stroke:#f9a825,stroke-width:2px style How fill:#c8e6c9,stroke:#388e3c,stroke-width:2px

여기서 가장 중요한 것은 사람이 직접 풀이 방법을 만든다는 점입니다.
입력과 출력의 관계가 단순하고 분명할 때는 알고리즘만으로도 충분히 문제를 해결할 수 있습니다.

Part 2 — 머신러닝과 딥러닝

Machine Learning

머신러닝은 알고리즘과 다르게, 사람이 풀이 방법을 만들지 않습니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR Who(["기계가"]) --> What(["어떤 문제에 대한\n풀이 방법을"]) What --> How(["학습하여,\n컴퓨터 프로그램으로\n만듭니다"]) style Who fill:#bbdefb,stroke:#1565c0,stroke-width:3px style What fill:#fff9c4,stroke:#f9a825,stroke-width:2px style How fill:#c8e6c9,stroke:#388e3c,stroke-width:3px

여기서 핵심 단어는 두 개입니다. 기계학습.
사람이 일일이 if/else 를 짜는 대신, 기계가 데이터를 보고 스스로 패턴을 익히게 합니다.

AI · ML · DL의 관계

이 셋은 흔히 같은 말로 쓰이지만, 엄밀히는 포함 관계입니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart TB subgraph AI["Artificial Intelligence (AI)"] direction TB subgraph ML["Machine Learning (ML)"] direction TB subgraph DL["Deep Learning (DL)"] direction TB dl_inner[" "] end end end style AI fill:#e1bee7,stroke:#8e24aa,stroke-width:3px,rx:20,ry:20 style ML fill:#fff9c4,stroke:#f9a825,stroke-width:3px,rx:15,ry:15 style DL fill:#bbdefb,stroke:#1976d2,stroke-width:3px,rx:10,ry:10 style dl_inner fill:#bbdefb,stroke:#bbdefb
  • AI(인공지능): 가장 큰 개념. 사람의 지능을 흉내내는 모든 시도
  • ML(머신러닝): AI 중에서도 데이터로부터 학습하는 방식
  • DL(딥러닝): ML 중에서도 깊은 신경망을 사용하는 방식

왜 머신러닝이 필요한가요?

알고리즘만으로 해결하기 어려운 문제들이 있습니다.
예를 들어 자동차 사진을 받아서 “Car"라고 답하는 함수를 만들어야 한다면 어떨까요?

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR I["자동차\n이미지"] --> F["알고리즘?"] --> O["Car"] style I fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style F fill:#f8bbd0,stroke:#c2185b,stroke-width:2px,rx:10,ry:10 style O fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10

이 경우 문제가 두 가지 생깁니다.

  1. 입력이 복잡해집니다. 숫자 하나가 아니라 수많은 픽셀의 모음입니다
  2. 입력에 따라 알고리즘이 계속 변해야 합니다. 사진의 각도·조명·차종에 따라 if/else가 폭발적으로 늘어납니다

28×28 이미지의 경우의 수

머신러닝이 다루는 가장 단순한 이미지 문제 — MNIST 손글씨 숫자 분류를 살펴봅시다.

MNIST 이미지는 가로 28픽셀, 세로 28픽셀의 흑백 이미지입니다.
각 픽셀은 0(검은색)부터 255(흰색)까지 256가지 값을 가질 수 있습니다.
참고로 MNIST는 검은 배경에 흰 글씨입니다 — 글씨가 있는 자리의 픽셀 값이 255에 가깝습니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR P0["0"] --- P31["31"] --- P63["63"] --- P95["95"] --- P127["127"] --- P159["159"] --- P191["191"] --- P223["223"] --- P255["255"] style P0 fill:#000000,stroke:#000000,color:#ffffff style P31 fill:#1f1f1f,stroke:#000000,color:#ffffff style P63 fill:#3f3f3f,stroke:#000000,color:#ffffff style P95 fill:#5f5f5f,stroke:#000000,color:#ffffff style P127 fill:#7f7f7f,stroke:#000000,color:#ffffff style P159 fill:#9f9f9f,stroke:#000000,color:#000000 style P191 fill:#bfbfbf,stroke:#000000,color:#000000 style P223 fill:#dfdfdf,stroke:#000000,color:#000000 style P255 fill:#ffffff,stroke:#000000,color:#000000 linkStyle default stroke:#cfcfcf,stroke-width:1px

각 박스 안의 숫자가 픽셀 값이고, 박스의 실제 색이 그 값에 해당하는 회색조입니다.
0은 완전한 검은색, 255는 완전한 흰색이며 그 사이는 회색조의 그라데이션입니다.

따라서 28×28 흑백 이미지가 가질 수 있는 모든 경우의 수는 다음과 같습니다.

$$ 256^{28 \times 28} = 256^{784} \approx \infty $$

이것은 사실상 무한대에 가까운 숫자입니다.
모든 경우에 대해 if/else로 분기하는 알고리즘은 만들 수 없습니다.

1
2
3
4
5
6
if (
  i[2][10] == 1 &&
  i[3][10] == 1 &&
  i[4][10] == 1 && ...) {
  return 1;
}

이런 식으로 모든 픽셀 조합을 나열하는 것은 불가능합니다.

28×28 픽셀을 직접 만져봅시다

아래 캔버스에 직접 그림을 그려보면서, 28×28 한 칸 한 칸의 픽셀이 어떤 값을 가지는지 살펴보세요.
0(검은색)부터 255(흰색)까지의 값이 있고, 각 픽셀에 마우스를 올리면 우측 패널에 그 값이 표시됩니다.

근사로 풀어봅시다

정답이 아니더라도, 근사로 풀어보면 어떨까요?
무한대의 변수가 필요한 문제를 유한한 w개의 가중치로 풀어내는 것 — 이것이 머신러닝의 핵심 아이디어입니다.

그런데 한 가지 직관이 더 필요합니다. $256^{784}$가 무한대에 가까워도,
실제로 우리가 마주치는 손글씨 이미지는 그 안의 극히 일부 영역에만 분포합니다.

픽셀이 완전히 랜덤하게 찍힌 이미지는 거의 의미 있는 숫자처럼 보이지 않으며,
“사람이 쓴 숫자"라는 패턴은 그 거대한 공간 안의 매우 좁은 부분 공간(manifold)에만 모여 있습니다.

머신러닝은 이 좁은 부분 공간만 잘 표현할 수 있는 함수를 찾으면 되기 때문에,
유한한 가중치로도 충분히 근사가 가능합니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR I["28×28\n흑백 이미지"] --> M["w개의\n가중치를\n가진 모델"] --> O["어떤 숫자일까?\n2 / 3 / 4 ..."] style I fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style M fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10 style O fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10

물론 이 w가 잘못 결정되면 결과가 다르게 나옵니다.
그래서 w를 어떻게 잘 결정할 것인가가 머신러닝의 가장 중요한 문제가 됩니다.

Part 3 — 신경망 구조

입력층 · 은닉층 · 출력층

신경망은 노드(뉴런)들이 층을 이루고 있는 구조입니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR subgraph 입력층["입력층"] i1["$$i_{1}$$"] i2["$$i_{2}$$"] in["$$i_{784}$$"] end subgraph 은닉층["은닉층"] a1["$$a_{1}$$"] a2["$$a_{2}$$"] a3["$$a_{3}$$"] a4["$$a_{4}$$"] end subgraph 출력층["출력층"] o1["$$o_{1}$$"] o2["$$o_{2}$$"] o3["$$o_{3}$$"] o0["$$o_{0}$$"] end i1 --> a1 & a2 & a3 & a4 i2 --> a1 & a2 & a3 & a4 in --> a1 & a2 & a3 & a4 a1 --> o1 & o2 & o3 & o0 a2 --> o1 & o2 & o3 & o0 a3 --> o1 & o2 & o3 & o0 a4 --> o1 & o2 & o3 & o0 style i1 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:20,ry:20 style i2 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:20,ry:20 style in fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:20,ry:20 style a1 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:20,ry:20 style a2 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:20,ry:20 style a3 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:20,ry:20 style a4 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:20,ry:20 style o1 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:20,ry:20 style o2 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:20,ry:20 style o3 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:20,ry:20 style o0 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:20,ry:20 style 입력층 rx:15,ry:15 style 은닉층 rx:15,ry:15 style 출력층 rx:15,ry:15
  • 입력층: 28×28 = 784개의 픽셀이 그대로 들어옵니다
  • 은닉층: 입력층과 출력층 사이의 모든 층. 신경망의 핵심
  • 출력층: 결과가 나오는 곳. 0~9 숫자 분류라면 10개의 노드

각 노드 사이를 잇는 화살표 하나하나가 곧 가중치(weight) 입니다.
입력과 가중치를 곱해서 더한 후, 출력층에서는 보통 softmax를 통해 확률로 변환합니다.

가중치(weight)와 편향(bias)

각 노드에서 일어나는 계산은 사실 두 가지 값이 관여합니다.

  • 가중치(weight, w): 입력값에 곱해지는 값. 화살표 하나당 하나씩 존재합니다
  • 편향(bias, b): 가중합에 더해지는 값. 노드마다 하나씩 존재합니다

식으로 쓰면 한 노드의 출력은 이렇게 계산됩니다.

$$ \text{output} = f\left(\sum_{i} w_i \cdot x_i + b\right) $$

여기서 f가 다음 절에서 설명할 활성화 함수입니다.
편향은 입력과 무관하게 노드의 출력을 위/아래로 평행이동시키는 역할을 하며, 표현력에 중요한 역할을 합니다.

활성화 함수 (Activation Function)

만약 신경망이 단순히 “곱하고 더하기"만 한다면, 아무리 층을 깊게 쌓아도 전체 함수는 결국 하나의 선형 변환으로 압축됩니다.
두 개의 선형 함수를 합쳐도 또 다른 선형 함수가 되기 때문입니다.
이러면 층을 쌓는 의미가 없어지고, 직선으로 그을 수 있는 단순한 패턴밖에 표현하지 못합니다.

여기서 등장하는 것이 활성화 함수입니다.
가중합 결과에 비선형 함수를 한 번 통과시켜서, 신경망이 곡선을 표현할 수 있게 만들어줍니다.
대표적인 활성화 함수는 다음과 같습니다.

이름특징
ReLU$\max(0, x)$가장 많이 쓰이는 함수. 음수는 0, 양수는 그대로
Sigmoid$1 / (1 + e^{-x})$0~1 범위로 압축. 옛날에 많이 쓰였음
Softmax$e^{x_i} / \sum_j e^{x_j}$여러 출력값을 합이 1이 되는 확률로 변환

은닉층에서는 보통 ReLU를 쓰고, 분류 문제의 출력층에서는 Softmax를 사용합니다.
Softmax는 10개의 출력값을 받아서 “각 클래스에 속할 확률"로 만들어주는 함수라고 이해하면 됩니다.

활성화 함수 시각화

각 활성화 함수의 모양을 직접 확인해보세요.
입력 슬라이더를 움직이면 그에 해당하는 출력 값이 함께 표시됩니다.

특히 “선형 (없음)” 탭을 선택해보세요.
활성화 함수가 없으면 그냥 직선이 되어버리는 것을 볼 수 있습니다.
이런 직선 함수들만 합성하면 결국 또 다른 직선이 되기 때문에, 비선형 활성화 함수가 반드시 필요합니다.

뉴런 계산기 — 한 노드의 계산을 직접 해보기

지금까지 배운 가중치, 편향, 활성화 함수가 하나의 뉴런 안에서 어떻게 결합되는지 인터랙티브하게 확인해봅시다.
슬라이더를 움직이면 가중합과 활성화 결과가 실시간으로 계산됩니다.

이 데모에서 보이는 화살표 굵기는 가중치의 절대값에, 색은 부호에 대응합니다. (파란색=양수, 빨간색=음수)
가중치가 0에 가까우면 그 입력은 결과에 거의 영향을 주지 않고, 가중치가 크면 큰 영향을 줍니다.
신경망 학습은 결국 이 가중치와 편향 값을 데이터로부터 자동으로 찾아가는 과정입니다.

계산 예시

수식으로도 한 번 따라가봅시다.
입력 이미지의 첫 번째 픽셀들이 다음과 같다고 합시다.

픽셀
2525/255 ≈ 0.098
255255/255 = 1.0
126126/255 ≈ 0.49

그리고 해당 가중치가 0.1, 0.5, 4.7 이라면, 다음 노드의 가중합은 이렇게 계산됩니다.

$$ 0.098 \times 0.1 + 1.0 \times 0.5 + 0.49 \times 4.7 = 2.8128 $$

이 값에 편향을 더하고 활성화 함수를 통과시킨 결과가 다음 층의 입력이 됩니다.
이렇게 층을 거쳐 마지막 출력층에서는 softmax를 통과해 0~1 사이의 확률값이 나옵니다.

Deep Learning

은닉층이 한 층뿐인 신경망(shallow network)도 이론적으로는 어떤 함수든 근사할 수 있다는 것이 알려져 있습니다.
(universal approximation theorem)
그런데 실제로는 한 층을 매우 넓게 만드는 것보다, 여러 층을 깊게 쌓는 쪽이 훨씬 효율적이고 성능도 좋다는 사실이 발견되었습니다.
이렇게 은닉층을 여러 층 쌓은 깊은 신경망을 사용하는 것이 딥러닝입니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR I["입력층\n[i]"] --> L1["[layer1]"] --> L2["[layer2]"] --> L3["[layer3]"] --> L4["[layer4]"] --> O["출력층\n[o]"] style I fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style L1 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style L2 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style L3 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style L4 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style O fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10

레이어가 많아질수록 학습해야 할 가중치도 많아지고, 더 복잡한 패턴을 표현할 수 있게 됩니다.
앞쪽 층은 단순한 특징(선·곡선)을 잡고, 뒤쪽 층은 더 추상적인 특징(눈·코, 숫자의 형태 등)을 잡는 식으로 역할이 자연스럽게 분담됩니다.

신경망 구조: 활성화 함수와 Softmax 실습
https://colab.research.google.com/drive/1KUtBC8WPzXkMDzymxk57Zh2UJ8wQVIy1

Part 4 — 트레이닝의 비유

가중치 w를 어떻게 잘 학습할까요? 두 가지 비유로 직관을 잡아봅시다.

비유 1 — 하루에 100번 농구공 던지기

매일 100번 농구공을 던지는 사람을 상상해봅시다.

  • Day 1: 처음에는 거리 감각이 없어서 공이 골대에 한참 못 미칩니다
  • Day 2: 어제보다 조금 더 세게 던져봅니다. 그래도 멉니다
  • Day 3: 더 세게! 이제 골대에 가까워집니다
  • Day 4: 드디어 골대에 들어갑니다 🎉

이 과정에서 두 가지 핵심 개념이 등장합니다.

미분 (Differentiation)

농구를 할 때 슛이 들어가지 않으면 우리는 자연스럽게 “더 세게 던져야겠다"고 생각합니다.
그런데 얼마나 더 세게? 이것이 중요합니다.

힘을 1만큼 줄였더니 공이 더 멀리 가거나,
힘을 3만큼 줄였더니 더 조금 가거나,
힘을 2만큼 늘렸는데 바로 앞에 떨어지거나…
실제로는 이런 식으로 동작하지 않습니다.

공학적 의미에서 미분이 가능하다는 것은,
값을 조금씩 변화시켰을 때 우리가 원하는 방향으로 결과가 변한다는 것을 의미합니다.
딥러닝의 손실 함수가 미분 가능해야 하는 이유도 여기에 있습니다.
— 어느 방향으로 가중치를 움직여야 손실이 줄어드는지 알 수 있어야 하기 때문입니다.

학습률과 옵티마이저 (Learning rate · Optimizer)

4일차나 되어서야 드디어 골대에 들어가네요.
차라리 처음부터 확 거리를 늘리면 조금 덜 고생할 수 있지 않을까요?

이 “한 번에 얼마나 변화시킬지"를 결정하는 값이 학습률(learning rate) 이고,
학습률을 사용해 가중치를 어떻게 업데이트할지 결정하는 알고리즘이 옵티마이저(optimizer) 입니다.
학습률이 너무 작으면 학습이 한없이 느리고, 너무 크면 골대를 지나쳐 버려서 영영 도달하지 못할 수도 있습니다.

직접 던져봅시다

아래 데모에서 학습률을 바꿔가며 농구공을 던져보세요.
“던지기” 버튼을 누르면 현재 힘으로 공을 던지고,
결과(골대까지 얼마나 부족했는지/넘쳤는지)를 보고 자동으로 힘을 조정합니다.
“자동 학습"을 켜면 이 과정이 반복되면서 Loss가 줄어드는 과정을 볼 수 있습니다.

  • 학습률이 작을 때(0.05~0.10): 매우 조금씩 힘을 바꿔서 수렴은 하지만 느립니다
  • 학습률이 적당할 때(0.20~0.40): 빠르게 적정 힘을 찾습니다
  • 학습률이 클 때(0.60 이상): 힘이 들쭉날쭉 — 지나쳤다 모자랐다를 반복하며 수렴하지 못합니다

비유 2 — 눈 가리고 걸어가기

이번에는 두 사람이 있다고 합시다. 수행자(눈을 가린 사람)와 지시자.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR 수행자["수행자\n(눈을 가림)"] -. "한 걸음" .-> 위치["현재 위치"] 위치 -. "거리 측정" .-> 지시자["지시자"] 지시자 -. "방향 지시" .-> 수행자 style 수행자 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style 위치 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style 지시자 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10

수행자는 눈을 가린 채 어떤 목적지까지 걸어가야 하고, 지시자는 수행자에게 방향을 알려줍니다.
“왼쪽으로 두 걸음”, “앞으로 계속”, “아니 너무 갔어, 오른쪽” 같은 식으로요.

여기서 지시자가 목적지와 수행자 사이의 거리를 계산하는 것이 바로 Loss Function입니다.
그리고 그 거리 정보를 받아서 다음에 어느 방향으로 얼마나 움직일지 결정하는 것이 옵티마이저입니다.

좀 더 정교하게 만들기 위해 격자를 그리고 “10cm 움직이는 것을 한 스텝"으로 약속한다면,
더 세밀한 컨트롤이 가능해집니다.
이것이 학습률(learning rate)의 개념입니다.

직접 걸어봅시다

아래 데모에서 눈을 가린 수행자(파란 원)가 목적지(주황 별)를 향해 한 걸음씩 이동합니다.
수행자는 목적지를 볼 수 없고,
지시자가 알려주는 방향(녹색 화살표 = gradient)거리(Loss) 정보만 가지고 움직입니다.

학습률(보폭)을 바꿔가며 시도해보세요.

  • 보폭이 너무 작으면: 아주 천천히 가지만 정확하게 도착합니다
  • 보폭이 적당하면: 빠르고 안정적으로 도착합니다
  • 보폭이 너무 크면: 목적지를 지나쳐서 왔다 갔다 합니다

격자 크기를 늘려보면 문제가 더 넓어져서, 같은 학습률로도 더 많은 스텝이 필요하다는 것을 알 수 있습니다.
— 실제 딥러닝에서도 모델이 커지면 학습에 더 많은 시간이 필요합니다.

비유와 실제 학습 과정의 매핑

이 두 비유를 딥러닝의 실제 용어로 정리하면 다음과 같습니다.

비유딥러닝 용어
한 번 던지기 / 한 걸음 걷기한 번의 forward pass (순전파)
결과를 보고 거리 측정Loss function 계산
다음에 얼마나 다르게 할지 결정Backpropagation + Optimizer (역전파)
한 번에 얼마나 변할지Learning rate (학습률)
100번 던지기 / 1일 훈련1 epoch (전체 데이터를 한 번 사용)

특히 역전파(backpropagation) 는 비유에서 “결과를 보고 다음 시도를 조정한다"에 해당하는 핵심 과정입니다.
출력층에서 계산된 손실(loss)이 마지막 층의 가중치부터 시작해서 입력층 방향으로 거꾸로 전달되며,
각 가중치에 대해 “얼마나 줄여야/늘려야 손실이 줄어드는지"를 미분으로 계산합니다.

경사 하강법 직접 체험하기

말로만 들으면 모호한 “기울기 따라 내려가기"를 직접 해봅시다.
아래 데모는 가중치 1개짜리 손실 함수에서 학습률을 바꿔가며
한 스텝씩 또는 자동으로 최저점에 도달하는 과정을 보여줍니다.

학습률을 다양하게 시도해보세요.

  • 너무 작게(0.05 이하) — 거의 움직이지 않습니다. 수렴까지 영원이 걸립니다.
  • 적당히(0.10~0.30) — 안정적으로 최저점에 수렴합니다.
  • 너무 크게(0.80 이상) — 최저점을 지나쳐 좌우로 튀거나 발산합니다.

이 데모의 손실 함수에는 작은 굴곡(local minimum)이 있어서,
시작점이나 학습률에 따라 진짜 최저점을 못 찾고 작은 굴곡에 빠져버리는 것도 볼 수 있습니다.
— 실제 딥러닝에서도 흔히 겪는 문제입니다.

트레이닝의 비유: 경사 하강법과 학습률 실습
https://colab.research.google.com/drive/1uma35xZcat3dNlEHDjjDHWsfe45zn4JS

Part 5 — 트레이닝 · 검증 · 테스트 · 예측

데이터의 3분할

딥러닝에서 데이터는 보통 세 그룹으로 나눕니다.

종류용도
트레이닝 (Training)모델의 가중치를 직접 학습시키는 데 사용
검증 (Validation)학습 도중 모델 성능을 모니터링하고 하이퍼파라미터 조정
테스트 (Test)학습이 완전히 끝난 뒤, 처음 보는 데이터로 최종 평가

학습에 사용한 데이터로 평가하면 모델은 무조건 좋은 점수를 받습니다. (이미 답을 봤기 때문)
그래서 진짜 실력은 처음 보는 데이터로 측정해야 합니다.
— 이게 테스트의 핵심 목적인 일반화(generalization) 측정입니다.

딥러닝의 입출력

MNIST 모델의 입출력을 정리하면 다음과 같습니다.

항목형태
입력28×28 모양의 float 타입 텐서
모델w개의 가중치를 가진 모델
출력길이 10인 벡터 (각 숫자에 대한 확률)

예를 들어 숫자 1 이미지를 입력하면, 정답 라벨은 두 번째 자리(인덱스 1)가 1.0이고 나머지는 0.0인 벡터입니다.
모델은 학습이 진행될수록 출력이 이 정답 벡터에 가까워지도록 가중치를 조정합니다.

Softmax — 점수를 확률로 바꾸는 마지막 단계

출력층의 마지막 단계인 softmax가 정확히 어떻게 동작하는지 직접 확인해봅시다.
모델이 내놓은 10개의 점수(logit)를 슬라이더로 조절하면,
softmax가 그것을 어떻게 확률 분포로 변환하는지 실시간으로 볼 수 있습니다.

특히 한 logit만 크게 키워보세요.
— 작은 차이가 지수 함수에 의해 크게 증폭되어, 한 클래스의 확률이 거의 100%가 되는 것을 볼 수 있습니다.
이게 모델이 “확신"을 표현하는 방식입니다.

트레이닝 (Training)

트레이닝은 모든 데이터를 보면서 가중치를 업데이트하는 과정입니다.

아래 데모에서 60개 데이터가 8개씩(배치) 처리되는 과정을 직접 관찰해보세요.
“다음 배치"를 누를 때마다 8개 데이터가 처리되고(= 1 스텝), 60개를 모두 훑으면 1 에포크가 완료됩니다.

  • 주황색 테두리가 지금 처리 중인 배치입니다
  • 파란색으로 변한 데이터는 이번 에포크에서 이미 본 데이터입니다
  • 상단 진행바가 끝까지 차면 1 에포크 완료 — 데이터를 섞고 다음 에포크가 시작됩니다
  • 에포크가 반복될수록 모델은 같은 데이터를 여러 번 보면서 가중치를 점점 더 정교하게 다듬습니다

몇 가지 핵심 용어를 짚고 넘어갑시다.

  • 배치(batch): 가중치를 한 번 업데이트할 때 사용하는 데이터 묶음. 보통 32, 64, 128 같은 값을 씁니다
  • 스텝(step): 한 배치를 처리하는 한 번의 업데이트. 60,000개를 batch=32로 학습하면 1 epoch 당 1,875 step
  • 에포크(epoch): 전체 데이터셋을 한 번씩 다 본 단위. epochs=10이면 60,000장을 10번 반복
1
history_1 = model_1.fit(train_images, train_labels, epochs=10)

Keras의 fit은 기본 batch size가 32입니다.
따라서 위 코드는 매 epoch마다 1,875 step을 거치게 됩니다.

테스트 (Test)

테스트는 학습 때 보지 않은 별도의 데이터로 모델의 일반화 성능을 측정합니다.

중간고사·기말고사의 문제를 미리 알려주지 않고 시험보는 것과 같습니다.

1
model_1.evaluate(test_images, test_labels)

이 점수가 곧 모델의 진짜 실력이고, 여러 모델을 같은 테스트 데이터로 평가해서 비교하면 어떤 모델이 더 나은지 판단할 수 있습니다.

모델정확도
모델1 (에포크10)60.13%
모델1 (에포크20)70.01%
모델2 (에포크10)63.27%
모델2 (에포크20)74.42%
모델3 (에포크10)65.01%
모델3 (에포크20)63.77%

여기서 흥미로운 점은 모델3의 경우 에포크를 늘렸더니 정확도가 오히려 떨어졌다는 것입니다. (65.01% → 63.77%)
이건 우연이 아니라 다음 절에서 설명할 오버피팅(overfitting) 의 전형적인 신호입니다.

예측 (Predict / Inference)

학습이 끝난 모델로 새로운 데이터의 답을 추론하는 단계입니다.

1
predictions_1 = model_1.predict(test_images)

사용자는 트레이닝 과정을 알 필요 없이, 학습된 모델만 가지고 답을 얻을 수 있습니다.
정확도와 처리 시간의 trade-off를 고려해서, 가장 적합한 모델을 골라 사용하면 됩니다.

데이터 3분할과 배치·에포크 실습
https://colab.research.google.com/drive/1L1S-SNoWoi6g_6tuLUczhhBPrTkwbRTN

Part 6 — 딥러닝은 모든 것을 해결할까?

답은 “아니오"입니다.
딥러닝이 잘못된 결과를 내거나 100%에 도달하지 못하는 데에는 여러 원인이 있습니다.

1. 오버피팅 (Overfitting)

가장 흔하면서도 중요한 한계입니다.
오버피팅은 모델이 트레이닝 데이터에 너무 잘 맞춰진 나머지,
처음 보는 데이터에 대해서는 오히려 성능이 떨어지는 현상입니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR E1["에포크가\n적을 때"] -- "학습 부족" --> U["언더피팅\n(Underfitting)"] E2["에포크가\n적당할 때"] -- "균형" --> G["좋은 일반화"] E3["에포크가\n너무 많을 때"] -- "과학습" --> O["오버피팅\n(Overfitting)"] style U fill:#ffcdd2,stroke:#c62828,stroke-width:2px,rx:10,ry:10 style G fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10 style O fill:#ffcdd2,stroke:#c62828,stroke-width:2px,rx:10,ry:10

비유로 표현하면 — 시험 공부를 할 때 기출문제만 외워서 100점 받는 학생을 상상해보세요.
기출문제는 완벽하게 푸는데, 시험에서 새로운 문제가 나오면 풀지 못합니다.
모델도 마찬가지로 트레이닝 데이터를 통째로 외워버리면, 처음 보는 테스트 데이터에서는 점수가 낮아집니다.

앞서 본 표에서 모델3가 에포크 20에서 정확도가 떨어진 것이 바로 이 사례입니다.
오버피팅을 막기 위해 여러 기법(검증 데이터로 모니터링, early stopping, dropout, regularization 등)이 사용됩니다.

오버피팅 곡선 직접 보기

아래 데모는 가상의 모델 학습 과정을 보여줍니다.
슬라이더로 에포크를 늘려가면, 트레이닝 정확도(파란색)는 계속 올라가지만,
검증 정확도(빨간색)는 어느 시점부터 오히려 떨어지기 시작합니다.

두 곡선이 갈라지는 시점이 바로 오버피팅이 시작되는 지점입니다.

이런 현상을 미리 잡아내기 위해, 학습 도중 검증 데이터에서의 정확도를 모니터링하다가
더 이상 좋아지지 않으면 학습을 멈추는 방식을 early stopping이라고 합니다.

2. 트레이닝 데이터에 의한 편향

Chicken3gg의 트윗에서 화제가 된 사례가 있습니다.
저해상도 오바마 대통령의 사진을 고해상도로 복원하는 모델(PULSE)을 사용했더니, 백인 남성의 얼굴이 나왔습니다.

이 모델은 내부적으로 StyleGAN이라는 사전학습된 얼굴 생성 모델을 사용했는데,
StyleGAN의 학습 데이터에 백인 얼굴이 압도적으로 많아서 생성 가능한 얼굴 분포 자체가 한쪽으로 치우쳐 있었습니다.

학습 데이터에 어떤 분포가 들어 있는지가 곧 모델이 만들 수 있는 결과의 분포가 됩니다.
— 데이터가 편향되면 모델도 편향됩니다.

3. 모델 구조에 의한 차이

같은 이미지 초해상도(super-resolution) 문제도 어떤 모델 구조를 쓰느냐에 따라 결과가 크게 달라집니다.

모델PSNR
Bicubic24.04 dB
SC25.58 dB
NE+LLE25.75 dB
KK27.31 dB
ANR25.90 dB
A+27.24 dB
SRCNN27.95 dB

SRCNN이 다른 방법보다 좋은 결과를 보입니다.
모델 구조를 잘 설계하는 것이 곧 성능과 직결되며,
그래서 새로운 구조를 제안하는 논문이 매년 쏟아져 나옵니다.

4. 랜덤으로 결정되는 w에 의한 차이

가장 헷갈리는 점입니다.
같은 모델을 같은 데이터로 학습해도 결과가 매번 다릅니다.

왜냐하면 가중치 w의 초기값이 랜덤으로 결정되고,
미니배치 샘플링 순서도 매번 달라지기 때문입니다. 시작점이 다르면 학습 경로도 달라지고 도착점도 달라집니다. 이 때문에 딥러닝 실험에서는
같은 설정으로 여러 번 학습해서 평균과 표준편차를 함께 보고하는 것이 일반적입니다.

그래서 100%는 왜 어려운가요?

위의 한계들이 합쳐져서, 대부분의 딥러닝 모델은 **테스트 정확도 100%**에 도달하지 못합니다.
주된 이유는 다음과 같습니다.

  • 데이터 자체에 노이즈가 있음: 사람이 봐도 7인지 1인지 헷갈리는 글씨가 존재합니다
  • 트레이닝 데이터 ≠ 테스트 데이터: 두 데이터의 분포가 완벽히 같지 않으면 일반화에 한계가 생깁니다
  • 모델 표현력의 한계: 가중치 수가 많아도 모든 패턴을 완벽히 외울 수는 없습니다
  • 오버피팅 vs 일반화의 trade-off: 트레이닝 정확도를 100%까지 올리려 하면 보통 테스트 정확도는 오히려 떨어집니다
딥러닝의 한계: 오버피팅과 랜덤 초기값 실습
https://colab.research.google.com/drive/1qKCiMinCDYhjLxGG0AZKjq0jpd9fGMl-

Part 7 — 모델은 어떻게 선택되는가

실제로 “Sonnet 4"처럼 하나의 이름으로 발표되는 모델 뒤에는 수많은 조합 실험이 숨어 있습니다.

우리가 Part 6에서 살펴본 것처럼, 모델 구조에 따라 결과가 다르고,
데이터에 따라 다르고, 랜덤 초기값에 따라서도 다릅니다.
그렇다면 실제로 모델을 만드는 팀은 이 모든 변수를 어떻게 다룰까요?

1. 모델 구조 변형

같은 “Sonnet 4"라는 프로젝트 안에서도 내부적으로는 구조가 다른 여러 버전을 만들어 봅니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR S["Sonnet 4\n프로젝트"] --> M1["mark 1\n기본 구조"] S --> M2["mark 2\n은닉층 확장"] S --> M3["mark 3\n어텐션 개선"] style S fill:#f8bbd0,stroke:#c2185b,stroke-width:2px,rx:10,ry:10 style M1 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style M2 fill:#ce93d8,stroke:#7b1fa2,stroke-width:2px,rx:10,ry:10 style M3 fill:#80cbc4,stroke:#00897b,stroke-width:2px,rx:10,ry:10
  • 은닉층 수를 바꾸거나, 어텐션 메커니즘을 개선하거나, 파라미터 수를 조절하는 식입니다
  • 어떤 구조가 이 문제에 가장 잘 맞는지는 학습시켜 보기 전까지 알 수 없습니다

2. 데이터셋 변형

같은 모델이라도 어떤 데이터로 학습하느냐에 따라 성능이 크게 달라집니다.

  • dataset_2021_2024 — 수집 범위가 넓어 다양한 패턴을 포함
  • dataset_2022_2024 — 최신 데이터에 집중하여 최근 경향에 강함

데이터를 어떻게 수집하고, 어떻게 정제(cleaning)하고, 어떤 비율로 섞느냐에 따라 서로 다른 데이터셋 버전이 만들어집니다.

3. 반복 트레이닝과 랜덤 초기값

Part 6에서 배운 것처럼, 같은 모델 + 같은 데이터라도 랜덤 초기값 때문에 매번 결과가 다릅니다.

그래서 동일한 조합(모델 구조 + 데이터셋)으로 여러 번 트레이닝합니다.

조합트레이닝 #1트레이닝 #2트레이닝 #3평균
mark_1 + DS-A86.7%90.4%88.1%88.4%
mark_1 + DS-B87.9%89.2%90.0%89.0%
mark_2 + DS-A89.5%91.1%90.3%90.3%
mark_2 + DS-B90.8%91.5%89.7%90.7%
mark_3 + DS-A90.2%92.0%91.4%91.2%
mark_3 + DS-B91.8%92.3%91.0%91.7%

한 번만 높은 점수가 나온 것은 일 수 있습니다. 평균이 안정적으로 높은 조합을 선택해야 합니다.

4. 별도의 테스트 데이터셋으로 평가

위 표의 점수는 모두 트레이닝에 사용하지 않은 별도의 테스트 데이터셋으로 측정한 것입니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR subgraph 모델구조["① 모델 구조 변형"] M1["mark 1\n(기본)"] M2["mark 2\n(은닉층 확장)"] M3["mark 3\n(어텐션 개선)"] end subgraph 데이터셋["② 데이터셋 변형"] D1["2021–2024\n데이터"] D2["2022–2024\n데이터"] end subgraph 트레이닝["③ 반복 트레이닝"] T["트레이닝 #1\n트레이닝 #2\n트레이닝 #3\n(랜덤 초기값)"] end subgraph 평가["④ 테스트·선택"] E["별도 테스트 데이터로\n평균 점수 비교"] B["🏆 최고 평균\n= 최종 모델"] end 모델구조 --> 데이터셋 --> 트레이닝 --> 평가 E --> B style M1 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style M2 fill:#ce93d8,stroke:#7b1fa2,stroke-width:2px,rx:10,ry:10 style M3 fill:#80cbc4,stroke:#00897b,stroke-width:2px,rx:10,ry:10 style D1 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style D2 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style T fill:#ffccbc,stroke:#e64a19,stroke-width:2px,rx:10,ry:10 style E fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10 style B fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,rx:10,ry:10 style 모델구조 rx:15,ry:15 style 데이터셋 rx:15,ry:15 style 트레이닝 rx:15,ry:15 style 평가 rx:15,ry:15

모든 조합의 평균 점수를 비교해서 가장 좋은 조합이 최종 모델로 발표됩니다.
위 예시에서는 mark_3 + dataset_2022_2024의 3회 트레이닝 평균이 91.7%로 최고이므로, 이 조합이 “Sonnet 4"로 공개됩니다.

5. 실제로는 더 많은 것을 시도합니다

위에서는 모델 구조 x 데이터셋 x 반복 트레이닝 3가지 축만 보았지만, 실제로는 하이퍼파라미터까지 함께 조합합니다.

  • 학습률 (learning rate): 0.001, 0.0003, 0.0001 등
  • 배치 크기 (batch size): 32, 64, 128, 256 등
  • 에포크 수 (epochs): 10, 50, 100 등
  • 드롭아웃 비율 (dropout): 0.1, 0.3, 0.5 등
  • 스케줄러 (learning rate scheduler): StepLR, CosineAnnealing 등

이 모든 것을 조합하면 시도해야 할 경우의 수는 수백~수천 가지가 됩니다.
대규모 모델의 경우 한 번 트레이닝에 수천 GPU-시간이 소요되기도 하므로,
효율적으로 좋은 조합을 찾아내는 것 자체가 중요한 연구 주제입니다.

인터랙티브 데모 — 모델 선택 과정 시뮬레이터

아래 시뮬레이터에서 3개 모델 구조 x 2개 데이터셋 x 3회 트레이닝 = 18개 조합을 직접 실행해 보세요.
각 조합의 테스트 점수를 확인하고, 평균이 가장 높은 조합이 최종 모델로 선택되는 과정을 관찰할 수 있습니다.

모델은 어떻게 선택되는가: 조합 실험 실습
https://colab.research.google.com/drive/1hSFKr_EZoi_txLexsWDxF8CX9foHafuM

Part 8 — MNIST 실습 흐름

이제 전체 딥러닝 과정을 6단계로 나누어 따라가봅시다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart TB S0["입출력 정의"] --> S1["데이터 세트\n(준비 → 처리)"] S1 --> S2["모델\n(컴파일 정의 → 모델 구조 생성)"] S2 --> S3["트레이닝, 검증\n(전처리 → 트레이닝)"] S3 --> S4["테스트\n(전처리 → 테스트)"] S4 --> S5["예측\n(전처리 → 예측)"] style S0 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style S1 fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style S2 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style S3 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style S4 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10 style S5 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10

1. 입출력 정의

먼저 모델이 무엇을 받고 무엇을 내보낼지 정해야 합니다.

  • 입력: 고정된 크기(28×28)의 숫자 이미지
  • 출력: 카테고리(0~9) 별 확률
  • 오차 측정 방식 (Loss Function): SparseCategoricalCrossentropy

2. 데이터 세트

MNIST 데이터셋에서 데이터를 준비하고 전처리합니다.

  • 70,000장의 28×28 흑백 이미지 (각 숫자별로 약 7,000장씩)
  • 60,000장은 트레이닝용, 10,000장은 테스트용으로 분리
  • 라벨은 정수(1, 2, 3, …) 형태와 원-핫 벡터([0,1,0,...]) 형태 두 가지가 가능합니다

라벨 형식과 사용할 손실 함수는 짝이 맞아야 합니다.

라벨 형식사용할 손실 함수
정수 (3)SparseCategoricalCrossentropy
원-핫 ([0,0,0,1,0,...])CategoricalCrossentropy

이 강의에서는 정수 라벨을 그대로 사용하므로 SparseCategoricalCrossentropy를 씁니다.

3. 모델

3-1. 컴파일 정의

1
2
3
4
5
model_1.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)
인자의미
optimizer가중치를 변화시키는 전략. adam, sgd, rmsprop
loss오차를 계산하는 방법. 반드시 미분 가능해야 합니다
metrics사람이 보고 평가할 지표. 분류 문제에서는 보통 accuracy를 씁니다

lossmetrics의 차이를 짚고 넘어갑시다.

loss는 모델 학습에 직접 쓰이기 때문에 미분 가능해야 합니다. (역전파를 위해)
metrics는 사람이 모델 성능을 직관적으로 평가하기 위한 지표이며, 미분 가능할 필요가 없습니다.
예를 들어 정확도(accuracy)는 “맞춘 개수 / 전체 개수"로 미분이 안 되지만,
사람이 이해하기 가장 쉬운 지표라서 metrics로 자주 씁니다.

3-2. 예제 1 — Dense만 있는 모델

1
2
3
4
5
def mnist_model_1(image_size=(28, 28)):
    input_1 = tf.keras.layers.Input(image_size)
    flatten_1 = tf.keras.layers.Flatten()(input_1)
    dense_1 = tf.keras.layers.Dense(10, activation="softmax")(flatten_1)
    return tf.keras.models.Model(inputs=[input_1], outputs=[dense_1])
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR I["Input\n(28×28)"] --> F["Flatten\n(784)"] --> D["Dense\nunits=10\nsoftmax"] --> O["Output\n(10)"] style I fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style F fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style D fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10 style O fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10
  • Input: 28×28 모양의 이미지 한 장
  • Flatten: 28×28을 784개의 1차원 벡터로 펼침
  • Dense (10, softmax): 784개 입력을 10개의 출력으로 변환, softmax로 확률화
  • 가중치 개수: 784 × 10 + 10(편향) = 7,850개

참고: 실제로 학습 시에는 한 장씩이 아니라 여러 장(예: 32장)을 묶은 배치 단위로 처리되므로,
텐서 모양은 (batch, 28, 28)(batch, 784)(batch, 10) 으로 흐릅니다.

3-3. 예제 2 — Conv를 활용한 모델

이미지의 공간적 특성을 활용하기 위해 Conv2D를 추가한 모델입니다.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e3f2fd','primaryBorderColor':'#90caf9','lineColor':'#546e7a','textColor':'#333','mainBkg':'#fafafa','nodeBorder':'#90a4ae','clusterBkg':'#f5f5f5','clusterBorder':'#bdbdbd'}}}%% flowchart LR I["Input\n28×28×1"] --> C1["Conv2D\n32, 3×3\nrelu"] C1 --> P1["MaxPool2D\n2×2"] P1 --> C2["Conv2D\n64, 3×3\nrelu"] C2 --> P2["MaxPool2D\n2×2"] P2 --> F["Flatten"] F --> D["Dense\nsoftmax"] D --> O["Output\n10"] style I fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10 style C1 fill:#b3e5fc,stroke:#0288d1,stroke-width:2px,rx:10,ry:10 style C2 fill:#b3e5fc,stroke:#0288d1,stroke-width:2px,rx:10,ry:10 style P1 fill:#f8bbd0,stroke:#c2185b,stroke-width:2px,rx:10,ry:10 style P2 fill:#f8bbd0,stroke:#c2185b,stroke-width:2px,rx:10,ry:10 style F fill:#fff9c4,stroke:#f9a825,stroke-width:2px,rx:10,ry:10 style D fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,rx:10,ry:10 style O fill:#bbdefb,stroke:#1976d2,stroke-width:2px,rx:10,ry:10

Dense 레이어는 28×28 픽셀을 모두 1차원으로 펼쳐서 처리하기 때문에, 픽셀 사이의 공간적 위치 관계를 잃어버립니다.
반면 Conv2D는 작은 윈도우(예: 3×3)를 이미지 위에서 슬라이딩시키며 지역적 특징(테두리, 곡선 등)을 추출하기 때문에,
이미지의 공간적 구조를 잘 활용합니다.
같은 데이터셋이라도 Dense만 쓴 모델보다 정확도가 더 높게 나오는 것이 보통입니다.

4. 트레이닝 및 검증

1
2
3
4
5
history_1 = model_1.fit(
    train_images, train_labels,
    epochs=10,
    validation_split=0.1  # 트레이닝 데이터의 10%를 검증용으로 사용
)

validation_split을 지정하면, 트레이닝 데이터의 일부를 떼어서 학습에는 사용하지 않고,
매 에포크 끝에 별도로 평가합니다.

이렇게 얻은 검증 정확도가 떨어지기 시작하면 오버피팅이 의심됩니다.

입력 정규화 (Normalization)

트레이닝 시작 전에 입력값을 0~1 범위로 정규화하는 것이 중요합니다.

1
2
train_images = train_images / 255.0
test_images = test_images / 255.0

왜 정규화해야 할까요? 픽셀값(0~255)을 그대로 넣으면 가중합 결과가 매우 큰 값이 되고,
이로 인해 손실 함수의 기울기(gradient)가 너무 가파르거나 너무 작아져서 학습이 불안정해집니다.
입력 스케일을 작게 맞춰주면 가중치 업데이트가 안정적으로 이루어집니다.

트레이닝의 내부 동작

배치 하나에 대해 다음 과정이 반복됩니다.

  1. 순전파(Forward): 입력 → Flatten → Dense → softmax → 확률 출력
  2. 손실 계산(Loss): 정답(예: 3)과 예측 확률 분포의 차이를 계산
  3. 역전파(Backward): 각 가중치가 손실에 얼마나 기여했는지 미분으로 계산
  4. 가중치 업데이트(Optimizer step): 옵티마이저가 학습률만큼 가중치를 조정

이 과정을 60,000장 모두에 대해 반복하면 1 에포크가 완료됩니다.
epochs=10 이면 이걸 10번 반복합니다.
매 에포크마다 손실은 점점 줄어들고, 정확도는 점점 올라갑니다. (학습이 정상적으로 진행되는 경우)

Forward → Loss → Backward → Update 직접 보기

아래 데모에서 이 4단계를 직접 관찰해보세요.
“1 Batch” 버튼을 누르면 상단의 ①②③④ 단계가 순차적으로 활성화되면서,
결정 경계(점선)가 한 스텝 이동합니다.

  • 주황 테두리: 현재 배치에 해당하는 데이터
  • 주황 테두리 + 반대색 영역: 분류가 틀린 데이터
  • “자동 학습"을 켜면 Loss 곡선이 내려가고 Accuracy가 오르는 것을 볼 수 있습니다

트레이닝 로그 예시

1
2
3
4
5
6
7
Epoch 1/10
1875/1875 [===] - 6s 3ms/step - loss: 0.4980 - accuracy: 0.8246
Epoch 2/10
1875/1875 [===] - 5s 3ms/step - loss: 0.3739 - accuracy: 0.8650
...
Epoch 10/10
1875/1875 [===] - 5s 2ms/step - loss: 0.2368 - accuracy: 0.9116

여기서 1875/1875는 한 에포크당 1,875 step을 거친다는 뜻입니다.
— 60,000장을 batch size 32로 나눈 값이지요. 10 에포크를 마쳤을 때 트레이닝 정확도가 91.16%에 도달했습니다.
가중치가 랜덤으로 초기화되고 처음 값에 따라 업데이트되므로,
같은 코드를 다시 실행하면 결과는 미묘하게 달라집니다.

또한 데이터가 옷 분류(FashionMNIST) 등으로 어려워지면, 같은 모델이라도 정확도가 떨어질 수 있습니다.

5. 테스트

1
model_1.evaluate(test_images, test_labels)

별도의 테스트 데이터로 모델을 평가합니다.

중요: 트레이닝 시와 동일한 형태의 전처리(예: /255.0)를 적용해야 합니다.
그렇지 않으면 모델이 학습 때 본 입력 분포와 달라져서, 점수가 비정상적으로 낮게 나옵니다.

여러 모델/에포크 조합을 비교하면 다음과 같은 표를 얻을 수 있습니다.

모델정확도
예제1 model_1 (epoch 10)90.2%
예제1 model_2 (epoch 10)89.7%
예제1 model_2 (epoch 20)91.3%
예제2 model_45 (epoch 50)95.5%

6. 예측

1
predictions_1 = model_1.predict(test_images)

실제 데이터에 대해 예측합니다. 마찬가지로 트레이닝 시와 동일한 전처리가 필요합니다.

예측 결과는 길이 10인 확률 벡터입니다. 가장 높은 확률을 가진 인덱스가 모델의 답이 됩니다.

1
2
import numpy as np
predicted_labels = np.argmax(predictions_1, axis=1)

사용자는 트레이닝 과정에 대해 알 필요가 없으며,
정확도와 처리 시간의 trade-off를 통해 가장 적합한 모델을 사용하면 됩니다.

정확도가 가장 높은 모델이라고 항상 좋은 것은 아닙니다.
— 응답 속도, 메모리 사용량, 배포 환경도 함께 고려해야 합니다.

MNIST 실습: 전체 딥러닝 파이프라인 (PyTorch)
https://colab.research.google.com/drive/1Qtn0_2-d_yRRKpjWrNMuygeHHdp-MRcU

마치며

이 강의에서 다룬 내용을 한 줄로 요약하면 이렇습니다.

딥러닝은 무한대의 변수가 필요해 보이는 문제를, 유한한 가중치 w로 근사해서 풀어내는 기술입니다.
w를 데이터로부터 자동으로 학습하기 때문에, 사람이 일일이 알고리즘을 짜지 않아도 됩니다.

물론 딥러닝은 만능이 아닙니다.
오버피팅, 데이터 편향, 모델 구조의 한계, 랜덤 초기값 등 여러 변수에 의해 결과가 달라집니다.
하지만 이 한계를 이해하고 사용한다면,
알고리즘만으로는 풀 수 없는 많은 문제들을 해결할 수 있는 강력한 도구가 됩니다.

다음 단계로 추천드리는 학습 주제는 다음과 같습니다.

  • CNN 심화: Conv·Pool·Padding·Stride의 원리, ResNet 같은 깊은 구조
  • RNN/LSTM: 시계열 데이터 처리
  • Transformer: 현재 LLM의 기반이 되는 구조
  • LMM: 텍스트를 넘어선 멀티모달 모델