선형회귀: 이론부터 구현까지
선형회귀는 하나 이상의 입력 변수와 연속형 목표 변수 사이의 관계를 모델링하는 알고리즘입니다. 이 글에서는 일변량 선형회귀의 기본 원리부터 비용함수와 경사하강법의 유도, NumPy를 이용한 구현까지 하나의 흐름으로 정리합니다.
각 계산을 직접 확인할 수 있도록 작은 주택 가격 데이터셋을 사용합니다. 주택 크기 1단위는 1,000제곱피트, 가격 1단위는 1,000달러를 의미합니다.
이 글은 Andrew Ng 교수의 Supervised Machine Learning: Regression and Classification 강의를 수강하며 작성한 학습 노트를 재구성한 것입니다. 설명과 코드는 학습 목적에 맞게 별도로 정리하고 단순화했습니다.
1. 문제 정의
지도학습에서는 입력 $x$와 정답 $y$로 구성된 데이터로부터 두 변수 사이의 대응 관계를 학습합니다. 회귀는 가격, 온도, 수요처럼 연속적인 값을 예측하며, 분류는 정해진 범주 중 하나를 예측한다는 점에서 구분됩니다.
예제 데이터는 다음 두 관측값으로 구성됩니다.
| 주택 크기 $x$ (1,000 ft²) | 가격 $y$ (1,000 USD) |
|---|---|
| 1.0 | 300.0 |
| 2.0 | 500.0 |
일변량 선형회귀의 예측함수는 다음과 같습니다.
\[f_{w,b}(x) = \hat{y} = wx+b\]각 기호의 의미는 다음과 같습니다.
- $x$: 입력 특성(feature)
- $y$: 실제 목표값(target)
- $\hat{y}$: 모델의 예측값
- $w$: 가중치(weight), 즉 직선의 기울기
- $b$: 편향(bias), 즉 직선의 절편
두 관측값을 정확히 지나는 직선은 다음과 같습니다.
\[\hat{y}=200x+100\]따라서 크기가 $x=3$인 주택의 예상 가격은 70만 달러입니다.

