忍者ブログ
統計、機械学習、AIを学んでいきたいと思います。 お役に立てば幸いです。

【Kaggle挑戦記】Digit Recognizer:画像認識にLightGBMで挑む

S4E11の「名前(Name)」というノイズとの戦いを経て、今回は心機一転、画像認識の登竜門である「Digit Recognizer(手書き数字認識)」に挑戦しました。 画像認識といえばディープラーニング(CNN)が定石ですが、まずは慣れ親しんだLightGBMで「どこまで通用するか」をデバッグします。

1. 戦略:画像を「784個の変数」と見なす

28x28ピクセルの画像データを、意味を持つ「形」として捉えるのではなく、0から255の数値が入った784個のカラムとして扱います。 Macのローカル環境(ターミナル)にて、以下の多クラス分類(0〜9)用パラメータで実行しました。

params = {
    'objective': 'multiclass',
    'num_class': 10,
    'metric': 'multi_logloss',
    'verbosity': -1,
    'boosting_type': 'gbdt',
    'learning_rate': 0.1
}

2. 実行結果:Macターミナルのログ

学習はスムーズに進み、検証データにおいて非常に高い精度をマークしました。

Training until validation scores don't improve for 50 rounds
Did not meet early stopping. Best iteration is:
[100] valid_0's multi_logloss: 0.0857917

--- Validation Accuracy: 0.97417 ---
--- Submission file created: submission_digit_lgb.csv ---

3. リーダーボードの結果

Kaggleへ提出した結果、最終的な正解率は以下の通りとなりました。

正解率:0.97139

画像認識の専用モデルを使わずとも、約97.1%という精度を叩き出すことができました。 これは「ピクセルごとの輝度値」だけでも、数字の特徴を捉えるには十分な情報量が含まれていることを示唆しています。

4. 考察:エンジニアとしての気づき

前回のS4E11では「データの中身(Name)を疑う」ことが鍵でしたが、今回は「純粋な数値のパターン」が勝負でした。

  • 特徴量の多さ: 784個の変数を同時に扱う負荷も、Macのローカル環境で軽快に処理できました。
  • 多クラス分類の挙動: 0か1かの二値分類とは異なり、10種類の確率を計算する「multiclass」の動きをログから確認できたのは収穫です。

97%を超えたここから先は、CNNを導入して「形や線のつながり」を学習させる領域になります。 しかし、エンジニアの「手癖」としてのLightGBMが、画像認識においてもここまで強力なベースラインになることを確認できた、実りある修行となりました。


次なる実戦「Playground Series S4E12」の開始、あるいは地質予測の新コンペへの参戦に向け、 この「数値の羅列をねじ伏せる感覚」を研ぎ澄ませておきたいと思います。



PR

【Kaggle挑戦記】S4E11 #2:ノイズを削って精度を出す。名前(Name)削除の劇的ビフォーアフター

前回、予測の重要度ランキングで「Name(名前)」が1位という、エンジニアとして看過できない「仕様バグ」に近い状態に直面しました。 今回はこの不適切な依存関係を排除し、コードをリファクタリングして再挑戦した結果を報告します。

1. 実装:不要な依存関係を排除した「クリーン」なモデル

「名前で予測する」というハードコーディングに近い過学習を防ぐため、物理的にカラムをドロップしました。 また、コンペの締め切り後(After Deadline)でも確実に評価を通すため、型変換と出力を最適化した「修正版コード」を投入しました。

import pandas as pd
import lightgbm as lgb
from sklearn.preprocessing import LabelEncoder

# 1. データの読み込み
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

# 2. 前処理:ターゲットの数値化
le = LabelEncoder()
if train['Depression'].dtype == 'object':
    train['Depression'] = le.fit_transform(train['Depression'])

# 3. 特徴量の選択(「名前」を明示的にドロップ!)
drop_cols = ['id', 'Name']
X = train.drop(drop_cols + ['Depression'], axis=1)
y = train['Depression']
X_test = test.drop(drop_cols, axis=1)

# 4. カテゴリ変数の処理(LightGBM用)
cat_cols = X.select_dtypes(include=['object']).columns.tolist()
for col in cat_cols:
    X[col] = X[col].astype('category')
    X_test[col] = X_test[col].astype('category')

# 5. モデルの構築
params = {
    'objective': 'binary',
    'metric': 'binary_error',
    'verbosity': -1,
    'random_state': 42
}

model = lgb.LGBMClassifier(**params)
model.fit(X, y)

