- 投稿日:2019-12-21T23:42:49+09:00
HerokuとTensorFlowを利用した、顔画像の推論
はじめに
Django
で作成したTensorFlow
のアプリケーションを、Heroku
へ移植します。Heroku
のUbuntu 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
には、gunicorn
でDjango
のプロジェクトを起動するように設定します。Heroku
でコンテナを使う場合は、不要な設定です。ただし、ローカル環境でデバッグする場合は必要です。- なので、デプロイには、必要ありません。
web: gunicorn project.wsgi --log-file -
Heroku
で利用するPython
のバージョンを指定します。特にこだわりがないので、最新にしています。runtime.txtpython-3.7.5
- ライブラリをインストールします。
Heroku
へデプロイするために追加したのは、django-heroku
とgunicorn
です。django-heroku
は、Heroku
でDjango
を利用するための色々な設定等をしてくれるものです。requirements.txtboto3 django django-heroku gunicorn numpy==1.16.5 opencv-python pillow tensorflow==1.14.0
.env
にHeroku
をローカルでデバッグする時の環境変数を設定します。AWS_ACCESS_KEY_ID='your-aws-access-key' AWS_SECRET_ACCESS_KEY='your-aws-secret-access-key' BUCKET=abe-or-ishihara
- 設定ファイルの末尾に、
Heroku
関連の設定をします。project/settings.pySTATIC_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.pyDEBUG = False ALLOWED_HOSTS = ['abe-or-ishihara.herokuapp.com']Heroku へデプロイ
Heroku
のコンテナの設定をします。- アドオンで
PostgreSQL
を設定しています。コンテナではない、通常のHeroku
なら設定無しでOKなのですが、コンテナの場合は明示が必要です。AWS S3
の設定をします。これは、Heroku
のコンパネに環境変数が設定され、コンテナの起動時に利用される感じです。languages
でpython
を指定します。バージョンは、runtime.txt
に記載のバージョンが利用されます。pip
のopencv-python
には、apt
のlibopencv-dev
が必要なので、インストールします。heroku.ymlsetup: 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 へアクセス
- https://abe-or-ishihara.herokuapp.com/ でアプリケーションを確認します。
- https://abe-or-ishihara.herokuapp.com/admin/app/log/ でログを確認します。
おわりに
Heroku
でDjango
TensorFlow
とOpenCV
を利用したアプリケーションのデプロイをしました。Heroku
は、コンテナでUbuntu
を使えるので、大体の事ができます。今回は、OpenCV
を使うため、コンテナを利用しました。CPU
は、AVX (Advanced Vector Extensions)
に対応しているので、TensorFlow
もOKです。今時、AVX
対応していない方が、珍しいですけどね。- これで、一連のアプリケーション作成が終了しました。次回は、まとめをしたく思っております。
- 投稿日:2019-12-21T20:23:35+09:00
DjangoとTensorFlowを利用した、顔画像の推論
はじめに
Django
とTensorFlow
を使い、顔画像の推論を実施します。- 最終的に
Heroku
へデプロイする事を考慮しました。Django
なら、データベースや CSRF 対策など、機能が充実しているからですね。- ソース一式は ここ です。
概要
- フロント
- Bootstrap
- jQuery
- SpinKit https://github.com/tobiasahlin/SpinKit
- バックエンド
- Django
- NumPy
- OpenCV
- Pillow
- TensorFlow
- ストレージ
- S3
ライブラリ
- 以下を
pip
でインストールします。requirements.txtboto3 django numpy==1.16.5 opencv-python pillow tensorflow==1.14.0Django
セットアップ
- まず、プロジェクトとアプリの雛形を作成します。
$ django-admin startproject project . $ python manage.py startapp app
- 上記で作成した
app
を追加する。- テンプレートのフォルダを追加する。
- 日本語への修正をする。
- タイムゾーンの修正をする。
- スタティックファイルのフォルダを追加する。
project/settings.pyINSTALLED_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/
にアクセスし、設定したユーザー名とパスワードを入力します。
- ブラウザで
http://127.0.0.1:8000/admin
にアクセスします。トップ画面を作成
app/views.py
にトップ画面のテンプレートを設定します。app/views.pydef index(request): return render(request, 'index.html')
- 上記の
index.html
はtemplates/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.pydef api(request): log = Log()
- 画像は、
POST
で送信されているかチェックします。- また、事前に
Django
がCSRF
チェックをしている形になります。app/views.pyif request.method != 'POST': log.status = 'post error' log.save() raise Http404
- 画像を読み込みます。ファイル名、ファイルサイズも取得します。
app/views.pytry: 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.pyif filesize > 10000000: log.status = 'filesize error' log.save() return server_error(request)
- ファイルデータを取得し、
jpeg
png
のチェックをします。app/views.pytry: 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.pytry: 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.pydef 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.pyimage = 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.pyface_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.pytry: 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.pypredict_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.pydef 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.pyimage = 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.pyfrom 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.pyclass 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.pyclass 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/
下記のような画面が準備できました。S3、IAMの設定
IAM
Django
の推論API のdef save_image(filedata, filename):
でファイルを保存しています。AWS_ACCESS_KEY_ID
とAWS_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
にしました。- リージョンは、米国東部(バージニア北部)にしました。
Heroku
のus
リージョンにデプロイする予定です。us
は、バージニアなので、距離的に近い方が良いでしょう。起動シェル
AWS
のIAM
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推論のテスト
- 画像をドラッグ&ドロップしてみます。
- 管理サイトでログを確認してみます。
- 左上では、
MESSAGE
の内容を検索できます。右側では、推論APIへのアクセス日時で絞り込んだり、ステータスで絞り込んだりできます。
S3
にも保存できました。おわりに
TensorFlow
で学習したモデルを使い、Django
で推論アプリケーションを作成しました。Flask
よりは、難しいですが、CSRF
対策、データベース等の機能が使えるのが便利ですね。- 次は、
Heroku
へデプロイを実施する予定です。
- 投稿日:2019-12-21T17:12:18+09:00
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 → TensorFlowLiteimport 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.dockerfileFROM 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
- 投稿日:2019-12-21T16:14:05+09:00
TFXとは何だったのか、現状どうなっているのか
この記事では機械学習パイプラインを本番環境にデプロイするためのエンドツーエンドなプラットフォームの提供を目指す、TFXについて述べます。
TL;DR
The TFX User Guide が一番詳しいのでこれを読みましょう。
TFX とは
TensorFlow Extended (TFX) は次の3つのうちのいずれかを指します。
- 機械学習パイプラインの設計思想
- 設計思想に基づいて機械学習パイプラインを実装するためのフレームワーク
- フレームワークの各コンポーネントで用いられるライブラリ
以降ではまず、設計思想としての TFX に触れ概略を紹介します。次に各コンポーネントで用いられるライブラリを見ることで、それぞれのライブラリが提供する機能について紹介します。最後に、コンポーネントを機械学習パイプラインとしてまとめ上げ、構築を行うライブラリについて紹介します。
設計思想としての TFX
まずは TensorFlow を前提とした機械学習パイプラインの設計思想としての TFX を見ていきます。
背景
機械学習は様々な活用事例が発表されていますが、それをシステムに組み込む際にはそれ以外にも様々なものが必要になることが知られています。有名なものとしては例えば、Hidden Technical Debt in Machine Learning Systems では次の図を用いてそれを説明しています。
中央の小さな黒い四角が機械学習モデルであり、周囲には設定、データ収集、特徴量の計算、データの検証、計算資源の管理、分析ツール、プロセス管理ツール、それらを乗せるインフラ、監視といった様々なものがあり、全体として複雑性が予想以上に増すことを示しています。
TFX ではこれらに対処することを目指しています。初出は TFX: A TensorFlow-Based Production-Scale Machine Learning Platform で、この論文では Google で実装した機械学習パイプラインの設計について述べています。
そこではこの図のようにパイプラインに必要な処理を整理し、フロントエンドと、オーケストレーション、データへのアクセス権限管理とガーベージコレクション、一貫したストレージが必要なことが記されています。パイプラインを構成するそれぞれのコンポーネントには次のものが含まれています。
- Data Analysis
- Data Transformation
- Data Validation
- Trainer
- Model Evaluation & Validation
- Serving
それぞれのコンポーネントの役割について見ていきましょう。
1. Data Analysis
ここでは機械学習パイプラインに投入するデータにについて、様々な分析を行います。データに含まれる特徴量の数や、特徴量ごとの欠損値の有無、各特徴量の各種統計量を計算し記録することで、入力されたデータがどのような分布をしているのかを把握します。
2. Data Transformation
ここではいわゆる前処理を行います。例えば、カテゴリカルな値にIDを割り振るといった処理はここに含まれます。このコンポーネントを学習時と推論時に使うことで、推論時には学習時と違う処理が行われてしまってうまく動かないことを防げます。
3. Data Validation
ここでは入力されているデータに対しての検証を行います。TFXでは本番環境を想定しているため、機械学習パイプラインにはデータが継続的に投入されますが、様々な事情でデータの形式や分布は変わっていきます。このコンポーネントでは予めデータのスキーマを定めておくことで、新たに投入されたデータがそのスキーマに合致しているかどうかを検証します。
上図では "category" という特徴量について、スキーマに定義されている値 ("GAMES" と "Business") 以外の値 ("EDUCATION") が入力されたことと、"num_impressions" という特徴量にスキーマに定義された型 ("int") 以外の値 ("NULL") が入力されたことの検知を行っている様子を示しています。
4. Trainer
ここでは機械学習モデルを訓練させます。機械学習モデルはそれぞれ別の形の入力データを要求し、用いられるアルゴリズムもまちまちであるため、様々な訓練処理を統一的に扱うためのハイレベルに抽象化されたインターフェースが必要になります。そこで登場したのが Estimator です。
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 です。
設計思想で説明されたコンポーネントと、フレームワークで提供されるコンポーネントを対比すると次のようになります。
設計思想におけるコンポーネント フレームワークで提供されるコンポーネント (なし) 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 ではこれらの特徴をもとに、コンポーネント間で共通するインターフェースと、各コンポーネントの標準的な実装を提供しています。
コンポーネントは次の3つの部分からなります。
- Driver : コンポーネントを起動し、データを読み込む
- Executor : コンポーネントで行う処理そのものを行う
- 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 を用いると機械学習パイプラインを次のステップで構築できます。
- 各コンポーネントを作成する
- それらを Pipeline を用いてつなぎ合わせる
- 各種 Runner に Pipeline を渡し、実行する
Runner では次のものを処理基盤として利用できます。
- Apache Airflow
- Apache Beam
- 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 には無い機能が必要になるので、実装を行うための各種ライブラリが存在します。
それぞれのライブラリについて、これまでに見てきたフレームワーク、モジュールとの関連は次のようになっています。
設計思想におけるコンポーネント フレームワークにおけるコンポーネント コンポーネントが利用するライブラリ (なし) 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 Metadata と ML Metadata (MLMD) があります。
以降ではそれぞれのライブラリの提供する機能について見ていきます。
TensorFlow Data Validation (TFDV)
TensorFlow Data Validation (TFDV) は次の機能を提供します。
- 学習データ・テストデータの要約統計量の算出
- データのスキーマの自動算出
- データの欠損や、閾値を超えた値などのデータの異常値の検知
- 上記を補佐するビューワー
学習データ・テストデータの要約統計量算出では Facets を用いてインタラクティブにデータの様子を知ることができます。
また、データのスキーマは次のように自動算出されます。これは自動算出したものをそのままサービスで使うというものではなく、自動算出したものをもとに人手で修正してサービスで利用するという意図のものである点には注意が必要です。
データの欠損や、閾値を超えた値などのデータの異常値の検知については、与えた2つのデータ (例えば、評価データとテストデータ) が同一の分布をしているかという観点で行われます。こちらも Facets を用いた可視化が可能です。
また、異常値については作成済みのスキーマを用いた比較が行われます。例えば、次の例ではカテゴリカル変数である
company
とpayment
にスキーマ作成には定義されていない値が出現していることを示しています。これらの機能については、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) はモデルの評価を行うためのライブラリです。特徴量を指定すると、その特徴量の値に対するモデルの制度をヒストグラムとして見ることができます。また、こちらも対話的に指標を色々と変更して確認ができます。
また、モデルに異なるデータセットを入力したときの精度の比較も可能です。これは例えば、機械学習モデルの精度を日毎に比較したい場合に役立つでしょう。
TensorFlow Metadata (TFMD)
TensorFlow Metadata (TFMD) はこれまでに見てきた TFX の各ライブラリで用いるためのメタデータを提供します。
ML Metadata (MLMD)
ML Metadata (MLMD) は TFX のコンポーネントが入出力を行うためのメタデータストアを提供します。SQLite または MySQL をバックエンドとして、メタデータの管理を行います。
MLMD は次の3つの役割を持ちます。
- 機械学習パイプラインの各コンポーネントの生成物に関するメタデータの保管
- 機械学習パイプラインの各コンポーネントの実行状態の保管
- 機械学習パイプラインそのものに関するメタデータの保管
MLMD により次の事柄が可能になります。
- 特定の型を持った Artifacts の一覧の取得
- 同じ型を持った Artifacts 同士の比較
- ある DAG に関連するすべての処理と入出力結果の表示
- ある出力結果に関連するすべてのイベントの取得
- ある入力から作成された出力結果の特定
- 過去に同一の入力に対する処理が実行されているかどうかの確認
- ワークフローが実行されるときのコンテキストの記録
TFX に関連するライブラリ利用者
TFX に関連する各種ライブラリの利用者についてはいくつかの企業が存在するようです。 TensorFlow Extended (TFX) Overview and Pre-training Workflow (TF Dev Summit '19) では次の企業が紹介されています。
- 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 での取り組みが取り込まれつつあるという開発速度の早さも魅力の一つです。
機械学習パイプラインのベストプラクティスについて多大な示唆を与えてくれるため、個人的には今後も継続してキャッチアップを続ける価値があるプロジェクトだと考えます。
フレームワークなんですが PyPI からインストールできるのでモジュールとしています ↩
TFX version 0.15 から ↩
TFX 0.14 を前提に書いているので現在では異なる書き方が必要でしょう ↩
例えば、この記事を書くためにチュートリアルを改めて実行しましたがうまく動かなかったため、修正のPRを作成しました。 このようにサンプルコードが動かなかったりドキュメントがなかったりすることは現状では日常茶飯事です。 ↩
- 投稿日:2019-12-21T02:27:17+09:00
【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を使う方法です。
やることは2つだけ。
・Python3の新しいノートブックを開く
・ランタイムからGPUを有効にする
これでGPUが使えるようになりました。
セルにコードを貼り付けて実行(ショートカットはCTRL+ENTER)するだけで動きます。mnistについて
手書き文字画像のデータセットで、機械学習のチュートリアルでよく使用されます。
内容:0~9の手書き文字
画像サイズ:28px*28px
カラー:白黒
データサイズ:7万枚(訓練データ6万、テストデータ1万の画像とラベルが用意されています)Fine-tuningとは
すでにある優れたモデルのパラメータを初期値として利用し、別のタスクに対応すること。こうすることで計算コストの削減と、精度の向上が望めます。
今回でいえば、
- 0~4の画像を分類するモデルを作成(もとになる重みを作成する)
- 作成したモデルの画像の特徴を抽出する層の重みを固定し、変更できないようにする
- 5~9の画像を学習させる(全結合層=分類する部分の重みのみを更新する)
- 最終的に、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)いよいよ学習です。
- 0~4の画像を分類するモデルを作成(もとになる重みを作成する)
- feature_layersの重みを固定し、変更できないようにする
- 5~9の画像を学習させる(全結合層=分類する部分の重みのみを更新する)
- 最終的に、5~9の5種類の手書き文字を入力として受け取り、5~9のいずれであるか5種類に分類するモデルの完成!
おわりに
以上で全3回のソース解説記事は終わりです。
解説は終わりですが、おまけとして次回、モデルの保存とロード、利用方法の記事をアップ予定です。
作っただけで保存をしないと、せっかく作ったのに消えちゃうので・・・。