Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

ProtoPieでAppStore動かしてみた!ファイナル 第3回

Galapagos Advent Calendar 企画、21日の記事になります。

adventar.org

こんにちは。UIデザイナーのまあのんです。



ProtoPie動かしてみた!レポートラストファイナル最終回になります。
過去の記事はこちらからどうぞ! ↓

gtech.hatenablog.com

gtech.hatenablog.com


ブログって継続的に続けられた記憶がないのですが、アドベントカレンダーのように前もって日程が決まっているとやらねば!って気もちになります。素晴らしい文化です。




それではやっていきましょう。

あらためて流れを説明すると、

-動かす流れおさらい-

  • スクロールする

  • イメージをタップすると詳細ページに遷移する

  • 詳細ページに遷移する時にイメージが拡がる

  • 一覧に戻る時イメージが縮む


今回で最後まで完成させます!


Step3.イメージをタップして遷移して拡がる


f:id:glpgsinc:20171215192530g:plain:w200
この動きを、、つくりたいのです、、



結論からいうと、レスポンスの動作は下記の形になりました。それぞれ説明していきます。
f:id:glpgsinc:20171221050047p:plain:w700

①グラデーション背景、虹のイラスト
表現したい挙動:ふわっと広がって画面上部に移動
レスポンス:Scale→拡大、Move→画面上部に移動 共にEasingはEase In&Out

第二回ではグループごと動かしてみましたが、それぞれ別レイヤーに変更することで空間に奥行きを感じるようなモーションになりました。
また、Easing機能はEase In&Outをチョイス。他にも多種多様なアニメーションカーブが揃ってます↓
f:id:glpgsinc:20171221060324p:plain:w500

ProtoPie - Basics

公式サイトより。これでも一部です。カスタムできないからなのか、正直AfterEffects先輩もびっくりなラインナップ数。

マテリアルデザインのモーションデザインの原則によると、現実世界のような普段から慣れ親しんだ動きを取り入れることで、使う人が心地よく理解しやすいモーションになると考えられています。動きをつける際は一度マテリアルガイドラインに目を通しておくことをおすすめします!

Material motion - Motion - Material Design


②今日のAPP/値段/アプリ名テキスト、アプリアイコン
表現したい挙動:画面上部に移動
レスポンス:Move→画面上部に移動

③タブバー/ステータスバー
表現したい挙動:すっと消える
レスポンス:Opacity→100%から0%に変更

④テキストエリア
表現したい挙動:画像下からすっと出てきて、テキストが遅れて表示
レスポンス:Opacity→0%から100%に変更
テキストはタイムライン上でかなり後に設定しています。


一通りの動きをつけ終えました。
ここでプレビューを見てみましょう。

f:id:glpgsinc:20171221103209g:plain:w200


ヌルヌル!!!!







f:id:glpgsinc:20171221062210p:plain:w500


忘れてはいけない大切なレスポンス、Jump。


■遷移系のインタラクションでかかせない『Jump』

Jumpはシーン間の移動ができます。このJump以外のレスポンスで十分遷移されたように見せることは出来ますが、レイヤー構造が複雑で遷移先のアクションも必要な場合は一度Jumpでシーン移動することをおすすめします。ちなみにシーン移動する際はフェードインなど簡単なTransitionを追加することができます。

今回は遷移時に複雑なアクションが必要だったため、一旦このシーンで遷移後の画面を作成→それと同じ画面のシーンをJumpで差し換えてアクションを行います。




Step4.詳細画面でのアクション→一覧に戻る時にイメージが縮む



Jumpでシーン移動した後のアクションになります。 f:id:glpgsinc:20171221071536p:plain:w700

詳細画面に表示されるポップアップの動きもさくっと再現していきます。↓

f:id:glpgsinc:20171221111346g:plain

①アプリのポップアップ
表現したい挙動:スクロールして上部イメージが見えなくなると下からぽよんと現れる
トリガー:Range 対象→スクロール範囲 Property→スクロール position450(テキストエリアあたり) ≦ ScrollOffsets of スクロール
レスポンス:Move ×2→下から定位置より少し上に移動/定位置に移動 共にEasingはEase In&Out

