티스토리 뷰

 

Titanic - Machine Learning from Disaster | Kaggle

 

www.kaggle.com

모든 AI, DataScience 꿈나무들이 한 번은 도전해보는 캐글의 Titanic competition.

첫 도전은 판다스를 쓸 줄 몰라서 망했지만 이제는 다르다. 정말 1년 만에 다시 도전해보는 데이터셋.

 

데이터 전처리

결과에 영향을 주지않거나 난해해서 처리하기 어려운 특성들을 제거해주었다.

train = train.drop("Name", axis=1)
train = train.drop("Ticket", axis=1)
train = train.drop("Cabin", axis=1)
train = train.drop("PassengerId", axis=1)
train = train.drop("Embarked", axis=1)

이름과 티켓 넘버, 승객 번호와 출발지는 해당 승객이 사고에서 살아남을지, 죽을지와 연관이 없어 보인다.

Cabin (선실) 번호와 위치를 나타내 줄 것 같긴 한데 결측 값이 너무나도 많았다 (무려 78%가 null 값이다)

 

이후 남은 특성들은 다음과 같았다.

Pclass 좌석의 등급
Sex 성별
Age 나이
SibSp 같이 탄 형제, 배우자 수
Parch 같이 탄 부모, 자식 수
Fare 요금

물론 모든 특성들이 결과에 영향을 주었겠지만 나는 Pclass에 더 관심을 두고 접근해봤다.

실제로 1등실에 탄 사람들의 생존확률이 높았으며 이중에서도 여성의 생존율이 상당히 높았다.

 

일단 Pclass를 기준으로 비율이 동일하게 훈련 셋과 테스트 셋을 나눠주었다

from sklearn.model_selection import StratifiedShuffleSplit as SSS

