CUBE SUGAR CONTAINER

技術系のこと書きます。

ソースコードから Python をインストールするときにビルドされないモジュールを確認する

ソースコードから Python をインストールするとき、環境によってはビルドされないモジュールが出てくる。 今回は、どんなモジュールがビルドされなかったかを確認する方法について。 先に結論から書くと、ビルドされなかったモジュールがあるときはログにメッセージが残る。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
$ uname -r
4.15.0-20-generic

下準備

まずは、あえてビルドされないモジュールが出るように環境を整える。 最低限のビルドだけはできるように build-essential パッケージだけインストールしておこう。

$ sudo apt-get -y install build-essential

もちろん使う環境によるけど、これだと結構なモジュールがビルドされないはず。

続いて Python のソースコードを取得する。

$ wget -O - https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tgz | tar zxvf -
$ cd Python-3.6.6

Python をビルドする

あとは ./configuremake を使って Python をビルドする。

$ ./configure
$ make 2>&1 | tee make.log

上記で、ちゃんとビルドのログを残しておくのがポイント。

すると、ビルドしたディレクトリに Python のインタプリタが作られる。

$ file python
python: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=27b252471eb5f2c16941f992d26f2e13ec5e3ada, with debug_info, not stripped

実行すると、ちゃんと動く。

$ ./python -c "print('Hello, World')"
Hello, World

ただし、上記だとおそらくビルドされないモジュールがある。 具体的には、ビルドログを以下の文字列で検索をかければ分かる。

$ grep -A 3 "The necessary bits to build these optional modules were not found:" make.log 
The necessary bits to build these optional modules were not found:
_bz2                  _curses               _curses_panel      
_dbm                  _gdbm                 _lzma              
_sqlite3              _tkinter

色々と出てきた。

例えば _sqlite3 という文字列が見えるので sqlite3 パッケージをインポートしてみよう。

$ ./python -c "import sqlite3"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/vagrant/Python-3.6.6/Lib/sqlite3/__init__.py", line 23, in <module>
    from sqlite3.dbapi2 import *
  File "/home/vagrant/Python-3.6.6/Lib/sqlite3/dbapi2.py", line 27, in <module>
    from _sqlite3 import *
ModuleNotFoundError: No module named '_sqlite3'

想定通りではあるけど ModuleNotFoundError というエラーになってしまった。

もちろん、上記のインポートはディストリビューションにデフォルトで入っている Python ならちゃんと動く。

$ which python3
/usr/bin/python3
$ python3 -c "import sqlite3"

つまりビルドした環境に色々とパッケージが足りていない。

ビルドされないモジュールを無くす

ここからは、先ほどビルドされなかったモジュールが、ちゃんとビルドされるように直していこう。 例えば Python のインストールマネージャの pyenv では、各環境用に必要なパッケージを示している。

github.com

ここに記載されているコマンドを、そのまま実行してみよう。

$ sudo apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev \
  libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
  xz-utils tk-dev libffi-dev liblzma-dev

インストールできたら、再度 Python をビルドする。

$ ./configure && make 2>&1 | tee make.log

もう一度ログを確認すると、ほとんどのモジュールはビルドできたようだ。 ただ、ごく一部に関してはまだ残っている。

$ grep -A 3 "The necessary bits to build these optional modules were not found:" make.log 
The necessary bits to build these optional modules were not found:
_dbm                  _gdbm                                    
To find the necessary bits, look in setup.py in detect_modules() for the module's name.

該当するモジュールに必要なパッケージをインストールして再度チャレンジする。

$ sudo apt-get install libgdbm-dev libdb-dev
$ ./configure && make 2>&1 | tee make.log

ログを確認すると、今度は grep に何も引っかからなくなった。

$ grep -A 3 "The necessary bits to build these optional modules were not found:" make.log 

先ほどエラーになったインポートなどを試しても、今度は問題なく動いている。

$ ./python -c "import sqlite3"
$ ./python -c "import dbm"
$ ./python -c "from dbm import gnu"

ばっちり。

pyenv を使う場合

ちなみに pyenv を使って Python をインストールする場合にも同じ問題が起こる恐れがある。 そのときはインストールするときに -v オプションをつけてビルドログを残しておこう。

$ pyenv install 3.6.6 -v 2>&1 | tee make.log

あとは同じ要領でビルドされなかったモジュールを確認できる。

めでたしめでたし。

SSHFS を使ってリモートホストのディレクトリをマウントする

SSH でログインできるリモートホストとのファイルのやり取りは SCP を使うことが多い。 ただ、頻繁にやり取りするときは、それも面倒に感じることがある。 ただ、あんまり手間のかかる設定作業はしたくない。 そんなときは SSHFS を使うと手軽に楽ができそう。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
$ uname -r
4.15.0-20-generic

下準備

まず、ファイルを頻繁にやり取りする先として 192.168.33.10 という IP アドレスのホストがいたとする。

$ ping -c 3 192.168.33.10
PING 192.168.33.10 (192.168.33.10) 56(84) bytes of data.
64 bytes from 192.168.33.10: icmp_seq=1 ttl=64 time=0.761 ms
64 bytes from 192.168.33.10: icmp_seq=2 ttl=64 time=0.473 ms
64 bytes from 192.168.33.10: icmp_seq=3 ttl=64 time=0.646 ms

--- 192.168.33.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2029ms
rtt min/avg/max/mdev = 0.473/0.626/0.761/0.121 ms

このホストには、あらかじめ SSH の公開鍵をインストールしておく。

$ ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
Generating public/private rsa key pair.
Your identification has been saved in /home/vagrant/.ssh/id_rsa.
Your public key has been saved in /home/vagrant/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:Yby6vBiMZBv1q76ncrJYOQAMiVbHOaTEFmcGX09pTXQ vagrant@vagrant
The key's randomart image is:
+---[RSA 2048]----+
|o.o==*.. .=o E   |
|=..oB+..oo ..    |
|o...... =.       |
|.  . . . o       |
|. +   . S        |
| + *   o         |
|  * o o          |
| oo..=..         |
|. .**++.         |
+----[SHA256]-----+
$ ssh-copy-id -i ~/.ssh/id_rsa.pub vagrant@192.168.33.10
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/vagrant/.ssh/id_rsa.pub"
The authenticity of host '192.168.33.10 (192.168.33.10)' can't be established.
ECDSA key fingerprint is SHA256:8qF9F+rTQA5Mqn+DhSCuAdo6jvL6RrXNBDQAEeuSkRk.
Are you sure you want to continue connecting (yes/no)? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
vagrant@192.168.33.10's password: 

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'vagrant@192.168.33.10'"
and check to make sure that only the key(s) you wanted were added.

これで、パスワードを入力しなくてもホストに SSH でログインできるようになった。

$ ssh vagrant@192.168.33.10 "echo Hello, World!"
Hello, World!

続いてローカルのマシンには SSHFS をインストールしておく。

$ sudo apt-get -y install sshfs

これで下準備が整った。

SCP を使ってファイルをやり取りする場合

まず、前提として SCP を使ってファイルをやり取りする場合に必要となる作業を確認しておこう。 scp コマンドを使って、転送元と転送先を指定する。

$ echo "Hello, SCP!" > greet.txt
$ scp greet.txt vagrant@192.168.33.10:~/
greet.txt                                                                                                 100%   12     9.7KB/s   00:00    

SSH で確認すると、ちゃんとファイルが転送できている。

$ ssh vagrant@192.168.33.10 "cat greet.txt"
Hello, SCP!

ただ、何度もこれをやるのは結構しんどい。

基本的な使い方

続いては SSHFS を使ってみよう。

まずはマウントポイントになるディレクトリを用意する。

$ mkdir sshfsmnt

続いて sshfs コマンドを使って、マウントしたいリモートホストのディレクトリと、ローカルホストのマウント先のディレクトリを指定する。

$ sshfs vagrant@192.168.33.10:/home/vagrant/ sshfsmnt

これで df コマンドの結果に SSHFS でマウントした内容が表示されるようになる。

$ df
Filesystem                           1K-blocks     Used Available Use% Mounted on
udev                                    484508        0    484508   0% /dev
tmpfs                                   100916     5440     95476   6% /run
/dev/mapper/vagrant--vg-root          64800356  1849852  59629060   4% /
tmpfs                                   504560        0    504560   0% /dev/shm
tmpfs                                     5120        0      5120   0% /run/lock
tmpfs                                   504560        0    504560   0% /sys/fs/cgroup
vagrant                              244810132 88761060 156049072  37% /vagrant
tmpfs                                   100912        0    100912   0% /run/user/1000
vagrant@192.168.33.10:/home/vagrant/  64800356  1793032  59685880   3% /home/vagrant/sshfsmnt

マウントしたディレクトリの中身を確認すると、先ほど scp コマンドで転送したファイルもちゃんと見えている。

$ ls sshfsmnt/
greet.txt
$ cat sshfsmnt/greet.txt 
Hello, SCP!

試しに、このファイルを書き換えてみよう。

$ echo "Hello, SSHFS!" > sshfsmnt/greet.txt 

改めて ssh コマンドを使ってリモートホスト上でファイルの内容を確認する。

$ ssh vagrant@192.168.33.10 "cat greet.txt"
Hello, SSHFS!

ちゃんとリモートホスト上のファイルが書き換わっていることが分かる。

いじょう。

Python: ベイズ最適化で機械学習モデルのハイパーパラメータを選ぶ

機械学習モデルにおいて、人間によるチューニングが必要なパラメータをハイパーパラメータと呼ぶ。 ハイパーパラメータをチューニングするやり方は色々とある。 例えば、良さそうなパラメータの組み合わせを全て試すグリッドサーチや、無作為に試すランダムサーチなど。

今回は、それとはちょっと違ったベイズ最適化というやり方を試してみる。 ベイズ最適化では、過去の試行結果から次に何処を調べれば良いかを確率分布と獲得関数にもとづいて決める。 これにより、比較的少ない試行回数でより優れたハイパーパラメータが選べるとされる。

Python でベイズ最適化をするためのパッケージとしては Bayesian OptimizationskoptGPyOpt などがある。 今回は、その中でも Bayesian Optimization を使ってみることにした。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.6
BuildVersion:   17G65
$ python -V          
Python 3.6.6

下準備

まずは今回使う Python のパッケージを一通りインストールしておく。

$ pip install bayesian-optimization matplotlib tqdm

基本的な考え方

最初に、単純な例を使って基本的な考え方を説明しておきたい。

まず、機械学習モデルにおけるハイパーパラメータの選択は、未知の関数の出力を最大化あるいは最小化する問題と捉えられる。 これは、ハイパーパラメータを入力として、それで学習した機械学習モデルを関数、モデルを評価して得られた何らかの性能指標 (出力) を最も良くする (最大化・最小化) ことが目的のため。 つまり、未知の関数の出力が最大あるいは最小となる点を見つけることができるなら、それはハイパーパラメータの選択にも適用できる可能性がある。

ごく単純な例として、次のような関数の出力を最大化することを考えてみよう。 グラフからは、だいたい x が 2 あたりで y が最大の 1.4 前後となることが分かる。 それ以外の場所では、だいたい 0 と 6 あたりに局所解が存在していて、そこでは y が 1 くらいになる。

f:id:momijiame:20180818132951p:plain

ただし、この関数はあくまで本来は未知で、上記のグラフはあらかじめ分からないという前提になる。 その状況で、どうやら x が 2 あたりで y が最大になるっぽいぞ、ということを知りたい。

ちなみに、このグラフは次のようなコードで作った。 この中にある関数 f() が未知の関数ということになる。

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

import math

import numpy as np

from matplotlib import pyplot as plt


