フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

あなたの身近にもあるかもしれない『ダブりデータ』

みなさんこんにちは。エンジニアの佐藤です。今回はIoTの話題をご紹介させていただきたいと思います。

重複データの蠢き

先日、とあるIoTシステムのログデータをETL(Extract, transform, load)処理によりDWHに収めました。このシステムは運用暦約1年で、利用量は右肩上がりで上昇してきました。蓄積データの活用が企画され、数億件のデータがDWHに集積されました。多数のデバイスが毎日送信して積み上げてきたものです。

ですが、およそコンピュータと名の付く物、何事も疑いの目で見なければなりません。筆者がまず気になったのは、「まともなデータなのか」という点です。数億レコードともなると、もはや個別確認はできません。どんなゴミが混入しているのかわかったものではありません。

「まともなデータ」の基準はいろいろあると思いますが、筆者が一番重要だと思うのは、一意性です。RDBMSで言えば「テーブルのキー項目は何か」ということになります。DWHにユニーク制約はありませんから、スキーマが一意性を担保することはありません。まずこの点から確認する必要があります。

このシステムの場合、送信データにはデバイスのID、対象事象発生時のデバイス時刻、シーケンス番号が含まれています。シーケンス番号は電源OFFでリセットされますから、一意ではありませんが、デバイスのIDは重複がないことが保証されています。時刻は外部の信頼できる時計に自動補正ですから、一旦正確だと前提しましょう。詳細は省きますが、 バイスID、事象発生時のデバイス時刻、ログ到着時刻の全てが重複することは、仕様上無いはずです。 DWHにはBigQueryを使っていますので、以下のようにクエリしてこの点を確認してみました。

#standardSQL
SELECT 
  xxxxx AS dt_log,
  xxxxx AS device_id,
  xxxxx AS device_time,
  COUNT(*) count
FROM table02 
WHERE _PARTITIONTIME = TIMESTAMP('2018-06-10')
GROUP BY dt_log, device_id, device_time
HAVING count > 1
※ 一部は伏字または仮の値にしています。

+-------------------------+-----------+---------------------+-------+
|         dt_log          | device_id |     device_time     | count |
+-------------------------+-----------+---------------------+-------+
| 2018-06-10 00:00:00.045 | 4641      | 2018-06-09 23:59:56 |     2 |
| 2018-06-10 00:00:00.572 | 9458      | 2018-06-09 23:59:57 |     2 |
| 2018-06-10 00:00:00.014 | 9499      | 2018-06-09 23:59:57 |     2 |
| 2018-06-10 00:00:00.235 | 1686      | 2018-06-09 23:59:58 |     2 |
| 2018-06-10 00:00:00.195 | 1712      | 2018-06-09 23:59:58 |     2 |
+-------------------------+-----------+---------------------+-------+

それ見たことか。ダブりデータです。 日付が変わった直後から約1秒間の間で発生しているように見えます。念には念を入れて受信した生データも比較してみましたが、完全に重複しています。 これはいわばデータの「バグ」。原因を特定せねばなりません。

何と何がダブっているのか

同じデータが2回DWHに登録された理由は何か?この原因追求のために役立つのが、私が「経路情報」と呼んでいる、レコードの出自に関する記録です。ETL処理の節目ごとにこの経路情報を追記していき、全てのレコードについて元ネタが特定できるように仕掛けておくのです。

今回のダブりデータについて経路情報(log_file_pos)を表示すると、以下のようになりました。簡単に解説しますと、「2018-06-09/1/343159(00717206)/00」は、2018年06月09日のログファイルに記録された、受信ホスト1番が出力したログファイルの第343159番目のデータで、ファイルの第717206行目に記録されている、という意味です。(末尾の「/00」は説明を省略します。)

+-------------------------+-----------+---------------------+----------------------------------+
|         dt_log          | device_id |     device_time     |           log_file_pos           |
+-------------------------+-----------+---------------------+----------------------------------+
| 2018-06-10 00:00:00.235 | 1686      | 2018-06-09 23:59:58 | 2018-06-09/1/343159(00717206)/00 |
| 2018-06-10 00:00:00.235 | 1686      | 2018-06-09 23:59:58 | 2018-06-10/1/003(00000006)/00    |
| 2018-06-10 00:00:00.195 | 1712      | 2018-06-09 23:59:58 | 2018-06-09/1/343158(00717205)/00 |
| 2018-06-10 00:00:00.195 | 1712      | 2018-06-09 23:59:58 | 2018-06-10/1/002(00000005)/00    |
| 2018-06-10 00:00:00.045 | 4641      | 2018-06-09 23:59:56 | 2018-06-09/1/343157(00717202)/00 |
| 2018-06-10 00:00:00.045 | 4641      | 2018-06-09 23:59:56 | 2018-06-10/1/001(00000002)/00    |
| 2018-06-10 00:00:00.572 | 9458      | 2018-06-09 23:59:57 | 2018-06-09/1/343160(00717207)/00 |
| 2018-06-10 00:00:00.572 | 9458      | 2018-06-09 23:59:57 | 2018-06-10/1/004(00000007)/00    |
| 2018-06-10 00:00:00.014 | 9499      | 2018-06-09 23:59:57 | 2018-06-09/1/343156(00717201)/00 |
| 2018-06-10 00:00:00.014 | 9499      | 2018-06-09 23:59:57 | 2018-06-10/1/000(00000001)/00    |
+-------------------------+-----------+---------------------+----------------------------------+

この経路情報をダブりデータで比較してみると、連続する2つの日付の、ちょうど境界あたりに分布していることがわかります。ここまでくると、ダブりの原因は、日付ごとにログファイルを分ける処理にある可能性が高いと考えるのが妥当でしょう。

原因は AWS API CloudWatch Logs CreateExportTask の仕様誤認+だった

今回ETL処理のインプットとなったログファイルは、AWS CloudWatch Logsに出力されたログを定期的にエクスポートしてS3へ保存させたものです。このエクスポート処理の設定は定時ジョブで実行しており、以下のようになっています。

client = boto3.client('logs')
response = client.create_export_task(
    logGroupName      = log_group_name,
    fromTime          = from_ts * 1000,
    to                = to_ts * 1000,
    destination       = s3_bucket_name,
    destinationPrefix = s3_prefix
)

from_ts と to_ts はちょうど24時間(=86400秒)離れています。一見良さそうですが、この種の境界設定については、常に「inclusive(指定値を含む)」なのか「exclusive(指定値は含まない)」かを注意する必要があります。以下のURLからAPIの仕様を確認してみましょう。

https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateExportTask.html

Request Parameters

  • from
    • The start time of the range for the request, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. Events with a time stamp earlier than this time are not exported.
  • to
    • The end time of the range for the request, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. Events with a time stamp later than this time are not exported.

