SudachiPy でユーザ辞書を作ってみた

前回のブログでGiNZA2.0 の導入について記述した。

toohsk.hateblo.jp

形態素解析を行ったところ期待した粒度で分割されていないことがあった。
これは形態素解析を行う上で仕方ない部分であり、
こういう場合には分割されてほしくない単語をユーザ辞書で管理する必要がある。

GiNZA はトークン化するためにSudachiPy を利用している。 1
なので、ユーザ辞書はSudachiPy にそう形で用意する必要がある。

前提

前回と同様、pyenv でPython 3.7.3 をインストールした環境でのコードを書いていきます。

注意

本来、GiNZA をインストールすると合わせてSudachiPy がインストールされるが、今回は手動でインストールする。

SudachiPy のインストール

SudachiPy とSudachiDict_coreをインストールします。

$ pip install SudachiPy
$ pip install https://object-storage.tyo2.conoha.io/v1/nc_2520839e1f9641b08211a5c85243124a/sudachi/SudachiDict_core-20190531.tar.gz

ユーザ辞書の元データ

前回、 金閣寺金閣 の2つに分割されてしまったので、 金閣寺 と出力されるように 金閣寺 をユーザ辞書に登録します。

金閣寺,4786,4786,5001,金閣寺,名詞,固有名詞,一般,*,*,*,キンカクジ,きんかくじ,*,*,*,*,*

4カラム目の 5001 はコストに相当するものですが、今回は適当に指定しています。

バイナリ化

SudachiPy でユーザ辞書を扱うためには、バイナリに変換した辞書を指定する必要があります。
以下のコマンドでバイナリ化できます。

$ sudachipy ubuild -s /path/to/system.dic user_dict.txt

ちなみに system.dic のありかですが、
今回、pyenv でSudachiPy をインストールしていることもあり、 pyenv 配下で使用している python バージョンの site-packages 以下に site-packages/sudachidict_core/resources/system.dic としてありました。
参考にしてください。

次回はこのバイナリ化した辞書を使って、GiNZA で形態素解析を行います。


  1. GiNZA のリポジトリSudachiおよびSudachiPy 参照

GiNZA 2.0 を触ってみた。

GiNZA はmegagonlabs が開発している日本語の形態素解析器です。 フレームワークとしてspaCy をFramework として利用しています。 今日2.0 がリリースされたので、インストールとユーザ辞書について触っていきたいと思います。

前提

今回、pyenv でPython 3.7.3 をインストールした環境でのコードを書いていきます。

インストール手順

インストール手順はこちらのページにあります。 2通りのインストール方法があるみたいです。
1つは、圧縮ファイルをダウンロードして、そのあとpip でインストールする方法です。
そして、もう一つはリンクをpip インストールコマンドで指定する方法です。
今回は、後者の方でインストールをしてみました。

pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"

この場合、 latest がダウンロードされるみたいです。
バージョンを指定する場合は、 latest を指定すれば良さそう。

形態素解析

形態素解析のコード例もこちらに載っています。

import spacy
nlp = spacy.load('ja_ginza')
doc = nlp('依存構造解析の実験を行っています。')
for sent in doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)
    print('EOS')

実行結果はこちらになりました。

0 依存 依存 NOUN 名詞-普通名詞-サ変可能 compound 2
1 構造 構造 NOUN 名詞-普通名詞-一般 compound 2
2 解析 解析 NOUN 名詞-普通名詞-サ変可能 nmod 4
3 の の ADP 助詞-格助詞 case 2
4 実験 実験 NOUN 名詞-普通名詞-サ変可能 obj 6
5 を を ADP 助詞-格助詞 case 4
6 行っ 行う VERB 動詞-一般 ROOT 6
7 て て SCONJ 助詞-接続助詞 mark 6
8 い 居る AUX 動詞-非自立可能 aux 6
9 ます ます AUX 助動詞 aux 6
10 。 。 PUNCT 補助記号-句点 punct 6
EOS

恐らく、 token.i が出力のインデックス、 token.orth_ が表層、 token.lemma_ が原型、 token.pos_ が英語での品詞、 token.tag_ が日本語での品詞と活用のようです。 token.dep_token.head.i はよくわかっていないので、後日調べて記事にしたいですが、 token.head.i係り受け先の id のように見えます。

ユーザ辞書

