自己符号化器でイラストが再現できるかな

参考文献
  1. Building Autoencoders in Keras の Convolutional autoencoder のモデル
  2. かわいいフリー素材集 いらすとや
手順
  • いらすとや(参考文献2)のカレー、ハンバーガー、ラーメンの画像を保存します。
  • 画像を読み込んで 64×64 にリサイズします。
  • 以下の自己符号器を学習します(参考文献1)。次元は少し異なります。エンコード後の shape は (8, 8, 8) です。

f:id:cookie-box:20170507222625p:plain:w720

結果

上段が元画像(リサイズで解像度が落ちています)、下段がエンコード→デコード後の画像です。
エンコード→デコード後はぼやけていますがカレー、ハンバーガー、ラーメンが再現できていると思います。

f:id:cookie-box:20170508220651p:plain:w480

2017-05-09 追記
学習した自己符号器でいらすとやの別のいらすとが再現できるかもテストしてみます。
いらすとやの別のハンバーガー、ラーメンをテストします(右側の2枚)。
→ 訓練用の3枚の再現に特化しすぎていて全然駄目でした。
f:id:cookie-box:20170509061643p:plain:w800

スクリプト
# -*- coding: utf-8 -*-
import numpy
import cv2
import matplotlib.pyplot as plt
import sys

from keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D
from keras.models import Model

if __name__ == "__main__":
  #============================================================
  # 画像を読み込みます
  image_files = ['/Users/cookie/Downloads/food_curryruce.png',
                 '/Users/cookie/Downloads/food_hamburger.png',
                 '/Users/cookie/Downloads/food_ramen_iekei.png']
  n_image = len(image_files)
  images = []
  size = (64, 64)
  for image_file in image_files:
    image = cv2.imread(image_file)
    image = cv2.resize(image, size)
    # plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    # plt.show()
    image = image.astype('float32') / 255.
    images.append(image)

  images = numpy.array(images)

  #============================================================
  # 自己符号化器を構築します
  input_img = Input(shape=(64, 64, 3))
  x = Conv2D(16, 3, 3, activation='relu', border_mode='same')(input_img)
  x = MaxPooling2D((2, 2), border_mode='same')(x)
  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(x)
  x = MaxPooling2D((2, 2), border_mode='same')(x)
  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(x)
  encoded = MaxPooling2D((2, 2), border_mode='same')(x)

  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(encoded)
  x = UpSampling2D((2, 2))(x)
  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(x)
  x = UpSampling2D((2, 2))(x)
  x = Conv2D(16, 3, 3, activation='relu', border_mode='same')(x)
  x = UpSampling2D((2, 2))(x)
  decoded = Conv2D(3, 3, 3, activation='sigmoid', border_mode='same')(x)

  autoencoder = Model(input_img, decoded)
  print(autoencoder.summary())

  autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

  autoencoder.fit(images, images, nb_epoch=10000, batch_size=3)

  #============================================================
  # 結果を表示します
  decoded_images = autoencoder.predict(images)

  for i in range(n_image):
    # 元画像の表示
    ax = plt.subplot(2, n_image, i+1)
    plt.imshow(cv2.cvtColor(images[i], cv2.COLOR_BGR2RGB))
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # エンコード→デコードした画像の表示
    ax = plt.subplot(2, n_image, i+1+n_image)
    plt.imshow(cv2.cvtColor(decoded_images[i], cv2.COLOR_BGR2RGB))
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

  plt.show()

Keras で自己符号化器を学習したい

Kerasブログの自己符号化器のチュートリアルをやります。
Building Autoencoders in Keras


このチュートリアルではMNISTの手書き数字のデータを例に色々な種類の自己符号化器を示しています。スクリプトは全て記事内に示されているので割愛します。上記の記事内でのモデルは Sequential() で生成したインスタンスに層を add していくのではなく、Model (functional API) で組み立てています。 この方法だと自己符号化器(エンコーダ + デコーダ)全体を学習して後からエンコーダ(デコーダ)部分のみ利用するというのが容易にできました。

以下はチュートリアル内で紹介されているモデルの理解のためのお絵描きです(この記事はお絵描きだけです)。