否定形で書かれていますが、本質的には inclusiveです。 つまり、ちょうど24時間分(=86400000ミリ秒)離れた2つの値をtoとfromに指定した場合、1ミリ秒分はダブる ということです。この点については、不注意だったと言えます。

原因がわかりました!と言いたいところですが、あれ?何かおかしいですよね。ダブりデータは「2018-06-10 00:00:00.045」から「2018-06-10 00:00:00.572」に分布していたのです。1ミリ秒ではなく、1秒ですね。結局AWSのこのAPI、ミリ秒指定と言っておきながら、ミリ秒部分は評価していないようです。

上記のAPIリファレンスの文章に当てはめてみましょう。2018-06-09のログファイルについて、

from: Events with a time stamp earlier than 2018-06-09 00:00:00.000 are not included.
to: Events with a time stamp later than 2018-06-10 00:00:00.000 are not included.

となっているわけですが、実際には 2018-06-10 00:00:00.000 から、少なくとも2018-06-10 00:00:00.572 までのログが、入ってしまっているわけです。 ミリ秒単位で指定させておきながら、これはひどいよな、と思います。

ExclusiveにすればOK、なのか?

AWSAPI仕様はわかりました。では、以下のようにエクスポート設定コードを改めればOKでしょうか?

client = boto3.client('logs')
response = client.create_export_task(
    logGroupName      = log_group_name,
    fromTime          = from_ts * 1000,
    to                = (to_ts * 1000) - 1,
    destination       = s3_bucket_name,
    destinationPrefix = s3_prefix
)

つまり、2018-06-09のログファイルについては以下のような指定になるわけです。

from: Events with a time stamp earlier than 2018-06-09 00:00:00.000 are not included.
to: Events with a time stamp later than 2018-06-09 23:59:59.999 are not included.

筆者は、これは危険な設定だと思います。今度はエクスポートの欠損が起こる可能性があるからです。 例えば、CloudWatch Logsにログが 2018-06-09 23:59:59.9995 に到着したら、取りこぼされてしまいます。

では、本当に23:59:59.9995などというタイミングでログが到着したという事象が発生するのでしょうか?

以下のCloudWatch Logsログ取得APIの仕様によると、

https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_GetLogEvents.html

https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_OutputLogEvent.html

ログの収録時刻(ingestionTime)は「ミリ秒単位の整数」となっていますので、23:59:59.9995は、23:59:59.999か、翌日00:00:00.000に丸められるでしょうから、現時点では大丈夫そうではあります。

しかし現在巷で使用されているハードウェアクロックの時間分解能は1ナノ秒となっており、AWS CloudWatch Logsがミリ秒よりさらに細かい到着時刻記録をサポートした場合、取りこぼしの心配が生じる可能性があります。(あくまで仕様上の話ですが。。。)

筆者の結論としては、このエクスポート設定コードは、このままにしておくことにしました。 データの受信記録が永久的に失われるというのは非常に恐ろしい出来事ですので、潜在的な危険はできるだけ排除したい。それなら多少の手間をかけ、後から重複を除去した方がマシ、だと思ったのです。

これで終わり、ではなかった!

これでダブりデータの原因は特定されました。安心して眠れそうです。。。しかし、本当にそうなのでしょうか…?

疑い深い筆者は、今度は「00:00:00.000 ~ 00:00:00.999 以外の時間帯のダブりデータ」を探してみました。そして。。。見つけてしまったのです。

+-------------------------+-----------+---------------------+---------------------------------+
|         dt_log          | device_id |     device_time     |          log_file_pos           |
+-------------------------+-----------+---------------------+---------------------------------+
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:36:54 | 2018-06-12/2/65245(00158574)/00 |
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:37:00 | 2018-06-12/2/65258(00158587)/00 |
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:37:00 | 2018-06-12/2/65244(00158573)/00 |
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:37:03 | 2018-06-12/2/65246(00158575)/00 |
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:37:03 | 2018-06-12/2/65250(00158579)/00 |
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:36:54 | 2018-06-12/2/65248(00158577)/00 |
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:36:57 | 2018-06-12/2/65257(00158586)/00 |
| 2018-06-12 14:37:31.449 | 4896      | 2018-06-12 14:36:57 | 2018-06-12/2/65247(00158576)/00 |
| 2018-06-13 00:44:05.565 | 2164      | 2018-06-13 00:43:57 | 2018-06-13/2/61588(00078056)/00 |
| 2018-06-13 00:44:05.565 | 2164      | 2018-06-13 00:43:57 | 2018-06-13/2/61587(00078055)/00 |
+-------------------------+-----------+---------------------+---------------------------------+

このダブりはこれまで特定した原因だけでは説明できません。一体何が起こったのか、筆者は元ログを調査しました。その結果、同時刻に同一データの到着が2回記録されていることを発見してしまいました。

158575  2018-06-12 23:37:31.449 : trackingProc len:36 data:4dxxxx...
158579  2018-06-12 23:37:31.449 : trackingProc len:36 data:4dxxxx...
※ ここでは時刻はデバイスからのデータを受信するインスタンスのシステム時刻で、JSTで書かれている。

そんなばかなと思ってログファイルのエクスポート元のAWS CloudWatch Logsもクエリしてみましたが、上記の2行のログは、全く同時刻に到着しています。

$ aws logs get-log-events --cli-input-json "`cat query_aws_logs.json`" --profile=xxxx | jq -r '.events[]|select(.message | contains("4dxxxx..."))|.ingestionTime'
1528814254774
1528814254774

次はネットワーク通信ログを、と言いたいところですが、残念ながらここまでは記録していませんでした。どこかで想定外の重複動作があったのでしょうが、真実はやぶの中です。原因が特定できないので対策方針が立つわけもなく、データクレンジングで対処するしかありません。今回の場合は同一デバイス・同一時刻に発生する物理事象はひとつに決まっていますから、ダブりデータのうちひとつだけを採用すれば良いでしょう。

この類の現象にどんな姿勢で臨めば良いのか

ここまでくると、「もう何を信じて良いやら」という気持ちになってきました。結局ログの記録、転送のそれぞれの段階で、ぴったり完璧とは行かない例外事象が起こってしまっているのです。そしてマニュアルも厳密でなく、オーナーシップがあるわけでもないので仕様は変わる。こういう状態でデータ分析をやるときは、いかなるポリシーを定めて事に取りかかればいいのでしょうか?

筆者の結論は以下のようなものです。

  • まず原理的に、分散環境でデータ転送する時は、「消えてもいいから最大1回(at most once)」「消えないがダブる可能性あり(at least once)」の2択。
  • そしてデータが途中で消えるのは困るので、ダブりを許容するしかない。
  • ダブりは、最後に洗い落とすしかない。

プログラムコードのデバッグにも似た対策措置が、データに対しても必要なようです。

