AWS Step FunctionsとLambdaでディープラーニングの訓練を全自動化する

動機とやったことの概要

詳細

Lambdaに付与する権限

たぶん以下くらいの権限がLambda実行時に必要。

AWSLambdaAMIExecutionRole
AmazonS3FullAccess
AmazonEC2SpotFleetRole
AWSLambdaBasicExecutionRole
AmazonSNSFullAccess
EC2ReadOnly ("ec2:DescribeSpotInstanceRequests"リソースへのアクセスを追加)

Step Functionsの入力

{
    "exec_name": "pix2pix-20161231",
    "repository_url": "https://github.com/mattya/chainer-pix2pix.git",
    "repository_name": "chainer-pix2pix",
    "data_dir": "/home/ubuntu/data",
    "output_dir": "/home/ubuntu/result",
    "data_get_command": "/home/ubuntu/.pyenv/shims/aws s3 cp s3://pix2pixfacade/ /home/ubuntu/data --recursive",
    "exec_command": "/home/ubuntu/.pyenv/shims/python /home/ubuntu/chainer-pix2pix/train_facade.py -g 0 -e 100 -i /home/ubuntu/data --out /home/ubuntu/result --snapshot_interval 10000"
}
変数名 説明
exec_name この実行の名前。バケット名にもなるため、アンダースコアを使わずkebab-case推奨
repository_url git cloneする対象のリポジトリURL
repository_name git cloneしたあと取得できるリポジトリ
data_dir データを格納するディレクト
output_dir 訓練結果等を格納するディレクト
data_get_command データを取得するなど、訓練開始前に実施する
exec_command 訓練実施コマンド

やってることの中身

Step Functionの定義

{
  "Comment" : "Machine learning execution with spot instance",
  "StartAt" : "CreateS3Bucket",
  "States"  : {
    "CreateS3Bucket": {
      "Type"      : "Task",
      "Resource"  : "arn:aws:lambda:ap-northeast-1:999999999999:function:create_s3_bucket",
      "Next"      : "RequestSpotInstance"
    },
    "RequestSpotInstance": {
      "Type"      : "Task",
      "Resource"  : "arn:aws:lambda:ap-northeast-1:999999999999:function:request_spot_instance",
      "Next"      : "WaitBidding"
    },
    "WaitBidding": {
      "Type"      : "Wait",
      "Seconds"   : 30,
      "Next"      : "CheckBiddingResult"
    },
    "CheckBiddingResult": {
      "Type"      : "Task",
      "Resource"  : "arn:aws:lambda:ap-northeast-1:999999999999:function:check_bidding_result",
      "Next": "ChoiceBiddingResult"
    },
    "ChoiceBiddingResult": {
      "Type" : "Choice",
      "Choices": [
        {
          "Variable": "$.request_result",
          "BooleanEquals": true,
          "Next": "NotifyRequestSuccess"
        },
        {
          "Variable": "$.request_result",
          "BooleanEquals": false,
          "Next": "NotifyRequestFailed"
        }
      ],
      "Default": "NotifyRequestFailed"
    },
    "NotifyRequestFailed": {
      "Type" : "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:999999999999:function:send_sms_message",
      "Next": "SpotRequestFailed"
    },
    "SpotRequestFailed": {
          "Type": "Fail",
          "Error": "SpotRequestError",
          "Cause": "Spot price bidding too low"
    },
    "NotifyRequestSuccess": {
      "Type" : "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:999999999999:function:send_sms_message",
      "Next": "WaitTaskComplete"
    },
    "WaitTaskComplete": {
      "Type"      : "Wait",
      "Seconds"   : 10,
      "Next"      : "CheckTaskCompleted"
    },
    "CheckTaskCompleted": {
      "Type" : "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:999999999999:function:check_task_completed",
      "Next": "ChoiceTaskCompleted"
    },
    "ChoiceTaskCompleted": {
      "Type" : "Choice",
      "Choices": [
        {
          "Variable": "$.task_completed",
          "BooleanEquals": true,
          "Next": "NotifyTaskCompleted"
        },
        {
          "Variable": "$.task_completed",
          "BooleanEquals": false,
          "Next": "WaitTaskComplete"
        }
      ],
      "Default": "WaitTaskComplete"
    },
    "NotifyTaskCompleted":{
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:999999999999:function:send_sms_message",
      "Next": "WaitInstanceDelete"
    },
    "WaitInstanceDelete": {
      "Type"      : "Wait",
      "Seconds"   : 1800,
      "Next"      : "DeleteSpotInstance"
    },
    "DeleteSpotInstance": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:999999999999:function:delete_ec2_instance",
      "End": true
    }
  }
}
  • 判断分岐以外は直列に流してるだけ。
  • 処理途中に生成されるID類はeventに追加しながら下流に流す
  • S3作成とスポットインスタンスリクエストはParallelにしても良いかも(面倒くさいのでやってない..)
  • 訓練完了から30分は削除せずに待つ。サーバに未練があればこの間に実行を停止する。

S3バケット作成

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import boto3
import json
import os

def lookup(s3, bucket_name):
  try:
    s3.meta.client.head_bucket(Bucket=bucket_name)
  except botocore.exceptions.ClientError as e:
    error_code = int(e.response['Error']['Code'])

    if error_code == 404:
      return False

    return True

def create_bucket(bucket_name):
    s3 = boto3.resource('s3')
    response = ''
    if not lookup(s3, bucket_name):
       response = s3.create_bucket(Bucket=bucket_name)

    return response

def lambda_handler(event, context):
    response = create_bucket(event['exec_name'])
    return event
  • eventからexec_nameを取り出してバケット名に
  • その名前のバケットがなければ作る

スポットインスタンスのリクエスト

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import boto3
import json
import logging
import base64
import os

SPOT_PRICE = '0.8'
REGION = 'ap-northeast-1'
AMI_ID = 'ami-9999999f'
KEY_NAME = 'your_keyname'
INSTANCE_TYPE = 'g2.2xlarge'
SECURITY_GRUOP_ID = ['sg-9999999']

def request_spot_instance(user_data):
    ec2_client = boto3.client('ec2',
        region_name = REGION
    )
    response = ec2_client.request_spot_instances(
        SpotPrice = SPOT_PRICE,
        Type = 'one-time',
        LaunchSpecification = {
            'ImageId': AMI_ID,
            'KeyName': KEY_NAME,
            'InstanceType': INSTANCE_TYPE,
            'UserData': user_data,
            'Placement':{},
            'SecurityGroupIds': SECURITY_GRUOP_ID
        }
    )
    return response

