まだタイトルない

アウトプット用です

機械学習コンペ(テーブルデータ)をする時の特徴量管理を考える #1

kaggleなどでのテーブルコンペの公開ノートブックではそのノートですべてを完結させるという意味でもノート内で特徴量を作成していることが多いです。

しかし、長期間のコンペになると実験数は増えるし、処理に時間がかかる特徴量を使うケースも増えてきます。実験のたびに特徴量を計算するのは地球にやさしくない。。。

この問題に対するシンプルな対策は作った特徴量をファイルで保存しておいて読み取るだけにすることだと思います。

具体的な方法は kaggle 特徴量 管理 でググれば素晴らしい記事がいくつも出て来ると思います。

今回は特徴量毎に数値特徴量かカテゴリ特徴量かの情報も欲しくなったのでその情報も一緒に管理できるようにしていたのでその管理方法を紹介したいと思います。

そのまま誰かの役に立てば幸いですし、何かフィードバックが得られれば嬉しいです。

内容

  • trainの特徴量、testの特徴量、数値特徴量名、カテゴリ特徴量名をdictionaryに入れて保存
  • あまりにもデータが多い時は容量の無駄になるが、mergeでなくconcatで繋げる想定
    • reset_indexしておかないとconcatで行数が増えてバグるから注意(何回かやった

特徴量を作るとき

例えば特徴量を作るときはこのように作って保存

import umap

def get_train_test():
    _train = pd.read_csv(INPUT_DIR / "train.csv")
    _test = pd.read_csv(INPUT_DIR / "test.csv")
    return _train, _test

def save_pickle(filename, obj):
    with open(filename, mode='wb') as f:
        pickle.dump(obj, f)

n_dim = 6
df_train, df_test = get_train_test()
len_train, len_test = len(df_train), len(df_test)
df = pd.concat([df_train,df_test])
cont_cols = [col for col in df_train.columns if pd.api.types.is_numeric_dtype(df_train[col])]
for col in ['id','pm25_mid']:
    cont_cols.remove(col)

# 次元削減する
mapper = umap.UMAP(n_components=n_dim, random_state=29)
x_embedded = mapper.fit_transform(df[cont_cols])
df = pd.DataFrame(x_embedded).add_prefix('UMAP')

# 戻す
df_train = df[:len_train]
df_test = df[len_train:]

# 保存
COL_UMAP = df.columns.to_list()
d = dict(train=df_train[COL_UMAP],test=df_test[COL_UMAP],cate_cols=[],cont_cols=COL_UMAP)
save_pickle(FEAT_DIR / 'UMAP.pkl', d)

読み込むとき

読み込むときはfeatsの中に使う特徴量を羅列していく

# 使う特徴を並べる
feats = [
    'OE',
    'Country_TE',
    'near_city1',
#     'min_max_diff',
    'near1_City_LE',
    'UMAP',
]
for f in feats:
    d = load_pickle(FEAT_DIR / f"{f}.pkl")
    df_train = pd.concat([df_train, d["train"].reset_index(drop=True)],axis=1)
    df_test = pd.concat([df_test, d["test"].reset_index(drop=True)],axis=1)
    CFG.cate_cols += d["cate_cols"]
    CFG.cont_cols += d["cont_cols"]
    print(f'Load... {f} , {len(d["cate_cols"])+len(d["cont_cols"])}features')
print(f'{len(CFG.cate_cols)}cate_cols, {len(CFG.cont_cols)}cont_cols')

CFG.feature_cols =  CFG.cont_cols.copy() + CFG.cate_cols.copy()

LightGBM

Catboost

  • カテゴリ変数をstringにする必要がある
    • df_train[CFG.cate_cols] = df_train[CFG.cate_cols].astype(str)
  • Poolを作るときにカテゴリ変数を指定する
    • Pool(df_test[CFG.feature_cols],cat_features=CFG.cate_cols)

XGBoost

  • 特になし

DNN

  • DNNだとOne-Hotやラベルエンコードをする方法もありますが、nn.Embedding をつかって埋め込み表現をすることが多いみたいです。
  • 数値特徴量をスケーリングする時
for col in tqdm(CFG.cont_cols):
    qt = QuantileTransformer(random_state=0, output_distribution='normal')
    df_train.loc[:,col] = qt.fit_transform(df_train[[col]].to_numpy())
    df_test.loc[:,col] = qt.transform(df_test[[col]].to_numpy())
CFG.n_categories = []
for cat in CFG.cate_cols:
#     CFG.n_categories.append(pd.concat([df_train,df_test])[cat].nunique()+1)
    CFG.n_categories.append(int(pd.concat([df_train,df_test])[cat].max()+1))
  • Datasetを作るとき(Pytorch
TabularDataset(x_num = self._df_train[self._cfg.cont_cols].to_numpy(),
               x_cat = self._df_train[self._cfg.cate_cols].to_numpy(),
               y = self._df_train[[self._cfg.target_col]].to_numpy(),)

課題

拙いコードだと思いますがここまでご覧いただきありがとうございます。この方法はtestデータが手元にない場合には使えません。あらかじめtrainでfitさせたものの用意とかもっと別の形のものがあると思います。

ということで課題や改善余地を残して#1として今回はここまでとします。