머신러닝·신경망

DL / 3번째 글

선형 모델: 선형 회귀와 로지스틱 회귀

같은 선형 결합에 무엇을 씌우느냐가 회귀와 분류를 가른다. 최소 제곱법과 시그모이드, MSE와 BCE, Ridge·Lasso와 C 파라미터까지 선형 모델 두 갈래를 한 편에서 정리한다.

PALDYN Team50 MIN READ

지난 글에서 지도 학습이 정답 붙은 데이터로 입력과 출력의 대응을 배운다고 했다. 그 대응을 담아 두는 것이 모델이다 — 데이터를 보고 정한 숫자 몇 개를 안에 들고 있다가 입력을 받으면 출력을 내놓는 식 하나를 말한다. 그 식을 가장 단순한 형태로 적으면 선형 모델(Linear Model)이 된다. 입력마다 곱하는 수(가중치)를 하나씩 붙여 전부 더한 값 하나로 답을 내는 모델이다. 이 한 줄짜리 식에서 지도 학습의 두 기본 알고리즘이 갈라져 나온다. 알고리즘은 그 숫자들을 데이터에서 어떻게 정하는지 적어 둔 절차이고, 절차와 그 결과로 나온 식을 같은 이름으로 부르는 것이 보통이다 — 연속값을 예측하는 선형 회귀(Linear Regression)와 클래스를 맞히는 로지스틱 회귀(Logistic Regression)는 알고리즘 이름이면서 모델 이름이다.

둘을 한 편에서 다루는 이유가 있다. 이름이 다르고 푸는 문제가 다르지만 속을 열면 같은 식이 들어 있다. 차이는 그 식이 뱉은 숫자에 무엇을 씌우느냐, 그리고 그 결과를 무엇으로 재느냐 두 가지뿐이다. 따로 배우면 알고리즘이 둘이지만 같이 보면 하나에 스위치가 둘 달린 것에 가깝다. 1800년대 Legendre와 Gauss가 천문 관측의 오차를 줄이려고 만든 방법이 200년 뒤 신경망의 첫 층과 마지막 층에 그대로 남아 있는 것도 이 단순함 덕이다. 여기서 층은 입력을 받아 곱하고 더해 숫자를 내놓는 계산 한 덩어리이고, 그런 층을 여러 개 이어 붙인 것이 신경망이다 — 사이에 무엇이 더 들어가는지는 마지막 절에서 본다.

선형 결합

가중치와 편향

선형 모델이 하는 일은 하나다. 입력 특성마다 가중치를 곱하고 전부 더한 뒤 상수 하나를 얹는다. 특성은 입력을 이루는 개별 항목 하나다 — 집값을 맞힌다면 면적이 특성 하나이고 역까지의 거리가 또 하나다. 그렇게 특성 값이 한 벌 모여 정답 하나와 짝을 이룬 것이 샘플 하나이고, 앞으로 「특성 100개」는 열의 수를, 「샘플 100개」는 행의 수를 세는 말이다. 이렇게 곱해서 더하기만 한 식을 선형 결합(linear combination)이라 부른다.

z=w1x1+w2x2+⋯+wnxn+bz = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b

특성이 여럿이면 행렬로 묶어 z=Xw+bz = Xw + b 로 쓴다. 여기서 ww 는 가중치(weight), 즉 그 특성이 1만큼 늘 때 점수가 얼마나 움직이는지를 적은 값이고, bb 는 편향(bias), 모든 특성이 0일 때의 기준점이다. 학습이 찾는 것은 이 둘뿐이다. 데이터를 보고 이 숫자들을 정하는 것이 학습이고, 다 정하고 나면 모델은 곧 그 숫자다 — 「ww 와 bb 를 찾는다」와 「모델을 만든다」는 같은 말이다.

숫자를 하나 넣어 보자. 집값을 면적(x1x_1, 평)과 역까지 거리(x2x_2, km) 둘로 예측한다고 하고, 학습 결과가 w=(3.2, −1.1)w = (3.2,\ -1.1), b=12b = 12 로 나왔다고 하자. 30평이고 역에서 2km인 집이면 점수는 12+3.2×30−1.1×2=105.812 + 3.2 \times 30 - 1.1 \times 2 = 105.8 이다. 단위가 천만 원이면 10억 5,800만 원을 예측한 것이다.

여기서 선형 모델의 값어치가 드러난다. 계수를 그대로 문장으로 읽을 수 있다. 「1평 넓어질 때마다 3,200만 원 오르고 역에서 1km 멀어질 때마다 1,100만 원 내려간다」가 모델에서 바로 나온다. 의학이나 경제 연구에서 아직도 선형 모델을 쓰는 이유가 정확도가 아니라 이것이다.

항등과 시그모이드

이 zz 를 그대로 답으로 내놓으면 선형 회귀다. 출력 범위는 (−∞,+∞)(-\infty, +\infty) 이고 집값이든 기온이든 매출이든 연속된 값이면 무엇이든 될 수 있다.

zz 에 함수를 하나 씌워 (0,1)(0, 1) 사이로 눌러 담으면 로지스틱 회귀가 된다. 그 함수가 시그모이드고, 눌러 담은 결과는 확률로 읽는다. 이름에 「회귀」가 붙어 있지만 분류 알고리즘인 것은 이 때문이다 — 회귀로 확률을 예측한 뒤 그 확률을 잘라 클래스를 정한다.

항목 선형 회귀 로지스틱 회귀
선형 결합 zz 같다 같다
씌우는 함수 없음 (항등) 시그모이드
출력 실수 확률 (0,1)(0,1)
손실 MSE BCE
최종 답 값 그 자체 임계값으로 자른 클래스
학습이 찾는 것 w,bw, b w,bw, b

