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

【Kaggle挑戦記】Mechanisms of Action (MoA) Prediction:ベースラインモデルの構築と手元検証

前回、データの構造と役割を確認した「Mechanisms of Action (MoA) Prediction」コンペ。今回は、あえて高度な特徴量抽出などの加工は一切行わず、素のデータをそのままLightGBMに投入して手元での基準スコア(ベースライン)を算出しました。手元の環境である Python 3.13.5 で試してみた検証ログと、マルチラベル特有のスコアの読み方、そして実際に使用したコードを整理します。

0. 検証モデルの設計(5-Fold CV)

今回のターゲットは206種類に及ぶマルチラベル構造です。今回は最もシンプルかつ確実なアプローチとして、「206個のターゲットに対して、それぞれ独立したLightGBMの二値分類モデルを5分割交差検証(5-Fold CV)で合計1,030回ループさせて解く」という愚直なパイプラインを構築しました。

前処理としては、LightGBMがエラーを起こさないための最低限の置換(cp_doseの数値化)と、生物学的に正解がすべて「0」と決まっている偽薬データ(cp_type == 'ctl_vehicle')の除外のみを行っています。

1. 実装した交差検証コード

手元での検証(Cross Validation)を厳密に行うために作成したスクリプトの全文です。PandasとLightGBM、そしてscikit-learnを組み合わせてシンプルに記述しています。

import numpy as np
import pandas as pd
from lightgbm import LGBMClassifier
from sklearn.metrics import log_loss
from sklearn.model_selection import KFold
from sklearn.preprocessing import LabelEncoder
import warnings

# 不要な警告ログを非表示にする
warnings.filterwarnings("ignore")

# ==========================================
# 1. データの読み込み
# ==========================================
print("Loading data...")
train_features = pd.read_csv("train_features.csv")
train_targets = pd.read_csv("train_targets_scored.csv")

# ==========================================
# 2. 最低限の前処理(LightGBMが動くためだけ)
# ==========================================
print("Preprocessing...")

# cp_type が 'ctl_vehicle'(偽薬)のデータは、すべての正解が 0 と決まっています。
# ここは混ぜると学習のノイズになるため、純粋な薬のデータ(trt_cp)だけを抽出して学習させます。
train_mask = train_features["cp_type"] == "trt_cp"
X = train_features[train_mask].reset_index(drop=True)
y = train_targets[train_mask].reset_index(drop=True)

# IDと、不要になった cp_type カラムを削除
X = X.drop(columns=["sig_id", "cp_type"])
y = y.drop(columns=["sig_id"])

# 文字列のカテゴリ(cp_dose: D1/D2)を数値(0/1)に変換
le = LabelEncoder()
X["cp_dose"] = le.fit_transform(X["cp_dose"])

# ==========================================
# 3. 交差検証(5-Fold CV)のセットアップ
# ==========================================
kf = KFold(n_splits=5, shuffle=True, random_state=42)

# すべてのターゲット、すべての行の予測値を格納するゼロ行列を用意
oof_preds = np.zeros(y.shape)
target_cols = y.columns

print(f"Dataset shape: {X.shape}")
print(f"Number of targets: {len(target_cols)}")
print("Starting Cross Validation...")

# ==========================================
# 4. 206個のターゲットをループで1つずつ学習
# ==========================================
for idx, col in enumerate(target_cols):
    y_target = y[col]

    # そのターゲットに「1(効き目あり)」が1つもない特殊なケースの例外処理
    if y_target.sum() == 0:
        continue

    # 5分割のクロスバリデーションを実行
    for train_idx, val_idx in kf.split(X, y_target):
        X_train, y_train = X.iloc[train_idx], y_target.iloc[train_idx]
        X_val, y_val = X.iloc[val_idx], y_target.iloc[val_idx]

        # デフォルト設定のままのLightGBM(二値分類)
        model = LGBMClassifier(
            objective="binary",
            random_state=42,
            n_estimators=100,
            learning_rate=0.05,
            verbose=-1,
        )

        model.fit(X_train, y_train)

        # 「1」になる予測確率を対応する位置に格納
        oof_preds[val_idx, idx] = model.predict_proba(X_val)[:, 1]

    # 進行状況を20個ごとに表示
    if (idx + 1) % 20 == 0 or (idx + 1) == len(target_cols):
        print(f" Trained targets: {idx + 1}/{len(target_cols)}")

