이번 주 학습 내용

지난주 개념으로만 훑었던 머신러닝을 이번 주엔 실제로 손으로 돌려봤다. 경사하강법을 직접 구현하는 것부터 시작해서 회귀, 분류, 트리, 앙상블, SVM까지 나흘 동안 정말 많은 걸 다뤘다. 특히 타이타닉과 실제 캐글 데이터셋으로 EDA부터 전처리, 여러 모델 비교, 하이퍼파라미터 튜닝까지 한 사이클을 통째로 돌려본 게 가장 크게 남는다.

01. 회귀의 기초 — 경사하강법 손으로 구현하기

사이킷런의 LinearRegression을 바로 쓰기 전에, w=0, b=0에서 시작해서 경사하강법으로 직선을 맞춰가는 과정을 직접 짜봤다. epoch=0일 때 loss가 105.72였던 게 800번 반복 후 0.996까지 떨어지면서 w≈4.02, b≈6.16으로 수렴했는데, 실제 데이터를 만들 때 쓴 식이 y=6+4X+noise였다는 걸 생각하면 값이 거의 정확히 맞아떨어진 셈이었다.

  • 같은 데이터를 LinearRegression으로 한 줄에 풀어보니 coef_=2.0, intercept_=-0.2, R²=0.98로 바로 나왔다. 손으로 반복문을 돌리던 과정이 저 한 줄 안에 압축돼 있다는 걸 체감했다
  • 방 개수와 면적 두 변수로 집값을 예측하는 다중선형회귀도 연습했다. 3D로 회귀평면을 그려보니 변수가 두 개면 "선"이 아니라 "평면"으로 데이터를 맞추는 거라는 게 눈으로 확 들어왔다
  • PolynomialFeatures로 다항회귀와 단순선형회귀를 비교하고, learning_curve()로 학습/검증 오차 곡선을 그려서 과소적합·과적합을 눈으로 진단하는 법도 익혔다
for epoch in range(1000): y_pred = w * X + b loss = np.mean((y - y_pred) ** 2) dw = -2 * np.mean(X * (y - y_pred)) db = -2 * np.mean(y - y_pred) w -= lr * dw b -= lr * db # epoch=0, loss=105.72 → epoch=800, loss=0.996 (w=4.07, b=6.11)

02. 타이타닉으로 분류 입문 — 그리고 하이퍼파라미터 튜닝

결측치 처리(Age 177개는 평균으로, Cabin 687개는 앞글자만 뽑아 카테고리를 줄이는 식으로)와 인코딩부터 시작해서, 결정트리·랜덤포레스트·로지스틱회귀 세 모델을 기본 설정으로 비교했다. 정확도는 DT 0.7877, RF 0.8547, LR 0.8659 순으로 로지스틱회귀가 가장 높게 나왔는데, 여기서 끝내지 않고 튜닝까지 이어간 게 이번 주의 핵심이었다.

  • GridSearchCV로 결정트리의 max_depth, min_samples_split, min_samples_leaf를 격자 탐색했더니 {max_depth: 3, min_samples_leaf: 2} 조합에서 테스트 정확도가 0.7877 → 0.8715까지 올라갔다. 튜닝 하나로 이 정도 차이가 날 수 있다는 게 인상적이었다
  • Optuna도 처음 써봤다. 200번의 시도(trial)를 자동으로 돌려가며 최적 조합을 찾아주는데, 격자 탐색과 달리 이전 시도 결과를 참고해서 다음 시도할 값을 정한다는 점이 달랐다. DT는 0.8324, RF는 0.8659까지 나왔다
  • 로지스틱회귀를 Optuna로 튜닝하는 중엔 ConvergenceWarning: lbfgs failed to converge 경고가 떴다. max_iter를 866까지 늘려서야 수렴했는데, 반복 횟수가 부족하면 최적해를 못 찾고 중간에 멈출 수 있다는 걸 처음 알았다
