파이토치로 구현하는 선형회귀

선형회귀는 머신러닝과 딥러닝을 처음 배울 때 반드시 거치는 가장 기본적인 모델이다. 겉으로 보기에는 단순히 직선 하나를 데이터에 맞추는 문제처럼 보이지만, 실제로는 학습의 핵심 요소가 모두 들어 있다. 입력 데이터를 텐서로 만들고, 모델을 정의하고, 예측값과 정답의 차이를 손실 함수로 계산하고, 역전파와 옵티마이저를 통해 파라미터를 업데이트하는 흐름이 그대로 담겨 있기 때문이다.

그래서 선형회귀를 파이토치로 직접 구현해보는 과정은 단순한 입문 예제가 아니라, 이후의 신경망 학습 코드를 이해하기 위한 가장 좋은 출발점이 된다. CNN이든 RNN이든 Transformer든 결국 공통적으로 따르는 구조는 같다. 예측하고, 오차를 계산하고, 그 오차를 줄이는 방향으로 파라미터를 수정하는 반복이다.

이번 글에서는 파이토치로 단항 선형회귀와 다중 선형회귀를 구현하는 과정을 정리하면서, 코드가 어떤 수학적 의미를 가지는지까지 함께 살펴본다.


1. 선형회귀란 무엇인가

선형회귀는 입력과 출력 사이의 관계를 선형식으로 표현하는 모델이다. 가장 기본적인 형태는 다음과 같다.

[
y = Wx + b
]

여기서
$x$는 입력값,
$W$는 가중치,
$b$는 편향,
$y$는 출력값이다.

조금 더 정확히 말하면, 모델은 입력값 $x$를 받아 예측값 $\hat{y}$를 만든다.

[
\hat{y} = Wx + b
]

이때 모델의 목표는 데이터에 가장 잘 맞는 $W$와 $b$를 찾는 것이다.
즉, 주어진 데이터의 경향을 가장 잘 설명하는 직선을 학습하는 과정이라고 볼 수 있다.

예를 들어 입력이 1, 2, 3이고 정답이 2, 4, 6이라면 사람 눈으로는 거의 $y = 2x$라는 관계가 바로 보인다. 하지만 모델은 처음부터 이 관계를 아는 것이 아니다. 초기에는 임의의 가중치와 편향으로 시작하고, 반복적인 학습을 거치면서 점차 이 관계에 가까워진다.


2. 왜 선형회귀가 중요한가

선형회귀가 중요한 이유는 단순해서가 아니라, 학습의 본질을 가장 작은 예제로 보여주기 때문이다.

파이토치 기반의 딥러닝 학습은 대체로 다음 순서를 따른다.

  1. 입력 데이터를 모델에 넣어 예측값을 만든다.
  2. 예측값과 정답을 비교해 손실을 계산한다.
  3. 손실을 기준으로 gradient를 구한다.
  4. gradient를 이용해 파라미터를 갱신한다.
  5. 이 과정을 반복한다.

선형회귀는 이 전체 흐름을 가장 직관적으로 보여준다.
즉, 선형회귀를 제대로 이해하면 이후에 나오는 더 복잡한 모델도 구조적으로 쉽게 받아들일 수 있다.

여기서 한 가지 중요한 포인트는, 선형회귀 예제가 단순한 수식 설명으로 끝나지 않는다는 점이다. 실제로는 텐서의 shape, 자동미분, 손실 함수의 의미, 학습률의 역할 같은 핵심 개념까지 모두 연결된다.


3. 학습 데이터 준비와 텐서 shape의 의미

단항 선형회귀에서는 보통 다음과 같은 데이터를 사용한다.

x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])

이 데이터에서 중요한 것은 숫자 자체도 맞지만, 텐서의 모양이 더 중요하다는 점이다.

입력 텐서 x_train의 shape은 $(3, 1)$이다.
정답 텐서 y_train의 shape도 $(3, 1)$이다.

이는 각각 다음을 의미한다.

  • 샘플 수는 3개
  • 입력 특성 수는 1개
  • 출력도 각 샘플마다 1개