def f(x):
    """最大値を見つけたい関数"""
    return math.exp(-(x - 2) ** 2) + math.exp(-(x - 6) ** 2 / 10) + 1 / (x ** 2 + 1)


def main():
    # x が -5 ~ +15 の範囲を 0.1 刻みでプロットする
    X = [x for x in np.arange(-5, 15, 0.1)]
    y = [f(x) for x in X]
    plt.plot(X, y)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

上記を適当な名前で保存して実行すると、先ほどのグラフが得られる。

$ python fplot.py

ベイズ最適化で関数の最大値を見つける

先ほどの関数を例にして、実際に Bayesian Optimization を使って関数が最大となる場所を探してみよう。

早速、以下にサンプルコードを示す。 大体の使い方はコメントを見れば分かると思う。 基本的には、探すパラメータとその範囲、そして初期点と探索するイテレーションの数を指定するだけで良い。

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

import math

from bayes_opt import BayesianOptimization


def f(x):
    """最大値を見つけたい関数"""
    return math.exp(-(x - 2) ** 2) + math.exp(-(x - 6) ** 2 / 10) + 1 / (x ** 2 + 1)


def main():
    # 探索するパラメータと範囲を決める
    pbounds = {
        'x': (-5, 15),
    }
    # 探索対象の関数と、探索するパラメータと範囲を渡す
    bo = BayesianOptimization(f=f, pbounds=pbounds)
    # 最大化する
    bo.maximize(init_points=3, n_iter=10)
    # 結果を出力する
    print(bo.res['max'])


if __name__ == '__main__':
    main()

実行結果は次の通り。 尚、結果は実行する毎に違ったものになる。 試行によっては局所解を答えてしまう場合もあるかも。

$ python bo.py             
Initialization
-----------------------------------------
 Step |   Time |      Value |         x | 
    1 | 00m00s |    0.87632 |    4.6393 | 
    2 | 00m00s |    0.12396 |   -2.6651 | 
    3 | 00m00s |    0.75934 |   -0.5850 | 
Bayesian Optimization
-----------------------------------------
 Step |   Time |      Value |         x | 
    4 | 00m00s |    0.00520 |   14.6498 | 
    5 | 00m00s |    0.39154 |    9.1121 | 
    6 | 00m00s |    1.39524 |    2.0885 | 
    7 | 00m00s |    1.17158 |    1.4247 | 
    8 | 00m00s |    0.80735 |    3.1369 | 
    9 | 00m00s |    0.97558 |    6.6884 | 
   10 | 00m00s |    0.04176 |   11.7992 | 
   11 | 00m00s |    0.03847 |   -5.0000 | 
   12 | 00m00s |    1.02148 |    5.7120 | 
   13 | 00m00s |    0.74644 |    7.7734 | 
{'max_val': 1.3952415083439782, 'max_params': {'x': 2.0884969890991476}}

上記の試行では、関数が最大となる場所は x が 2.08 あたりで、値は 1.39 くらいという結果が得られた。 先ほどグラフから目視で読み取った内容と整合している。

試行の過程を可視化してみる

先ほどの例では何となく上手くいくことは分かった。 ただ、ベイズ最適化が具体的に何処をどう探索しているのかよく分からない。 そこで、続いてはその過程を可視化してみることにする。

次のサンプルコードでは、ベイズ最適化の過程をグラフとしてプロットしている。

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

import math

import numpy as np

from bayes_opt import BayesianOptimization

from matplotlib import pyplot as plt


def f(x):
    """最大値を見つけたい関数"""
    return math.exp(-(x - 2) ** 2) + math.exp(-(x - 6) ** 2 / 10) + 1 / (x ** 2 + 1)


def plot_bo(bo):
    # プロット範囲 (決め打ち)
    X = [x for x in np.arange(-5, 15, 0.1)]

    # 真の関数
    y = [f(x) for x in X]
    plt.plot(X, y, label='true')

    # サンプル点
    xs = [p['x'] for p in bo.res['all']['params']]
    ys = bo.res['all']['values']
    plt.scatter(xs, ys, c='green', s=20, zorder=10, label='sample')

    # 予測結果
    mean, sigma = bo.gp.predict(np.array(X).reshape(-1, 1), return_std=True)
    plt.plot(X, mean, label='pred')  # 推定した関数
    plt.fill_between(X, mean + sigma, mean - sigma, alpha=0.1)  # 標準偏差

    # 最大値
    max_x = bo.res['max']['max_params']['x']
    max_y = bo.res['max']['max_val']
    plt.scatter([max_x], [max_y], c='red', s=50, zorder=10, label='pred_max')


def main():
    # 探索するパラメータと範囲を決める
    pbounds = {
        'x': (-5, 15),
    }

    # 探索対象の関数と、探索するパラメータと範囲を渡す
    bo = BayesianOptimization(f=f, pbounds=pbounds)
    # 最大化する
    bo.maximize(init_points=3, n_iter=10)

    # 結果をグラフに描画する
    plot_bo(bo)

    # グラフを表示する
    plt.legend()
    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

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

$ python boplot.py 
Initialization
-----------------------------------------
 Step |   Time |      Value |         x | 
    1 | 00m00s |    0.06077 |   -3.9330 | 
    2 | 00m00s |    0.74088 |    7.7945 | 
    3 | 00m00s |    0.12176 |   10.6689 | 
Bayesian Optimization
-----------------------------------------
 Step |   Time |      Value |         x | 
    4 | 00m00s |    1.12790 |    2.6188 | 
    5 | 00m00s |    0.87412 |    4.6285 | 
    6 | 00m00s |    1.04842 |    0.0374 | 
    7 | 00m01s |    1.14774 |    1.3868 | 
    8 | 00m01s |    1.39986 |    1.9524 | 
    9 | 00m00s |    0.00473 |   15.0000 | 
   10 | 00m00s |    0.26949 |   -1.6584 | 
   11 | 00m00s |    1.02154 |    6.1962 | 
   12 | 00m00s |    0.01546 |   12.8301 | 
   13 | 00m00s |    0.39699 |    9.0894 | 