def lambda_handler(event, context):
    REPOSITORY_URL  = event["repository_url"]
    REPOSITORY_NAME = event["repository_name"]
    BUCKET_NAME = event["exec_name"]

    shell='''#!/bin/sh
    sudo -s ubuntu
    cd /home/ubuntu
    sudo -u ubuntu mkdir /home/ubuntu/.aws
    sudo -u ubuntu mkdir /home/ubuntu/completed
    sudo -u ubuntu git clone {5}
    sudo -u ubuntu mkdir {0}
    sudo -u ubuntu mkdir {1}

    sudo -u ubuntu echo "[default]" >> /home/ubuntu/.aws/credentials
    sudo -u ubuntu echo "aws_access_key_id={2}" >> /home/ubuntu/.aws/credentials
    sudo -u ubuntu echo "aws_secret_access_key={3}" >> /home/ubuntu/.aws/credentials

    sudo -u ubuntu echo "*/5 * * * * /home/ubuntu/.pyenv/shims/aws s3 sync {1} s3://{4} > /dev/null 2>&1" >> mycron
    sudo -u ubuntu echo "*/1 * * * * /home/ubuntu/.pyenv/shims/aws s3 cp {1}/log s3://{4} > /dev/null 2>&1" >> mycron
    sudo -u ubuntu echo "*/1 * * * * /home/ubuntu/.pyenv/shims/aws s3 cp /home/ubuntu/trace.log s3://{4} > /dev/null 2>&1" >> mycron
    sudo -u ubuntu echo "*/1 * * * * /home/ubuntu/.pyenv/shims/aws s3 sync /home/ubuntu/completed s3://{4} > /dev/null 2>&1" >> mycron

    sudo -u ubuntu /usr/bin/crontab mycron
    sudo -u ubuntu /bin/rm /home/ubuntu/mycron

    PATH="/usr/local/cuda/bin:$PATH"
    LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"

    sudo -u ubuntu cd /home/ubuntu/{6}

    sudo -u ubuntu touch trace.log
    sudo -u ubuntu echo `pwd` >> trace.log  2>&1
    sudo -u ubuntu echo `which python` >> trace.log  2>&1
    sudo -u ubuntu echo 'repository_name: {6}' >> trace.log 2>&1
    sudo -u ubuntu echo 'dataget_command: {7}' >> trace.log 2>&1
    sudo -u ubuntu echo 'exec_command: {8}' >> trace.log 2>&1
    sudo -u ubuntu {7}  > /dev/null 2>> trace.log
    sudo -u ubuntu echo `ls /home/ubuntu/data | wc` >> trace.log

    PATH="/usr/local/cuda/bin:$PATH"
    LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
    sudo -u ubuntu -i {8}  >> trace.log 2>&1
    sudo -u ubuntu touch /home/ubuntu/completed/completed.log
    '''

    shell_code = shell.format(
        event["data_dir"],
        event["output_dir"],
        os.environ.get('S3_ACCESS_KEY_ID'),
        os.environ.get('S3_SECRET_ACCESS_KEY'),
        event["exec_name"],
        event["repository_url"],
        event["repository_name"],
        event["data_get_command"],
        event["exec_command"]
        )
    user_data = base64.encodestring(shell_code.encode('utf-8')).decode('ascii')
    response = request_spot_instance(user_data)
    event["spot_instance_request_id"] = response["SpotInstanceRequests"][0]["SpotInstanceRequestId"]
    return event
  • インスタンスタイプや入札価格は定数にして、StepFunction実行時の入力(event)からは引かないようにしている(eventはコードの実行条件のみにし、環境調達条件はLambda側に持たせるポリシーのつもり)
  • AMIは、chainer、CUDA等はインストール完了いているものがある前提
  • インスタンスをリクエストしたあとuser_dataをシェルスクリプトにして流し込んでる
  • 大体の汚い処理はここのシェルスクリプトに凝縮されている
    • S3へのupload系タスクはcronに登録
    • その後、パスを通して訓練の開始
  • S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEYはIAMのwrite権限のある鍵をLambda Functionの環境変数に登録しておく。
  • 実行時のログはtrace.logに出力 > これもS3に随時Up
  • 実行完了後に、completed.logを出力。これがS3のバケットに入ると、StepFunctions側でタスク完了とみなされる

入札結果確認

def check_bidding_result(spot_instance_request_id):
    ec2_client = boto3.client('ec2',
        region_name = REGION
    )
    response = ec2_client.describe_spot_instance_requests(
      SpotInstanceRequestIds = [spot_instance_request_id]
    )
    return response

def lambda_handler(event, context):
    response = check_bidding_result(event["spot_instance_request_id"])
    event["request_result"] = (response['SpotInstanceRequests'][0]['Status']['Code']==u'fulfilled')

    if event["request_result"]:
        event["instance_id"] = response['SpotInstanceRequests'][0]['InstanceId']

    return event
  • スポットインスタンスリクエスト時に取得した'SpotInstanceRequests'から、入札の結果を確認する

通知

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import boto3
import json
import os

TOPIC_ARN = 'arn:aws:sns:ap-northeast-1:9999999999:training_end_notification_mail' # Mail
REGION = 'ap-northeast-1'


def send_sms_message(event, context):
    sns = boto3.client('sns',
        region_name = REGION
    )

    message = ''
    subject = ''
    if "completed" in event:
        subject = 'Training ended'
        message = '''task completed!
        result: https://console.aws.amazon.com/s3/home?bucket={0}

        -----
        {1}
        '''.format(event["exec_name"], event)
    else:
        if event["request_result"]:
            subject = 'request fulfilled'
            message = '''
            Spot Request Fulfilled! {0}
            '''.format(event["exec_name"])
        else:
            subject = 'request failed'
            message = '''
            Spot Request Fails! {0}
            '''.format(event["exec_name"])

        response = sns.publish(
            TopicArn = TOPIC_ARN,
            Subject = subject,
            Message = message
        )

    return response

def lambda_handler(event, context):
    response = send_sms_message(event, context)
    return event
  • 通知の宛先、通知手段は、事前にSNS側に登録し、Topic ARNを発行しておく
  • 作成されたインスタンスのIDはeventに追加して下流に流す

その他のLambda

  • あとは特別なことはしていないリポジトリをご参照ください

リポジトリ

github.com

改良案とか、◯◯をXXでやらないなんて有りえない!とかあればお気軽に @mizti までコメントください (AWS今までちゃんと触ってこなかった勢なので話せれば嬉しいです)

f:id:mizti:20170101205025p:plain

生成モデルpix2pixを動かしてみる

pix2pix is 何

  • 2016年11月に発表された、任意の画像を入力にして、それを何らかの形で加工して出力する、というある種の条件付きGAN。
  • GANって何: 画像等のデータ入力を真似て偽造する生成器と、そのデータが生成されたものか本物かを識別する鑑別器を互いに競わせるように訓練することで、本物によく似たなデータを作り出せるようにする「生成モデルおよびその訓練手法」

    • 2014年に公表された当初は訓練の不安定性の問題が大きかったが、Batch Normalizationの導入や条件付けなど安定を増すノウハウが蓄積され、ここ2年注目を浴びている。
    • 先週GAN / DCGANについてまとめてみたエントリを書いたので参考になるかも
  • 「画像を入力にして画像を出力にする」ようなタスクは世の中に無数に存在していて、その潜在的な適用範囲の広さが特徴。(着彩、塗り分け、単純化etc..)

  • 下記はpix2pixサイトのサンプル。 f:id:mizti:20161217233325p:plain

  • より詳細なサンプル例は下記で見られる