例えば 足利義満は金閣寺を建立しました。 のような文章を形態素解析をしたい場合に、以下のような結果になります。

0 足利 足利 PROPN 名詞-固有名詞-人名-姓 compound 1
1 義満 義満 PROPN 名詞-固有名詞-人名-名 nsubj 6
2 は は ADP 助詞-係助詞 case 1
3 金閣 金閣 NOUN 名詞-普通名詞-一般 compound 4
4 寺 寺 NOUN 接尾辞-名詞的-一般 obj 6
5 を を ADP 助詞-格助詞 case 4
6 建立 建立 VERB 名詞-普通名詞-サ変可能 ROOT 6
7 し 為る AUX 動詞-非自立可能 aux 6
8 まし ます AUX 助動詞 aux 6
9 た た AUX 助動詞 aux 6
10 。 。 PUNCT 補助記号-句点 punct 6
EOS

このようにデフォルトの辞書で形態素解析を行うと 金閣寺 がまとまった名詞として扱われることを期待していた場合に、異なった結果になる場合があります。
このような場合にユーザ辞書を利用します。
GiNZA は SudachiPy を利用しているようなので、ユーザ辞書は SudachiPy の書き方に従って書けばユーザ辞書が使えそうです。

ユーザ辞書を扱うまでをブログの内容にしたかったのですが、 次回にまわしたいと思います。

Pandas DataFrame のX軸の単位を調整する

内容

DataFrame で histogram を描画したときの x, y 軸のスケールの表示を調整する方法について、ハマったのでメモとして残しておきます。

TL;DR

DataFrame から直接 df.hist とするのではなく、
一度、 df.plotmatplotlib.axes._subplots.AxesSubplot を取り出し、
その matplotlib.axes._subplots.AxesSubplotticklable_formatstyle オプションを変更することで整数表示や浮動小数点表記での表示などを指定することができます。

ax = sample_df.plot(figsize=(15, 5), kind='hist', bins=100)
ax.ticklabel_format(style='plain', axis='x') # 浮動小数点表記で表示したい場合、 style='sci' とする

Pandas の version

0.24.1 を使ってます。

import pandas as pd
pd.__version__
'0.24.1'

どういう事象?

元々、データ分析の案件で発生した事象で、
少数を含む、かつ、取りうる値の範囲が広いデータを DataFrame で扱い、それを df.hist で描画しようとしました。

今回は、だいたいのデータとして、以下のデータで再現実験をします。

sample_size = 10000
sample_data = np.random.rand(sample_size)
sample_data = [d * np.random.randint(low=1, high=10000000) for d in sample_data]
sample_category = ["a_{}".format(i) for i in range(sample_size)]
d = {'col1': sample_category, 'col2': sample_data}
sample_df = pd.DataFrame(d)

以下のようなデータが作成されます。

pd.options.display.float_format = '{:.2f}'.format
sample_df.head(10)

    col1    col2
0  a_0     6809432.40
1  a_1     3095277.45
2  a_2     1515776.87
3  a_3     6291255.66
4  a_4     429976.20
5  a_5     422715.54
6  a_6     5695883.51
7  a_7     2793741.89
8  a_8     2343652.35
9  a_9     4571542.77

分布としては、以下のような感じ。

sample_df.describe()

        col2
count   10000.00
mean    2498833.72
std         2212640.08
min  128.83
25%    662876.83
50%    1854733.14
75%    3875713.08
max  9861186.18

桁のオーダを見ると 102 から 106 のデータが生成されています。
このデータを df.hist で描画すると

f:id:toohsk:20190401105232p:plain
x軸が浮動小数点表記表示になっている histogram

のような形式になりました。 X 軸のスケールに注目すると自動的に浮動小数点表記に変換されていることがわかります。

今回のようなランダムに生成されたデータの場合、このような表現でもわかりますが、
この結果を他の方にシェアする場合、実際の値としてみた方が感覚をつかみ安い方もいるかと思います。

どうやって整数表示にするか

x, y 軸の表示に関する操作はどうやら matplotlib.axes.Axes class にて操作することは調べて想定がつきました。
ただ、問題なのは、 df.hist で返される値の型は numpy.ndarray でした。

hist = sample_df.hist(bins=100, figsize=(15, 5))
type(hist)

numpy.ndarray

なので、 matplotlib.axes.Axes class を操作しようとしても操作できず、 軸のスケールを操作することができません。