モデル1: 単純な自己符号化器

f:id:cookie-box:20170507160835p:plain:w600
このモデルの自分の環境での実行結果は以下のようになりました。
f:id:cookie-box:20170507160620p:plain:w770

モデル2: エンコーダがスパースな自己符号化器

f:id:cookie-box:20170507181112p:plain:w600
エンコーダの重みベクトルについてL1正則化項をペナルティにしてスパースにします。

モデル3: 多層な自己符号化器

f:id:cookie-box:20170507182856p:plain:w600

モデル4: 畳み込み自己符号化器

f:id:cookie-box:20170507222625p:plain:w800

モデル5: ノイズ除去モデル

f:id:cookie-box:20170507223517p:plain:w480

モデル6: 系列データに対する自己符号化器

f:id:cookie-box:20170507224843p:plain:w480
系列データに対する自己符号化器はこのように実現するらしいですが実例はありませんでした。

モデル7: Variational autoencoder(VAE)→ 別の記事

VAE についてのお絵描きと実行結果は以下の記事に書きました。
Keras で変分自己符号化器(VAE)を学習したい - クッキーの日記

雑記: 交差エントロピーって何

機械学習をしているとよく交差エントロピーを最小化させられると思います。
でも冷静に考えると交差エントロピーが何かよくわかりませんでした。むしろエントロピーがわかりませんでした。
以下の記事を読み、もっと無理がない例で短くまとめたかったのですが、やはり例に無理があり、長くなってしまいました。
参考文献

以下はこれら記事の劣化アレンジです。



A国、B国、C国があります。
A国では、一日の天気は25%ずつの確率で晴れ、曇り、雨、雪になります。B国では、晴れになる確率が50%、曇りになる確率が25%、雨か雪になる確率が12.5%ずつです。C国では天気は晴れにしかなりません。この3国の天気の確率関数をグラフに表すと以下のようになります。
f:id:cookie-box:20170506183749p:plain:w550

または、それぞれの天気になる確率を横向きの積み上げ棒グラフで表すと以下のようになります。
f:id:cookie-box:20170506184007p:plain:w250

あるとき、A国の気象庁に勤めるアリスは、A国内の大学から「過去の天気のデータをなるべくたくさんの日数ほしい」と申請を受けました。A国の法律で、気象庁からの情報提供は1枚のフロッピーディスク(死語)によって行うことになっていましたが、この架空の世界では記録媒体技術が残念だったのでフロッピーディスクの容量が1KB(8000ビット)しかありませんでした。しかも経費削減のため1件の情報提供にディスクは1枚しかつかえませんでした。そこで、アリスは容量を最大限に活用するためそれぞれの天気を「晴:00、曇:01、雨:10、雪:11」にエンコードし、1日の天気の情報を2ビットで表現することで、4000日分のデータを提供しました(8000 ÷ 2 = 4000)。

同じころ、B国の気象庁に勤めるボブもB国内の大学から同じ申請を受けました。B国も情報提供に係る制約はA国と同じです。ボブは、最初はアリスのように「晴:00、曇:01、雨:10、雪:11」というエンコードで情報を提供しようとしましたが、B国ではそれぞれの天気の確率に偏りがあることに気付きました。ボブは「晴:0、曇:10、雨:110、雪:111」と、確率が大きい晴れに短いビット数を割り振り、その代わり確率が小さい雨と雪に長いビット数を割り振ることで、1日の天気の情報の平均ビット数を 0.5*1 + 0.25*2 + 0.125*3 + 0.125*3 = 1.75 ビットにしました。こうすることで、8000ビットのディスクで4500日以上分のデータを提供しました(8000 ÷ 1.75 > 4500)。
f:id:cookie-box:20170506213818p:plain:w660
なお、C国の気象庁に勤めるクリスのもとには申請はありませんでした。C国の人々は申請するまでもなく過去の天気が晴れでしかないことを知っていました。いわば、C国のある1日の天気の情報を伝えるのに必要なビット数は0ビットでした。

