CUBE SUGAR CONTAINER

技術系のこと書きます。

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件) を見る

Python: pandas の DataFrame, Series, Index を拡張する

Python でデータ分析をするときに、ほぼ必ずといって良いほど使われるパッケージとして pandas がある。 そのままでも便利な pandas だけど、代表的なオブジェクトの DataFrame, Series, Index には実は独自の拡張を加えることもできる。 これがなかなか面白いので、今回はその機能について紹介してみる。

ただし、あらかじめ断っておくと注意点もある。 独自の拡張を加えると、本来は存在しないメソッドやプロパティがオブジェクトに生えることになる。 そのため、便利だからといってこの機能を使いすぎると、コードの可読性が低下する恐れもある。 使うなら、後から別の人がコードを読むときにも困らないようにしたい。 具体的には、使用するにしても最小限に留めたり、あるいはパッケージ化やドキュメント化をしておくことが挙げられる。

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

$ 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 をインストールしておこう。

$ pip install pandas

ここからは Python の REPL を使って解説していく。

$ python

DataFrame を拡張する

まずは一番よく使うであろう DataFrame の拡張方法から。 あんまり実用的な例じゃないけど、ひとまず API がどんな感じになってるかを紹介したいので。

pandas のオブジェクトを拡張するときは、基本的に pandas.api.extensions 以下にある API を用いる。 例えば DataFrame を拡張するなら @pandas.api.extensions.register_dataframe_accessor() デコレータでクラスを修飾する。 次のサンプルコードでは DataFrame に helloworld という名前空間で greet() メソッドと length プロパティを追加している。

>>> import pandas as pd
>>> 
>>> # "helloworld" という名前空間で DataFrame を拡張する
... @pd.api.extensions.register_dataframe_accessor('helloworld')
... class HelloWorldDataFrameAccessor(object):
...     """DataFrameを拡張するためのクラス"""
...     def __init__(self, df):
...         self._df = df
...     # DataFrame#helloworld に greet() メソッドを追加する
...     def greet(self):
...         """標準出力にメッセージを出す"""
...         print('Hello, World!')
...     # DataFrame#helloworld に length プロパティを追加する
...     @property
...     def length(self):
...         """DataFrameの長さを返す"""
...         return len(self._df)
... 

これだけで DataFrame の拡張ができる。

実際に DataFrame のインスタンスを作って、上記の動作を確認してみよう。

>>> df = pd.DataFrame(list(range(1, 11)), columns=['n'])
>>> df
    n
0   1
1   2
2   3
3   4
4   5
5   6
6   7
7   8
8   9
9  10

特に意味はないけど DataFrame#helloworld#greet() メソッドを実行すると標準出力にメッセージが出るようになる。

>>> df.helloworld.greet()
Hello, World!

あとは DataFrame#helloworld#length プロパティを参照すると DataFrame の長さが得られるようになる。

>>> df.helloworld.length
10

たしかに DataFrame に自分で拡張したメソッドやプロパティを生やすことができた。

Series を拡張する

続いては Series の拡張方法を紹介する。 基本的にやることは先ほどと同じなので、次はもうちょっと実用的な例を紹介してみる。

例えば Series をマルチプロセスで並列に処理したい、というシチュエーションを考えてみよう。 使ってるマシンの CPU コアがたくさんあって、扱うデータセットが大きいときは結構やりたくなるんじゃないかな? 典型的には、次のような高階関数を用意するはず。

>>> import multiprocessing as mp
>>> import numpy as np
>>> 
>>> def parallelize(f, data, n_jobs=None):
...     """関数の適用をマルチプロセスで処理する"""
...     if n_jobs is None:
...         # 並列度の指定がなければ CPU のコア数を用いる
...         n_jobs = mp.cpu_count()
...     # データを並列度の数で分割する
...     split_data = np.array_split(data, n_jobs)
...     # プロセスプールを用意する
...     with mp.Pool(n_jobs) as pool:
...         # 各プロセスで関数を適用した結果を結合して返す
...         return pd.concat(pool.map(f, split_data))
... 

続いて、マルチプロセスで適用したい関数を適当に用意する。

>>> def square(x):
...     return x * x
... 

そして、こんな感じで使う。

>>> parallelize(square, df.n)
0      1
1      4
2      9
3     16
4     25
5     36
6     49
7     64
8     81
9    100
Name: n, dtype: int64

先ほどの使い方でも構わないんだけど pandas のオブジェクトから直接呼び出せると便利そうなので拡張してみよう。 次のようにして Series に parallel という名前空間で apply() メソッドを追加する。

>>> # "parallel" という名前空間で Series を拡張する
... @pd.api.extensions.register_series_accessor('parallel')
... class ParallelSeriesAccessor(object):
...     """Seriesを拡張するためのクラス"""
...     def __init__(self, s):
...         self._s = s
...     # Series#parallel に apply() というメソッドを定義する
...     def apply(self, f):
...         """Series に対して関数を並列で適用する"""
...         return parallelize(f, self._s)
... 

すると Series#parallel#apply() メソッドが使えるようになる。

>>> df.n.parallel.apply(square)
0      1
1      4
2      9
3     16
4     25
5     36
6     49
7     64
8     81
9    100
Name: n, dtype: int64

呼び出し方が違うだけで、やっていることは先ほどと変わらない。

Index を拡張する

続いては Index を拡張してみよう。