すると、例えば以下のようなグラフが得られる。 ここで true となっているのが真の関数で pred がベイズ最適化で推定した関数となる。 推定するのに用いた点は sample で、見つけた最大値と思われる場所は pred_max で図示している。 網掛けになっているのは、その周辺を調べていないことから、まだ推定にバラつきが大きいことを示している。

f:id:momijiame:20180818135603p:plain

上記を見ると、いくつかの点を調べることで的確に真の関数に近いものを推定し、最大となる箇所を見つけていることが分かる。

獲得関数 (Acquisition Function)

ベイズ最適化では、次に何処を探すのかを確率分布にもとづいて獲得関数が決める。 使う獲得関数によって、まだ調べていない場所の探索に重きを置くのか、それとも着実な改善が見込める場所を探すのか、といった味付けが変わる。

Bayesian Optimization で使える獲得関数は以下の三つで、デフォルトは "ucb" になっている。

  • Upper Confidence Bound (ucb)
  • Probability of Improvement (poi)
  • Expected Improvement (ei)

ちなみに Probability of Improvement は局所解に陥りやすいため、残り二つのどちらかを使うのが良さそう。

以下のサンプルコードでは、それぞれの獲得関数がどのように試行するのかをグラフに可視化してみた。 獲得関数のパラメータも二種類ずつ設定している。

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

import math

import numpy as np

from bayes_opt import BayesianOptimization

from matplotlib import pyplot as plt

from tqdm import tqdm


def f(x):
    """最大値を見つけたい関数"""
    return math.exp(-(x - 2) ** 2) + math.exp(-(x - 6) ** 2 / 10) + 1 / (x ** 2 + 1)


def plot_bo(bo):
    # プロット範囲 (決め打ち)
    X = [x for x in np.arange(-5, 15, 0.1)]

    # 真の関数
    y = [f(x) for x in X]
    plt.plot(X, y, label='true')

    # サンプル点
    xs = [p['x'] for p in bo.res['all']['params']]
    ys = bo.res['all']['values']
    plt.scatter(xs, ys, c='green', s=20, zorder=10, label='sample')

    # 予測結果
    mean, sigma = bo.gp.predict(np.array(X).reshape(-1, 1), return_std=True)
    plt.plot(X, mean, label='pred')  # 推定した関数
    plt.fill_between(X, mean + sigma, mean - sigma, alpha=0.1)  # 標準偏差

    # 最大値
    max_x = bo.res['max']['max_params']['x']
    max_y = bo.res['max']['max_val']
    plt.scatter([max_x], [max_y], c='red', s=50, zorder=10, label='pred_max')


def main():
    # 探索するパラメータと範囲を決める
    pbounds = {
        'x': (-5, 15),
    }

    acq_params = [
        ('ucb_kappa_1', {
            'acq': 'ucb',
            'kappa': 1,
        }),
        ('ucb_kappa_10', {
            'acq': 'ucb',
            'kappa': 10,
        }),
        ('ei_xi_1e-4', {
            'acq': 'ei',
            'xi': 1e-4,
        }),
        ('ei_xi_1e-1', {
            'acq': 'ei',
            'xi': 1e-1,
        }),
        ('poi_xi_1e-4', {
            'acq': 'poi',
            'xi': 1e-4,
        }),
        ('poi_xi_1e-1', {
            'acq': 'poi',
            'xi': 1e-1,
        }),
    ]

    plt.figure(figsize=(8, 12))

    for i, (name, acq_param) in tqdm(list(enumerate(acq_params))):
        # 探索対象の関数と、探索するパラメータと範囲を渡す
        bo = BayesianOptimization(f=f, pbounds=pbounds, verbose=0)
        # 最大化する
        bo.maximize(init_points=3, n_iter=10, **acq_param)

        # 結果をグラフに描画する
        plt.subplot(3, 2, i + 1)
        plt.title(name)
        plot_bo(bo)

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

パラメータは、まだちゃんと理解できていないものの、次のように解釈できるっぽい。

  • kappa
    • 獲得関数が "ucb" のときに有効なパラメータ
    • 大きくするほど「探索」に重きを置く
  • xi
    • 獲得関数が "poi" と "ei" で有効なパラメータ
    • 大きくするほど既知の最大値から改善が見込める幅が大きな場所を探す

上記を適当な名前で保存して実行しよう。 終わるのには結構時間がかかるし、いくらか警告が出るかもしれない。

$ python boacq.py
...

すると、次のようなグラフが得られる。

f:id:momijiame:20180818201854p:plain

獲得関数が "poi" でパラメータの "xi" が 1e-4 のパターンでは局所解に陥っていることが分かる。

機械学習モデルにベイズ最適化を適用する

だいぶ回り道したけど、ここからが今回の本題になる。 続いては、ベイズ最適化を使ってサポートベクターマシンのハイパーパラメータを選んでみよう。 RBF カーネルのサポートベクターマシンには "C" と "gamma" という二つのハイパーパラメータがある。 これをベイズ最適化を使って選ぶことにする。

以下にサンプルコードを示す。 基本的にやっていることは先ほどと変わらない。 まず、返り値を最大化したい関数を f() として用意している。 この関数は、渡されたハイパーパラメータを使ってサポートベクターマシンを学習する。 そして、学習したモデルを 3-Fold CV を使って精度 (Accuracy) で評価して、各結果の平均を返すことになる。 あとは、その関数の精度がなるべく高くなるようにベイズ最適化でハイパーパラメータを選ぶ。

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

import functools

from sklearn.svm import SVC
from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score
from sklearn import datasets


import numpy as np

from bayes_opt import BayesianOptimization

from matplotlib import pyplot as plt
from matplotlib import cm


def f(X, y, **params):
    """最大化したい関数 (モデルを交差検証して得たスコアを返す)"""
    svm = SVC(kernel='rbf', **params)
    kf = KFold(n_splits=4, shuffle=True, random_state=42)
    scores = cross_val_score(svm, X=X, y=y, cv=kf)
    return scores.mean()