アリスよりボブが同じ容量のディスクでよりたくさんの日数の情報を提供できたのは、ボブの方が優れたエンコードをしたからではありません。何ならクリスはディスクをつかうまでもなく無限の日数の情報を提供できています。1回の試行の結果(ある1日の天気)を伝えるのに必要な平均ビット数の最小値は確率分布によってのみ決まります。つまり、確率  P(\omega) で起きるできごとに長さ  \log (1/P(\omega)) のコードを割り当てるのが最適になります(証明略)。この最適なエンコードのとき、「晴、曇、雨、雪」を伝えるのに要するそれぞれのビット数  \log (1/P(\omega))情報量(選択情報量)、その期待値(1回の試行の結果を伝えるのに要する平均ビット数)をエントロピー(平均情報量)とよびます(以下)。
 \displaystyle H(P) = E_P \left[ \log \frac{1}{P(\omega)} \right] = \sum_{\omega \in \Omega} P(\omega) \log \frac{1}{P(\omega)}

A国、B国、C国それぞれの、「1日の天気を調べる」という試行のエントロピーは以下です。
 \begin{cases} H(P_A)=E_{P_A}\left[ \log_2(1/P_A(\omega)) \right]=0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)=2 \\ H(P_B)=E_{P_B}\left[ \log_2(1/P_B(\omega)) \right]=0.5 \log_2 (1/0.5)+0.25 \log_2 (1/0.25)+0.125 \log_2 (1/0.125)+0.125 \log_2 (1/0.125)=1.75  \\ H(P_C)=E_{P_C}\left[ \log_2(1/P_C(\omega)) \right]=1 \log_2 (1/1) =0\end{cases}

情報量を「その試行の結果を知らされたときの価値」、エントロピーを「1回の試行の結果の結果を知らされたときの価値の期待値」と解釈する人もあります。あるできごとを知らされたときの価値は、そのできごとが珍しいほど高くなります。A国ではどの天気になる確率も等しいので、どの天気だと知らされても等しく2の価値があります。B国では晴れになる確率が高いので、晴れだと知らされたときの価値は1しかありませんが、雨や雪になる確率は低いので、雨や雪だと知らされたときの価値は3あります。また、C国で晴れだと知らされても価値は0です。この価値の平均としてのエントロピーは不確実さが大きいほど(確率分布がばらつくほど)大きくなります。天気の不確実さは A国 > B国 > C国 の順になっているといえるでしょう。

ところであるとき、B国の大学でA国の天気のデータが必要になりました。B国の大学の人はボブ式のエンコードに慣れていたので、A国のアリスに、B国のボブ式のエンコードでA国の天気のデータを送るよう依頼しました。アリスは言われた通りにしましたが、A国の天気をボブ式にエンコードすると、1日あたりの平均ビット長が 1*0.25 + 2*0.25 + 3*0.25 + 3*0.25 = 2.25 になってしまうことに気付きました。この世界の残念なフロッピーディスクでは3600日分のデータも提供することができませんでした(8000 ÷ 2.25 < 3600)。

同じころボブもA国の大学の人にアリス式のエンコードでB国の天気データを提供するよう要請されました。アリス式のエンコードはどの天気にも2ビットをあてがうので、B国の天気をエンコードしても平均ビット長が2ビットになりました。ボブは、「自分のエンコードなら、この国の天気を1日あたり1.75ビットに圧縮できるのになあ」と思いました。
f:id:cookie-box:20170507075306p:plain:w220

ある確率分布に最適化された方式で別の確率分布をエンコードしたときの平均ビット長を交差エントロピーとよびます。
 \displaystyle H(P, Q) = E_P \left[ \log \frac{1}{Q(\omega)} \right] = \sum_{\omega \in \Omega} P(\omega) \log \frac{1}{Q(\omega)}

 \begin{cases} H(P_A, P_B)=E_{P_A}\left[ \log_2(1/P_B(\omega)) \right]=0.25 \log_2 (1/0.5)+0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.125)+0.25 \log_2 (1/0.125)=2.25 \\ H(P_B, P_A)=E_{P_B}\left[ \log_2(1/P_A(\omega)) \right]=0.5 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)+0.125 \log_2 (1/0.25)+0.125 \log_2 (1/0.25)=2 \end{cases}

これはエンコードを誤ったようなイメージです。エンコードを誤ると、めずらしいできごとに小さなビット長を割り振ってしまう影響よりも、めずらしくないできごとに大きなビット長を割り振ってしまう影響の方が必ず大きくなり、必要な平均ビット長が大きくなってしまいます( H(P_A, P_B) > H(P_A, P_A) = H(P_A) )。つまり、ビット長の無駄が生じます。

H(P_A, P_B) は「B国の情報の価値尺度でA国の情報を受け取ったときの平均的な価値」と解釈することもできます。B国ではめずらしい雨や雪が、A国では比較的めずらしくないので、その分A国の天気をA国の尺度で受け取るよりも、A国の天気をB国の尺度で受け取った方が価値が上がってしまいます。

このビット長の無駄/価値の誤差 H(P_A, P_B) - H(P_A) = D_{\rm KL}(P_A \, || \, P_B)カルバック・ライブラー情報量とよびます。
 \displaystyle D_{\rm KL}(P \, || \, Q) =H(P,Q)-H(P)= E_P \left[ \log \frac{1}{Q(\omega)} - \log \frac{1}{P(\omega)} \right] = E_P \left[\log \frac{P(\omega)}{Q(\omega)} \right] = \sum_{\omega \in \Omega} P(\omega) \log \frac{P(\omega)}{Q(\omega)}

 \begin{cases} D_{\rm KL}(P_A \, || \, P_B)=E_{P_A}\left[ \log_2(1/P_B(\omega)) - \log_2(1/P_A(\omega)) \right]=0.25 (1-2)+0.25 (2-2)+0.25 (3-2)+0.25 (3-2)=0.25 \\ D_{\rm KL}(P_B \, || \, P_A)=E_{P_B}\left[ \log_2(1/P_A(\omega)) - \log_2(1/P_B(\omega)) \right]=0.5 (2-1)+0.25 (2-2)+0.125 (2-3)+0.125 (2-3)=0.25 \end{cases}

ここではたまたま D_{\rm KL}(P_A \, || \, P_B)=D_{\rm KL}(P_B \, || \, P_A) になっていますが、一般に、KL情報量は確率分布の交換について非対称です。「A国の天気をB国方式でエンコードしたときのビット長の無駄」と「A国の天気をB国方式でエンコードしたときのビット長の無駄」は一般には等しくなりません。例えば、天気が晴れにしかならないC国では、晴れ以外の天気は無限大のコード長をもつことになるので、C国の方式で他の国の天気をエンコードした交差エントロピーも、カルバック・ライブラー情報量も無限大になります。しかし、他の国の方式でC国の天気をエンコードした場合は無限大にはなりません。
 \begin{cases} H(P_A, P_C)=E_{P_A}\left[ \log_2(1/P_C(\omega)) \right]=+\infty \\ H(P_C, P_A)=E_{P_C}\left[ \log_2(1/P_A(\omega)) \right]=2 \end{cases}

 \begin{cases} D_{\rm KL}(P_A \, || \, P_C)=H(P_A, P_C)-H(P_A)=+\infty \\ D_{\rm KL}(P_C \, || \, P_A)=H(P_C, P_A)-H(P_C)=2 \end{cases}

 \begin{cases} H(P_B, P_C)=E_{P_B}\left[ \log_2(1/P_C(\omega)) \right]=+\infty \\ H(P_C, P_B)=E_{P_C}\left[ \log_2(1/P_B(\omega)) \right]=1 \end{cases}

 \begin{cases} D_{\rm KL}(P_B \, || \, P_C)=H(P_B, P_C)-H(P_B)=+\infty \\ D_{\rm KL}(P_C \, || \, P_B)=H(P_C, P_B)-H(P_C)=1 \end{cases}

KL情報量は2つの分布の間の距離のようなものと表現されることがあります。KL情報量は非対称なので、D_{\rm KL}(P \, || \, Q) P から見た  Q の近さといった方がいいのかもしれません。