■条件がそろうと発生するアクション、『Range』
レイヤーのプロパティが変わる際、ユーザーが設定した条件を満たした場合にレスポンスが行われます。
ポップアップの場合、スクロール内の特定の範囲のみ表示する仕組みだったため、対象をスクロールコンテナ、条件をイメージとテキストエリアの間辺りから下へスクロールすると発生する形にしました。


レスポンスではMoveを2回。軽くバウンドするような動きをつけるためタイムラインをずらしています。
ぽよんぽよんって動き、、ほんとは動かしてる時めっっちゃくちゃ楽しいから色々遊びたくなる。。けど、今回はあくまで模写修行です。我慢我慢


②アプリのポップアップ
表現したい挙動:表示されてる状態でイメージのエリアまでスクロールするとスッと消える
トリガー:Range 対象→スクロール範囲 Property→スクロール  ScrollOffsets of スクロール ≦ position260(イメージエリアあたり)
レスポンス:Opacity→100%から0%に変更


バツボタン
表現したい挙動:バツボタンをタップすると一覧画面に遷移する
トリガー:Tap 対象→バツボタン
レスポンス:Jump→シーンを最後のアクション追加用の画面に変更

最後はやっぱりJump追加です。このシーンではjumpで遷移後に縮むモーションを追加していきます。


最後のシーンに遷移してきました! f:id:glpgsinc:20171221082151p:plain:w700

ここでは詳細から一覧の画面に戻るだけなので、レスポンス的にはお馴染みのもので逆戻りしているだけです。
トリガーのStartをご説明します。
f:id:glpgsinc:20171221082920p:plain:w500

■シーン移動後自動的にうごきだす『Start』
プロトタイプが実行・またはシーンが展開された際にレスポンスが展開されます。Twitterのようなアニメーションのスプラッシュも作れちゃいます!

これをトリガーに縮むモーションを追加して、完成です!




完成、、、、

完成、、、、、、、、、、、、




ぜひ実機で、さわってみてください。↓

share.protopie.io



とはいえ、プロトタイプ



とはいえ、プロトタイプです。表現の幅に限度があるようです。

実際に触ってみるとわかるのですが、一覧画面で初期位置からズラしてタップすると、広がるはずのイメージ画像が画面の上部固定で表示されず、強制的に詳細画面に遷移するような挙動になります。ムムム。
どうやらスクロール可能なグループから複雑な遷移のアクションは指定が難しいようで、公式で配布されてるデータでは複雑な遷移のものは決まって画面固定でした。
(ひとつだけ、pagingメインでタップすると画面内の指定の場所に配置されるギャラリーを発見したものの、解読に断念)
これに苦戦しているようならいっそXcodeを体得したほうがいいのではとも、、
このこがどこまで出来て、何を苦手としているか、これからもっと探求していきます!


もし解決方法しってるよ!とがいらっしゃいましたらお気軽にメッセージお願いしますm(._.)m

とはいえ、便利



とはいえ便利です。実際にクライアントへデザインをプレゼンする際にProtoPieをつかってみたところ、
いつものプロトタイプと比べてヌルヌルしてるじゃん!すごいじゃん!といつもよりテンションが1段階あがっていました。
(提案物に対しての反応ではないので、素直に喜んでいいのか分からなかったけど)

これまでは静止画のプロトタイプ、AfterEffectsで作成した動画を流してプレゼンしていましたが、パット見のビジュアルで判断され、結果的にユーザーの使い勝手を損なう危険をはらんでいました。

思い描いた動きを短時間で作成でき、かつ実機で直接操作感をチェックできるProtopieは
デザインに大きな説得力を持たせてくれる、UIデザイナーにとって理想の具現化ツールなのかもしれません!


以上、Protopieでつくってみたレポートこれにて終了です。

adventar.org

Galapagos Advent Calendarも、のこり2記事となりました。最後までお見逃しなくです。