以下のサンプルコードでは Index が整数という前提で偶数・奇数だけを抜き出す機能を追加している。 また、あんまり実用性がない例になっちゃった。

>>> # "sampling" という名前空間で Index を拡張する
... @pd.api.extensions.register_index_accessor('sampling')
... class SamplingIndexAccessor(object):
...     """Indexを拡張するためのクラス"""
...     def __init__(self, idx):
...         self._idx = idx
...     # Index#sampling に even というプロパティを定義する
...     @property
...     def even(self):
...         return self._idx[self._idx % 2 == 0]
...     # Index#sampling に odd というプロパティを定義する
...     @property
...     def odd(self):
...         return self._idx[self._idx % 2 != 0]
... 

早速試してみよう。

>>> df.index.sampling.even
Int64Index([0, 2, 4, 6, 8], dtype='int64')
>>> df.index.sampling.odd
Int64Index([1, 3, 5, 7, 9], dtype='int64')

ちゃんと偶数・奇数だけ取り出すことができた。

めでたしめでたし。

参考

Extending Pandas — pandas 0.23.3 documentation

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

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

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

Python: gzip モジュールを使ってデータを圧縮・解凍する

今回は Python の標準ライブラリの gzip モジュールの使い方について。 上手く使えば Python から大きなデータを扱うときにディスクの節約になるかな。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V
Python 3.6.5

まずは Python の REPL を起動しておく。

$ python

基本的な使い方

gzip モジュールの基本的な使い方としては、組み込み関数の open() っぽい使い勝手の gzip.open() 関数を使う。 この関数を通して得られたファイルライクオブジェクトに書き込むと、自動的に書き込んだデータが GZip で圧縮される。

試しに、実際にデータを書き込んでみよう。

>>> import gzip
>>> with gzip.open('example.txt.gz', mode='wt') as fp:
...     fp.write('Hello, World!\n')
... 
14

注意点としては、テキストデータ (ユニコード文字列) を扱うときは mode 引数に t を明示的に指定しなきゃいけない。 これは gzip.open() 関数がデフォルトではバイナリデータを扱うように作られているため。 明示的に t を指定しないとバイナリモードになる。 Python 3 における組み込みの open() 関数はテキストモードがデフォルトなので、ここは気をつける必要がある。

別のターミナルからファイルを確認すると、ちゃんと GZip 圧縮されたファイルができている。

$ file example.txt.gz           
example.txt.gz: gzip compressed data, was "example.txt", last modified: Wed Aug  1 13:23:58 2018, max compression

gzcat コマンドで内容を確認すると、ちゃんと書き込んだ内容が見える。

$ gzcat example.txt.gz 
Hello, World!

元の Python インタプリタに戻って、今度は読み込みをしてみよう。

>>> with gzip.open('example.txt.gz', mode='rt') as fp:
...     fp.read()
... 
'Hello, World!\n'

ちゃんと元の文字列が解凍できた。

gzip コマンドで圧縮したファイルからデータを読み出してみる

続いては Python 以外のアーカイバを使って圧縮したファイルを解凍できるか試してみよう。

gzip コマンドを使って圧縮したファイルを用意しておく。

$ echo "Hello, GZip" > greet.txt
$ gzip greet.txt
$ file greet.txt.gz  
greet.txt.gz: gzip compressed data, was "greet.txt", last modified: Wed Aug  1 13:27:00 2018, from Unix

先ほどと同じようにファイルからデータを読み込んでみよう。

>>> with gzip.open('greet.txt.gz', mode='rt') as fp:
...     fp.read()
... 
'Hello, GZip\n'

ちゃんと読み出せた。

ただし、公式ドキュメントを読むとサポートしていない形式もあるようだ。

13.2. gzip — gzip ファイルのサポート — Python 3.6.5 ドキュメント

バイナリデータを扱ってみる

登場機会として多そうなのは pickle モジュールとの組み合わせかな。 これも試してみよう。

pickle モジュールについては以下の記事で取り扱った。

blog.amedama.jp

以下の辞書データを GZip ファイルとして保存したい。

>>> d = {'message': 'Hello, World!'}

そこで、まずは pickle モジュールを使って、上記をバイト列に変換する。

>>> import pickle
>>> data = pickle.dumps(d)

こんな感じになった。

>>> data
b'\x80\x03}q\x00X\x07\x00\x00\x00messageq\x01X\r\x00\x00\x00Hello, World!q\x02s.'

上記のバイト列を gzip モジュール経由でファイルに書き込む。

>>> with gzip.open('dict.pickle.gz', mode='wb') as fp:
...     fp.write(data)
... 
41

別のターミナルから確認すると、ちゃんと GZip ファイルができている。

$ file dict.pickle.gz 
dict.pickle.gz: gzip compressed data, was "dict.pickle", last modified: Wed Aug  1 13:28:15 2018, max compression

書き込みはできたので、今度は読み込みを。

>>> with gzip.open('dict.pickle.gz', mode='rb') as fp:
...     data = fp.read()
... 
>>> data
b'\x80\x03}q\x00X\x07\x00\x00\x00messageq\x01X\r\x00\x00\x00Hello, World!q\x02s.'

さっきと同じバイト列が得られた。

pickle モジュールに読み込ませると、ちゃんと辞書データが元に戻せた。

>>> pickle.loads(data)
{'message': 'Hello, World!'}

いじょう。