표의 마지막 줄이 핵심이다. 찾는 대상이 같으므로 학습 코드도 거의 같다. PyTorch로 쓰면 둘 다 nn.Linear 한 줄이고, 바뀌는 것은 손실 함수 이름 하나다.

MSE와 BCE

손실은 모델이 낸 답이 정답에서 얼마나 어긋났는지를 숫자 하나로 잰 값이다. 갈래가 무엇이든 학습은 결국 이 값을 가장 작게 만드는 ww 와 bb 를 찾는 일이고, 그래서 손실을 무엇으로 재느냐가 학습이 어느 쪽으로 가느냐를 정한다. 씌우는 함수가 바뀌면 손실도 함께 바뀐다. 출력이 무엇이냐가 「틀렸다」를 재는 방식을 정하기 때문이다.

값을 예측할 때 틀림의 크기는 차이다. 300만 원 빗나간 것과 3,000만 원 빗나간 것은 열 배 차이가 나고, 그 열 배가 손실에 그대로 들어가야 한다. 그래서 그 차이를 제곱해서 더한다.

확률을 예측할 때 틀림의 크기는 확신의 정도다. 확률은 뺄셈으로 재면 안 된다 — 망설이다 틀린 것과 확신을 갖고 틀린 것이 뺄셈에서는 거의 같은 값으로 나오기 때문이다. 그래서 로그를 씌운다. 얼마나 달라지는지는 아래 「BCE와 최대 우도」에서 숫자로 본다.

두 손실이 이렇게 갈리는 데는 계산상의 이유도 하나 더 있는데, 그것은 아래 「MSE의 한계」에서 본다.

선형 회귀

최소 제곱법

예측값 y^\hat y 와 실제값 yy 의 차이를 잔차(residual)라 한다. 선형 회귀는 이 잔차를 제곱해 평균 낸 값, 즉 평균 제곱 오차(MSE)를 최소화한다.

L=1n∑i=1n(y^i−yi)2L = \frac{1}{n}\sum_{i=1}^{n}(\hat y_i - y_i)^2

제곱이 하는 일은 셋이다. 부호를 없애 위로 빗나간 것과 아래로 빗나간 것이 상쇄되지 않게 하고, 어디서나 미분 가능하게 만들고, 큰 오차를 불균형하게 강조한다. 마지막 성질은 양날이다 — 크게 틀린 샘플을 빨리 고치지만 이상치 하나에 직선 전체가 끌려간다.

그런데 MSE는 ww 에 대해 아래로 볼록한 이차식이다. 그래서 미분해서 0으로 놓으면 답이 바로 나온다.

w∗=(X⊤X)−1X⊤yw^* = (X^\top X)^{-1} X^\top y

이것이 최소 제곱법(Ordinary Least Squares, OLS)이고, 학습 루프 없이 한 번의 행렬 계산으로 최적해를 얻는다는 점에서 딥러닝과 가장 다른 자리다. 반복도 학습률도 초기값도 없다.

대가는 둘이다. 첫째, X⊤XX^\top X 의 역행렬을 구하는 비용이 특성 수의 세제곱에 비례한다. 특성 100개면 순식간이지만 100,000개면 손을 댈 수 없다. 둘째, 두 특성이 거의 같은 정보를 담고 있으면 그 역행렬이 아예 존재하지 않거나 수치적으로 무너진다. 이 상태를 다중공선성(multicollinearity)이라 한다 — 특성들이 서로 겹쳐 있어 「이 특성이 혼자 무엇을 하는가」가 데이터만으로 갈리지 않는 상태다. 면적을 평으로 적은 열과 제곱미터로 적은 열을 함께 넣은 경우가 극단적인 예다. 두 열이 정확히 상수배면 X⊤XX^\top X 의 행렬식이 0이 되어 역행렬 자체가 없고, 완전히 같지는 않고 상관계수가 0.99쯤이면 역행렬은 존재하되 성분이 터무니없이 커진다. 실무에서 역행렬을 직접 구하지 않고 lstsq 같은 최소 제곱 풀이 함수를 쓰는 이유가 이것이다. 이 함수는 역행렬 대신 특잇값 분해를 거쳐 답을 내므로 겹친 특성이 있어도 계산이 무너지지 않고, 겹친 만큼은 계수를 작게 나눠 갖는 해를 고른다.

import numpy as np

X_aug = np.column_stack([X_train, np.ones(len(X_train))])   # 편향을 특성 하나로 붙인다
w_ols = np.linalg.lstsq(X_aug, y_train, rcond=None)[0]      # 역행렬을 직접 구하지 않는다
print(f"w={w_ols[:-1]}, b={w_ols[-1]:.3f}")

경사 하강

닫힌 해가 있는데도 경사 하강을 쓰는 자리가 있다. 특성이 아주 많을 때, 데이터가 메모리에 한 번에 안 들어갈 때, 그리고 같은 루프를 신경망에 그대로 쓸 수 있을 때다. MSE를 ww 로 미분하면 기울기가 이렇게 나온다.

∂L∂w=2nX⊤(y^−y)\frac{\partial L}{\partial w} = \frac{2}{n} X^\top (\hat y - y)