실제 데이터는 대개 하나의 직선 위에 정확히 놓이지 않습니다. 따라서 학습의 목표는 전체 예측 오차를 가장 작게 만드는 $w$와 $b$를 찾는 것입니다.
하나의 특성부터 시작한 이유
처음 내용을 정리할 때에는 모델 자체보다 $f_{w,b}(x)$, $x^{(i)}$, $\hat{y}^{(i)}$ 같은 표기가 더 어렵게 느껴졌습니다. 입력 특성을 하나로 제한하니 각 기호의 역할을 다음 순서로 분리해서 이해할 수 있었습니다.
- 하나의 관측값 $x^{(i)}$를 선택합니다.
- 이를 $wx+b$에 대입합니다.
- 예측값을 실제값 $y^{(i)}$와 비교합니다.
- 모든 관측값에 대해 같은 비교를 반복합니다.
위 첨자 $(i)$는 거듭제곱이 아니라 데이터의 순서를 나타내는 인덱스입니다. 즉, $x^{(2)}$는 $x$의 제곱이 아니라 두 번째 관측값입니다. 비용함수에서 합 기호가 등장한 뒤에는 이 구분이 특히 중요합니다.
매개변수를 기하학적으로 해석하면 더 직관적입니다. $w$를 바꾸면 직선이 회전하고, $b$를 바꾸면 직선 전체가 위아래로 이동합니다. 따라서 선형회귀의 학습은 관측값에 가장 잘 맞도록 직선을 이동하고 회전시키는 과정으로 볼 수 있습니다.
이 예제에서는 두 점을 이용해 정답을 직접 구할 수 있습니다.
\[w=\frac{500-300}{2-1}=200\] \[b=300-200\times1=100\]직접 계산한 값은 경사하강법의 결과가 올바른지 확인하는 기준이 됩니다. 두 점을 잇는 직선을 찾는 데 반드시 수치 최적화가 필요한 것은 아닙니다. 이 예제에서 경사하강법을 사용하는 이유는, 같은 최적화 구조가 손으로 풀기 어려운 더 큰 모델에도 확장되기 때문입니다.
2. 비용함수
$i$번째 관측값의 잔차(residual)는 다음과 같습니다.
\[e^{(i)} = f_{w,b}(x^{(i)})-y^{(i)}\]잔차를 그대로 더하면 양수와 음수가 서로 상쇄될 수 있습니다. 잔차를 제곱하면 이러한 상쇄를 방지할 수 있으며 큰 오차에는 더 큰 불이익을 줄 수 있습니다. 관측값이 $m$개일 때 이 글에서 사용하는 비용함수는 다음과 같습니다.
\[J(w,b)=\frac{1}{2m}\sum_{i=1}^{m}\left(f_{w,b}(x^{(i)})-y^{(i)}\right)^2\]이 식은 평균제곱오차(MSE)의 절반입니다. 앞의 $1/2$은 비용을 최소화하는 매개변수에 영향을 주지 않으며, 미분 과정에서 생기는 계수 $2$를 상쇄해 식을 간결하게 만듭니다.
개별 손실(loss)과 전체 비용(cost)을 구분하는 것도 도움이 되었습니다. 하나의 관측값에 대한 제곱오차는 하나의 예측이 얼마나 틀렸는지를 나타냅니다. $J(w,b)$는 전체 학습 데이터에 대한 오차를 평균한 값이며 배치 경사하강법이 실제로 최소화하는 대상입니다. 두 용어가 일상적으로 혼용되기도 하지만, 이를 구분하니 뒤의 수식이 훨씬 명확해졌습니다.
예를 들어 예측값이 $[100, 400, 500]$, 실제값이 $[200, 300, 500]$이라면 비용은 다음과 같습니다.
\[J=\frac{(-100)^2+(100)^2+0^2}{2\times3}=3333.33\]$J$가 작을수록 학습 데이터에 더 잘 맞는 모델입니다. $J=0$이라면 모든 예측값이 실제값과 정확히 일치합니다.
잔차를 제곱하는 이유
| 양수와 음수의 잔차가 상쇄되는 현상을 막는 방법은 제곱만 있는 것이 아닙니다. 평균절대오차는 $ | \hat{y}-y | $를 사용합니다. 여기에서 제곱오차를 사용하는 주된 이유는 다음과 같습니다. |
- 모든 지점에서 미분할 수 있어 기울기 식이 단순합니다.
- 큰 잔차에 상대적으로 더 큰 불이익을 부여합니다.
두 번째 성질은 장점이면서 단점입니다. 큰 실수를 적극적으로 줄이게 만들지만 이상치에도 민감해집니다. 따라서 제곱오차와 절대오차 중 무엇을 사용할지는 관습이 아니라 데이터의 성격에 따라 결정해야 합니다.
두 매개변수가 비용의 표면이 되는 과정
$w$와 $b$를 하나씩 정하면 모델은 하나의 비용값을 반환합니다. $b$를 고정하고 $w$만 변화시키면 $J(w,b)$를 그릇 모양의 2차원 곡선으로 나타낼 수 있습니다. 두 매개변수를 모두 변화시키면 비용은 $(w,b)$ 평면 위의 표면이 됩니다. 등고선은 지도에서 높이가 같은 지점을 연결하듯 비용이 같은 매개변수 조합을 연결합니다.
선형회귀의 제곱오차 비용함수는 볼록함수입니다. 따라서 최적이 아닌 지역 최솟값이 존재하지 않으며, 모든 지역 최솟값은 곧 전역 최솟값입니다. 아래 등고선은 서로 다른 $w$와 $b$에 대한 비용과 경사하강법의 이동 경로를 나타냅니다.