# 6. 予測と提出ファイルの作成
submission = pd.DataFrame({
    'id': test['id'],
    'Depression': model.predict(X_test)
})
submission['Depression'] = submission['Depression'].astype(int)
submission.to_csv('submission_final.csv', index=False)

2. コンソール出力:健全なランキングへの変遷

実行後、コンソールに表示されたランキングは、前回とは全く異なる「納得感」のある顔ぶれになりました。 可読性のために標準的なログスタイルで出力結果を記載します。

--- Submission file created successfully! ---
            feature  importance
2             City          494
12            Degree          388
4          Profession          359
1                Age          315
15  Financial Stress          224

3. スコア:ノイズを消して「実力」が向上

締め切り後(After Deadline)のLate Submissionですが、結果は意外なものでした。 名前を消したことで、むしろスコアが上昇したのです。

指標前回(Nameあり)今回(Nameなし)
Public Score 0.94008 0.94152 (↑)
Private Score 0.93868 0.93961 (↑)

4. 考察とまとめ

「名前」という強力なノイズがモデルを惑わせていたことが、スコアの向上によって証明されました。 不要なカラムを削除したことで、モデルが「City(都市)」や「Degree(学位)」といった、メンタルヘルスに真に影響を与えるコンテキストに集中できるようになった結果です。

「なぜ動くか分からないコード」を放置せず、違和感のある変数を削ぎ落とす。 バックエンドのデバッグと同じ姿勢で取り組むことが、Kaggleにおける精度向上にも直結することを実感した一戦でした。


Kaggleの履歴に「After Deadline」と「Error」のログが残ることは、試行錯誤の証。 次の「本物のコンペ」では、このデバッグの知見を最初からぶつけていきたいと思います。



【Kaggle挑戦記】S4E11 #1:全特徴量を投入して見えた「名前」の怪しい貢献度

KaggleのPlayground Series S4E11「メンタルヘルス予測」に参戦しました。 まずはエンジニアとしての定石通り、複雑な加工はせず「まずは動くもの」を目指します。 今回はあえて「id以外の全特徴量をぶっ込む」という、力押しのベースライン構築からスタートです。

1. 戦略:全件投入のフルスキャン・アプローチ

「どの変数が有効か?」と悩む前に、LightGBMの処理能力を信じて全データを投入しました。 通常なら除外するはずの「Name(名前)」カラムも、あえてそのまま。 カテゴリ変数は、LightGBMのcategory型にキャストするだけで、内部的に最適化されるように実装しました。

# 特徴量は全投入(idのみ除外)
X = train.drop(['id', 'Depression'], axis=1)
y = train['Depression']

# 全object型カラムをcategory型へキャスト
cat_cols = X.select_dtypes(include=['object']).columns.tolist()
for col in cat_cols:
    X[col] = X[col].astype('category')

2. リザルト:驚愕のスコアと「ログ」の違和感

Macで実行すること数秒。生成されたsubmission.csvをSubmitした結果、驚きのスコアが返ってきました。

Public Score: 0.94008 / Private Score: 0.93868

いきなり0.94超え。しかし、エンジニアとして「中身」をデバッグせずに喜ぶわけにはいきません。 モデルが何を根拠に判断したか(Feature Importance)を確認したところ、奇妙な事実が判明しました。

--- Feature Importance (Top 5) ---
1. Name : 1308
2. City : 223
3. Age : 204
4. Financial Stress : 174
5. Profession : 159

3. 考察:なぜ「名前」の貢献度が異常に高いのか?

予測の決め手が、年齢や仕事のストレスを差し置いて「Name(名前)」になっています。 バックエンドのロジックとして考えれば、名前の文字列そのものがメンタルヘルスに影響を与えるはずがありません。

考えられる仮説はいくつかあります:

  • 過学習(Overfitting): モデルが名前をユニークなキーとして認識し、個々のデータを「暗記」してしまった。
  • データリーク: 名前の付け方や順序に、正解データへのヒントが紛れ込んでいる(システムバグに近い状態)。
  • プロキシ変数の可能性: 名前が特定の居住地域や年齢層、あるいは家系的な背景を代理してしまっている。

4. まとめ:次回のデバッグ指針

「全部入り」で投げた結果、0.94という高いベンチマークと同時に、「名前という名の巨大なノイズ(あるいは不正解)」を炙り出すことができました。

この「名前」がスコアを不当に押し上げているのか、それとも何か深い意味があるのか。 次回は「Name」をdropした状態で再学習を行い、モデルの真の実力をデバッグしてみたいと思います。




