🐒

分類問題に対する最後の悪あがき

2024/02/12に公開
2

SIGNATEの「第2回 金融データ活用チャレンジ」に参加しました。テーブルデータによる分類精度を競うコンペなのですが、いくら頑張ってもスコアがなかなか伸びません。〆切り間近ですが思いついた手法をメモ代わりに投稿します。
https://signate.jp/competitions/1325

まず一通り特徴量エンジニアリングを頑張って、モデルのパラメータ調整も頑張って、アンサンブルを試してみたり、スタッキングを試してみたり、それはもう色々やりました。

そして「打つ手がなくなったかな?」と諦めかけていたところ、混合行列を眺めいたら、
ふと閃いたんです( ゚д゚)ハッ!

『不正解だったやつの正解率を上げたらいいんじゃね?』って

誰もが思いつきそうな陳腐な発想です。失礼いたしましたm(__)m
とりま、不正解データのindexを抽出します。

# 不正解データの抽出

# pred_trainの計算
pred_train = sum(globals()[f'pred_train_{i}'] for i in range(5)) / 5
pred_train_ = (pred_train > 0.5).astype(int)

# 正解データと不正解データのindexを取得
misclassified_indices = np.where(y != pred_train_)[0]
correct_indices = np.where(y == pred_train_)[0]

correct_data = X_new.loc[correct_indices]
misclassified_data = X_new.loc[misclassified_indices]

y_correct = y.loc[correct_indices]
y_misclassified = y.loc[misclassified_indices]

でも、待てよ、これ使って再学習させても、今まで正解してたやつが不正解になったらやばくねぇ?ってことで、少し正解データも混ぜてみます。とりま、1:1にしましたが、まあ、時間が無いので調べるのはやめにしときます。データの性質や学習器の癖に依るんでしょうけど、どのくらいの比率で混ぜたら良いかなんて、さっぱりわかりません( ´∀` )、ratioの値を変えながら試すしかないですね。

# 正解データの一部を不正解データに結合
print('misclassified_data_use:',len(misclassified_data))
ratio = 1.0
num_misclassified_to_use = int(ratio*len(misclassified_data))
print('num_misclassified_to_use:',num_misclassified_to_use)
correct_data_to_misclassify = correct_data.sample(num_misclassified_to_use, random_state=42)

X_combined = pd.concat([misclassified_data, correct_data_to_misclassify])
print('X_combined.index',X_combined.index)
X_excluded = X_new.drop(index=X_combined.index)
print('X_excluded',X_excluded)

# 目的変数の再結合
y_combined = pd.concat([y_misclassified, y.loc[correct_data_to_misclassify.index]])

つぎは、こいつを使って交差検証です。交差検証して、学習に使ってない部分の予測を繋ぎ合わせることで X_combined に対する予測データを集めます。スタッキングと同じ手法ですね。そしておつぎは、その結果を使って、pred_train(不正解データの抽出以前の予測)とpred_train_f(不正解データの割合が増えてる学習データで学習した学習器による予測)を良い塩梅に混ぜ合わせるわけです。こんな感じ・・・

# meanF1scoreのベスト閾値を探索
best_score = 0
base_s = 0.
best_s = 0.

for s in tqdm(np.arange(1000)/1000):
    pred_train_ = pred_train*(1-s) + pred_train_f*s
    score_value = f1_score(y, (pred_train_ > 0.5).astype(int), average='macro')
    if score_value > best_score:
        best_score = score_value
        best_s = s

pred_train_ = pred_train*(1-best_s) + pred_train_f*best_s
score_value = f1_score(y, pred_train_ > 0.5, average='macro')
print('best_score=', best_score,' (check:)score_value=',score_value)
print('s=',best_s)

# 結果保存
y_pred = pred*(1-best_s) + pred_f*best_s
sample_submit[1] = (y_pred > 0.5).astype(int)

それでもだめなら、、、↑でスタッキングと同じって言いましたよね。このデータをメタ特徴量として追加して更に学習器作っちゃうです( ノД`)シクシク…さて、どうなることやら
(完)

Discussion

ピン留めされたアイテム
saru_da_monsaru_da_mon

追記)結局、要のところはこうやって扱う事にしました。

不均衡データの2値分類問題に対し、ターゲットエンコーディング、スタッキングを駆使して得られた結果がこちらです。選んだ特徴量が悪いのか、ターゲットエンコーディングが悪さをしてるのか、meanF1score の値も平凡ですね(/ω\)この方法でリークが生じてるとはとても思えないので安心ではあります。

さて、混合行列に着目してみましょう。識別を誤ったデータ: 2069+2918=4987(個)を正しく識別させたいわけです。これらを再学習するのに、正解データも一緒に学習させます。
TP: 35688
TN: 1622
のうち小さいのはTNだから、TPから同数の 1622 個をサンプリングし、併せて 8231(個)のデータを新たな学習データとして採用します。コードは以下のようになります。