즉, 단순히 숫자 세 개를 넣은 것이 아니라, 샘플 3개와 특성 1개를 가진 2차원 텐서로 표현한 것이다.

이 부분은 초보자가 자주 놓치는 디테일이다. 파이토치의 선형 계층은 마지막 차원을 기준으로 입력 특성 수를 해석하기 때문에, 입력이 하나여도 shape을 단순한 1차원 벡터가 아니라 $(N, 1)$ 형태로 맞춰주는 것이 중요하다.

정답 텐서도 마찬가지다. 모델의 출력이 $(3, 1)$로 나오기 때문에 정답도 같은 shape으로 맞춰야 손실 계산이 자연스럽게 이루어진다.

여기서 해보면 좋은 심화 예제

입력 텐서를 다음처럼 1차원으로 바꿔보고 어떤 문제가 생기는지 확인해보면 좋다.

x_train = torch.FloatTensor([1, 2, 3])

이렇게 했을 때 모델 입력과 shape이 어떻게 달라지는지, 그리고 왜 2차원 형태가 더 일관적인지 직접 확인해보면 텐서 구조에 대한 감각이 빨리 생긴다.


4. 파이토치에서 선형 모델 정의하기

파이토치에서는 선형회귀 모델을 다음처럼 만들 수 있다.

model = nn.Linear(1, 1)

이 코드는 입력 특성이 1개이고 출력 특성도 1개인 선형 계층을 의미한다.
수식으로는 다음 연산을 수행한다.

[
\hat{y} = Wx + b
]

여기서 학습 대상은 두 개다.

[
W,\quad b
]

즉, 선형회귀의 학습이란 결국 가중치 $W$와 편향 $b$를 데이터에 맞게 조정하는 과정이다.

파이토치에서는 이 파라미터가 자동으로 생성되고, 학습 가능한 형태로 등록된다. 실제로는 다음처럼 확인할 수 있다.

list(model.parameters())

이때 반환되는 값 안에는 가중치와 편향이 들어 있다.
처음 값은 보통 랜덤하게 초기화되기 때문에, 학습 전 예측값은 정답과 다를 가능성이 크다.

이것도 중요한 포인트다.
모델은 처음부터 정답을 알고 있는 것이 아니라, 손실을 줄여가는 과정 속에서 조금씩 배워간다.


5. 예측값은 어떻게 계산되는가

모델에 입력을 넣으면 예측값이 생성된다.

y_pred = model(x_train)

이 한 줄은 단순해 보이지만, 내부적으로는 모든 샘플에 대해 다음 연산이 수행된다.

[
\hat{y}^{(i)} = Wx^{(i)} + b
]

예를 들어 현재 가중치가 0.5이고 편향이 0.1이라고 가정해보자.

[
W = 0.5,\quad b = 0.1
]

그러면 입력 1, 2, 3에 대한 예측값은 다음과 같다.

[
\hat{y}_1 = 0.5 \cdot 1 + 0.1 = 0.6
]

[
\hat{y}_2 = 0.5 \cdot 2 + 0.1 = 1.1
]

[
\hat{y}_3 = 0.5 \cdot 3 + 0.1 = 1.6
]

하지만 실제 정답은 2, 4, 6이다.
즉, 현재 모델은 많이 틀린 예측을 하고 있는 상태다.

이제 필요한 것은 이 차이를 수치로 나타내는 기준이다. 그 역할을 하는 것이 손실 함수다.


6. 손실 함수와 평균제곱오차

회귀 문제에서 가장 대표적으로 사용하는 손실 함수는 평균제곱오차다.

[
MSE = \frac{1}{n}\sum_{i=1}^{n}(\hat{y}_i - y_i)^2
]

여기서
$\hat{y}_i$는 예측값,
$y_i$는 실제 정답,
$n$은 전체 샘플 수다.

즉, 예측값과 정답의 차이를 제곱한 뒤 평균을 내는 방식이다.

파이토치에서는 다음처럼 사용한다.