【Kaggle挑戦記】S4E11 #1:メンタルヘルス予測コンペ参戦。LightGBMで「0.94」の初陣

次なるターゲットに選んだのはKaggle Playground Series S4E11。 実在のアンケート結果をベースにした「メンタルヘルス(うつ病)の予測」です。 実務的なデータ構造を相手に、まずは何も考えずLightGBMでベースラインを構築してみました。

1. コンペの概要:生体ログから「心の状態」をデバッグする

今回のミッションは、年齢、性別、仕事のストレス、睡眠時間、食事習慣といった多角的なデータから、 対象者がうつ病(Depression)の状態にあるかどうかを予測する2値分類です。

主要な特徴量エンジニア的解釈
Academic/Work Pressure システムの負荷状況(リソース逼迫度)
Sleep Duration / Dietary Habits メンテナンス(自己回復)のログ
Financial Stress 外部環境によるエラー因子

2. 実装:特徴量は「すべてぶっ込んだ」最短ルート

「どの項目が重要か?」を人間が考える前に、まずはマシンパワーに任せてみます。 今回はID以外の特徴量をすべてぶっ込み、カテゴリ変数はLightGBMのcategory型指定で一気に処理する、いわば「全件スキャン」的なアプローチをとりました。

通常なら除外するはずの「Name(名前)」のような文字列データも、モデルがどう解釈するかを見るためにあえて残しています。

import pandas as pd
import lightgbm as lgb
from sklearn.preprocessing import LabelEncoder

# データの読み込み
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

target_col = 'Depression'

# ターゲットの数値化
le = LabelEncoder()
train[target_col] = le.fit_transform(train[target_col])

# 特徴量は全投入(idのみ除外)
X = train.drop(['id', target_col], axis=1)
y = train[target_col]
X_test = test.drop(['id'], axis=1)

# カテゴリ型への一括変換
cat_cols = X.select_dtypes(include=['object']).columns.tolist()
for col in cat_cols:
    X[col] = X[col].astype('category')
    X_test[col] = X_test[col].astype('category')

# モデル構築(2値分類:Binary)
model = lgb.LGBMClassifier(objective='binary', random_state=42)
model.fit(X, y)

# 予測と提出ファイルの作成
submission = pd.DataFrame({
    'id': test['id'],
    target_col: model.predict(X_test)
})
submission.to_csv('submission.csv', index=False)

3. 結果と考察:見えてきた「データの罠」

初回のSubmit結果は以下の通りです。

Public Score: 0.94008 / Private Score: 0.93868

なかなかの高スコアですが、Feature Importance(重要度)を見るとデバッグすべき点が見つかりました。

--- Feature Importance Top 5 ---
1. Name              : 1308
2. City              : 223
3. Age               : 204
4. Financial Stress  : 174
5. Profession        : 159

「Name(名前)」が重要度のトップに君臨しています。 「全特徴量投入」の結果、モデルは名前に含まれる特定のパターン(あるいは個別のID的な性質)を、うつ病の判定材料として「暗記」してしまったようです。 これは典型的な過学習の予兆であり、システム開発で言えば「テストデータのみに通るハードコーディング」に近い状態かもしれません。

4. まとめと次回の課題

まずは「動くもの」を作り、0.94というベンチマークを得ることに成功しました。
次回は、この「Name」というノイズを除去した際にスコアがどう変化するか、そしてLog Lossを意識した確率予測のチューニングに挑みます。


【Kaggle挑戦記】House Prices #1:予測対象は「価格」。回帰モデルによる住宅見積もり

1. 新たな戦場:House Prices - Advanced Regression Techniques

Spaceship Titanicを終え、次に挑むのは「住宅価格予測」です。 アイオワ州エイムズにある住宅のスペックから、その販売価格(SalePrice)を予測します。 前回の「転送されたか否か(0/1)」の分類とは異なり、今回は具体的な数値を当てる「回帰(Regression)」問題に挑みます。

2. 「分類」と「回帰」の決定的な違い

エンジニアとして、まずは評価指標を頭に叩き込みます。出力型が根本から変わるため、デバッグの指標も切り替える必要があります。

  • ターゲット: Boolean(生存/死亡)から Float(住宅価格)へ。 「0か1か」のラベルではなく、連続的な数値を予測します。
  • 評価指標: RMSE(Root Mean Squared Error:平均平方二乗誤差)。 「何人当たったか」ではなく、予測価格と実際の価格の「ズレ(誤差)」を評価します。
  • モデル: LGBMClassifier ではなく、回帰専用の LGBMRegressor を使用します。

