20191221のTensorFlowに関する記事は5件です。

HerokuとTensorFlowを利用した、顔画像の推論

はじめに

  • Django で作成した TensorFlow のアプリケーションを、Heroku へ移植します。
  • HerokuUbuntu 18.04 のコンテナを利用することで、OpenCV を利用出来るようになります。
  • ソース一式は ここ です。
  • アプリケーションは https://abe-or-ishihara.herokuapp.com/ です。

Herokuのセットアップ

  • アカウントは、事前に作成しています。
  • Mac でのセットアップしています。
  • Heroku でコンテナを利用し、かつ yml ファイルによる Postgres のデプロイには、beta のプラグインが必要です。
$ brew install heroku/brew/heroku

$ heroku login

$ heroku update beta

$ heroku plugins:install @heroku-cli/plugin-manifest
  • Procfile には、gunicornDjango のプロジェクトを起動するように設定します。
  • Heroku でコンテナを使う場合は、不要な設定です。ただし、ローカル環境でデバッグする場合は必要です。
  • なので、デプロイには、必要ありません。
web: gunicorn project.wsgi --log-file -
  • Heroku で利用する Python のバージョンを指定します。特にこだわりがないので、最新にしています。
runtime.txt
python-3.7.5
  • ライブラリをインストールします。Heroku へデプロイするために追加したのは、django-herokugunicorn です。
  • django-heroku は、HerokuDjango を利用するための色々な設定等をしてくれるものです。
requirements.txt
boto3
django
django-heroku
gunicorn
numpy==1.16.5
opencv-python
pillow
tensorflow==1.14.0
  • .envHeroku をローカルでデバッグする時の環境変数を設定します。
AWS_ACCESS_KEY_ID='your-aws-access-key'
AWS_SECRET_ACCESS_KEY='your-aws-secret-access-key'
BUCKET=abe-or-ishihara
  • 設定ファイルの末尾に、Heroku 関連の設定をします。
project/settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
import django_heroku
django_heroku.settings(locals())

Heroku ローカルサーバーの起動

  • .env の環境変数を読み込みローカルサーバを起動します。
  • http://0.0.0.0:5000/http://0.0.0.0:5000/admin/ へのアクセスを確認します。
  • 推論API や S3 へデータが保存されることを確認しました。
$ heroku local web
[OKAY] Loaded ENV .env File as KEY=VALUE Format
22:07:55 web.1   |  [2019-12-21 22:07:55 +0900] [59955] [INFO] Starting gunicorn 20.0.4
22:07:55 web.1   |  [2019-12-21 22:07:55 +0900] [59955] [INFO] Listening at: http://0.0.0.0:5000 (59955)
22:07:55 web.1   |  [2019-12-21 22:07:55 +0900] [59955] [INFO] Using worker: sync
22:07:55 web.1   |  [2019-12-21 22:07:55 +0900] [59973] [INFO] Booting worker with pid: 59973

デバッグモード、許可ホストの修正

  • 設定ファイルを修正します。
project/settings.py
DEBUG = False

ALLOWED_HOSTS = ['abe-or-ishihara.herokuapp.com']

Heroku へデプロイ

  • Heroku のコンテナの設定をします。
  • アドオンで PostgreSQL を設定しています。コンテナではない、通常の Heroku なら設定無しでOKなのですが、コンテナの場合は明示が必要です。
  • AWS S3 の設定をします。これは、Heroku のコンパネに環境変数が設定され、コンテナの起動時に利用される感じです。
  • languagespython を指定します。バージョンは、runtime.txt に記載のバージョンが利用されます。
  • pipopencv-python には、aptlibopencv-dev が必要なので、インストールします。
heroku.yml
setup:
  addons:
    - plan: 'heroku-postgresql:hobby-dev'
      as: DATABASE
  config:
    AWS_ACCESS_KEY_ID: 'your-aws-access-key'
    AWS_SECRET_ACCESS_KEY: 'your-aws-secret-access-key'
    BUCKET: abe-or-ishihara
build:
  languages:
    - python
  packages:
    - libopencv-dev
run:
  web: gunicorn project.wsgi --log-file -
  • Heroku に環境を作ります。上記の heroku.yml 等に基づき環境が作成されます。
$ heroku create abe-or-ishihara --manifest
Reading heroku.yml manifest... done
Creating ⬢ abe-or-ishihara... done, stack is container
Adding heroku-postgresql:hobby-dev... done
Setting config vars... done
https://abe-or-ishihara.herokuapp.com/ | https://git.heroku.com/abe-or-ishihara.git
  • デプロイするフォルダを git init します。
  • フォルダを Heroku のリポジトリと連携します。
$ git init
Initialized empty Git repository in /Users/maeda_mikio/abe_or_ishihara/django/.git/

$ heroku git:remote -a abe-or-ishihara
set git remote heroku to https://git.heroku.com/abe-or-ishihara.git
  • デプロイ対象を git add . します。
$ git add .

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   app/__init__.py
    new file:   app/admin.py
    new file:   app/apps.py
    new file:   app/data/checkpoint
    new file:   app/data/model.data-00000-of-00001
    new file:   app/data/model.index
    new file:   app/data/model.meta
    new file:   app/font.ttf
    new file:   app/haarcascade_frontalface_default.xml
    new file:   app/migrations/0001_initial.py
    new file:   app/migrations/__init__.py
    new file:   app/models.py
    new file:   app/tests.py
    new file:   app/views.py
    new file:   heroku.yml
    new file:   manage.py
    new file:   project/__init__.py
    new file:   project/asgi.py
    new file:   project/settings.py
    new file:   project/urls.py
    new file:   project/wsgi.py
    new file:   requirements.txt
    new file:   runtime.txt
    new file:   static/abe_or_ishihara.png
    new file:   static/index.css
    new file:   static/index.js
    new file:   static/spinner.css
    new file:   static/title.png
    new file:   templates/index.html
  • コミットして、デプロイします。
$ git commit -am "create tensorflow face predict app"

$ git push heroku master
  • Heroku 上でデータベースの確認、マイグレーションと管理ユーザーの作成をします。
$ heroku pg
=== DATABASE_URL
Plan:                  Hobby-dev
Status:                Available
Connections:           0/20
PG Version:            11.6
Created:               2019-12-21 13:39 UTC
Data Size:             7.7 MB
Tables:                0
Rows:                  0/10000 (In compliance)
Fork/Follow:           Unsupported
Rollback:              Unsupported
Continuous Protection: Off
Add-on:                postgresql-tapered-15660
$ heroku run python manage.py migrate
$ heroku run python manage.py createsuperuser
Running python manage.py createsuperuser on ⬢ abe-or-ishihara... up, run.2569 (Free)
ユーザー名 (leave blank to use 'u49624'):
メールアドレス:
Password:
Password (again):
Superuser created successfully.

Heroku へアクセス

image.png

image.png

おわりに

  • HerokuDjango TensorFlowOpenCV を利用したアプリケーションのデプロイをしました。
  • Heroku は、コンテナで Ubuntu を使えるので、大体の事ができます。今回は、OpenCV を使うため、コンテナを利用しました。
  • CPU は、AVX (Advanced Vector Extensions) に対応しているので、TensorFlow もOKです。今時、AVX対応していない方が、珍しいですけどね。
  • これで、一連のアプリケーション作成が終了しました。次回は、まとめをしたく思っております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DjangoとTensorFlowを利用した、顔画像の推論

はじめに

  • DjangoTensorFlow を使い、顔画像の推論を実施します。
  • 最終的に Heroku へデプロイする事を考慮しました。Django なら、データベースや CSRF 対策など、機能が充実しているからですね。
  • ソース一式は ここ です。