loss = nn.MSELoss()(y_pred, y_train)

예를 들어 예측값이 0.6, 1.1, 1.6이고 정답이 2, 4, 6이라면 손실은 다음처럼 계산된다.

[
MSE = \frac{(0.6-2)^2 + (1.1-4)^2 + (1.6-6)^2}{3}
]

[
= \frac{1.96 + 8.41 + 19.36}{3}
]

[
= \frac{29.73}{3}
]

[
= 9.91
]

손실 값이 크다는 것은 모델의 예측이 많이 틀렸다는 뜻이다.
학습의 목표는 이 손실 값을 줄여나가는 것이다.

왜 제곱을 하는가

평균제곱오차에서 오차를 제곱하는 이유는 두 가지다.

첫째, 양수와 음수 오차가 상쇄되는 것을 막기 위해서다.
예를 들어 어떤 샘플은 2만큼 크게 예측하고, 다른 샘플은 2만큼 작게 예측하면 단순 합은 0이 될 수 있다. 하지만 둘 다 틀린 예측이다. 제곱을 하면 이런 상쇄가 사라진다.

둘째, 큰 오차에 더 큰 패널티를 주기 위해서다.
오차가 1이면 제곱해서 1이지만, 오차가 3이면 제곱해서 9가 된다. 큰 실수를 더 강하게 반영할 수 있다.

여기서 해보면 좋은 심화 예제

평균제곱오차 대신 평균절대오차를 직접 구현해서 비교해보면 좋다.

[
MAE = \frac{1}{n}\sum_{i=1}^{n} |\hat{y}_i - y_i|
]

같은 데이터에서 MSE와 MAE가 큰 오차를 얼마나 다르게 반영하는지 직접 계산해보면 손실 함수의 성격 차이가 더 잘 보인다.


7. 경사하강법과 파라미터 업데이트

손실이 계산되었다면 이제 해야 할 일은 명확하다.
이 손실을 줄이는 방향으로 파라미터를 수정해야 한다.

이때 사용하는 방법이 경사하강법이다.

가중치 $W$에 대한 업데이트 식은 다음처럼 쓸 수 있다.

[
W \leftarrow W - \alpha \frac{\partial L}{\partial W}
]

편향 $b$도 같은 방식으로 업데이트된다.

[
b \leftarrow b - \alpha \frac{\partial L}{\partial b}
]

여기서
$L$은 손실 함수,
$\alpha$는 학습률,
$\frac{\partial L}{\partial W}$와 $\frac{\partial L}{\partial b}$는 각각 손실을 가중치와 편향으로 미분한 gradient다.

직관적으로 보면, gradient는 손실이 어느 방향으로 증가하는지를 알려주는 값이다.
그래서 손실을 줄이고 싶다면 그 반대 방향으로 이동해야 한다.

즉, 학습은 현재 파라미터에서 손실이 줄어드는 방향으로 조금씩 이동하는 반복 과정이다.


8. 학습률은 왜 중요한가

학습률은 한 번의 업데이트에서 파라미터를 얼마나 이동시킬지를 결정하는 값이다.

파이토치에서는 보통 다음처럼 지정한다.

optimizer = optim.SGD(model.parameters(), lr=0.01)

여기서 0.01이 바로 학습률이다.

학습률이 중요한 이유는 다음과 같다.

  • 너무 크면 최솟값을 지나쳐 버리거나 발산할 수 있다.
  • 너무 작으면 손실은 줄어들더라도 학습 속도가 지나치게 느려진다.

이걸 산을 내려가는 비유로 생각하면 이해가 쉽다.
너무 크게 점프하면 낮은 지점을 지나쳐 버릴 수 있고, 너무 조금씩 움직이면 내려가는 데 시간이 너무 오래 걸린다.

초보자가 자주 놓치는 부분 중 하나는 손실이 잘 줄지 않을 때 모델 구조만 의심한다는 점이다. 하지만 실제로는 학습률 설정이 문제인 경우도 많다.

여기서 해보면 좋은 심화 예제