pix2pixの構成

  • 大きく3つのネットワークから構成される:

    • Encoder: 画像の畳み込みにより、入力画像の特徴量を圧縮する。
    • Decoder: Encoderで圧縮された特徴量を逆畳み込み(転置畳み込み)x6層したあと畳み込みx1層により画像に変換する
    • Discriminator: 「入力画像」と「真の出力サンプルまたはDecoderの出力」2つの画像入力を行う、「本物らしさ」を出力する

    • (追記) Decoder  n - i 層は、直前の層の他に  i 層のEncoderの出力も同時に受け取る(U-Netというらしい)

    • (追記) Discriminatorは、"Patch GAN"と名付けられているようだけどちゃんと読み解けてないです...
  • 3つのネットワークの損失関数:

    • Encoder: 「生成された偽画像と真の画像の差異」と「Enc->Decが出力した画像がDiscriminatorに偽物と思われた度合いのsoftplus->バッチ・画素数平均」の重み付き和
    • Decoder: Encoderの損失関数と同じ
    • Discriminator: 本物の画像(t_out)を偽物と判定した度合い(softplus->バッチ画素平均)と偽物の画像(x_out)を本物と判定した度合いの和

動かしてみる

例によってMattyaさんが例によって神速でChainer実装しているので、今日はそのまま動かしてみる。

github.com

  1. git cloneする
  2. 訓練用データとなるFacadeという様々な建物の前面部分写真/構成情報のデータを落としておく。 CMP Facade Database
  3. データの場所、出力の場所、処理するGPU番号等をオプションで指定し起動する

と、たったこれだけ。 (前提環境を整えるのは先回のエントリの記載内容。ただし、chainerは1.19以上が必要)

  • 訓練条件は、デフォルトのまま
    • ミニバッチサイズ1
    • 300枚の画像セットについてランダムな順番で300イテレーションで1エポック
    • エポック数200で全訓練完了

結果

入力画像 / 生成画像 / 正解画像

f:id:mizti:20161218015605p:plain:w180 f:id:mizti:20161218015414p:plain:w180 f:id:mizti:20161218015423p:plain:w180

入力画像 / 生成画像 / 正解画像

f:id:mizti:20161218015658p:plain:w180 f:id:mizti:20161218015712p:plain:w180 f:id:mizti:20161218015706p:plain:w180

入力画像 / 生成画像 / 正解画像

f:id:mizti:20161218015817p:plain:w180 f:id:mizti:20161218015825p:plain:w180 f:id:mizti:20161218015838p:plain:w180

入力画像 / 生成画像 / 正解画像

f:id:mizti:20161218015923p:plain:w180 f:id:mizti:20161218015933p:plain:w180 f:id:mizti:20161218015936p:plain:w180

入力画像 / 生成画像 / 正解画像

f:id:mizti:20161218020413p:plain:w180 f:id:mizti:20161218020017p:plain:w180 f:id:mizti:20161218020019p:plain:w180

(生成し損ねた..)

  • 損失関数の推移

f:id:mizti:20161218113548p:plain

思ったこととか

  • 1万回目くらいで既に建物にしか見えない画像を生成できるようになってる。まずこれがすごい

  • Enc / Decの損失関数って、「正解画像との一致度」「Discriminator騙せた度合い」両方損失として使えそうだけど どうやるんだろう... > からの「重みづけして足すだけ」という分かりやすさ。この重み変えると学習結果にどう影響するのか興味ある。 きっと重要なハイパーパラメータ。

  • 回が進むにつれて、画像による明度差が生まれやすくなっている。

    • 色相の差が生まれづらいのは、入力画像と建物色の間にはっきりした相関関係が無いから?
      • じゃ何で明度差を生み出せたんだろう(?)
  • まっすぐあるべき線がまっすぐにならないのは他の生成手法でもよくある。(ニューラルネットワークって「まっすぐな線」引くの苦手ですよね...)生成画像の黒ずみと相俟ってスラムみたいな印象に...

  • 損失関数の推移だけみると、まだ学習が収束してないのでepoch数を増やしても良さそう

  • Mattyaさんの実装で使われていた「CBR層」(畳み込み/BN/ReLU/Dropoutをセットにした層)が便利。

    • up / down指定するだけで畳み込み / 逆畳み込み両方に使える(!)

新しいタスクへの適用

  • 新しいデータセットにしたいと思って、pixivからイラスト拾い集めてみている(やっと500枚集まった)。うまくいけば着彩タスクを作れるかもしれない
    • 未着彩の線画提供 / 着彩させてblogに載せさせてやってもいいぜっていう絵師さんいませんか

Numpy逆引きメモ

  • Numpyでよく使う操作のメモ。自分用、随時追記

生成

リストで指定した内容の行列を生成

>>> x = np.array([[0,1],[3,4]])
>>> x
array([[0, 1],
       [3, 4]])

numpy行列からlistへの変換

>>> x = np.array([3,4,5])
>>> x
array([3, 4, 5])
>>> list(x)
[3, 4, 5]

2次元配列で同じことをすると

>>> x
array([[0, 1],
       [3, 4]])
>>>
>>> list(x)
[array([0, 1]), array([3, 4])]

となる。もし多重リスト形式に変換したければ、

>>> x = np.array([[0,1],[3,4]])
>>> x.tolist()
[[0, 1], [3, 4]]
>>> x_list = x.tolist()
>>> x_list
[[0, 1], [3, 4]]

1で埋めた行列を生成

>>> np.ones([2,3])
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.]])

0で埋めた行列を生成

>>> np.zeros([2,3])
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.]])

対角行列の生成

>>> np.eye(4)
array([[ 1.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.],
       [ 0.,  0.,  0.,  1.]])

必ず正方行列になる。

ランダム要素で埋めた行列を生成

一様分布

np.random.uniform(2, -3, (5, 4))

上記の意味: 2から-3の間の値で埋めた5, 4の行列を生成する (4つの値が入ったリストが5つ入っているリスト)

指定区間の数列を生成する

>>> X = np.arange(10)
>>> X
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Stepや始点・終点も指定可

>>> X = np.arange(8,20,2)
>>> X
array([ 8, 10, 12, 14, 16, 18])

指定数がStepの倍数でない場合

>>> X = np.arange(70,40,-11)
>>> X
array([70, 59, 48])

型指定も可

>>> X = np.arange(10.,dtype=np.float32)
>>> X
array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.], dtype=float32)

情報取得

行列の要素数を取得する

>>> h = np.array([2,3,4])
>>> h.size
3
>>> h = np.array([[1,2,3],[4,5,6]])
>>> h.size
6

行列の形状を取得する

>>> h = np.array([[1,2,3],[4,5,6]])
>>> h.shape
(2, 3)

行列の次元数を取得する