参考記事
https://www.protopie.io/learn/
さよなら Pixate, よろしくProtoPie – heru – Medium
【Protopieのススメ】UIデザイナーのためのインタラクションモックアップツール - Qiita

CoreMLでリアルタイムなスタイル変換

これはGalapagos Advent Calendar 20日目の記事です。

二度目まして。iOSチームの高橋です。好きな金額は二兆円です。

今回はiOS上で簡単にニューラルネットのモデルを実行させられるCoreMLを利用して、リアルタイムなスタイル変換を実装する話をします。

準備

Kerasモデルファイルの入手

さて、リアルタイムなスタイル変換を行う手法としてはarXiv:1603.08155が存在しますが、なんと!昨日の記事でまんださんがこれをKerasで実装してくれています!(しらじら)

なので、できあがったh5モデルファイルをもらうことにしました。これさえあればモネ風のスタイル変換ができる、はずです。

mlmodelへの変換

Kerasのモデルファイルをもらったので、coremltoolsを使ってmlmodelファイルに変換します。 coremltoolsは現時点ではPython2系にしか対応していないようなので、しぶしぶPython2を使います。

$ pip install -U coremltools
>>> import coremltools
WARNING:root:Keras version 2.1.2 detected. Last version known to be fully compatible of Keras is 2.0.4 .
WARNING:root:TensorFlow version 1.4.1 detected. Last version known to be fully compatible is 1.1.1 .
>>> coreml_model = coremltools.converters.keras.convert('./monet2.h5', input_names='input_1', image_input_names='input_1', output_names='transform_output')

(中略)

ValueError: Unknown layer: InputNormalize

はい、失敗しましたね。今日のblogはここまでです。ありがとうございました。

……というわけにはゆかないので、今回はこれを乗り越えてみせたいと思います。

なぜなのか

なぜ失敗するのかというと、元になったKerasのモデルがKerasにはない独自のレイヤーを定義して使用しているからです1。 するとcoremltoolsは「そんなレイヤー知りません」という顔でエラーを投げて寄越すわけです。

# たとえばこういうレイヤーがあると変換に失敗する
class InputNormalize(Layer):
    def __init__(self, **kwargs):
        super(InputNormalize, self).__init__(**kwargs)

    def build(self, input_shape):
        pass

    def compute_output_shape(self,input_shape):
        return input_shape

    def call(self, x, mask=None):
        return x/255.

いまさら純Kerasで作り直してもらうわけにもゆかないので、これは困りました。

tf-coreml

ところで話は変わりますが、TensorFlowのモデルからmlmodelへ変換するスクリプトが実は存在します(いくらかの制限つきではありますが)。

github.com

ということは、KerasのモデルをTensorFlowのモデルに変換することができれば、この問題を乗り越えることができるかもしれません。 そしてKerasのモデルがTensorFlowをバックエンドとして利用しているならば、それは原理的には可能なはずです。

Keras to TensorFlow

そう思って検索をかけてみると、先行研究を発見することができます。 だよね〜、と思いながらモデルファイルを投入します。

$ python keras_to_tensorflow.py -input_model_file ./monet2.h5 -output_model_file ./monet2.pb
('input args: ', Namespace(f=None, graph_def=False, input_fld='.', input_model_file='../monet2.h5', num_outputs=1, output_fld='.', output_graphdef_file='model.ascii', output_model_file='../monet2.pb', output_node_prefix='output_node'))

(中略)

ValueError: Unknown layer: InputNormalize

コケてしまいました。やはり「そんなレイヤー知りません」ということのようです。えっじゃあKerasは独自に定義したレイヤーを含むモデルは配布できないってことですか?と思って検索すると、こういうコメントを発見しました:

model = keras.models.load_model('temp_model.h5',
              custom_objects={'Melspectrogram':kapre.time_frequency.Melspectrogram})

なるほど、load_model時にレイヤークラスを渡してやればいいようです。なのでkeras_to_tensorflow.pyをすこし修正して再挑戦。

