ABEJA Tech Blog

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

AWS Greengrassの気になる所を調べてみた!

エンジニアの河崎です。

昨日、AWS GreengrassがGAになりました

AWS GreengrassはLambda等のAWSで利用可能なサービスの一部をローカルデバイス上で実行できるようにする事で、エッジ側でのアプリケーション実行を簡単にできるサービスです。 ABEJAではエッジコンピューティングを行っていますので、さっそく調査をしてみました。

以降の検証は Raspberry Pi 3 Model B で行っています。

※2017年6月9日時点の情報です。また、できるだけ正確な情報を書くようにしましたが、間違っている可能性もありますのでご留意ください。

調べてみた

AWS Greengrassとは?

公式ドキュメント

What Is AWS Greengrass? - AWS Greengrass

Developers.IOのブログエントリ

【新サービス】AWS GreengrassがGA(一般利用開始)になりました! | Developers.IO

どうやって動かすの?

GreengrassCoreのインストールされたデバイス上で常時起動型のLambda functionを実行するサンプル

Deploying a simple Lambda function to AWS Greengrass - AWS Greengrass

GreengrassCoreのインストールされたデバイスをハブにして、IoTデバイス間の通信をするサンプル

AWS Greengrass Example Scenario - AWS Greengrass

Developers.IOのブログエントリ

AWS Greengrass CoreをEC2でセットアップする | Developers.IO

Greengrass Coreの動作要件は?

AWS Greengrass FAQs - Amazon Web Servicesに記載があります。

  • Operating Systems: Ubuntu 14.04 LTS, Jessie Kernel 4.¼.4, and other Linux distributions with Kernel 4.4 or greater
  • CPU Architectures: x86_64, Armv7, Aarch64 (ArmV8)

また、以下のカーネルコンフィグの項目が有効化されている必要があります。世の中にはcgroupsやoverlayfsが有効化されていないカーネルが乗ったデバイスが有るので気をつけたいですね。

自分のカーネルでカーネルコンフィグの項目が有効化されているかはdockerのチェックスクリプトを使うと良いかもしれません。

2.Kernel configuration

·Mqueue: CONFIG_POSIX_MQUEUE

·Overlay: CONFIG_OF_OVERLAY

·Overlay FS: CONFIG_OVERLAY_FS

·Seccomp Arch Filter: CONFIG_HAVE_ARCH_SECCOMP_FILTER

·Seccomp Filter: CONFIG_SECCOMP_FILTER

·Seccomp: CONFIG_SECCOMP

·Devpts: CONFIG_DEVPTS_MULTIPLE_INSTANCES

 

3.Kernel configuration for Namespace - Kernels must be built with these configurations enabled:

·IPC isolation: CONFIG_IPC_NS

·Network isolation: CONFIG_NET_NS

·UTS isolation: CONFIG_UTS_NS

·User isolation: CONFIG_USER_NS

·PID isolation: CONFIG_PID_NS

 

4.Kernel configuration for Cgroup - Kernels must be built with these configurations enabled:

·Enable cgroups: CONFIG_CGROUPS

·Enable Memory cgroup: CONFIG_MEMCG

·Enable freezer cgroup: CONFIG_CGROUP_FREEZER

·Enable devices cgroup: CONFIG_CGROUP_DEVICE

·Enable pids cgroup: CONFIG_CGROUP_PIDS

デバイス上でLambdaが動くってどういうこと?

GreengrassCoreがインストールされたデバイス上でpythonのスクリプトを実行する事ができます。Lambda functionの実行はMQTTでメッセージを受けた時に実行、定期的に実行、常時起動などができるようです。 ちなみに、GreengrassCoreで実行されるLambda functionには実行時間の制限はありません。

Lambda functionの実行ログは?GreengrassCoreのログは?

以下の2種類のログを取得可能。それぞれ、CloudWatchとLocalファイルシステムに保存する事ができます。Localファイルシステムの場合はどれくらいのサイズまで保存するかを指定できます。

  • User lambda logs
  • Greengrass system logs

検証してみた

GreengrassからAWSの他のサービスへアクセスする権限はどうやって指定するの?

IAM Roleを使う

GrenngrassからCloudWatch Logsへログを送る場合は、Greengrass GroupにIAM Roleを付与します。また、LambdaからCloudWatch Logsや他のサービスにアクセスする時も同様にLambda functionにIAM Roleを付与します。

Greengrassからどういう感じでAWSサービス扱うのかな?

基本的にはMQTTでクラウドに送った後にRules Engineを使うのがよさそうです。Lambda functionに適切なIAM Roleを付与すればSDKからKinesisやDynamoDBにアクセスする事は可能だと思うので、ユーザ側の要件によりそう。

CloudWatch Eventsからデバイス側のLambdaを呼び出せる?

今のところできなさそう?

Greengrassコアがオフラインの状態でもLambda functionの実行はできる?

多分できる。未検証

このサンプルを動かしてる状態で、ネットワークの状態をいじると検証できます。

Lambda functionからpythonライブラリや設定ファイル等にアクセスできる?

可能

