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 で実現できるかを考えたほうが良さそうだなと思った。

DockerCompose で ES + Kibana 環境を構築しました!

前回の記事で ElasticSearch と Kibana の両方を Docker で起動する方法を紹介した。

toohsk.hateblo.jp

しかし、 Docker の Link などを使って管理していたので、手動で管理するものがたくさんあり、大変だった。
なので、今回は Docker Compose を利用する方法を紹介する。

Docker Compose

今回の ElasticSearch と Kibana のように、複数のコンテナを連携して管理する方法をオーケストレーションという。
このオーケストレーションの方法はいくつかあり、今話題の Kubernetes などがあるが、今回はその一つの Docker Compose を使った。
Kubernetes の場合、複数の pods 間で service discovery を行ってくれる機能や pods のスケーリングなどの機能がある。
Docker Compose は kubernetes のような複雑な機能はないが、一つのファイルで複数のコンテナを管理できる。
なので、今回のように実験的に環境を作ることに関して言えば、 Kubernetes は必要過多なサービスになると判断して、
Docker Composeでコンテナのビルド、起動、シャットダウンなどを一括で管理できるメリットを取った。

ES + Kibana with Docker Compose

複数のファイルにまたがるので、 Github 上にリポジトリを作成しました。

github.com

ElasticSearch も Kibana も 6.4.0 を利用している。

ElasticSearch

日本語の文章を解析するためには形態素解析器を使って文章を品詞ごとに分解する必要がある。
そのため、 ElaskticSearch の Dockerfile では、形態素解析器に elasticsearch-analysis-kuromoji のパッケージをダウンロードした。
パッケージをダウンロードするために標準入力でインストールを許可する入力が必要となる場合がある。
Docker でイメージをビルドする際、標準入力できないので、今回は echo y を利用して y を標準入力している。

docker-compose.yml

このファイルの中で ElasticSearch と Kibana のイメージを管理している。
yaml ファイル形式になっており、読み方は key-value として読んでいくと良いかと思う。
詳しくは、リファレンス Compose ファイル・リファレンス — Docker-docs-ja 17.06.Beta ドキュメント を読むのがいいと思う。 このリポジトリ内で注意するべき点がいくつかあるので、以下にメモしておく。

build

4行目と24行目に build key がある。
これは value 名のディレクトリに存在する Dockerfile をビルドする。
なので、ここの valuedocker-compose.yml がある構築用コンテクストのパスを指定する必要がある。
そして、各行の次の行で container-name でコンテナ名をつけている。こちらはときにディレクトリとは関係ないので自分が好きな名前をつければ良い。
ちなみに build 済みのイメージを利用することもでき、 image を利用し、イメージ名を指定すればよい。
詳しくは、 Compose ファイル・リファレンス — Docker-docs-ja 17.06.Beta ドキュメント に記載されている。

volumes

Docker は基本的にコンテナ内のデータは揮発性であり、毎回コンテナを起動すると初期状態で起動される。
つまり、前回 ElasticSearch に入力したデータは残らない。
データを保存したい場合、 docker の data volume として保存する必要がある。
今回は、 docker-compose.yml の30行目で data volume をローカルの docker 環境に構築し、利用するようにしている。
なので、真新しい環境を構築するためには、コンテナの再構築・再起動以外に、この volume を削除する必要がある。

辞書のマウント

kuromoji で形態素解析を行うにあたり、カスタマイズした辞書を利用するように template に記載している。
そのため、 docker-compose.yml の8行目で辞書をマウントしている。
この辞書は csv 形式で1行が一つの形態素解析の結果を表しており、
本文中の形式,形態素解析の結果,ヨミ,品詞 の形にしなければならない。

まとめ

今回は DockerCompose を利用した ES + Kibana 環境を構築した。
ES 環境で日本語の解析を試しに行いたい場合は、お手軽に試せるので是非活用してください。

わからないところなどはコメントや Github の issue にお願いします!