from sklearn.model_selection import GridSearchCV param_grid = {'max_depth': [3, 5, 7, None], 'min_samples_split': [2, 5, 10], 'min_samples_leaf': [1, 2, 4]} grid = GridSearchCV(DecisionTreeClassifier(), param_grid, cv=5) grid.fit(X_train, y_train) # best: {max_depth: 3, min_samples_leaf: 2, min_samples_split: 2} → test acc 0.8715

03. 결정트리 — 지니 불순도와 정보이득을 수식으로

plot_tree로 iris 데이터의 트리 구조를 그려본 뒤, 트리가 어떤 기준으로 가지를 나누는지 지니 불순도와 엔트로피, 정보이득 공식을 직접 계산해봤다. 지니는 1 - Σp²로 0.444, 엔트로피는 -Σp·log2(p)로 0.918이 나왔고, 분할 전후 엔트로피 차이인 정보이득은 0.459였다.

  • 같은 iris 데이터로 트리 깊이를 제한 없이 뒀을 때 정확도 1.0, max_depth=2로 제한했을 때 0.9778이 나왔다. 완벽한 정확도가 오히려 과적합의 신호일 수 있다는 걸 숫자로 확인한 셈이었다

04. 회귀 실전 — 노래 인기도 예측이 생각보다 어려웠다

캐글의 song-popularity 데이터셋(18,835행)으로 danceability, energy, loudness 같은 오디오 특성만으로 인기도를 예측해봤다. VIF로 다중공선성이 없는 걸 확인하고, IQR로 이상치를 걸러내니 18,835행에서 11,183행으로 40% 가까이 줄었다.

  • LinearRegression으로 돌려보니 Test R²가 0.051. 오디오 특성만으로는 인기도의 5% 정도밖에 설명이 안 된다는 뜻이었다. 다항회귀로 바꿔봐도 0.046으로 별 차이가 없어서, 이 문제 자체가 선형적인 관계로는 잘 안 풀리는 문제라는 걸 알게 됐다
  • RandomForestRegressor로 바꾸니 Test R²가 0.406까지 올라갔다. 다만 Train R²는 0.906이어서 학습 데이터에는 꽤 과적합된 상태였다
  • feature_importances_를 뽑아보니 loudness, audio_valence, danceability, acousticness, energy 순으로 비슷비슷하게 중요도가 나뉘어 있었다. 하나가 압도적으로 중요한 게 아니라 여러 특성이 골고루 조금씩 기여하는 구조였다

05. 분류 실전 — 성격 유형 예측과 Pipeline

personality_dataset(2,900행, Extrovert 51.4% / Introvert 48.6%로 균형잡힌 데이터)으로 내향/외향을 분류하는 모델을 만들었다. 이 노트북에서 처음으로 PipelineColumnTransformer를 제대로 써봤는데, 전처리 단계를 모델마다 따로 짜지 않고 하나로 묶어서 여러 모델을 공정하게 비교할 수 있다는 게 편했다.

  • 로지스틱회귀 기본 모델이 테스트 정확도 0.9172, F1 0.9170으로 꽤 잘 나왔다. 반면 랜덤포레스트는 학습 정확도 0.9841인데 테스트는 0.8983으로 격차가 커서, 스스로 "RF 과적합 의심, 튜닝 필요"라고 메모까지 남겨뒀었다
  • ColumnTransformer + Pipeline으로 로지스틱회귀·랜덤포레스트·GradientBoosting을 동일한 전처리로 비교하니 로지스틱회귀와 GradientBoosting이 f1_weighted 0.9172로 나란히 1위, 랜덤포레스트가 0.9155로 근소하게 낮았다
  • 가상의 새 사람 데이터를 만들어서 best_pipeline.predict()로 실제 예측까지 돌려봤다. "Extrovert"라는 결과가 나왔는데, 학습시킨 모델에 진짜 새 입력을 넣어보는 게 은근히 재밌었다
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer pipeline = Pipeline([ ('preprocessor', ColumnTransformer([...])), ('classifier', LogisticRegression()) ]) pipeline.fit(X_train, y_train)

06. SVM — 커널 트릭으로 곡선 경계 만들기