>>> h = np.array([[1,2,3],[4,5,6]])
>>> h.ndim
2

行列のrankを取得する

>>> A
array([[1, 2],
       [3, 4]])
>>> np.linalg.matrix_rank(A)
2

形の一致する二つの行列で値が一致する数をカウントする

>>> y=np.array([1,1,2,3,7])
>>> t=np.array([1,2,3,4,7])
>>>
>>> np.sum(y==t)
2

行列から最大値を抽出する

>>> hoge = np.array([[3,4,5],[2,3,4]])
>>> hoge.__class__
<class 'numpy.ndarray'>
>>> np.max(hoge)
5

行列から最大値のindexを取り出す

>>> hoge = np.ndarray([[3,4,5],[2,3,4]])
>>> np.argmax(hoge)
2

軸を指定してsumの最大値を取り出す

>>> A = np.array([[3,4,5],[2,3,4]])
>>> np.max(A, axis=0)
array([3, 4, 5])
>>> np.max(A, axis=1)
array([5, 4])

特定の条件を満たす要素のindexを取得する

>>> a
array([ 1,  3,  4,  2,  5,  8, 10])
>>> np.where(a>3)
(array([2, 4, 5, 6]),)
>>>
>>> np.where(a==3)
(array([1]),)

形状操作

行列の次元数を追加する

>>> h = np.array([1,2,3])
>>> h
array([1, 2, 3])
>>>
>>> h = h[np.newaxis,:]
>>> h
array([[1, 2, 3]])

>>> h = h[:, np.newaxis]
>>> h
array([[1],
       [2],
       [3]])

行列を結合する(連結する)

>>> A
array([[1, 2],
       [3, 4]])
>>> B
array([5, 6])

のとき、

縦に連結

>>> np.r_[A,B]
array([[1, 2],
       [3, 4],
       [5, 6],
       [7, 8]])

横に連結

>>> np.c_[A,B]
array([[1, 2, 5, 6],
       [3, 4, 7, 8]])

Flattenする

>>> a = np.array([[1,2], [3,4]])
>>> a.flatten()
array([1, 2, 3, 4])

行列を切り落とす(y軸)

>>> h
array([[1, 2, 3],
       [3, 4, 5],
       [4, 5, 6]])
>>> h[1:]
array([[3, 4, 5],
       [4, 5, 6]])
>>> h[:1]
array([[1, 2, 3]])

行列を切り落とす(x軸)

>>> h
array([[1, 2, 3],
       [3, 4, 5],
       [4, 5, 6]])

>>> h[:,1:]
array([[2, 3],
       [4, 5],
       [5, 6]])

>>> h[:,:2]
array([[1, 2],
       [3, 4],
       [4, 5]])

軸操作

多重リスト要素の最大値のインデックスをリストで返す

>>> y = [[1,2,3],[5,6,7]]
>>> t = [[0,0,1],[0,1,0]]
>>> np.argmax(y, axis=1)
>>> y = np.argmax(y, axis=1)
>>>y
array([2, 2])
>>> t = np.argmax(t, axis=1)
>>> t
array([2, 1])

多次元行列から特定次元だけ抜き出す

data = [
[1           ,0.4834],
[2           ,0.5526],
[3           ,0.6076],
[4           ,0.5436],
...
[299         ,0.9718],
[300         ,0.97]
]

という2次元行列があるときに、

[1, 2, 3, ... 299, 300]

を抜き出すには

data[: , 0]
[0.4834, 0.5526, 0.6076, ... 0.97]

を抜き出すには

data[:, 1]

とすればよい

他の例:

>>> y
array([[0, 1],
       [2, 3],
       [4, 5]])

に対して、

>>> y[:,0]
array([0, 2, 4])
>>> y[:,1]
array([1, 3, 5])
>>> y[:,-1]
array([1, 3, 5])

(要素数2なので、前から数えて1番目と後ろから数えて-1番目は同じ)

要素の順番を入れ替える

>>> y
array([[0, 1],
       [2, 3],
       [4, 5]])
>>> y[:,::-1]
array([[1, 0],
       [3, 2],
       [5, 4]])

演算

行列の積を求める

>>> X = np.array([[2,3],[3,4]])
>>> Y = np.array([2,2])
>>> X.dot(Y)
array([10, 14])

"*"による演算は、要素同士の掛け算になるため注意。

>>> X = np.array([[2,3],[3,4]])
>>> Y = np.array([2,2])
>>> X*Y
array([[4, 6],
       [6, 8]])

行列要素の和を取る

>>> A
array([[1, 2],
       [3, 4]])
>>> np.sum(A)
10

線形代数的操作

行列を転置する

>>> h
array([[1, 2, 3],
       [3, 4, 5],
       [6, 7, 8]])
>>> np.transpose(h)
array([[1, 3, 6],
       [2, 4, 7],
       [3, 5, 8]])

もしくは

>>> h
array([[1, 2, 3],
       [3, 4, 5],
       [6, 7, 8]])
>>> h.transpose()
array([[1, 3, 6],
       [2, 4, 7],
       [3, 5, 8]])

transposeは、数式に似せて

>>> h.T
array([[1, 3, 6],
       [2, 4, 7],
       [3, 5, 8]])

と書くこともできる

多次元行列については、引数を与えなければx,y,z => z,y,xのように逆順になるが、 順番を与えることもできる

>>> x.transpose(0,1,2)
array([[[ 0,  1,  2],
        [ 3,  4,  5]],

       [[ 6,  7,  8],
        [ 9, 10, 11]]])
>>> x.transpose(0,2,1)
array([[[ 0,  3],
        [ 1,  4],
        [ 2,  5]],

       [[ 6,  9],
        [ 7, 10],
        [ 8, 11]]])
>>> x.transpose(2,0,1)
array([[[ 0,  3],
        [ 6,  9]],

       [[ 1,  4],
        [ 7, 10]],

       [[ 2,  5],
        [ 8, 11]]])

行列式を出力する

>>> A
array([[1, 2],
       [3, 4]])
>>> np.linalg.det(A)
-2.0000000000000004
>>> P
array([[2, 3],
       [4, 5]])
>>> np.linalg.det(P)
-2.0

行列のtraceを出力する

>>> A
array([[1, 2],
       [3, 4]])
>>> np.trace(A)
5 

行列の対角要素を抜き出す

>>> A
array([[1, 2],
       [3, 4]])

>>> np.diag(A)
array([1, 4])

np.diag(A)は、Aの対角要素を返す。

対角要素から対角行列を生成する

>>> a
array([ 1.,  2.,  3.])

>>> np.diag(a)
array([[ 1.,  0.,  0.],
       [ 0.,  2.,  0.],
       [ 0.,  0.,  3.]])

ある行列の固有値の対角行列を得る

>>> np.diag(np.linalg.eigvals(A))
array([[-0.37228132,  0.        ],
       [ 0.        ,  5.37228132]])

行列の固有値を求める

>>> A
array([[1, 2],
       [3, 4]])