def main():
    dataset = datasets.load_iris()
    X, y = dataset.data, dataset.target

    # 最大化したい関数にデータセットを部分適用する
    pf = functools.partial(f, X, y)

    pbounds = {
        'C': (1e+0, 1e+2),
        'gamma': (1e-2, 1e+1),
    }
    bo = BayesianOptimization(f=pf, pbounds=pbounds)
    # 最大化する
    bo.maximize(init_points=3, n_iter=20)
    # 結果を出力する
    print(bo.res['max'])

    # 試行をプロットする
    xs = [p['C'] for p in bo.res['all']['params']]
    ys = [p['gamma'] for p in bo.res['all']['params']]
    zs = np.array(bo.res['all']['values'])
    s_zs = (zs - zs.min()) / (zs.max() - zs.min())  # 0 ~ 1 の範囲で標準化する
    sc = plt.scatter(xs, ys, c=s_zs, s=20, zorder=10, cmap=cm.cool)
    plt.colorbar(sc)

    plt.xlabel('C')
    plt.xscale('log')
    plt.ylabel('gamma')
    plt.yscale('log')
    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

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

$ python bosvm.py
...(省略)...
{'max_val': 0.9797297297297298, 'max_params': {'C': 31.622294778743424, 'gamma': 0.011103966665153005}}

上記から、最も汎化性能が高くなるのは C が 31.6 前後で gamma が 0.01 前後のとき、ということが分かった。

同時に、次のようなグラフが得られる。 これは、各ハイパーパラメータの値ごとに汎化性能を対数スケールの散布図で表したもの。 ハイパーパラメータによる精度の違いは元の数値ではぶっちゃけ微々たるものなので、数値は 0 ~ 1 に正規化してある。 また、汎化性能の高低は色で表現している。

f:id:momijiame:20180818232846p:plain

参考までに、グリッドサーチで調べた結果も以下に示す。

f:id:momijiame:20180819173530p:plain

上記のグラフを描くのに使ったソースコードは次の通り。

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

import numpy as np

from sklearn import datasets
from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV

from matplotlib import pyplot as plt
from matplotlib import cm


def main():
    # アイリスデータセットを読み込む
    dataset = datasets.load_iris()
    X, y = dataset.data, dataset.target

    # 優れたハイパーパラメータを見つけたいモデル
    clf = SVC(kernel='rbf')

    # 試行するパラメータを羅列する
    params = {
        'C': [1e+0, 1e+1, 1e+2],
        'gamma': [1e-2, 1e-1, 1e+0, 1e+1],
    }
    
    # グリッドサーチで優れたハイパーパラメータを探す
    grid_search = GridSearchCV(clf,
                               param_grid=params,
                               cv=3)
    grid_search.fit(X, y)

    # スコアとパラメータをグラフにプロットする
    score_and_params = np.array([(params['C'], params['gamma'], mean) for params, mean, _ in grid_search.grid_scores_])
    scores = score_and_params[:, 2]
    standard_scores = (scores - scores.min()) / (scores.max() - scores.min())
    sc = plt.scatter(score_and_params[:, 0], score_and_params[:, 1], c=standard_scores, s=20, zorder=10, cmap=cm.cool)
    plt.colorbar(sc)

    plt.xlabel('C')
    plt.xscale('log')
    plt.ylabel('gamma')
    plt.yscale('log')
    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

まとめ

  • ベイズ最適化を使うと未知の関数の出力が最大あるいは最小になる場所を見つけることができる
  • 機械学習モデルにおけるハイパーパラメータの選択は未知の関数の出力を最大化あるいは最小化する問題と捉えられる
  • つまり、機械学習モデルのハイパーパラメータ選びにベイズ最適化を適用できる場合がある

Apache Hive で SELECT した結果から ARRAY を作る

Apahe Hive を使っていて、テーブルから SELECT してきた結果から ARRAY 型のカラムを作る方法が分からなくて調べた。 結論から先に述べると COLLECT_LIST() を使えば良い。

使った環境は次の通り。

$ cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core) 
$ uname -r
3.10.0-862.3.2.el7.x86_64
$ hive --version
Hive 2.3.3
Git git://daijymacpro-2.local/Users/daijy/commit/hive -r 8a511e3f79b43d4be41cd231cf5c99e43b248383
Compiled by daijy on Wed Mar 28 16:58:33 PDT 2018
From source with checksum 8873bba6c55a058614e74c0e628ab022
$ hadoop version
Hadoop 2.9.1
Subversion https://github.com/apache/hadoop.git -r e30710aea4e6e55e69372929106cf119af06fd0e
Compiled by root on 2018-04-16T09:33Z
Compiled with protoc 2.5.0
From source with checksum 7d6d2b655115c6cc336d662cc2b919bd
This command was run using /home/vagrant/hadoop-2.9.1/share/hadoop/common/hadoop-common-2.9.1.jar

ARRAY 型について

以前、このブログで ARRAY 型について書いたことがある。 ただ、この記事では SELECT してきた結果から ARRAY を作る方法については扱わなかった。

blog.amedama.jp

上記の記事にあるけど Apache Hive における ARRAY 型というのは、公式にジェイウォークをするための方法という感じ。 使うと一行のレコードに複数行の情報を詰め込むことができる。 Apache Hive はアーキテクチャ的に JOIN の処理が割と苦手なので、こういう機能を使う必要が出てくることもある。

下準備

まずは Hive のシェルを起動しておく。

$ hive

動作確認用のテーブルとして、名前 (name) と性別 (gender) の入るテーブル (users) を作っておく。

hive> CREATE TABLE users (
    >   name STRING,
    >   gender STRING
    > );
OK
Time taken: 0.06 seconds

適当な名前と性別でユーザを追加する。