クラウドでの実行時と同様にDeployment Packageを作る事ができます。この際、AWS Greengrass Core SDKをダウンロードしてパッケージに含める必要があります。また、設定ファイルもパッケージにいれれば読み込みできそうです。 しかし、デプロイパッケージの容量制限は50MBなので、特定用途では使えない事がありそうです。 また、パッケージする時にコンパイルが走るライブラリは、GreengrassCore上のランタイムでは動かないと思われるので一工夫必要です。多くの場合、パッケージしたいマシンはx86でGreengrassCoreが動くデバイスはARMですよね。また、コンテナ内には必要依存ライブラリも存在しません。

こういうプロジェクトを参考にがんばるとよさそうですね。 GitHub - vitolimandibhrata/aws-lambda-numpy: NumPy Python Library for AWS Lambda

Lambda functionからデバイス側のファイルシステムにアクセスできる?

読み込みのみ可能

Lambda function起動時に、デバイスのファイルシステムを起点として、コンテナのファイルシステムをoverlayfsで構築しているようです。Lambda functionで書き込んだファイルは、デバイス側のファイルシステムに反映されませんし、Lambda functionを再起動した時はLambdaから見えているファイルシステムはリセットされています。 /tmpはコンテナ起動時にリセットされるので、デバイス側の/tmpとコンテナ側の/tmpは完全に独立しています。

以上の特性を利用すれば、Lambdaからデバイス側のファイルを参照だけする事ができますが、推奨されない気がします。

Lambda functionからデバイス側のカメラやGPIO等にアクセスできる?

今のところできない

Lamda functionの中から/devを見てみましたが、カメラデバイスやgpioデバイスは見えていませんでした。

ローカルネットワークでのデバイス間通信ってどんな感じでやるの?

APIを使う事でGreengrass Coreのローカルネットワーク内でのIPをIoTデバイス側から取得できます。 AWS IoT device SDKを使う事で簡単に、IoTデバイスから同一ネットワーク内のGreengrass Coreに接続する事ができます。Greengrass CoreにはMQTT brokerと同一GreengrassCore内のDeviceShadowを保持するDBが有るので、IoTデバイスとGreengrass CoreがAWSに繋がらない状態でも、デバイス同士のメッセージの交換や、DeviceShadowを使った状態の同期が可能です。このサンプルを動かしてみると良いと思います。

Local Rules Engineって何?

Subscriptionsの事だと思います。メッセージのソース、ターゲット、メッセージを流すトピックフィルターを指定できます。ソースとターゲットは、IoT Cloud(クラウド側のMQTT broker)、Local Device Shadow、同一Grenngrass Group内のデバイス、Lambda functionを指定できます。このサンプルをいじれば色々検証できそうです。 f:id:toshitanian:20170609101719p:plain

おわりに

現時点ではGreengrassCore上でのLambda functionは、センサー等を使ったヘビーな処理を実行するというよりは、ゲートウェイとして必要になるメッセージの処理と、デバイスの操作を簡易的に行うために使う事が想定されているのかなという感想を持ちました。 まだドキュメントが少ないので、増えていくと検証もすすみそうですね。当面は、APIドキュメントを読んでいこうと思います。

AWSを使う事でクラウドだけでなく、エッジ側でもシステムを簡単に構築・運用できるようになっていってほしいですね。

宣伝

ABEJAではエッジ・クラウドを問わずディープラーニングを実行できるプラットフォームを提供しています。 テクノロジーの最先端に挑戦したいイケてるしヤバイエンジニアの方のご参画をお待ちしております!

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

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

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

学生向けの勉強会もしています!ガンガン応募してください!

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

タカハシ春の GAN 祭り!〜 一日一GAN(๑•̀ㅂ•́)و✧ 〜

f:id:taka_t0m0:20170530141554j:plain

ABEJAでリサーチャーをしています高橋です。

昨今 deep learning 界隈では Generative Adversarial Net(GAN) が流行っていて、世はまさにガンガン行こうぜ時代ですね。

GAN を用いると綺麗な絵が作成できたり二つの絵の中間のような絵を生成できたりします。例えばこの論文のような感じです。このように GAN は有用なモデルである一方、最近の GAN では急によくわからない式が出てきたりするので、勉強も兼ねて「一日一GAN」をやってみました。今回読んだ論文のリストは以下です。

それぞれの論文の概要と自分なりの考察を報告できればと思います! 論文を読む前や読んでいる途中に参考になったなら幸いです。

一応実装も行い こちらで公開していますが、パラメータの調節などはしておらずそのままでうまくいかなさそうである旨ご了承ください。今後各 GAN の比較実験を行なった際には、その結果や使い勝手なども報告できればと考えています。一日一GANでパラメータ調節や比較実験までやるのは厳しかったです。

本筋に入る前に


GAN の 概要

本筋に入る前に GAN について説明します。

GAN のたとえ話

数式で説明する前に、GAN の学習過程についてよくあるたとえ話を述べたいと思います。

GAN で行なっていることは、贋作師と真贋鑑定士の対決、つまりガンの飛ばし合いです。贋作師は世に偽物を売るために有名な絵とそっくりな絵を描く能力が重要で、真贋鑑定士は偽物が世の中に出回らないように偽物と本物を区別する能力が重要となります。真贋鑑定士が偽物と本物を見分ける能力が向上したとします。すると贋作師は自分の偽物の絵が世に出回らなくなり困ったことになるので、更に本物に近い絵を描けるように能力の向上を目指します。能力が向上し偽物が出回ると、今度は真贋鑑定士が困ったことになるので真贋鑑定士は本物と偽物を見分ける能力の向上を目指します。そうすると、また贋作師が困ったことになって。といった具合に、二人が対決することで二人の能力がどんどん上がっていくわけです。特に贋作師の方に注目すれば、最終的には本物と区別がつかないような絵を作れるようになる、という話です。

GAN の objective

上記のような話を考えるのは簡単ですが、具体的にどういう学習をするの、とか、うまくいく理論保証はあるの、と言った部分が気になります。ということでこの部分についても触れたいと思います。以下、絵など模倣したい対象 data set があり、そのうちの一つを $x$ と書くことにします。また、$x$ は 確率分布$p_{data}(x)$に従っているとします。

真贋鑑定士 Discriminator $D$は、data 内にある $x$ を本物と判定できる様に、つまり、$x\sim p_{data}(x)$に対して$D(x) = 1$ とできる様に学習を行います。贋作師 Generator $G$ はどの様に偽物を作るのかと言えば、何らかの事前に決めた分布 $p_z$ に従う $z$ を用意し、$G$ は $z$ を入力として贋作を出力します。つまり、Generator は $D(G(z)) = 1$ とする様に頑張り、Discriminator は $D(G(z)) = 0$ となる様に学習を行います。

以上から、例えば以下の様な objective を設定して学習を行えば良さそう、と考えられます。 \begin{align} \min_{G}\max_D \left[\mathbb{E}_{x\sim p_{data}}\left[\log D(x)\right]+\mathbb{E}_{z\sim p_{z}}\left[\log(1 - D(G(z)))\right]\right]\ . \end{align} 以上で良さそうだと思えるのは、$G$ を固定すれば$D$ は $D(x)$ を大きくし、$D(G(x))$ を小さくする様に学習され、$D$ を固定すれば $G$ は $D(G(z))$ を大きくする様に学習されるためです。

理論解析

この学習で良い偽物を作れる理由を述べたいと思います。 以下 $p_z$ と $G(z)$から誘導される確率分布$p_g(x)$ と書くことにすると、 良い偽物が作れるようになる理由は、この最適化問題の global optimum が $p_g(x)$と $p_{data}(x)$が一致する時だからです。以下でそれを確認します。

まず、$G$ を固定した際、最良の $D_G$ は以下のように計算できます。 objective $\mathcal{L}$ が \begin{align} \mathcal{L}(G, D) &= \int p_{data}(x) \log(D(x)) dx+ \int p_z(z)\log(1 - D(G(z)))dz \\ &=\int p_{data}(x) \log(D(x)) + p_g(x) \log(1 - D(x)) dx \end{align}

と書き直せますが、$f(x) = a\log(x) + b\log(1 - x)$ の最大値は $x = a/(a + b)$で取るから \begin{align} D_G = \frac{p_{data}}{p_{data} + p_g} \end{align} となります。これを用いてちょっと計算すると \begin{align} \max_{D}\mathcal{L}(G, D) &= \mathbb{E}_{x\sim p_{data}}\left[\log\left(\frac{p_{data}}{p_{data} + p_g}\right)\right] +\mathbb{E}_{x\sim p_g} \left[\log\left(\frac{p_g}{p_{data} + p_g}\right)\right] \\ &= -\log(4) + 2 JS(p_{data}||p_g)\ . \end{align} ここで JS は Jensen-Shannon divergence。JS は $p_{data} = p_g$ となる際に最小値を取るので、上式はこの時に最小となります。

以上をまとめれば、objective $\mathcal{L}$ を min max 最適化をしていけば、 確率分布が一致するという意味で $G$ は対象の data を良く模倣できるようになる、というのが理論的な根拠となります。

長くなりましたが、ようやく準備終了。以下では、一日一GAN を報告していきます!

Day 1 EBGAN


論文は https://arxiv.org/abs/1609.03126 です。

EBGAN の概要

Original GAN とやることは同じで、 objective だけを変更した論文で、違う objective でも二つの確率分布が一致することを示した論文です。 論文同様ポンと objective を紹介すると、EBGAN では以下を objective としています。

\begin{align} \mathcal{L} = \sum_{x\in p_{data}}D(x) + \sum_{z\in p_z}\max(0, m - D(G(z)))\ . \end{align}

この論文では、この objective でも original GAN の様に min max 最適化をしていけば、 $p_{data}$ と$p_g$ が一致するようになることを示しています。

理論解析

二つの確率分布$p_{data}$と$p_g$が一致するようになる証明は通常の GAN と似ているが少しだけ違う感じです。 GAN と同じように、任意の $G$ に対して最良の $D$を探し、$\mathcal{L}$ に代入しちょっと計算すると

\begin{align} \max_{D}\mathcal{L}(G, D) = m + m\int \mathbb{1}_{p_{data}(x) < p_g(x)}(p_{data}(x) - p_g(x)) dx \end{align}

となります。最良の $G$ を考えるとこれの第二項が $0$ にならなければならず、それは二つの確率が一致する時、という証明です。第二項が$0$になることの詳細は論文を。GAN と似ていますが、JS divergence に帰着させないのが違いです。

ちょっとした考察

論文を読んでいただければ分かるかと思いますが、論文の一番最初の式が上の objective なんですよね。いやぁ急にこの objective をポンと与えられても、というのが率直な感想ではないかと思います。

EBGAN は energy base ということなので、なんらかの分布と繋がってればと考えたのですが、ここ ではそれは無理と主張されています。それにもめげずにちょっと考えてみたところ、完璧な対応関係とはほど遠いですが、ある極限では GAN の objective と EBGAN の objective は Fermi分布を通して繋がっていそうと思えてきたので述べてみます。予め断っておきますが、本当に完璧な対応関係からはほど遠いです。。。

GAN の discriminator は[0, 1]の確率を返すので、ここでは Fermi 分布だと思いましょう。

\begin{align} D_{GAN}(x) = \frac{1}{1 + \exp( (E(x) -\mu)/k_BT )}\ . \end{align}

ここで $E(x) \geq 0$は energy, $\mu$ は chemical potential に対応する positive な constant、$T$ は温度に対応する定数、$k_B$は Boltzman constant。特に今回は低温($k_BT << \mu$)な Fermi 分布だとします。 この場合、$E(x) \sim 0$において

\begin{align} D_{GAN}(x) \sim 1 - \exp( (E(x) -\mu)/k_BT )\ \end{align}

$E(x) >> \mu$ に対して

\begin{align} D_{GAN}(x) \sim \exp(-E(x)/k_BT) \end{align}

と近似できます。この近似を利用して、GAN の discriminator にとって非理想的な極限(本物を偽物と思い、偽物を本物と思う)を考えると、

\begin{align} L_{GAN}& = -\sum_{x\in p_{data}}\log\left(D_{GAN}(x)\right) - \sum_{z\in p_z}\log\left(1-D_{GAN}(G_{GAN}(z))\right)\\ &\sim \frac{1}{k_BT}\left[\sum_{x\in p_{data}}E(x) + \sum_{z\in p_z}(\mu - E(G_{GAN}(z)))\right] \end{align}

となりEBGANの objective に近しいものが得られます。$max(0, *)$ がない(理論解析ではこの max が本質)などズレも大きいですが、ただポンと objective が与えられるよりかは energy という気分が伝わるかと思います!

Day 2 WGAN


論文は https://arxiv.org/abs/1701.07875 です。

WGAN 概要

WGAN も original GAN から objective を変更した論文です。

基本的に、GAN は $p_{data}$ と $p_g$ を一致させることで Generator が実際のデータを良く模倣できるという話なので、確率分布間の距離を定義して、その距離を近くすれば良いというのも自然な話です。最初に見たように、original GAN では間接的には JS divergence という距離を最小化していることに対応しています。WGAN では、Earth Mover’s distance(EMD) で距離を測り、直接これの最小化をすることで二つの分布を一致させることを狙います。EMD は扱いが面倒なので、それと等価である Wasserstein distance

\begin{align} D(p_{data},p_g) = \sup_{f \in 1-Lipschitz}\left[\mathbb{E}_{x\sim p_{data}}\left(f(x)\right)-\mathbb{E}_{x\sim p_{g}}\left(f(x)\right)\right] \end{align}

を用いて二つの確率分布を近づけるように学習を行う、というのがこの論文の話です。

理論解析

上記したように、確率分布間の距離最小化なので、original GAN のような理論解析は不要かと思います。

ちょっとした考察

この論文では、EMD の定義だとか Wasserstein distance が EMD の双対だとかポンと出てくる印象です。ここでは定義とか双対性に付いて少し考えてみようと思います。「ちょっとした」と書きましたが、結構長くなるのでご注意ください。

discrete 版 EMD

EMD の定義の気分を知ったり双対と言った部分をはっきりさせるために、以下、discrete に切って考えてみます。discrete に切るとは、$x\in R^n$をそのまま考えるのではなく、格子状に切って${x_i}$という集合を作って考えることを意味します。

まずは EMD から。論文には訳のわからない定義が書いてありますが、discrete に切ると、以下のような線形の最適化問題に帰着します。

\begin{align} &\min_{x_{ij}} \sum_{ij} d_{ij}x_{ij}\\ &s.t. x_{ij} \geq 0, \sum_{i}x_{ij} = p_{data}(x_j), \sum_{j}x_{ij} = p_g(x_{i}) \end{align}

ここに $d_{ij}$ が二点間の距離で、$x_{ij}$ が求めたい変数。このように書き下してみると、この問題は「最も効率よく$p_g$という山を$p_{data}$という山に移すためにはどうすれば良いか」という問題に対応することが分かります。というのも、$x_{ij}$が$i\rightarrow j$と輸送する量(対角成分は $i$ 地点に残す量)だと思えば、各制約は

  • 一つ目の制約$i\rightarrow j$ に輸送する量は$0$以上と言う自明な制約
  • 二つ目は $j$ 地点に輸送して欲しい量の制約
  • 三つ目は $i$ 地点から運び出すことができる量の制約

をそれぞれを表しているためです。「効率よく」というのは総移動距離$\times$輸送量の最小化を意味しています。

この線形最適化問題について、例えば、$p_{data} = p_g$ であれば、全部残せば良いので自明に最良解は $0$ となり、確率分布に少しでも差が出れば、輸送が必要になるので最良の目的関数値は $0$ より大きくなります。そのため、上記線形計画問題が二つの確率分布の一致具合を測る指標となるわけです。

EMD と Wasserstein distanceの双対性

EMD の気分が分かったので、 Wasserstein distance との双対性を確認してみます。まず、上記最適化問題は線形の最適化問題なので、最良の目的関数値を知る上ではLagrange 双対問題を考えても何ら問題ありません。Lagrange 双対の詳細はここ をご参照ください。上記最適化問題の Lagrange双対問題は