# 正解データと不正解データのindexを取得
misclassified_indices = np.where(y != pred_train_)[0]
correct_indices = np.where(y == pred_train_)[0]

correct_data = X.loc[correct_indices]
misclassified_data = X.loc[misclassified_indices]

y_correct = y.loc[correct_indices]
y_misclassified = y.loc[misclassified_indices]

# 正例と負例の正解数を数える
num_positive_samples = sum((y == pred_train_) & (y == 1))
num_negative_samples = sum((y == pred_train_) & (y == 0))

# 正解した正例と負例の数を同じにするためのサンプリング数を計算
num_samples_to_use = min(num_positive_samples, num_negative_samples)

# 正例のデータをサンプリング
positive_samples = X[(y == pred_train_) & (y == 1)].sample(n=num_samples_to_use, random_state=42)
y_positive_samples = y.loc[positive_samples.index]

# 負例のデータをサンプリング
negative_samples = X[(y == pred_train_) & (y == 0)].sample(n=num_samples_to_use, random_state=14)
y_negative_samples = y.loc[negative_samples.index]

# 正例と負例を結合
X_combined = pd.concat([positive_samples, negative_samples])
y_combined = pd.concat([y_positive_samples, y_negative_samples])

print('misclassified_data_use:', len(misclassified_data))
print('TP:',sum((y == pred_train_) & (y == 1)))
print('TN:',sum((y == pred_train_) & (y == 0)))
print('num_samples_to_use:', num_samples_to_use)
print('X_combined:',X_combined.shape)

# 不正解データを結合
X_combined = pd.concat([misclassified_data, X_combined])
y_combined = pd.concat([y_misclassified, y_combined])
X_combined = X_combined.sort_index()
y_combined = y_combined.sort_index()
X_combined_index = X_combined.index
print('X_combined.index',X_combined.index)

# X_combined 以外のデータを作成
X_excluded = X.drop(index=X_combined.index)
X_excluded = X_excluded.sort_index()
X_excluded_index = X_excluded.index
print('X_excluded.index',X_excluded_index)

# X_combined 以外のデータをtestデータに結合
test.index = test_index
X_all = pd.concat([X_excluded,test],axis=0)
X_all = X_all.sort_index()
X_all_index = X_all.index

交差検証をさせると同時に全学習データに対する予測とtestデータの予測を作りたいので、少し工夫が必要です。出てきた結果を操作するのに、以下の関数を定義します。

def adjust_shape():
    # pred_train_fの計算
    pred_train_f = pd.DataFrame(sum(globals()[f'pred_train_f{i}'] for i in range(3)) / 3)
    pred_train_f.index = X_combined.index
    pred_train_f = pred_train_f.sort_index()
    # print(pred_train_f)

    # pred_fの計算
    pred_f = pd.DataFrame(sum(globals()[f'pred_f{i}'] for i in range(3)) / 3)
    pred_f.index = X_all.index
    pred_f = pred_f.sort_index()
    # print(pred_f)

    df_copy = pd.concat([pred_train_f,pred_f]).sort_index()
    pred_train_f = df_copy[0:len(train)]
    pred_f = df_copy[len(train):]

    pred_f = pd.DataFrame(pred_f).squeeze()
    pred_train_f = pd.DataFrame(pred_train_f).squeeze()
    
    print(pred_train_f)
    print(pred_f)
    return pred_train_f, pred_f.reset_index(drop=True)

上記の関数を交差検証の各出力に対し実行させます。

# kFoldのlightGBMを実行
pred_train_f0, pred_f0 = kFold_LightGBM_2(NCV, X_combined, y_combined, X_all, 431)
pred_train_f1, pred_f1 = kFold_LightGBM_2(NCV, X_combined, y_combined, X_all, 849)
pred_train_f2, pred_f2 = kFold_LightGBM_2(NCV, X_combined, y_combined, X_all, 313)

pred_train_LGB_f, pred_LGB_f = adjust_shape()

こうやって、様々なモデルで(X_combined, y_combined)を学習データとしてメタ特徴量を作っていきます。もとの学習データ(X,y)を使って得られたメタ特徴量と合わせて最後に学習器にかけて結果を出します。こちらが、その結果です。

このコンペ、最終日の15時過ぎ頃から鯖落ちで、この結果は提出できずに終わりました。非常に歯痒い思いです_(:3 」∠ )_もう頑張れない・・・

saru_da_monsaru_da_mon

【注意】この手法は、target encoding のもとではリークを起こす可能性があります。今回のコンペのデータがAIに疑似生成させたもので特殊であることが原因なのか、それは分かりません。この手法について、別のコンペでも試してみます。その都度、報告しようとは思います。