hive> INSERT INTO TABLE users
    > VALUES
    >   ('alice', 'female'),
    >   ('bob', 'male'),
    >   ('carol', 'female');
...
OK
Time taken: 19.046 seconds

これで準備ができた。

SELECT した結果から ARRAY を作ってみる

ユーザを性別 (gender) で集計した上で、名前 (name) も一緒に確認したいという状況で考えてみよう。 この場合、性別と一緒に名前が ARRAY で得られると嬉しい。

この場合 GROUP BY に gender を指定した上で COLLECT_LIST() を使う。

hive> SELECT
    >   gender,
    >   COLLECT_LIST(name) AS names
    > FROM users
    > GROUP BY gender;
...
OK
female  ["alice","carol"]
male    ["bob"]
Time taken: 25.689 seconds, Fetched: 2 row(s)

見事、性別とそれに該当する名前が ARRAY で得られた。

ちなみに、COLLECT_LIST() から得られる結果は重複を許す。 もし、重複を許さない結果がほしいときは COLLECT_SET() を使えば良い。

プログラミング Hive

プログラミング Hive

  • 作者: Edward Capriolo,Dean Wampler,Jason Rutherglen,佐藤直生,嶋内翔,Sky株式会社玉川竜司
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2013/06/15
  • メディア: 大型本
  • この商品を含むブログ (3件) を見る

Python: pandas と Google BigQuery を連携させる

ぶっちゃけ pandas は大規模なデータセットを扱うのが苦手だ。 だいたい一桁 GB なら我慢と工夫で何とかなるけど、二桁 GB を超えると現実的な処理時間で捌けなくなってくる。 そこで、今回は pandas を Google BigQuery と連携させることで重たい処理をオフロードする方法を試してみる。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V         
Python 3.6.5
$ pip list --format=columns | grep -i pandas
pandas                   0.23.3
pandas-gbq               0.5.0

Google BigQuery を使う下準備

ここから BigQuery を使うための下準備が結構長いので、既に使ったことがある人は読み飛ばしてもらって構わない。

まずは Homebrew Cask を使って GCP を操作するためのコマンドラインツールをインストールしておく。

$ brew cask install google-cloud-sdk

インストールしたら自分の GCP アカウントで認証する。

$ gcloud init

認証ができたら pandas との連携を試すためのプロジェクトを新しく作っておこう。

$ gcloud projects create pandas-bq

プロジェクトを作ったら支払いの設定を有効にする。

console.cloud.google.com

続いてプロジェクトにデータセットとテーブルを作る。

$ bq mk pandas-bq:example
Dataset 'pandas-bq:example' successfully created.
$ bq mk pandas-bq:example.iris
Table 'pandas-bq:example.iris' successfully created.

名前から分かる通り、みんな大好き Iris データセットを読み込む。

UCI のリポジトリから Iris データセットをダウンロードする。

$ wget https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data

上記からダウンロードしたデータセットには末尾に空行が入っているので取り除く。

$ brew install gnu-sed
$ alias sed="gsed"
$ sed -i -e "/^$/d" iris.data

空行が入っていると bq コマンドが無理やり内容を解釈しようとするので。

続いてテーブルのスキーマを用意する。

$ cat << 'EOF' > schema.json
[
  {"name": "sepal_length", "type": "float", "mode": "required"},
  {"name": "sepal_width", "type": "float", "mode": "required"},
  {"name": "petal_length", "type": "float", "mode": "required"},
  {"name": "petal_width", "type": "float", "mode": "required"},
  {"name": "label", "type": "string", "mode": "required"}
]
EOF

あとはデータをアップロードする。

$ bq load --replace --project_id pandas-bq example.iris iris.data schema.json

これで、次の通りデータが読み込まれた。

$ bq show pandas-bq:example.iris
Table pandas-bq:example.iris

   Last modified                 Schema                 Total Rows   Total Bytes   Expiration   Time Partitioning   Labels  
 ----------------- ----------------------------------- ------------ ------------- ------------ ------------------- -------- 
  05 Aug 20:50:57   |- sepal_length: float (required)   150          7100                                                   
                    |- sepal_width: float (required)                                                                        
                    |- petal_length: float (required)                                                                       
                    |- petal_width: float (required)                                                                        
                    |- label: string (required)                                                                             
                                                                
$ bq query --project_id pandas-bq "SELECT * FROM example.iris LIMIT 10"
Waiting on bqjob_r457fdda779482a15_0000016509ed844f_1 ... (0s) Current status: DONE   
+--------------+-------------+--------------+-------------+-----------------+
| sepal_length | sepal_width | petal_length | petal_width |      label      |
+--------------+-------------+--------------+-------------+-----------------+
|          5.1 |         2.5 |          3.0 |         1.1 | Iris-versicolor |
|          5.0 |         2.0 |          3.5 |         1.0 | Iris-versicolor |
|          5.7 |         2.6 |          3.5 |         1.0 | Iris-versicolor |
|          6.0 |         2.2 |          4.0 |         1.0 | Iris-versicolor |
|          5.8 |         2.6 |          4.0 |         1.2 | Iris-versicolor |
|          5.5 |         2.3 |          4.0 |         1.3 | Iris-versicolor |
|          6.1 |         2.8 |          4.0 |         1.3 | Iris-versicolor |
|          5.5 |         2.5 |          4.0 |         1.3 | Iris-versicolor |
|          6.4 |         3.2 |          4.5 |         1.5 | Iris-versicolor |
|          5.6 |         3.0 |          4.5 |         1.5 | Iris-versicolor |
+--------------+-------------+--------------+-------------+-----------------+

これで Google BigQuery 側の準備は整った。

pandas で BigQuery を使えるようにする下準備

続いて pandas と Google BigQuery を連携させるための準備を進める。

まずは必要なパッケージとして pandas と pandas-gbq をインストールする。

$ pip install pandas pandas-gbq

Python の REPL を起動する。

$ python

pandas をインポートする。

>>> import pandas as pd

これで下準備が整った。