\begin{align} &\max_{\lambda,\nu} \sum_{i}\lambda_i p_g(x_i) + \sum_{j}\nu_j p_{data}(x_j)\\ &s.t.\ \ \lambda_i + \nu_j \leq d_{ij} \end{align} となります。ここに$\lambda, \nu$は双対変数を表します。以下ではこの問題を Original Problem と呼び、この問題の最適解を $z^{*}$ と書くことにします。

このOriginal Problemですが、実は以下のような$\lambda_i + \nu_i = 0$という制約を追加したような問題(Modified Problem)を考えても最良の目的関数値は同じになります。以下ではそのことを示します。

\begin{align} &\max \sum_{i}\lambda_i p_g(x_i) - \sum_{j}\lambda_j p_{data}(x_j)\\ &s.t.\ \ \lambda_i - \lambda_j \leq d_{ij} \end{align}

この Modified Problem の最適な目的関数値を $z_{mod}^*$と書くことにすれば、$z_{mod}^*\leq z^*$ と $z^*\leq z_{mod}^*$の両方が示せれば良く以下ではそれを確認します。

まずは前者ですが、これは自明。上述したように、Modified Problem は Original Problemに制約を追加して作っているため、最良の目的関数値は悪くなるため。 ということで、$z^*\leq z_{mod}^*$を頑張って示せば良い事になります。

OriginalProblem の最適解を $\lambda_i^*, \nu_j^*$と書くことにします。これらを使って、新しく次のような量を定義します。

\begin{align} \mu_i := \min_j\left(d_{ij} - \nu_j^* \right) \end{align} この$\mu$には、以下のような二つの性質があります。

  1. $\lambda_i^*\leq \mu_i\leq -\nu_i^*$
  2. $|\mu_i - \mu_j|\leq d_{ij}$

以下ではこの二つの性質を示しますが、この二つが示せた場合のご利益を先に述べます。Original Problemにおいて$p_{data}$, $p_g$が positiveであることから、一つ目の性質から、単なる値比較をすると

\begin{align} z^*\leq \sum_{i}\mu_i p_g(x_i) - \sum_{j}\mu_j p_{data}(x_j) \end{align} という不等式が得られます。よく見ると、右辺は Modified Problemの目的関数と同じ形をしており、さらに $\mu$ の二つ目の性質から、$\mu_i$は Modified Problemの制約を満たしています。このこととModified Problemが最大化問題であることを考慮すれば、 \begin{align} z^*\leq \sum_{i}\mu_i p_g(x_i) - \sum_{j}\mu_j p_{data}(x_j)\leq z_{mod}^* \end{align} という評価が得られ証明終了とななります。ということで頑張って$\mu_i$の性質をcheck できればOK。

まずは一つ目。$\lambda_i^*$がOriginalProblem の制約を満たしているので、$\lambda_i^* \leq d_{ij} - \nu_j^*$が成立。これがすべての$j$について成り立ち、 $\mu_i$の定義を思い出せば、$\lambda_i^{*}\leq \mu_i$が成立します。また$\mu_i$の定義から$\mu_i\leq d_{ii} - \nu_{i} = -\nu_i$となるので一つ目の性質は成り立ちます。 続いて二つ目。各$\mu_i = \min_k\left(d_{ik} - \nu_k^* \right)$について、右辺を最小とする$k$を$k_i$と書くことにすれば、任意の$j$に対して、$\mu_i = \min_k\left(d_{ik} - \nu_k^* \right)= d_{ik_i} - \nu_{k_i}^*\leq d_{ik_j} - \nu_{k_j}^*$が成立します。$\mu_i > \mu_j$のとき、この性質と三角不等式を使えば、

\begin{align} |\mu_i - \mu_j| \leq |d_{ik_j} - \nu_{k_j}^* - (d_{jk_j} - \nu_{k_j}^*)|\leq |d_{ik_j} - d_{jk_j}| \leq d_{ij} \end{align} となります。$\mu_i < \mu_j$の際もほぼ同様の式変形で同じ評価が得られるので、二つ目の性質が成り立ちます。 よって、Modified Probelm と Original Problem は同じ最良値を持つことが示せました。

Modified Probelm の目的関数から、これの連続版は Wasserstein distance の目的関数となることが想像できると思いますし、Modified Problem の制約から 1-Lipschitz の制約が出てくるのも見えるかと思います!

ふぅ、疲れた。。。

Day 3 LSGAN


論文は https://arxiv.org/abs/1611.04076 です。

LSGAN 概要

Original GAN から objective を変えた論文です。

そもそも Discriminator の学習では、本物に対しては $1$ を返し偽物に対しては $0$ を返せるようになれば良く、 Generator の学習では Discriminator の出力を $1$ にできるようにすれば良いです。とすると、素直に以下のような objective を minimize して行けば良いのでは、と思えてきます。

\begin{align} \mathcal{L}_D &= \mathbb{E}_{x\sim p_{data}}\left(D(x) - 1\right)^2 + \mathbb{E}_{z\sim p_{z}}\left(D(G(z))\right)^2\\ \mathcal{L}_G &= \mathbb{E}_{z\sim p_{z}}\left(D(G(z)) - 1\right)^2 \label{lsgan_naiive} \end{align} このように変更すると、sigmoid cross entropy よりも勾配損失が起こりにくくなり安定して計算がしやすくなりそう、というのがご利益です。