net_model = load_model(weight_file_path, custom_objects={'InputNormalize': InputNormalize,
                                                         'ReflectionPadding2D': ReflectionPadding2D,
                                                         'Denormalize': Denormalize,
                                                         'VGGNormalize': VGGNormalize,
                                                         'dummy_loss': dummy_loss})
Converted 122 variables to const ops.
('saved the freezed graph (ready for inference) at: ', '././monet2.pb')

やった!これでTensorFlowのモデルファイルが手に入りました。

TensorFlow to CoreML

手に入ったTensorFlowモデルをtf-coremlに投入してやります。

>>> import tfcoreml
WARNING:root:Keras version 2.1.2 detected. Last version known to be fully compatible of Keras is 2.0.6 .
WARNING:root:TensorFlow version 1.4.1 detected. Last version known to be fully compatible is 1.2.1 .
>>> coreml_model = tfcoreml.convert(tf_model_path='monet2.pb', mlmodel_path='monet2.mlmodel', input_name_shape_dict={'input_1:0': [1, 256, 256, 3]}, output_feature_names=['transform_output/mul:0'], image_input_names=['input_1:0'])

(中略)

 Core ML model generated. Saved at location: monet2.mlmodel

Core ML input(s):
 [name: "input_1__0"
type {
  imageType {
    width: 256
    height: 256
    colorSpace: RGB
  }
}
]
Core ML output(s):
 [name: "transform_output__mul__0"
type {
  multiArrayType {
    shape: 3
    shape: 256
    shape: 256
    dataType: DOUBLE
  }
}
]

これでmlmodelファイルが手に入った……ように見えますが、よく見ると出力の型がmultiArrayTypeになっています。これだと画像として出力されないので、Swift側で変換コードを書いてやる必要があるのですが、それはイケてないなあと思っていたところ、AppleのDeveloper Forumに解決方法が書かれていました。

forums.developer.apple.com

こういう関数を定義して噛ませればよいようです:

def convert_multiarray_output_to_image(spec, feature_name, is_bgr=False):
    """
    Convert an output multiarray to be represented as an image
    This will modify the Model_pb spec passed in.
    Example:
        model = coremltools.models.MLModel('MyNeuralNetwork.mlmodel')
        spec = model.get_spec()
        convert_multiarray_output_to_image(spec,'imageOutput',is_bgr=False)
        newModel = coremltools.models.MLModel(spec)
        newModel.save('MyNeuralNetworkWithImageOutput.mlmodel')
    Parameters
    ----------
    spec: Model_pb
        The specification containing the output feature to convert
    feature_name: str
        The name of the multiarray output feature you want to convert
    is_bgr: boolean
        If multiarray has 3 channels, set to True for RGB pixel order or false for BGR
    """
    for output in spec.description.output:
        if output.name != feature_name:
            continue
        if output.type.WhichOneof('Type') != 'multiArrayType':
            raise ValueError("%s is not a multiarray type" % output.name)
        array_shape = tuple(output.type.multiArrayType.shape)
        channels, height, width = array_shape
        from coremltools.proto import FeatureTypes_pb2 as ft
        if channels == 1:
            output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('GRAYSCALE')
        elif channels == 3:
            if is_bgr:
                output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('BGR')
            else:
                output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('RGB')
        else:
            raise ValueError("Channel Value %d not supported for image inputs" % channels)
        output.type.imageType.width = width
        output.type.imageType.height = height

実際に試してみます。

>>> spec = coreml_model.get_spec()
>>> convert_multiarray_output_to_image(spec, 'transform_output__mul__0')
>>> newModel = coremltools.models.MLModel(spec)
>>> newModel.save('monet2.mlmodel')

これで……

f:id:glpgsinc:20171218155353p:plain

できました!!!

あとはこのmlmodelファイルをスッとXcodeに投入すれば、画像の変換を行うクラスが生成されます。長かった…… 便利な世の中ですね。

実装

メインロジック

実装のメイン部分はCoreMLのおかげで非常に簡潔です。こんな風に:

import AVFoundation
import CoreImage
import UIKit

