[PyTorch] 선형 회귀(Linear Regression) - 2

beaver.zip·2026년 3월 7일
post-thumbnail

경사하강법이란?

  • 경사하강법(Gradient Descent)은 머신러닝의 최적화 알고리즘 중 하나로, 손실 함수에서 모델의 가중치 ww바이어스 bb의 최적 값을 찾기 위해 사용된다.
  • 쉽게 말해, 손실 함수의 "경사(기울기)"를 따라 내려가면서 손실이 최소가 되는 지점을 찾는 알고리즘이다.

작동 방식

경사하강법의 작동 방식을 이해하기 위해 간단한 예시를 살펴본다.

트레이닝 데이터가 (x1,t1)=(1,0.5)(x_1, t_1) = (1, 0.5), (x2,t2)=(2,1)(x_2, t_2) = (2, 1), (x3,t3)=(3,1.5)(x_3, t_3) = (3, 1.5), (x4,t4)=(4,2)(x_4, t_4) = (4, 2)로 주어졌을 때, 가중치 ww에 따라 손실 값이 어떻게 변하는지 확인한다. (편의상 b=0b = 0으로 가정)

l(w,0)=14i=14[ti(wxi)]2l(w, 0) = \frac{1}{4} \sum_{i=1}^{4} [t_i - (w \cdot x_i)]^2

ww손실 l(w,0)l(w, 0)
-0.57.5
01.875
0.50 (최소)
11.875
1.57.5

그래프로 그리면 w=0.5w = 0.5에서 최솟값을 가지는 U자 형태의 포물선이 된다.

경사(기울기)란?

특정 지점 (w,l(w,b))(w, l(w, b))에서의 경사는 손실 함수를 ww에 대해 편미분한 값이다.

경사=l(w,b)w=l(w,b)의 증가량w의 증가량\text{경사} = \frac{\partial l(w,b)}{\partial w} = \frac{l(w,b)\text{의 증가량}}{w\text{의 증가량}}

체인 룰을 적용하면:

l(w,b)w=l(w,b)yyw=1n(2)i=1n(tiyi)xi\frac{\partial l(w,b)}{\partial w} = \frac{\partial l(w,b)}{\partial y} \cdot \frac{\partial y}{\partial w} = \frac{1}{n} \cdot (-2) \sum_{i=1}^{n}(t_i - y_i) \cdot x_i

  • y=wxy = wx이므로 yw=xi\frac{\partial y}{\partial w} = x_i

w=0.5w = -0.5, b=0b = 0을 대입하면:

l(w,b)ww=0.5=14(2)(1+4+9+16)=15\frac{\partial l(w,b)}{\partial w}\bigg|_{w=-0.5} = \frac{1}{4} \cdot (-2)(1 + 4 + 9 + 16) = -15

  • 기울기가 음수ww오른쪽(양의 방향)으로 이동시켜야 손실이 줄어듦

PyTorch에서는 loss.backward()로 자동 미분을 수행하여 기울기를 계산한다.

가중치 업데이트

계산된 기울기를 사용하여 가중치를 업데이트하는 수식:

w=wαl(w,b)ww^* = w - \alpha \frac{\partial l(w,b)}{\partial w}

  • α\alpha: 학습률(learning rate)

w=0.5w = -0.5일 때:

w=(0.5)α(15)=0.5+15αw^* = (-0.5) - \alpha \cdot (-15) = -0.5 + 15\alpha

기울기가 음수이므로, ww는 양의 방향으로 이동한다. 이 과정을 반복하면 ww가 점점 최솟값(0.5)에 가까워진다.

바이어스 bb도 같은 방식으로 최적값을 찾을 수 있다: b=bαl(w,b)bb^* = b - \alpha \frac{\partial l(w,b)}{\partial b}

PyTorch에서는 optimizer.step()으로 가중치를 업데이트하고, 업데이트 전에는 반드시 optimizer.zero_grad()로 이전 기울기를 초기화해야 한다.

학습률 (Learning Rate)

  • 학습률은 가중치가 업데이트되는 크기를 결정하는 하이퍼파라미터임.
  • 너무 크면 최적값을 지나쳐 발산하고, 너무 작으면 수렴이 매우 느려진다.
  • 모델과 데이터에 따라 달라지므로, 결국 시행착오를 거쳐 최적의 값을 찾아야 함.

경사하강법의 한계