LSGAN論文ではもっと一般的に、 \begin{align} \mathcal{L}_D = \mathbb{E}_{x\sim p_{data}}\left(D(x) - b\right)^2 + \mathbb{E}_{z\sim p_{z}}\left(D(G(z)) - a\right)^2\\ \mathcal{L}_G = \mathbb{E}_{x\sim p_{data}}\left(D(x) - c\right)^2 + \mathbb{E}_{z\sim p_{z}}\left(D(G(z)) - c\right)^2 \end{align} とした場合に、どのような$(a, b, c)$だとうまく二つの確率分布が一致するのか、を調べています。なお、$\mathcal{L}_G$ の第一項は、最適化計算では関係ありませんが、テクニカルな理由で付けてあります。

理論解析

以下では二つの分布が一致する条件を調べますが、証明は完全に original GAN で紹介したものと同じ道筋を辿ります。original GAN では、任意の $G$ に対して最良の $D$ を計算し、それを元の objective に代入してちょっと計算すると JS divergence になって、という証明でした。LSGAN も全く同様の計算をしていけば大丈夫です。 $\mathcal{L}_D$ から $G$ を固定した場合の最良の $D$ は \begin{align} D_G = \frac{bp_{data} + ap_g}{p_{data} + p_g} \end{align} となります。これを $\mathcal{L}_G$ に代入してちょっと計算すると \begin{align} \mathcal{L}_G = \int \frac{( (b - c)(p_{data} + p_g) - (b - a)p_g)^2}{p_{data} + p_g} \end{align} という評価が得られます。この評価にたどり着く際に、先に付け加えた項が効いてきます。この評価から、例えば \begin{align} b - c = 1, b - a = 2 \end{align} の場合には \begin{align} \mathcal{L} = \chi^2_{Pearson}(p_{data} + p_g||2p_g) \end{align} と Pearson $\chi$ squared divergence の形になり、やっぱり $p_{data} = p_g$ の際に最小となることが示せます。

ちょっとした考察

LSGAN の最初に述べた objective は $b = c = 1, a = 0 $なので、条件を満たしていないように感じます。しかし、少し置き換えをして計算してやればやっぱりPearson $\chi$ squared の形になることを示せます。 論文では $(a, b, c)$として条件に合うものに注目していますが、論文推奨のパラメータ以外でももっと良いものがあるかもです!

Day 4 f-GAN


論文は https://arxiv.org/abs/1606.00709 です。

f-GAN の概要

f-GAN も original GAN から objective を変更した論文です。

GAN は二つの確率分布を同じにするのが基本です。なので、二つの確率分布間の距離を定義し、$p_{data}$ と $p_g$の距離を $0$ にするように学習するのは素直な話です。Wasserstein GAN もそうでしたね。Wasserstein GAN は EMD あるいは Wasserstein distance で考えていましたが、f-GAN では f-divergence class \begin{align} D(p_{data}||p_g) = \int p_gf\left(\frac{p_{data}}{p_{g}}\right) dx\ . \end{align} をベースとして考えています。ここに $f$ は $f(1) = 0$を満たす関数で、例えば、$f(x) = x\log(x)$ とすれば KL divergence になり、$f(x) = x^2-1$とすれば Pearson $\chi$ sqruaredとなります。この f-divergene ですが、凸解析の定理や Jensen不等式などを使用すると下から評価でき、f-GAN ではその下からの bound を良い指標と思って学習を行います。

理論解析

他の項目の理論解析とは少々異なりますが、ここでは f-divergence の下からの評価を確認したいと思います。

定理と定義

論文では凸解析の定理を使っているので、ここでは、定理の適用範囲を確認ということで定義とか定理を述べるだけ述べておこうと思います。詳しくは凸解析の参考書などをご覧ください。

論文で使用している定理は「閉真凸関数であれば、$f^{**} = f$」です。ということで、以下では閉真凸と$f^{*}$ の定義だけ述べておきます。

以下では $f: R^N \rightarrow [-\infty, \infty]$ とします。

  • 凸で、全ての $x$ について$f(x) > -\infty$ かつ $f(x) < \infty$ という $x$ が存在する関数が真凸関数。
  • 真凸かつ下半連続(任意の$x_0$ について $\liminf_{x \rightarrow x_0} f(x)\geq f(x_0)$)である関数が閉真凸関数。

$f^{*}$ の定義は以下です。

  • 真凸関数に対して $f^{*}(\xi) = \sup_x\left(\xi^Tx - f(x)\right)$ が Fenchel 双対。

今回利用している定理は最初にも述べたように「閉真凸関数であれば、$f^{**} = f$」なので、使う際には$f$ が上記した閉真凸であることの確認が必要になるのでご注意を。

f-GAN の objective

先に紹介したように、$f$ が閉真凸関数であった場合、$f = f^{**}$が成立するので \begin{align} f(x) = f^{**}(x) = \sup_{\xi}\left(x^T\xi - f^{*}(\xi)\right) \end{align} が成り立ちます。これを f-divergence の定義 \begin{align} D(p_{data}||p_g) = \int p_gf\left(\frac{p_{data}}{p_{g}}\right) dx\ . \end{align} に代入してJensen 不等式などを利用すると、 \begin{align} D(p_{data}||p_g) \geq \sup_{D}\left[ \mathbb{E}_{x \in p_{data}}\left(D(x)\right) - \mathbb{E}_{x\in p_g}\left(f^{*}(D(x))\right)\right] \end{align} という評価を得られます。右辺を f-divergence の良い指標と思い GAN のような min max 学習を行っていくのが f-GAN です。