3. 경사하강법
경사하강법은 비용이 감소하는 방향으로 매개변수를 반복해서 조정합니다. 학습률을 $\alpha>0$이라고 하면 업데이트 식은 다음과 같습니다.
처음에는 비용 곡선의 모양을 본 뒤 $w$를 임의의 값만큼 늘리거나 줄이면 된다고 생각했습니다. 그러나 현재 위치에 따라 이동해야 할 방향이 달라지고, 적절한 이동 폭도 표면의 위치마다 달라집니다. 미분값은 현재 지점에서의 방향과 경사를 제공하고, 학습률은 그 제안된 이동을 얼마나 반영할지를 결정합니다.
\[w \leftarrow w-\alpha\frac{\partial J}{\partial w}\] \[b \leftarrow b-\alpha\frac{\partial J}{\partial b}\]일변량 선형회귀에서 각 편미분은 다음과 같습니다.
\[\frac{\partial J}{\partial w} =\frac{1}{m}\sum_{i=1}^{m} \left(f_{w,b}(x^{(i)})-y^{(i)}\right)x^{(i)}\] \[\frac{\partial J}{\partial b} =\frac{1}{m}\sum_{i=1}^{m} \left(f_{w,b}(x^{(i)})-y^{(i)}\right)\]편미분 식의 유도
$f_{w,b}(x^{(i)})=wx^{(i)}+b$를 비용함수에 대입하면 다음과 같습니다.
\[J(w,b)=\frac{1}{2m}\sum_{i=1}^{m} \left(wx^{(i)}+b-y^{(i)}\right)^2\]$w$에 대해 연쇄법칙을 적용하면
\[\begin{aligned} \frac{\partial J}{\partial w} &=\frac{1}{2m}\sum_{i=1}^{m} 2\left(wx^{(i)}+b-y^{(i)}\right)x^{(i)} \\ &=\frac{1}{m}\sum_{i=1}^{m} \left(f_{w,b}(x^{(i)})-y^{(i)}\right)x^{(i)} \end{aligned}\]을 얻습니다. $b$에 대해서도 같은 방식으로 계산하면
\[\frac{\partial J}{\partial b} =\frac{1}{m}\sum_{i=1}^{m} \left(f_{w,b}(x^{(i)})-y^{(i)}\right)\]을 얻습니다.
각 편미분의 부호는 매개변수의 이동 방향을 결정합니다. $\partial J/\partial w$가 양수이면 이를 빼서 $w$를 감소시키고, 음수이면 $w$를 증가시킵니다. $b$에도 같은 원리가 적용됩니다.
첫 번째 업데이트를 숫자로 따라가기
업데이트 한 번을 직접 계산해보니 경사하강법의 의미가 훨씬 분명해졌습니다. 초기값을 다음과 같이 정합니다.
\[w=0,\qquad b=0,\qquad \alpha=0.02\]초기 예측값과 잔차는 다음과 같습니다.
\[\hat{\mathbf{y}}=[0,0], \qquad \hat{\mathbf{y}}-\mathbf{y}=[-300,-500]\]따라서 초기 비용은
\[J(0,0)=\frac{(-300)^2+(-500)^2}{2\times2}=85{,}000\]입니다. 두 방향의 기울기는 다음과 같습니다.
\[\frac{\partial J}{\partial w} =\frac{(-300)(1)+(-500)(2)}{2}=-650\] \[\frac{\partial J}{\partial b} =\frac{-300+(-500)}{2}=-400\]두 값이 모두 음수이므로 이를 빼면 두 매개변수는 증가합니다.
\[w_{\text{new}}=0-0.02(-650)=13\] \[b_{\text{new}}=0-0.02(-400)=8\]$(w,b)=(13,8)$에서 예측값은 $[21,34]$가 되며 비용은
\[J(13,8)=\frac{(21-300)^2+(34-500)^2}{4}=73{,}749.25\]로 감소합니다. 한 번의 업데이트만으로는 충분하지 않지만, 기울기가 비용이 더 낮은 영역을 가리킨다는 사실을 직접 확인할 수 있습니다. 같은 계산을 반복하면 등고선 그래프의 주황색 경로를 따라 이동하게 됩니다.
“수렴할 때까지 반복”을 코드로 표현하는 방법
수식에서는 흔히 “수렴할 때까지 반복”한다고 표현하지만, 프로그램에는 명시적인 종료 조건이 필요합니다. 대표적인 방법은 다음과 같습니다.
- 정해진 반복 횟수에 도달하면 종료합니다.
-
$ J_t-J_{t-1} $가 허용 오차보다 작아지면 종료합니다. - 기울기의 크기가 충분히 작아지면 종료합니다.
- 비용이 유한하지 않거나 계속 증가하면 조기에 종료합니다.
아래 구현에서는 학습률별 실험을 동일한 조건으로 비교하기 쉽도록 고정 반복 횟수를 사용합니다. 실제 학습 코드에서는 허용 오차를 이용한 종료 조건이 수렴 상태를 더 직접적으로 보여줍니다.
매개변수의 동시 업데이트
두 편미분은 반드시 동일한 매개변수 상태에서 계산해야 합니다. 따라서 한 번의 반복은 다음과 같이 구현합니다.
dj_dw, dj_db = compute_gradient(x, y, w, b)
w = w - alpha * dj_dw
b = b - alpha * dj_db
$w$를 먼저 갱신한 다음 새로운 $w$로 $b$의 편미분을 다시 계산하면 원래 수식과 다른 최적화 규칙이 됩니다. 단순한 예제에서는 이러한 순차 업데이트도 수렴할 수 있지만, 위 식으로 정의한 배치 경사하강법과는 동일하지 않습니다.
이전 실험에서는 $w$를 먼저 갱신하고 기울기를 다시 계산한 뒤 $b$를 갱신했습니다. 이 순차 업데이트가 정해진 반복 횟수에서 우연히 조금 더 낮은 비용을 출력했기 때문에 처음에는 더 나은 방식일 수 있다고 생각했습니다. 그러나 이는 서로 다른 두 알고리즘을 임의의 한 종료 지점에서 비교한 것에 불과합니다. 중간 비용이 낮다는 사실만으로 안정성이나 수렴성이 우수하다고 결론 내릴 수 없습니다. 두 편미분을 먼저 계산하면 구현이 제시한 수식과 일치하며 각 반복의 의미도 명확해집니다.
지역 최솟값에 관한 기존 이해의 수정
처음 정리할 때에는 경사하강법이 지역 최솟값에 갇힐 수 있다는 일반적인 설명을 이 예제에도 연결하고, Adam 같은 최적화 알고리즘을 해결책으로 생각했습니다. 그러나 제곱오차를 사용하는 일반적인 선형회귀의 비용함수는 볼록함수입니다. 따라서 이 문제에는 경사하강법이 잘못 수렴할 열등한 지역 최솟값이 없습니다.
이 예제에서 실제로 중요한 문제는 학습률, 수치의 스케일, 비용 표면의 조건수입니다. 신경망의 비볼록 목적함수에서는 최적화 지형이 더 복잡해지지만, 이는 선형회귀의 발산이나 느린 수렴을 설명하는 문제와 구분해야 합니다.
4. NumPy 구현
다음 코드는 NumPy의 벡터 연산을 이용해 선형회귀 학습 과정을 구현합니다. 전역 변수를 사용하지 않으며, 분석에 필요한 값만 기록합니다.
벡터화하기 전에는 수식을 다음과 같이 반복문으로 거의 그대로 옮길 수 있습니다.
def compute_gradient_loop(x, y, w, b):
m = len(x)
dj_dw = 0.0
dj_db = 0.0
for i in range(m):
error = (w * x[i] + b) - y[i]
dj_dw += error * x[i]
dj_db += error
return dj_dw / m, dj_db / m
이 형태는 각 코드가 합 기호 안의 어떤 항에 대응하는지 직접 확인할 수 있어 학습 과정에서 유용합니다. 대응 관계를 이해한 뒤에는 같은 계산을 NumPy 벡터 연산으로 간결하게 표현할 수 있습니다.
import numpy as np
def predict(x, w, b):
"""1차원 특성 배열에 대한 예측값을 반환합니다."""
return w * x + b
def compute_cost(x, y, w, b):
"""평균제곱오차의 절반을 반환합니다."""
residuals = predict(x, w, b) - y
return np.mean(residuals ** 2) / 2
def compute_gradient(x, y, w, b):
"""w와 b에 대한 비용함수의 편미분을 반환합니다."""
residuals = predict(x, w, b) - y
dj_dw = np.mean(residuals * x)
dj_db = np.mean(residuals)
return dj_dw, dj_db
def gradient_descent(x, y, w_init, b_init, alpha, iterations):
"""배치 경사하강법으로 w와 b를 최적화합니다."""
w = float(w_init)
b = float(b_init)
cost_history = [compute_cost(x, y, w, b)]
parameter_history = [(w, b)]
for _ in range(iterations):
dj_dw, dj_db = compute_gradient(x, y, w, b)
# 두 업데이트 모두 동일한 매개변수 상태의 기울기를 사용합니다.
w -= alpha * dj_dw
b -= alpha * dj_db
cost_history.append(compute_cost(x, y, w, b))
parameter_history.append((w, b))
return w, b, np.asarray(cost_history), np.asarray(parameter_history)
이제 예제 데이터셋으로 모델을 학습합니다.
x_train = np.array([1.0, 2.0])
y_train = np.array([300.0, 500.0])
w, b, cost_history, parameter_history = gradient_descent(
x=x_train,
y=y_train,
w_init=0.0,
b_init=0.0,
alpha=0.02,
iterations=6_000,
)
print(f"w = {w:.6f}")
print(f"b = {b:.6f}")
print(f"cost = {cost_history[-1]:.6e}")
print(f"x=3일 때의 예측값: {predict(3.0, w, b):.2f}")
학습이 진행되면 $w$는 200, $b$는 100에 가까워지고 $x=3$일 때의 예측값은 700에 수렴합니다.