같은 데이터에 대해 학습률을 0.1, 0.01, 0.001로 바꿔가며 손실 곡선을 비교해보면 좋다.
어떤 경우는 너무 크게 흔들리고, 어떤 경우는 지나치게 천천히 감소하는 모습을 직접 볼 수 있다.


9. 파이토치 학습 코드의 핵심 3줄

파이토치 학습에서는 아래 세 줄이 핵심이다.

optimizer.zero_grad()
loss.backward()
optimizer.step()

이 세 줄은 매우 자주 보이지만, 각각의 의미를 정확히 이해해야 한다.

1) optimizer.zero_grad()

이전 단계에서 계산된 gradient를 초기화한다.

파이토치는 gradient를 기본적으로 누적하는 방식이기 때문에, 이전 값을 지우지 않으면 새로 계산된 gradient 위에 계속 더해진다. 따라서 매 반복마다 초기화가 필요하다.

2) loss.backward()

손실을 기준으로 각 파라미터의 gradient를 계산한다.
즉, 파이토치의 자동미분 기능이 계산 그래프를 따라가며 다음 값들을 구해준다.

[
\frac{\partial L}{\partial W},\quad \frac{\partial L}{\partial b}
]

선형회귀처럼 간단한 경우는 손으로도 미분할 수 있지만, 딥러닝 모델이 깊어질수록 수동 계산은 사실상 불가능해진다. 그래서 자동미분이 핵심 역할을 한다.

3) optimizer.step()

계산된 gradient를 바탕으로 실제 파라미터를 갱신한다.
즉, 이 시점에서 비로소 $W$와 $b$가 바뀐다.

이 세 줄은 선형회귀에만 쓰이는 것이 아니라, 거의 모든 파이토치 학습 코드의 핵심 패턴이다.


10. 전체 학습 루프의 구조

선형회귀의 학습 루프는 보통 다음 형태를 가진다.

epochs = 1000

for epoch in range(epochs + 1):
    y_pred = model(x_train)
    loss = nn.MSELoss()(y_pred, y_train)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if epoch % 100 == 0:
        print(f"Epoch: {epoch}/{epochs} Loss: {loss.item():.6f}")

이 코드는 아주 짧지만, 학습의 본질이 모두 들어 있다.

먼저 현재 파라미터로 예측값을 만든다.
그다음 예측값과 정답을 비교해 손실을 계산한다.
이후 gradient를 초기화하고, 손실을 기준으로 미분을 수행하고, 마지막으로 파라미터를 업데이트한다.

이 과정을 여러 번 반복하면 모델은 점점 데이터에 잘 맞는 방향으로 이동한다.

epoch는 무엇인가

epoch는 전체 학습 데이터를 한 번 모두 사용한 횟수다.
이 예제에서는 데이터가 아주 작아서 한 번의 forward 과정에서 전체 데이터가 모두 들어간다. 따라서 반복문 한 바퀴가 곧 한 epoch가 된다.

데이터가 적다고 해서 한 번만 학습하는 것은 아니다.
한 번의 업데이트만으로는 최적의 가중치를 찾기 어렵기 때문에 여러 번 반복하면서 점차 오차를 줄여간다.


11. 학습 전과 학습 후의 차이

학습이 끝난 뒤에는 가중치와 편향이 데이터에 맞게 조정된다.
예를 들어 데이터가 다음 관계를 가진다면

[
(1, 2),\quad (2, 4),\quad (3, 6)
]

이상적으로는

[
W \approx 2,\quad b \approx 0
]

에 가까워질 것이다.

그러면 새로운 입력 5를 넣었을 때 예측값은 다음처럼 계산된다.

[
\hat{y} = 2 \cdot 5 + 0 = 10
]

실제 학습 결과는 정확히 10이 아니라 9.99 또는 10.01처럼 근사한 값이 나올 수도 있다.
이것은 이상한 것이 아니라, 옵티마이저가 수치적으로 조금씩 접근하는 방식으로 학습하기 때문에 자연스러운 결과다.

