CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: gensim の FAST_VERSION 定数の意味について

Python の gensim には自然言語処理 (NLP) に関する様々な実装がある。 そして、その中のいくつかのモジュールには FAST_VERSION という定数が定義されている。 この定数は環境によって異なる値を取って、値によってパフォーマンスが大きく異なる場合がある。

今回は、この数値が何を表しているかについて調べた。 結論から先に述べると、この定数は次のような対応関係にある。

  • -1
    • Cython で書かれた拡張モジュールが使えない
  • 0
    • Cython で書かれた拡張モジュールが使える
    • BLAS のドット積を計算する関数 (dsdot()) が倍精度浮動小数点型 (double) を返す
  • 1
    • Cython で書かれた拡張モジュールが使える
    • BLAS のドット積を計算する関数 (dsdot()) が単精度浮動小数点型 (float) を返す
  • 2
    • Cython で書かれた拡張モジュールが使える
    • BLAS のドット積を計算する関数 (dsdot()) が使えない
      • 代わりに Cython でループする

なお、パフォーマンス的には 0 > 1 > 2 > -1 だと考えられる。 また、これはあくまで「現時点で既知の値に関しては」なので、将来的に変わったり異なるモジュールは異なる意味になる可能性がある。

使った環境は次のとおり。

$ sw_vers               
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V                                                                       
Python 3.7.7
$ pip list | grep -i gensim                                           
gensim          3.8.3

下準備

とりあえず gensim をインストールしておく。

$ pip install gensim

gensim の FAST_VERSION について

たとえば、Word2Vec や fastText のモジュールに FAST_VERSION という定数が定義されている。 Word2Vec のものについて、定数の値を追ってみよう。 はじめに次の場所で、特定のモジュールがインポートできないときに -1 を取る。

gensim/word2vec.py at 3.8.3 · RaRe-Technologies/gensim · GitHub

インポートしようとしたモジュールでは Cython の拡張モジュールが使われている。 定数を初期化しているのは以下の関数で、それぞれの意味について記述されている。

gensim/word2vec_inner.pyx at 3.8.3 · RaRe-Technologies/gensim · GitHub

意味は前述したとおりで、0 または 1 なら Cython の拡張モジュールで BLAS の dsdot() 関数が使える。 2 のときは使えないので、代わりにドット積を計算するのに Cython のループを使うことになる。

Cython の拡張モジュールが使えない場合 (FAST_VERSION == -1) のパフォーマンス

試しに、Cython の拡張モジュールが使えない環境を用意した。

$ python -c "import gensim;print(gensim.models.word2vec.FAST_VERSION)" 2>/dev/null
-1
$ python -c "import gensim;print(gensim.models.fasttext.FAST_VERSION)" 2>/dev/null
-1

Text8 コーパスを使って Word2Vec を学習させてみよう。 はじめに、コーパスをダウンロードして展開する。

$ wget http://mattmahoney.net/dc/text8.zip
$ unzip text8.zip

次のようなベンチマーク用のモジュールを用意する。

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

import logging

from gensim.models.word2vec import Text8Corpus
from gensim.models import Word2Vec


def main():
    # 学習の過程をログに残す
    log_format = "%(threadName)s - %(name)s - %(levelname)s - %(message)s"
    logging.basicConfig(format=log_format, level=logging.INFO)

    # Text8 コーパスを Word2Vec で学習する
    corpus = Text8Corpus('text8')
    Word2Vec(
        corpus,
        sg=1,  # Skip-gram タスクを最適化する
        hs=1,  # 損失の計算に Hierarchical Softmax を使う
        negative=5,  # ポジティブサンプルに対するネガティブサンプル (ノイジーワード) の比率
        size=50,  # 埋め込む次元数
        iter=3,  # 学習エポック数
    )


if __name__ == "__main__":
    main()

上記に適用な名前をつけたら実行してみよう。 なお、実行が完了まで見守る必要はない。 学習時に出力されるスループットさえ確認できれば良い。

$ python benchmark.py 
unable to import 'smart_open.gcs', disabling that module
MainThread - gensim.models.base_any2vec - WARNING - consider setting layer size to a multiple of 4 for greater performance
/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/gensim/models/base_any2vec.py:743: UserWarning: C extension not loaded, training will be slow. Install a C compiler and reinstall gensim for fast training.
  "C extension not loaded, training will be slow. "
MainThread - gensim.models.word2vec - INFO - collecting all words and their counts
MainThread - gensim.models.word2vec - INFO - PROGRESS: at sentence #0, processed 0 words, keeping 0 word types
...
MainThread - gensim.models.base_any2vec - INFO - training model with 3 workers on 71290 vocabulary and 50 features, using sg=1 hs=1 sample=0.001 negative=5 window=5
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 0.06% examples, 151 words/s, in_qsize 5, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 0.18% examples, 447 words/s, in_qsize 6, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 0.24% examples, 309 words/s, in_qsize 5, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 0.29% examples, 379 words/s, in_qsize 5, out_qsize 0

上記を見ると、だいたい 300 ~ 400 words/s くらいかな? ちなみに、実行直後には「C 拡張が使えないので遅くなるよ」という旨の警告が表示されている。

Cython の拡張モジュールが使える場合 (FAST_VERSION == 0) のパフォーマンス

続いては拡張モジュールが使える場合で、特に BLAS の dsdot() 関数が倍精度浮動小数点型 (double) を返す環境を用意した。

$ python -c "import gensim;print(gensim.models.word2vec.FAST_VERSION)" 2>/dev/null
0

この環境は、たとえば C コンパイラや OpenBLAS が使える環境でソースコードから gensim をビルドすればできると思う。たぶん。

$ xcode-select --install
$ brew install openblas
$ pip install -U --no-binary gensim gensim

この環境でも、先ほどのベンチマーク用の実行してみよう。