한 걸음을 손으로 따라가 보자. 데이터가 x=2x = 2, y=6y = 6 한 점뿐이고 w=0w = 0 에서 시작한다고 하자. 편향은 0에 고정하고 ww 하나만 움직인다 — 둘을 함께 갱신해도 되지만 그러면 숫자 둘이 섞여 걸음의 성격이 안 보인다. 예측은 0, 잔차는 0−6=−60 - 6 = -6, 기울기는 2×(−6)×2=−242 \times (-6) \times 2 = -24 다. 학습률 0.01이면 w←0−0.01×(−24)=0.24w \leftarrow 0 - 0.01 \times (-24) = 0.24 로 올라간다. 다음 걸음에서 예측은 0.24×2=0.480.24 \times 2 = 0.48, 잔차는 −5.52-5.52, 기울기는 −22.08-22.08 이라 ww 는 0.4608이 된다. 그다음은 0.6639, 그다음은 0.8508이다.

여기서 제곱 오차의 성질이 보인다. 잔차가 줄면 걸음 폭도 함께 준다. 첫 걸음은 0.24만큼 갔는데 두 번째는 0.2208, 세 번째는 0.2031이다. 정답인 w=3w = 3 에 가까워질수록 갱신이 잘게 쪼개지므로 최적점을 지나쳐 진동하는 일이 적다. 절댓값 오차는 기울기가 부호뿐이라 이 감속이 없고, 그래서 수렴 후반에 학습률을 따로 낮춰 줘야 한다.

같은 숫자에서 학습률의 성격도 드러난다. 걸음 폭은 학습률과 기울기의 곱이고 기울기에는 xx 가 두 번 들어간다 — 한 번은 예측을 만들 때, 한 번은 미분의 연쇄 법칙에서다. 그래서 xx 가 10배 큰 데이터에서는 같은 학습률이 100배 큰 걸음이 된다. 위 예에서 학습률을 0.01 대신 0.3으로 잡으면 첫 걸음에 ww 가 7.2까지 뛰어 정답 3을 훌쩍 넘고, 다음 걸음은 반대편으로 −2.88-2.88, 그다음은 11.232로 진폭이 커지며 발산한다. 학습률을 데이터에 맞춰 매번 다시 잡는 대신 특성 스케일을 먼저 맞춰 두는 편이 나은 이유가 이것이고, 그 이야기는 마지막 절에서 다시 한다.

import torch
import torch.nn as nn

model = nn.Linear(in_features=2, out_features=1)   # ŷ = w₁x₁ + w₂x₂ + b
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

for epoch in range(1000):
    optimizer.zero_grad()
    loss = criterion(model(X_t), y_t)
    loss.backward()
    optimizer.step()

이 여섯 줄이 신경망 학습 루프의 전부다. nn.Linear 한 줄이 곧 층 하나이므로 지금 만든 것은 층 하나짜리 신경망이고, 층을 더 쌓고 사이에 비선형을 끼우면 그대로 딥러닝이 된다.

다항 특성

「선형」이라는 말이 자주 오해를 부른다. 선형 모델이 직선만 그릴 수 있다는 뜻이 아니다. 가중치에 대해 선형이라는 뜻이지 입력에 대해 선형이라는 뜻이 아니다.

그래서 입력을 미리 변환해 넣으면 곡선을 그린다. xx 하나를 xx 와 x2x^2 두 특성으로 늘리면 모델은 여전히 「가중치를 곱해 더하는」 일만 하는데 결과는 포물선이다. 아래처럼 y≈x2y \approx x^2 인 여덟 점을 놓고 2차 다항 특성으로 선형 회귀를 걸면 예측이 실제값과 거의 그대로 겹친다. 얼마나 겹치는지를 재는 값이 결정계수(R2R^2)로, 모델이 데이터의 흩어짐을 얼마나 설명하는지를 0과 1 사이로 적은 것이다. 여기서는 0.9998이 나온다. 정의는 마지막 절에서 식으로 다시 본다.

import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression

x = np.arange(1, 9).reshape(-1, 1)
y = np.array([1.2, 5.1, 9.8, 17.0, 25.1, 36.2, 49.0, 64.1])   # y ≈ x²

poly = make_pipeline(PolynomialFeatures(degree=2, include_bias=False),
                     StandardScaler(), LinearRegression()).fit(x, y)
print(f"{poly.score(x, y):.4f}")      # 0.9998

여기서 실제로 벌어진 일을 짚어 둘 값어치가 있다. PolynomialFeatures가 열 하나를 [x, x2][x,\ x^2] 두 열로 늘렸고, LinearRegression은 그 두 열에 계수를 하나씩 붙여 더했을 뿐이다. 곡선을 그리는 능력은 모델이 아니라 특성 쪽에 들어가 있다. 같은 방식으로 log⁡x\log x, x\sqrt x, 두 특성의 곱 x1x2x_1 x_2 를 열로 넣으면 로그 곡선도 상호작용도 선형 모델이 그린다. 특히 상호작용 항은 「면적이 넓을수록 역세권 프리미엄이 더 크게 붙는다」처럼 두 특성이 서로의 효과를 바꾸는 관계를 담는 자리라 실무에서 자주 쓴다.

대가는 특성 수의 폭발이다. 입력이 하나면 2차 다항 특성은 둘이지만, 입력이 10개면 65개가 되고 3차로 올리면 285개가 된다. 여기서 가진 데이터를 둘로 갈라 두는 습관이 필요해진다 — ww 와 bb 를 정하는 데 쓰는 훈련 데이터와, 손대지 않고 남겨 두었다가 마지막에 채점만 하는 테스트 데이터다. 훈련에서 배운 것이 그 남겨 둔 쪽에서도 통해야 쓸모가 있고, 이 글에서 「새 데이터」라 부르는 자리가 전부 그것이다. 그런데 특성이 샘플 수에 가까워지면 모델은 훈련 데이터의 잡음까지 통과하는 곡선을 그려 낸다. 그래서 차수를 올리는 것은 늘 정규화와 짝을 이뤄야 한다. 여기서 정규화(regularization)는 계수의 크기 자체를 손실로 벌해 억누르는 장치를 말한다 — 뒤에서 「규제」라고도 부르는 그것이고, 특성의 단위를 맞추는 작업(스케일링)과는 다른 말이다. 다섯 번째 절의 주제가 이것이다.