# ==========================================
# 5. 全体のスコア(Log Loss)を計算
# ==========================================
losses = []
for idx, col in enumerate(target_cols):
    loss = log_loss(y[col], oof_preds[:, idx], labels=[0, 1])
    losses.append(loss)

cv_score = np.mean(losses)
print("\n" + "=" * 30)
print(f"手元での交差検証スコア(CV Score): {cv_score:.5f}")
print("=" * 30)

2. 交差検証の実行ログ

手元の環境(Mac M3、Python 3.13.5)で上記のスクリプトを試してみました。

Loading data...
Preprocessing...
Dataset shape: (21948, 874)
Number of targets: 206
Starting Cross Validation...
 Trained targets: 20/206
 Trained targets: 40/206
 Trained targets: 60/206
 Trained targets: 80/206
 Trained targets: 100/206
 Trained targets: 120/206
 Trained targets: 140/206
 Trained targets: 160/206
 Trained targets: 180/206
 Trained targets: 200/206
 Trained targets: 206/206

==============================
手元での交差検証スコア(CV Score): 0.03933
==============================

3. スコア「0.03933」の正体と、log(対数)がもたらすペナルティ

一見すると「0.039」という数字は非常に低く、モデルが30%くらい予測を外している(正解率70%程度)ように錯覚してしまいます。しかし結論から言うと、予測のハズレ割合(件数)としてはすでに1%未満に抑え込まれています。これこそが本コンペの評価指標であるLog Loss(対数損失)の最大の特徴です。

Log Lossは予測の正誤だけでなく「モデルの自信度合い(確率)」を厳しく採点する指標で、数式にlog(対数)が含まれています。そのため、間違いが大きくなるほどペナルティが倍々ゲーム(指数関数的)に大炎上する仕組みになっています。

  • 「0(効き目なし)」のエリア: 正解の99%以上が0なので、モデルが「99%の確率で0」と安全な予測を出すだけで、ペナルティはほぼ0点になります。ここで大量の貯金を稼いでいます。
  • たまに登場する「1(効き目あり)」のエリア: ここでモデルが「うーん、自信がないから確率5%くらいで1かな…」と弱気に見逃すと、一発で3点〜4点という特大のペナルティ(罰点)を喰らいます。

この「たまにある1を見逃したときの特大ペナルティ」を、2.4万行 × 206個という膨大なデータの海(分母)で薄く平均化した結果、最終的に「0.03933」という極小の数字として残っているのがこのスコアの正体です。つまり、件数としてはほぼ当たっていますが、本物の「1」に対してモデルがまだ自信を持てずにモヤモヤしている状態と言えます。

初期基準(ベースライン):CV Score = 0.03933ここからの戦いは、件数を増やすのではなく、特徴量を使ってモデルに決定的な証拠を教え、「1」のときに自信満々で高い確率を出させる戦いになります。

4. 次なる一手

何一つ工夫をしていない「素のLightGBM」でここまでのベースラインを測定できたため、手元の検証環境(CV)としては100点満点の仕上がりです。ここからは、先述した「投与時間(cp_time)」や「投与量(cp_dose)」を軸にした特徴量生成や、遺伝子データ(g-)と細胞データ(c-)の統計量を組み合わせ、このスコアをどこまで削り落とせるかの本格的な実験フェーズに移行します。



PR

【Kaggle挑戦記】Mechanisms of Action (MoA) Prediction:データの取得と構造の確認

手書き文字認識の「Digit Recognizer」などを経て、今回は生物・創薬系のデータを扱った「Mechanisms of Action (MoA) Prediction」に挑戦しました。今回はコンペのアーカイブデータを活用し、Late Submissionによる順位確認を目指します。まずは特徴量抽出などの複雑な加工は一切行わず、生のデータをそのままLightGBMに投入する「ベースライン構築」に向けて、データの入手手順と構造を整理します。

0. このコンペティションの概要