ところで、機械学習では、分類器を学習するときに、正解の分布 P と分類器の予測分布 Q の交差エントロピー H(P, Q) を損失関数とすることがよくあります。交差エントロピーQ=P のときに最小となるので、分類器を学習して QP に近づくよう改善していく作業は、正解の分布に合わせてエンコードを最適化しようとする作業に似ています。P が固定ならば、H(P, Q) の最小化は  D_{\rm KL}(P \, || \, Q) の最小化に他なりません。また、学習の初期にC国のような分布からはじめてしまうと、正解の分布から見た距離がどうしようもなく遠くなってしまうことが予想されます(ちょうどA国の分布から見たC国の分布の交差エントロピーが無限大であるように)。

分布と分布がどれだけずれているかの尺度は何も交差エントロピーだけでなく、2乗誤差なども考えられます。2乗誤差も交差エントロピーも、小さくなるようにパラメータを更新し続ければ分類器が改善していくことが期待されます。交差エントロピーの利点は、分布がまるで期待外れのとき(誤差が大きいとき)学習の速度が速い点です。2乗誤差は、誤差が大きいときに必ずしも学習が速くなりません。例えばシグモイド関数やソフトマックス関数で活性化された分類器の出力があるクラスにかなり偏ってしまっているとき、それが期待とはほど遠い状態であるにもかかわらず、2乗誤差の勾配はとてもなだらかになってしまいます。この状態でパラメータを少し動かしたところで、分布はほとんど変わらず、2乗誤差はほぼ変わらないからです。交差エントロピーの場合、分類器の出力が期待せずかなり偏ってしまった場合、真の分布から見た交差エントロピーがそれこそ無限大の勢いで大きくなるので(A国の分布から見たC国の分布の交差エントロピーのように)、勾配も大きくなります。この期待外れの時に勾配が大きいという望ましい性質により、交差エントロピーは広く採用されています。

LSTMでどのキャラクターのセリフか判別する

Qiita に投稿しました。
qiita.com

Keras の LSTM で「数学ガール」に出てくるセリフを誰のセリフか判別しました。
数学ガール/フェルマーの最終定理 (数学ガールシリーズ 2) | 結城 浩 |本 | 通販 | Amazon
モデルの中身は以下の記事(映画レビューのポジティブ/ネガティブ分類)です。
Sequence Classification with LSTM Recurrent Neural Networks in Python with Keras - Machine Learning Mastery

所感

  • 今回のモデルでセリフの話者を正しく判定できなかったのは以下です(セリフは「数学ガール」からの引用です)。
    A:僕、B:ミルカさん、C:テトラちゃん、D:ユーリ
    f:id:cookie-box:20170504220718p:plain:w700
    • 「あたし」が出てきたらテトラちゃんのセリフだとわかってほしいような。
  • モデルの構造もハイパーパラメータも自由度がありすぎてどうするといいのかよくわかりませんでした。
    • ただ、「数学ガール」に出てくるセリフの話者を判別するのに必要な特徴量ベクトルの次元(LSTMの出力の次元)は映画レビューと違って100もない気がします(?)。例えば人間的な感覚では「女っぽさ」「先生っぽさ」「後輩っぽさ」の3次元くらいで判別できそう。「先生っぽさ」だけ強ければ「僕」、「女っぽさ」と「後輩っぽさ」が強ければ「テトラちゃん」とか…。ただ、LSTMの出力の次元をそこまで極端に減らすとかえって精度が悪くなりました。
    • 元モデルは英語の単語列に対するもので、今回は日本語の形態素列に対するものですが、畳み込みのフィルターサイズやマックスプーリングのプールサイズは変更するとかえって悪くなるようでした。
  • セリフの話者が判別できると何がうれしいの? → 特にないです。
  • 全キャラクターのセリフが100個以上収集できるまで、数学ガールを読み返しながらタイプしていたのですが、テトラちゃんが一番無口でボトルネックでした…。いや、テトラちゃんが無口というより、他の3人がしゃべるしゃべる…。

Keras の LSTM で時系列の予測がしたい

深層学習で時系列の予測がしたいときとかあると思います。
以下の記事を参考にさせていただきます。
qiita.com

それで時系列データが手元にないので以下のサイトにある日経平均株価の日足をつかいます。
  日経平均株価 1時間足 時系列データ CSVダウンロード
