STUDENT_ID = "20240000" # 본인 학번으로 바꾼다
NAME = "홍길동" # 본인 이름으로 바꾼다과제 1: 의료비 예측, 한 사이클을 끝까지
과제 안내
실습 4에서 자동차 표 하나로 나누기 → 훈련 통계로 변환 → 미니배치 → 학습 → 러닝커브 → 최적 지점 복원 → 테스트 평가를 따라갔다. 이번에는 다른 표로 같은 사이클을 직접 완성한다. 코드의 뼈대는 주어져 있고, 핵심 부분 여덟 곳이 ____로 비어 있다.
데이터: 미국 의료보험 가입자 1,338명의 나이, 성별, 체질량지수(BMI), 자녀 수, 흡연 여부, 거주 지역과 연간 의료비(charges, 달러). 여섯 변수로 의료비를 예측한다.
대응 이론과 실습: Ch04 학습의 실전, 실습 4. 실습 4의 코드를 옆에 열어 두고 진행한다.
제출 규칙
- 아래 첫 셀의
STUDENT_ID와NAME을 본인 것으로 바꾼다. ____를 모두 채운 뒤 런타임 다시 시작 후 전체 실행으로 위에서부터 끝까지 오류 없이 실행한다.- 변수 이름과 함수 이름을 바꾸지 않는다. 채점은 아래 표의 이름으로 값을 읽어 자동으로 이루어진다.
- 마지막 “제출 전 확인” 셀이
통과를 출력하는지 확인한다. - 출력이 포함된 노트북을
hw01_학번.ipynb로 내려받아(파일 → 다운로드 → .ipynb) 제출한다.
| 이름 | 무엇인가 |
|---|---|
train_df, val_df, test_df |
나눈 세 표 |
mu, sd |
훈련 세트의 숫자형 열 평균과 표준편차 |
prep, train_p, val_p, test_p |
전처리 함수와 그 결과 |
X_tr, y_tr, X_va, y_va, X_te, y_te |
텐서 |
train_loader, val_loader, test_loader |
데이터 로더 |
train_one_epoch, evaluate |
학습 함수, 평가 함수 |
model, criterion, optimizer |
모델, 손실, 갱신 도구 |
history, best_val, best_epoch, best_state |
러닝커브 기록과 최적 지점 |
test_rmse, baseline_rmse |
테스트 RMSE와 기준선 RMSE |
채점 기준은 실습 4에서 배운 절차를 그대로 지켰는가이다. 값이 맞더라도 절차가 틀리면(예: 전체 표로 평균을 구함) 해당 항목은 점수를 받지 못한다.
1. 표 읽기 — 제공 코드
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import copy
URL = 'https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/insurance.csv'
insurance = pd.read_csv(URL)
print(insurance.shape)
insurance.head()print(insurance.dtypes)
for c in ['sex', 'smoker', 'region']:
print(c, sorted(insurance[c].unique()))숫자형 열은 age, bmi, children이고 범주형 열은 sex, smoker, region이다. 종속변수는 charges다. 범주형 열의 값은 아래 CATS에 고정해 둔다.
num_cols = ['age', 'bmi', 'children']
CATS = {
'sex': ['female', 'male'],
'smoker': ['no', 'yes'],
'region': ['northeast', 'northwest', 'southeast', 'southwest'],
}2. 훈련·검증·테스트로 나누기 — 채워 넣기 ①
train_test_split을 두 번 써서 70 / 15 / 15로 나눈다. 두 번 모두 random_state=42를 쓴다.
from sklearn.model_selection import train_test_split
train_df, rest_df = train_test_split(insurance, test_size=____, random_state=42)
val_df, test_df = train_test_split(rest_df, test_size=____, random_state=42)
print(len(train_df), len(val_df), len(test_df))3. 훈련 통계로 표준화하고 원-핫으로 바꾸기
3.1 평균과 표준편차 — 채워 넣기 ②
숫자형 열의 평균과 표준편차를 구한다. 어느 세트에서 구해야 하는지가 이 문제의 전부다. 표준편차는 분모를 \(n\)으로 둔다.
mu = ____[num_cols].mean()
sd = ____[num_cols].std(ddof=____)
print(pd.DataFrame({'평균': mu, '표준편차': sd}).round(3))3.2 전처리 함수 — 채워 넣기 ③
숫자형 열을 표준화하고, 범주형 열 세 개를 CATS의 범주로 고정한 뒤 원-핫으로 바꾼다. 세 세트에 같은 함수를 적용한다.
def prep(df):
out = df.copy()
out[num_cols] = (out[num_cols] - ____) / ____
for col, cats in CATS.items():
out[col] = pd.Categorical(out[col], categories=____)
return pd.get_dummies(out, columns=____)
train_p, val_p, test_p = prep(train_df), prep(val_df), prep(test_df)
print(train_p.shape, val_p.shape, test_p.shape)
print(train_p[num_cols].mean().round(4))세 세트의 열 개수가 같고, 훈련 세트의 숫자형 열 평균이 0인지 확인한다.
4. 텐서와 미니배치
4.1 텐서 — 제공 코드
feat = [c for c in train_p.columns if c != 'charges']
def to_tensor(df):
X = torch.tensor(df[feat].to_numpy(dtype='float32'))
y = torch.tensor(df['charges'].to_numpy(dtype='float32')).unsqueeze(1)
return X, y
X_tr, y_tr = to_tensor(train_p)
X_va, y_va = to_tensor(val_p)
X_te, y_te = to_tensor(test_p)
print(X_tr.shape, X_va.shape, X_te.shape)4.2 데이터 로더 — 채워 넣기 ④
훈련 로더는 배치 32, 매 에폭 순서를 섞는다. 검증과 테스트 로더는 배치 64이고 섞지 않는다.
from torch.utils.data import TensorDataset, DataLoader
train_loader = DataLoader(TensorDataset(X_tr, y_tr), batch_size=____, shuffle=____)
val_loader = DataLoader(TensorDataset(X_va, y_va), batch_size=64)
test_loader = DataLoader(TensorDataset(X_te, y_te), batch_size=64)
print(len(train_loader))5. 학습 함수와 평가 함수
5.1 한 에폭의 학습 — 채워 넣기 ⑤
배치마다 실습 3의 다섯 줄을 실행한다. 함수의 이름과 인자, 반환값은 그대로 둔다.
def train_one_epoch(model, loader, criterion, optimizer):
model.train()
total = 0.0
for xb, yb in loader:
____ # 이전 기울기 지우기
pred = ____ # 예측
loss = ____ # 손실
____ # 기울기 계산
____ # 가중치 갱신
total += loss.item() * len(yb)
return total / len(loader.dataset)5.2 평가 — 채워 넣기 ⑥
가중치를 바꾸지 않고 손실만 계산한다. 평가 모드로 두고, 기울기 계산을 끈다.
def evaluate(model, loader, criterion):
model.____()
total = 0.0
with torch.____():
for xb, yb in loader:
total += criterion(model(xb), yb).item() * len(yb)
return total / len(loader.dataset)6. 모델과 학습 — 제공 코드와 채워 넣기 ⑦
모델, 손실, 갱신 도구는 주어진 대로 쓴다.
torch.manual_seed(42)
model = nn.Sequential(
nn.Linear(len(feat), 64), nn.ReLU(),
nn.Linear(64, 32), nn.ReLU(),
nn.Linear(32, 1),
)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
print(evaluate(model, val_loader, criterion))200 에폭을 돌며 훈련 손실과 검증 손실을 기록하고, 검증 손실이 가장 낮았던 순간의 가중치를 복사해 둔다.
history = {'train': [], 'val': []}
best_val, best_state, best_epoch = float('inf'), None, 0
for epoch in range(200):
tr = train_one_epoch(model, train_loader, criterion, optimizer)
va = evaluate(model, val_loader, criterion)
history['train'].append(tr)
history['val'].append(va)
if ____:
best_val = ____
best_state = ____
best_epoch = ____
print("최적 에폭:", best_epoch, " 최적 검증 RMSE:", round(best_val ** 0.5, 1))
print("마지막 검증 RMSE:", round(history['val'][-1] ** 0.5, 1))러닝커브 — 제공 코드
import matplotlib.pyplot as plt
plt.plot(history['train'], label='train')
plt.plot(history['val'], label='validation')
plt.axvline(best_epoch, color='green', linestyle='--', label=f'best epoch {best_epoch}')
plt.yscale('log')
plt.xlabel('Epoch')
plt.ylabel('MSE')
plt.legend()
plt.grid(alpha=0.3)
plt.show()7. 최적 지점 복원과 테스트 평가 — 채워 넣기 ⑧
보관한 가중치를 모델에 되돌린 뒤, 테스트 세트로 한 번 평가한다. RMSE는 MSE의 제곱근이다.
model.____(best_state)
test_mse = evaluate(____, ____, criterion)
test_rmse = ____
baseline_rmse = (((y_te - y_tr.mean()) ** 2).mean() ** 0.5).item()
print("테스트 RMSE :", round(test_rmse, 1))
print("기준선 RMSE :", round(baseline_rmse, 1))기준선은 훈련 세트의 평균 의료비로만 예측했을 때의 RMSE다. 모델의 RMSE가 기준선보다 얼마나 작은지 본다.
8. 참고 비교 — 제공 코드
같은 전처리 결과로 그래디언트 부스팅 모델을 학습시켜 테스트 RMSE를 비교한다. 그대로 실행한다.
from sklearn.ensemble import GradientBoostingRegressor
gb = GradientBoostingRegressor(random_state=42).fit(train_p[feat], train_p['charges'])
gb_rmse = float(np.sqrt(((gb.predict(test_p[feat]) - test_p['charges']) ** 2).mean()))
print("부스팅 테스트 RMSE:", round(gb_rmse, 1))9. 해석 — 두 문장씩
아래 셀의 답: 뒤에 본인의 답을 쓴다.
Q1 = "답: " # 러닝커브에서 최적 에폭 이후 검증 손실은 어떻게 움직였고, 그것이 왜 조기 종료의 근거가 되는가?
Q2 = "답: " # 신경망과 부스팅의 테스트 RMSE를 비교하고, 이 데이터에서 어느 쪽을 쓰겠는지 근거와 함께 쓴다.제출 전 확인 — 제공 코드
아래 셀이 통과를 출력해야 한다. 오류가 나면 해당 항목을 다시 확인한다.
assert STUDENT_ID != "20240000" and NAME != "홍길동", "학번과 이름을 바꾸세요"
assert len(train_df) + len(val_df) + len(test_df) == len(insurance), "세 세트의 합이 전체와 다릅니다"
assert list(train_p.columns) == list(val_p.columns) == list(test_p.columns), "세 세트의 열이 다릅니다"
assert X_tr.shape[1] == len(feat) and y_tr.shape[1] == 1, "텐서 모양을 확인하세요"
assert len(history['train']) == 200, "200 에폭을 기록해야 합니다"
assert best_state is not None and isinstance(test_rmse, float), "최적 지점과 테스트 RMSE를 확인하세요"
assert not Q1.endswith("답: ") and not Q2.endswith("답: "), "해석을 작성하세요"
print("통과")