1) 대규모 데이터셋의 계산 비용

  • 전체 데이터셋을 사용하여 기울기를 계산하므로, 데이터가 많을수록 계산 비용이 매우 커진다.

2) 로컬 미니마(local minima) 문제

  • 손실 함수가 전역 최소값(global minimum)이 아닌 지역 최소값에 머무를 수 있다.
  • 전체 데이터의 기울기 평균을 사용하므로, 로컬 미니마에 갇힐 가능성이 높음.


확률적 경사하강법 (SGD)

왜 필요한가?

  • 경사하강법은 모든 데이터의 오차를 계산하여 업데이트하므로, 정확하고 안정적이지만 대규모 데이터셋에서 비효율적임.
  • 확률적 경사하강법(Stochastic Gradient Descent)각각의 데이터 포인트마다 오차를 계산하여 wwbb를 업데이트하는 방식이다.
  • 각 데이터 포인트별로 기울기를 계산하므로 기울기에 노이즈가 포함되고, 이 노이즈 덕분에 로컬 미니마를 탈출하기 용이하다.

수식 표현

w=wα(1n(2)(tiyi)xi)w^* = w - \alpha \left( \frac{1}{n} \cdot (-2)(t_i - y_i) \cdot x_i \right)

전체 데이터 합산(\sum)이 아니라 개별 데이터 포인트 (tiyi)xi(t_i - y_i) \cdot x_i로 계산하는 것이 핵심이다.

앞선 예시에서 첫 번째 데이터 포인트 (1,0.5)(1, 0.5)만 사용하면 (α=0.01\alpha = 0.01):

w=(0.5)0.01(14(2)(0.50.5)1)=0.5+0.02=0.48w^* = (-0.5) - 0.01 \cdot \left(\frac{1}{4} \cdot (-2)(0.5 - 0.5) \cdot 1\right) = -0.5 + 0.02 = -0.48

두 번째 데이터 포인트 (2,1)(2, 1)을 사용:

w=(0.48)0.01(14(2)(1.96)2)=0.48+0.0196=0.4604w^* = (-0.48) - 0.01 \cdot \left(\frac{1}{4} \cdot (-2)(1.96) \cdot 2\right) = -0.48 + 0.0196 = -0.4604

이런 식으로 데이터 하나하나를 순회하며 가중치를 업데이트해 나간다.

SGD 코드

import torch.optim as optim

# 손실 함수 및 옵티마이저 정의
loss_function = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
  • optim.SGD: 확률적 경사하강법 옵티마이저
  • model.parameters(): 모델의 학습 가능한 파라미터(ww, bb)를 옵티마이저에 전달
  • lr: 학습률

학습 루프에서의 핵심 3줄:

optimizer.zero_grad()  # 이전 기울기 초기화
loss.backward()        # 역전파로 기울기 계산
optimizer.step()       # 기울기 기반 가중치 업데이트

에폭 (Epoch)

에폭이란?

  • 에폭(epoch)이란 모델이 전체 데이터셋을 한 번 완전히 학습하는 과정을 의미한다.
  • 예를 들어, 데이터셋에 30개의 데이터가 있고 에폭 수가 1이면, 모델은 30개 데이터를 한 번 학습한다.
  • 동일한 데이터셋으로 여러 번 반복 학습하여 모델의 성능을 향상시킨다.
  • 단, 에폭 수가 너무 많으면 과적합(overfitting)이 발생할 수 있다.
    • 과적합: 트레이닝 데이터에 너무 맞춰져서 새로운 데이터에 대한 일반화 능력이 떨어지는 현상

에폭 코드

num_epochs = 1000  # 에폭 수 설정
loss_list = []     # 손실 값 기록용 리스트

for epoch in range(num_epochs):
    y = model(x_tensor)                # 순전파: 예측값 계산
    loss = loss_function(y, t_tensor)  # 손실 계산

    optimizer.zero_grad()   # 기울기 초기화
    loss.backward()         # 역전파
    optimizer.step()        # 가중치 업데이트

    loss_list.append(loss.item())  # 손실 값 기록

    if (epoch+1) % 100 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item()}')
        for name, param in model.named_parameters():
            print(f'{name}: {param.data}')
  • loss.item(): 텐서에서 스칼라 값을 추출
  • model.named_parameters(): 모델의 파라미터 이름과 값을 함께 확인 (디버깅용)

데이터 표준화 (Standardization)