# n_splits는 몇 번 섞을지 결정
# StratifiedShuffleSplit이 알아서 각 계층에서 동일한 비율을 가지고 샘플을 뽑게 해준다.
split = SSS(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(train, train["Pclass"]):
    strat_train_set = train.loc[train_index]
    strat_test_set = train.loc[test_index]

test_size도 0.1~0.3까지 모두 시도해봤는데 0.2 (20%)가 가장 높은 정확도를 가졌다.

 

이제 남은 특성에서 결측치는 '나이'만 남아있었고 이는 SimpleImputer를 이용하여 평균값으로 메워주었다.

이것이 가능하다고 생각했던 이유는 대다수 연령대가 20~40세 사이에 몰려있었고 평균을 내었을 때 실제 값과 동일할 가능성이 높다고 생각했다.

 

StandardScaler를 이용하여 각 특성마다의 스케일을 비슷하게 맞춰주고 성별과 Pclass에 대해서 원-핫 인코딩을 진행하였다.

 

원-핫 인코딩을 진행한 이유는 Pclass의 경우 1,2,3 이렇게 나누어지는데 이를 그대로 이용하면 스케일링이 진행되면서 특성이 작아질 수도 있고, 이 과정에서 1등석과 3등석의 차이가 사라질 것 같았다. 물론 1-2 등석과 2-3등석의 연관관계가 어느 정도 존재할 수 있겠지만 일단은 원 핫 인코딩으로 나누어서 접근해보았다.

 

그 결과 최종 파이프라인이 다음과 같이 짜였다.

# ColumnTransformer는 서로다른 col들이 독립적으로 transformer(달라도 됨)을 적용받게 해준다.
# 이후 이들을 합쳐주는 역할을 하는데 만약 희소 행렬과 밀집 행렬이 섞여있으면 밀집 정도에 따라 결정
# 변환 단계가 많기 때문에 연속된 변환을 처리하여 주는 Pipeline 클래스를 이용

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OrdinalEncoder

ordi_encoder = OrdinalEncoder()

numeric_val = ["Age", "Fare", "SibSp", "Parch"]
one_hot_val = ["Sex", "Pclass"]

num_pipeline = Pipeline([('imputer', SimpleImputer(strategy="median")),
                         ('std_scaler', StandardScaler())])

full_pipeline = ColumnTransformer([
    ("num", num_pipeline, numeric_val),
    ("one_hot", OneHotEncoder(), one_hot_val),
])

요약하면 numeric 한 특성들은 imputer로 결측 값을 평균값으로 메워주고 std_scaler로 스케일링을 진행해주고

이외의 값들은 원핫 인코딩으로 묶어 주었다.

 

모델 선택 및 훈련

가장 많이 시간을 쓴 부분

우선 RandomForestClassifier와 SGDClassifier 둘 사이에서 고민을 되게 했다.

우선 둘 다 이용해 보고 정확도와 roc_auc_score를 보고 따지기로 결정했다.

ROC의 경우 이진 분류에서 자주 쓰이는 성능의 평가 지표이다.

전체 양성 값들을 양성으로 판정하는 비율인 TPR과 음성을 양성으로 판정하는 비율인 FPR 사이의 관계를 보여준다.

보통은 TPR이 올라가면 FPR 역시 올라가고 TPR이 감소하면 FPR 역시 감소한다.

쉽게 말하면 양성을 잘 판정해낼수록 음성을 양성으로 판정할 확률 또한 증가하는 셈이다.

만약 TPR이 증가하는데도 FPR이 크게 증가하지 않는다면 해당 모델은 좋은 모델이라고 생각할 수 있다.

만약 좋은 모델이라면 그래프가 거의 직각을 그릴 것이고 곡선 아래쪽 넓이, 즉 AUC (Area Under Curve)가 1이 나올 것이다.

따라서 나도 이 지표를 활용해보기로 했고 우선 각 모델별 최적의 파라미터를 찾고 비교하기로 했다.

우선 GridSearchCV를 이용해서 여러 파라미터를 각 모델별로 적용했다.

처음에는 큰 범위로 파라미터들을 설정해주었다가 점점 좁혀나가는 식으로 접근했다.

 

RandomForest

from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

forest_clf = RandomForestClassifier()
# dict에 있는 모든 조합들을 시도해보고 최적의 파라미터를 반환하여준다.
param_grid = [{
    "min_samples_leaf": [1,2,3,4],
    "min_samples_split": [5, 6, 7, 8],
    "n_estimators": [45, 46, 47, 48,49,50],
    'max_features': [2, 3, 4, 5],
    'max_depth': [7, 8, 9, 10,11],
}]

grid_search = GridSearchCV(forest_clf,
                           param_grid,
                           cv=10,
                           scoring='accuracy',
                           return_train_score=True)
grid_search.fit(train_prepared, train_y.ravel())
print(grid_search.best_params_)
print(grid_search.best_estimator_)
print(grid_search.best_score_)

SGD

from sklearn.linear_model import SGDClassifier
from sklearn.model_selection import GridSearchCV
import warnings

warnings.filterwarnings('ignore')

SGD_clf = SGDClassifier()
param_grid2 = [{
    'max_iter': [2000, 2500, 3000, 3500],
    'tol': [1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6],
    'eta0': [0.1, 0.01, 0.001, 0.0001],
    'penalty': ['l1', 'elasticnet'],
    'l1_ratio': [0.15, 0.3, 0.45, 0.6],
    'learning_rate': ['constant', 'optimal'],
}]

grid_search2 = GridSearchCV(SGD_clf,param_grid2,cv=10,
                          scoring='accuracy',
                          return_train_score=True)
grid_search2.fit(train_prepared,train_y.ravel())
print(grid_search2.best_params_)
print(grid_search2.best_estimator_)
print(grid_search2.best_score_)

이후 roc_auc_score를 재보니 SGD는 0.82 RandomForest는 0.87이 나와서 RandomForest를 이용하기로 했다.

 

테스트 셋 검증 및 제출

사실 테스트 셋 검증은 크게 어렵지 않았다.

미리 훈련 셋을 처리하면서 같이 처리해준 테스트 셋을 cross_val_score를 이용하여 평균 정확도를 구해주었다.

(여기서 나온 결괏값과 Kaggle에 제출한 결괏값과 거의 유사했다)

 

만족스러운 결과가 나왔다면 제출할 데이터에 Survived를 predict() 메서드를 이용하여 추가시켜주고 불필요한 부분들을 삭제시켜서 제출한다.

 

result = pd.read_csv("datasets/titanic/test.csv")
result["Survived"] = best_clf.predict(final_prepared)
submission = result[['PassengerId', 'Survived']].copy()
submission["Survived"] = submission["Survived"].astype(int)
submission.to_csv('submission.csv', index=False)

이 전체 과정을 계속 반복하면서 최적의 값을 찾으려고 노력했다.

 

우선 Pclass에 대해 몇 번 변화가 있었다.

처음에는 1, 2, 3으로 되어있는 Pclass값을 그대로 이용하였다. 1등실과 2등실의 간의 관계와 2등실과 3등실의 간의 관계가 우선 존재한다고 생각되어서 특성 값 간의 관계를 줄여주는 원핫 인코딩보다 그냥 사용하는 것이 좋겠다고 생각했었기 때문이다.

하지만 시험 삼아 원핫 인코딩을 진행해본 결과 오히려 성능이 좋아지는 결과를 가져왔다.

그 이유를 알아보니 실제 타이타닉 참사 당시 통계를 보면 1등실과 2등실 탑승객들의 생존율이 상대적으로 높은 것은 사실이지만 3등실에 탑승한 승객들의 생존율이 저조했다. 이는 3등실 위치가 구명정과 가장 멀었고, 3등실이 탑승한 승객들이 미국으로 이민 가려고 했던 외국인들이 많아 언어가 서로 통하지 않았고 대피명령을 제대로 듣지 못해 배에서 헤매었기 때문이었다.

 

물론 그 당시 관행때문에 성별과 나이가 생존에 가장 큰 영향을 주었지만 좌석 등급과 요금 (사실 둘이 같은 셈) 또한 영향을 크게 주었음을 알 수 있다. (실제로 모델의 FeatureImportances를 조사하면 똑같이 나온다)

 

계속해서 수정을 거듭한 결과 568등에 0.79665 정확도를 얻게 되었다 (0.8이 정말 통곡의 벽 같다)

만약 정확도가 80%만 넘었어도 상위권으로 들어갈 수 있었을 텐데... 여기부터는 데이터를 더 정제할 필요성이 보였다.

우선 성별과 Pclass에 좀 더 가중치를 더 줄 필요가 있었고 형제, 배우자, 자식 수 특성을 더 고려해야겠다.

 

그리고 과연 Age 특성에서 결측 값을 평균으로 내준 게 최선이었을까...? 좀 더 공부해보고 다른 값으로 대체해봐야겠다.

그리고 자체적으로 데이터를 시각화해서 분석해보지 못한 게 가장 아쉽다. matplotlib에 대해 좀 더 공부해야겠다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday