読者です 読者をやめる 読者になる 読者になる

ABEJA Tech Blog

株式会社ABEJAのエンジニアたちがAI, IoT, Big Dataなど様々なテーマについて発信します

機は熟した!グラフ構造に対するDeep Learning、Graph Convolutionのご紹介

はじめまして。ABEJAでResearcherをやらせていただいている白川です。

先日、化合物の物性推定をDeep Learningをつかって従来手法より300,000倍高速に処理するという論文がでました([1], [2])。この論文の手法は、Graph Convolutionというグラフ上に定義されたConvolution演算がベースとなっています。物性推定に限らず、グラフ解析全般を Deep Learning で上手にこなせるようになれば、Deep Learningのアプリケーションの幅がぐっと拡がり、さらなるイノベーションが起きそうな予感がします。 ICMLやNIPSなどの機械学習系の主要国際会議でも数年前からGraph Convolutionについての論文がちらほら出現しはじめており、とくに最近その勢いが増してきている印象があります。個人的にも最近(前から?)にわかにグラフづいていたので、この機会にグラフ上のDeep Learning、とくにGraph Convolutionについて整理して、このEmerging Technologyを盛り上げていければと思います!

グラフとして表現されるデータ

ここでいうグラフとは棒グラフとか円グラフとかのグラフではなくて、何かと何かがつながっている様を表すネットワーク構造のことを表します。 ネットワークと言うとニューラルネットワークのネットワークや通信ネットワークなどと混同しがちなので、ここではグラフ/Graphと呼ぶことにします(もっとも、ニューラルネットワーク、通信ネットワークもグラフ/ネットワークであることは間違いないですね)。 グラフはノードがエッジでむすばれることにより構成されます。エッジに向きがついているとき有向グラフといい、そうでないとき無向グラフといいます。同一ノードを結ぶエッジ(ループ)を考えたり、同じノード間に複数のエッジ(多重エッジ)が張られた状況を考える場合もあります。

f:id:daynap1204:20170425111114p:plain

グラフの典型的な例としては、ソーシャルネットワークがあります。

f:id:daynap1204:20170424171825p:plain

皆さんの身近にもグラフの例は無数にあります。それこそ「つながり」が存在するものすべてはグラフといえるかもしれません。 グラフやグラフ上でのアクティビティなどすぐに思いつくだけでも

  • Internet
  • Web
  • 電力供給網
  • 交通網
  • 購買データ(誰が何を買ったか)
  • 動画や音楽の視聴履歴
  • Webアクセス履歴
  • 論文の共著関係
  • メールのやりとり
  • サプライチェーン
  • 化合物の分子構造
  • タンパク質
  • センサーネットワーク
  • 感染症の伝播経路
  • 進化系統樹
  • 神経系
  • 脳内の各部位の関係性ネットワーク
  • 3Dオブジェクトのポリゴン表現
  • プログラムのクラス図

などなど。 こちらのWebサイトでは様々なデータをグラフとして描画していて眼に楽しいです。

www.visualcomplexity.com

この他にも画像やテキストもグラフとして捉えることができます。

f:id:daynap1204:20170426004520p:plain

画像の場合、グラフのノード=ピクセルにはRGBの色情報が載っています。テキストの場合は単語を出現順に結んだグラフ(上の遷移)となります。 テキストについては、係り受け関係でグラフをつくってもよいかもしれません。 このように画像やテキストなどの従来からDeep Learningで取り扱われてきた対象も、その上部構造としてグラフ構造を想定することができます。

後々のため、グラフに対する要件を整理しておきましょう。

  • エッジには向きがついている場合(有向)とそうでない場合(無向)がある
  • ふたつのノードの間に複数種のエッジが張られている場合がある
  • ループ(同一ノードを結ぶエッジ)がありえる
  • ノードには属性が紐付いていることがある(画像のRGBなど)
  • エッジには属性が紐付いていることがある
  • エッジには重みが紐付いていることがある、等々

Graph Convolution

グラフ上でDeep Learningを展開する技術、なかでもGraph Convolutionと呼ばれる技術が最近のトレンドになってきていると感じています。 以下では、Graph Convolutionの実現方法について、いくつかの論文を引き合いに出しながらご説明したいと思います。

どこがむずかしいのか

実は最近までGraph上のDeep Learningは一部の研究者を除くと、それほど注目されていなかったように感じます。個人的にはそれは技術的な理由によるところが大きいと考えています。 というのも、最近のDeep Learningのキーとなる構成要素はConvolutionという操作なのですが、これをGraph上に適切に定義するのが一筋縄ではいかないのです (発展途上の技術のため、Deep Learningのフレームワークのサポートが十全ではないのも理由かもしれません)。

まずは画像の場合のConvolutionを考えてみます。 Convolutionは画像の各ピクセルに対してその周辺のピクセルの値を重み付けして足し合わせる操作のことを指します。 この際、「周辺のピクセル」としては3x3とか5x5といった、注目しているピクセル中心の正方形領域(フィルターと言います)を考えることが多いです。 またConvolutionの重みは、いずれのピクセルを中心に考えるときにも同じ重みを使います。たとえば下図([3])の場合、青色の7x7の画像上を3x3のフィルターが走査することで5x5の出力が得られますが、このフィルターの3x3の各マス目には固有の重みが紐付いていて、各出力はその重みで配下の画像のピクセル値を重み付け和することで計算されます。

f:id:daynap1204:20170417175330g:plain

Graphについても画像と同様にConvolutionを定義したいのですが、画像の場合と異なり決定的な難点が立ち現れます。それはグラフの注目ノードの周りの周辺ノードの接続関係が注目ノードごとに不定形であるかもれしれないという点です。

f:id:daynap1204:20170426133624p:plain

この難点をヒューリスティックもしくは理論的なアプローチにより解決したのが今回ご紹介するGraph Convolutionになります。

Graph Convolution の構成

Graph Convolutionの構成方法を思い切って分類すると、下記の2タイプになります。

  1. Graph Fourier変換を利用した構成
  2. より直接的な構成

それぞれご説明します。

Graph Fourier 変換を利用した Graph Convolution の構成

といいつつ以下の議論は少し技術的になってしまうので、読み飛ばせるように先に結論だけまとめておきます。

  • ループや多重エッジをもたない重み付き無向グラフに対しては、理論由来のわりと正統的な方法で Graph Convolution が定義できる

もうちょっと詳細(技術的)には、

  1. ループや多重エッジを持たない重み付き無向グラフに対しては Graph Fourier 変換という、通常の意味での Fourier 変換のアナロジーを定義することができる
  2. Graph Fourier変換に対してConvolution Theoremを適用することでGraph Convolutionを定義することができる
  3. うまくパラメータ付けすることで、ノードの隣接関係を考慮したGraph Convolutionも定義できる([6])

という具合になります。Graph Fourier変換とは、グラフ上の信号(ノードに対して付与されたベクター値)に対して定義される、Fourier変換に似た操作です。 Fourier変換は波形信号を周波数成分ごとに成分分解する変換ですが、Graph Fourier変換はグラフ上で定義された信号を「ゆるやかな信号」や「急峻な信号」へ成分分解する変換となります。

もし仮にGraph Fourier変換が定義できたとすると(構成方法はあとで説明します)、ConvolutionはFourier Domainにおける要素積に相当するという定理(Convolution Theorem)

$$\displaystyle \widehat{f * g} = \widehat{f} \odot \widehat{g} \ \ \ \ ( \widehat{f}はfのFourier変換、*, \odotはそれぞれConvolution, 要素積を表す) $$

を適用することで

  1. グラフ上の信号(= 各ノードに割り振られた特徴ベクター)に対してGraph Fourier変換を施す
  2. 変換された信号に対して、何かと要素積をとる
  3. 要素積の結果に対して逆Graph Fourier変換を施す

というようにして間接的にグラフに対してConvolutionを定義することができます。 ここで2の「何か」はFourier領域において表現されたConvolutionです。

Graph Fourier 変換

以下の議論はだいぶ技術的になります。詳細については[5]を参照してください。 ループや多重エッジをもたない、重み付き無向グラフ $ \mathcal{G}=(\mathcal{V}, \mathcal{E}, \mathcal{W}) $を考えます。$ \mathcal{V}, \mathcal{E} $はそれぞれグラフ$ \mathcal{G}$のノード、エッジの集合です。$ \mathcal{W}$は $(i,j)$ 成分 $W_{i,j} \geq 0$ がエッジ $(i,j)$ の重みを表す対称行列です。$N=|\mathcal{V}|$をグラフ$\mathcal{G}$のノード数とします。

Graph Fourier変換はグラフ $\mathcal{G}$ 上の信号に対するある種の信号変換として定義されます。ここでグラフ上の信号とは、グラフの各ノードに対する $d$ 次元ベクターの割り当てのことを指します。以下の図([5]より引用)は、$d=1$ のケースの信号です。$d>1$ の場合、こういった1次元信号が $d$ 個あると思ってください。

f:id:daynap1204:20170425114703p:plain

Graph Fourier変換のしたいことは、与えられた信号を「なだらかな信号」や「急峻な信号」など、信号の緩急に応じた成分へ分解することです。 $\mathbf{x} \in \mathbb{R}^{N}$ を1次元信号とするとき、信号のなだらかさや急峻さを下記で定義します。

$$ \displaystyle \sum_{i, j} {W_{i, j} ( x_i - x_j )^2} \tag{1}. $$

上式を最小化する(つまりもっともなだらかな)信号は定数信号です。そのうちから二乗ノルムが 1 のものをとって、これを $\mathbf{u}_0$ と表すことにします。 $i=0,1,2,…,N-2$に対して帰納的に

$$ \displaystyle \mathbf{u}_{i+1} = \mathop{\arg \min}\limits_{\mathbf{x} \in \mathbb{R}^{N}, | \mathbf{x} | = 1, \mathbf{x} \perp { \mathbf{u}_0, … , \mathbf{u}_i } } \sum_{i, j} W_{i, j}(x_i - x_j) ^ 2 $$

と定めると、${ \mathbf{u}_0, …, \mathbf{u}_{N-1} }$ は $\mathbb{R}^N$ の正規直行系になります。

なお、(1)式はGraph Laplacian $L$を用いて簡易に表現することができます。 Graph Laplacianは、Graphの次数行列(Degree Matrix)、隣接行列(Adjacency Matrix)を用いて下図([4])のように定義される行列です。 次数行列は、対角成分に各ノードの次数=接続エッジ数を並べたもの、隣接行列は $(i,j)$ 成分に $i$ 番目のノードと $j$ 番目のノードにエッジが張られている場合に1を、そうでない場合に0を並べた行列です。

f:id:daynap1204:20170425120416p:plain

このGraph Laplacianを用いると(1)式は $$\displaystyle \sum_{i, j} {W_{i, j} ( x_i - x_j )^2} = 2 x^T L x $$ と表現されることが知られています。さらに${ \mathbf{u}_0, …, \mathbf{u}_{N-1} }$は$L$の固有ベクターとなることもわかります(これらは簡単な計算で確かめられます)。${ \mathbf{u}_0, …, \mathbf{u}_{N-1} }$はたとえば下記のような具合になります([5])。 f:id:daynap1204:20170427003642p:plain

さて本題のGraph Fourier変換ですが、これは $\mathbf{x}$ の基底 ${ \mathbf{u}_0, …, \mathbf{u}_{N-1} }$ に関する直行変換

$$ \displaystyle \mathbf{x} = \sum_k \alpha_k \mathbf{u}_k, \ \ \ \ \ $$

における係数ベクターへの対応 $\mathbf{x} \rightarrow \widehat{\mathbf{x}} = \mathbf{\alpha} = (\alpha_0, \alpha_1, …, \alpha_{N-1})$ として定義されます(ここで $\alpha_k = \langle \mathbf{x}, \mathbf{u}_k \rangle$ は$\mathbf{x}$と$\mathbf{u}_k$の内積です)。$U=(\mathbf{u}_0, …, \mathbf{u}_{N-1} )$ととれば、Graph Fourier変換は$\widehat{\mathbf{x}} = U^T \mathbf{x}$と簡潔に表現できます。逆Graph Fourier変換を、$\widehat{\mathbf{x}} \rightarrow \widehat{\widehat{\mathbf{x}}} = U \widehat{\mathbf{x}}$として定義します。

Graph Fourier変換、逆Graph Fourier変換が定義できたので、Graph Convolutionを定義することができます。Convolution TheoremによるとConvolutionはFourier変換後には要素積として表現されるので、 乗じる係数ベクターを$\mathbf{\theta}=(\theta_0, …, \theta_{N-1})$とすると、Graph Convolutionは

  1. グラフ上の信号(=各ノードの特徴ベクター)に対してGraph Fourier変換を施す: $\mathbf{x} \rightarrow \widehat{\mathbf{x}} = U^T \mathbf{x}$
  2. 変換された信号に対して、$\mathbf{\theta}$と要素積をとる: $\widehat{\mathbf{x}} \rightarrow \mathbf{\theta} \odot \widehat{\mathbf{x}}$
  3. 要素積の結果に対して逆Graph Fourier変換を施す: $ \mathbf{\theta} \odot \widehat{\mathbf{x}} \rightarrow U (\mathbf{\theta} \odot \widehat{\mathbf{x})}$

となります。合わせるとGraph Fourier変換により定義されるGraph Convolutionは $$ \displaystyle \mathbf{x} \rightarrow U (\mathbf{\theta} \odot (U^T {\mathbf{x}))} \tag{2} $$ という対応になります。いまは1次元信号を考えましたが、$d$次元信号($d \ge 1$)を考える場合は、各次元ごとにconvolutionをした結果を総和します。

なお、上記は比較的一般的な構成でしたが、$L$の$(\mathbf{u}_0, \mathbf{u}_1, …, \mathbf{u}_{N-1})$に対応する固有値を$\mathbf{\lambda} = (\lambda_0, \lambda_1, …, \lambda_{N-1})$として、それらを対角にならべた行列を$\Lambda = diag(\lambda_0, \lambda_1, …, \lambda_{N-1})$とすると、実対称行列の固有分解により$L=U \Lambda U^T$ですので、$\mathbf{\theta}=\mathbf{\lambda}$ととったとき(2)式は $$ \displaystyle \mathbf{x} \rightarrow U (\mathbf{\theta} \odot (U^T {\mathbf{x}))} = U \Lambda U^T \mathbf{x} = L\mathbf{x} $$ と簡単に表されます。この特殊な場合のGraph Convolutionは各ノードごとにそのノードの隣接ノードのみに依存します。同様に$L^2 \mathbf{x}, L^3 \mathbf{x}, …$は各ノードから2エッジ、3エッジ、…離れたノードにのみ依存します(局所性)。論文[6]では$L$のかわりに$L$の$K$次多項式 $g(L) = \sum_{k = 0}^K \alpha_k L^k$ ($\alpha_k$ はパラメータ)を作用させることで局所性を考慮したGraph Convolutionを提案しています。 また、論文[7]では$g(L)$の次数を一次($K=1$)までに限定したGraph Convolutionをノードの半教師あり分類問題に適用する手法を提案しています。 これらの手法については、下記の記事も参考になります。

www.inference.vc

より直接的なGraph Convolutionの構成

Graph Fourier変換由来の定義の場合、技術的な理由から、ループや多重エッジをもたない、重み付き無向グラフに限定する必要があります。 ですが、そもそもGraph Fourier変換を介してGraph Convolutionを定義する必然性はあるのでしょうか? グラフの接続関係は既知なので、これだけを使って定義する事はできないのでしょうか?(そもそもGraph Laplacian自体、接続関係のみから定義されるような代物でした)

そのような観点にたつと、より直接的なGraph Convolutionの定義をしたくなります。例によってこちらも技術的になるので先に結論を述べておきます。

  • ノードやエッジの接続関係のみから、より直接的にGraph Convolutionを定義することができる(理論由来ではない)
  • こうして定義されたGraph Convolutionには有向グラフや多重エッジ、ループなどのより複雑な構造も自然に導入できる

なお、冒頭に上げたGoogleによる化合物の物性推定へGraph Convolutionを適用した際のGraph Convolutionはこちらの直接的な定義にもとづいています。 ほかにもKnowledge Baseの解析にも適応されたりしています[8]。

どう定義するか

考え方の基本は簡単です。多種データの取り扱いに困ったときは、ひとまず線形和にしてNeural Networkに入力。これだけです。非常にDeep Learning的な考え方です。

具体的なケースで考えやすいように、[8]にしたがってKnowledge Baseを例にとって考えてみます。ここではKnowledge Baseを「AはBである(A, is-a, B)」とか「AはBを持っている(A, has-a, B)]のような3つ組が羅列された集合とします。このときKnowledge Baseは下図([8]より引用)のように、A, Bをノードとしてis-aやhas-aなどの関係性をAからBへの有向エッジとする多重有向グラフとして表現されます

f:id:daynap1204:20170426104936p:plain

このグラフの場合、下記を考慮する必要があります。

  • エッジは有向
  • ノードに属性がある

目標は、各ノード$v$にそのノードの何らかの属性を表す信号$\mathbf{x}_v \in \mathbb{R}^d$が乗っているものとして、それらの信号を他のノードとの接続関係をもとに Graph Convolution を定義して、信号を更新していくことです。最終的にそうして更新されていった信号からノードの属性予測や未知の関係性の予測を行います。先にあげたKnowledge Baseの図で言えば、赤いノード、エッジが予測により補完されるべきノードの属性、関係性になります。

[8]ではこれらを下記のように構築します。

f:id:daynap1204:20170426105040p:plain

この図の意味するところを説明します。まず、各ノード$v$ごとにその隣接ノードを接続のされ方に応じて分類します。具体的には、

  • ノード$v$に入力されたエッジか出力されたエッジか
  • そのエッジの表す関係性は何か

の2種の情報の組み合わせとして接続のされ方を区別します。これらの接続のされ方ごとにノード$v$の近傍が変わるものと考えます。 ついで、それぞれの接続のされ方に応じた近傍ごとに、近傍ノードの信号の総和(もしくは平均)をとった後、接続のされ方に応じた線形変換を施し足しこんでいきます。これにより、エッジの向きやエッジの関係性を加味した足し込みがなされますが、この足し込まれた表現をこのケースでのGraph Convolution操作として定義します。[8]で提示されている式を示しておきます(ノード $i$ の信号の更新式になっています。Graph Convolution後に非線形変換$\sigma$を施しています)。

$$ \displaystyle h^{(l+1)}_i = \sigma \left( \sum_{r \in \mathcal{R}} \sum_{j \in \mathcal{N}^r_i} \frac{1}{c_{i, r}} W^{(l)}_r h^{(l)}_j + W^{(l)}_0 h^{(l)}_i \right). $$

コンセプトが非常にわかりやすい論文なので、[8]はオススメです。考え方が素直なので、グラフがより複雑になっても素直に拡張できそうですね。

冒頭でご紹介したGoogleの化合物物性予測の論文([1],[2])は、既存のGraph Convolutionの実現法を

  1. message (近傍からどんな情報を取得するか)
  2. update (messageからどのように信号を更新するか)
  3. readout (グラフの信号から「グラフ自体」の特徴をどう読み取るか)

に機能整理した上でフレームワーク化しています(MPNN, Message Passing Neural Networks framework)。 既存研究の整理としては非常にわかりやすい論文です。 具体的なモデルや実装の詳細があまり明らかになっていないのですが、物性予測のための手法としては、今後、こういったGraph Convolution由来の 手法がベンチマークとなっていくのではないでしょうか。

おわりに

お付き合い頂きありがとうございます。 Graph Convolutionはまだまだ発展途上の技術ですが、最近になってようやく機が熟してきた感があります。 静的なグラフの解析だけでなく、RNN/LSTMと組み合わせることで動的なグラフの解析を可能にする手法([9],[10])や、テキストの係り受け関係を捉えるのにGraph Convolutionを適用した論文([11])など、Graph Convolutionはすでに要素技術として確立してきています。 本記事執筆中に開催されている、Deep Learning周辺の先端的な話題を取り扱う国際的なworkshop会議ICLR2017にも、Graph Convolution関連の論文が数本採択されていたりと、これから間違いなく来る技術と信じていますので、これからも継続的に研究していきたいと思います。

宣伝

ABEJAでは最先端技術にピピっとくる、イケててヤバいエンジニアを募集しています。 一緒にGraph Convolutionを盛り上げていきたい方、その他Emerging Technologyを開拓していきたい方等々、是非お待ちしております!

ABEJAが発信する最新テクノロジーに興味がある方は、是非ともブログの読者に!

ABEJAという会社に興味が湧いてきた方はWantedlyで会社、事業、人の情報を発信しているので、是非ともフォローを!! www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓

参考文献

[1] J. Gilmer et al., “Neural Message Passing for Quantum Chemistry”, arXiv preprint arXiv:1704.01212, 2017

[2] https://research.googleblog.com/2017/04/predicting-properties-of-molecules-with.html

[3] https://github.com/vdumoulin/conv_arithmetic

[4] https://en.wikipedia.org/wiki/Laplacian_matrix

[5] D. I. Shuman et al., “The Emerging Field of Signal Processing on Graphs: Extending High-Dimensional Data Analysis to Networks and Other Irregular Domains”, IEEE Signal Processing Magazine, 30(3):83–98, 2013

[6] B. Defferrard et al., “Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering”, NIPS2016

[7] T. N. Kipf et al., “Semi-Supervised Classification with Graph Convolutional Networks”, ICLR2017

[8] M. Schlichtkrull et al., “Modeling Relational Data with Graph Convolutional Networks”, arXiv preprint arXiv:1703.06103, 2017

[9] Y. Seo et al., “Structured Sequence Modeling with Graph Convolutional Recurrent Networks”, ICLR2017

[10] F. Manessi et al., “Dynamic Graph Convolutional Networks”, arXiv preprint arXiv:1704.06199, 2017

[11] D. Marcheggiani et al., “Encoding Sentences with Graph Convolutional Networks for Semantic Role Labeling”, arXiv preprint arXiv:1703.04826, 2017

【IoT】SORACOM AirとRaspberryPiで作るインフルエンザ注意報

f:id:hiroyuki_abeja:20170309201833j:plain

はじめに

初めまして。新卒2年目エンジニア、大田黒(オオタグロ)です。主に、ABEJA Platformの開発を担当しています。この記事では、会社で使っている技術について紹介しつつ、簡単なIoTデバイスとアプリケーションのメイキングについて書きます。

モチベーション

デバイスを作り始めた当初(2月)、周囲ではインフルエンザが流行していました。 ちょうどその時、趣味で購入したRaspberryPiとSORACOM AirのSimカード(+モデム)が眠っていたので 何か作ってみようと思い立ちました。

厚生労働省 平成28年度インフルエンザQ&Aによると空気が乾燥すると気道粘膜の防御機能が低下し、インフルエンザにかかりやすくなるとあります。

空気の乾燥を通知することができれば「加湿器を動かす」「マスクを付ける」等のアクションのきっかけを作る事ができ、インフルエンザ対策になります。今回は空気の乾燥を検知し、Slackに自動通知してくれるIoTデバイスを作ろうと思います。

作成物

全体像

f:id:hiroyuki_abeja:20170309091434p:plain Fig.1 「インフルエンザ注意報」のシステム構成

デバイス

役割:特定のデータ(今回の場合は湿度)を取得&加工し、アプリケーション側(後述)にデータ・イベント情報をおくる

今回は、下記の理由から「Raspberry Pi」というARMプロセッサ搭載の小型軽量のボードコンピューターを利用します。

  • 本体の入手性が良い
  • 利用例が多く、資料の入手性が良い
  • 価格が比較的安い
  • センサと融合しやすい
  • LinuxベースのOSが動く
  • USBポート/LANポートが存在

通信ネットワーク

役割:デバイスとアプリケーション間でのデータの橋渡しをする

「どこでも持ち運びができるシステムにしたい!」というコンセプトの元、3G/LTE回線を利用します。 今回は下記の理由からIoT向けのデータ通信サービス(MVNO)として「SORACOM Air」、モデムとして「AKA-020」を利用します。

  • SIMの入手性が良い(最近はAmazonでも手軽に購入できる)
  • SIMの有効化・各種設定が全てWebコンソール上でできる
    • クレジットカードがあればすぐに有効化できる
    • 利用状況をWebから確認できる
  • 細かな設定をAPIから制御できる
  • 閉域網の構築が容易 (今回は使いません)

アプリケーション

役割:データを受け取り、解析・保存・アクションを行う

今回は、アプリケーション構築のためにFunction-as-a-Service(FaaS)の一種である「AWS Lambda」というサービスを利用します。 FaaSを活用すると「サーバ」という管理単位を意識しなくても、アプリケーションを構築することができます。

最近はFaaSを積極的に活用した「サーバレスアーキテクチャ」による開発を行っています。 今回は、業務で使っている技術紹介も兼ねて、アプリケーション部分をAWS Lambdaを使って実装を行います。

  • インフラ環境の整備の必要がなく、開発に集中できる
    • 実行に必要なインフラの準備を行わなくて良い
    • スケーラビリティや可用性の確保を自動で行ってくれる
  • サーバを常に稼働させるより安い
    • イベントの実行回数・実行時間で課金される
  • Python / Node.js / Java等でコーディングできる

参考:サーバレスアーキテクチャという技術分野についての簡単な調査

必要なもの

部品リスト

f:id:hiroyuki_abeja:20170309200006j:plain

TABLE 1 部品表

部品名 型番・性能 入手先
RaspberryPi 3 ModelB 秋月電子 通販コード(M-10414)
スイッチングACアダプタ5V2.5A AD-B50P250 秋月電子 通販コード(M-10507)
ブレッドボード EIC-801 秋月電子 通販コード(P-00315)
3G USBドングル AK-020 SORACOM スターターキット Amazon
湿度センサ TDK CHS-GSS 千石電商 管理コード6A4A-HRFF
A/D変換器 MCP3204 秋月電子 通販コード(I-00239)
高精度温度計 LM35DZ 秋月電子 通販コード(I-00116)
ブレッドボード・ジャンパーワイヤ 秋月電子 通販コード(P-00288)
ブレッドボード・ジャンパーワイヤ(オス-メス) 秋月電子 通販コード(P-03472)

各パーツの役割について

パーツを「ブラックボックス」のまま使いたくない方のために、代表的なパーツの役割・諸元について簡単に説明します。

A/D 変換器(MCP3208)

一般的にセンサとデバイスを接続する時は、センサで計測される温度・湿度といった「物理量」を電圧といった「電気信号」に変換して扱う事が多いです。一方で、電気信号(アナログ)は、デジタル論理で動くコンピューターでは直接扱う事ができません。従って、センサから得た電圧(連続値)をコンピューターで扱えるデジタル値に変換するための「変換器」が必要となります。この役割を持つのがA/D変換器(Analog to Digital Converter)と呼ばれるものです。

f:id:hiroyuki_abeja:20170328005932p:plain:w200

Fig.2 MCP3208のピン配置 (転載元:データシート)

※後述の作成過程において必要になります。印刷しておくと便利です。

今回、A/D変換器としてMCP3208を利用します。MCP3208には以下のような特徴があります。

  • 入力:8本
    • 図中のPin 0〜7を使う事で8本の電圧信号をA/D変換できる
  • 分解能:12bit (4096段階)
    • 基準電圧を用いて4096段階で入力された電圧を表現できる
  • 変換速度:100ksps (電源5V時)
    • 1秒間に100000回のA/D変換ができる (1sps = 1 sample per sec)
  • シリアル・ペリフェラル・インタフェース(SPI)を利用
    • 4本の通信用信号線(図中のPin10,11,12,13)を使い、デジタル信号でデータのやり取りができる
    • 一般的な通信手法であるため、幅広いデバイスと接続できる

温度センサ(LM35DZ)

f:id:hiroyuki_abeja:20170403042803p:plain:w300

Fig.3 LM35DZのピン配置 (転載元:データシート)

今回は温度センサとして、TI社LM35DZを利用します。電源を供給するだけで、温度に比例した電圧が中央のピンから出力される仕組みになっています。従って、前述のA/D変換器に直接接続し、デバイス側で読み込む事ができます。

データシートによると出力電圧と温度の関係は、0 [mv] +10[mV/℃]とあるため、10℃時に100[mV] 、20℃時に200[mV]の電圧が出力されます。 この関係を利用することで、デバイス内部で読み込んだ電圧から温度を計算可能です。

湿度センサ(CHS-GSS)

f:id:hiroyuki_abeja:20170403043626p:plain:w200

Fig.4 CHS-GSSのピン配置 (転載元:データシート)

今回は湿度センサとしてTDK社のCHS-GSSを利用します。このセンサも前述の温度センサと同様で、電源を供給するだけで湿度に比例した電圧が出力される仕組みになっています。湿度センサも同様、A/D変換器に直接接続できます。データシートによると、100%(RH)時に1.0[V]が出力されます。この関係を利用することで、デバイス内部で読み込んだ電圧から湿度を計算可能です。

事前準備

今回作成を進めるにあたり、下記のものが必要になります。

  • SORACOM ユーザーコンソール用のアカウント
  • AWS アカウント
  • HDMIケーブル
  • LANケーブル
  • USBキーボード
  • AWS コマンドラインインターフェイス(aws-cli)
  • Slack API トークン
  • SD <=> MicroSD変換機 (任意)
  • SSHクライアント(任意)

作り方

f:id:hiroyuki_abeja:20170310015011p:plain

Fig.5 作成回路の全体感

Raspberry Piのセットアップ

MicroSDカード作成

f:id:hiroyuki_abeja:20170403045229p:plain:w300

Fig.6 Raspbianのダウンロード画面

Download Raspbian for Raspberry Piより、RaspberryPi用のイメージをダウンロードし、OSイメージをMicroSDカードに書き込んでください。

※環境によってSDカードの作成手順が異なります。Macの方は下記ページを参考にしてください。

参考:Mac OS X で Raspberry PiのOSイメージを焼く

動作チェック

SDカード作成後、RaspberryPiにHDMIケーブル・USBキーボード・LANケーブルを接続し、電源アダプタを接続してください。 電源アダプタを接続すると、RaspberryPi上の緑色LEDが点灯します。

f:id:hiroyuki_abeja:20170328000947j:plain

Fig.7 RaspberryPi 動作時の様子

下記のような画面が出れば成功です。

f:id:hiroyuki_abeja:20170403030421j:plain

Fig.8 RaspberryPi OSブート時の画面

SPI通信の有効化

RaspberryPiをAD変換器に接続するにあたって、SPI通信用のカーネルモジュールを有効化します。 下記のコマンドから、raspi-config(RaspberryPiの設定ツール)を開き、「7 Advanced Options」→「A6 SPI」へと進み、SPI通信を有効化してください。

$ sudo raspi-config

f:id:hiroyuki_abeja:20170403045327p:plain:w300

Fig.9 raspi-config起動の様子

回路作成

この章では、実際に回路構築を行います。配線ミスはデバイスやパーツの故障に繋がるため、慎重に作業を行ってください。

RaspberryPiとA/D変換器の接続

接続対応表
RaspberryPi側端子名 RaspberryPi側ピン番号 MCP3208側端子名 MCP3208側ピン番号 説明
DC Power 5V #02 Vdd #16 電源供給用
DC Power 5V #02 Vref #15 基準電圧供給用
Ground #06 DGND #09 電源供給用
Ground #06 ANGD #14 電源供給用
SPI_MOSI #19 Din #11 SPI通信データ転送用
SPI_MISO #21 Dout #12 SPI通信データ転送用
SPI_SCLK #23 CLK #13 SPI通信クロック供給用
SPI_CE0_N #24 CS/SHDN #10 SPI通信スレーブ選択用

表中のピン番号と物理的なピン配置の関係は下記の通りです。

f:id:hiroyuki_abeja:20170403030837p:plain:w400

Fig.10 Raspberry Pi のピン配置

上記の図は、公式サイトより転載しました。 MCP3208のピン配置はFig.2をご確認ください。

実際の配線方法

実際の配線には、部品表のブレッドボードとジャンパーワイヤを使って接続を行います。 接続対応表・下記の実体配線図を接続を行ってください。

f:id:hiroyuki_abeja:20170403024229p:plain

Fig.11 実体配線図

下記の写真は、配線完了時の様子です。

f:id:hiroyuki_abeja:20170328001105j:plain

Fig.12 実際の接続後の様子

A/D変換器とセンサの接続

今回はMCP3208のCH0(チャンネル0)に温度センサ、CH1(チャンネル1)に湿度センサを接続します。

接続対応表
温度センサ

TABLE MCP3208 <=> LM35DZ 接続対応表

MCP3208側端子名 MCP3208側ピン番号 LM35DZ側端子名 LM35DZ側ピン番号 説明
Vdd #16 +Vs #01 電源供給用
CH0 #01 Vout #02 センサー出力取り込み用
DGND #09 GND #03 電源供給用
湿度センサ

TABLE MCP3208 <=> CHS-GSS 接続対応表

MCP3208側端子名 MCP3208側ピン番号 CHS-GSS側端子名 CHS-GSS側ピン番号 説明
CH1 #02 Vout #01 センサー出力取り込み用
DGND #09 GND #02 電源供給用
Vdd #16 +Vs #03 電源供給用
実際の配線方法

RaspberryPiとMCP3208の時と同様、ジャンパーワイヤを使って接続を行ってください。 CHS-GSSは部品表面にピン配置が書いてありますが、LM35DZは書いてありません。 接続ミスしないように注意してください。

f:id:hiroyuki_abeja:20170403035328p:plain

Fig.13 実体配線図

f:id:hiroyuki_abeja:20170403041541j:plain

Fig.14 実際の接続後の様子

SORACOM Airの準備

大まかな流れは下記のとおりです。

  1. SIMのアクティベーション
  2. SIMのモデムへのマウント
  3. RaspberryPi側の設定

SIMのアクティベーション

f:id:hiroyuki_abeja:20170328023103p:plain Fig.15 SIM登録画面

「SORACOM ユーザーコンソール」にログイン後、「➕SIM登録」を押してください。 上の様な画面が出てくるので、そこに必要な情報を記載してください。 (SIMカードのマウントされているプラスチックの裏側に、IMSI等が記述されています)

SIMカードのマウント

f:id:hiroyuki_abeja:20170328021818j:plain

Fig.16 SIMカードマウント後の様子

アクティベーションが完了したので、SIMカードを利用することができます。このSIMカードをモデムに挿すことで利用ができるのですが、サイズが異なるため、直接挿すことができません。 スターターキットにはサイズ変換用アダプタが同封されています。このアダプタを使い、写真のようにSIMカードをマウントしてください。

PPP接続準備

SORACOM AirのSIMを使いモデムからPPP接続を行うために、wvdialを導入する必要があります。 wvdialはPPP(Point-to-Point Protocol)のダイアラであり、これを使うことで楽にRaspberryPiからPPP発信が可能です。 下記のコマンドからwvdialをインストール可能です。(ejectは後述のスクリプトで利用します)

$ sudo apt-get update
$ sudo apt-get install -y eject wvdial

wvdialのための設定ファイルを生成します。

接続用シェルスクリプトの用意

下記のスクリプトを任意の場所に配置してください。 自動起動スクリプト(/etc/rc.local)から呼び出すため、実行権限を付ける必要性があります。

スクリプトパス例:/home/pi/connect_with_soracom.sh

接続テスト

f:id:hiroyuki_abeja:20170328025547p:plain Fig.17 PPPリンクが確立している様子 (SSH経由で確認)

上記のシェルスクリプトを実行し、ifconfigを実行するとppp0という項目が確認できると思います。 ppp0がしばらく待っても生成されない場合、何かしらの問題が発生している可能性が高いです。

プログラミング(アプリケーションサイド)

この章では、AWS Lambda上で動かすコード(Lambdaファンクション)の作成方法・API経由で呼び出す方法について説明します。 ここからの作業はAWSのWebコンソールからでも可能ですが、最近業務で使っていて便利だったAWS コマンドラインインターフェイス(aws-cli)を使って説明を行います。

CLIの初期設定

$ aws configure

上記のコマンドを実行すると以下4項目が順番に聞かれるので、適切に入力してください。 AccessKeyId・SecretAccessKeyは、AWS Identity and Access Management (IAM)から取得する事ができます。

AWS Access Key ID [None]: xxxxxxxxxx
AWS Secret Access Key [None]: xxxxxxxxxx
Default region name [None]: us-east-1 (ご自身の環境にあわせてください)
Default output format [None]: json

Lambdaを使ったアプリケーションの構築

これから実際にAWS コマンドラインインターフェイスを使ったアプリケーション構築の説明にうつります。 作業手順に登場するREGION,ACCOUNT_IDは、各自の環境に置き換える必要性があります。

Lambdaファンクションの準備

下記のスクリプトは、今回のアプリケーションの中核を担うAWS Lambda上で動くファンクションです。Lambda上のランタイム Python 2.7で動くことを想定して書いています。

スクリプトパス例:<WORK_DIR>/lambda_function.py

※ スクリプト中のSlack用APIトークンですが、事前に取得したAPIトークンに書き換える必要性があります。

本スクリプトでは、SlackClientというライブラリを利用しています。 しかしLambda上ではpipインストールが利用できないので、サードパーティ製ライブラリを直接導入する事ができません。今回は、Lambdaファンクションを必要なライブラリと一緒にZipで圧縮し、Lambdaに登録します。

Lambdaファンクション登録

ここまででLambdaファンクションの準備ができました。次にLambdaファンクションの登録を行います。 大まかな流れは下記のとおりです。

  1. Lambda実行用ロール作成
  2. Lambdaファンクション登録

API Gatewayの設定

前述のLambdaファンクションをAPI経由で呼び出すために、API Gatewayの設定を行います。 今回は下記の前提条件のもと、設定を行います。

  • デバイス側から送信したデータは、POSTメソッドを使いJSON形式でデータを受け取る
  • 今回は認証はもうけない
  • ステージ名はdevとする

大まかな流れは下記のとおりです。

  1. API作成&定義
  2. APIとLambdaの連携・権限付与
  3. APIのレスポンス設定
  4. APIのデプロイ

ここまででAPI Gatewayの設定は完了です。 API Gatewayで作成したエンドポイントは、https://<REST_API_ID>.execute-api.<REGION>.amazonaws.com/<STAGE>/<ENDPOINT>の形でアクセスが可能になっています。

今回の例では、https://ou89mo95s6.execute-api.us-east-1.amazonaws.com/dev/fluReportとなります。

プログラミング(デバイスサイド)

ここまででアプリケーションサイドの構築が終わりました。 ここからは、アプリケーションにデータを送るデバイスサイドのプログラミングを行います。

PiPiperの導入

今回、RaspberryPiからA/D変換器にSPI通信でアクセスする為にPiPiperというライブラリを利用します。 基本的には下記のコマンドで準備が可能です。

$ sudo apt-get install ruby ruby1.9.1-dev libssl-dev
$ sudo gem install pi_piper

計測・データ送信用スクリプト

下記のスクリプトは、RaspberryPi上で動く計測(A/D変換)・データ送信用のスクリプトです。 大まかな処理の流れは以下のとおりです。

  1. SPI通信でA/D変換器と通信し、電圧データを取得する
  2. 電圧データから温度[℃]・湿度[%]を計算する
  3. API Gatewayに送る用のオブジェクトを生成する
  4. API Gatewayにjson形式でデータ送信
  5. 一定時間待つ
  6. 1へ戻る

このスクリプトを実行する事で、定期的に温度・湿度データがアプリケーション側へと送信されます。 スクリプト内の送信先エンドポイントは、前のステップで作成したエンドポイントを指定する必要性があります。

スクリプトパス例:/home/pi/sender.rb

※ このスクリプトはSPI通信を行うため、管理者権限が必要です。sudoを付けて実行する必要性があります。

自動起動化

ここまでで最低限のアプリケーションの用意が終わりました。 最後にPPP接続自動化と計測・データ送信用スクリプトを自動起動する設定を行います。

/etc/rc.localに下記の行を追加してください。

次から電源投入をする事で、自動でAPI Gatewayに計測データが転送されるようになります。

動作風景

f:id:hiroyuki_abeja:20170309201833j:plain

Fig.18 オフィス内にデバイスを設置したときの様子

作成したデバイスを早速で社内でランニングさせました。 上記の図は、デバイス設置時の様子です。

f:id:hiroyuki_abeja:20170309202234p:plain:w200

Fig.19 Slackでの通知の様子

この日は加湿器が電源OFFの状態で暖房が動いていたため極端に湿度が低く、電源を付けたら早速アラートが飛んでいきました。 社内の様子を見ていると、アラートを見た社員が加湿器の電源を入れる・予防マスクを付けるといった狙い通りアクションを実施していることが確認できました。今回のIoTデバイスの運用によって社内のインフルエンザに対する危機意識向上に貢献できたと思います。

記録したデータを可視化すると、以下のような感じになりました。 手元の温度計・湿度計と比較したところほぼ一致していました。計測の妥当性は問題なさそうです。

f:id:hiroyuki_abeja:20170309203905p:plain

Fig.20 温度変化プロット

f:id:hiroyuki_abeja:20170309203916p:plain

Fig.21 湿度変化プロット

感想

業務で得た知識を活かせている

今回使っている「SORACOM Airを用いたデータ通信」や「サーバレスアーキテクチャ」ですが、入社後に業務を通して得た知識であり、業務ではもちろん、趣味の開発でも積極的に利用しています。今まで、Webアプリケーションを構築する時は、

  1. 最初に仮想(物理)サーバを用意
  2. プロビジョニングを実施
  3. ファイアウォールを設定
  4. Nginx等のミドルウェアをインストール
  5. アプリケーションをフレームワークを使って記述
  6. デプロイ
  7. 必要に応じてロードバランサーを配置

といった作業を行っていました。今では、

  1. 必要なインターフェースを定義(API Gateway)・認証設定
  2. 必要なロジックを記述(AWS Lambda)
  3. デプロイ

上記のプロセスで済むので、機能の高速な実装・仮説検証のイテレーションが回るようになっています。

技術的な成長を感じた

実は私は2015年にABEJAに入社したのですが、入社当時はアプリケーション開発に関する知識・経験がなく、開発に必要な知識のインプットから始まりました。(バッググラウンドが生体医工学・電気電子工学だったので….) 1年目は、デバイスからプラットフォームまで開発・運用の実践経験をひたすら積みました。ブログ執筆を通して入社時当時の自分を思い出し、成長できたなと感じる事ができました。

自社プロダクトのバリューの再認識

ABEJA Platformは、Deep Learning を活用し、様々な大量データの取得・蓄積・学習・解析・出力・フィードバックを行うことができる先進的なプラットフォームです。

今回は、社内技術の中身を記事にする都合上、AWS Lambda等を利用したアプリケーション構築を行いましたが デバイスとABEJA Platformを連携させれば、もっと楽にアプリケーション構築が可能です。今回の例でいうと、デバイスと通信ネットワークを用意するだけで、データの取得・蓄積・データの解析・フィードバック(Slackへの通知)をABEJA Platformをおまかせできます。

本記事ではデバイス管理・認証の仕組み・データ管理の仕組みを一切実装していませんが、実際に大量のIoTデバイスの繋がるアプリケーション部分を作るためにはこれらの仕組みを実装する必要があり大変です。これらの仕組みもABEJA Platformにおまかせできるため、エンジニアは本質的なバリューに集中できます。

今回は、デバイスからアプリケーションまでの一通りの実装を通して自社で開発しているプラットフォームのバリューについて改めて理解できました。

最後に

IoT・ビッグデータ・AIをキーワードにバリバリ活躍したい方!! ご興味のある方は以下のリンクより、Wantedlyページをご確認ください。

ABEJAが発信する最新テクノロジーに興味がある方は、是非ともブログの読者に!

ABEJAという会社に興味が湧いてきた方はWantedlyで会社、事業、人の情報を発信しているので、是非ともフォローを!! www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓

pandas DataFrameを省メモリにpickleする

ABEJAでデータエンジニアをしています、千葉です。

少し前に、pandasのDataFrameをファイルに読み書きする際にメモリを消費しすぎる問題を発見したので、解決策を含めて紹介します。

通常手法の紹介

通常、DataFrameをファイルに保存する際には、pandasの提供するIOモジュールを使用します。

今回は、細かい変換規則を書く必要のないPython Pickleをベースとしたto_pickle機能について取り上げます。

# Dumping pandas.DataFrame
import pandas
df = pandas.DataFrame(..., columns=...)

df.to_pickle(output_file_path)
# Restoring pandas.DataFrame
import pickle

with open(input_file_path, 'rb'):
    df = pickle.load()

上記のようにして、非常に簡単にDataFrameを完全に入出力できます。

通常手法の課題

ここで、注意すべき点として、pickleのメモリ効率の悪さが挙げられます。 実際に実メモリサイズがGBクラスのDataFrameを作成し、パフォーマンスを測定したログが下記になります。

テストコード

GB = 1024 * 1024 * 1024
df = None

def prepare_data():
    global df
    row_count = 40000000
    print('generating data.')
    print('row count', row_count)
    series_1 = numpy.random.randn(row_count)
    series_2 = numpy.random.randn(row_count)
    series_3 = numpy.random.randn(row_count)
    df = pandas.DataFrame({'a': series_1,
                           'b': series_2,
                           'c': series_3})
    return df


def run_to_pickle():
    result_path = 'run_to_pickle.bin'
    df.to_pickle(result_path)


def run_load_pickle():
    result_path = 'run_to_pickle.bin'
    df = pickle.load(open(result_path, 'rb'))

結果

-------prepare_data-------
generating data.
row count 40000000
sizeof df: 2305.9765625MB

-------run_to_pickle-------
Maximum memory usage: 4137.2109375MB
Elapsed time: 3.702019843040034

-------run_load_pickle-------
Maximum memory usage: 2322.0859375MB
Elapsed time: 5.678721209987998

※各関数の実行後には、ガベージコレクションを実行しています。

※メモリ使用量の測定には、memory_profiler.memory_usageを使用しています。

※経過時間の測定には、timeitを使用しています。

残念ながら、約2GBのDataFrameに対して、出力時に最大約4GBほどのメモリを使用しています。およそ2倍です。

調査した結果、

  • オブジェクトのコピーが発生すること
  • 逐次入出力をできないこと

が原因のようです。

解決手法

今回の解決手法では、下記のような性質を持つ入出力用のラッパークラスを設計します。

  • DataFrameをチャンク化して逐次入出力をさせることで、最大メモリ使用量を削減する。
  • pandas系オブジェクトをnumpy系オブジェクトに変換して扱うことで、高速化・小型化する。

イメージ図 - エンコーディング

f:id:archibay:20170202152706p:plain

