예제 
이번에는 파이토치를 사용해 더 어려운 문제를 해결하겠습니다. 파이토치가 제공하는 신경망을 활용합니다. 보스턴 집값 데이터는 원래 81개의 특징을 고려한 큰 데이터셋입니다. 우리는 사이킷런(scikit-learn)에서 정제한 14개 특징만을 사용합니다. 결과를 예측하는 데 사용되는 데이터 요소를 '특징(feature)'이라고 부릅니다. 모델은 특징에 가중치를 반영해 결과를 도출합니다. 이번에는 집값만을 예측하므로 출력은 하나입니다.
1.
파이토치에서 모듈은 신경망을 구성하는 기본 객체입니다. 모듈에는 구성 요소를 정의하는 init() 함수와 순전파의 동작을 정의하는 forward() 함수가 있습니다.
2.
간단한 신경망은 nn.Sequential, 복잡한 신경망은 nn.Module을 이용합니다.
3.
회귀는 MSE, 분류는 CE 손실을 이용합니다. MSE(평균 제곱 오차)는 값의 차이를 제곱한 평균이고, CE(크로스 엔트로피)는 두 확률 분포의 차이입니다.
4.
다중분류는 신경망의 입력을 여러 범주로 분류하는 알고리즘입니다.
5.
피처는 신경망의 입력으로 들어오는 값으로 데이터가 갖고 있는 특징입니다. 말 그대로 특징으로도 부릅니다.
6.
배치는 데이터셋의 일부로 신경망의 입력으로 들어가는 단위입니다. 에포크는 전체 데이터를 모두 한 번씩 사용했을 때의 단위입니다. 이터레이션은 하나의 에포크에 들어 있는 배치 수입니다.
7.
최적화 알고리즘은 역전파된 기울기를 이용해 가중치를 수정합니다. Adam은 모멘텀과 RMSprop을 섞어놓은 가장 흔하게 사용되는 최적화 알고리즘입니다. 모멘텀은 기울기를 계산할 때 관성을 고려하는 것이고, RMSprop은 이동평균을 이용해 이전 기울기보다 현재의 기울기에 더 가중치를 두는 알고리즘입니다.
파이토치의 인공 신경망
파이토치는 딥러닝에 사용되는 대부분의 신경망을 torch.nn 모듈에 모아놨습니다. 앞에서 딥러닝은 신경망 층을 깊게 쌓아올린다고 말씀드렸는데, 파이토치도 여러 층을 쌓아서 딥러닝 모델을 만듭니다. 층(layer)은 nn.Module 객체를 상속받아 정의되고, 층이 쌓이면 딥러닝 모델이 완성됩니다. 프로그래밍 초보자라면 '객체를 상속받는다'는 말이 지금은 조금 어려울 수 있는데, 'nn.Module의 모든 구성요소를 복사해오는 거다'라고 생각하면 됩니다. 부모의 재산을 자식이 그대로 물려받는 것처럼요.
3.2.3 모델 정의 및 학습하기
이번에 사용할 알고리즘은 선형회귀입니다. 선형회귀는 직선을 그리는 방식으로 미지의 값을 예측하는 가장 간단한 방법입니다. 회귀에 의해 얻은 결과를 실제 데이터와 비교해 오차를 줄여나가는 형식으로 학습합니다. 이때 오차의 제곱에 대한 평균을 취하는 평균 제곱 오차(Mean Square Error, MSE)를 사용합니다. 평균 제곱 오차를 사용하면 작은 오차와 큰 오차를 강하게 대비시킬 수 있어 유용합니다.
▼ 학습 루프
3.1절 '사인 함수 예측하기'에서는 직접 함수를 만들어 변수를 지정해줬지만, 이번에는 파이토치에서 제공하는 함수를 활용하겠습니다. torch.nn.Sequential() 객체에 모듈(여기서는 선형 회귀 모듈)을 집어넣어주면 파이토치가 알아서 순서대로 계산합니다. 선형 회귀 모델이므로 파이토치의 nn.Linear 모듈을 이용하면 됩니다. 다음 그림은 선형회귀에 이용할 다층 신경망, 즉 MLP 모델을 나타낸 그림입니다. MLP 층은 각 층의 뉴런이 다음 층의 모든 뉴런과 연결되어 있기 때문에 전연결층(fully connected layer, FC)이라고도 부릅니다.
▼ 선형회귀 MLP 모델
❶ 먼저 입력층에 입력 데이터가 들어옵니다.
❷ 입력 데이터는 은닉층의 입력으로 들어가 특징으로부터 정보를 추출합니다.
❸ 출력층의 예측값과 실제 정답을 비교해 손실을 계산합니다. 정보가 입력층부터 출력층까지 흘러갔기 때문에 이런 형태를 정보가 순전파됐다고 부릅니다.
❹ 손실을 계산했으면 가중치를 수정하기 위해 오차를 역전파합니다.
신경망을 만들려면 배치와 에포크 개념을 알아야 합니다. 컴퓨터의 메모리는 한정되어 있기 때문에 모든 데이터를 한 번에 처리할 수 없습니다. 따라서 전체 데이터를 나눠서 학습합니다. 이때 떼어서 학습하는 단위가 배치입니다. '배치 크기'만큼 학습해서 전체 데이터를 모두 학습하면 '1에포크'를 학습했다고 부릅니다. 예를 들어 데이터가 총 1,000개일 때 배치 크기가 100이면, 배치가 10번 반복되어야 데이터 1,000개를 전부 사용합니다. 이를 1에포크의 학습이 이루어졌다고 합니다. 100에포크를 학습한다면 데이터 1,000개 모두를 사용하는 학습을 100번 반복하는 겁니다. 이때 반복 횟수를 이터레이션이라고 부릅니다.
▼ 배치, 에포크, 이터레이션
앞에서 설계한 학습 루프대로 학습 코드를 구현해봅시다.
▼ 선형회귀 MLP 모델 설계
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from torch.optim import Adam
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 1) 데이터 로드
housing = fetch_california_housing()
X = housing.data
y = housing.target.reshape(-1, 1) # (N,1)로 맞춤
# 2) train/val 분리
X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
# 3) 표준화(입력은 필수, 타깃은 선택)
x_scaler = StandardScaler().fit(X_tr)
X_tr = x_scaler.transform(X_tr)
X_val = x_scaler.transform(X_val)
# (선택) 타깃도 표준화하면 수렴이 더 안정적일 수 있음
# y_scaler = StandardScaler().fit(y_tr)
# y_tr = y_scaler.transform(y_tr); y_val = y_scaler.transform(y_val)
# 4) 텐서/데이터로더
X_tr_t = torch.from_numpy(X_tr).float()
y_tr_t = torch.from_numpy(y_tr).float()
X_val_t = torch.from_numpy(X_val).float()
y_val_t = torch.from_numpy(y_val).float()
train_loader = DataLoader(TensorDataset(X_tr_t, y_tr_t), batch_size=128, shuffle=True)
val_loader = DataLoader(TensorDataset(X_val_t, y_val_t), batch_size=256, shuffle=False)
# 5) 모델
model = nn.Sequential(
nn.Linear(X_tr.shape[1], 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
criterion = nn.MSELoss()
optim = Adam(model.parameters(), lr=1e-3)
# 6) 학습 루프(에포크 ‘평균 손실’ 로깅)
for epoch in range(200):
model.train()
tr_loss_sum, n_tr = 0.0, 0
for xb, yb in train_loader:
optim.zero_grad()
preds = model(xb) # (B,1)
loss = criterion(preds, yb) # (B,1) vs (B,1)
loss.backward()
optim.step()
tr_loss_sum += loss.item() * xb.size(0)
n_tr += xb.size(0)
tr_loss = tr_loss_sum / n_tr
# 검증
model.eval()
with torch.no_grad():
val_loss_sum, n_val = 0.0, 0
for xb, yb in val_loader:
preds = model(xb)
vloss = criterion(preds, yb)
val_loss_sum += vloss.item() * xb.size(0)
n_val += xb.size(0)
val_loss = val_loss_sum / n_val
if epoch % 20 == 0:
print(f"epoch {epoch:3d} | train MSE: {tr_loss:.4f} | val MSE: {val_loss:.4f}")
# 7) 예측(스케일 되돌리기 선택)
model.eval()
with torch.no_grad():
one_pred = model(X_val_t[:1]).item()
one_real = y_val_t[:1].item()
# 만약 y를 표준화했다면 역변환 필요:
# one_pred = y_scaler.inverse_transform([[one_pred]])[0][0]
# one_real = y_scaler.inverse_transform([[one_real]])[0][0]
print(f"예측값: {one_pred:.2f}, 실제값: {one_real:.2f}")
Python
복사
출력결과
epoch 0 | train MSE: 1.4312 | val MSE: 0.6208
epoch 20 | train MSE: 0.2925 | val MSE: 0.3057
epoch 40 | train MSE: 0.2632 | val MSE: 0.2803
epoch 60 | train MSE: 0.2518 | val MSE: 0.2765
epoch 80 | train MSE: 0.2426 | val MSE: 0.2693
epoch 100 | train MSE: 0.2358 | val MSE: 0.2750
epoch 120 | train MSE: 0.2279 | val MSE: 0.2705
epoch 140 | train MSE: 0.2194 | val MSE: 0.2645
epoch 160 | train MSE: 0.2153 | val MSE: 0.2666
epoch 180 | train MSE: 0.2046 | val MSE: 0.2649
예측값: 0.49, 실제값: 0.48
Plain Text
복사
파이프라인은 데이터 로드 → 학습/검증 분리 → 입력 표준화 → 텐서화 & DataLoader → 모델 정의 → 학습 루프(평균 손실 로깅) → 검증 → 단일 샘플 예측 순서로 진행됩니다.
❶ 경고 억제 & 주요 라이브러리 임포트
import warnings; warnings.filterwarnings('ignore')
import pandas as pd
import torch, torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from torch.optim import Adam
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
Python
복사
•
warnings.filterwarnings('ignore'): 디프리케이션/경고 출력으로 노트북/콘솔 지저분해지는 것을 방지.
•
torch, nn, DataLoader: 모델·학습 루프·배치 생성의 핵심.
•
StandardScaler: 입력 스케일 맞춤(평균 0, 표준편차 1) → MLP 수렴성 대폭 개선.
❷ 데이터 로드 & 타깃 모양 맞추기
housing = fetch_california_housing()
X = housing.data # shape: (N, 8)
y = housing.target.reshape(-1, 1) # shape: (N, 1)
Python
복사
•
X: 8개 특징(방 개수, 소득 등). 실수형 연속값.
•
y: 주택 중위가(단위: $100,000). 회귀니까 (N,1) 열벡터로 맞춰 브로드캐스팅 문제 예방.
◦
MSE 계산 시 (B,1) vs (B,1)로 정렬되어야 안전해.
참고: pd.DataFrame(housing.data, columns=housing.feature_names)로 만들면 컬럼명을 사용한 전처리/EDA도 쉬움.
❸ 학습/검증 분리 (재현성 포함)
X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
Python
복사
•
검증셋 20% 확보로 일반화 성능을 모니터링.
•
random_state=42로 연구/디버깅 재현성 보장 (동일 분할).
❹ 입력 표준화(필수), 타깃 표준화(선택)
x_scaler = StandardScaler().fit(X_tr)
X_tr = x_scaler.transform(X_tr)
X_val = x_scaler.transform(X_val)
# (선택) y도 표준화하면 수렴이 더 안정될 수 있음 (예측 해석 시 역변환 필요)
# y_scaler = StandardScaler().fit(y_tr)
# y_tr = y_scaler.transform(y_tr); y_val = y_scaler.transform(y_val)
Python
복사
•
왜 필요한가?
◦
MLP의 선형층은 (z = xW + b). 입력 스케일이 제각각이면,
초기 가중치 분포 대비 활성화가 포화되거나 기울기 스케일이 불안정해져 수렴이 느려지거나 발산.
•
언제 y도 표준화?
◦
타깃 분포의 스케일이 크거나 왜도가 심할 때.
◦
단, 추론 후 inverse_transform으로 원 스케일로 복원해야 해.
❺ 텐서화 & DataLoader 구성
X_tr_t = torch.from_numpy(X_tr).float()
y_tr_t = torch.from_numpy(y_tr).float()
X_val_t = torch.from_numpy(X_val).float()
y_val_t = torch.from_numpy(y_val).float()
train_loader = DataLoader(TensorDataset(X_tr_t, y_tr_t), batch_size=128, shuffle=True)
val_loader = DataLoader(TensorDataset(X_val_t, y_val_t), batch_size=256, shuffle=False)
Python
복사
•
TensorDataset: (입력, 타깃) 쌍을 인덱싱 가능하게 래핑.
•
DataLoader:
◦
학습: shuffle=True → 배치 구성이 매 에포크 섞여 일반화에 도움.
◦
검증: 순서 무관이므로 shuffle=False, 큰 배치로 빠르게.
❻ 모델 정의 (MLP: 8→128→64→1)
model = nn.Sequential(
nn.Linear(X_tr.shape[1], 128), # 8 → 128
nn.ReLU(),
nn.Linear(128, 64), # 128 → 64
nn.ReLU(),
nn.Linear(64, 1) # 64 → 1
)
Python
복사
•
선형층(Linear): ( y = xW + b )
◦
첫 층 파라미터 수: (8\times128 + 128 = 1{,}152)
◦
둘째 층: (128\times64 + 64 = 8{,}256)
◦
마지막 층: (64\times1 + 1 = 65)
◦
총 파라미터 ≈ 9,473개. (소형 모델 → 과적합 위험 적당)
•
ReLU: (\max(0, x)). 기울기 소실 완화 & 계산 효율 높음.
배치정규화/드롭아웃은 여기선 생략했지만, 과적합 징후가 보이면 넣을 수 있어.
❼ 손실함수 & 최적화기
criterion = nn.MSELoss()
optim = Adam(model.parameters(), lr=1e-3)
Python
복사
•
MSE: (\text{MSE} = \frac{1}{B}\sum_{i=1}^{B} ( \hat{y}_i - y_i )^2)
•
Adam: 1차·2차 모멘트 추정으로 적응적 학습률 적용.
◦
시작 lr=1e-3은 실전 기본값. 불안정하면 1e-4로 줄여.
❽ 학습 루프 — 에포크 평균 손실 로깅의 정확한 의미
for epoch in range(200):
model.train()
tr_loss_sum, n_tr = 0.0, 0
for xb, yb in train_loader:
optim.zero_grad() # (1) 이전 스텝의 grad 초기화
preds = model(xb) # (2) 순전파
loss = criterion(preds, yb)# (3) 배치 손실
loss.backward() # (4) 역전파 → 모든 파라미터의 grad 계산
optim.step() # (5) Adam 규칙으로 파라미터 갱신
tr_loss_sum += loss.item() * xb.size(0) # 배치 손실×배치 샘플 수
n_tr += xb.size(0)
tr_loss = tr_loss_sum / n_tr # **에포크 평균 train MSE**
Python
복사
•
왜 배치 손실에 배치 크기를 곱해 누적?
◦
각 배치의 평균을 단순 평균하면 배치 크기가 다를 때 가중치가 왜곡.
◦
샘플 수로 가중합 후 전체 샘플 수로 나누면 정확한 에포크 평균이 됨.
❾ 검증 루프 — 일반화 모니터링
model.eval()
with torch.no_grad():
val_loss_sum, n_val = 0.0, 0
for xb, yb in val_loader:
preds = model(xb)
vloss = criterion(preds, yb)
val_loss_sum += vloss.item() * xb.size(0)
n_val += xb.size(0)
val_loss = val_loss_sum / n_val
Python
복사
•
model.eval()은 드롭아웃/배치정규화가 있을 때 추론 모드로 동작하게 함.
•
torch.no_grad()는 추론 시 그래프 생성/메모리 낭비 방지.
•
val_loss가 train_loss와 비슷하거나 약간 높다면 적절.
◦
벌어지면 과적합 신호(정규화/데이터 증대/얼리스탑 고려).
❿ 로깅 (주기 출력)
if epoch % 20 == 0:
print(f"epoch {epoch:3d} | train MSE: {tr_loss:.4f} | val MSE: {val_loss:.4f}")
Python
복사
•
20 에포크마다 학습/검증 MSE 출력 → 수렴/과적합/발산 판단.
•
보통 val MSE 최소 지점을 베스트 모델로 저장하는 게 좋다.
3.2.4 모델 성능 평가하기
학습시키기는 했지만 모델이 정말 예측을 잘하는지 아직 알기 어렵습니다. 모델이 잘 학습됐는지 알아보려면 모델을 시험해야 합니다. 데이터셋에서 행 하나를 추출해 실젯값과 예측값을 비교해봅시다.
▼ 모델 성능 평가
model.eval()
with torch.no_grad():
one_pred = model(X_val_t[:1]).item()
one_real = y_val_t[:1].item()
# y를 표준화했다면 inverse_transform으로 복원 필요
print(f"예측값: {one_pred:.2f}, 실제값: {one_real:.2f}")
Python
복사
<실행 결과>
예측값: 0.49, 실제값: 0.48
실제 집값은 0.48이지만 우리 모델이 예측한 집값은 대략 0.49입니다. 실제 집값과 크게 차이가 나지 않습니다.
예측값 (0.49) | 모델이 학습을 통해 계산한 주택 가격 예측 결과 | "이 집의 중위가격은 약 0.49"라고 모델이 판단한 것 |
실제값 (0.48) | 데이터셋에 기록된 진짜 주택 가격(정답 레이블) | 실제 조사된 가격은 0.48이었다는 뜻 |
파이토치를 이용해 간단한 인공 신경망을 만들어보았습니다. 직접적인 값을 예측하면 회귀 문제, 어느 레이블에 속하는지에 대한 확률 분포를 예측하면 분류 문제입니다. 딥러닝 학습의 흐름을 명심해주세요.
1.
모델의 정의
2.
최적화 함수 정의
3.
모델의 예측과 실제 값의 차이 계산(손실 계산)
4.
오차 역전파
5.
최적화 진행



