26일차는 전날 배운 머신러닝 기본 흐름에서 한 단계 더 들어가, 타이타닉 데이터를 실제 모델링 가능한 형태로 만들고, 여러 모델을 비교하고, 하이퍼파라미터 튜닝까지 이어가는 과정이었다. 25일차에는 load_digits()처럼 이미 정리된 샘플 데이터로 fit → predict → score 흐름을 봤다면, 이번에는 Kaggle 스타일의 train.csv를 가져와 결측치, 문자열 컬럼, 불필요한 컬럼, validation 분리, K-Fold, RandomizedSearchCV, GridSearchCV까지 한 번에 이어졌다.
특히 이번 날은 “모델을 돌리는 코드”보다 모델이 돌아갈 수 있는 상태를 만드는 코드가 더 중요하게 느껴졌다. 타이타닉 데이터에는 Name, Sex, Age, Ticket, Cabin, Embarked처럼 숫자와 문자가 섞여 있었고, Age, Cabin, Embarked에는 결측치도 있었다. 머신러닝 모델은 결국 수학적인 함수를 최적화하는 구조이기 때문에, 모든 입력값은 숫자여야 하고 결측치도 없어야 했다. 이 전처리 과정이 흔들리면 뒤에서 아무리 좋은 모델을 써도 제대로 비교하기 어렵다.
1. 타이타닉 데이터는 바로 모델에 넣을 수 없었다
처음에는 Kaggle의 타이타닉 train.csv를 불러왔다. 이 데이터는 승객별 정보와 생존 여부가 함께 들어 있는 데이터였다. Survived는 정답지 역할을 하고, 나머지 컬럼들은 생존 여부를 예측하기 위한 문제지 역할을 한다.
import numpy as np
import pandas as pd
path = "/content/train.csv"
data = pd.read_csv(path)
data.head()
처음 head()로 보면 데이터가 잘 들어온 것처럼 보이지만, info()를 보면 바로 모델링에 사용할 수 없는 이유가 보인다.
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
여기서 봐야 할 것은 크게 두 가지였다. 첫째, Age, Cabin, Embarked에 결측치가 있다. 둘째, Name, Sex, Ticket, Cabin, Embarked는 문자열 컬럼이다. 머신러닝 모델은 2D 행렬 형태의 숫자 데이터를 받아야 하므로, 결측치를 처리하고 문자 컬럼을 숫자로 바꾸는 작업이 필요했다. 코드 자료에서도 타이타닉 데이터는 모델링 전에 결측치 처리와 인코딩이 필요한 데이터로 정리되어 있었다.
2. 결측치 처리는 정답이 아니라 정책이었다
Age 컬럼은 891개 중 714개만 값이 있었다. 대략 20% 정도가 비어 있는 상태라서 그냥 컬럼을 삭제하기에는 아깝고, 행을 전부 삭제하기에도 손실이 컸다. 그래서 이번 실습에서는 진행 편의를 위해 평균값으로 채웠다.
data.loc[:, "Age"].plot(kind="hist")
data.loc[:, "Age"].mean()
data.loc[:, "Age"].fillna(data.loc[:, "Age"].mean(), inplace=True)
다만 평균으로 채우는 것이 항상 좋은 방법은 아니다. 실제로 평균값 하나로 결측치를 채우면, 분포의 중앙 근처가 인위적으로 높아질 수 있다. 자료에서도 Age를 평균으로 채운 뒤 분포를 다시 보면 중앙 부분이 더 높아지는 왜곡이 생길 수 있다고 정리되어 있었다.
Cabin과 Embarked는 일단 정보가 없다는 의미로 "N"을 넣었다.
data.loc[:, "Cabin"].fillna("N", inplace=True)
data.loc[:, "Embarked"].fillna("N", inplace=True)
이때 Pandas에서 FutureWarning이 떴다. df[col].method(..., inplace=True) 형태는 앞으로 Pandas 3.0에서 동작이 바뀔 수 있다는 경고였다. 지금은 코드가 돌아가지만, 나중에는 df.method({col: value}, inplace=True) 같은 방식이나 명시적으로 값을 다시 할당하는 방식으로 바꿔야 할 수 있다. 이 부분은 단순 경고처럼 보이지만, 앞으로 같은 전처리 함수를 계속 쓸 거라면 수정해둘 필요가 있어 보였다.
3. Label Encoding은 train 기준으로 만들어야 했다
문자열 컬럼은 숫자로 바꿔야 한다. 대표적으로 Label Encoding과 One-Hot Encoding이 있었다. Label Encoding은 종류마다 0, 1, 2 같은 정수값을 붙이는 방식이고, One-Hot Encoding은 각 종류를 별도 컬럼으로 펼쳐서 해당 여부를 0/1로 표시하는 방식이다.
Label Encoding
대한민국 → 0
미국 → 1
호주 → 2
One-Hot Encoding
나라_대한민국, 나라_미국, 나라_호주
One-Hot Encoding은 의미가 명확하지만 종류가 많으면 컬럼이 너무 많이 늘어난다. 특히 대부분의 값이 0인 희소행렬이 생길 수 있다. 반대로 Label Encoding은 컬럼 수는 유지되지만, 숫자의 크기가 순서처럼 해석될 수 있다는 문제가 있다. 그래서 무조건 하나가 정답이라기보다, 데이터와 모델에 맞게 선택해야 했다.
간단한 예제로 LabelEncoder를 먼저 사용했다.
from sklearn.preprocessing import LabelEncoder
items = ["TV", "냉장고", "세탁기", "TV", "냉장고"]
label_enc = LabelEncoder()
label_enc.fit(items)
label_enc_result = label_enc.transform(items)
print(items)
print(label_enc_result)
['TV', '냉장고', '세탁기', 'TV', '냉장고']
[0 1 2 0 1]
중요했던 건 test에 처음 보는 값이 나오는 경우였다.
train_items = ["TV", "냉장고", "세탁기", "TV", "냉장고"]
test_items = ["TV", "세탁기", "세탁기", "스타일러"]
label_enc = LabelEncoder()
label_enc.fit(train_items)
label_enc.transform(test_items)
이렇게 하면 스타일러는 train에서 본 적 없는 값이라 에러가 난다. 반대로 train과 test를 합쳐서 fit_transform()하면 코드는 돌아가지만, 논리적으로는 test 정보를 미리 본 것이 된다. 자료에서도 이것을 test 치팅으로 보고, train 기준으로 만든 변환 룰을 test에 적용해야 한다고 정리되어 있었다.
그래서 FM에 가까운 방식은 train에서 기준 룰을 만들고, test에서 처음 보는 값이 나오면 예외 처리하는 구조였다.
label_enc = LabelEncoder()
label_enc.fit(train_items)
prev_classes = list(label_enc.classes_)
for label in np.unique(test_items):
if label not in prev_classes:
prev_classes.append(label)
label_enc.classes_ = np.array(prev_classes)
label_enc.transform(test_items)
이 부분이 꽤 중요했다. 전처리도 모델 학습과 마찬가지로 train 기준으로 만들어야 하고, test는 “모르는 데이터”라고 가정해야 한다. 코드가 돌아가는 것과 논리적으로 맞는 것은 다르다는 점이 남았다.
4. 전처리는 함수로 묶어야 나중에 수정하기 쉬웠다
타이타닉 데이터에서는 결측치 처리, 불필요한 컬럼 삭제, 인코딩을 함수로 나눴다. 먼저 결측치 처리 함수다.
def check_fillna(df):
df.loc[:, "Age"].fillna(df.loc[:, "Age"].mean(), inplace=True)
df.loc[:, "Cabin"].fillna("N", inplace=True)
df.loc[:, "Embarked"].fillna("N", inplace=True)
df.loc[:, "Fare"].fillna(0, inplace=True)
return df
다음은 불필요한 컬럼 제거 함수다. Name, PassengerId, Ticket은 이번 실습에서는 제거했다.
def drop_features(df):
df.drop(["Name", "PassengerId", "Ticket"], axis=1, inplace=True)
return df
Cabin은 종류가 너무 많았다. 그냥 있는 그대로 라벨 인코딩하면 100개가 넘는 값이 생기기 때문에, 앞 글자만 가져와 구역 정보처럼 줄였다.
data.loc[:, "Cabin"].apply(lambda x: str(x)[:1]).value_counts()
실제 인코딩 함수는 Cabin, Sex, Embarked에 대해 Label Encoding을 적용했다.
def encode_feature(df):
df.loc[:, "Cabin"] = df.loc[:, "Cabin"].apply(lambda x: str(x)[:1])
cols = ["Cabin", "Sex", "Embarked"]
for col in cols:
le = LabelEncoder()
le.fit(df.loc[:, col])
df.isetitem(
df.columns.get_loc(col),
le.transform(df.loc[:, col])
)
return df
마지막으로 전체 전처리 함수를 하나로 묶었다.
def titanic_preprocessing(df):
df = check_fillna(df)
df = drop_features(df)
df = encode_feature(df)
return df
이렇게 묶어두면 나중에 Cabin을 다른 방식으로 처리하거나, 특정 컬럼을 제거 대상에 추가하거나, 인코딩 방식을 바꾸고 싶을 때 함수 내부만 수정하면 된다. 자료에서도 전처리는 한 번에 끝나는 작업이 아니라 여러 번 실험하면서 바뀌기 때문에, 모듈화가 필요하다고 강조했다.
5. 문제지와 정답지를 분리하고, 모델 입력 형태를 확인했다
타이타닉 데이터에서는 Survived가 정답이다. 그래서 먼저 정답지와 문제지를 분리했다.
y_titanic = data.loc[:, "Survived"]
X_titanic = data.drop("Survived", axis=1)
그 다음 문제지에 전처리를 적용했다.
X_titanic = titanic_preprocessing(X_titanic)
X_titanic.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Pclass 891 non-null int64
1 Sex 891 non-null int64
2 Age 891 non-null float64
3 SibSp 891 non-null int64
4 Parch 891 non-null int64
5 Fare 891 non-null float64
6 Cabin 891 non-null int64
7 Embarked 891 non-null int64
이제 모든 컬럼이 숫자형이고, 결측치도 없었다. 이 상태가 되어야 최소한 모델에 넣어볼 수 있다. 코드 자료에서도 전처리 후 Pclass, Sex, Age, SibSp, Parch, Fare, Cabin, Embarked 총 8개 컬럼이 남고, 모두 숫자형으로 바뀐 것을 확인했다.
6. Kaggle test 정답이 없어서 validation을 따로 만들었다
Kaggle에서는 실제 test.csv의 정답을 내가 가지고 있지 않다. 최종 제출을 해야 점수를 알 수 있는 구조다. 그래서 train 데이터 안에서 일부를 떼어 validation으로 사용했다. 이 validation은 학습에 쓰면 안 되고, 자체 평가용으로만 써야 한다.
먼저 생존/사망 비율을 확인했다.
y_titanic.value_counts(normalize=True)
Survived
0 0.616162
1 0.383838
대략 6:4 정도였다. 아주 심한 불균형은 아니지만, 그래도 비율을 최대한 유지해서 나누는 것이 더 안정적일 수 있다. 먼저 일반 train_test_split()으로 나누고, 이후 stratify를 적용했다.
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(
X_titanic,
y_titanic,
test_size=0.2,
random_state=1234,
stratify=y_titanic
)
stratify=y_titanic을 넣으면 정답 비율을 최대한 유지해서 train과 validation을 나눠준다. 자료에서도 생존/사망 비율을 확인한 뒤, 비율을 맞춰서 나누고 싶으면 stratify를 사용한다고 정리되어 있었다.
이 부분에서 train_test_split이라는 이름에 너무 갇히면 안 된다는 것도 다시 나왔다. 이 함수는 꼭 train/test만 나누는 것이 아니라, 어떤 데이터든 두 덩어리로 나눌 때 쓸 수 있다. 여기서는 train 데이터를 train/validation으로 나누는 데 사용했다.
7. K-Fold는 하이퍼파라미터 비교를 더 안정적으로 하기 위한 장치였다
하나의 train/validation 분리만 가지고 모델을 비교하면 우연성이 생길 수 있다. 특정 validation set에만 잘 맞는 파라미터가 선택될 수도 있다. 그래서 train 데이터를 여러 fold로 나눠 돌려가며 평가하는 K-Fold를 사용했다.
from sklearn.model_selection import KFold
kfold = KFold(
n_splits=5,
shuffle=True,
random_state=1234
)
cv=5처럼 숫자만 넣으면 매번 나뉘는 구성이 달라질 수 있다. 그래서 재현성을 위해 KFold 객체를 직접 만들고 random_state를 고정했다. 정답 비율을 유지해야 하는 경우에는 StratifiedKFold를 사용할 수 있다.
from sklearn.model_selection import StratifiedKFold
str_kfold = StratifiedKFold(
n_splits=5,
random_state=1234,
shuffle=True
)
데이터가 적은데 K-Fold를 하고 싶을 때는 반복 K-Fold도 사용할 수 있었다.
from sklearn.model_selection import RepeatedKFold
from sklearn.model_selection import RepeatedStratifiedKFold
rkfold = RepeatedKFold(
n_splits=5,
random_state=1234,
n_repeats=10
)
이날 K-Fold에서 계속 강조된 건 재현성이었다. 같은 데이터, 같은 모델, 같은 파라미터로 비교하려면 데이터를 나누는 방식도 같아야 한다. 그렇지 않으면 성능 차이가 모델 때문인지, 데이터 분리 때문인지 애매해진다.
8. KNN baseline을 잡고, RandomizedSearchCV와 GridSearchCV를 비교했다
먼저 KNN으로 baseline을 확인했다. 이때 cross_val_score()를 사용했다.
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_jobs=-1)
scores = cross_val_score(
knn,
X_train,
y_train,
cv=kfold,
scoring="accuracy"
)
scores
array([0.72727273, 0.70629371, 0.65492958, 0.68309859, 0.78169014])
평균과 표준편차를 보면 KNN이 대략 어느 정도 성능을 내는지 확인할 수 있다.
for iter, acc in enumerate(scores):
print(f"KNN의 {iter}시도 acc:{acc}")
print("*" * 30)
print(f"KNN 평균 acc :{scores.mean()}")
print(f"KNN std acc :{scores.std()}")
KNN 평균 acc :0.7106569486851178
KNN std acc :0.042908832008865715
이제 KNN의 세부 파라미터를 조정했다. 먼저 넓은 범위에서 랜덤하게 일부 조합만 확인하는 RandomizedSearchCV를 사용했다.
from sklearn.model_selection import RandomizedSearchCV
parameters = {
"n_neighbors": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
"algorithm": ["auto", "ball_tree", "kd_tree"]
}
knn = KNeighborsClassifier(n_jobs=-1)
knn_kf_rgs = RandomizedSearchCV(
knn,
param_distributions=parameters,
cv=kfold,
scoring="accuracy",
n_iter=10,
random_state=1234,
n_jobs=-1
)
knn_kf_rgs.fit(X_train, y_train)
결과는 n_neighbors=9, algorithm='auto'가 가장 좋게 나왔다.
knn_kf_rgs.best_params_
knn_kf_rgs.best_score_
{'n_neighbors': 9, 'algorithm': 'auto'}
0.7191273515217178
그 다음 이 주변을 조금 더 자세히 보기 위해 GridSearchCV를 사용했다.
from sklearn.model_selection import GridSearchCV
parameters = {
"n_neighbors": [7, 9, 11, 13, 15],
"algorithm": ["auto", "ball_tree", "kd_tree"]
}
knn = KNeighborsClassifier(n_jobs=-1)
knn_kf_gs = GridSearchCV(
knn,
param_grid=parameters,
cv=kfold,
scoring="accuracy",
n_jobs=-1
)
knn_kf_gs.fit(X_train, y_train)
RandomizedSearchCV는 넓은 공간에서 일부만 찍어보는 느낌이고, GridSearchCV는 내가 지정한 조합을 전부 확인하는 방식이었다. 자료에서도 큰 범위는 RandomizedSearch나 Bayesian Optimization으로 보고, 괜찮은 구간 주변을 GridSearch로 자세히 보는 식으로 접근할 수 있다고 설명했다.
9. Random Forest는 KNN보다 baseline이 높게 나왔다
다음 모델은 Random Forest였다. Random Forest는 여러 Decision Tree를 만들어서 의견을 종합하는 bagging 계열 모델이다. 내부에는 여러 개의 tree가 있고, 데이터를 샘플 방향과 컬럼 방향으로 랜덤하게 변형해 diversity를 확보한다. 자료에서도 Random Forest는 여러 Decision Tree를 사용하는 ensemble 모델이고, n_estimators, max_features, max_depth, min_samples_split 같은 파라미터를 조정한다고 설명했다.
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(
n_jobs=-1,
random_state=1234
)
Random Forest도 먼저 baseline을 확인했다.
scores = cross_val_score(
rf,
X_train,
y_train,
cv=kfold,
scoring="accuracy"
)
scores.mean()
KNN이 대략 0.71 정도였다면, Random Forest는 대충 돌려도 0.80 근처가 나왔다. 그래서 같은 데이터, 같은 K-Fold, 같은 accuracy 기준에서 비교했을 때 Random Forest가 더 나은 후보로 보였다. 자료에서도 KNN은 0.7 내외였고, Random Forest는 0.8 내외로 더 높게 나왔다고 정리되어 있었다.
Random Forest에서 중요한 파라미터는 다음과 같았다.
n_estimators
= 몇 개의 나무를 만들 것인가
max_features
= 각 tree에서 사용할 feature 개수를 어떻게 제한할 것인가
max_depth
= 나무의 최대 깊이를 어디까지 허용할 것인가
min_samples_split
= 노드를 나누기 위해 최소 몇 개의 샘플이 필요할 것인가
이 파라미터들은 모델 복잡도와 diversity를 조절한다. n_estimators가 많으면 여러 tree를 만들 수 있지만 시간이 오래 걸린다. max_features를 너무 크게 잡으면 tree들이 서로 비슷해질 수 있고, 너무 작게 잡으면 정보가 부족할 수 있다. max_depth와 min_samples_split은 Decision Tree가 너무 깊고 복잡하게 자라 overfitting되는 것을 막는 장치였다.
10. Random Forest 튜닝은 RandomizedSearchCV 후 GridSearchCV로 좁혀갔다
Random Forest는 조정할 파라미터가 많아서 처음부터 모든 조합을 다 보는 것은 부담이 컸다. 그래서 넓은 범위에서 RandomizedSearchCV를 먼저 사용했다.
from sklearn.model_selection import RandomizedSearchCV
rf = RandomForestClassifier(
n_jobs=-1,
random_state=1234
)
parameters = {
"n_estimators": [10, 30, 50, 70, 100],
"max_features": [2, 3, 4, 5, 6, 7],
"max_depth": [2, 3, 4, 5, 6, 7, 8, 9, 10, 20],
"min_samples_split": [1, 3, 5, 7, 9]
}
rf_rgs = RandomizedSearchCV(
rf,
param_distributions=parameters,
cv=kfold,
scoring="accuracy",
n_iter=20,
random_state=1234,
n_jobs=-1
)
rf_rgs.fit(X_train, y_train)
여기서 일부 조합은 min_samples_split=1 때문에 warning이나 실패가 났다. min_samples_split은 최소 2 이상이어야 의미가 있는데, 일부러 1을 넣어 경고가 어떻게 뜨는지도 확인했다. 자료에서도 20개의 랜덤 조합 중 일부는 실패했고, 이런 경우 어떤 파라미터 값이 잘못 들어갔는지 확인해야 한다고 설명했다.
RandomizedSearchCV 결과에서 가장 좋은 조합은 대략 n_estimators=50, min_samples_split=7, max_features=6 쪽이었다.
rf_rgs.best_score_
rf_rgs.best_params_
0.829...
{'n_estimators': 50, 'min_samples_split': 7, 'max_features': 6, ...}
이후에는 이 주변을 GridSearchCV로 더 좁혀봤다.
parameters = {
"n_estimators": [30, 50, 70, 100],
"max_features": [5, 6, 7],
"max_depth": [5, 6, 7],
"min_samples_split": [7]
}
rf = RandomForestClassifier(
n_jobs=-1,
random_state=1234
)
rf_gs = GridSearchCV(
rf,
param_grid=parameters,
cv=kfold,
scoring="accuracy",
n_jobs=-1
)
rf_gs.fit(X_train, y_train)
이런 식으로 RandomizedSearchCV는 넓은 바다에서 위치를 찾는 역할, GridSearchCV는 그 주변을 촘촘하게 찍어보는 역할에 가까웠다. 낚시 비유로 보면, 먼저 어군 탐지기로 큰 위치를 찾고, 그 근처에서 실제 낚싯대를 여러 개 내려보는 느낌이었다.
11. validation 성능이 train CV보다 높게 나올 수도 있었다
튜닝한 모델을 validation set으로 평가했다.
rf_best = rf_gs.best_estimator_
rf_pred = rf_best.predict(X_val)
rf_acc = accuracy_score(y_val, rf_pred)
rf_acc
여기서 validation accuracy가 train CV 평균보다 조금 높게 나오는 상황이 있었다. 일반적으로는 학습 과정에서 본 데이터 쪽 성능이 더 높고, validation은 더 낮게 나오는 경우가 많다. 그런데 이번에는 validation set의 샘플 수가 179개 정도로 적었고, 우연히 쉬운 샘플이 많이 들어갔을 수 있었다. 자료에서도 validation 성능이 더 높게 나온 이유로 validation 샘플 수가 적어서 한두 개 차이로 성능이 흔들릴 수 있다고 설명했다.
이 부분은 꽤 현실적이었다. 숫자가 높게 나왔다고 무조건 좋아할 것이 아니라, 왜 이런 현상이 생겼는지 봐야 한다. 특히 validation set이 작으면 한두 문제 차이로 accuracy가 꽤 흔들릴 수 있다.
12. 모델도 저장하고 불러올 수 있어야 했다
Random Forest나 XGBoost처럼 시간이 걸리는 모델은 매번 다시 학습시키기 부담스럽다. 클라우드 환경에서 비싼 리소스를 사용해 학습했다면, 학습한 모델을 저장해두는 것이 필요하다. 이를 위해 joblib을 사용할 수 있었다.
import joblib
joblib.dump(rf_best, "rf_model.pkl")
불러올 때는 load()를 사용한다.
rf_backup = joblib.load("rf_model.pkl")
불러온 모델이 제대로 같은 예측을 하는지 확인할 수도 있다.
(rf_best.predict(X_val) != rf_backup.predict(X_val)).sum()
0
다만 운영체제가 다르거나 scikit-learn 버전이 다르면 모델 파일이 제대로 호환되지 않을 수 있다. 자료에서도 조원끼리 모델을 공유할 때 OS나 scikit-learn 버전 차이 때문에 문제가 생길 수 있고, 이 경우 버전을 맞추거나 pickle을 binary 모드로 명시해 저장/로드하는 방식이 필요하다고 설명했다.
import pickle
with open("rf_model.pkl", "wb") as f:
pickle.dump(rf_best, f)
with open("rf_model.pkl", "rb") as f:
rf_model = pickle.load(f)
이 부분은 나중에 프로젝트에서 중요할 것 같다. 모델을 한 번 학습하고 끝내는 것이 아니라, 저장하고 다시 불러와서 예측하거나 공유해야 하는 상황이 생기기 때문이다.
13. XGBoost는 Boosting 계열 모델로 이어졌다
마지막에는 XGBoost도 다뤘다. XGBoost는 Boosting 계열 모델이고, 앞 모델이 잘 못 맞춘 부분을 다음 모델이 보완하는 방식으로 이어진다. Random Forest가 여러 tree를 비교적 독립적으로 만들어 종합하는 느낌이라면, Boosting은 순차적으로 이어달리기하는 느낌에 가까웠다.
XGBoost는 Colab에서는 기본 설치되어 있었지만, 로컬에서 사용하려면 따로 설치가 필요할 수 있다.
conda install -c conda-forge xgboost
Python에서는 scikit-learn 스타일로 감싼 XGBClassifier를 사용했다.
from xgboost import XGBClassifier
xgb = XGBClassifier(
random_state=1234,
n_jobs=-1
)
XGBoost는 파라미터가 많고, scikit-learn wrapper에서 보이는 이름과 원본 XGBoost 문서에서 쓰는 이름이 조금 다를 수 있었다. 예를 들어 원본에서는 eta라고 부르는 것이 scikit-learn 스타일에서는 learning_rate로 보일 수 있다. 그래서 XGBoost를 제대로 보려면 Python package 문서와 원본 parameter 문서를 왔다 갔다 하며 확인해야 했다. 자료에서도 XGBoost는 scikit-learn과 호환되도록 포장된 wrapper를 쓰는 것이고, 세부 파라미터는 원본 문서를 같이 봐야 한다고 설명했다.
Boosting 계열에서는 n_estimators, learning_rate, max_depth, subsample, colsample_bytree 같은 파라미터가 중요하게 연결된다. 특히 Boosting은 에러를 계속 보완하는 구조라서 성능은 잘 나올 수 있지만, 파라미터를 과하게 만지면 시간도 오래 걸리고 overfitting도 생길 수 있다.
마무리
26일차는 타이타닉 데이터를 이용해 머신러닝 프로젝트의 기본 흐름을 한 번에 이어본 날이었다. 결측치를 처리하고, 문자 컬럼을 숫자로 바꾸고, 불필요한 컬럼을 제거한 뒤, Survived를 정답지로 분리했다. 이후 train 안에서 validation을 따로 만들고, K-Fold로 모델을 비교할 준비를 했다.
모델링에서는 먼저 KNN으로 baseline을 잡고, RandomizedSearchCV와 GridSearchCV로 하이퍼파라미터를 튜닝했다. 이후 Random Forest를 적용하면서 ensemble 계열 모델이 KNN보다 더 좋은 성능을 낼 수 있다는 것을 확인했고, n_estimators, max_features, max_depth, min_samples_split 같은 파라미터가 모델의 성격을 어떻게 바꾸는지도 봤다.
가장 크게 남은 건 전처리와 평가 구조였다. 모델 성능을 올리는 것도 중요하지만, train 기준으로 전처리 룰을 만들고, validation을 학습에 쓰지 않고, K-Fold의 재현성을 맞추고, RandomizedSearch와 GridSearch를 구분해서 쓰는 흐름이 더 기본이었다. 마지막에 모델 저장과 XGBoost까지 이어지면서, 단순히 모델 하나를 돌리는 것이 아니라 실험을 반복하고 결과를 남기는 방식까지 생각하게 됐다.
26일차는 타이타닉 데이터를 모델이 먹을 수 있는 숫자 행렬로 만들고, K-Fold와 하이퍼파라미터 튜닝을 통해 여러 모델을 비교하는 머신러닝 실험 흐름을 잡은 날이었다.
'[SK플래닛] ASAC 빅데이터전문가 11기 > 학습기록' 카테고리의 다른 글
| [SK플래닛] ASAC 빅데이터전문가 11기 | 28일차 (0) | 2026.05.28 |
|---|---|
| [SK플래닛] ASAC 빅데이터전문가 11기 | 27일차 (0) | 2026.05.27 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 25일차 (1) | 2026.05.22 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 24일차 (0) | 2026.05.21 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 22일차 (0) | 2026.05.20 |