선형 회귀: 최소 제곱법과 경사 하강법

로지스틱 회귀

시그모이드와 로짓

시그모이드 함수는 어떤 실수도 (0,1)(0, 1) 안으로 눌러 담는 S자 곡선이다.

σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}

값을 몇 개 넣어 보면 성격이 보인다. z=−6z = -6 이면 0.0025, z=−1z = -1 이면 0.269, z=0z = 0 이면 정확히 0.5, z=1z = 1 이면 0.731, z=6z = 6 이면 0.9975다. 0 근처에서는 zz 가 조금만 변해도 확률이 크게 움직이고, 양 끝에서는 아무리 밀어도 거의 변하지 않는다. 이 양 끝의 평평한 구간을 포화(saturation)라 부른다.

미분하면 σ′(z)=σ(z) (1−σ(z))\sigma'(z) = \sigma(z)\,(1 - \sigma(z)) 로 자기 자신으로 표현된다. 최대값은 z=0z = 0 에서 0.25이고 z=6z = 6 에서는 0.0025까지 떨어진다. 포화 구간에서 기울기가 사라진다는 뜻이고, 이 사실이 다음 소절에서 결정적으로 작동한다.

시그모이드에 넣기 전의 zz 를 로짓(logit)이라 부른다. 시그모이드를 뒤집으면 그 정체가 드러난다.

z=log⁡p1−pz = \log \frac{p}{1-p}

즉 로짓은 로그 오즈다. 덕분에 로지스틱 회귀의 계수도 문장으로 읽힌다 — 어떤 특성의 ww 가 0.7이면, 그 특성이 1 늘 때 오즈가 e0.7≈2e^{0.7} \approx 2 배가 된다.

MSE의 한계

선형 회귀로 곧장 이진 분류를 시도하면 두 군데가 무너진다.

첫째는 출력 범위다. 직선의 출력은 위아래로 열려 있어 확률로 읽을 수 없다. 게다가 정답 1인 샘플 하나가 아주 오른쪽에 놓이면 MSE는 그 샘플에서도 1을 맞히려고 직선 전체를 기울이고, 그러면 원래 잘 갈리던 자리의 경계까지 밀려난다. 분류에서는 이미 확실히 맞힌 샘플을 「더 맞히려는」 압력이 해롭다.

둘째는 기울기다. 시그모이드를 씌운 뒤 MSE를 쓰면 손실을 ww 로 미분할 때 σ′(z)\sigma'(z) 가 곱해져 나온다. 정답이 1인데 모델이 0.001을 준 상황을 보자. 크게 틀렸으니 크게 배워야 하는데 zz 가 −7-7 부근이라 σ′≈0.001\sigma' \approx 0.001 이고, 기울기가 그만큼 짓눌린다. 가장 크게 틀린 샘플에서 가장 적게 배우는 셈이다. 여기에 더해 MSE에 시그모이드를 씌운 손실은 ww 에 대해 볼록하지 않아 지역 최솟값이 생긴다.

BCE와 최대 우도

분류의 손실은 이진 교차 엔트로피(Binary Cross-Entropy, BCE)다. 정답 클래스에 준 확률의 로그에 음수를 붙인 값이다.

L=−[ ylog⁡y^+(1−y)log⁡(1−y^) ]L = -\big[\,y \log \hat y + (1-y)\log(1-\hat y)\,\big]

정답이 1일 때 숫자로 보면 이렇다. y^=0.9\hat y = 0.9 면 0.105, 0.50.5 면 0.693, 0.10.1 이면 2.303, 0.010.01 이면 4.605다. 앞 절에서 미뤄 둔 비교가 여기서 답을 얻는다 — 0.49를 준 것과 0.01을 준 것은 뺄셈으로는 0.51 대 0.99라 두 배도 차이가 안 나지만 BCE로 재면 0.713 대 4.605로 여섯 배를 넘는다. 확률이 0에 붙을수록 값은 한없이 커진다. 확신에 찬 오답 하나가 잘 맞힌 샘플 마흔넷과 맞먹는다. 라벨이 잘못 붙은 데이터가 학습을 크게 흔드는 이유도 여기 있다 — 모델이 맞게 예측했는데 라벨이 틀렸다면 그 샘플은 확신에 찬 오답으로 계산되어 가장 큰 벌을 받는다.

그런데 왜 하필 로그인가. 이 식은 「크게 틀린 것을 더 벌하고 싶다」는 기분으로 고른 벌점이 아니라 유도해서 나온 것이다. 모델이 어떤 ww 를 들고 있을 때, 각 샘플의 정답이 나올 확률을 모델이 얼마로 봤는지를 전부 곱한 값을 우도(likelihood)라 한다 — 지금의 ww 가 눈앞의 데이터를 얼마나 그럴듯하게 설명하는지를 재는 값이다. 정답이 1인 샘플에서는 y^\hat y, 0인 샘플에서는 1−y^1 - \hat y 를 곱해 나간다. 학습의 목표를 「이 곱을 가장 크게 만드는 ww 찾기」로 두면 그것이 최대 우도 추정(Maximum Likelihood Estimation, MLE)이다.

