GPUを使ってCSVファイルを読み込むcuDFを使ってみた

はじめに

こんにちは! 18年10月にデータマイニング推進部に中途入社したサワイと申します。
これまでは主にシステムの運用やSQLを使用してシステムテストなどを経験してきました。
エム・フィールド入社後も社内研修だけでなく、社外でもLT(Lightning talks、5~10分程度の短い発表)を行ったり、勉強会に参加しています。

今回の記事では18年10月に参加したPyData.tokyo One-day Conference 2018という勉強会で紹介されていたNVIDIAが開発したRAPIDSというライブラリ群の1つ、cuDFというライブラリについて紹介します。

コードを使った実例に入る前にNVIDIAとGPUについてすこし紹介します。
NVIDIAはGPU(Graphics Processing Unit)を開発して販売しているアメリカの企業です。
GPUは大量の単純な計算を高速に行うことが求められる場面で使われる演算装置で、使い方によっては、GPUはCPUよりも高速に演算できるなど優れた性能を発揮します。
機械学習・ディープラーニングなどデータ分析においてこれまでもGPUは使われてきましたが、一部の工程に過ぎませんでした。
今回紹介するcuDFは、エンドツーエンドでGPUを使用できるRAPIDSというプラットフォームの中でデータの読み込みを担うライブラリです。
NVIDIAはGPUメーカーなので、GPUの仕様場面を増やすことを狙っているのだと思います。

PyData.tokyo One-day Conference 2018に参加した際にGPUを使用したプラットフォームとしてRAPIDSが紹介されていました。
RAPIDSのサイトはコチラ→https://rapids.ai/

RAPIDSの中でデータを読み込んだ後のDataFrameの機能を担うのがcuDFになります。
cuDFのGithubはコチラ→https://github.com/rapidsai/cudf

環境について

環境についてはAWS上にEC2インスタンスを建て、その中にNVIDIADocker2環境を構築し、JupyterLab経由でローカルPCから接続しています。
その際AWSのコンソール上のセキュリティグループ設定でJupyterLabを使用するのに必要なポート(8888、8787、8786など)の通信を許可してください。

サーバ AWS EC2インスタンス:RAPIDSの使用にはパスカル世代以降のGPUを使用する必要があり、Tesla V100を積んだp3.2xlargeを選択しました。
なおデフォルトではインスタンスの申請の制限がかかっているためサポートセンター経由で制限を解除してください。

OS:Ubuntu16.04
CUDA:9.2

使用したライブラリのバージョン(NVIDIADocker)
pandas 0.20.3
cudf 0.4.0
scikit-learn 0.20.0
xgboost 0.80 ※rapids対応版のXGBoostがプリインストールされていましたが、エラーで動作しなかったためconda経由でCPU版XGBoostを再インストールして使用

公式の環境構築については以下に記載があります。
https://rapids.ai/documentation.html
cuDFは現在開発中のライブラリになりますので、読者の皆様におかれましては、環境構築手順は公式のページを参照されるのが一番かと思います。
それでは実際のコードを見ていきたいと思います。

サンプルコードの流れ

以下では
1.データ読み込みをCPUを使用したPandasとGPUを使用したcuDFで速度の比較を行います。
2.XGBoostを用いて訓練データの学習を行います。
3.結果考察
4.まとめ
といった構成で紹介します。

ライブラリの読み込み

In [1]:
import cudf
import pandas as pd
import numpy as np

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

import os
import gc
import xgboost as xgb
SEED = 2019

サンプルデータの作成

cuDFは巨大なデータセットでもGPUを使用して素早く読み込める点が売りかと思い、 Kaggleなどで大きなサイズのcsvを探しましたが、手ごろなデータがありませでした。
そこでsklearnのmake_classificationを利用してデータセットを生成することにします。
なお、巨大なデータセットを作成しているため環境によってはメモリエラーなどスペックが足りない場合があります。

In [2]:
# サンプルデータ生成
#カラム数
N_FEATURES = 1000

"""
n_samples:行数
n_features:カラムの数
n_classes:2クラス分類なので2を指定
n_clusters_per_class
random_state:乱数。固定しないと毎回結果が変わる
"""
X, y = make_classification(n_samples=100000, n_features=N_FEATURES, n_informative=50, n_redundant=0, n_repeated=0,n_classes=2,
                            n_clusters_per_class=8, random_state=SEED)
In [3]:
cols = ["col_" + str(i) for i in range(1,N_FEATURES+1)]
X = pd.DataFrame(X,columns=cols)
y = pd.DataFrame(y,columns=['target'])
data = pd.merge(X,y,left_index=True,right_index=True)