SVC(kernel='linear')로 직선으로 나뉘는 데이터를 분리해본 뒤, kernel='rbf'로 동심원 모양의 데이터(make_circles)를 분리해봤다. 직선으로는 절대 못 나눌 모양인데 rbf 커널을 쓰니 곡선 형태의 결정 경계가 만들어지면서 깔끔하게 분리됐다. 회귀 버전인 SVR로 사인파 곡선을 따라가는 것도 같이 연습했다.

07. 앙상블 기법 총정리

iris 데이터로 Voting, Bagging, RandomForest, GradientBoosting, Stacking 다섯 가지 앙상블 기법을 한 번에 써봤다. 데이터 자체가 쉬워서 다섯 개 모두 정확도 1.0이 나왔지만, 목적은 정확도 비교보다 각 기법의 사용법 차이를 익히는 거였다.

  • Voting은 여러 모델의 예측을 투표(또는 확률 평균)로 합치고, Bagging은 같은 모델을 여러 번 부트스트랩 샘플로 학습시켜 평균 내고, Boosting은 이전 모델의 오차를 다음 모델이 보완해가는 방식이라는 차이를 코드로 직접 짚어볼 수 있었다
  • SVC(probability=True)를 Voting에 쓸 때 FutureWarning이 떴다. 사이킷런 최신 버전에서 이 옵션이 사용 중단(deprecated) 예정이고 대신 CalibratedClassifierCV를 쓰라는 안내였다. 라이브러리도 계속 바뀐다는 걸 실습하다 우연히 알게 됐다

08. 분류 알고리즘 수학 복습

한 주를 마무리하며 분류 알고리즘 밑바탕에 있는 수식들을 다시 손으로 짚어봤다. 유클리드·맨해튼·코사인·마할라노비스 네 가지 거리 척도를 직접 계산해보고, 시그모이드 함수와 소프트맥스 함수, Log Loss를 코드로 구현해봤다.

  • 소프트맥스로 3-클래스 점수를 확률로 바꾸면 [0.659, 0.242, 0.099]처럼 합이 1인 확률 벡터가 나온다는 걸 직접 확인했다. 이진분류의 시그모이드와 다중분류의 소프트맥스가 같은 계열의 함수라는 게 새삼 정리됐다
  • Confusion Matrix의 TP/TN/FP/FN을 numpy 불리언 연산만으로 직접 세어보는 것도 해봤다. classification_report가 내부적으로 이 네 개 숫자에서 정확도·정밀도·재현율을 계산한다는 걸 손으로 확인하니 그동안 그냥 출력만 읽던 지표들이 더 명확해졌다

KPT 회고

Keep

개념을 코드 한 줄로 바로 쓰기 전에 경사하강법이나 지니 불순도처럼 수식을 직접 구현해보는 순서가 좋았다. 사이킷런이 내부에서 뭘 하고 있는지 감이 잡히니까 결과 수치를 볼 때도 훨씬 이해가 잘 됐다.

타이타닉과 personality 데이터셋에서 여러 모델을 같은 조건으로 비교하고 튜닝까지 이어간 흐름이 몸에 익은 게 만족스럽다. EDA → 전처리 → 베이스라인 비교 → 튜닝 → 평가로 이어지는 순서가 이제 자연스러워졌다.

Problem

song_popularity에서 랜덤포레스트의 Train R²(0.906)와 Test R²(0.406) 차이를 보고도 바로 다음 데이터로 넘어가버려서, 과적합을 줄이는 튜닝까지는 손대지 못했다.

모델 하나하나의 개별 파라미터는 이해했는데, 여러 모델을 한 번에 비교할 때 어떤 상황에서 어떤 모델을 먼저 시도해야 하는지에 대한 감은 아직 부족하다고 느꼈다.

Try

과적합 갭이 큰 모델을 만나면 바로 다음으로 넘어가지 말고 max_depth나 정규화 파라미터를 조정해서 갭을 줄여보는 연습을 한 번씩 해본다. 새 데이터셋을 만나면 가장 단순한 모델(선형/로지스틱회귀)로 먼저 기준선을 잡고, 그 다음에 트리 기반 모델로 넘어가는 순서를 습관으로 만들어본다.