「Mechanisms of Action (MoA) Prediction」は、新薬の候補化合物が人間の細胞(遺伝子や細胞株)に与える影響を分析し、その薬が「どのような効き目(作用機序=MoA)」を持っているかを予測するコンペティションです。データの主な仕様は以下の通りです。

  • データ規模: 訓練用の入力データが約2.4万行、テストデータが約4,000行弱。横方向には800を超える大量の特徴量カラムが並ぶ大規模な表形式データです。
  • 入力データ: 細胞の写真などの画像ではなく、100%数値化されたデータです。約770種類の遺伝子発現変化量(g-特徴量)と、100種類の細胞生存率(c-特徴量)、そして実験条件(時間や投与量)で構成されています。
  • タスク: 用意された206種類に及ぶ「効き目のチェックリスト」に対して、それぞれの効き目が「ある(1)」か「ない(0)」かをすべて同時に予測する「マルチラベル二値分類」です。評価指標は「Log Loss(交差エントロピー誤差)」の平均値が採用されています。

終了済みのコンペですが、テストデータ(test_features.csv)に対する予測ファイルを提出する「Late Submission」に対応しています。これにより、当時のリーダーボード(順位表)のどの位置に自分が食い込めていたかのスコアと暫定順位を正確に確認し、腕試しをすることができます。

1. データの入手

データの取得手順は以下の通りです。

  • Kaggleにログインします。
  • 検索窓で「Mechanisms of Action (MoA) Prediction」または「Kaggle MoA」を検索します。
  • コンペティションのページから「Data」タブを選択します。
  • 右上の「Download All」ボタンから、ZIP形式で一括ダウンロードします。
  • ダウンロードしたZIPファイルを適当なディレクトリで展開します。

展開すると、ディレクトリ内に役割の異なる6つのCSVファイルが生成されます。

2. 主要なCSVファイルの解説

展開して得られたCSVファイルの中から、初期のLightGBMベースライン構築において直接使用する重要なファイルを整理しました。カラム構造とそれぞれの役割は以下の通りです。

ファイル名 / カラム名解説
train_features.csv 学習用の入力データ(約2.4万行)。モデルの「入力(X)」となるファイルです。
├ sig_id 各実験サンプルの固有識別子(ID)
├ cp_type / cp_time / cp_dose 実験条件(薬か偽薬か、投与時間 24/48/72時間、投与量 高/低)
├ g-0 ~ g-771 772種類の遺伝子発現データ(すべて小数点付きの数値)
├ c-0 ~ c-99 100種類の細胞生存率データ(すべて小数点付きの数値)
train_targets_scored.csv 学習用の正解データ(教師データ)。モデルの「目標(Y)」となるファイルです。
└ 206種類の作用機序カラム 各薬の効き目の有無を示すバイナリフラグ(0 = なし、1 = あり)。1行の中に「1」が複数存在し得るマルチラベル構造です。
test_features.csv Kaggleに提出する予測値を計算するための、本番用入力データです(正解は含まれません)。

このコンペティションの最大のポイントは、入力データが「実験条件を表すカテゴリ変数」と「ひたすら横に並ぶ872個の純粋な数値」だけで美しく整理されている点にあります。面倒な文字列処理や画像の埋め込みが一切ないため、初期の数値データそのままでLightGBMへダイレクトに投入可能なデータ構造となっています。

予測タスク:実験条件と872個の数値データから、206種類のターゲットフラグ(0 または 1)を同時に予測するマルチラベル分類

あえて高度な特徴量抽出は行わず、この数値の塊をそのままLightGBMに学習させ、206個の各ターゲットに対してどのような境界線を描くことができるのか、まずはストレートな実力を検証します。なお、オマケデータである「train_drug.csv」やスコア対象外の「train_targets_nonscored.csv」は初手では一切使用せず、完全に無視して進めます。

3. 今後の流れ

必要な3つの基本CSVファイルが手元に綺麗に揃いました。まずは実験条件(cp_time や cp_dose など)をLightGBMが読める形式にカテゴリ数値化する最低限の前処理だけを行い、そのまま206回ループさせてLightGBMの二値分類モデルを回すベースラインパイプラインの構築に移ります。


【Kaggle挑戦記】DNA Classification Dataset:データの取得と構造の確認

手書き文字認識の「Digit Recognizer」を経て、今回は「DNA Classification Dataset」に挑戦しました。今回はコンペではなく、データセットでの分析にチャレンジします。モデル作成の前に、データの入手手順、データの読み方、およびこのデータにおける予測ターゲットの考え方を整理します。

