予測できるかを予測してみよう

吐きだめのような正確性のない文章

【Ubuntu18.04】lightGBMをGPUで処理させて速度を比較した

概要

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

最後にまとめておりますが、lightGBMをGPUで動作させた場合、処理時間を25%短縮することができました。 XGBoostやcatboostなど他のGBDT系のライブラリに比べるとGPU化の恩恵は少ないですが、25%の短縮は大きいと思います。

環境構築

実行環境

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

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

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

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

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

$ 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

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

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

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

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

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

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

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

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

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

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

$ 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を使うので、追加でインストールしておきます。

$ pip install catboost

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

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

動作確認

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

lightGBM CPU動作

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

#!/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版は以下の通り。

#!/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
# 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
# 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系ライブラリのほうが高速に動作します。 (この論文このページを参照)

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

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

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

参考文献

  1. LightGBM ~GPU Tuning Guide and Performance Comparison

  2. Python: CatBoost を GPU で学習させる