실습 4-1 · 표 하나를 나누고 텐서로 만들기까지

Open In Colab

Open In Colab

⏱ 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("준비 완료!")
준비 완료!

✅ 준비 완료!가 나오면 됩니다.


1. 표에는 무엇이 들어 있나

표를 받으면 크기와 앞부분부터 봅니다. .shape는 (행 수, 열 수), .head()는 앞의 다섯 행입니다.

# ✍️ 직접 입력
print(auto.shape)
auto.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()는 값마다 몇 번 나오는지 셉니다.

# ✍️ 직접 입력
auto['origin'].value_counts()
origin
1    245
3     79
2     68
Name: count, dtype: int64

💬 결과 해설 — 미국(1) 245대, 일본(3) 79대, 유럽(2) 68대입니다. - 이 열은 4절에서 원-핫으로 바꿉니다. 그대로 넣으면 모델이 “일본은 미국의 3배”로 읽습니다. - 나머지 여섯 열은 크기에 의미가 있는 숫자형 변수입니다. 이름을 리스트에 담아 둡니다.

# ▶ 그대로 실행하세요 — 숫자형 여섯 열의 이름
num_cols = ['cylinders', 'displacement', 'horsepower', 'weight', 'acceleration', 'model_year']

2. 왜 세 세트로 나누는가

새 자동차에서 얼마나 맞히는지 재려면, 학습에 쓰지 않은 자동차를 남겨 두어야 합니다. 세 세트의 역할은 다음과 같습니다.

세트 쓰임 언제 보나
훈련 가중치를 학습합니다 매 에폭
검증 언제 멈출지 고릅니다 매 에폭, 보기만 합니다
테스트 최종 성능을 보고합니다 마지막에 한 번

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가 나오면 맞습니다.


3. 훈련 세트의 눈금으로 표준화하기

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이면 맞습니다. 자기 평균을 빼고 자기 표준편차로 나눴으니 당연합니다.

검증 세트는 검증 세트의 평균이 아니라 훈련 세트의 평균과 표준편차로 변환합니다.

# ✍️ 직접 입력
w_val = (val_df['weight'] - w_mean) / w_std
print(w_val.mean(), w_val.std(ddof=0))
-0.08372189489963786 0.9789892659943903

💬 결과 해설 — 평균 약 -0.08, 표준편차 약 0.98입니다. 0과 1이 아닙니다. - 훈련 세트의 값을 썼기 때문입니다. 모델이 배우는 것은 훈련 세트의 눈금이므로, 새 데이터도 그 눈금으로 잽니다. - 검증과 테스트 세트는 항상 이렇게 변환합니다.

같은 계산을 숫자형 여섯 열에 한 번에 합니다. mu와 sd는 열마다 값이 하나씩 있는 벡터이고, .round(2)는 소수 둘째 자리로 반올림해 보여 줍니다.

# ✍️ 직접 입력
mu = train_df[num_cols].mean()
sd = train_df[num_cols].std(ddof=0)
print(mu.round(2))
cylinders          5.50
displacement     196.74
horsepower       104.88
weight          2986.49
acceleration      15.52
model_year        76.14
dtype: float64
# ✍️ 직접 입력
train_num = (train_df[num_cols] - mu) / sd
print(train_num.mean().round(4))
cylinders       0.0
displacement    0.0
horsepower      0.0
weight         -0.0
acceleration   -0.0
model_year     -0.0
dtype: float64

✅ 체크포인트 — 여섯 열의 평균이 모두 0이면 맞습니다.


4. 이름인 열은 원-핫으로

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이 됩니다.


5. 전처리를 함수 하나로 조립하기

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개면 맞습니다. 범주를 고정했으므로 열이 같습니다.


6. 표를 텐서로

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)가 두 번 나오면 맞습니다.


7. 배치로 꺼내면 어떻게 나오나

실습 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로 로더를 한 바퀴 돌며 배치마다 모양을 찍어 봅니다.

# ✍️ 직접 입력
for xb, yb in train_loader:
    print(xb.shape, yb.shape)
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로 첫 배치를 꺼냅니다. 첫 배치의 연비 세 개를 봅니다.

# ✍️ 직접 입력
xb, yb = next(iter(train_loader))
print(yb[:3, 0])
tensor([13., 26., 25.])

한 번 더 꺼냅니다.

# ✍️ 직접 입력
xb, yb = next(iter(train_loader))
print(yb[:3, 0])
tensor([16., 23., 24.])

💬 결과 해설 — 다른 자동차가 나옵니다. 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배치입니다.


8. 실습 과제 (15분)

📝 과제 1 · 80 / 10 / 10으로 나누면 몇 대씩인가

훈련 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대가 됩니다.

📝 과제 2 · 평균을 전체 표에서 구하면 무엇이 달라지나

평균을 훈련 세트가 아니라 전체 표 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이 아닙니다. 전체 평균에는 검증·테스트 데이터가 섞여 있기 때문입니다. 차이는 작지만 테스트 데이터를 미리 들여다본 것이므로 하지 않습니다.

📝 과제 3 · 배치 크기를 바꾸면 이터레이션 수는

배치 크기 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처럼 한 에폭이 한 이터레이션입니다.

📝 과제 4 · 생각해 보기

검증 세트로 “언제 멈출지”를 골랐다면, 최종 성능은 왜 검증 세트가 아니라 테스트 세트로 보고해야 할까요? 한두 문장으로 적습니다.

✏️ 나의 답:

예시 답: 검증 세트를 보고 멈출 지점을 골랐으므로 검증 세트도 학습에 쓴 셈입니다. 그 세트의 점수는 실제보다 좋게 나옵니다. 아직 한 번도 쓰지 않은 테스트 세트만이 새 데이터에 대한 성능입니다.


✅ 정리

배운 것 핵심
세 세트 훈련 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에서 이 로더로 학습하고, 검증 손실로 멈출 곳을 고르고, 테스트 세트로 한 번 평가합니다.