$ python benchmark.py                                                             
unable to import 'smart_open.gcs', disabling that module
MainThread - gensim.models.base_any2vec - WARNING - consider setting layer size to a multiple of 4 for greater performance
MainThread - gensim.models.word2vec - INFO - collecting all words and their counts
MainThread - gensim.models.word2vec - INFO - PROGRESS: at sentence #0, processed 0 words, keeping 0 word types
...
MainThread - gensim.models.base_any2vec - INFO - training model with 3 workers on 71290 vocabulary and 50 features, using sg=1 hs=1 sample=0.001 negative=5 window=5
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 1.47% examples, 177603 words/s, in_qsize 5, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 3.06% examples, 187492 words/s, in_qsize 5, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 4.64% examples, 189657 words/s, in_qsize 5, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 6.23% examples, 190525 words/s, in_qsize 5, out_qsize 0
...

すると、こちらの環境では 180k ~ 190k words/s のスループットが出ている。 Cython の拡張モジュールを使う場合と使わない場合で、だいたい 500 倍の違いが出た。

おまけ: Word2Vec や fastText の学習時に CPU の論理コアを使い切る

ちなみに現行の Word2Vec や fastText を学習させるときはワーカープロセスの数が 3 で固定されている。 そのため CPU のコア数が多い環境では、そこが学習のボトルネックになる。

CPU のコアを使い切りたいときは、multiprocessing モジュールをインポートして...

import multiprocessing

Word2Vec や FastText の引数として workers に環境の論理コア数を指定する。

    workers=multiprocessing.cpu_count(),  # 論理コア数分のワーカープロセスを使って学習する

同じようにベンチマークを実行してみよう。

$ python benchmark.py
...
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 2.41% examples, 294657 words/s, in_qsize 16, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 5.11% examples, 312823 words/s, in_qsize 15, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 8.00% examples, 323193 words/s, in_qsize 15, out_qsize 0
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 10.58% examples, 321791 words/s, in_qsize 15, out_qsize 1
MainThread - gensim.models.base_any2vec - INFO - EPOCH 1 - PROGRESS: at 13.35% examples, 324695 words/s, in_qsize 15, out_qsize 0
...

今回の環境は CPU の論理コアが 8 あるので、さらにスループットを上げることができた。

めでたしめでたし。

Python: gensim を使った Word Embedding の内省的評価について

以下の書籍では、Word Embedding の評価方法として内省的評価 (intrinsic evaluation) と外省的評価 (extrinsic evaluation) という 2 つのやり方が紹介されている。 内省的評価では、人間が判断した単語間の類似度や、単語の持つ意味を使ったアナロジーを、Word Embedding が適切に表現できているかを評価する。 それに対し、外省的評価では Word Embedding を応用する先となる最終的な目的を表したタスクを使って評価する。

今回は、gensim に用意されている内省的評価の仕組みがどんなことをやっているのか気になって調べた内容を書いてみる。

使った環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V
Python 3.7.7

下準備

はじめに、gensim をインストールしておく。

$ pip install gensim

続いて、評価したい Pretrained Word Embeddings を用意する。 今回は Facebook の公開している fastText を選んだ。 なお、評価用データとして gensim に組み込みで用意されている英語のコーパスを使うので、ひとまず英語を使ったものにする。 このファイルはサイズが 6GB ほどあるので結構な時間がかかる。

$ wget https://dl.fbaipublicfiles.com/fasttext/vectors-wiki/wiki.en.vec

ダウンロードできたら Python のインタプリタを起動しておく。

$ python

先ほどダウンロードした Pretrained Word Embeddings を gensim から読み込む。

>>> import gensim
>>> model = gensim.models.KeyedVectors.load_word2vec_format('wiki.en.vec', binary=False)

ちなみに、この操作にもかなりの時間 (数分以上) がかかるので気長に待つ。

内省的評価に使うデータのフォーマットについて

gensim には、内省的評価をするための API として単語間類似度を使ったものとアナロジータスクがサポートされている。 そして、英語のコーパスに関してはラベル付きデータも組み込みで提供されている。 ひとまず、どういったフォーマットになっているか紹介しておく。

単語間類似度

はじめに単語間類似度について。 単語間類似度は、2 つの単語について、どれだけ類似しているかを人間の主観で評価したもの。 単語間類似度を評価するためのデータとして、gensim では wordsim353.tsv というデータが組み込みで提供されている。

ファイルの場所は次のようにして gensim.test.utils.datapath() を使って得られる。

$ python -c "from gensim.test.utils import datapath; print(datapath('wordsim353.tsv'))" 2>/dev/null
/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/gensim/test/test_data/wordsim353.tsv