곱은 그대로 다루기 나쁘다. 1보다 작은 수를 수천 번 곱하면 값이 0으로 내려앉아 컴퓨터에서 구분이 안 되고, 미분도 곱의 미분이라 지저분하다. 로그를 씌우면 곱이 합으로 바뀌고 최대가 되는 자리는 그대로다(로그는 단조 증가하므로 순서를 바꾸지 않는다). 마지막으로 「최대화」를 「최소화」로 맞추려고 부호를 뒤집으면 위의 BCE 식이 정확히 나온다. BCE를 최소화하는 것은 최대 우도 추정을 수행하는 것이고, 로그는 그 과정에서 필연적으로 붙은 것이지 우리가 골라 붙인 장식이 아니다. 아래에서 볼 미분이 깔끔해지는 성질은 그 뒤에 따라온 이득이다.

그리고 BCE를 쓰는 진짜 이유는 미분에서 나온다. 시그모이드와 BCE를 합쳐 미분하면 σ′\sigma' 가 정확히 소거되고 기울기가 이렇게 남는다.

∂L∂w=(y^−y) x\frac{\partial L}{\partial w} = (\hat y - y)\,x

선형 회귀의 기울기와 같은 꼴이다. 포화 구간에서도 기울기가 죽지 않고, 잔차가 클수록 크게 배운다. 앞 소절에서 MSE가 잃었던 성질을 BCE가 되찾는 것이다.

구현에서는 하나만 지키면 된다. 확률을 먼저 만들고 로그를 씌우면 y^\hat y 가 0에 붙었을 때 log(0)이 음의 무한대가 되므로, 프레임워크는 시그모이드와 로그를 한 연산 안에서 처리하는 손실을 따로 둔다. 모델은 시그모이드 없이 로짓을 그대로 내보내고 손실이 나머지를 맡는다. 그래서 학습할 때와 추론할 때가 지나는 길이 갈린다 — 추론은 학습을 마친 모델을 실제로 써서 답을 내는 단계이고, 시그모이드는 학습 경로에는 없고 이 자리에만 붙어 로짓을 확률로 옮긴다.

model = nn.Linear(n_features, 1)          # 시그모이드를 붙이지 않는다
criterion = nn.BCEWithLogitsLoss()        # 시그모이드 + BCE를 한 번에

# 추론할 때만 확률로 옮긴다
with torch.no_grad():
    probs = torch.sigmoid(model(X_test_t)).squeeze()
    preds = (probs >= 0.5).long()

모델 마지막에 nn.Sigmoid를 붙여 두고 BCEWithLogitsLoss를 쓰는 것이 가장 흔한 실수다. 손실이 이미 확률인 값에 시그모이드를 한 번 더 걸어 버리므로 최종 출력이 0.5와 0.731 사이에만 갇힌다 — 모델이 확률 0을 내놓아도 σ(0)=0.5\sigma(0) = 0.5 라 절반 밑으로 못 내려가고, 1을 내놓아도 σ(1)=0.731\sigma(1) = 0.731 이 천장이다. 모델이 확신 있는 예측을 아예 낼 수 없게 된다. 그런데 손실 값은 그 좁은 구간 안에서나마 조금씩 줄어들기 때문에 학습이 도는 것처럼 보이고, 정확도만 어느 선에서 더 오르지 않는다. 의심이 들면 검증 배치의 예측 확률에서 최솟값과 최댓값을 한 번 찍어 본다. 0.5 밑이 하나도 없으면 시그모이드가 두 번 걸린 것이다.

로지스틱 회귀: 시그모이드와 결정 경계

결정 경계와 임계값

선형 분리

확률 0.5를 기준으로 클래스를 가른다고 하자. 시그모이드는 z=0z = 0 에서 정확히 0.5이므로, y^≥0.5\hat y \ge 0.5 는 z≥0z \ge 0 과 완전히 같은 조건이다. 즉 두 클래스를 가르는 결정 경계(decision boundary)는 다음 식이다.

w1x1+w2x2+⋯+wnxn+b=0w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b = 0

특성이 둘이면 직선, 셋이면 평면, 그 이상이면 초평면이다. 시그모이드를 씌웠는데도 경계가 곧게 남는 이유는 시그모이드가 단조 함수라 순서를 바꾸지 않기 때문이다 — 구부러지는 것은 확률값이지 경계가 아니다.

그래서 로지스틱 회귀는 직선 하나로 갈리지 않는 문제를 풀 수 없다. XOR처럼 대각선 두 자리가 같은 클래스인 배치가 대표적이다. 푸는 방법은 둘뿐이다. 앞에서 본 것처럼 특성을 곱해 늘리거나(그러면 원래 공간에서는 곡선이 된다), 층을 쌓아 신경망으로 만들거나.

임계값 조정

0.5는 수학이 정해 준 값이 아니라 관례다. 임계값은 학습으로 정해지지 않으므로 우리가 정해야 한다. 앞에서 본 학습률과 다항 차수, 뒤에 나올 λ\lambda 와 C도 같은 갈래다 — 학습이 데이터를 보고 정하는 ww·bb 와 달리 학습을 돌리기 전에 사람이 넣어 주는 값이고, 이런 값을 하이퍼파라미터(hyperparameter)라 부른다.

암 진단을 예로 들면 두 오류의 값이 전혀 다르다. 환자를 정상으로 잘못 보내는 것과 정상인을 추가 검사로 보내는 것 중 앞쪽이 압도적으로 비싸다. 그래서 임계값을 0.3으로 내린다. 그러면 양성으로 판정되는 범위가 넓어져 놓치는 환자가 줄고, 대신 헛짚는 건수가 늘어난다.