なので、 histogram の描画を df.hist で行うのではなく、
df.plot で描画し、 option の kind で kind='hist' とすることで、 matplotlib.axes.Axes class を戻り値として受け取れるようにしました。

ax = sample_df.plot(figsize=(15, 5), kind='hist', bins=100)
type(ax)

matplotlib.axes._subplots.AxesSubplot

これにより、目的だった matplotlib.axes.Axes class を操作することができました。 matplotlib.axes.Axes class の ticklabel_formatstyle='plain とすることで軸のスケールを整数表示することができました。

ax = sample_df.plot(figsize=(15, 5), kind='hist', bins=100)
ax.ticklabel_format(style='plain', axis='x') # axis='y' とすると y 軸のスケールを整数にすることができる

f:id:toohsk:20190401111909p:plain
x軸が整数表示になっている histogram

反対に、明示的に浮動小数点表記としたい場合は、 ticklabel_format の style オプションを style=sci とすればよく、
y 軸のスケールを変えたい場合は、 ticklabel_format の axis オプションを axis=y とすればよいみたいです。

Pandas DataFrame で圧縮したデータファイルを読み込む

概要

圧縮されたファイルを Pandas DataFrame で読み込むための手順を紹介する。
毎回解凍してから読み込みをしていたのを、圧縮したまま読み込めたので、忘備録として残す。 今回は gzip 圧縮した前提で記載します。

内容

すごくシンプルだったので、まずコードを載せます。

import pandas as pd

gz_file_path = 'file.tsv.gz'
df = pd.read_table(gz_file_path, compression='gzip')
df.head()

gzip 圧縮したファイルのパスを gz_file_path に格納しています。 その後、 pd.read_table で読み込みをしています。
その際に compression オプションで圧縮フォーマットを指定するだけで、 読み込んでくれます。
自分の手元では、 header に column 名が載っているデータを読み込ませたところ、 df.head() で DataFrame の先頭の5件を表示させたときに header 行の内容が表示されていました。
もし、 header 行が無いデータの場合は、 names オプションにカラム名を指定すればよい。

扱える圧縮のファイル形式

扱えるフォーマットは、

がある。 compression オプションで圧縮フォーマットを指定しない場合、
ファイルの拡張子から圧縮フォーマットを推定してくれるみたいだが、
明示的に記載している方がよいと思われる。

OSS に Contribute しました

簡単な変更でしたが、OSS に Contribute しました。

github.com

内容と所感

内容はドキュメントに favicon.ico を追加しただけという…笑
実は OSS にコミットという点で言えば、初めてのコミットだった。
しかも、今日初めて読むプロジェクトに Sphinx という不慣れなフレームワークでの修正。
なので、小さな変更だがすごく達成感があり、気持ちとしては晴れ晴れしている。

元々は OSS にコミット、さらに言えば、機械学習系の OSS にコミットしていきたいという気持ちがあった。
なので、今回の変更は小さいけれど、これをきっかけにもっとコミットしていきたいと思っている。

Good first issue

そもそもなんで OSS に Contribute したいと思っていたのに、できなかったか。
正直、OSS にコミットしたくていろいろ資料よんだけど、
結局のところ、何すれば良いのか、というのがわからなかったというのが大きかった。

そんななか、この資料を読んだ。

speakerdeck.com

OSSリポジトリをみる時に、ざっくり issue を読んでいた。
しかし、上記の資料を見た時、どの issue に注目すれば良いかわかったので、
今回のような些細だが Contribute するきっかけを得られたと思っている。

今後

引き続き、 blueoil は引き続き注目して ML の範囲で Contribute できるように挑戦していきたい。

Python で標準出力をリアルタイムに出力する方法

Jenkinsで実行ログをリアルタイムで確認したい

Jenkis ではジョブの実行ログをリアルタイムに確認することができる。
しかし、 ( Python ではスクリプト言語のため)、処理を高速化するために標準出力をバッファしてまとめて出力する。
なので、 Jenkins でジョブの動作を確認する場合、まとまった単位で標準出力のログが出力される。
毎日、定時に実行するようば定例バッチの場合はそれでもいいが、
開発途中の場合、リアルタイムにログがみたい場合があると思う。
そのような場合の忘備録を残す。

Pythonでリアルタイムに標準出力するには