Docker で ElasticSearch + Kibana 環境を構築してみた。

本業で ElasticSearch を使うシステム開発を行うことになった。 機能の POC を確認するため、 Docker で ElasticSearch と Kibana を起動することにした。 自分は Docker for Mac を利用している。

Note

Linux 環境や Mac ユーザだけど Docker for Mac を利用されていない方は、各コンテナに接続確認する場合の localhost 部分を適宜 $(docker-machine ip <machine-name>) に置換してください。

ネットワーク

複数のコンテナを利用するので、 Bridge ネットワークでコンテナ同士をつなぐ。

docker network create elasticsearch --driver bridge

コンテナ

ElasticSearch コンテナ起動

docker run -d \
    -e "http.host=0.0.0.0" \
    -e "transport.host=127.0.0.1" \
    -e "xpack.security.enabled=false" \
    -e "xpack.monitoring.enabled=false" \
    -e "xpack.watcher.enabled=false" \
    -e "xpack.graph.enabled=false" \
    -e "xpack.ml.enabled=false" \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -p 9200:9200 \
    -p 9300:9300 \
    --name elasticsearch \
    --network="elasticsearch" \
    docker.elastic.co/elasticsearch/elasticsearch:6.4.0

ElasticSearch コンテナが起動しているかを確認するには、

curl localhost:9200

でレスポンスを見てみる。 以下、みたいなレスポンスが返ってきた。

