株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

iOSアプリエンジニアの @hiragram です。VASILYにジョインしてだいたい3か月経ちました。 今回は、僕がジョインしたプロジェクトに導入した、APIリクエストの抽象レイヤーの設計について紹介します。また、記事の最後にこの抽象レイヤーのコードをフレームワークとして切り出したもののリンクがありますので、興味がある方は見てみてください。 課題と方針 当プロジェクトでは、リアクティブフレームワークに RxSwift 、通信ライブラリに APIKit 、JSONマッパーに Himotoki を採用しています。 従来のアプリの実装には、 ViewModelが直接APIKitをインポートして通信しており、通信のトリガーやレスポンスの処理が画面によってバラバラ APIが取得対象のリソースを常に results というキーに配列で返すようになっており、必ず1個しか返さないAPIのレスポンスも results.first とOptionalになる形で取り出している ある画面で取得したデータ(例えば自分のユーザーデータなど)を他の画面に反映するための仕組みが整備されておらず、反映漏れが多発 といった課題がありました。 そこで、今回のAPI抽象レイヤーの導入によって、 VMのレイヤーから通信やAPIの構造を隠蔽し目的のデータのみを意識すればよい設計 Observableベースのリアクティブなアプリ内のデータフロー 取得した結果を他の画面にブロードキャストするためのObservableを用いたシンプルなデータ反映 を獲得することを目的としました。 エンドポイント定義 エンドポイントを定義するための EndpointDefinition というプロトコルを用意しました。 protocol EndpointDefinition { // 返されるJSONがどのような構造か associatedtype Response : ResponseDefinition static var path : String { get } // 接続先(デフォルトは`.production`、 他に`.mock` など) static var environment : Environment { get } // 送られるパラメータ var parameters : [String: Any] { get } // HTTPメソッド(アンダースコアはAPIKitと名前の衝突を防ぐため) var method : _HTTPMethod { get } } 例えば、今日のおすすめメニューを以下のようなJSONで返すエンドポイントを考えます。 { " name ": " チキンカレー ", " price ": 750 } このエンドポイントは以下のように定義できます。 struct RecommendedMenu : EndpointDefinition { // Dishオブジェクトを1つ返す typealias Response = SingleResponse < Dish > static let path = "/menu/recommended" var method : _HTTPMethod = . get var parameters : [String: Any] = [ : ] init () {} } レスポンスのデータ構造 先述の RecommendedMenu の Response で使用している SingleResponse<Dish> は、「Dishオブジェクトとしてデコードできるオブジェクトが1つ」という構造のJSONが返されることを示します。 例えば、おすすめメニューが複数あり配列で返ってくる場合は ArrayResponse<Dish> とすることで [ { " name ": " チキンカレー ", " price ": 750 } , { " name ": " オムライス ", " price ": 700 } ] このようなJSONをパースできるようになります。 SingleResponse 、 ArrayResponse は ResponseDefinition というプロトコルに準拠しています。 protocol ResponseDefinition { // レスポンスの本質であるオブジェクトの型 associatedtype Result // `JSONSerialization.jsonObject(with:options:)` で得たオブジェクトのキャスト先 associatedtype JSON var result : Result { get } // JSONのオブジェクトを受け取って、resultに取り出した値をセットするイニシャライザ init (json : JSON ?) throws } 次に、複数のキーにオブジェクトが入っている複雑なJSONの場合を考えます。例えば、ページングの情報を示す info とメニューの配列を返す dishes があるようなケースです。 { " info ": { " total_count ": 50 , " total_page ": 3 } , " dishes ": [ { " name ": " ポテトサラダ ", " price ": 400 } , { " name ": " シーザーサラダ ", " price ": 450 } , { " name ": " オムライス ", " price ": 700 } , { " name ": " とんかつ定食 ", " price ": 800 } , { " name ": " 和風ハンバーグ ", " price ": 650 } ] } このようなJSONのデータ構造を定義するための CombinedResponse という型があります。 これは DataResponseDefinition に準拠する型を型パラメータに持つジェネリック型です( SingleResponse や ArrayResponse も DataResponseDefinition に準拠しています)。 これを使うと、上記のJSONは CombinedResponse<SingleResponse<Info>, ArrayResponse<Dish>> と表現できます。 CombinedResponse の具体的な実装はここでは省略しますが、興味がある方はコードを見てみてください。 APIKitとのブリッジ さて、ここまででエンドポイントのモデル化が出来ました。次に実際にリクエストを飛ばすためにAPIKitとの境界になる部分を書きます。 struct GenericRequest <Endpoint: EndpointDefinition> { fileprivate let endpoint : Endpoint init (endpoint : Endpoint ) { self .endpoint = endpoint } } GenericRequest は先ほどまでに定義したエンドポイントの型を型パラメータに持つジェネリック型です。この型に、APIKitの Request に準拠するエクステンションを書きます。 extension GenericRequest : Request { typealias Response = Endpoint.Response.Result // プロトコルに準拠するための様々な実装(省略) ... func response (from object : Any , urlResponse : HTTPURLResponse ) throws -> Endpoint.Response.Result { guard let jsonObj = object as ? Endpoint.Response.JSON else { fatalError() } // JSONオブジェクトから値を取り出す処理 return try Endpoint.Response. init (json : jsonObj ).result } } API抽象レイヤーの本体 実際に通信を行うためのAPIKitとのブリッジも出来ました。続いて実際に抽象レイヤーとして機能する部分を書いていきます。 public struct Repository { fileprivate static func request <Endpoint: EndpointDefinition> (_ endpoint : Endpoint ) -> Single <Endpoint.Response.Result> { return Single.create(subscribe : { (observer) -> Disposable in let request = GenericRequest. init (endpoint : endpoint ) let task = Session.send(request, callbackQueue : nil , handler : { (result) in switch result { case .success( let response ) : observer(.success(response)) case .failure( let error ) : observer(.error(error)) } }) return Disposables.create { task?.cancel() } }) } } request(_:) は GenericRequest を使ってリクエストを投げて得られた結果をそのリクエストのオブジェクトが流れてくるRxSwiftの Single を返すメソッドです。 このメソッドはfileprivateにしておき、VMから利用するためのメソッドを生やします。 public extension Repository { public static func recommendedMenu () -> Single <Dish> { let endpoint = Endpoint.RecommendedMenu. init () return request(endpoint) } } これで、VMからは Repository.recommendedMenu().subscribe(onSuccess : { (dish) in print( "本日のおすすめは \(dish.name) ( \(dish.price) 円)です。" ) }).disposed(by : bag ) このようにシンプルなコードでデータにアクセスすることができるようになりました。 取得した結果を他の画面にも反映できるようにする APIの抽象レイヤーができたので、他画面へのデータ反映の仕組みを追加します。具体的には、 各モデルオブジェクト用にブロードキャストのためのストリームを用意して、他の画面の通信結果を受け取りたいVMがそれを購読します。抽象レイヤーは通信に割り込んで得られたオブジェクトをそのストリームに流します。 public final class GlobalStream <T> { fileprivate let subject = PublishSubject < T > . init () func publish (_ element : T ) { subject.onNext(element) } } extension GlobalStream : ObservableType { public typealias E = T public func subscribe <O> (_ observer : O ) -> Disposable where O : ObserverType , O.E == E { return subject.retry().subscribe(observer) } } public extension Repository { public struct GlobalStreams { public static let dish = GlobalStream < Dish > . init () } } ブロードキャスト用のストリームが用意できたので、値を流す側のコードを書きます。 Observableに、流れてきた値をGlobalStreamに流すbranchというメソッドを追加します。 public extension Observable { func branch (to globalStream : GlobalStream <E> ) -> Observable <E> { return flatMap { element -> Observable <E> in globalStream.publish(element) return Observable.just(element) } } } 次におすすめメニューのリクエストにbranchオペレータを追加して、取得したDishオブジェクトをブロードキャストするようにします。 public extension Repository { public static func recommendedMenu () -> Single <Dish> { let endpoint = Endpoint.RecommendedMenu. init () return request(endpoint) .asObservable() .branch(to : GlobalStreams.dish ) .asSingle() } } 他の画面で取得したデータを自分も受け取りたいVMは、GlobalStreamを購読することでデータを受け取ることが出来ます。 Repository.GlobalStreams.dish.subscribe(onNext : { (dish) in print(dish) }).disposed(by : bag ) まとめ APIへのリクエストを抽象化して一箇所でのみ管理する仕組みを導入して上のレイヤーが知りたいことだけ公開する事によって、VMのコードがシンプルになりメンテナンス性が向上します。また、通信を一箇所で集中管理することによってすべての通信で共通して行いたい処理を追加したり、通信ライブラリを入れ替えたりするのが容易になります。 今回紹介したAPI抽象レイヤーを実装するためのプロトコルや構造体をAbstractionKitという名前でフレームワーク化して公開しました。全く同じコードでは無いのですが、今後も業務で得た知見をAbstractionKitに還元していきたいと思っているので、興味がある方はぜひ見てみてください。サンプルアプリもあります。 hiragram/AbstractionKit 次のステップとして、この記事で紹介したエンドポイントの定義をするコードをSwaggerのYAMLドキュメントから自動生成するジェネレータを開発しています。こちらも導入できたらブログで紹介できればと思っています。 VASILYではプロトコル指向なコードがかけるSwiftエンジニアを募集しています。ぜひオフィスに遊びに来てください。
こんにちは、VASILYのバックエンドエンジニアの塩崎です。 9/17〜9/20にかけて広島で開催されたRubyKaigi2017に、VASILYから4人が参加しました。 3日間で約50個の講演があり、参加者も数百人を超えるであろう大変大規模なカンファレンスでした。 たくさんの講演の中で、VASILYのエンジニアが興味を持ったものを、この記事でいくつか紹介いたします。 The many faces of module Ruby Lauguage Server Ruby and Distributed Storage System Progress of Ruby/Numo: Numerical Computing for Ruby Improve extension API: C++ as better language for extension An introduction and future of Ruby coverage library How Close is Ruby 3x3 For Production Web Apps? Type Checking Ruby Programs with Annotations Busting Performance Bottlenecks: Improving Boot Time by 60% Smalruby : The neat thing to connect Rubyists and Scratchers Writing Lint for Ruby Compacting GC in MRI Development of Data Science Ecosystem for Ruby API Development in 2017 まとめ おまけ 弊社の技術顧問であるMatzさんとの記念写真 弊社CTO今村とマネキン餃子娘 どんなに飲んでも飲み足りない系エンジニアと特大レモンサワー 口いっぱいにおにぎりを頬張るフロントエンドエンジニア スライドの紹介に問題がある場合はご連絡ください。 The many faces of module Rubyのパパであり、弊社VASILYの技術顧問でもある Matz さんの発表です。 Rubyに取り入れたsimulaやLispの機能を紹介しながら、オブジェクト指向での継承の利点と問題点を説明されていました。 特に多重継承の問題点に光を当て、Lispで発明されたflavorという機能の紹介をしていました。 この機能はRubyではMixinという名前で導入されました。 もともとはMixinを実現するためのものであるmoduleが現在では以下の7つの機能を持っていることを紹介していました。 module as mixin module as namespace singleton module as set of methods module as unit od method combination(Module#prepend) module as refinement structural signature(提案段階) 前半の4つについては初期のRubyから備わっている機能ですが、後半のものは最近のRubyで拡張されたmoduleの機能です。 そして、RubyはもはやMatzさん個人の言語ではなく、私たちRubyコミュニティの言語である。 また、Rubyをより良いものにすることによって、世界をより良いものにしたいという言葉によって締められました。 Ruby Lauguage Server Quipperの @mtsmfm さんによる、Ruby用のLanguage Serverの実装に関する話でした。 Language Serverとはエディターに対してシンタックスハイライト、Lint、コードフォーマットなどの機能を提供するプログラムです。 Language Serverがない場合は言語とエディタの組み合わせごとにこれらの機能を実装する必要があります。 Ruby用のLanguage Serverがまだなかったので、開発を始めたそうです。 syntax check機能については、 ruby -cw コマンドを呼び出して対応しているそうです。 また、自動補完や、メソッド定義へのジャンプは開発中らしいです。 トップレベルの自動補完は動的解析によって行われているために、ファイルIOなどの副作用のあるコードを実行すると、実際に実行されてしまうという欠点もあるそうです。 今後の展望としては、まだまだ実装していない機能が多いため、それらを実装することや、syntax checkの指摘箇所を行単位からカラム単位にするなどが挙げられていました。 Ruby and Distributed Storage System Ruby and Distributed Storage Systems from SATOSHI TAGOMORI Treasure Dataの @tagomoris さんによる、planet scaleな分散ストレージシステムであるBigdamについての話でした。 TreasureDataに入力されるデータを最初に受け取る部分を作り直ししているそうです。 世界中にエッジロケーションを配置したplanet scaleなシステムになるそうです。 Bigdamは6つのマイクロサービスからなっており、システムそのものはJavaやGoで書かれているそうです。 Rubyは開発を円滑に進めるために用いたそうです。 開発の初期段階でRubyを使いmock serverやInterface testを作ってから、それぞれのマイクロサービスの開発を行ったそうです。 また、データ量が増加した時に、スケールするのかどうかの検証にもRubyを用いたそうです。 このシステムは最終的にOSSになる予定らしいです。 今から、公開が待ち遠しいですね。 Progress of Ruby/Numo: Numerical Computing for Ruby 筑波大の @masa16tanaka さんによる、Rubyでデータサイエンスを行うために必要なN次元配列演算ライブラリについての講演でした。 データサイエンティストがコードを書くときにはPythonを用いることが多く、Rubyを使ってデータサイエンスを行う人は決して多くはありません。 Pythonが広く使われている理由はpandansやmatplotlibなどの非常に充実したライブラリ群(SciPyスタック)にあるそうです。 そして、それらの土台をなしているのがN次元配列の計算を行うライブラリであるNumPyです。 そのため、Rubyのデータサイエンス用ライブラリを充実させるためには、まずこのNumPyのRuby版が必要です。 Numo::NArrayはまさにそのような目的のライブラリです。 NumPyが提供している363個の関数のうち、217個はすでにNArrayでもカバーされているそうです。 さらに、NumPyが提供している以下の3つの便利機能もNArrayに実装されているそうです。 view of slice broadcasting masking 速度の面ではまだNumPyには敵わない印象を受けましたが、速度に関する最適化には未着手とのことでしたので、これからさらに高速化していく余地は十分にあるそうです。 さらにNumoプロジェクトではNArrayだけではなく、さらに高度な計算を行うためのライブラリも提供されています。 線形代数ライブラリNumo::Linalg、科学計算ライブラリNumo::GSL、高速フーリエ変換ライブラリNumo::FFTWなどです。 内部的にNArrayを使っており、NArrayとの親和性が高いそうです。 これらのライブラリが充実することにより、Rubyでもデータサイエンスが行えるような未来を目指すという内容でした。 参考: Nomoのプロジェクトページ Improve extension API: C++ as better language for extension Improve extension API: C++ as better language for extension ClearCodeの @ktou による、C++を使って拡張ライブラリを実装する話でした。 Rubyの拡張ライブラリを書くためにはCを使うことがほとんどかと思いますが、C APIは冗長な記法になりがちです。 そこで、Ext++というライブラリを作り、C++11で拡張ライブラリを実装できるようにしたそうです。 C++11では、モダンな機能が提供されており、シンプルな記法で拡張ライブラリの実装を行うことができるようです。 例えば、型推論やラムダ式、cast演算子のカスタマイズなどの機能です。 しかし、Rubyの例外は内部でsetjmpを使っているため、RAII(Resource Acquisition Is Initialization)との相性が悪いなどの問題点もあるそうです。 An introduction and future of Ruby coverage library Cookpadでフルタイムコミッターになられた @mametter さんによる、Rubyのテストカバレッジライブラリの紹介とcoverage.soの拡充計画についてのお話でした。 Rubyでは、品質を保証するための実用的な手段はいまのところ、テストしかありません。 そしてテストの良さを計測するのにカバレッジはとても必要なものです。 カバレッジ計測だけでは仕様の漏れなどは発見できませんが、カバレッジ計測をうまく利用して設計の網羅性を高くすることは可能です。 理想的なカバレッジの上げ方は、「テストされていないコードを全体的から探し、どういう観点のテストが足りていないのかを振り返って考え、足りなかった観点のテストを全体的に確認しながら書いていく」という方法だそうです。 Rubyのカバレッジ計測についてですが、SimpleCovがgemとしてよく使用されています。SimpleCovは、coverge.soのラッパーです。 coverage.soは、発表者の遠藤さんが開発されたライブラリで、現在line coverageのみサポートしています。 しかし、line coverageだけでは後置ifなどの条件分岐を網羅することはできず、指標として弱いです。 なぜline coverageしかサポートしていないかというと、coverage.soはRubyのテストを拡充するために作ったものなので、他で使われることをあまり想定していなかったそうです。 そのため現在は、後方互換性を残しつつ機能を拡充する開発を行っているそうです。 実際に、branch coverageとfunction coverageの機能が最近コミットされています。 したいことはたくさんあるようで、メソッドのカバレッジ計測・&.のブランチカバレッジ対応・メソッドチェーンのメソッドごとのブランチカバレッジにも対応したいとおっしゃっていました。 カバレッジ等すべて丁寧にお話しされていたため、とてもためになる発表でした! coverage.soの機能拡充によりRubyの堅牢性向上が期待できますね! 参考: RubyKaigi 2017 の予稿 How Close is Ruby 3x3 For Production Web Apps? AppFolioの @codefolio さんによる、Ruby3x3プロジェクトのために、ベンチマークを開発した話です。 実際のWebアプリケーションを用いて、Rubyのバージョンアップに伴いWebアプリケーション自体どれほど速度改善したかをご紹介されていました。 計測には、 Discourse というコミュニティサイトをテスト対象のアプリケーションとして使用しています。 また、計測を行うための環境を定めたり、railsが起動しているインスタンスをDedicated Hostにしたりするなど、厳密な計測を行おうとする姿勢が伺えました。 結果としては、Rubyのバージョンが上がるについれて、徐々に速度は改善されています。 HTTPリクエストの処理時間については、Discourse自体がバージョンアップにより重くなったのを考慮するとRuby2.0.0からRuby2.4.1では150%程度速度が改善しているそうです。 また、初回リクエストにかかる時間に関しては、Ruby2.0.0からRuby2.4.1で30%程度改善されています。 Matzさんが、Rubyの速度改善にはwarm upの速度改善が重要とおっしゃっていましたが、warm upも少しづつではありますが、速度が改善されているようです。 また、ベンチマークであるrails_ruby_benchは公開されておりAWSやローカルで試すことができます。 参考 発表スライド rails_ruby_bench Type Checking Ruby Programs with Annotations SideCIの @soutaro 、Rubyで型チェックを行う手法が紹介です。 Rubyで型チェックを行う取り組みは以前からありますが、コード内に型定義を書いておき、ランタイムでチェックを行う点がこの手法の特徴です。 型定義には大まかに2通りの記法があります。 1つはコメントでの定義、もう1つは別ファイルでの定義です。後者はC言語のヘッダーファイルでの宣言に少し似ています。 コメントでの型定義(test.rb) # @type const Conference: Conference.module # @type var year: Integer conference = Conference.new(name: :RubyKaigi, year: 2017) year = conference.name 別ファイルでの型定義(hello.rbi) class Conference def initialize:(name: String, year: Integer) -> any def name: -> String def year: -> Integer end ※ 上記のコード例は発表者Soutaro Matsumotoさんの ブログ から引用しました。 この型チェックはsteepというgemになっており、誰でも試すことが出来ます。 型を明記できるのは画期的ですが、一方でRubyらしい簡潔さがやや失われてしまう印象も受けました。 しかし、ここで紹介された手法は、Ruby3に向けた型の議論に大きな影響を与えることになるでしょう。 参考: 発表者であるSotaro Matsumotoさんのブログ記事 発表中で紹介されていた、型チェックを行うためのgem steep Busting Performance Bottlenecks: Improving Boot Time by 60% Shopifyの @jules2689 さんによる、Rubyスクリプトの起動時間改善に関する発表です。 Railsの rails server の起動で数秒待たされた経験が誰しも1度はあるのではないでしょうか。 これを解消するために、まず原因の切り分けを行っています。 具体的にはコンパイル、YAMLの設定ファイル読込、定数参照に切り分けを行い、キャッシュにより起動時間を改善しています。 更にBundlerの速度がバージョン1.15で大きく改善されたことにも触れています。 スクリプトの起動で待たされることがなくなることで、エンジニアのモチベーションや生産性が改善されることが期待されます。 参考: 発表スライド Smalruby : The neat thing to connect Rubyists and Scratchers Kouji TakaoさんとNobuyuki Hondaさんによる、プログラミング初心者用の言語であるScratchと互換の機能をRubyで提供するという内容でした。 Scratchはifやwhileに対応するコード要素をブロックのように組み合わせてプログラミングを行う、Visual Programming言語です。 Scratchと同等の機能を持ったsmalrubyというライブラリを使い実際に島根で小・中学生にプログラミングを教えているそうです。 Scratchのコード要素をRubyに移植した時に、とても自然な形の表現になっていたのが印象的でした。 近年ではプログラミング教育への関心が高まり、2020年からは小学校でのプログラミング教育も始まる予定です。 そのような状況の中で、教育用言語としてのRubyを活用していこうという気概が感じられました。 参考: smalrubyのgithubリポジトリ Writing Lint for Ruby SideCiの @p_ck_ さんによる、静的解析ツールであるLintと、Rubyの静的解析ツールの一つであるRubocopについての話でした。 Lintとはコードを静的に(コードを実際に動かさずに)解析を行い、バグを発見するためのものです。 例えば、以下のようなコードは構文的にはOKですが、実行時にエラーが発生してしまいます。 if 10 < x < 20 このようなコードやエラーは発生しないけど、意味が曖昧になってしまいがちなコードに対して指摘するのがLintです。 この講演ではRubyコードを抽象構文木に変換し、簡単な解析を行うLintの実装や、Rubocopに新しいCop(ルール)を追加する方法について説明していました。 特にLintがコードを静的に解析することによる利点・欠点についての説明が興味深かったです。 実際にプログラムを動かしてメソッドの入出力を確認するテストだけでなく、高速かつ全てのコードを網羅的に確認することができるLintも組み合わせることで、より品質の高いプログラムを書くことができると感じました。 参考: 発表スライド ブログ Compacting GC in MRI Githubという小さなベンチャー(笑)で働いている @tenderlove さんによる、compaction GCに関する発表でした。 GC compactionを行うことによって、Page Faultを減らし、Unicornの動作速度を上げることを目標としていました。 プロセスをforkした直後は親プロセスと子プロセスは同じメモリを共有します。 そして、親子のプロセスの間でメモリに差異が生じた瞬間になって初めてメモリーのコピーが実行されます。 この仕組みはCopy on Writeと呼ばれ、これが発生すると、スピードの低下の原因となります。 Copy on Writeはメモリーのページという単位で行われるため、メモリー内のオブジェクトの配置が断片化していると、Copy on Writeが多くのページで発生してしまいます。 そのため、GCにメモリの断片化を解消するためのcompaction機能を実装したそうです。 オブジェクトの移動を行った後に、そのオブジェクトへの参照を書き換えるという操作を行います。 現状の仕組みでは、Hashのキーになっているオブジェクトや、文字列リテラルなどの移動はできず、典型的なRailsアプリケーションの場合は、約46%のオブジェクトが移動可能だそうです。 しかし、最終的にはCで書かれた拡張ライブラリから参照されているオブジェクト以外の全てのオブジェクトを移動可能にすることを目標としていました。 VASILYでもアプリケーションサーバーとしてUnicornを使用しているため、この機能の実現によるスピードアップが楽しみです。 Development of Data Science Ecosystem for Ruby Speeeの @mrkn さんによる、RubyからPythonの関数を呼ぶためのライブラリであるPyCallに関する発表でした。 Rubyで作られたシステムに機械学習などの機能を組み込む場合には、Rubyだけで頑張る方法と、データ処理部分をPythonで作ってその間をJSON APIで繋ぐという2種類の方法があります。 しかし、前者ではできることに限りがあり、後者ではデータ転送のコストが必要です。 そこで、第3の選択肢としてのPyCallの紹介をされていました。 PyCallではRubyから直接Pythonを呼び出すため、データを転送するコストを低く抑えることができます。 講演の最中にはPyCallを使った、幾つかのデモが行われていました。 その中でも特にSSD300のモデルを読み込ませ、物体検出を行っていたのが印象的でした。 また、その後に開催されたワークショップでは、ハンズオン形式でPyCallの使い方をレクチャーしていただきました。 使ってみての感想としては、予想以上にPython感がなく、すごく自然な記法でPythonのメソッドを呼び出せることに驚きました。 VASILYでもRuby on Railで作られたシステムの中に機械学習を活用した機能を組み込むことが多いため、これらの技術は今後活用する機会があるかもしれません。 参考: PyCallを使ったデモアプリ pandasで可視化を行っています API Development in 2017 Drecomの onk さんによる、JSON API開発についての発表でした。 HTTP通信を受け付けて、JSONを返すようなJSON APIをどのようにして効率的に開発するのかという内容でした。 JSON APIとnative clientを同時に作り結合テストをすると、思っていたのと違うJSONスキーマが返ってきて困った経験をしたことがある人は多いかと思います。 その1つの理由として、RESTfulなAPIはエントリーポイントに、どのリソースが返ってくるのかという情報は埋め込まれているものの、そのリソースがどんなJSON構造で返ってくるのかという情報がないことが挙げられます。 そのため、具体的なJSON構造はAPI serverのお気持ち次第ということになってしまいがちです。 この問題を解決するためにSchema First Developmentという手法を紹介していました。 この手法では、クライアント側とAPI側が最初にOpenAPI形式のSchemaを定義します。 そして、このSchemaに従うようにそれぞれの開発を行います。 Schemaを書かないと絶対にAPI開発ができないようにする仕組みを導入したことによって、Schamaが100%正しいドキュメントとして機能するようになったそうです。 さらに、Schemaとserializerの間で記述が重複していたため、SchemaからSerializerを自動生成したそうです。 また、RESTfulではないAPIとして最近注目されているGraphQLの紹介もありました。 OpenAPI形式はSwaggerで使われているSchema定義の形式であり、弊社のTECH BLOGにも紹介記事がいくつかありますので、よろしければそちらもご覧になってみてください。 tech.vasily.jp tech.vasily.jp まとめ VASILYエンジニアはRubyを使って、サービス開発をしたいエンジニアを募集しています。 興味ある方は以下のリンクからご応募ください。 おまけ 広島食べ歩きの写真などを紹介します。 弊社の技術顧問であるMatzさんとの記念写真 弊社CTO今村とマネキン餃子娘 どんなに飲んでも飲み足りない系エンジニアと特大レモンサワー 口いっぱいにおにぎりを頬張るフロントエンドエンジニア
こんにちは。iOSエンジニアの遠藤です。 9/15〜17にかけiOSDC Japan 2017が開催され、VASILYでもiOSチーム全員で参加しました。 また、私はLTとしてShift_JISのURLデコードについて発表させていただきました。 以下、発表資料です。 speakerdeck.com https://twitter.com/re___you スタッフ、スポンサー、スピーカー、参加者の皆さんお疲れ様でした。 とても楽しかったです! 今年はトラック数も多く、幅広いジャンルの発表がありました。 どの発表も面白く興味深い内容でしたが、今回はVASILYでも実装やプロダクトに組み込んでいきたい内容を含んだ発表についていくつか紹介します。 iOSDC Japan 2017 iOSDC(iOS Developers Conference)とはiOSと周辺技術を題材としたカンファレンスです。 iosdc.jp 発表紹介 以下、VASILYで取り入れたい内容を含む発表についての紹介です。 インタラクティブ画面遷移の実践的解説 具体例とクイズで学ぶ、Swiftの4種類のエラーの使い分け 両OSやるマンという選択 Build high performance and maintainable UI library RxSwiftのObservableとは何か 15分でわかるバックグラウンドアップロード インタラクティブ画面遷移の実践的解説 speakerdeck.com https://twitter.com/shmdevelop 概要 UIPercentDrivenInteractiveTransition を使用した画面遷移の話です。 モーダル画面、同一画面でのインタラクティブな画面遷移の実装方法を丁寧に説明してあります。 所感 画面遷移のカスタマイズをあまりしたことが無いのですが、実装方法や躓いた点を詳しく説明して頂いたのでとても参考になりました。 インタラクティブな画面遷移を実装することで、一気にアプリがリッチな雰囲気になるので取り入れてみたいです。 具体例とクイズで学ぶ、Swiftの4種類のエラーの使い分け speakerdeck.com https://twitter.com/koher 概要 Swiftで処理が失敗したことを表現する手段として例えば 返り値をOptionalにする throwsにする fatalErrorを使う preconditionを使う などが挙げられますが、それらを「エラーをどのようにハンドリングしたいか、あるいはさせたくないか」という視点から適切に使い分ける方法について解説したトークです。 所感 エラーをSimple domain errors / Recoverable errors / Universal errors / Logic failuresの4つの概念で理解を深めることができました。 それによって普段書いているコードがどれに当たるのかを正しく判断できるということが分かりました。 特にpreconditionとコード最適化の関係など知らなかった点があったので勉強になりました。 両OSやるマンという選択 speakerdeck.com https://twitter.com/jumboOrNot 概要 iOSとAndroidの両OSを一人で開発しているエンジニアの話です。
両OSのアプリを開発する上で、マーケット、デザインガイドライン、開発環境、CI、開発言語などの多くの視点での知見がまとめられたとても実用的な内容でした。 所感 エンジニア向けのイベントなので、SwiftとKotlinの言語的な違いに終始しがちなテーマだと思っていました。 しかし実際は、App Store / Google Playでのアプリの売れ方による違いや、双方のデザインガイドラインの特徴などにも触れられていて参考になりました。 iOS / AndroidエンジニアがこれからAndroid / iOSの開発を始めるにあたって、ざっくりその違いを理解するのにとても役立ちます。 Build high performance and maintainable UI library speakerdeck.com https://twitter.com/k_katsumi 概要 高速なUIを作るための実装についてと、メンテナンスしやすいコードについての発表です。 高速なUIを作る上で何がパフォーマンスを悪くしているのか、改善する上での注意点などの内容でした。 「メンテナンスしやすいコード」とは「テストしやすいコード」です。どのようにすればテストしやすいコードになるか例を用いて説明していました。 具体的には、依存が大きく単体でテストがしづらいViewをどのようにテストしやすいコードに分割できるかという内容でした。 所感 高速なUIを作るには、負荷の高い処理を減らすことが大事です。 しかしそれにはコードの可読性とのトレードオフになるのでしっかり見極めていきたいです。 メンテナンスしやすいコードについては、私たちはなかなかテストを書くことに時間を割けていませんが、テストを書くことでコードの信頼性を高めていきたいと感じました。 まずは、テストを書きやすくするために依存関係の少ないコードの設計、実装からスタートしていきたいです。 RxSwiftのObservableとは何か RxSwiftのObservableとは何か | iOSDC Japan 2017 from GOMI NINGEN www.slideshare.net qiita.com https://twitter.com/gomi_ningen 概要 RxSwiftを構成する要素の1つであるObservableに注目した内容です。 Obsevableを学びReactiveExtensionsについて理解を深めるために、 ObserverパターンからObservableの仕組みまでを丁寧に解説されていました。 所感 最近RxSwiftを使用して実装していますが、あまり内部の仕組みについて理解できていませんでした。 今回の発表ではコードを主体とした説明でとてもわかりやすく、Obsevableがどのようなものなのかを理解が深まりました。 発表で使用されていたコードを実際に動かしてより理解を深め、プロダクト実装で活かしていきたいです。 15分でわかるバックグラウンドアップロード speakerdeck.com https://twitter.com/yimajo 概要 バッググラウンドアップロードの仕様から実装、デバッグで役に立つツールについての発表です。 実装でのハマリポイントなどもあり実用的にまとまっている内容です。 所感 これまで私たちは収集したユーザーの行動ログを、 UIApplicationDelegate の applicationDidEnterBackground(_:) のタイミングでサーバーに送っていました。 しかし、ログが大きい場合全てを送信することはできず、アプリ起動時や復帰時に残ったログを送るようしています。実装が散らばってしまうので今回のバッググラウンドアップロードを使用して全てのログをバッググラウンドで送れるような仕組みにしていきたいです。 最後に 以上がiOSDC Japan 2017の中で取り入れたい内容の紹介でした。 実装やプロダクトに取り入れた際には取り入れてみての感想などを記事で紹介していきます。 ぜひお楽しみに! VASILYでは積極的に技術のインプットをしてプロダクトに取り入れていくエンジニアを募集しています。 興味のある方はこちらからご応募ください。 https://www.wantedly.com/projects/88978 www.wantedly.com
iOSエンジニアの庄司です。最近Android開発をはじめて、Android Studioのコード補完力の高さに驚かされています。 iOS11のリリースが間近ですが、今回は最近開発したiOSアプリで実装したアニメーションについてご紹介します。 こんなものを作りました GitHubにサンプルプロジェクトを上げておきました。 https://github.com/WorldDownTown/CurvingProgressBarSample ポイント残高や、工程の進捗率を表現したりするのに使えるViewです。 一見すると動きはシンプルなのですが、意外と複雑な実装になっているため説明していきます。 このアニメーションの動作ポイントは下記の4点です。 数値がパラパラと増える ゲージが増加する 数値によってテキストとゲージの色が変わる アニメーションにイージングをかける 何が面倒なのか アニメーション中に色が複数回切り替わる アニメーションの実装といえば、 UIView のクラスメソッドである animate(withDuration:animations:) が手っ取り早い方法です。 ですが、このメソッド実現できるアニメーションは frame や backgroundColor などの状態をA→Bに変更することです。 ゲージの長さを0→100にアニメーションさせている最中に、色が複数回切り替わるような実装は animate(withDuration:animations:) では実装できません。 UILabelの増加具合にもイージングをかける 上で述べたように、 UIView の animate(withDuration:animations:) は状態をA→Bに変更させることができます。 しかし、 UILabel の text をパラパラと切り替わるようなアニメーションを実現できません。 そこにさらにイージングをかけようと思うとひと苦労です。 どう解決するか CADisplayLink ゲージが増加する途中でゲージや文字の色を変更するために CoreAnimation.framework の CADisplayLink を使いました。 CADisplayLinkは画面のリフレッシュレートと同期して描画させるタイマーオブジェクトです。 ざっくり下記のようなコードになります。 (サンプルプロジェクト上では こちら ) displayLink = CADisplayLink(target : self , selector : #selector(updateTimer)) // ディスプレイ描画ごとに updateTimer が実行される displayLink.preferredFramesPerSecond = 60 displayLink.add(to : .current, forMode : .commonModes) displayLink.isPaused = false // アニメーション開始 @objc private func updateTimer () { let duration : TimeInterval = 1.0 // アニメーションは1.0秒 let elapsed : TimeInterval = Date.timeIntervalSinceReferenceDate - startTimeInterval let progress : CGFloat = (elapsed > duration) ? 1.0 : CGFloat (elapsed / duration) // アニメーション時間の進捗率 // cubic bezier let y : CGFloat = unitBezier.solve(t : progress ) // 0.0〜1.0 下で詳細を説明します progressBlock?(y) if progress == 1.0 { displayLink.isPaused = true // 一周したらアニメーションを止める } } ディスプレイの描画ごとに updateTimer() が呼ばれ、アニメーション時間に対する進捗率 ( y ) を計算してクロージャーに渡します。 クロージャー側では、渡された進捗率を元にゲージの量や色、ラベルのテキストを描画します。 イージングを実装 ゲージの増加だけであれば、 CAAnimation と CAMediaTimingFunction を組み合わせて使うことで幾つかの用意されたイージングを実装することができます。 しかし最初のGIFのように、ゲージの増加、色の変更、ラベルテキストの更新をイージングをかけながらアニメーションさせることはできませんでした。 この条件を満たしつつイージングをかけるために、自前のイージング処理を実装しました。 「自前のイージング処理」とは、アニメーションの経過時間を元にアニメーション自体の進捗率を計算する処理のことです。 ベジェ曲線 CAMediaTimingFunction と同じようなイージングを実装するには、ベジェ曲線の計算をすることになります。 (ベジェ曲線の基本は こちら ) ベジェ曲線といえば UIBezierPath を思い出します。 UIBezierPath はベジェ曲線を「描く」ことはできますが、描画したりアニメーションのパスに使うことしかできません。 今回イージングを実装するにあたって、 WebKitのベジェ曲線のC++実装 をSwiftで書き換えてみました。 処理が複雑で長いので、リンクだけ貼っておきます。 UnitBezier.swift こんな風に使います。 let curve : AnimationCurve = .easeInOut let unitBezier = UnitBezier(p1 : curve.p1 , p2 : curve.p2 ) @objc private func updateTimer () { let elapsed : TimeInterval = Date.timeIntervalSinceReferenceDate - startTimeInterval let progress : CGFloat = (elapsed > duration) ? 1.0 : CGFloat (elapsed / duration) // アニメーション進捗率を計算 let animationProgress : CGFloat = unitBezier.solve(t : progress ) progressBlock?(y) if progress == 1.0 { displayLink.isPaused = true } } さらに サンプルコードには、数種類のアニメーションカーブの種類を enum で用意しています。 enum AnimationCurve { case linear, ease, easeIn, easeOut, easeInOut, original(CGPoint, CGPoint) original(CGPoint, CGPoint) を使って、ベジェ曲線の制御点を設定することで自由にイージング具合を調整することができます。 まとめ 今回は複雑な変化を発生させるアニメーションの実装方法を紹介しました。 CADisplayLink とベジェ曲線をつかってイージングを自由にカスタマイズすることで、複数の要素に多方面にイージングを書けることができるようになります。 ゲージの描画やアニメーション開始までの具体的な処理など、紹介しきれていない実装がいくつもあります。サンプルプロジェクトの方も覗いてみてください。 また、アニメーションについて興味があれば、先月公開された Lottieの記事 も御覧ください。 さいごに 弊社では凝ったアニメーション実装が得意なアプリエンジニアを大募集しています。 興味をもっていただけましたら、是非Wantedlyからご応募お願いいたします。
こんにちは、VASILYバックエンドエンジニアの塩崎です。 RubyKaigi2017の開催時期が間近に迫っていますが、皆さんの広島グルメ探訪の予定はいかがでしょうか? 今年のRubyKaigiにはVASILYから4人が参加する予定で、そのうちの3人は初参加です。 発表の要旨はすでに公開されていて以下のページで確認できますが、まだどれを見て回ろうかを決めかねている人もいるかと思います。 http://rubykaigi.org/2017/schedule そこで、Rubyのパパであり、VASILYの技術顧問でもあるまつもとゆきひろさん(以下、Matzさん)にRubyKaigi2017の見所を聞いてみました。 この記事がRubyKaigiに参加をされる方々の参考になれば幸いです。 注) Matzさんは事前にすべての発表内容を正確に把握しているわけではないため、ここで紹介した内容がRubyKaigi2017で確実に発表されるわけではありません。 また、ここで紹介した発表以外にも素晴らしい発表がいくつもあるので、あくまで一つの参考としてご覧ください。 1日目 How Close is Ruby 3×3 For Production Web Apps? Hanami - New Ruby Web Framework Development of Data Science Ecosystem for Ruby 2日目 Progress of Ruby/Numo: Numerical Computing for Ruby Type Checking Ruby Programs with Annotations Automated Type Contracts Generation for Ruby Ruby Language Server 3日目 Compacting GC in MRI Bundler 2 まとめ 1日目 How Close is Ruby 3×3 For Production Web Apps? これはVASILYさん的には面白いかと思います。 Ruby 3で3倍早くなったかどうかを確認するためのベンチマークの話です。 早くなったことを検証するためには、いいベンチマークがないと意味がないんですよね。 マイクロベンチマークでは3倍になったけど、実際のアプリケーションでは5%しか早くなっていないみたいなのはよくありますよね(笑)。 でも、かといって、ものすごい複雑なベンチマークだと検証が大変ですよね。 そうならないように、ちょうど良いベンチマークを作るように彼に依頼を出しています。 Hanami - New Ruby Web Framework 午後の休憩後にはHanamiの話がありますね。 Ruby on Railsに対するアンチテーゼで新しいフレームワークはこんな感じだよという発表かと思います。 まぁ、私自身はweb開発をしないんで、どちらが優れているかって聞かれてもよくわからないんですが(笑)。 こういうフレームワークもあるんだよという意味では面白いかもしれませんね。 Development of Data Science Ecosystem for Ruby あと、初日ではmrknさんの発表がオススメですね。 データサイエンティストがコーディングをするときにはPythonを使うことが多いと思うのですが、データサイエンスをRubyでやるにはどうやってやるのかの話をすると思います。 現状では3つのアプローチがあるんですが、そのうちの1つであるRubyからPythonの関数を呼ぶ方法の話です。 ちなみに残りの2つはSciRubyとApacheArrowですね。 SciRubyはSciPyやNumPyをRubyで実装するというアプローチです。 3つの中で1番筋は良さそうなんですが、いばらの道ですね。 というのも、Pythonの方が先を走っているので、追いつくためにはPythonよりも早く走らなければならないんです。 ですが、Python側の方が開発者は多いんですよね。 とはいえ、頑張っている人たちもいます。 最後のApacheArrowはプログラミング言語間でデータをやり取りするためのフォーマットです。 ファイルのフォーマットとメモリのフォーマットの両方があります。 計算処理はPythonにやらせてApacheArrow形式で書き出しておくと、Rubyで読むことができます。 いちいちファイルに書き出して、Ruby側でトラバースすると遅いのでそういうコストがなくて早いと思います。 あと、列指向なので、データサイエンスの分野での効率が良いとも聞いています。 2日目 Progress of Ruby/Numo: Numerical Computing for Ruby さっきの話の2つ目のアプローチを採っているのがNumoですね。 数値を効率的に扱うことができるデータ構造です。 いばらの道を頑張っている人です。 Type Checking Ruby Programs with Annotations この発表も面白いと思います。 発表者の松本宗太郎さんは博士論文でRubyに型をつけるための研究をした人ですね。 結果的にはうまくいかなかったんですが、TypeAnnotationをつけて頑張ろうっていうのを余暇でやっているらしく、その発表をしてくれます。 Ruby 3で導入しようとしている型システムは型推論だけでどうにかしようというアプローチなんですが、それとは別のアプローチですね。 Automated Type Contracts Generation for Ruby これも型に関する発表ですね。 Ruby 3とは違うアプローチで型をつけようとしています。 2010年代のプログラミング言語は型ありなのが当たり前になってきているので、Rubyはだいぶ置いてけぼり感(笑)があります。 ですので、コミュニティ内での危機感も高いと思います。 Ruby Language Server Ruby Language Serverに関する発表もありますね。 LanguageServerっていうのはエディターと通信して、リアルタイム構文解析やコード補完の機能を提供するものです。 もともと、コンパイラとエディタは独立しているので、IDEとかは内部的にコードを解析してエラーが出た時にはポップアップを出しますよね。 そういう機能を任意の言語でできるようにするのがLanguage Serverです。 これに対応するとLanguage Serverプロトコルに対応しているあらゆるエディタでコード補完が使えるようになります。 開発効率アップの可能性がありますね。 3日目 Compacting GC in MRI この発表ではRubyのGCの話をするんですが、RubyのGCってCompactionしないんですよね。 Cのスタックでは数値であるかポインタであるかの情報を持っていないので、そこから参照されているオブジェクトを動かすことができないんですよ。 でも、Rubyのオブジェクトからしか参照されていないオブジェクトだったらこの問題が発生しないので動かせるという発想ですね。 CompactionによってRubyがより省メモリになる可能性があります。 Bundler 2 あと、私は使わないけど、bundlerの話もいいかもね。 近いうちにbundlerが標準Rubyに組み込まれることが決まりました。 実はRubyGemsがbundlerに依存するという話が先に出たんです。 そうなるとbundlerも標準Rubyにバンドルするしかないという感じで決まりました(笑)。 まとめ Rubyの開発者であるMatzさんに今年のRubyKaigiでのオススメな発表を伺いました。 どの発表も興味深い内容なので、RubyKaigiが待ち遠しいです。 VASILYではRubyKaigiなどのカンファレンスに参加し、最新技術を取り入れることに積極的なエンジニアを募集しています。 興味のある方は以下のリンクからご応募ください。
こんにちは。インフラエンジニアの光野です。 弊社ではネットワークの構築と管理に AWS CloudFormation と AWS OpsWorks を導入しました。 本記事では、その効果と導入に際しての工夫について紹介いたします。 目次 Before / After 効果 CloudFormarionとは CloudFormation or Terraform OpsWorksとは SSH/sudo管理 CloudFormationとOpsWorksの役割分担 CloudFormationテンプレートの分離方針 OpsWorksマルチレイヤーによるインスタンスの管理 OpsWorksでのdry-runとdiff YAML版CloudFormationでOpsWorks(Chef)を定義する場合の注意点 まとめ Before / After CloudFormationとOpsWorksを導入するまで、弊社のネットワーク管理は以下のようになっていました。 ネットワークの構築やChefの実行は全て手作業です。これがCloudFormationとOpsWorksの導入後は以下のようになりました。 GitHubにpushすると、syntax checkが走ります。masterにマージされるとCloudFormationにchange setが作成されるため、変更内容を確認して反映させます。 各インスタンスの設定はOpsWorksで管理されており、任意のタイミングでChef実行の依頼を行います。リソース変更やChefコマンドの実行を手作業で行うことはありません。 なおchange setとはこれからどのリソースがどのように変更されるか(中断なし・一時的に中断・置換)を事前に知ることができるものです。 意図しない変更を避けるために最後だけは人の目による変更承認フローを挟むようにしています。 効果 CloudFormationとOpsWorks導入で大きく3つの効果がありました。 再利用性の向上 ノウハウの蓄積 履歴管理 再利用性の向上 OpsWorksごとCloudFormationを使って定義することで、ネットワークとインスタンスの構成管理が一枚のテキストで完結するようになりました。 Webコンソールが気を利かせてくれていた部分も含めてテキストで記述するので、イニシャルコストは大きいですが一度作ってしまえばその後は楽ができます。 アプリケーションは案件ごとに変わっても、ネットワーク要素は定形であることが多く、初期構築の時間を別のことへ回せるようになりました。 ノウハウの蓄積 感覚値として、EC2インスタンスを立てるくらいであれば手作業の方が若干早いです。 上で挙げた通り、複製が簡単。初期構築のハードルが下がる。というのは事実ですが、CloudFormationに作業時間の短縮を期待していると意外に裏切られます。 導入によって得られる効果は、ノウハウの蓄積です。RDSやElastiCacheのパラメータグループのように、設定を忘れると後で痛い目をみる項目も全てテキストで管理されます。そのため、別の誰かへノウハウを伝えるということができるようになりました。 また、後で悩みがちなセキュリティグループやIAMのルールについては積極的にコメントで補足しています。 履歴管理 CloudFormation・OpsWorks共に実行履歴がすべて記録されます。これまではいつどの何を変更したのかを思い出すのに苦労していましたが、今は記録が残るので重宝しています。 CloudFormationとは AWS CloudFormationは、AWSリソースをJSON/YAMLで表現したテンプレートを元に、その構成を自動で構築してくれるサービスです。2016年9月のアップデートでYAMLに対応し可読性と保守性が高まりました。 以下のYAMLは「VPC」「Subnet」「RouteTable」を作って関連付けるサンプルテンプレートです。テンプレートを元に作られるAWSリソースのグループをスタックと呼びます。 --- AWSTemplateFormatVersion : 2010-09-09 Parameters : VPCCidrBlock : Type : String Default : '10.0.0.0/16' PublicFirstCidrBlock : Type : String Default : '10.0.0.0/24' Resources : # VPCを作成 EC2VPC : Type : 'AWS::EC2::VPC' Properties : # [ NOTE ] 作成時にCidrBlockを指定することでテンプレートを使いまわすことができる CidrBlock : !Ref VPCCidrBlock EnableDnsSupport : true EnableDnsHostnames : true InstanceTenancy : 'default' # [ NOTE ] 組み込み変数を参照して定形の名前をつける Tags : - Key : 'Name' Value : !Sub '${AWS::StackName}-vpc' # RouteTableを作成 EC2RouteTablePublic : Type : "AWS::EC2::RouteTable" Properties : VpcId : !Ref EC2VPC Tags : - Key : 'Name' Value : !Sub '${AWS::StackName}-route-table-public' # Subnetを一つ作成 EC2SubnetPublicFirst : Type : "AWS::EC2::Subnet" Properties : # [ NOTE ] スタックを作るリージョンの0番目のAZにSubnetを作る。東京ならaz-a AvailabilityZone : Fn::Select : - 0 - Fn::GetAZs : !Ref 'AWS::Region' CidrBlock : !Ref PublicFirstCidrBlock MapPublicIpOnLaunch : true Tags : - Key : 'Name' Value : !Sub '${AWS::StackName}-public-subnet-first' VpcId : !Ref EC2VPC # SubnetとRouteTableの対応付け EC2SubnetRouteTableAssociationPublicFirst : Type : "AWS::EC2::SubnetRouteTableAssociation" Properties : RouteTableId : !Ref EC2RouteTablePublic SubnetId : !Ref EC2SubnetPublicFirst CloudFormation Designerというテンプレートを視覚的に表示・編集できるツールもあります。これは上のYAMLを読み込ませた例です。 CloudFormation or Terraform ネットワークの構成管理というと Terraform by HashiCorp が有名です。弊社でも導入前に検討を行いました。 CloudFormation Terraform 記述言語 JSON/YAML 独自のDSL dryrun・変更内容の確認 ある(変更セット) ある(plan) 新機能への追従 数か月遅れ コミュニティ次第(自力でも何とかできる) 対応するサービス AWS 様々 Terraformが登場した2014年頃は「CloudFormationではできないことができる」という印象がありましたが、この数年でその差もだいぶ無くなりました。 最終的に、CloudFormation採用の決め手としたのはdryrunの安心感です。ドキュメントを読むとプロパティ1つずつに変更の反映方法が記載されています。 VPCを作成するというテンプレートを例にすると、 CidrBlock と InstanceTenancy は変更すると「置換」という更新が行われます。この場合、新しいリソースが作成されたあと、古いリソースが削除されます。副作用のある更新が事前にわかるのは大変重要です。 EC2VPC : Type : 'AWS::EC2::VPC' Properties : CidrBlock : !Ref VPCCidrBlock # 置換 EnableDnsSupport : true # 中断なし EnableDnsHostnames : true # 中断なし InstanceTenancy : 'default' # 置換 Tags : # 中断なし - Key : 'Name' Value : !Sub '${AWS::StackName}-vpc' cf. AWS EC2 VPC - AWS CloudFormation 一方で、CloudFormationはAWSの新機能への追従がそれほど速くありません。Regional WAFへの対応は1.5か月ほどでしたがこれは比較的早いという印象です。 cf. AWS WAF on ALB with Cloudformation - AWS Developer Forums 特に告知があるわけでもないため、新機能から数ヶ月が経ち、ふとドキュメントを見ると追加されているといったことがあります。 OpsWorksとは AWS OpsWorksスタックとAWS OpsWorks for Chef Automateの2つがあり、弊社で利用しているのは前者です。 Stack > Layer >Instances という階層構造でインスタンスを管理します。 OpsWorksは管理下のインスタンスに対してChefを実行することで構成管理を行います。Chefの実行対象は1インタンスからスタック全台同時まで自由に選べます。 また同一スタック間に所属するインスタンス間では /etc/hosts が常に同期されておりSSHするときに大変便利です。 $ hostname gateway1 $ cat /etc/hosts # This file was generated by OpsWorks # any manual changes will be removed on the next update. # OpsWorks Layer State 127.0.0.1 localhost.localdomain localhost 127.0.1.1 gateway1.localdomain gateway1 < private ip > gateway1.localdomain gateway1 < public ip > gateway1.localdomain-ext gateway1-ext < private ip > web1.localdomain web1 < private ip > web2.localdomain web2 SSH/sudo管理 OpsWorksが提供するのはChefの実行だけではありません。OSがLinuxであれば、IAMユーザとLinuxのユーザを紐付けて管理することができます。 公開鍵を登録すると、IAMユーザに対応したSSHユーザがスタック単位で作られます。 あとは、チェックボックスでSSH許可・sudo許可を選択するだけでそのユーザがインスタンス側に作成されます。 CloudFormationとOpsWorksの役割分担 CloudFormationでもヘルパースクリプトを使うと、インスタンスに対してパッケージのインストールなどを行うことができます。 しかし、継続的に運用するのであれば、diffを見たりdry-runをしたり何が起こるのかを事前に把握してから変更を加えたくなります。そのため、インスタンス内の構成管理は全てOpsWorks(Chef)に集約する形を取りました。OpsWorks自体の設定はCloudFormationで行えるため、いずれの場合でも設定が散逸することはありません。 OpsWorksLayerMemcached : Type : "AWS::OpsWorks::Layer" Properties : # ... CustomRecipes : Setup : - 'memcached' CustomJson : memcached : cachesize : 6554 listen : 0.0.0.0 maxobjsize : 1m # ... CloudFormationでOpsWorksレイヤーと、Chefレシピに渡すattributesを定義しています。 CloudFormationテンプレートの分離方針 CloudFormationを使う上で悩ましいのは、テンプレートをどのように分離するかということです。 同一テンプレート内であれば、組み込み関数を使って互いを参照することが可能です。 そのため、可能な限り1つのテンプレートでリソース定義を行う方が依存を解決しやすくなります。 一方で、1つのテンプレートに全てを記述すると再利用性を損ねる可能性があります。たとえば個人ごとのIAMユーザは、あるアカウント内で1つにしておきたいものです。 そこで、IAMなどをまとめたテンプレートとVPCを基準に分離するテンプレートの2つに分けて定義することにしました。 便宜上、前者をインフラテンプレート、後者をサービステンプレートと呼んでいます。 個人ごとのIAMユーザやグループ、ACMの証明書やCloudTrailはインフラテンプレートにまとめます。 一方、VPCやS3、EC2インスタンスやIAMインスタンスプロファイルはサービステンプレートにまとめます。 開発に伴ってステージング環境や負荷検証用の環境が欲しい場合は、プロパティを変えつつ同じサービステンプレートからスタックを量産します。 また、この時インフラテンプレートで宣言した要素は クロススタック参照 を使って、サービステンプレートで参照させています。 # インフラテンプレート Resources : # ... IAMRoleRdsEnhancedMonitoring : Type : "AWS::IAM::Role" Properties : # ... ManagedPolicyArns : - 'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole' # ... Outputs : OutputIAMRoleRdsEnhancedMonitoringArn : Value : !GetAtt IAMRoleRdsEnhancedMonitoring.Arn Export : Name : !Sub "${AWS::StackName}-iam-role-rds-enhanced-monitoring-arn" # サービステンプレート Parameters : IAMRoleRDSEnhancedMonitoringArn : Type : String Default : '<stack name>-iam-role-rds-enhanced-monitoring-arn' Resources : # ... RDSDBInstance : Type : "AWS::RDS::DBInstance" Properties : # ... MonitoringRoleArn : Fn::ImportValue : !Ref IAMRoleRDSEnhancedMonitoringArn # インフラテンプレートの要素を参照 参照先をべた書きせず組み込みのRef関数で解決したいというのが主な理由ですが、不用意な要素の削除を禁止するという狙いがあります。 クロススタック参照で参照された値は、すべての参照が消えるまで要素の削除が禁止されます。 この例の場合だと、サービステンプレート中で IAMRoleRdsEnhancedMonitoringArn が消えない限り、インフラテンプレートから IAMRoleRdsEnhancedMonitoring を消すことができません。 OpsWorksマルチレイヤーによるインスタンスの管理 OpsWorksでは1つのインスタンスを複数のレイヤーに所属させることが可能です。弊社では、これを積極的に使っており共通の設定をまとめたレイヤーを作成し、全てのインスタンスは必ずそこへ所属させています。この共通設定をまとめたレイヤーは便宜上コモンレイヤーと表記します。 コモンレイヤーの目的は3つです。 Chefの記述をDRYに行うため レイヤー同士の関係性を明らかにするため CloudWatch Logsの設定を簡易にするため Chefの記述をDRYに行うため 元々弊社では、設定をDRYに行うことを目的にChefのロールに親子関係を持たせて管理していました。 base configure # カーネルパラメータや監視設定 -> API configure # Rubyやnginxの設定 -> サービスAのAPI configure # ホスト名やサービス依存の設定 -> サービスBのAPI configure 同様のことをOpsWorksで実現するため、 base configure に相当する部分をコモンレイヤーの設定にまとめ、全インスタンスを所属させています。 レイヤー同士の関係性を明らかにするため 次に、レイヤー同士の関係を明らかにするためです。これはOpsWorksにすることで前々からの課題が解決されました。 例えば、APIとBATCHで同じ親を持っているとします。 2階層であれば憶えられますが、階層が深くなってくると思わぬところで知らない依存ができている可能性があります。 OpsWorksではデプロイ画面でインスタンスを選択すると所属するすべてのレイヤーでチェックがつくため、これが視覚的に分かりやすくなりました。 ただし、OpsWorksのレイヤー自体に親子関係という考え方はありません。親と子に相当するレイヤーをインスタンスに割り当てる部分は、自らで管理する必要があります。後から変更することもできますが、インスタンスが再起動するので気軽には行なえません。 CloudWatch Logsの設定を簡易にするため 最後はCloudWatch Logsとの連携を簡易にするためです。 OpsWorksではレイヤー単位で任意のログを指定して、CloudWatch Logsへ集約することができます。 これはレイヤー単位で設定するものなので、「全台のauth.logを回収したい」という時は、全レイヤーにその設定をして回る必要があります。この項目はCloudFormationで設定できないので手作業です。コモンレイヤーがあれば、一度設定してやるだけで全台で回収してくれます。 OpsWorksとdryrunとdiff OpsWorksによるChefの実行は大変便利なのですが、2つ難点があります。 dryrun(ChefではWhy-runと呼ぶ)ができないこと 変更の差分が見えないこと 実行ログにこういったdiffも出力されないため、成功しても何がどう変わったのかがわかりません。とても不安です。 そこでdryrunとdiffが利用できるようにChefレシピを書いてコモンレイヤーで読み込むことにしました。 dryrun こちらは簡単です。 Chef :: Config [ :why_run ] = node[ ' opsworks_helper ' ][ ' whyrun ' ] Chef::Config[:why_run] が true の時、Chefはdryrunモードになります。 OpsWorksはChef実行時に追加でattributesを渡せるので、そこにtrue/falseを入れてやります。 { " opsworks_helper ": { " whyrun ": true } } diff こちらは少し面倒です。 結論から言えば、OpsWorksが使うChefラッパーを書き換えることで解決します。 以下が、実際に使っているパッチです。 --- chef_command_wrapper.sh.org 2017-07-26 19:39:01.000000000 +0900 +++ chef_command_wrapper.sh 2017-07-26 19:48:45.000000000 +0900 @@ -1,4 +1,4 @@ -#! /bin/bash +#!/bin/bash # Wrapper for chef to catch STDOUT and STDERR into a file, # necessary because this is run with sudo @@ -92,9 +92,8 @@ echo -e "$LOG_LINE_TO_PREPEND" >> "$CHEF_LOG_FILE" fi -exec &> >(tee -a "$CHEF_LOG_FILE") 2>&1 -RUBYOPT="$_RUBYOPT" "$CHEF_CMD" -j "$JSON_FILE" -c "$CHEF_CONFIG" $RUN_LIST -CHEF_RETURN_CODE=$? +RUBYOPT="$_RUBYOPT" "$CHEF_CMD" -j "$JSON_FILE" --format doc -l warn -c "$CHEF_CONFIG" $RUN_LIST 2>&1 | tee -a "$CHEF_LOG_FILE" +CHEF_RETURN_CODE=${PIPESTATUS[0]} if [ -n "$LOG_LINE_TO_APPEND" ] then 重要なのは、execの代わりにパイプを使うことと、 PIPESTATUS を使う部分です。 OpsWorksがChefを実行する場合、 chef_command_wrapper.sh を使います。このスクリプトの実体は /opt/aws/opsworks/current/bin/chef_command_wrapper.sh にあり、Chefを実行する直前に標準出力をexecでteeに流しています。ここのexecがdiffを闇に葬っています。これを回避するため上のパッチではexecを消しパイプでログを受け取るよう変更します。 そして、パイプにしたことで $? ではChefのExitコードが受け取れなくなりました。OpsWorksは CHEF_RETURN_CODE で成功・失敗を判断しているため、常に成功扱いになってしまいます。そのため PIPESTATUS を使ってパイプで繋いだChefのExitコードを取得するようにします。 このパッチもコモンレイヤーのChefレシピに含まれており、サーバ起動後2回目のChef実行からdiffが有効になります。1回目の実行はまっさらなインスタンスが初期化されるだけなので問題になりません。 YAML版CloudFormationでOpsWorks(Chef)を定義する場合の注意点 YAMLが使えるようになったことで、人間的な設定ファイルを書くことができるようになりました。 ただし、YAMLを使ってCloudFormationでOpsWorks(Chef)を設定する場合は注意する必要があります。 attributesがすべて文字列になってしまうという点です。 例えば mackere-agent が勝手に起動しないよう、レシピで start_on_setup を与えたいと思います。 OpsWorksLayerCommon : Type : "AWS::OpsWorks::Layer" # ... CustomJson : mackerel-agent : start_on_setup : false しかしこれは期待通りに動作しません。JSONとして次のように評価されます。 { " mackerel-agent ": { " start_on_setup ": " false " } } falseが文字列です。レシピの中で"false"は真の扱いとなり、 start_on_setup=true になってしまいます。これは他のレシピでも起こりえます。 そのため、場合によってはYAMLの中でJSONを記述してやる必要があります。 OpsWorksLayerCommon : Type : "AWS::OpsWorks::Layer" # ... CustomJson : | { "mackerel-agent" : { "start_on_setup" : false } } まとめ 弊社ではネットワークの構築と管理にAWS CloudFormationとAWS OpsWorksを導入しました。 その結果、手作業で行っていた作業の大部分がなくなり、作業履歴の記録やノウハウの蓄積が可能になりました。 また、CloudFormationとOpsWorksを使ってより効率的に管理するためには幾つかの工夫が必要でした。 本記事が利用を検討している方の一助となれば幸いです。 さいごに 弊社では既存の枠組みにとらわれず、理想の形に向かって挑戦できるインフラエンジニアを大募集しています。 興味をもっていただけましたら、是非Wantedlyからご応募お願いいたします。
こんにちは、バックエンドエンジニアのじょーです。 以前、月額課金型のサーバーサイドでのレシート検証の記事を書きました。( iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ ) 今回は、消耗型課金のサーバーサイド実装について書きます! 注意 この情報は2017年8月25日現在のものです。 目次 消耗型課金全体の処理フロー レシート検証について 課金アイテムの扱い方について 消耗型課金全体の処理フロー 消耗型課金とは、AppStoreで登録できる使い切りのアイテムへの課金のことをいいます。 たとえば、ゲームアプリでライフを購入するときなどは使い切りのアイテムなので消耗型課金になります。一方、1か月など決まった期間サービスが受けられる課金のことを月額課金や、自動更新購読といったりします。 (実際のアイテム登録画面) アプリで消耗型課金商品を購入してからの処理の順番は、下記の図のようになります。 アプリからAppStoreへ購入リクエスト AppStoreからレシートを受取る サーバーサイドへBase64エンコード済みのレシートを投げる サーバーサイドはBase64エンコード済みのレシートを使ってAppStoreにレシート問い合わせ AppStoreからJSON形式のレシートを受取る レシート情報を元に、購入が不正に行われたものでないかを検証 正常な購入であれば購入結果をDBに反映 アプリに検証結果を返却 処理完了のリクエストをAppStoreへ送信 また、本記事ではサーバーサイドで行う処理に着目しているため、工程の①〜③に関しての詳細説明は割愛し、サーバーサイドで行う④〜⑥のレシート検証処理の工程を詳しく説明していきます。 レシート検証について レシート検証とは? レシート検証とは、アプリから購入リクエストが届いた際、アプリを介さずにAppStoreに直接購入情報を問い合わせることで、不正な購入や意図しない購入でないかを検証することです。この処理によって、アプリから偽装されたレシートが送られてきた場合や二重の購入、購入処理の漏れなどを防ぐことができます。 アプリで商品を購入した場合、課金の証明としてAppStoreがレシートを発行します。 レシートと言ってもAppStoreが紙のレシートを送りつけてくるわけではなく、電子的な購入情報のことをレシートと呼びます。 その際に、AppStoreのサーバーにHTTPのPOSTリクエストでJSON形式のレシートを問い合わせ、現在の課金状況を知ることができます。その結果を元に、レシートが不正なレシートでないかをサーバーサイドでチェックします。 レシート問い合わせの方法 処理フローでの④,⑤について説明します。 レシート情報の問い合わせは、AppStoreのサーバーにHTTPのPOSTリクエストを送ることで問い合わせることができます。 リクエストURL テスト環境用のURLと、production用のURLで分かれています。 環境 URL 用途 production https://buy.itunes.apple.com/verifyReceipt 本番用 sandbox https://sandbox.itunes.apple.com/verifyReceipt 開発時のテスト環境用 sandboxにはsandbox用の、productionにはproduction用のレシートがあり、productionのURLにsandbox用のレシートを送るとエラーが返ってきます。 sandboxを利用するには、Appleのテストアカウントを取得して課金処理のテストを行います。 リクエストbody 下記の1つだけでOKです。 key 値 サンプル receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI... receipt-dataのサンプルは省略してありますが、実際はかなり長いです。12KB程度のデータです。 処理フローの③で示した通り、クライアントからBase64エンコード済みのデータを受け取ります。購入時にクライアントからレシートを送ってもらわないとサーバーサイドとAppStoreで直接レシート検証のやり取りができません。 AppStoreから返ってくるレシート情報の項目 下記が、レシートを問い合わせた際に返ってくるレシートサンプルです。処理フローでいうと⑤でAppStoreが返す情報です。 RubyのHash型で記してありますが、実際にはJSON形式で返ってきます。 { " status " => 0 , " environment " => " Sandbox " , " receipt " => { " receipt_type " => " ProductionSandbox " , " adam_id " => 0 , " app_item_id " => 0 , " bundle_id " => " jp.hoge.hoge " , " application_version " => " 1 " , " download_id " => 0 , " version_external_identifier " => 0 , " receipt_creation_date " => " 2017-07-18 04:03:48 Etc/GMT " , " receipt_creation_date_ms " => " 1500350628000 " , " receipt_creation_date_pst " => " 2017-07-17 21:03:48 America/Los_Angeles " , " request_date " => " 2017-07-18 05:32:59 Etc/GMT " , " request_date_ms " => " 1500355979599 " , " request_date_pst " => " 2017-07-17 22:32:59 America/Los_Angeles " , " original_purchase_date " => " 2013-08-01 07:00:00 Etc/GMT " , " original_purchase_date_ms " => " 1375340400000 " , " original_purchase_date_pst " => " 2013-08-01 00:00:00 America/Los_Angeles " , " original_application_version " => " 1.0 " , " in_app " => [ { " quantity " => " 1 " , " product_id " => " productのid " , " transaction_id " => " 1284721948247 " , " original_transaction_id " => " 1000000316178057 " , " purchase_date " => " 2017-07-18 03:20:05 Etc/GMT " , " purchase_date_ms " => " 1500348005000 " , " purchase_date_pst " => " 2017-07-17 20:20:05 America/Los_Angeles " , " original_purchase_date " => " 2017-07-18 03:20:05 Etc/GMT " , " original_purchase_date_ms " => " 1500348005000 " , " original_purchase_date_pst " => " 2017-07-17 20:20:05 America/Los_Angeles " , " is_trial_period " => " false " }, { " quantity " => " 1 " , " product_id " => " productのid " , " transaction_id " => " 1284721948248 " , " original_transaction_id " => " 1000000316185518 " , " purchase_date " => " 2017-07-18 04:03:48 Etc/GMT " , " purchase_date_ms " => " 1500350628000 " , " purchase_date_pst " => " 2017-07-17 21:03:48 America/Los_Angeles " , " original_purchase_date " => " 2017-07-18 04:03:48 Etc/GMT " , " original_purchase_date_ms " => " 1500350628000 " , " original_purchase_date_pst " => " 2017-07-17 21:03:48 America/Los_Angeles " , " is_trial_period " => " false " } ] } 今回は、レシート検証(処理フローで言う④,⑤の部分)に venice というgemを使いました。こちらのgemを使うと、開発環境ではsandboxを、本番環境ではproductionのエンドポイントを叩いてくれたり、JSONをパースしてRubyのHash型でレシートの値を返してくれます。 検証項目 ⑤で返ってきたJSON形式のレシートを使って、サーバーサイドで検証すべき内容を検証します。処理フローで言うと、⑥の部分です。 レシートから返ってきた項目のうち検証すべき項目は以下です。 項目 項目の内容 検証内容 status 0であれば正常なレシート、その他は不正なレシート( エラーコード表 参照) AppStoreから正常なレシートが返ってきているか in_app 購入情報の配列 課金処理すべき購入情報のチェック in_app: transaction_id in_appの1購入ごとに存在する固有のid すでに処理されたレシートでないか bundle_id iTunesConnectで設定したCFBundleIdentifierの値 自分のアプリのものか product_id iTunesConnectで設定したproductIdentifierの値 意図した商品への課金か、存在している商品への課金か 消耗型課金の場合、上記の表以外の項目はデバッグに用いる値と考えて良いと思います。他に検証すべき項目があればコメントをください。よろしくお願いします。 これらの項目の中で注意すべきは、 in_app キーの中身です。 in_app の中身は、購入1回分の購入情報の配列です。注意しなければならないポイントは、 in_app の配列の中身が複数返ってくることがあるというところです。 AppStoreでは、①〜⑨までの処理にトランザクションをはっており、どこかで通信エラーやサーバーエラー等のエラーが起きて⑨の完了処理を実現できなかった場合、トランザクションが完了せずに未処理の購入となります。 未処理の購入は下記の2パターンの方法で再度処理を行う必要があります。 ⑨が実現できていないという情報がStoreKitからアプリに通知されたタイミングで再度③から課金処理を行う 別のアイテムの購入処理をした際に、AppStoreから未処理の購入情報も一緒に返ってくるのでそのタイミングで課金処理を行う(トランザクションが完了していない購入がある場合同じアイテムは購入できない) パターン2の場合、別の購入処理で問い合わせたレシートの in_app の中に未処理の購入が含まれて返ってきます。 この場合、 in_app の中に返ってくる購入情報は新しく購入されたか、一度処理を失敗して残っている情報なので in_app の中身すべてを処理する必要があります。 また、下記の図のように、サーバーサイドのアイテム付与の処理はすべて終了しているにも関わらず、⑧で通信が失敗してしまい、⑨の処理が完了していないというパターンが存在します。その際、サーバーサイド側に過去に処理した transaction_id を保持しておき、過去に処理した transaction_id であればサーバーサイドでは二度同じ処理をせずに⑨の購入完了処理のみをアプリ側に行ってもらうようにしておく必要があります。 課金アイテムの扱い方について 消耗型課金は、課金アイテムが一つでない限り、課金アイテムのリストをアプリに表示する必要があります。たとえば、ルビー1200個購入で10000円、12個で120円など、アイテムが複数ある場合です。 その際に、AppStoreにないアイテムはそもそも購入できないため、アイテムがAppStoreに存在しているかをチェックしてからアイテムリストをアプリに表示します。 また、逐一AppStoreにアイテムリストを問い合わせる理由として、価格の変化があります。AppStoreで登録した商品は、為替レートの変動により価格が変わることがあるので、毎回現状の価格をAppStoreに問い合わせなければアプリで表示している価格と実際にAppStoreで決済される価格が食い違ってしまいます。 AppStoreへの課金アイテム問い合わせフロー このような手順で課金アイテムリストを表示します。 AppStoreにアイテムを登録する際、アイテムを識別するユニークなkey名を product_id と呼びます。 また、課金アイテムリストは、サーバーサイドでも管理します。 AppStoreへアクセスすればサーバーサイドでアイテムリストを管理する必要がないのでは?と思うかもしれませんが、サーバーサイドでもアイテムリストを持ったほうがいい理由は3つあります。 AppStoreでは最小限の商品情報しか持てない たとえば、 ルビー1200個購入で10000円 という商品の場合、 ルビー1200個 という情報はAppStoreでは登録できず、参照名、product_id(製品ID)と価格くらいしか保持していません。(下図参照) よって、サーバーサイドでproduct_idと商品の内容を紐付けたテーブルをDBに持つ必要があります。 課金履歴などのデータを保持しやすくなる 期間限定のアイテムなどを配信したい際にサーバーサイドでコントロールできる (課金アイテムの登録画面) 参照名、製品ID(product_id)、価格が登録できます。 まとめ 本記事では、消耗型課金におけるサーバーサイドに必要な実装について説明しました。 参考資料 In-App Purchaseプログラミングガイド Validating Receipts With the App Store ※ バージョンによって仕様が変わるので、もともとの仕様書をよく読むことをおすすめします。 最後に VASILYでは、積極的に挑戦していけるエンジニアを募集しています。興味のある方は以下のリンクからぜひご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
こんにちは、データチームの後藤です。 VASILYデータチームは2017年8月7日〜10日にかけて、広島で行われた第20回画像の認識・理解シンポジウム(以下、MIRU2017)に参加しました。本記事では、発表の様子や参加した感想をお伝えしたいと思います。 MIRU2017 MIRUはMeeting on Image Recognition and Understandingの略で、国内最大規模の画像の認識と理解技術に関する会議です。事前に選定された口頭発表と国際会議の採択論文から選ばれた招待講演を中心に、ポスター発表、デモ発表、特別講演で構成されます。 今年の投稿件数は、以下のような内訳でした。 口頭発表:23件 一般論文:206件 デモ論文:13件 招待講演:12件 企業展示:15件 VASILYのデータチームは「トリプレット損失関数の重み付けによる学習の効率化」というタイトルで一般論文の枠で投稿し、4日目の8月10日にポスター発表を行いました。 発表 ポスターセッションでは始終、多くの人に話を聞いていただき、VASILYの取り組みを伝えることができました。今回の我々の研究は数式と文字による説明が多く、ひと目でわかりやすい研究ではなかったかもしれませんが、皆様の積極的に理解しようとする姿勢がとてもうれしくやりがいがありました。研究者の方々からその場で改善に関する貴重なアドバイスをいただいたり、ファッションと機械学習について改めて意見交換をする話になったりと、実りのある時間になったと思います。技術的な内容については別の記事で扱いたいと思います。 MIRU2017 発表ポスター 感想 国内外の素晴らしい研究成果を学ぶことで、研究のアイデアを多く得られたことがなによりの収穫でした。弊社の機械学習を使ったプロダクトでは物体検出や生成モデル、属性予測モデルなどでCNNを活用していますが、その高度化に使えそうな研究が多くありました。招待講演は、すでに有名な国際会議で発表されている研究を紹介する企画でしたが、勉強不足で把握していなかった研究やアイディアが多くあったため、とても有難かったです。弊社では早速、今回の学会参加で得られた知見を取り入れる実験を開始しています。 また、ファッションの画像を使った研究に取り組んでいる研究者や学生と議論や意見交換をできたことも良かったです。ファッションの画像の研究という特定の分野に携わっている人たちが集まる機会はなかなか無いため、論文として発表されない限り、どこでどんなことに取り組まれているのかを知ることはできません。MIRUに参加することで、様々な研究機関の取り組みと取り組んでいる人たちを把握できたことは貴重な機会でした。 気になった研究 個人的に興味をもった研究を簡単にご紹介します。 [PS1-28] キャッシュ型k-d木:クエリの偏りを用いた近似最近傍探索法 香川 椋平, 和田 俊和(和歌山大) 近似最近傍探索において、登録データの統計的分布を利用する手法は多くある一方で、クエリの統計的偏りは検討されてきませんでした。この研究では、k-d木による探索のうち、木探索とpriority searchの結果が異なるクエリをキャッシュすることで時間の掛かるpriority searchを省略するという方法を提案し、k-d木による探索の高精度化・高速化に成功しました。弊社でも大量の画像を検索するタスクが多いため、クエリの偏りを利用してキャッシュしておくというアイデアは利用できそうです。 [DS-3] キーポイントの幾何学的一貫性を考慮した実時間画像検索システム 大倉 有人, 和田 俊和(和歌山大) 画像検索において、クエリにカメラで撮影した画像を用いる場合、検索したい物体以外のものが含まれてしまうことが多いです。この研究では、データベース上の画像とクエリ画像の、SURFで検出されたキーポイントの各組から相似変換を推定し、推定された各相似変換を集計します。最も投票の多かった相似変換に従うキーポイントの組を採用することで、クエリ画像上の検索対象と無関係な背景の効果を実時間のうちに除去することが可能になりました。 [OS4-1] Dynamic Fashion Cultures Kaori Abe, Teppei Suzuki, Shunya Ueta(AIST), Akio Nakamura(Tokyo Denki Univ.), Yutaka Satoh, Hirokatsu Kataoka(AIST) この研究では、世界中のファッショントレンドを時系列で分析・可視化することを目的とし、位置と時間の情報を持った人物画像の巨大データベースを作成しました。Yahoo! Creative Commons 100M Databaseから人物を切り出し、タイムスタンプとジオタグを付与しています。画像特徴量をクラスタリングして、各クラスタの数の時系列変化を見ることで、特徴的なファッショントレンドを抽出しています。 この研究では1年ごとのトレンドを見ていますが、ファッション業界からは1週間スケールでのトレンドを把握したいという要望があるようです。これは非常に難しい課題ですが、応用第一のファッションテックにおいて、とても重要な課題になるでしょう。 [PS2-27] ディープラーニングによるファッションコーディネートの分類 高木 萌子, シモセラ エドガー, 飯塚 里志, 石川 博(早大) ファッションアイテムの推薦はユーザーのスタイルを把握していなければ難しいタスクです。この研究では、13000枚以上のトータルコーディネートの画像を収集し14種類のファッションカテゴリに分類したデータセットを作成し、CNNを用いたファッションカテゴリ判別器を学習しました。ファッションの専門家と素人の判別精度の中間程度の精度を達成しています。ポスター発表ではCNNが判別の際に注目している位置を可視化しており、その判断基準には納得感のあるものが多かったです。弊社の推薦システムにおいても、ユーザーの好むファッションスタイルを理解することが重要なので、この研究を発展させていきたいです。 [PS1-59] 衣服情報の活用による姿勢推定の精度向上に関する研究 金子 直史, 鷲見 和彦(青学大) この研究では、人の体型を画像から測る目的から、精度の高い姿勢推定のネットワークを作成しています。通常の姿勢推定用のネットワークの出力に、衣服のセグメンテーションを行った結果を加えることでより精度の高い姿勢推定を行うネットワークを実現しました。この研究では2つのネットワークを単純に合わせたものが提案されていましたが、姿勢推定の誤差を修正するネットワークも有効なのではないかという議論をしました。 [OS5-6] 2D-QRNNを導入したDCNNによるセマンティックセグメンテーションの高精度化と高速化 古川 弘憲, 山下 隆義, 山内 悠嗣, 藤吉 弘亘(中部大), 石井 育規, 羽川 令子(パナソニック) 局所的な情報を活用するCNNに、文脈情報を考慮できるRNNを組み合わせることにより、セマンティックセグメンテーションの精度を上げられることが知られています。しかし、通常のRNNでは並列演算が出来ず、推論に掛かる時間のボトルネックとなってしまいます。この研究では、二次元に拡張したQuasi-Recurrent Neural Networksを用いることで、DAG-RNNと同程度の精度を保ちながら推論にかかる時間を29%まで削減できました。 [IT-8]【招待講演】Generative Attribute Controller With Conditional Filtered Generative Adversarial Networks (CVPR2017) Takuhiro Kaneko, Kaoru Hiramatsu, Kunio Kashino 論文: http://openaccess.thecvf.com/content_cvpr_2017/papers/Kaneko_Generative_Attribute_Controller_CVPR_2017_paper.pdf この研究では、Conditional GANを拡張し、ノイズ変数を属性情報でフィルタリングするアーキテクチャを加えたCFGANを提案しています。画像の属性情報を操作して、画像を変換するタスクにおいて、提案手法は属性の表現力と操作性を向上させることに成功しています。 フィルタリングの方法を変えることにより、ラジオボタンやスライドバーによる変換など、従来よりも操作性の高い方法を実現できています。ファッションの画像検索においても、このような属性変換による画像検索は実用性が高いと考えます。 [OS5-5] Segmentation based on Transform Invariant Auto-encoder Tadashi Matsuo, Nobutaka Shimada(Ritsumeikan Univ.) この研究では、上図のように平行移動やバックグラウンドに不変なオートエンコーダーを提案しています。変換無し画像の復元画像と、平行移動などの変換を施した画像の復元画像が近くなるように設計したTransform variance termと、変換を施した画像と復元画像が近くなるよう設計した Restoration error termをコスト関数に用いて学習します。 設計の意図の通り、入力画像の位置が異なっても(上のFig.2)、出力はほとんど同じものとなっています(上のFig.4)。 [PS2-20] 適応的ランク誤差に基づくTriplet 局所特徴表現学習 田良島 周平, 黒住 隆行, 杵渕 哲也(NTT) 通常のトリプレット損失関数は、アンカー(a)と負例(n)の距離がアンカーと正例(p)の距離よりも固定のマージン以上に小さい場合に誤差をとる損失関数です。しかし、上図の左のように各距離がマージンよりも十分に大きい場合、相対的にマージンの効果が小さくなり、誤差が発生しない状況が増えてしまいます。この研究では、固定していたマージンをアンカーと正例の距離に対して適応的に変化させることで、各距離がマージンよりも十分に大きい場合の弁別性の向上を狙いました。提案手法は、パッチペアの分類推定と射影変換推定のタスクにおいて、Hard miningを行う手法以外の多くの場合で、既存手法を上回る性能を発揮しています。 おまけ 本記事の趣旨とはずれますが、学会の合間に広島を満喫しましたので写真で紹介します。 厳島神社 干潮時の厳島神社の鳥居。カニを捕まえるのに夢中になってしまいました。 鹿(宮島) 宮島は鹿が出迎えてくれます。我々と一緒に歩いてくれる優しい鹿もいました。 お好み焼き(ロペズ) 横川のロペズでお好み焼きを食べました。とても有名なお店だそうで、一時間近く並びました。ハラペーニョをトッピングした風変わりなお好み焼きを提供してくれます。 あなご竹輪(宮島) あなご感、特になし…!! 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。本記事のように、業務の一環として学会に参加することを認められる環境です。 興味のある方はこちらからご応募ください。
こんにちはフロントエンジニアの茨木です。一ヶ月ほど前からSwiftでiOSアプリ開発をやっています。iOS開発経験は浅いですが、Lottieというライブラリを使用し、いきなりアニメーションごりごりの画面を担当してみました。 Lottie はAirbnb社が開発したライブラリで、僅かなコードでアニメーションを実装できるスグレモノです。 本記事では、SwiftにおけるLottieの使い方を説明します。 Lottieの特徴 Lottieの最も大きな特徴は、Adobe After Effectsで出力したアニメーションデータをそのまま読み込むだけでアニメーションを実装できることです。その為、沢山のコードや画像が不要なのはもちろんのこと、デザイナーが作ったアニメーションを忠実に再現することが可能です。更に、LottieはAndroidやReact Nativeもサポートしているので、クロスプラットフォームでアニメーションを再利用することもできます。 Lottieの導入方法 アニメーションデータの出力 Lottieでアニメーションデータを読み込むには、Adobe After Effectsの拡張機能 Bodymovin を使ってJSON形式で出力する必要があります。出力の詳細な手順はここでは割愛します。また、LottieはAfter Effectsの全機能をサポートしているわけではないので注意が必要です。詳しくは こちら をご覧ください。 アニメーションの実装 とりあえず実装してみる 出力したJSONファイルをXcodeプロジェクト内の任意の場所に置き、アニメーションを実装したいViewControllerにコードを4行追加するだけです。 import UIKit // ライブラリをインポート import Lottie final class LottieExampleViewController : UIViewController { override func viewDidLoad () { super .viewDidLoad() // アニメーションのviewを生成 let animationView = LOTAnimationView(name : "heartAnimation.json" ) // ViewControllerに配置 self .view.addSubview(animationView) // アニメーションを開始 animationView.play() } } LOTAnimationView(name: String) の引数nameにはJSONのファイル名を指定します。拡張子はなくても問題ありませんが、ファイル名にパスを含めてしまうと読み込みに失敗します。また、外部データを取得する場合は LOTAnimationView(name: String) の代わりに LOTAnimationView(contentsOf: URL) を使います。 LOTAnimationViewについて LOTAnimationViewはUIViewを継承しており、デフォルトの幅・高さはアニメーションの幅・高さと同じになります。LOTAnimationViewは、通常のviewのようにframeやAutoLayoutでレイアウトしたり、contentModeでビュー内のコンテンツ配置を調整したりできます。 更に、LOTAnimationViewにはアニメーションに関する様々なオプションがあり、ループや速度の設定が可能です。設定できるオプションは以下のようなものがあります。 LOTAnimationViewのプロパティ 詳細 animationDuration: CGFloat { get } アニメーションの開始から終了までの時間を取得できます。 animationProgress: CGFloat { get set } 開始を0.0、終了を1.0とした場合のアニメーションの経過時点を取得/指定できます。値を指定した場合、指定時点で静止したアニメーションが表示されます(isAnimationPlayingがtrueの間は指定しても静止しません)。 animationSpeed: CGFloat { get set } 元の速度を基準としたアニメーションの速度を取得/指定できます。 loopAnimation: Bool { get set } アニメーションのループの有無を取得/指定できます。 isAnimationPlaying: Bool { get } アニメーションが動作中かどうかを取得できます。 実装例 「いいね!」をタップした時などに出てきそうなハートのアニメーションを実装してみた例です。 このようなアニメーションも、Lottieを使えばレイアウトの調整含め5行ぐらいのコードで実装できます。 まとめ Lottieによりアニメーションの実装コストが大幅に削減されました。更に、デザインに忠実なアニメーションの実装が可能になりました。とても便利なライブラリなので、読者の皆様も一度試してみることをオススメします。Lottie用のアニメーションは LottieFiles でダウンロードすることもできるので、アニメーションを作るのはちょっと…という方も是非試してみて下さい。 最後に VASILYではやったことないことにも果敢にチャレンジできるようなエンジニアを募集しています。 興味がある方はこちらからご応募ください。
こんにちは、フロントエンドエンジニアの権守です。 既にお気づきの方も多いと思われますが、こちらのテックブログは今月から装いを新たにしています。これは 先日行った弊社コーポレートサイトのリニューアル に合わせたものです。 この記事では、今回行ったコーポレートサイトリニューアルについて実装面から紹介します。 特徴 今回のリニューアルの特徴は以下の3つです。 Vue.jsによるSPA (Single Page Application)としての実装 レスポンシブ対応 アニメーションによるリッチな表現 それぞれについて詳しく紹介していきます。 Vue.js Vue.js はユーザーインタフェースを構築するためのJSフレームワークの1つです。Reactなどのフレームワークと同様にSPAの実装に使えますが、段階的に導入していくことも可能な点が特徴です。 その段階的に導入可能という点が、既存のサービスへの導入を容易にしていることもあり、弊社ではVue.jsを一部導入しています。現状、段階的に使っている上ではVue.jsは非常に便利という印象でしたが、今後、本格的に導入していく上でSPAフレームワークとしても満足のいくものであるかはわかりませんでした。 そこで、今回はVue.jsのSPAフレームワークとしての使い勝手を検証する意味も込めてコーポレートサイトの構築に全面的に導入しました。 Vue.jsの優れた点 結論から言うと、Vue.jsはSPAフレームワークとしても非常に便利であり、今後SPAを開発する際に弊社で採用する可能性は非常に高いと思います。 その判断に至った大きな要因は 単一ファイルコンポーネント による関心の分離のスマートさです。Vue.jsの単一ファイルコンポーネントでは、 こちら で述べられている「関心の分離はファイルタイプの分離と等しくない」という理論の元、作られています。具体的には1つのファイルにテンプレート(HTML)・ロジック(JavaScript)・スタイル(CSS)の記述を可能としており、一貫性と保守性を高めています。 以下にコーポレートサイト内で用いている単一ファイルコンポーネントの例を示します。 main_heading.vue < template lang = "pug" > h1 {{title}} </ template > < script > export default { name: 'heading' , props: [ 'title' ] , } </ script > < style lang = "sass" scoped> @media ( max-width : 1039px ) h 1 display: none @media ( min-width : 1040px ) h 1 color : # 333 text-align: center font-family: 'DIN' font-weight: bold font-size: 30px letter-spacing: 0.75px line- height : 200px </style> 上の例からテンプレート・ロジック・スタイルが1つのファイルに書かれていることがよくわかると思います。また、style部分にscopedと書かれていますが、これは単一ファイルコンポーネントが有するScoped CSSという機能を利用することを示しています。この機能を利用することで、そのコンポーネント内に記述されたスタイルが他のコンポーネントを汚染することを心配する必要がありません。それによってネームスペースを確保するためだけの余計なクラスを付与する必要もなくなります。これらの徹底した関心の分離はアプリケーションが大規模になるほど、より効果的になると考えています。 レスポンシブ対応 PC、タブレット、スマホといった具合にサイトを訪れる方のデバイスは様々です。それらのどれでアクセスしたとしても、弊社の魅力を最大限伝えられるように、レスポンシブ対応を全面的に行いました。 CSSのメディアクエリを用い、画面サイズによってPC用とモバイル用でスタイルを切り替えています。また、一部コンポーネントではモバイル版の中でもサイズによってレイアウトを2カラムから3カラムに切り替えるといったことも行っています。 CSSだけでなく、 以前書いた記事 で紹介した画像のレスポンシブ対応ももちろん取り入れています。 実際にどのようにCSSによるレスポンシブ対応をしているかの雰囲気がわかるように2カラムから3カラムに切り替わるコンポーネントのスタイル部分を示します。 article_card.vue < style lang= "sass" scoped > article display : block overflow : hidden text-align : left . container position : relative width : 100% overflow : hidden img position : absolute top : 50% left : 50% -webkit- transform : translate( -50% , -50% ) transform : translate( -50% , -50% ) height : 100% margin : auto . blog display : inline-block color : #333 font-family : 'DIN' font-weight : bold & : hover text-decoration : underline time display : inline-block float : right color : #999 font-size : 10px . title display : block display : -webkit-box -webkit- box-orient : vertical -webkit-line-clamp: 3 overflow : hidden color : #333 text-align : left @media ( max-width : 330px ) article .title font-size: 10px line- height : 15px @media ( min-width : 331px ) and ( max-width : 1039px ) .title font-size: 11px line- height : 17px @media ( max-width : 557px ) article &:before padding-top: 153.62% width : calc( 50% - 5px ) @media ( min-width : 558px ) and ( max-width : 1039px ) article &:before padding-top: calc( 100% + 90px ) width : calc( 33.3% - 6.66px ) @media ( max-width : 1039px ) .meta padding: 0 3px 0 5px .blog font-size: 12px margin-top: 13px .title margin-top: 7px time margin: 14px 2px 0 0 article >a width : 100% height : 100% display: block position: absolute top: 0 left: 0 &:before display: block content: '' .container &:before display: block content: '' padding-top: 100% @media ( min-width : 1040px ) article width : 260px height : 310px transition: margin-top 0 . 25 s ease-out &:hover margin-top: -20px .container height : 200px .blog font-size: 13px margin-top: 15px time margin-top: 15px .title font-size: 13px line- height : 20px margin-top: 9px </style> また、このレイアウトが切り替わった際の要素の再配置部分は Masonry を用いて実装しました。CSSだけで実装しなかったのは、高さの異なる要素を縦方向に詰めて配置することができなかったからです。 アニメーションによるリッチな表現 今回のサイトデザインは、読みやすさを重視したシンプルなデザインな一方で、冷たい印象になりすぎないようにリッチなアニメーションを散りばめています。アニメーションの実装にはCSS3のアニメーションを中心に使いつつ、より複雑なアニメーションが求められる箇所では D3.js を用いました。 2つピックアップして実装について紹介します。 lazy-box スクロールに応じて、コンテンツが出現するアニメーションをlazy-boxというコンポーネントとして実装しました。 lazy_box.vue < template lang = "pug" > .lazy-box(ref="lazyBox" :class="{active: active}") slot(:active="active") </ template > < script > export default { mounted () { window .addEventListener ( 'scroll' , this .handleScroll ) ; this .handleScroll () ; } , destroyed () { window .removeEventListener ( 'scroll' , this .handleScroll ) ; } , data () { return { active: false , handleScroll: ( e ) => { if ( window .innerHeight - this .$refs.lazyBox.getBoundingClientRect () . top > 0) { this .active = true ; } } } } } </ script > < style lang = "sass" scoped> .lazy-box opacity: 0 transition: opacity 2s , transform 0.5s @media ( max-width : 1039px ) transform: translateY( 100px ) @media ( min-width : 1040px ) transform: translateY( 200px ) &.active opacity: 1 transform: translateY( 0 ) </style> lazy-boxはVue.jsのコンポーネントの slot機能 を用いることで任意のコンテンツに対してアニメーションを付与することが可能です。 D3.jsを用いたグラフ Recruitページ のグラフはD3.jsを使って実装しています。D3.jsはデータに基づいてドキュメントを操作するライブラリで、データ可視化によく用いられます。今回は、棒グラフとその数値のアニメーションの実装に用いました。 具体的には次に示すようなメソッドをVueコンポーネントのmethodsに設定し、コンポーネントがactiveになったタイミングで各メソッドを呼ぶようにしています。 methods: { barTransition() { var selection = d3.select( this .$refs.bar); selection .transition() .ease(d3 [this .ease ] ) .duration( this .duration) .styleTween( "width" , () => { var i = d3.interpolate(0, '100%' ); return function (t) { return i(t); } ; } ); } , valueTransition() { var selection = d3.select( this .$refs.value); var value = this .value; selection .transition() .ease(d3 [this .ease ] ) .duration( this .duration) .tween( "span" , function (d) { var i = d3.interpolate(0, value); var self = this ; return function (t) { d3.select( self ).text(parseInt(i(t))); } ; } ); } } まとめ 今回は、先日行った弊社のコーポレートサイトリニューアルについてその特徴と実装について紹介しました。 レスポンシブ対応をしっかり行ったので、画面サイズの変更やランドスケープモードへの切り替えなど、ぜひ色々試しながらVASILYのことを今一度知っていただけると幸いです。 最後に VASILYでは、積極的に挑戦していけるエンジニアを募集しています。興味のある方は以下のリンクからぜひご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
こんにちは。データチームの後藤です。 弊社のデータサイエンティストは職務の1つとしてファッション×機械学習の研究・開発に取り組んでいます。このファッション×機械学習の分野は世界中の大学や研究機関で精力的に研究されているため、我々も最新の動向を日々追いかけて、技術検証やサービスへの実用化を進めています。 本記事では、ファッション×機械学習の最新の研究動向を理解するための比較的新しい研究論文を紹介します。この記事を読むとファッション×機械学習の応用例を把握することができると思います。特に注目している研究の紹介には論文中の図とコメントを残しましたので、追いかける際の参考にしてください。なお、本記事内に掲載されている論文の中にはarXivのみに投稿されているものもあります。「査読を通しておらず内容が保証されない」「今後バージョンアップされ内容が変更される」といった可能性があります。ご了承ください。 一言でファッション×機械学習といっても、その内容は多岐にわたります。今回はファッションに関する行動のうち、 流行を知る 商品を探す アイテムの組み合わせを考える というタスクを機械学習システムで補助するという状況で利用できそうな研究を紹介します。 流行を知る Fashion Forward: Forecasting Visual Style in Fashion (2017) StreetStyle: Exploring world-wide clothing styles from millions of photos (2017) Changing Fashion Cultures (2017) その他関連研究 商品を探す 推薦システム VBPR: Visual Bayesian Personalized Ranking from Implicit Feedback(2015) Sherlock: Sparse Hierarchical Embeddings for Visually-aware One-class Collaborative Filtering(2016) その他関連研究 画像検索システム Cross-domain Image Retrieval with a Dual Attribute-aware Ranking Network(2015) Deep Learning based Large Scale Visual Recommendation and Search for E-Commerce(2017) Memory-Augmented Attribute Manipulation Networks for Interactive Fashion Search (2017) その他関連研究 アイテムの組み合わせを考える Mining Fashion Outfit Composition Using An End-to-End Deep Learning Approach on Set Data (2017) Trip Outfits Advisor: Location-Oriented Clothing Recommendation(2017) その他関連研究 最後に 参考 流行を知る 「現在のファッショントレンドのうち、流行り続けるのはどれか」「かつて流行っていたスタイルは今後ふたたび流行するか?」といったファッショントレンドの未来予測は、ファッション業界のデザイナーや販売業者、製造業者にとって重要なタスクです。大抵の消費者は、今後廃れていくスタイルの服よりも、今後主流になっていく服を購入したいと考えるでしょう。 トップダウン式に流行が決まると言われているファッションにおいて、データドリブンなファッショントレンドの未来予測は可能なのでしょうか。過去の傾向をデータで定量化する研究はいくつかありますが、未来を予測する研究は意外にも少ないようです。 Fashion Forward: Forecasting Visual Style in Fashion (2017) この研究 1 では、複数の機械学習タスクをうまく組み合わせることによって、データドリブンなファッショントレンドの予測に成功しました。 この研究のポイントは、 属性予測タスクを学習させたConvolutional Neural Networks(以下、CNNと呼ぶ)による特徴量抽出 Non-Negative Matrix Factorizationによるスタイル情報の抽出&画像データのグルーピング シンプルなExponential smoothing modelによる購買頻度の予測 ファッショントレンドの未来予測に効くのは、タグやテキストのメタ情報よりも画像情報 です。 上図の青線は各スタイルの服がAmazonで購入される頻度の、年ごとの推移を表したものです。緑の線が論文で提案されている手法での予測結果、その他の線は比較用のモデルの予測結果を表します。ほとんどの場合において、論文で提案された手法が実際の結果に近い値を予測していることがわかります。 StreetStyle: Exploring world-wide clothing styles from millions of photos (2017) 上記の研究と同様に、この研究 2 でもファッショントレンドの定量化をCNNによる特徴量抽出と、クラスタリングによって行っています。この研究は、Instagramから地域別・時期別に人の画像が集められ、分析に用いられている点が特徴です。 画像特徴量のクラスタリングにより、抽出されたファッションスタイルの時間変化やクラスター内の地域別の内訳を調べることができます。上図の左は、各クラスターに含まれる画像の例、真ん中がクラスター内の平均画像、右がクラスター内のデータの月別・地域別の分布を表します。 上図の3行目では、黄色と黒色のストライプ柄のユニフォームを着ているクラスターの人たちの投稿が、特定の時期・地域で増えていることがわかります。これはワールドカップが開催された影響だと考えられています。 この研究ではデータの前処理を丁寧に行っています。各画像から人物を検出し、顔の位置を特定して画像のスケールを合わせています。さらに、Amazon Mechanical Turkをつかい、各画像に12種類の属性を付与しています。研究で用いられたデータはSTREETSTYLE-27Kというデータセットとして公開され利用できるようになります。 Changing Fashion Cultures (2017) この研究 3 では、世界のファッショントレンドを分析できるポテンシャルを秘めている巨大なデータセットを提供しています。 上の表から、データ数の点で他を圧倒していることがわかります。DeepFashion 4 や上述のSTREETSTYLE-27のような細かな属性付与はないようなので、その点はうまく処理する必要がありそうです。しかし、データの量の点で困ることはしばらくないでしょう。 その他関連研究 Who are the Devils Wearing Prada in New York City? 5 ニューヨークのファッションショーが消費者のファッションに与える影響について定量化 Fashion Conversation Data on Instagram 6 InstagramのFashionに関わるImageとconversation dataを提供 marketingに有効なInstagramの投稿の特徴量を評価 上述の研究から、画像データをトレンド分析で使える形に定量化する手法や、トレンド情報を含むデータセットが揃ってきたことがわかります。弊社では、タグやテキストベースのトレンドの可視化を行ってきましたが、より高度な分析をする際にこれらの手法が参考になると考えます。 商品を探す ここでは、ECサイトで商品を探すという状況を想定しています。多くのECサイトでは、ユーザーがクエリを入力して主体的に商品を探す検索システムと、行動ログに基づいて商品の表示を変化させる推薦システムが活用されています。ファッションのドメインにおいてはテキスト情報やメタ情報よりも見た目の情報が重要になる場合が多く、画像特徴量を使う研究が盛んです。 推薦システム 推薦システムはユーザーに新しい商品との出会いを促す手助けをしてくれます。ユーザーが付けたアイテムに対するレーティング情報やクリックログを元に構築されることが多いです。しかし、ビジュアルが重要な役割をもつファッションのドメインにおいては、商品の見た目の情報がユーザーの行動に強い影響を与えていると考えられます。また、見た目の情報を活用するにしても推薦システムは計算時間も膨大になりがちなので、学習データの量に対してスケーラブルであることも求められます。ここでは、見た目の情報を使いながらも現実的な計算量に収めて推薦を実現している研究を選びました。 VBPR: Visual Bayesian Personalized Ranking from Implicit Feedback(2015) この研究 7 はBayesian Personalized Ranking(以下、BPRと呼ぶ)に商品の見た目の情報を加えて推薦の精度を向上させています。 (論文中でFはCNNの出力の次元、KがItem Latent Factorsの次元となっているので、図中の「F×1」は「K×1」の間違いだと考えられます) この研究のポイントは、 商品の見た目の特徴量を学習済みCNNを使って抽出 計算オーダーはLatent factorの次元KとVisual latent factorの次元Dに対して線形 ユーザーの評価が少ないコールドスタートなアイテムの推薦に強いことを示唆 です。 論文中の実験結果から、服に関するデータセット(Amazon Women、Amazon Men、Tradesy.com)に関してBPRに対しVBPRのAUCが大きく向上していることがわかります。一方で、携帯電話に関するデータセット(Amazon Phones)の推薦ではAUCの大きな伸びは見られませんでした。このことは、ファッション商品の推薦において見た目の情報を取り入れることの大切さを示しています。 Sherlock: Sparse Hierarchical Embeddings for Visually-aware One-class Collaborative Filtering(2016) この研究 8 では、上述のVBPRと同じ著者が、見た目の情報に商品カテゴリの階層構造を加えることでさらに精度を向上させています。 CNNから得た画像特徴量を関数(Embedding matrixと呼ぶ)を使って次元圧縮をし、Visual latent factorとして推薦に使うところは上記のVBPRと同じです。この研究ではそれに加えてEmbedding matrixにカテゴリの階層情報の制約を与えて学習させます。 このような構造を取り入れることで、全アイテムに共通する要素(明度や彩度など)の特徴量が扱えるようになっています。上図は、学習されたEmbedding matrixの各次元(D0、D1、D8、D9)で順位付けされたアイテムを表しています。D0、D1はTop-levelのカテゴリに対応付けられる次元、D8、D9はBottom-levelのカテゴリに対応付けられる次元です。D0、D1は商品の明度や彩度などのグローバルな特徴と反応し、D8、D9は商品のフォーマルさなどの細かな特徴と反応していることがわかります。VBPRに比べ精度も向上し、計算量も大幅には増えないため、有効な改良の1つと言えるでしょう。 弊社でもファッションアイテムの推薦に見た目の情報を取り入れる実験・検証は進められており、TECH BLOGでも紹介しています。 tech.vasily.jp その他関連研究 Exploiting both Vertical and Horizontal Dimensions of Feature Hierarchy for Effective Recommendation(2017) 9 構造の扱いを親子関係だけでなく同じ階層の関係(兄弟・従兄弟関係)にも拡張したモデル 見た目の情報が取り入れられるようになったMatrix Factorizationベースのモデルも、まだ改良の余地が残されています。例えば、移り変わるトレンドの情報に対応する、個人の好きなブランドの情報を補助情報を扱えるように改良するなどです。より様々な文脈で活用できる推薦システムを構築することが今後の目標です。 画像検索システム 画像検索システムには単純に見た目が似ている画像を返すものの他に、 写り方や見た目が異なる同一商品を返す検索(Cross-domain Image Search) 画像の他に属性情報をクエリに加えて検索結果を柔軟に変化させる検索(Cross-modal Image Search) もあります。特に前者の、ファッションモデルの着こなしの画像や個人が撮影した写真をクエリにECサイトの商品画像を検索するという需要は高いです。 ここでは、実際にImage Searchを実装する上で使えそうな工夫を行っている論文を選びました。 Cross-domain Image Retrieval with a Dual Attribute-aware Ranking Network(2015) Cross-domain Image Searchの研究は数多くありますが、この研究 10 は検索精度を上げるための重要な工夫をしています。 ドメイン固有の表現を学習させるために,ショップ画像とストリート画像のネットワークは分けて学習させる トリプレット損失関数によるランキング学習だけでなく、複数の属性の予測問題を木構造的に学習 背景の影響を小さくするために、R-CNNによる検出を活用 などです。 上の表は、各ネットワークの検索精度の評価です。ANが属性予測タスクを学習したネットワーク、ARNがANに加えてランキングも学習したネットワークを表しています。著者らの提案したDARNはドメイン毎にサブネットワークを使い分け、ドメイン固有の表現を学習したネットワークです。この中ではDARNが最もよい性能を発揮しており、ドメイン毎にネットワークを分ける工夫が検索精度に効くことが示唆されています。 ただし、パラメータの多いCNNを2つも使っている点で学習のコストが上がるというデメリットもありそうです。 Deep Learning based Large Scale Visual Recommendation and Search for E-Commerce(2017) インドのECサイトFlipkartの取り組み 11 もとても参考になります。 この研究のポイントは、 ディープなCNNと並列にShallow Layersを加え、low-levelな特徴量(色や模様など)を使っている点 区別の難しいin-clsss negativeをつかってトリプレットを構成し、微妙なニュアンスの違いを学習させている点(下図) です。 通常のトリプレットでは、クエリ画像に対して同じクラスに属するpositiveと、他のクラスに属するnegativeを組にして学習に用いますが、この研究では、同じクラスに属していながら見た目が微妙に異なるin-class negativeもnegativeとして学習させています。定量的な比較は述べられていませんが、著者等によると、細かな違いに対する感度が上がるようです。 Memory-Augmented Attribute Manipulation Networks for Interactive Fashion Search (2017) 「見た目はこんな雰囲気なんだけど、色は青でふわふわな襟のものが欲しい」というちょっとわがままなクエリに答えるシステムも研究 12 されています。この研究では、以下の図のように、服の色や形の属性を操作して画像検索の結果を変化させることができるシステムを構築しています。 属性をインプットするモジュールでは、属性(色、丈、素材、形など)の特徴量ベクトルを、その属性を持つ実際の画像の特徴量の平均値で初期化し保持しておきます。画像と属性のインプットに対して、保持しておいた情報を画像に加え、属性変換後の画像特徴量としてトリプレット損失関数を計算します。 この研究とネットワークのアーキテクチャは大きく異なりますが、弊社でも同様のタスクを研究しIBIS2016で発表しました。VAEとGANの生成モデルを活用した属性変換による画像検索システムです。興味のある方は読んでみてください。 tech.vasily.jp その他関連研究 Visual Discovery at Pinterest(2017) 13 画像ブックマークサービスのPinterestにおける画像検索・推薦エンジンの紹介 Visual Search at eBay(2017) 14 eBayでの画像検索の実装例 アイテムの組み合わせを考える 明日どんな服を着ようか、と考える際、様々な要素(ドレスコード、流行、季節、天気、色の調和、動きやすさなど)を考慮し手持ちの服を組み合わせます。そんな複雑な意思決定を機械学習システムで補助するのがOutfit Support Systemです。(装い一式のことを英語ではOutfitといい、日本でよく使われる「コーディネート」という表現はしないそうです。以下ではコーディネートのことをOutfitと表記します。) 通常、ファッションアイテムの組み合わせの提案は雑誌のコンテンツやショップの店員さんなど、おしゃれ上級者によってなされます。そして、組み合わせの良し悪しはとても繊細で、人によって評価が分かれるなど主観に左右されることもあります。このような複雑な判断を機械学習システムにやらせようとすると「組み合わせのモデリングの難しさ」と「ファッションのコンセプトの捉えにくさ」に直面します。 ここでは、評価の難しいOutfitを上手く定量化している研究を選びました。 Mining Fashion Outfit Composition Using An End-to-End Deep Learning Approach on Set Data (2017) この研究 15 で提案されているシステムは、ユーザーがすでに組み合わせているアイテムに対して、マッチするアイテムを教えてくれます。このシステムでは以下の図のようにデータを扱い学習します。上段ではアイテムの組み合わせからOutfitの質を評価する手順を、下段では1つ1つのファッションアイテムを特徴量に変換する手順を示しています。 この研究のポイントは、 組み合わされるアイテムの数が可変であることから、Outfitの入力をRecurrent Neural Networks(RNN)を使って評価 スタイルやブランドなどの文脈情報を考慮するために、画像、カテゴリ、タイトルのmulti-modalなデータを同時に利用 End-to-Endでの最適化 です。 この研究ではPolyvore.comのOutfitのデータを使っていますが、弊社の運用するIQONに投稿されたデータも同等の情報をもっているため、類似のシステムを構築することが可能だと考えています。 Trip Outfits Advisor: Location-Oriented Clothing Recommendation(2017) 旅行先を入力すると旅行先に合ったOutfitを推薦してくれるシステムの研究 16 です。 この研究のポイントは、 観光地の文脈も考慮(中国のビーチでは保守的な人が多いため、ビキニよりもロンパースを提案する、お寺ではスカートを提案しない) メジャーな観光地を背景とした際に映える色の組み合わせを提案 です。 アイテムの組み合わせの良し悪しやロケーションとの関係について研究が進みつつあります。実際の購買行動に結びつける場合はパーソナライズが必要であると考えます。観点としては、 好み 体型 手持ちの商品 予算 などが挙げられます。 その他関連研究 Intelligent fashion styling using genetic search and neural classification (2015) 17 標準的でない体型の女性に対するOutfitの提案システム GetDressed: A Personalized and Contextually-Aware Clothing Advisor for the Home (2014) 18 手持ちのアイテムからOutfitを決めるシステム 上述の観点をすべて盛り込んだ専属スタイリストの役割をシステムで実現することは非常に難しい問題です。しかし、上述のOutfit自体の評価方法やロケーションとOutfitの関係の研究が揃ってきたので、それぞれの手法を統合したより柔軟なシステムの構築が可能だと考えます。 最後に 今回は、ファッションに関する行動のうち「流行を知る」「商品を探す」「アイテムの組み合わせを考える」というタスクを補助する機械学習システムの研究を紹介しました。ファッションに関するデータに機械学習を組み合わせることにより、人の判断を助ける様々なシステムが実現可能であることがわかっていただけたかと思います。 今回紹介できませんでしたが、ほかにも Object Detection Human Parsing Attribute Prediction Style Understanding などファッション×機械学習の興味深い様々な分野があります。 今年の10月にはコンピュータビジョンの国際会議ICCV2017がイタリア開催されます。この会議では"Computer Vision for Fashion"なるファッションをテーマにしたワークショップが開かれる予定です。ファッション×機械学習の研究分野はさらなる盛り上がりを見せる勢いです。 VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 参考 Z. Al-Halah, R. Stiefelhagen, K. Grauman. Fashion Forward: Forecasting Visual Style in Fashion. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1705.06394.pdf ↩ K. Matzen, K. Bala, N. Snavely. StreetStyle: Exploring world-wide clothing styles from millions of photos. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1706.01869.pdf ↩ K. Abe, T. Suzuki, S. Ueta, A. Nakamura, Y. Satoh, H. Kataoka. Changing Fashion Cultures. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1703.07920.pdf ↩ Z. Liu, P. Luo, S. Qiu, X. Wang, X. Tang. Deepfashion: Powering robust clothes recognition and retrieval with rich annotations. In Proc. CVPR, 2016. Retrieved from http://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Liu_DeepFashion_Powering_Robust_CVPR_2016_paper.pdf ↩ K. Chen, K. Chen, P. Cong, W. H. Hsu, J. Luo. 2015. Who are the devils wearing prada in new york city? In Proc. ICM, 2015. Retrieved from https://arxiv.org/pdf/1508.04785.pdf ↩ YI. Ha, S. Kwon, M. Cha, J. Joo. Fashion Conversation Data on Instagram. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1704.04137.pdf ↩ R. He, J. McAuley. VBPR: Visual Bayesian Personalized Ranking from Implicit Feedback. In Proc. AAAI, 2016. Retrieved from https://arxiv.org/pdf/1510.01784.pdf ↩ R. He, C. Lin, J. Wang, J. McAuley. Sherlock: sparse hierarchical embeddings for visually-aware one-class collaborative filtering. arXiv, 2016. Retrieved from https://arxiv.org/pdf/1604.05813.pdf ↩ Z. Sun, J. Yang, J. Zhang, A. Bozzon. Exploiting both Vertical and Horizontal Dimensions of Feature Hierarchy for Effective Recommendation. In Proc. AAAI, 2017. Retrieved from https://aaai.org/ocs/index.php/AAAI/AAAI17/paper/view/14679 ↩ J. Huang, RS. Feris, Q. Chen, S. Yan. Cross-domain image retrieval with a dual attribute-aware ranking network. In Proc. ICCV, 2015. Retrieved from http://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Huang_Cross-Domain_Image_Retrieval_ICCV_2015_paper.pdf ↩ D. Shankar, S. Narumanchi, H. A. Ananya, P.Kompalli, K. Chaudhury. Deep Learning based Large Scale Visual Recommendation and Search for E-Commerce. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1703.02344.pdf ↩ B. Zhao, J. Feng, X. Wu, S. Yan. Memory-Augmented Attribute Manipulation Networks for Interactive Fashion Search. In Proc. CVPR, 2017. Retrieved from http://www.zhaobo.me/papers/cvpr_atman.pdf ↩ A. Zhai, D. Kislyuk, Y. Jing, M. Feng, E. Tzeng, J. Donahue, Y. L. Du, T. Darrell. Visual discovery at pinterest. In Proc. IWWWC, 2017. Retrieved from https://arxiv.org/pdf/1702.04680.pdf ↩ F.Yang, A.Kale, Y.Bubnov, L.Stein, Q.Wang, H.Kiapour, R. Piramuthu. Visual Search at eBay. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1706.03154.pdf ↩ Y. Li, L. Cao, J. Zhu, J. Luo. Mining Fashion Outfit Composition Using An End-to-End Deep Learning Approach on Set Data. IEEE Transactions on Multimedia, 2017. Retrieved from https://arxiv.org/pdf/1608.03016.pdf ↩ X. Zhang, J. Jia, K. Gao, Y. Zhang, D. Zhang, J. Li, Q. Tian. Trip Outfits Advisor: Location-Oriented Clothing Recommendation. IEEE Transactions on Multimedia, 2017. Retrieved from http://ieeexplore.ieee.org/abstract/document/7907314/ ↩ A. Vuruskan, T. Ince, E. Bulgun, C. Guzelis. Intelligent fashion styling using genetic search and neural classification. International Journal of Clothing Science and Technology, 2015. Retrieved from https://www.researchgate.net/profile/Cueneyt_Guezelis/publication/275257326_Intelligent_fashion_styling_using_genetic_search_and_neural_classification/links/56e45fb708ae68afa11061a5.pdf ↩ Z. Liu, J. Suarez, Y. Wu, F. Yu. GetDressed: A Personalized and Contextually-Aware Clothing Advisor for the Home. Retrieved July 21, 2017, https://static1.squarespace.com/static/5357036ee4b04cbb6439b475/t/54697fd6e4b0bb15f1889fda/1416200150924/GetDressed_FinalPaper.pdf ↩
こんにちは、バックエンドエンジニアの塩崎です。 先日、会社の広報のためのインターン生紹介記事にメンターとして掲載していただきました。 大学四年生のインターン生と一緒に写真撮影を行ったのですが、見た目だけではどちらが年上かわからなかったので、「メンターの塩崎(右)」という表記をされてしまいました(笑) インターンでも実際のサービスに触れ、課題を解決!〜VASILY DEVELOPERS BLOGが公開されました〜 さて、VASILYではData WarehouseとしてGoogle BigQuery(BigQuery)を利用しています。 BigQuery内にはプロダクトのマスタデータとユーザーの行動ログが格納されています。 そして、それらに対する横断的なクエリを発行することでプロダクトの成長のためのKPIをモニタリングしています。 そのためAmazon Relational Database Service(RDS)に保存されているマスタデータをBigQueryに同期する処理を定期的に実行する必要があります。 先日、ワークフローエンジンであるDigdagとバルクデータローダーであるEmbulkを利用して、この処理を行うシステムを構築しました。 本記事ではどのようにしてDigdagとEmbulkを連携させたのかなどの具体的な方法を、設定ファイルを交えながら説明します。 なお、本システムが対象としているRDSのDBエンジンはAuroraですが、MySQLでも問題なく動作します。 EmbulkとDigdagについて Embulk Digdag DigdagとEmbulkを組み合わせる システムの概要 1. 同期対象のテーブル情報の取得 2. Embulkの起動 3. AuroraからBigQueryへの同期 4. 実行ログの保存 Digdag導入の理由 タスクのリトライ処理 並列実行 HA構成 Digdag UIでタスクの実行ログを確認 設定ファイル Digdag aurora_to_bigquery.dig sync_one_table.dig Embulk aurora_to_bigquery.yml.liquid Digdagサーバーの構成 デーモン化 /etc/systemd/system/digdag.service /etc/init.d/digdag.sh /etc/digdag/config.properties Digdag UI デプロイ まとめ 参考リンク EmbulkとDigdagについて まずは本システムを構築するために使用したEmbulkとDigdagについて説明します。 どちらともTreasure DataさんによるOSSであり、ロゴの動物が可愛いです。 Embulk EmbulkはいわゆるETLツールと呼ばれるソフトウェアです。 大量のデータを取り出し(Extract)、それを加工し(Transfer)、書き出す(Load)ことを主目的としたツールです。 EmbulkではMySQLやBigQueryなどの具体的なデータソースとの入出力に関する部分のソフトウェアはプラグインとして提供されております。 そのため、それらを組み合わせることによって様々な入出力に対応できることが特徴です。 Digdag もう一方のDigdagはワークフローエンジンと呼ばれるソフトウェアです。 複数個のタスク間の依存関係からなるワークフローを定義し、そのワークフローの実行及び管理を行うソフトウェアです。 これだけですと、いまいち導入のメリットが分からないかもしれないので、具体例をお見せします。 例えば、処理1を実行した後に、処理2、処理3、処理4を順番に実行するというケースを考えます。 そして、これらの一連の処理(ワークフロー)を1日1回00:00に実行するとします。 この時、cronでこの問題を解決しようとすると、典型的には以下のようになります。 0 0 * * * 処理1 0 10 * * * 処理2 # 処理1は10分以内には終わるはず 0 20 * * * 処理3 # 処理2は10分以内には終わるはず 0 50 * * * 処理4 # 処理3は30分以内には終わるはず ところが、このcronの設定には様々な問題があります。 何かしらの原因によって処理1の実行時間が10分以上かかってしまった時に、処理2以降は実行を待つべき 処理1が失敗した時には処理2、3、4の実行を取りやめるべき この程度の複雑さであれば、以下のように書けば解決できます。 0 0 * * * 処理1 && 処理2 && 処理3 && 処理4 ですが、例えば以下のような複雑な要望があった場合にはcronではお手上げです。 各処理に対して3回までのリトライを行いたい 処理2・処理3・処理4は互いに独立な処理なので並列実行したい 同時に並列実行する数は2に抑えたい Digdagであればこのような場合でも以下に示すようなシンプルな設定ファイルでこれらの要望を叶えることができます。 +task1 : sh> : 処理1 +parallel_tasks : _parallel : true +task2 : _retry : 3 sh> : 処理2 +task3 : _retry : 3 sh> : 処理3 +task4 : _retry : 3 sh> : 処理4 なお、ワークフローエンジンというジャンルのソフトウェアにはJenkins、Airflow、Luigiなどがありますが、Digdagはそれらと比べて以下のメリットを持ちます。 YAMLベースのシンプルな設定ファイルなので学習コストが低い High Availability(HA)構成が容易 分散環境での動作が容易 DigdagとEmbulkを組み合わせる DigdagとEmbulkを組み合わせることによって、Embulkの良さをさらに引き出すことができます。 Embulkはあくまでも1つのETL処理を効率良く行なうためのツールなので、複数個のETL処理の管理はできません。 例えば、Auroraに保存されている複数個のテーブルをすべてBigQueryに転送する場合を考えます。 この時にはテーブル数と同じ回数だけEmbulkを起動する必要があります。 転送途中でエラーが発生してしまっても、リトライ処理や、失敗したことの通知はEmbulkではできません。 さらに、テーブルの同期の前後に任意の処理を挟み込むこともEmbulkではサポートされていません。 Digdagを利用することによって、Embulk複数回起動を効率的に管理したり、ETLの前後に処理を挟み込むことが容易になります。 システムの概要 以下に今回構成したシステムの概要図を示します。 まず、上図で1〜4の番号が書かれた部分を説明します。 設定された時刻(現状では1日1回)になると、Digdagは以下の処理を行います。 1. 同期対象のテーブル情報の取得 同期対象のテーブル情報をAuroraから取得します。 デフォルトではAuroraに存在するすべてのテーブルの同期を行います。 2. Embulkの起動 DigdagがEmbulkを起動します。 複数テーブルの同期処理は互いに独立であるため、処理の高速化のために複数プロセスのEmbulkを起動し、並列処理を行います。 3. AuroraからBigQueryへの同期 EmbulkがAuroraからBigQueryへの同期処理を行います。 AuroraやBigQueryへの接続に必要な情報はDigdagから渡されます。 Digdagから渡された情報はEmbulkに組み込まれているテンプテートエンジンのLiquidによって展開されます。 4. 実行ログの保存 1〜3の各処理の実行ログはS3にアップロードされます。 実行ログにはタスクの開始時刻・終了時刻・標準出力へ出力した内容などが含まれます。 また、タスクが失敗した場合はSlackにそのことが通知されます(上図右上)。 さらに、タスクの実行ログの確認や失敗したタスクのリトライ処理をDigdagに組み込まれているweb UI(Digdag UI)から行うことができます(上図左上)。 Digdag導入の理由 TECH BLOGの以前の記事ではgoラッパーとEmbulkを利用した同期システムを紹介しましたが、今回のシステムを構築するにあたってはgoラッパーは使用しませんでした。 Embulkを利用したデータ転送基盤の構築 このままgoラッパーを拡張しながら利用し続けることと比べて、Digdagを採用することで以下の問題を簡単に解決することができたからです。 タスクのリトライ処理 タスクの1回あたりの成功確率が十分に高かったとしても、そのタスクを連続で行うと、すべてのタスクの成功する確率は低くなります。 例えば、成功確率が99.9%のタスクを1,000回連続で行う場合、それらすべてが成功する確率は36.8%しかありません。 10,000回連続で行う場合では、わずか0.0045%になってしまいます。 しかし、失敗した時にリトライ処理を1回行うだけで、これらの確率はそれぞれ99.9%、99.0%になります。 実際に、今回のようなネットワークアクセスを行うタスク(Aurora、BigQueryなど)はネットワークの不調によってタイムアウトすることがあります。 その影響でタスクの実行に失敗することもあるため、リトライ処理は必須です。 以下のグラフに連続でタスクを実行した時のすべてが成功する確率(縦軸)とタスクの連続回数(横軸)の関係を示します(注:縦横軸は対数軸)。 3回までのリトライを許容すると、10 9 (10億)回連続で実行しても99.9%の確率で全てのタスクが成功します。 このように、リトライ処理は非常に効果的な一方で、実装やテストが面倒なためしばしば忘れがちな処理でもあります。 Digdagではタスク定義の中に1行の設定を書くだけでリトライ処理を行うことができるので、リトライ処理を簡単に導入することができます。 _retry : 3 +task1 : sh> : sometimes_fail.sh +task2 : sh> : sometimes_fail.sh +task3 : sh> : sometimes_fail.sh また、ワークフローの後半部分のタスクが失敗した場合、ワークフローの最初からやり直すのは無駄です。 特にワークフロー前半部分のタスクの処理時間が長い場合には無駄が顕著です。 そのため、失敗したタスクのみのリトライ処理をしたいです。 Digdagではタスク毎にリトライを設定することでこの問題を解決することができます。 +task1 : _retry : 3 sh> : sometimes_fail.sh +task2 : _retry : 3 sh> : sometimes_fail.sh +task3 : _retry : 3 sh> : sometimes_fail.sh 並列実行 互いに独立なタスクを並列実行することによって、ワークフロー全体の実行時間を短くすることができます。 また、それぞれのタスクが使うマシンリソースの種類(CPU、メモリ、ネットワークなど)が違う場合にはマシンリソースの有効活用をすることができます。 しかし、ミスなく並列処理を行うプログラムを書くのは難しく、再現性のないバグと戦うことも少なくありません。 Digdagを使用することによって、この並列処理の面倒な部分をDigdagが肩代わりしてくれるため、Digdag利用者は個々のタスクに集中することができます。 さらに、並列実行数の調整もDigdag起動時のオプションで簡単に行うことができます。 HA構成 ワークフロー管理をcronで行う場合の問題点の1つとしてあげられるのは、そのサーバーが落ちた場合にどうするかということです。 cronサーバーはステートレスなサーバーではないため、簡単にスケールアウトできません。 Digdagはワークフローそのものやワークフローの状態をPostgreSQLに保存することによってこの問題を解決しています。 タスクの実行を行うworkerやDigdag UIは状態をステートレスなため簡単にスケールアウトすることができ、HA構成にすることができます。 もちろんDigdagをHA構成にするときには、RDSをMulti-AZ構成にするなどの方法でPostgreSQL自体もHA構成にする必要があります。 Digdag UIでタスクの実行ログを確認 Digdagサーバーにはweb UI(Digdag UI)が付属しており、これを利用して以下の操作などを行うことができます。 ワークフロー実行ログの確認 失敗したワークフローのリトライ ワークフローを今すぐに実行 これらの操作をDigdag UIから行うことができるため、Digdagのコマンドに詳しくないメンバーでも運用作業を行うことができます。 Digdag UIの起動方法はドキュメントのどこにも書いてありませんが、実はDigdagサーバーを起動した時に立ち上がっています。 デフォルトでは、65432ポートで立ち上がっているので、 http://localhost:65432/ にアクセスすることでDigdag UIを見ることができます。 なお、GitHubのREADMEのRequirementsにはNode.js 7.xと書いてありますが、Digdag UIの開発時以外にはNode.jsは不要です。 設定ファイル それではここからは具体的な設定ファイルを交えながら説明をします。 Digdag aurora_to_bigquery.dig timezone : Asia/Tokyo # Slack通知のためのプラグインの読み込み _export : plugin : repositories : - https://jitpack.io dependencies : - com.github.szyn:digdag-slack:0.1.1 webhook_url : https://hooks.slack.com/services/HOGE workflow_name : aurora_to_bigquery # 認証情報の取得 +get_db_info : _retry : 3 _export : docker : image : ruby:2.4.1 build : - gem install aws-sdk rb> : Prepare.get_db_info require : 'tasks/prepare' # 同期対象テーブルの取得 +get_tables_to_sync : _retry : 3 _export : docker : image : ruby:2.4.1 build : - gem install mysql2 rb> : Prepare.get_tables_to_sync require : 'tasks/prepare' # 同期処理 +sync : for_each> : table : ${TABLES} _parallel : true _do : call> : sync_one_table.dig このワークフローは大きく3つの部分に分かれています。 それぞれはget_db_info、get_tables_to_sync、syncという3つのタスクです。 最初のget_db_infoではAuroraに接続するためのパスワードやGoogle Cloud Platform(GCP)に接続するためのJSONキーを取得します。 これらはAmazon Key Management System(KMS)に格納されているため、Ruby版aws-sdkで取得され、Digdagの変数にセットします。 2つ目のget_tables_to_syncでは同期対象のテーブルの一覧を取得します。 その内部では、 SHOW TABLES を実行し、テーブルの一覧を取得しています。 そして最後のsyncが同期処理の本体で、内部ではEmbulkを呼び出しています。 上流の2つのタスクによって設定された変数を使い、複数個のテーブルの同期処理を行います。 テーブル毎にパラメーターを変更しながらEmbulkを実行するために call オペレーターを利用しています。 また、 _parallel: true が設定されているため、並列処理が行われます。 同時に並列実行される数はDigdagサーバーを起動する時の --max-task-threads オプションによって制御されています。 そのため、すべてのテーブルの同期処理が同時に実行されることはなく、適切にスレッドプールが使われます。 これらのタスクの実行する環境を隔離するために、Dockerを利用しています。 これによって、Digdagが動いているサーバーそのものにRubyやgemなどを入れる必要がなくなり、サーバー構築の手間を減らすことができます。 自分たちでDockerfileやDockerイメージの管理をするのは煩雑ため、DockerイメージはDockerHubに保存されている公式イメージを利用しています。 そして、それらのイメージの上に必要なライブラリなどをインストールしています。 そのため、初回実行時はgemのインストールのために少々待たされます。 ですが、2回目以降には構築済みのDockerイメージが利用されるため、コンテナが高速に起動します。 sync_one_table.dig +get_timestamp_columns : _retry : 3 _export : docker : image : ruby:2.4.1 build : - gem install mysql2 rb> : ColumnOption.get_timestamp_columns require : 'tasks/column_option' +get_columns_to_drop : _export : docker : image : ruby:2.4.1 rb> : ColumnOption.get_columns_to_drop require : 'tasks/column_option' +run_embulk : _retry : 1 _export : EMBULK_INPUT_TABLE : ${table} EMBULK_OUTPUT_TABLE : ${table} docker : image : java:7-jre build : - curl --create-dirs -o /bin/embulk -L https://dl.bintray.com/embulk/maven/embulk-0.8.25.jar - chmod +x /bin/embulk sh> : embulk gem install embulk-input-mysql embulk-filter-column embulk-output-bigquery && embulk run aurora_to_bigquery.yml.liquid _error : slack> : failed-to-sync-table-template.yml このサブワークフローでテーブル毎の設定を取得し、それに基づきEmbulkを起動します。 このワークフローは前述したaurora_to_bigqueryからcallオペレーターによって呼び出されます。 このワークフローはget_timestamp_columns、get_columns_to_drop、run_embulkからなります。 前半の2つのタスクでテーブル毎の固有の情報を環境変数にセットし、最後のタスクrun_embulkでEmbulkを起動します。 get_timestamp_columnsではDATETIME型のカラム名を環境変数に設定しています。 デフォルトのまま同期するとAuroraでDATETIME型のカラムはBigQueryではTIMESTAMP型になります。 Auroraでの日付型がBigQueryでも日付型になるので一見すると問題ないようにも思えます。 しかし、BigQueryのweb UIでクエリの実行結果を確認する時に表示がUTCになってしない、脳内で9時間ずらすのがやや煩雑です。 そのため、 %Y-%m-%d %H:%M:%S%:z というフォーマットのSTRING型で保存することによって見やすさと日付計算のしやすさのバランスを取っています。 このフォーマットであれば通常はJSTで表示されるため人間に確認がしやすく、また、TIMESTAMP型への変換も容易であるため日付計算を行いやすいです。 ちなみに、内部的には以下のSQLを発行し、テーブルのスキーマ情報を読みだしています。 SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ( SELECT database()) AND TABLE_NAME = '#{table_name}' get_columns_to_dropでは同期対象外のカラムを環境変数に設定します。 本システムで扱うデータにはセンシティブなデータも含まれるため、それらのカラムを同期対象から外します。 どのカラムを除外するのかはYAMLによって制御されています。 この処理のためには embulk-filter-column プラグインを利用しました。 最後のrun_embulkタスクによってEmbulkが呼び出されます。 同期処理のために必要なEmbulk gemのインストールを行った後に、Embulkを起動してテーブルの同期処理を行います。 この処理の最中にエラーが発生した場合はSlackに通知が行われます。 Embulkに引数として渡している aurora_to_bigquery.yml.liquid ファイルについての詳細は次節で説明します。 Embulk aurora_to_bigquery.yml.liquid in : type : mysql host : {{ env.EMBULK_INPUT_DB_HOST }} user : {{ env.EMBULK_INPUT_DB_USER }} password : {{ env.EMBULK_INPUT_DB_PASS }} database : {{ env.EMBULK_INPUT_DB_DATABASE }} table : {{ env.EMBULK_INPUT_TABLE }} { % if env.EMBULK_FILTER_DROP_COLUMN != '' % } filters : - type : column drop_columns : { % assign drop_columns = env.EMBULK_FILTER_DROP_COLUMN | split : ',' % } { % for column in drop_columns % } - { name : {{ column }} } { % endfor % } { % endif % } out : type : bigquery mode : replace auth_method : json_key json_keyfile : content : | {{ env.EMBULK_OUTPUT_JSON_KEY_CONTENT }} project : {{ env.EMBULK_OUTPUT_PROJECT }} dataset : {{ env.EMBULK_OUTPUT_DATASET }} table : {{ env.EMBULK_OUTPUT_TABLE }} open_timeout_sec : 300 send_timeout_sec : 300 read_timeout_sec : 300 retries : 5 gcs_bucket : {{ env.EMBULK_OUTPUT_GCS_BUCKET }} auto_create_gcs_bucket : false compression : GZIP source_format : NEWLINE_DELIMITED_JSON default_timezone : "Asia/Tokyo" column_options : { % assign columns = env.EMBULK_OUTPUT_TIMESTAMP_COLUMNS | split : ',' % } { % for column in columns % } - { name : {{ column }} , type : STRING, timestamp_format : "%Y-%m-%d %H:%M:%S%:z" } { % endfor % } このファイルがEmbulkに渡す設定ファイルです。 Embulkに組み込まれたテンプレートエンジンであるLiquidを使用しています。 shオペレータを使ってコマンドを実行すると、Digdagの変数が環境変数にセットされます。 そのため、Embulk内では、env.<環境変数名>という記法でそれを読み出すことができます。 環境変数にはスカラー型の情報しか格納できないため、配列として格納したい時にはカンマ区切りで格納して、それをsplitしています。 また、以下の環境変数はKMSに保存されており、上流のget_db_infoタスクによってセットされた状態でEmbulkが起動されます。 EMBULK_INPUT_DB_PASS EMBULK_OUTPUT_JSON_KEY_CONTENT 認証情報をYAMLやソースコードに直に埋め込む必要がなくなったため、良いアイデアかと思いましたが、web UIでこの認証情報が丸見えになってしまいます。 なので、何かしらの対策が今後の課題として残っています。 secret機能を利用することで、ログに認証情報が平文で残ることがなくなるので、この機能の利用を検討しています。 Digdagサーバーの構成 デーモン化 Digdagサーバーはsystemdを利用してデーモン化を行っています。 デーモン化に必要なファイルについては、以下のgistを参考にしました。 https://gist.github.com/uu59/23968e3983adcf5b4cce400710b51cb2 ワークフローやタスクの状態を管理するDBにはH2DBを利用し、DigdagサーバーのローカルディレクトリにDBファイルを配置します。 今回の構成ではDigdagサーバーは1台だけなので、local-agentとexecutor-loopの両方を有効にします。 ワークフローのソースコードやタスクの実行ログはS3に保存します。 S3にアクセスするための認証情報はIAMインスタンスプロファイルを利用するために、設定ファイルには書き込んでいません。 /etc/systemd/system/digdag.service [Unit] Description=digdag [Service] User=root Restart=always TimeoutStartSec=30s Type=simple ExecStart=/etc/init.d/digdag.sh [Install] WantedBy=multi-user.target /etc/init.d/digdag.sh #!/usr/bin/env bash set -ue exec >> /var/log/digdag.log exec 2>> /var/log/digdag-error.log exec /usr/local/bin/digdag server --database /var/digdag/database --access-log /var/digdag/log/access_log --config /etc/digdag/config.properties /etc/digdag/config.properties server.bind=127.0.0.1 server.port=65432 database.type=h2 archive.type=s3 archive.s3.bucket=<s3 bucket name> archive.s3.path=digdag/archive log-server.type=s3 log-server.s3.bucket=<s3 bucket name> log-server.s3.path=digdag/log Digdag UI Digdag UIに対してVPCの外側からアクセスをするためにElastic Load Balancer(ELB)を利用しました。 今回のケースですとDigdagサーバーが1台しかないため、ELBを負荷分散のためには使用していません。 しかし、DigdagがHA構成を簡単に構築することを考慮し、HA構成に変更するときのためにELBの下にDigdagを紐付けました。 また、Digdag UIにアクセスするとワークフローに関するほぼすべての操作を行うことができます。 そのために、Digdag UIにアクセスできるユーザーを制限する必要があります。 現状では、ELBで接続元のIPアドレスを見てアクセス制限を行っています。 デプロイ ワークフローのデプロイはCircleCIから行います。 GitHubでのmasterブランチに対するmergeをhookし、CircleCI上のコンテナから digdag push コマンドを発行します。 まとめ DigdagとEmbulkを併用することでAuroraに保存されているマスタデータをBigQueryに同期するシステムを構築することに成功しました。 Embulk単体では使いづらい部分がいくつかありましたが、Digdagがその部分を補うことによってシステムとしての完成度を高めることができました。 特に、リトライ処理機能、並列実行の機能、web UIによる実行ログの確認機能は自力で書くのが大変なため、Digdagがこれらの面倒を見てくれることに大変助かっています。 Digdagに関する資料は少ないので、問題があった時にStack OverflowやQiitaを見ても情報が全く見つからない時もあります。 そのため、Digdagのソースコードを読んで問題の解決を行うこともしばしばありました。 VASILYではこのような困難に挑戦しながらも新技術を積極的に導入したい仲間を大募集中しています。 興味のある方は以下のバナーからご応募ください。 参考リンク Digdag公式HP Digdagの公式HPです。 Digdagの機能に関するビデオを見ると、一通りの機能について知ることができます。 Digdag公式ドキュメント Digdag公式ドキュメントです。 まだまだドキュメントに書かれていない部分も多いので、詳しい挙動が気になったときにはソースコードを読むのが良いでしょう。 Embulk公式HP Embulkの公式HPです。 Embulkは公式HPとドキュメントページが共通です。 digdag-slack Slackに通知を行うためのDigdagプラグインです。 httpオペレーターを使ってSlackに通知するのと比べると、より柔軟な通知が可能です。 embulk-input-mysql MySQLからデータを読みだすためのEmbulkプラグインです。 Auroraからデータを読みだすことにも使えます。 embulk-output-bigquery BigQueryにデータを書き出すためのEmbulkプラグインです。 一旦Google Cloud Strage(GCS)にデータを転送し、そこからBigQueryにロードすることで、処理の安定化・高速化を行うことができます。 embulk-filter-column 特定のカラムに対する処理を行うためのEmbulkプラグインです。 今回は特定のカラムを削除するために使用しましたが、それ以外の用途にも使うことができます。
この記事ではOpenAPI Specification v2に関する内容を取り上げています。しかし、2023年9月現在での最新の仕様はOpenAPI Specification v3となっています。最新の仕様に基づいて実装や学習を行いたい方は、公式ドキュメントやそれに関連する資料をご参照ください。 こんにちは! バックエンドエンジニアのりほやんです。 以前、テックブログでAPIモックと仕様書を作成することができるSwaggerについてご紹介しました。 Swaggerそのものについて知りたい場合やSwaggerを実際に導入したい場合は、こちらの記事がとても参考になります。 techblog.zozo.com 本記事では、SwaggerのAPI定義を行うSwagger YAMLの記法についてまとめてみました。 使い初めはとっつきにくいSwaggerですが、この記事がSwaggerを使う方の参考になれば幸いです。 目次 目次 Swagger Editorの紹介 初級編 基本の記述 swagger info paths パスのURL HTTPメソッド(get, post, put, delete等) エントリポイントのリクエストとレスポンスに関する記述 parameters responses スキーマオブジェクトについて JSONオブジェクト 配列 中級編 Swaggerを構成するオブジェクト definitionsを使う definitionsを複数呼び出す 上級編 definitionsを入れ子にする Swagger YAMLを書く上で気をつけたい点 レスポンスの配列に複数データを入れたい formatを指定しても、デフォルトで値が入らない Amazon API Gatewayではexampleが使えない まとめ 参考 Swagger Editorの紹介 Swagger YAMLを書く際には、 Swagger Editor がとても便利です。 画面左側がエディター、右側がSwagger UIとなっておりリアルタイムで記法のチェックや定義書を確認できます。 Swaggerを書くエディタはいろいろありますが、気軽に記法を試す際にはSwagger Editorがとても便利です。 ぜひこれから紹介する記法を試す際にも、ぜひ使ってみてください。 初級編 基本の記述 初めにSwagger YAMLを記述するにあたり必須であるswagger, info, pathsについて説明します。 上記の基本的な構成で記述したシンプルなSwagger YAMLがこちらです。 swagger : "2.0" info : description : "これはペットストアに関するAPIです。" version : "1.0.0" title : "Petstore API" termsOfService : "http://swagger.io/terms/" contact : email : "apiteam@swagger.io" license : name : "Apache 2.0" url : "http://www.apache.org/licenses/LICENSE-2.0.html" paths : /pet/{petId} : get : summary : "ペット情報API" description : "指定されたpetIdの情報を返します" parameters : - name : "petId" in : "path" description : "取得したいペットのID" required : true type : "integer" format : "int64" responses : 200 : description : "成功時のレスポンス" schema : type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" 以降、このサンプルを基に記法を説明します。 実際に上記のSwagger YAMLをSwagger Editorに入力すると、画面右側にAPI定義書が下記のように作成されます。 それでは、 swagger, info, pathsそれぞれのオブジェクトの書き方について説明します。 swagger swaggerには、Swaggerのバージョンを記述します。 変更する理由がない限りはここの値は 2.0 にしておきます。 info タイトル・説明・バージョンなど、APIについての情報を記載します。 infoには、以下の情報を記述できます。 フィールド名 型 説明 必須 version string APIのバージョン 必須 title string ドキュメントのタイトル。一番上に表示される。 必須 description string ドキュメントについての説明 termsOfService string 利用規約 contact contact object APIについての問い合わせ先 license license object APIのライセンス paths APIのエントリポイントを記述します。 サンプルでは下記の部分にあたります。 ここで定義する情報をもとにエントリポイントが作成されるため、pathsはとても重要なパートとなります。 pathsには、下記のような階層形式で情報を記載します。 パスのURL(/pet/{PetId}) HTTPメソッド(get, postなど) エントリポイントのリクエストとレスポンスに関する記述 パスのURL 実際に定義したいエントリポイントのパスを記述します。 サンプルでいうところの /pet/{petId} です。 HTTPメソッド(get, post, put, delete等) パスの下には、パスのHTTPメソッドを記述します。 下記のように複数記述することもできます。 /pet/{petId} : get : summary : "ペット情報API" description : "指定されたpetIdの情報を返します" parameters : 省略 responses : 省略  delete : summary : "ペット情報API" description : "指定されたpetIdを削除します" parameters : 省略 responses : 省略 エントリポイントのリクエストとレスポンスに関する記述 HTTPメソッドの下層には、エントリポイントがどのようなリクエストを受け取り、どのようなレスポンスを返すかを記述します。 サンプルでは、この部分にあたります。 設定できるフィールドは以下になります。 フィールド名 型 説明 必須 responses response object 返ってくるレスポンス 必須 parameters parameter object リクエストのパラメーター tags array swaggerオブジェクトで定義するどのtagに紐付けたいかを記述 summary string エントリポイントの概要(120文字以内) description string エントリポイントの説明 consumes/produces array MIME Type schemes array APIの通信プロトコル。必ずhttp, https, ws, wssの4種類のどれかを記述する。 security security requirement object 適用するセキュリティ externalDocs external docs document 外部リンク deprecated boolean Deprecatedかどうかをtrueかfalseで記述。デフォルトはfalse。 もっとも重要なparametersとresponsesについて補足説明します。 parameters リクエストの際に渡すパラメーターを記述します。 サンプルでは下記の部分にあたります。 記述するフィールドは以下です。 フィールド名 型 説明 必須 name string パラメーター名 必須 in string パラメータの場所。query, header, path, formDataの4種類のどれかを記述してください 必須 description string パラメータの説明 required boolean 必須パラメーターかどうかをtrueかfalseで記述 schema schema object bodyのパラメーターをスキーマオブジェクトとして記述。スキーマオブジェクトについては後述。inがbodyである場合に使用。 inがbodyである場合、必須 type string パラメーターのタイプ。 必ずstring, number, integer, boolean, array, fileの中から選ぶ。inがbody以外である場合に使用。 inがbody以外である場合、必須 format string パラメーターの型。 こちら から選ぶ。inがbody以外である場合に使用。 typeとformatの指定は、 こちら が参考になります。 パラメーターが記述されると定義書ではこのように表示されます。 responses 返ってくるレスポンスを記述します。 サンプルでは下記の部分にあたります。 返したいHTTPステータスコードごとに、定義を行います。 定義できる設定は下記です。 フィールド名 型 説明 必須 description string レスポンスの説明 必須 schema schema object レスポンスのbody。スキーマオブジェクトで記述する。スキーマオブジェクトについては後述。 headers headers object レスポンスヘッダーを記述 example example object レスポンス例。レスポンスの値を自分で定義したいときに用いる。 レスポンスを設定すると定義書が下記のように生成されます。 スキーマオブジェクトについて parameters, responsesを記述する際に、schemaを記述することができます。 このschemaにはスキーマオブジェクトを記述します。 スキーマオブジェクトは、bodyに用いるデータのタイプを定義することができるオブジェクトです。 主に配列かJSONオブジェクトを表現するときに使います。 JSONオブジェクト schema : type : object と指定すると、JSONオブジェクトを返すことができます。 例えば、APIのレスポンスを下記のように返したいとします。 { " id ": 1 , " name ": " doggie " } この場合はschemaをこのように書きます。 schema : type : object properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" type: object を指定した場合は、propertiesを設定します。 propertiesでは、カラムの情報を記述します。 propertiesには基本的に下記3つが記述されていれば動作します。 フィールド名 型 説明 type string パラメーターのタイプ。必ずstring, number, integer, boolean, array, fileの中から選ぶ。 format string パラメーターの型。 example typeで選んだ型 レスポンスで返したい文言 typeとformatの指定は、 こちら が参考になります。 配列 schema : type : array と指定すると、配列を定義することができます。 例えば、APIのレスポンスを下記のように返したいとします。 [ { " id ": 1 , " name ": " doggie " } ] この場合はschemaをこのように書きます。 schema : type : array items : type : "object" properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" type: array を指定した場合は、itemsを設定します。 itemsには、配列の中のオブジェクトを記述します。 配列の中に、JSONを定義したい場合は、 type:object を指定します。 schema : type : array items : type : "object" properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" また、配列の中に文字列を定義したい場合は、 type: string を記述します。 schema : type : array items : type : "string" 中級編 Swaggerを構成するオブジェクト Swaggerは初級編で紹介したswagger, info, pathsも合わせ計15種類のオブジェクトから成り立っています。 多く感じられますが、すべてが必須というわけでなく、必須であるswagger, info, pathsが記載されていれば動きます。 必須以外のものは定義書に記載したいものがあれば記述します。 フィールド名 型 説明 必須 swagger string swaggerのバージョン。特に指定がなければ、デフォルトの2.0のままで大丈夫です。 必須 info info object タイトル・説明・バージョンなど、APIについての情報を記載します。 必須 paths paths object 提供するAPIのパスを書いていきます。Swagger定義の要です。 必須 host string API通信を行うサーバーのホスト。無記載であればドキュメントが動いているサーバーのホストとなります。 basePath string ホストに続くパス。スラッシュ(/)から始まる必要があります。無記載の場合、ホストの直下になります。 hostを example.com basePathを /v2 とした場合、APIのパスは http://example.com/v2/〜 となります。 schemes array APIの通信プロトコル。必ずhttp, https, ws, wssの4種類のどれかを記載してください。無記載の場合、Swaggerの定義書がアクセスしているスキームになります。 produces array APIが提供できるMIME Typeの指定。選択肢は こちら consumes array APIが使用するMIME Typeの指定。選択肢は こちら definitions definitions object レスポンスやパラメータに使用するデータ定義を記載します。 parameters parameters definitions object パラメーターを定義します。pathsの下に直接パラメーターを書くこともできますが、複数回同じパラメーターを使う場合に便利です。 responses responses definitions object レスポンスを定義します。pathsの下に直接レスポンスを書くこともできますが、複数回同じレスポンスを使う場合に便利です。 securityDefinitions security definitions object セキュリティに関する定義を行えます。Oath認証についてもこちらで定義します。 security security requirement object securityDefinitionsにて定義したセキュリティの中で何を適用するかを指定します。 tags [ tag object ] タグの名前と説明を定義します。定義したタグをpathに紐付けることで、タグごとにpathがまとまりドキュメントが見やすくなります。 externalDocs external documentation object 外部リンクを定義します。ドキュメントに貼りたいリンクがある場合、記載することでリンクが作成されます。 それぞれのオブジェクトの記述は、 Swagger Editor にデフォルトで入っているPet store APIがわかりやすく参考になります。 definitionsを使う 同じスキーマオブジェクトを複数回使用したい場合、definitionsを使用しテンプレートとして定義することができます。 definitionsを使う際のポイントは、下記のようになります。 テンプレート化したいスキーマオブジェクトを、definitionsに定義をする 呼び出したい箇所に、$refを使用して、definitionsのオブジェクトを呼び出す サンプルをdefinitionsを用いて書き換えると以下のようになります。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : $ref : "#/definitions/Pet" # definitionsで定義されたスキーマオブジェクトを呼び出す definitions : Pet : # テンプレート名 type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" definitionsは本当に便利です。 definitionsにレスポンスのスキーマを全て定義しpathsではそれらを呼び出すだけというような記述にすると、Swagger YAMLが見やすくなるのでオススメです。 definitionsを複数呼び出す definitionsにて定義したスキーマオブジェクトを複数呼び出すことも可能です。 下記のように、Storeの情報をdefinitionsで定義して呼び出してみます。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : type : "object" properties : pet : $ref : "#/definitions/Pet" store : $ref : "#/definitions/Store" definitions : Pet : type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" Store : type : "object" properties : id : type : "integer" format : "int64" example : 1 store_name : type : "string" example : "ABC PET STORE" レスポンスはこのようになります。 { " pet ": { " id ": 0 , " name ": " doggie " } , " store ": { " id ": 1 , " store_name ": " ABC PET STORE " } } schema : type : "object" 上記のように記述し、JSONオブジェクトのそれぞれのキーにdefinitionsで定義したスキーマオブジェクトを呼び出します。 上級編 definitionsを入れ子にする 中級編では、レスポンスにdefinitionsで定義したスキーマオブジェクトを呼び出しました。 definitionsはスキーマオブジェクトを定義し、呼び出すことができるのでschemaが使えるところではどこでも呼び出すことができます。 例えば、definitionsの中でスキーマオブジェクトを呼び出すことも可能です。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : $ref : "#/definitions/Pet" definitions : Pet : type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" stores : type : "array" items : $ref : "#/definitions/Store" # Storeを呼び出す Store : type : "object" properties : id : type : "integer" format : "int64" example : 1 store_name : type : "string" example : "ABC PET STORE" 上記のようにレスポンスを定義した場合、レスポンスはこのように生成されます。 { " id ": 0 , " name ": " doggie ", " stores ": [ { " id ": 1 , " store_name ": " ABC PET STORE " } ] } storesの配列の中で、itemsの1つとしてStoreオブジェクトを呼び出しています。 Swagger YAMLを書く上で気をつけたい点 ここからは、実際に私がSwagger YAMLを書く上でうまくいかなかったりつまづいた点を共有の意味を込めてご紹介します。 レスポンスの配列に複数データを入れたい 通常レスポンスに配列を定義する際は、このように記述します。 schema : type : array items : type : "object" properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" この記述で返ってくるレスポンスは [ { id: 1, name: "doggie" } ] と要素が1つしか返ってきません。 2つ以上の配列要素を追加したい場合は、オブジェクト自体のexampleを記述します。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : type : "object" properties : pet : $ref : "#/definitions/Pet" definitions : Pet : type : "array" items : example : - id : 1 name : "doggie" - id : 2 name : "pochi" formatを指定しても、デフォルトで値が入らない Swaggerのドキュメント には、スキーマオブジェクトのformatにemailやuuidを指定するとexampleを設定しなくても値が入るという記述があります。 実際、Swagger Editorでは下記のようにデフォルトで値が入ります。 しかし、Swagger Codegenを使ってモックアプリケーションを生成する場合には、formatを指定してもデフォルトで値が入りません。 formatのデフォルト値を使用する際は、ご自分の環境でデフォルト値が入るかどうかを確認してから使う方が良いと思います。 Amazon API Gatewayではexampleが使えない Swaggerを用いてAPIを生成するサービスの1つに、 Amazon API Gateway があります。 API Gatewayは、Swagger YAMLをインポートすることで簡単にAPIを作成することができます。 API Gatewayを用いてAPIを作成する方法については、下記の2つが参考になるのでぜひ読んでみてください。 チュートリアル: サンプルをインポートして REST API を作成する - Amazon API Gateway http://tech.vasily.jp/entry/swagger_api_mock API GatewayにSwagger YAMLをインポートしてモックアプリケーションを作成する場合は、exampleが使えません。 exampleが記述されているSwagger YAMLをAPI Gatewayでロードすると Invalid model schema specified というエラーが返ってきます。 API Gatewayでレスポンス例を記述したい時は、下記のように総合レスポンスの設定時に記述しましょう。 まとめ 以上、基本的なSwagger YAMLの書き方についてご紹介しました! Swaggerは高機能であるゆえに、最初はとてもとっつきにくく感じてしまいます。 しかし一度覚えてしまえば、Swaggerは開発効率を上げることができるとても便利なフレームワークです。 この記事では紹介しきれなかった機能も多くあるので、ぜひ こちら のドキュメントを参考にしてみてください。 VASILYでは、一緒にバックエンド開発をする仲間を大募集しています♪ ご興味のある方は、こちらからご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com 参考 petstore.yaml Swagger Specification
こんにちは。バックエンドエンジニアインターンの田島です。 VASILYでは分析にBigQueryを使用しており、MySQLのデータを毎日BigQueryに同期しています。この同期処理を行うシステムは、約2年前にRubyで書かれたもので、プロダクトの成長に伴うデータ量の増加に耐えることができなくなり始めていました。そのため、同期処理を行うシステムを一から作り直しましたので、その詳細についてご紹介します。 弊社DEVELOPERS BLOGでは以前、『 インターン生がデータ転送基盤を一から設計する、VASILYバックエンドインターンの紹介 』としてシステムの概要・開発の流れをご紹介しましたが、今回はシステムの詳細についてご紹介します。 (photoクレジット *1 ) データ同期ツールの紹介 新たなデータ同期システムとして、次のように利用する社内ツールを作成しました。 以下のような環境変数を設定し、コマンドを実行するだけで、指定したデータベースの全てのテーブルがMySQLからBigQueryに同期されます。 export DB_HOST= DBホスト名 export DB_USER= DBユーザー名 export DB_PASS= DBパスワード export DB_DATABASE= DBデータベース名 export GCP_KEY= GCPのkeyfileへのパス $ mysql_to_bigquery また、以下のように引数を渡すことで、同期したいテーブルだけを指定することも可能です。 $ mysql_to_bigquery --tables tablename1,tablename2 システム構成 構成は以下のようになっています。 (ファイル・YAMLファイル・ほうれん草の画像クレジット *2 ) 実装詳細 Embulkについて 今回、データ同期処理に Embulk を利用しました。Embulkとは、ログ収集でよく利用されるfluentdのバッチ版のようなツールです。データベースやストレージからデータを吸い出し、別のデータベースやストレージにロードできます。YAML形式で設定ファイルを書くことで、その設定を元にEmbulkがデータを同期します。 Embulkプラグイン Embulkではプラグインが採用されており、今回の同期のシステムでは以下の2種類のプラグインを利用しています。 embulk-input-mysql embulk-output-bigquery embulk-input-mysql embulk-input-mysqlはembulk-input-jdbcの1つです。embulk-input-mysqlを利用することで、MySQLからのデータの吸い出しが可能です。以下のように設定ファイルに記述することで、テーブルごとに出力することができます。optionsを設定することで、jdbcオプションを渡すことができます。 in : type : mysql host : ホスト名 user : ユーザー名 password : パスワード database : 対象のデータベース table : 対象のテーブル select : "*" options : { useLegacyDatetimeCode : false , serverTimezone : Asia/Tokyo } 今回利用したオプションの {useLegacyDatetimeCode: false, serverTimezone: Asia/Tokyo} はTime Zoneに関するオプションです。今回のシステムでは、Embulkが動作するサーバーのTime ZoneがAsia/Tokyoとなっています。上記オプションを付けない場合、EmbulkがMySQLのTime ZoneをUTCであると判断してしまい、9時間ずれた状態でTimeStamp型のデータを取得してしまいます。 このオプションについては、以下の記事の中で教えて頂きました。Embulkコミュニティは親切な人が多いのが特徴です。 embulk-input-jdbcのMySQLプラグインで9時間時間がずれる embulk-output-bigquery embulk-output-bigqueryを利用することで、BigQueryへのデータ出力が可能です。以下のように設定ファイルを記述することで、任意の場所に吸い出したデータを格納できます。以下のように gcs_bucket を設定することで、吸い出したデータを一度GCSに格納します。これにより、BigQueryへのinsert処理の高速化とjob数の節約ができます。 out : type : bigquery mode : replace auth_method : json_key json_keyfile : json_keyfileへのパス project : プロジェクト名 dataset : データセット名 table : テーブル名 gcs_bucket : GCSのバケット名 auto_create_gcs_bucket : false compression : GZIP source_format : NEWLINE_DELIMITED_JSON default_timezone : "Asia/Tokyo" Goラッパー 今回のデータ同期システムを実装する上で、Embulkだけでは実現できないことがありました。そこでGoラッパーを作成しました。Goラッパーを作成する主な目的として以下が挙げられます。 Goラッパーを作成する目的 設定ファイル生成とEmbulkの実行 logの管理と通知 設定ファイル生成 Goラッパーを作成する1つ目の目的として設定ファイルの生成が挙げられます。Embulkでは1つのテーブルにつき1つの設定ファイルを作成する必要があります。同期するべきテーブルが複数あるので、全ての設定ファイルを手で作成することは現実的ではありません。そこで、Goラッパーによる設定ファイルの自動生成を行うことにしました。 設定ファイル生成と実行の流れ MySQLからスキーマの情報を取得 得られた情報から設定ファイルを生成 各設定ファイルを元に、Embulkを実行 MySQLからスキーマの情報を取得 複数のテーブル同期するために、最初にMySQLのスキーマ情報を取得する必要があります。そこで、 SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ( SELECT database()) というSQLを発行し、テーブル名、カラム名、カラム型を取得します。この情報を利用し、 {name: '' , colomns: [{name: '' , type : '' }]} というデータ構造を作成し保持します。 得られた情報からYAMLファイルを生成 Embulkの設定ファイルにはテンプレートエンジンの"Liquid"がデフォルトで利用できるようになっています。しかし、今回はGoとの相性を考え、Goのテンプレートエンジンである"pongo2"を利用しました。"pongo2"は"jinja"ライクなテンプレートエンジンで以下のように利用しています。 in : type : mysql host : {{ host }} user : {{ user }} password : {{ password }} database : {{ database }} table : {{ table }} select : "*" options : { useLegacyDatetimeCode : false , serverTimezone : Asia/Tokyo } out : type : bigquery mode : replace auth_method : json_key json_keyfile : json_keyfile_path project : project_name dataset : dataset_name table : {{ table }} gcs_bucket : gcs_bucket_name auto_create_gcs_bucket : false compression : GZIP source_format : NEWLINE_DELIMITED_JSON default_timezone : "Asia/Tokyo" 上記のテンプレートにMySQLから取得したスキーマ情報をそれぞれ流します。これにより得られた設定をtable_name.ymlとして保存することで、各テーブルの同期設定ファイルを作成します。 各YAMLファイルを元に、Embulkを実行 最後に、作成された設定ファイルを元にEmbulkを実行することで、テーブルが同期されます。 ログ収集と通知 Goラッパーの2つ目の目的としてはログの収集と、通知処理が挙げられます。Embulkだけでは、柔軟なログ収集とエラー発生時の通知ができないので、Go側で管理する必要があります。 弊社ではエラーの収集にhorensoを利用しています。horensoについては弊社DEVELOPERS BLOG『 horensoで作るモダンなcronスクリプト監視環境 』で紹介しています。horensoに対して適切にエラー発生を通知するには、stderrにログを出力する必要があります。Embulkではエラー発生時のログも合わせて、全てstdoutに出力されるようになっています。その為、Go側でEmbulkの終了ステータスを利用し、適切にログの出力先を変える必要があります。 まとめ Embulkを利用することで同期処理が安定し、プロダクトの成長を支えるデータ同期基盤を構築することに成功しました。また、Goラッパー作成により、運用の効率化に成功しました。Embulkはまだまだ発展中のプロジェクトでプラグインも増え続けています。Embulkのコミュニティは親切な人が多いので、困ったことがあっても素早く解決することができます。ぜひ、Embulkを利用してみてください。 最後に このシステムは、短期インターン時に一から作ったものです。VASILYのインターンでは、このような実践的・挑戦的な課題に挑むことができます。 サマーインターンへのご応募をお待ちしております!! https://www.wantedly.com/projects/103184 www.wantedly.com *1 : photo: Christopher Michel *2 : File icon: Icons made by Freepik from www.flaticon.com is licensed by Flaticon Basic License YML icon: Icons made by Yannick from www.flaticon.com is licensed by CC 3.0 BY Salad icon: Icons made by Madebyoliver from www.flaticon.com is licensed by Flaticon Basic License
こんにちは、フロントエンドエンジニアの茨木です。 VASILYでは通期でエンジニアインターンを募集しており、厳しい選考を突破した学生をインターン生として受け入れています。中にはインターン後選考に進んで内定を獲得するケースもあります。フロントエンドチームでも3月に来たT君が短期インターンを経てめでたく内定しました。本記事では、彼の事例をご紹介したいと思います。サマーインターンを考えている方々の参考になれば幸いです。 学生について T君は電気系の学科で勉強している大学4年生です。フロントエンドの経験はインターン直前までほとんど無かったそうですが、独学で弊社のフロントエンドインターン課題を突破してきました。この課題はテーマに沿ってサービスを開発するものですが、要件を満たすだけでなく開発効率化やスクレイピングへのチャレンジを行っていたのが印象的でした。 課題 T君には短期インターンの課題として、弊社が運営するIQONのAMP対応に取り組んでもらいました。AMPはGoogleなどが推進しているモバイルページ高速化の仕組みで、AMP対応により検索エンジンから来るユーザーのユーザビリティが向上します。AMP対応ではガイドラインに沿った専用ページを新たに作成する必要があります。IQONには多くのコンテンツがありますが、T君には店舗ページ・コーディネートページのAMP対応をやってもらいました。 店舗ページ ( https://www.iqon.jp/store/%E6%9D%B1%E4%BA%AC%EF%BC%88%E6%B8%8B%E8%B0%B7%EF%BC%89/19370/ ) コーディネートページ ( https://www.iqon.jp/sets/3823957/ ) 日程 今回は以下のような日程でインターンに取り組んでもらいました。 1日目 アカウント準備や開発環境の構築を済ませてから、肩慣らしで簡単な改修に取り組んでもらいました。弊社のフロントエンドインターンは初日デプロイがしきたりになっていますが、彼も無事に初日デプロイを成し遂げました。 2日目 2日目からは店舗ページのAMP対応に取り組んでもらいました。AMPページには通常ページと同じコンテンツを掲載する必要があるので、Google Mapsの地図もAMPページに掲載する必要があります。Google MapsをAMPページに埋め込むケースは初めてだったので、まず導入方法を調査してもらいました。AMP関係の公式ドキュメントはほとんど英語なのですが、彼は問題なく読みこなしていました。 3〜5日目 調査が無事完了したので実装に取り掛かってもらいました。実装完了後にはプルリクエストを出してもらい、コードレビューを行いました。チームでの開発経験が少なかった彼は20件程の指摘にやや驚いていましたが、全てしっかり対応してくれました。コードレビュー完了後はデザイナーやディレクターにも確認してもらう必要があるので、その確認依頼も彼にやってもらいました。確認完了後にデプロイ作業を行い、めでたく店舗ページのAMP対応が完了しました。 6〜8日目 店舗ページのAMP対応が早めに完了したので、コーディネートページのAMP対応をやってもらいました。 9〜10日目 VASILYのインターンでは最終日に役員に向けた成果発表があります。9日目からはそれに向けて資料作成や練習をしてもらいました。学生ではなかなか経験しない発表準備に苦労している部分もありましたが、エンジニアではない役員もいる中で全員にしっかり伝わる発表ができました。 成果 店舗ページ、コーディネートページのAMP対応が完了しました。通常のスマートフォン用ページと比べても遜色のない出来になっており、既存の使い勝手を損なうことなく検索エンジンから来るユーザーのユーザビリティを改善しました。 通常の店舗ページ AMP版店舗ページ 本人の感想 10日間のインターンはとても楽しくあっという間に過ぎていきました。 今回の課題であったAMPページの実装は、比較的新しく、これからより機能が充実して普及していく技術だと思うのでとてもいい経験になりました。 社員の皆さんはとても陽気で仕事熱心だったので、私も楽しく全力で仕事に取り組むことができました。 今回メンターをして頂いた茨木さんは、私をとても丁寧にサポートしてくださり感謝してもしきれません。 この10日間のインターンの経験を活かして、エンジニアとしてより一層精進し、茨木さんを超えて一流のエンジニアになりたいです! まとめ VASILYのインターンの特徴は社員と同様のタスクに取り組んでもらい、リリースまで責任をもってやりきってもらうことです。これは生半可なことではないので、メンターも気を引き締めてしっかりサポートしています。 もうすぐ本格的に夏が到来しますが、VASILYではサマーインターンを募集しています。やりがいや学びが多いチャレンジングなインターンであること間違いなしですので、我こそはという情熱のある学生のご応募をお待ちしております! https://www.wantedly.com/projects/103184
初めまして、データチームの上月です。 今回はVASILYテックブログ初の論文紹介、テーマは 自己回帰型モデル(Autoregressive, AR)です。 はじめに VASILYではIQONの類似画像検索にAutoencoderを適用しています。 具体的にはアイテム画像で学習したAutoencoderの潜在変数を特徴量として類似画像検索を行っていますが、背景やモデルの影響を受けやすいなどの課題があります。 この問題は「潜在変数にどのような情報を持たせるか」を調整することで解決できる可能性が高く、潜在変数の分布にはdecoderの表現力が関係しています。 最近ではVAEのdecoderとしてRNNや自己回帰型モデルなどといった表現力のあるモデルを用いたり 1 、 decoderの後にPixelCNN 2 を追加することで潜在変数がもつ情報を目的にあったものにする例があります(PixelVAE 3 )。 そこで今回はARの中でも特に、画像と相性が良いといわれている畳み込み層がメインの 自己回帰型モデルを幾つか紹介します。 はじめに生成モデルとしてメジャーなVAE 4 とGAN 5 について触れた後にARを紹介します。 VAEとGAN 生成モデルでは、データ を生成する分布 を学習することが目的になります。潜在変数 を仮定して が与えられた元での の確率 を学習するのがVAEとGANです。 VAE VAEでは潜在変数の分布を正規分布か一様分布に仮定し、 からEncoderが潜在変数 の分布の統計量 を抽出し、それらを用いて を抽出してDecoderが を生成します。 このナイーブな方法では確率的勾配降下法(以後、SGD)で直接最適化できないのでreparameterization trickを用います。 ここではEncoderが出力した と 、乱数 を用いて、潜在変数を で表すことで end-to-endの最適化が可能になります。 一方で、適切に最適化されても潜在変数の情報がほとんど無視されることがあり、optimization challengesと言われています(詳細はVLAE, InfoVAEを参照)。 下図は乱数から生成される画像の一例です。 GAN GANは をGeneratorでモデリングするためにGeneratorとDiscriminatorの2つのモデルを用いて学習します。この時、Discriminatorは与えられたデータが データセットに含まれるデータか、Generatorが生成したデータかを識別します。ナイーブなGANではDiscriminatorの予測に対する交差エントロピーを誤差として用いますが、最近はWasserstein GAN 6 やCramer GAN 7 をはじめとした確率分布間の尺度を用いて学習の安定化を図る例が増えています。 GAN自体はmin-max gameであり、学習が不安定ですが生成されるデータはVAEに比べてよりリアルなものになります。 GAN全体の目的関数はJS divergenceと関係があります。 VAEは変分下界で評価する一方、GANでは評価基準が定まっておらず、 画像の見た目でモデルを評価することが多いのが1つの欠点です。 自己回帰型モデル 自己回帰型モデルでは、VAEやGANではできなかった直接的な対数尤度最大化が可能です。 番目のデータはそれまでに生成された から 番目のデータ全てに依存すると仮定して学習を行います。 そのためVAEやGANに比べると推論が遅いという欠点があり、例えばPixelCNN++では32x32の画像16枚からなるミニバッチを 生成するのに11分かかります 8 。 画像のcontextについて 1. 生成する順序 2. 同一チャネルでのピクセル間の依存関係 3. チャネル間の依存関係 mask Aは最初に用いるmaskで同一チャネルの情報の利用を認めないマスク、mask Bは同一チャネルの情報の利用を許すマスクです。 ここでは、以下の自己回帰型モデルを紹介します。 MADE DRAW PixelRNN, PixelCNN SketchRNN MADE Germain, M., Ca, M. G., Gregor, K., Com, K. G., Deepmind, G., Murray, I., … Larochelle, H. (n.d.). MADE: Masked Autoencoder for Distribution Estimation. Retrieved from https://arxiv.org/pdf/1502.03509.pdf 課題 従来の自己回帰型モデルの深層学習はシンプルなものより 倍の計算が必要だった 解決策・優位性 マスクを適用することでモデルのアーキテクチャ・計算方法を変えずにARにした 新規性 全結合層のみからなるautoencoderを自己回帰型モデルにするためのマスクを提案 計算量削減 各層の全ユニットに を割り当てて依存関係を管理 概要 3層のMADE 3層のAutoencoderで入力層・隠れ層を結ぶ重みを 、隠れ層から出力層へ結ぶ重みを 、それぞれのマスクを とすると、このマスクを適用したAutoencoderは で表せます。 ここで、マスクは で定義されます。2つのマスクの積 の各要素が と  の依存関係を表しますが、 の要素はすべて になります。 Deep MADE 第 層のユニット数を 、 と 層の結合の重みを とします。各層の各ユニットに を割り当てます。 マスクは と同じ形の行列で表され、 で定義されます。 また、論文では を固定するのではなく、パラメータを更新するたびに をサンプリングすることで精度が上がったことが報告されています。以下、論文に掲載されている生成画像です。 この実験で使われているのはbinarized MNISTで、モデルは全結合層のみですがそれなりの精度でデータが出力されていることがわかります。 DRAW Gregor, K., Danihelka, I., Graves, A., & Wierstra, D. (2014). DRAW: A Recurrent Neural Network For Image Generation. Icml-2015, 1–16. 以下は著者が公開しているDRAWの動作の様子ですが、赤枠で囲まれている注目領域(Attentiion)の遷移がこの手法のポイントです。 www.youtube.com 課題 従来の生成モデルでは1回でデータを生成 画像の全ピクセルが同じ分布に属している状態を想定しているのと等価 そもそも人は書いたり消したりという微修正の反復で絵を生成 解決策・優位性 sequentialなVAEと canvas canvas に各時刻でのdecoderの出力を書き足していく 動的にAttentionの領域を確定 新規性 sequentialなVAEにAttentionを適用 canvasにdecoderの出力を加えることでし、 回に分けて画像を生成 Attention領域の決定には を反映 Attentionを read 関数で動的に決定 一般的なbackpropagationで学習可能 Attentionを用いる学習では強化学習で一般的な方策勾配法を使うことが多い。 概要 一回で画像を生成するのはスケールしにくいという考えから段階的に画像を生成していくモデルです。モデルは下の図のようにsequentialなVAEをアーキテクチャになっており、変分下界の最大化を目的にしています。 実際の目的関数は DRAWでは各イテレーションで画像の一部 だけ に注目(Attention)してデータを生成していきます。上の図からもわかりますが注目領域を決定する際に前のイテレーションでのdecoderの出力を利用します。 次に read と write について説明します。 read 関数によってAttentionの領域を決定します。 read, writeに関して 実際にAttentionはこのように変化していきます。 表記 画像サイズ: ガウシアンフィルタ: グリッド中心: ストライド: 中央値の位置: (i列j行) ガウシアンフィルタの分散: フィルタの出力に対する係数: Attentionに関するパラメータは の線形変換で求めます: これらの値をフィルターバンク でまとめて管理します。 は正則化項です。 read 関数はフィルターバンクを用いて で定義されます。 一方 write では新たに を から定義し、 になります。binarized MNISTでの実験結果では当時ではNLL(負の対数尤度)が最小でAttentionの有無で約7ポイントも変わっています。 PixelRNN・PixelCNN・PixelCNN++ PixelRNN & PixelCNN Oord, A. van den, Kalchbrenner, N., & Kavukcuoglu, K. (2016). Pixel Recurrent Neural Networks. Retrieved from http://arxiv.org/abs/1601.06759 モデル及び高速化の詳細はこちらの スライド を参照ください。 この論文は昨年話題になったDeepMindの WaveNet やNLPの ByteNet などの元となったとも言われています。 論文中では Row LSTM Diagonal BiLSTM PixelCNN の3つが提案されています。 課題 ピクセル間の高次元・広範囲・非線形なピクセル間の関係と、複雑な条件付き分布のモデル化には非常に表現力のあるsequenceモデルが必要 解決策・優位性 2D RNN(LSTM)を用いた12層のモデルを提案 ピクセルの値を256クラス分類問題として定義 ピクセルの値として不適切なものを出力しないようになる 自然画像では0や255がでやすいなどのガウス分布では表現できない多峰性を獲得している 新規性 2D RNN Residual Connections ピクセルの値をチャネル毎に256クラスの分類問題として定義 最終層はsoftmax 混合ガウス分布では表しにくい多峰性 0 ~ 255以外の値を取らない 概要 PixelRNNの2つは最初と最終層でそれぞれmask A、mask Bを適用しますが。PixelCNNではすべての層でmask Aあるいはmask Bを適用します。 PixelRNNは2D LSTMレイヤーでstateに適用し、畳み込みによって特定の次元のstateの計算を一回で行っています。この2D LSTMレイヤーにはRow LSTMとDiagonal BiLSTMの2種類があります。 しかしPixelRNNでは(潜在的には)どんな長さの依存関係も考慮できますが、計算コストが非常に大きいです。この代用として、CNNのみで構成したのがPixelCNNです。PixelCNNでは空間解像度を落とさず、poolingも適用していません。 反映可能な依存関係は小さくなりますが、parallelに学習できるというメリットがあります。 256クラス分類にするメリット 左上はRチャンネルの1番目のピクセルの値の分布を表しているが0と255が多い特徴を捉えていることからも256クラス分るにするメリットが伺えます。 実験・結果 実験でのアーキテクチャは以下です。 画像の補完 画像全体の整合性は取れているように見えます。 対数尤度 発表時では初の70台を達成しましたが、PixelCNN++に抜かれています。 PixelCNNの高速化 PixelCNN++ Salimans, T., Karpathy, A., Chen, X., & Kingma, D. P. (2017). PixelCNN++: Improving the PixelCNN with Discretized Logistic Mixture Likelihood and Other Modifications. Retrieved from http://arxiv.org/abs/1701.05517 課題 PixelRNNの単純化&高速化したものであるPixelCNNでも計算が遅いこと 各チャネル・各ピクセルでの256クラス分類を行っていること メモリ・計算効率が悪い 勾配が非常にスパース 解決策・優位性・新規性 softmaxを利参加したロジスティック回帰に変更 実際に0や255が近傍の値より多いという特徴が現れている 画像全体に対してconditioning dilated convolutionの代わりにダウンサンプリング ダウンサンプリングで失われた情報はショートカットで補完 dropoutによる正則化 その他 PixelCNNの高速化としては - Ramachandran, P., Paine, T. Le, Khorrami, P., Babaeizadeh, M., Chang, S., Zhang, Y., … Huang, T. S. (2017). Fast Generation for Convolutional Autoregressive Models. Retrieved from http://arxiv.org/abs/1704.06001 - Reed, S., Oord, A. van den, Kalchbrenner, N., Colmenarejo, S. G., Wang, Z., Belov, D., & de Freitas, N. (2017). Parallel Multiscale Autoregressive Density Estimation. Retrieved from https://arxiv.org/abs/1703.03664 など他にもあります。 前者はWaveNetを21倍、PixelCNNを183倍高速化に成功、後者はPixelCNNでは の計算量だったが を達成し、8x8から512x512の超解像も成功しています。 Sketch RNN 最後にRNNを用いたSketch RNNを紹介します。 実装は GitHub にあります。 課題 VAE、GANそしてARはピクセル画像が対象となっている ヒトは世界をピクセル画像のようには理解していないし、線だけで描かれた抽象的なスケッチでコミュニケーションをとる 各スケッチのデータは点のリストで表されているのでスケッチを終える判断が困難 スケッチが終わる点の数は少ないためデータに偏りがある 解決策・優位性 手書きスケッチのデータセットからヒトのようなペンの動かし方を学習する スケッチ同士の足し算ができる スケッチの終了判定を克服 新規性 ベクター画像のデータセットを作成・公開 各スケッチのデータは点のリスト(5つの要素)で表されていて、2要素はオフセット、他3要素はその点における描写の状態を表すone-hotベクター。 描写の状態には「描写を終える」も含まれている。 ベクター画像の生成とそれに特化した学習フロー 潜在空間での加減算 概要 seq2seqなVAEでベクター画像を生成するモデルを学習する手法を提案しています。 ベクター画像の場合、与えられるのは点に関するデータのリストなので描写の終了判断が求められ、その判定が難しいですが克服し、 スケッチ画像に特化した学習手法を提案しています。 データセット データセットは点のリストです。 リストの要素、つまりスケッチの各点の情報は5つの要素のベクトル です。ベクトルの初めの2要素は1つ前の点からの距離を表し、残りの3要素はone-hotベクターで各要素は : ペンが紙についていてまだ書き続ける : ペンが紙から離れる : 書く作業が終了していて、この点と後続の点がレンダリングされない ことを表しています。 モデル 図にある通り、seq2seqのVAEをベースにしています。 encoderは双方向で入力は各スケッチを表す点のリストと逆順にしたリストです。 2つのencoderの出力は結合された後に潜在変数 を表すための2つのベクトル に射影されます。 はVAE同様reparameterization trickで と計算されます。 また、decoderでは各 で を予測します。 は 個のガウス分布からなる混合ガウス分布で、 はカテゴリカル分布で をモデルします。 各 でのdecoderの出力 は 個のパラメータを持ちます。 内訳は 個の の二変数ガウス分布のパラメータ とガウス分布の混合重みの 個のパラメータ、そして です。 学習 の分布は非常に偏っているため「どこでスケッチをやめるか」の学習は非常に困難ですが、 この論文ではスケッチのデータセットの中で最も点が多いリストの要素数を として 以下のようにシンプルなロス関数を設計して学習しました。 目的関数はVAE同様にreconstruction lossとKL divergenceの和で表されます。 reconstruction lossは の負の対数尤度と の和で とおきます。 また。KL divergenceの項は潜在変数の次元数を とし、 で表されます。 最終的な目的関数は係数 を用いて となります。 実験・結果 推論時には温度パラメータ を用いて と変形することで終了するタイミングのランダム性を変更できます。 スケッチの生成 潜在変数からの生成 sketchを入力した時の出力 左右の一番下の入力はおそらくデータセットには含まれていませんが出力はデータセットに含まれるような画風に近づけられているように見えます。 潜在変数の足し引き 上段では胴体が書き足され、下段では胴体が除かれているので適切な類推が行われているように見えます。 最後に 今回は自己回帰型モデルを中心に紹介しました。 全体としてまだまだGANやVAEに比べると推論にかかる時間がネックですが対数尤度を直接最適化できるのは大きな魅力ですし 今後高速化の手法が多く出ると信じています。 データチームでは普段からarxivのcs.CVやstat.MLで画像解析・レコメンドの新規手法をチェックし、毎週のTECH ミーティングでは 1週間で読んだ主にファッションに関する論文をエンジニアに紹介しています。 研究成果を実際に課題解決に適用し、サービス改善を行うメンバーを募集しています。 少しでも興味を抱いた方はこちらから応募ください。 Søren, M. F., Sønderby, K., Paquet, U., & Winther, O. (n.d.). Sequential Neural Models with Stochastic Layers. Retrieved from https://arxiv.org/pdf/1605.07571.pdf ↩ Oord, A. van den, Kalchbrenner, N., & Kavukcuoglu, K. (2016). Pixel Recurrent Neural Networks. Retrieved from http://arxiv.org/abs/1601.06759 ↩ Gulrajani, I., Kumar, K., Ahmed, F., Taiga, A. A., Visin, F., Vazquez, D., & Courville, A. (n.d.). PIXELVAE: A LATENT VARIABLE MODEL FOR NATURAL IMAGES. ↩ Kingma, D. P., & Welling, M. (2013). Auto-Encoding Variational Bayes, (Ml), 1–14. Retrieved from http://arxiv.org/abs/1312.6114 ↩ Goodfellow, I., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., … Bengio, Y. (2014). Generative Adversarial Nets. Advances in Neural Information Processing Systems 27, 2672–2680. Retrieved from http://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf ↩ Arjovsky, M., Chintala, S., & Bottou, L. (2017). Wasserstein GAN. Retrieved from http://arxiv.org/abs/1701.07875 ↩ Bellemare, M. G., Danihelka, I., Dabney, W., Mohamed, S., Lakshminarayanan, B., Hoyer, S., & Munos, R. (n.d.). The Cramer Distance as a Solution to Biased Wasserstein Gradients. ↩ Ramachandran, P., Paine, T. Le, Khorrami, P., Babaeizadeh, M., Chang, S., Zhang, Y., … Huang, T. S. (2017). Fast Generation for Convolutional Autoregressive Models. Retrieved from http://arxiv.org/abs/1704.06001 ↩
2017年6月7日、第4回目となるFashion Tech meetupを開催しました。今回はVASILYが主催となり、バックエンドチーム、フロントエンドチーム、データチームが業務で行っている開発・運用のノウハウを発表しました。 本記事で弊社の登壇資料を公開しますので、ご参加できなかった方、Fashion Tech meetupを初めて知った方、是非ご一読ください。 メインセッション VASILY流CSSコーディング メインセッションの最初はフロントエンド担当の権守がCSSコーディングについて発表しました。 CSSコーディング規約や運用方法について紹介しています。また、デザインデータを細部まで表現するために使用しているツールや、デザイナーとのやり取りなども紹介しました。中でも大規模リファクタリング時に使用している、弊社で自作したPhantomJSを使用した差分検出ツールに注目が集まっていました。 新サービスの紹介と使用技術について 次にバックエンド担当の北條が新サービスの紹介と主要技術について発表しました。サービスの全体像から、課題とその解決のためのアプローチ方法まで詰まっています。是非スライドをご覧ください。 参加者の方々にはファッション関係の方も多く、名寄せ処理や表記ゆれに関しての共感が得られていました。また、業界問わず必要とされるクローラーについては皆さん気になっていたようです。 新サービスはα版のため、正式公開は後日となりますが、サービスの方も期待して待って頂ければと思います。 ディープラーニングでコーデを提案 最後にデータチームの中村が、ディープラーニングでコーディネートを提案するための実現方法の紹介をしました。ファッション分野では、アイテム画像だけでなく着用イメージが湧くようなスナップ画像もニーズがあります。アイテム画像をクエリとし、スナップ画像を検索する技術の開発をすることで、このニーズを満たすということを行なっています。弊社ではR&Dも行なっており、研究開発した技術はサービスへの導入実績もあります。実験結果つきでスライドを用意していますので、是非ご覧ください。 まとめ 今回はFashion Tech meetup vol.4の様子をお伝えしました。ほんの一部ではありますが、VASILYの技術を共有出来たと思います。また、本記事ではメインセッションの資料紹介となりましたが、イベントではFashion x Technologyを題材したLTや懇親会も行われています。 おわりに 今回は 株式会社リクルートマーケティングパートナーズ に会場提供をして頂きました。素敵なオフィスをお貸しして頂き、ありがとうございました。 会場でVASILYに興味を持った方、資料を見てもっと詳しく知りたいと思った方は、是非オフィスへ遊びにきてください。
データサイエンティストの中村です。 ファッションアイテムの画像から抽出した特徴量は検索以外にも利用することができます。 今回はレコメンドにおける画像特徴量の活用について、以下の3トピックを考えてみたいと思います。 画像特徴量を利用したコンテンツベースレコメンド モデルベース協調フィルタリングにおけるコールドスタート問題の軽減 画像特徴量を利用したモデルベース協調フィルタリングの高度化 画像特徴量を利用したコンテンツベースレコメンド あるアイテム を好きなユーザーは とよく似たアイテムも好きであると仮定します。このユーザーへの推薦は、 と似ているアイテムを推薦すれば良いわけですが、このとき、アイテムの類似度を評価する尺度として画像特徴量が使えます。 アイテム の画像特徴量を としたとき、アイテム とアイテム の類似度は と の間の距離を用いて表現できます。類似度(距離)にはコサイン類似度やユークリッド距離がよく使われます。 実際にAutoencoderで抽出した特徴量を使ってアイテムを推薦すると、以下のようになります。 コンテンツベースのレコメンドでは、画像特徴量に基づくアイテム類似度を予め計算しておき、直近の履歴に応じて推薦アイテムを決定します。 類似度計算にはトランザクションデータを必要としない為、コールドスタートに強い手法と言えます。 一方で、結果を見て分かる通り、推薦されるアイテムはどれも似通ってしまいます。同じデザインで価格やブランドが違う服を提案したいときには使えますが、新しい発見を得る機会は少なくなります。 モデルベース協調フィルタリングにおけるコールドスタート問題の軽減 モデルベース協調フィルタリングの例として行列分解(Matrix Factorization)があります。 行列分解のインプットはレーティング行列 であるため、レーティングが存在しないアイテムは推薦対象に含めることができません。 この問題は画像特徴量を利用することで軽減することが可能です。 行列分解 行列分解を用いてレーティング行列 を と の積に分解できたとします。このときユーザー とアイテム の相性は 中の対応する要素の内積で表現できます。 (バイアス項は省略しています) はベクトルで、アイテムの特徴量とみなすことができます。 はファクターの数です。 画像特徴量の利用 ファッションECサイトには毎日新しいアイテムが追加されています。レコメンドにもなるべく早い段階で新着アイテムを反映したいという思いがあります。 上の式を参照すると、新着アイテムの特徴量 さえ計算できれば、ユーザーベクトル と内積をとることですべてのユーザーとの相性を計算できそうです。 行列分解の計算後に画像特徴量を利用して新着アイテムの特徴量を無理矢理与えてしまおうというのがここで紹介する方法です。 レーティングが存在するアイテムの集合を 、新着アイテムを で表記します。画像特徴量はレーティングの有無に関係なくすべてのアイテム について計算可能です。新着アイテム の画像特徴量 をクエリに近傍探索を実行すれば、新着アイテムの近傍 を得ることができます。 このとき、新着アイテムの特徴量 を以下のように定義します。 新着アイテムの特徴量 は、近傍のアイテムの特徴量 の重み付き線形和とし、重みは新着アイテムとの類似度に依存する形で定義します。 これにより新着アイテムの特徴量が計算できました。実際に計算された特徴量を可視化すると以下のようになります。 左はクエリ画像、中央は画像が似ているアイテム、右は行列分解で計算した特徴量が似ているアイテムです。クエリ画像はレーティングの付いていない新着アイテムですが、行列分解で計算した特徴量を使って比較しても違和感のないアイテムが並んでいます。 今回は重みを近傍法を利用して定義しましたが、この重みは学習によって求めることも可能です。詳しくはGantner2010 *1 をご覧ください。 画像特徴量を利用したモデルベース協調フィルタリングの高度化 ここでは行列分解のモデル自体を高度化する方法を考えます。 あるユーザーのファッションアイテムに対する好みには、アイテムの見た目も影響を与えていそうです。 この直感を定式化するため、一般的な行列分解の式に画像に依存する項を加えます。 ここで、 はユーザー のアイテム の見た目に対する重み、 はアイテムの特徴量を と同じ次元にマッピングするための行列、 はアイテム の画像自体に対するバイアスです。 右辺第1項だけだと通常の行列分解となります。第2項および第3項が画像に関する項で、第2項はユーザーと画像の相互作用、第3項は画像自体の魅力を表現しています。 これをBayesian Personalized Ranking(BPR)という手法で解いたのがHe2015 *2 です。彼らはこの手法をVisual Bayesian Personalized Ranking(VBPR)と呼んでいます。 実装 VBPRをchainerで実装しました。VBPRのアルゴリズムはシンプルなのでフレームワークを使うまでもありませんが、深層学習フレームワークを利用して実装すれば実績のある最適化アルゴリズムなどの恩恵を受けることができます。 VBPRのモデルの定義は以下のようになります。 import numpy as np import chainer from chainer import functions as F from chainer import links as L from chainer import Variable class VBPR (chainer.Chain): def __init__ (self, n_user, n_item, n_latent, n_visual, d_image, reg= 0.0 ): self.n_user = n_user self.n_item = n_item self.n_latent = n_latent self.n_visual = n_visual self.d_image = d_image self.reg = reg self._layers = { 'latent_u' : L.EmbedID(self.n_user, self.n_latent), 'latent_i' : L.EmbedID(self.n_item, self.n_latent), 'visual_u' : L.EmbedID(self.n_user, self.n_visual), 'visual_i' : L.Linear(self.d_image, self.n_visual, nobias= True ), 'bias_v' : L.Linear(self.d_image, 1 , nobias= True ), } super (VBPR, self).__init__(**self._layers) for param in self.params(): param.data[...] = np.random.uniform(- 0.1 , 0.1 , param.data.shape) def __call__ (self, u, i, j, xi, xj): gamma_u = self.latent_u(u) gamma_i = self.latent_i(i) gamma_j = self.latent_i(j) theta_u = self.visual_u(u) theta_i = self.visual_i(xi) theta_j = self.visual_i(xj) bias_i = self.bias_v(xi) bias_j = self.bias_v(xj) x_uij = F. sum (gamma_u * (gamma_i - gamma_j), axis= 1 ) x_uij += F. sum (theta_u * (theta_i - theta_j), axis= 1 ) x_uij += F.reshape(bias_i - bias_j, (u.shape[ 0 ], )) loss = F.log1p(F.exp(-x_uij)) if self.reg > 0 : loss += self.reg * F. sum (gamma_u*gamma_u, axis= 1 ) loss += self.reg * F. sum (gamma_i*gamma_i, axis= 1 ) loss += self.reg * F. sum (gamma_j*gamma_j, axis= 1 ) loss += self.reg * F. sum (theta_u*theta_u, axis= 1 ) loss += self.reg * F. sum (theta_i*theta_i, axis= 1 ) loss += self.reg * F. sum (theta_j*theta_j, axis= 1 ) loss += self.reg * F. sum (bias_i *bias_i , axis= 1 ) loss += self.reg * F. sum (bias_j *bias_j , axis= 1 ) loss = F. sum (loss) / u.shape[ 0 ] chainer.report({ 'loss' : loss,}, self) 行列 の各行はユーザー/アイテムの分散表現とみなせるので、 EmbedID で定義します。画像特徴量を任意の次元数にマッピングする場合は Linear が使えます。 d_image という変数は画像特徴量の次元数です。 forwardはユーザーID、アイテムIDおよび対応する画像の画像特徴量(ベクトル)を引数に取ります。 実験 参考程度に実験を行いました。実験用に加工したIQONのデータセットを使います。ユーザー数は21382、アイテム数は99337、レーティングの数は547880です。レーティングの値は{0,1}のバイナリになっています。BPR、VBPR、VBPRの第2項と第3項(画像に関する項)(ここではVLRと呼ぶことにします)の3手法をRecall@100とnDCG@100で比べた結果、以下のようになりました。 残念ながら大勝というわけにはいきませんでした。ただし良い感触がまったくないわけではなく、オリジナルのVBPRを再現してパラメータを丁寧に決めればそれなりに差はつくのではないかと考えています。 まとめ レコメンドに画像の情報を反映する方法を紹介しました。 とくに2つ目のコールドスタート対策は簡単に拡張できて使い勝手が良いのでおすすめです。IQONデータセットの場合、この手法を適用することで10万以上のアイテムを推薦候補に加えることができます。 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 *1 : Gantner, Z., Drumond, L., Freudenthaler ,C., Rendle ,S., Schmidt-Thieme, L.: Learning Attribute-to-Feature Mappings for Cold-Start Recommendations. In: ICDM. (2010) *2 : R. He., J. McAuley.: VBPR: Visual bayesian personalized ranking from implicit feedback. In: CoRR. (2015)
こんにちは、Webフロントエンドエンジニアの権守です。 弊社では200以上の提携ECサイトから集めた大量の商品写真を取り扱っています。そのサービスの性質上、画像配信の最適化は非常に重要な課題の1つです。今回は最適化の一環として画像のレスポンシブ対応を導入しましたので、その際に調査した内容やハマったポイントなどを紹介します。 はじめに RetinaディスプレイなどのDPR(Device Pixel Ratio)の高いディスプレイの普及に伴い、Webサービス側でもその対応が必要となっています。基本的には高解像度の画像を用意すればよいですが、高解像度になればその分ファイルサイズは大きくなり、配信にかかる時間も長くなります。そのため、適切な画像を選んで配信する必要があります。 DPRについては こちら のページがわかりやすくまとまっています。 画像の配信制御 ベクター画像 高DPRな環境に対応するにあたり、コーディングという観点から一番簡単な方法はSVGなどのベクター画像を使うことでしょう。 < img src = "logo.svg" > ベクター画像であればいくら拡大しても画像が粗くなるようなことはないので、高DPRでも問題ありません。また、1つの画像をサイズ違いで複数用意し、コーディングするという手間も必要ありません(サイズ違いの画像を用いる場合のコーディングについては後述します)。 一方で、デメリットとして画像解像度が小さい場合にラスタ画像と比べるとファイルサイズが大きくなりやすい点や、写真への適用は難しいという点が挙げられます。 写真などの複雑な画像をベクター画像に変換することは難しく、変換時に画質の劣化やファイルサイズの増大が起こるので現実的に利用することは稀だと思います。ベクター画像への変換が何故難しいかはここでは割愛します。 VASILYでは、多少ファイルサイズが大きくなっても鮮明に表示したいサービスロゴなどの画像に対してSVGを使っています。 アイコンフォント ベクター形式の利用という観点では Font Awesome などのアイコンフォントを使うという手もあります。 アイコンフォントは便利ですが、いくつか欠点もありVASILYでは採用してません。アイコンフォントの欠点については こちら によくまとまっているので、参照するとよいと思います。 多様な画像フォーマットのサポート 高解像度の画像を用意しつつもファイル容量を抑えるのであれば、 WebP や JPEG XR などの新しい画像フォーマットの利用も検討してもよいかもしれません。しかし、これらのフォーマットのサポート状況はブラウザによって異なります。そのため、異なるブラウザのために複数のフォーマットで画像を用意する必要があります。 このような場合にはPicture要素とSource要素を使います。Source要素のtypeにMIMEタイプを指定することで、そのブラウザでサポートされているかどうかを上から順に判定し、最初に見つかった表示可能な画像を表示します。 < picture > < source srcset= "photo1.webp" type = "image/webp" > < source srcset= "photo1.jxr" type = "image/vnd.ms-photo" > < img src = "photo1.jpg" > </ picture > 参考URL 複数の解像度のサポート 表示する環境毎に必要な大きさの画像を選択して表示することで、転送量を抑えつつユーザ体験も保つことが出来ます。 DPR毎に出し分け サイズが固定のレイアウトでRetinaディスプレイなどの高DPRに対応する場合には、srcsetのxデスクリプションを使うとよいです。 < img src = "photo-100.jpg" srcset= "photo-200.jpg 2x, photo-300.jpg 3x" width = "100" height = "100" > 上のようにsrcsetを記述することでDPRの大きさに応じて画像を選択してくれます。srcsetをサポートしていないブラウザではsrcに指定された画像を表示するのでsrcの記述も必須です。 DPRによる画像の切り替えを検証する際には各ブラウザの開発者ツールでDPRを切り替えて確認することができます。Chromeの場合は次のように検証できます。 Retinaディスプレイの端末であってもDPR1の動作を検証できます。 [ サンプルコード ][ サンプル ] 表示サイズ毎に出し分け ウィンドウ幅の80%を横幅とするように画像を表示するといったレイアウトでは、予め画像が表示されるサイズはわかりません。こういった場合にはsrcsetのwデスクリプションを使うとよいです。 < img src = "photo-100.jpg" sizes= "80vw" srcset= "photo-200.jpg 200w, photo-400.jpg 400w, photo-600.jpg 600w, photo-800.jpg 800w" > wデスクリプションを使うことで、ブラウザ側でDPRを考慮した上でsizesから必要な画像の大きさを計算し、srcsetから必要最低限な大きさの画像を選択してくれます。仮にウィンドウサイズが320px、DPRが2だったとした場合の計算例は下のようになります。 必要な解像度 = DPR * 80vw = 2 * 320 * 0.8 = 512 この場合、必要な解像度は512pxなのでそれ以上の解像度を持つ中で一番小さいphoto-600.jpgが表示されます。 * 注意 Chromeの開発者ツールでDPRに1より大きい値を設定した場合に必要な解像度の計算が正しくありません。Androidの実機やiPhoneの実機では問題なく動作しているので、PCのChrome特有のバグかもしれません。 [ サンプルコード ][ サンプル ] また、sizesはその名の通り複数指定することが可能であり、次のように指定すればレイアウトを切り替えることもできます。 < img src = "photo-100.jpg" sizes= "(max-width: 300px) 50vw, 33.3w" srcset= "photo-200.jpg 200w, photo-400.jpg 400w, photo-600.jpg 600w, photo-800.jpg 800w" > この例ではウィンドウ幅が300px以下の場合にはウィンドウ幅の半分のサイズで画像を表示し、そうでない場合にはウィンドウ幅の3分の1の大きさで画像を表示します。このような記述をすることで、横並びのレイアウトであればウィンドウ幅に応じて2カラムの表示を3カラムの表示に切り替えられます。 [ サンプルコード ][ サンプル ] サンプルはDPRに1を設定した状態で確認することをお薦めします。 srcsetの注意点 sizes属性を指定されたimgはsizesから選ばれた大きさになりますが、CSSで明示的に大きさを指定されている場合はCSSで指定された大きさが優先されます。しかし、その場合であってもwデスクリプションの計算に用いられる大きさはsizesのものです。つまり、適切に必要な解像度を求めるためには、CSSでは大きさを指定しないかsizesとCSSで指定するimgの幅は統一する必要があります。 srcsetは同じ画像を複数の解像度で用意することを想定しているため、一度大きな画像をロードをした後にウィンドウを小さくしたとしても、小さい画像を改めて読み込むことはしません。また、同じsrcsetでsizesの小さいimgが同ページ内に存在する場合にもキャッシュされた大きな画像を利用します。 言い換えると、ウィンドウ幅に応じて画像のアスペクト比を切り替えるような指定をした場合に、ウィンドウサイズを小さくしたとしても意図した画像は表示されません。こういった画像の切り替えを行う場合には次に説明するアートディレクションを行う必要があります。 [ サンプルコード ][ サンプル ] サンプルはDPRに1を設定した状態で確認することをお薦めします。 アートディレクション ウィンドウサイズや画面の向きなどの表示している環境に応じて配信する画像の内容を変えることをアートディレクションといいます。例えば、ウィンドウサイズの横幅が大きくなった場合に背景全体も写った画像に切替えることが考えられます。 これをHTMLで表現するにはPicture要素とSource要素を使う必要があります。 < picture > < source media = "(min-width: 45em)" srcset= "large.jpg" > < source media = "(min-width: 32em)" srcset= "med.jpg" > < img src = "small.jpg" alt = "" > </ picture > [ サンプルコード ][ サンプル ] これまで説明した画像配信は組み合わせることも可能です。各組み合わせのサンプルが こちら のページにまとまっているので、合わせて参照されるとよいと思います。 遅延読み込み 画像の配信容量を抑えるテクニックの1つとして遅延読み込みを採用されている方も多いと思います。VASILYでも Lazy Load を用いて遅延読み込みを行っていました。しかし、こちらのライブラリはsrcsetに対応していません。そのため、VASILYではsrcsetの導入に伴い lazysizes を採用しました。採用に至った理由としてはsrcsetに対応していること、jQueryに非依存であることなどが挙げられます。 まとめ 今回は、Webサイトのレスポンシブ対応の中でも特に画像に着目して紹介しました。レスポンシブ対応の導入を検討している方の参考になれば幸いです。 img要素のsizesとsrcsetは HTML5.1 で策定されたものです。今回紹介した技術は比較的新しいものが多く、ブラウザによってはサポートされていないものもあるので、 Can I use などでサポート状況を確認した上での動作検証や導入をお薦めします。 最後に VASILYでは、ユーザー体験をより良くしていけるエンジニアを募集しています。興味がある方は以下のリンクから是非ご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
こんにちは。バックエンドエンジニアのじょーです。 みなさんは、開発初期の段階でWeb API(以下API)の実装が追いつかずクライアント側が開発できないという経験をしたことはありますか? クライアント側はAPIがないと開発が滞ってしまうことがありますが、かといってAPIの開発も始まったばかりではすぐに必要なAPIを提供することができません。その問題を解決し、両者でスムーズに開発をすすめるために有効な方法の1つに、APIモックの作成があります。 弊社では、開発初期の段階でWeb APIのモックを作成し、スムーズに開発できるようにしています。 以前は、Apiaryをモック作成ツールとして利用していましたが、記法やエディターに使いづらい点があり最近Swaggerに移行しました。 本記事では、Swaggerを使ったAPIモックの作成方法と手順、また気をつけるべき点などを紹介します。 目次 Swaggerとは? Swagger記法の一例 Swaggerのエディター選択 SwaggerAPIモックの動かし方 結論 まとめ Swaggerとは? Swaggerとは、REST APIを定義するためのフレームワークです。また、それを記述するためのSwaggerEditorやドキュメントツールであるSwaggerUIなどの関連ツール群のことを指します。 Swaggerを使うメリット Swaggerを使うメリットとしては、下記のような項目があります。 YAMLの記法で書ける(※JSONでも書ける) YAMLで記述したAPI定義を元に、 多数の言語によるAPIモックアプリケーションを簡単に作成できる APIドキュメントの自動生成ができる 各エントリーポイントに対してパラメーターも指定できる AWSのAPI Gatewayに反映できる Swaggerで作るAPI定義はYAMLによる記述が可能なので、1回書いたモックを引用することができ、繰り返し同じダミーデータを書く必要がなくなります。 さらに、定義したYAMLファイルを元に下記のようなAPIドキュメントを自動的に生成してくれるSwaggerUIというツールがあります。 クライアントに渡すAPIドキュメントをいちいち手書きすることもなくなるのでとても便利です。 また、米国等ではSwaggerがREST API定義のデファクトスタンダードになっているようで、最近ではAWSのAPI GatewayでもSwaggerの記法をimportすることができるようになりました。今後様々なサービスでSwaggerが使えるようになる可能性があります。 Swaggerを使うデメリット デメリットとしては下記があると感じました。 周辺ツールが豊富すぎてベストな開発方法を見つけるのに時間がかかる REST APIの定義のために作られているため、モックで実現したい記法がドキュメントになく迷うことがある まずひとつとして周辺ツールが多いので開発を始める際、調査に時間がかかります。 これについては、エディターの説明やAPIモックサーバーの立ち上げ方でいくつかの開発手法を紹介します。 ふたつめの、迷いがちな記法については後日追って同テックブログにて公開します。 Swagger記法の一例 まずはイメージが湧きやすいように、シンプルなSwagger記法(YAML)の一例を載せます。 Swaggerの記法は ドキュメント にまとまっていますが、一読しただけでは少しわかりにくい部分があるので基本的な構成を紹介します。 info : # モックのメタデータを記述する schemes : # APIモックサーバーのプロトコルの選択 paths : # エンドポイントの記述 definitions : # モデル定義の記述 properties : # モデルの詳細定義 example : # 各値の例の記述 実際に上記の基本的な構成で記述したシンプルなSwagger YAMLがこちらです。 swagger : '2.0' # モックのメタデータを記述する info : description : | This is a sample server Petstore server. You can find out more about Swagger at [ http : //swagger.io ] (http://swagger.io) or on [ irc.freenode.net, #swagger](http://swagger.io/irc/). version : 1.0.0 title : Swagger Petstore license : name : Apache 2.0 url : http : //www.apache.org/licenses/LICENSE-2.0.html # モックサーバーのプロトコルの選択 schemes : - http # エンドポイントの記述 paths : /pet/{petId} : get : tags : - pet summary : Find pet by ID description : Returns a single pet operationId : getPetById produces : - application/json parameters : - name : petId in : path description : ID of pet to return required : true type : integer format : int64 responses : 200 : description : successful operation schema : $ref : '#/definitions/Pet' 400 : description : Invalid ID supplied 404 : description : Pet not found # モデル定義の記述 definitions : Category : type : object # モデルの詳細定義 properties : id : type : integer format : int64 name : type : string Tag : type : object properties : id : type : integer format : int64 name : type : string Pet : type : object required : - name - photoUrls properties : id : type : integer format : int64 category : $ref : '#/definitions/Category' name : type : string # 各値の例の記述 example : doggie photoUrls : type : array items : type : string tags : type : array items : $ref : '#/definitions/Tag' status : type : string description : pet status in the store enum : - available - pending - sold このような記法でAPIの内容を記述していきます。 こちらのYAMLから生成したAPIモックアプリケーションをローカルに立ち上げると下記のようになります。 ↓ http://localhost:8080/docs paths で指定したパスがAPIドキュメントになっているのがわかります。 下記が、各エントリーポイントのモックの一例です。 ↓ http://localhost:8080/pet/1 propertiesで指定したモデルの構成と、exampleで指定した実際の値がモックに反映されているのがわかります。 Swaggerのエディター選択 記法がなんとなくわかったところで、どのように開発を進めていくかを紹介します。 Swaggerのエディターには下記の3つが存在しています。 SwaggerOnlineエディター SwaggerHub のエディター Localエディター この3つのエディターのそれぞれのメリット、デメリットを説明します。 また、上記3つのエディターを使ず、手元の慣れたエディターで開発する手法についても紹介します。 SwaggerOnlineエディターを使う SwaggerOnlineエディターは、Swaggerが提供している無料のOnlineエディターです。機能が限定されていますが、シンプルな分扱いやすいのが特徴です。 アカウント作成の必要もなく、誰でもすぐ使うことができます。 http://editor.swagger.io/#/ 機能は本当にシンプルで、 手元にあるYAMLをimportする YAMLを編集してAPIモックアプリケーションを自動生成する 編集したYAMLをexportする くらいしかありません。 下記は、SwaggerOnlineエディターを使うことのメリット、デメリットの表です。 内容 メリット ・扱いが簡単 ・無料で使える デメリット ・機能が少ない 簡単なモックを作りたいだけのときにはこちらのエディターで十分だと思います。 SwaggerHubのエディターを使う SwaggerHub とは、OSSのSwaggerに幾つかの機能を追加して拡張したサービスで、様々な機能を提供してくれます。 OSSのSwaggerとSwaggerHubの機能の比較の表はこちらになります。 比較表 拡張機能の中でもメリットに感じた部分とデメリットに感じた部分を下記の表にしました。 内容 メリット ・チーム(Organization)を設定してエディターの共有ができる ・APIモックサーバーをホスティングしてくれる ・エディターの編集内容をGithubに自動連携できる デメリット ・Githubにpushしたコードをエディタに取り込むことはできない ・ほとんどの機能が有料 まず注目なのはGithubに自動連携できるということです。Githubのリポジトリと紐付け、エディターでアップデートした分を自動的にcommitしてくれます。これにより、リアルタイムで複数人の開発が可能になります。ただし、手元のエディター(vimなど)で編集し、GithubにpushしたものをSwaggerHubのエディターに取り込むことはできません。 また、デプロイ機能もついており、自前でホスティングしなくてもAPIモックサーバーを提供してくれます。 デメリットは、チームを設定する機能やGithubとの連携機能が有料なことです。登録初期には無料トライアル期間がありますが、数週間のみです。 下記は記事公開時点での無料/有料の機能表です。 (出典: https://app.swaggerhub.com ) 無料なのは、上の3つのみで後はほぼ有料となります。 SwaggerLocalエディターを使う 内容 メリット ・無料で使える ・pluginを書けばエディターを拡張できる デメリット ・環境を作る必要がある SwaggerLocalエディターはSwaggerOnlineエディターをlocalに立てたものです。 Localエディターの立て方は幾つか方法があるのでSwaggerToolsの公式ドキュメントとOSSとして公開されているコードを参照してください。 (参照: SWAGGER TOOLS DOCS , swagger-editor ) デフォルトの仕様ではSwaggerOnlineエディターと機能はほとんど変わりませんが、pluginを書くことで好きなように拡張できるのが特徴です。 特に拡張などをする気がない方はSwaggerOnlineエディターで十分だと感じましたが、好きなようにエディターを拡張したい方にはおすすめです。 手元の慣れたエディターを使う ここで誰しもが思うのが、手元の慣れたエディターで開発をすすめられないかということではないでしょうか。Swaggerのエディターを使わず、手元の慣れたエディターで開発する方法があります。 SwaggerのYAMLを手元のエディターで編集し、 SwaggerCodegen を使ってAPIモックアプリケーションを生成する手法です。SwaggerCodegenとは、各種言語のAPIモックアプリケーションを吐くコマンドツールです。 さらに、GithubでYAMLを管理すればチーム開発も可能です。編集するメンバーが少人数の場合はこちらの手法は有効です。 内容 メリット ・開発しやすい ・Githubで管理すればチームで共有可能 デメリット ・自前でホスティングする必要がある(localで立ち上げてテストや確認をする必要がある) ・Swaggerの文法エラーの詳細が出ない デメリットとして、他のSwagger専用エディターと違ってSwaggerの文法エラーの詳細を出してくれないという点があります。 その際には、SwaggerOnlineエディターやローカルエディターにYAMLをロードしてエラー箇所を確認するという手段を取る必要があります。そういった点から、こちらの手法はある程度Swaggerの開発に慣れて来た場合にオススメできる方法ですが、慣れたエディターを使うので開発効率が圧倒的に良かったです。 SwaggerAPIモックの動かし方 エディターを選出してSwaggerのYAMLを記述したら、実際にSwaggerのAPIモックを動かしてみましょう。 APIモックの動かし方は4つあります。 SwaggerCodegenを使ってAPIモックアプリケーションを自動生成する 各Swaggerエディター経由でAPIモックアプリケーションを自動生成する SwaggerHubを使う API Gatewayを使う SwaggerCodegenを使ってAPIモックアプリケーションを自動生成する 内容 メリット ・YAMLをロードするだけで簡単に動くサーバーが作れる ・Swaggerの文法をそのまま適用することができ、exampleが書きやすい デメリット ・自前でサーバーを用意する必要がある 手順 1. swagger.yamlを作成する swagger.yamlを好きなエディターで編集 2. swagger-codegenをlocalにダウンロード $ git clone https://Github.com/swagger-api/swagger-codegen $ cd swagger-codegen $ ./run-in-docker.sh mvn package (複数のやり方があるので自分の環境に合ったやり方を選択する 参照: SwaggerCodegen ) 3. コマンドでモックサーバーアプリケーションを生成する $ cd swagger-codegen $ java -jar modules/swagger-codegen-cli/target/swagger-codegen-cli.jar generate -i /path/to/swagger.yaml -l nodejs-server -o samples/server/test/nodejs (今回はnodejsのアプリケーションを作成) 4. 生成したサーバーを立ち上げる $ node ./samples/server/test/nodejs/index.js => Your server is listening on port 8080 (http://localhost:8080) Swagger-ui is available on http://localhost:8080/docs 5. /docs と各エントリーポイントを見てみる http://localhost:8080/docs にアクセスする ↓ http://localhost:8080/pet/1 一旦SwaggerCodegenをlocalにダウンロードした後は、手順 3. から下の手順のみになります。もしもGithubで共有している場合は、YAMLをGithubにPushして共有のサーバーでアプリケーションを立ち上げればチームでアプリケーションを共有できます。 各Swaggerエディター経由でAPIモックアプリケーションを自動生成する 内容 メリット ・YAMLをロードするだけで簡単に動くサーバーが作れる ・Swaggerの文法をそのまま適用することができ、exampleが書きやすい デメリット ・自前でサーバーを用意する必要がある 手順はシンプルで、各エディターのヘッダー部分にGenerateServerのボタンがあるのでそこから好きな言語を選択するだけでAPIモックアプリケーションの作成ができます。 上記で説明したエディター3種類のSwaggerEditorのどれもがこのGenerateServerの機能を提供しています。 SwaggerHubを使う 内容 メリット ・ホスティングしてくれる ・Swaggerの文法をそのまま適用することができ、exampleが書きやすい デメリット ・有料 こちらの画面がSwaggerHubのエディターです。 記述したエントリーポイントを開き、 Execute ボタンを押すとモックサーバーのURLを返してくれるのでそこにアクセスするだけです。 API Gatewayを使う 内容 メリット ・ホスティングしてくれる デメリット ・Swaggerでサポートされている文法とAPI Gatewayでサポートされている文法が微妙に違う(exampleが使えないなど) ・API定義をインポートした後のコンソール上での作業が多い 手順 SwaggerYamlをロードする 各エントリーポイントごとにMockサーバーを立てる 2. の手順が長いので、画面で説明します。 ①YAMLロード後の画面からエントリーポイントを選択後(今回の場合はpetsのGET)、総合リクエストを選択 ②Mockを選択 ③保存後、YAMLロード後の画面に戻って 総合レスポンス を選択後し、本文マッピングテンプレートでモックとして返したいモデルを選択 ④YAMLロード後の画面に戻り、テストを選択 こちらの画面でテストを選択後、モックのレスポンスが表示される ⑤YAMLロード後の画面に出ているURLにアクセスするとモックを見ることができる このように、モックにするとなると画面上での工程が多かったです。 エントリーポイントが複数ある場合においてはSwaggerCodegenやSwaggerHubのほうがオススメです。 また、API Gatewayではexampleが使えなかったり、definitionsのモデルを必ずキャメルケースで定義する必要があるなど、Swagger定義の仕様と異なる部分があったので手こずりました。 結論 エディターの選択とSwaggerAPIモックの動かし方を紹介しましたが、どのツールを使うかは好みやシーンによって様々です。 参考までに、弊社で採用した開発方法は下記のような感じです。 vimで編集したSwaggerYAMLをGithubで管理し、localでテストする際はSwaggerCodegenで生成したAPIモックアプリケーションをlocalで立ち上げてテストしています。また、テストできたらSwaggerYAMLを共用サーバーにデプロイし、SwaggerCodegenで生成したアプリケーションを立ち上げてチームで共通のモックアプリケーションを見られるようにしています。 上記を採用した理由としては、 手元の慣れたエディターで編集したい チームで1つのSwaggerYAMLを共有したい 無料 があります。 まとめ SwaggerはAPIのモックを作成してくれ、ドキュメントにもまとめてくれる機能があるのでとても便利です。ぜひSwaggerのAPIモックを試してみてください! 最後に VASILYでは、一緒にサービスを盛り上げてくれるエンジニアを探しています。 ぜひオフィスに遊びに来てください!