class ViewController: UIViewController {

    @IBOutlet private weak var imageView: UIImageView!

    let model = monet2()
    let imageSize = CGSize(width: 256, height: 256)

    /// メインロジック。CVPixelBufferを受け取って変換して表示する
    ///
    private func pixelBufferDidUpdate(pixelBuffer: CVPixelBuffer) {
        let output = try! model.prediction(input_1__0: pixelBuffer)
        let image = UIImage(ciImage: CIImage(cvPixelBuffer: output.transform_output__mul__0))
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }

    let session: AVCaptureSession = AVCaptureSession()
    let videoQueue: DispatchQueue = DispatchQueue(label: "videoqueue")
    var pixelBuffer: CVPixelBuffer? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        heavyLifting()
    }

}

で、残る問題は動画のキャプチャとピクセルバッファのクロップなのですが、それについては以下で簡単に説明したいと思います。

AVFoundationによる動画のキャプチャ

今回はリアルタイムなスタイル変換を行いたいので、カメラからの入力をリアルタイムに受け取る必要があります。 こういう場合にはAVFoundationを使えばよいらしいので、そのためのセットアップを行います2

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func heavyLifting() {
        CVPixelBufferCreate(kCFAllocatorDefault,
                            Int(imageSize.width), Int(imageSize.height),
                            kCVPixelFormatType_32BGRA, nil, &pixelBuffer)

        let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)!
        device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 30)

        let videoInput = try! AVCaptureDeviceInput(device: device)
        session.addInput(videoInput)

        session.sessionPreset = .hd1280x720

        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
        videoDataOutput.setSampleBufferDelegate(self, queue: videoQueue)
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        session.addOutput(videoDataOutput)

        let connection = videoDataOutput.connection(with: .video)
        connection?.videoOrientation = .portrait

        session.startRunning()
    }

冒頭のCVPixelBufferCreateは、CoreMLとのやりとりに使うピクセルバッファを作成するためのもので、AVFoundationとは関係ありません。 その下では背面カメラから動画の入力を受ける設定をしており、動画サイズは1280×720だという指定が続きます。最後に出力の受け先をselfに向けて撮影を開始しています。

CoreImageによるフレームの切り抜き

あとはキャプチャしたフレームを適切な大きさにクロップしてCVPixelBufferに書き込む必要がありますが、これはCoreImageを経由することで実現できるようです。筆者はCoreImageに明るくないのでけっこう苦労しましたが、Stack Overflowに助けられてなんとか画像の中央を切り出すことに成功しました。

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        let ciImage = CIImage(cvImageBuffer: CMSampleBufferGetImageBuffer(sampleBuffer)!)
        let originalRect = ciImage.extent
        let cropRect = CGRect(x: (originalRect.width - imageSize.width) / 2,
                              y: (originalRect.height - imageSize.height) / 2,
                              width: imageSize.width, height: imageSize.height)
        let cropped = ciImage.cropped(to: cropRect)
        let transformed = cropped.transformed(by: CGAffineTransform(translationX: -cropRect.origin.x, 
                                                                    y: -cropRect.origin.y))
        let context = CIContext()
        context.render(transformed, to: pixelBuffer!)
        pixelBufferDidUpdate(pixelBuffer: pixelBuffer!)
    }

}

こうしてできたpixelBufferを上で書いたメインロジックに渡してやれば、CoreMLの力で画像が変換されるので、CIImageを経由してUIImageにしてUIImageViewに表示してやれば完成です。

結果

弊社エントランスにかざってあるクリスマスツリーをモネ風にしてみました。端末はiPhone Xを使用していますが、iPhone 7でも似たような速度で動きます。

f:id:glpgsinc:20171218185207g:plain

いかがでしょうか。モネ風かどうかはともかく、なにかしらスタイルが変換されていることがおわかりいただけるかと思います3。 関係ないですが、端末がまあまあ熱くなるので、無茶をしているのだなあという気持ちになります。

おわりに

ということで、iOS端末上でリアルタイムにスタイル変換をするアプリを簡単に作ることができました。次はこっちの論文なんかも実装してみたら楽しいかもしれませんね。