個人的な印象ですが、motivation があり導出が一直線な気持ち良い論文です!

ちょっとした考察

実は、真凸などでも $f\geq f^{**}$ は成立するので、上記不等式自体はもう少し広いクラスでも成立はします。まぁ、真凸と閉真凸の差は$dom_f$の boundery くらいなので真凸ではあまり面白いことはできないかと思いますが。。。

Day 5 DualGAN


論文は https://arxiv.org/abs/1704.02510 です。

DualGAN の概要

GAN を利用したアプリケーション系の論文です。

やりたいことは domain 変換です。例えば、イラスト $\leftrightarrow$ 写真といった変換で、イラストを与えた時にそれっぽい写真を作成したり、その逆をしたりできるように学習を行います。 この論文の手法の特徴としては、pix2pix とは異なり、データが対にはなっていないこと。つまり、学習データとして「イラストとそれに対応する写真」が与えられるのではなく、「イラストの集合と写真の集合」がデータとして与えられている状況で上記の学習を行います。

DualGAN の学習方法

他の項目では理論解析と称していましたが、 アプリケーションよりの論文ということで学習方法という項とさせて頂きます。

学習の基本戦略は以下の図の通りです。図は論文より引用。 f:id:taka_t0m0:20170522161949j:plain 図のように、Generator と Discriminator を二組み用意します。Generator で domain 変換、Discriminator は違う domain からの絵を偽物と判定、といった感じで学習を行いますが、詳細な手順は以下の通りです。

domain U の絵に対しては、

  1. domain U から絵を draw。この絵については Discriminator B で True と判定。
  2. その絵を Generator A で domain V に変換
  3. 変換された絵を Discriminator A で Flase と判定
  4. 変換された絵を Generator B で domain U に戻し、reconstruction error を加える。

とします。domain V に対しても $A \leftrightarrow B$, $U \leftrightarrow V$ とした手続きをすれば OKです。 このように学習を行い、二つの Generator で domain が行き来できるようになることを狙います。

ちょっとした考察

具体的にどうやれば良いのか見えていませんが、auto encode する際に、$p(x^{\prime}|x) = \sum_y p(x^{\prime}|y)p(y|x)$ と思って sum を考慮したらどうなるかが気になります。DualGAN の論文でも指摘されているように、今回の変換は機械翻訳の話から inspire されています。そして機械翻訳の場合にはこれを考慮していることがあります。例えば、https://arxiv.org/abs/1606.04596 です。これを考慮できるのか否かや入れて良くなるかどうかは全く見えていませんが、今後も考察をと考えています!

Day 6 infoGAN


論文は https://arxiv.org/abs/1606.03657 です。

infoGAN の概要

GAN の学習ができれば、latent の各次元にも意味が出てくることが期待されます。例えば、MNIST であれば、latent の変数をうまく動かせば違う数字を出力できるようになる、といった具合です。ただ、どの変数をどうすればどう変わるのか、というのは予めは分からないので、そこをなんとかできないか、と考えたのが infoGAN です。つまり、latent の中である次元に指定の意味を持つように学習を行います。この論文では、それを目指すために相互情報量を正則化項として加えています。

理論解析

理論解析ではありませんが、正則化に用いる項や objective を紹介できればと思います。

上記した通り、latent の一部に明確な意味を持たせるために、相互情報量を正則化項として加えます。明確な意味を持たせるには、generator に食わせる latent を $z_{full} = (c, z)$ と分けた時、なんらかの事後確率 $P(c|G(c, z))$ が高くなりなさい、という制約のもとで学習を行えば良いことが想像されます。例えば、MNIST であれば、c を数字の種類だと思うと、$c$ と $z$ を与えて絵を generate し、その絵は $c$ である確率が高くありなさいという条件を課せば、latent の中でも$c$は数字の種類の情報を持つことが期待できます。論文では $P(c|G(c, z))$ ではなく相互情報量を正則化項として加えています。ここに相互情報量の定義は以下の通りです。 \begin{align} I(c; G(c, z)) = H( c) - H(c|G(c, z)) = -\log(p( c)) + \log(p(c|G(c, z)))\ . \end{align} 本当の $P(c|G(c, z))$ を求めるのは難しいので、DNN で近似した $Q(c|G(c, z))$ を用いると、上記相互情報量を以下のように下から抑えることができます。(詳細は論文を。) \begin{align} I(c; G(c, z)) \geq H( c) + \mathbb{E}_{c\in P( c), x \in G(c, z)}\left[Q(c| G(c, z))\right] := \mathcal{L_{m}}\ . \end{align}

infoGAN ではこの下限を用いて、 \begin{align} \mathcal{L} = \mathcal{L}_{GAN} - \lambda\mathcal{L_{m}} \end{align} という objective を利用して学習を行います。$\mathcal{L}_m$を見ればわかるように、$P( c)$から $c$ を選び、その $c$ について、$Q(c|G(c, z))$ が高くなるという制約のもと $G$ の学習が進めるので、これで目的が達成できる訳です。

