エッジデバイスとクラウドのエンドポイントの出力結果を合わせるのに苦労した話

本ブログへいらっしゃったみなさま、初めまして。aptpod Advent Calendar 2019 5日目担当のsetoです。入社4ヶ月(2019/12/05現在)で、機械学習系の案件や自社プロダクト付加価値向上のための技術調査に従事してます。社内メンバーの技術領域が多彩で刺激を受けながらお仕事しています。

本記事では、エッジデバイスに採用したJetson Nanoの出力結果とクラウド推論に採用したAmazon SageMakerのエンドポイントの出力結果が同じにならない問題に遭遇し、解決するのに苦労した話です。

この記事を要約しますと、"読み込んだ画像のピクセル値が環境が違うと同じにならない"という検証すればすぐわかる問題に自分の思い込みのせいでなかなかたどり着けなかったという経験談を書いております。

取り組みの背景

弊社が開発を進めているシステムは、ディープラーニングの推論をクラウドとエッジデバイスの別々の環境でおこなっています。推論結果の一貫性を保つため同じ入力で同じ結果が出力できるように調査と対応を行うのが取り組みの背景です。

具体的には、クラウドの推論にAmazon SageMakerでApache MXNetを使用した機能を利用し、エッジ側はAmazon SageMaker NeoとNeo-AI-DLRを利用しました。この取り組みのゴールはこれらの異なるフレームワークから出力される推論結果を一致させることです。

使用しているフレームワーク

Amazon SageMaker

クラウド側の推論はAmazon SageMaker(以下、SageMaker)を活用しています。本テックブログの2日目のキシダさんも同じフレームワークを活用しており、こちらの記事もご覧いただけると理解が深まると思います。本ブログでは、SageMaker上でApache MXNetを動作させる枠組みを用いたものを利用しています。Apache MXNet(以下、mxnet)は,ディープラーニングのフレームワークの一つです。

Amazon SageMaker Neo

エッジ側の推論はAmazon SageMaker Neo(以下、SageMaker Neo)とSagMaker NeoでコンパイルしたモデルをJetson Nanoで動作させるランタイム Neo-AI-DLR(以下、dlr)を活用しています。本ブログでエッジ側で動作させる場合は、これらのフレームワークを活用して動作させていることになります。

調査の詳細

今回、調査した推論機能のパイプラインは下図のとおりです、特殊なことはなにもしていません。

f:id:aptpod_tech-writer:20191203154441p:plain
図1. 推論処理パイプライン

調査をおこなう際にSageMakerのエンドポイントを立て続けて検証することはコストが高かったため、同一の結果がでることを確認したGPU付きのローカルマシンを代わりに使用しました。

調査の流れは、

  1. 推論処理の一致確認(計算が複雑で一致が難しいと判断したためはじめに対応)
  2. 前処理の結果の一致確認(1.よりは簡易であると考え2番目に対応)
  3. 画像の読み込みの一致確認(予想外の対応)

という項目を行いました.3.は予想しておらず、2.が問題だという思い込みが苦労した大きい要因でもあります。調査に思い込みは厳禁であることを溶かした時間で学びました😇

推論処理の一致確認の詳細

この調査のゴールは、入力データを同じにすればエンドポイントで利用しているmxnetとエッジ側のdlrの推論結果の一致を確認することです。手続きとして以下の2段階の分けて確認を行いました。

  1. 全て1にした配列を入力して出力結果が一致するかを確認(一様な入力で一致するのか)
  2. 0.0~1.0までのランダムな数字で出力結果が一致するかを確認(多様な入力で一致するのか)

この検証の際は、並列計算の加算が多く行われているため全ての数字の一致は望めないため、1e-5の桁まで数値が同じであれば一致しているとしました。これらの調査の結果、クラウド側のSageMakerの推論結果とエッジ側のdlrを用いた推論結果は一致することがわかりました。つまり、推論に入力する前処理済みのデータが一致していないことがわかりました。