ところで

弊社では機械学習に興味があったりなかったりするエンジニアを募集しています。ご応募お待ちしております。

www.glpgs.com

以上です。明日はUIデザイナーのまあのんさんがProtoPieについて書いてくれるそうですね。お楽しみに。


  1. という記事を書いている間に判明したのですが、iOS 11.2以降は独自のレイヤーに対応させることが(がんばれば)可能なようですね(参考)。

  2. あっ、Info.plistのNSCameraUsageDescriptionを設定しておくのを忘れないでくださいね。

  3. もっとも、元からわりと派手なクリスマスツリーだという話はあるのですが。

Kerasのお勉強のついでにStyleTransfer

これはGalapagos Advent Calendar 19日目の記事です。

こんにちは、AIチームの中の人まんだです。 アドベントカレンダー二度目の登場です。前回は合同勉強会の参加レポートを投稿しましたが、 今回はせめてAIチームらしく、機械学習の話題をしたいと思います。

今回の内容は、「Neural-Style-Transferを題材にKerasのお勉強を始めよう」です。

背景

ガラパゴスAIチームでは長らく機械学習モデルはTensorFlowで書いていたのですが、 最近「Kerasって便利そうだよね〜。」「次に新しく組むモデルからはKerasを採用しても良いのでは?」 という話がちらほらと上がり始めていました。 これは、勉強せないかんなぁ〜と考えていたところで、12月のアドベントカレンダーの話が来たので、 「そうだ!アドベントカレンダーねたに抱き合わせでKerasのお勉強をしてしまおう」と思い立ってこの記事が作られています。 ただKerasのお勉強をしてもつまらないので、何かモデルをいじってみようということで、今回はNeural-Style-Transferを題材に選んでみました。

Neural-Style-Transferとは

NeuralStyleTransferとは、論文1で2016年に発表された、画像(絵画とか)のスタイル(画風・雰囲気)を別の画像(写真など)に転写できるようにするネットワークのことです。

このモデルでは、1回の画像のスタイル変換ごとにノイズ画像を出発地点として、ForwardとBackwardの計算が必要になるために出力されるまでそこそこ時間がかかります。 なので、今回ターゲットにするのは、リアルタイムでスタイル変換することを可能にしたモデル2を使います。

StyleTransferとKeras

というわけで、Style-TranferをKerasで...と思ったら、当然のようにすでに実装はされているわけでして。 今回はこの実装コードを参考にしながらKerasのお勉強を進めていきます。

モデルの中身

高速スタイル変換の中身自体の解説は、この記事などで詳しく紹介されています。 ここでは、kerasでの実装コードを眺めてどのようにモデルが記述されているか見てみましょう。

Kerasでのモデルの定義

Kerasで利用出来るモデルは次の二つがあります。

  1. Sequential Model
  2. functional APIを用いたモデル

Sequential Model

以下はドキュメントから持ってきた例です。

model = Sequential()
model.add(Dense(32, input_shape=(500,)))
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

addを使ってレイヤーをただただ追加していくシンプルな作りですね。

functional API

シンプルなモデルだったらSequentialモデルで良いのですが、少し洒落たことがしたいとなったら functional APIを使いましょう。 またまたドキュメントから例を持ってくると

from keras.models import Model
from keras.layers import Input, Dense

a = Input(shape=(32,))
b = Dense(32)(a)
model = Model(inputs=a, outputs=b)

と書けます。

Neural-Style-TransferでのモデルをKerasで書くと?

というわけで、さっそくNeural-Style-TransferのモデルをKerasで定義してみましょう。 Githubにあるtrain.pyの中身を見てみると

net = nets.image_transform_net(img_width, img_height, tv_weight)
model = nets.loss_net(net.output, net.input, img_width, img_height, style_image_path, content_weight, style_weight)
model.summary()
optimizer = Adam()  # Adam(lr=learning_rate,beta_1=0.99)
model.compile(optimizer,  dummy_loss)  # Dummy loss since we are learning from regularizes

