lightGBMでGPUを利用した高速化【Ubuntu18.04】


# 概要
タイトルの通りでlightGBMをGPU経由で動作させる方法を自分の備忘録のためにまとめました。
ビルドしてインストールする方法は弊環境ではうまくいかなったため、pip経由でインストールする方法になります。

最後にまとめておりますが、lightGBMをGPUで動作させた場合、弊環境では処理時間を25%短縮することができました。
CPUはAMD Ryzen 9 5950Xを利用しているので、CPUはあまり高性能じゃないけどGPUは高性能なものを利用している、という方は更に高速化の恩恵があるかもしれません。


# 環境構築

## 実行環境
環境は以下の通り。
- OS: Ubuntu 18.04.6 LTS
- CPU: AMD Ryzen 9 5950X 
- GPU: RTX2070

## NVIDIAドライバのインストール

以下のコマンドでNVIDIAのドライバをapt install経由でダウンロードできるようにします。
ppa追加時にENTERを押します。

```sh
$ sudo add-apt-repository ppa:graphics-drivers/ppa 
$ sudo apt update 
```

以下のコマンドでダウンロードできるドライバの一覧を確認します。
今回は推奨(recommended)されている`nvidia-driver-470`をインストールすることにしました。


```sh
$ ubuntu-drivers devices

WARNING:root:_pkg_get_support nvidia-driver-515: package has invalid Support PBheader, cannot determine support level
WARNING:root:_pkg_get_support nvidia-driver-510: package has invalid Support PBheader, cannot determine support level
== /sys/devices/pci0000:00/0000:00:03.1/0000:08:00.0 ==
modalias : pci:v000010DEd00001F02sv00001462sd00003734bc03sc00i00
vendor   : NVIDIA Corporation
driver   : nvidia-driver-515 - third-party non-free
driver   : nvidia-driver-470 - third-party non-free recommended
driver   : nvidia-driver-510 - third-party non-free
driver   : xserver-xorg-video-nouveau - distro free builtin
```

以下のコマンドでドライバをインストールし．再起動します。

```sh
$ sudo apt install nvidia-drivers-470
$ sudo reboot
```

## lightGBM(GPU版)のインストール

再起動した後，再度ターミナルを開いて必要なパッケージをインストールしていきます。
以下のコマンドを入力します。

```sh
$ sudo apt install --no-install-recommends cmake build-essential clinfo opencl-headers libboost-all-dev
```

今回，python3.7＆仮想環境で動作確認をしたいため，以下のコマンドを入力してpython3.7をインストールしておきます。
また，インストールした後に仮想環境を作って立ち上げておきます。
不要な方は、以下のコマンドは飛ばしてください。

```sh
$ sudo apt install python3.7 python3.7-venv
$ python3.7 -m venv venv
$ source ./venv/bin/activate
```

以下のコマンドで必要なpythonパッケージをインストールします。

```sh
$ pip install -U pip setuptools wheel
$ pip install numpy scipy scikit-learn 
```

ここまできて，ようやくGPU版のlightGBMをインストールすることができます。
以下のコマンドを入力してインストールします。

```sh
$ pip install lightgbm --install-option=--gpu --install-option="--opencl-library=/usr/lib/x86_64-linux-gnu/libOpenCL.so.1"
```

以上でGPUに対応したlightGBMをインストールすることができました。

## GPU認識確認

システム上からGPUが認識されているかを確認します。
GPUの認識にcatboostを使うので、追加でインストールしておきます。

```sh
$ pip install catboost
```

以下のコマンドを入力します。。
GPUが認識されていれば「1」が表示されます。1が表示されない場合は再起動をしてみてください。

```sh
$ python -c "from catboost.utils import get_gpu_device_count; print(get_gpu_device_count())"
```

# 動作確認

実際にCPUを利用した場合とGPUで動作させた場合で比較をしてみました。
ソースは以下のサイトのものを参考にさせていただきました。この場を借りてお礼を申し上げます。