概要

ライブラリ

  • 以下を pip でインストールします。
requirements.txt
boto3
django
numpy==1.16.5
opencv-python
pillow
tensorflow==1.14.0

Django

セットアップ

  • まず、プロジェクトとアプリの雛形を作成します。
$ django-admin startproject project .
$ python manage.py startapp app
  • 上記で作成した app を追加する。
  • テンプレートのフォルダを追加する。
  • 日本語への修正をする。
  • タイムゾーンの修正をする。
  • スタティックファイルのフォルダを追加する。
project/settings.py
INSTALLED_APPS = [
    'app.apps.AppConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,


LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
  • manage.py が存在するフォルダと同じレベルで、上記で設定したスタティックファイルとテンプレートのフォルダを作成する。
$ mkdir static templates
  • デバッグ用のデータベースを作成します。
  • デフォルトでは、SQLite が利用され、db.sqlite3 が作成されます。
$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK
  • 管理者ユーザーを作成する。
$ python manage.py createsuperuser
ユーザー名 (leave blank to use 'maeda_mikio'):
メールアドレス:
Password:
Password (again):
Superuser created successfully.
  • 管理画面を起動します。
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
December 21, 2019 - 14:09:05
Django version 3.0.1, using settings 'project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
  • ブラウザで http://127.0.0.1:8000/ にアクセスし、設定したユーザー名とパスワードを入力します。

image.png

  • ブラウザで http://127.0.0.1:8000/admin にアクセスします。

image.png

image.png

トップ画面を作成

  • app/views.py にトップ画面のテンプレートを設定します。
app/views.py
def index(request):
    return render(request, 'index.html')
  • 上記の index.htmltemplates/index.html に作成します。
  • テンプレートは、Bootstrap を利用しました。
  • 画像をドラッグ&ドロップ出来る様にしています。static/index.js で処理をしています。
  • また、推論の処理中は、SpinKit でアニメ処理をしています。static/spinner.css で処理をしています。
templates/index.html
  <head>
省略
    <!-- Spinner CSS -->
    <link rel="stylesheet" href="/static/spinner.css">
省略
  </head>

  <body>
省略
    <div class="starter-template container">
      <img src="/static/title.png" class="img-fluid"><br />
      <img id="img" src="/static/abe_or_ishihara.png" class="img-fluid" height="400">
      <div class="spinner" style="display: none;">
        <div class="rect1"></div>
        <div class="rect2"></div>
        <div class="rect3"></div>
        <div class="rect4"></div>
        <div class="rect5"></div>
      </div>
    </div>
    <input type="file" id="file" name="file" accept="image/*" style="display: none;" required>
   {% csrf_token %}

省略
    <script src="/static/index.js"></script>
  </body>

画像の送信

  • Django の CSRF 対策を利用しています。
  • index.html 内の {% csrf_token %} からトークンを取得します。
  • こちらは、Django公式ドキュメントをそのまま利用しています。
static/index.js
// https://docs.djangoproject.com/en/3.0/ref/csrf/
function getCookie(name) {
  var cookieValue = null;
  if (document.cookie && document.cookie !== '') {
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
      var cookie = jQuery.trim(cookies[i]);
      // Does this cookie string begin with the name we want?
      if (cookie.substring(0, name.length + 1) === (name + '=')) {
        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
        break;
      }
    }
  }
  return cookieValue;
}

var csrftoken = getCookie('csrftoken');

function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }
});
  • 画像は、クリックしてファイルを選択する、もしくはドラッグ&ドロップで選択することにしました。
  • スマホなら、タップが使えるはずです。
static/index.js
// クリックしてファイルを選択
$('#img').on('click', function() {
  $('#file').click();
});

$('#file').change(function() {
  var files = this.files;
  if (checkImg(files)) {
    file = files[0];
    readImg(file);
    predict(file);
  }
});


// ファイルをドラッグ&ドロップ
var img;

img = document.getElementById('img');
img.addEventListener('dragenter', dragenter, false);
img.addEventListener('dragover', dragover, false);
img.addEventListener('drop', drop, false);

function dragenter(e) {
  e.stopPropagation();
  e.preventDefault();
}

function dragover(e) {
  e.stopPropagation();
  e.preventDefault();
}

function drop(e) {
  e.stopPropagation();
  e.preventDefault();

  var dt = e.dataTransfer;
  var files = dt.files;
  if (checkImg(files)) {
    file = files[0];
    readImg(file);
    predict(file);
  }
}
  • 画像は、同時に複数選択出来るため、1ファイルに限定する様にしています。
  • また、jpeg png のみとしました。
  • サイズも 10MB 以上は処理をしないことにします。
static/index.js
// 1ファイル以上、jpeg、png、10MB以上の場合は処理をしない
function checkImg(files) {
  if (files.length != 1 ) {
    return false;
  }
  var file = files[0];
  console.log(file.name, file.size, file.type);
  if (file.type != 'image/jpeg' && file.type != 'image/png') {
    return false;
  }
  if (file.size > 10000000) {
    return false;
  }
  return true;
}
  • ファイルが条件が問題なければ、ファイルを読み込みます。
static/index.js
// ファイルの読み込み
function readImg(file) {
  var reader = new FileReader();
  reader.readAsDataURL(file);
  reader.onload = function() {
    $('#img').attr('src', reader.result);
  }
}
  • Django で作成する /api/ パスに画像を送信します。
  • 結果を受信するまでは、SpinKit でアニメ処理をします。
static/index.js
// 推論API
function predict(file) {

  $('#img').css('display', 'none');
  $('.spinner').css('display', '');

  var formData = new FormData();

  formData.append('file', file);

  $.ajax({
    type: 'POST',
    url: '/api/',
    data: formData,
    processData: false,
    contentType: false,
    success: function(response) {
      console.log(response);
      $('#img').attr('src', response);
      $('.spinner').css('display', 'none');
      $('#img').css('display', '');
    },
    error: function(response) {
      console.log(response);
      $('#img').attr('src', '/static/abe_or_ishihara.png');
      $('.spinner').css('display', 'none');
      $('#img').css('display', '');
    }
  });
}

推論API

  • 処理中のログをデータベースに保存するためのモデルを生成します。
app/views.py
def api(request):

    log = Log()
  • 画像は、POST で送信されているかチェックします。
  • また、事前に DjangoCSRF チェックをしている形になります。
app/views.py
    if request.method != 'POST':
        log.status = 'post error'
        log.save()
        raise Http404
  • 画像を読み込みます。ファイル名、ファイルサイズも取得します。
app/views.py
    try:
        formdata = request.FILES['file']
        filename = formdata.name
        filesize = formdata.size
    except Exception as err:
        log.message = err
        log.status = 'formdata error'
        log.save()
        return server_error(request)
    log.filename = filename
    log.filesize = filesize
    log.save()
  • ファイルサイズは、フロント側でチェックしていますが、もう一度チェックします。
app/views.py
    if filesize > 10000000:
        log.status = 'filesize error'
        log.save()
        return server_error(request)
  • ファイルデータを取得し、jpeg png のチェックをします。
app/views.py
    try:
        filedata = formdata.open().read()
    except Exception as err:
        log.message = err
        log.status = 'filedata error'
        log.save()
        return server_error(request)

    ext = imghdr.what(None, h=filedata)
    if ext not in ['jpeg', 'png']:
        log.message = ext
        log.status = 'filetype error'
        log.save()
        return server_error(request)
  • 後で Heroku で公開をする予定です。
  • Heroku いわゆるオブジェクトストレージがないため、AWS S3 へ保存することにしました。
