학습이란 무엇인가: 데이터에 함수 맞추기
📍 현재 위치: 제III부 · 처음부터 학습하기 — 8장. 고정점은 더 이상 새로운 것이 나오지 않을 때까지 규칙을 실행함으로써 우리의 규칙을 47개 원자로 이루어진 모델로 닫았습니다. 이제 우리는 규칙집을 내려놓고, 대신 예제로부터 곧바로 매핑을 학습합니다.
지금까지 모든 장은 같은 방식으로 답을 얻어냈습니다. 사실과 규칙을 적어놓고, 엔진이 논리적으로 따라 나오는 모든 것을 끝까지 계산하게 하는 방식이었습니다. 7장은 이 발상을 극한까지 밀어붙였습니다 — 고정점(fixpoint), 즉 규칙을 한 번 더 실행해도 아무것도 바뀌지 않는 상태가 우리의 23개 기초 사실을 47개 원자로 이루어진 모델로 닫아버렸습니다. 이것이 지능적이 되는 하나의 완전한 방법입니다: 답을 도출하는 것입니다. 이번 장은 또 다른 방법을 소개합니다. 무엇이 따라 나오는지 명시하는 규칙 대신, 우리에게는 예제(examples) — 우리가 원하는 답과 짝지어진 입력들 — 가 주어지고, 우리는 기계의 추측이 그 답과 일치할 때까지 숫자 더미를 조정합니다. "교수"에 대한 규칙은 어디에도 적혀 있지 않습니다. 기계가 데이터로부터 그것을 읽어냅니다.
오래된 라디오의 다이얼을 돌리는 상황을 상상해 보십시오. 방송국의 주파수를 모르는 채로, 그저 다이얼을 돌리며 소리를 듣습니다. 음악이 더 또렷해지면 그 방향으로 계속 돌리고, 더 나빠지면 반대로 돌립니다. 공식으로부터 주파수를 도출하는 것이 아니라, 신호가 가장 좋아질 때까지 조정하는 것입니다. 머신러닝은 이 다이얼을 확장한 것입니다: 다이얼은 어떤 공식 안에 들어 있는 숫자들이고, "가장 좋다"는 것은 추측이 얼마나 틀렸는지로 측정되며, 다이얼을 돌리는 일은 자동으로, 한 번에 조금씩, 항상 잡음을 줄이는 방향으로 이루어집니다.
이 장에서 다루는 내용
- 도출에서 맞춤으로의 전환 — 논리는 규칙을 건네주고 그 귀결을 계산해 줍니다; 지도 학습(supervised learning)은 특징에서 레이블로(features to a label) 이어지는 예제를 건네주고 그 규칙을 복원하라고 요구합니다.
- 모델, 손실, 그리고 최소화 — 모델(model)은 파라미터를 가진 함수이고, 손실(loss)은 그 모델이 예제들에서 얼마나 틀렸는지를 측정하며, 학습이란 그 손실을 작게 만드는 것에 지나지 않습니다. 모든 기호는 사용되기 전에 먼저 풀어서 설명합니다.
- 직선 위에서 유도하는 경사 하강법 — 평균제곱오차의 그래디언트를 연쇄 법칙으로 한 단계씩 유도하고, 이를 함께 제공되는 코드와 줄 단위로 대응시킨 다음 손으로 직접 검산합니다: 첫 번째 스텝은 프로그램이 말하는 지점에 정확히 도달합니다.
- 두 개의 완전한 수치 추적 — 직선 맞춤이 에포크마다 손실 44.182에서 0.0214로 떨어지는 과정, 그리고 로지스틱 분류기의 손실이 ln 2에서 0.010으로 떨어지는 과정을, 사람별 점수 및 확률 표와 함께 보여줍니다.
- 학술 세계에 적용한 로지스틱 회귀 —
advises관계에서 곧바로 읽어낸 정직한 두 숫자만으로 교수와 학생을 구분해 다섯 명 모두를 맞히는 분류기, 그리고 그 결정 경계가 왜 반드시 기울어져야 하는지. - 일반화 대 암기, 얕은 이유로 옳았던 것의 정직한 함정, 그리고 학습의 세 갈래.
도출에서 맞춤으로
논리에서는 진행 방향이 고정되어 있습니다: 규칙과 사실이 답을 산출합니다. 엔진에 advises(alice, bob)과 "교수가 학생을 지도한다"는 규칙을 주면, 엔진은 뒤따르는 것을 도출할 뿐 규칙 자체에 의문을 품지 않습니다. 학습은 그 화살표를 거꾸로 돌립니다. 우리에게 주어지는 것은 오직 답뿐입니다 — kb.py(40–44행)에서 이미 교수 또는 학생으로 표시되어 있는 다섯 사람과, 그들에 대해 측정 가능한 몇 가지 특징(feature)입니다 — 그리고 우리는 규칙이라면 부호화했을 특징에서 레이블로의 매핑을 복원해내야 합니다. 이것이 지도 학습(supervised learning)입니다: 모든 훈련 예제는 정답과 짝지어진 입력이며, 목표는 그 출력들을 재현하고, 바라건대 한 번도 본 적 없는 입력으로까지 확장되는 함수입니다. "지도(supervised)"라는 단어는 문자 그대로의 의미입니다 — 교사는 이미 모든 예제에 올바른 답을 표시해 두었고, 학습자가 할 일은 오직 나중에 그 교사를 대신할 수 있을 만큼 충분히 잘 흉내 내는 것뿐입니다 [1].
짝이 되는 모듈 examples/logic/learning.py는 자신의 독스트링(docstring, 3–7행)에서 이 대조를 명시적으로 드러냅니다: 1권의 논리 모듈들은 규칙으로부터 결론을 도출하지만, 이 모듈은 "예제에서 시작해 예측이 맞아떨어질 때까지 숫자를 조정한다"고 말합니다. 같은 학술 세계, 정반대의 엔진입니다. 고정점 장에서 나온 23개의 기초 사실과 47개 원자로 이루어진 폐포(closure)는 여전히 우리의 정답 근거(ground truth)입니다; 여기서 우리는 그 세계의 한 조각(누가 교수인지)을 연역하는 대신, 단서로부터 재구성해내라고 숫자 더미에게 요구합니다.
모델, 손실, 그리고 우리가 최소화하는 것
세 가지 개념이 모든 일을 해내므로, 기호를 만나기 전에 먼저 이들을 있는 그대로 살펴봅시다.
모델(model)은 파라미터를 가진 함수(parametric function)입니다: 고정된 형태에 조절 가능한 손잡이들이 달려 있는 것입니다. 가장 단순한 예는 직선입니다. 입력(하나의 실수)을 로, 모델이 산출하는 출력을 로 쓰고, 두 손잡이를 (가중치(weight), 즉 기울기)와 (편향(bias), 즉 절편)라 부르면, 모델은
위의 모자(hat)는 이것이 모델의 추측임을 나타내며, 우리가 맞추려는 참값 와 구별하기 위한 표시입니다. 형태 — 직선이라는 것 — 는 우리가 정한 것이고, 손잡이 와 가 바로 파라미터(parameter)이며, 학습이 조정하는 대상은 바로 이것입니다. 의 설정이 달라지면 서로 다른 직선이 나오며, 학습이란 가장 잘 맞는 쌍을 찾는 탐색입니다. 어떤 모델이든 조정 가능한 모든 숫자를 하나의 목록으로 모아 파라미터 벡터(parameter vector) (그리스 문자 세타)로 쓰면 편리합니다. 여기서 는 파라미터의 개수를 세며, 직선의 경우 이고 입니다.
"가장 좋음"에는 숫자가 필요하며, 그 숫자가 바로 손실(loss)입니다: 모델이 틀렸을 때 커지고 맞았을 때 작아지는, 0 이상인 단 하나의 값이며, 0이면 완벽하다는 뜻입니다. 직선을 맞추는 데 있어 자연스러운 선택은 평균제곱오차(mean squared error, MSE)입니다. 개의 훈련 예제가 있고 가 부터 까지 훑는다고 합시다; 예제 는 입력과 그 참값의 쌍 입니다. 그러면
기호를 하나씩 풀어봅시다. 아래첨자 는 하나의 예제를 골라내므로, 는 번째 입력이고 는 그 참값입니다. 대문자 시그마 은 "다음 식을 부터 까지 모두 더하라"는 뜻입니다. 안쪽의 는 예제 에서 모델의 추측에서 참값을 뺀 것, 즉 그 지점에서의 오차이며, 이를 잔차(residual) 라 부릅니다. 이를 제곱한 은 넘치는 것과 모자라는 것 모두를 양의 오차로 셈하게 하고, 큰 실수를 작은 실수보다 훨씬 무겁게 벌합니다. 으로 나누는 것은 예제들에 대해 평균을 내는 것이므로, 손실이 데이터셋의 크기에 따라 단순히 커지지 않도록 해 줍니다. 손실이 0이라는 것은 직선이 모든 점을 정확히 통과한다는 뜻입니다.
이 두 요소만 있으면, 학습은 그저 최소화(minimization)일 뿐입니다: 손실을 가능한 한 작게 만드는 파라미터를 찾는 것입니다. 이것이 게임의 전부입니다 — 모델을 고르고, 손실을 정의하고, 내려가는 것입니다 [2]. 이 장의 나머지는 "내려간다"는 것이 무엇을 의미하는지, 그리고 그것이 제대로 작동할 때와 조용히 속임수를 쓰고 있을 때 각각 어떤 느낌인지에 관한 이야기입니다.
하나의 그림으로 보는 학습: 자신이 얼마나 틀렸는지를 정의하고(왼쪽, 손실 계곡), 직선이 점들에 맞아떨어지거나(가운데) 경계가 두 부류를 분리할 때까지(오른쪽) 파라미터를 내리막으로 이동시킵니다.
저자가 AI의 도움을 받아 직접 제작한 원본 도해.
내리막 걷기: 경사 하강법
답을 미리 알지 못한 채 어떻게 손실을 최소화할 수 있을까요? 우리는 손실 자체의 기울기(slope)를 이용합니다. 한 변수의 함수라면 어떤 점에서의 기울기는 그 도함수(derivative)입니다; 여러 파라미터를 가진 함수라면, 다른 파라미터들을 고정한 채 특정 하나의 파라미터 방향으로의 기울기는 편도함수(partial derivative)이며, 곧은 대신 둥근 로 씁니다. 모든 파라미터에 대한 편도함수들을 하나의 벡터로 모으면 기울기(gradient)가 되며, 이는 로 쓰고(거꾸로 된 삼각형 는 "델(del)" 또는 "그래드(grad)"라고 읽습니다):
기울기는 손실이 가장 빠르게 커지는 방향을 가리키므로, 그 반대 방향으로 이동하면 손실이 가장 빠르게 줄어듭니다. 경사 하강법(gradient descent)이란 이 걸음을 계속해서 반복하는 것입니다: 기울기를 측정하고, 모든 파라미터를 그에 반대되는 방향으로 조금씩 옮기고, 이를 반복합니다. 업데이트 규칙은, 화살표 가 "왼쪽을 오른쪽으로 대체하라"는 뜻일 때,
여기서 (그리스 문자 에타)는 학습률(learning rate)로, 매 스텝마다 얼마나 멀리 이동할지를 정하는 작은 양수이며, 데이터 전체를 한 번 훑는 것을 에포크(epoch)라 합니다. 음의 기울기가 왜 가장 빠르게 내려가는 길인지, 그리고 스텝이 넘쳐서 발산하기 전까지 가 얼마나 커도 되는지는 다음 장이 열어젖힐 바로 그 질문들입니다; 여기서는 그 방향을 일단 믿고 그것이 작동하는 모습을 지켜보며, 코드를 읽는 데 필요한 단 하나의 기울기만을 유도합니다.
직선의 경우 그 기울기는 두 개의 편도함수이며, 연쇄 법칙(chain rule)이 이를 깔끔하게 내어줍니다. 잔차를 로 쓰면 가 됩니다. 연쇄 법칙은 합성된 식의 도함수가 바깥쪽의 도함수 곱하기 안쪽의 도함수라고 말합니다. 바깥쪽은 제곱이고 그 도함수는 입니다; 안쪽은 이며, 에 대한 그 도함수는 입니다(왜냐하면 는 항 를 통해서만 를 포함하기 때문입니다), 에 대한 도함수는 입니다. 이 조각들을 항별로 합치면:
이 두 공식이 바로 코드 그 자체입니다. examples/logic/learning.py의 fit_line(22–37행)에서 그대로 가져온 이 선형 학습기(linear learner) 전체는, 그 작동 방식을 라이브러리 속에 아무것도 숨기지 않고 순수 파이썬(Python)으로 유지합니다:
def fit_line(xs, ys, lr: float = 0.02, epochs: int = 2000):
"""Return (w, b, loss_start, loss_end) for the least-squares line through
the data, found by gradient descent on the mean squared error."""
w, b = 0.0, 0.0
n = len(xs)
def mse(w, b):
return sum((w * x + b - y) ** 2 for x, y in zip(xs, ys)) / n
loss_start = mse(w, b)
for _ in range(epochs):
dw = sum(2 * (w * x + b - y) * x for x, y in zip(xs, ys)) / n
db = sum(2 * (w * x + b - y) for x, y in zip(xs, ys)) / n
w -= lr * dw
b -= lr * db
return w, b, loss_start, mse(w, b)
33번째 줄 dw = sum(2 * (w * x + b - y) * x for ...) / n은 를 기호 그대로 옮겨 적은 것입니다; 34번째 줄은 입니다; 그리고 35–36번째 줄인 w -= lr * dw와 b -= lr * db는 업데이트 규칙 을 두 좌표로 나눈 것이며, lr이 의 역할을 합니다. 어떤 미적분도 라이브러리 속에 숨어 있지 않습니다; 이 반복문 자체가 곧 앞서 적은 공식들입니다.
첫 번째 스텝을 손으로 확인하기
코드가 시작하는 지점, 즉 에서 시작하는 바로 그 첫 번째 스텝을 지켜봅시다. 그러면 모든 잔차는 이므로, 시작 손실은 그저 답들의 제곱의 평균일 뿐입니다:
이는 loss_start와 정확히 일치합니다. 파일 안의 다섯 점은 와 로, 직선 에 바짝 붙어 있습니다. 원점에서의 두 기울기 조각은
둘 다 음수이며, 이는 와 가 커질수록 손실이 줄어든다는 뜻이므로, 경사 하강법은 이 둘을 키우게 됩니다. 일 때 첫 번째 업데이트는
이는 프로그램이 1회 에포크 이후에 갖는 와 정확히 일치합니다. 단 한 번의 스텝만으로도 기울기는 이미 에서 목표값인 대략 까지 가는 길의 대부분을 끌어당겼습니다. 전체 실행은 계속되며, 손실은 단조롭게 떨어집니다:
| 에포크 | 손실 | ||
|---|---|---|---|
| 0 | 0.00000 | 0.00000 | 44.18200 |
| 1 | 0.88160 | 0.24080 | 12.30296 |
| 2 | 1.34640 | 0.36618 | 3.45685 |
| 5 | 1.78989 | 0.47905 | 0.13083 |
| 10 | 1.86646 | 0.48410 | 0.05595 |
| 100 | 1.92458 | 0.28618 | 0.03155 |
| 500 | 1.98566 | 0.06565 | 0.02144 |
| 2000 | 1.99000 | 0.05000 | 0.02140 |
2000번째 에포크에서 프로그램은 다음을 출력합니다 [2]:
line: y = 1.990 x + 0.050 (loss 44.182 -> 0.0214)
손실은 44.182에서 0.0214로 떨어졌고, 복원된 기울기는 1.990, 절편은 0.050입니다: 에서 시작해 잡음 섞인 다섯 개의 점만을 본 학습기가, 그 점들을 만들어낸 대략 2라는 기울기를 스스로 재발견해낸 것입니다. 이것이 바로 축소판으로 본, 데이터에 함수를 맞추는 일입니다: 직선에 대한 공식은 전혀 주어지지 않았고, 오직 데이터와 "덜 틀리게 하라"는 지시만 있었을 뿐입니다. 손실이 0에 도달하지 않는다는 점에도 주목하십시오. 점들에 잡음이 섞여 있어 어떤 직선도 다섯 점 모두를 정확히 꿰지는 못하기 때문에 손실은 에 자리를 잡습니다; 경사 하강법은 완벽한 직선이 아니라 가장 좋은 직선을 찾아내며, 남은 손실은 그 직선이 설명해낼 수 없는 잡음입니다.
교수와 학생 분류하기
연속적인 숫자를 예측하는 것은 회귀(regression)입니다. 이산적인 레이블 — 교수 또는 학생 — 을 예측하는 것은 분류(classification)이며, 그 주력 도구가 로지스틱 회귀(logistic regression)입니다. 요령은 선형 부분은 그대로 두되, 이를 압착 함수(squashing function)에 통과시켜 출력이 확률처럼 읽히게 만드는 것입니다. 이제 와 는 더 이상 단일 숫자가 아니라 짧은 리스트입니다 — 특징마다 가중치 하나, 사람마다 특징 값 하나 — 그리고 의 가운데 점은 내적(dot product)으로, 각 가중치를 그에 대응하는 특징에 곱한 뒤 그 결과들을 더하는 것이어서, 특징이 두 개일 때 가 됩니다. 선형 점수, 즉 로짓(logit)은 입니다. 압착 함수는 시그모이드(sigmoid) (그리스 문자 시그마)로, 어떤 실수든 받아 과 사이의 열린 구간으로 구부려 넣습니다:
여기서 은 오일러 수(Euler's number)이고 는 지수 함수(exponential function)로, 를 제곱한 것입니다; 물결 모양의 는 "근사적으로 같다"고 읽습니다. 는 항상 양수이므로 분모 는 항상 보다 크고, 따라서 그 분수는 엄밀하게 과 사이에 머무릅니다. 바로 이것이 그 출력을 확률로 읽을 수 있는 이유입니다: 즉, 레이블이 (교수)일 모델의 추정 확률입니다. 큰 양수 는 를 쪽으로 몰아 출력을 쪽으로 이끕니다("거의 확실히 교수"); 큰 음수 는 그것을 쪽으로 몰아갑니다("거의 확실히 학생"); 그리고 은 정확히 을 주는데, 이는 둘 사이의 경계선입니다. learning.py(41–46행)의 sigmoid 코드는 이 공식에 수치적 안전장치를 더한 것입니다:
def sigmoid(z: float) -> float:
if z < -60:
return 0.0
if z > 60:
return 1.0
return 1.0 / (1.0 + math.exp(-z))
앞의 두 개의 이른 반환(early return)은 안전망입니다: 가 대략 을 넘어서면 시그모이드는 이미 또는 과 구별할 수 없으며, 그 지점에서 math.exp(-z)를 계산하면 부동소수점(floating-point) 범위를 넘쳐버리므로, 극한값을 곧바로 돌려줍니다.
참인 레이블 는 또는 이며(기호로는 이라 쓰고, 여기서 은 "~의 원소이다"라고 읽습니다), 손실은 더 이상 제곱 오차가 아니라 이진 교차 엔트로피(binary cross-entropy, BCE)입니다:
여기서 은 자연로그(natural logarithm)로, 지수 함수의 역함수입니다: 이고, 과 사이의 숫자에 대한 은 음수이며, 그 숫자가 에 가까워질수록 한없이 를 향해 커집니다. 이 손실은 올바른 부류에 부여된 확률에 대한 벌점으로 읽으면 됩니다. 이면 첫 번째 항만 남아 가 되며, 이는 일 때 이고 일수록 폭발합니다; 이면 두 번째 항만 남아 가 되는데, 이는 그 거울상입니다. 이 공식은 개별 예제 에 대한 손실 를 주며, 훈련 반복문이 추적하는 숫자는 개 예제 전체에 대한 그 평균, 입니다 — MSE가 제곱된 잔차들을 평균 냈던 것과 정확히 같은 방식이며, 아래 표들의 손실 열을 채우는 것도 바로 이 평균 입니다. 그러므로 확신에 차 있지만 틀린 추측(참인 부류에 대한 확률이 에 가까운 경우)은 그 로그를 거대한 손실로 폭발시키는 반면, 확신에 차 있으면서 맞은 추측은 손실을 을 향해 몰아갑니다. 확신에 찬 오답을 망설이는 오답보다 훨씬 더 가혹하게 벌하는 이 비대칭성이야말로 목표가 예/아니오 레이블일 때 알맞은 형태이며, 이것이 제곱 오차가 아니라 교차 엔트로피가 시그모이드의 표준 짝인 이유입니다 [2].
시작 손실은 정확히 못 박을 수 있습니다. 분류기는 에서 시작하므로, 모든 로짓은 이고 모든 예측은 입니다: 훈련되지 않은 뉴런은 모두에 대해 최대한 확신이 없는 상태입니다. 그러면 각 예제는 만큼을 기여하므로, 평균 손실은 이며, 이는 _bce가 에서 정확히 돌려주는 값입니다: 훈련 반복문이 시작하는 0번째 에포크의 손실입니다. fit_logistic(67–85행) 훈련 반복문은 다섯 사람 전체에 대해 기울기를 누적하고 에포크마다 한 번씩 스텝을 밟습니다:
for xi, yi in zip(X, y):
p = sigmoid(sum(wj * xj for wj, xj in zip(w, xi)) + b)
err = p - yi
for j in range(dim):
gw[j] += err * xi[j]
gb += err
w = [wj - lr * gj / n for wj, gj in zip(w, gw)]
b -= lr * gb / n
기억해 둘 단 하나의 줄은 err = p - yi(79행)입니다: 시그모이드를 거친 교차 엔트로피의 기울기는 예측 확률에서 참값을 뺀 깔끔한 오차 신호(error signal) 로 무너져 내리며, 파라미터별 기울기는 그저 이 오차에 특징을 곱한 것, 즉 입니다. 81행 gw[j] += err * xi[j]는 이 곱을 배치 전체에 대해 합한 것이고, 82행 gb += err는 편향에 대한 기울기입니다. 의 지저분한 도함수가 왜 이토록 깔끔한 것으로 무너지는지는 다음 장이 온전히 유도해낼 작은 기적입니다; 지금은 그 결과를 받아들이고 그것이 마땅히 움직여야 할 방향으로 움직이는지만 확인합니다.
advises 관계에서 특징 읽어내기
이제 정직하게 밝힐 부분입니다: 분류기는 실제로 무엇을 보고 있을까요? 진행 예시(running example)의 advises 사실들로부터 곧이곧대로 읽어낸 사람당 두 개의 숫자이며, advise_features(49–64행)가 59행에서 진출차수를, 60행에서 진입차수를 셉니다:
def advise_features():
"""Two features per person, straight from the advises relation:
[out-degree = how many you advise, in-degree = how many advise you].
Label 1 = professor, 0 = student. Neither feature alone separates the two
groups, so the classifier leans on both."""
people = ["alice", "bob", "carol", "dave", "erin"]
out_deg = {p: 0 for p in people}
in_deg = {p: 0 for p in people}
for f in FACTS:
if f[0] == "advises":
out_deg[f[1]] += 1
in_deg[f[2]] += 1
profs = {"alice", "bob"}
X = [[float(out_deg[p]), float(in_deg[p])] for p in people]
y = [1 if p in profs else 0 for p in people]
return people, X, y
진출차수(out-degree) 은 자신이 몇 명을 지도하는지를 세고, 진입차수(in-degree) 는 몇 명이 자신을 지도하는지를 셉니다. 하나의 특징 값을 개별적으로 지칭해야 할 때는 예제 의 특징 를 로 쓰므로, 은 사람 의 진출차수이고 는 그 사람의 진입차수입니다. kb.py(47–50행)에는 정확히 네 개의 advises 사실이 있습니다: alice→bob, bob→carol, bob→dave, carol→erin. 이를 손으로 집계하면, 각 사실은 지도하는 사람의 진출차수에 1을 더하고 지도받는 사람의 진입차수에 1을 더합니다:
| 사람 | 진출차수 | 진입차수 | 레이블 |
|---|---|---|---|
| alice | 1 (bob을 지도) | 0 (아무에게도 지도받지 않음) | 교수 (1) |
| bob | 2 (carol, dave를 지도) | 1 (alice에게 지도받음) | 교수 (1) |
| carol | 1 (erin을 지도) | 1 (bob에게 지도받음) | 학생 (0) |
| dave | 0 | 1 (bob에게 지도받음) | 학생 (0) |
| erin | 0 | 1 (carol에게 지도받음) | 학생 (0) |
이것이 바로 코드가 만들어내는 표입니다. 이제 각 사람은 두 개의 숫자로 이루어진 특징 공간(feature space)의 한 점이 되며, 레이블은 61행의 profs 집합(alice와 bob)에서 나오는데, 이는 kb.py의 기초 professor 사실과 일치합니다. 벌써 눈에 띄는 것은, 어떤 단일 특징도 모두를 갈라내지 못한다는 점입니다: alice와 carol은 을 공유하지만 레이블이 반대이고, bob과 carol은 을 공유하지만 역시 레이블이 반대입니다. 올바른 규칙이라면 반드시 두 숫자를 결합해야 합니다.
학습 결과와 결정 경계
기본값(, 에포크)으로 fit_logistic을 실행하면, 손실은 에서 시작해 을 향해 떨어집니다:
| 에포크 | 손실 | |||
|---|---|---|---|---|
| 0 | 0.0000 | 0.0000 | 0.0000 | 0.69315 |
| 1 | 0.0600 | −0.0600 | −0.0300 | 0.66662 |
| 10 | 0.5464 | −0.5043 | −0.2257 | 0.49320 |
| 100 | 2.4575 | −2.4546 | −0.8210 | 0.15900 |
| 1000 | 5.7214 | −6.3606 | −1.9076 | 0.02774 |
| 3000 | 7.7549 | −8.5903 | −2.7587 | 0.00987 |
에포크 1의 행은 손으로 확인해 볼 가치가 있으며, 오차 신호 규칙을 확인시켜 줍니다. 원점에서는 모든 이므로, 각 사람의 오차는 입니다: 즉 두 교수에 대해서는 이고 세 학생에 대해서는 입니다. 배치 전체에 대해 가중치 기울기를 합하면 이고 이며, 편향의 합은 입니다. 로 평균을 내고 으로 스텝을 밟으면 , , 이 되며 — 이는 정확히 에포크 1의 행과 같습니다. 벌써 그 방향은 옳습니다: 진출차수 가중치는 올라갔고(많은 사람을 지도하는 것은 교수의 증거입니다) 진입차수 가중치는 내려갔습니다(지도받는 것은 학생의 증거입니다).
에포크 3000에서 프로그램은 다음을 출력합니다:
logistic: weights=[7.75, -8.59] bias=-2.76 loss=0.010
alice label=prof pred=prof
bob label=prof pred=prof
carol label=student pred=student
dave label=student pred=student
erin label=student pred=student
다섯 명 전원이 올바르게 분류되었고, 손실은 0.010까지 낮아졌습니다. 학습된 규칙은 가중치에서 읽어낼 수 있습니다: 진출차수는 강한 양의 가중치(+7.75, 사람들을 지도하는 것은 교수 쪽을 가리킵니다)를 얻었고, 진입차수는 강한 음의 가중치(−8.59, 지도받는 것은 학생 쪽을 강하게 가리킵니다)를 얻었습니다. 모델은 한 번도 알려준 적 없는 규칙을 발견해낸 것이며, 이는 논리가 아니라 두 개의 숫자와 편향으로 표현되어 있습니다.
왜 이것이 작동하는지 보려면, 각 사람의 특징을 학습된 로짓 에 넣고 시그모이드로 압착해 봅시다. 결정 경계(decision boundary)는 , 즉 가 되는 점들의 집합입니다; 분류기는 이면 교수를, 그렇지 않으면 학생을 예측합니다. 코드의 predict_logistic(98–99행)이 바로 이 값들을 계산합니다:
| 사람 | 로짓 | 확률 | 예측 | ||
|---|---|---|---|---|---|
| alice | 1 | 0 | +4.996 | 0.9933 | 교수 |
| bob | 2 | 1 | +4.161 | 0.9846 | 교수 |
| carol | 1 | 1 | −3.594 | 0.0268 | 학생 |
| dave | 0 | 1 | −11.349 | 0.000012 | 학생 |
| erin | 0 | 1 | −11.349 | 0.000012 | 학생 |
두 교수는 경계 위쪽, 즉 에 가까운 확률 지점에 자리 잡고, 세 학생은 그 아래 훨씬 먼 곳에 자리 잡습니다. dave와 erin은 아무도 지도하지 않으면서 지도받고 있기에 그 정도가 가장 심합니다. 경계선 이 특징 평면에서 비스듬한 것은 정확히 두 가중치가 모두 0이 아니기 때문입니다: 이 선은 alice와 bob을 한쪽에, carol, dave, erin을 다른 쪽에 남겨 두도록 기울어져 있으며, 이는 수평선이나 수직선(단일 특징만 사용하는 경계)으로는 결코 해낼 수 없는 일입니다.
일반화 대 암기
훈련 예제에 대한 손실 은 목표가 아닙니다. 목표는 한 번도 본 적 없는 사람들에 대해서도 옳은 모델입니다. 이 둘 사이의 간극이 바로 진짜 패턴을 포착하는 일반화(generalization)와, 보여준 답을 그저 기록해 두는 것에 불과한 암기(memorization)의 차이입니다. 조회 테이블(lookup table)은 훈련 손실이 0이지만 아무것도 배우지 못하며, 새로운 예제를 처음 마주하는 순간 실패합니다.
표준적인 심판은 훈련/테스트 분할(train/test split)입니다: 일부 예제를 따로 떼어 두고, 나머지로만 훈련시킨 다음, 한 번도 건드리지 않은 그 남겨둔 예제들로 모델을 채점합니다. 암기한 모델은 훈련 집합에서는 완벽하지만 테스트 집합에서는 비틀거리고, 일반화한 모델은 둘 다에서 좋은 성적을 냅니다. 이 하나의 습관이야말로 정직한 머신러닝의 근간이며, 이후 장들은 이를 끊임없이 의지합니다 [1]. 우리의 학술 세계에는 다섯 명밖에 없으므로 따로 남겨둘 것이 없습니다 — 바로 그렇기 때문에 여기서 "다섯 명 전원 정답"은 더 넓은 세계에 대한 실력을 주장하는 것이 아니라 그 작동 방식을 보여주는 시연일 뿐입니다.
정직한 함정: 얕은 이유로 맞힌 정답
carol과 alice를 갈라놓는 것이 무엇인지 다시 살펴봅시다. 둘 다 정확히 한 명을 지도하므로(), 그 특징에 관한 한 둘은 동일합니다; 모델은 전적으로 다른 특징에 기대어 둘을 구분합니다 — carol은 지도받고 있고(), alice는 그렇지 않으며(), 이는 저 커다란 음의 가중치에 의존한 결과입니다. 진입차수는 이 분류기가 가장 즐겨 쓰는 손잡이입니다. −8.59로 모델 안에서 가장 큰 절댓값의 가중치이며, "누군가 당신을 지도한다"를 학생이라는 강력한 증거로 읽어냅니다.
하지만 다섯 명 전원을 갈라내는 단일 특징은 존재하지 않으며, 지도받는다는 것은 "학생"에 대한 깔끔한 대리 지표(proxy)가 아닙니다. bob 역시 지도받고 있지만() 그럼에도 교수입니다; 진입차수 하나에만 의존하는 규칙이라면 그를 잘못 분류할 것입니다 — 다섯 명 중 네 명만 맞고 다섯 명은 아닙니다. bob을 구해내는 것은 다른 특징입니다: 그는 두 사람을 지도하며(), 진출차수의 양의 가중치가 그의 진입차수에 대한 불이익을 압도할 만큼 커서 그를 다시 교수 쪽으로 밀어냅니다. 그의 로짓은 으로 계산되며(출력된 소수점 둘째 자리 가중치로는 약 ), 충분히 양수이므로 입니다. carol은 그 거울상에 해당하는 사례입니다 — 그녀는 누군가를 지도하지만() 학생이며, 그녀의 로짓 은 그녀를 안전하게 학생 쪽에 남겨 놓습니다. 바로 이것이 경계가 평평하지 않고 비스듬한 정확한 이유입니다: 두 축 모두가 비중을 지니며, 다섯 명 전원을 맞히는 완벽한 결과는 어떤 단일한 지름길이 아니라 둘을 결합하는 데서 나옵니다.
그럼에도 모델은 그 진입차수 대리 지표에 가장 무겁게 기대고 있으며, "누군가 당신을 지도한다"는 것은 "학생"에 대한 얕은 대역일 뿐, 그 의미 자체는 아닙니다. 아무도 지도하지 않는 갓 임용된 교수, 혹은 후배들을 지도하는 상급 학생이 있다면 이 패턴은 즉시 깨질 것입니다 — 모델은 확신에 찬 채 틀릴 것이고, 그것도 스스로 이해한 적 없는 이유로 틀릴 것입니다. 이는 순수한 맞추기(fitting)가 지닌 반복되는 위험입니다: 학습자는 데이터에서는 성립하지만 세상에서는 성립하지 않는 표면적 상관관계에 기대어 틀린 이유로 정답을 맞힐(right for the wrong reason) 수 있습니다. 5권은 이 실패에 이름을 붙이고 이를 온전히 다룹니다 — 모델이 우리가 의도한 것이 아닌 개념을 통해 정답에 도달하는 추론 지름길(reasoning shortcuts)이며, 이는 이번 장에서 우리가 잠시 밀어두었던 논리와 학습을 다시 이어 붙여야 하는 가장 날카로운 논거 중 하나입니다.
학습의 세 가지 계열
우리가 한 일 — 레이블이 달린 예제로부터 학습하는 것 — 은 지도 학습(supervised learning)이며, 이는 세 가지 고전적 계열 중 하나입니다. 비지도 학습(unsupervised learning)에서는 레이블이 아예 존재하지 않습니다: 학습자는 입력만을 건네받고 스스로 구조를 찾아내야 합니다. 예를 들어 우리의 다섯 사람을 소속 기관별로 군집화(clustering)하는 것(affiliated 사실들은 그들을 mit과 cmu로 묶습니다)을, 그 집단이 무엇인지는 전혀 알려주지 않은 채 해내는 식입니다. 강화 학습(reinforcement learning)에서도 정해진 답은 없고 오직 보상(reward)만 있습니다: 에이전트(agent)가 행동을 취하고, 피드백을 받고, 시간이 지남에 따라 보상을 최대화하도록 조정합니다 — 게임 플레이와 로봇 제어의 배후에 있는 틀입니다. 세 계열 모두 이 장의 DNA를 공유합니다 — 모델, 최적화할 목적 함수, 파라미터에 대한 탐색 — 하지만 교사가 무엇을 제공하는지에서 차이가 납니다: 정답인지, 답이 전혀 없는지, 아니면 점수인지의 차이입니다 [3].
아직 풀리지 않은 부분
학습이 훈련 데이터만으로는 답할 수 없는 불편한 질문은 이것입니다: 정말로 학습한 것인가, 아니면 그저 암기한 것인가? 손실이 낮고 남겨둔 예제도 없다면 — 다섯 명뿐인 우리의 상황이 정확히 그렇습니다 — 이 둘은 구분되지 않습니다. 더 나쁜 것은, 이 어려움이 단지 데이터 부족의 문제가 아니라 원리의 문제라는 점입니다. 세상이 어떤 식으로 이루어져 있다는 가정 — 가까운 입력에는 가까운 답이 주어져야 한다거나, 참된 패턴이 잡음보다 단순하다는 가정 — 을 하지 않는 한, 훈련 예제에 완벽하게 맞춘다고 해서 본 적 없는 예제에 대해 아무것도 보장되지 않습니다. 그 가정을 바꾸면 전혀 다른 모델이 같은 점들에 똑같이 잘 들어맞으면서도 새로운 점들에 대해서는 정반대로 예측할 수 있습니다. 가정 없이 어느 한쪽을 선호할 방법은 없습니다; 정직함이란 그 가정을 명시적으로 밝히고, 남겨둔 데이터로 검증하며, 자신이 설명할 수 없는 이유로 정답을 맞히는 모델을 계속 의심하는 데 있습니다 [1]. 우리의 분류기는 모든 훈련 예제를 통과하면서도 여전히 얕은 "지도받고 있음" 신호에 가장 크게 기대고 있습니다 — 낮은 손실이 학습의 필요조건일 뿐 결코 충분조건이 아니라는 사실을 생생히 일깨워 주는 사례입니다.
왜 중요한가
이번 장은 이 시리즈의 나머지가 하나로 융합할 두 엔진 중 두 번째를 설치합니다. 논리는 도출하고, 학습은 맞춥니다 — 그리고 뉴로-심볼릭 AI(neuro-symbolic AI)란 이 둘을 하나의 시스템으로 작동하게 만드는 학문입니다. 여러분이 앞으로 마주칠 거의 모든 신경망 모델은, 아무리 크더라도, 여기서 조립한 두 가지 요소 — 파라미터 함수와 경사 하강법으로 낮춘 손실 — 로 만들어져 있습니다. 그러니 다섯 사람에 대한 로지스틱 회귀는, 축소판으로 보면, 수십억 개의 파라미터를 가진 네트워크가 대규모로 하는 일과 같습니다. 그리고 그 정직한 함정이야말로 "심볼릭" 절반이 결코 사라지지 않는 이유입니다: 순수한 학습자는 표면적 신호에 매달려 자신의 예제들을 완벽히 맞힐 수 있으므로, 논리의 구조와 보증은 정확히 맞추기 스스로는 제공할 수 없는 것입니다. 맞추기가 어디에서 성공하는지 — 그리고 어디에서 소리 없이 틀린 이유로 정답을 맞히는지 — 를 아는 것이야말로, 추론하는 모델과 그저 패턴만 맞추는 모델을 갈라놓습니다.
핵심 용어
- 지도 학습(supervised learning) — 레이블이 달린 예제들로부터 함수를 맞추는 것으로, 각 예제는 정답과 짝지어진 입력입니다.
- 특징(feature) — 모델에 주어지는 측정 가능한 입력(여기서는
advises관계에서 읽어낸 진출차수와 진입차수)입니다. - 모델 / 파라미터 함수(parametric function) — 조절 가능한 파라미터(parameter) 를 지닌 고정된 함수 형태이며, 직선 가 가장 단순한 예입니다.
- 가중치와 편향(weight and bias) — 학습이 조정하는 파라미터 (기울기)와 (절편)입니다.
- 손실 함수(loss function) — 모델이 얼마나 틀렸는지를 나타내는, 0 이상인 단 하나의 숫자로, 회귀에는 평균제곱오차(mean squared error, MSE), 분류에는 이진 교차 엔트로피(binary cross-entropy, BCE)를 씁니다.
- 잔차(residual) — 예제 에서 예측과 참값 사이의 부호 있는 차이 이며, MSE는 이를 제곱해 평균 냅니다.
- 경사 하강법(gradient descent) — 핵심 반복문으로, 손실의 기울기인 기울기(gradient) 을 읽고, 을 통해 매 에포크(epoch)마다 한 번씩 모든 파라미터를 그에 반대되는 방향으로 학습률(learning rate) 만큼 옮기는 것입니다.
- 선형 회귀(linear regression) — 연속적인 숫자를 예측하기 위해 직선을 맞추는 것입니다.
- 로지스틱 회귀(logistic regression) — 로짓 를 시그모이드(sigmoid) 에 통과시켜 확률을 산출한 뒤 이를 에서 임계값으로 판정하는 분류 방식입니다.
- 오차 신호(error signal) — 교차 엔트로피의 기울기가 무너져 내리는 값인 (예측에서 참값을 뺀 것)로, 각 가중치 업데이트를 이끕니다.
- 결정 경계(decision boundary) — 특징 공간에서 분류기가 레이블을 뒤바꾸는 경계면 이며, 여기서는 두 특징 모두가 비중을 지니므로 비스듬합니다.
- 일반화 대 암기(generalization versus memorization) — 진짜 패턴을 포착하는 것(본 적 없는 입력에서도 통함)과, 답을 그저 기록해 두는 것(그런 입력에서는 실패함)의 차이이며, 훈련/테스트 분할(train/test split)로 판정합니다.
- 추론 지름길(reasoning shortcut) — 모델이 의도된 개념이 아니라 얕은 대리 지표를 통해 정답에 도달하는 현상이며, "지도받고 있음" 신호가 그 작은 예입니다.
- 비지도 학습 / 강화 학습(unsupervised / reinforcement learning) — 레이블 없는 데이터로부터 구조를 학습하는 것, 그리고 레이블이 달린 답이 아니라 보상으로부터 학습하는 것입니다.
이 다음으로 이어지는 것
우리는 학습을 하강으로 정의하고 직선 맞춤에 필요한 단 하나의 기울기를 유도했지만, 두 가지는 그저 믿고 받아들였습니다: 기울기에 반대되는 방향으로 걷는 것이 가장 빠르게 내려가는 길이라는 것, 그리고 학습률 가 넘치지 않으리라고 믿을 수 있다는 것입니다. 우리는 또한 시그모이드를 거친 교차 엔트로피의 기울기가 깔끔한 오차 신호 로 무너져 내린다는 것도 증명 없이 그저 진술했습니다. 다음 장인 경사 하강법은 이 엔진을 온전히 열어젖힙니다: 손실을 가중치 공간 위의 표면으로 정의하고, 코시–슈바르츠 부등식(Cauchy–Schwarz inequality)을 이용해 음의 기울기가 유일한 최급강하 방향임을 증명하며, 1차원 점화식(recurrence)으로부터 안전한 학습률의 정확한 구간을 유도하고, 가 나타나게 되는 교차 엔트로피의 소거 과정을 다룹니다 — learning.py에 나왔던 바로 그 두 맞춤을, 이제 마지막 도함수까지 낱낱이 분해합니다.
짝이 되는 코드: examples/logic/learning.py는 두 개의 예제(fit_line(회귀, 평균제곱오차)과 fit_logistic(분류, 교차 엔트로피))를 모두 구현하며, 위에서 유도한 그대로 기울기를 손으로 적어낸 순수 파이썬(Python)으로 되어 있습니다. python3 examples/logic/learning.py를 실행하면 이 장의 모든 숫자를 재현할 수 있습니다; 검수 하네스(acceptance harness) examples/logic/validate.py는 두 맞춤이 각각의 목표 손실에 도달하는지, 그리고 분류기가 다섯 명 모두에게 올바른 레이블을 붙이는지 검사합니다.