# ▶ 준비 셀 — 그대로 실행하세요. 표를 내려받아 auto로 읽습니다.
import pandas as pd
import torch
URL = 'https://raw.githubusercontent.com/ralbu85/Lecture_DeepLearning_2022/main/auto.csv'
auto = pd.read_csv(URL)
print("준비 완료!")준비 완료!
⏱ 60분 · 4주차 실습 1/2 — 표를 학습할 수 있는 형태로 만들기 → (4-2) 배치로 학습하고, 멈출 곳을 고르고, 한 번만 평가하기
실습 3에서는 학습에 쓴 데이터로 결과를 확인했습니다. 그 숫자는 시험 문제를 미리 보고 푼 점수입니다. 이번 주의 질문은 이것입니다.
학습에 쓰지 않은 자동차의 연비를 얼마나 맞힐 수 있을까요?
이 질문에 답하려면 표를 세 세트로 나누고, 훈련 세트의 눈금으로 변환하고, 배치로 꺼내야 합니다. 이번 시간에는 그 준비를 끝까지 합니다. 학습은 4-2에서 합니다.
데이터: 자동차 392대의 연비(mpg)와 일곱 가지 특성이 담긴 표입니다.
이번 시간에 반복할 패턴: train_test_split으로 나누기 · 훈련 세트의 평균·표준편차로 표준화 · get_dummies와 pd.Categorical로 원-핫 · TensorDataset과 DataLoader로 배치 꺼내기.
대응 이론: Ch04 학습의 실전
| 표시 | 뜻 |
|---|---|
| ▶ | 그대로 실행합니다 |
| ✍️ | 보여 준 코드를 손으로 타이핑해서 실행합니다. 빈칸 ___은 채웁니다 |
| 💬 | 결과를 함께 읽습니다 |
| ✅ | 이 숫자가 나오면 맞습니다 |
| 📝 | 과제. 힌트를 보고 스스로 풉니다 |
교수자 메모 (실습본에는 없음). 60분 운영: 준비·질문 5분 → 1·2절 표와 나누기 10분 → 3절 표준화 12분 → 4·5절 원-핫과 조립 6분 → 6·7절 텐서와 로더 순회 12분 → 📝 과제 15분. ✅ 숫자: 392×8 · 274/59/59 · weight 평균 2986.5, 표준편차 837.4 · 검증 weight 평균 -0.08 · 열 10개 · X_tr (274, 9) · 9배치, 마지막 18건.
# ▶ 준비 셀 — 그대로 실행하세요. 표를 내려받아 auto로 읽습니다.
import pandas as pd
import torch
URL = 'https://raw.githubusercontent.com/ralbu85/Lecture_DeepLearning_2022/main/auto.csv'
auto = pd.read_csv(URL)
print("준비 완료!")준비 완료!
✅ 준비 완료!가 나오면 됩니다.
표를 받으면 크기와 앞부분부터 봅니다. .shape는 (행 수, 열 수), .head()는 앞의 다섯 행입니다.
(392, 8)
| cylinders | displacement | horsepower | weight | acceleration | model_year | origin | mpg | |
|---|---|---|---|---|---|---|---|---|
| 0 | 8 | 307.0 | 130.0 | 3504.0 | 12.0 | 70 | 1 | 18.0 |
| 1 | 8 | 350.0 | 165.0 | 3693.0 | 11.5 | 70 | 1 | 15.0 |
| 2 | 8 | 318.0 | 150.0 | 3436.0 | 11.0 | 70 | 1 | 18.0 |
| 3 | 8 | 304.0 | 150.0 | 3433.0 | 12.0 | 70 | 1 | 16.0 |
| 4 | 8 | 302.0 | 140.0 | 3449.0 | 10.5 | 70 | 1 | 17.0 |
💬 결과 해설 — 392행 × 8열입니다. 한 행 = 자동차 한 대이고, 예측할 종속변수는 마지막 열 mpg(연비), 나머지 일곱 열이 독립변수입니다.
모든 열이 숫자입니다. 그런데 origin의 1, 2, 3은 크기가 아니라 미국·유럽·일본이라는 이름입니다. .value_counts()는 값마다 몇 번 나오는지 셉니다.
💬 결과 해설 — 미국(1) 245대, 일본(3) 79대, 유럽(2) 68대입니다. - 이 열은 4절에서 원-핫으로 바꿉니다. 그대로 넣으면 모델이 “일본은 미국의 3배”로 읽습니다. - 나머지 여섯 열은 크기에 의미가 있는 숫자형 변수입니다. 이름을 리스트에 담아 둡니다.
새 자동차에서 얼마나 맞히는지 재려면, 학습에 쓰지 않은 자동차를 남겨 두어야 합니다. 세 세트의 역할은 다음과 같습니다.
| 세트 | 쓰임 | 언제 보나 |
|---|---|---|
| 훈련 | 가중치를 학습합니다 | 매 에폭 |
| 검증 | 언제 멈출지 고릅니다 | 매 에폭, 보기만 합니다 |
| 테스트 | 최종 성능을 보고합니다 | 마지막에 한 번 |
train_test_split은 표를 무작위로 두 조각으로 나눕니다. test_size=0.3이면 30%를 떼어 내고, random_state=42를 주면 실행할 때마다 같은 조각이 나옵니다.
# ✍️ 직접 입력
from sklearn.model_selection import train_test_split
train_df, rest_df = train_test_split(auto, test_size=0.3, random_state=42)
print(len(train_df), len(rest_df))274 118
💬 결과 해설 — 274대와 118대입니다. 274대가 훈련 세트입니다. 남은 118대를 반으로 나눠 검증과 테스트를 만듭니다.
# ✍️ 직접 입력
val_df, test_df = train_test_split(rest_df, test_size=0.5, random_state=42)
print(len(train_df), len(val_df), len(test_df))274 59 59
✅ 체크포인트 — 274 59 59가 나오면 맞습니다.
weight 열은 수천, acceleration 열은 십 단위입니다. 열마다 평균 0, 표준편차 1로 맞추면 학습이 안정됩니다. 이때 평균과 표준편차는 훈련 세트에서 구합니다. 먼저 weight 열 하나로 해 봅니다. .std(ddof=0)은 분모를 \(n\)으로 두는 표준편차입니다.
# ✍️ 직접 입력
w_mean = train_df['weight'].mean()
w_std = train_df['weight'].std(ddof=0)
print(w_mean, w_std)2986.485401459854 837.4454518494796
💬 결과 해설 — 훈련 세트 274대의 무게 평균은 약 2986, 표준편차는 약 837입니다.
각 값에서 평균을 빼고 표준편차로 나눕니다.
# ✍️ 직접 입력
w_train = (train_df['weight'] - w_mean) / w_std
print(w_train.mean(), w_train.std(ddof=0))-6.969283220274705e-17 0.9999999999999999
✅ 체크포인트 — 평균은 0(e-17처럼 아주 작은 수), 표준편차는 1이면 맞습니다. 자기 평균을 빼고 자기 표준편차로 나눴으니 당연합니다.
검증 세트는 검증 세트의 평균이 아니라 훈련 세트의 평균과 표준편차로 변환합니다.
-0.08372189489963786 0.9789892659943903
💬 결과 해설 — 평균 약 -0.08, 표준편차 약 0.98입니다. 0과 1이 아닙니다. - 훈련 세트의 값을 썼기 때문입니다. 모델이 배우는 것은 훈련 세트의 눈금이므로, 새 데이터도 그 눈금으로 잽니다. - 검증과 테스트 세트는 항상 이렇게 변환합니다.
같은 계산을 숫자형 여섯 열에 한 번에 합니다. mu와 sd는 열마다 값이 하나씩 있는 벡터이고, .round(2)는 소수 둘째 자리로 반올림해 보여 줍니다.
cylinders 5.50
displacement 196.74
horsepower 104.88
weight 2986.49
acceleration 15.52
model_year 76.14
dtype: float64
cylinders 0.0
displacement 0.0
horsepower 0.0
weight -0.0
acceleration -0.0
model_year -0.0
dtype: float64
✅ 체크포인트 — 여섯 열의 평균이 모두 0이면 맞습니다.
origin은 범주형 열입니다. pd.Categorical로 범주를 [1, 2, 3]으로 정해 두고, get_dummies로 열 세 개로 바꿉니다. 미국(1)이면 origin_1만 1입니다. 범주를 미리 정해 두는 것은 세 세트의 열 개수를 같게 하기 위해서입니다.
# ✍️ 직접 입력
train_df['origin'] = pd.Categorical(train_df['origin'], categories=[1, 2, 3])
pd.get_dummies(train_df, columns=['origin']).head()| cylinders | displacement | horsepower | weight | acceleration | model_year | mpg | origin_1 | origin_2 | origin_3 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 109 | 4 | 108.0 | 94.0 | 2379.0 | 16.5 | 73 | 22.0 | False | False | True |
| 17 | 6 | 200.0 | 85.0 | 2587.0 | 16.0 | 70 | 21.0 | True | False | False |
| 318 | 4 | 119.0 | 92.0 | 2434.0 | 15.0 | 80 | 37.0 | False | False | True |
| 24 | 6 | 199.0 | 90.0 | 2648.0 | 15.0 | 70 | 21.0 | True | False | False |
| 126 | 6 | 250.0 | 100.0 | 3336.0 | 17.0 | 74 | 15.0 | True | False | False |
✅ 체크포인트 — 표 오른쪽 끝에 origin_1, origin_2, origin_3 세 열이 생기고 행마다 True가 하나씩이면 맞습니다. True/False는 텐서로 바꿀 때 1과 0이 됩니다.
3절의 표준화와 4절의 원-핫을 한 함수에 넣어 세 세트에 똑같이 적용합니다. df.copy()는 원본 표를 두고 복사본에서 바꾸는 것입니다. 함수 안의 mu, sd는 훈련 세트에서 구한 값입니다. 빈칸을 채웁니다.
# ✍️ 빈칸(___)을 채운 뒤 실행하세요
#* def prep(df):
#* out = df.copy()
#* out[num_cols] = (out[num_cols] - ___) / ___
#* out['origin'] = pd.Categorical(out['origin'], categories=___)
#* return pd.get_dummies(out, columns=___)
#*
#* train_p = prep(train_df)
#* print(train_p.shape)
#* train_p.head(3)
def prep(df):
out = df.copy()
out[num_cols] = (out[num_cols] - mu) / sd
out['origin'] = pd.Categorical(out['origin'], categories=[1, 2, 3])
return pd.get_dummies(out, columns=['origin'])
train_p = prep(train_df)
print(train_p.shape)
train_p.head(3)(274, 10)
| cylinders | displacement | horsepower | weight | acceleration | model_year | mpg | origin_1 | origin_2 | origin_3 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 109 | -0.873266 | -0.854863 | -0.285913 | -0.725403 | 0.342749 | -0.868721 | 22.0 | False | False | True |
| 17 | 0.293929 | 0.031360 | -0.522352 | -0.477029 | 0.167473 | -1.699056 | 21.0 | True | False | False |
| 318 | -0.873266 | -0.748902 | -0.338455 | -0.659727 | -0.183081 | 1.068728 | 37.0 | False | False | True |
💬 결과 해설 — 열이 10개입니다. 숫자형 6개, mpg, 원-핫 3개입니다. 같은 함수를 검증과 테스트 세트에 적용합니다.
# ✍️ 직접 입력
val_p = prep(val_df)
test_p = prep(test_df)
print(train_p.shape, val_p.shape, test_p.shape)(274, 10) (59, 10) (59, 10)
✅ 체크포인트 — 세 세트 모두 열이 10개면 맞습니다. 범주를 고정했으므로 열이 같습니다.
mpg를 뺀 아홉 열이 X, mpg가 y입니다. drop(columns='mpg')는 그 열을 뺀 표를 돌려주고, to_numpy(dtype='float32')는 표를 실수 배열로 바꿉니다. 그 배열을 torch.tensor에 넣습니다. 실습 3과 같이 X는 (데이터 수, 변수 수), y는 (데이터 수, 1)입니다.
# ✍️ 직접 입력
X_tr = torch.tensor(train_p.drop(columns='mpg').to_numpy(dtype='float32'))
y_tr = torch.tensor(train_p[['mpg']].to_numpy(dtype='float32'))
print(X_tr.shape, y_tr.shape)torch.Size([274, 9]) torch.Size([274, 1])
💬 결과 해설 — (274, 9)와 (274, 1)입니다. y에 [['mpg']]로 대괄호를 두 번 쓴 것은 열 하나짜리 표를 만들어 (274, 1)을 얻기 위해서입니다.
검증과 테스트도 같은 두 줄이므로 함수로 만듭니다.
# ✍️ 직접 입력
def to_tensor(df):
X = torch.tensor(df.drop(columns='mpg').to_numpy(dtype='float32'))
y = torch.tensor(df[['mpg']].to_numpy(dtype='float32'))
return X, y
X_va, y_va = to_tensor(val_p)
X_te, y_te = to_tensor(test_p)
print(X_va.shape, X_te.shape)torch.Size([59, 9]) torch.Size([59, 9])
✅ 체크포인트 — (59, 9)가 두 번 나오면 맞습니다.
실습 3에서는 매번 274건 전체로 기울기를 구했습니다. 이번에는 32건씩 나누어 구합니다. 먼저 TensorDataset으로 X의 한 행과 y의 한 행을 짝지어 담습니다. 길이는 데이터 수이고, 번호로 한 쌍을 꺼낼 수 있습니다.
# ✍️ 직접 입력
from torch.utils.data import TensorDataset, DataLoader
train_ds = TensorDataset(X_tr, y_tr)
print(len(train_ds))
print(train_ds[0])274
(tensor([-0.8733, -0.8549, -0.2859, -0.7254, 0.3427, -0.8687, 0.0000, 0.0000,
1.0000]), tensor([22.]))
💬 결과 해설 — 길이는 274이고, train_ds[0]은 첫 자동차의 입력 9개와 연비 하나의 쌍입니다.
DataLoader는 데이터셋을 batch_size씩 잘라 줍니다. shuffle=True는 한 바퀴 돌 때마다 순서를 섞습니다. 그 앞의 torch.manual_seed(42)는 섞는 순서를 고정해 실행할 때마다 같은 결과가 나오게 합니다.
# ✍️ 직접 입력
torch.manual_seed(42)
train_loader = DataLoader(train_ds, batch_size=32, shuffle=True)
print(len(train_loader))9
✅ 체크포인트 — 9가 나오면 맞습니다. 274건을 32씩 나누면 8배치에 18건이 남아 9배치입니다.
for로 로더를 한 바퀴 돌며 배치마다 모양을 찍어 봅니다.
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([32, 9]) torch.Size([32, 1])
torch.Size([18, 9]) torch.Size([18, 1])
💬 결과 해설 — (32, 9)와 (32, 1)이 여덟 번 나오고, 마지막은 남은 18건입니다. - 이 한 바퀴가 한 에폭, 배치 하나가 한 이터레이션입니다. - 4-2의 학습 반복문은 이 for 안에 실습 3의 다섯 줄을 넣은 것입니다.
iter(train_loader)로 새 바퀴를 시작하고 next로 첫 배치를 꺼냅니다. 첫 배치의 연비 세 개를 봅니다.
한 번 더 꺼냅니다.
💬 결과 해설 — 다른 자동차가 나옵니다. shuffle=True라서 바퀴마다 배치의 구성이 달라집니다.
검증과 테스트 로더는 섞지 않습니다. 평가는 순서와 무관합니다.
# ▶ 그대로 실행하세요 — 검증·테스트 로더
val_loader = DataLoader(TensorDataset(X_va, y_va), batch_size=64)
test_loader = DataLoader(TensorDataset(X_te, y_te), batch_size=64)
print(len(val_loader), len(test_loader))1 1
💬 59건은 64보다 작으므로 각각 1배치입니다.
훈련 80%, 검증 10%, 테스트 10%가 되도록 나누고 세 세트의 크기를 출력합니다. 먼저 크기를 예상해 봅니다.
# 📝
#> 힌트: 2절의 두 줄에서 첫 번째 test_size만 0.2로 바꿉니다. 두 번째는 그대로 0.5입니다. random_state=42를 유지합니다.
tr, rest = train_test_split(auto, test_size=0.2, random_state=42)
va, te = train_test_split(rest, test_size=0.5, random_state=42)
print(len(tr), len(va), len(te))313 39 40
✏️ 나의 답: 훈련 _대, 검증 _대, 테스트 ___대
예시 답: 313, 39, 40. 392대의 20%인 79대가
rest로 가고, 반으로 나뉘어 검증 39대와 테스트 40대가 됩니다.
평균을 훈련 세트가 아니라 전체 표 auto에서 구한 뒤 훈련 세트를 표준화하면, 훈련 세트의 평균이 0이 되지 않습니다. 확인합니다.
# 📝
#> 힌트: 3절에서 mu를 만든 줄의 train_df를 auto로 바꿔 wrong_mu를 만듭니다. 그다음 (train_df[num_cols] - wrong_mu) / sd 의 열 평균을 .round(4)로 봅니다.
wrong_mu = auto[num_cols].mean()
print(((train_df[num_cols] - wrong_mu) / sd).mean().round(4))cylinders 0.0142
displacement 0.0225
horsepower 0.0109
weight 0.0106
acceleration -0.0067
model_year 0.0440
dtype: float64
✏️ 나의 답: 여섯 열의 평균이 0과 얼마나 다른가?
예시 답: 여섯 열의 평균이 소수 둘째 자리 정도의 값으로 0이 아닙니다. 전체 평균에는 검증·테스트 데이터가 섞여 있기 때문입니다. 차이는 작지만 테스트 데이터를 미리 들여다본 것이므로 하지 않습니다.
배치 크기 64와 274로 로더를 만들어 len()을 출력합니다. 먼저 예상해 봅니다.
# 📝
#> 힌트: 7절에서 train_loader를 만든 줄을 두 번 더 쓰되 batch_size만 64, 274로 바꾸고, 각각 len()을 print 합니다.
print(len(DataLoader(train_ds, batch_size=64)))
print(len(DataLoader(train_ds, batch_size=274)))5
1
| 배치 크기 | 에폭당 이터레이션 | 마지막 배치 크기 |
|---|---|---|
| 32 | 9 | 18 |
| 64 | ✏️ | ✏️ |
| 274 | ✏️ | ✏️ |
예시 답: 64면 5배치(마지막 18건), 274면 1배치(274건). 배치 크기가 데이터 수와 같으면 실습 3처럼 한 에폭이 한 이터레이션입니다.
검증 세트로 “언제 멈출지”를 골랐다면, 최종 성능은 왜 검증 세트가 아니라 테스트 세트로 보고해야 할까요? 한두 문장으로 적습니다.
✏️ 나의 답:
예시 답: 검증 세트를 보고 멈출 지점을 골랐으므로 검증 세트도 학습에 쓴 셈입니다. 그 세트의 점수는 실제보다 좋게 나옵니다. 아직 한 번도 쓰지 않은 테스트 세트만이 새 데이터에 대한 성능입니다.
| 배운 것 | 핵심 |
|---|---|
| 세 세트 | 훈련 274 · 검증 59 · 테스트 59. train_test_split 두 번 |
| 표준화 | 평균·표준편차는 훈련 세트에서만. 검증·테스트는 그 값으로 변환 (검증 weight 평균 -0.08) |
| 원-핫 | pd.Categorical로 범주를 고정한 뒤 get_dummies. 세 세트 모두 열 10개 |
| 텐서 | drop(columns='mpg') → to_numpy(dtype='float32') → torch.tensor. (274, 9)와 (274, 1) |
| 로더 | TensorDataset + DataLoader(batch_size=32, shuffle=True). 9배치, 마지막 18건. 바퀴마다 순서가 바뀜 |
다음: 실습 4-2에서 이 로더로 학습하고, 검증 손실로 멈출 곳을 고르고, 테스트 세트로 한 번 평가합니다.