5. 학습률 선택
학습률은 한 번의 업데이트에서 매개변수가 이동하는 크기를 결정합니다.
- $\alpha$가 너무 작으면 안정적으로 학습되지만 수렴 속도가 느립니다.
- 충분히 큰 $\alpha$는 수렴 속도를 높일 수 있습니다.
- $\alpha$가 지나치게 크면 최솟값을 반복해서 지나치며 비용이 발산할 수 있습니다.
다음 비교에서는 모든 실험의 초기값을 $w=0$, $b=0$으로 설정했습니다.
| 학습률 | 반복 횟수 | 관찰 결과 |
|---|---|---|
| 0.001 | 10,000 | 안정적이지만 비교적 느리게 수렴 |
| 0.02 | 6,000 | 이 데이터셋에서 안정적으로 수렴 |
| 0.5 | 300 | 진동하면서 빠르게 수렴 |
| 0.6 | 300 | 발산 |

각 실험의 해석
$\alpha=0.001$에서는 비용이 초반에 빠르게 감소한 뒤 매우 천천히 내려갑니다. 10,000번을 반복해도 정확한 해에 도달하지 못합니다. 비용 그래프의 초반만 확인하면 이 긴 꼬리 구간을 놓치기 쉽습니다.
$\alpha=0.02$에서는 안정성을 유지하면서 한 번에 더 크게 이동합니다. 6,000번 반복한 결과는 대략 다음과 같습니다.
\[w=199.998344,\qquad b=100.002679\] \[J\approx3.62\times10^{-7}\]$\alpha=0.5$에서는 매개변수가 최솟값의 양쪽을 반복해서 오가지만 진동 폭이 점차 줄어들어 수렴합니다. 이 작은 데이터셋에서 빠르게 수렴했다는 이유만으로 0.5를 다른 문제의 기본값으로 사용할 수는 없습니다.
$\alpha=0.6$에서는 최솟값을 지나치는 폭이 매번 더 커집니다. 결국 매개변수와 비용이 계속 증가해 부동소수점으로 안정적으로 표현할 수 없는 상태에 도달합니다. 이는 수렴이 느린 것이 아니라 발산한 것입니다.
이 2차 비용함수에서는 안정성의 경계를 수식으로도 확인할 수 있습니다. 헤시안 행렬은
\[H=\frac{1}{m}X^TX= \begin{bmatrix} 2.5 & 1.5\\ 1.5 & 1.0 \end{bmatrix}\]이며 가장 큰 고윳값은 약 $3.427$입니다. 이 2차 함수에서 경사하강법이 안정적으로 수렴하려면 학습률이
\[0<\alpha<\frac{2}{3.427}\approx0.584\]범위에 있어야 합니다. 따라서 0.5는 경계 안에 있고 0.6은 경계를 벗어났다는 실험 결과를 설명할 수 있습니다. 데이터나 특성의 스케일이 달라지면 이 경계도 달라집니다.
위 수치는 이 데이터셋과 특성의 스케일에서만 유효합니다. 일반적인 기본값으로 사용할 수는 없습니다. 여러 특성의 수치 범위가 크게 다르다면 특성 스케일링을 적용해 비용함수의 형태를 개선하고 학습을 안정화할 수 있습니다.
이 실험으로 알 수 있는 것과 알 수 없는 것
예제에는 두 개의 점만 있고 두 점 모두 하나의 직선 위에 정확히 놓여 있습니다. 따라서 최적화 과정을 따라가기에는 적합하지만 모델의 일반화 성능을 평가할 수는 없습니다. 비용이 0에 가깝다는 것은 학습 데이터에 잘 맞는다는 의미일 뿐입니다. 실제 데이터에서는 별도의 검증 또는 테스트 데이터로 보지 못한 입력에도 관계가 유지되는지 확인해야 합니다.
또한 이 실험은 전처리의 중요성을 보여줍니다. 주택 크기를 1,000제곱피트 단위가 아니라 실제 제곱피트 단위로 입력하면 $x$의 크기와 $J$의 곡률이 크게 달라집니다. 그러면 동일한 숫자의 학습률도 전혀 다른 결과를 낼 수 있습니다.
실제 구현에서는 고정된 반복 횟수만 사용하기보다 비용의 감소량이나 기울기의 크기를 관찰해 수렴 여부를 판단하기도 합니다. 또한 최적화 경로를 분석하거나 시각화할 목적이 없다면 모든 반복의 매개변수를 저장할 필요는 없습니다.
6. 전체 실행 코드
import numpy as np
import matplotlib.pyplot as plt
def predict(x, w, b):
return w * x + b
def compute_cost(x, y, w, b):
residuals = predict(x, w, b) - y
return np.mean(residuals ** 2) / 2
def compute_gradient(x, y, w, b):
residuals = predict(x, w, b) - y
return np.mean(residuals * x), np.mean(residuals)
def gradient_descent(x, y, w_init=0.0, b_init=0.0,
alpha=0.02, iterations=6_000):
w = float(w_init)
b = float(b_init)
costs = [compute_cost(x, y, w, b)]
for _ in range(iterations):
dj_dw, dj_db = compute_gradient(x, y, w, b)
w -= alpha * dj_dw
b -= alpha * dj_db
costs.append(compute_cost(x, y, w, b))
return w, b, np.asarray(costs)
x_train = np.array([1.0, 2.0])
y_train = np.array([300.0, 500.0])
w, b, costs = gradient_descent(x_train, y_train)
print(f"w={w:.6f}, b={b:.6f}, J={costs[-1]:.3e}")
print(f"x=3일 때의 예측값: {predict(3.0, w, b):.2f}")
x_line = np.linspace(0.5, 3.2, 200)
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].scatter(x_train, y_train, label="training data")
axes[0].plot(x_line, predict(x_line, w, b), label="fitted model")
axes[0].set(xlabel="House size (1,000 ft²)", ylabel="Price (1,000 USD)")
axes[0].legend()
axes[1].semilogy(costs)
axes[1].set(xlabel="Iteration", ylabel="Cost J(w, b)")
plt.tight_layout()
plt.show()
7. 정리
일변량 선형회귀는 연속형 목표값을 $\hat{y}=wx+b$로 표현합니다. 모델 학습의 목적은 비용함수를 최소화하는 매개변수를 찾는 것입니다. 제곱오차 비용함수에 경사하강법을 적용하면 $w$와 $b$에 대한 편미분을 계산한 뒤 두 매개변수를 동시에 반복해서 갱신합니다.
간단한 구현이지만 핵심 구조는 더 큰 머신러닝 모델에도 공통으로 적용됩니다.
- 예측함수를 정의합니다.
- 목적함수로 오차를 수치화합니다.
- 목적함수의 기울기를 계산합니다.
- 적절한 학습률로 매개변수를 갱신합니다.
- 수렴 여부를 확인하고 학습된 모델을 평가합니다.
댓글남기기