CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: Keras で imdb データセットを読もうとするとエラーになる問題と回避策について

今回は、表題の通り Keras の API を使ってダウンロードできる imdb データセットを読もうとするとエラーになる問題について。

これは数ヶ月前から既知の問題で、以下のチケットが切られている。 内容については細かく読まなくても、詳しくは後述する。

github.com

問題を修正するコードは Git リポジトリの HEAD にはマージされている。 しかし、現時点 (2019-06-14) ではまだ修正済みのバージョンがリリースされていない。

github.com

そして、この問題について検索すると、以下の二つの回避策の提案が見つかる。

  • NumPy のバージョンを 1.16.2 以下にダウングレードする
  • インストール済みの Keras のソースコードを手動で書き換える

最初のやり方は、実は潜在的に脆弱性のある NumPy のバージョンを使うことを意味している。 また、二番目のやり方は正直あまりやりたくない類のオペレーションのはず。 そこで、上記とは異なる第三の回避策としてモンキーパッチを使う方法を提案してみる。

今回使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V                          
Python 3.7.3
$ pip list | egrep -i "(keras|numpy)"
Keras               2.2.4  
Keras-Applications  1.0.8  
Keras-Preprocessing 1.1.0  
numpy               1.16.4 

再現環境を作る

ひとまず再現環境を作るための準備として Keras とバックエンドの TensorFlow をインストールしておく。

$ pip install keras tensorflow

準備ができたら Python のインタプリタを起動する。

$ python

問題を再現する

この問題を再現するのは非常に簡単で、ただ imdb データセットを読み込もうとすれば良い。

まずは imdb モジュールをインポートする。

>>> from keras.datasets import imdb
Using TensorFlow backend.

そして、load_data() 関数を呼ぶだけ。 すると、以下のように例外になってしまう。