이 두 가지에는 각각 이름이 있다. 재현율(recall)은 실제 양성 중 모델이 잡아낸 비율이다 — 놓친 환자가 적을수록 높다. 정밀도(precision)는 모델이 양성이라 부른 것 중 실제로 양성인 비율이다 — 헛짚은 건수가 적을수록 높다. 임계값을 내리면 재현율이 오르고 정밀도가 내려가며, 올리면 반대다. 둘은 같은 손잡이의 양 끝이라 한쪽만 올리는 방법이 없고, 그래서 「어느 쪽 오류가 더 비싼가」가 임계값을 정하는 유일한 기준이 된다. 스팸 필터는 암 진단의 정반대다 — 중요한 메일 하나를 스팸함에 넣는 비용이 커서 임계값을 올리고, 정밀도를 재현율보다 우선한다.

여기서 중요한 것은 이 손잡이가 재학습 없이 돌아간다는 점이다. 모델은 그대로 두고 자르는 자리만 옮기면 된다. predict가 아니라 predict_proba를 받아 두는 습관이 필요한 이유가 이것이다.

y_proba = clf.predict_proba(X_test)[:, 1]     # P(y=1)
y_pred = (y_proba >= 0.30).astype(int)        # 재현율을 우선하는 임계값

소프트맥스와 다중 분류

클래스가 셋 이상이면 클래스마다 선형 결합을 하나씩 두고, 그 점수들을 소프트맥스(Softmax)로 확률 분포로 만든다.

P(y=k∣x)=ezk∑jezjP(y = k \mid x) = \frac{e^{z_k}}{\sum_j e^{z_j}}

지수를 씌우면 전부 양수가 되고 합으로 나누면 총합이 1이 된다. 클래스가 둘일 때 이 식을 정리하면 z1−z2z_1 - z_2 하나에 대한 시그모이드로 환원되므로, 소프트맥스는 시그모이드의 일반형이다. 로지스틱 회귀와 다중 분류가 다른 알고리즘이 아니라는 뜻이다.

PyTorch에서는 CrossEntropyLoss가 소프트맥스와 로그를 품고 있으므로, 이진 분류에서와 똑같이 마지막 층은 로짓 벡터를 그대로 내보낸다. 다만 한 샘플에 정답이 여럿인 다중 레이블 문제는 소프트맥스가 아니다 — 확률의 합을 1로 묶으면 두 라벨이 서로의 확률을 빼앗으므로, 그때는 클래스마다 시그모이드를 따로 걸고 BCE를 쓴다.

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(max_iter=500)     # 3클래스면 소프트맥스로 자동 전환
clf.fit(X_train, y_train)
proba = clf.predict_proba(X_test[:5])      # 각 행의 합 = 1

로지스틱 회귀 구현 및 다중 분류

정규화

Ridge 회귀

특성이 많고 샘플이 적으면 계수가 커진다. 특히 앞에서 본 다중공선성이 있으면 모델은 겹친 두 특성 중 한쪽에 +1000+1000, 다른 쪽에 −998-998 을 주는 식의 해를 찾아낸다. 두 값이 거의 상쇄되므로 훈련 데이터에서는 멀쩡하지만, 새 데이터에서 두 특성의 관계가 조금만 흔들려도 예측이 폭주한다. 계수가 크다는 것은 그 특성이 1만큼 흔들릴 때 예측이 1000만큼 움직인다는 뜻이고, 그 흔들림은 새 데이터에서 반드시 온다.

Ridge 회귀는 손실에 계수의 제곱 합을 더해 이것을 막는다.

Lridge=1n∑(y^i−yi)2+λ∑jwj2L_{\text{ridge}} = \frac{1}{n}\sum(\hat y_i - y_i)^2 + \lambda \sum_j w_j^2

미분하면 각 계수의 기울기에 2λwj2\lambda w_j 가 더해진다. 부호가 계수와 같으므로 매 스텝 계수를 원점 쪽으로 조금씩 당긴다. 해석적 해에서는 X⊤XX^\top X 가 X⊤X+λIX^\top X + \lambda I 로 바뀌는데, 대각선에 값을 더한 덕에 역행렬이 늘 존재하게 된다. 다중공선성으로 무너지던 계산이 그대로 살아난다.

λ\lambda 는 저울이다. 데이터가 말하는 것과 「계수는 작아야 한다」는 우리 쪽 믿음 사이의 비율이고, scikit-learn에서는 alpha라는 이름으로 넘긴다. 값을 키우면 계수가 고르게 줄지만 0에는 닿지 않는다 — 당기는 힘 2λw2\lambda w 자체가 계수와 함께 작아지기 때문이다.

PyTorch에는 Ridge라는 이름의 클래스가 따로 없다. 같은 일을 옵티마이저가 한다 — torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)처럼 weight_decay를 주면 매 스텝 계수를 그만큼 원점 쪽으로 당기고, 이것이 손실에 λ∑w2\lambda \sum w^2 를 더한 것과 같은 효과를 낸다. 손실 함수를 고치는 대신 갱신 규칙을 고치는 것이라 코드에서는 손실 쪽에 아무 흔적이 없고, 그래서 「정규화를 안 걸었는데 계수가 줄더라」로 읽히기 쉬운 자리다.

Lasso와 특성 선택