どうやらnets.pyにスタイル変換ネットワークとロスネットワークが記述されている様子です。 ともあれmodelが定義されたらoptimizerを指定して、model.compileするだけ。お手軽ですね。 今回別口でlossを定義しているので、compile時点ではdummy_lossを指定してます。

スタイル変換ネットワーク

では、nets.image_transform_net()の中身を確認してみましょう。そっとnets.pyを開いてみます。

from keras.layers import Input
from keras.layers.merge import concatenate
from keras.models import Model, Sequential
from layers import InputNormalize, VGGNormalize, ReflectionPadding2D, Denormalize, conv_bn_relu, res_conv, dconv_bn_nolinear
from loss import StyleReconstructionRegularizer, FeatureReconstructionRegularizer, TVRegularizer
from keras import backend as K
from VGG16 import VGG16
import img_util


def image_transform_net(img_width, img_height, tv_weight=1):
    x = Input(shape=(img_width, img_height, 3))
    a = InputNormalize()(x)
    a = conv_bn_relu(32, 9, 9, stride=(1, 1))(a)
    a = conv_bn_relu(64, 3, 3, stride=(2, 2))(a)
    a = conv_bn_relu(128, 3, 3, stride=(2, 2))(a)
    for i in range(5):
        a = res_conv(128, 3, 3)(a)
    a = dconv_bn_nolinear(64, 3, 3)(a)
    a = dconv_bn_nolinear(32, 3, 3)(a)
    a = dconv_bn_nolinear(3, 9, 9, stride=(1, 1), activation="tanh")(a)
    # Scale output to range [0, 255] via custom Denormalize layer
    y = Denormalize(name='transform_output')(a)

    model = Model(inputs=x, outputs=y)

    if tv_weight > 0:
        add_total_variation_loss(model.layers[-1], tv_weight)

    return model

ふむふむ。keras.layersからインポートされているInputで入力を規定している様子。その後InputNormalizeは自作しているようですが、[0, 255]→[0, 1]に正規化しているご様子。 その後、conv_bn_reluが3回繰り返されています。