イメージ図 - デコーディング

f:id:archibay:20170202152710p:plain

コード例

# 部品番号と部品データ格納用のクラス
# indexは、複数のDataFrameを一括で入出力するための索引
class SerializationParts(object):
    def __init__(self, name, index, data):
        self.name = name
        self.index = index
        self.data = data

    def blueprint(self, binary_size):
        return SerializationParts(self.name, self.index, binary_size)

SerializationPartsに部品番号と、部品データを格納します。 設計図生成時には、バイト長を記録するようにします。

# propertiesに登録された変数を SerializationParts に格納し、
# yieldする機能のベースクラス
class WrapperBase():
    properties = []

    def __custom_encode__(self, property_name, index):
        # Please override this function
        yield SerializationParts(name=property_name, index=index, data=getattr(self, property_name))

    def encode(self, index):
        # 最初にクラス情報をパーツとして作成
        yield SerializationParts('base', index, self.__class__)
        for property_name in self.properties:
            # プロパティをそれぞれパーツとして作成
            for a_parts in self.__custom_encode__(property_name, index):
                yield a_parts

    def __custom_decode__(self, parts):
        # Please override this function
        setattr(self, parts.name, parts.data)

    def decode(self, parts):
        self.__custom_decode__(parts)

WrapperBaseに、逐次パーツ作成機能を持たせておきます。 custom_encode、 custom_decodeをオーバーライドすることで、サブクラスは 逐次処理を制御できます。

# DataFrameのラッパークラス

class DataFrameWrapper(WrapperBase):
    properties = ['df', 'max_rows']

    def __init__(self, df, max_rows=1000000):
        super(DataFrameWrapper, self).__init__()
        self.df = df
        self.max_rows = max_rows

    def __custom_encode__(self, property_name, index):
        max_rows = self.max_rows
        # dfプロパティは複数パーツに分割
        if property_name == 'df':
            # 列名、型、Indexを構造情報としてパーツ化
            structure = {'index': numpy.array(self.df.index),
                         'columns': list(self.df.columns),
                         'dtypes': list(self.df.dtypes)}
            yield SerializationParts(['df', 'structure'], index, structure)
            row_counts = len(self.df.index)
            # 数値データは、列毎・max_rows毎にパーツ化
            for series in list(self.df.columns):
                series_name = series
                row_loop_count = 1 + int(row_counts / max_rows)
                for i in range(row_loop_count):
                    # numpy.arrayを使うことで、飛躍的に省メモリ化・高速化を実現できる。
                    yield SerializationParts(
                        ['df', series_name, i], index,
                        (max_rows,
                         numpy.array(self.df[series_name][max_rows * i: min(max_rows * (i + 1), row_counts)])))
        else:
            # dfプロパティ以外は、WrapperBaseの機能を使用
            return super(DataFrameWrapper, self).__custom_encode__(property_name, index)

    def __custom_decode__(self, parts):
        if isinstance(parts.name, list) and parts.name[0] == 'df':
            if parts.name[1] == 'structure':
                l = len(parts.data['index'])
                dtype_dict = OrderedDict()
                for k, v in zip(parts.data['columns'], parts.data['dtypes']):
                    # numpy.arrayを使うことで、飛躍的に高速化を実現できる。
                    dtype_dict[k] = numpy.ndarray(shape=l, dtype=v)
                # 現状、最も高速な行数固定の空DataFrameの生成処理。
                self.df = pandas.DataFrame(dtype_dict, index=parts.data['index'])
                del dtype_dict
            else:
                row_counts = len(self.df.index)
                max_rows, series_data_parts = parts.data
                series_name = parts.name[1]
                series_parts_no = parts.name[2]
                series_data_parts = pandas.Series(series_data_parts)
                # 数値データの設定処理
                self.df[series_name][max_rows * series_parts_no: min(max_rows * (series_parts_no + 1), row_counts)] = \
                    series_data_parts
        else:
            # dfプロパティ以外は、WrapperBaseの機能を使用
            super(DataFrameWrapper, self).__custom_decode__(parts)

WrapperBase クラスを継承し、pandas.DataFrame 用のラッパーを作成します。 プロパティ df に pandas.DataFrame をセットできるようにします。 構造情報の別パーツ化、数値データのChunk処理を行い、逐次処理可能にします。 エンコード・デコードの過程に、pandas.Seriesやpandas.DataFrameの使用を極力避け、 numpy.arrayを使用することで、パフォーマンスを飛躍的に向上させることができます。

# エンコーダークラス
class Encoder(object):
    def __init__(self, data_file, blueprint_file=None):
        self.data_file = data_file
        if blueprint_file is not None:
            self.blueprint_file = blueprint_file
        else:
            self.blueprint_file = '{data_file}.bp'.format(data_file=self.data_file)

    def encode(self, data: [WrapperBase]):
        data_out = open(self.data_file, 'wb')
        blueprint_out = open(self.blueprint_file, 'wb')
        print('Start encoding', self.data_file)
        print('Dumping blueprint')
        blueprint = {'blueprint': [], 'data_length': len(data)}
        for i, d in enumerate(data):
            # パーツを逐次エンコード
            for parts in d.encode(i):
                # 現在のカーソルを記録
                current_cursor = data_out.tell()
                print('Encoding %s.%s' % (i, parts.name))
                # パーツのデータ部分を書き出し
                pickle.dump(parts.data, data_out, protocol=-1)
                # 書き込み後のカーソルを記録
                new_cursor = data_out.tell()
                # ブループリントにカーソル移動量(パーツデータのバイト長)を記録
                blueprint['blueprint'].append(parts.blueprint(new_cursor - current_cursor))
        pickle.dump(blueprint, blueprint_out, protocol=-1)
        logging.info('Finish encoding')

データファイルと設計図ファイルに分けて格納します。 pickleを使用して部品データをエンコードし、バイト長を設計図ファイルに書き出します。

# デコーダークラス
class Decoder(object):
    def __init__(self, data_file, blueprint_file=None):
        self.data_file = data_file
        if blueprint_file is not None:
            self.blueprint_file = blueprint_file
        else:
            self.blueprint_file = '{data_file}.bp'.format(data_file=self.data_file)

    def decode(self) -> [WrapperBase]:
        data_in = open(self.data_file, 'rb')
        blueprint_in = open(self.blueprint_file, 'rb')
        print('Start decoding', self.data_file)
        print('Loading blueprint')
        # ブループリントデータの読み込み
        blueprint = pickle.load(blueprint_in)
        # initialize output_data
        data = [None for _ in range(blueprint['data_length'])]
        for bp in blueprint['blueprint']:
            # ブループリントのバイト長分だけ読み込み
            parts_data = data_in.read(bp.data)
            index = bp.index
            name = bp.name
            # バイトデータを読み込み
            bp.data = pickle.loads(parts_data)
            if name == 'base':
                data_class = bp.data
                # 空インスタンスの作成
                instance = data_class.__new__(data_class)
                data[index] = instance
            else:
                data[index].decode(bp)
        logging.info('Finish decoding')
        return data

データファイルと設計図ファイルから読み込みます。 設計図に記録された部品番号とバイト長を元に、pickleを使用してデコードします。

# テストコード
def run_my_encode():
    result_path = 'run_my_encode.bin'
    encoder = Encoder(result_path)
    encoder.encode([DataFrameWrapper(df=df)])


def run_my_decode():
    result_path = 'run_my_encode.bin'
    decoder = Decoder(result_path)
    df_wappers = decoder.decode()
    df = df_wappers[0].df

結果

-------prepare_data-------
generating data.
row count 40000000
sizeof df: 2305.9765625MB

-------run_to_pickle-------
Maximum memory usage: 4137.2109375MB
Elapsed time: 3.702019843040034

-------run_load_pickle-------
Maximum memory usage: 2322.0859375MB
Elapsed time: 5.678721209987998

-------run_my_encode-------
Maximum memory usage: 2465.16015625MB
Elapsed time: 3.972677645040676

-------run_my_decode-------
Maximum memory usage: 2184.8671875MB
Elapsed time: 4.480759038007818

※run_my_decode時のメモリ使用量が、元のdfよりも少なくなっていますが、デコード結果には正しい数値が格納されていました。

下記のようにパフォーマンスが改善しました。

  • 出力時の実行時間がほぼ変わらず、メモリ使用量が約40%改善。
  • 入力時の実行時間が約20%改善し、メモリ使用量がほぼ変わらない。

ただし、あらゆる構造のDataFrameに対応しているかというと、Noです。 例えば、列が入れ子になっている場合などは、正しくエンコードできません。

まとめ

pandas.DataFrameの入出力パフォーマンスを改善するラッパーを作成し、

  • 出力時のメモリパフォーマンス
  • 入力時の実行時間

を大幅に改善するすることに成功しました。

デメリットとしては、標準の方法に比べて対応可能なDataFrameが限定的な点です。

みなさまも、大きなデータの入出力を行う際には、 ライブラリを信頼しすぎずに、パフォーマンスチェックをしてみてはいかがでしょうか。

宣伝

ABEJAでは、技術課題を解決し、ブレイクスルーを生み出すエンジニアを募集しています。 今回の記事をきっかけに、少しでも興味が湧いた方は、ぜひ一度話を聞きに来てみてください。

ABEJAが発信する最新テクノロジーに興味がある方は、是非ともブログの読者に!

ABEJAという会社に興味が湧いてきた方はWantedlyで会社、事業、人の情報を発信しているので、是非ともフォローを!! www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓

PythonでScalaのようなlambda式を書いてみた。

f:id:higumachan:20170119112038p:plain

はじめに

はじめまして、ABEJA最年少メンバーでリサーチャーをやっている日熊です。 普段は、Deep Learningに関する研究をやっています。 仕事ではPythonを使っていますが、実際はScalaとかRustとかHaskellに最近ハマっています。

本日は趣味で作ったPythonのライブラリについて紹介します。 この記事では主に以下の様なことが起こります。

  • PythonでScalaっぽいlambda式を書けるようにした
  • どうやってlambda式を実現したのか説明
  • 既存ライブラリより高速だった

目次

今回作ったもの

Scalaのようなlambda式をPythonで実現するライブラリを目指して作りました。 Scalaでは以下のようなコードが書けます。

val numbers = Array(1, 2, 3, 4, 5)
val sum = numbers.reduceLeft[Int](_+_)
 
println("" + sum)

上のコードで出力が

15

となります。

注目すべき点は

val sum = numbers.reduceLeft[Int](_+_)

です。

_ + _

と書くと2変数を受け取る関数になっています。

Pythonで書くと

numbers = [1, 2, 3, 4, 5]
s = reduce(lambda x, y: x + y, numbers)
 
print(s)

となります。

numbers = [1, 2, 3, 4, 5]
s = reduce(_ + _, numbers)
 
print(s)

Pythonでもこう書けると気持ちいい。
そう思いませんか? そう思いますよね!
僕はそう思ったので、このライブラリを作ってしまいました。

github.com

試したい方はこちら
pip install pyscalambda
from pyscalambda import _