>>> np.linalg.eigvals(A)
array([-0.37228132,  5.37228132])

行列の固有値固有ベクトルを求める

>>> A
array([[1, 2],
       [3, 4]])

>>> la, P = np.linalg.eig(A)
>>> la
array([-0.37228132,  5.37228132])
>>> P
array([[-0.82456484, -0.41597356],
       [ 0.56576746, -0.90937671]])

laが固有値、Pが固有ベクトル。 la[n]がP[n,:]に対応する。

行列の逆行列を作る

>>> np.linalg.inv(A)
array([[-2. ,  1. ],
       [ 1.5, -0.5]])

行列の対角化を行う

>>> A
array([[ 0, 14,  2],
       [-1,  9, -1],
       [-2,  4,  8]])

のとき、

>>> l, P = np.linalg.eig(A)
>>> l
array([ 4.,  6.,  7.])
>>> P
array([[  9.42809042e-01,  -9.12870929e-01,  -8.94427191e-01],
       [  2.35702260e-01,  -3.65148372e-01,  -4.47213595e-01],
       [  2.35702260e-01,  -1.82574186e-01,   2.25257672e-15]])

lが固有値、Pが固有ベクトルを組み合わせて作った行列。 l[n]がP[n,:]に対応する(nは0,1,2)

固有値を対角行列化する

>>> D = np.diag(l)
>>> D
array([[ 4.,  0.,  0.],
       [ 0.,  6.,  0.],
       [ 0.,  0.,  7.]])

PDP-1がAと一致することを確認

>>> P.dot(np.diag(l)).dot(np.linalg.inv(P))
array([[  9.79179023e-16,   1.40000000e+01,   2.00000000e+00],
       [ -1.00000000e+00,   9.00000000e+00,  -1.00000000e+00],
       [ -2.00000000e+00,   4.00000000e+00,   8.00000000e+00]])

ベクトルの内積を求める

>>> x
array([1, 2, 3])
>>> y
array([3, 1, 0])
>>> x.dot(y)
5

行列のノルムを求める

>>> x
array([1, 2, 3])
>>> np.linalg.norm(x)
3.7416573867739413

ベクトル同士のcos類似度を求める

内積を双方のノルムで割れば良いので

>>> x
array([1, 2, 3])
>>> y
array([3, 1, 0])
>>> x.dot(y) / (np.linalg.norm(x) * np.linalg.norm(y))
0.42257712736425829

その他よく使うイディオム

多次元配列の要素をイテレーションする

配列の要素のインデックスを順番に取得できる

>>> x
array([[0, 1],
       [3, 4]])
>>> it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
>>> it
<numpy.nditer object at 0x10a444c10>
>>>
>>> it.multi_index
(0, 0)
>>> it.iternext()
True
>>> it.multi_index
(0, 1)
>>> it.iternext()
True
>>> it.multi_index
(1, 0)
>>> it.iternext()
True
>>> it.multi_index
(1, 1)
>>> it.iternext()
False

行列要素の型を変換する

>>> A
array([[1, 2],
       [3, 4]])

>>> A.astype("float32")
array([[ 1.,  2.],
       [ 3.,  4.]], dtype=float32)

(わかりづらいが、小数点が付いてfloat型になっている)

map適用相当の関数適用

x = np.array([1, 2, 3, 4, 5])
squarer = lambda t: t ** 2
squares = np.array([squarer(xi) for xi in x])

printしたときに省略せずに表示する

np.set_printoptions(threshold=np.inf)

をコードのどこかで宣言したあとでprint

Chainer環境をAWSのUbuntu16.04+CUDA8.0上に構築する

目的

AWSUbuntu 16.04上にChainer環境を構築する

構成

  • OS: Ubuntu 16.04 (AMI ID: ami-0567c164)
  • CUDA 8.0
  • Cudnn8.0
  • Chainer 1.18

手順

cudaのインストール

$ wget https://developer.nvidia.com/compute/machine-learning/cudnn/secure/v5.1/prod/8.0/cudnn-8.0-linux-x64-v5.1-tgz
$ sudo dpkg -i cuda-repo-ubuntu1604_8.0.44-1_amd64.deb
$ sudo apt-get update
$ sudo apt-get install cuda

cudnnのインストール

agreementのせいでwgetで落とせないので、 https://developer.nvidia.com/rdp/cudnn-download から一旦PCに落としてscpでupした。

$ tar zxvf cudnn-8.0-linux-x64-v5.1.tgz

cudaディレクトリが解凍されるので、