前処理の結果の一致確認の詳細

この調査のゴールは、同じ実装を使った前処理の出力結果の一致を確認することです。前処理は、どのデバイスでもだいたいpipで入れることができる、pillow(6.1.0)ライブラリを使って実装しました。図1.から前処理はリサイズとクロップをおこなう処理となります。前処理は、画像でみると図2.のように変形処理を施していきます。

f:id:aptpod_tech-writer:20191203161847p:plain
図2.前処理のイメージ図

確認方法は、サンプル画像を入力し結果が一致するかを調査しました。結果は出力の配列が不一致となりました。このため、個々の処理を調べる必要があると判断し、はじめに出力に近いリサイズの調査を行いました。この結果、同じバージョン、同じ補完方法でも結果が一致しませんでした。このため、他のライブラリを使ってみたり、githubのソースコード見にいったり色々調査をしましたが、これだと強く主張できる原因を見いだせていませんでした。ここで大きく時間をかけてしまいました。このときに自分の考えにバイアスがあり入力に着目するまで考えが及ばなかったことが本件の時間浪費の原因でした。バイアスよくない😰

画像読み込みの一致確認の詳細

何がきっかけか覚えていませんが、そもそも読み込んだ画像データに違いがあれば、前処理の結果が一致するはずがないと思い至り、入力画像を読み込んだデータで比較しました。その結果、読み込んだ画像のピクセル値が一部一致しないことがわかりました。ついに原因を特定することができました!以下、公開して良いデータを使って一致しない結果を再現しました。

※調査当時はGPUサーバーを使いましたが、MacBook Proも画像読み込みの結果がGPUサーバーと一致したので、再現はMacBook Proを使用しています。

以下、エッジ側で画像を保存するためのスクリプトです。非常に簡易なスクリプトでpillowで画像を読み込んだ後にndarrayに変換しnpyで保存しています。このnpyファイルをMacBook Proに持ってきて比較を行いました。

#!/usr/bin/env python
# coding: utf-8

import argparse
import numpy as np
from PIL import Image


if __name__ == '__main__':

    parser = argparse.ArgumentParser()

    parser.add_argument('-f', '--file-name', required=True, type=str)

    args = parser.parse_args()

    img_dir = './sample-images/IMG-0269.JPG'
    img = Image.open(img_dir)
    img_arr = np.array(img)
    store_name = args.file_name
    np.save(store_name, img_arr)

以下、比較したnotebookです。7セル目のFalseが比較結果になります。 このの経験からpython上で扱うライブラリのバージョンが同じでも、結果が一致する保証はないという教訓を得ることができました。

notebook

問題はどのように対処したのか

エッジ側はライブラリを色々インストール、アンインストールしても変えることができなかったので、クラウド側の画像読み取りを変えました。pillowではなくmxnetでの読み込みに置き換えたら画像のデータが一致したので、mxnetでの読み込みを採用しました。notebookの8セル目と9セル目の結果が再現となっています。

調査しきれなかったこと,わからなかったこと

おそらくpillowとmxnetが画像読み込み時に利用しているlibjpegのバージョンなどが異なっていることが原因であると判断しました。しかし、エッジ側のlibjpegのバージョンを変更しても解決できませんでした。このため、きちんと原因を追い切れているわけではなく、再現しない可能性もあります。

まとめ

異なるフレームワーク、ライブラリを使って推論結果をあわせるのは予想以上に苦労しました。ディープラーニングのフレームワークをまたいで処理結果を近似することは経験していてそこが一番苦労すると思っていたことが落とし穴でした。また、データ読み込みに一貫性がないわけがないとう確信がより問題の特定に時間をかけてしました。これを教訓に調査する、比較するといったときはこうあるだろうなどのバイアスをなるべく持たないようにしたいと思います。

最後に,本ブログをここまで読んでいただきありがとうございます。これからも苦労したことや検証してみた内容などを引き続きブログにアップロードしていきたいと思います!