直近1000営業日の値動きをプロットすると以下です。縦軸は学習のためにスケーリング済みです。

f:id:cookie-box:20170409085713p:plain:w700

以下のコードで学習します。参考記事のままです。前日まで10営業日分の株価を入力して当日の株価を予測するというモデルにします。900営業日分のデータで学習し、100営業日分のデータでテストすることにします。

# -*- coding: utf-8 -*-
import numpy
import pandas
import matplotlib.pyplot as plt

from sklearn import preprocessing
from keras.models import Sequential
from keras.layers.core import Dense, Activation
from keras.layers.recurrent import LSTM

# 学習用データを抽出する関数
def _load_data(data, n_prev=10):
  docX, docY = [], []
  for i in range(len(data) - n_prev):
    docX.append(data.iloc[i:(i+n_prev)].as_matrix())
    docY.append(data.iloc[i+n_prev].as_matrix())
  alsX = numpy.array(docX)
  alsY = numpy.array(docY)
  return alsX, alsY

if __name__ == "__main__":
  # 株価データの読み込み
  data = None
  for year in range(2007, 2018):
    data_ = pandas.read_csv('indices_I101_1d_' + str(year) +  '.csv')
    data = data_ if (data is None) else pandas.concat([data, data_])
  data.columns = ['date', 'open', 'high', 'low', 'close']
  data['date'] = pandas.to_datetime(data['date'], format='%Y-%m-%d')
  data['close'] = preprocessing.scale(data['close'])
  data = data.sort_values(by='date')
  data = data.reset_index(drop=True)
  data = data.loc[:, ['date', 'close']]
  data = data[1516:2515]

  # plt.plot(data['date'], data['close'])
  # plt.show()

  # 学習の設定
  length_of_sequences = 10
  in_out_neurons = 1
  hidden_neurons = 300

  # データ準備
  X_train, y_train = _load_data(data[['close']].iloc[0:900],    length_of_sequences)
  X_test,  y_test  = _load_data(data[['close']].iloc[900:1000], length_of_sequences)

  # ニューラルネットの定義
  model = Sequential()
  model.add(LSTM(hidden_neurons, \
            batch_input_shape=(None, length_of_sequences, in_out_neurons), \
            return_sequences=False))
  model.add(Dense(in_out_neurons))
  model.add(Activation("linear"))
  model.compile(loss="mean_squared_error", optimizer="rmsprop")

  # 学習
  model.fit(X_train, y_train, batch_size=100, nb_epoch=100, validation_split=0.05)

  # テスト結果表示
  predicted = model.predict(X_test)
  result = pandas.DataFrame(predicted)
  result.columns = ['predict']
  result['actual'] = y_test
  result.plot()
  plt.show()

テスト結果は以下です。当日の株価を予測するというか、前日をなぞっているだけですね。しかもなんかオフセットが付いているし。こんなの予測をしているといえない。知ってた。

f:id:cookie-box:20170409084531p:plain:w720

ネットワークに予測性能があるか確認するのに、もう少し「直前のN点から次の1点を予測できそう」な時系列をつかいたいです。そこで、R 組み込みの UKgas をつかいます。以下のようなデータです。四半期毎のデータなので周期性をもちます(冬はガス消費量が多く、夏は少ない)。
f:id:cookie-box:20170303235217p:plain:w700

以下の箇所以外さっきと同じコードで学習します。さっきは「直前までの10点から次の1点を予測」にしましたが、今回は四半期毎のデータなのでなんとなく4の倍数で12にします。UKgas は107点ありますが、最初の90点を学習につかい、最後の17点が予測できるかをテストします。

  length_of_sequences = 12
  in_out_neurons = 1
  hidden_neurons = 300

  X_train, y_train = _load_data(data[['gas']].iloc[0:90],    length_of_sequences)
  X_test,  y_test  = _load_data(data[['gas']].iloc[78:107], length_of_sequences)

  model.fit(X_train, y_train, batch_size=30, nb_epoch=100, validation_split=0.05)

テスト結果は以下です。予測結果も周期性をもっており、前回をなぞるだけではなく予測めいたことをしているようにはみえます。よかった。

f:id:cookie-box:20170409092832p:plain:w720