pandas と BigQuery を連携させる

さっき作ったプロジェクトの ID と BigQuery で実行したいクエリを用意する。

>>> project_id = 'pandas-bq'
>>> query = 'SELECT * FROM example.iris LIMIT 10'

クエリは先ほど実行したものと同じ。

あとは pandas.read_gbq() に実行したいクエリとプロジェクトの ID を渡すだけ。 すると認証の URL が表示するので、それをブラウザで開く。 認証が成功すると認証コードが表示されるので、それを REPL に貼り付けよう。

>>> pd.read_gbq(query, project_id, dialect='standard')
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?...
Enter the authorization code: ...
sepal_length  sepal_width       ...         petal_width            label
0           5.1          2.5       ...                 1.1  Iris-versicolor
1           5.0          2.0       ...                 1.0  Iris-versicolor
2           5.7          2.6       ...                 1.0  Iris-versicolor
3           6.0          2.2       ...                 1.0  Iris-versicolor
4           5.8          2.6       ...                 1.2  Iris-versicolor
5           5.5          2.3       ...                 1.3  Iris-versicolor
6           6.1          2.8       ...                 1.3  Iris-versicolor
7           5.5          2.5       ...                 1.3  Iris-versicolor
8           6.4          3.2       ...                 1.5  Iris-versicolor
9           5.6          3.0       ...                 1.5  Iris-versicolor

[10 rows x 5 columns]

これでクエリが実行される。

二回目の実行からは結果がキャッシュされるので、認証について聞かれることはない。

>>> query = 'SELECT COUNT(1) AS count FROM example.iris'
>>> pd.read_gbq(query, project_id, dialect='standard')
   count
0    150

pandas.read_gbq() の結果は DataFrame として得られる。

>>> query = '''
... SELECT
...   AVG(sepal_length) AS sepal_length_avg,
...   AVG(sepal_width) AS sepal_width_avg,
...   AVG(petal_length) AS petal_length_avg,
...   AVG(petal_width) AS petal_width_avg
... FROM example.iris
... '''
>>> new_df = pd.read_gbq(query, project_id, dialect='standard')
>>> type(new_df)
<class 'pandas.core.frame.DataFrame'>
>>> new_df
   sepal_length_avg       ...         petal_width_avg
0          5.843333       ...                1.198667

[1 rows x 4 columns]

DataFrame を BigQuery に書き込む

先ほどの例は BigQuery のテーブルにクエリを発行して結果を読み出すものだった。 今度はローカルの DataFrame の内容を BigQuery に書き出してみる。

サンプルの DataFrame を用意する。

>>> data = [
...   ('alice', 150),
...   ('bob', 160),
...   ('carol', 170),
... ]
>>> df = pd.DataFrame(data, columns=['name', 'height'])

あとは DataFrame#to_gbq() メソッドを実行する。 このときデータセット名、テーブル名、プロジェクト ID を指定する。

>>> df.to_gbq('example.users', project_id)

コマンドラインツールで確認すると、ちゃんとテーブルができてデータが入っていることが分かる。

$ bq show pandas-bq:example.users
Table pandas-bq:example.users

   Last modified          Schema         Total Rows   Total Bytes   Expiration   Time Partitioning   Labels  
 ----------------- -------------------- ------------ ------------- ------------ ------------------- -------- 
  05 Aug 21:05:36   |- name: string      3            43                                                     
                    |- height: integer                                                                       

$ bq query --project_id pandas-bq "SELECT * FROM example.users"
+-------+--------+
| name  | height |
+-------+--------+
| alice |    150 |
| bob   |    160 |
| carol |    170 |
+-------+--------+

ばっちり。

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

  • 作者: Wes McKinney,瀬戸山雅人,小林儀匡,滝口開資
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/07/26
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Ubuntu 18.04 LTS に後から GUI (X Window System) を追加する

Ubuntu 18.04 LTS をサーバ版でインストールするとデスクトップ環境が入らない。 とはいえ後から欲しくなるときもあるので、その方法について。 ちなみに必要なパッケージの名称は Ubuntu 16.04 LTS と同じだった。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
$ uname -r
4.15.0-20-generic

もくじ

下準備

最初に apt-get update をして更新サイトを最新の状態にしておく。

$ sudo apt-get update

デスクトップ環境が必要なとき

デスクトップ環境をインストールするには apt-get installubuntu-desktop を入れるだけ。

$ sudo apt-get -y install ubuntu-desktop

かなり依存パッケージが多いので気長に待つ。

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

$ sudo shutdown -r now

あとは自動的にデスクトップ環境が有効な状態で起動してくる。

f:id:momijiame:20180804225226p:plain

X Window System だけで良いとき

デスクトップ環境は不要で X Window System さえあれば良いときは、インストールするパッケージを xserver-xorg にする。

$ sudo apt-get -y install xserver-xorg

あとは SSH を使って X11 Forwarding が有効な状態でログインすれば、リモートで X のアプリケーションが使えるようになる。

いじょう。

Apache Hive 1.x の INSERT 文の仕様でハマった話

今回は、タイトルの通り Apache Hive の 1.x を使っていたとき INSERT 文の仕様でハマった話。 先に概要を説明しておくと Apache Hive の 1.x と 2.x ではサポートする構文が変わっている。 具体的には 1.x では INSERT INTO ... SELECT 文で後続に FROM ... が必要なんだけど 2.x ではそれが不要になっている。

使った環境は次の通り。 ディストリビューションやマネージドサービスは特に使わずに構築してある。

$ cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core) 
$ uname -r
3.10.0-862.3.2.el7.x86_64
$ hive --version
Hive 1.2.2
Subversion git://vgumashta.local/Users/vgumashta/Documents/workspace/hive-git -r 395368fc6478c7e2a1e84a5a2a8aac45e4399a9e
Compiled by vgumashta on Sun Apr 2 13:12:26 PDT 2017
From source with checksum bd47834e727562aab36c8282f8161030
$ hadoop version
Hadoop 2.9.1
Subversion https://github.com/apache/hadoop.git -r e30710aea4e6e55e69372929106cf119af06fd0e
Compiled by root on 2018-04-16T09:33Z
Compiled with protoc 2.5.0
From source with checksum 7d6d2b655115c6cc336d662cc2b919bd
This command was run using /home/vagrant/hadoop-2.9.1/share/hadoop/common/hadoop-common-2.9.1.jar