先頭について確認すると、こんな感じ。 タブ区切りで、ある単語と別の単語の類似度が浮動小数点で記述されている。 シャープ (#) からはじまる行は、単なるコメントなので読み飛ばして構わない。

$ head $(python -c "from gensim.test.utils import datapath; print(datapath('wordsim353.tsv'))" 2>/dev/null)
# The WordSimilarity-353 Test Collection (http://www.cs.technion.ac.il/~gabr/resources/data/wordsim353/)
# Word 1   Word 2  Human (mean)
love    sex 6.77
tiger   cat 7.35
tiger   tiger   10.00
book    paper   7.46
computer    keyboard    7.62
computer    internet    7.58
plane   car 5.77
train   car 6.31

アナロジータスク

もうひとつのアナロジータスクでは、特定の単語の分散表現に別の単語を足したり引いたりして目当ての単語となるかどうかを評価する。 アナロジータスクを評価するためのデータとして、gensim では questions-words.txt というデータが組み込みで提供されている。

先頭について確認すると以下のとおり。 基本的に、スペース区切りで 4 つの単語が並んでいる。 1 列目の単語から 2 列目の単語を引いて、3 列目の単語を足したとき 4 列目の単語になるかを評価することになる。 先頭のコロン (:) からはじまる行については、タスクのジャンルを表している。 ちなみに、タスクのジャンルのことはセクションと呼ぶようだ。

$ head $(python -c "from gensim.test.utils import datapath; print(datapath('questions-words.txt'))" 2>/dev/null)
: capital-common-countries
Athens Greece Baghdad Iraq
Athens Greece Bangkok Thailand
Athens Greece Beijing China
Athens Greece Berlin Germany
Athens Greece Bern Switzerland
Athens Greece Cairo Egypt
Athens Greece Canberra Australia
Athens Greece Hanoi Vietnam
Athens Greece Havana Cuba

単語間類似度を使った評価

それでは、実際に単語間類似度を使った評価を試してみよう。 単語間類似度は、WordEmbeddingsKeyedVectors#evaluate_word_pairs() を使って評価する。

>>> from gensim.test.utils import datapath
>>> similarities = model.evaluate_word_pairs(datapath('wordsim353.tsv'))

得られる結果はタプルで、最初の要素がピアソンの相関係数になっている。 中身もまたタプルになってるけど、先頭が相関係数で二番目は「相関がないこと」を帰無仮説とした仮説検定の p-value らしい。

>>> similarities[0]
(0.6987675497727828, 5.237592231656601e-53)

次の要素はスピアマンの相関係数で、実装には scipy を使っているようだ。

>>> similarities[1]
SpearmanrResult(correlation=0.7388081960366618, pvalue=3.98104844873057e-62)

評価用のデータに記述された単語間の類似度を表す点数と、Word Embedding が出した単語ベクトル間のコサイン類似度の相関が高いほど、より優れていると評価することになる。

アナロジータスクを使った評価

続いてアナロジータスクを使った評価を試してみる。 アナロジータスクは WordEmbeddingsKeyedVectors#evaluate_word_analogies() を使って評価する。

>>> analogy_scores = model.evaluate_word_analogies(datapath('questions-words.txt'))

上記では、前述したとおり 1 列目の単語から 2 列目の単語を引いて、3 列目の単語を出した結果が 4 列目の単語になるか評価している。 ただし、ピンポイントで一致しなくともコーパスの中でベクトルが最も似ている TOP5 の中にさえ入っていれば正解としているようだ。

メソッドの返り値として得られるのは、こちらもタプルとなっている。 最初の要素は、正解ラベルが TOP5 に入ったか否かの二値で評価した Accuracy となっている。 この値が高いほど、より優れた Word Embedding と捉えることになる。

>>> analogy_scores[0]
0.7492042304138001

次の要素はリストで、これはアナロジータスクをセクションごとに正解したデータと不正解したデータで分けたもの。 questions-words.txt は 15 のセクションに分かれているらしい。

>>> type(analogy_scores[1])
<class 'list'>
>>> len(analogy_scores[1])
15

それぞれの要素は辞書になっている。

>>> analogy_scores[1][0].keys()
dict_keys(['section', 'correct', 'incorrect'])

内容を確認すると、セクションの名前や正解したタスク、正解できなかったタスクが入っている。

>>> analogy_scores[1][0]['section']
'capital-common-countries'
>>> from pprint import pprint
>>> pprint(analogy_scores[1][0]['correct'][:10])
[('ATHENS', 'GREECE', 'BAGHDAD', 'IRAQ'),
 ('ATHENS', 'GREECE', 'BANGKOK', 'THAILAND'),
 ('ATHENS', 'GREECE', 'BEIJING', 'CHINA'),
 ('ATHENS', 'GREECE', 'BERLIN', 'GERMANY'),
 ('ATHENS', 'GREECE', 'BERN', 'SWITZERLAND'),
 ('ATHENS', 'GREECE', 'CAIRO', 'EGYPT'),
 ('ATHENS', 'GREECE', 'CANBERRA', 'AUSTRALIA'),
 ('ATHENS', 'GREECE', 'HANOI', 'VIETNAM'),
 ('ATHENS', 'GREECE', 'HAVANA', 'CUBA'),
 ('ATHENS', 'GREECE', 'HELSINKI', 'FINLAND')]
>>> pprint(analogy_scores[1][0]['incorrect'][:10])
[('ATHENS', 'GREECE', 'LONDON', 'ENGLAND'),
 ('BAGHDAD', 'IRAQ', 'CANBERRA', 'AUSTRALIA'),
 ('BAGHDAD', 'IRAQ', 'LONDON', 'ENGLAND'),
 ('BANGKOK', 'THAILAND', 'LONDON', 'ENGLAND'),
 ('BEIJING', 'CHINA', 'LONDON', 'ENGLAND'),
 ('BERN', 'SWITZERLAND', 'LONDON', 'ENGLAND'),
 ('CAIRO', 'EGYPT', 'LONDON', 'ENGLAND'),
 ('CANBERRA', 'AUSTRALIA', 'LONDON', 'ENGLAND'),
 ('HANOI', 'VIETNAM', 'LONDON', 'ENGLAND'),
 ('HANOI', 'VIETNAM', 'BERLIN', 'GERMANY')]

最初の要素で示されている Accuracy の値を、次の要素を使って検算してみよう。

>>> from itertools import chain
>>> correct = len(list(chain.from_iterable(s['correct'] for s in analogy_scores[1])))
>>> incorrect = len(list(chain.from_iterable(s['incorrect'] for s in analogy_scores[1])))
>>> correct / (correct + incorrect)
0.7492042304138001

上記のとおり、ちゃんと値が一致した。

いじょう。

Python: 使わない変数を "_" (アンダースコア) に代入するイディオム

Python には、使わない変数であることを明確に示すためにアンダースコアに代入するというイディオムがある。 今回は、そのイディオムについてあらためて紹介してみる。

使った環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V     
Python 3.7.7

イディオムを使わない場合

たとえば、以下のサンプルコードでは、関数 f() がタプルを返す。 この関数の返り値のうち、最初の要素は使わない場合を考えてみよう。 イディオムを使わない場合、添字を使って取り出すことになる。

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


def f():
    """タプルを返す関数"""
    return ('foo', 'bar')


def main():
    # 関数を呼び出す
    ret = f()
    # 必要な要素を添字で取り出す
    i_want_this_one = ret[1]
    # 取り出した変数を使って何かする
    ...


if __name__ == '__main__':
    main()

イディオムを使う場合

一方で、イディオムを使ったサンプルコードが次のとおり。 タプルを展開しつつ、使わない要素に関してはアンダースコアに代入している。 このイディオムを使うことで、ソースコードを読む相手に「この要素は処理で使わない」ことを明確に示すことができる。

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


def f():
    """タプルを返す関数"""
    return ('foo', 'bar')


def main():
    # 使わない要素はアンダースコアに代入してしまう
    _, i_want_this_one = f()
    # 取り出した変数を使って何かする
    ...


if __name__ == '__main__':
    main()

Python には変数のアクセス制御をするための構文がない。 そのため、慣例的に名前を使ってアクセス範囲を示す文化がある。 アンダースコアは、一般的に外部からアクセスしてほしくないプライベートに相当するオブジェクトの名前の先頭に付与される。 それの応用に近い形で、アンダースコアに使わない変数を代入するイディオムが広まったのだと考えられる。

注意点

ところで、このイディオムは Python の REPL (Read-Eval-Print Loop: 対話的実行環境) では使わない方が良い。 なぜなら、REPL ではアンダースコアが「最後の評価されたオブジェクト」を表す変数になっているため。

実際に動作を確認するために Python の REPL を起動しよう。

$ python

そして、何か適当なオブジェクトを評価させる。 ここでは整数値の 42 という値を使った。

>>> 42
42

それでは、アンダースコアを参照してみよう。 すると、先ほど評価されたオブジェクトが代入されていることがわかる。

>>> _
42

まとめ

  • Python には使わない変数を "_" (アンダースコア) に代入するイディオムがある
  • ソースコードを読む相手に「この要素は使わない」ことを明確に示せる
  • REPL を使うときはアンダースコアが別の用途で使われるため注意が必要になる

MySQL の InnoDB でトランザクション分離レベルの違いを試す

今回は MySQL の InnoDB を使ってトランザクション分離レベル (Transaction Isolation Level) の違いを試してみる。 トランザクション分離レベルは、SQL を実装したシステムの ACID 特性において I (Isolation) に対応する概念となっている。 利用する分離レベルによって、複数のトランザクション間でデータの一貫性に関する振る舞いが変化する。 なお、この概念は ANSI SQL で定義されているもので、MySQL に固有というわけではない。

InnoDB では、次の 4 種類のトランザクション分離レベルがサポートされている。 下にいくほど、より厳格にトランザクションを分離できる一方で、上にいくほど処理のオーバーヘッドは少ない。 言いかえると、トランザクション分離レベルを切り替えることでパフォーマンスと一貫性のバランスを調整できる。 なお、デフォルトでは REPEATABLE READ が用いられる。

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

トランザクション分離レベルを落とすと、複数のトランザクション間で分離が不十分になる現象が生じる。 古典的な定義において、生じる現象には次のようなものがある 1

  • ダーティーリード (Dirty Read)
  • ノンリピータブルリード (Non-repeatable Read / Fuzzy Read)
  • ファントムリード (Phantom Read)

トランザクション分離レベルと現象には、次のような関係性がある。 表で「X」になっている項目は、起こる可能性があることを示す。 ただし、MySQL の InnoDB では、例外的に REPEATABLE READ でもファントムリードが生じない。

ダーティーリード ノンリピータブルリード ファントムリード
READ UNCOMMITTED X X X
READ COMMITTED - X X
REPEATABLE READ - - X 1
SERIALIZABLE - - -

今回、検証に使った環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ mysql --version
mysql  Ver 8.0.19 for osx10.14 on x86_64 (Homebrew)

下準備

まずは Homebrew を使って MySQL をインストールしておく。

$ brew install mysql

インストールしたら、次に MySQL のサービスを開始する。

$ brew services start mysql
$ brew services list | grep mysql   
mysql   started amedama /Users/amedama/Library/LaunchAgents/homebrew.mxcl.mysql.plist

MySQL クライアントを起動して、MySQL サーバにログインする。

$ mysql -u root

ログインできたら、サンプルとして使うデータベースを用意する。

mysql> CREATE DATABASE example;
Query OK, 1 row affected (0.01 sec)

mysql> USE example;
Database changed

続いて、サンプルとして使うテーブルを用意する。

mysql> CREATE TABLE users (
    ->     id INTEGER PRIMARY KEY,
    ->     name VARCHAR(32) NOT NULL
    -> );
Query OK, 0 rows affected (0.02 sec)

mysql> DESC users;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int         | NO   | PRI | NULL    |       |
| name  | varchar(32) | NO   |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

テーブルにレコードを 1 つ追加しておく。

mysql> INSERT INTO users VALUES (1, "Alice");
Query OK, 1 row affected (0.01 sec)

データベースエンジンを確認する

続いて、テーブルで利用されているデータベースエンジンが InnoDB になっていることを確認する。

mysql> SELECT
    ->     TABLE_NAME,
    ->     ENGINE
    -> FROM
    ->     information_schema.TABLES
    -> WHERE
    ->     TABLE_SCHEMA = "example";
+------------+--------+
| TABLE_NAME | ENGINE |
+------------+--------+
| users      | InnoDB |
+------------+--------+
1 row in set (0.00 sec)

もし InnoDB でなければデータベースエンジンを変更しておく。

mysql> ALTER TABLE users ENGINE = "InnoDB";
Query OK, 0 rows affected (0.21 sec)
Records: 0  Duplicates: 0  Warnings: 0

なお、テーブルを作る段階でエンジンを指定することもできる。

トランザクション分離レベルの変更について

トランザクション分離レベルはグローバル変数とセッション変数で管理されている。 一時的に変更するときはセッション変数を変更すれば良い。

mysql> SELECT
    ->     @@global.transaction_isolation,
    ->     @@session.transaction_isolation;
+--------------------------------+---------------------------------+
| @@global.transaction_isolation | @@session.transaction_isolation |
+--------------------------------+---------------------------------+
| REPEATABLE-READ                | REPEATABLE-READ                 |
+--------------------------------+---------------------------------+
1 row in set (0.00 sec)

ここではセッション変数を使って変更する。 たとえば、READ UNCOMMITTED に変更するときは次のようにする。

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT
    ->     @@global.transaction_isolation,
    ->     @@session.transaction_isolation;
+--------------------------------+---------------------------------+
| @@global.transaction_isolation | @@session.transaction_isolation |
+--------------------------------+---------------------------------+
| REPEATABLE-READ                | READ-UNCOMMITTED                |
+--------------------------------+---------------------------------+
1 row in set (0.00 sec)

プロンプトを 2 つ用意する

ここからは複数のトランザクションを扱うので、ターミナルを 2 つ用意してどちらも MySQL サーバにログインしておく。

それぞれのプロンプトを区別するために、次のようにして表示を切りかえる。 ひとつ目は "mysql1" という名前にする。

mysql> PROMPT mysql1> 
PROMPT set to 'mysql1> '

もうひとつは "mysql2" にしておく。

mysql> PROMPT mysql2> 
PROMPT set to 'mysql2> '

ダーティーリード

前置きが長くなったけど、ここから実際の検証に入る。 ダーティーリードを一言でいうと、あるトランザクションでコミットしていない変更が、他のトランザクションから見えてしまうというもの。

あらかじめ、両方のプロンプトのトランザクション分離レベルを READ UNCOMMITTED に変更しておく。

mysql1> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql2> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

両方のプロンプトでトランザクションを開始する。

mysql1> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql2> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

片方のトランザクションで、既存のレコードのカラムを変更してみよう。

mysql1> UPDATE
    ->     users
    -> SET
    ->     name = 'Bob'
    -> WHERE
    ->     id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

そして、もう一方のトランザクションから読み取ってみる。 すると、先ほど "mysql1" でレコードに加えた変更が "mysql2" から見えてしまっている。 この現象をダーティーリードという。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
+----+------+
| id | name |
+----+------+
|  1 | Bob  |
+----+------+
1 row in set (0.00 sec)

振る舞いを確認できたら、両方のトランザクションをロールバックしておく。

mysql1> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

mysql2> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

ノンリピータブルリード

続いてはノンリピータブルリード、もしくはファジーリードと呼ばれる現象について。 この現象を一言で表すと、あるトランザクションでコミットした変更が、他のトランザクションから見えてしまうというもの。

はじめに、プロンプトのトランザクション分離レベルを READ COMMITTED に変更しておく。

mysql1> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql2> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

そして、トランザクションを開始する。

mysql1> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql2> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

片方のトランザクションで、先ほどと同じようにレコードに変更を加えてみよう。

mysql1> UPDATE
    ->     users
    -> SET
    ->     name = 'Carol'
    -> WHERE
    ->     id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

この時点では、変更はコミットされていない。 もう一方のトランザクションから変更は見えていないので、今回はダーティーリードは生じていない。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+
1 row in set (0.00 sec)

それでは、トランザクションをコミットしてみよう。

mysql1> COMMIT;
Query OK, 0 rows affected (0.00 sec)

すると、もう一方のトランザクションから変更が見えてしまった。 この現象をノンリピータブルリード、またはファジーリードという。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
+----+-------+
| id | name  |
+----+-------+
|  1 | Carol |
+----+-------+
1 row in set (0.00 sec)

コミットしていない方のトランザクションはロールバックしておこう。

mysql2> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

ファントムリード

続いてはファントムリードについて。 この現象を一言で表すと、あるトランザクションでコミットしたレコードの追加や削除が、別のトランザクションから見えてしまうというもの。

なお、ファントムリードは、本来であればトランザクション分離レベルが REPEATABLE READ 以下のときに生じる。 しかし、MySQL の InnoDB では REPEATABLE READ でも例外的にファントムリードが生じない。 そのため、この検証は先ほどに引き続き READ COMMITTED を使って行う。

はじめに、トランザクションを開始する。

mysql1> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql2> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

そして、片方のトランザクションでレコードを削除する。

mysql1> DELETE
    -> FROM
    ->     users
    -> WHERE
    ->     id = 1;
Query OK, 1 row affected (0.00 sec)

トランザクションはコミットされていない。 この時点では、もう一方のトランザクションからレコードの削除は見えていない。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
+----+-------+
| id | name  |
+----+-------+
|  1 | Carol |
+----+-------+
1 row in set (0.00 sec)

それでは、トランザクションをコミットしてみよう。

mysql1> COMMIT;
Query OK, 0 rows affected (0.01 sec)

すると、もう一方のトランザクションからレコードが見えなくなった。 つまり、あるトランザクションでレコードを削除した内容が、別のトランザクションから見えてしまっている。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
Empty set (0.00 sec)

動作が確認できたらコミットしていないトランザクションをロールバックしておこう。

mysql2> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

REPEATABLE READ のときの挙動を確認しておく

念のため、トランザクション分離レベルを REPEATABLE READ にしたときの振る舞いも確認しておこう。

両方のプロンプトのトランザクション分離レベルを REPEATABLE READ に変更する。

mysql1> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

mysql2> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

サンプルとなるレコードをあらためて追加しておく。

mysql1> INSERT INTO users VALUES (1, "Alice");
Query OK, 1 row affected (0.00 sec)

ダーティーリードが生じないことを確認する

両方のプロンプトでトランザクションを開始する。

mysql1> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql2> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

一方のプロンプトからレコードのカラムに変更を加える。

mysql1> UPDATE
    ->     users
    -> SET
    ->     name = 'Bob'
    -> WHERE
    ->     id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

この時点でレコードを確認してもカラムは変更されていない。 つまり、ダーティーリードが生じていないことがわかる。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+
1 row in set (0.00 sec)

ノンリピータブルリードが生じないことを確認する

先ほどの状況から、変更を加えたトランザクションをコミットする。

mysql1> COMMIT;
Query OK, 0 rows affected (0.00 sec)

この状態でもう一方のトランザクションから確認してもレコードのカラムは変更されていない。 つまり、ノンリピータブルリードが生じていないことがわかる。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+
1 row in set (0.00 sec)

ファントムリードが生じないことを確認する

トランザクションをあらためて開始した上でレコードを削除してコミットする。

mysql1> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql1> DELETE
    -> FROM
    ->     users
    -> WHERE
    ->     id = 1;
Query OK, 1 row affected (0.00 sec)

mysql1> COMMIT;
Query OK, 0 rows affected (0.00 sec)

この状況でもう一方のトランザクションから確認してもレコードは存在しているように見える。 つまり、ファントムリードが生じていないことがわかる。

mysql2> SELECT
    ->     *
    -> FROM
    ->     users;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+
1 row in set (0.00 sec)

参考

https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdfwww.microsoft.com

dev.mysql.com


  1. ただし、ANSI SQL における定義の曖昧さから、厳密にはより様々な現象が生じることが知られている。

  2. MySQL の InnoDB では生じない。

Python: 学習済み機械学習モデルの特性を PDP で把握する

機械学習を用いるタスクで、モデルの解釈可能性 (Interpretability) が重要となる場面がある。 今回は、モデルの解釈可能性を得る手法のひとつとして PDP (Partial Dependence Plot: 部分従属プロット) を扱ってみる。 PDP を使うと、モデルにおいて説明変数と目的変数がどのような関係にあるか理解する上で助けになることがある。 なお、今回は PDP を計算・描画するのに専用のライブラリは使わず、自分で実装してみることにした。

使った環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V          
Python 3.7.7

下準備

まずは、あらかじめ必要なパッケージをインストールしておく。

$ pip install scikit-learn pandas matplotlib

題材とするデータセットとモデルについて

今回は scikit-learn 付属の糖尿病 (Diabetes) データセットを題材として話を進める。 このデータセットは、患者の情報が説明変数で、糖尿病の進行具合が目的変数となっている。

たとえば、これをランダムフォレストで学習させることを考えてみよう。 次のサンプルコードでは、モデルにデータセットを学習させた上で、特徴量の重要度をグラフにプロットしている。

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

import numpy as np
from matplotlib import pyplot as plt
from sklearn import datasets
from sklearn.ensemble import RandomForestRegressor


def main():
    # 糖尿病データセットを読み込む
    dataset = datasets.load_diabetes()
    X, y = dataset.data, dataset.target

    # ランダムフォレストを学習させる
    clf = RandomForestRegressor(n_estimators=100,
                                random_state=42)
    clf.fit(X, y)

    # ジニ不純度を元に計算した重要度を得る
    importances = clf.feature_importances_
    feature_names = np.array(dataset.feature_names)
    sorted_indices = np.argsort(importances)[::-1]

    # 重要度をプロットする
    plt.bar(feature_names[sorted_indices],
            importances[sorted_indices])
    plt.show()


if __name__ == '__main__':
    main()

上記をファイルに保存して実行してみよう。

$ python giniimp.py

すると、次のような棒グラフが得られる。 モデルにおいて、s5bmi といった特徴量が重要視されていることがわかる。

f:id:momijiame:20200507173935p:plain
Gini / Split Importance

なお、ここで計算している重要度は決定木系のモデルに特有のもの。 具体的には、決定木がデータを分割する上でジニ不純度をうまく下げることのできた特徴量を重要なものと判断している。 一般的には Gini Importance や Split Importance と呼ばれている。

この他にも、モデルに依存しない特徴量の重要度として Permutation Importance などが知られている。

blog.amedama.jp

BMI に着目してみる

先ほど計算した重要度では、モデルが特定の特徴量を予測する上で重要視していることがわかった。 しかし、その特徴量が予測する上でどのように重要とされているのかはわからない。

ここでは、先ほど重要とされた BMI にひとまず着目してみよう。 まずは、BMI に含まれる値を確認しておく。 次のサンプルコードでは、BMI に含まれる値をヒストグラムにプロットしている。

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

import pandas as pd
from sklearn import datasets
from matplotlib import pyplot as plt


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

    # 特定カラムのヒストグラムを描く
    df = pd.DataFrame(X, columns=dataset.feature_names)
    df['bmi'].plot.hist()

    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python bmihist.py

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

f:id:momijiame:20200507174148p:plain
BMI のヒストグラム

上記のグラフから、BMI は -0.1 ~ +0.15 前後の範囲に値を持つことがわかった。

特定の行 (患者) について BMI を変化させて予測を観察する

続いて、今回扱う PDP の根底となる考え方を説明する。 それは、ある行 (患者) において他の条件は固定したまま特定の説明変数を変化させたとき、予測がどのように変化するかを観察するというもの。

今回であれば、その他の特徴量は固定したまま、BMI の値を色々と変化させたとき、予測がどう変化するかを観察する。 次のサンプルコードでは、先頭の 1 行 (1 人の患者) について BMI を 10 段階で変化させながらモデルの予測を折れ線グラフにプロットしている。

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

import numpy as np
import pandas as pd
from sklearn import datasets
from sklearn.ensemble import RandomForestRegressor
from matplotlib import pyplot as plt


def main():
    # データセットを読み込む
    dataset = datasets.load_diabetes()
    X, y = dataset.data, dataset.target
    df = pd.DataFrame(X, columns=dataset.feature_names)

    # あらかじめ解釈したいモデルを学習させておく
    clf = RandomForestRegressor(n_estimators=100,
                                random_state=42)
    clf.fit(X, y)

    # 特定のカラムが取りうる値を等間隔で計算する
    resolution = 10  # 間隔数
    target_column = 'bmi'  # 対象とするカラム名
    min_, max_ = df[target_column].quantile([0, 1])
    candidate_values = np.linspace(min_, max_, resolution)

    # 例として先頭の行を取り出す
    target_row = df.iloc[0]

    # 特定のカラムの値を入れかえながらモデルに予測させる
    y_preds = np.zeros(resolution)
    for trial, candidate_value in enumerate(candidate_values):
        target_row[target_column] = candidate_value
        y_preds[trial] = clf.predict([target_row])

    # 予測させた結果をプロットする

    plt.plot(candidate_values, y_preds)
    plt.xlabel('factor')
    plt.ylabel('target')
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python icehead.py

すると、次のようなグラフが得られる。 どうやら BMI の値が大きくなると、概ね病気の進行具合も進んでいると判断されるようだ。

f:id:momijiame:20200507175751p:plain
先頭行の ICE Plot

とはいえ、一人の患者だけを見て判断するのは早計なはず。 もっと、たくさんの行も確認して、より大局的な傾向を把握したい。

次のサンプルコードでは、全データの 10% をサンプリングした上で先ほどと同じことをしている。

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

import numpy as np
import pandas as pd
from sklearn import datasets
from matplotlib import pyplot as plt
from sklearn.ensemble import RandomForestRegressor


def main():
    dataset = datasets.load_diabetes()
    X, y = dataset.data, dataset.target
    df = pd.DataFrame(X, columns=dataset.feature_names)

    clf = RandomForestRegressor(n_estimators=100,
                                random_state=42)
    clf.fit(X, y)

    resolution = 10
    target_column = 'bmi'
    min_, max_ = df[target_column].quantile([0, 1])
    candidate_values = np.linspace(min_, max_, resolution)

    # いくつかの行について、特定のカラムの値を入れかえながらモデルに予測させる
    sampling_factor = 0.1
    sampled_df = df.sample(frac=sampling_factor,
                           random_state=42)
    y_preds = np.zeros((len(sampled_df), resolution))
    for index, (_, target_row) in enumerate(sampled_df.iterrows()):
        for trial, candidate_value in enumerate(candidate_values):
            target_row[target_column] = candidate_value
            y_preds[index][trial] = clf.predict([target_row])

    # 予測させた結果をプロットする
    for y_pred_row in y_preds:
        plt.plot(candidate_values, y_pred_row, color='b')
    plt.xlabel('factor')
    plt.ylabel('target')
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python ice.py

すると、次のようなグラフが得られる。 やはり、概ね BMI が大きくなると病気の進行具合も進んでいると判断できるようだ。

f:id:momijiame:20200507175821p:plain
いくつかの行に対する ICE Plot

上記の手法を ICE (Individual Conditional Expectation: 個別条件付き期待値) Plot という。

ICE の要約統計量をグラフにプロットする

ICE Plot を使うことで、個別の行について説明変数と目的変数の関係性を把握できる。 一方で、たくさん折れ線グラフがあるとぶっちゃけ見にくい。 そこで、平均値などの要約統計量をグラフにプロットしてみよう。

次のサンプルコードでは ICE の平均値と、バラつきを確認するために 1 SD (Standard Division: 標準偏差) を描画している。

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

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from sklearn import datasets
from sklearn.ensemble import RandomForestRegressor


def main():
    dataset = datasets.load_diabetes()
    X, y = dataset.data, dataset.target
    df = pd.DataFrame(X, columns=dataset.feature_names)

    clf = RandomForestRegressor(n_estimators=100,
                                random_state=42)
    clf.fit(X, y)

    resolution = 10
    target_column = 'bmi'
    min_, max_ = df[target_column].quantile([0, 1])
    candidate_values = np.linspace(min_, max_, resolution)

    # いくつかの行について、特定のカラムの値を入れかえながらモデルに予測させる
    sampling_factor = 0.5
    sampled_df = df.sample(frac=sampling_factor)
    y_preds = np.zeros((len(sampled_df), resolution))
    for index, (_, target_row) in enumerate(sampled_df.iterrows()):
        for trial, candidate_value in enumerate(candidate_values):
            target_row[target_column] = candidate_value
            y_preds[index][trial] = clf.predict([target_row])

    # 予測させた結果をプロットする
    mean_y_preds = y_preds.mean(axis=0)  # 平均
    sd_y_preds = y_preds.std(axis=0)  # 標準偏差
    # 平均 ± 1 SD を折れ線グラフにする
    plt.plot(candidate_values, mean_y_preds)
    plt.fill_between(candidate_values,
                     mean_y_preds - sd_y_preds,
                     mean_y_preds + sd_y_preds,
                     alpha=0.5)
    plt.xlabel('factor')
    plt.ylabel('target')
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python pdp.py

すると、次のようなグラフが得られる。 ICE をたくさんプロットするよりも見やすい。 そして、このグラフこそ、今回の本題である PDP というらしい。

f:id:momijiame:20200507175849p:plain
PDP

同じ要領で、他の特徴量についても PDP をプロットしていくことで説明変数と目的変数の関係性を把握する。

補足

なお、scikit-learn には PDP を描画するための専用の API もある。 今回は勉強がてら自分で書いてみたけど、普段はこちらを使う方が手っ取り早いはず。

scikit-learn.org

いじょう。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

Python: PySpark で UDAF が作れない場合の回避策について

PySpark では、ごく最近まで UDAF (User Defined Aggregate Function: ユーザ定義集計関数) がサポートされていなかった。 Apache Spark 2.3 以降では Pandas UDF を使うことで UDAF に相当する処理を書くことができるようになっている。 今回は、それ以前のバージョンを使っているときに、同等の処理を書くための回避策について書いてみる。

使った環境は次のとおり。

$ cat /etc/redhat-release 
CentOS Linux release 7.7.1908 (Core)
$ uname -r
3.10.0-1062.18.1.el7.x86_64
$ hadoop version
Hadoop 2.9.2
Subversion https://git-wip-us.apache.org/repos/asf/hadoop.git -r 826afbeae31ca687bc2f8471dc841b66ed2c6704
Compiled by ajisaka on 2018-11-13T12:42Z
Compiled with protoc 2.5.0
From source with checksum 3a9939967262218aa556c684d107985
This command was run using /home/vagrant/hadoop-2.9.2/share/hadoop/common/hadoop-common-2.9.2.jar
$ pyspark --version
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 2.4.5
      /_/
                        
Using Scala version 2.11.12, OpenJDK 64-Bit Server VM, 1.8.0_242
Branch HEAD
Compiled by user centos on 2020-02-02T19:38:06Z
Revision cee4ecbb16917fa85f02c635925e2687400aa56b
Url https://gitbox.apache.org/repos/asf/spark.git
Type --help for more information.

下準備

はじめに、PySpark の REPL を起動しておく、

$ pyspark --master yarn

次のようにして、サンプルとなる DataFrame を用意する。 このデータは `'category`` カラムを使ってグループ化できる。

>>> data = [
...   ('A', 'Alice', 10),
...   ('A', 'Bob', 15),
...   ('A', 'Carol', 20),
...   ('B', 'Daniel', 25),
...   ('B', 'Ellie', 30),
...   ('C', 'Frank', 35),
... ]
>>> df = spark.createDataFrame(data, ('category', 'name', 'age'))

組み込みの集計関数について

はじめに、PySpark に組み込みで用意されている集計関数はどのように使うのかおさらいしておく。 たとえば、カテゴリーごとの平均値を計算してみよう。

たとえば年齢の平均を計算するときは、次のようにする。 DataFrame#groupBy() からは GroupedData というクラスのインスタンスが返る。 さらに、そのインスタンスに対して GroupedData#agg() を使って集計関数を適用する。

>>> df.groupBy('category').agg({'age': 'mean'}).show()
+--------+--------+                                                             
|category|avg(age)|
+--------+--------+
|       B|    27.5|
|       C|    35.0|
|       A|    15.0|
+--------+--------+

集計する処理を辞書と文字列で表す以外に pyspark.sql.functions を使う方法もある。 たとえば、上記と同じ処理を書いてみよう。

>>> from pyspark.sql import functions as F
>>> df.groupBy('category').agg(F.mean('age').alias('mean-age')).show()
+--------+--------+                                                             
|category|mean-age|
+--------+--------+
|       B|    27.5|
|       C|    35.0|
|       A|    15.0|
+--------+--------+

以上が組み込みの集計関数を使う方法になる。 基礎的な統計量などを計算するだけなら、これでも問題ないはず。 しかし、集計の処理は組み込みの関数だけで完結しない場合も多い。 そこで、ユーザ定義の関数で集計の処理をしたくなる。

UDAF 代わりの処理の書き方

前述したとおり Apache Spark 2.3 以降では Pandas UDF を使って UDAF に相当する処理が書ける。 ただし、ここではそれについての具体的な紹介はしない。 紹介するのは、Pandas UDF が使えない環境での回避策となる。

回避策のキモは pyspark.sql.functions.collect_list を使うところ。 この関数は GroupedData の特定カラムを、グループ単位でリストに入れて返すことができる。 言葉で説明するよりも、次のサンプルコードを見てもらった方が早いかもしれない。

>>> agg_df = df.groupBy('category').agg(F.collect_list('age').alias('grouped-age'))
>>> agg_df.show()
+--------+------------+                                                         
|category| grouped-age|
+--------+------------+
|       B|    [25, 30]|
|       C|        [35]|
|       A|[10, 15, 20]|
+--------+------------+

この状態までくれば、あとは単なる UDF (User Defined Function) を使って集計できる。 例として平均を計算する UDF を書いてみよう。

>>> def mean(values):
...     return sum(values) / len(values)
... 
>>> mean_udf = F.udf(mean)

上記の UDF をリストのカラムに対して適用する。

>>> agg_df.withColumn('mean-age', mean_udf('grouped-age')).show()
+--------+------------+--------+                                                
|category| grouped-age|mean-age|
+--------+------------+--------+
|       B|    [25, 30]|    27.5|
|       C|        [35]|    35.0|
|       A|[10, 15, 20]|    15.0|
+--------+------------+--------+

ばっちり。

参考

stackoverflow.com

入門 PySpark ―PythonとJupyterで活用するSpark 2エコシステム

入門 PySpark ―PythonとJupyterで活用するSpark 2エコシステム

ピクセラ PIX-MT100 を iPad から使ってみる

外出先でパソコンからインターネットを使いたいときがある。 そんなときのために、普段はピクセラの PIX-MT100 という LTE 対応 USB ドングルに MVNO の SIM カードを入れて持ち歩いている。

ピクセラ LTE対応USBドングル ホワイト  PIX-MT100

ピクセラ LTE対応USBドングル ホワイト PIX-MT100

  • 発売日: 2016/06/17
  • メディア: Personal Computers

この製品は Mac などのパソコンに USB 端子でつなぐと、有線の Ethernet デバイスとして認識する。 つまり、USB Ethernet アダプタを挿して LAN ケーブルをつないだような状態になる。 また、使う上でとくに専用のドライバを入れる必要もない。 テザリングに比べると無線 LAN チャネルの混雑状況に関係なく安定した通信ができるので意外と重宝している。

で、今回は PIX-MT100 が iPad から使えるのかが気になった。 製品のサポートページを見ると、一応 iPhone / iPad は iOS 8 以降でサポートされているようだ。

www.pixela.co.jp

とはいえ、事前に Web を軽く調べても、実際に検証している人が見当たらなかったので自分で試してみることにした。

確認に使った iPad の環境は次のとおり。

f:id:momijiame:20200501212844p:plain
検証に使った iPad の情報

結論としては、ちゃんと使えることが分かった。 USB-A to C アダプタ経由で挿すと、しばらくして "4G Modem" という名前で Ethernet デバイスを認識する。 以下の写真を見ると分かるとおり、PIX-MT100 のステータスランプも正常を示す青色が点灯する。

f:id:momijiame:20200501193752j:plain
PIX-MT100 を iPad OS が認識した状態

設定情報を確認すると、ちゃんと IPv4 / IPv6 の設定が配布されていることがわかる。

f:id:momijiame:20200501193802j:plain
ネットワークの設定情報

もちろん、無線 LAN などを切った状態でちゃんとインターネットが使えることも確認できた。

実際にこの状態で使うかは別として、iPad でも PIX-MT100 を使うことはできるようだ。 いじょう。