いつの間にかCatBoostが超進化、テキスト項目の前処理まで不要になってた件

<img width="450" src="https://images.unsplash.com/photo-1485546246426-74dc88dec4d9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2700&q=80">
<a style="background-color:black;color:white;text-decoration:none;padding:4px 6px;font-family:-apple-system, BlinkMacSystemFont, &quot;San Francisco&quot;, &quot;Helvetica Neue&quot;, Helvetica, Ubuntu, Roboto, Noto, &quot;Segoe UI&quot;, Arial, sans-serif;font-size:12px;font-weight:bold;line-height:1.2;display:inline-block;border-radius:3px" href="https://unsplash.com/@benwhitephotography?utm_medium=referral&amp;utm_campaign=photographer-credit&amp;utm_content=creditBadge" target="_blank" rel="noopener noreferrer" title="Download free do whatever you want high-resolution photos from Ben White"><span style="display:inline-block;padding:2px 3px"><svg xmlns="http://www.w3.org/2000/svg" style="height:12px;width:auto;position:relative;vertical-align:middle;top:-2px;fill:white" viewBox="0 0 32 32"><title>unsplash-logo</title><path d="M10 9V0h12v9H10zm12 5h10v18H0V14h10v9h12v-9z"></path></svg></span><span style="display:inline-block;padding:2px 3px">Ben White</span></a>


# はじめに
    