まずは Hive のシェルを起動しておく。

$ hive

続いて、動作確認に使うテーブルを定義する。

hive> CREATE TABLE example_table (
    >   message STRING
    > );
OK
Time taken: 0.059 seconds

ここで INSERT INTO ... SELECT 構文を使ってデータを追加してみよう。 すると、以下のようなエラーになってしまう。

hive> INSERT INTO TABLE example_table
    > SELECT "Hello, World";
...
FAILED: ParseException line 2:21 Failed to recognize predicate '<EOF>'. Failed rule: 'regularBody' in statement

上記のエラーメッセージで検索しても「スキーマ定義で予約語を使ってるんじゃない?」みたいな関係ないものしか出てこない。

実際には Apache Hive 1.x の場合 INSERT INTO ... SELECT に続いて FROM がないとエラーになるのが原因。 以下ではサブクエリを使って無意味な FROM を無理やり追加することで解決している。

hive> INSERT INTO TABLE example_table
    > SELECT "Hello, World"
    > FROM (SELECT "dummy") dummy;
...
OK
Time taken: 16.697 seconds

これでちゃんとレコードが追加できた。

hive> SELECT * FROM example_table;
OK
Hello, World
Time taken: 0.059 seconds, Fetched: 1 row(s)

ちなみに INSERT INTO ... VALUES を使えば FROM なしでレコード追加ができる。

hive> INSERT INTO TABLE example_table
    > VALUES ("Hello, World 2");
...
OK
Time taken: 17.4 seconds
hive> SELECT * FROM example_table;
OK
Hello, World
Hello, World 2
Time taken: 0.034 seconds, Fetched: 2 row(s)

じゃあ INSERT INTO ... SELECT なんか使わなければいいじゃん!ってなるんだけど、そうもいかない場合がある。 まず、一つ目の理由は設定か何かで解決できると思うんだけど INSERT INTO ... VALUES を使うとテンポラリなテーブルが作られる。

hive> SHOW TABLES;
OK
example_table
values__tmp__table__1
Time taken: 0.008 seconds, Fetched: 2 row(s)

中身はレコードを追加するときに使って VALUES 以降の内容になっている。

hive> SELECT * FROM values__tmp__table__1;
OK
Hello, World 2
Time taken: 0.031 seconds, Fetched: 1 row(s)

まあ上記は良いとして問題がもう一つあって。 複合型 (STRUCT や ARRAY など) を使おうとすると INSERT INTO ... VALUES だとレコードが追加できない。

hive> CREATE TABLE example_table_with_array (
    >   messages ARRAY<STRING>
    > );
OK
Time taken: 0.032 seconds

複合型が含まれるレコードを INSERT INTO ... VALUES で追加しようとすると、次のようなエラーになる。

hive> INSERT INTO TABLE example_table_with_array
    > VALUES (ARRAY("Message1", "Message2"));
FAILED: SemanticException [Error 10293]: Unable to create temp file for insert values Expression of type TOK_FUNCTION not supported in insert/values

INSERT INTO ... SELECT ならエラーにならない。

hive> INSERT INTO TABLE example_table_with_array
    > SELECT ARRAY("Message1", "Message2")
    > FROM (SELECT "dummy") dummy;
...
OK
Time taken: 17.318 seconds
hive> SELECT * FROM example_table_with_array;
OK
["Message1","Message2"]
Time taken: 0.072 seconds, Fetched: 1 row(s)

なので INSERT INTO ... SELECT を使うことになる。

Apache Hive 2.x の場合

ちなみに最初に記述した通り Apache Hive 2.x では FROM をつけなくても良くなっている。 実際に環境を用意して試してみよう。

$ hive --version
Hive 2.3.3
Git git://daijymacpro-2.local/Users/daijy/commit/hive -r 8a511e3f79b43d4be41cd231cf5c99e43b248383
Compiled by daijy on Wed Mar 28 16:58:33 PDT 2018
From source with checksum 8873bba6c55a058614e74c0e628ab022
$ hadoop version
Hadoop 2.9.1
Subversion https://github.com/apache/hadoop.git -r e30710aea4e6e55e69372929106cf119af06fd0e
Compiled by root on 2018-04-16T09:33Z
Compiled with protoc 2.5.0
From source with checksum 7d6d2b655115c6cc336d662cc2b919bd
This command was run using /home/vagrant/hadoop-2.9.1/share/hadoop/common/hadoop-common-2.9.1.jar

複合型が含まれるテーブルを定義する。

hive> CREATE TABLE example_table_with_array (
    >   messages ARRAY<STRING>
    > );
OK
Time taken: 8.459 seconds

Apache Hive 1.x ではエラーになった INSERT INTO ... SELECT を発行してみよう。

hive> INSERT INTO TABLE example_table_with_array
    > SELECT ARRAY("Message1", "Message2");
...
OK
Time taken: 28.527 seconds

今度はエラーにならない。

ちゃんとレコードも追加されている。

hive> SELECT * FROM example_table_with_array;
OK
["Message1","Message2"]
Time taken: 0.223 seconds, Fetched: 1 row(s)

いじょう。

ビッグデータ分析・活用のためのSQLレシピ

ビッグデータ分析・活用のためのSQLレシピ

プログラミング Hive

プログラミング Hive

  • 作者: Edward Capriolo,Dean Wampler,Jason Rutherglen,佐藤直生,嶋内翔,Sky株式会社玉川竜司
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2013/06/15
  • メディア: 大型本
  • この商品を含むブログ (3件) を見る