새롭게 발견한 것

완벽한 정확도는 오히려 의심할 신호. iris 데이터에 깊이 제한 없는 결정트리를 학습시키면 정확도 1.0이 나온다. 처음엔 "잘 맞췄다"고 생각했는데, max_depth=2로 제한하니 0.9778로 살짝 떨어지면서도 더 일반화된 모델이 된다는 걸 알게 됐다. 학습 데이터를 100% 외운 모델과 새 데이터에도 잘 통하는 모델은 다르다는 걸 숫자로 처음 체감했다.

격자 탐색과 Optuna의 차이. GridSearchCV는 정해둔 조합을 전부 시도하지만, Optuna는 이전 시도 결과를 바탕으로 다음에 시도할 값을 스스로 정해간다. 타이타닉 실습에서는 오히려 GridSearchCV 쪽 결과(0.8715)가 Optuna(0.8324)보다 좋게 나왔는데, 탐색 범위와 시도 횟수를 어떻게 잡느냐에 따라 결과가 달라질 수 있다는 것도 함께 배웠다.

선형 모델로는 안 풀리는 문제도 있다. song_popularity에서 선형회귀는 R² 0.05 수준으로 사실상 예측력이 없었는데, 같은 데이터에 랜덤포레스트를 쓰니 0.4까지 올라갔다. 데이터와 타깃 사이의 관계가 애초에 비선형이면 아무리 다항식을 더해도 선형 모델로는 한계가 있다는 걸 실감했다.

트러블슈팅

문제 : 로지스틱회귀가 수렴하지 않는다는 경고

Optuna로 로지스틱회귀를 튜닝하던 중 ConvergenceWarning: lbfgs failed to converge (status=1) 경고가 반복해서 떴다. 기본 max_iter=100으로는 최적해를 찾기 전에 반복이 끝나버린 것이었다.

-- 수정 전 LogisticRegression() # ConvergenceWarning: lbfgs failed to converge after 117 iteration(s)

max_iter 값을 늘려주니 경고 없이 수렴했다. 반복 횟수가 부족하면 모델이 아직 최적점에 도달하기 전에 학습이 강제 종료된다는 뜻이라는 걸 알게 됐다.

-- 수정 후 LogisticRegression(max_iter=866) # 경고 없이 수렴

경고가 뜬다고 결과가 아예 못 쓰는 건 아니지만, 수렴 여부는 모델이 안정적으로 학습됐는지를 보여주는 신호라서 그냥 넘기지 않는 게 좋겠다고 생각했다.

문제 : SVC의 probability 옵션 사용 중단 경고

Voting 앙상블에 SVC(probability=True)를 넣고 soft voting을 시도했더니 FutureWarning: The probability parameter was deprecated in 1.9 경고가 떴다.

-- 수정 전 SVC(probability=True) # FutureWarning: Use CalibratedClassifierCV(SVC(), ensemble=False) instead

사이킷런 최신 버전에서는 SVC에 확률 출력 기능을 내장하는 대신, 확률 보정을 별도 클래스로 분리하는 방향으로 바뀌고 있었다. 지금 당장은 동작하지만, 다음에 실전 프로젝트에서 SVM을 쓸 땐 안내된 방식으로 바꿔봐야겠다.

다음 주 목표

  • 새 데이터셋을 만나면 단순 모델로 기준선을 먼저 잡고 트리 기반 모델로 넘어가는 순서를 습관화하기
  • 과적합 갭이 큰 모델을 만나면 넘어가지 말고 파라미터 조정으로 갭을 줄여보는 연습하기
  • 이번 주 배운 튜닝·평가 흐름을 팀 프로젝트 데이터에 적용해보기
개념만 알던 알고리즘들을 실제 데이터에 돌려보니, 숫자가 예상대로 안 나올 때가 훨씬 더 많이 배우게 되는 순간이었다.
다음엔 이 모델들을 더 깊이 파고드는 한 주가 되길.