分割前後のデータでyの割合が変わらないようにtrain_test_splitを使用して層化サンプリングを行います。

In [4]:
%%time
train, test = train_test_split(data, stratify=data['target'],test_size=0.3,random_state = SEED)
#trainとtestに分割して保存する。
train.to_csv('train.csv',index=False)
test.to_csv('test.csv',index=False)
CPU times: user 2min 37s, sys: 6.53 s, total: 2min 43s
Wall time: 2min 44s

分割後のサイズを確認します。

In [5]:
print('trainの行数と列数:{}'.format(train.shape))
print('testの行数と列数:{}'.format(test.shape))
trainの行数と列数:(70000, 1001)
testの行数と列数:(30000, 1001)

分割の割合を確認します。

In [6]:
for df in [train,test,y]:
    name =[x for x in globals() if globals()[x] is df and x != 'df'][0]
    print('----'+name+'----')
    print(df['target'].value_counts()/df.shape[0] * 100)
----train----
0    50.024286
1    49.975714
Name: target, dtype: float64
----test----
0    50.023333
1    49.976667
Name: target, dtype: float64
----y----
0    50.024
1    49.976
Name: target, dtype: float64

ほぼ50対50で分割できていることがわかります。
次にファイルサイズを確認します。trainとtestが560MBと1.3GB程度のcsvファイルとして生成できていることが確認できます。

In [7]:
!ls -alh
total 1.9G
drwxr-xr-x 3 root root 4.0K Jan 29 00:38 .
drwxr-xr-x 1 root root 4.0K Jan  9 08:18 ..
drwxr-xr-x 2 root root 4.0K Jan 18 09:28 .ipynb_checkpoints
-rw-r--r-- 1 root root  17K Jan 29 00:38 Blog.ipynb
-rw-r--r-- 1 root root 561M Jan 29 00:40 test.csv
-rw-r--r-- 1 root root 1.3G Jan 29 00:39 train.csv

PandasとcuDFでそれぞれcsvを読み込んで比較したいと思います。

In [8]:
%%time
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
CPU times: user 26.8 s, sys: 836 ms, total: 27.6 s
Wall time: 27.6 s

cuDFはcsvを読み込む際、カラム名とデータ型を指定する必要があるため、Pandasのカラムと型を使用します。

In [9]:
%%time
train_cu = cudf.read_csv('train.csv', names=train.columns.tolist(), dtype=train.dtypes.to_dict(),skiprows=1)
test_cu  = cudf.read_csv('test.csv', names=test.columns.tolist(), dtype=train.dtypes.to_dict(),skiprows=1)
CPU times: user 1.49 s, sys: 1.48 s, total: 2.97 s
Wall time: 3.03 s

大雑把な比較ですがcsv読み込みに関してはPandasよりcuDFを使用した方が遥かに速いことがわかります。

PandasのDataFrameを読み込んでXGBoostで学習

Pandasのdfを読み込んでXGBoostでtrainのセットを学習してtestセットで評価してみます。
冒頭にも書いたのですが、残念ながらRAPIDS版XGBoostはエラーとなってしまったため、通常のXGBoostをインストールして使用しています。
評価指標はROC AUCです。

In [10]:
X_train = train.drop(['target'],axis=1)
y_train = train['target']
X_test  = test.drop(['target'],axis=1)
y_test  = test['target']
In [11]:
%%time
d_train =xgb.DMatrix(X_train.as_matrix(),label=y_train.as_matrix().ravel())
d_test =xgb.DMatrix(X_test.as_matrix())
param = {
    'max_depth':8,
    'objective':'binary:logistic',   
    'verbose':True,
    'seed':SEED
}
xgb_model = xgb.train(params=param,dtrain=d_train)
[00:41:15] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 504 extra nodes, 0 pruned nodes, max_depth=8
[00:41:18] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 500 extra nodes, 0 pruned nodes, max_depth=8
[00:41:21] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 488 extra nodes, 0 pruned nodes, max_depth=8
[00:41:24] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 466 extra nodes, 0 pruned nodes, max_depth=8
[00:41:27] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 486 extra nodes, 0 pruned nodes, max_depth=8
[00:41:30] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 474 extra nodes, 0 pruned nodes, max_depth=8
[00:41:33] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 484 extra nodes, 0 pruned nodes, max_depth=8
[00:41:36] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 488 extra nodes, 0 pruned nodes, max_depth=8
[00:41:39] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 464 extra nodes, 0 pruned nodes, max_depth=8
[00:41:42] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 498 extra nodes, 0 pruned nodes, max_depth=8
CPU times: user 4min 10s, sys: 832 ms, total: 4min 11s
Wall time: 32.4 s
In [12]:
preds =np.where(pd.DataFrame(xgb_model.predict(d_test))<0.5,0,1)
roc_auc_score(preds,y_test)
Out[12]:
0.7615348136794244