3. 設計思想:なぜ「最小二乗法」ではなく「決定木」なのか

回帰といえば「最小二乗法(線形回帰)」で一本の直線を引くイメージが強いですが、本攻略では現代的なLightGBM(決定木モデル)を採用します。

  • 伝統的な回帰(最小二乗法): 「面積が2倍なら価格も2倍」といった単純な比例関係に強いが、市場の複雑な歪みに弱い。
  • 現代的な回帰(LightGBM): 「築年数が古くても、リフォーム済みなら高値」といった、条件の組み合わせ(非線形な関係)を数千もの分岐でデバッグし、多角形的に価格を近似していきます。

住宅市場の「不連続な境界線(このエリアに入った瞬間に価格が跳ね上がる、など)」を捉えるには、決定木によるアプローチが極めて有効です。

4. 本アプローチの設計図

■ 今回使う特徴量(数値データに限定)

79個のカラムのうち、初回は「数値データ(int/float)」のみを抽出して投入します。 敷地面積(LotArea)、築年(YearBuilt)、1階の広さ(1stFlrSF)、バスルームの数(FullBath)などが含まれます。文字列(立地など)は一旦除外します。

■ 指標:二乗誤差の最小化

本コンペのゴールは、二乗誤差(の平均)を最小化することです。 誤差(予測 - 実測)を二乗することで、大きな予測ミスに対して指数関数的に厳しいペナルティを課し、モデルに「大外れを出すな」と教育します。 ※最終スコアは「対数をとった後のRMSE」になりますが、まずは生の数値で誤差を削ります。

5. 【初陣】回帰ベースライン・ソースコード

import pandas as pd
import numpy as np
import lightgbm as lgb

# 1. データの読み込み
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

# 2. ターゲットの設定
y_train = train['SalePrice']
# 数値データのみを抽出し、IDとターゲットを除外
X_train = train.select_dtypes(include=[np.number]).drop(['Id', 'SalePrice'], axis=1)
X_test = test.select_dtypes(include=[np.number]).drop(['Id'], axis=1)

# 3. 欠損値の補完
# 回帰問題の初期デバッグとして中央値(median)で埋める
X_train = X_train.fillna(X_train.median())
X_test = X_test.fillna(X_test.median())

# 4. 回帰モデルの構築
# 二乗誤差の最小化(regression)を目的関数に設定
model = lgb.LGBMRegressor(
    objective='regression',
    n_estimators=1000, 
    learning_rate=0.05,
    random_state=1
)

# 5. 学習
model.fit(X_train, y_train)

# 6. 予測(出力は具体的なドル建て価格の配列)
predictions = model.predict(X_test)

# 7. 提出用ファイルの作成
output = pd.DataFrame({'Id': test['Id'], 'SalePrice': predictions})
output.to_csv('submission_v1_baseline.csv', index=False)

print("✅ LightGBM Regressor baseline trained.")
print(f"Sample Predictions: {predictions[:5]}")

6. 実行結果のデバッグ:スコア 0.14679 の意味

Macのターミナルに出力された結果を解読します。

[LightGBM] [Info] Start training from score 180921.195890
Sample Predictions: [124527.61, 154677.30, 183652.70, ...]
Score: 0.14679

この 0.14679 というスコアは、ざっくり言うと「平均して14〜15%程度の見積もり誤差がある」状態を指します。

  • 0.14〜0.15(現在): 数値データのみの「とりあえず動くシステム」。
  • 0.12付近: 文字列データ(カテゴリ変数)を適切に処理した「実用レベル」。
  • 0.10以下: 上位ランカー。高度な特徴量生成とアンサンブルが必要な「プロの仕事」。

文字列データを一切使わず、数値のみでこの数値が出たのは、かなり幸先の良いスタートと言えます。

7. まとめ:次なる一歩

数値データだけで土俵に立ちましたが、まだ以下の「伸び代」が残っています。

  1. 無視された文字列データ: 立地(Neighborhood)などの、価格に直結する重要な情報がまだ「コメントアウト」されている状態です。
  2. 価格の歪み: 100万ドルの家と10万ドルの家では、同じ1万ドルのミスでも意味が違います。これを比率で評価させる「対数変換」の導入が必要です。

一本の直線を引くのではなく、数千の分岐で見積もりを研ぎ澄ます。15%の誤差を削る、新たな戦いの始まり。