{
  "name" : "MAzzjYI",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "wEvHKuikQyeE1Ca7UogUZQ",
  "version" : {
    "number" : "6.4.0",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "595516e",
    "build_date" : "2018-08-17T23:18:47.308994Z",
    "build_snapshot" : false,
    "lucene_version" : "7.4.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

Kibana コンテナ起動

docker run -d \
    --name kibana \
    -p 5601:5601 \
    -e "ELASTICSEARCH_URL=http://elasticsearch:9200" \
    -e "xpack.graph.enabled=false" \
    -e "xpack.security.enabled=false" \
    -e "xpack.ml.enabled=false" \
    --network="elasticsearch" \
    docker.elastic.co/kibana/kibana:6.4.0

Kibana が起動しているかはブラウザでチェックする。

open http://localhost:5601

f:id:toohsk:20180903203020p:plain

できた。

所管

恐らく本当は k8s とか何かしらのオーケストレーションツールを使って管理するのが望ましい気がする。 本当は kubernetes を使ってやりたいけど、一旦、次は docker-compose を使ってみようかな。

Prestoで2つの配列を同時に行に変換する

最近、Treasure Dataに触る機会が増えた。 その中で1行に2つのカラムにそれぞれコンマ区切りのデータがあり、それを行展開するのにハマったポイントがあったのでメモしておく。 そして長くなるので、最初に結論を載せておく。

どういうデータか

以下のようなデータ形式を想定している。

column1 column2
a,b,c 1,2,3

そして、このデータを以下のデータ形式に展開する。

column1 column2
a 1
b 2
c 3

前提としては、column1もcolumn2も同数の要素数が含まれていることとする。

どうするか

以下のように CROSS JOIN UNNEST の中に複数の split 関数を入れる。

SELECT 
  clmA,
  clmB
FROM
  tbl1
CROSS JOIN
  UNNEST(
    split(
      tbl1.column1,
      ','
    ),
    split(
      tbl1.column2,
      ','
    )
  ) AS t(clmA, clmB);

これにより上記のような展開ができます。

どういうことをしたか

ググってみると、Prestoでカンマ区切りのデータを展開した記事はよく出てくる。

qiita.com

これを見たときは、『そっか、じゃあ CROSS JOIN UNNEST をもう1段挟めばいいのか』
と安易に考えてしまいました。 ざっと以下のような感じ。()

SELECT 
  t1.clmA,
  t2.clmB
FROM
  tbl1
CROSS JOIN
  UNNEST(
    split(
      tbl1.column1,
      ','
    ) AS t1(clmA)
CROSS JOIN
  UNNEST(
    split(
      tbl1.column2,
      ','
    )
  ) AS t2(clmB);

そうすると以下のような結果が得られた。

column1 column2
a 1
a 2
a 3
b 1
b 2
b 3
c 1
c 2
c 3

まさに CROSS JOIN。

なので、次は、『そっか、じゃあWITH句をつかって、一回テーブルとして展開してから、ID付きで展開すればうまくいくんじゃね?』 と思い、やりました。 が、やはり上記のようなクエリの結果になってしまいました。 なかなか奥深いですね…

勉強会に登壇してきました。

機械学習エンジニアとして副業をしていることもあり、FindyでCTOしている @ma3tk からのお誘いもあり登壇してきました。

engineer-parallel-work.connpass.com

発表資料はこちら。

speakerdeck.com

副業をしていて感じていることを話してきた。

まだ初めて数ヶ月だが、いい振り返りになった。
視座を高めるために知識の input だけでなく、 output を行い経験値を増やすこと。
そのサイクルを回すことで自身の視座を高くしていく努力をしていくこと。
それを再認識できた発表だった。

懇親会であった質問

ものすごくありがたいことに参加者から、『発表が良かった』と言ってくださる方が多くいらっしゃった。
その方々といろいろ話すことができたので、質問と自分の考えをまとめておく。

始め方はどうされたのですか?
僕の場合、スライドに載せているFISMという会社のCTOをしている @kzkohashi と同僚であり、 そのつながりで始めることができた。 元々興味はあったが、なかなか始める決意がつかなかったが、とてもいいきっかけをくれたと思っている。

いいなと思っている会社があり、そこで副業できればなと思っていますが、どう思いますか?
副業として受け入れてくださるのであれば、積極的に取り組めばいいと思っている。
正直、外から見ていていいなーと思う会社はいっぱいある。 ただ、入ってみて、あれ?と思うこともある。 そうなってしまったときに、現職を退職してまでって後悔がすごく、これが現職にとどまり続ける要因の一つなのかと思う。
そういう意味では副業として参加し、正社員と肩を並べて働いてみて、直に会社の文化に触れることはいいスタートの切り方なのではないだろうか? そして、思ったより肌に合わないと判断できた場合は、信頼を稼いでつながりを構築することに専念すればいいと思っている。

アウトプットの仕方どうしていますか?
データ分析の場合、データを分析して最後の可視化と考察の部分が重要だと思っている。
結局のところ期待されているのはアウトプットであり、例えば、

  • 必要なタスクに対して実現可能なのか不可能なのか
  • 不可能な場合、だいたい手段はあるのかもっと作業を進めれば可能性はでてくるのか

などが期待されているのではないだろうか。
データ分析といえばデータの前処理が重要であることは重々承知している。 しかし副業だと、発注者側にデータ分析を理解してくれる人がいない場合、 最初から前処理をコツコツやっていても、「何してるんですか?」ってなると思う。
それよりも、 「既存のデータを使ってこうしました。ここの部分の改善をすることで精度があがると思うので、次にここを着手します。」 と道筋を見せて上げたほうが納得感が得られるし、自分自身必要な処理が見えてくるはずである。
なので、アウトプットを細かく見せることはすごく重要だと考えている。

ポートフォリオはどうされていますか?
ここで悩んでいる人が多かった印象だった。
正直、自分は知り合いの相談から副業がスタートしているので、ポートフォリオとかを考えなくても良かった。 しかし、もっといろんな会社の案件を触ってみたいなと思ったときに、ポートフォリオをつくらないとなとなった。
恐らく、ポートフォリオとして上がるのは、「エンジニアならgithubだろ!」というのが定石なのではないだろうか。 その声がすごく多かったし、それには賛成である。
しかし、機械学習のエンジニアはデータがないとなかなかアピールポイントがないので、 githubにコードを上げることは難しいのではないかと思う。
そこで僕はブログもセットでポートフォリオを組むといいのではないかと思っている。
データ前処理の中で得られた気づきは、大抵、gistやgithubに上げるほどではない。 だが、その一部をブログとして残すことで自分がどの程度データに触れているのかというのをアピールすることに繋げられる。
積もり積もればれっきとした履歴書であるし、コード以外の考えをアピールすることができるのでよいと思う。 ブログのアウトプットについては @kakakakku のブログに対する考え方は参考になるので、 一度は読まれたほうがいいと思う。

kakakakakku.hatenablog.com

最後に

今回は本当に貴重な会で登壇させていただけた。 今後もいろんな勉強会に登壇できるよう、自分の成長に繋げられるように input/output を引き続きがんばろう。

PandasでJSONファイルを保存するときの注意点

個人的にはTensorFlowでモデリングするようになってから、numpyを触る頻度が高くなって、それに伴いpandasを触る機会が減っていった。
データの可視化もpyplotとかseaboneとかを使って、可視化の方法を調べながら模索している状況が増えていった。

しかし、先日副業の案件で学習データをサクッと可視化させようと思ったときに選んだのは、pandasだった。

やはりpandasは優秀かな

テキストデータを読み込んで、異常値を探すとかその場でフィルタリングしようかと思うと、
やはりpandasは楽だなと改めて思たった。(ただnumpy力が低いだけかもしれないが…) 例えば、とりあえずテキストに保存したデータセットをDataFrameに読み込んで、
データセットの概要を知りたいなーと思ったら、

df.describe()

とすれば、カラム毎の最大値・最小値、四分位数とかを出してくれるし、
histグラムで可視化したいなーとおもったら、

df.plot.hist(y=[<可視化するカラム名>], bins=<ビン数>)

とすれば、指定したカラムだけのヒストグラムが可視化できて、すごく楽だった。
分布はどのような形なのか、どの範囲で異常値を切り出すのかなど判断して
そのままDataFrameでフィルターすればいいので、短時間でデータセットのチェックと更新ができた。

DataFrameの保存には注意が必要

前提

前提として、データセットJSON形式で保存しており、 すでに実験するノートブックを用意してある、としてます。
なので、同じJSONの形式で保存することで実験のノートブックを再利用することが前提です。

問題点

通常、PythonでdictionaryをJSONとして保存した場合、

import json


dict_list = []

for i in range(10):
    dict_obj = {}
    dict_obj["single"] = i
    dict_obj["double"] = i * 2
    dict_obj["square"] = i * i
    dict_list.append(dict_obj)

with open('dict2json.json', 'w') as f:
    json.dump(dict_list, f)

のような処理になる。 そして、この場合テキストにはざっくりと

[{"single": 0, "double": 0, "square": 0}, {"single": 1, "double": 2, "square": 1}, {"single": 2, "double": 4, "square": 4}, {"single": 3, "double": 6, "square": 9}, {"single": 4, "double": 8, "square": 16}, {"single": 5, "double": 10, "square": 25}, {"single": 6, "double": 12, "square": 36}, {"single": 7, "double": 14, "square": 49}, {"single": 8, "double": 16, "square": 64}, {"single": 9, "double": 18, "square": 81}]

このような形式で保存されている。
いわゆる、key-valueが配列に格納されている形である。 なので、配列の1要素が1行(1レコード)のデータを表現している。

そして、何も意識せずにDataFrameをJSONファイルとして保存すると痛い目に遭った。 それが今回の教訓だった。

PandasでJSONを読み込むには、

import pandas as pd

df = pd.read_json('dict2json.json')

とすればよい。 そして、このDataFrameをJSONとして保存する場合、以下のように書けば良い。

df.to_json("df2json.json")

人によりけりだとは思うが、簡単で直感的だ。ただし、この場合保存されたファイルの中身は、

{"double":{"0":0,"1":2,"2":4,"3":6,"4":8,"5":10,"6":12,"7":14,"8":16,"9":18},"single":{"0":0,"1":1,"2":2,"3":3,"4":4,"5":5,"6":6,"7":7,"8":8,"9":9},"square":{"0":0,"1":1,"2":4,"3":9,"4":16,"5":25,"6":36,"7":49,"8":64,"9":81}}

となる。 形としては、カラム毎に、 "index: value" の形で保存されている。

この場合、保存されたファイルを

with open('df2json.json') as f:
    json_data = json.load(f)

として読み込むと、1レコードずつとして読み込まれず、keyが想定していたものと異なる結果になる。

Key-Valueの形式で保存するには、

DataFrameをkey-valueの形式のJSONに保存するときには orient オプションに record を指定する必要があることに注意する。

df.to_json(filtered_data_filepath, orient='records')

そうすると、

[{"double":0,"single":0,"square":0},{"double":2,"single":1,"square":1},{"double":4,"single":2,"square":4},{"double":6,"single":3,"square":9},{"double":8,"single":4,"square":16},{"double":10,"single":5,"square":25},{"double":12,"single":6,"square":36},{"double":14,"single":7,"square":49},{"double":16,"single":8,"square":64},{"double":18,"single":9,"square":81}]

と保存された。

JupyterLabでアニメーションが動作しないときの対処

背景・目的

最近、深層強化学習を勉強していて、教材としては

https://www.amazon.co.jp/dp/4839965625

を使っている。 コードはノートブックに記述していて、JupyterLabを利用している。 Toy problem として自作で3×3マスの迷路を自作して、選択した行動をアニメーションとして描画するところで問題があったのでメモしておく。

エージェントが通るパスをアニメーションとして表示するコードを部分的に抜粋して載せておく。

from matplotlib import animation
from IPython.display import HTML
%matplotlib inline

def init():
    line.set_data([], [])
    return (line,)

def animate(i):
    state = state_history[i]
    x = (state % 3) + 0.5
    y = 2.5 - int(state / 3)
    line.set_data(x, y)
    return (line,)

anim = animation.FuncAnimation(fig, animate, init_func=init, 
                               frames=len(state_history), interval=200, repeat=False)
anim.save("maze.gif", writer = "imagemagick")
HTML(anim.to_jshtml())

state_historyにはエージェントが通るパスのが保存されていて、それをアニメーションとして動かす。 今回問題になったは HTML(anim.to_jshtml()) の部分である。

アニメーションとして動かすには

Jupyter内で図をアニメーションとして動かすには、 matplotlibanimation を利用すると簡単に実現できる。 実際に、書籍では恐らくJupyter Notebookで実験することを想定しており、簡単にアニメーションを動かすことができた。

しかし、JupyterLabではJavaScriptの問題のためかアニメーションを再生することができない。 この問題はGithub上でissueとして上がっている。 ここでは、animationのオブジェクトにある to_jshtml() を使っているが、 その他にも to_html5_video() 関数があり、これを使ってもアニメーションを再生できなかった。

どうやって解決したか

JupyterのNotebookはコードのみでなく、Markdownとして文章を記述することが可能である。 今回は、アニメーションをgifとして保存し、そのファイルをMarkdownで読み込みアニメーションを再生することで回避した。 上記のコードの中の

anim.save("maze.gif", writer = "imagemagick")

でアニメーションをgifとして保存している。 ここで保存したファイルをMarkdown

<div class="pull-left">
![Animated GIF](maze.gif)
<div>

と記述した。 これにより、Jupyter NotebookでもJupyterLabでもアニメーションを表示することができた。 難点としては、アニメーションが無限にリピート再生されてしまうところくらいかなと思っている。 それよりも簡単に可視化されどのような挙動になるのか、可視化されながら説明できる点のほうがはるかに重要だと思うので、 回避方法が見つかってよかった。 おなじ問題に出くわしている人の解決方法になれば幸いです。 また、JupyterLabで to_jshtml()to_html5_video() などコードでアニメーションが実現できた人はコメントなどでぜひとも教えてください!