最後までお読みいただき、ありがとうございました。

Cloud Vision APIでWebサイトを比較する

みなさんこんにちは。エンジニアの佐藤です。今回はCloud Vision APIの活用について書かせていただきたいと思います。

モチベーション

先日の Google I/O 2018 で、チャットボットが電話をかけて音声で美容室を予約するデモがあり、先進的事例として話題になりました。こうなると「この種の単純作業はAIに置き換わり、失業が社会問題になるのではないか。」という不安が募ります。しかし筆者の知っている範囲では、コンピュータの認識能力の柔軟性は、未だ人間には遠く及ばないと思います。 ただし、現時点で既に人間が追い越されている特性もあると思います。それは スピードと評価の安定性です 。24時間休みなく多数の画像判断を完全に一定の基準で実行することは、人間にはできません。しかしコンピュータは十八番です。 今回はこの特性を評価して、 Webサイトの画像全部の総合評価とWebサイト間の比較 を試みたいと思います。

筆者が選んだのは、大手リテールサイトのR社とA社です。どちらもホームページには100枚以上の商品などの画像があり、商品ジャンルも多岐にわたっています。 比較しようにも、評価観点が定まりそうにありません。そこで、これらの画像を全部、Cloud Vision APIでラベル検出させてみます。

道具立て

最初に両サイトの画像を個別ファイルとして保存し、次にそれぞれのファイルをCloud Vision APIでラベル検出させます。 結果はすべてPostgresqlに保存し、SQL言語でクエリして評価します。

Cloud Vision APIの結果

ラベル検出の仕様は以下のサイトに書かれていますが、

https://cloud.google.com/vision/docs/labels そのレスポンスは以下のように、「何が」「どのくらい確からしく」写っていたのかを答えてくれるというものです。