손실 값이 줄어들지 않는 문제

학습을 돌려보면 손실 값이 수천만 단위로 매우 큰 경우가 있다.

일반적으로 손실 값이 클 때는 학습률을 낮추거나, 이상치를 확인하거나, 에폭 수를 늘려볼 수 있다. 하지만 이번 실습에서는 위 세 가지를 적용해도 손실 값이 줄어들지 않았다. 원인은 특징 변수(YearsExperience)와 목표 변수(Salary)의 스케일 차이가 너무 크기 때문이다.

표준화란?

  • 특징 변수와 목표 변수의 값 차이가 클 때, 두 변수의 평균을 0, 분산을 1로 맞추는 전처리 방법이다.
  • 표준화를 수행하면 데이터의 분포 형태는 동일하게 유지되지만, 값의 범위가 변환된다.

표준화 후 학습하면 손실 값이 약 0.043으로 크게 감소한다.

표준화 코드

from sklearn.preprocessing import StandardScaler

# 특징 변수 표준화
scaler_x = StandardScaler()
x_scaled = scaler_x.fit_transform(x.reshape(-1, 1))

# 목표 변수 표준화
scaler_t = StandardScaler()
t_scaled = scaler_t.fit_transform(t.reshape(-1, 1))
  • StandardScaler(): 평균 0, 분산 1로 변환하는 스케일러 객체
  • .fit_transform(): 데이터의 평균/표준편차를 계산(fit)하고 변환(transform)을 동시에 수행
  • .reshape(-1, 1): 1차원 배열을 2차원으로 변환 (StandardScaler는 2차원 입력을 기대)

표준화된 데이터를 Tensor로 변환:

x_tensor = torch.tensor(x_scaled, dtype=torch.float32).view(-1, 1)
t_tensor = torch.tensor(t_scaled, dtype=torch.float32).view(-1, 1)

전체 실습 코드

데이터 로딩 → 표준화 → 모델 정의 → 학습 → 시각화까지의 전체 흐름이다.

import torch
import torch.nn as nn
import torch.optim as optim
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler

# 1. 데이터 로딩
data = pd.read_csv("Salary_dataset.csv", sep=",", header=0)
x = data.iloc[:, 1].values  # YearsExperience
t = data.iloc[:, 2].values  # Salary

# 2. 데이터 표준화
scaler_x = StandardScaler()
x_scaled = scaler_x.fit_transform(x.reshape(-1, 1))

scaler_t = StandardScaler()
t_scaled = scaler_t.fit_transform(t.reshape(-1, 1))

# 3. Tensor 변환
x_tensor = torch.tensor(x_scaled, dtype=torch.float32).view(-1, 1)
t_tensor = torch.tensor(t_scaled, dtype=torch.float32).view(-1, 1)

# 4. 모델 정의
class LinearRegressionModel(nn.Module):
    def __init__(self):
        super(LinearRegressionModel, self).__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        y = self.linear(x)
        return y

model = LinearRegressionModel()

# 5. GPU 지원
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
x_tensor = x_tensor.to(device)
t_tensor = t_tensor.to(device)

# 6. 손실 함수 및 옵티마이저 정의
loss_function = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 7. 학습
num_epochs = 1000
loss_list = []

for epoch in range(num_epochs):
    y = model(x_tensor)
    loss = loss_function(y, t_tensor)

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

    loss_list.append(loss.item())

    if (epoch+1) % 100 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item()}')
        for name, param in model.named_parameters():
            print(f'{name}: {param.data}')

# 8. 손실 값 시각화
plt.figure()
plt.plot(loss_list, label='Train Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.title('Loss Trend')
plt.show()

학습 정리

  • 경사하강법은 손실 함수의 기울기를 따라 내려가면서 최적의 ww, bb를 찾는 알고리즘이다.
  • 확률적 경사하강법(SGD)은 각 데이터 포인트마다 기울기를 계산하므로, 대규모 데이터에서 효율적이고 로컬 미니마 탈출에 용이하다.
  • 에폭은 전체 데이터셋을 한 번 완전히 학습하는 과정이며, 너무 많으면 과적합이 발생할 수 있다.
  • 표준화는 특징 변수와 목표 변수의 스케일 차이가 클 때 평균 0, 분산 1로 맞추어 학습 안정성을 높이는 전처리 기법이다.
profile
NLP 일짱이 되겠다.

0개의 댓글