app/views.py
    try:
        s3_key = save_image(filedata, filename)
    except Exception as err:
        log.message = err
        log.status = 's3 error'
        log.save()
        return server_error(request)
    log.s3_key = s3_key
    log.save()
  • S3 には、日付とファイル名で保存します。
  • アクセスキー、バケット等は、環境変数から読みだす様にします。
app/views.py
def save_image(filedata, filename):

    now = (datetime.datetime.utcnow() + datetime.timedelta(hours=9)).strftime('%Y-%m-%dT%H:%M:%S+09:00')
    key = '{}_{}'.format(now, filename)
    resource = boto3.resource('s3', aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'], aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'])
    resource.Object(os.environ['BUCKET'], key).put(Body=filedata)

    return key
  • 画像データを OpenCV で扱える様に NumPy で変換します。
  • その後、HAAR Cascade で顔認識をします。
  • また、app/haarcascade_frontalface_default.xml に保存しています。
app/views.py
    image = np.fromstring(filedata, np.uint8)
    image = cv2.imdecode(image, 1)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    face_cascade = cv2.CascadeClassifier(os.path.join(os.path.dirname(__file__), 'haarcascade_frontalface_default.xml'))
    faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=5)

    if type(faces) != np.ndarray:
        log.status = 'faces error'
        log.save()
        return server_error(request)
  • 顔認識出来た場合は、TensorFlow で推論出来る様にデータをリサイズ、グレースケール等に変換します。
  • 複数の顔に対応しています。
app/views.py
    face_list = []
    for (x, y, w, h) in faces:
        face = image[y:y+h, x:x+w]
        face = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
        face = cv2.resize(face, (IMG_ROWS, IMG_COLS))
        face = np.array(face, dtype=np.float32) / 255.0
        face = np.ravel(face)
        face_list.append(face)
  • TensorFlow で学習したモデルで、推論を実施します。
  • 学習したモデルは、app/data/ に保存しています。
app/views.py
    try:
        percent_list = predict(face_list, dtype='int')
    except Exception as err:
        log.message = err
        log.status = 'predict error'
        log.save()
        return server_error(request)
  • 顔の推論結果に基づいて、元の画像に修正を加えます。
  • 今回、10人の顔画像の分類があって、0番目と1番目は特別に、青色、赤色で識別出来る様にしました。
  • 顔は、cv2.rectangle で四角で選択され、下側に write_text で推論結果を表示します。
app/views.py
    predict_list = []
    for (x, y, w, h), percent in zip(faces, percent_list):
        max_index = np.argmax(percent)
        max_value = np.amax(percent)
        if max_index == 0:
            color = (177, 107, 1)
        elif max_index == 1:
            color = (15, 30, 236)
        else:
            color = (0, 0, 0)
        text = '{} {}%'.format(CLASSES[max_index], max_value)
        image = write_text(image, text, (x, y+h+10), color, int(h/10))
        cv2.rectangle(image, (x, y), (x+w, y+h), color, thickness=2)
        predict_list.append(text)
    log.message = ','.join(predict_list)
  • 推論結果を画像に描きたいのですが、OpenCV では、日本語が利用できません。
  • そこで、Pillow を利用します。
  • 最初に、OpenCV から Pillow の形式へ変換します。
  • フォントサイズも、顔のサイズに合わせて、適当に修正します。
  • フォントは、コーポレート・ロゴ のフリーフォントを利用しました。
  • フォントは app/font.ttf に保存しています。
app/views.py
def write_text(image, text, xy, color, size):

    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image = Image.fromarray(image)

    fill = (color[2], color[1], color[0])
    size = size if size > 16 else 16
    font = ImageFont.truetype(os.path.join(os.path.dirname(__file__), 'font.ttf'), size)
    draw = ImageDraw.Draw(image)
    draw.text(xy, text, font=font, fill=fill)

    image = np.array(image, dtype=np.uint8)
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

    return image
  • 最後に、画像データを BASE64 に変換し、フロントへ送信します。
app/views.py
    image = cv2.imencode('.jpeg', image)[1].tostring()
    response = 'data:image/jpeg;base64,' + base64.b64encode(image).decode('utf-8')

    log.status = 'success'
    log.save()

    return HttpResponse(response, status=200)

URLのマッピング

  • トップページの / と 推論APIの /api/ を追加しました。
project/urls.py
from app import views

urlpatterns = [
    path('', views.index, name='index'),
    path('api/', views.api, name='api'),
    path('admin/', admin.site.urls),
]

ログ保存のモデルを作成

  • 今回は、ログの保存のためのモデルを作成します。
  • 推論APIで log = Log()log.save() などで記載していた箇所です。
  • ログの作成日時、更新日時、受信したファイル名、ファイルサイズ、S3のファイルキー、ステータスと状況に応じてメッセージを追加出来るようにしました。
app/models.py
class Log(models.Model):

    create = models.DateTimeField(auto_now_add=True)
    update = models.DateTimeField(auto_now=True)

    filename = models.CharField(max_length=100, null=True, blank=True)
    filesize = models.IntegerField(null=True, blank=True)
    s3_key = models.CharField(max_length=100, null=True, blank=True)

    message = models.TextField(null=True, blank=True)
    status = models.CharField(max_length=100, null=True, blank=True)
  • ログを管理画面から確認出来るようにします。
  • カラム、カラムのフィルター、メッセージに関しては検索出来るようにしました。
app/admin.py
class LogAdmin(admin.ModelAdmin):

    list_display = ('id', 'create', 'status', 'filename', 'filesize', 's3_key', 'message')
    list_filter = ['create', 'status']
    search_fields = ['message']


admin.site.register(Log, LogAdmin)

ログのモデルのマイグレーション

  • 作成したログのモデルをデータベースへ反映します。
$ python manage.py makemigrations app
Migrations for 'app':
  app/migrations/0001_initial.py
    - Create model Log

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
  Applying app.0001_initial... OK

管理サイトでログを確認

  • http://127.0.0.1:8000//admin/app/log/ 下記のような画面が準備できました。

image.png

S3、IAMの設定