機械学習周りのツールの進歩はあまりに早く、数か月確認していないといつの間にか新しい機能が実装されていた経験は多い。
CatBoostがいつの間にかテキスト項目までモデル内部で処理できるようになっていたらしい。
[CatBoostのテキストカラム指定機能を試す](https://upura.hatenablog.com/entry/2020/03/03/195929)

調べてみると、2020/3/3にリリースされた`v0.22`から追加されたようだ。
そりゃ知りませんわ…笑
どういった処理がされるのか気になったので、さっそく調べてみた。

# 環境
- Windows10
- Python 3.7.4
- catboost 0.22
 - テキスト項目の処理にはGPUのみ対応、CPUは未対応

# 参考
詳細は下記参照、ここでは適宜抜粋していく。

- [リファレンス](https://catboost.ai/docs/concepts/algorithm-main-stages_text-to-numeric.html?lang=en)
- [チュートリアル](https://github.com/catboost/tutorials/blob/master/text_features/text_features_in_catboost.ipynb)

# 使い方
## データ準備

チュートリアル同様、`rotten_tomatoes`をデータとして使用する。
0～10の`rating_10`を目的変数とした数値項目、カテゴリ項目、テキスト項目混在のデータセット。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167671/4bc6eda2-5e8f-5aa9-ab4e-8e90cb1606e3.png)

```py
from catboost import Pool, CatBoostClassifier
from catboost.datasets import rotten_tomatoes
from sklearn.metrics import accuracy_score

# カテゴリ項目リスト
list_cat_features = ['rating_MPAA', 'studio', 'fresh', 'critic', 'top_critic', 'publisher']

# テキスト項目リスト
list_text_features = ['synopsis', 'genre', 'director', 'writer', 'review']

def get_processed_rotten_tomatoes():
    train, test = rotten_tomatoes()
    
    def fill_na(df, features):
        for feature in features:
            df[feature].fillna('', inplace=True)

    def preprocess_data_part(data_part):
        # 動作確認なので、特徴作成に手間の掛かる項目はdrop
        data_part = data_part.drop(['id', 'theater_date', 'dvd_date', 'rating', 'date'], axis=1)
        
        fill_na(data_part, list_cat_features)
        fill_na(data_part, list_text_features)

        X = data_part.drop(['rating_10'], axis=1)
        y = data_part['rating_10']
        
        return X, y
    
    X_train, y_train = preprocess_data_part(train)
    X_test, y_test = preprocess_data_part(test)

    return X_train, X_test, y_train, y_test


# trainとtestに分割
X_train, X_test, y_train, y_test = get_processed_rotten_tomatoes()

# テキスト項目のみ表示
X_train[list_text_features].head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167671/441c220d-ba55-6608-7ca8-d8f20557b609.png)


## 設定方法
`category_features`と同様に項目名のリストを`text_features`に渡してやるだけ。

`text_features`を引数に取ることができるのは下記のクラスとメソッド。

- CatBoost.fit()
- CatBoostClassifier.fit()
- Pool()

ここでは`Pool()`に設定する。

```py

# trainデータセット
train_pool = Pool(
    X_train, 
    y_train, 
    cat_features=list_cat_features,
    text_features=list_text_features,
    feature_names=list(X_train)
)

# testデータセット
test_pool = Pool(
    X_test, 
    y_test, 
    cat_features=list_cat_features,
    text_features=list_text_features,
    feature_names=list(X_test)
)

catboost_default_params = {
    'iterations': 1000,
    'learning_rate': 0.03,
    'eval_metric': 'Accuracy',
    'task_type': 'GPU',  # 'CPU'は未対応、CatBoostErrorとなる
    'random_seed': 0, 
    'verbose': 100

}

# 多クラス分類
clf = CatBoostClassifier(**catboost_default_params)
clf.fit(train_pool)
```


# 評価指標

```py
y_pred = clf.predict(X_test)
print(f"accuracy = {accuracy_score(y_test, y_pred):.4f}")
```

```
accuracy = 0.4699
```

チュートリアルでは`random_seed`が設定されていないので、チュートリアルの`accuracy`と完全一致はしていないが概ね変わらない結果となっている。
チュートリアルの検証結果では、テキスト特徴量をドロップした場合は0.4562なので数ポイントの精度向上となっている。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167671/6c9baafb-8530-b82c-45de-e096c028ee7e.png">

# 解説
`text_features`で指定された項目に対してテキスト項目から数値項目へと変換する際、3つの要素を設定できる。


## 引数
- `tokenizers` [[doc]](https://catboost.ai/docs/references/tokenizer_options.html)<br>
トークン化する際の設定を定義。<br>
delimiterやlowercasingの指定が可能。
- `dictionaries` [[doc]](https://catboost.ai/docs/references/dictionaries_options.html)<br>
トークンを辞書化する際の設定を定義。<br>
単語単位か文字単位か、n-gramのnの指定などが可能。
- `feature_calcers` [[doc]](https://catboost.ai/docs/references/text-processing__feature_calcers.html)<br>
- dictionariesの処理方法の設定。<br>
 - Bag of words
 - NaiveBayes
 - BM25


## 処理イメージ（tokenizer）
```py:tokenizers前
['cats so cute :)',
 'mouse skare ...',
 'cat defeated mouse',
 'cute : mice gather army !',
 'army mice defeated cat :(',
 'cat offers peace',
 'cat skared :(',
 'cat mouse live peace :)']
```

```py:tokenizers後（半角スペースを区切り文字として分割）
[['cat', 'so', 'cute', ':)'],
 ['mouse', 'skare', '...'],
 ['cat', 'defeat', 'mouse'],
 ['cute', ':', 'mice', 'gather', 'army', '!'],
 ['army', 'mice', 'defeat', 'cat', ':('],
 ['cat', 'offer', 'peace'],
 ['cat', 'skare', ':('],
 ['cat', 'mouse', 'live', 'peace', ':)']]
```

`feature_processing`で上記の組み合わせを指定することが可能。
それらの設定は`catboost_params`に引数でdictとして渡す。


## 初期設定
```py:初期設定
{
    "tokenizers" : [{
        "tokenizer_id" : "Space",  # "Space"の名前でtokenizersを定義
        "separator_type" : "ByDelimiter",  # 区切り文字により分割
        "delimiter" : " "  # 区切り文字は半角スペース
    }],

    "dictionaries" : [{
        "dictionary_id" : "BiGram",  # "BiGram"の名前でdictionariesを定義
        "max_dictionary_size" : "50000", 
        "occurrence_lower_bound" : "3",
        "gram_order" : "2"  # n-gramのn=2
    }, {
        "dictionary_id" : "Word",  # "Word"の名前でdictionariesを定義
        "max_dictionary_size" : "50000",
        "occurrence_lower_bound" : "3",
        "gram_order" : "1"  # n-gramのn=1
    }],

    "feature_processing" : {
        "default" : [{  # tokenizers, dictionaries, feature_calcersの組み合わせを定義
            "dictionaries_names" : ["BiGram", "Word"],
            "feature_calcers" : ["BoW"],
            "tokenizers_names" : ["Space"]
        }, {
            "dictionaries_names" : ["Word"],
            "feature_calcers" : ["NaiveBayes"],
            "tokenizers_names" : ["Space"]
        }],
    }
}
```
https://catboost.ai/docs/references/text-processing__test-processing__default-value.html

様々な組み合わせを定義できるので拡張性は高いが、設定値の選択肢が多いのでリファレンス要参照。


# おわりに
以前からカテゴリ項目の内部処理化でOneHotEncodingが不要になっていた。
今回テキスト項目まで対応できるようになったことで、テキスト項目含んだ
テーブルデータでも特徴量作成せずにベースラインモデルが作成できるようになった。
最近のKaggleではベースラインモデルとしてLightGBMが最も使われているが、
今後はCatBoostも増えてきそうだ。
自分も取り入れようと思う。

ところで、この数か月の間に主要パッケージで大きなアップデートが続いた。
個人的に気になった内容は下記。

- [scikit-learn v0.22（2019/12）](https://scikit-learn.org/stable/auto_examples/release_highlights/plot_release_highlights_0_22_0.html)
 - stackingの新規実装
 - permutation importanceの新規実装
 - ROC AUCの可視化に対応
- [pandas v1.0（2020/01）](https://pandas.pydata.org/pandas-docs/version/1.0.0/whatsnew/v1.0.0.html#what-s-new-in-1-0-0-january-29-2020) 
 - 欠損値をpd.NAに統一化
 - pandas.dtypeに文字列型の追加

インパクトのある機能追加だけでなくbag fixも進んでいる。
追いかけるのは大変だが、まさに嬉しい悲鳴といったところ。