제곱 대신 절댓값을 더하면 성격이 달라진다. ∣w∣|w| 의 기울기는 부호뿐이라 계수가 0에 가까워져도 당기는 힘이 그대로다. 그래서 쓸모없는 계수를 정확히 0으로 보낸다. 이것이 Lasso 회귀이고, 정규화를 하면서 특성 선택을 겸한다.

고르는 기준은 목적에 있다. 특성 200개가 다 조금씩 쓸모 있을 것 같고 계수를 안정시키는 것이 목적이면 Ridge다. 200개 중 실제로 의미 있는 것이 20개쯤일 것 같고 어느 20개인지 알아내는 것 자체가 목적이면 Lasso다. 둘을 섞은 ElasticNet은 상관관계가 높은 특성 무리에서 Lasso가 하나만 남기고 나머지를 임의로 버리는 문제를 완화한다.

from sklearn.linear_model import Ridge, Lasso

ridge = Ridge(alpha=1.0).fit(X_train_s, y_train)
lasso = Lasso(alpha=0.1, max_iter=5000).fit(X_train_s, y_train)
print(f"Lasso가 0으로 보낸 계수: {(lasso.coef_ == 0).sum()}개")

C와 클래스 가중치

로지스틱 회귀도 같은 정규화를 쓰는데 손잡이의 방향이 반대다. LogisticRegression은 alpha가 아니라 C를 받고 C=1/λC = 1/\lambda 로 정의되어 있다. 즉 C가 작을수록 규제가 강하다. Ridge에서 alpha를 올리던 감각으로 C를 올리면 정반대 방향으로 가므로, 값을 훑을 때는 0.01·0.1·1·10처럼 로그 간격으로 보고 검증 정확도를 함께 적어 두는 편이 안전하다.

분류에는 회귀에 없는 문제가 하나 더 있다. 클래스 불균형이다. 양성이 전체의 1%면 무조건 음성이라 답하는 모델도 정확도 99%를 받는다. 손실 쪽에서 이것을 바로잡는 방법이 클래스마다 손실 배수를 다르게 주는 것이다. class_weight='balanced'는 클래스 빈도의 역수를 가중치로 넣어, 드문 클래스의 샘플 하나가 흔한 클래스의 샘플 여럿만큼 무겁게 계산되도록 만든다. 비율을 직접 정하고 싶으면 {0: 1, 1: 10} 처럼 딕셔너리로 넘긴다.

지금까지의 조각을 한 줄로 꿰면 이렇게 된다. 데이터를 나누고, 스케일러를 훈련에서만 배우고, C를 로그 간격으로 훑고, 마지막에 정확도 하나가 아니라 지표 표를 받아 본다.

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report

X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)

scaler = StandardScaler()
X_tr_s, X_te_s = scaler.fit_transform(X_tr), scaler.transform(X_te)

for C in [0.01, 0.1, 1.0, 10.0]:
    clf = LogisticRegression(C=C, class_weight='balanced', max_iter=500)
    print(f"C={C:<5}: {clf.fit(X_tr_s, y_tr).score(X_te_s, y_te):.4f}")

best = LogisticRegression(C=1.0, class_weight='balanced', max_iter=500).fit(X_tr_s, y_tr)
print(classification_report(y_te, best.predict(X_te_s)))

class_weight는 앞의 임계값 조정과 목적이 비슷해 보이지만 층위가 다르다 — 가중치는 학습을 바꾸고 임계값은 이미 학습된 모델의 판정선만 옮긴다. 학습을 다시 돌릴 수 있으면 가중치가 낫고, 이미 배포된 모델의 동작만 바꿔야 하면 임계값밖에 없다. 둘을 함께 쓰면 효과가 겹쳐 과교정이 되기 쉬우므로 한쪽씩 켜고 검증 곡선을 본다.

Ridge와 Lasso가 계수를 당기는 힘의 차이

실전 적용

스케일링

선형 모델에는 특성마다 제각각인 단위를 맞춰 두는 전처리가 거의 항상 따라붙는다. 이것을 스케일링이라 부른다 — 앞 절의 정규화(계수를 억누르는 쪽)와 이름이 헷갈리기 쉬운데, 여기서 손대는 것은 계수가 아니라 입력 특성의 단위다. 그런데 이 전처리가 필요한 이유의 절반이 그 정규화에 있다.

OLS의 해석적 해만 놓고 보면 스케일은 상관없다. 면적을 평으로 재든 제곱미터로 재든 계수가 그만큼 반대로 조정되어 예측은 같다. 그런데 정규화를 걸면 이야기가 달라진다. 정규화가 벌하는 것은 계수의 크기인데, 면적을 제곱미터로 바꾸면 숫자가 3.3배 커지고 계수는 3.3분의 1로 작아진다. 같은 특성인데 단위만 바꿔 벌을 덜 받게 된 것이다. 즉 단위가 큰 특성이 자동으로 규제를 피한다.

경사 하강 쪽에도 이유가 있다. 특성 범위가 제각각이면 손실 곡면이 한쪽으로 길게 눌린 골짜기 모양이 되고, 그러면 갱신이 골짜기 벽을 왔다 갔다 하느라 수렴이 느려진다.

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)   # 훈련에서만 평균·표준편차를 배운다
X_test_s = scaler.transform(X_test)         # 테스트에는 그 값을 적용만 한다

마지막 두 줄의 구분이 중요하다. 테스트 데이터에 fit을 다시 걸면 테스트 분포 정보가 전처리에 새어 들어간다. 손대지 않은 데이터로 채점하려고 갈라 둔 것인데 전처리가 그 데이터를 먼저 들여다본 셈이 되고, 그러면 점수가 실제보다 좋게 나온다.

평가 지표