$ sudo cp -r cuda/include/* /usr/local/cuda/include
$ sudo cp -r cuda/lib64/* /usr/local/cuda/lib64

として、既存の/usr/local/cuda配下に配置する

cudaへのパスを通す

/usr/local/cuda/bin と /usr/local/cuda/lib64 にパスを通すために、

export PATH="/usr/local/cuda/bin:$PATH"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"

を.bashrc (ログインシェル環境なら.bash_profileに)追記する

linux-genericのインストール

AWSのイメージには、GPUを動かすのに必要なlinux-genericがインストールされていない という情報があったため、実施。(やらなくても動くかも?未検証)

$ sudo apt install linux-generic
$ sudo reboot 
(再起動後)
$ sudo apt remove linux-virtual
$ sudo apt remove linux-virtual
$ sudo apt autoremove

pipのインストール / アップグレード

$ sudo apt install python-pip
$ pip install --upgrade pip

pyenvで必要になるので、opensslをインストール

$ sudo apt-get install libssl-dev

pyenv環境の構築

  • Chainer等のバージョンアップが今後多くなることを想定し、pyenv環境を作っておく。
$ cd ~
$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
  • 以下コマンドによって.bash_rc(もしくは.bash_profile)に追記し、読み込まれるようにしておく
echo 'export PYENV_ROOT="${HOME}/.pyenv"' >> ~/.bashrc
echo 'if [ -d "${PYENV_ROOT}" ]; then' >> ~/.bashrc
echo '    export PATH=${PYENV_ROOT}/bin:$PATH' >> ~/.bashrc
echo '    export PATH=${PYENV_ROOT}/shims:$PATH' >> ~/.bashrc
echo '    eval "$(pyenv init -)"' >> ~/.bashrc
echo 'fi' >> ~/.bashrc
  • その場でも読み込んでおく(再ログインでも可)
$ source ~/.bashrc

上記は、インタラクティブシェルの場合。ログインシェル環境なら.bash_profileに追記し、source ~/.bash_profileする

  • pyenv-virtualenvもインストールする
$ cd ~/.pyenv/plugins
$ git clone git://github.com/yyuu/pyenv-virtualenv.git
  • pyenvで利用するpythonをインストール(インストールできるものはpyenv install -l で確認)
$ pyenv install 3.5.2
  • インストール完了の確認
$ pyenv versions
* system (set by /home/ubuntu/.pyenv/version)
  3.5.2
  • pipもpyenv化されていることを確認する
$ which pip
/home/ubuntu/.pyenv/shims/pip

/home/ubuntu/.local/bin/pip となっていたらおそらくパス設定をミスしている。

  • 利用するpyenvを設定する
$ pyenv global 3.5.2
  • 念のためログインし直しても同じ状態をキープしているか確認しておく
$ pyenv versions
  system
* 3.5.2 (set by /home/ubuntu/.pyenv/version)

pip パッケージのインストール

下記の内容をrequirements.txtとして保存して、依存パッケージのインストール

appdirs==1.4.0
chainer==1.18.0
cupy==1.0.0
cycler==0.10.0
decorator==4.0.10
filelock==2.0.7
matplotlib==1.5.3
nose==1.3.7
numpy==1.11.2
Pillow==3.4.2
protobuf==3.1.0.post1
py==1.4.31
pyparsing==2.1.10
pytest==3.0.4
python-dateutil==2.6.0
pytools==2016.2.4
pytz==2016.7
six==1.10.0

Pillowとかは最初なくてもよいかも。

$ pip install -r requirements.txt

これで、3.5.2 env配下にpipがインストールされた。chainerのバージョンをあげたりした場合は、 envを消して別のenvを定義して再インストールすればよい。

動作確認

  • CUDAの動作確認を行う。
$ python
>>> import chainer
>>> chainer.cuda.available
True
>>> chainer.cuda.cudnn_enabled
True
  • 公式サンプルを落として、実際に動かしてみる。
$ git clone https://github.com/pfnet/chainer.git
$ cd chainer/examples/mnist/
$ python train_mnist.py -g=0
  • 実行できることを確認
  • 標準出力冒頭で表示されたGPUGPU: 0であることを確認

  • -g=0オプションありと無しで実行時間を比較してみた。

GPUなし:
real     12m44.315s
user     30m1.920s
sys     60m23.720s
GPUあり:
real     1m20.846s
user     1m22.468s
sys     0m1.708s

10倍速になっており、確かにCUDAによる高速化が実現できている。

お掃除

不要なディレクトリやファイルを削除する

$ cd ~
$ rm cuda-repo-ubuntu1604_8.0.44-1_amd64.deb
$ rm cudnn-8.0-linux-x64-v5.1.tgz
$ rm -r cuda
$ rm requirements.txt

経緯:

AWSGPU演算環境を確保しようと思い、適切なAMIを探していた。CUDAが最初からインストールされたAMIを使おうとしたらスポットリクエストには対応していなかったため、デフォルトのUbuntuでAMIを作成するのが結局割安に付きそうだったためクリーンインストールしてマイAMIを作成した

できるだけ丁寧にGANとDCGANを理解する

目的

  • Chainerの扱いに慣れてきたので、ニューラルネットワークを使った画像生成に手を出してみたい
  • いろいろな手法が提案されているが、まずは今年始めに話題になったDCGANを実際に試してみるたい
  • そのために、 DCGANをできるだけ丁寧に理解することがこのエントリの目的
  • 将来GAN / DCGANを触る人の助けになったり、理解間違ってるところにツッコミがあると嬉しい

本エントリの構成

  1. DCGANの前提となっているGANの論文の要点をまとめる
  2. DCGANでGANとの差分として提案されている要点をまとめる
  3. DCGANのmattyaさんの実装を読み通して詳細を理解する

1. GANについて

  • GANは、サンプル群と似たような性質を持つ出力を生成するためのフレームワーク
  • 2014年にIan J. Goodfellowらによって提案された
  • 論文: Generative Adversarial Nets [リンク]

  • 以下の2つのモデルの訓練を同時に進め、互いに競わせる

    • D: Discriminator(鑑別器): (生成したいサンプルとGの出力物を正しく鑑別できることを目指す)
    • G: Generator(生成器):(ランダムノイズを入力として、Dが誤ってサンプルであると認識する率を高めることを目指す)
  • GANは下記の式の価値関数V(G, D)で表現されるminimaxゲームとして定義できる
 
\begin{eqnarray} 
\min_{G}\max_{D} V(D, G) =  \mathbb{E}_{x \sim p_{data}(x)}\big[\log D(x)\big] +  \mathbb{E}_{x \sim p_{z}(x)}\big[\log (1-D(G(z)))\big]
\end{eqnarray} 

  • この式を、前提含め日本語で書き下してみると
[前提]
・ p_zはある種の任意の分布(例えば一様分布)を表す。zは個々のノイズサンプルを表す。
・Gはzを入力とし、 p_gに分布させる。 p_gはジェネレータGから生成された出力の分布を示す
・Dは入力がサンプルから来た確率を表す(1であれば入力はサンプル分布 p_xから、0であればGの出力分布 p_gからと判断)
[左辺]
・価値関数Vは関数DとGを引数に取る、右辺で表される関数である
・Dについての最大、Gについての最小となるようD, Gを定める
[右辺]
・確率変数xは確率分布 p_{data}に従う
・確率変数zは確率分布 p_zに従う
・このとき、 log D(x)の期待値と、 log(1-D(G(z)))の期待値の和を評価関数とする
(Dがサンプルを正しくサンプルと判定できれば log D(x)が大きくなり、DがGの出力をサンプルだと判定すると log(1-D(G(z)))が小さくなる)

となる。(と思うのですが、間違っていたらご指摘いただければ嬉しいです..)

  • この式自体は、G、Dがニューラルネットワークであることを前提とはしていない(言い換えれば、別な関数最適化手法であっても適用できる、かもしれない)

  • 論文ではD, Gにニューラルネットワークを使うことで、既存の最尤推定による生成モデルで手に負えないほど計算量が増える問題をbackpropagationで回避できるとしている

  • 論文掲載のアルゴリズムは下記となる

    1. ミニバッチサイズm個のノイズ {z_1}...{z_m} p_g(z)から取り出す(生成する)
      (論文は p_gからになってるけど、 p_zの誤植のような..?)
    2. ミニバッチサイズm個のサンプル {x_1}...{x_m}をデータ生成分布 p_{data}(x)から取り出す
    3. 下記式の、  \theta_d における確率的勾配を上るように鑑別器Dを更新する
       \begin{eqnarray}   \frac{1}{m}\sum_{i=0}^m \big(\log D(x^{(i)}) + \big(\log (1 - D(G(z^{(i)} ))) \big) \end{eqnarray}
    4. 上記までをk回繰り返す
    5. ミニバッチサイズm個のノイズ {z_1}...{z_m} p_g(z)から取り出す (ここも p_zが正しいような..?)
    6. 下記式の、 \theta_g における確率的勾配を下るように生成器Gを更新する
       \begin{eqnarray}   \frac{1}{m}\sum_{i=0}^m \log (1 - D(G(z^{(i)} ))) \end{eqnarray}
    7. ここまで全てを、訓練回数分だけ繰り返す
  • 鑑別器Dを十分な回数(k回)更新した上で生成器Gを1回更新することで、常に鑑別器が新しいGの状態に適用できるように学習を進める

  • 4.1 ~ 4.2 [tex: p_g = p{data} ] の時にD, Gそれぞれについての最適化が達成される
    ==> このため、 p_g を [tex: p
    {data} ] に近似させることが上記評価関数の解への近似として正当化される

  • 利点と欠点

    • 欠点: 明示的な p_g が最初は存在せず、DはGとシンクロさせて訓練しなければならない (特に、DをupdateせずにGだけを訓練すると、Gが入力ノイズzの多くをxと同じ値に収束させてしまう点に注意)
    • 利点:
      • マルコフ鎖で複数のモデルを混ぜるためにぼやけたものになるが、GANではマルコフ鎖が不要でシャープな画像が生成できる。
      • 勾配を得るためにBPが使えるため、学習に近似が不要。
      • 様々なモデルを用いることができる
      • そして何より、「計算可能(computational)である」。
      • サンプルと直接比較するのではなく、Discriminatorの評価を介して生成するため、inputの部品をGがそのまま丸覚えすることを避けられる。

2. DCGANについて

  • GANは具体的なネットワークの構成に言及していない。(少なくとも論文中では)
  • DCGAN(Deep Convolutional Generative Adversarial Networks) は、GANに対して畳み込みニューラルネットワークを適用して、うまく学習が成立するベストプラクティスについて提案したもの。
  • 元になった論文 Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks リンク
  • 要点をまとめると下記のようになる
    • プーリング層を全て下記で置き換える
      • D(鑑別器): 畳み込み層(strided convolutions)(これはいわゆる普通の畳み込み層のことらしい)
      • G(生成器): 分数的にストライドする畳み込み層 (fractional-strieded convolutions)(これはすなわちdeconvolution a.k.a. transposed convolutionsのことらしい...)
    • バッチノルムを使用する(重要らしい)
    • 深い構成では全結合層を除去する
    • 生成器ではReLUを出力層以外の全ての層で活性化関数として使用し、出力層ではtanhを使用する
    • 識別器ではLeakyReLUを全ての層で使用する

3. DCGANのコードを読む

前処理

  • import / 定数宣言は解説省略
  • image_dirから全てのイメージを読み込んで、dataset配列に追加している。
  • ELU: exponential Linear Unitの定義。 数式でいうと \alpha > 0の元で
 
\begin{eqnarray} 

f(x) = \left\{
\begin{array}{ll}
x  & \text{if }  x > 0 \\
\alpha(\exp(x)-1) & \text{if } x \leq 0
\end{array}
\right.
,
f'(x) = \left\{
\begin{array}{ll}
1  & \text{if }  x > 0 \\
f(x)+\alpha & \text{if } x \leq 0
\end{array}
\right.

\end{eqnarray} 

グラフでいうと

f:id:mizti:20161210164101p:plain (出典: Djork-Arne Clevert, Thomas Unterthiner & Sepp Hochreiter 2016 https://arxiv.org/pdf/1511.07289v5.pdf )

となるもので、LeakyReLUをなめらかにした感じのものらしい。

Generatorの定義:

  • 一様分布ノイズz(1次元 / 100要素)を入力として、
    => 100入力、66512 出力のLinear Unit で(100, 512, 6, 6 )に=> BatchNormalization => relu => => 512チャネル入力、256チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D で(100, 256, 12, 12)に=> BN => relu
    => 256チャネル入力、128チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D で(100, 128, 24, 24)に=> BN => relu
    => 128チャネル入力、64チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D で(100, 64, 48, 48)に=> BN => relu
    => 64チャネル入力、3チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D
    として、最終的に(100, 3, 96, 96) (つまり、ミニバッチ100枚 x RGB3チャネル x w96 x h96)のデータが得られる。

Discriminatorの定義:

  • サンプルイメージ or Gの出力(3, 96, 96)を入力として、
    => 3チャネル入力、64チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで (64, 48, 48)に => 上述のELU
    => 64チャネル入力、128チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで(128, 24, 24)に => BN => ELU
    => 128チャネル入力、256チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで(256, 12, 12)に => BN => ELU
    => 256チャネル入力、512チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで(516, 6, 6)に => BN => ELU
    => 512 x 6 x 6 入力、2出力の全結合層

訓練

  • Gの訓練
    • Gの訓練時の入力zは、-1から1の間で一様分布の (100 x 100 )二次元のランダム値
    • Gからの出力xをDの入力とする
    • Dの出力ylは「Dが入力をGのものだと思う確率」を示している(元論文とは逆に設定されてるので注意)  すなわち、Dの出力が0であればデータセットからの出力、1であればGが"偽造"した出力と判断
    • Gの損失: ylとbatch数だけ並んだ0とのソフトマックスクロスエントロピー関数出力(ylが全て0であれば損失なし)
    • Dの損失: ylとbatch数だけ並んだ1とのソフトマックスクロスエントロピー関数出力(ylが全て1であれば損失なし)
  • Dの訓練
    • x2をDの入力にして、yl2を出力。
    • Dの損失に「yl2とbatch数だけ並んだ0とのソフトマックスクロスエントロピー関数出力」を足し合わせる
  • 勾配初期化とbackpropagateによる重み更新
  • image_save_interval回の訓練毎にGENを使って画像を100枚。
    この際の生成のタネにする乱数は訓練開始前に生成したzvisを常に利用する
  • 毎epoch完了ごとにdis, gen, o_dis, o_genを保存

わからなかった点

  • 何故Discriminatorが「2つ」の出力を持つようにしているのかわからない。入力の p_{data}かららしさだけを出力するのなら、1出力でよさそうに思えてしまう。損失関数の計算方法から見るに、どちらか1つの要素がG、もう一つがdataからの入力というわけでもなさそうに思える。

その他

Chainerでフォントを識別してみる (2)

前回、二つのフォントをいい感じに識別できていたので、識別対象フォントを12に増やしてみた。

今回識別対象にしたフォントたち

Helvetica
f:id:mizti:20161204123136p:plain
Bodoni
f:id:mizti:20161126200655p:plain
Futura
f:id:mizti:20161204123102p:plain
Optima
f:id:mizti:20161204123045p:plain
Clarendon
f:id:mizti:20161204123149p:plain
GillSans
f:id:mizti:20161204123203p:plain
Caslon
f:id:mizti:20161204123216p:plain
Garamond
f:id:mizti:20161204123227p:plain
Avenir
f:id:mizti:20161204123240p:plain
Times New Roman
f:id:mizti:20161204123251p:plain
American Typewriter
f:id:mizti:20161204123304p:plain
Trajan
f:id:mizti:20161204123322p:plain

訓練データとテストデータの構成

  • 訓練データは[a-mA-M]の64x64 グレースケール画像。画像ごとに位置はランダム
  • テストデータは[n-zN-Z]の64x64 グレースケール画像。同じく画像ごとに位置はランダム化してある

実験

試行1: 3層多層パーセプトロン(隠れ層ニューロン数100)

前回使ったのと々、隠れ層2層、出力層1層の多層パーセプトロン

=> 87%程度で識別率が頭打ちになった。2クラスへの識別だけであれば、同条件で97%まで行っていたので、
12クラスへの識別になって認識率が落ちたことが確認できる。

試行2: 3層多層パーセプトロン(隠れ層ニューロン数200)

2つの隠れ層の表現能力が足りないのかもしれないと思い、ニューロン数を200に増やしてみたパターン。

=> 90%程度で頭打ち。試行1よりかすかに識別率が上がった

試行3: CNN

CNN(畳み込みニューラルネットワーク)を導入。畳み込み+relu+Max Poolingを3セットの後にLinear層を2層。
epoch数100を超えたあたりで識別率が99.9%以上となった。

f:id:mizti:20161204134938p:plain

今後

・十分な汎化性能がありそうなので、多種多様な日本語フォントの識別を行ってみる
・各フォントにはItalicやBoldなどいろいろなスタイルもあるため、スタイル変更した際にも識別を行えるか試す
・スタイル転写とか使って、英字フォントから日本語フォント作れたら面白い気がする

Chainerでフォントを識別してみる (1)

Chainerで何か分類してみるにあたって、

1. 分類できて嬉しい
2. サンプル収集の目処が立ちそう
3. 莫大なデータ量にならない

な分類対象として、文字のフォントの分類をしてみようと考えた。

1. 分類対象クラス

分類クラス数は今後増やしていくとして、まずはセリフ体とサンセリフ体一種類ずつ、BodoniとHelveticaを分類してみることにした。

Bodoni

f:id:mizti:20161126200655p:plain

Helvetica

f:id:mizti:20161126200706p:plain

2. データセットの作成
  • 大文字もしくは小文字を一文字ずつ64*64の画像に印字したデータを作成する。これより画像が小さいと、ジャギが立ってしまうので、フォント判別に良く無いと判断した
  • 用意するデータ毎にx位置y位置をずらしておく。(フォントによって、ベースライン位置が異なっていたりするため、これを手掛かりにさせず、あくまで字形で分類させたいため)
  • Train用のデータセットでは[a-mA-M]、Test用のデータセットでは[n-zN-Z]の文字を用いる

train用
f:id:mizti:20161126180043p:plain
test用
f:id:mizti:20161126180016p:plain
(※データをちゃんと作れたか確認のため画像出力したが、実際にはメモリ上でリスト保持するのみ)

  • ChainerのOptimizer、Trainerを用いる事ができるように、DatasetMixinを継承したクラスとしてデータセットクラスを作成した。

(DataMixinを継承したクラスで__len__とget_exampleだけ定義してあげればChainerのIterationに渡せるようになる)

  • Datasetのコンストラクト引数で、サンプル数とtrain / testの指定を行えるようにした
import sys
import random
import numpy as np
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
import chainer
from chainer import cuda, Function, gradient_check, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions

class FontImageDataset(chainer.dataset.DatasetMixin):
	def __init__(self, datanum=10, normalize=True, flatten=True, train=True):
		self._normalize = normalize
		self._flatten = flatten
		self._train = train
		pairs = []
		for _ in range(datanum):
			image_array, label = self.generate_image()
			pairs.append([image_array, label])
		self._pairs = pairs

	def __len__(self):
		return len(self._pairs)

	def generate_image(self):
		fonts = [
		    'font_files/Helvetica.ttf',
		    'font_files/BodoniSvtyTwoITCTT-Book.ttf',
			'font_files/Futura-Medium.ttf',
			'font_files/Optima-Regular.ttf'
		]
		label = random.randint(0,len(fonts)-1)
		fontFile = fonts[label]
		font = ImageFont.truetype(fontFile, 60)

		train_characters = [
			'A','B','C','D','E','F','G','H','I','J','K','L','M',
			'a','b','c','d','e','f','g','h','i','j','k','l','m'
		]
		test_characters  = [
			'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
			'n','o','p','q','r','s','t','u','v','w','x','y','z'
		]
		text = ''

		if self._train:
			text = random.choice(train_characters)
		else:
			text = random.choice(test_characters)

		w, h = 64, 64
		text_w, text_h = font.getsize(text)
		text_x, text_y = (w - text_w) * random.random(), (h - text_h) * random.random()

		im = Image.new('L', (w, h), 255)
		draw = ImageDraw.Draw(im)
		draw.text((text_x, text_y), text, fill=(0), font=font)
		#im.save('image' + str(random.randint(0, 100)) + '.png')
		#if self._train:
		#	im.save('temp/image_train' + str(random.randint(0, 100)) + '.png')
		#else:
		#	im.save('temp/image_test' + str(random.randint(0, 100)) + '.png')

		image_array = np.asarray(im)

		if self._normalize:
		    image_array = image_array / np.max(image_array)

		if self._flatten:
			image_array = image_array.flatten()

		# type cast
		image_array = image_array.astype('float32')
		label = np.int32(label)
		return image_array, label

	def get_example(self, i):
		image_array, label = self._pairs[i][0], self._pairs[i][1]
		return image_array, label
3. 学習モデルと訓練

ほとんどまだチュートリアルのまま回した感じ。これから分類クラス数を増やしたり
モデルを差し替えたりハイパーパラメータをあれこれして楽しみたい

import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions
from font_image_dataset import *

train_data = FontImageDataset(5000, train=True)
test_data = FontImageDataset(5000, train=False)
train_iter = iterators.SerialIterator(train_data, batch_size=200, shuffle=True)
test_iter = iterators.SerialIterator(test_data, batch_size=200, repeat=False, shuffle=False)

class MLP(Chain):
    def __init__(self, n_units, n_out):
        super(MLP, self).__init__(
            l1 = L.Linear(None, n_units),
            l2 = L.Linear(None, n_units),
            l3 = L.Linear(None, n_out)
        )

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(x))
        y = self.l3(h2)
        return y

class Classifier(Chain):
    def __init__(self, predictor):
        super(Classifier, self).__init__(predictor=predictor)

    def __call__(self, x, t):
        y = self.predictor(x)
        loss = F.softmax_cross_entropy(y, t)
        accuracy = F.accuracy(y, t)
        report({'loss': loss, 'accuracy': accuracy}, self)
        return loss

model = L.Classifier(MLP(100, 2))
optimizer = optimizers.SGD()

optimizer.setup(model)

updater = training.StandardUpdater(train_iter, optimizer, device=-1)
trainer = training.Trainer(updater, (300, 'epoch'), out='result')
print("start running")
trainer.extend(extensions.Evaluator(test_iter, model))
trainer.extend(extensions.LogReport())
trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy']))
trainer.extend(extensions.ProgressBar())
trainer.run()
print("end running")
4. 結果
  • この設定なら非力なMacBookのCPUでも5分程度で学習完了。
  • エポック250くらいで識別率97%くらいまで上がった。

f:id:mizti:20161126193903p:plain

5. この後
  • 分類クラス数を増やしてみると識別率上限が低くなると思うのでいろんなモデルを試して見る