0. このデータセットの概要

「DNA Classification Dataset」は、ゲノムデータ分析、機械学習、およびバイオインフォマティクス研究のために設計された、3,000個の合成DNAサンプルを含むデータセットです。データの主な仕様は以下の通りです。

  • データ規模: 合計3,000行、13カラム(3,000サンプルのDNA配列および統計データ)
  • 入力データ: DNA配列の文字列に加え、GC含有率や各塩基の個数など、配列の統計属性があらかじめ特徴量として含まれています。
  • タスク: 与えられたDNAデータから、目的変数(ターゲット)を切り替えることで、生物種の分類や変異の有無、リスクの予測など、複数の異なる検証を行うことができる多角的な構造になっています。

通常のコンペと異なり、最初から特定のテストデータや単一のゴールが分かれて提供されているわけではありません。Kaggle側での自動採点もないため、手元にある3,000行のデータを自分で分割し、設定したタスクごとに交差検証(クロスバリデーション)によって手元で予測精度を評価・検証していく必要があります。

1. データの入手

データの取得手順は以下の通りです。

  • Kaggleにログインします。
  • 検索窓で「DNA Classification Dataset」を検索します。
  • 該当する「DNA Classification dataset」を選択します。
  • 右上のダウンロードから、ZIP形式でダウンロードします。
  • ダウンロードしたZIPファイルを適当なディレクトリで展開します。

展開すると、ディレクトリ内に「synthetic_dna_dataset.csv」ができます。

2. synthetic_dna_dataset.csvの解説

展開して得られたCSVファイルの中身を確認しました。このデータセットに含まれる13個のカラム一覧とそれぞれの解説は以下の通りです。

カラム名解説
Sample_ID 各DNAサンプルの固有の識別子(ID)
Sequence DNAの配列データ(A, T, C, Gの文字列)
GC_Content 配列中におけるグアニン(G)とシトシン(C)の割合(%)
AT_Content 配列中におけるアデニン(A)とチミン(T)の割合(%)
Sequence_Length 配列の総文字数(長さ)
Num_A 配列中に含まれるアデニン(A)の個数
Num_T 配列中に含まれるチミン(T)の個数
Num_C 配列中に含まれるシトシン(C)の個数
Num_G 配列中に含まれるグアニン(G)の個数
kmer_3_freq 3文字の塩基トリプレット(3-mer)の平均出現頻度スコア
Mutation_Flag 変異の有無を示すバイナリフラグ(0 = なし、1 = あり)。【予測ターゲット候補1:二値分類】
Class_Label サンプルの分類クラス(Human / Bacteria / Virus / Plant)。【予測ターゲット候補2:多値分類】
Disease_Risk サンプルに関連するリスクレベル(Low / Medium / High)。【予測ターゲット候補3:多値・順序分類】

このデータセットの特徴は、ゴールとなる目的変数を何に設定するかによって、全く異なる予測モデルの検証ができる点にあります。配列データや、あらかじめ計算されている塩基の個数などの特徴量をベースにして、どの予測タスクからアプローチするかを自由に選択できる構造になっています。

複数の予測タスク:生物種分類(Class_Label) / 変異判定(Mutation_Flag) / リスク予測(Disease_Risk)

配列のゆらぎや塩基の統計的特徴を組み合わせることで、それぞれのターゲットに対してLightGBMがどのように境界線を学習するのか、切り口を変えて複数の検証を回せるデータ構成となっています。

3. 今後の流れ

あらかじめ塩基の個数や割合といった数値の特徴量が豊富に用意されているため、生の文字列(Sequence)を加工しなくても、初期の数値データだけでLightGBMに投入可能な構造になっています。まずはこの綺麗なデータセットが手元に用意できたので、ここからどのタスクの検証作業に入るかを決定します。


次は、これらの予測候補の中から最初のターゲットを決定し、交差検証を回すための前処理を進めていきます。



【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」の開始、あるいは地質予測の新コンペへの参戦に向け、 この「数値の羅列をねじ伏せる感覚」を研ぎ澄ませておきたいと思います。



【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」のログが残ることは、試行錯誤の証。 次の「本物のコンペ」では、このデバッグの知見を最初からぶつけていきたいと思います。