ちょっとした考察

完全に的外れ感もあり、また、具体的な話ではありませんが、やはり相互情報量というとnegative sampling を考えてみたくなります。もちろんラベルが少ないうちはこんなものを考える必要はありませんが、多くなった場合などのためにうまく組み合わせられないかな、と考えています!

Day 7 CVAEGAN


論文は https://arxiv.org/abs/1703.10155 です。

CVAEGAN の概要

CVAEGAN は、GAN を利用したアプリケーションよりの論文です。

やりたいこととしては、人の写真から「表情は同じで違う人」の絵を作ることです。もう少し言えば、写真と人の名前を input として一つのベクトルを作り、そのベクトルと別な名前から対象の人で同じ表情の写真を作ることを目指します。同じ表情で違う人、だけであればCVAEでもできそうですが、VAE 系の場合、loss として pixel 毎の二乗誤差 を使うためか絵がボヤけることが多い印象があります。そこで、mean squared error で測るのではなく、Discriminator で測りましょうというのがこの論文です。

CVAEGAN の学習方法

学習方法ですが、以下の絵を見ればそのまんまです。絵は論文から引用。 f:id:taka_t0m0:20170522162007j:plain

VAE 部分については以下のような手続き。

  1. 絵とそのラベルを draw
  2. 絵とラベルから Encoder で hidden vector zを作成。
  3. z とラベルから絵を Generator が作成。作成した絵と元の絵で pixel wise mean squared error。

GAN 部分については以下のような手続き。

  1. 絵とそのラベルを draw。z も draw。
  2. z とラベルから絵を Generator が作成。その絵を Discriminator が False と判定。
  3. draw してきた絵を Discriminator が True と判定。

残りのロスは feature map loss です。

このように学習すれば、ある $(x, c)$から hidden $z$ を作り、その $z$ と $c^{\prime}$ を使って Generator にかけてやれば目的が達成できるかと思います。

ちょっとした考察

論文では supervised、つまり全ての絵にクラスが付いている状況でやっているようですが、おそらくこのモデルは semi-supervised に拡張可能かと思います。上記CVAE論文のごとくやれば行けると思います!

終わりに


以上、一日一GAN を報告しました。一応論文で急に出てきた式を考察したり、論文を読んでこういうこともできるかもを意識して書いてみましたが、いかがだったでしょうか? 考察中と述べた部分もございましたし、この辺り一緒に議論して頂ける人がいらっしゃいますと幸いです。最初にも述べましたが、それぞれ implementation はしたけれどもパラメータ調節などはしていない、という状況です。機会があればこれらの比較実験もして報告できれば、と思います! またその時まで!長々とありがとうございました。

宣伝


ABEJA では、イケててヤバいエンジニアを募集しています。GAN はしばらく流行りそうですし、今後 GAN でできることもガンガン広がっていくと思います。新しいGAN を含め最先端技術を使ってガンガン行きたい方是非お待ちしております!

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

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

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

学生向けの勉強会もしています!ガンガン応募してください!

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

参考文献

[1] Goodfellow, Ian, et al. “Generative adversarial nets.” Advances in neural information processing systems. 2014.

[2]Radford, Alec, Luke Metz, and Soumith Chintala. “Unsupervised representation learning with deep convolutional generative adversarial networks.” arXiv preprint arXiv:1511.06434 (2015).

[3] Zhao, Junbo, Michael Mathieu, and Yann LeCun. “Energy-based generative adversarial network.” arXiv preprint arXiv:1609.03126 (2016).

[4] Arjovsky, Martin, Soumith Chintala, and Léon Bottou. “Wasserstein gan.” arXiv preprint arXiv:1701.07875 (2017).

[5] Mao, Xudong, et al. “Least squares generative adversarial networks.” arXiv preprint arXiv:1611.04076 (2016).

[6] Nowozin, Sebastian, Botond Cseke, and Ryota Tomioka. “f-GAN: Training generative neural samplers using variational divergence minimization.” Advances in Neural Information Processing Systems. 2016.

[7] Yi, Zili, Hao Zhang, and Ping Tan Gong. “DualGAN: Unsupervised Dual Learning for Image-to-Image Translation.” arXiv preprint arXiv:1704.02510 (2017).

[8] Xi Chen, Yan Duan, Rein Houthooft, John Schulman, Ilya Sutskever, Pieter Abbeel. “InfoGAN: Interpretable Representation Learning by Information Maximizing Generative Adversarial Nets.” arXiv preprint arXiv:1606.03657(2016).

[9] Bao, Jianmin, et al. “CVAE-GAN: Fine-Grained Image Generation through Asymmetric Training.” arXiv preprint arXiv:1703.10155 (2017).

[10] http://www.inference.vc/are-energy-based-gans-actually-energy-based

[11] http://www.msi.co.jp/nuopt/glossary/term_c93514f7967b5ea6f4c77d569addef8d655072df.html

[12] Cheng, Yong, et al. “Semi-supervised learning for neural machine translation.” arXiv preprint arXiv:1606.04596 (2016).

[13]Kingma, Diederik P., et al. “Semi-supervised learning with deep generative models.” Advances in Neural Information Processing Systems. 2014.

機は熟した!グラフ構造に対する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個くらいはあっという間に埋まってしまいます。ただ枠線があるだけなのに、無地のノートより手を動かすのが早くなる不思議?