cudfのDataFrameを読み込んでXGBoostで学習

先ほどと同様にcuDFのdfを読み込んでXGBoostでtrainのセットを学習してtestセットで評価してみます。
通常のXGBoostではcuDFをそのまま読み込めないため、numpyに変換した上で読み込みます。
評価指標は先ほど同様にROC AUCです。

In [13]:
len(train_cu),len(test_cu)
Out[13]:
(70000, 30000)
In [14]:
y_train_cu = train_cu[['target']]
#cuDFはカラムを削除すると大元のDataFrameが変更されるようです。
train_cu.drop_column('target')
X_train_cu = train_cu

y_test_cu = test_cu[['target']]
test_cu.drop_column('target')
X_test_cu = test_cu
In [15]:
%%time
#numpyに変換してxgboostで学習します。
d_train_cu = xgb.DMatrix(X_train_cu.as_matrix(),label=y_train_cu.as_matrix().ravel())
d_test_cu  = xgb.DMatrix(X_test_cu.as_matrix())
param = {
    'max_depth':8,
    'objective':'binary:logistic',   
    'verbose':True,
    'seed':SEED
}
xgb_model_cu = xgb.train(params=param,dtrain=d_train_cu)
[00:41:48] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 504 extra nodes, 0 pruned nodes, max_depth=8
[00:41:51] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 500 extra nodes, 0 pruned nodes, max_depth=8
[00:41:54] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 492 extra nodes, 0 pruned nodes, max_depth=8
[00:41:57] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 482 extra nodes, 0 pruned nodes, max_depth=8
[00:42:01] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 476 extra nodes, 0 pruned nodes, max_depth=8
[00:42:04] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 440 extra nodes, 0 pruned nodes, max_depth=8
[00:42:06] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 480 extra nodes, 0 pruned nodes, max_depth=8
[00:42:09] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 482 extra nodes, 0 pruned nodes, max_depth=8
[00:42:12] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 480 extra nodes, 0 pruned nodes, max_depth=8
[00:42:15] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 462 extra nodes, 0 pruned nodes, max_depth=8
CPU times: user 4min 11s, sys: 1.01 s, total: 4min 12s
Wall time: 33.3 s
In [16]:
preds_cu =np.where(pd.DataFrame(xgb_model_cu.predict(d_test_cu))<0.5,0,1)
roc_auc_score(preds_cu,y_test_cu.as_matrix())
Out[16]:
0.7595666229025154

PandasとcuDFを使用した場合でROC AUCの評価結果が異なることがわかります。
この原因を探ります。

PandasとcuDFのnumpy変換の比較

同じ変形をしているつもりですが、有効桁数の問題などで、PandasとcuDFでは変換結果が異なるようです。

In [17]:
X_train.as_matrix() == X_train_cu.as_matrix()
Out[17]:
array([[False, False, False, ..., False,  True,  True],
       [False, False, False, ...,  True,  True, False],
       [False,  True,  True, ..., False,  True,  True],
       ...,
       [ True,  True, False, ..., False,  True, False],
       [ True, False, False, ...,  True, False,  True],
       [ True,  True, False, ..., False,  True, False]])
In [18]:
#From Pandas
X_train.as_matrix()[0][0]
Out[18]:
-0.012074360798521123
In [19]:
#From cuDF
X_train_cu.as_matrix()[0][0]
Out[19]:
-0.012074360798521127

数値の末尾まで見ていくと数値が異なることがわかります。

まとめ

cuDFの良い点は、GPUを使用しているのでCPUを使用したPandasよりもcsvの読み込みが速いことです。

いまいちな点は、
・DataFrameを読み込む際に、型とカラム名を自動的に判定してくれないので、事前に定義する必要があるのは少し面倒だと感じました。
・nvidia-dockerにインストールされたrapidsai版のXGBoostが動作しなかったため、一旦アンインストールして、conda経由で通常のXGBoostを再インストールする必要があったこと
・現在開発中のようですがrapidsaiのライブラリに教師あり学習のライブラリがないこと
などが挙げられます。
rapidsが、
・現在のスタンダードであるscikit-learn、PandasなどのライブラリとAPI形式を似せる
・Anaconda経由ですべて環境構築できるなど、難易度が下がる
ようになると広く使われるようになると思います。
今後に期待を込めて本記事を結びたいと思います。