회귀와 분류는 지표도 갈린다. 회귀에서는 MSE로 학습하되 보고는 RMSE(제곱근 평균 제곱 오차)로 하는 조합이 흔하다. 제곱근을 씌우면 단위가 원래대로 돌아와 「평균 3,200만 원 빗나간다」처럼 읽히기 때문이다. 이상치에 덜 끌리는 값을 함께 보고 싶으면 MAE(평균 절대 오차)를 옆에 둔다 — 잔차를 제곱하지 않고 절댓값으로 더한 값이라 크게 빗나간 몇 건에 덜 휘둘린다. RMSE가 MAE보다 훨씬 크면 그 차이 자체가 정보다. 대부분은 잘 맞히는데 소수의 샘플에서 크게 틀리고 있다는 신호다. 그리고 앞에서 한 번 나왔던 결정계수 R2R^2 는 모델이 yy 의 분산 중 몇 퍼센트를 설명하는지를 잰다.

R2=1−∑(yi−y^i)2∑(yi−yˉ)2R^2 = 1 - \frac{\sum (y_i - \hat y_i)^2}{\sum (y_i - \bar y)^2}

분모가 「그냥 평균만 답하는 모델」의 오차이므로, R2=0R^2 = 0 은 평균과 같은 수준이라는 뜻이고 음수도 나올 수 있다 — 평균보다 못하다는 뜻이다.

분류에서는 정확도 하나만 보면 불균형에 속는다. 양성이 1%인 데이터에서 정확도 99%는 아무것도 학습하지 않았다는 신호일 수 있다. 그래서 앞에서 본 정밀도와 재현율을 함께 본다. 둘 중 무엇을 우선할지 정하지 못했거나 한 숫자로 줄여 비교해야 할 때는 F1을 쓴다 — 정밀도와 재현율을 하나로 합친 값이고, 둘 중 낮은 쪽에 끌려가도록 만들어져 있어 한쪽만 높은 모델은 F1이 오르지 않는다. 그리고 임계값을 하나 고르지 않고 전 구간의 성능을 알고 싶으면 ROC-AUC를 쓴다. 임계값을 0에서 1까지 훑으며 성능을 적분한 값이라 임계값 선택과 무관하게 모델 자체를 비교할 수 있다. 실무의 순서는 대개 이렇다 — ROC-AUC로 모델을 고르고, 고른 모델에서 임계값을 정하고, 그 임계값에서의 정밀도와 재현율을 보고한다.

회귀 모델 평가 지표: MSE·RMSE·MAE·결정계수

신경망 속의 선형 층

이 글의 식은 딥러닝에서 사라지지 않는다. nn.Linear가 곧 선형 결합이고, 층을 쌓고 사이에 비선형 활성 함수를 끼운 것이 신경망이다. 분류 신경망의 마지막 층은 이름만 「분류 헤드」일 뿐 로지스틱 회귀이고, CrossEntropyLoss는 소프트맥스 회귀 그 자체다. 선형 모델을 이해하면 신경망의 양 끝을 이미 이해한 것이다.

그래서 실무에서 선형 모델의 자리는 「낡은 알고리즘」이 아니라 기준선이다.

선형 모델을 쓰는 자리 다른 것으로 옮길 자리
빠른 베이스라인이 필요할 때 관계가 분명히 비선형일 때
계수 해석이 결과의 일부일 때 특성 간 상호작용이 핵심일 때
특성은 많고 샘플이 적을 때 이미지·텍스트 같은 비정형 데이터
추론이 아주 빨라야 할 때 표 데이터에서 정확도가 최우선일 때

그리고 선형 모델이 잘 안 되는 것 자체가 정보다. 베이스라인이 무너졌다는 것은 데이터가 선형이 아니거나, 특성 엔지니어링이 부족하거나, 애초에 그 특성들로는 답을 낼 수 없다는 신호다. 복잡한 모델을 먼저 꺼내면 이 세 가지를 구분할 기회를 잃는다.

선형 모델은 데이터 전체를 관통하는 식 하나를 찾아 그것으로 모든 질문에 답한다. 다음 글에서는 정반대편에 선 방법을 본다 — 식을 세우지 않고, 새 데이터가 들어오면 훈련 데이터 중 가장 가까운 몇 개를 찾아 그들의 답을 그대로 따르는 방식이다. 학습이라 부를 단계가 거의 없는데도 잘 통하는 자리가 있고, 그 자리가 어디이며 「가깝다」를 무엇으로 재느냐가 주제다.


읽어주셔서 감사합니다. 😊

LATEST

머신러닝·신경망의 최신 글

머신러닝·신경망2026.05.08

문맥적 임베딩: ELMo부터 BERT까지

정적 임베딩의 다의어 문제를 해결하는 문맥적 임베딩의 원리, ELMo의 양방향 LSTM 레이어 표현, BERT의 트랜스포머 기반 서브워드 임베딩 추출법을 수식과 코드로 완전히 해설한다.

12 MIN
머신러닝·신경망2026.05.08

FastText: 부분 단어로 OOV를 정복하다

FastText가 문자 n-gram 기반의 부분 단어 모델로 OOV 문제를 해결하는 방법, 한국어 형태론에서의 강점, 실전 학습과 추론 코드를 완전히 해설한다.

11 MIN
머신러닝·신경망2026.05.08

GloVe: 전역 공기 통계로 단어 벡터를 만들다

GloVe가 공기 행렬의 전역 통계와 국소 문맥 창의 장점을 결합하는 방법, 목적 함수의 수학적 의미, 사전 학습 벡터 활용법을 깊이 있게 다룬다.

11 MIN