from keras.layers.core import Activation
from keras.layers.normalization import BatchNormalization
from keras.layers.convolutional import Conv2D
def conv_bn_relu(nb_filter, nb_row, nb_col, stride):   
    def conv_func(x):
        x = ReflectionPadding2D(padding=(nb_row // 2, nb_col // 2))(x)
        x = Conv2D(nb_filter, (nb_row, nb_col), strides=stride, padding='valid')(x)
        x = BatchNormalization()(x)
        x = Activation("relu")(x)
        return x
    return conv_func

これはConvでたたみ込み→バッチ正規化→Reluの処理をしています。 その後はResidual Blockを5層用意しています。さらにdconv_bn_nolinearでDeconvolutionしていますね。この辺りは中身はconv_bn_reluと大差ないので割愛します。 deconv_bn_nolinear()ではactivation関数はデフォルトがReluなのですが最後の層だけはtanhを指定しています。引数で渡すだけで切り替えられて便利ですね。 最後にDenormalize()で[0, 255]の範囲に戻しています。 そしてinputとoutputをModel()に渡してあげて完了です。

Lossネットワーク

VGG16の重みをセットして、Style LossとContent Lossをそれぞれ計算します。

from VGG16 import VGG16
def loss_net(x_in, trux_x_in, width, height, style_image_path, content_weight, style_weight):
    # Append the initial input to the FastNet input to the VGG inputs
    x = concatenate([x_in, trux_x_in], axis=0)
    
    # Normalize the inputs via custom VGG Normalization layer
    x = VGGNormalize(name="vgg_normalize")(x)

    vgg = VGG16(include_top=False, input_tensor=x)

    vgg_output_dict = dict([(layer.name, layer.output) for layer in vgg.layers[-18:]])
    vgg_layers = dict([(layer.name, layer) for layer in vgg.layers[-18:]])

    if style_weight > 0:
        add_style_loss(vgg, style_image_path, vgg_layers, vgg_output_dict, width, height, style_weight)

    if content_weight > 0:
        add_content_loss(vgg_layers, vgg_output_dict, content_weight)

    # Freeze all VGG layers
    for layer in vgg.layers[-19:]:
        layer.trainable = False

    return vgg

Style LossとContent Lossに関しては、元論文に準じています。詳細は元論文を参照してください。

学習

それではいよいよ学習してみましょう。 用意するものは、

  1. 学習したいスタイル画像1枚
  2. 学習用コンテンツ画像 たくさん(今回はMS COCOを使用)
  3. GPUマシン

今回は学習に社内のGPUマシン(GeForce GTX 1080搭載)を使いました。(社内マシンはDockerで運用しているので、このモデルも諸々Dockerに載せる作業がありましたが、詳細は割愛します。) 学習させるスタイルは、モネの睡蓮にしてみました。

f:id:glpgsinc:20171218140434j:plain (http://www.mam-e.it/wp-content/uploads/2017/01/NINFEE-claude-monet-992x538.jpg)

いざ学習へ

from keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator()
dummy_y = np.zeros((train_batchsize, img_width, img_height, 3))  # Dummy output, not used since we use regularizers to train
skip_to = 0

i = 0
t1 = time.time()
for x in datagen.flow_from_directory(train_image_path, class_mode=None, batch_size=train_batchsize, target_size=(img_width, img_height), shuffle=False):

    if i > nb_epoch:
        break

    if i < skip_to:
        i += train_batchsize
        if i % 1000 == 0:
            print("skip to: %d" % i)
        continue

    hist = model.train_on_batch(x, dummy_y)
    if i % 50 == 0:
        print(hist, (time.time() - t1))
        t1 = time.time()

    if i % 500 == 0:
        print("epoc: ", i)
        val_x = net.predict(x)

        display_img(i, x[0], style)
        display_img(i, val_x[0], style, True)
        model.save_weights(os.path.join(model_dump_dir, style) + '_weights.h5')
        model.save(os.path.join(model_dump_dir, style) + '.h5', include_optimizer=False)
    i += train_batchsize

学習自体はいたってシンプルです。 ImageDataGenerator().flow_from_directoryでバッチサイズ毎の画像読み出しを行って、model.train_on_batch()を呼ぶだけです。シンプル! 一応500ステップ毎にモデルのダンプをしています。

学習途中の画像はこんな感じになっています。

f:id:glpgsinc:20171218141551p:plainf:id:glpgsinc:20171218141555p:plainf:id:glpgsinc:20171218141602p:plainf:id:glpgsinc:20171218141559p:plainf:id:glpgsinc:20171218141611p:plainf:id:glpgsinc:20171218141607p:plain

最後の段は、約16万ステップでの出力になります。しっかり草原がモネの睡蓮っぽく(?)なってますね! きちんとスタイルが学習されています。よかったよかった。

まとめ

Kerasのお勉強を兼ねて、Neural-Style-Transferのモデルを学習させてみました。 実際のところキチンとKerasを使いこなすには、もう少し自分でちゃんと実装してみないとわかりませんね。 でもかなり便利に使えそうなので、じわじわ使っていこうと思います。

おわりに

明日はiosチームの高橋さんが、アプリ開発会社ガラパゴスらしく、このStyleTransferモデルを スマートフォンに載せてリアルタイム画風変換をやってくれるようです。お楽しみに。

さらにおわりに

弊社では、Kerasでバリバリ機械学習したい方、TensorFlowでガシガシ実装したい方、 機械学習興味ある方を絶賛募集中です。ご応募お待ちしております。

www.glpgs.com

以上になります。明日以降もアドベントカレンダーお楽しみに!


  1. Gatys, Leon A., Alexander S. Ecker, and Matthias Bethge. “Image style transfer using convolutional neural networks.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2016.

  2. Justin Johnson, Alexandre Alahi, Li Fei-Fei. “Perceptual Losses for Real-Time Style Transfer and Super-Resolution.“ arXiv. 2016