print((_ + 10 * (_ - 20 ))(1, 30))
print(all(map(_.isdigit(), ["1", "2", "3"]))
print(all(map(_.isdigit(), ["1", "2", "3", "a"]))

実行例

101
True
False

実際、Pythonで以下のようなライブラリを使いながら、関数型プログラミングっぽいことをするとlambda式が出まくって煩雑に思えてくる場面が多々あります。

実装概要紹介

実装方針

実装に当たって以下の3つの指針を建てました。

  • 早く作る
  • 速く作る
  • なるべく、おもしろそうな道を通ってみる (趣味なので)

上の指針に則って以下の様な流れの実装になりました。

  1. 演算子のhookingをしてオレオレ構文木を作る
  2. オレオレ構文木で表された式をlambda式コードの文字列に変換
  3. 引数リストを作る
  4. 定数辞書を作る
  5. lambda式コードの文字列をevalすることにより、lambda式に変換
  6. 出来上がったlambda式を実行する。 (evalを使う辺りがおもしろそうな道ですかね)

また、そのほかの実装上での工夫としては以下があります。 - lambda式のキャッシュ - _を引数に含む関数の呼び出し

ここでは

10 + -_ * 2

という式がいかにして

lambda x: 10 + -x * 2

というラムダ式に変換されるかを追っていくとします。

1. 演算子のhookingをしてオレオレ構文木を作る

演算子のhookingは以下のように行いました。

class Formula(object):
    def __add__(self, other):
        return self.do_operator2(other, "+")

    def __sub__(self, other):
        return self.do_operator2(other, "-")

    def __mul__(self, other):
        return self.do_operator2(other, "*")
...
    def __pos__(self):
        return self.do_operator1("+")

    def __neg__(self):
        return self.do_operator1("-")

    def __invert__(self):
        return self.do_operator1("~")

    def __getattr__(self, method):
        return self.do_methodcall(method)

    def __getitem__(self, key):
        return self.do_getitem(key)

このようにPythonのclassには演算子のoverloadがついているので、 すべての演算子をoverloadしたようなFormula(式)というclassを作ります。

このFormulaを継承した

  • Operator1(単項演算子)
  • Operator2(二項演算子)
  • Operand(オペランド)
  • MethodCall(メソッド呼び出し)
  • FunctionCall(関数呼び出し)
  • Underscore(引数オペランド)

このFormulaを継承したものがオレオレ構文木の葉(Operand, Underscore)や節(それ以外)になります。

継承関係は以下になってます。

f:id:higumachan:20170118115020p:plain

これにより、上のようなpythonの式を下のような構文木のような形に変換します。

10 + -_ * 2

f:id:higumachan:20170117155041p:plain

2. オレオレ構文木で表された式をlambda式コードの文字列に変換

オレオレ構文木をtraverseしながら、lambda式の本体部分を作っていきます。

class Operator1(Operator):
    ...
    def traverse(self):
        yield '('
        yield self.operator
        for t in self.value.traverse():
            yield t
        yield ')'
        # ["(", "演算子", [項のtraverseの結果], ")"] を出力する


class Operator2(Operator):
    ... 
    def traverse(self):
        yield '('
        for t in self.left.traverse():
            yield t
        yield self.operator
        for t in self.right.traverse():
            yield t
        yield ')'
        # ["(", [左項のtraverseの結果],  "演算子", [右項のtraverseの結果], ")"] を出力する
     ...

のようにtraverseするので、

"".join((10 + -_ * 2).traverse())

とすることで 以下のようなlambda式の本体部分を受け取ることが出来ます。

((CONST_0)+((-(___ARG1___))*(CONST_1)))

CONST_[NUMBER]と___ARG[NUMBER]___は後半で詳しく説明しますが、定数の置き換えと引数名の置き換えみたいなものです。

3. 引数リストを作る

lambda式本体が出来たので、lambda式引数リスト部分を作っていきます。 こちらはtraverse_argsという関数を作ります。

Underscoreだけ引数名を返すようにします。

class Underscore(Operand):
    ...
    def traverse_args(self):
         yield "___ARG{}___".format(self.id)
     ...

そして、Underscore以外のtraverse_argsの実装は

class Formula(object):
    def __init__(self):
        self.cache_lambda = None
        self.cache_consts = None
        self.children = []
     
    ...
    def traverse_args(self):
        for child in self.children:
            for t in child.traverse_args():
                yield t
    ...

という実装にしておきます。これは自分の子ノードの出力だけを返すだけの関数になります。

FormulaとUnderscore以外は以下のようにchildrenを指定しておけば勝手にtraverse_argsは使える状態になっています。

class Operator2(Operator):
    def __init__(self, operator, left, right):
        ...
        self.left = left
        self.right = right
        self.children = [self.left, self.right]

このように実装すると、実行したときに

print(dict((10 + -_ * 2).traverse_args())) 
["___ARGS1___"]

のような実行結果を得ることが出来ます。

ここをオレオレ構文木のtraverseじゃなくてリストのイテレーションにしてしまうと、_ + 10 * _こういう式のときに引数が入れ替わったりしちゃいます。

また、Underscoreは以下のようにinit時にidを自動的にふっています。

class Underscore(Operand):
    COUNTER = 1

    def __init__(self, id=None):
        super(Underscore, self).__init__()
        if id is None:
            self.id = Underscore.COUNTER
            Underscore.COUNTER += 1
        else:
            self.id = id

4. 定数辞書を作る

引数リストだけではなくて、式の中に出現する定数を辞書として保存しておきます。 こちらではtraverse_const_valuesという関数を定義していきます。 こちらもほとんど同じでConstOperandだけ

class ConstOperand(Operand):
    ...
    def traverse_const_values(self):
         yield "CONST_{}".format(self.id)
     ...

のような実装でそれ以外は

class Formula(object):
    def __init__(self):
        self.cache_lambda = None
        self.cache_consts = None
        self.children = []
     
    ...
    def traverse_const_values(self):
        for child in self.children:
            for t in child.traverse_args():
                yield t
    ...

のような実装にしていると。

print(dict((10 + -_ * 2).traverse_const_values())) 
{
    "CONST_0": 10,
    "CONST_1": 2
}

のような実行結果を得ることが出来ます。

また、ConstOperandもUnderscoreと同じようにinit時にidを自動的にふっています。

class ConstOperand(Operand):
    COUNTER = 0

    def __init__(self, value):
        super(ConstOperand, self).__init__()
        self.id = ConstOperand.COUNTER
        ConstOperand.COUNTER += 1
        self.value = value

これをやらないと、evalの中と外ではスコープが違うのでlambda式をevalで定義するときに上手く動いてくれません。

5. lambda式コードの文字列をevalすることにより、lambda式に変換

今まで作ったものをくっつけてevalに渡すことによりlambda式を生成することが出来ます。

binds = self.traverse_const_values()
lambda_body = "".join(self.traverse())
lambda_args = ",".join(self.traverse_args())
lambda_string = "lambda {}:{}".format(lambda_args, lambda_body)
lambda_function = eval(lambda_string, dict(binds))

evalの第2引数はevalのグローバル変数になります。ここに定数辞書を渡すことによりevalの内部で定数にアクセス出来るようになります。 これを以下のようにFormulaのcallにフックして実行することにより、Scalaのようなlambda構文が実現できます。

class Formula(object):
    ...
    def __call__(self, *args):
        binds = self.traverse_const_values()
        lambda_body = "".join(self.traverse())
        lambda_args = ",".join(self.traverse_args())
        lambda_string = "lambda {}:{}".format(lambda_args, lambda_body)
        lambda_function = eval(lambda_string, dict(binds))
        return lambda_function(*args)
     ...

全体の流れとしては以上になります。

lambda式のキャッシュ

今回ライブラリの速度が早いポイントはここにあります。 5. で書いた以下のコードだと

class Formula(object):
    ...
    def __call__(self, *args):
        binds = self.traverse_const_values()
        lambda_body = "".join(self.traverse())
        lambda_args = ",".join(self.traverse_args())
        lambda_string = "lambda {}:{}".format(lambda_args, lambda_body)
        lambda_function = eval(lambda_string, dict(binds))
        return lambda_function(*args)
     ...

関数呼び出しのたびにlambda式を生成するため構文木のtraverseを何度も行ってしまっているのでかなり効率が悪いです。

def __call__(self, *args):
        if self.cache_lambda is None:
            binds = self.traverse_const_values()
            lambda_string = self.create_lambda_string()
            self.cache_lambda = eval(lambda_string, dict(binds))
        return self.cache_lambda(*args)

のように一度evalして作ったlambda式はcacheしてしまって使いまわすようにすると、効率が良くなります。 cache有りとcache無しで速度を比べてみると以下のようになります。

gist49ee4e1cd8802cb83711c31081c397cd

実行結果

cached_time 0.440841913223
nocached_time 77.7346978188
176.332366517

のように構文木大きさによりますが180倍程度の高速化につながります。

_を引数に含む関数の呼び出し

例えば、関数呼び出しを行うときの引数側に_を含めた場合、

len(_)([1, 2, 3])

実行結果

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'Underscore' has no len()

となってしまいます。 そこで、scalambdable_funcという関数を作りました。

def scalambdable_func(f):
    @functools.wraps(f)
    def wraps(*args, **kwargs):
        if any(map(lambda x: issubclass(x.__class__, Formula), args)) or any(map(lambda x: issubclass(x.__class__, Formula), kwargs.values())):
            return FunctionCall(f, map(Formula.convert_oprand, args), vmap(Formula.convert_oprand, kwargs))
        return f(*args, **kwargs)
    return wraps

SF = scalambdable_func

関数の呼び出しの引数をすべてoperandへ変換し実行します。 これにより、引数にある_を加味した構文木を作成することで関数呼び出しに対応することが出来るようになっています。

SF(len)(_)([1, 2, 3])

実行結果

3

ちなみにこのscalambdable_funcはデコレータとしても使えます

@SF
def test(x):
    return x + 1

test(_)(100) # == 101

その他

その他にも色々頑張りました。

1. _以外にも_1, _2, ..., _9を作って複数回変数を利用するlambda式の生成を可能にする
from pyscalambda import _1, _2

(_1 + _2 * _1 * _2)(10, 20) # == 4010
2. Objectのメソッド呼び出しにも対応している
from pyscalambda import _

(_.split("_"))("ABEJA_ABEJA") # == ["ABEJA", "ABEJA"]
3. cacheしないモードも実装している
from pyscalambda import _

(_ + _ + _).nocache()(1, 2, 3) # == 6 (遅い)
4. _と_1, _2, ... , _9を同時に使うとSyntaxErrorにする
(_ + _1)(1, 2) # raise SyntaxError
5. ちゃんとtestを書いていたお陰でいじるとき楽だった

などなど、語りたいことはいっぱいありますが今回はこの辺にしておきます。

競合紹介

作った後に調べたところ、2つほど同じこと(似たこと)を実現しているライブラリを見つけました。

fnpy

github.com

特徴

  • lambda構文だけではなくPythonで関数型っぽく書く機構を揃えてる(monadとか遅延評価周り)
  • lambda構文は表現上は全く一緒
  • 中の設計がきれいで頭良く作られている

macropy

github.com

特徴

  • lambda構文のためではなく、pythonのAST(抽象構文木)に対して関数(Lispで言うマクロ)を適用するライブラリ群
  • lambda構文はやりたいことは同じだが構文が違う
  • これを使うともはやpythonではなくなる…(すごくイケてるしヤバイ感ある)

比較

比較実験

今回は書き方がまったく一緒である、fnpyとの比較実験を行いました。

比較コードは以下です。

vs

実行結果

pyscalambda_time 0.060153961181640625
fnpy_time 0.32550811767578125
5.41124992073

pyscalambdaの方が5倍程度早いです。

速度比考察

pyscalambdaはlambda式をそのまま作っているのに比べ、fnpyは関数をネストした形で作っています。

_ + 1 + 1

例としてこのような構文を変換したときに

pyscalambdaは

lambda x: x + 1 + 1

というような、lambda式を生成しています。

fnpyはイメージとしては

lambda x: add(add(x, 1), 1)

の様なlambda式を生成しています

2つを比べてみると、pyscalambdaのラムダ式に比べfnpyのlambda式は演算があるたびに関数呼び出し分のオーバーヘッドがあると考えられます。

追加実験

先程の考察を実証するために以下のような比較コードにより実験を行いました。

gist274b8f2a7a0b3f1b44dea35569c3451c

このコードは恣意的にfnpyの関数呼び出しするであろう場所を増やし速度比に変化が起きるかの実験です。

実行結果

pyscalambda_time 0.27565693855285645
fnpy_time 9.234592914581299
33.5003100704

pyscalambdaの方が34倍程度早いという結果となりました。 これにより、関数呼び出しによるオーバヘッドが両者の速度を分かつこととなりました。

考察

evalによりlambda式を文字列から生成しキャッシュする機構を取り入れることによって、 既存のコードよりも通常用途で約4倍、コーナーケースにおいて約34倍程度早くなることがわかりました。 lambda構文は性質上mapの中やfilterの中に取り入れられることが多いため、この速度向上には意味があると考えます。

感想

今回の取り組みについての感想は、趣味プログラミングでこのようなものを作ることで業務をこなしているだけでは身につかない部分に触ることが出来て良かったです。 また、趣味プロジェクトでも

  1. やりたいことを明確にする
  2. 既存システムを探す
  3. 既存システムに対してどのように優位性を作るかの方針を立てる
  4. 実装
  5. 評価

という、業務(研究)で身につけたフレームワークが役に立ちました。

このように趣味プログラミングと業務(研究)の間でエコサイクルを回しながらスキルを磨いていくことが楽しんでプログラミングをしていくためには重要だと思いました。

宣伝

ちなみに、ABEJAでは今回の話が「面白い or もっといいもの作れるからくだらない」と思えるイケてるしヤバいエンジニアを募集しています。

ABEJAが発信する最新テクノロジーに興味がある方は、是非ともブログの読者に!

ABEJAという会社に興味が湧いてきた方はWantedlyで会社、事業、人の情報を発信しているので、是非ともフォローを!! www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓

「らしさ」が伝わるロゴをつくる。ABEJAデザイナーの試行錯誤

f:id:takana8:20170111100953p:plain

はじめまして。骨とワニが好きなデザイナーの吹上(@takana8)です🐊

昨年夏、 ABEJA の主力サービスのひとつである ABEJA Platform のロゴ(上図)を制作しました。

ABEJA Platform とは、人工知能のブレークスルー技術である Deep Learning を活用し、様々な大量データの取得・蓄積・学習・解析・出力・フィードバックを行うことができる先進的なPaaS(Platform as a Service)です。

今回のテックブログでは、この ABEJA Platform のロゴをどのようなプロセスで、どのような考えに基づいて制作したのかをご紹介します。

どんなロゴが求められていたか

一般に、ロゴに求められる要件というと以下のようなものがあるかと思います*1

  1. 視認性:かたちや色がはっきり見えるか、文字を読み違えないか
  2. 展開性:ディスプレイでも印刷でも問題ないか(解像度や色数に制限があっても問題ないか)
  3. 普遍性:時間がたっても使えるか
  4. 国際性:海外展開は考慮されているか
  5. 環境適応性:既存クリエイティブ(Web やパンフ等)との関係性や今後のサービス展開について考慮されているか
  6. コンセプトの反映:サービス特性や理念とのズレはないか
  7. 独創性:オリジナリティが感じられるか
  8. 美的造形性:美的に好ましいものになっているか

今回制作するロゴに求められる基本的な要件を整理していくと、本プロジェクトで特に重要な観点は「環境適応性」、「国際性」、「コンセプトの反映」、の三点だということが分かりました。

「環境適応性」

デザイン制作時点で、サービスの今後の展開として、「ABEJA Platform」をベースに「ABEJA Platform for Retail」や「ABEJA Platform for Manufacture」などの様々な業界特化型サービス(SaaS:Software as a Service)を随時提供していくことが想定されていました。

このため今回は、サービスロゴのバリエーションが作りやすいか、それぞれの一貫性が保ちやすいか、といった「環境適応性」が重要な要件に挙げられました。

「国際性」

本サービスは日本だけでなく シンガポールをはじめとした海外での事業展開 も進められるため、「国際性」を満たすことは必須要件でした。

「コンセプトの反映」

ロゴはサービスのあり方をビジュアル面で支える軸であり、言わばブランドの顔です。そのロゴがサービス特性や企業の理念・価値観を適切に表現しているかという観点は、マーケティングやブランディングの活動全体に大きな影響を与えます。

今回のように、多様な領域に向けてサービスが広がっていくことを前提としている場合は、特にそうでしょう。

ロゴで表現したいことをまとめると

以上を踏まえ、今回制作するロゴを通して伝えたいことをまとめると、次のようになります。

  • 様々な業界や国で提供されるサービスであること
  • 最先端テクノロジーである人工知能をベースにしたサービスであること
  • イノベーションや倫理観を重視するという ABEJA の理念・価値観

完成したロゴ

早速ですが、完成したロゴはこちら↓

f:id:takana8:20170111100951p:plain

側面から見たヒトの脳を図案化し、ABEJA Platformのベースとなる技術である「人工知能」 をイメージさせるロゴです。

ぱっと見た印象はシンプルな脳のアイコンですが、実は、ちょっと特別な仕掛けが隠されています。

色分けされた各領域の頂点を数えてみると……

f:id:takana8:20170111101616p:plain

右下の小脳を表す領域は頂点が 3 つ、その左隣の脳幹部分は 4 つ、 というように各領域の頂点を1つずつ増やしながら、六角形を分割して切り出しています。このような、ある規則にもとづいた領域分割というギミックで、「アルゴリズム」や「合理性」、「論理性」といった特性を表現しました。

ロゴに込めた意味とデザインプロセス

なぜこのモチーフか

繰り返しになりますが、今回のプロジェクトで求められるロゴの要件は以下の3点でした。

  • 様々な業界や国で提供されるサービスであること
  • 最先端テクノロジーである人工知能をベースにしたサービスであること
  • イノベーションや倫理観を重視するという ABEJA の理念・価値観

これらの要件を満たモチーフやテーマを探るため、まずはひたすらラフスケッチをし*2、このうち見込みがありそうなアイデアをIllustratorで清書していきました。そこから4案ほどに絞り、制作意図をまとめてプレゼンテーションを行いました。

f:id:takana8:20170111101708p:plain

そして最終的に選ばれたのが、六角形のガイドをベースに「側面から見た脳」を図案化したシンボル(上図・右)。

「人工知能」というキーワードからこの「脳」のかたちを想起するのは、人類にとっては自然で普遍的といえるでしょう(ワニのためのロゴとなると、また違ったかたちになってしまうはず🐊)。

ちなみに、ここでガイドを六角形にした理由は、「 ABEJA 」という社名がスペイン語でミツバチを意味することから「ハチの巣(ハニカム)」を連想したことによります。

f:id:takana8:20170111101638p:plain

初期の段階(上図・左)もシンプルで悪くないかたちですが、これからさらに力強いロゴにするために試行錯誤を続けます。そして、手を動かすうちにふと、少し工夫すれば各領域の頂点を1つずつ増やしながら分割していくことができるのでは?と気が付きました。これぞブレークスルー💡 思い付いてしまえば実現するのはそう難しくありませんでした。

このような仕掛けは説明されなければきっとなかなか気付かないもので、ともすれば制作者の自己満足だと受け取られることもあるかもしれません。

しかし、このようなシンプルな図形に深い意図を込めるための「試行錯誤」やそれを実現する思考の「ブレークスルー」こそ、 ABEJA が重視する「イノベーション」に必要な要素そのもの。ロゴにこのような仕掛けと意図を込めることには、「 ABEJA らしさ」を伝えるという点で大きな意義があると考えました。

なぜこの色か

デザインにおいて、色のもつ影響力は思いのほか大きいもの。色を付けてしまうとロゴのかたちに意識が向きにくくなるので、はじめは白黒で作成し、かたちがある程度かたまった段階で色を付けていきました。

f:id:takana8:20170111100942p:plain

上図は検討した色の組み合わせの一部です。色によって印象が随分変わりますね。

いろいろな色の組み合わせを試した結果、今回は青から紫へと変化する鮮やかなグラデーションを採用することとしました。

青や紫は「知性」や「落ち着き」といった印象を与える色であり、ここからイノベーターらしい「先進性」とプラットフォーマーとしての「信頼感」が表現できると考えました。

なぜこの書体か

ロゴタイプの制作にあたっては、プラットフォームサービスの全体像を体現するロゴに適した、現代的かつ先進的な印象のサンセリフ書体を中心にピックアップし、検証を重ねました。どのような雰囲気の書体にするか、大文字か小文字か、シンボルとの組み合わせはどうか。これらを様々な組み合わせでテストし、最終的に「DIN」をベースにすべて大文字のロゴタイプを制作することとしました。

f:id:takana8:20170111100937p:plain

「DIN」は、もとはドイツの工業製品などに記載するために制作された書体です。そのため、スタンダードでかっちりした雰囲気があり、「信頼感」のイメージにもつながります。

今後、サービス提供範囲の広がりとともにロゴタイプ部分が多様化することを想定して、文字のかたちには手を加えすぎない方針とし、各種ロゴタイプを完成させました。

最後に

以上、「らしさ」が伝わるロゴをデザインするために考えたことをざっと紹介させていただきました。

じつは私は去年の夏ごろ ABEJA にジョインしたのですが、入社後の初仕事がこのロゴデザインプロジェクトでした。

会社のことを深く知らなければできない仕事に入社直後に取り組むという、導入としてはなかなかハードルの高いプロジェクト……しかし逆に考えると、この時に会社に対する理解が一気に深まったおかげでその後の仕事がとてもやりやすくなったので、結果オーライだったのかなと思っています。

さて、私たちいま、「試行錯誤」「ブレークスルー」「イノベーション」といったキーワードにビビッときた方を探しております。ぜひ、下記ページにて「話を聞きに行きたい」ボタンを押してみてください!

www.wantedly.com

最後までご覧いただき、ありがとうございました!

*1:参考:『 ロゴロジック

*2:スケッチをするときは無印良品の4コマノート(現行版はノートじゃなくて 短冊型メモ帳 になっています )が非常におすすめです。1コマに1アイデアをポンポン描いていくと、100個くらいはあっという間に埋まってしまいます。ただ枠線があるだけなのに、無地のノートより手を動かすのが早くなる不思議😳

フロントエンジニアとしてAWS re:Inventに行ってきました

はじめに

ABEJAでフロントエンド開発をやっている清水です。 先月末から今月初にかけてラスベガスで開催された AWS re:Invent に行ってきました。

この記事は、12月14日に弊社が運営しているコミュニティ主催で開催されたイベントで話した内容の補足になります。

https://abeja-innovation-meetup.connpass.com/event/45987/

re:Invent について

re:Invent は年に1度開催されるAmazon Web Service 主催の開発者向けカンファレンスで、 今年は11/28〜12/2までラスベガスで開催されました。

f:id:toshi6:20161201074631j:plain

行く前の準備

今回はABEJAからは一人で参加しました。私は英語を話すのが得意ではありません。 そして最近はフロントエンドを専門にやっているため、たくさんあるAWSのサービスを全部キャッチアップするのは難しく、 前提知識の面にも不安がありました。

悪あがきで行く前に準備したことを紹介すると

  • 英会話の勉強
  • AWSの復習
  • 事前説明会への参加

どれも付け焼き刃的なのですが、いくらか役に立ちました。 一番役に立ったのは事前説明会の参加で、ここで知り合った方々のおかげでイベントが楽しめました。

フロントエンドエンジニアとしての関心

最近ではLambdaの登場でサーバーレスアーキテクチャの話題も活発で、導入した事例もちらほら聞こえてきます。

私の担当しているABEJAのサービスもフロントはSPA、バックエンドのAPIはサーバーレスで作られています。 そのため、今回はサーバーレス系のセッションに的を絞ってスケジュールを組みました。

ラスベガスに行ってみて

ラスベガスに初めていきました。視界に入るのはホテルとカジノ f:id:toshi6:20161129000557j:plain

会場のホテルがとても広く、セッション会場間の移動にも20分以上かかります。 毎日歩き疲れて夕方にはヘロヘロになります。

キーノートは日韓来場者のために優先席に用意されていて間近で見ることができました。 目の前にSnowmobileが出てきた時は正直冗談かと思いました。 f:id:toshi6:20161130101955j:plain

夜は日本人だけのパーティー(Japan Night)に参加したり、re:Playに行ったり…

サーバーレスについての発表

サーバーレスに関係する発表も多数ありました。 LambdaのC#対応、Lambda Edge、Step Functions、 その他 Glue Athena Batch Pinpointなど今までは、EC2やEMRを使って構築する必要があったものが フルマネージドなサービスとして発表もされました。

サーバーレスの事例紹介

事例として、複数の AWS サービスの内部で Lambda がすでに使われていることや、 Finra 社では15兆もの証券取引を Lambda で実現しているという話がありました。

参加したセッション

新機能の発表でセッションも後から追加され [serverless lambda] で検索してみると117件もありました。 私は4日間で18個のセッションに参加しました。セッションの情報についてはビデオやスライドがすでに公開されているのでここでは割愛します。

来年行く人に向けて

来年行く方が楽しむためのアドバイスとしては

  • AWS全般の知識 (サービスの概要レベルだけでも知っているといいと思います)
  • やっぱり英語は大事 (AWSのエンジニアに質問できるチャンスがあります)
  • 仲間が大事 (おかげですごく楽しめました)
  • 余裕のあるスケジュール (聞きたいセッションを絞って余裕を持たせないとすごく疲れます)
  • 最後は体力 (5日間楽しむためには体力が必要)

フロントエンドエンジニアとして思うこと

re:Inventに参加される方はインフラエンジニアの方がほとんどなので、私はフロントエンドエンジニアとして感じたことを書きます。

サーバーレスのインパクト

サーバレスアーキテクチャの一番の特徴は構築し管理すべきサーバインスタンスが存在しないことがあると思います。 今まではサービスの要件にもよりますが、バックエンドのアーキテクチャを考える上でスケーラビリティやアベイラビリティ、弾力性などの非機能要件を考慮して考える必要があります。 そのためにはOSやミドルウェア、クラスタなど様々な知識を駆使して構築する必要がありました。そして構築した環境の運用、監視の方法も同時に考えていく必要もあります。 そのためには専門的なチームに分かれ、お互いが連携しながらサービスを開発していく必要がありました。

サーバレスアーキテクチャではこれらをプラットフォーム側が担保することによって、開発者はよりサービスやアプリケーションに集中できるようになります。

サーバレスアーキテクチャはまだ新しいため多くの課題もたくさんあります。 また、すべてのシステムにフィットすることもないと思いますが ABEJAのようにリソースも少なく、スピード感をもってサービスをスケールさせていく必要があるベンチャーにとっては有効性を感じます。

チーム構成の変化

私が担当するフロントエンドチームは、他のチームが開発した複数のサービスをAPIを通して利用します。 開発を進めるなかで BFF (Backend for FrontEnd) 層の必要性を感じます。

BFF層を設けることでUIのためにAPIを最適化でき、 バグや変更があった場合に別チームへ依頼を出して、リリースを待つことなくBFF層を使って解決できる余地が生まれます。

BFF層を作るのは面倒

しかし、BFF層を作るには当然サーバサイトの知識が必要になります。フロントエンドを開発するために要求される技術や知識もたくさんあります。 その上、サーバーサイドの知識を身につけてアップデートしてくのはとても大変です。

サーバーレスによりBFF層の導入が簡単に

しかし、サーバーレスの登場によりBFF層を構築することが容易になりました。 AWSのLambdaはNode.jsをサポートしているため、SPAとのモデルの共有やLambdaでSSR(Server Site Renddring)をするなどの選択肢が広がります。 f:id:toshi6:20161212190404p:plain

これからのフロントエンドチーム

今後サービスの顔ともいうべきフロントエンドチームは、フルマネージドなサービスを組み合わせて早くアイデアを形にする サービスチームとしての重要性が増してくるのではないでしょうか。 re:InventのセッションのServerless Architectural Patterns and Best Practices (ARC402) で話されていた「AppOps」の時代が来るかもしれません。 そのためには、フロントエンドのエンジニアもAWSのようなクラウドプラットフォームが出すサービスにも注目していく必要があると思います。

今後

AWSのLambdaだけではなく、GCP(Google Compute Platform) の Cloud Functions、 Azure の Azure Functionsなど他のクラウドプラットフォームでもサーバーレス開発のための機能が整備されてきています。 今後もフロントエンドエンジニアとして動向に注目していきたいと思っています。

宣伝

ABEJAでは最新のアーキテクチャで世界を変えるサービスの開発を行っています。 ご興味のある方は、ぜひご連絡ください

www.wantedly.com

Kubernetesのクラスタ監視について

この記事はKubernetes Advent Calendar 2016の20日目の記事です。

Kubernetesを安定的に運用する上で、どのように監視を行うかということを考える必要があると思います。

しかし、Kubernetesにデフォルトで使用されているcAdvisor, Heapsterやaddonとして提供されているkube-state-metricsの他、DataDogなど選択肢も様々で、その中からどれを選べば良いのか難しい状況です。

今回は、それらの監視ツールの中からKubernetesにデフォルトで使用されているcAdvisor, Heapsterと、addonとして提供されているkube-state-metricsについて調べてみました。

cAdvisor

cAdvisorはGoogle社が開発しているオープンソースのコンテナの監視ツールです。 Kubernetesの各ホスト上で1つづつ起動しており、デフォルトで1秒毎に同じホストにあるコンテナのメトリクス情報を収集してくれます。

f:id:i03yari2:20161220173424p:plain

cAdvisorの概要図

cAdvisorで取得できるメトリクスとしては

  • CPU
  • Memory
  • Network
  • FileSystem

などがあり、メモリ上に(デフォルトで)60秒分のメトリクスを保持し、APIとして提供してくれます。

また、cAdvisorはGUIも持っているため60秒分のメトリクスであれば簡単に確認することができます。

f:id:i03yari2:20161220110401p:plain

cAdvisorのダッシュボード

Heapster

alt

[5]より引用

Heapsterは、クラスタ単位のメトリクスの監視ツールであり、ホストを自動的に監視に追加しkubeletを通してcAdvisorからメトリクス情報を収集します。 メトリクス情報は、ラベルでPodをまとめてしエクスポートされます。また、Kubernetes 1.2以降であれば、コンテナ内のアプリケーションからカスタムメトリクスを取得することができます。

Heapsterを使うには、メトリクスのフォワード先のストレージバックエンドが必要になります。ストレージバックエンドにはInfluxDBが使われることが多いようです。

今回はInfluxDBとGrafanaを使ってメトリクスを可視化してみました。 (本当はこちらのガイドからKubernetes上のクラスタを監視したかったのですが、うまく動作せず今回はこちらを参考にコンテナを直に立ち上げて確認しています)

はじめに監視対象のホストでcAdvisorを起動しておきます。

$ docker run --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --publish=8080:8080 --detach=true --name=cadvisor google/cadvisor:latest

次にInfluxDBとGrafanaを起動します。Heapsterが、格納先のInfluxDBへリンクするため先に起動する必要があります。

$ docker run -d -p 8083:8083 -p 8086:8086 --name influxdb kubernetes/heapster_influxdb
$ docker run -d -p 80:8080 -e INFLUXDB_HOST=<influxdb_host_ip> kubernetes/heapster_grafana:v0.7

次にHeapsterですが、監視対象のホスト設定を書いたJSONファイルをコンテナにマウントする必要があります。 実際にはHeapsterとKubernetesが連携し自動でホストを監視対象に追加する形になります。 今回は監視対象のホストを2つ用意し、それぞれcAdvisorを動かしています。

$ vim /export/heapster/hosts
{
  "items": [
    {
      "ip": "192.168.33.10",
      "name": "host1"
    },
    {
      "ip": "192.168.33.20",
      "name": "host2"
    }
  ]
}
$ docker run --name heapster --link influxdb:influxdb -v /export/heapster/hosts:/var/run/heapster/hosts -d kubernetes/heapster:v0.14.2 --sink="influxdb:http://influxdb:8086" --source="cadvisor:external?cadvisorPort=8080" 

以上でGrafanaからメトリクスの確認ができるようになります。

f:id:i03yari2:20161220115846p:plain

Heapsterのメトリクス

左側がCPU使用量、右側がメモリ使用量、上段がコンテナ単位のメトリクス、下段がホスト単位のメトリクスのグラフです。 上段のグラフでは、動作しているホストに関係なくすべてのコンテナのメトリクスが表示されています。 複数のホスト上のcAdvisorがコンテナ単位で収集したメトリクスがHeapsterを通してInfluxDBにエクスポートされていることが確認できます。

kube-state-metrics

Heapsterが、CPU・メモリ・ネットワークといったリソース単位でメトリクスを管理しているのに対し、kube-stae-metricsはPod, Deployment, DeamonSetといったKubernetesで抽象化された単位でメトリクスを管理します。

また、kube-state-metricsは、デフォルトでKubernetesに組み込まれていないためkube-state-metricsコンテナを自分でデプロイする必要があります。

@helix_kazさん記事を参考に構築してみました。

構築に成功するとhttp://<node_ip>:30090/graphでPrometheusにアクセスすることができます。

今回はメトリクスとしてkubelet_running_pod_countを表示し、nginxのPodを手動で起動と削除を繰り返してみました。

f:id:i03yari2:20161220171353p:plain

kube-state-metricsのメトリクス

グラフからPod数が変動していることが分かると思います。

このようにkube-state-metricsを使えば、Kubernetesの抽象化単位をメトリクスとしてみることができます。

Heapsterとkube-state-metricsの比較

ストレージとの連携

Heapsterは、cAdvisorのメトリクスを取得しInfluxDBなどのバックエンドストレージにフォワードします。一方、kube-state-metricsは、メトリクスをメモリに保持しますがフォワードに責任を持ちません。そのため、kube-state-metricsの場合は、Prometheusなどのpull型の監視ツールと連携する必要があります。

メトリクスの違い

Kubernetesは、状況に応じてオートスケーリングしてくれるため、ホスト単位よりもラベル単位での管理が必要になってくると思います。監視という点でも、Kubernetesで抽象化されている単位で管理できる方がわかりやすく、問題の原因が特定しやすくなるのではないかと思います。その点、kube-state-metricsは、メトリクスの形式がPodやDeploymentといったKubernetesの抽象化単位でメトリクスが見れるため、利点があると感じました。

まとめ

Kubernetesのモニタリングについては、こちらの記事がよくまとまっていると思います。一部を要約すると、「Kubernetesを使う上ではどこでアプリケーションが動いているかを正確に知ることができないので、これまでの監視の考え方を再考する必要があるよ。タグとラベルで管理しトラッキングすることが重要になるよ。」ということを言っています。  今回の調査でkube-state-metricsがこの考え方に近く、実際にKubernetesを扱う上で直感的にメトリクスを見れると感じました。今後も監視系ツールは開発が進んでいくと思いますが、kube-state-metricsに注目しつつ見守っていきたいと思います。

宣伝

ちなみに、Abejaではコンテナを心穏やかに監視してくれる仲間を募集しております。 また、サーバレスアーキテクチャとかSparkとかSPAに興味がある方など幅広く募集しておりますのでどしどしご応募下さい!

→ Join us! ←

参考

[1] https://www.datadoghq.com/blog/monitoring-kubernetes-era/

[2] http://mmbash.de/blog/monitor-docker-containers-with-heapster-running-on-apache-mesos/

[3] https://www.youtube.com/watch?v=sxE1vDtkYps

[4] https://deis.com/blog/2016/monitoring-kubernetes-with-heapster/

[5] http://blog.kubernetes.io/2015/05/resource-usage-monitoring-kubernetes.html

[6] https://github.com/kubernetes/kube-state-metrics#kube-state-metrics-vs-heapster