"labelAnnotations": [
    {
        "topicality": 0.9604079,
        "score": 0.9604079,
        "mid": "/m/01bqk0",
        "description": "bicycle wheel"
    },
    {
        "topicality": 0.9501146,
        "score": 0.9501146,
        "mid": "/m/019sc",
        "description": "black"
    },
    ...
    {
        "topicality": 0.58060855,
        "score": 0.58060855,
        "mid": "/m/0h8n8m2",
        "description": "ventilation fan"
    },
    ...

この通りレスポンスには複数のラベルが返され、色や形状など、様々な観点が含まれています。この場合は黒色の卓上クリップ扇風機なのですが、認識結果として最も正しいのは最もscoreの低い「ventilation fan」です。(やはり単発では、コンピュータは人間にはかなわないのです。)

なお、ラベル検出を行う場合は、追加料金を払うことなく「セーフサーチ」チェックも仕掛けることができます。これはアダルト画像などを検出するもので、上記の卓上クリップ扇風機の写真の場合は、以下のような結果になります。

"safeSearchAnnotation": {
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adult": "VERY_UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "spoof": "VERY_UNLIKELY"
}

セーフサーチについては、以下のサイトに仕様が書かれています。 https://cloud.google.com/vision/docs/other-features#safe_search

結果

一応計数してみましょう。

  • 総画像数: 522
  • 検出されたラベルの総数: 4917
  • 検出されたラベルの種類: 661

661種類、4917個ものラベル検出があったわけですが、この結果をどうやって取りまとめたものでしょうか?

一番単純な方法は、「ラベルごとにスコアを加算する」やり方です。これをサイト画像全体について集計すると、どんな画像が多いのかを摑むことができそうです。以下のような結果になりました。

        url        |   label         |      score
-------------------+-----------------+------------------
 www.ra*****.co.jp | product         |     249.13065277
 www.ra*****.co.jp | font            |     178.09255501
 www.ra*****.co.jp | text            |     174.66525343
 www.ra*****.co.jp | line            |     115.06299671
 www.ra*****.co.jp | brand           |     112.54623938
 www.ra*****.co.jp | logo            | 96.9688526700001
 www.ra*****.co.jp | graphics        |      87.28768391
 www.ra*****.co.jp | black           |       82.1643062
 www.ra*****.co.jp | black and white |      79.13861794
 www.ra*****.co.jp | monochrome      |      54.89684521
(10 rows)

       url        | label       |    score
------------------+-------------+-------------
 www.am****.co.jp | product     | 203.6307481
 www.am****.co.jp | font        | 44.40389657
 www.am****.co.jp | text        | 32.15153994
 www.am****.co.jp | brand       | 30.75662519
 www.am****.co.jp | technology  | 22.13816894
 www.am****.co.jp | line        | 17.98218844
 www.am****.co.jp | yellow      | 13.99654375
 www.am****.co.jp | black       | 13.39340801
 www.am****.co.jp | hardware    | 12.35800936
 www.am****.co.jp | sleeve      | 12.21443735
(10 rows)

結果は両サイトでよく似ています。両サイトともリテールサイトですから、「product」や「brand」が上位に来るのは当然です。また、画像の中には商品画像だけでなく、Webサイト自身のフレームやアイコンも多量に含まれている他、商品写真にも文字が埋め込まれているものが多数あります。このため「font」「text」「line」や色名も共通で上位に入っています。

強いて傾向を指摘するとすれば、R社の「logo」と、A社の「technology」です。R社のサイトが企画サイトへのポータルサイトの機能を重視したデザインになっているのに対し、A社のサイトは家電の写真が大写しになっていました。このような大雑把なやり方でも、少しは両サイトの傾向の違いが出ていると言えるのではないでしょうか。とは言え、もう少しなんとかならないでしょうか。

次に思いついた方法は、サイトごと、各ラベルごとに1~100までの相対スコアを計算し、これを相互に比較する方法です。例えばあるサイトで最も合計スコアの高いラベルには100を、最も合計スコアが低いラベルには0を設定します。ラベルごとに突き合わせれば、片方のサイトで扱いの小さい(または無い)ラベルが他方のサイトでは大きく取り上げられている、などの傾向がわかるかもしれません。

この結果はなかなか興味深いものとなりました。以下に抜粋します。

                description                | r_rel_score | a_rel_score | rel_score_diff 
-------------------------------------------+-------------+-------------+----------------
 bottled water                             |             |          81 |            -81
 toy                                       |          10 |          87 |            -77
 electronics accessory                     |          23 |          92 |            -69
...

 clothing                                  |          93 |          94 |             -1
...
 product                                   |         100 |         100 |              0
...

 recipe                                    |          90 |          10 |             80
 hair coloring                             |          91 |             |             91

今度ははっきり以下の傾向が見て取れます。

  • A社のみ: bottled water
  • A社多い: toy, electronics accessory
  • 同じ: clothing, product
  • R社多い: recipe
  • R社のみ: hair coloring

確かにA社のサイトには、ミネラルウォーターとおもちゃが多いのです。また両サイトとも衣料品がかなりのスペースを占めています。更に、この時のR社サイトには食材販売企画の広告が多く、これが「recipe」と認識されたと考えられます。 水とおもちゃ・家電重視のA社と、食品と美容重視のR社という傾向が、かなりはっきりと抽出されました。

セーフサーチの結果

今回は大手リテール業者のトップサイトのため、セーフサーチで問題が指摘された画像はほとんどありませんでした。 しかし、ただひとつ、 「adult = POSSIBLE」となった商品があり、それは「目元エステ家電」でした 。商品紹介画像では女性が仰向けになって分厚いアイマスクのような商品を装着していたので、誤解されてしまったようです。

しかしこの指摘、わからないでもありません。今回評価した画像の中ではこの画像以上に指摘されそうな画像はありませんでしたので、これはこれで正しい評価と言えます。

終わりに

Cloud Vision APIは既成のモデルを使用しますので、ラベルの結果については受け入れるしかありません。 しかし、もう少し使い方のノウハウが蓄積できれば実用になりそうだ、というのが筆者の感想です。 既成AIサービスを効果的に使うには、それなりの経験を積む必要があるのかもしれません。

最後までお読みいただき、ありがとうございました。

AWS / Azure / GCP の動画コンテンツモデレーションサービスの比較

みなさんこんにちは。エンジニアの佐藤です。今回は主要パブリッククラウドのコンテンツモデレーションサービスについて書かせていただきたいと思います。

筆者の気付き

AIサービスのインテグレーション案件を手がけるようになって筆者が気が付いたこととして「現時点で既に、データはいたるところに蓄積されている」という現実があります。今時は日常のビジネス活動のほとんど全てがITインフラ上で行われており、結果として日々莫大なデータが吐き出され、「とりあえず保存」されている。もちろんそれらのほとんどは人知れず溜め込まれているのですが、何かのご縁で目にする機会があると、その量の多さに驚く場合がほとんどです。

それらのデータの中で、最も情報量が多いのは、動画ではないでしょうか。動画は静止画のかたまりです。毎秒数枚から数十枚。数秒間で100枚前後もの静止画情報がひとつのファイルの中に入っています。しかもこの動画、評価のためには再生して視聴するしかありません。 見ている間中、視覚と聴覚が拘束されます。テキストと違ってキーワード検索で要所だけ、というわけにも行きません。

そこで人間以上の速さと正確さで動画を「 代わりに見てくれる 」サービスが期待を集めています。主要パブリッククラウドサービスでも、以下のようなサービスが提供されています。

今回はこれらの「動画コンテンツモデレーション機能」を試し、相互比較してみたいと思います。

コンテンツモデレーションとは

筆者の理解では、コンテンツモデレーションとは、要するに世間通念上まずいコンテンツが公開されてブランドが毀損する事態を回避する活動のことです。今回取り扱うクラウドサービスでは、この活動のうち、まずい動画コンテンツを検出する機能が提供されています。

検出可能なまずいコンテンツとはどういったものでしょうか?それらは各サービスの以下の公式サイトに明記されています。

また同時に、GAとなっているRekognition VideoとCloud Video Intelligenceについては「検出は完全ではない」と免責されています。結局のところ、どの程度使えるかは、事前に試して勘所を掴む必要があるようです。

試してみる

では実際に試してみましょう。以下の2コンテンツを用意しました。

  • gEmgOGUA4_01.mp4
  • test01_06.mp4

詳しくは申し上げられませんが、いずれも60秒間のビデオクリップで、後者はより程度がひどいものとお考えください。

使い方

Rekognition Video / Video Indexer / Cloud Video Intelligenceで使い方のフローはほとんど同じです。

  • 対象となる動画を然るべき場所にアップロードする。
  • アップロード先を指定して解析作業をスタートする。
  • 解析作業の終了を待機して、結果を取得する。

具体的な手順は各サービスの公式サイトをご参照いただきたく、ここでは割愛させていただきます。

Rekognition Videoの結果

Rekognition Videoでは、解析結果は以下のようなJSONで返されます。

{
    "VideoMetadata": {
        "FrameHeight": 720,
        "FrameRate": 29.970029830932617,
        "Codec": "h264",
        "FrameWidth": 1280,
        "Format": "QuickTime / MOV",
        "DurationMillis": 60027
    },
    "ModerationLabels": [
        {
            "ModerationLabel": {
                "Name": "Suggestive",
                "Confidence": 67.57727813720703,
                "ParentName": ""
            },
            "Timestamp": 14381
        },
        {
            "ModerationLabel": {
                "Name": "Female Swimwear Or Underwear",
                "Confidence": 75.81449127197266,
                "ParentName": "Suggestive"
            },
            "Timestamp": 14581
        },
        ...

シグナルの内容と、検出された時点が延々と続いています。1枚のフレームで複数のシグナルが検出されることも多く、Pretty JSONで表示した場合の行数は数千行にもなります。

Video Indexerの結果

Video Indexerは現時点では無料プレビューとなっており、今後仕様が変わる可能性があります。現時点の解析結果のうち、コンテンツモデレーションに関連するのは以下の部分です。

{
    ...
    "breakdowns": [{
        ...
        "insights": {
            "contentModeration": {
                "adultClassifierValue": 0.90809,
                "racyClassifierValue": 0.9975,
                "bannedWordsCount": 0,
                "bannedWordsRatio": 0.0,
                "reviewRecommended": false,
                "isAdult": false
            }
        },
        ...
    }],
    ...
}

こちらはビデオクリップ全体の総合評価で、シグナル検出がいつだったのかという情報は書かれていません。

Cloud Video Intelligenceの結果

{
  "name": "asia-east1....",
  ...
  "response": {
    "@type": "type.googleapis.com/google.cloud.videointelligence.v1.AnnotateVideoResponse",
    "annotationResults": [
      {
        "inputUri": "/a_bucket/gEmgOGUA4_01.mp4",
        "explicitAnnotation": {
          "frames": [
            {
              "timeOffset": "0.746915s",
              "pornographyLikelihood": "UNLIKELY"
            },
            {
              "timeOffset": "1.646874s",
              "pornographyLikelihood": "UNLIKELY"
            },
            ...

こちらはRekognition Videoのように時系列順の評価ですが、シグナルは1種類だけです。また、シグナルの程度はLikelihoodとして5段階にまとめられています。それでもPretty JSONで表示した場合の行数は300行程度になりました。

コンテンツ別に結果を比較してみる

ご覧の通り、 いずれのサービスも結果はかなりの大きさのJSONです。 特にRekognition Videoは複数種類のシグナルそれぞれについてConfidenceが出力されるので、JSONを眺めているだけでは全体像を評価することはできません。そこで以下ではRekognition VideoとCloud Video Intelligenceについては、時系列で出力された結果をグラフにプロットして検討します。

gEmgOGUA4_01.mp4

Rekognition Videoの結果をグラフにプロットすると以下のようになりました。

f:id:masashi-sato-flect:20180504212755p:plain

Cloud Video Intelligenceは以下のようになりました。

f:id:masashi-sato-flect:20180504212840p:plain

比較してみると、t = 18s, 30s, 43sのピークが共通していますが、t = 50sについては意見が分かれているようです。Cloud Video Intelligenceで鋭いピークが報告されている一方、Rekognition Videoでは各種シグナルが入り乱れ、はっきりしません。どちらがよりコンテンツの内容を反映していると言えるのでしょうか?主観的判断ですが、筆者はCloud Video Intelligenceの方が正しい判断を下していると思います。ここでRekognition Videoが指摘したシグナルのいくつかは、コンテンツの内容を誤認しており、残念なところです。

とはいえ全体的には、 どちらもコンテンツ映像の時系列変化を正しく捉えており、評価は信頼できると筆者は感じました。

最後にVideo Indexerですが、以下のような結果となっています。

contentModeration
    .adultClassifierValue: 0.90809
    .racyClassifierValue:  0.9975

つまり2つの評価指標両方で、「ほぼ確実に該当」というわけです。これもまぁ、時系列変化のピーク値を基準に判断したのなら、妥当だと言えます。

test01_06.mp4

今度はより「程度がひどい」方の動画の解析結果です。

Rekognition Videoの結果をグラフにプロットすると以下のようになりました。

f:id:masashi-sato-flect:20180504212908p:plain

Cloud Video Intelligenceは以下のようになりました。

f:id:masashi-sato-flect:20180504212923p:plain

一見してわかりますが、Cloud Video Intelligenceのメーターが振り切れ、最高値に張り付いています。 Rekognition Videoの方はどうでしょうか。先ほどの gEmgOGUA4_01.mp4 とはシグナル構成が大幅に変わり、また60秒のビデオクリップ全体で多数検出されています。この test01_06.mp4 は gEmgOGUA4_01.mp4 より程度がひどい、という実状をそのまま反映する結果です。 Rekognition Videoの結果を詳細に見ていくと、前半30秒かけて徐々に高いConfidenceに進展している緑のシグナルと、t = 20sに大きなピーク、t = 43s, t = 55sに中程度のピークがある黄色のシグナルがあります。これらはコンテンツの内容をかなり具体的に反映しており、Rekognition Videoの高い認識性能を示していると思います。

では、Video Indexerはどうだったのでしょうか。なんとこちらは、エラーになってしまいました。以下のような内容です。

f:id:masashi-sato-flect:20180504213704p:plain

残念ながら、未だPREVIEWなので、その目的に準拠しないということで拒否されてしまったようです。程度がよりひどいということを認めてくれたということではあるのですが、残念です。

まとめると、どうか

結論として、現在GAリリースされている Rekognition Video と Cloud Video Intelligence は、公式サイトに記載されているコンテンツモデレーション機能ついては十分な完成度があると筆者は思います。クロかシロかだけが知りたければ Cloud Video Intelligence を、より詳細な内容が知りたければ Rekognition Video を利用すれば良いのではないでしょうか。

モデレーション以外の機能にも期待

動画を「代わりに見てくれる」サービスのこれらの機能は、モデレーションだけではありません。また、現在も鋭意開発されていることは間違いありませんから、今後多数の機能が追加されていくのではないでしょうか。私もできるだけ多くの機能を常日頃からウォッチし、インテグレーションの機会を探し続けたいと思います。

最後までお読みいただき、ありがとうございました。

Einstein Visionで微妙な判定の境界を探る

はじめに

エンジニアの佐藤です。こんにちは。 前回に引き続いて機械学習の話ですが。今回は Heroku Einstein Add-on について書かせていただきます。

ご存知の通り近頃はAIサービス関連のニュースを聞かない日はないほどです。ですが、この流行りに懐疑的な気持ちをお持ちの方も多いのではないでしょうか。実は筆者も、期待はしつつも楽観は危険と考える一人です。

遊びなら、失敗も愛嬌のうち。しかし失敗が「損失」に直結するビジネスでは、できるだけ失敗を避けたいと思うのが普通でしょう。では AIは本当に、十分簡単な課題なら確実に動作してくれるのでしょうか? 今回はこの点を確かめてみたいと思います。

今回紹介する Heroku Einstein Add-on はRESTインターフェイスで操作でき、機械学習の基本的な知識だけでモデルの実装までできます。詳細は他へゆずりますが、とても簡単に使えます。また、今回のような小規模用途であれば、完全無料で試すことができます。 Einstein Add-onの機能は大きく画像認識(Vision)と文書認識(Language)がありますが、今回は画像認識(Vision)を試してみます。

問題設定

画像認識のこの上なく単純な問題として、「boxかbarか」を考えてみます。これは辺の比率によって長方形を2つに分類し、細長い方をbar、他方をboxと判定させるものです。boxとは

f:id:masashi-sato-flect:20180331142540p:plain

で、barとは

f:id:masashi-sato-flect:20180331142605p:plain

です。比率の境界は1:2と決め、これと等しいか、より細長いものはbar、ほかはbox とします。背景は水色、長方形は黒です。回転なし、歪みなしの理想系でやってみましょう。これ以上ない簡単な分類と思いますが、さてEinstein Visionは 「十分簡単だから完璧に」 動作するでしょうか?

トレーニングデータと検証データ

今日流行りのAIは、大規模な機械学習でこれまで不可能だった新機能を実現するものです。機械学習とは前回ブログで書いた通り「データでアルゴリズムを削り出す作業」ですからデータが必要です。

では今回のデータとは?それは様々な比率の長方形以外にはありません。以下のようなプログラムで自動生成します。

from PIL import Image, ImageDraw

for i in range(200):
    img = Image.new('RGB', (400, 200), '#7FFFFF')

    x1 = 50
    y1 = 50
    x2 = 150 + i
    y2 = 150

    draw = ImageDraw.Draw(img)
    draw.rectangle(
        [(x1, y1), (x2, y2)],
        '#000000'
    )

    label = 'box' if i < 100 else 'bar'
    
    if i % 10 == 0:
        fileName = 'eval{:03d}.png'.format(i)
        self.__save_eval(img, label, fileName)
    else:
        fileName = 'train{:03d}.png'.format(i)
        self.__save_train(img, label, fileName)
# end of for (i)

400px x 200px の下地の上に 50px のマージンを確保し、高さは 100px固定、幅は 100pxから 300pxまで 1px刻みで変化させます。 i = 100 で比率が 1:2になりますので、これより前は「box」、これ以降は「bar」とラベル付けします。 また、 10回に 1回、つまり全 200画像の 20個を、テスト用データとしてトレーニングからはよけておきます。

f:id:masashi-sato-flect:20180331142642p:plain

詳しい手順はリファレンスにゆずりますが、このトレーニング画像集を zip形式でアーカイブし、あとは curlコマンドでちょこちょことやれば画像認識の機械学習モデルができあがります。単純極まるデータのためトレーニングも 5分もかからず終了。非常に簡単です。

トレーニング経過とモデルの確認

「非常に簡単」と書きましたが、 油断してはいけません。 これは遊びではなく、ビジネスなのです。およそコンピュータと名のつくもの、何物も疑いの目で見なければいけません。

最初にモデルのメトリクスを確認します。

curl -X GET -H "Authorization: Bearer $EV_TOKEN" -H "Cache-Control: no-cache" https://api.einstein.ai/v2/vision/models/[MODEL_ID] | jq .
{
  "metricsData": {
    "f1": [ 1,  1 ],
    "labels": [ "bar", "box" ],
    "testAccuracy": 1,
    "trainingLoss": 0.5564,
    "confusionMatrix": [
      [ 7, 0 ],
      [ 0, 8 ]
    ],
    "trainingAccuracy": 0.6963
  },
  (一部省略)
}

Einstein Visionのトレーニングでは、トレーニング用にアップロードされたデータのランダム選抜された10%がトレーニング結果の評価のために使われます。その結果は「testAccuracy: 1」つまり、全問正解だったとあります。他にもいろいろありますが、ひとまずは良さそうです。

次は学習曲線です。

curl -X GET -H "Authorization: Bearer $EV_TOKEN" -H "Cache-Control: no-cache" https://api.einstein.ai/v2/vision/models/[MODE_ID]/lc | jq -r -f lc.jq

lc.jqは Einstein Vision RESTレスポンスの調整に使用したもので、以下のような内容です。 .data[] | "(.epoch), (.metricsData.trainingAccuracy), (.metricsData.testAccuracy)"

epoch, trainingAccuracy, testAccuracy
1,  0.665,  1
2,  0.6978, 0.9333
3,  0.6909, 1
4,  0.7021, 0.9333
5,  0.6812, 1
6,  0.7095, 0.9333
7,  0.7075, 0.9333
8,  0.7021, 0.9333
9,  0.6963, 1
10, 0.7002, 0.9333
11, 0.7061, 0.9333

Epoch 1からいきなりtestAccuracyが 1(最高値)になり、以後ほとんど変化しません。 要するにトレーニングは「超 楽勝だった」ようです。 本当でしょうか?

本番テスト

では、今度はトレーニングに使用していない検証用データで判定させてみましょう。結果は以下のようになりました。

/eval/box/eval000.png  box:0.83328694 bar:0.16671309 
/eval/box/eval010.png  box:0.8120093  bar:0.18799073 
/eval/box/eval020.png  box:0.79243547 bar:0.20756452 
/eval/box/eval030.png  box:0.7771254  bar:0.22287455 
/eval/box/eval040.png  box:0.74343425 bar:0.25656578 
/eval/box/eval050.png  box:0.7180637  bar:0.28193632 
/eval/box/eval060.png  box:0.66442096 bar:0.33557907 
/eval/box/eval070.png  box:0.64682674 bar:0.35317332 
/eval/box/eval080.png  box:0.5968405  bar:0.4031595 
/eval/box/eval090.png  box:0.5487534  bar:0.45124662 

/eval/bar/eval100.png  bar:0.5115235  box:0.48847654 
/eval/bar/eval110.png  bar:0.55833775 box:0.44166228 
/eval/bar/eval120.png  bar:0.6156298  box:0.38437027 
/eval/bar/eval130.png  bar:0.5786646  box:0.42133546 
/eval/bar/eval140.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval150.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval160.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval170.png  bar:0.8948712  box:0.105128884
/eval/bar/eval180.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval190.png  bar:0.8948712  box:0.105128884 

雑ですいませんが、一番上が正方形(前述の画像作成コードで i = 0 で作成された画像)で、以後下へ行くほど長方形の長辺が伸長し、/eval/bar/eval100.png の時にちょうど 短辺:長辺 = 1:2 になります。

判定結果は、確かに完璧です。 しかも boxと barの境界である 短辺:長辺 = 1:2 に近づくほど、 boxラベルと barラベルの probabilityが拮抗し(つまり判定の確からしさが減少し)、我々人間が感じる感触に近くなっていて親近感がありますね。。。 興味深いのは、「文句なく細長く」なった /eval/bar/eval140.png以降は probabilityの変化がないことです。これはいわば「残りの領域は見る必要がない」という感じでしょうか(筆者の主観ですが)。

それでも疑ってみる

しかし、、、疑い深い筆者は考えてしまいます。これは何かの偶然ではないのかと。そこで もう一度、完全に同じデータでトレーニングとテストをやり直して見ます。

結果は以下のようになりました。

2つ目のモデルのメトリクス

{
  "metricsData": {
    "f1": [ 1, 1 ],
    "labels": [ "bar", "box" ],
    "testAccuracy": 1,
    "trainingLoss": 0.5796,
    "confusionMatrix": [
      [ 7, 0 ],
      [ 0, 8 ]
    ],
    "trainingAccuracy": 0.6826
  },
  (一部省略)
}

学習曲線

epoch, trainingAccuracy, testAccuracy
1,  0.6367, 0.8667
2,  0.6934, 1
3,  0.6826, 1
4,  0.6904, 0.9333
5,  0.6753, 0.9333
6,  0.6982, 0.9333
7,  0.6992, 0.9333
8,  0.6904, 0.8667
9,  0.6851, 0.8667
10, 0.6963, 0.9333
11, 0.7148, 0.9333

テスト結果

/eval/box/eval000.png  box:0.7758191  bar:0.22418085 
/eval/box/eval010.png  box:0.7284226  bar:0.27157742 
/eval/box/eval020.png  box:0.6806543  bar:0.31934574 
/eval/box/eval030.png  box:0.6620312  bar:0.33796886 
/eval/box/eval040.png  box:0.6430078  bar:0.35699224 
/eval/box/eval050.png  box:0.61368895 bar:0.38631108 
/eval/box/eval060.png  box:0.57791686 bar:0.4220831 
/eval/box/eval070.png  box:0.5619922  bar:0.43800783 
/eval/box/eval080.png  box:0.5478586  bar:0.4521414 
/eval/box/eval090.png  box:0.5113313  bar:0.48866868 

/eval/bar/eval100.png  bar:0.51804864 box:0.4819514 
/eval/bar/eval110.png  bar:0.5393514  box:0.4606486 
/eval/bar/eval120.png  bar:0.55827296 box:0.44172707 
/eval/bar/eval130.png  bar:0.56668216 box:0.43331784 
/eval/bar/eval140.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval150.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval160.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval170.png  bar:0.8327772  box:0.16722284
/eval/bar/eval180.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval190.png  bar:0.8327772  box:0.16722284 

比べてみると、全体的傾向は同じですが、個々の数字は微妙に違います。

数値をExcelでプロットして見ましょう。

学習曲線については、差は小さいですが、

f:id:masashi-sato-flect:20180331142724p:plain

意外と違っていたのがテスト時の probability値です。

f:id:masashi-sato-flect:20180331142738p:plain

どちらも boxと barの判定は期待通りですが、プロットしてみると 2つのラベルで probabilityが反転している点が異なり、 bar1 / box1の交点は bar2 / box2の交点より x値が大きいところにあります。

なぜこのような違いが生じるのでしょうか?筆者は主に以下の2つの要因があると考えています。

トレーニングのランダムの結果が異なるから

一般的にディープラーニングのトレーニングでは乱数を利用するステップがいくつかあり、乱数の結果によってトレーニングの進行が異なります。もちろん、 トレーニングデータが十分にあればこれらの結果による影響は少なく、トレーニング結果は一定の範囲に収束するはずです。

トレーニング結果の評価のためのデータの選抜結果が異なるから

トレーニングに再しては、アップロードされたデータの10%をトレーニング評価用に使いますが、この選抜方法はランダムであり、指定することはできず、選抜結果を取得することもできません(本稿執筆時点)。したがって、今回180画像アップロードした画像のどれがトレーニング評価用に選択されたかで、 barと boxの probabilityを反転させる最適ポイントが変わります。 したがって、今回の場合 テストデータの 10番と 11番の周辺に、トレーニング評価用のデータが 1つもなかった場合は、テストデータの 10番か 11番のどちらかの判定に失敗していた可能性があると思います。 しかしこういうケースの発生確率は小さく、 たいていは 大丈夫なのです。

結論として、「boxかbarか」という本課題については、 Einstein Visionは十分確実な仕事をしてくれると言えそうです。 ですが、現実の画像判定ソリューションの場合、トレーニングデータもテストデータももっと複雑であり、その内容と評価基準について今回のように考察することはほとんど不可能でしょう。結局のところ、本番で高確率で成功したければ、「実戦に近く、かつ、豊富な経験」しかないという、言われるまでもない自明の真理に戻ってきたような気がします。AIと言えど、マジックはないようです。

最後までお読みいただき、ありがとうございました。

プログラム開発技術者によるTensorflow超基礎:「1 + ? = 2」を機械学習で解かせるには?

はじめに

みなさんこんにちは。エンジニアの佐藤です。今回はTensorflowの話を書かせていただきたいと思います。

みなさんもきっと既にご存知でしょう「Tensorflow」、ディープラーニングフレームワークとして最もポピュラーなもののひとつで、昨今のAIブームの折、その応用技術を身に着けたいと思っている方もきっと多いことでしょう。筆者もその一人です。ポピュラーでありますから、ちょっとググればさまざまな事例や解説が山のように出てきます。このいくつかを読めば、だいたい勘所がわかるだろう。そう筆者は思っていました。 ところが、、、読めども読めども今ひとつよくわからないのでした。いえ、大体はわかるのですよ。筆者は機械学習についての基本的概念は理解しているつもりで、ニューラルネットワークも、業務で取り組んできた経験があります。なのでTensorflowの事例も、概念や考え方については何の疑問もありません。 わからないのは、Tensorflowがどうやって、人間の思考をコードに落としているかです。つまりプログラムがはいスタートとなってから、CPUがどういう順番でライブラリの中を駆け巡って、最終的なゴールにたどり着くのか。その実行フローのイメージが、まるでつかめなかった。しかし、公式サイトをほとんど読了したある日、ようやくわかったような気がしました。 おそらくTensorflowの世界に足を踏み入れた時、私と同じ悩みを抱えるシステム開発系の開発技術者は結構多いのでは?と思い、このブログを書かせていただきました。

ミニマムで考えてみる

私の理解では、機械学習とは「データでアルゴリズムを削り出す」作業です。難しい話は他へ譲るとして、ミニマムな事例を考えてみましょう。

「1 + x = 2。 xに入る数字は?」

言うまでもなく、答えは1です。我々はすぐにわかります。しかし、「データでアルゴリズムを削りだす」ポリシーを貫くとすると、どうでしょうか。 「何かを足すが、それはいくつなのか?」というのが課題で、データとはすなわち「足す相手が1だった場合、足し算の結果は2になる。」という事前情報です。 この事前情報があれば、試行錯誤が可能になります。「10を足すのかな?」「0.5を足すのかな?」というトライアルが計画でき、このトライアルと事前情報を比較することで、「2であるべき足し算の結果が、12になってしまった。10多かった。」「同、1.5になってしまった。0.5少なかった。」という評価があり、評価を繰り返しフィードバックすることで、「答えは1」に近づくことができます。 少々くどいですが、機械学習のミニマムストーリーができました。

では、機械学習フレームワークであるTensorflowで、やってみましょう。 「やってみる」とはつまり、以下のような処理計画の実行のことです。

  1. この課題と事前情報をインプットして、
  2. トライアルと評価結果のフィードバックを繰り返し行うプログラムフローを記述し
  3. 実行して動作の過程と結果を観察すること。

プログラム開発の経験がある皆さんなら、プログラムの構想はすぐに立つでしょう。そう、終了条件を定めてループで回せばいいのですよ。

そしてですが、Tensorflowでこのミニマムストーリーを記述すると、こうなります。

     1   import tensorflow as tf
     2  
     3  x      = tf.constant(1.0)
     4  y_true = tf.constant(2.0)
     5  
     6  w      = tf.get_variable('w', shape=())
     7  y_pred = x + w
     8  
     9  loss      = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)
    10  optimizer = tf.train.GradientDescentOptimizer(0.1)
    11  train     = optimizer.minimize(loss)
    12  
    13  with tf.Session() as sess:
    14      sess.run(tf.global_variables_initializer())
    15      while True:
    16          _, loss_value, val_w = sess.run((train, loss, w))
    17          print('loss:{} w:{}'.format(
    18              str(loss_value),
    19              str(val_w)
    20          ))
    21          if loss_value < 0.01:
    22              print('END')
    23              break

​ 「わかるような、わからないような」と感じられる方も多いのではないでしょうか。私も今でこそ課題設定とコードの対応関係がはっきりわかりますが、初めはさっぱりわかりませんでした。疑問の中心は、「試行錯誤の過程では、事前情報の参照とトライアル結果の評価があり、想定値(この場合は1へ近づいていく数)の修正があるはずだが、唯一のループ処理(15行目〜)の中にはこれらを実行している気配がない」点です。そんなはずはありません。当然仕組まれています。

順番に見ていきましょう。最初の行は

3 x      = tf.constant(1.0)
4 y_true = tf.constant(2.0)

で、これは「事前情報のインプット」に相当する処理なのですが、この行の処理はプログラム処理言語の「代入処理」 ではありません。 実はこの行「x が参照された時、常に数値1.0を供給する約束をするが、その供給処理の実行はこの時点では保留」という意味を持っています(y_trueについても同様)。どうしてこんなに回りくどいことになっているのか。それはその後を見ていくと徐々にわかると思います。

いきなり?なことを言ってしまったかもしれませんが、次も結構敷居の高い内容です。

6 w = tf.get_variable('w', shape=())

get_variableだから、wは変数です。しかし、このget_variableで返されるw、 ただのプログラム言語の変数ではありません。オプティマイザが調整できる変数」なのです。オプティマイザについてはこのあと解説します。

そして次が、Tensorflowの中でもCoreと呼ばれる基本機能のひとつで、Tensorflowを理解する上での肝(と筆者が思う部分)です。

7 y_pred = x + w

この行は、「足してみる」というトライアル内容に相当する部分です。が、 ここで足し算が実行されるわけではなく、y_predに計算結果が代入されるわけでもありません。y_predに代入されるのは、上記の通り固定的に1.0が返されるxと、他から調整されるwとの足し算を「将来実行する約束」です。 「+」演算子Python言語の機能で再定義され、元々の算術演算子とは違うものになっているのです。

次の行は関数実行で、その役割はトライアル結果の数値化を「将来実行する約束」を返すことです。

9 loss = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)

「mean square」ですから、簡単に言えば、その数値化方法は正解とトライアル結果の距離の計測であり、引数もその通りの内容となっています。引数はいずれも「数字を出力する約束」のオブジェクトで、なので結果は必然的に、変数lossに代入されるのは、またしても「評価を将来実行する約束」です。

次の行はアルゴリズムの選択です。

10 optimizer = tf.train.GradientDescentOptimizer(0.1)

まぁ、よくあるまともなアルゴリズムですということで、これ以上は追求しないことにしたいと思います。

これまで「約束、約束」と連鎖してきましたが、次でようやく終着点です。 次の行は直前行で選択したアルゴリズム(GradientDescentOptimizer)で、「最適化」を「実行する約束を」するメソッド呼び出しです。

11 train = optimizer.minimize(loss)

ところで、何を最適化するのでしょうか?それは試しに足してみる数値「w」以外にありません。しかし、オブジェクトoptimizerは、変数wを参照できるのでしょうか?普通にプログラムを見ているとできないような気がしますが、それが可能なのです。 これまでつないできた「約束の連鎖」が、optimizerとxをつないでいるのです。 この部分がTensorflowのサンプルを読むときの一つの重要留意点と筆者は思っています。なんとなく代入が繰り返され、その終端変数だけポンと後処理に回されることが多いのですが、実はこの 終端変数には、これまで代入されてきた変数がすべて、背後で紐づけられているのです。

では、約束の連鎖を駆動するのはいつなのか?Session.runがそれにあたります。このメソッドの引数に列挙された「約束」が、紐解かれ、実行されるのです。 Session.runは2箇所で実行されています。

14       sess.run(tf.global_variables_initializer())
...
16          _, loss_value, val_w = sess.run((train, loss, w))

14行目はTensorflowで実際の計算処理をスタートする際のおまじないと思ってスキップしてください。 本丸は16行目です。ここで引数に渡されるのは train, loss, w で、いずれも何らかの計算を実行する「約束」です。 Session.runでは、その約束を実際に実行し、最後の計算結果を戻り値で出力します。 この場合

  • trainはopitimizer.minimizeを実行し、loss値を小さくするためにloss値に関する唯一の変数wを変更します。(戻り値は捨てられます)
  • lossは、この変更されたwでloss値を計算した結果を出力(loss_value)し、
  • wは、この変更されたw自身を、戻り値として出力(val_w)します。

そしてこの trainを繰り返し実行することで、変数wが徐々に調整され、つまり機械学習アルゴリズムが良くなっていく というのが、いわゆる「機械学習モデルのトレーニング」そのものなのです。

このコードを実行すると、結果は以下のようになります。(実際の数値はwの初期値によって異なり、この場合初期値はランダムですので、場合によって違います。)

loss:4.47416 w:-0.692177
loss:2.86346 w:-0.353741
loss:1.83262 w:-0.082993
loss:1.17287 w:0.133606
loss:0.750639 w:0.306885
loss:0.480409 w:0.445508
loss:0.307462 w:0.556406
loss:0.196776 w:0.645125
loss:0.125936 w:0.7161
loss:0.0805993 w:0.77288
loss:0.0515836 w:0.818304
loss:0.0330135 w:0.854643
loss:0.0211286 w:0.883715
loss:0.0135223 w:0.906972
loss:0.00865427 w:0.925577
END

wは、我々人間が一瞬で見抜いた答え「1」に徐々に近づき、十分(loss < 0.01)近づいたところでプログラム終了です。

これまでの過程で、Tensorflowが計算する約束を「一旦実行保留のまま配置」し、のちにそれらの約束を実行することで目的の計算処理を実行していることがおわかりいただけたと思います。実際には機械学習の課題はもっともっと複雑なのが普通ですし、実装も様々に高抽象度化されていますから、ここまでコードフローを細かく気にする必要は無いことが多いのが現実でしょう。しかしそれらの場合も、地下で動いている仕掛けの基本は同じはずです。この基本を押さえておくことで、目に見えているコードの実行順序をより的確に判断できるようになるのではないかと思います。

ところで、なぜこんな回りくどい実装になっているのでしょうか。筆者の考えでは、それは「実装を変更することなく分散処理環境へ移行するため」だと思います。機械学習モデルのトレーニングは、今日ではGPU上で実行することが一般的です。しかし小規模な試行錯誤はCPU上で実行されるでしょうから、これまで見てきたように計算の約束をTensorflowのオブジェクトの相互参照という形で一旦表現し、Session.runとなったところで適切な実行環境へそれぞれ「展開」できるようになったのでしょう。

最後まで読んでいただき、ありがとうございました。

Jenkins × AWS CodeBuild × GitHubで複数コンテナを利用したビルドを試してみた

こんにちは、Cariot事業部の遠藤です。
Salesforceブログに続き、クラウドblogでも初投稿になります。

f:id:flect-endo:20170630235508p:plain

今回は、Jenkins × AWS CodeBuild × GitHubを組み合わせて、CodeBuild上で複数コンテナを使ったビルドを実行する仕組みを試してみたので、紹介します。
RDBMSやRedisなどの外部リソースを利用したテストを、CodeBuild上で走らせたい」「クリーンな実行環境でCI/CDを回したい」という方のヒントになれば幸いです。

それではどうぞ↓↓

続きを読む