Pythonでは使うバージョンによって、即時標準出力する方法が異なる。

Python3.3以降

これは有名な方法だが、 flush オプションを True で設定する。

print("プリントする文言", flush=True)

Python3.3より前 + Python2.X 系

2020年には Python 2系はメンテされなくなると思うが、一応載せておく。
sys.stdout.flush() を使うことで flush される。

import sys
print("プリントする文言")
# Python 2系なら
# print "プリントする文言"
sys.stdout.flush()

いちいち仕込むべきなのか

冒頭で書いたように、バッチの性質によって必ずしも常に flush する必要はないと思う。
しかし、リアルタイムに出力させたい場合に備えて、 flush を仕込む方針を取ると非常に厄介だと思う。
例えば、今ブログを書きながらで思いつく方法としては、
バッチ実行時に flush をするかどうかの判定を挟む方法だ。
例えば、環境変数に flush_flg を仕込んでそれを使って判定する、のような処理。
この場合、個人で開発している場合は統一できるかもしれないが、時間の経過と共に忘れてしまったりするし、 チームで開発している場合、人それぞれの書き方や基準があやふやになって推奨できない。 また、そのために共通関数化するのもたかが標準出力のためにバカらしい。 不用意な変数を仕込むのでバグの温床にもなりかねない。 なので、できるならシンプルな形式に留めるほうが良いと思う。

Python のオプションを使う

Python のオプションに -u オプションがある。
python -h とすると用途が見られる。 以下に出力を抜粋する。
ちなみに自分が使っているバージョンは Anaconda の Python 3.6.5 である。

$ python -V
Python 3.6.5 :: Anaconda, Inc.

$ python -h
... (抜粋)
-u     : force the binary I/O layers of stdout and stderr to be unbuffered;
         stdin is always buffered; text I/O layer will be line-buffered;
         also PYTHONUNBUFFERED=x
...

とある。
要約すると、標準出力とエラー出力を強制的にバッファしないようにする。とのこと。
つまり、このオプションを使えば flush と同じことができる。
なので、例えば開発環境などで随時標準出力を見たい場合は、 Jenkins のジョブ実行スクリプト画面で

python -u <実行するスクリプト>

すればよいし、そのまま定例実行に切り替えるときは、

python <実行するスクリプト>

とするだけでよい。

FYI

Python2 count down

https://pythonclock.org/

ElasticSearch に Bulk Insert をする

以前の記事でも記載したが、本業のプロジェクトで ElasticSearch を利用したシステムの構築をしようとしている。
Docker で作ったデモ環境では、 Embulk での bulk insert ができていた。 しかし、 AWS Elasticsearch Service では embulk-output-elasticsearch がサポート外のため、利用できなかった。

github.com

なので、別の手段で bulk insert を実装した。

Python の ElasticSearch Client を使う

コードは Python をベースで利用する。
なので、 ElasticSearch の Client として、

Python Elasticsearch Client — Elasticsearch 6.3.0 documentation

を利用する。 インストールは、

pip install elasticsearch

でインストールできる。
基本的には、インストールする Client のバージョンは、 ElasticSearch インスタンスのメジャーバージョンに合わせて利用するのが推奨されていそう。

pypi.org

なので、例えば ElasticSearch の 5.X.X を利用している場合、インストールする Client は

pip install elasticsearch>=5.0.0,<6.0.0

でインストールすることで、5系の中の最新バージョンの Client をインストールすることができる。

bulk insert

ElasticSearch に bulk insert する場合、 helpers の中の bulk method を利用することで bulk insert が実装できる。
参考として、以下に 1000 件のデータに対して bulk insert するコード例を載せる。

from elasticsearch import Elasticsearch, helpers

es = Elasticsearch(host='localhost', port=9200)

data = []

for i in range(1000):
    doc = {'hoge': 'foobar'}
    data.append(
        {'_index':'hoge-index', '_type':'hoge-index-type', '_source':doc}
    )

helpers.bulk(es, data)

100件などの単位で分割して insert したい場合、 for ループ内で適当な件数で bulk method を呼び出すようにすれば良い。

感想

Embulk で insert するのもとても楽だったが、Elasticsearch の helpers を利用して簡単に bulk insert が実装できた。
Elasticsearch の helpers には他にも便利な method が多くあるのでやりたいことを helpers 内の method で実現できるかを考えたほうが良さそうだなと思った。