이 부분도 의외로 놓치기 쉬운 디테일이다.
모델이 잘 학습되었다고 해서 반드시 딱 떨어지는 정수 값만 나오는 것은 아니다.

여기서 해보면 좋은 심화 예제

학습 전 예측값과 학습 후 예측값을 직접 출력해 비교해보는 습관을 들이면 좋다.

with torch.no_grad():
    print(model(x_train))

손실 값만 보는 것보다, 실제 예측이 어떻게 바뀌었는지를 확인하는 것이 학습 과정을 더 직관적으로 이해하는 데 도움이 된다.


12. 다중 선형회귀로 확장하기

단항 선형회귀는 입력 특성이 1개인 경우다.
하지만 실제 데이터는 대개 여러 특성을 동시에 사용한다.

다중 선형회귀의 수식은 다음처럼 확장된다.

[
y = W_1x_1 + W_2x_2 + W_3x_3 + b
]

즉, 각 입력 특성마다 별도의 가중치가 존재한다.

예를 들어 입력 데이터가 다음과 같다고 하자.

X_train = torch.FloatTensor([
    [73, 80, 75],
    [93, 88, 93],
    [89, 91, 90],
    [96, 98, 100],
    [73, 66, 70],
    [85, 90, 88],
    [78, 85, 82]
])

y_train = torch.FloatTensor([
    [152], [185], [180], [196], [142], [175], [155]
])

이 경우 입력 텐서의 shape은 $(7, 3)$이다.

  • 샘플 수는 7
  • 입력 특성 수는 3

따라서 모델은 다음처럼 정의해야 한다.

model = nn.Linear(3, 1)

즉, 입력 3개를 받아 출력 1개를 만드는 선형 계층이다.


13. 다중 선형회귀를 행렬로 이해하기

다중 선형회귀는 행렬 관점에서 보면 더 명확하다.

[
\hat{Y} = XW + b
]

여기서

  • $X$는 입력 행렬, shape은 $(N, d)$
  • $W$는 가중치 행렬, shape은 $(d, 1)$
  • $b$는 편향
  • $\hat{Y}$는 예측값, shape은 $(N, 1)$

즉, 입력 특성이 하나일 때는 단순한 직선처럼 보이지만, 특성이 여러 개가 되면 각 입력 방향에 대한 가중치를 한꺼번에 학습하는 구조가 된다.

이 때문에 다중 선형회귀에서는 텐서 shape 감각이 특히 중요하다.
입력이 3개라는 말은 단순히 숫자가 3개라는 뜻이 아니라, 모델이 기대하는 마지막 차원의 크기가 3이라는 뜻이다.


14. 왜 다중 선형회귀에서는 Adam을 자주 쓰는가

다중 선형회귀에서는 SGD 대신 Adam을 사용하는 경우도 많다.

optimizer = optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.MSELoss()

Adam은 대표적인 적응형 옵티마이저로, 기본 SGD보다 학습이 더 안정적으로 진행되는 경우가 많다.
특히 초기 학습 속도가 빠르고, 파라미터마다 업데이트 크기를 다르게 조절해준다는 장점이 있다.

입문 단계에서는 다음처럼 이해하면 충분하다.

  • SGD는 같은 규칙으로 단순하게 이동하는 방식
  • Adam은 최근 gradient의 흐름과 크기를 반영해 좀 더 안정적으로 이동하는 방식

물론 선형회귀의 원리를 배우는 데는 SGD만으로도 충분하다. 하지만 실제 실습에서는 Adam을 함께 경험해보는 것이 이후의 딥러닝 학습에도 도움이 된다.

여기서 해보면 좋은 심화 예제

같은 다중 선형회귀 데이터에 대해 SGD와 Adam을 각각 적용해보고, epoch별 손실 감소 양상을 비교해보면 좋다.
같은 모델이라도 옵티마이저에 따라 수렴 속도와 안정성이 어떻게 달라지는지 직접 볼 수 있다.


15. 단항 선형회귀와 다중 선형회귀의 공통점

단항 선형회귀와 다중 선형회귀는 겉보기에는 다르지만 본질은 같다.