IAM

  • Django の推論API の def save_image(filedata, filename): でファイルを保存しています。
  • AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY を取得します。
  • IAM のポリシーは、データの保存(PutObject) のみ許可しました。
  • 大量のバケットは、abe-or-ishihara にしました。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::abe-or-ishihara/*"
            ]
        }
    ]
}

S3

  • バケットは、abe-or-ishihara にしました。
  • リージョンは、米国東部(バージニア北部)にしました。
  • Herokuus リージョンにデプロイする予定です。us は、バージニアなので、距離的に近い方が良いでしょう。

起動シェル

  • AWSIAM S3 の情報を環境変数に設定した上で、Django の開発サーバーを起動します。
runserver.sh
#!/bin/bash

export AWS_ACCESS_KEY_ID='your-aws-access-key'
export AWS_SECRET_ACCESS_KEY='your-aws-secret-access-key'
export BUCKET=abe-or-ishihara

python manage.py runserver 0.0.0.0:5000

推論のテスト

  • 画像をドラッグ&ドロップしてみます。

image.png

download.jpg

download-1.jpg

  • 管理サイトでログを確認してみます。
  • 左上では、MESSAGE の内容を検索できます。右側では、推論APIへのアクセス日時で絞り込んだり、ステータスで絞り込んだりできます。

image.png

  • S3 にも保存できました。

image.png

おわりに

  • TensorFlow で学習したモデルを使い、Django で推論アプリケーションを作成しました。
  • Flask よりは、難しいですが、CSRF 対策、データベース等の機能が使えるのが便利ですね。
  • 次は、Heroku へデプロイを実施する予定です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KerasモデルをEdge TPU用モデルに変換メモ @ TensorFlow 2.0

概要

  • Kerasモデル(MobileNet)を、量子化したTensorflow Liteモデルに変換
  • 変換したTensorflow LiteモデルをEdge TPU向けにコンパイル
    • ※ Debian系の実行環境が必要
  • Coral USB Acceleratorで動作チェック
  • (余談)VirtualBoxやDockerコンテナ上から動かす

Keras → Tensorflow Lite

Python API を使って変換する。公式ドキュメントはこちら。https://www.tensorflow.org/lite/convert/python_api?hl=ja

以下は、KerasのMobileNet V2を、TensorFlow Lite用に変換するサンプル。

ポイントは、Full integer quantization(訳すとしたら、全量子化?)として変換すること。公式ドキュメントはこちら。https://www.tensorflow.org/lite/performance/post_training_quantization#full_integer_quantization_of_weights_and_activations

この変換には、converter.representative_datasetに必ずジェネレーターをセットする必要がある。8-bit整数への量子化時に行うキャリブレーションのために利用されるサンプルを指定するという目的があるみたい。

ドキュメントに、"Get sample input data as a numpy array in a method of your choosing." とあるので、推論時にモデルに与える入力データのいくつかをサンプルとして与えてあげる。

Keras → TensorFlowLite
import pathlib
import tensorflow as tf
from tensorflow.keras.applications.mobilenet_v2 import MobileNetV2

model = MobileNetV2(weights='imagenet')
converter = tf.lite.TFLiteConverter.from_keras_model(model)

def representative_dataset_gen():
    yield モデルへの入力データとなるサンプル

converter.representative_dataset = representative_dataset_gen
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8
converter.optimizations = [tf.lite.Optimize.DEFAULT]

tflite_model = converter.convert()
pathlib.Path('./mobilenet_imagenet.tflite').write_bytes(tflite_model)

Edge TPU で実行可能へ・・

上で変換したモデルは、そのままではEdgeTPUで処理してくれない。
EdgeTPUコンパイラでさらに変換が必要。

コンパイラの最新のインストール方法は、次のURLを参考にすればよい。
→Download項のコマンドを実行するだけ。あと、Debian系の環境が必須
https://coral.ai/docs/edgetpu/compiler/

EdgeTPUコンパイラのインストール
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | sudo tee /etc/apt/sources.list.d/coral-edgetpu.list
sudo apt update
sudo apt install edgetpu

コンパイルを実行すると、_edgetpuがファイル名の末尾についた目的のモデルが生成される。
量子化できていなかった場合、"Model not quantized" と怒られ変換できない。

コンパイルコマンド
edgetpu_compiler mobilenet_imagenet.tflite
# → mobilenet_imagenet_edgetpu.tflite が生成される

ちなみに、コンパイルで変換後のモデルは、普通にロードするとエラーが発生するので、Interpreter生成時にexperimental_delegatesの指定が必要になる。

動作チェック

Interpreterの生成部分にexperimental_delegatesを追加するだけで、その後の使い勝手は特に変わらない。

Interpreter生成
# CPU Only
interpreter = tf.lite.Interpreter(model_path='./mobilenet_imagenet.tflite')

# with Edge TPU
interpreter = tf.lite.Interpreter(model_path='./mobilenet_imagenet_edgetpu.tflite',
                                  experimental_delegates=[tf.lite.experimental.load_delegate('libedgetpu.so.1'),])

# あとは、同じ流れ
# 【割当】allocate_tensors()
# 【入力】set_tensor(入力層のインデックス情報(get_input_details()参照), ...)
# 【推論】invoke()
# 【出力】get_tensor(出力層のインデックス情報(get_output_details()参照))

EdgeTPUで処理できていると、Coral USB Acceleratorの白色LEDがピカピカと点滅する。

ちなみに、動作確認で作ったサンプルは、GitHubに。

余談1. VirtualBoxで

検証環境

  • Virtual Box 6.0
  • ホストOS:macOS Catalina
  • ゲストOS:Ubuntu Server 18.04.3 LTS

やり方

  • VirtualBox Extension PackでUSB3.0を利用可能にしておく。
  • VMにRuntime環境を入れておく。
  • Settings→USB に設定追加
    • Coral USB Acceleratorがホストで認識されてれば、Global Unichip Corp.が選択できるので、2回追加。
    • 一方のUSBの設定内容を、次のように変更する。
      • Name: Google Inc.
      • Vendor ID: 18d1
      • Product ID: 9302
  • VMを起動して、サンプルなどを動かす。

起動したてにlsusb打つとGlobal Unichip Corp.のままだったりするが、実行後(ライブラリアクセス後?)にGoogle Inc.になるのであまり気にしない。

Runtime環境 インストール別方法
git clone https://github.com/google-coral/edgetpu
sudo ./edgetpu/scripts/runtime/install.sh

余談2. Dockerで

検証環境

  • OS: Ubuntu 18.04.3 LTS
  • Docker 19.03

※物理環境のLinuxを用意したほうが良い。ホストのUSBをマウントする形になるので、Docker for Windows などハイパーバイザでは不向き。

やり方

Dockerファイルの例

EdgeTPU-runtime.dockerfile
FROM debian:buster

WORKDIR /workspace

RUN apt update && \
    apt install -y curl gnupg ca-certificates zlib1g-dev libjpeg-dev \
                   git python3 python3-pip && \
    apt clean && \
    rm -rf /var/lib/apt/lists/*

RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list && \
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
    apt update && \
    apt install -y libedgetpu1-std

RUN curl https://dl.google.com/coral/python/tflite_runtime-1.14.0-cp37-cp37m-linux_x86_64.whl > tflite_runtime-1.14.0-cp37-cp37m-linux_x86_64.whl && \
    pip3 install tflite_runtime-1.14.0-cp37-cp37m-linux_x86_64.whl

以下のように、USBが使えるようにマウント指定、privilege付与して、コンテナを起動する。

docker run -it --privileged --rm -v /dev/bus/usb:/dev/bus/usb edgetpu-runtime bash
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TFXとは何だったのか、現状どうなっているのか

この記事では機械学習パイプラインを本番環境にデプロイするためのエンドツーエンドなプラットフォームの提供を目指す、TFXについて述べます。

TL;DR

The TFX User Guide が一番詳しいのでこれを読みましょう。

TFX とは

TensorFlow Extended (TFX) は次の3つのうちのいずれかを指します。

  1. 機械学習パイプラインの設計思想
  2. 設計思想に基づいて機械学習パイプラインを実装するためのフレームワーク
  3. フレームワークの各コンポーネントで用いられるライブラリ

以降ではまず、設計思想としての TFX に触れ概略を紹介します。次に各コンポーネントで用いられるライブラリを見ることで、それぞれのライブラリが提供する機能について紹介します。最後に、コンポーネントを機械学習パイプラインとしてまとめ上げ、構築を行うライブラリについて紹介します。

設計思想としての TFX

まずは TensorFlow を前提とした機械学習パイプラインの設計思想としての TFX を見ていきます。

背景

機械学習は様々な活用事例が発表されていますが、それをシステムに組み込む際にはそれ以外にも様々なものが必要になることが知られています。有名なものとしては例えば、Hidden Technical Debt in Machine Learning Systems では次の図を用いてそれを説明しています。

image.png

中央の小さな黒い四角が機械学習モデルであり、周囲には設定、データ収集、特徴量の計算、データの検証、計算資源の管理、分析ツール、プロセス管理ツール、それらを乗せるインフラ、監視といった様々なものがあり、全体として複雑性が予想以上に増すことを示しています。

TFX ではこれらに対処することを目指しています。初出は TFX: A TensorFlow-Based Production-Scale Machine Learning Platform で、この論文では Google で実装した機械学習パイプラインの設計について述べています。

image.png

そこではこの図のようにパイプラインに必要な処理を整理し、フロントエンドと、オーケストレーション、データへのアクセス権限管理とガーベージコレクション、一貫したストレージが必要なことが記されています。パイプラインを構成するそれぞれのコンポーネントには次のものが含まれています。

  1. Data Analysis
  2. Data Transformation
  3. Data Validation
  4. Trainer
  5. Model Evaluation & Validation
  6. Serving

それぞれのコンポーネントの役割について見ていきましょう。

1. Data Analysis

ここでは機械学習パイプラインに投入するデータにについて、様々な分析を行います。データに含まれる特徴量の数や、特徴量ごとの欠損値の有無、各特徴量の各種統計量を計算し記録することで、入力されたデータがどのような分布をしているのかを把握します。

2. Data Transformation

ここではいわゆる前処理を行います。例えば、カテゴリカルな値にIDを割り振るといった処理はここに含まれます。このコンポーネントを学習時と推論時に使うことで、推論時には学習時と違う処理が行われてしまってうまく動かないことを防げます。

3. Data Validation

ここでは入力されているデータに対しての検証を行います。TFXでは本番環境を想定しているため、機械学習パイプラインにはデータが継続的に投入されますが、様々な事情でデータの形式や分布は変わっていきます。このコンポーネントでは予めデータのスキーマを定めておくことで、新たに投入されたデータがそのスキーマに合致しているかどうかを検証します。

image.png

上図では "category" という特徴量について、スキーマに定義されている値 ("GAMES" と "Business") 以外の値 ("EDUCATION") が入力されたことと、"num_impressions" という特徴量にスキーマに定義された型 ("int") 以外の値 ("NULL") が入力されたことの検知を行っている様子を示しています。

4. Trainer

ここでは機械学習モデルを訓練させます。機械学習モデルはそれぞれ別の形の入力データを要求し、用いられるアルゴリズムもまちまちであるため、様々な訓練処理を統一的に扱うためのハイレベルに抽象化されたインターフェースが必要になります。そこで登場したのが Estimator です。

image.png

Estimator を用いることで次のような記述ができます。

# Declare a numeric feature:
num_rooms = numeric_column(number-of-rooms)
# Declare a categorical feature:
country = categorical_column_with_vocabulary_list(
    country, [US, CA])
# Declare a categorical feature and use hashing:
zip_code = categorical_column_with_hash_bucket(
    zip_code, hash_bucket_size=1K)
# Define the model and declare the inputs
estimator = DNNRegressor(
    hidden_units=[256, 128, 64],
    feature_columns=[
        num_rooms, country,
        embedding_column(zip_code, 8)],
    activation_fn=relu,
    dropout=0.1)

# Prepare the training data
def my_training_data():
    # Read, parse training data and convert it into
    # tensors. Returns a mini-batch of data every
    # time returned tensors are fetched.
    return features, labels

# Prepare the validation data
def my_eval_data():
    # Read, parse validation data and convert it into
    # tensors. Returns a mini-batch of data every
    # time returned tensors are fetched.
    return features, labels

estimator.train(input_fn=my_training_data)
estimator.evaluate(input_fn=my_eval_data)

高度に抽象化されたインターフェースに依存することで、開発時の生産性を高く保つことができます。

5. Model Evaluation & Validation

ここではモデルの評価と妥当性の確認を行います。モデルの評価においてはオンラインでのテスト (A/Bテスト) を行うことはコストがかかるため、過去データを対象にしたオフラインテストを行い AUC などの指標が適切な範疇にあるかを評価します。また、カナリアリリースを行い、モデルが適切な振る舞いをしているかモニタリングをします。

6. Serving

ここではモデルを本番環境にデプロイします。モデルのデプロイには TensorFlow Serving を用いてスケーラブルにします。データのシリアライゼーションには tf.Example を利用します。

実例

ここまでで見てきた設計思想は実際に Google Play のリコメンドシステムに用いられたものです。上記の設計思想に基づく実装を行ったことで次の効果があったと述べられています。

  • Data Validation と Data Analysis コンポーネントによ、り学習時とサービス提供時のデータの歪みの検知を支援できた
  • 埋め込みを除き、モデルをスクラッチから学習させ続けることで、新鮮なデータを使って学習させたモデルを継続的にデプロイで来た
  • Model Validation コンポーネントにより、古いモデルと新しいモデルのパフォーマンスの差異に関するトラブルシュートを支援できた
  • Serving コンポーネントによりプロダクションへのモデルのデプロイを、高い性能と柔軟性を持って配備することができた

フレームワークとしてのTFX

ここまででは設計思想としての TFX を扱ってきましたが、次に、機械学習パイプラインのフレームワークとしての TFX を見ていきます。以降ではより具体的な実装についての話が多くなってきます。

概要

設計思想に基づき機械学習パイプラインをフレームワークとして実装しているものが TFX モジュール1 です。

image.png

設計思想で説明されたコンポーネントと、フレームワークで提供されるコンポーネントを対比すると次のようになります。

設計思想におけるコンポーネント フレームワークで提供されるコンポーネント
(なし) ExampleGen
Data Analysis StatisticsGen
Data Analysis SchemaGen
Data Validation ExampleValidator
Data Transformation Transform
Trainer Trainer
Model Evaluation Evaluator
Model Validation ModelValidator
Serving Pusher

以降では単に TFX やコンポーネントといったときにはモジュールそのものやモジュールで提供されるものを指すものとします。それぞれのコンポーネントの詳細な説明は、後ほどライブラリに関する説明をする際に行います。

コンポーネントの実装

TFX で提供されるコンポーネントはすべて共通の設計思想に基づいている点が特徴的です。

設計思想に出現する様々なコンポーネントには次の共通する特徴があります。

  • 要求される実行順序がある: 例えば、Trainer の前にはData Transform を実行する必要がある
  • 分岐する: Data Transform の出力は Data Validation でも Trainer でも利用される
  • 出力結果を保存する必要がある: トラブルシュートのため
  • コンポーネント単位でのスケーラビリティが要求される: そもそも大きなデータを扱っているため

TFX ではこれらの特徴をもとに、コンポーネント間で共通するインターフェースと、各コンポーネントの標準的な実装を提供しています。

image.png

コンポーネントは次の3つの部分からなります。

  1. Driver : コンポーネントを起動し、データを読み込む
  2. Executor : コンポーネントで行う処理そのものを行う
  3. Publisher : コンポーネントの処理結果を書き込む

Executor の処理は Apache Beam により分散処理が行われます。Beam の処理基板には Apache Flink, Google Cloud Dataflow, Spark など Beam がサポートされるものを利用できます。もちろん、Beam の Direct Runner を用いてローカルで実行することも可能です。

例えば、ExampleGen の1つであるExample GenではCSVを読み込む処理が次のように実装されています。

  parsed_csv_lines = (
      pipeline
      | 'ReadFromText' >> beam.io.ReadFromText(
          file_pattern=csv_pattern, skip_header_lines=1)
      | 'ParseCSVLine' >> beam.ParDo(csv_decoder.ParseCSVLine(delimiter=',')))
  column_infos = beam.pvalue.AsSingleton(
      parsed_csv_lines
      | 'InferColumnTypes' >> beam.CombineGlobally(
          csv_decoder.ColumnTypeInferrer(column_names, skip_blank_lines=True)))

  return (parsed_csv_lines
          | 'ToTFExample' >> beam.ParDo(_ParsedCsvToTfExample(), column_infos))

各コンポーネントの入出力は Artifact と呼ばれます2。これらの実態は ProtocolBuffer です。Artifact の型はコンポーネントごとにtfx.types に定義され、厳格に決められます。Artifact 自体はストレージ (例えば GCS やローカルのファイルストレージ) に保存されます。

また、コンポーネントはメタデータストアにアクセスすることで Artifact のメタデータ (例えば id やモデルの名前) を読み書きします。メタデータストア自体の役割についてはライブラリついて紹介する際に改めて行います。

オーケストレーション

機械学習パイプラインの設定や実行を管理するためのオーケストレーションも TFX では提供されます。

TFX を用いると機械学習パイプラインを次のステップで構築できます。

  1. 各コンポーネントを作成する
  2. それらを Pipeline を用いてつなぎ合わせる
  3. 各種 Runner に Pipeline を渡し、実行する

Runner では次のものを処理基盤として利用できます。

  1. Apache Airflow
  2. Apache Beam
  3. Kubeflow Pipelines

すべての Runner は TfxRunner を継承し、統一されたAPIで利用できます。例えば、ローカルで実行する場合には次のように実装します3

class DirectRunner(TfxRunner):
    """Tfx runner on local"""

    def __init__(self, config=None):
        """config には Apache Beamやメタデータストア、Airflowなどの設定が含まれる """
        self._config = config or {}

    def run(self, pipeline):
        """受け取ったpipelineからコンポーネントを取り出し、順に実行する"""
        # Merge airflow-specific configs with pipeline args
        self._config.update(pipeline.pipeline_args)

        for component in pipeline.components:
            self._execute_component(component)

        return pipeline

    def _execute_component(self, component):
        """コンポーネントを実行する、ここではローカルで実行するための処理を書いている"""
        # parse parameters
        input_dict = {key:value.get() for key, value in component.input_dict.items()}
        output_dict = {key: value.get() for key, value in component.outputs.get_all().items()}
        exec_properties = component.exec_properties

        # create executor
        additional_pipeline_args = self._config.get('additional_pipeline_args') or {}
        executor = component.executor(beam_pipeline_args=additional_pipeline_args.get('beam_pipeline_args'))

        executor.Do(input_dict, output_dict, exec_properties)

Runner を利用するときには次のようにします。

pipeline = Pipeline(
    pipeline_name="TFX Pipeline",
    pipeline_root=_pipeline_root,
    components=[
        example_gen, 
        statistics_gen, 
        infer_schema, 
        example_validator,
        transform, 
        trainer, 
        model_analyzer, 
        model_validator,
        pusher,
    ]
)

DirectRunner().run(pipeline)

このようにして、ローカルでの開発時と本番環境でのデプロイ時に同じコードを使い回すことができることは TFX の特徴の一つです。

サンプル

オーケストレーションに Airflow を用いて TFX を実行する場合のサンプルコードが GitHub のリポジトリにあります (tfx/taxi_pipeline_simple.py)。

コメントを含めて全部で157行と比較的短いコードで書かれていることがわかります。

TFX に関連するライブラリ

TFX で行われる様々な処理のために、各種ライブラリが開発されています。ここではそれらのライブラリについて見ていきます。

概要

コンポーネントで行われる処理には TensorFlow Core には無い機能が必要になるので、実装を行うための各種ライブラリが存在します。

image.png

それぞれのライブラリについて、これまでに見てきたフレームワーク、モジュールとの関連は次のようになっています。

設計思想におけるコンポーネント フレームワークにおけるコンポーネント コンポーネントが利用するライブラリ
(なし) ExampleGen (なし)
Data Analysis StatisticsGen Tensorflow Data Validation
Data Analysis SchemaGen Tensorflow Data Validation
Data Validation ExampleValidator Tensorflow Data Validation
Data Transformation Transform TensorFlow Transform
Trainer Trainer P TensorFlow
Model Evaluation Evaluator TensorFlow Model Analysis
Model Validation ModelValidator TensorFlow Model Analysis
Serving Pusher Serving

また、機械学習パイプライン全体を通じて関連するライブラリに、機械学習パイプラインに関するデータ形式の提供やメタデータの保管を行うためのライブラリとして、TensorFlow MetadataML Metadata (MLMD) があります。

以降ではそれぞれのライブラリの提供する機能について見ていきます。

TensorFlow Data Validation (TFDV)

TensorFlow Data Validation (TFDV) は次の機能を提供します。

  • 学習データ・テストデータの要約統計量の算出
  • データのスキーマの自動算出
  • データの欠損や、閾値を超えた値などのデータの異常値の検知
  • 上記を補佐するビューワー

学習データ・テストデータの要約統計量算出では Facets を用いてインタラクティブにデータの様子を知ることができます。

image.png

また、データのスキーマは次のように自動算出されます。これは自動算出したものをそのままサービスで使うというものではなく、自動算出したものをもとに人手で修正してサービスで利用するという意図のものである点には注意が必要です。

image.png

データの欠損や、閾値を超えた値などのデータの異常値の検知については、与えた2つのデータ (例えば、評価データとテストデータ) が同一の分布をしているかという観点で行われます。こちらも Facets を用いた可視化が可能です。

image.png

また、異常値については作成済みのスキーマを用いた比較が行われます。例えば、次の例ではカテゴリカル変数である companypayment にスキーマ作成には定義されていない値が出現していることを示しています。

image.png

これらの機能については、TensorFlow Data Validationのチュートリアル から試すことができます。 (TFDV のインストール後、エラーが発生したときに手動でランタイムの再起動が必要 な点には注意が必要です)

TensorFlow Transform (TFT)

TensorFlow Transform (TFT) は TensorFlow を用いてデータを前処理するためのライブラリです。Apache Beam を用いて並列処理を行う点が特徴的です。

TFT による前処理を行うためにはまず、preprocessing_fn と呼ばれる関数を定義ます。実装は例えば次のようになります。

def preprocessing_fn(inputs):
    """Preprocess input columns into transformed columns."""
    x = inputs['x']
    y = inputs['y']
    s = inputs['s']
    x_centered = x - tft.mean(x)
    y_normalized = tft.scale_to_0_1(y)
    s_integerized = tft.compute_and_apply_vocabulary(s)
    x_centered_times_y_normalized = (x_centered * y_normalized)
    return {
        'x_centered': x_centered,
        'y_normalized': y_normalized,
        's_integerized': s_integerized,
        'x_centered_times_y_normalized': x_centered_times_y_normalized,
    }

実装した preprocessing_fn を Beam に渡して実行します。

def main():
  # Ignore the warnings
  with tft_beam.Context(temp_dir=tempfile.mkdtemp()):
    transformed_dataset, transform_fn = (  # pylint: disable=unused-variable
        (raw_data, raw_data_metadata) | tft_beam.AnalyzeAndTransformDataset(
            preprocessing_fn))

  transformed_data, transformed_metadata = transformed_dataset  # pylint: disable=unused-variable

  print('\nRaw data:\n{}\n'.format(pprint.pformat(raw_data)))
  print('Transformed data:\n{}'.format(pprint.pformat(transformed_data)))

TensorFlow Model Analysis (TFMA)

TensorFlow Model Analysis (TFMA) はモデルの評価を行うためのライブラリです。特徴量を指定すると、その特徴量の値に対するモデルの制度をヒストグラムとして見ることができます。また、こちらも対話的に指標を色々と変更して確認ができます。

image.png

また、モデルに異なるデータセットを入力したときの精度の比較も可能です。これは例えば、機械学習モデルの精度を日毎に比較したい場合に役立つでしょう。

image.png

TensorFlow Metadata (TFMD)

TensorFlow Metadata (TFMD) はこれまでに見てきた TFX の各ライブラリで用いるためのメタデータを提供します。

ML Metadata (MLMD)

ML Metadata (MLMD) は TFX のコンポーネントが入出力を行うためのメタデータストアを提供します。SQLite または MySQL をバックエンドとして、メタデータの管理を行います。

MLMD は次の3つの役割を持ちます。

  1. 機械学習パイプラインの各コンポーネントの生成物に関するメタデータの保管
  2. 機械学習パイプラインの各コンポーネントの実行状態の保管
  3. 機械学習パイプラインそのものに関するメタデータの保管

image.png

MLMD により次の事柄が可能になります。

  • 特定の型を持った Artifacts の一覧の取得
  • 同じ型を持った Artifacts 同士の比較
  • ある DAG に関連するすべての処理と入出力結果の表示
  • ある出力結果に関連するすべてのイベントの取得
  • ある入力から作成された出力結果の特定
  • 過去に同一の入力に対する処理が実行されているかどうかの確認
  • ワークフローが実行されるときのコンテキストの記録

TFX に関連するライブラリ利用者

TFX に関連する各種ライブラリの利用者についてはいくつかの企業が存在するようです。 TensorFlow Extended (TFX) Overview and Pre-training Workflow (TF Dev Summit '19) では次の企業が紹介されています。

image.png

  • airbnb : TensorFlow Serving を利用している
  • paypal : 詳細不明
  • Twitter : モデルの解釈のために TFMA を利用している

他にも、Spotify は彼らのリポジトリの中で spotify-tensorflow/spotify_tensorflow/tfx として TFX のライブラリに関するサンプルコードを提供しています。

また、 merpay が利用しているとの話も聞いたことがありますが、こちらはどうやら公開されている情報はないようです。

TFX に関する現状の課題

最後に、TFX に関する現状の課題についていくつか見ていきましょう。

設計思想としての TFX

TFX の設計思想については、設計に活かしやすいように思います。実際、最近の機械学習のデザインパターンに関する調査でも、TFX の設計思想と共通するものがいくつか見られます。

フレームワークとしての TFX

TFX と関連するライブラリは現在アクティブに開発が進んでいるもののため、アップデートが頻繁に行われているほか、ドキュメントとコードの整合性も保たれていない状態です。現状では機械学習パイプラインの運用を TFX を用いて行うのは相当な覚悟と努力4が必要とされるでしょう。

TFX の提供するライブラリ群

TFX の提供するライブラリ群については確かに魅力的なものも多いため、既存のパイプラインに追加で組み込み、運用上で監視やトラブルシュートに用いることは可能だと考えます。

ただし、すべてのライブラリはまだアクティブに開発中であるため、プロダクションへの一連のデータの流れを止めないような配慮は必要でしょう。例えば、データの流れをフォークして、データやモデルの学習結果の検証用のコンポーネントを、サービスには影響を与えない形で実行するのが望ましいでしょう。

最後に

TFX は機械学習パイプラインについてのエンドツーエンドなプラットフォームを提供するという野心的なプロジェクトであり、筆者が最も注目しているプロジェクトの一つでもあります (もう一つは Swift for TensorFlow) 。また、 Fairness での取り組みが取り込まれつつあるという開発速度の早さも魅力の一つです。

機械学習パイプラインのベストプラクティスについて多大な示唆を与えてくれるため、個人的には今後も継続してキャッチアップを続ける価値があるプロジェクトだと考えます。


  1. フレームワークなんですが PyPI からインストールできるのでモジュールとしています 

  2. TFX version 0.15 から 

  3. TFX 0.14 を前提に書いているので現在では異なる書き方が必要でしょう 

  4. 例えば、この記事を書くためにチュートリアルを改めて実行しましたがうまく動かなかったため、修正のPRを作成しました。 このようにサンプルコードが動かなかったりドキュメントがなかったりすることは現状では日常茶飯事です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AI初心者向け】mnist_transfer_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)

はじめに

この記事は全3回予定の第3回の記事です。
この記事はmnist_transfer_cnn.pyを1行ずつ解説していくだけの記事です。
前回と重なる部分もありますが、記事単体で読みやすくするため、重複して書いている内容もありますのでご了承ください。
AIに興味があるけどまだ触ったことはない人などが対象です。これを読めばディープラーニングの基本的な学習の流れが理解できるはず、と思って書いていきます。(もともとは社内で研修用に使おうと思って作成していた内容です)
1. 【AI初心者向け】mnist_mlp.pyを1行ずつ解説していく(KerasでMNISTを学習させる)
2. 【AI初心者向け】mnist_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)
3. 【AI初心者向け】mnist_transfer_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)

動作確認方法について

MNISTは画像なので、このコードを動かすにはGPUがあったほうがいいです(CPUだとちょっと辛いです)。
おすすめの方法はGoogle Colaboratoryを使う方法です。
colab.gif
やることは2つだけ。
・Python3の新しいノートブックを開く
・ランタイムからGPUを有効にする
これでGPUが使えるようになりました。
セルにコードを貼り付けて実行(ショートカットはCTRL+ENTER)するだけで動きます。

mnistについて

手書き文字画像のデータセットで、機械学習のチュートリアルでよく使用されます。
内容:0~9の手書き文字
画像サイズ:28px*28px
カラー:白黒
データサイズ:7万枚(訓練データ6万、テストデータ1万の画像とラベルが用意されています)

Fine-tuningとは

すでにある優れたモデルのパラメータを初期値として利用し、別のタスクに対応すること。こうすることで計算コストの削減と、精度の向上が望めます。

今回でいえば、

  1. 0~4の画像を分類するモデルを作成(もとになる重みを作成する)
  2. 作成したモデルの画像の特徴を抽出する層の重みを固定し、変更できないようにする
  3. 5~9の画像を学習させる(全結合層=分類する部分の重みのみを更新する)
  4. 最終的に、5~9の5種類の手書き文字を入力として受け取り、5~9のいずれであるか5種類に分類するモデルが完成する

5~9の画像を分類できるよう学習させたので、今回最終的に完成したモデルでは0~4の画像分類はできません。

コードの解説

準備

'''Trains a simple convnet on the MNIST dataset.
Gets to 99.25% test accuracy after 12 epochs
(there is still a lot of margin for parameter tuning).
16 seconds per epoch on a GRID K520 GPU.
'''
'''Transfer learning toy example.
1 - Train a simple convnet on the MNIST dataset the first 5 digits [0..4].
2 - Freeze convolutional layers and fine-tune dense layers
   for the classification of digits [5..9].
Get to 99.8% test accuracy after 5 epochs
for the first five digits classifier
and 99.2% for the last five digits after transfer + fine-tuning.
'''

# 特に必要ないコードです(Pythonのバージョンが3だが、コードがPython2で書かれている場合に必要になる)
from __future__ import print_function

# 必要なライブラリをインポート
import datetime
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K

# 現在時刻を取得
now = datetime.datetime.now

# 定数
batch_size = 128   # バッチサイズ。1度に学習するデータサイズ
num_classes = 5    # 分類するラベル数。今回は手書き画像を5~9の5種類に分類する
epochs = 5         # エポック数。全データを何回学習するか
img_rows, img_cols = 28, 28  # 入力画像の次元数
filters = 32       # 畳み込みのフィルター数
pool_size = 2      # マックスプーリングするサイズ
kernel_size = 3    # 畳み込みのフィルター(カーネル)サイズ
# データの形
if K.image_data_format() == 'channels_first':
    input_shape = (1, img_rows, img_cols)
else:
    input_shape = (img_rows, img_cols, 1)

データの形の部分ですが、詳細は【AI初心者向け】mnist_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)で確認してください。データの前処理の部分に記載しています。

KerasのバックエンドがTheano(channels_first)か tensorflow(channels_last)かで画像の書式が異なるのを判断しています。今回はtensorflowなので、(28, 28, 1)になります。

データの前処理

# mnistのデータを読み込み、訓練データ(6万件)とテストデータ(1万件)に分割する
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# ラベルが5以上か未満かで分けたデータセットを作成
# ラベル値が5未満の訓練画像
x_train_lt5 = x_train[y_train < 5]
# ラベル値が5未満の訓練ラベル
y_train_lt5 = y_train[y_train < 5]
# ラベル値が5未満のテスト画像
x_test_lt5 = x_test[y_test < 5]
# ラベル値が5未満のテストラベル
y_test_lt5 = y_test[y_test < 5]

# ラベル値が5以上の訓練画像
x_train_gte5 = x_train[y_train >= 5]
# ラベル値が5以上の訓練ラベルから5を引いたデータセット(5~9⇒0~4にする)
y_train_gte5 = y_train[y_train >= 5] - 5
# ラベル値が5以上のテスト画像
x_test_gte5 = x_test[y_test >= 5]
# ラベル値が5以上のテストラベルから5を引いたデータセット(5~9⇒0~4にする)
y_test_gte5 = y_test[y_test >= 5] - 5

まず、0~4の画像を分類するモデルを作成するために、0~4のデータと5~9のデータに分けておきます。

次に、5~9のデータに関してはラベルを5~9⇒0~4に変更しておきます。

モデルの定義

# モデルの定義(.add()メソッドを使わないパターン)

# 畳み込み処理で特徴を学習
feature_layers = [
    # 畳み込み層(フィルター:32枚、フィルターサイズ:(3, 3)、受け取る入力サイズ:(28, 28, 1))
    Conv2D(filters, kernel_size,
           # padding='valid'で0パディングしない。0パディングするときは'same'を指定する
           padding='valid',
           input_shape=input_shape),
    # 活性化関数:Relu
    Activation('relu'),
    # 畳み込み層(フィルター:32枚、フィルターサイズ:(3, 3))
    Conv2D(filters, kernel_size),
    # 活性化関数:Relu
    Activation('relu'),
    # プーリング層
    MaxPooling2D(pool_size=pool_size),
    # 0.25の確率でドロップアウト
    Dropout(0.25),
    # データを1次元に変換
    Flatten(),
]

# 全結合層で分類を学習
classification_layers = [
    # 全結合層(128ユニット)
    Dense(128),
    # 活性化関数:relu
    Activation('relu'),
    # 0.5の確率でドロップアウト
    Dropout(0.5),
    # 全結合層(5ユニット)
    Dense(num_classes),
    # 活性化関数:softmax(分類問題のため)
    Activation('softmax')
]

# Sequentialクラスにfeature_layersとclassification_layersを渡したものをインスタンス化
model = Sequential(feature_layers + classification_layers)

今回は第1回、第2回と違い.add()メソッドを使わずにモデルを定義しています。
Kerasでは、層を順番に並べたリストをSequential()に渡してインスタンス化することでもモデルの定義ができます。

なぜこのような書き方をしたのかというと、Fine-tuningでは、学習時に特定の層のみ重みを変更しない(または変更する)ということをします。そのためにあえてこのような書き方をしています。

画像の特徴を抽出する層と、分類をする層を分けて定義しておくことで、どちらかの重みのみを更新するということが簡単にできるようになります。

学習させる関数

# 学習させる関数を作成
def train_model(model, train, test, num_classes):

    # データの前処理
    # データの形式をreshapeして合わせておく
    x_train = train[0].reshape((train[0].shape[0],) + input_shape)  # (30596, 28, 28) -> reshape(30596, 28, 28, 1)
    x_test = test[0].reshape((test[0].shape[0],) + input_shape)     # (5139, 28, 28)  -> reshape(5139, 28, 28, 1)
    # 画像データは0~255の値をとるので255で割ることでデータを標準化する
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')
    # .astype('float32')でデータ型を変換する。(しないと割ったときにエラーが出るはず)
    x_train /= 255
    x_test /= 255
    print('x_train shape:', x_train.shape)
    print(x_train.shape[0], 'train samples')
    print(x_test.shape[0], 'test samples')

    # ラベルデータをone-hot-vector化
    '''one-hot-vectorのイメージはこんな感じ
    label  0 1 2 3 4 
    0:    [1,0,0,0,0]
    3:    [0,0,0,1,0]'''
    y_train = keras.utils.to_categorical(train[1], num_classes)
    y_test = keras.utils.to_categorical(test[1], num_classes)

    # 学習プロセスを設定する
    model.compile(loss='categorical_crossentropy',  # 損失関数を設定。今回は分類なのでcategorical_crossentropy
                  optimizer='adadelta',  # 最適化アルゴリズムをadadeltaにしている
                  metrics=['accuracy'])  # 評価関数を指定

    # 学習開始時間を取得
    t = now()
    # 学習させる
    model.fit(x_train, y_train,       # 学習データ、ラベル
              batch_size=batch_size,  # バッチサイズ(128)
              epochs=epochs,          # エポック数(5)
              verbose=1,              # 学習の進捗をリアルタムに棒グラフで表示(0で非表示)
              validation_data=(x_test, y_test))  # テストデータ(エポックごとにテストを行い誤差を計算するため)
    # 学習にかかった時間を出力
    print('Training time: %s' % (now() - t))

    # 評価
    # テストデータを渡す(verbose=0で進行状況メッセージを出さない)
    score = model.evaluate(x_test, y_test, verbose=0)
    # 汎化誤差を出力
    print('Test score:', score[0])
    # 汎化性能を出力
    print('Test accuracy:', score[1])

今回、5~9の画像を分類するモデルを作るために2回学習させます。

学習

# 上で作成した関数を使い学習させる
# 5未満の画像に0~4のラベルを与えて学習(分類させる)
train_model(model,
            (x_train_lt5, y_train_lt5),
            (x_test_lt5, y_test_lt5), num_classes)

# trainable=Falseで、レイヤーに学習をさせなくする
# 畳み込みを行う部分であるfeature_layersのパラメータ更新をしないよう設定し、classification_layersのパラメータのみ更新する
# 変更を有効にするには、プロパティの変更後のモデルでcompile()を呼ぶ必要がある
for l in feature_layers:
    l.trainable = False

# 上で作成した関数を使い学習させる
# 5以上の画像に0~4のラベルを与えて学習(分類させる)
train_model(model,
            (x_train_gte5, y_train_gte5),
            (x_test_gte5, y_test_gte5), num_classes)

いよいよ学習です。

  1. 0~4の画像を分類するモデルを作成(もとになる重みを作成する)
  2. feature_layersの重みを固定し、変更できないようにする
  3. 5~9の画像を学習させる(全結合層=分類する部分の重みのみを更新する)
  4. 最終的に、5~9の5種類の手書き文字を入力として受け取り、5~9のいずれであるか5種類に分類するモデルの完成!

おわりに

以上で全3回のソース解説記事は終わりです。
解説は終わりですが、おまけとして次回、モデルの保存とロード、利用方法の記事をアップ予定です。
作っただけで保存をしないと、せっかく作ったのに消えちゃうので・・・。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む