- [CUBE SUGAR CONTAINER 　Python: CatBoost を GPU で学習させる](https://blog.amedama.jp/entry/catboost-gpu)

## lightGBM CPU動作


CPU版は以下のソース参照。

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import time
import logging
from contextlib import contextmanager

import lightgbm as lgb
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import log_loss


LOGGER = logging.getLogger(__name__)


@contextmanager
def timeit():
    """処理にかかった時間を計測してログに出力するコンテキストマネージャ"""
    start = time.time()
    yield
    end = time.time()
    elapsed = end - start
    LOGGER.info(f'Elapsed Time: {elapsed:.2f} sec')


def main():
    logging.basicConfig(level=logging.INFO,
                        stream=sys.stderr,
                        )

    # 疑似的な教師信号を作るためのパラメータ
    dist_args = {
        # データ点数
        'n_samples': 100_000,
        # 次元数
        'n_features': 1_000,
        # その中で意味のあるもの
        'n_informative': 100,
        # 重複や繰り返しはなし
        'n_redundant': 0,
        'n_repeated': 0,
        # タスクの難易度
        'class_sep': 0.65,
        # 二値分類問題
        'n_classes': 2,
        # 生成に用いる乱数
        'random_state': 42,
        # 特徴の順序をシャッフルしない (先頭の次元が informative になる)
        'shuffle': False,
    }
    # 教師データを作る
    train_x, train_y = make_classification(**dist_args)
    # データセットを学習用と検証用に分割する
    x_tr, x_val, y_tr, y_val = train_test_split(train_x, train_y,
                                                test_size=0.3,
                                                shuffle=True,
                                                random_state=42,
                                                stratify=train_y)
    # CatBoost が扱うデータセットの形式に直す
    train_pool = lgb.Dataset(x_tr, label=y_tr)
    valid_pool = lgb.Dataset(x_val, label=y_val)
    # 学習用のパラメータ
    params = {
        # タスク設定と損失関数
        'objective': 'binary',
        # 学習率
        'learning_rate': 0.02,
        # 学習ラウンド数
        'num_boost_round': 5_000,
        # 検証用データの損失が既定ラウンド数減らなかったら学習を打ち切る
        # NOTE: ラウンド数を揃えたいので今回は使わない
        # 'early_stopping_rounds': 100,
        # 乱数シード
        'random_state': 42,
        # 学習に GPU を使う場合
        # 'device': 'gpu',
    }
    # モデルを学習する
    with timeit():
        model = lgb.train(params, 
                          train_pool,
                          valid_sets=[valid_pool], 
                          verbose_eval=100
                          )

    # 検証用データを分類する
    y_pred = model.predict(x_val)
    # ロジスティック損失を確認する
    metric = log_loss(y_val, y_pred)
    LOGGER.info(f'Validation Metric: {metric}')


if __name__ == '__main__':
    main()
```

## lightGBM GPU動作

GPU版は以下の通り。

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import time
import logging
from contextlib import contextmanager

import lightgbm as lgb
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import log_loss


LOGGER = logging.getLogger(__name__)


@contextmanager
def timeit():
    """処理にかかった時間を計測してログに出力するコンテキストマネージャ"""
    start = time.time()
    yield
    end = time.time()
    elapsed = end - start
    LOGGER.info(f'Elapsed Time: {elapsed:.2f} sec')


def main():
    logging.basicConfig(level=logging.INFO,
                        stream=sys.stderr,
                        )

    # 疑似的な教師信号を作るためのパラメータ
    dist_args = {
        # データ点数
        'n_samples': 100_000,
        # 次元数
        'n_features': 1_000,
        # その中で意味のあるもの
        'n_informative': 100,
        # 重複や繰り返しはなし
        'n_redundant': 0,
        'n_repeated': 0,
        # タスクの難易度
        'class_sep': 0.65,
        # 二値分類問題
        'n_classes': 2,
        # 生成に用いる乱数
        'random_state': 42,
        # 特徴の順序をシャッフルしない (先頭の次元が informative になる)
        'shuffle': False,
    }
    # 教師データを作る
    train_x, train_y = make_classification(**dist_args)
    # データセットを学習用と検証用に分割する
    x_tr, x_val, y_tr, y_val = train_test_split(train_x, train_y,
                                                test_size=0.3,
                                                shuffle=True,
                                                random_state=42,
                                                stratify=train_y)
    # CatBoost が扱うデータセットの形式に直す
    train_pool = lgb.Dataset(x_tr, label=y_tr)
    valid_pool = lgb.Dataset(x_val, label=y_val)
    # 学習用のパラメータ
    params = {
        # タスク設定と損失関数
        'objective': 'binary',
        # 学習率
        'learning_rate': 0.02,
        # 学習ラウンド数
        'num_boost_round': 5_000,
        # 検証用データの損失が既定ラウンド数減らなかったら学習を打ち切る
        # NOTE: ラウンド数を揃えたいので今回は使わない
        # 'early_stopping_rounds': 100,
        # 乱数シード
        'random_state': 42,
        # 学習に GPU を使う場合
        'device': 'gpu',
    }
    # モデルを学習する
    #model = lgb.train(params, train_pool,valid_sets=[valid_pool])
    with timeit():
        model = lgb.train(params, 
                          train_pool,
                          valid_sets=[valid_pool], 
                          verbose_eval=100
                          )
    model.save_model('model_gpu.txt'
                     #, num_iteration=model.best_iteration
                     )
        #model.fit(train_pool,
        #          eval_set=valid_pool,
        #          verbose_eval=100,
        #          use_best_model=True,
        #          )
    # 検証用データを分類する
    y_pred = model.predict(x_val)
    # ロジスティック損失を確認する
    metric = log_loss(y_val, y_pred)
    LOGGER.info(f'Validation Metric: {metric}')


if __name__ == '__main__':
    main()
```

## 実行時間の比較

それぞれ実行すると、以下のような結果となりました。

| |time|log loss|
|--|--|--|
|CPU|203.67 sec|0.114378|
|GPU|154.56 sec|0.113432|


```sh
# CPU
[4900]	valid_0's binary_logloss: 0.116022
[5000]	valid_0's binary_logloss: 0.114378
INFO:__main__:Elapsed Time: 203.67 sec
INFO:__main__:Validation Metric: 0.11437848050873242
```
```sh
# GPU
[4900]	valid_0's binary_logloss: 0.115171
[5000]	valid_0's binary_logloss: 0.113433
INFO:__main__:Elapsed Time: 154.56 sec
INFO:__main__:Validation Metric: 0.11343276824400272
```


# まとめ

多少調べたことがある方は解ると思いますが、lightGBMはツリー系のなかでもあまりGPU処理の恩恵が得られないというライブラリです。タスクによってはCPUとあまり差異がなかったり、他のGBDT系ライブラリのほうが高速に動作します。
（[この論文](https://arxiv.org/abs/1706.08359)や[このページ](https://www.kaggle.com/code/raimonds1993/gbms-cpu-vs-gpu-400-features-augmentation/notebook)を参照）

弊環境・今回のタスクではDNNのような劇的な高速化は得られませんでしたが、およそ25％の処理スピードアップとなりました。

lightGBMはあまりGPUの利用率が高くないため、高価で高性能なGPUを利用しても処理速度の向上には寄与しないと考えれられます。
一方で、弊環境のAMD Ryzen9 5950Xの16コアCPUよりGPUを利用したほうが高速で処理させることができたため、コア数の少ないCPUを利用している場合は、GPU利用の恩恵があるかもしれません。

少しでもトレーニング時間の短縮を目指す方はぜひ利用してみてください！


## 参考文献

1. [LightGBM ~GPU Tuning Guide and Performance Comparison](https://lightgbm.readthedocs.io/en/latest/GPU-Performance.html)

2. [Python: CatBoost を GPU で学習させる](https://blog.amedama.jp/entry/catboost-gpu)