공통적으로 다음 구조를 따른다.

  • 선형 계층으로 예측값 생성
  • 평균제곱오차로 손실 계산
  • gradient 초기화
  • 역전파 수행
  • 옵티마이저로 파라미터 갱신

차이점은 입력 특성 수와 그에 따른 가중치 개수뿐이다.

즉, 다중 선형회귀는 새로운 개념이라기보다, 단항 선형회귀를 여러 입력 특성으로 확장한 형태라고 보는 것이 맞다.


16. 이 실습에서 놓치기 쉬운 디테일

이번 구현에서 꼭 짚고 넘어가야 할 디테일을 따로 정리하면 다음과 같다.

FloatTensor를 사용하는 이유

선형 계층과 손실 함수는 보통 부동소수점 연산을 전제로 한다.
그래서 입력과 정답을 실수형 텐서로 만드는 것이 자연스럽다.

정답도 2차원 형태로 맞추는 이유

모델 출력이 $(N, 1)$이기 때문에 정답도 같은 형태를 맞춰주는 것이 좋다.
shape이 어긋나면 불필요한 broadcasting이 일어나거나 경고가 발생할 수 있다.

loss.item()을 사용하는 이유

손실은 텐서 형태로 계산된다.
이를 출력할 때는 파이썬 숫자로 바꾸기 위해 item을 사용한다.

zero_grad를 빼먹지 말아야 하는 이유

gradient는 기본적으로 누적되기 때문에, 초기화를 하지 않으면 이전 step의 정보가 계속 남아 학습이 이상해질 수 있다.

학습과 추론을 구분하는 습관

학습이 끝난 뒤 예측만 할 때는 gradient 계산이 필요 없다.
그래서 다음처럼 작성하는 습관이 중요하다.

with torch.no_grad():
    pred = model(new_input)

이 습관은 이후 검증 단계, 테스트 단계, 실제 서비스 추론 코드까지 이어지는 기본기다.


17. 선형회귀를 통해 배우는 딥러닝의 본질

선형회귀는 단순한 직선 맞추기 예제를 넘어서, 딥러닝의 핵심 사고방식을 보여준다.

학습의 본질은 결국 다음과 같이 정리할 수 있다.

[
\text{학습} = \text{예측} \rightarrow \text{오차 계산} \rightarrow \text{파라미터 수정의 반복}
]

선형회귀에서는 이 구조가 아주 단순한 형태로 드러난다.
하지만 신경망이 깊어지고 구조가 복잡해져도 본질은 크게 달라지지 않는다.

즉, 선형회귀를 제대로 이해한다는 것은 단지 하나의 모델을 아는 것이 아니라, 앞으로 배우게 될 거의 모든 딥러닝 모델의 공통 뼈대를 이해하는 것과 가깝다.


18. 마무리

파이토치로 구현하는 선형회귀는 가장 작은 예제로 학습의 전체 구조를 보여주는 실습이다.

단항 선형회귀에서는 하나의 입력으로부터 출력을 예측하는 과정을 통해 모델, 손실, 경사하강법의 관계를 이해할 수 있다.
다중 선형회귀에서는 입력 차원이 늘어나더라도 학습 구조 자체는 변하지 않는다는 점을 확인할 수 있다.

이번 구현에서 특히 중요하게 봐야 할 것은 다음이다.

  1. 텐서의 shape은 단순한 형식 문제가 아니라 모델 입력 구조 그 자체다.
  2. 손실 함수는 예측이 얼마나 틀렸는지를 수치화하는 기준이다.
  3. backward는 gradient를 계산하고, step은 파라미터를 갱신한다.
  4. 선형회귀는 단순한 직선 모델이 아니라, 딥러닝 학습 흐름을 이해하는 가장 좋은 출발점이다.

결국 이 예제를 통해 얻어야 하는 가장 큰 이해는 하나다.
모델은 처음부터 정답을 아는 것이 아니라, 오차를 줄이는 방향으로 반복적으로 수정되며 학습된다는 점이다.