>>> imdb.load_data()
Downloading data from https://s3.amazonaws.com/text-datasets/imdb.npz
17465344/17464789 [==============================] - 3s 0us/step
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/amedama/.virtualenvs/example/lib/python3.7/site-packages/keras/datasets/imdb.py", line 59, in load_data
    x_train, labels_train = f['x_train'], f['y_train']
  File "/Users/amedama/.virtualenvs/example/lib/python3.7/site-packages/numpy/lib/npyio.py", line 262, in __getitem__
    pickle_kwargs=self.pickle_kwargs)
  File "/Users/amedama/.virtualenvs/example/lib/python3.7/site-packages/numpy/lib/format.py", line 696, in read_array
    raise ValueError("Object arrays cannot be loaded when "
ValueError: Object arrays cannot be loaded when allow_pickle=False

問題の詳細

この問題は NumPy の脆弱性に対する対応と、imdb が Pickle 形式の npz フォーマットで配布されていることに起因している。

まず、発端は以下の脆弱性 CVE-2019-6446 に始まる。 この脆弱性は、誤って信頼できない (細工された) Pickle を NumPy で読み込んでしまうと任意のコード実行が生じるというもの。

nvd.nist.gov

上記の脆弱性に対する対応として、NumPy はバージョン 1.16.3 以降で以下のようにコードを修正した。 具体的には、意図的にフラグ (allow_pickle=True) を有効にしない限り Pickle フォーマットのデータを読めないようにしている。

github.com

その煽りを受けたのが Keras の imdb データセットだった。 Pickle 形式のデータセットを NumPy デフォルトのオプションで読み込んでいた。 そのため、前述したように NumPy 1.16.3 以降を使うと例外になってしまう。

上記のような事情があるため、前述した通り Web を探すと以下のような回避策が提案されている。

  • NumPy のバージョンを 1.16.2 以下にダウングレードする
  • インストール済みの Keras のソースコードを書き換える

とはいえ、どちらもあまりやりたくないのは前述した通り。

第三の選択肢 (モンキーパッチ)

そこで提案するのが、モンキーパッチを使うやり方。 これは、データセットを読み込むタイミングだけ、一時的にピンポイントでコードを動的に書き換えてしまうというもの。 問題は NumPy の load() 関数がデフォルトのオプションのまま呼び出される点にある。 だとすると、関数が呼ばれるタイミングだけオプションを一時的に上書きしてしまえば良い。

具体的には、次のように関数のパラメータを部分適用して上書きする。

>>> from functools import partial
>>> import numpy as np
>>> np.load = partial(np.load, allow_pickle=True)  # monkey patch

この状態なら、エラーにならずにデータセットを読み込むことができる。

>>> from keras.datasets import imdb
Using TensorFlow backend.
>>> imdb.load_data()  # エラーにならずデータが得られる

もし、そのままになっているのが気持ち悪いのであれば、読み込みが終わった後でまた元のパラメータに戻してやれば良い。

>>> np.load = partial(np.load, allow_pickle=False)

まあ次の Keras のリリース版が出るまでの短い間だけ必要な回避策だけど、スクリプト言語ならこんなやり方もありますよということで。

PythonとKerasによるディープラーニング

PythonとKerasによるディープラーニング

パスワード付き ZIP ファイルを hashcat + JtR + GPU で総当たりしてみる

少し前に以下のツイートが話題になっていた。 hashcat というツールと GTX 2080 Ti を 4 台積んだマシンで ZIP ファイルのパスワードを探索するというもの。 このツイートでは 15 桁までわずか 15 時間 (!) で探索できたとしている。 その探索速度はなんと 22.7 ZH/s (Z = ゼッタ = Giga<Tera<Peta<Exa<Zetta) に及ぶらしい。

ただし、これは PKWARE 社の暗号化方式に存在する脆弱性を利用して計算量を削減した場合の結果らしい。 一般的に用いられている形式 (Traditional PKWARE Encryption) については、ここまで高速には探索できないとのこと。 今回のエントリは、上記を見て一般的なものはどれくらいのスピードで探索できるのか気になって実際に試してみた。

使った環境は次の通り。 GPU には Tesla V100 を 1 台使っている。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.2 LTS"
$ uname -r
4.15.0-1033-gcp
$ nvidia-smi
Sat Jun  8 05:14:29 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  On   | 00000000:00:04.0 Off |                    0 |
| N/A   36C    P0    37W / 300W |      0MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

もくじ

下準備

まずは必要となるパッケージを最初に一通りインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install clinfo wget git zip p7zip-full build-essential libssl-dev zlib1g-dev 

OpenCL (NVIDIA CUDA Runtime) をインストールする

hashcat の動作には OpenCL のランタイムが必要になる。 そこで CUDA のランタイムをインストールする。

まず、以下の Web サイトから CUDA のインストール用リポジトリの入った deb ファイルを取得する。

developer.nvidia.com

wget などを使ってダウンロードしてくれば良い。

$ wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.1.168-1_amd64.deb

リポジトリを登録して CUDA をインストールする。

$ sudo dpkg -i cuda-repo-ubuntu1804_10.1.168-1_amd64.deb
$ sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub
$ sudo apt-get update
$ sudo apt-get -y install cuda

インストールが終わったら、一旦マシンを再起動しておく。

$ sudo shutdown -r now

すると、次のように NVIDIA のグラフィックドライバと CUDA がインストールされた。

$ nvidia-smi
Sat Jun  8 05:14:29 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  On   | 00000000:00:04.0 Off |                    0 |
| N/A   36C    P0    37W / 300W |      0MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

次のように OpenCL のランタイムが認識されている。

$ clinfo | grep -A 3 "Number of platforms"
Number of platforms                               1
  Platform Name                                   NVIDIA CUDA
  Platform Vendor                                 NVIDIA Corporation
  Platform Version                                OpenCL 1.2 CUDA 10.1.152

hashcat をインストールする

hashcat は各種ハッシュ値を探索するためのツール。

現時点でリリース済みのバージョン (v5.1.0) から HEAD はだいぶ差分があるようなので Git のリポジトリからインストールする。

$ git clone https://github.com/hashcat/hashcat.git
$ cd hashcat
$ make && sudo make install

hashcat -I コマンドで以下のように GPU が認識されていれば上手くいっている。

$ hashcat -I
hashcat (v5.1.0-1138-g581839d4) starting...

CUDA Info:
==========

CUDA.Version.: 10.1

Backend Device ID #1 (Alias: #2)
  Name...........: Tesla V100-SXM2-16GB
  Processor(s)...: 80
  Clock..........: 1530
  Memory.........: 16130 MB

OpenCL Info:
============

OpenCL Platform ID #1
  Vendor..: NVIDIA Corporation
  Name....: NVIDIA CUDA
  Version.: OpenCL 1.2 CUDA 10.1.152

  Backend Device ID #2 (Alias: #1)
    Type...........: GPU
    Vendor.ID......: 32
    Vendor.........: NVIDIA Corporation
    Name...........: Tesla V100-SXM2-16GB
    Version........: OpenCL 1.2 CUDA
    Processor(s)...: 80
    Clock..........: 1530
    Memory.........: 4032/16130 MB allocatable
    OpenCL.Version.: OpenCL C 1.2 
    Driver.Version.: 418.67

JtR (JohnTheRipper) をインストールする

hashcat はハッシュの探索に特化したツールなので、肝心のハッシュ値は別のツールを使って調べる必要がある。 ZIP ファイルに関しては JohnTheRipper というツールを使うのが一般的なようだ。

こちらも Git のリポジトリからインストールしておく。

$ git clone https://github.com/magnumripper/JohnTheRipper.git
$ cd JohnTheRipper/src
$ ./configure
$ make -s clean && make -sj$(grep processor /proc/cpuinfo | wc -l)
$ sudo make install
$ sudo ln -s $(pwd)/../run/zip2john /usr/local/bin/

以下のように zip2john コマンドが使えるようになっていれば良い。

$ zip2john
Usage: zip2john [options] [zip file(s)]
Options for 'old' PKZIP encrypted files only:
 -a <filename>   This is a 'known' ASCII file. This can be faster, IF all
    files are larger, and you KNOW that at least one of them starts out as
    'pure' ASCII data.
 -o <filename>   Only use this file from the .zip file.
 -c This will create a 'checksum only' hash.  If there are many encrypted
    files in the .zip file, then this may be an option, and there will be
    enough data that false possitives will not be seen.  If the .zip is 2
    byte checksums, and there are 3 or more of them, then we have 48 bits
    knowledge, which 'may' be enough to crack the password, without having
    to force the user to have the .zip file present.
 -m Use "file magic" as known-plain if applicable. This can be faster but
    not 100% safe in all situations.
 -2 Force 2 byte checksum computation.

NOTE: By default it is assumed that all files in each archive have the same
password. If that's not the case, the produced hash may be uncrackable.
To avoid this, use -o option to pick a file at a time.

パスワード付き ZIP ファイルを用意する

以下のようにしてパスワードが password の ZIP ファイルを作る。 辞書攻撃であれば一瞬で解ける脆弱なパスワードだけど、今回は総当たりなのでサンプルとしては構わないかな。

$ echo "Hello, World" > greet.txt
$ zip -e --password=password greet.txt.zip greet.txt
  adding: greet.txt (stored 0%)
$ file greet.txt.zip
greet.txt.zip: Zip archive data, at least v1.0 to extract

ハッシュ値を取得する

zip2john コマンドを使って次のようにハッシュ値を記録したファイルを作成する。

$ zip2john greet.txt.zip | cut -d ":" -f 2 > greet.txt.zip.hash
ver 1.0 efh 5455 efh 7875 greet.txt.zip/greet.txt PKZIP Encr: 2b chk, TS_chk, cmplen=25, decmplen=13, crc=40F63A90

取得できたハッシュ値は以下。

$ cat greet.txt.zip.hash
$pkzip2$1*2*2*0*19*d*40f63a90*0*43*0*19*40f6*2adb*e6c233aef1ba5a982f025c9bcdcdc86c4fa27c949c7871dc01*$/pkzip2$

hashcat でハッシュ値を探索する

hashcat は計算対象のハッシュ値を自動では認識してくれない。 なので、先ほどのハッシュ値の内容と以下のページの内容を見比べて適切なハッシュ形式を探す。 今回であれば 172xx のいずれかだろう、となる。

https://hashcat.net/wiki/doku.php?id=example_hasheshashcat.net

ここまでできたら、あとは hashcat コマンドを使って探索するだけ。 ハッシュ形式は -m オプションで指定する。 -a オプションはアタックモードで、総当たり (Brute-force) なら 3 を指定する。 -w オプションはワークロードプロファイルで、全力で探索するときは 4 を指定する。

$ hashcat -m 17210 -a 3 -w 4 \
    --session helloworld \
    -o result.txt \
    greet.txt.zip.hash

実行すると、次のように探索が始まって状況が表示される。 探索速度は 28043.6 MH/s なので、約 28 GH/s となる。

$ hashcat -m 17210 -a 3 -w 4 \
    --session helloworld \
    -o result.txt \
    greet.txt.zip.hash
...

Session..........: helloworld
Status...........: Running
Hash.Name........: PKZIP (Uncompressed)
Hash.Target......: $pkzip2$1*2*2*0*19*d*40f63a90*0*43*0*19*40f6*2adb*e...kzip2$
Time.Started.....: Sat Jun  8 05:24:42 2019 (15 secs)
Time.Estimated...: Sat Jun  8 05:27:58 2019 (3 mins, 1 sec)
Guess.Mask.......: ?1?2?2?2?2?2?2?3 [8]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined 
Guess.Queue......: 8/15 (53.33%)
Speed.#1.........: 28043.6 MH/s (93.32ms) @ Accel:32 Loops:1024 Thr:1024 Vec:1
Recovered........: 0/1 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 437382021120/5533380698112 (7.90%)
Rejected.........: 0/437382021120 (0.00%)
Restore.Point....: 5242880/68864256 (7.61%)
Restore.Sub.#1...: Salt:0 Amplifier:6144-7168 Iteration:0-1024
Candidates.#1....: 0uc3aen1 -> Ciskoe86
Hardware.Mon.#1..: Temp: 54c Util: 99% Core:1530MHz Mem: 877MHz Bus:16

...

デフォルトではアルファベットと数字だけが探索対象なので、8 桁でも数分もあれば見つかる。 結果は -o オプションでファイルに書き出しているので、表示させてみよう。

$ cat result.txt 
$pkzip2$1*2*2*0*19*d*40f63a90*0*43*0*19*40f6*2adb*e6c233aef1ba5a982f025c9bcdcdc86c4fa27c949c7871dc01*$/pkzip2$:password

ちなみに探索を中止してもセッションに名前をつけてあれば、以下のように再開できる。

$ hashcat --session helloworld --restore

セッションの情報は以下のように保存されている。

$ ls ~/.hashcat/sessions/
hashcat.log  hashcat.restore  hellosymbol.log  hellosymbol.restore  helloworld.log  helloworld.restore

探索する文字種別に記号を含めてみる

探索が必要な空間は、文字種別と桁数で指数関数的に増える。 そのため探索する文字種別に記号を含めると、単位時間あたりに探せる桁数はぐっと落ちることになる。 そこで、次はパスワードに記号を含めて試してみよう。

$ zip -e --password='pswd_+' greet.txt.zip greet.txt
updating: greet.txt (stored 0%)

ハッシュを取り直す。

$ zip2john greet.txt.zip | cut -d ":" -f 2 > greet.txt.zip.hash
ver 1.0 efh 5455 efh 7875 greet.txt.zip/greet.txt PKZIP Encr: 2b chk, TS_chk, cmplen=25, decmplen=13, crc=40F63A90
$ cat greet.txt.zip.hash 
$pkzip2$1*2*2*0*19*d*40f63a90*0*43*0*19*40f6*2adb*7f04312cbe0aab6e4a19fad645aecda94ea6fee0c2b3710fb0*$/pkzip2$

使用する文字種別は -1 ~ -4 オプションで登録できる。 それをマスク (以下の ?1?1... となっている部分) として利用する。 以下では記号を含む一通りの文字種別で 10 桁までインクリメンタルに探索する設定となる。

$ hashcat -m 17210 -a 3 -w 4 \
    --session hellosymbol \
    -o result.txt \
    -1 ?a \
    --increment \
    greet.txt.zip.hash \
    ?1?1?1?1?1?1?1?1?1?1

ちなみに、今回のケースではわずか 7 桁でも探索に 40 分ほどかかることが分かった。

...

Session..........: hellosymbol                     
Status...........: Exhausted
Hash.Name........: PKZIP (Uncompressed)
Hash.Target......: $pkzip2$1*2*2*0*19*d*40f63a90*0*43*0*19*40f6*2adb*7...kzip2$
Time.Started.....: Sat Jun  8 05:41:19 2019 (28 secs)
Time.Estimated...: Sat Jun  8 05:41:47 2019 (0 secs)
Guess.Mask.......: ?1?1?1?1?1?1 [6]
Guess.Charset....: -1 ?a, -2 Undefined, -3 Undefined, -4 Undefined 
Guess.Queue......: 6/10 (60.00%)
Speed.#1.........: 26579.9 MH/s (86.36ms) @ Accel:32 Loops:1024 Thr:1024 Vec:1
Recovered........: 0/1 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 735091890625/735091890625 (100.00%)
Rejected.........: 0/735091890625 (0.00%)
Restore.Point....: 81450625/81450625 (100.00%)
Restore.Sub.#1...: Salt:0 Amplifier:8192-9025 Iteration:0-1024
Candidates.#1....: 3<5x$~ ->  ~ ~}z
Hardware.Mon.#1..: Temp: 53c Util: 99% Core:1530MHz Mem: 877MHz Bus:16

とはいえ、実際に使ったパスワードの長さは 6 桁なので、数十秒あれば見つかる。

$ cat result.txt 
$pkzip2$1*2*2*0*19*d*40f63a90*0*43*0*19*40f6*2adb*e6c233aef1ba5a982f025c9bcdcdc86c4fa27c949c7871dc01*$/pkzip2$:password
$pkzip2$1*2*2*0*19*d*40f63a90*0*43*0*19*40f6*2adb*7f04312cbe0aab6e4a19fad645aecda94ea6fee0c2b3710fb0*$/pkzip2$:pswd_+

より強固な暗号化方式 (AES256) を用いる

例えば、もっと強固な方式を用いると探索速度はどのように変化するだろうか? 例として AES256 を使って暗号化してみることにした。

$ 7za a -tzip -ppassword -mem=AES256 greet.txt.zip greet.txt

ハッシュを取り直す。

$ zip2john greet.txt.zip | cut -d ":" -f 2 > greet.txt.zip.hash
$ cat greet.txt.zip.hash 
$zip2$*0*3*0*cf86e2828c42995d3a631f9dfe159ce8*2fa8*d*0a5619e079e4f6389e7d8da029*55fd0328aae560645e58*$/zip2$

実行してみよう。

$ hashcat -m 13600 -a 3 -w 4 \
    --session zipaes \
    -o result.txt \
    -1 ?a \
    --increment \
    greet.txt.zip.hash \
    ?1?1?1?1?1?1?1?1?1?1

すると、このパターンでは探索速度がわずか 2194 kH/s (2 MH/s) しか出ていない。 先ほどと比べると、およそ 10,000 分の 1 となった。

...

Session..........: zipaes
Status...........: Running
Hash.Name........: WinZip
Hash.Target......: $zip2$*0*3*0*cf86e2828c42995d3a631f9dfe159ce8*2fa8*.../zip2$
Time.Started.....: Sat Jun  8 05:45:57 2019 (11 secs)
Time.Estimated...: Sat Jun  8 05:46:34 2019 (26 secs)
Guess.Mask.......: ?1?1?1?1 [4]
Guess.Charset....: -1 ?a, -2 Undefined, -3 Undefined, -4 Undefined 
Guess.Queue......: 4/10 (40.00%)
Speed.#1.........:  2194.1 kH/s (74.89ms) @ Accel:32 Loops:249 Thr:1024 Vec:1
Recovered........: 0/1 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 23149125/81450625 (28.42%)
Rejected.........: 0/23149125 (0.00%)
Restore.Point....: 0/857375 (0.00%)
Restore.Sub.#1...: Salt:0 Amplifier:27-28 Iteration:249-498
Candidates.#1....: oari -> o ~}
Hardware.Mon.#1..: Temp: 50c Util: 99% Core:1530MHz Mem: 877MHz Bus:16

まとめ

  • パスワード付きの ZIP ファイルを hashcat + JtR + GPU で総当たりしてみた
    • さほど恐ろしさを覚える探索速度は出なかった
    • 仮に並列度を上げても総当たりなら定数倍の改善にとどまるはず
    • また、より強固な暗号化方式を使うとさらに総当たりが難しくなる
    • ただしパスワードは十分に長く記号を含んだものを使うことが前提となる
  • 今回使った環境での長さに関する相場感
    • 数字のみ: 12 桁の探索に 1 分
    • 数字 + アルファベット: 8 桁の探索に 3 分
    • 数字 + アルファベット + 記号: 7 桁の探索に 40 分

いじょう。

Python: LightGBM を Git のソースコードからインストールする

今回は LightGBM の Python パッケージを Git のソースコードからインストールする方法について。 まだリリースされていない最新の機能を使いたい、あるいは自分で改造したパッケージを使いたい、といった場合に。

なお、インストール方法は以下に記載されている。

github.com

使った環境は次の通り。

$ sw_vers                
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V
Python 3.7.3

下準備

まずはビルドに必要なパッケージをインストールしておく。

$ brew install cmake libomp

LightGBM のリポジトリをチェックアウトして python-package ディレクトリに移動しておく。

$ git clone https://github.com/microsoft/LightGBM.git
$ cd LightGBM/python-package 

インストール

公式のマニュアルを見ると、次のようにインストールすると書いてある。 ただ、これだと依存パッケージが一緒に入らない。

$ python setup.py install

なので、まずはソースコード配布物などのパッケージをまずはビルドした上で、それを使ってインストールするのが楽だと思う。

$ python setup.py sdist

これなら依存パッケージが同時に入る。

$ pip install dist/lightgbm-2.2.4.tar.gz

現時点 (2019-06-07) で未リリースのバージョンがインストールされた。 もちろん、これは Git の HEAD を使った開発版なので正式なバージョンがついているわけではない。

$ pip list | grep -i lightgbm                     
lightgbm     2.2.4  

作業ディレクトリを移動して lightgbm パッケージがインポートできることを確認する。

$ pushd /tmp && python -c "import lightgbm as lgb"

いじょう。

統計的学習の基礎 ―データマイニング・推論・予測―

統計的学習の基礎 ―データマイニング・推論・予測―

  • 作者: Trevor Hastie,Robert Tibshirani,Jerome Friedman,杉山将,井手剛,神嶌敏弘,栗田多喜夫,前田英作,井尻善久,岩田具治,金森敬文,兼村厚範,烏山昌幸,河原吉伸,木村昭悟,小西嘉典,酒井智弥,鈴木大慈,竹内一郎,玉木徹,出口大輔,冨岡亮太,波部斉,前田新一,持橋大地,山田誠
  • 出版社/メーカー: 共立出版
  • 発売日: 2014/06/25
  • メディア: 単行本
  • この商品を含むブログ (6件) を見る

Python: LightGBM の学習曲線をコールバックで動的にプロットする

LightGBM の学習が進む様子は、学習させるときにオプションとして verbose_eval などを指定することでコンソールから確認できる。 ただ、もっと視覚的にリアルタイムで確認したいなーと思ったので、今回はコールバックと Matplotlib を使って学習曲線を動的にグラフとしてプロットしてみることにした。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V            
Python 3.7.3

下準備

下準備として LightGBM と Matplotlib をインストールしておく。 Seaborn は本来は必要ないんだけどデータセットの読み込みにだけ使っている。

$ pip install lightgbm matplotlib seaborn

学習曲線を動的にプロットする

今回書いてみたサンプルコードは次の通り。 Seaborn から Titanic データセットを読み込んで LightGBM のモデルが学習していく過程を可視化している。 グラフのプロットは LearningVisualizationCallback というコールバックを実装することで実現している。 そのままだとグラフが寂しいので、カスタムメトリックとして Accuracy も追加してみた。

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

from collections import defaultdict

import numpy as np
import lightgbm as lgb
import seaborn as sns
from sklearn.model_selection import StratifiedKFold
from matplotlib import pyplot as plt


class LearningVisualizationCallback(object):
    """学習の過程を動的にプロットするコールバック"""

    def __init__(self, fig=None, ax=None):
        self._metric_histories = defaultdict(list)
        self._metric_history_lines = {}
        self._metric_type_higher_better = {}
        self._best_score_lines = {}
        self._best_score_texts = {}

        # 初期化する
        self._fig = fig
        self._ax = ax
        if self._fig is None and self._ax is None:
            self._fig, self._ax = plt.subplots()
        self._ax.set_title('learning curve')
        self._ax.set_xlabel('round')
        self._ax.set_ylabel('score')
        self._fig.canvas.draw()
        self._fig.show()

    def __call__(self, env):
        # メトリックを保存する
        evals = env.evaluation_result_list
        for _, name, mean, is_higher_better, _ in evals:
            self._metric_histories[name].append(mean)

            # 初回だけの設定
            if env.iteration == 0:
                # メトリックの種別を保存する
                self._metric_type_higher_better[name] = is_higher_better
                # スコアの履歴を描画するオブジェクトを生成する
                history_line, = self._ax.plot([], [])
                history_line.set_label(name)
                self._metric_history_lines[name] = history_line
                # ベストスコアの線を描画するオブジェクトを生成する
                best_line = self._ax.axhline(0)
                best_line.set_color(history_line.get_color())
                best_line.set_linestyle(':')
                self._best_score_lines[name] = best_line
                # ベストスコアの文字列を描画するオブジェクトを生成する
                best_text = self._ax.text(0, 0, '', weight='bold')
                best_text.set_color(history_line.get_color())
                self._best_score_texts[name] = best_text

        # 可視化する
        for name, values in self._metric_histories.items():
            # グラフデータを更新する
            history_line = self._metric_history_lines[name]
            history_line.set_data(np.arange(len(values)), values)
            best_line = self._best_score_lines[name]
            best_find_func = np.max if self._metric_type_higher_better[name] else np.min
            best_score = best_find_func(values)
            best_line.set_ydata(best_score)
            best_text = self._best_score_texts[name]
            best_text.set_text('{:.6f}'.format(best_score))
            best_text.set_y(best_score)

        # グラフの見栄えを調整する
        self._ax.legend()
        self._ax.relim()
        self._ax.autoscale_view()

        # 再描画する
        plt.pause(0.001)

    def show_until_close(self):
        """ウィンドウを閉じるまで表示し続ける"""
        plt.show()


def accuracy(preds, data):
    """精度 (Accuracy) を計算する関数
    NOTE: 表示が eval set の LogLoss だけだと寂しいので"""
    y_true = data.get_label()
    y_pred = np.where(preds > 0.5, 1, 0)
    acc = np.mean(y_true == y_pred)
    return 'accuracy', acc, True


def main():
    # Titanic データセットを読み込む
    dataset = sns.load_dataset('titanic')

    # 重複など不要な特徴量は落とす
    X = dataset.drop(['survived',
                      'class',
                      'who',
                      'embark_town',
                      'alive'], axis=1)
    y = dataset.survived

    # カテゴリカル変数を指定する
    categorical_columns = ['pclass',
                           'sex',
                           'embarked',
                           'adult_male',
                           'deck',
                           'alone']
    X = X.astype({c: 'category'
                  for c in categorical_columns})

    # LightGBM のデータセット表現に直す
    lgb_train = lgb.Dataset(X, y)

    # 学習の過程を可視化するコールバックを用意する
    visualize_cb = LearningVisualizationCallback()
    callbacks = [
        visualize_cb,
    ]

    # 二値分類を LogLoss で評価する
    lgb_params = {
        'objective': 'binary',
        'metrics': 'binary_logloss',
    }
    # 5-Fold CV
    skf = StratifiedKFold(n_splits=5,
                          shuffle=True,
                          random_state=42)
    lgb.cv(lgb_params, lgb_train,
           num_boost_round=1000,
           early_stopping_rounds=100,
           verbose_eval=10,
           folds=skf, seed=42,
           # Accuracy も確認する
           feval=accuracy,
           # コールバックを登録する
           callbacks=callbacks)

    # ウィンドウを閉じるまで表示し続ける
    visualize_cb.show_until_close()


if __name__ == '__main__':
    main()

上記に適当な名前をつけて実行してみよう。

$ python lgblearnviz.py

すると、モデルの学習に伴って次のようなアニメーションが表示される。

f:id:momijiame:20190903233754g:plain

いいかんじ。

なお、表示されているのは Validation Set に対するメトリックとなる。 Training Set も確認したかったんだけど、どうやら次のリリース (2.2.4?) でオプションに eval_train_metric が入るのを待つ必要がありそう。

あと、Jupyter Notebook で使うときは %matplotlib notebook マジックコマンドを使うと良い。

Python: google-api-python-client とサービスアカウントで Google Docs のファイルをダウンロードする

今回は Google Cloud Platform のサービスアカウントと google-api-python-client を使って Google Docs のファイルをダウンロードしてみる。 サービスアカウントというのは、人間ではなくアプリケーションなどのシステムが使うアカウントのこと。 例えば CI などの環境で Google Docs にあるファイルを操作するのに使えるかな。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V
Python 3.7.3

Google Cloud Platform にプロジェクトを作成する

まずは Google Cloud Platform にプロジェクトを用意する。 なお、プロジェクトの ID は Google Cloud Platform 上で一意な必要がある点に注意する。

プロジェクトは Google Cloud Console から作っても良いけど、今回は Google Cloud SDK の CLI から作る。 そこで、まずは Google Cloud SDK を Homebrew Cask でインストールしておく。

$ brew cask install google-cloud-sdk

インストールしたら gcloud コマンドが使えるようになるので、早速プロジェクトを作成する。

$ gcloud projects create <project-id>

今回は example-gdrive-service-account という名前のプロジェクトを使った。

$ gcloud projects create example-gdrive-service-account

プロジェクトを作ったら、以下のようにしてデフォルトのプロジェクトに設定しておくと良い。

$ gcloud config set project example-gdrive-service-account

上記の操作が終わったら、Google Cloud Console で Google Drive API を有効化しておく。 Google Docs のデータは Google Drive に保存されているため。 また、この作業だけは gcloud コマンドから実行できない。

プロジェクトにサービスアカウントを作成する

続いてプロジェクトにサービスアカウントを追加する。

プロジェクトにサービスアカウントを追加するには、以下のように gcloud iam service-accounts create コマンドを使う。

$ gcloud iam service-accounts create <sa-id> \
    --display-name <sa-name>

今回は、次のように gdrive-access-account という名前のアカウントを作った。

$ gcloud iam service-accounts create gdrive-access-account \
    --display-name gdrive-access-account

サービスアカウントに紐付いた認証情報をダウンロードする

サービスアカウントができたら、次はサービスアカウントに紐付いた認証情報をダウンロードする。 この認証情報があればサービスアカウントのできる操作は全てできてしまうので注意して管理しよう。

認証情報は次のように gcloud iam service-accounts keys create コマンドでダウンロードできる。 対象となるサービスアカウントは --iam-account というオプションで指定する。 このときメールアドレスのような形式でサービスアカウントを特定する。

$ gcloud iam service-accounts keys create <key-file> \
      --iam-account <sa-name>@<project-id>.iam.gserviceaccount.com

例えば今回であれば次のようになる。

$ gcloud iam service-accounts keys create ./key.json \
      --iam-account gdrive-access-account@example-gdrive-service-account.iam.gserviceaccount.com

実行すると、次のように認証情報がダウンロードされる。 デフォルトでは JSON 形式となる。

$ file key.json
key.json: ASCII text, with very long lines

操作したい Google Docs のファイルまたはディレクトリをサービスアカウントに共有する

続いて Google Drive をブラウザで開いてサービスアカウントから操作したいファイルまたはディレクトリに移動する。 そして、メニューから「ファイル > 共有」を選択する。 ここで、招待する相手としてサービスアカウントのメールアドレスを入力する。

または、公開しても問題ないファイルであれば共有メニューの「詳細設定」からアクセスできるユーザを変更する。 具体的には「ウェブ上で一般公開」または「リンクを知っている全員」に指定すれば良い。 ただ、この場合はクレデンシャルなしでアクセスできるので、別にサービスアカウントを作る必要はないはず。

サービスアカウントの認証情報と google-api-python-client を使ってファイルにアクセスする

ここまでで下準備は完了した。 続いては Python のクライアントからファイルを操作する作業に入る。

まずは必要なパッケージをインストールする。

$ pip install google-api-python-client

Python のインタプリタを起動しよう。

$ python

まずは、先ほどダウンロードしてきた認証情報を元に Credentials のインスタンスを作る。

>>> from google.oauth2.service_account import Credentials
>>> credential_file = 'key.json'
>>> c = Credentials.from_service_account_file(credential_file)

続いて、認証情報を元に Google Drive にアクセスするためのオブジェクトを作る。

>>> from googleapiclient import discovery
>>> service = discovery.build('drive', 'v3', credentials=c)

続いてファイル ID を指定して実際にファイルをダウンロード (エクスポート) する。 ファイル ID はファイルの共有メニューにある「共有可能なリンクを取得」から確認できる。 例えば Google 図形描画であれば以下の `<file-id> の部分がそれに当たる。

https://docs.google.com/drawings/d/<file-id>/edit

今回ダウンロードしたかったのは Google 図形描画の PNG ファイルなので mimeType オプションに image/png を指定した。

>>> file_id = '<file-id-to-download>'
>>> request = service.files().export(fileId=file_id, mimeType='image/png')
>>> image_data = request.execute()

ファイルのバイト列が取得できたら、あとはそれを使うだけ。 例えばローカルのディスクに書き出すのであれば次のようにする。

>>> with open('drive.png', mode='wb') as f:
...     f.write(image_data)
... 

いじょう。

Ubuntu 18.04 LTS に OpenCL (NVIDIA CUDA Runtime) をインストールする

OpenCL は、CPU や GPU など様々なプラットフォームを抽象化して並列計算に用いるためのフレームワーク。 今回は Ubuntu 18.04 LTS + NVIDIA Tesla T4 の環境に OpenCL の NVIDIA CUDA ランタイムをインストールしてみる。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.2 LTS"
$ uname -r
4.15.0-1033-gcp
$ nvidia-smi
Tue Jun  4 13:22:36 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla T4            On   | 00000000:00:04.0 Off |                    0 |
| N/A   58C    P8    10W /  70W |      0MiB / 15079MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

下準備

利用できる OpenCL のランタイムを確認するために clinfo パッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install clinfo

最初の状態では、次の通り利用できるランタイムが存在しない。

$ clinfo
Number of platforms                               0

NVIDIA CUDA をインストールする

実は OpenCL は NVIDIA CUDA をインストールすると自動的に使えるようになる。 なので NVIDIA CUDA をインストールしていく。

NVIDIA CUDA のインストール用 deb ファイルは以下から取得できる。

developer.nvidia.com

ちなみに、使う GPU のアーキテクチャ (Compute Capability) によってサポートされている CUDA のバージョンが異なる点に注意しよう。

Web サイトからファイルをダウンロードする。

$ sudo apt-get -y install wget
$ wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.1.168-1_amd64.deb

システムに CUDA のリポジトリを登録する。

$ sudo dpkg -i cuda-repo-ubuntu1804_10.1.168-1_amd64.deb
$ sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub
$ sudo apt-get update

CUDA をインストールする。

$ sudo apt-get -y install cuda

インストールが終わったらマシンを再起動する。

$ sudo shutdown -r now

マシンが再起動してきたら、nvidia-smi コマンドで GPU の状況が確認できるはず。

$ nvidia-smi
Tue Jun  4 13:20:22 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla T4            On   | 00000000:00:04.0 Off |                    0 |
| N/A   76C    P0    32W /  70W |      0MiB / 15079MiB |      6%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

また、clinfo コマンドを実行すると次のようにプラットフォームが認識されるはず。 現時点での OpenCL の最新バージョンは 2.1 なので、それに比べると 1.2 はちょっと古い。

$ clinfo | grep "Number of platforms" -A 3
Number of platforms                               1
  Platform Name                                   NVIDIA CUDA
  Platform Vendor                                 NVIDIA Corporation
  Platform Version                                OpenCL 1.2 CUDA 10.1.152

とはいえ、これで OpenCL を使った並列計算が NVIDIA の GPU を使って実行できる。

いじょう。

Ubuntu 16.04 LTS に OpenCL (Intel CPU Runtime) をインストールする

OpenCL は、CPU や GPU など様々なプラットフォームを抽象化して並列計算に用いるためのフレームワーク。 今回は Ubuntu 16.04 LTS の環境に Intel の CPU ランタイムをインストールしてみる。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.6 LTS"
$ uname -r
4.4.0-148-generic

下準備

利用できる OpenCL のランタイムを確認するために clinfo パッケージをインストールしておく。

$ sudo apt-get -y install clinfo

初期の状態では何も利用できない。

$ clinfo
Number of platforms                               0

Intel CPU Runtime をインストールする

最初に、インストールに必要なパッケージをインストールしておく。

$ sudo apt-get -y install lsb-core

Intel CPU Runtime は以下の Web サイトからダウンロードする。

software.intel.com

上記からはいくつかのプラットフォーム向けのランタイムがダウンロードできる。 その中でも Intel® Xeon® Processor or Intel® Core™ Processor を選択する。

ダウンロードしたファイルを解凍する。

$ tar xvf l_opencl_p_18.1.0.015.tgz
$ cd l_opencl_p_18.1.0.015/

インストールスクリプトを実行する。

$ sudo sh install.sh

次のような手順で選択すればインストールできる。

5 (Installation) -> q (pager) -> accept -> 2 (I do NOT consent to the collection of my Information) -> 1 (Accept configuration and begin installation [ default ])

インストールが完了したら、先ほどと同じように clinfo コマンドを実行する。 次のように CPU Runtime が表示されれば上手くいってる。

$ clinfo | grep -A 3 "Number of platforms"
Number of platforms                               1
  Platform Name                                   Intel(R) CPU Runtime for OpenCL(TM) Applications
  Platform Vendor                                 Intel(R) Corporation
  Platform Version                                OpenCL 2.1 LINUX

いじょう。