IQ1なのでハイパラ探索をしたくない(lightgbm_tunerの使い方）

この記事は[IQ1の2枚目っ Advent Calendar 2019](https://adventar.org/calendars/4711)に投稿されています．

# IQ1の機械学習
機械学習をするためにはデータの前処理とモデルのハイパラ探索は避けて通れません．でもIQが1なのでデータの前処理もハイパラ探索もできればやりたくないです．
データの前処理はこのご時世勾配ブースティング木系のアルゴリズム（lightgbmとか）に入れてあげれば欠損値も一応そのまま扱えるしカテゴリ変数の前処理をしなくて済むので比較的IQ1にも優しい世界になりました．
一方でハイパラ探索はIQ1には難しすぎます．モデルのハイパラがそれぞれどういうものでどれくらいの範囲を探索すればいいのかが分かっていないといけないので非常に困難です．

# lightgbm_tuner
最近optunaがlightgbmのハイパラ探索を自動化するために`optuna.integration.lightgbm_tuner`というモジュールを公開しました．このモジュールは色んな理由でIQ1にも優しいです．
1. ハイパラの探索を完全に自動でやってくれる
2. step-wiseで探索（各パラメータごとに探索）するので高速に探索できる
3. 既存のlightgbmのコードを数箇所書き換えるだけですぐに利用できる（後述）

# IQ1の自動ハイパラ探索
最新版のoptunaをインストールしといてください．0.19.0とかだった気がします．`pip install optuna --upgrade`とかで入ります．
## データセットの用意
今回用いるデータセットはkaggleのHouse Pricesにします．
(https://www.kaggle.com/c/house-prices-advanced-regression-techniques/overview)
このデータセットは土地の情報とか建物の情報からその家の価格を予測するというデータセットです．

まずはデータをkaggleのAPI使ってダウンロードします

```
$ kaggle competitions download -c house-prices-advanced-regression-techniques
```
適当にzipを解凍したらtrain.csvとtest.csvとかが出てきます．今回はsubmitはめんどくさいのでしないのでtrain.csvしか使いません．
適当にjupyter notebookでもなんでもいいのでpythonを開いてpandasでreadしましょう．

```python
import pandas as pd

df=pd.read_csv("train.csv")
print(df.shape)
```
```.out
(1460, 81)
```

1460件のデータで列が81なことがわかりました．この中にはIDと予測する`SalePrice`が入っているので使える特徴量は79次元です．

とりあえずいらない列は落として目的変数は別に置いておきましょう

```python
y=df.SalePrice
X=df.drop(["Id","SalePrice"],axis=1)
```
次に欠損値の確認をします．が，lightgbmに突っ込むので処理はしません．確認だけです．

```python
X.loc[:,pd.isnull(X).any(axis=0)].columns
```
```.out
Index(['LotFrontage', 'Alley', 'MasVnrType', 'MasVnrArea', 'BsmtQual',
       'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2',
       'Electrical', 'FireplaceQu', 'GarageType', 'GarageYrBlt',
       'GarageFinish', 'GarageQual', 'GarageCond', 'PoolQC', 'Fence',
       'MiscFeature'],
      dtype='object')
```
次に要素が`String`の列についてカテゴリ変数としてlightgbmで扱ってもらうためにlabelencodingをしてからdtypeを`category`とします．labelencodingは一言で言えば要素がダブらないように文字列を整数値に置き換えるだけです．

```python
from sklearn.preprocessing import LabelEncoder

for name in X.columns:
    if X[name].dtype=="object":
        # LabelEncoderにNaNが入力できないため"NAN"とする
        X[name]=X[name].fillna("NAN")
        le = LabelEncoder()
        le.fit(X[name])
        encoded = le.transform(X[name])
        X[name] = pd.Series(encoded).astype('category')
```

## 普通のlightgbmで学習
前処理が終わったのでlightgbmに学習させます．こちらのコードが普通のlightgbmに突っ込んだ時のものです

```python
import lightgbm as lgb
from sklearn.model_selection import train_test_split

X_train,X_test,y_train,y_test=train_test_split(X,y,random_state=1)

train_dataset=lgb.Dataset(X_train,y_train)
valid_dataset=lgb.Dataset(X_test,y_test,reference=train_dataset)

#時間測定用
#%%time
params={"objective":"regression",
                    "learning_rate":0.05}
model=lgb.train(params,
                train_set=train_dataset,
                valid_sets=[valid_dataset],
                num_boost_round=300,
                early_stopping_rounds=50)
```
```.out
...（省略）...
Early stopping, best iteration is:
[113]	valid_0's l2: 6.65263e+08
```
```
CPU times: user 3.11 s, sys: 537 ms, total: 3.65 s
Wall time: 4.47 s
```
学習できました．学習にかかる時間は4.47秒でした．
ちなみに予測結果をプロットしたらこんな感じでした．横軸が予測値で縦軸が真値です．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188403/be269c1c-b7e2-a0c3-b808-b382655321fa.png)


## lightgbm_tunerで学習
上記のコードを書き換えてIQ1のハイパラ探索をします

```python
import lightgbm as lgb
import optuna.integration.lightgbm_tuner as lgb_tuner
from sklearn.model_selection import train_test_split

X_train,X_test,y_train,y_test=train_test_split(X,y,random_state=1)

train_dataset=lgb.Dataset(X_train,y_train)
valid_dataset=lgb.Dataset(X_test,y_test,reference=train_dataset)

params={"objective":"regression",
                    "learning_rate":0.05,
                    "metric":"l2"}
model=lgb_tuner.train(params,
                      train_set=train_dataset,
                      valid_sets=[valid_dataset],
                      num_boost_round=300,
                      early_stopping_rounds=50)
```

書き換わった場所わかりますか？IQ1の間違い探しです．

書き換わった場所は以下の3箇所です
1. import文の追加
2. `lgb.train`から`lgb_tuner.train`に変更
3. `params`に最適化する値`metric`を追加

ちなみに学習時間は以下の通りです．思ったより遅かった…

```.out
CPU times: user 3min 24s, sys: 33.8 s, total: 3min 58s
Wall time: 3min 48s
```
validationデータのスコアは以下の通りです．

```python
model.best_score
```
```.out
defaultdict(dict, {'valid_0': {'l2': 521150494.1730755}})
```

今回の実験結果を比較するとこんな感じです．チューニング後はちゃんと性能上がってますね．

|  | lightgbm | lightgbm_tuner |
|:-----------|------------:|------------:|
| 学習時間 | 4.47 s | 228 s |
| validデータの精度(MSE) | 6.65263e+08 | 5.21150e+08 |

プロットを見てもPriceの高い方（右のほう）がいい感じになってるのがわかりますね
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188403/2b89bc71-70bc-d4a7-705c-4539c2ae50c8.png)


# おわり
これ使えばIQ1でも機械学習ができそうですね！！
ちなみにこれでできたモデルで提出したら2000位くらいでした．（参加人数がsample_submission.csv以上で4900人なのでIQ1以下の人がたくさんいました）

# 追記
気分でoptunaを使ってハイパラ探索をしてみたらvalidationスコアは上がったけどsubmitスコアは少し悪くなりました．（提出の際には`learning_rate=0.05,num_boosting_round=1000,early_stopping_rounds=50`で再学習させてます）
validationデータに対してoverfitしちゃったんですかね．やっぱりIQ1にはハイパラ探索は難しいですね．

**もし何か実践的なハイパラ探索のアドバイスがあればコメントください！！**

今回のチューニング戦略は以下の通りです．

- チューニングできそうなパラメータをひたすらに並べて雑に広めに範囲とる
- `learning_rate`を粗く設定して試行回数を多くする

（スコアは低い程よい）

- デフォパラのlightgbmでの提出スコア: 0.13852
- lightgbm_tunerでの提出スコア： 0.13174
- optunaでの提出スコア： 0.13401

```python
import optuna

def objective(trial):
    '''
    trial:set of hyperparameter    
    '''
    # hypyer param
    bagging_fraction = trial.suggest_uniform("bagging_fraction",0,1)
    bagging_freq = trial.suggest_int("bagging_freq",0,10)
    feature_fraction = trial.suggest_uniform("feature_fraction",0,1)
    lambda_l1 = trial.suggest_uniform("lambda_l1",0,50)
    lambda_l2 = trial.suggest_uniform("lambda_l2",0,50)
    min_child_samples = trial.suggest_int("min_child_samples",1,50)
    num_leaves = trial.suggest_int("num_leaves",2,50)
    max_depth = trial.suggest_int("max_depth",0,8)
    params={"learning_rate":0.5,
                    "objective":"regression",
                    "bagging_fraction":bagging_fraction,
                    "bagging_freq":bagging_freq,
                    "feature_fraction":feature_fraction,
                    "lambda_l1":lambda_l1,
                    "lambda_l2":lambda_l2,
                    "min_child_samples":min_child_samples,
                    "num_leaves":num_leaves,
                    "max_depth":max_depth}

    model_opt = lgb.train(params,train_set=train_dataset,valid_sets=[valid_dataset],
                          num_boost_round=70,early_stopping_rounds=10)
 
    return model_opt.best_score["valid_0"]["l2"]

study = optuna.create_study()
study.optimize(objective, n_trials=500)
```
```
...（省略）...
[I 2019-12-01 15:02:35,075] Finished trial#499 resulted in value: 537618254.528029. Current best value is 461466711.4731979 with parameters: {'bagging_fraction': 0.9973929186258068, 'bagging_freq': 2, 'feature_fraction': 0.9469601028256658, 'lambda_l1': 10.1589501379876, 'lambda_l2': 0.0306013767707684, 'min_child_samples': 2, 'num_leaves': 35, 'max_depth': 2}.
```
validationスコアは4.61467e+08でした