Quantcast
Channel: クックパッド開発者ブログ
Viewing all 726 articles
Browse latest View live

モバイルアプリのアーキテクチャを考える

$
0
0

こんにちは、サービス開発部の森川 (@morishin127) です。主にクックパッドの iOS アプリの開発に携わっています。

日々アプリを開発する中で、近頃は最適なアーキテクチャとは何かを考えながら色々な形を試行錯誤しています。世の中で採用されているモバイルアプリのアーキテクチャには様々なものがあります。MVC, MVP, MVVM, VIPER, Clean Architecture などなど。開発している、あるいは開発しようとしているアプリケーションでどういったアーキテクチャを選択するかというのは難しい問題です。選択するためにはアーキテクチャに求める要件を定義する必要があります。この記事では私がアーキテクチャに求める要件と、それらをある程度満たすと考えた MVVM と Flux という2つのアーキテクチャで実装したサンプルを見つつその長所・短所について考えてみようと思います。

アーキテクチャに求める要件

アプリケーションの性質や開発チームの状況によって適したアーキテクチャが異なるため、モバイルのアプリのアーキテクチャには様々なアイデアがあります。クックパッドの iOS アプリはおよそ十数人程のメンバーで同時に開発しており、ある人の書いた実装を別の人が修正あるいは機能追加することも日常的に行われています。こういった開発体制の中で私がアーキテクチャに満たしてほしいと思う要件は下記のようなものです。

  • レイヤ毎の役割分担が明確

    • 実装者によって実装の仕方がバラバラになるのを防ぐため
  • アプリケーションが持ちうる状態と状態遷移を人間が把握しやすい

    • 想定外の状態はバグを生むため
  • レイヤ間が疎結合でありにユニットテストが書きやすい

    • テストがあると安全に変更を加えられるため

Apple の UIKit には UIViewController というクラスがあり、素直に実装すると Model と ViewController の二層構造になります。この構造が悪いという話ではないのですが、機能が増え実装が複雑になっていくにつれ、上記の要件を満たすのが困難になってきます。

これらの要件を満しつつスケールしやすいアーキテクチャを考える中で、いわゆる MVVM というパターンと Flux というパターンに倣って簡単な画面を実装してみました。実際に作っているアプリケーションでは Model 層よりも Controller (あるいは ViewController) 層が肥大化することが多かったため、状態をどのように管理し UI に反映するかという View 寄りのロジックに工夫のあるこの2つを選択しました。それぞれの実装を見ながら長所・短所を考えていきます。

サンプルの実装には RxSwiftというライブラリを使っています。

サンプルの仕様

  • 画面が表示されるとサーバーからデータを取得しリスト表示
  • 末尾までスクロールすると続きのデータをサーバーから取得し表示
  • リクエストエラー時にはアラートを表示

というシンプルなアプリケーションです。

f:id:morishin127:20170518114305g:plain:w320

MVVM

MVVM とは

Model-View-ViewModel の略で、下記のような3つのレイヤに役割を分割します。

f:id:morishin127:20170518132739p:plain:w860

  • View
    • ViewModel の持つ状態を UI に反映する
    • UI イベントの発生を ViewModel に伝える
  • ViewModel
    • 状態を持つ
    • UI イベントに応じたオペレーションを行う
    • 状態が更新されたら View に伝える
  • Model
    • ViewModel からリクエストされたデータをデータソースから取得し整形して返す

実装

MVVM パターンで実装したサンプルコードです。以下で実装の概要を説明します。

github.com

ViewModel のコードの骨子はこのようになっています。

classViewModel {
    varmodels:Observable<[Model]>= Observable.empty()

    init(inputs: (refreshTrigger:Observable<Void>, loadMoreTrigger:Observable<Void>)) {
        // UI イベントのストリームをマージして一つのストリームにするletrequestTrigger:Observable<TriggerType>= Observable
            .merge(
                inputs.refreshTrigger.map { .refresh },
                inputs.loadMoreTrigger.map { .loadMore }
            )

        // UI イベントの種類に応じて API リクエストを行いレスポンスから models のストリームを生成する
        models = requestTrigger
            .flatMapFirst { [weak self] triggerType ->Observable<ModelRequest.Response>in// 中略: API リクエスト
            }
            .startWith([])
            .shareReplay(1)
    }
}

https://github.com/morishin/RxMVVMExample/blob/master/RxMVVMExample/ViewModel.swift

initでは View からの UI イベントが流れてくるストリームを受け取り、アプリケーションの状態を表すストリーム modelsを生成します。View は UI イベントの発生をストリームに流して ViewModel に伝え、また ViewModel の modelsを購読し変更があれば UI に反映します。

View (ViewController) からは下記のように ViewModel を生成し、 modelsを購読します。

overridefuncviewDidLoad() {
    super.viewDidLoad()

    // 一度目の viewWillAppear 時にイベントが流れるletrefreshTrigger= rx.sentMessage(#selector(viewWillAppear))
        .take(1)
        .map { _ in }

    // 最後のセルが表示される時にイベントが流れるletloadMoreTrigger= tableView.rx.willDisplayCell
        .filter { [weak self] (cell, indexPath) ->BoolinguardletstrongSelf=selfelse { returnfalse }
            letisLastCell= indexPath.row == strongSelf.tableView.numberOfRows(inSection:indexPath.section) -1return isLastCell
        }
        .map { _ in }

    // UI イベントのストリームを渡して ViewModel を生成letviewModel= ViewModel(inputs: (refreshTrigger:refreshTrigger, loadMoreTrigger:loadMoreTrigger))

    // ViewModel の状態を購読
    viewModel.models
        .bind(to:tableView.rx.items(cellIdentifier:String(describing:UITableViewCell.self))) { (row, model, cell) in
            cell.textLabel?.text = model.name
        }
        .disposed(by:disposeBag)
}

https://github.com/morishin/RxMVVMExample/blob/master/RxMVVMExample/ModelTableViewController.swift

サンプルでは modelsの他に networkStatesという状態を持ち、リクエスト中であるか、あるいはエラーが発生しているかという状態を View に伝え、インジケーターやアラートの表示を行っています。詳しくはリポジトリを御覧ください。

長所・短所

  • 長所
    • ViewController に集中しがちなロジックを ViewModel に切り出すことができ、役割分担がはっきりする
    • ロジックを ViewModel に切り分けたことによりユニットテストが書きやすくなる
    • レイヤ間のデータフローが分かりやすい
  • 短所
    • UI イベントに応じた各種アクションと状態の管理を ViewModel が担うので大きくなりがち

ViewModel が肥大化する場合は VIPER の Presenter と Interactor のような役割のレイヤを用意してそれらに機能を分割するのもよいかもしれません。

次に同じ仕様のアプリケーションを Flux パターンで実装したものを示します。

Flux

Flux とは

Facebook が提唱しているアーキテクチャで、同名の JavaScript フレームワークがあります。

Flux には下記のような4つの要素があります。

  • Store
    • アプリケーションで用いるデータ・状態を保持する
  • View
    • Store の持つ状態を UI に反映する
  • Action
    • UI イベントに起因して作られる Store への要求
  • Dispatcher
    • Action を Store へ送る

これらの要素で下図のようなデータフローを構築します。View は Store の持つ状態を購読し、変更があると UI に反映します。ユーザーによるタップやスクロールといった UI イベントが起きると、Action Creator がイベントに応じた Action (API リクエスト等) を発行し Dispatcher へ渡します。Dispatcher は送られた Action を Store へ伝え、Store は Action に応じて状態を更新します。この繰り返しでアプリケーションが動作します。

f:id:morishin127:20170518114457p:plain

GitHub - facebook/flux: Application Architecture for Building User Interfaces

実装

Flux パターンで実装したサンプルコードです。以下で実装の概要を説明します。

github.com

サンプルでは Dispatcher は定義せず、Action Creator から直接 Store へデータを流しています。

Store の定義は下記のようになっています。

classStore {
    staticletinitialState= State(
        models:[],
        nextPage: .nextPage(State.initialPage),
        networkState: .nothing
    )

    varstates:Observable<State>= .empty()
    varcurrentState:State {
        return try! stateCache.value()
    }

    privateletstateCache:BehaviorSubject<State>= BehaviorSubject(value:Store.initialState)

    init(inputs:Observable<View.Event>) {
        states = inputs
            .flatMap { event in ActionCreator.action(for:event, store:self) }
            .scan(Store.initialState, accumulator:Store.reduce)
            .multicast(stateCache)
            .refCount()
    }

    staticfuncreduce(state:State, action:Action) ->State {
        varnextState= state

        switch action {
        caselet .refreshed(models, nextPage):
            nextState.models = models
            nextState.nextPage = nextPage
            nextState.networkState = .nothing
        caselet .loadedMore(models, nextPage):
            nextState.models += models
            nextState.nextPage = nextPage
            nextState.networkState = .nothing
        case .requested:
            nextState.networkState = .requesting
        caselet .errorOccured(error):
            nextState.networkState = .error(error)
        }

        return nextState
    }
}

https://github.com/morishin/RxFluxExample/blob/master/RxFluxExample/ModelTableViewController.swift#L25-L67

initで View からの UI イベントが流れてくるストリームを受け取ります。ActionCreator.action関数によって UI イベントを Action のストリームに変換し、Store.reduceでそれを状態遷移のストリーム stetesにしています。そしてこの statesを View から購読して UI に反映することで Flux のデータフローが完成します。 Store.reduceは現在の状態と Action を取り、次の状態を返す関数です。全ての状態遷移はここに集約されます。

UI イベントを Action に変換する ActionCreator.action関数の定義は下記のようになっています。

structActionCreator {
    staticfuncaction(for event:View.Event, store:Store) ->Observable<Action> {
        letcurrentState= store.currentState

        switch event {
        case .firstViewWillAppear:ifcase .requesting = currentState.networkState {
                return Observable.just(.requested)
            } else {
                letrequest= ModelRequest(page:State.initialPage)
                letresponse:Single<ModelRequest.Response>= MockClient.response(to:request)
                return response.asObservable()
                    .map { response ->Actioninreturn .refreshed(models:response.models, nextPage:response.nextPage)
                    }
                    .catchError { error ->Observable<Action>inreturn .just(.errorOccured(error:error))
                    }
                    .startWith(.requested)
            }
        case .reachedBottom:switch currentState.nextPage {
            case .reachedLast:return .empty()
            caselet .nextPage(nextPage):ifcase .requesting = currentState.networkState {
                    return .just(.requested)
                }
                letrequest= ModelRequest(page:nextPage)
                letresponse:Single<ModelRequest.Response>= MockClient.response(to:request)
                return response.asObservable()
                    .map { response ->Actioninreturn .loadedMore(models:response.models, nextPage:response.nextPage)
                    }
                    .catchError { error ->Observable<Action>inreturn .just(.errorOccured(error:error))
                    }
                    .startWith(.requested)
            }
        }
    }
}

https://github.com/morishin/RxFluxExample/blob/master/RxFluxExample/ModelTableViewController.swift#L69-L110

返り値が Actionでなく Observable<Action>になっているのは非同期処理の完了後に Action を送出したい場合があるためです。例えば API リクエストを行う場合、リクエスト開始時には .requestedという Action を Store へ送り View はインジケーターを表示し、非同期にレスポンスを取得し完了時に .refreshedという Action を Store へ送り View はインジケーターを非表示にし取得したデータを画面に描画します。

最後に View の実装です。

typealiasView= ModelTableViewController
classModelTableViewController:UIViewController, UITableViewDataSource {
    fileprivate enumEvent {
        case firstViewWillAppear
        case reachedBottom
    }

    privateletstore:Storeprivateletevents= PublishSubject<Event>()
    privatelettableView= UITableView()
    privateletactivityIndicator= UIActivityIndicatorView(activityIndicatorStyle: .gray)
    privateletdisposeBag= DisposeBag()

    privatevarmodels:[Model]= []

    init() {
        // UI イベントを流すストリームを渡して Store を生成
        store = Store(inputs:events)
        super.init(nibName:nil, bundle:nil)
    }

    overridefuncviewDidLoad() {
        super.viewDidLoad()

        // Store の状態遷移のストリームを購読し、状態に変更があれば render を呼ぶ
        store.states
            .observeOn(MainScheduler.instance)
            .subscribe(onNext:self.render)
            .disposed(by:disposeBag)

        view.addSubview(tableView)
        tableView.frame = view.bounds
        tableView.dataSource =self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier:String(describing:UITableViewCell.self))

        view.addSubview(activityIndicator)
        activityIndicator.center = view.center

        // 一度目の viewWillAppear 時に .firstViewWillAppear イベントを送出
        rx.sentMessage(#selector(viewWillAppear))
            .take(1)
            .subscribe(onNext: { [weak self] _ inself?.events.onNext(.firstViewWillAppear)
            })
            .disposed(by:disposeBag)

        // 末尾までスクロールされたら .reachedBottom イベントを送出
        tableView.rx.willDisplayCell
            .subscribe(onNext: { [weak self] (cell, indexPath) inguardletstrongSelf=selfelse { return }
                if indexPath.row == strongSelf.tableView.numberOfRows(inSection:indexPath.section) -1 {
                    strongSelf.events.onNext(.reachedBottom)
                }
            })
            .disposed(by:disposeBag)
    }

    privatefuncrender(state:State) {
        // 中略: 状態に応じた描画処理
    }
}

https://github.com/morishin/RxFluxExample/blob/master/RxFluxExample/ModelTableViewController.swift#L112-L197

initで UI イベントを流すストリームを渡して Store を生成します。また viewDidLoadStore.statesを購読することで Flux のデータフローを構築します。これで eventsに UI イベントを流すとそれに応じた Action が実行され、状態が更新されると View の renderが呼び出されるようになります。render関数は状態を受け取って UI の描画を行う関数です。サンプルでは状態に応じたインジケーターやアラートの表示、データによるテーブルビューの更新を行っています。

長所・短所

  • 長所
    • 画面が持ちうる状態と状態遷移が把握しやすい
    • 要素の役割分担がはっきりしている
    • View -> Action Creator -> Store -> View のデータフローが一方向になっていて分かりやすい
    • ステートレスな要素が多いのでユニットテストが書きやすい
  • 短所
    • コード量が膨らむ
    • 状態の一部が書き換わるだけで全ての再描画が走るのでパフォーマンスが良くない

Reactなんかは Virtual DOM によってパフォーマンスの短所を解決していますね。iOS でも実装の仕方の問題で、状態を一つのストリームにまとめずにいくつかに分割して、それぞれの状態に関係する View のみを更新する関数を接続するといった対処で改善できるかもしれません。

まとめ

例として MVVM と Flux の実装を見てきました。両者とも粒度は違うものの役割を分割することで、従来の M-VC という形よりもどこに何を実装すべきかわかりやすくなっているかと思います。またユニットテストが書きやすくなっているのも魅力です。Flux はアーキテクチャの構成という点の他にも、状態の更新を一箇所に集約し、そこ以外では状態が書き換わらないといった長所もありました。

いかがでしたでしょうか。アーキテクチャの選択はアプリケーションの性質にも依りますし、開発チームの状態にも依ります。それらを考慮しアーキテクチャに求める要件を挙げ、適したアーキテクチャを選択するように心がけましょう。


時系列データベースに関する基礎知識と時系列データの符号化方式について

$
0
0

こんにちは。インフラストラクチャー部 SRE グループの吉川 ( @rrreeeyyy ) です。今期オススメのアニメはツインエンジェル BREAK です。

普段の業務並びに趣味の一環として、サーバのモニタリング環境の調査や改善に取り組んでいます。 そこで本稿では、モニタリングのコンポーネントの一つとして外すことが出来ない、時系列データベースの基礎知識に関して紹介します。

そもそも時系列データ・時系列データベースとは?

時系列データというのは、特定の時間ごとに何らかの値を取得した際の、取得した一連の値を指します。 例えば、以下のようなフォーマットをしたデータなどは時系列データにあたるでしょう。

timestamp1,key,value1
timestamp2,key,value2
timestamp3,key,value3
:

時系列データベースとは、上記のような時系列データの保存・処理に特化したデータベースです。 Web インフラストラクチャーの文脈では、サーバのメトリクス等が時系列データにあたります。

Web サービスの文脈では、Web サーバの増加や複雑化により、高い解像度の様々なメトリクスを長期間保持したいという要望や、 サーバのメトリクスをより統計的に解析し、アラーティングの精度を向上させたいという要望などがあり、 様々な会社や組織で独自の時系列データベースが開発されているという事情があります。

数年前の時系列データベース

最近の時系列データベースの話をする前に、数年前の時系列データベース界隈の状況を少しだけ振り返ってみます。 数年前に主流であった時系列データベースとして、RRDtoolGraphiteなどがあります。

これらの時系列データベースはシンプルであるため、時系列データベースの基本的な機能を知るには有用です。しかし、様々な問題点があるのも事実です。

RRDTool では、時系列データの作成・更新・グラフの作成をする際にコマンドラインでオプションを複数指定する必要があります。 グラフの作成で使えるコマンドラインオプションは、柔軟である一方で、SQL などの標準化された書式などではなく、一部の習熟したエンジニアのみが扱える状況でした。 また、時系列データの内部的な扱いに関しても、符号化や分散などのスケーラビリティを考慮されたものではなく、大量のデータを蓄積にするにつれて、 更新や、複雑な解析をした場合に掛かる時間が線形的に増えていき、要求を満たせないという問題点がありました。

Graphite に関しては、現在でも非常に安定して動いており、様々な会社で採用されている優れた時系列データベースと言えそうです。 スケーラビリティに関しても、Graphite に関する様々なツールが OSS で開発されていたり、様々な分散構成の資料がカンファレンスなどで公開されています。 データの更新方法に関しても、HTTP API を用意しており、Graphite 形式と互換の HTTP API で書き込むことが出来る時系列データベースが新たに開発されている程度には普及しています。 一方で、時系列データを保存する構造に関しては非常に単純であるために、時系列データにタグを付けて管理・解析することや、 数秒単位でデータを書き込み続けた場合に、高性能なストレージを複数用意する必要があるなどの、いくつかの課題が未だに残っているような状態です。

その他にも、検索エンジンや RDBMS 等を時系列データベースとして利用するケースも増えてきています。 これらは、非常に安定したストレージエンジンとしての実績や、標準化された SQL などを統計手法として扱うことが出来るという特色があります。 その一方で、時系列データ専用に作られたものではないため、時系列データの解像度を上げ、数秒程度の間隔で同じデータを書き込み続けた場合、ストレージ容量の肥大化が発生したり、 頻繁に解析を行うためには、メモリを多く確保する必要がある等の問題点があることがあります。

上記のような都合から、更に時系列データの取り扱いや、時系列データの統計的解析のみに更に特化したデータベースを開発するという流れが発生してきていました。

最近の時系列データベース

最近出てきた時系列データベースとして、InfluxDBや、Gorilla(Beringei)などがあります。 これらを含め、最近の時系列データベースでは共通して、時系列データの符号化方式や、メモリもしくはストレージの使用法などのアーキテクチャに工夫がなされており、 ストレージ容量の節約や、解析時に使用するメモリ量の削減などの効果を上げることに成功しています。 また、ほとんどの時系列データベースにおいて、SQL 等をベースにしたデータ解析の方法が用意されているという特色もあります。

double-delta-encoding や XOR encoding と呼ばれるような、時系列データ内の差分を利用した符号化方法は、最近の多くの時系列データベースで使用されています。 詳細は、Facebook の作っている時系列データベースである Gorilla の論文にありますが、簡単に説明します。

double-delta-encoding (timestamp)

double-delta-encoding は、時系列データのタイムスタンプが等間隔に配置される事に着目し、タイムスタンプ値の符号化を行う方法です。 具体的には、次のように計算された値を使用して、タイムスタンプ値として実際に格納する値を決定します。 ここで、t_nは n 番目の時系列データのタイムスタンプ値とします。

f:id:rrreeeyyy:20170525225447p:plain

書き込む値は、上記で計算した Dの値によって決まります。具体的には次のようなルールになります。

  • Dの値が 0 の場合、0をそのまま書き込むため、単一ビットのみの容量で済みます。
  • Dの値が [-63, 64]の区間内にあった場合、10の後に D (7bits) の値を格納します。
  • Dの値が [-255, 256]の区間内にあった場合、110の後に D (9bits) の値を格納します。
  • Dの値が [-2047, 2048]の区間内にあった場合、1110の後に D (12bits) の値を格納します。
  • 上記のルール以外の場合、1111の後に 32bits を使用して Dの値を全て格納します。

このルールだけだと分かりづらいので、具体的な例で見ていきましょう。次のような時系列データのブロックを仮定します。 このブロックが作られた時間を、先頭に値無しのタイムスタンプとして表現しています。

1343232000
1343232062,value1
1343232122,value2
1343232182,value3

delta 並びに delta-of-delta を計算してみます。

1343232000        #=> block header
1343232062,value1 #=> delta: 62
1343232122,value2 #=> delta: 60, delta-of-delta: -2
1343232182,value3 #=> delta: 60, delta-of-delta: 0

先頭のタイムスタンプは、時系列データのブロックヘッダとして格納します (64bits)。

value1 のタイムスタンプは、まだ差分の差分を計算することが出来ません。 Gorilla では、時系列データのブロックを 4 時間 (16,384 秒) ごとに作成する事を仮定しているため、 最大限遅れたとしてもブロックヘッダのタイムスタンプと value1 のタイムスタンプの差分は、16383 (14bits) で収まります。 そのため、ブロックヘッダの次の値としては 14bits を利用して、62 (00000000111110) を書き込みます。

その先からは、先ほど説明したルールに基づいて値を書き込んでいきます。 value2 の delta-of-delta は -2 であるため、10を書き込んだ後に 7bits を使用して -2 (1111110) を書き込みます。

value3 の delta-of-delta は 0 であるため、そのまま 0を書き込みます。

これで 4 つ分のタイムスタンプの表現を書き込むことが出来ました。 ブロックヘッダから順に数えていくと、64bits, 14bits, 9bits, 1bit となり、 合計 88bits で 4 つ分のタイムスタンプを表現することに成功しています。

仮に、4 つ分のタイムスタンプを全て符号化無しで書き込んだ場合は、64 * 4 = 256bits を使用することになるため、 大きく符号化することに成功していると言えそうです。

また、この符号化方法は、時系列データが規則正しく並んでいれば並んでいるほど符号化効率が良くなる事が重要になっています。

XOR encoding

先ほどはタイムスタンプの符号化について確認しました。 タイムスタンプの場合と同じく、サーバのメトリクスやセンサデータについても、 多くの場合で、異常がない限り似た値を推移する可能性が高い、という性質があります。

次のような double 型の値を持つ時系列データを書き込む事を考えます。

timestamp1,12.0
timestamp2,24.0

近い値で推移する double 値を効率よく書き込むため、まず 2 値の XOR を取る戦略を取ります。 これは、近い値の XOR を取った場合、浮動小数点の符号部と、指数部並びに仮数部の上位ビットは 0 になりやすい、という特性があるためです。

例として、いくつかの近い 2 値の XOR を取った値を列挙していきます。

12.0 ^ 24.0 #=> 0000000000010000000000000000000000000000000000000000000000000000
24.0 ^ 15.0 #=> 0000000000010110000000000000000000000000000000000000000000000000
15.0 ^ 12.0 #=> 0000000000000110000000000000000000000000000000000000000000000000

この時、12.0 ^ 24.0では先頭の 11 個の 0並びに 1の後に続く全ての 0が符号化出来そうだと考えられます。 この時の有意なビット (meaningful bits) は 1のみであると考えられます。

24.0 ^ 15.0でも、先頭の 11 個の 0並びに、1011の後に続く全ての 0が符号化できそうです。 この時の有意なビット (meaningful bits) は 1011であると考えられます。

このような性質を利用し、double 値の書き込みは、次のルールに従って行われます。

  • 前の値との XOR が 0 (=同じ値) の場合は 0を書き込みます
  • 前の値との XOR が 0 でない場合、まず 1を書き込んだ後に次を計算します
    • meaningful bits が前の値の meaningful bits と同じであれば 0を書き込んだ後に meaningful bits を書き込みます
    • meaningful bits が前の値と違う場合、1を書き込んだ後に次の 3 つを順番に書き込みます
      • 先頭の 0の数を 5bits 表現で書き込みます
      • meaningful bits の長さを 6bits 表現で書き込みます
      • meaningful bits 自体を書き込みます

先ほど同様に、次のような時系列データのブロックを仮定します。

timestamp1,12.0
timestamp2,12.0
timestamp3,24.0

timestamp1 の値は最初の値なのでそのまま書き込まれます(64bits)。 timestamp2 の値は前の値との XOR を取り、同じ値であるので 0が書き込まれます。

timestamp3 の値は前の値との XOR を取り、違う値であるので 1を書き込んだ後、 前の値の meaningful bits を確認し、同じではないため 1を書き込みます。 その後、先頭の 0 のビットの数 (11 個) を 5bits を使って書き込みます。 その後、meaningful bits の長さ (1) を 6bits を使って書き込みます。 最後に、meaningful bits 自体 (1) を書き込みます。この時長さは 1 なので 1 bit になります。

結果として、3 つの double 型の値を書き込むのに、64bits, 1bit, 14bits の合計 79 bits で済んでいます。

仮に、3 つ分の double 値を全て符号化無しで書き込んだ場合は、64 * 3 = 192bits を使用することになるため、 こちらも、大きく符号化することに成功していると言えそうです。

また、この符号化方法も、double 値が近い値で推移するほど符号化効率が良くなる事が重要になっています。

timestamp と value の符号化をまとめた図は次のようになります (Gorilla の論文 Figure 2 から引用)。

f:id:rrreeeyyy:20170525225443p:plain

ただし、これらのエンコーディング方式を用いた場合、時系列データ内のランダムアクセスが不可能になる、といったデメリットもあります。 Gorilla では、上記のように時系列データを符号化し、基本的に全ての時系列データをメモリ上に乗せる事が可能なサイズにすることで、 時系列データのほぼ全てをメモリ上で処理し、処理速度を高速にするといった戦略を取っています。 また、長期間のデータに関しては、別のストレージを用意し、定期的にそちらに書き込むことで、別途参照が可能なようになっているようです。

様々な時系列データベースについて

先ほどは、最近の時系列データベースの象徴であるような Gorilla を例に取り紹介しましたが、 現在開発されているそれぞれの時系列データベースにも様々な特徴があるため、いくつかピックアップして簡単に紹介をします。

Beringei (Gorilla)

Beringeiは、先ほど紹介した Gorilla のアイデアをオープンソースで再実装したものです。

オープンソースとして発表された事が最近であるため、Beringei 自体に書き込みをするライブラリがあまりないことや、 読み込みに SQL などの言語を使うことが出来ないことに加え、Gorilla の論文にて紹介されている全ての機能(例えば、長期間のデータの書き出しなど)が実装されていないため、 使用されている例はあまり聞きませんが、今後の動きに注目したい時系列データベースの一つです。

InfluxDB

InfluxDBは、InfluxData 社が開発している Go 製の時系列データベースです。 2013 年頃から開発が進められており、2017/05 時点での最新バージョンは 1.2.4 になっており、安定してきていると言えそうです。

InfluxDB ではタイムスタンプにナノ秒を使用することができ、タイムスタンプ自体のエンコーディング・圧縮には delta-encoding や simple8b 等の方法が使用されています。 書き込む事が出来る値は、Integer, Float, String, Boolean など様々で、Float の符号化には先ほど説明した XOR encodig が使用されていたり、 String に対しては Snappy 圧縮が使用されています。

また、ストレージエンジンの構造として、LSM-Tree を参考に時系列データ用にチューニングした TSM-Tree というデータ構造が使われています。 Storage Engineの章に詳細が書かれているので、興味がある人は読んでみてください。

なお、InfluxDB には Enterprise 版があり、Enterprise 版では Raft を利用して複数ノードで分散して InfluxDB を運用できる事が出来るようになります。 バージョン 0.8 までは OSS 版でも使用できた機能なので、ある程度の実装は公開されており、こちらの実装も興味深いものになっています。

DalmatinerDB

DalmatinerDBは、他の時系列データベースとは少し毛色が違った、Erlang 製の時系列データベースです。 本来は SmartOS用に最適化されているようですが、Outlayer(旧 Dataloop.IO) 社が Linux 版のメンテナンスをしているようです。

DalmatinerDB は、時系列データベース自体に圧縮の機能はあまりついておらず、Snappy によるデータの圧縮が存在している程度です。 これは、データの圧縮等をファイルシステムで処理させる、という方針を取っているためで、ZFS が推奨のファイルシステムとなっています。

メタデータサーバは時系列データベース自体と分離しており、PostgreSQL が使用されているようです。

DalmatinerDB 用に作られている proxy である ddb_proxyが非常に多くのプロトコルに対応しており、 様々なメトリクス取得ツールからデータを送信できる事が強みの一つとしてありそうです。

また、Outlayer 社のベンチマークでは、他の時系列データベースより書き込みが 2 〜 3 倍程度高速であるという結果が出ていることや、 内部で Riak Core を使用して実装されているため、クラスタリングが行える、といった旨が書かれており、こちらも興味深いデータベースになっています。

Prometheus

Prometheus は厳密には時系列データベースではなく、監視システムそのものですが、 内部で実装されている時系列データベースには、様々な興味深い工夫が施されています。

例えば、時系列データのエンコーディングとして、Gorilla の delta-of-delta encoding や XOR encoding を参考にして作られた、 varbit encodingというエンコーディングが導入されていることや、 時系列データを chunk という 1024 bytes の固定長のデータに分割してメモリ上に保持し、定期的にディスクに書き込む等の方法を使用し、書き込み・読み込みが高速で行えるようになっています。

また、PromQL という Prometheus 上の時系列データを処理するための言語がよく出来ており、様々な時系列データの処理を簡単に行うことが出来るようになっています。

Prometheus そのものは時系列データベースではなく監視ツールですが、監視システムと合わせて使う用途の時系列データベースとしては非常に洗練されている印象があります。

DiamonDB

日本製の時系列データベースとして、株式会社はてなのウェブオペレーションエンジニアである @y_uuk1さんが作っている、 DiamonDBというものもあります。 こちらはまだ WIP となっていますが、AWS のマネージドサービスと連携して動く時系列データベースは構想として珍しく、 現場で大規模な時系列データベースを実際に運用してきた知見が生きていると推察でき、今後に期待をしています。

その他の時系列データベース

上記で紹介した時系列データベース以外にも、Spotify の作っている Heroicや、 Netflix の作っている atlasHadoop ベースの OpenTSDBなど、本当に様々な時系列データベースがあります。

その他の時系列データベースに関しては、少し古い内容ですが、Outlayer (旧 Dataloop.IO) という会社の Top 10 Time Series Databasesという記事並びに、 当該の記事中にある Open Source Time Series DB Comparisonというページが非常によくまとまっています。

もし時系列データベースに興味を持たれた方がいたら、上記のページも目を通してみることをおすすめします。

まとめ

時系列データや時系列データベースとは何かというところから、 時系列データベースのエンコーディング手法や、ここ数年間の幾つかの時系列データベースについて紹介しました。

大量のデータ・書き込み・読み込み処理に対して、アーキテクチャやデータ構造の工夫によって改善をしていくのは、 調査していても非常に面白く、普段の業務でも意識して取り組みたい事の一つだと改めて感じられました。 引き続き、内部アーキテクチャやデータ構造を正しく理解しつつ、ミドルウェアの選定・作成を行っていきたいと思います。

参考文献

本記事を書くにあたり、以下の記事・スライド・論文を参考にしました。

Android アプリのリソース定義ポリシーを整備した話

$
0
0

技術部モバイル基盤グループの児山(@nein37)です。 モバイル基盤グループではモバイルアプリの開発だけでなく、開発環境の整備や開発効率の向上も重要な目的の一つとしています。

昨年、開発効率向上の一環として行っているアプリのリソース整理の取り組みについてAndroidアプリのリソースを整理して開発効率を改善した話という記事で紹介させて頂きました。 今回はそれから1年が経過してリソース整理の状況がどのように変わったか説明していきたいと思います。

前回のあらすじ

詳細は前回の記事に書いてありますが、大体以下のような取り組みを行いました。

  • 未使用リソースの削除
  • Theme の定義
  • Style の整理
  • TextAppearance の定義

これらの作業により、無法地帯だったクックパッドアプリのリソースを整理し、開発効率を大幅に改善することができました。 (と、その当時は思っていました…)

その後発生した様々な問題

トップ画面の大規模変更

去年の9月ごろ行った更新により、アプリのトップ画面の構成が大きく変化しました。

変更前変更後
f:id:nein37:20170601193414p:plainf:id:nein37:20170601193420p:plain

この変更にあわせて、以下のようなリソース修正が行われました。

  • 新しいトップ画面にあわせて Style や TextAppearance を追加した
  • 新しいトップ画面に関する大量の Dimen 定義が追加された
  • 元々の定義の一部が他の画面から利用されていたため消さずに残した

画面ごとの Style の乱立

アプリ全体で利用できる汎用的な Style や TextAppearance に関しては前回の作業で定義済みだったのですが、実際にアプリの更新をしていく上で汎用的なリソースでは表現できない様々なデザイン上の例外が追加されていきました。

  • この画面のこの場所は目立たせたいので 16sp にしたい
  • この場所は普通のグレーじゃなくて暖かみのあるグレーにしたい… などなど

この当時の命名規則が細かく定まっていなかったため、ある Style がどの画面用のものなのかわからなくなってしまうという問題も発生しました。 Style のスコープがわからないため元々ある Style を再利用しても良いのかどうか判断できず、似たような Style 定義がどんどん追加されていくだけの状態になっていました。

Style 定義の度に質問が飛んでくる

すでに説明してきたとおり、 Style を新規追加するときの命名規則が緩く、定義を追加するかどうか判断するためのフローもなかったため、以下のような質問が押し寄せてくることになりました。

  • この定義を追加したいんですが良いですか?
  • この Style のはこの名前で良いですか?
  • 継承元これで良いですか?

などなど。 それぞれ考えるとちゃんと答えは出るものの、毎回エンジニアもデザイナーも悩ませてしまい、単純な Style の追加作業に関して手間取らせてしまっていました。

改善に向けて

上記のように本当にいろいろな問題が出てきましたが、これまでのポリシーでも追加するときは既存の定義を真似して追加されていくので一定の秩序はありました。 すくなくとも、去年までの無法地帯よりははるかにマシです。

そこで、これまでのポリシーをベースにしながら今回出てきた問題を踏まえてこれまでの運用で問題が出たところを見直し、新しいポリシーを整備することにしました。 ここまでで出てきた反省点を改めてまとめると以下のようなものです。

  • 汎用リソースだけではカバーできない部分が多い
    • 例外の存在を前提にする必要があった
  • 例外的なリソースを定義する方法が未定
    • リソースの定義方法(命名規則、定義場所)を決める必要があった
    • Style(TextAppearance) 以外のリソースでも決めておいたほうが良かった
  • あたらしくリソース定義をした場合、どれが汎用リソースでどれが例外リソースなのかわからない
    • 命名規則や記述ファイルで区別可能にしておく必要があった

これらの反省点を踏まえ、以下のような方針でリソースの定義ポリシーを整えることにしました。

  • 本当に汎用的に使える部分のみ汎用リソースとして定義する
    • 汎用リソースは記述ファイル名、命名規則などで容易に判別可能にする
    • 将来的に汎用にできるかも…などの曖昧な根拠で汎用リソースにしない
  • 汎用リソースで表現できないものに関しては「画面ごとのリソース」として定義する
    • 画面ごとのリソースは記述ファイル名、命名規則などで適用範囲を判別可能にする
    • 他の「画面ごとのリソース」と同じ定義をしようとしている場合、そのリソースの汎用リソース化を検討する

いままでのポリシーとの違いは主に「アプリ全体で利用するリソースとある画面のみで利用するリソースを明確に区別できるようにする」「あるリソースを汎用リソースにするかどうかを決めるタイミングを明確化する」という2点です。

次項ではリソースの種類ごとに実際の命名規則について説明していきます。

実際の定義ポリシー

Color

Color リソースはその名前の通り色の定義を行うものです。 前項と若干矛盾しますが、 Color リソースに関しては定義数が多くなく今後も一括管理可能な範囲に収まるという認識で画面ごとのスコープは設けていません。

クックパッドアプリでは Color リソースの定義に以下のような制約を持たせています。

  1. 実装上の制限がない限り、色の指定は Color リソースを参照させる
  2. アプリ内で多用される基本的な色には orange 、 green などの汎用的な名前をつける
  3. 各色については用途別に名前をつける(2.で定義した色を参照しても良い)
  4. ColorStateList 、gradient 用の色は名前を揃える(2.で定義した色を参照しても良い)

実際の定義は以下のようになります。

<!-- 2. で言及している基本的な色群 --><color name="green">#8bad00</color><color name="orange">#ff7f00</color><color name="red">#ef6074</color><!-- 3. で言及している用途別の色群 --><color name="recipe">@color/green</color><color name="ranking_arrow_up">@color/orange</color><color name="ranking_arrow_stay">#b8af93</color><color name="ranking_arrow_down">#32a9c0</color><!-- 4. で言及している用途別の色群 --><color name="button_background_primary">@color/orange</color><color name="button_background_primary_pressed">#da6e00</color><color name="button_background_primary_disabled">#ffc17d</color><color name="button_text_primary">@color/white</color><color name="button_text_primary_pressed">#d9d9d9</color><color name="button_text_primary_disabled">#ffe5c9</color>

Dimen

Dimen リソースはViewのサイズや文字サイズ、マージンなどを定義するためのものです。

こちらは画面ごとの定義が非常に多いため、画面ごとのスコープを導入し、以下のような制約を持たせています

  1. 実装上の制限がない限りサイズ指定は Dimen リソースを参照させる
  2. アプリ全体で利用する Dimen の基本単位は dimens_base.xmlに記述する
    • このとき定義するリソース名は dimen_xxdp とする
  3. アプリ全体で利用する汎用的な定義は dimens_base.xmlに記述する
    • このとき定義するリソース名は general_用途名 とする
    • 値の定義は可能な限り dimen_xxdp を参照する
  4. 画面ごとの定義は dimens.xmlに記述する
    • このとき定義するリソース名は 画面名_用途名 とする
    • 値の定義は可能な限り dimen_xxdp を参照する

dimens_base.xmlの定義は以下のようになります。

<!-- 2. で言及している Dimen の基本単位群 --><dimen name="dimen_2dp">2dp</dimen><dimen name="dimen_4dp">4dp</dimen><dimen name="dimen_8dp">8dp</dimen><dimen name="dimen_12dp">12dp</dimen><dimen name="dimen_16dp">16dp</dimen><dimen name="dimen_20dp">20dp</dimen><dimen name="dimen_24dp">24dp</dimen><dimen name="dimen_32dp">32dp</dimen><dimen name="dimen_48dp">48dp</dimen><dimen name="dimen_56dp">56dp</dimen><dimen name="dimen_64dp">64dp</dimen><!-- 3. で言及している汎用的な定義 --><dimen name="general_padding">@dimen/dimen_12dp</dimen><dimen name="general_text_padding">@dimen/dimen_12dp</dimen><dimen name="general_card_padding">@dimen/dimen_12dp</dimen><dimen name="general_text_drawable_padding">@dimen/dimen_8dp</dimen><dimen name="general_dialog_padding">@dimen/dimen_8dp</dimen>

dimens.xmlの定義は以下のようになります。

<!-- 4. で言及している画面ごとの定義 --><dimen name="user_registration_activity_top_margin">@dimen/dimen_24dp</dimen><dimen name="user_registration_activity_bottom_margin">@dimen/dimen_24dp</dimen><dimen name="user_registration_contents_margin_top">@dimen/dimen_12dp</dimen><dimen name="user_registration_paragraph_margin_top">@dimen/dimen_32dp</dimen><dimen name="user_registration_paragraph_margin_top_small">@dimen/dimen_24dp</dimen>

わざわざ dimen_xxdp という Dimen リソースを定義しているのが不思議に思えるかもしれませんが、これはデザイナーが画面設計をする際に値をなるべく選択肢の中から選ぶようにすることで統一感を出しやすくするための仕組みです。 また、dimen_xxdp を参照していないリソースを「設定値からみても明らかに例外として定義されているもの」として判別することができるようになり、後のリソース整理でも役立てることができ(る予定になってい)ます。

Style

Styleリソースはある種類の View の属性をまとめて定義するためのものです。 詳しい定義ポリシーに入る前に、 Style 特有の注意点についていくつか説明していこうと思います。

再利用性を高めるために

アプリデザインの統一感を保つ上で、 Style は非常に重要です。 複数の画面で同一の Style を使いまわすことで View の見た目を揃えることができるため、 Style 定義においては定義内容の再利用性を高めることが重要になります。 個人的に再利用性を高めるためになるべく Style に定義しないほうが良いと考えている属性は以下のようなものです。

  • layout_gravity
  • layout_weight
  • layout_above
  • layout_below(など、配置に関するもの全般)

これらの layout_*という属性は View ではなく LayoutParams の属性で、親Viewの種類や属性によっては機能しなかったり意図しない動作になったりします。 layout_width, layout_height, layout_margin あたりは大抵の ViewGroup で動作しますが、上に挙げたような属性は特定の ViewGroup でしか動作しません。 (このあたりは 各 LayoutParams の継承を見るとわかりやすいです) Android ではレイアウトファイルの include による再利用もできるようになっているので、特定のView階層構造に依存している場合はincludeを活用するなど再利用性を高める工夫をしていくと良いと思います。

継承の仕組み

Style リソースは「 parent 指定による継承」と「名前による継承」の2つの仕組みを持っています。 この2つを組み合わせることで様々な Style 定義を効率的に行うことができます。

parent 指定による継承

parent 指定による継承では親 Style を直接継承して派生 Style を作ることができます。 これは主に Android やサポートライブラリの Style を継承する場合に利用します。

<!-- AppCompatのボタンStyle定義を継承してクックパッドのボタンStyle定義を行う例 --><style name="CookpadStyle.General.Button"parent="Widget.AppCompat.Button"><item name="android:background">@drawable/button_background_default</item><item name="android:textAppearance">@style/CookpadFont.General.Button.Text</item><item name="android:paddingTop">@dimen/button_padding_vertical</item><item name="android:paddingBottom">@dimen/button_padding_vertical</item><item name="android:paddingLeft">@dimen/button_padding_horizontal</item><item name="android:paddingRight">@dimen/button_padding_horizontal</item><item name="android:drawablePadding">@dimen/button_drawable_padding</item><item name="android:minWidth">@dimen/button_min_width</item><item name="android:minHeight">@dimen/button_min_height</item><item name="android:textColor">@color/button_text_state</item></style>

名前による継承

すでに存在する Style 名に .(ドット) で続けて別の名前を与えることでその Style を継承することができます。 これは主に定義済みの Style のバリエーションを増やすために利用します。

<!-- CookpadStyle.General.Button を継承して Primaryボタン用のstyle定義を行う例 --><style name="CookpadStyle.General.Button.Primary"><item name="android:background">@drawable/button_background_primary</item><item name="android:textColor">@color/button_text_primary_state</item></style>

クックパッドにおける Style 定義のポリシー

以上の注意点を踏まえたクックパッドの Style 定義は以下のようになります。

  1. 汎用的な Style 定義は styles_widget.xmlに記述し、以下の要素をドットで繋いだリソース名とする
    • CookpadStyle(prefix)
    • General(汎用Style定義)
    • View(対象Viewの種類)
    • Variation(バリエーション、必須ではなく複数でも可)
  2. 汎用 Style 定義のうち、バリエーションが特に多いものに関しては View 種類ごとにファイルを切り出す
    • styles_button.xmlなど
  3. それ以外の Style 定義については styles.xmlに記述し、以下の要素をドットで繋いだリソース名とする
    • CookpadStyle(prefix)
    • Screen(画面)
    • View(対象Viewの種類)
    • Variation(バリエーション、必須ではなく複数でも可)

汎用 Style 定義の例

<!-- 汎用のボタンStyleのうち、最も基本的なもの --><style name="CookpadStyle.General.Button"parent="Widget.AppCompat.Button"><item name="android:background">@drawable/button_background_default</item><item name="android:textAppearance">@style/CookpadFont.General.Button.Text</item><item name="android:paddingTop">@dimen/button_padding_vertical</item><item name="android:paddingBottom">@dimen/button_padding_vertical</item><item name="android:paddingLeft">@dimen/button_padding_horizontal</item><item name="android:paddingRight">@dimen/button_padding_horizontal</item><item name="android:drawablePadding">@dimen/button_drawable_padding</item><item name="android:minWidth">@dimen/button_min_width</item><item name="android:minHeight">@dimen/button_min_height</item><item name="android:textColor">@color/button_text_state</item></style><!-- 汎用のレシピボタンStyle --><style name="CookpadStyle.General.Button.Recipe"><item name="android:background">@drawable/button_background_recipe</item><item name="android:textColor">@color/button_text_recipe_state</item></style><!-- 汎用のレシピボタンStyleにマージンを付与したもの --><style name="CookpadStyle.General.Button.Recipe.WithMargin"><item name="android:layout_marginTop">@dimen/button_margin_vertical</item><item name="android:layout_marginBottom">@dimen/button_margin_vertical</item><item name="android:layout_marginRight">@dimen/button_margin_horizontal</item><item name="android:layout_marginLeft">@dimen/button_margin_horizontal</item></style>

画面ごとの Style 定義の例

<!-- ユーザー登録画面用のEditText Style --><style name="CookpadStyle.UserRegistration.EditText"><item name="colorControlActivated">@color/orange</item><item name="android:textCursorDrawable">@drawable/edit_text_cursor_orange</item><item name="android:background">?android:attr/editTextBackground</item><item name="android:textColorHint">@color/extra_light_gray</item></style>

汎用 Style 定義と画面ごとの Style 定義を比べてみると、まず第2句が General かどうかで汎用 Style かどうか判別可能になっていることがわかると思います。 General であればアプリ全体に適用可能な汎用 Style 、そうでなければ画面ごとのスコープをもった Style です。 また、第4句以降が存在している場合、元々存在する Style のバリエーションであることもわかり、 Style 名から定義内容を追う上で役立ちます。

TextAppearance

TextAppearance は文字表示に関する属性だけをまとめた Style のサブセットです。 継承などの仕組みは Style に準じていますが、クックパッドアプリではStyleとの大きな違いとして Base TextAppearance という概念が導入されているので先にそちらを説明します。

Base TextAppearance の定義

前回のリソース整理記事でもちらっと触れていますが、クックパッドアプリでは基本的な文字サイズ・文字色・文字スタイルの組み合わせを Base TextAppearance として定義しています。 これは dimen_xxdp 同様、デザイナーの新しい TextAppearance に制約を課すために使われています。

  1. Base TextAppearance は text_appearance_base.xmlに記述し、以下の要素をドットで繋いだリソース名とする
    • CookpadFont.Base(prefix)
    • 文字サイズ(5種類:ExtraLarge/Large/Default/Small/ExtraSmall)
    • 文字色(7種類:未指定(Black)/Gray/LightGray/Green/Orange/Red/White)
    • 文字スタイル(2種類:未指定(標準)/Bold)
  2. 上記の定義について、すべての組み合わせを定義する
  3. Base TextAppearance は これを継承した TextAppearance を定義する以外の目的で利用しない
    • レイアウトファイル内などで直接参照しないこと

Base TextAppearance の定義例

<style name="CookpadFont.Base.ExtraLarge"><item name="android:textColor">@color/black</item><item name="android:textSize">@dimen/extraLargeTextSize</item></style>

...

<style name="CookpadFont.Base.ExtraLarge.Gray"><item name="android:textColor">@color/gray</item></style>

...

<style name="CookpadFont.Base.ExtraLarge.Gray.Bold"><item name="android:textStyle">bold</item></style>

という感じで名前による継承を使いつつ70種類を網羅しています。

TextAppearance の定義

  1. TextAppearance は text_appearance.xmlに記述する
  2. 汎用的なTextAppearance 定義は以下の要素をドットで繋いだリソース名とする
    • CookpadFont(prefix)
    • General(汎用)
    • Purpose(目的)
    • Style(書体:Emphasis/Main/Sub/Weaken)
  3. それ以外の TextAppearance 定義は以下の要素をドットで繋いだリソース名とする
    • CookpadFont(prefix)
    • Screen(画面)
    • Purpose(目的)
    • Style(書体:Emphasis/Main/Sub/Weaken)

Purpose(目的) はその TextAppearance を何の表示のために定義するかを書くところで、例えば「RecipeTitle」や「UserName」といったものが入ります。 Style(書体)はその TextAppearance が同一のスコープ、目的の中でどういう位置づけにあるかを示しています。 このStyle(書体)の概念は Base TextAppearance の文字サイズや太字などとは無関係に定義されるもので、「このスコープ、目的に関してはこの組み合わせが Main 」「この組み合わせが Sub 」という定義をデザイナーが相談しながら決定しています。

TextAppearance の定義例

<!-- 汎用のレシピタイトル --><style name="CookpadFont.General.RecipeTitle.Main"parent="CookpadFont.Base.Large.Green.Bold" /><!-- 汎用のレシピタイトル(小) --><style name="CookpadFont.General.RecipeTitle.Sub"parent="CookpadFont.Base.Default.Green.Bold" /><!-- 汎用のレシピタイトル(強調) --><style name="CookpadFont.General.RecipeTitle.Emphasis"parent="CookpadFont.Base.ExtraLarge.Green.Bold" /><!-- レシピ一覧画面におけるレシピタイトル --><style name="CookpadFont.RecipeList.RecipeTitle.Main"parent="CookpadFont.Base.Small.Green.Bold" />

Styleと同様に、第2句の内容を見ることでその TextAppearance が汎用定義なのか画面ごとの定義なのかを知ることができるようになりました。 TextAppearance はすべての定義が Base TextAppearance を parent によって継承するため名前による継承が行われないので少しシンプルですね。

まとめ

これまで説明してきたようなリソースの定義ポリシーを策定したことで、各リソースのスコープや意味合いについて格段に管理がしやすくなりました。 アプリの改修によってリソース自体が増殖していくという問題は引き続き発生しますが、画面スコープなどの導入によって今後無秩序な状態に陥ることはなくなったと思います。 こういった Style や TextAppearance のルールづくりは非常に地味で面倒な作業ですが、長期的にはエンジニア・デザイナー双方の作業を効率化することができます。 今回定義ポリシーを策定するにあたってはデザイナーとつきっきりで「どういう場合に例外リソースの定義が必要か」「例外はどういったスコープで捉えれば良いか」というような事について話しながら進めました。 特に dimen_xxdp による制約や TextAppearance の文字サイズの定義と連動しない4段階の Style(書式) といった概念はデザイナーの提案から生まれたものです。 こういった細かい部分にまでデザイナーの思想を反映していくことでエンジニア・デザイナーの意思疎通が少しでも簡単になればと思っています。

Kuroko2の近況

$
0
0

技術部開発基盤グループの大石です。

先日、弊社主催のイベント CookpadTechKitchen#8 〜舞台裏を支える黒衣たち〜にて、「Kuroko2の近況とクックパッドのバッチ周りの概況」というテーマで発表させて頂きました。今回はこの発表内容の中でも Kuroko2 についてピックアップして紹介したいと思います (今回の記事ではクックパッドのバッチの概況については特に触れませんが下記資料を参照ください)。

Kuroko2 とは

Kuroko2とは、Ruby製のWebベースのジョブスケジューラーです。2014年にクックパッド社内で開発され、2016年の秋にオープンソースとして公開しました

詳細については、当ブログの クックパッドのジョブ管理システム Kuroko2 の紹介Kuroko2 リポジトリのドキュメントをご覧ください。

また、Kuroko2 のオリジナル作者である弊社高井の社内用のLT資料 The Design Philosophy of Kuroko2が公開されており、kuroko1 の事例を元に Kuroko2 が採用しているアーキテクチャの背景を知ることができます。 Kuroko2 の内部についても説明されているため、Kuroko2 へコントリビュートする際にとても参考になる良い資料だと思います。

今回はこれらの資料にある背景に加えて Kuroko2 をOSS化するにあたって意識していた Kuroko2 の方針、そして実際に Kuroko2 をカスタマイズする方法を紹介したいと思います。

Kuroko2の方針

ジョブ管理、ワークフロー管理への要求はトレンドや新たな技術の登場によって細かな要求が今後も変わってくることが予想されます。 そのため、Kuroko2 はメンテナンスや拡張が行いやすいシンプルな設計を保つこと、様々な現場にあわせた拡張性を担保するために本体はシンプルに保っていくべきだと考えています。

そのために以下を方針として意識しました。

1. 現実的な運用を見据えた安定した設計を保つ

The Design Philosophy of Kuroko2 p.19にある通り、Kuroko2 は実際にクックパッドのバッチ運用を通して現実的な現場を意識して作られたものです。 闇雲に最先端の技術を採用するのではなく、あえて管理のしやすさを優先して枯れたアーキテクチャを採用し、安定した運用ができるような設計を保っていくべきだと考えています。

2. 煩雑になりがちなバッチジョブ管理の問題をUIで解決する

Kuroko2 の特徴として、煩雑になりがちなバッチ管理の方法をUIがフレームワーク的にアシストしていることが挙げられます。 具体的には、ジョブ定義の際にジョブの説明を書くためのテンプレートが用意されていたり、ジョブのお気に入り機能、自分の管理するジョブが一覧で見れるダッシュボードなどクックパッドでの運用を通して必要とされたものが実装されています。

この点は Kuroko2 の良い点であり今後も重点的に改善を進めていくべき点だと思っています。ただし、当初よりフロントエンドのアーキテクチャが古くなっており、その改良は課題であると思っています。

3. スケジューラーと汎用性の高いワークフロー管理に徹する

Kuroko2 のコアになる役割は、スケジューラーとワークフロー管理に限定し、新しい役割を定義はしないようにします。 また本体にあるタスク(docs/task.md) は汎用性の高いものだけを定義し、Kuroko2 が利用されているドメインに特化したものはカスタムタスクを利用することで柔軟性を確保するようにします。

4. うまく外の機構と連携する

ワーカーが実行する処理については、現在 command-executor からシェル経由でコマンドが実行されるのみです。この部分についても特定の機構に依存するものを本体にいれるべきではなく、うまく外の機構と連携できるように目指すべきだと考えています。 弊社が採用している例として、DWHに関連する処理についてはSQLバッチフレームワークである Bricolage を利用し、AWS ECS タスクの実行は Hako を利用することでそれぞれのドメインに合わせた連携を行っています。

ただしこの点において、シェル経由は起動コストが高いという問題があり、高頻度のジョブなどで問題が出ているため command-executor と kuroko2 console との間の連携の抽象化をしてもう少し効率的な連携が行えるような改善の必要性があると認識しています。

5. 必要な部分だけに開かれた拡張性

Kuroko2 は Rails の Mountable Engine になっており、それを gem として配布しています。 この方法を採用した理由は、Kuroko2本体の機能は常にアップデート可能なように管理できて、かつ必要に応じてカスタマイズしたかったためです。 具体的には、先述したカスタムタスクの定義や、後述する Kuroko2::ApplicationControllerを拡張という機能を想定しています。

Kuroko2 はバッチを管理する上で生じるドメインに特化した細かなカスタマイズを行えるようにして、様々な現場に合わせるための拡張性を適切に提供できるようにしたいと考えています。

Kuroko2 を拡張してみる

それでは、先述したカスタムタスクと Kuroko2::ApplicationControllerへの拡張の具体的な方法のサンプルを紹介したいと思います。

カスタムタスク

まず kuroko2 gem をマウントしているRailsアプリケーション内にカスタムタスクを置く場所を作ります (後述する kuroko2.yml の設定でnamespaceは任意に分けることはできますが、今回は簡単のため Kuroko2::Workflow::Task以下にしています)。

$ cd your_kurko2_rails_apps/
$ mkdir-p lib/kuroko2/workflow/task/

次にカスタムタスク本体のコードを上記のディレクトリ以下に置きます。

moduleKuroko2moduleWorkflowmoduleTaskclassMyProjectRunner< Executedefchdir'/home/alice/my_project'enddefshell"./bin/rails runner -e production #{Shellwords.escape(option)}"endendendend

この例では、ワーカーに設置された任意のRailsアプリケーションの中で option で渡された任意のコードを実行できます。

このカスタムタスクを kuroko2.yml で設定して、利用可能な状態にします。

....
  custom_tasks:
    my_project_runner: MyProjectRunner
....

以上の設定で、kuroko2 script 内で

 env: VAL1=A
 env: VAL2=B
 my_project_runner: MyProject::Batch.run

のようにKuroko2とは別のRailsアプリケーション内のバッチを実行するための専用のカスタムタスクを設定することができます。

クックパッドでもこのようなアプリケーション毎に設定などをまとめた専用のカスタムタスクを定義していたり、Hako を利用した Docker アプリケーションへのオプション定義のショートカットとして利用しています。 また、実験的なタスクを試したりすることもできるので、Kuroko2 本体にコントリビュートする際にも事前に検証などが行なえます。

Kuroko2::ApplicationController の拡張

次に Kuroko2::ApplicationControllerを拡張する方法を紹介します。

ここでなぜこのような拡張が必要なのかという理由を先に述べておきます。 クックパッドではアプリケーションのエラートラッキングに Sentryを採用しており、kuroko2 gem をマウントしたアプリケーションも他のアプリケーションと同様にエラーが発生した場合に Sentry で管理したいということがあったためです。

それでは、実際に拡張する例を書いてみます。

カスタムタスクと同様に以下の拡張コードを your_kurko2_rails_apps/lib以下に置きます。(アプリケーションがロードできる場所であればどこでもよいです)

moduleControllerExtentionextendActiveSupport::Concern
  included do
    before_action :additional_before_actionendprivatedefadditional_before_actionif signed_in?
      # do somethingendendend

次に、こちらもカスタムタスクと同様に kuroko2.yml に設定を行います。

...
extentions:
  controller:
    - ControllerExtention
...

今回の例はユーザーがログインしている場合になにかを行うような例にしました。 先述したクックパッドで行っている Sentry を用いたエラートラッキングでは、before_actionでログインユーザーやエラーが発生したときに必要なコンテキストを設定するようなことを行っています。

Mountable Engine を採用した利点として、 kuroko2 gem がマウントされたRailsアプリケーションからある程度のカスタマイズが可能になる点です。 いまは ApplicationControllerだけですが、必要があればこのような拡張性はある程度確保したいと考えています。

Kuroko2 のこれから

方針の中でもすこし触れましたが、具体的には、

  • UIをモダンに改善する
  • command-executor と kuroko2 console 間の連携の抽象化
  • command-executor 自体を Rails に依存しないようにして、軽量化する
  • ドキュメントの充実

など、Kuroko2 の方針に追いつけていない部分がまだまだあると認識しています。 ひとまずはこれらの課題に対して取り組んでいく必要があると考えています。

イベントで出た一部の質問とその回答

権限管理は実装しないのか

実装する予定はいまのところありません。

ジョブの実行前は確認のモーダルウィンドウが出るので誤って実行されにくいUIをしています。 さらにクックパッドではジョブを管理しているチーム以外にもたとえば障害が起きたときなど SRE や開発基盤がジョブの description をみて適宜リトライを行うようなオペレーションを行っています。DWHの領域では部署をまたがったジョブの連携が行われておりお互いにアクセスできる必要があります。またジョブに対する操作が行われた場合、いつ誰が実行・リトライしたかをログとして記録しています。 このため厳しく権限管理をするよりも性善説にたって誰でもアクセスできるようにしておいたほうがメリットが大きいと考えています。

Kuroko2::ApplicationControllerの拡張を使ってカスタマイズという形で実装することは可能だと思うので、必要があればこちらの方法をおすすめします。

補足: 現在認証については Google の G Suite のみがサポートされていますが、この点については将来的に他の認証方法などの対応は必要になってくるのかなとは感じています。

command-executor がメモリを食う

Railsに依存しすぎている部分がたしかにあるので対応したいです。 必要以上にRailsに依存しないようにすることと、方針の中で触れた command-executor の抽象化も含めて今後の課題だと認識しています。

最後に

Kuroko2 の設計方針とこれからの課題、拡張方法について紹介しました。 この記事で興味を持たれたり、導入してみたいという方は是非 Kuroko2までコントリビュートお待ちしています。 また、実際に導入したなどの事例やフィードバックなどもご連絡頂けると幸いです。

f:id:eisuke-oishi:20170614172135p:plain

Android TVアプリの自動化されたテストの小話

$
0
0

技術部の松尾(@Kazu_cocoa)です。

クックパッドでは、2年程前からAndroid TVに対してアプリをリリースしています。以前、Cookpad Android TV Appのデザインで考えたことにて触れられたこともあります。

f:id:kazucocoa:20170622175302p:plain

みなさんがGoogle Play Storeからダウンロードするクックパッドアプリには、1つのバイナリ(または apk)にスマートフォン/タブレット向けの実装とAndroid TV向けの実装が含まれています。そのため、スマートフォン/タブレット向けのクックパッドアプリのリリースサイクルと同じ周期でAndroid TV向けのアプリも更新されています。

1つのパッケージに全てのプラットフォームの実装を含めることで、どのプラットフォームにおいてもユーザーはただひとつのクックパッドアプリを探してインストールすれば良くなります。開発者側としても、パッケージ管理が煩雑にならずに済むという利点があります。一方で、例えばスマートフォン/タブレット向けの対応だと思っていたものや、共通して利用しているライブラリの更新などによりTV向けの機能が意図せず壊れる可能性があります。(ここはトレードオフになりますね)

クックパッドでは、そのような破壊が含まれないように、スマートフォン/タブレット同様、自動化されたいくつかのテストを実施しています。Android TV向けに関しては、日頃行うリリースフローの中では人間の手による確認は不要となっています。

この記事では、そのようなAndroid TV向けの、少しニッチな世界のテストコードのお話をします。

アプリの変更頻度

Android TV向けのUI変更を含んだ機能開発は、ここ1年以上の間、スマートフォン/タブレットに比べてほとんどありません。また、アプリの画面遷移数や機能も、スマートフォン/タブレット版と比べるとはるかに少ないです。そのため、リリース頻度はそれなりにあるが、リリース毎の変更はない状態を保っていました。

不具合の発見

ここ半年程度を振り返ると、2回ほど、スマートフォン/タブレット側の修正に影響され、Android TVでクックパッドアプリを操作した時にクラッシュを引き起こす不具合が混入していることがありました。1つは画像ライブラリに関係するもの、もう1つは不要な画像リソースを減らす時の対応漏れです。それらは、あらかじめ用意していた自動化されたテストだけで検出されています。

社内のエンジニアの多くは、通常はスマートフォン/タブレットを使い開発していますし、全ての開発においてほとんど影響のないAndroid TVの確認も要求することは効率的ではありません。(Android TV向けの多くのコードは分離されているため)そのため、あらかじめ想定していた戦略に沿って、期待したタイミングでちゃんと不具合を見つけることができました。

なお、そのテストコードのメンテナンスコストを考える方もいるかと思いますが、Android TV向けのテストシナリオだけを必要に迫られて修正した回数は2年間で1回です。他にはEspresso全体で共通して使ってるメソッドの置換などです。(例えば以下を見ると、fixと修正しているのは1年程度前のものだけ)

f:id:kazucocoa:20170622175321p:plain

このように、シェアが低い・対応優先度が低いものに対してテスト実行する量を減らすではなく、自動化に倒してリグレッションテストとして不具合をリリース前に検出できる形にしていました。

自動化されたテストの種類

ここからは、少し具体例を混ぜながらどのようなテストコードを書いているのかを載せていきます。以下ではテストサイズの区分を元に言葉を使っています。合わせて軽く補足を足しますが、もう少し区分を把握したい方は先ほどのリンク先を参照ください。

Sサイズ/Mサイズのテスト

広く単体テストと呼ばれるような粒度の自動テストです。これらは、スマートフォン/タブレット側と共通して使っているもの以外はほとんど書かれていません。これは、Android TV自体がログイン機能を持たないなど、最小限の機能だけを持っているため、複雑な内部ロジックを持たないビューアになっていたためです。そのため、このサイズではあまり多くをカバーせず、後述する範囲で必要なぶんだけの領域をカバーしています。

Lサイズの単体・シナリオテスト

UIの単体テストとして、もしくは一連の短い画面遷移ベースのシナリオをもとに各画面の確認をLサイズのテストとして実施しています。ここではEspressoベースのテストコードに書き上げています。

キーマッピング定義

Android TVではリモコンの上下・左右などに対して以下のようにKeyEventがあり当てられていました。そのため、Android TVの文脈における用語の対応を用意し、Espressoのシナリオを記述するときに表現が実際のTVの操作に近づくようにしました。

これは、シナリオコードの可読性をあげるためのちょっとした工夫ですね。

publicenum Keys {
    TV_UP(KeyEvent.KEYCODE_DPAD_UP),
    TV_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
    TV_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT),
    TV_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
    TV_ENTER(KeyEvent.KEYCODE_DPAD_CENTER),
    TV_BACK(KeyEvent.KEYCODE_BACK),
    TV_HOME(KeyEvent.KEYCODE_HOME),
    DEVICE_BACK(KeyEvent.KEYCODE_BACK);

    privatefinalint keyCode;

    Keys(int pKeyCode) {
        this.keyCode = pKeyCode;
    }

    public ViewAction press() {
        return pressKey(keyCode);
    }
}

また、Android TVではその特性上、特定の要素に対してスマートフォンなどでいう タップ操作がありません。基本的にはKeyEventを繰り返し入力することでカーソルを移動させたり、決定したりする必要があります。そのため、例えば以下のようにViewを特定するための一連の操作を1つのメソッドにまとめて記述し、確認したいシナリオに対してノイズになるような表現を減らしたりもしました。

private ViewInteraction onRelatedRecipeCardOnMainActivity() {
        return onView(isRoot())
                .perform(Keys.TV_DOWN.press())
                .perform(Keys.TV_DOWN.press())
                .perform(Keys.TV_DOWN.press())
                .perform(Keys.TV_DOWN.press())
                .perform(Keys.TV_DOWN.press());
    }

Eテストとアプリの更新テスト

アプリ更新時に何らかのマイグレーションなどの処理が実行されたりする場合や、ローカルデータの不整合などに出くわすとアプリが更新直後にクラッシュしたり、次回以降の起動に失敗するなど発生することがあります。 そのため、Android TVに対しても、すでにスマートフォン/タブレットに対して行なっている自動化されたUIテストの中から1つ前のアプリバージョンからのアプリ更新の確認を自動化し、検証されるようにしています。Android TVはネットワークに接続されていればほぼ強制的にアプリの更新が実行されるため、スマートフォン/タブレットでは過去7バージョンに対する更新確認を行っている一方で、1つ前の公開されているバージョンのみの確認にとどめています。

ここで行っているバージョンアップの確認は非常に簡単で、以下のコマンドのように1つ前のバージョンがローカルに保存したデータを保持したまま、新しいバージョンに更新するというものです。マイグレーション処理などがうまくいかないなどあれば、この更新した後のアプリ起動の時に処理がおかしくなります。

# 1つ古いバージョンのアプリをインストールする
$ adb shell install com.example.app
$ adb shell am start -n com.example.app/.Main

# 新しい、テストしたいアプリをインストールする
$ adb shell install -r com.example.app
$ adb shell am start -n com.example.app/.Main

まとめ

少しニッチな話題として、Android TVにおける自動化されたテスト環境、それらがどの程度のメンテナンスコストで行われているのかを書きました。このように、大きく機能追加はないが継続してリリースする必要のある機能に対しては自動化されたテストは非常に効率的に機能します。そんな状態になっているアプリの一例でした。

TVに関するとちょっとしたよもやま話

最後に、ちょっとしたAndroid TVに関わる知見を共有しておきます。UI_MODE_TYPE_TELEVISIONの値に関してです。UI_MODE_TYPE_TELEVISIONでAndroid TVかどうか判定する場合、実はAndroid TV ではない 4.x 系のセットトップボックス端末とかが引っかかることがあります。そのため、UI_MODE_TYPE_TELEVISIONをもつがAndroid TVではない環境下で、Android TV向けのAPIを呼んでしまった場合、アプリがクラッシュしてしまいます。国内ではいくつかこの状態になる端末が存在するらしく、私たちも再現するまで気づかなかったのですがこのような状態になるようです。(ただ、 UI_MODE_TYPE_TELEVISIONによる判定はGoogle公式にも書かれている方法ではあるのですが)

Android開発のコードレビューbotを乗り換えた話

$
0
0

モバイル開発で利用しているコードレビューbotを最近乗り換えた話をします。

コードレビューbotとは

コードレビューbotはPull Request(以下PR)に対して、静的解析した結果などをコメントする機能を持つプログラムの事を指します。 コードレビューbotを導入すると、些末な内容はbotが勝手に指摘してくれるため、レビューワーがより重要な内容のレビューに時間を使うことが期待できます。 有名なサービスにHoundSideCIなどがあります。

Android開発でのレビューbotの役割

CookpadのAndroid開発では、下記の項目をPR毎に実行しています。

今まではこれら全てをDokumiと呼ばれるツールで行っていました。(上記の通りDokumiではコードレビューだけではなくdeploygateへのアップロードなども受け持っていたため、 コードレビューツールと呼ぶのが適切かもしれません)

Dokumi時代

CookpadのiOS/Android開発では、自社製のDokumiというレビューbot(レビューツール)を長年愛用してきました。詳しくは下記のエントリに紹介されています。
Dokumi (日本語)

長い間お世話になってきたDokumiも、利用していく間にいくつか問題点が生まれました。

  • 依存関係が難しい
    • OSSであるdokumiと社内で利用しているdokumi-cookpad-customというmoduleの関係性が複雑だった
  • セットアップが難しい
  • デプロイ手順が煩雑(gemifyされていない)
  • Pull Request Builderが使えない(Dokumi内部でcloneしているため)
  • Dokumi以外の仕組みで動かしてる機能もあり、それらを統合したい
    • Dokumiはプラグイン機構がなく機能追加が容易ではない
  • dokumiの設定がAndroidの開発レポジトリと別で扱いづらかった
    • dokumi-cookpad-customレポジトリ内に設定が管理されていた

このような理由で、iOSの開発チームが@giginetを中心にDangerへの乗り換えを進めていました。 それに続くようにAndroid開発の環境もDangerに移行する運びになりました。 Android版Cookpadのアプリの開発環境をDokumiからDangerに移行する作業は@_litmon_が対応してくれました。

Dangerの特徴

Danger - Stop Saying "You Forgot To…" in Code Review

  • Ruby製
  • 多くのCIシステムをサポートしている
  • 多くのコードホスティングサービスをサポートしている
  • 設定をDangerfileとしてレポジトリに含める

コアは小さく、様々な機能はプラグインによって提供されています。 Dangerのプラグインは充実しているとはいえませんが、iOS環境は比較的揃っている印象です。Android関連ではFindBugs,AndroidLint,JUnitのプラグインがあります。

pluginの作成

DokumiからDangerへ移行するに当たりfindbugsプラグインがDanger側に無かったため、findbugs-dangerを作成しました。 Dangerはpluginのテンプレートをコマンドラインで作成できるなどサポートが手厚く、下記の記事を読み進めていくとrubyやruby-gemsのエコシステムに詳しくなくてもハードルは高くない印象でした。

Creating your first Plugin

Dangerの導入

導入手順は下記のページにまとめられていますが、簡単に紹介します。

Getting Set Up

Dangerはgemとして公開されています。gem install dangerとしても利用できますが、bundlerを経由した利用が推奨されているのでGemfileをプロジェクト直下に用意しましょう。

# frozen_string_literal: true
source "https://rubygems.org"

gem 'danger'

danger initを実行すると初期セットアップが行われDangerfileが作成され、セットアップウィザードのようなものが表示されます。

bundle install
bundle exec danger init

ひたすら長文のメッセージが流れてくるので、enterで進めていきましょう。

  • Step 1はDangerFileを作成したというメッセージが表示されます。
  • Step 2はbot用のGithubアカウントを作成を促されます。クールなアイコン画像の設定をすることを忘れてはいけません。
  • Step 3でbotアカウントでアクセストークンを作るように言われます。Publicなレポジトリの場合 public_repoの権限だけで問題ないそうです。
  • Step 4でCI側の設定を求められますが、詳しい案内はないので、setting-up-danger-to-run-on-your-ciを見ると良さそうです。
  • 全てのセットアップが完了したら、CIからbundle exec dangerを実行する様にセットアップしましょう。

danger initで作成されるDangerFileはとてもシンプルなので必要に応じてカスタマイズしましょう。導入に成功するとbotがPRに対してコメントを投げてくれるようになります。

※ FindBugsの結果からdangerが指摘した例

CookpadのDanderfile

2017/6末地点で、Android開発で利用しているDangerfileを公開します。導入の際はぜひ参考にしてください。

###### github comment settings#####
github.dismiss_out_of_range_messages

###### for PR#####if github.pr_title.include? "[WIP]" || github.pr_labels.include?("WIP")
  warn("PR is classed as Work in Progress") 
end# Warn when there is a big PR
warn("a large PR") if git.lines_of_code > 300# Warn when PR has no milestone
warn("A pull request must have a milestone set") if github.pr_json["milestone"].nil?

# Warn when PR has no assignees
warn("A pull request must have some assignees") if github.pr_json["assignee"].nil?

###### Findbugs#####
findbugs.report_file = "your_module/build/reports/findbugs/findbugs.xml"
findbugs.gradle_module = "your_module_name"
findbugs.report(true)

###### Android Lint#####
android_lint.gradle_task = "your_module:lint"
android_lint.report_file = "your_module/build/reports/lint/lint-result.xml"
android_lint.filtering = true
android_lint.lint(inline_mode: true)

まとめ

Cookpadにおけるコードレビューbotの役割や、ツールを乗り換えた経緯、Dangerの特徴などを紹介などをしました。コードレビューbotをもし導入されていないのであればDangerはおすすめ出来ます。手軽にセットアップが可能なのでぜひお試しください。

UICollectionView の Layout で悩んだら

$
0
0

こんにちは、サービス開発部の氏です。
主にiOSのクックパッドアプリの開発を担当しています。

UICollectionViewLayoutみなさん使ってますか?
UICollectionViewでレイアウトを組む際、実際触り始めると実装するための選択肢が複数あり、どれが最適なのか悩ましい場面に遭遇する人もいるのではないかと思います。
今回は、自分が業務で触れた際に得た知見について軽くお話したいと思います。

UICollectionVIewLayout とは

UICollectionViewは Cellのサイズや余白等のレイアウトを管理するため、プロパティとして、 UICollectionViewLayoutを所持しています。
この UICollectionViewLayoutに手をいれることによって、レイアウトを好きな形に変更することができます。

レイアウトを組み立てるときの複数の選択肢

実際に UICollectionViewLayoutをいじろうとすると、大きく分けて三つの選択肢が出てきます。

  1. UICollectionViewFlowLayoutを調整する
  2. UICollectionViewDelegateFlowLayoutを実装する
  3. UICollectionViewLayout (Custom)を作成する

つづけて、各Layoutで出来ること、出来ないことを挙げていきたいと思います。
どんなレイアウトの組み上げ方をすればよいか等、判断に困った際の参考にしていただければ幸いです。

1. UICollectionViewFlowLayoutを調整する

一つ目は UICollectionViewFlowLayoutをそのまま利用する方法です。
InterfaceBuilderで UICollectionViewを設置すると、初期値としてこの UICollectionViewFlowLayoutが設定されています。
UICollectionViewFlowLayoutでは、CellやHeader/FooterのSize等がプロパティとして用意されており、それを変更するだけで良い感じに組み上げてくれます。

letflowLayout= UICollectionViewFlowLayout()  
letmargin:CGFloat=3.0  
flowLayout.itemSize = CGSize(width:100.0, height:100.0)  
flowLayout.minimumInteritemSpacing = margin  
flowLayout.minimumLineSpacing = margin  
flowLayout.sectionInset = UIEdgeInsets(top:margin, left:margin, bottom:margin, right:margin)  
letcollectionViewController= CollectionViewController(collectionViewLayout:flowLayout)  

ですが、Cellの大きさを決める itemSizeでは、動的な変更が行なえません。
全てのCellを同じ大きさで表示するのであれば、UICollectionViewFlowLayoutを利用すると良いでしょう。

2. UICollectionViewDelegateFlowLayoutを実装する

二つ目は UICollectionViewDelegateFlowLayoutを実装する方法です。 UICollectionViewDelegateFlowLayoutUICollectionViewDelegateを継承した Protocolになっており、各種便利メソッドが用意されています。
基本的には、 UICollectionViewFlowLayoutのプロパティと同等なものが準備されています。

extensionCollectionViewController:UICollectionViewDelegateFlowLayout {  

    funccollectionView(_ collectionView:UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAt indexPath:IndexPath) ->CGSize {  
        if indexPath.row %3==0 {  
            return CGSize(width:100.0, height:100.0)  
        }  
         return CGSize(width:60.0, height:60.0)  
    }  

    funccollectionView(_ collectionView:UICollectionView, layout collectionViewLayout:UICollectionViewLayout, insetForSectionAt section:Int) ->UIEdgeInsets {  
        return UIEdgeInsets(top:margin, left:margin, bottom:margin, right:margin)  
    }  

    funccollectionView(_ collectionView:UICollectionView, layout collectionViewLayout:UICollectionViewLayout, minimumLineSpacingForSectionAt section:Int) ->CGFloat {  
        return margin  
    }  

    funccollectionView(_ collectionView:UICollectionView, layout collectionViewLayout:UICollectionViewLayout, minimumInteritemSpacingForSectionAt section:Int) ->CGFloat {  
        return margin  
    }  
}  

このレイアウトの利点としては、 indexPathの情報を参照出来るため、動的なCellのサイズ変更が可能です。
ですが、Protocolとして用意されているものでしか変更を行うことが出来ないため、アニメーションを伴う変化には余り適していないと思います。

3. UICollectionViewLayout (Custom)を作成する

最後は UICollectionViewLayoutを継承した独自レイアウトを作成する手段です。

自由にレイアウトを組める反面、今までに挙げた2通りの様に良しなにレイアウトを組んでもらえません。
CellやSectionなど各要素の配置先を計算する必要があり手間がかかりますが、その分動的なサイズ変更やレイアウト変更を好きなように行うことができます。(自分で書くので当然ですが…)

UICollectionViewLayoutを継承して利用するには、下記の処理を実装する必要があります。

collectionViewContentSize: CGSize

UICollectionViewcontentSizeを返します。
UICollectionViewは、この contentSizeをもとにスクロール量を判断します。
その為、ここでは表示させたい要素に応じた正確な contentSizeを返さないと思った通りの位置までスクロールをしてくれません。

layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

IndexPathに応じたCellの UICollectionViewLayoutAttributesを返します。
UICollectionViewLayoutAttributesIndexPathに応じたセルのレイアウト属性です。
この layoutAttributesにCellのサイズと座標を指定しておくと、指定通りの座標に表示されます。
この中でCellのサイズ計算等を行う場合、時間がかかる処理などがあるとカクつきの原因となります。
よくある方法として、prepare()でレイアウト情報を先に計算して配列などに用意しておきここではその情報を返すだけとするケースが多いです。

layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

範囲内に含まれる UICollectionReusableView (cellやsupplementary view)の UICollectionViewLayoutAttributesの配列を返します。
基本的には、その範囲に含まれる layoutAttributesForItem(at indexPath: IndexPath)を取得してくる形になるでしょう。

アニメーションについて

レイアウトを作っていると、アニメーションを求められるケースがそれなりにあるかと思います。
レイアウト変更時のアニメーションは下記の様な形でアニメーションを行うことができます。

collectionView.setCollectionViewLayout(newLayout, animated:true)  

他には、Cellの生成時や削除時のレイアウト属性を返すメソッドがあり、それを実装することで insertItems, deleteItemsでもアニメーションをさせる事ができます。

  • initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath)
  • finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath)

また、「UICollectionViewController, UINavigationControllerの組み合わせでのみ」という制限がありますが、
UICollectionViewControlleruseLayoutToLayoutNavigationTransitionsの値を trueにしてpushさせると UINavigationBarと連携した遷移が可能です。

letviewController= UICollectionViewController(collectionViewLayout:newLayout)  
viewController.useLayoutToLayoutNavigationTransitions =true  
navigationController?.pushViewController(viewController, animated:true)  

UICollectionViewController以外への遷移アニメーションは、 UIViewControllerAnimatedTransitioningを実装してあげると良いでしょう。

まとめ

UICollectionViewのレイアウトを作るに当たって、ほとんどのケースでは UICollectionViewFlowLayoutUICollectionDelegateFlowLayoutで事足りるかと思います。

独自レイアウトを採用するケースとしては、行ベース、グリッドベース以外のレイアウトが必要なケースや、各要素のレイアウトが頻繁に変化する場合に必要になってきます。(カバーフローのようなものだったり)

UICollectionViewはiOS10から prefetchUICollectionViewFlowLayoutAutomaticSize等の新しい機能も追加され、表現の幅も増えています。

クックパッド内で利用するのはまだ少し先かもしれませんが、ユーザーがより良い体験を提供できるよう常に心がけていきたいですね。

Cookpad Ruby Hack Challenge

$
0
0

f:id:koichi-sasada:20170629153022p:plain

技術部の笹田です。Ruby インタープリタの開発をしています。最近は Fiberまわりを10年ぶりにいじってます。

2017/08/30, 31 に、Cookpad Ruby Hack Challengeというイベントを行いますので、その宣伝をさせてください。

Cookpad Ruby Hack Challenge とは

クックパッドで Ruby インタプリタを Hack しよう!

クックパッドをはじめ、多くのウェブアプリケーション開発でプログラミング言語 Ruby が利用されています。Ruby で書かれたプログラムを動かすときは Ruby インタプリタで実行します。

Cookpad Ruby Hack Challenge は、この Ruby インタプリタに対して機能を追加したり、改良したり、性能向上させたりする方法、つまり Ruby インタプリタを Hack する方法を、二日間かけてお伝えするイベントです。

イベント概要

二日間かけて、Ruby インタプリタをハックします。一日目は共通課題として、用意する資料を手順どおりに進めて頂きます。二日目は発展課題として、Ruby インタプリタに残る未解決問題に取り組んで頂きます。両日とも、講師は Ruby コミッタの笹田が務めます。

また、二日目には特別企画として、クックパッド以外の Ruby 開発者とも交流できる場を用意したいと思っています。Ruby という言語や、Ruby インタプリタに対する疑問や意見がある人は、この機会にぜひ議論してもらえればと思います。

学生の方には、ぜひ夏休みのアクティビティの一つとして楽しんで貰えると幸いです。

なお、本イベントは、以前ご紹介した社内イベント Hackarade: MRI Internal Challengeを、よりわかりやすくブラッシュアップしたものになります。

こんな方に来てほしい

  • Ruby インタプリタの Hack がしてみたい方
  • Ruby プログラムは書けるけど、どうやって動いているのか知りたい方
  • プログラミング言語 Ruby および Ruby インタプリタを普段開発しているような人達が、何を考えているのか知りたい方
  • 難易度の高いプログラミングに挑戦してみたい方
  • 夏休みの思い出が欲しい方

共通課題(1日目)のゴール

  • Ruby のソースコードの構造を知る
  • Ruby のビルドができるようになる
  • Ruby の中身を弄ることができるようになる

発展課題(2日目)のゴール(できれば)

  • 未解決問題を解決する
  • 実際に Ruby インタプリタへの貢献を体験する
  • 開発コミュニティへの参加を体験する

スケジュール

注意:時間等は変わる可能性があります。

開催前

  • 7/27 (木) 募集締め切り
  • 7/29 (土) 参加者抽選決定
  • Gitter を用いたオンライン予習サポート(希望者)

8/30 (水) 一日目

  • 10:00 オープニング
  • 10:30 ハックに必要となる事前知識の講義
  • 12:00 ランチ
  • 13:00 共通課題
  • 16:00 発展課題の紹介と割り振り

8/31 (木) 二日目

  • 10:00 発展課題の開始
  • 11:30 まつもとゆきひろ氏 特別講演
  • 12:00 Ruby開発者を交えてのランチ
  • 13:00 Ruby開発者との Q&A セッション
  • 14:00 発展課題の再開
  • 18:00 打ち上げパーティー

なんでクックパッドで開催するの?

前節までは、募集ページそのままの内容でした。これだけではなんなので、本イベントを開催する動機について、少し触れておきます。

いくつかあるのですが、まずは IT エンジニアコミュニティへの還元です。クックパッドは「毎日の料理を楽しみにする」という目標のために、多くの IT 技術を活用しています。我々は OSS としてプロダクトを公開したり、この開発者ブログでの情報発信をすることで、エンジニアコミュニティへの還元を行っていますが、本イベントはその一環です。おそらく、本イベントに興味を持つ方は、向上心があり、これからのエンジニアコミュニティを牽引して下さる、かもしれない、方々だと思うので、そのような方々へ支援することは大事なことだと思っています。

笹田個人の動機としては、このようなイベントをきっかけに言語処理系といったシステムソフトウェア開発に興味を持つ人が少しでも増えてくれたり、優秀な Ruby コミッタが増えてくれると嬉しいです(Ruby にはまだまだ改良の余地がありますが、人手が足りていません)。最近、システムソフトウェアといったコンピュータの「裏方」に興味を持ってくれる人が少なくなっているような気がします。いろいろな理由があるかと思いますが、一つは難しそう、といった「とっつきづらさ」があるのではないかと思います。本イベントを通して、そのようなハードルを越えるお手伝いができればと思っています。

このような思いを実現するためには、長期的に継続して活動を行っていく必要があると考えており、本イベントはその第一歩です。うまくいけば、第二弾や、別テーマでの開催も検討できるのではないかと思っています。そもそも、クックパッド1社でやってもスケールしないので、もっと広げたいと思っており、夢(だけ)は広がります。というわけで、まずは本イベントがうまくいくといいなぁ。

おわりに

というわけで、Cookpad Ruby Hack Challengeのご紹介でした。 ご興味とお時間がある方は、ぜひご応募ください。


ファイルを直接読み込んで集計する

$
0
0

こんにちは。マーケティングプロダクト開発部の中村です。今回は大量のデータを対象に集計できる Hive の使い方について説明しようと思います。

前提

私が所属しているマーケティングプロダクト開発部では広告配信も行っています。その広告配信では大量のアクセスログを蓄積しています。通常ですとそのログは Amazon Redshift で簡単に集計できます。しかし、ログファイルを直接集計しなければならない場合が稀にあります。その際に使用しているのが Amazon EMR です。今回は Hive を用いてその集計を手元の端末で試してみます。

インストール

まずは動作環境を作るために Hive をインストールします。

brew install hive

集計する前の準備

Hive は任意のディレクトリを作業ディレクトリとすることができます。まず、その作業ディレクトリを作成し、そのディレクトリに移動しておきます。

mkdir -p /tmp/cookpad/logs
cd /tmp/cookpad

次に、その作業ディレクトリで使用する Schema の種類を指定する必要があります。今回は資料でデフォルトで使われてる derbyを選択します。

schematool -initSchema -dbType derby

ここまでの作業で、ローカルで起動させるための準備ができました。次に、実際に起動させてみます。

hive

Hive のコンソールが立ち上がれば成功です。

集計してみる

次にサンプルをもとに簡単に集計します。今回は Nginx のアクセスログをサンプルとして集計してみようと思います。具体的には以下の内容のファイルを /tmp/cookpad/logs/nginx.logとして、先程作成したディレクトリ以下に保存します。

172.17.0.1 - - [14/Jul/2017:07:48:37 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:48:38 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:48:38 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:19 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:40 +0000] "GET /hoge HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:42 +0000] "GET /hoge HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:51 +0000] "GET /piyo HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:52 +0000] "GET /piyo HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:53 +0000] "GET /piyo HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"

次にそのファイルを集計するクエリを用意します。なお Hive は起動時にクエリをファイルから読み込めるため、ファイルに書いた方が後から参照できて便利です。具体的には以下の内容で /tmp/cookpad/sample.qとして保存します。

drop table nginx_logs;

create external table nginx_logs (
    remote_addr string
    , remote_user string
    , time_local string
    , method string
    , path string
    , protocol string
    , status int
    , body_bytes_sent int
    , http_referer string
    , http_user_agent string
)
row format serde
  'org.apache.hadoop.hive.serde2.RegexSerDe'
with serdeproperties (
    "input.regex" = "([0-9\\.]+) - ([^ ]*) \\[([^\\]]*)\\] \"([^ ]*) ([^ ]*) ([^ ]*)\" ([0-9]*) ([0-9]*) \"(.*)\" \"(.*)\""
)
location
    'file:///tmp/cookpad/logs'
;

select
    *
from
    nginx_logs
where
    path = '/hoge'
;

その保存したファイルを指定して Hive を起動します。

cd /tmp/cookpad/
hive -f /tmp/cookpad/sample.q

クエリが実行されてアクセスログが表示されれば成功です。

UDF を書いて Hive に組み込む

Hive には様々な関数が組み込まれています。通常の集計ではその関数で充分なのですが、時折複雑な条件で集計したくなります。そのようなときは関数を自作して組み込んで使用することができます。

実際に関数を組み込んで集計してみます。ただし、少し準備することが多いので事前にコードは GitHub に用意しておきました。以下のリポジトリを任意の場所に clone してください。

https://github.com/devisualy/udf

そのクローンした場所に移動してビルドします。具体的には以下のコマンドを実行します。

mvn package

ビルド成功すると target/devisualy_udf.jarができるはずなので Hive の作業ディレクトリに配置します。

cp target/devisualy_udf.jar /tmp/cookpad/devisualy_udf.jar

Hive を起動して組み込んでみます。

cd /tmp/cookpad
hive
hive> ADD JAR /tmp/cookpad/devisualy_udf.jar;
hive> CREATE TEMPORARY FUNCTION converter as 'devisualy.Converter';

OKのような表示が出れば成功しています

UDF を使って集計する

上記までで Hive 上で自作の関数を使う準備は整いました。次に実際にクエリを投げてその関数を使ってみます。具体的には以下のようなクエリを Hive のコンソール上で実行します。

select method, converter(method) from nginx_logs limit 1;

クエリ内で同じ method を参照していますが converter という関数の戻り値が method とは異なっているのを確認できます。

これまでの作業でできるようになったこと

上記までの作業で以下のことができるようになりました。

  • 集計対象ファイルを Hive で読み込めるようになった
  • そのファイルに対してクエリを投げられるようになった
  • そのクエリから自作の関数を呼べるるようになった

その中でも自作の関数を呼べるようになったのは強力です。クエリだけではなかなか表現しきれないビジネスロジックを表現できます。また UDF ファイルを共有することにより他人がその表現を簡単に流用することができます。

まとめ

Hive の基本的な使い方について説明しました。前提にも書きましたが、私が所属しているマーケティングプロダクト開発部では広告配信も行っているため、大量のアクセスログを蓄積しています。そのログに対してローカルで Hive を使うということは無く Amazon EMR を使って高速にログを集計しています。

AWS には、似たようなことが簡単に可能な Amazon Athena というサービスもあります。しかし、集計対象のデータが大きいと料金が高くなるため Amazon EMR を使ったほうが良い場合もあるかと思います。

これらの技術により、大量のデータを高速にかつ簡単に集計できるようになりました。次はその集計結果をどのように活用できるかです。いつかログからユーザーを想像できるようになれればいいなと思っています。

Slack 上のエンジニア同士の会話を増やした一つの工夫 + ちょっとした OSS の紹介

$
0
0

こんにちは、技術部開発基盤グループの小室 (id:hogelog) です。

最近エンジニアが全員集まる Slack のチャンネルからデプロイ通知等の機械的な通知を排除したらエンジニア同士のコミュニケーションがほぼ毎日発生するようになり満足しています。自分のような無名なペーペーエンジニアも業界に名を馳せる著名エンジニアもフラットに属しているチャンネルが通知で埋まっていて人間の会話がまったく発生しないなんてもったいないですからね。Slack のチャンネルをどう運用するか会社によって文化の違いがあると思いますが、良い運用は参考にしたいので各社どんどん発信してほしいのでよろしくおねがいします。

さてそんな話で終わっても良いのですが、ここは開発者ブログだしせっかくなので最近開発した Slack 関連のアプリケーションを紹介します。

tokite で GitHub から Slack への通知をカスタマイズ

クックパッドでは GitHub Enterprise (以下 GHE) 上で日々開発やレビューなどをおこなっています。そこで生まれる Issue や Pull Request などを Slack に通知するためのツールとして tokite というツールを実装しました。

https://github.com/hogelog/tokite

今までは主にメールとJasper *1等を利用して GHE 上でのレビューやコミュニケーションを進めていました。 Slack 通知も利用していましたが、GitHub の Slack 通知はリポジトリ毎の設定しか存在せず、自分が見たい通知のみを特定のチャンネルに流すといったことはできませんでした。

それを解決するため作成したのがユーザ毎のルールで通知を設定できる tokite というツールです。

f:id:hogelog:20170719111939p:plain

tokite のルール

tokite ではユーザ毎に任意個数の通知ルールを設定します。ルールにはそれぞれクエリと通知先等を設定し、どのようなイベントを Slack のどのチャンネルに流したいかを決められます。

社内で運用している tokite に自分が設定しているルールは以下の3つです。

ルール名クエリ解説
opsrepo:tech-dept/ops開発基盤グループのタスク管理リポジトリの Issue を流すルール
呼ばれたbody:/sunao-komuro|dev-infra|hogelog/メンション等で自分のアカウント名、グループが呼ばれた時に流すルール
dev-infra-memberuser:/XXX|YYY|ZZZ/チームメンバーの発言を流すルール

また通知先のチャンネルは自分専用の通知チャンネルとして運用し、全発言が Notification を出すように設定しているためこれらのルールにマッチするイベントが発生すれば Slack 経由で通知が届くようになっています。

tokite の動作

tokite は各リポジトリの Webhook を元に、それぞれのイベントにマッチするルールがあれば Slack の対象チャンネルに通知するという動きをします。 ルールのクエリ実装はなにか既存のライブラリやフォーマットがないものだろうか、と少し探したのですがちょうど良いものが見当たらなかったため parslet *2という PEG ライクなパーサライブラリを利用しました。PEG ベースのパーサライブラリは使ったことがなかったのですが、ドキュメントも丁寧で使いやすいライブラリでした。 ユーザ認証には GitHub を利用していて Webhook 追加も各ユーザのトークンをそのまま利用しています。

tokite で得られたもの

tokite を使うことでリアクションが必要な GHE 上で発生する同僚の行動 (Pull Request, Issue, Issue Comment) の通知をほぼ Slack に集約し、すぐに気付けるようになりました。

tokite を使う前でもメール、Jasper 等の経路でそれらの行動を観測することはできていました。ただし、僕にとってはそれはどうもかなり意識的におこなわなければできないことで、見逃しも度々ありました。一方で自分は Slack の通知ならばすぐに気づき自然に読み始めることができています*3。tokite による通知だけで完結するという程には至らずメールの通知や Jasper に頼っているところもあるのですが、Slack という経路が一つ増やせたのは割と実装した意味があったなと思っています。

今後の予定

tokite はある方が便利だなと感じていますが、今のところ機能は色々と足りていません。今後も OSS の形で公開しながら改善を続けていこうと思うので、よろしくおねがいします。

https://github.com/hogelog/tokite

*1:http://techlife.cookpad.com/entry/2017/03/14/100000

*2:http://kschiess.github.io/parslet/

*3:何故だろう。Slack のアプリケーションがよくできているからというのが大きい気がしている

時差のあるリモートワークをやってみて

$
0
0

こんにちは、インフラストラクチャー部データ基盤グループの井上寛之(@inohiro)です。私事ですが今年の3月から、時差のあるリモートワークを行っています。今のところ主観的にも、客観的にもうまくいっている状況です。友人・知人にそのことを話すと、「実際のところどうなの?」「どうやってるの?」と聞かれることも多く、今回は日本にいるチームメンバーとの仕事のやり方、また私自身が心がけていることを紹介します。

背景

私が所属している インフラストラクチャー部データ基盤グループは、主にデータウェアハウス(DWH)の開発を行っています。具体的には、サービスのログやユーザーのマスターデータを継続的に取り込み、サービス開発のためのデータ分析や広告配信のためのシステム(DMP)に貢献しています。また、DWHユーザーのアカウントを発行したり、分析的なSQLの相談に対応したりしています。

クックパッドは、現在(2017年3月末時点)62カ国17の言語でサービスを提供しています。日本以外で比較的大きな拠点がブリストル(UK)、アリカンテ(スペイン)、ジャカルタ(インドネシア)に存在し、サービス開発およびコミュニティのサポートを行っています。日本のクックパッドと、海外のクックパッドのコードベースは異なりますが、データ分析のプラットフォームはDWHで統一されています。また、DWHの開発はデータ基盤グループが行っています。そこで、個人スキーマ作成や、新しいデータの取り込みなどの依頼は日本のみならず、世界各地のDWHユーザーから行われます。

前述の通り私は2017年の3月より、家族の都合でアメリカ西海岸に引越しましたが、引き続きデータ基盤グループの仕事を行っています。渡米の前に、部署を変えること(必然的に仕事内容も多少の変化があるはず)を検討しましたが、結局同じチームのままになりました。この辺の細かいところは個人で状況が異なると思うので省略しますが、私にとってはそれまでの仕事を引き続き行うという判断が正解だったと思っています。これまで一緒に仕事をしていたチームメンバーなので、仕事のやり方が理解できていると言うのは大きいです。

肝心のタイムゾーンは太平洋標準時(PST, UTC-8)ですが、現在は夏時間の期間なので太平洋夏時間(PDT, UTC-7)になります。日本が土曜日の朝になったとき、こちらが金曜日の夕方という感じです。ちなみにブリストルやアリカンテのあるヨーロッパが金曜日の夕方になる時、こちらは金曜日の朝になります。

クックパッド社内でリモートワークをやっている人は私だけではありません。個人や家庭の都合でリモートワークをやっている人、またある国では現地にコミュニティマネージャーしか社員がいなく、オフィスがまだ無いのでリモートワークと言う人もいます(アメリカにも現在オフィスはありません)。ただし、それぞれのやり方はほぼ共有されていないのが実情です。

具体的にやっていること

さて、具体的にチームと仕事をどうやっているのか紹介したいと思います。

1. 毎日朝会にビデオ参加する

日本でもチームで朝会を行っていたように、最近全社に導入されたZoomを使って、5分程度の朝会を行っています。日本の11時、こちらの19時なので私にとってはその日やったこと報告、相談する場になっています。

2. 重要なプロジェクトでは週一でビデオ会議をする

日本にいた頃から継続していた複数人で進めているプロジェクトについては、週に1回程度のビデオ会議を行いました。書くまでもありませんが、お互いの認識に相違が生まれないように、また発生してしまった相違を早い段階でなくすことができます。

3. 時差を活かす

時差があることは一見不便な点が多くありそうですが、視点を変えると便利だったりもします。例えば、こちらの昼間は日本の深夜なので、落ちたバッチジョブに迅速に対応することができています。また、前述の通り世界各地にある拠点から発生するデータ関連の依頼に対して、日本の営業時間よりも早く対応することができています(データ基盤に関するチームは現在日本にしかありません)。

4. 柔軟にやる

これはどちらかと言うと考え方になります。チームが必要と思えば、やり方をすぐに変えてみよう、うまくいくやり方を見つけようという考え方です。上で書いた朝会にビデオで参加する、というのは、渡米直後には行っていませんでした。チームのマネージャーとは週1程度でビデオで面談をやる予定を決めていましたが、朝会をやったほうがチームとして仕事がしやすくなると判断し、毎日行うようになりました。

やはりコミュニケーションが課題になるため、柔軟にやらないとどんどん考えていることがズレていってしまうという危機感が常にあります。

心がけていること

また、私個人が心がけていることを紹介します。

1. 仕事が見えるようにする

仕事が進んでいるのが分かるように意識しています。近年では Slack にGithub へのコメントが流れたりするので、ちゃんとやってれば勝手に見えるようになります。しかし、少し早めの段階でプルリクエストしたり、雑にイシューを立てておいたりすることで、より「仕事してますよ」感を出していくことが重要だと考えています。

さらに、これは日本にいたときからの習慣ですが、任意の日報を社内ブログに書いています。実際のところチームメンバーが見ているかどうかは不明ですが、自分のための記録としておすすめできます。

ちなみにチームとしてのタスク管理にはPivotal Trackerを使っています。PivotalTracker を見ることで誰がどのタスクを担当していて、いまどんな状態(着手、未着手、完了、…)なのかが分かります。また2週間に1度はイテレーションミーティングを開催し、そのイテレーション(2週間)に片付けたタスク、次のイテレーションで誰が何をやるのかをチーム全員で見直しています。もちろん私はビデオで参加しています。

2. 脱線を予防する

仕事中は完全に一人になることが多く、脱線を予防することが重要と考えています。業務上 2,30 分かかるようなSQLを実行することが多く、待っている間にふらっと SNS などを眺めだしてしまうと危険です。古典的ですが、/etc/hostsファイルに任意のサービスのドメインを localhost に向けると言うのは結構効き目があったりします(SNSは息抜きに携帯電話から見るようにする)。

また、私は家からフルリモートという働き方だと、経験上なかなかスイッチを切り替えるのに時間がかかると判断し、コワーキングスペースから仕事をするようにしています。

3. 仕事にやりにくさについて率直に聞く

私が仕事をやりやすいと思っていても、日本にいるチームメンバーがやりにくいと考えているならば何か対策をする必要があります。一時的に帰国した時や面談で「実際リモートワークやりにくくないですか?」と率直に聞くようにしています。

まとめ

時差のあるリモートワークから得られた知見や経験について、特に日本にいるチームメンバーとの仕事のやり方、また私自身が心がけていることを紹介しました。

クックパッドに限らず、リモートワーク等の比較的自由な働き方が増えつつあると感じていますが、時差があるとよりコミュニケーションに気を使う必要があると考えています。今までと同じように仕事ができることや、チームメンバーの協力に感謝しています。一方で、私は「リモートだからできない」と言うような、リモートを言い訳にしないように、むしろ時差を活かしたリモートだからこそできることを増やしていきたいと考えています。

他社で同じような状況でお仕事されている方の Tips など教えていただけると参考になります。

スキューのない世界を目指して

$
0
0

こんにちは。インフラストラクチャー部データ基盤グループの小玉です。

先日Amazon Redshift(以下、Redshift)で32TBのテーブルを全行スキャンするクエリを3本同時に走らせたまま帰宅し、クラスターを落としてしまいました。 普段はRedshiftのクエリをチューニングしたり、データ基盤周りの仕組みを慣れないRubyで書いたりしています。

突然ですが、スキュー(skew)という単語をご存じでしょうか。 「skew 意味」で検索すると「斜め」とか「傾斜」といった訳が出てきますが、コンピューティング界隈では「偏り」という訳語が定着していると思います。 さらに、分散並列DB界隈で単にスキューもしくは偏りと言った場合、それはしばしばデータの偏りを指します。

データが偏っているとは

データが偏っているとは、複数ノードで構成される分散並列DBにおいて、各ノードが保持するデータ量(行数)に差異があるということです。

例えば、Node1、Node2、Node3という3ノードで構成される分散並列DBがあり、そこに行数が30のテーブルが一つあるとします。 この場合に、ノード間で行が均等に分散している状態、つまり各ノードが行を10行ずつ保持している状態が、偏っていない状態です。 一方、Node1に3行、Node2に7行、Node3に20行というように、ノード間で保持する行数に差が生じている状態が、 偏っている状態です。

f:id:shimpeko:20170725183536p:plain

データの偏りとクエリパフォーマンス

分散並列DBでクエリのパフォーマンスチューニングを行う際に、データの偏り具合を確認することはとても重要です。 なぜなら、データが偏っている場合、データを多く保持しているノードに引っ張られるかたちで、クエリのパフォーマンスが低下してしまうからです。

なぜパフォーマンスが低下してしまうのか、上記と同様に3ノードのシステムに30行のテーブルがある場合を例に考えてみます。 なお、ここでは仮に1ノードで1行のスキャンに1秒かかることとします。

データが偏っていない場合

まず、データが偏っていない場合、テーブルの全行をスキャンするのにかかる時間は10秒です。 この場合、Node1からNode3はそれぞれ10行ずつ行を保持しているため、各ノードにおけるスキャン行数は10行で、所要時間も10秒になります。 そして、各ノードにおけるスキャンは並列で行われるため、テーブルの全行、30行のスキャンも同じく10秒で終わります。

データが偏っている場合

一方、データが偏っている場合、例えば、Node1に3行、Node2に7行、Node3に20行のデータが保持されている場合は、全行スキャンに20秒かかってしまいます。 この場合、各ノードのスキャン所要時間はそれぞれ保持している行数に応じて、3秒(Node1)、7秒(Node2)、20秒(Node3)になります。 スキャンは並列で行われますが、クエリの結果を返すためには全ノードでスキャンが終了している必要があります。 そのため、一番時間のかかるNode3に引っ張られるかたちで、全体の所要時間も20秒になってしまいます。

さらに極端な例ですが、Node3に30行全てが保持されており、他のノードには1行も無い場合、全行スキャンに30秒かかることになります。 この場合、分散並列DBといいつつも、実際に処理を行うのはNode3だけであり、並列化の恩恵を全く享受できていないことになります。

ここまでで、分散並列DBにおけるデータの偏りと、それがパフォーマンスに及ぼす影響についてなんとなく理解していただけたと思います。 ここからは、弊社で利用している分散並列DBであるRedshiftを例に、ノード間のデータの分散方式と、よくある偏りの原因について見ていきます。

Redshiftにおけるデータの分散方式

RedshiftはEvenKeyAllという3つのデータ分散方式をサポートしています。どの分散方式を利用するかは、テーブル作成時指定することが出来ます。また、Key方式を利用する場合に限り、分散方式の指定に加えて、分散キーとなるカラムを指定する必要があります。

それぞれの分散方式の概要は以下の通りです。 なお、Redshiftは"ノードスライス(以下、スライス)"という単位でデータを保持し、並列処理を行うため、以下では"ノード"に代えて"スライス"という単語を使います。

Even方式

  • ラウンドロビン形式で各スライスに行を出来るだけ均等に割り振る
  • 長所: データが偏りにくい
  • 短所: ジョイン時にデータの再分散が発生しやすい

Key方式

  • 指定されたカラム(分散キーカラム)の値に基づいて、各スライスに行を割り振る。カラムの値が同じ行は、同じスライスへ割り当てられる
  • 長所: 同じ分散キーカラムを持つテーブル同士のジョインでは、データの再分散が発生しない
  • 短所: データが偏る場合がある

Key方式は、他の方式より少し特殊なため補足します。以下はKey方式を指定したCREATE TABLE文の例です。

create table access_log (accsss_time timestamp, user_id int, user_agent varchar(512),....) diststyle key distkey(user_id)
;

distkey(user_id)という部分で、テーブルの分散キーとして、user_idカラムを指定しています。これにより、行がuser_idの値に基づいて各スライスに分散されるようになります。つまり、同じuser_idの値を持つ行は、同じスライスへ保存されるということです。なお、この分散は値そのものではなく、値のハッシュ値に基づいて行われるため、一般的にはハッシュ分散と呼ばれます。

少し話が逸れますが、Key方式には、テーブルのサイズ(ストレージ使用量)が小さくなるという効果もあります。これは、上で述べた通り、分散キーカラムの値が同じ行が、同じスライスに保存されるので、圧縮が効きやすくなるためだと考えています。社内では、分散方式を"Key"から"Even"に変更したところ、テーブルサイズが約2倍になってしまった例もあります。

All方式

  • 各スライスにテーブルの全行を保持する
  • 長所: データが偏らない。ジョイン時にデータの再分散が発生しない
  • 短所: 各スライスにテーブルの全行を保持するため、スライス数×行数のストレージ容量を消費する

このほかにも、Redshiftではサポートされていませんが、レンジ分散も一般的なデータ分散方式の一つです。興味の有る方は調べてみてください。

さて、上記の3つの分散方式を、データの偏りという観点に限って比べると、偏りが生じないEvenやAll方式が優れていると言えます。 しかし、All方式はストレージを多く消費してしまいますし、Even方式は ジョイン時にスライス間でデータの再分散が発生してしまうというデメリットがあります。

ジョイン時のデータ再分散とは

「ジョイン時にスライス間でデータの再分散が発生してしまう」とはどういうことか、以下のクエリを例に解説します。

accsss_logテーブルとusersテーブルをuser_idカラムでジョインしてuser_id毎のアクセス数を集計するクエリ

select
    l.user_id
    , count(*) as pv
from
    access_log l
    inner join users u
    on l.user_id = u.user_id
group by
    l.user_id
;

まず前提として、Redshiftにおいてジョインを実行する場合、結合される行同士、すなわちジョインキーの値が同じ行同士は、同じスライスにある必要があります。上記のクエリで、ジョインキーはuser_idです。よって、user_id=1access_logの行と、user_id=1usersの行は、同じスライスにある必要があります。

もし、ジョイン実行時にそれらの行が同じスライスに無い場合、ネットワークを通じてデータの移動が行われます。これが、データの再分散です。ネットワークを通じたデータの移動は時間がかかるため、できる限り避けたい処理です。

分散方式がEvenのテーブル同士をジョインする場合や、EvenとKeyのテーブルをジョインする場合は、この再分散が必ず発生します。再分散の方法には、両方のテーブルの行をジョインキーの値に基づいて移動する場合と、片方のテーブルの全行を各スライスに移動する場合の2パターンがあります。ただ、いずれにせよ再分散は避けられません。

一方、Key方式を採用し、かつ同じ分散キーカラムを持つテーブル同士のジョインでは、再分散が発生しません。例えば、access_logusersの両テーブルでKey方式を採用し、分散キーカラムとしてuser_idを指定している場合です。この場合、二つのテーブルで同じuser_idの値を持つ行は、同じスライスに保存されています。そのため、再分散無しでジョインが実行出来るのです。

ジョイン時に再分散の発生しないKey方式を上手く利用すると、Redhshift(をはじめとする分散並列DB)の急所であるジョインのパフォーマンスを向上させることが出来ます。そのため、特に頻繁にジョインするテーブルにおいては、データの偏りを考慮する手間を惜しまずに、積極的にこの方式を利用すべきです。

データが偏りやすい分散キーカラムの特徴

Key方式を採用しつつ、偏りを避けるためには、データが偏りにくい分散キーカラムを選択する必要があります。この際に確認するのは、カラムに含まれる値の統計的な特徴です。以下では、データが偏りやすい、避けるべき分散キーカラムの特徴をご紹介します。

ユニークな値の数が少ない

性別カラムでジョインをするからといって、性別カラムを分散キーにしてしまうと、性別の数のスライス数にしかデータが分散しません。クラスタ内に100スライスあっても、数スライスしか使われないことになります。分散キーに指定するカラムは、クラスタのスライス数に対して、十分な数のユニークな値を保持している必要があります。なお、テーブルの行数に対するユニークな値の数の度合いは、カーディナリティー(選択度)と呼ばれています。カーディナリティーが高いカラムは、偏りにくいカラムといえます。

ユニークな値の数を確認するクエリ(大きいほど良い)

select count(distinct column_name) from table;

ある値の数が他の値より多い(少ない)

ユニークな値の数が問題なさそうに見えても、ある値の数が他の値より多い(少ない)と、偏りが発生してしまいます。 例えば、一部の少数のユーザのアクセス数が他のユーザと比べて突出している場合、アクセスログには特定のユーザIDを持つ行の割合が多くなります。これを、ユーザIDカラムで分散すると、アクセス数の多いユーザIDが割り当てられたスライスの行数が、他のスライスより多くなってしまいます。

あるカラムについて特定の値の数の最大を確認するクエリ(1に近いほど良い)

select max(val_cnt) from (select column_name, count(*) as val_cnt from table group by 1);

nullがある

「ある値の数が他の値より多い(少ない)」の中で見逃しがちなのが、nullの数です。null以外の値の数が、ほどよく散らばっていても、例えば全体の10%がnullの場合、10%のデータが同じスライスに割り当てられることになるため、注意が必要です。

(番外)中間データの偏り

分散キーカラムに問題が無い場合でも、クエリによっては、処理中に作られる中間データが偏ってしまう場合があります。中間データに不要な偏りが生じていると思われる場合は、統計情報を更新したり、クエリの書き方を変えたりすることで、生成される実行プランが変わり、偏りを解消出来ることがあります。

Redshiftで、中間データの偏り具合を調べたい時は、STL_QUERY_METRICSや、SVL_QUERY_METRICS_SUMMARYといったシステムテーブルが使えます。これらのテーブルを使うと、スライス間のI/OやCPU使用量の偏り具合を調べることが出来ます。また、AWSのWEBコンソールでも、以下のようにスライス毎の平均所用時間と最大所用時間を確認することが可能です。

f:id:shimpeko:20170725183542p:plain

なお、同様に各テーブルの偏り具合は、SVV_TABLE_INFOというシステムテーブルのskey_rowsカラムで調べることが出来ます。skew_rowsは「最も多くの行を含むスライスの行数と、最も少ない行を含むスライスの行数の比率。」であり、1に近いほど偏りが少ないということになります。アクセスパターンにも依りますが、1.2くらいまでは許容範囲だと思います。

まとめ

この記事では、まず前半で分散並列DBににおけるデータの偏りと、そのパフォーマンスへの影響について解説しました。 また、後半ではRedshiftを例に、サポートされているデータ分散方式とそれぞれの特徴、そしてKey分散方式を採用した場合に偏りの原因となるデータの特徴をご紹介しました。

今回ご紹介したデータの偏りという観点は、並列分散DBだけでなく、Hadoopなどの他の分散システムを使う場合にも、トラブルシューティングやパフォーマンスチューニングの役に立ちます。初歩的な内容でしたが、なにかのお役に立てば幸いです。

最後になりましたが、クックパッドでは共にスキューの無い世界を目指せるデータ基盤エンジニアを募集しています(業務内容は、データ基盤の構築と運用です。詳しくは募集要項ページをごらんください)。

2nd Hackarade: Machine Learning Challenge

$
0
0

研究開発部の菊田(@yohei_kikuta)です。機械学習を活用した新規サービスの研究開発(主として画像分析系)に取り組んでいます。 最近読んだ論文で面白かったものを3つ挙げろと言われたら以下を挙げます。

以前本ブログで紹介した Hackarade: MRI Internal Challengeですが、その第二回として機械学習を題材にしたハッカソンが七月末に開催されました。 Hackarade ではエンジニアにとって長期的に有益となる技術を題材にしようという想いがあります。 今回はクックパッドの研究開発部が発足して一年経ち成長したというタイミングも重なることもあり、機械学習こそが時宜にかなったものであろうということでテーマが決まりました。

隆盛を極めている機械学習をほぼ全てのエンジニアが経験するという有意義な会となりましたので、この記事ではその様子についてお伝えします。

第二回 Hackarade の概要

第二回 Hackarade の概要を簡単に紹介します。 同様のイベントを開催しようと考えている方も少なくないと思いますので、参考になれば幸いです。

目標

目標は次のように設定しました。 全エンジニアが参加するイベントなので知識や経験のばらつきが多いことを考慮し、全員に持ち帰ってもらいたいものをベースとしつつ、機械学習に詳しい人にも有益となるよう発展的な内容も盛り込むよう努力しました。

  • 参加者全員
    • 機械学習を自分の言葉で定義できるようになる
    • 機械学習がどのような問題に適用できるのか理解する
    • 自分自身で機械学習のモデルを作る経験をする
    • 機械学習に関連する話題に興味を持つようになる
  • 機械学習に強い興味のある参加者
    • 独力で機械学習の勉強を続けていけるようになる
    • 最先端の機械学習トピックの一端を理解する
    • サービス改善や開発に機械学習を使って貢献できるようになる

時間割

時間割は次のように設定しました。 講義のコマでは私が講義をして、実習のコマではクックパッドのデータを使って機械学習を体験し、ハッカソンのコマでは各自が興味のあるトピックに取り組むという流れで進めました。 機械学習に馴染みが薄い人も多かったので、系統的な講義や実習を多めにして実感を掴んでもらえるよう留意しました。

  • 10:00-10:10 オープニング
  • 10:10-11:00 [講義] 機械学習とは何か
  • 11:10-12:00 [実習] レシピ分類(テキストデータ)
  • 13:00-14:30 [講義] Deep Learning(画像分析)
  • 14:40-16:10 [実習] レシピ分類(画像データ)
  • 16:20-19:00 [ハッカソン] 各自が興味あるトピックにチャレンジ
  • 19:00- パーティー & 成果発表

講義と実習の内容紹介

講義と実習で具体的に何をやったかという内容を簡単に説明します。資料は後日近い内容のものをインターンでも使用してそちらを公開しますので、興味のある方は今しばらくお待ち下さい。 クックパッドには非日本語話者も多いため、説明は日本語で実施しましたが、講義資料や実習資料などは全て英語で作成しました。

全エンジニアが参加する大きなイベントなので、資料作成はかなり気合を入れて一ヶ月前から着手しました。 資料の情報密度は相当高いものとなりましたが、参加者の能力を信じてやり切りました。 結果的には、消化不良の部分も当然あったものの、非常に満足度の高いイベントとすることができました。

講義

20170809133746

講義は「機械学習とは何か」と「Deep Learning(画像分析)」の二本立てでした。

機械学習とは何か

機械学習とは何かという定義から始まり、機械学習を俯瞰できるように以下の内容を説明しました。 機械学習界隈でよく出てくる言葉の意味や関係を説明し、頭の中を整理できるような構成になっています。 また、自己学習ができるように機械学習を学ぶための書籍やウェブ上の有用な情報などもまとめて共有しました。

  • 機械学習が使われている事例
  • 回帰や分類など、どのような問題に機械学習を適用できるのか
  • 機械学習の学習アルゴリズムの種類
  • 機械学習をサービスで活用するためのポイント
  • 機械学習の発展を追うのに有用な情報源

講義では機械学習の “expert” になるための最短経路の話もしました。 当然ここで述べている “expert” は冗談ですが、最低限を経験してみるという意味ではこれくらいの内容が必要かなと考えており、Hackarade でも可視化の部分以外は体験してもらうことにしました。

20170809133738

Deep Learning(画像分析)

Deep Learning に関しては、基礎的な説明の後に CNN にフォーカスして詳しく説明しました。 概念だけでなく具体的な演算としてどんなことをしているかにも踏み込み、Deep Learning の内部では実際に何が行われいるかが理解できるような構成になっています。 クックパッドでは画像分析(に限らずですが)の様々なタスクに取り組んでいるので、その適用事例に関しても共有しました。

  • Deep Learning の定義(なぜ昔のアイデアがうまくいくようになったのか)
  • 表現力、段階的で自動的な特徴量抽出、などの Deep Learning の特徴
  • CNN の動機とその基礎的な構成要素
  • CNN の進化
  • クックパッドにおける CNN の応用事例

一時間半の講義でしたが、内容が凝縮されたかなり濃い講義になりました。 パーセプトロンや誤差逆伝播法のような基礎から、以下のような CNN の進化に関しても少し言及しました。

20170809133733

この後に各モデルの鍵となるアイデアとその意味が説明するスライドが続きます。 時間的に一つ一つを丁寧に説明することは出来ませんでしたが、こうやって主要なモデルをいくつか並べてみるとどんなアイデアが鍵になって発展しているのかが分かり、興味深いですよね。

実習

実習は「レシピ分類(テキストデータ)」と「レシピ分類(画像データ)」の二本立てでした。 分析の環境を効率よく構築するために、準備した Dockerfile を用いて各自のノートPCで docker image をビルドしてもらい、立ち上げたコンテナで jupyter notebook を使って分析をするという形にしました。 そのため GPU は使用しませんでしたが、nvidia-docker を使えば同じスクリプトで GPU を使った分析もできるようになっています。

レシピ分類(テキストデータ)

レシピのタイトルや材料や手順のテキストデータを特徴ベクトル化して、該当のカテゴリ(e.g., ご飯もの、スイーツ)のレシピか否かを当てる二値分類モデルを作成しました。 テキストデータが対象だったため、MeCab を用いた形態素解析、不要な情報を除くための各種前処理、tf-idf を用いた特徴ベクトルの作成などが経験できるように準備をしました。 モデルは scikit-learn の Random Forest と Xgboost の Gradient Boosting Decision Tree を使いました。

そもそも jupyter notebook に不慣れだったり特徴ベクトルが具体的にどのような値になっているか不明瞭であったりで難しい点もありましたが、モデル構築の経験やモデルが正解する場合や間違える場合の具体的な例を見るなどして、実際のサービスで扱っている問題設定と同様のものを経験する機会となりました。

レシピ分類(画像データ)

まずは MNIST のデータを用いて、講義で学んだ MLP や CNN といったモデルを動かしてみました。 簡単にモデルを構築できるように、今回は TensorFlow backend の Keras を使いました。 一通り経験した後には、オリジナルのモデルを構築してその精度を確かめてもらいました。

次にレシピの画像からどのカテゴリ(e.g., パスタ、ラーメン)のレシピかを当てる多値分類モデルを作成しました。 実際の業務と近い分析をしてもらうために、ImageNet で事前学習をした InceptionV3 モデルを fine-tuning するというタスクに取り組んでもらいました。 学習に時間がかかるので画像は600枚程度とかなり少なめにしましたが、それでも10分単位で時間がかかり、最近のモデルは CPU では学習が困難だという実感が得られたのではないかと思います。

それ以外にも、公開されている学習済みのモデルを使えば物体検出などもお手軽に試せるということも軽く紹介し、手元で動かせるようになってもらいました。

CNN は学習は大変ですが、画像は見た目にも分かりやすいので楽しんで取り組んでもらえたように見受けられました。 モデルが間違えた画像を調べることでそもそも答えのラベルが合っているのかを疑問視するという気付きを得られたことも、実際のサービスに適用する場合には重要なので良い経験になったと思います。 また、学習の際の各種パラメタをどうやって決めればいいのかという疑問がたくさん出てきましたが、なかなか難しい話なので私も教えてもらいたいですね。

ハッカソンの紹介

ハッカソンでは各自が興味のあるトピックに対して issue を切り、そこに達成した成果や困難だと感じた点などを記述していく方式で進めました。 二時間半ほどの短い時間で新しいことに取り組むという難しい挑戦だったので、事前にトピックを考えてきてもらったり、取り組みやすそうな様々な問題を準備しておいて提示するといった工夫をしました。 結果として 40 個もの issue が切られ、多くの参加者が楽しんで主体的に取り組んでくれました。

ここではそのうちのいくつかを紹介したいと思います。

正規表現を Neural Network で解く

正規表現エンジンを NN で作れるかということを題材にして、16文字のランダムな文字列を生成し、/a+b+c/ にマッチするか否かを解いてみたという話です。 限られた時間でデータの準備から結果の検証までを行ったお手本のようなトピックでした。 対象が画像ではないですが CNN の kernel を 1*1 にして適用することで精度が上がることも実験していて、パラメタ数と精度の関係やモデルの中でどうやって認識しているのかなどの議論が発生する興味深いものとなりました。

モバイルで自分たちが学習したモデルを動かす

TensorFlow の example をベースとしてクックパッドアプリの料理きろくでも使っている料理/非料理判定モデルをモバイルで動かしてみようという話です。 実務的に興味がありながらもなかなか着手できていなかったこのトピックも、今回のハッカソンによって Android, iOS 共に料理/非料理判定モデルを動かすところまで実装ができ、みんなの興味を惹きました。 あまり機械学習を経験してこなかったモバイルエンジニアも、実習で一通り触ってからタスクに取り組むことで業務に利用していける感覚を掴めたという好例でした。

キッチンカメラの画像で人検出をして人数をカウントする

クックパッドのキッチンには様子を確認する用途のキッチンカメラがありますが、人検出を利用することでキッチンに人が何人いるかをカウントして slack に通知を出すという話です。 機械学習のモデルとしては研究開発部が作成している API があったため、そちらを使用しています。 多くの要素が絡む総合格闘技的なトピックでしたが、スピーディーに実装してみんなが確認できるものが出来上がったので、どうすれば人数カウントの精度が上がるかという議論も含めて盛り上がっていました。

究極のカレーレシピを錬成する

RNN を使ってカレーのレシピを生成してみようという話です。 講義では RNN には触れませんでしたが、torch-rnn を使ってカレーのレシピを生成するモデルを学習し、実際にレシピを生成して出力するというところまでやり切っていました。 生成系は難しいためみんなの笑いを誘うような出力(例えば材料に玉ねぎが三回も出てくる)もありましたが、カレーという特定のカテゴリに絞ったことで材料や手順がある程度理解ができるものであったのは興味深いものでした。

業務と結びつきの深いトピックを考えてみる

プロの作ったレシピとそれ以外を判別する、広告CTRを予測するモデルを構築する、料理教室のレッスンの説明文から特徴ベクトルを作って比較をする、サービス上の重複画像を排除する、アクセスログデータからの攻撃検出、など実際のサービスを意識したにしたトピックも数多く挙げられました。 問題設計やデータセットの構築に時間を要するためすぐに結果を出すというのは難しいものがほとんどでしたが、実際のサービスに活かせそうな部分を考えてみる良いきっかけとなったかと思います。

No Free Lunch Theorem の証明を理解する

話としては何度も聞いたことがあるがどのように証明するかは知らない人が多い No Free Lunch Theorem の証明を理解しようという話です。 硬派なトピックであり、個人的にはこれを選んだ人がいてテンションが上がりました。 評価関数の関数空間を探索アルゴリズムによって分割するという考えは、アルゴリズムの優劣を理論的に論じるのにも有用ですね!

その他

全部は紹介できませんが、その他にも、データセット構築する、新しいライブラリを試してみる、マインスイーパーを解かせる、アニメの速報テロップやL字を判定する、など面白いトピックが盛り沢山でした。 また、公開されているレポジトリを触っていたらバグを発見したため修正 PR を送って OSS に貢献をする人もいました。

これらの成果は美味しい食事を楽しみながら発表をして、歓声や質問が飛び交うとても楽しい時間になりました。 今回の食事のメインは TensorFlow ロゴのライスケーキで、実現が難しいオーダーにも関わらず料理人の方に素敵に仕上げてもらいました。

20170809133757

反省点

概ね上手くいきましたが、以下の点が反省点として挙げられます。

  • 開始時にみんなで一斉にレポジトリを clone して帯域を圧迫してしまった。
  • docker や jupyter notebook に馴染みのない人向けの基本的な説明があるとよかった。
  • 長時間椅子に座って講義を聞いたり実習をしたりするのは大変。
  • みんな楽しみすぎて、ブログに載せるための良い写真があまり撮れていなかった。

まとめ

社内のエンジニアには機械学習に馴染みのない人も少なくありませんでしたが、最高の講義だった、機械学習の系統的な理解が得られて良かった、実際に触ってみることでサービスへの活用方法などがイメージできた、など好評を博しました。 企画側としては限られた時間でのハッカソンが盛り上がるかを特に懸念していましたが、優秀なエンジニアが多いため面白い取り組みをして結果を出すところまで到達する人も多く、実に楽しく有意義なものとなりました。 機械学習は広く深い分野なので一日のイベントでできることには限りがありますが、これを契機に個々人が自発的に機械学習に取り組むようになってくれれば嬉しい限りです。

いかがでしたでしょうか。 クックパッドでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。
クックパッド株式会社 研究開発部 採用情報

Genymotion On Demandを使うようになってAndroidのCIがさらに1分短縮した話

$
0
0

こんにちは。技術部モバイル基盤グループの門田(@_litmon_)です。

モバイル基盤グループでは、エンジニアの方々が快適に開発できる環境を整えるため、日々アプリのビルド時間やCIの実行時間などを短くする方法を模索しています。

今回は、Genymotion On Demandを使ってみた結果、CI上でのAndroidアプリのinstrumentation testの実行時間が1分短くなった話をしようと思います。

前回のあらすじ

今回の記事は、OpenSTFでAndroidのCIを2倍早くするの続編のような記事で、AndroidのCI環境を整えている話です。 まだ読んでいない方はぜひ上の記事から読むことをオススメします。

前回は、Jenkins上でAndroidのエミュレータを起動して使用する方法から、OpenSTFというリモートで実機端末を操作することが出来るオープンソースツールを使用する方法に切り替えた結果、エミュレータの起動時間などが削られ実行時間が約9分近く削ることが出来ました。

しかし、OpenSTFをしばらく運用していくといくつかの問題点が見つかりました。

OpenSTFをCIで使う際の問題点

OpenSTFはエミュレータの起動を待つこと無くテストを行うことが出来るため、とても優秀なツールだったのですが、実際に運用を進めていくと以下のような問題点が見つかりました。

  1. OpenSTFサーバー自体が不安定になることが時々あり、サーバーのプロセスが終了してしまうことが度々あった
  2. 実機の状態が不安定で、時々接続に失敗することがあったりしてメンテナンスコストが高い
  3. CI専用の端末を用意する必要があり、slaveごとに端末が必要になるのでスケールしづらい

1.のOpenSTFサーバーが不安定な点に関しては、正しくメンテナンスを行うことで回避することが出来たかもしれませんが、しばらくはプロセスが終了しており失敗している場合は手作業で立ち上げ直すという運用を取っていました。だいたい、2週間に一度は調子が悪くなり、そのたびに再起動を行っている、という状況でした。

また、2.に関しては、実機の状態が不安定で"なぜか"OpenSTFに認識されないときがあり、そのたびに検証端末が置かれた棚を開きにいっており、毎回原因を追求することまではせず運用でカバーしていました。そのため、端末を管理するコストが大きくなっていました。

現状の問題点

ということで、現状の問題点を整理すると、

  • 自社でOpenSTFサーバーを立てると相応のメンテナンスコストがかかる
  • 物理端末に依存することで、端末の管理コストがかかりスケールしづらい

という2点が挙げられます。

これらの問題点を解決するため、様々なサービスを検討した結果、Genymotion On Demandにたどり着きました。

Genymotionとは

Genymotionは、Genymobile Inc.が出している非公式のAndroidエミュレータです。 Android Studioに標準で搭載されている公式のエミュレータよりも多機能なエミュレータで、x86仮想化を使った公式エミュレータが利用できなかった頃はお世話になったAndroidエンジニアのみなさんも多いのではないでしょうか?

Genymotion On Demandとは

Genymotion On Demandは、そんなGenymotionをAmazon EC2(EC2)インスタンス上で起動し、扱うことが出来るものです。 EC2インスタンス上に起動したエミュレータとADB接続でき、ADBコマンドから端末を操作することが出来るすぐれものです。

Genymotion On Demandを導入することで、物理端末の制約から逃れ、快適なCI環境を手に入れることが出来ると考え、クックパッドでは5月頃から導入を始めています。

Genymotion On Demandの使い方

ここで、簡単にGenymotion On Demandの使い方を紹介します。

といっても、手順は公式のチュートリアルにとてもよくまとまっているので、そのとおり進めるだけで簡単に使うことが出来ます。

Genymotion on Demand – Tutorial –

おおまかな流れとしては、以下のような形になります。とても簡単ですね。

  • AWS ConsoleからGenymotion On Demandのインスタンスを購入する
  • sshでエミュレータに接続し、ADBを有効に設定する
    • この設定はインスタンスごとに一度行えばよく、再起動時には不要
  • sshでport:5555に対して接続することで、エミュレータをADBで認識できるようにする

Genymotion On Demandで使用できるAndroid OSバージョンは、現在5.1, 6.0, 7.0の3種類になります。クックパッドでは、導入時の最新である6.0を使用しています(5月時点では5.1, 6.0の2バージョンだった)。

また、Genymotion On Demandでは、EC2のインスタンスタイプも指定できますが、t2.smallではテスト実行時間が若干遅くなるものの、それぞれ大きな差はありませんでした。 クックパッドでは現在m4.largeを使用しています。

CI上でのGenymotion On Demandの利用

現在、クックパッドではJenkinsのSlaveとしてEC2インスタンスを用いています。(参照: OpenSTFでAndroidのCIを2倍早くする)

そして、Slave 用のインスタンスの起動時に localhost:5555 を Genymotion On Demand のインスタンスへと転送する簡易プロキシを起動しています。 こうすることで、Slaveを立ち上げた時点ですでにGenymotionのエミュレータがADBに認識された状態になるため、Job側でなにも設定することなくエミュレータを使うことが出来ます。

そのため、既存のJobの設定を編集する際も、OpenSTFのプラグインの設定を無効にするだけで簡単に移行が完了します。

f:id:litmon:20170822115529p:plain

導入しただけでinstrumentation testの実行時間が1分短く!

導入してしばらく様子を見ていたのですが、明らかにJobの実行時間が短くなっているのを体感しました。 以下の画像は、縦軸が時間で横軸が実行したジョブの番号を示しています。そして、 #1899以降がGenymotion On Demandを導入した後になっています(※ときどき失敗しているのは、不安定なテストがあるせいです。気にしないでください)。

f:id:litmon:20170822115903p:plain

このグラフから分かる通り、7分近くあったジョブの実行時間が6分弱に収まっていることが分かります。約1分弱の短縮に成功しました!🎉

詳しく状況を見てみると、以下のようになっていました。

  • 端末との接続を確立するまでで 20秒 短縮
  • ./gradlew :cookpad:connectedAndroidTestStagingDebugが 45秒 短縮
  • ./gradlew :cookpad:uninstallAllが 10秒 短縮

なんと、instrumentation testの実行時間が45秒も短くなっているではありませんか! それだけではなく、端末との接続時間や、アプリのアンインストール時間が短縮されていることが分かります。

仮説ですが、OpenSTFは自社のローカルネットワーク上で起動しており、Genymotion On DemandはJenkins Slaveと同じEC2インスタンスのため、ネットワークの接続(apkの転送時間など)に影響があったのではないかと考えています。

安定感がスゴイ!

そんなGenymotion On Demandですが、やはり気になるのは安定性です。 現在、導入して3ヶ月ほど経ちました。弊社のJenkins Slaveは2台あり、内1台はフルタイムで活動しています。そして、Genymotion On DemandのインスタンスはSlaveの起動時に同時に起動し接続しているため、内1台は常に活動したままです。

にも関わらず、5月31日に起動して以来今までGenymotionのエミュレータの調子がおかしくなったことはありません。すごい安定性ですね! これだけ安定して使えるのであれば、ということで、6月の半ば辺りから徐々に既存のJobで使われているOpenSTFをGenymotion On Demandに置き換えていって、今ではほとんどのJobがGenymotion On Demandを使うようになっています。

Genymotion On Demandの制限・注意点

ここまで、Genymotion On Demandの良いところをたくさん挙げてきましたが、いくつか制限もあります。

  • Google Play Serviceが利用できない
  • instrumentation testでタイムゾーンに依存するテストがある場合、失敗する可能性がある
    • 一度これでハマってテストが落ち続けていたので、注意する…
    • エミュレータのタイムゾーンを変更することは可能です

現在、クックパッドの日本のアプリでは上記2つともそこまで大きな問題ではないのですが、プロジェクトによっては導入する際の障害になりうるので、参考になればと思います。

まとめ

OpenSTFを使った実機でのinstrumentation testをGenymotion On Demandを使ったGenymotionエミュレータでのテストに置き換えた結果、CIの実行時間が1分近く短縮されました。また、安定性やスケーラビリティなど、実機で管理していた際の問題点も解消することが出来ました。

クックパッドでは、アプリの開発ももちろんのこと、こういったアプリのビルドやCI周りに対しても様々な取り組みを積極的に行っています。 アプリの基盤の仕組みを整えたり、新たに作り出していける、いきたいAndroidエンジニアのみなさんを募集しています。 もし興味があったらぜひぜひ遊びに来てください。お待ちしています!

クックパッド株式会社 採用情報

施策の質と職務能力を高めたい!ディレクター会の取り組み

$
0
0

こんにちは。サービス開発部 ディレクターの五味です。 Android版クックパッドアプリのリリースマネージャーと、アプリ利用者に関わるいくつかのプロジェクトを担当しています。今回は私たちの部で実施している、ディレクターの定例会について紹介します。

f:id:natsuki53:20170829223631p:plain

サービス開発部

クックパッドの開発体制は、2年前に私が ディレクター知見共有会についてのエントリー*1を書いた頃から少し変遷を経て、2017年からはサービス開発部が、レシピ検索・投稿などの基幹機能と、サービス全体のユーザー体験を一手に管轄するようになっています。

部のメンバーは現在40人ほどおり、部の注力指標からブレイクダウンしたKPIをベースに9つのプロジェクトチームに分かれています。チームの編成や人数は様々で、状況に合わせて入れ替わりもOK、KPI達成に向かっていれば、各チーム主体的に動くことが推奨される柔軟な組織を試みています。

プロジェクトチームで働く中で

このような体制の利点は、自分のチームのミッションに対して裁量を持って施策を考え取り組めることです。やりがいがある反面、以下のような悩みを感じるようになりました。

  • 部の目標に対するチーム横断での進捗度や、自分のチームの遅れが見えづらい
    • チームで決めた施策を進めるだけで、施策数や速度は本当に十分なの?
  • チームが自律的に動く反面、チーム間の情報連携や相互補完が難しい
    • 他のチームは目標をどう考えてどんな施策をしているのか、知りたいけど聞きづらい…
  • ディレクターとしての自分の成長がわからない
    • この職種に必要なスキルは何なのか、自分のパフォーマンスは足りているんだろうか?

ディレクター会の発足

これらの悩みを持ち掛けた方々から助言を得て、部のディレクターがチームを越えて集うディレクター会を始めることにしました。部内のディレクター職の他、ディレクター不在のチームからは同等の役割を担っている他職種の方にも声をかけます。

初回の開催で、会の目的とアジェンダを以下のように決めました。

  • 会の目的
    • サービス開発部でディレクターの役割を持つ人の情報・知見をチーム横断で共有する
  • 成功のイメージ(会の参加者に対して)
    • 担当施策について目標に対する成果を把握し、責任を持って報告できるようになる
    • 部内の施策の内容・効果を横断的に把握し、自分の提案に活かせる
    • 定期的に悩み相談や意見交換をする機会を得て、施策の精度が部署全体で上がる
    • ディレクターとしてのスキルアップに積極的に取り組めるようになる
  • アジェンダ(60分)
    • ① 実施した施策の共有 30分
    • ② 施策やチーム運営の相談 20分
    • ③ その他アナウンス、連絡事項 10分

意識したことは「先週これをやりました、今週これをやります」という業務進捗報告に時間を割かないことです。他のチームの施策の進捗を聞いても必要な情報や問題を見出すのは難しいことと、ディレクターなのでチームの進捗管理は各自できている前提にしたかったためです。

会議の時間は1時間、開催頻度は週1回と仮決めしてスタートしましたが、これは毎週ちょっとだけ時間が足りないくらいアジェンダがある状態が続けられているので、そのまま継続しています。

「実施施策の共有」について

ディレクター会のメインコンテンツにしている施策の共有について少し紹介します。

この会では、部で実施する施策をできるだけすべて議題にあげたいので、施策共有用に手間のかかる資料は作らないことにし、GitHub Issue に報告事項の箇条書きだけ準備する方式にしました。

ただし、箇条書きの項目はテンプレートで決まっており、報告には、仮説・試算・実数・考察・次のアクションの5項目が必要です。PDCAを回せるような設計がきちんとできていない施策はこの5つに埋められない項目が出てくるため、施策を考える人の自浄装置のような働きをしています。

例えばこのディレクター会をテンプレートに沿って報告しようとすると、下記のようになります。

# 施策名:サービス開発部のディレクター週例
- 仮説
  - ディレクターが定期的に施策情報を共有し意見交換できる場ができると、部全体の施策の精度とスピードが上がる
- 試算
  - 部の施策数が週5本(各チーム2週に1本)になる
  - 部の目標達成の進捗度が10%上がる
- 実数
  - 施策報告数:2〜3本/週
  - 部の目標達成進捗度:変化なし
- 考察
  - 定性意見より、会があることで施策/プロジェクトの成功への責任者意識は強まった
  - 他チームの成功・失敗事例やお互いの助言を担当案件に活かせる機会はできた
  - ただ、実際の施策のスピードやKPIの進捗に変化が起こるほどの成果には至っていない
- 次のアクション
  - アジェンダの見直し:参加メンバーに課題提起し、次の会で改善策を話す時間を取る

また直接この会に起因することではありませんが、最近サービス開発部では、施策結果のレポートをPull Requestで作ってチームでレビューする手法が採られ始めています。何かをリリースして完了ではなく、検証内容を振り返り次にどう進めるのかの判断にチームで取り組めることと、メンバーがレビューに入ることで、施策に対するチームの理解が揃う利点があります。

ディレクター会ではこれらの箇条書きやPull Requestを見ながら、施策共有に使える30分を週ごとの施策数で割って時間配分を決め、どんどん報告していってもらいます。報告を聞いている側の人は、気になる点や使える知見があれば自由に発言してもらい、特筆すべき意見は後で議事録に残して使ってもらいます。

ディレクター会の効果と課題

現在、この会を始めて2ヶ月ほどが経ったところです。前段の報告テンプレートの事例で少し前述していますが、現時点で良かったと感じている点は以下です。

  • 他チームの成功・失敗事例や、他のメンバーの助言など、自分の施策に活かせる第三者からの情報を得やすくなった
  • 週ごとに報告できる施策の数から、各チームの進捗スピードが推し測れるようになった
  • ディレクター:プロジェクトを成功に進める責任者という意識を合わせ、施策に取り組めるようになった

反面、まだ成果は定性的なものに止まっており、施策のスピードや部の目標達成の進捗に効果が表れるには至っていません。またディレクターのスキルアップのような長期の取り組みには手を出せていない状況です。

ちょうど先週これらを課題として改善策を相談し、次から以下の2つを変更してみる予定です。

  • 施策報告を、終了した施策だけでなく、これから実施する施策も対象にする
    • 結果だけだとチームが何を考えてその施策をしたのかわからない、終了施策にツッコミをもらっても「次頑張ります」としか言えないという意見から。施策の改善の余地に事前に気づいて検証の精度を上げられるように。
  • 進行中施策に直接紐づかない大きめのトピックも持ち込むようにする
    • 仮説定義や分析手法のノウハウなど、具体的な解がすぐ出せないから話題にしづらいが、各自悩みの深い相談を持ち掛けられるように。

このような取り組みを継続させるコツ

前回ディレクター知見共有会のエントリーを読んだ方から「うちはこういう会を始めても3回で自然消滅します…」という感想をいただいたので、大変僭越ですが、複数のメンバーを巻き込んで定常的な取り組みを行う際に意識していることを紹介させていただきます。

1. 参加者のコストを必要最小限にする

時間と手間を取りすぎないことを念頭に置いています。 今回であれば、会議が1時間を過ぎないよう時間配分することと、準備はGitHubのIssueにテンプレートに沿った箇条書きで済むようにしています。

2. 参加者がすぐ活かせる粒度の情報を入れる

「ディレクターに必要なスキルとは?」といった少し高い次元の議論だけでなく、明日から自分の業務に使える実用的な情報を得られる議題を含めることで、参加の利点を感じやすくします。 そのためディレクター会では実施施策の話題に時間を厚めに充てています。

3. “他人事” になっているメンバーを放置しない

取り組みが軌道に乗ってから1番気を配る点です。会議中ぼんやり聞いているだけの人が出てくるようになったら要注意です。敢えてその人に指名で意見を求めてみたりして反応を見ながら、会議の内容自体に原因がないか見直しを考えます。

最後に

ディレクターはエンジニアやデザイナーに比べて職務定義が難しいということをよく聞きます。また1つのプロジェクトに複数名でアサインされることは少なく、1人で複数のプロジェクトを掛け持ちすることは多いため、各自が抱える情報や知見を共有するには意識的な働きかけが必要だと感じます。

ただ、どんなプロジェクトでどのような働きをしているにせよ、ゴールに向かってチームを進めていく大事な役割を担っていることは確実だと考えます。

開発者がすごい!と言われるクックパッドですが、「ディレクターもすごいんです!」と言えるよう、今後も頑張っていきたいと思います。

そして、そんなチームに一緒に加わって頑張ってくださるメンバーを募集しておりますので、よろしくお願いいたします! https://info.cookpad.com/careers

*1:注: 「ディレクター知見共有会」はそのあと対象を広げ、今は参加者の職種は問わず様々な部署の体制や取り組みについて聞ける場として継続されています。


クックパッド サマーインターンシップ2017 「17day 技術インターンシップ」を開催しました

$
0
0

いつもお世話になっております。エンジニア統括マネージャーの高井です。

クックパッドでは毎年恒例となりつつある、クックパッドのサマーインターンシップのうち「17day 技術インターンシップ」を開催しました。インターンに来てくれた学生のみなさんは本当に優秀で、毎日真剣に取り組んでくれました。本当に感謝しています!

インターンは、前半の「サービス開発講義・課題」パートと後半の「サービス開発実践」から構成されています。前半パートでの講義について資料を公開いたしますので、みなさまぜひご覧ください。

f:id:takai_naoto:20170831080358j:plain


【1日目】サービス開発

初日は、クックパッドで実践されているサービス開発の手法について学ぶワークショップです。グループでのユーザーインタビューを通じてサービスの設計をしました。

【2日目】Rails・TDD・Git

昨年に引き続き、講義初日はGit、TDD、Railsを1日で一巡りするという、忙しい構成でした。

【3日目】モバイルアプリケーション

3日目は、 iOS と Android のふたつに分かれて、 Google 社の Firebase をつかった Cookpatodon というマイクロブログ風のアプリケーションを題材に学習をしました。アプリケーションの基本部分を実装したあとは各自で自由に機能を実装してもらい、最後に成果発表会という形で発表してもらいました。皆ユニークな機能を実装して大変盛り上がりました。

【4日目】インフラストラクチャー

Web アプリケーションのインフラについてAWSをつかいながら、Railsアプリケーション動作させるところから、パフォーマンスチューニング、スケールアウト、キャッシュなどのトピックについて触れています。

(資料は公開準備中です)

【5日目】SQL

Redshiftで構築されたデータウェアハウスをつかって、分析用のSQLを書いていました。クックパッドの実際のデータをつかったので、参加者たちは億単位のレコードがあるテーブルと格闘していました。

(内部データを利用した講習のため資料の公開はありません。どのようなものだったかを知りたい方はこちらまで。こちらの書籍でも概要を知ることができます)

【6日目】機械学習

機械学習は講義と実習のセットになっており、講義では「機械学習とは何か」という概観とディープラーニング(特にCNN)を学んだうえで、実習ではクックパッドのデータを使ったレシピ分類に取り組みました。最後は各々が興味をトピックを取り組んでもらってその成果を提出しました。

【7日目】Ruby

最終日のRubyの講義では、RubyでRubyのコンパイラを実装したり、その最適化を行ないました。


番外編

前半の講義が終わった懇親会では、先輩社員による就職活動の体験談LTなどが行なわれ、参加者のみなさんが楽しんでいました。

f:id:takai_naoto:20170831080459j:plain

その後の二次会も盛り上ったようです。

クックパッドと分散トレーシング

$
0
0

こんにちは、技術部の Taiki (@taiki45) です。

近年の Web サービスの開発ではマイクロサービスに代表されるように分散アーキテクチャが採用されるようになってきました。大規模でも素早いプロダクト開発をするために、クックパッドでもマイクロサービスを採用し分散アーキテクチャへの移行を進めています*1。今回は、そのような分散アーキテクチャを利用したシステム構築において必須のコンポーネントになりつつある分散トレーシングについて、クックパッドでの事例を紹介したいと思います。

分散トレーシングとは

マイクロサービスのような分散アーキテクチャでは、個々のサービス同士の通信が複雑になるため、モノリシックアーキテクチャと比較して、システム全体としての振る舞いを把握することが難しくなります。これはプロダクト開発においては、障害発生時の原因究明が難しくなったり、あるいはシステム全体でのパフォーマンスの分析が難しくなるといった問題として顕在化します。 分散トレーシングはこのような問題に対処するためのツールです。開発者が、特定のクライアントリクエストを処理するのに関わったサービスを探したり、レイテンシに関するパフォーマンスをデバッグする時に利用されます。

分散トレーシングの実現のアプローチには大きくわけて2種類あり、一つは Black-box schemes *2、もう一つが Annotation-based schemes と呼ばれています。 前者の Black-box schemes はシステム内の各サービスに手を入れる必要がないことが利点ですが、それと引き換えに特定のリクエストに対する分析はできません。後者の Annotation-based schemes は各サービスに分散トレーシング用のメタデータを下流サービス*3へと伝播させる実装を加えることが必要になるという欠点がありますが、特定のリクエストを分析することができます。Annotation-based schemes は Google の Dapper*4や Twitter の Zipkin*5等に採用されており、Web サービス業界では主流なようです。

Annotation-based schemes に基づく実装

Annotation-based schemes に基づいた分散トレーシングシステムの実装の仕組みを大まかに説明すると、ユーザーからリクエストをうける最初のポイントで “トレースID” という分散トレーシングシステム内で一意となる文字列を発行し、トレースIDや “アノテーション” と呼ばれる処理結果等の追加情報を含んだログをストレージに保存し、さらに下流に存在するサービスへリクエストを発行する際にトレースIDとアノテーションを伝播していきます。このような「トレースIDに紐付く一連のログのまとまり」を “トレース” と呼びます。このトレースをトレースIDをキーにしてストレージから検索することにより、特定のリクエストに関わったサービスを特定したり、また複数のトレース情報を集計することで分散システム内のコミュニケーションパターンを分析することができます。また、各サービスがリクエストの処理を開始/終了した時刻もトレースログに一緒に保存すれば、レイテンシの算出もできます。Google の Dapper や Twitter の Zipkin といった実装では、トレースログのタイムスタンプから各ログの親子関係を算出するのではなく、 各トレース内で一意となる文字列である “スパンID” をログの識別子として利用し、ログの親子関係をスパンIDで表現するようになっています。

ほとんどの分散トレーシングシステムは次のようなコンポーネントに分解できます:

f:id:aladhi:20170905163926p:plain

  • Instrumented library: 各サービスのアプリケーションに組み込み、トレースIDの採番や伝播やトレースログの送信を担うライブラリ
  • Log Collector: 各サービスインスタンスから送信されるトレースログの集約を担う
  • Storage and Query:トレースデータの保存と検索を担う
  • UI: 人間がトレースデータを検索・分析する際に利用する

Instrumented library は各言語向けに整備する必要があるので、各分散トレーシングシステム普及のボトルネックになっています。この問題を緩和すべく OpenTracing*6のように Instrumented library API の標準化を進めているプロジェクトもあります。

クックパッドでの導入

分散トレーシングシステムの選定

クックパッドで分散トレーシングを導入するに当たり、いくつかの点を考慮して AWS が提供するマネージドサービスである AWS X-Ray*7を採用しました。クックパッド内では一般的なユースケースを想定しているので、既存の分散トレーシング実装を利用することを決めました。分散トレーシングシステム実装として採用例も多く開発も活発な Zipkin に焦点を当てましたが、大規模な環境で Zipkin を利用するには Cassandra/HBase/Manhattan いずれかの運用が必要であり、データストアを自分たちで運用するよりは、解決したい問題にフォーカスできるマネージドサービスの利用に比較優位がありました。クックパッドでは AWS を積極的に活用するインフラストラクチャを構築していることもあり AWS X-Ray の検証を始めました。

検証開始時点では AWS X-Ray が提供する Instrumented library は Java/Node.js/Python のみのサポート*8で、クックパッドではほとんどのサービスは Ruby を用いて実装されているため、そのままでは AWS X-Ray は利用できませんでした。サードパーティ製のものも特に存在しなかったのですが、Instrumented library の実装方法については目処が立っていたこと、及び Instumentation library を自分たちで管理できることで他の分散トレーシングシステムへ低いコストで移行できる余地を残せる利点があったので、自作することにしました。自作した Instrumented library である aws-xray gem は OSS として公開しています*9

現状の構成

AWS X-Ray を利用したクックパッドでの分散トレーシングは以下のような構成で実現されています:

f:id:aladhi:20170905163959p:plain

  • Instrumented library: aws-xray gem を利用
  • Log Collector: AWS X-Ray の提供する X-Ray daemon というソフトウェア*10を利用
    • ECS を利用しているアプリケーションではいわゆる Sidecar 構成を取っています
    • EC2 インスタンス上で動作しているアプリケーションについては EC2 インスタンスの上に X-Ray daemon プロセスを動作させています
    • Instrumented library から UDP で X-Ray daemon にトレースログを送信し、X-Ray daemon がバッファリングと AWS の管理する API へのトレースログの送信を担います
  • Storage and Query: Storage はこちら側からは見えません。Query として AWS X-Ray の提供する API*11を利用します
  • UI: AWS コンソールに組み込まれています*12

aws-xray gem の実装において、トレードオフを考慮しつつモンキーパッチを活用することにより、ほとんどの Rails アプリケーションでは gem の導入と X-Ray daemon への接続情報を設定するのみで、トレースログの収集を開始できるようになっています。

今後の展望

現状はデータを AWS X-Ray に集めるところまでで、まだ本格的なトレースデータの活用には至っていません。データの収集については、社内の主要なサービスをカバーしており、サービスマップのノード数は現在約70ほどです。

f:id:aladhi:20170905164033p:plain

エラートラッカーなどに記録されているトレースIDから該当リクエストに関する分析ができるようになっています:

f:id:aladhi:20170905164117p:plain

サンプリング方式については Head-based coherent sampling*13を採用しており、ユーザーからリクエストを受ける最初のサービスで sampled/not sampled を決めて下流サービスに伝播させています。サンプリングレートについては、特に rps の高いサービスのみ1%設定、他のサービスについては100%設定で運用しています。サンプリングについては課題があり、ミッションクリティカルなサービス*14の処理を含むトレースはトラブルシューティング用途に全件保存しておきたいですが、流量の高いサービスが上流にいるケースではサンプルされるトレースの割合が少なく、トラブルシューティングを行うユースケースで支障があります。その対策として、パス毎によるサンプリング設定等を実装・導入する予定です*15

クックパッドでは Barbeque*16という非同期ジョブシステムを利用して非同期ジョブを実行しています。多くのジョブは Web アプリケーションのリクエストによりトリガーされているので、リクエストとジョブ実行との紐付けを記録できるようにする予定です。

また、システム全体のレイテンシ変化を検知できるように、AWS X-Ray の API を利用して監視システムを構築する予定です。監視システムについては自前で実装する以外にも AWS X-Ray の機能追加にも期待しています。

おわりに

AWS X-Ray を利用した分散トレーシングの実現について、クックパッドでの事例を紹介しました。クックパッドでは比較的大規模な Web サービス開発が行われており、分散アーキテクチャ周辺に存在する興味深い問題が多々あります。このような課題解決を一緒に取り組む仲間を積極的に募集しています

*1:http://techlife.cookpad.com/entry/2016/03/16/100043

*2:M. K. Aguilera, J. C. Mogul, J. L. Wiener, P. Reynolds, and A. Muthitacharoen. Performance Debugging for Dis- tributed Systems of Black Boxes. In Proceedings of the 19th ACM Symposium on Operating Systems Principles, December 2003.

*3:ここではユーザーからリクエストを受けるフロント側を “上流"、その反対側を "下流” と呼びます

*4:https://research.google.com/pubs/pub36356.html

*5:http://zipkin.io/

*6:http://opentracing.io/

*7:https://aws.amazon.com/xray/

*8:今では Go 言語向けのライブラリもサポートされました https://aws.amazon.com/jp/about-aws/whats-new/2017/08/aws-x-ray-sdk-for-go-beta/

*9:https://github.com/taiki45/aws-xray

*10:http://docs.aws.amazon.com/xray/latest/devguide/xray-daemon.html

*11:http://docs.aws.amazon.com/xray/latest/api/Welcome.html

*12:http://docs.aws.amazon.com/xray/latest/devguide/xray-console.html

*13:http://www.pdl.cmu.edu/PDL-FTP/SelfStar/CMU-PDL-14-102_abs.shtml

*14:例えば課金系サービス

*15:http://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-configuration.html#xray-sdk-java-configuration-sampling

*16:https://github.com/cookpad/barbeque

データ分析からUI改善

$
0
0

こんにちは。サービス開発部デザイナーの平塚です。

クックパッドでは一部のデザイナーは日々の業務でSQLを書いて数値を見たり、リリースした施策の分析を行っています。 このエントリーでは機能をリリースしてデータ分析し、そこからUI改善を行った事例について紹介したいと思います。

なぜデザイナーがデータ分析?

サービスやプロダクトを改善するには現状について定性的・定量的の両方を理解しておく必要があります。
そのため、自分が進める施策やデザインするものを数値で把握しておくことで、より納得感を持って施策を進められます。
データ分析というと数学や分析の深い知識が必要そう…と構えてしまう印象ですが、日頃から自分の担当分野の基本的な数値を見ておくだけでもデザインで悩んだときの判断材料として使えるなど、デザイナーが数値をみる利点は多々あります。

分析の流れ

私は最近からデータ分析に取り組み始めたのですが、今はこのような流れで分析しています。

1.分析に必要な数値・グラフを決める

GitHubで分析用issueを立てて何を知りたいのか、そのためにはどんな数値をどんな形で見れると良いかを決めてから数値出しに進みます。

2.数値を出す

数値を出したらSQLが正しいかエンジニアにレビューしてもらいます。 レビューが通ったら数値から考えられることをチームで話しながら分析します。

3.分析レポートにまとめてGitHubのPull Request(以下PR)をだす

f:id:tsukasio:20170911145527p:plain

分析レポートを「仮説・試算・実数・考察・次のアクション」で整理してPRを出します。
レビューを受けることでさらに理解を深めるきっかけになったり、客観的な意見でアイディアをもらえたりします。
また、サービス開発部ではディレクター定例で施策の共有を行っていて、施策の分析結果はこのPRを共有しています。
ディレクター定例についてはこちらで詳しく説明されています。

つくれぽを簡単に送れる機能の分析

一部のユーザ向けに、よく見たレシピを利用したつくれぽを簡単に送れる機能を公開し、仮説通りの効果はえられたのか?どのように使われているのか?などを分析しました。

f:id:tsukasio:20170911145848p:plain

分析を進めていくうちに、アプリを起動してかたつくれぽを送るまでの推移をファンネルグラフでみたところ、離脱ポイントが2つあることがわかりました。

  • 「投稿する」の分岐で離脱
  • レシピをフリックしてから離脱

f:id:tsukasio:20170911145750p:plain

まず「投稿する」の分岐での離脱はどんなユーザーが離脱しているのか調べました。結果はつくれぽを送ったことがないユーザーがほとんどで、興味本位で「投稿する」をタップしていた可能性がありそうです。
次に、つくれぽしようと思ってこの画面に来たのにフリックした後離脱してしまうのはなぜかを考えました。

  • つくれぽを送れるレシピがなかった
    • 実際には作っていなかった?
  • 料理画像がなかった
    • レシピをみて料理を作ったが料理画像を撮り忘れた?
  • つくれぽしようと思うレシピがわからなかった
    • 似たようなレシピが複数並んでいた?
    • レシピ名をきちんと覚えていなかった?

いくつか仮説を立てた中で、3つ目はUIで解決できそうだということになり改善を進めました。

UI改善

この機能のデザインをした時に考えたことは、新しい機能とはいえつくれぽを送るというアクションなので、ユーザーが戸惑わないように既存のつくれぽ画面を参考にデザインしました。また、さくさく送れる感じを出したかったのでレシピをフリックして見れるようにしました。
ただ、既存のレシピ詳細画面からのつくれぽはすでにレシピを決めているので、つくれぽ画面ではレシピ名だけでもスムーズにつくれぽできていたという違いに気づきました。
この機能はつくれぽ送信画面に来てからつくれぽするレシピを探すので、どのレシピかがきちんと分かるUIが良いのではと思い、レシピ画像とレシピ作者名を入れたUIに変更しました。

beforeafter
f:id:tsukasio:20170911150004p:plainf:id:tsukasio:20170911150637p:plain

まとめ

現在は施策を企画する段階とリリース後の2つのタイミングで定量データを見ることを心がけています。
漠然とした「使いづらい」「分かりづらい」から改善を進めるのではなく、定量的なデータからその機能がどう使われているかを把握した上で仮説を立てると、より良い改善に繋がります。
ただ数値がすべてというわけではなく、定量データから見えない課題はユーザーテストで掘り下げるなど定量分析・定性調査をバランスよく見ていくと良いと思います。

料理きろくにおける料理/非料理判別モデルの詳細

$
0
0

研究開発部の菊田(@yohei_kikuta)です。機械学習を活用した新規サービスの研究開発(主として画像分析系)に取り組んでいます。 最近読んだ論文で面白かったものを3つ挙げろと言われたら以下を挙げます。

クックパッドのアプリには「料理きろく」という機能があります。 携帯端末から料理画像のみを抽出して表示することで自分が食べたものを振り返れるようになっており、ここからレシピ投稿やつくれぽを送ることもできるようになっています。

20170914153231

料理きろくはユーザ数が約12万8千人、累積の写真判別枚数が約7900万枚、そのうち料理と判別された画像が約960万枚(数字は20170912時点)と多くの方々に使っていただいている機能です。 本記事では、その料理きろくのコアの技術部分である、機械学習による料理/非料理判別の詳細に関してお伝えします。 料理きろく全体のアーキテクチャに関してはここでは述べませんが、ご興味のある方は AWS Summit Tokyo での発表資料をご覧ください。

料理きろくの背景と機械学習の役割

我々は、新しい技術を駆使してユーザ体験をより良いものに改善することで、クックパッドの使用頻度を増やしてもらったり、ユーザからもっとレシピやつくれぽを投稿してもらう、などの目標を持っています。 料理きろくはその目標に資する一つの施策であり、これは携帯端末に大量の食事情報が記録されていることから、それを活用してクックパッドのサービスに連携することを狙いとしています。 過去の自分の食事を振り返ることで食への楽しみや関心を高めてもらい、そこからレシピやつくれぽ投稿につなげたいというのが最初のターゲットとなっています。

ちなみに料理きろくではユーザのプライベートな画像を判別することになるため、全てサーバ上で処理がなされ、我々はユーザの画像を一切閲覧できないようになっています。

料理きろくにおける機械学習の役割は、料理/非料理を判別する高性能のモデルを提供することです。 機械学習によって正確に画像の料理/非料理が判別できるかどうかがサービス全体の質に直結するため、ここに Convolutional Neural Network (CNN) を用いたモデルを適用しました。 一口に CNN と言っても実に多様なので、データも拡充しながら、様々なモデルを試行錯誤をして改善を図りました。

以降では本番環境にデプロイしたモデルを中心に、どのような試行錯誤で改善をしていったのかをご紹介します。

最初にデプロイしたモデル

  • モデル:CaffeNet
  • データ:{料理,非料理}の二値ラベルの画像データ
  • フレームワーク:Chainer

最初のリリースでは CaffeNetというモデルを使っています。 これは ILSVRC2012で優勝した AlexNetを少し変更したもので、pooling が Local Response Normalization の前に来ています。

最初の段階では、素早くリリースまで持っていきたい、社内に画像分析の知見がまだ蓄積していなかった*1、などの理由で、事例も多いこのモデルを採用しました。 データはシンプルに料理画像と非料理画像をとりあえず手当たり次第に集めたものを使用しました。 テストデータも集めた画像の一部をテスト用に切り出して、料理判別の精度と再現率をチェックするというごくごく基本的な手法でした。

本番運用に際しては、精度が低いと料理ではないものが表出されてユーザ体験が悪くなるという考えのもと、再現率をある程度犠牲にしても精度を高めるという方向で閾値を調整しました。 単純な二値判別では softmax の出力が0.5を超えれば料理と判別されることになりますが、このモデルでは閾値を0.9で設定しました。 これは閾値を変えながらテストデータでの結果を目視でチェックしながら定めたものです。

最初のモデルは手探りの部分も多かったですが、素早くリリースまで到達できたことは非常に良かった点で、継続的改善を遂行していける土台が整えることができたので後の改善につながりました。

一回目のモデルアップデート

  • モデル:Inception-v3
  • データ:{料理,非料理,複数の間違えやすい非料理カテゴリ}の多値ラベルの画像データ
  • フレームワーク:TensorFlow

最初にデプロイしたモデルでも結構精度が高かったのですが、自分たちでも使っていくうちに間違いやすい画像があることが分かってきました。 具体的には植物や赤ちゃんの画像などが間違いやすい傾向があることが判明したため、これらの画像にもロバストなモデルを作りたいという要望が出てきました。 また、CNN のモデルも様々な発展があるため、それらを検証してより基本的な性能が高いモデルを採用したいという考えもありました。

そこで、まずは様々な CNN のモデルを比較検証をして良いモデルを探すという実験をしました。 この頃には画像分析ができる人員も増えていたため、手分けをして実験をして GHE の wiki に情報を集約し、最終的に我々のデータセットに対して最も良い結果を出した Inception-v3を採用することにしました。 モデルとして試したのは、{Inception-v3, GoogLeNet, ResNet, VGG, GAN を使った classification, NIN(軽量なモデルにも興味がある), …}、です。

フレームワークに関しても、TensorFlow を使う人が多くなったため、Chainer から切り替えました。 モデルの学習には Keras with TensorFlow backend を使っていますが、本番にデプロイする時は TensorFlow のみで動かすようにしています。 料理きろくのアーキテクチャはモデル部分を疎結合にしてあるので、この変更はそれほど大きなコストもなく実現できました。

次に単純な二値判別では問題として単純化しすぎているのではという考えのもと、多値判別に切り替えるという実験をしてみました。 思考回路としては、仮に世の中の料理画像の全集合が手に入ればその補集合が非料理画像だがそれは不可能→モデルは我々が集めた(非)料理画像の集合から(非)料理らしさを学習→これらは多様なので一つのカテゴリに集約し切るのは無理がありそう→特に間違いやすいものに関しては陽にカテゴリを作ってそちらに誘導したほうが我々が望むモデルができそう、という感じです。 料理/非料理ともに画像(二値ではなく多値のラベルを付与したもの)を追加収集して、それぞれが単一カテゴリ(この場合は多値の情報を潰して二値として扱う)の場合と複数カテゴリの場合とで性能を比較しました。 モデルの出力は一般に多値になりますが、判別結果としては多値の情報を潰して料理/非料理の二値判別として扱うようにしています。 全体的な性能を上げつつ特に精度を高めるものとして、料理は単一カテゴリとして非料理は多値カテゴリ(具体的には植物や人物を含む5カテゴリ)として扱うことに決定しました。

これらの改善によって、手元のデータで試験したところ、精度も再現率も向上し、特に間違えやすかった植物の画像に対しては間違いが約 1/3 に、赤ちゃんの画像に対しては間違いが約 1/20 ほどになりました。

これの取り組みは以前クックパッドで開催された Cookpad Tech Kitchenでも発表しています(発表資料)。

また、2017年度 人工知能学会全国大会やIJCAIのワークショップとして開催された 9th Workshop on Multimediafor Cooking and Eating Activitiesなどの学術的な場でも発表をしています。

二回目のモデルアップデート

  • モデル:Inception-v3 + patched classification
  • データ:{料理,非料理}の二値ラベルの画像データ、それらを14×14のパッチにしたもの
  • フレームワーク:TensorFlow

一回目のアップデートで大きく改善はしましたが、画像中に人と料理が同時に写っている場合はモデルが判断に迷う(人と判断すべきか、料理と判断すべきか)という問題が残っていました。 我々は「画像中の一部を切り取ってクックパッドのレシピとして掲載できそうなものは料理と判断する」という基準で料理/非料理を判断しているので、十分に料理が写っているのにモデルが非料理と判断されているものは改善の余地がありました。

この問題に取り組むために、まずはマルチラベルの判別をすることを考え、そのために Keras の ImageDataGenerator 辺りを改修したりもしましたが、データ準備のコストが高いため一旦保留としました。 次に、問題の根本は複数のものが写っているのにそれらをまとめて判別してしまっていることだと考え、画像をパッチに分割してパッチ毎に料理/非料理を判別するというモデルを構築しました。 具体的には、通常の Inception-v3 の出力付近で使う GlobalAveragePooling と Dense を、Conv2D や Dropout などを組み合わせて出力が 14×14×1 にするように置き換えて、sigmoid の出力で binary cross entropy を計算するようにしています。 パッチに分けることで料理と非料理を区別しやすくなることが分かったため、再び二値分類のモデル(ただしパッチ毎)になっています。 パッチサイズの 14×14 に関してはいくつかのパターンを実験した結果最も良い結果を返すものを選択しました。

このモデルの学習にはパッチ毎にラベルが付与されたデータが必要ですが、これは単純に元データをパッチに分割して、全パッチに元データと同じラベルを付与するという作り方でデータ準備を簡略化しました。 ただしこの作り方だと特に画像の端の部分が悪さをする可能性があるので、適切なラベルが得られるように一部の画像を crop したりもしています。

また、本番にデプロイした場合の性能を見積もるために、本番でのデータ分布に近くなるように社員から許諾を得て携帯端末のデータを提供してもらいました。 プライベートな画像のため、閲覧権限を絞って、特定の人が正解ラベルを付与してそれを使ってモデルの詳細な性能検証を実施しました。

このような改善を経て本番環境にデプロイされたモデルの結果の一例が以下の図となります。 色がついている領域が料理らしさが高い領域で、閾値以上のパッチを取り出してその領域が一定以上であれば料理と判別するというモデルになっています。

20170914153255

この改善によって、テストデータに対して精度を落とさずに再現率を5%以上改善することができました。 料理きろくは1000万というオーダーで画像を処理しているので、改善のインパクトは大きなものとなります。

モデル構築に使用したデータ

クックパッドには大量の料理画像があるので正例のデータには事欠きませんが、モデルの学習には負例のデータも重要になるため Creative Commons のデータや社員からデータを集めたりして様々な画像を収集しました。 プロジェクトの進行と共に画像を追加して試行錯誤してという作業を繰り返し、結果としてトータルで数十万枚程度の画像を扱いました。

どのような画像が重要になったかは上述のモデルアップデートでもご紹介しましたが、改めてモデルの性能向上に重要であった特徴的なデータをいくつか挙げたいと思います。

  • 料理 
    当然ながら正例としての料理画像は最重要のデータとなります。 クックパッドの豊富な料理画像データを用いて充実したデータセットを構築することができました。
  • 人(特に赤ちゃん) 
    人物画像は判別を間違えた場合のリスクも高い(ユーザ体験の観点から)ため、負例の中でも特に気をつけて画像を収集して学習に用いました。
  • 植物 
    想像に難くないですが、色合いや形状から誤判別される場合が多かったのが植物の画像でした。
  • 植木鉢 
    植物と似ていますが、植木鉢の場合は器もあるためにより一層誤判別されるものが多かったです。 そのため意識して負例としてデータを収集しました。
  • 料理が乗っていない空の皿
    料理には皿がつきものなので、皿があれば正例と勘違いしかねないため何も乗っていない皿も負例としてデータに追加しています。
  • 本番運用時にモデルが扱うデータ分布に近いテストデータ
    本番ではユーザの携帯端末中の画像が対象ですが、学習は収集したラベル付きの限定的なデータを用いているので、モデルの正しい性能が測りづらいという問題があります。 ユーザの画像は我々も閲覧できないため、この問題はオフライン/オンラインに限らず原理的な問題です。 そこで、許諾を得た社員のスマホのデータを使い、人力でラベルを付け、これを使ってモデルの評価を実施しました。 プライベートな画像であるため、閲覧権限を必要最低限の人間に絞って詳細な性能評価をして、他の人には統計情報のみを共有するという手法を採りました。

やはり学習データは最重要ですね。 料理きろくにおいては、多くの人の協力によって様々なデータを収集することができたので、非常に心強かったです。

今後の展望

料理きろくのプロジェクトを通じて、やはり継続的な試行錯誤に基づいた適切な改善策を講じることが重要であることが再認識できました。 それゆえに、これで完成ではなく、日進月歩の Deep Learning 技術を取り込んだり、データを拡充したりして、よりユーザにとって有用なモデルを構築し続けるのが大事だと考えています。

例えば最新の SENetや料理ドメインに特化した 横方向に広くスライスした畳み込みを試してみたり、自分たちで使いながら判別を間違った画像を収集してその傾向を探る、より高速に動作するモデルを構築するなど、改善の方向性は様々です。

「自分ならもっと良いモデルが作れる!」という人がいましたら是非一緒にやりましょう。

おまけ

以前 Twitter で話題になっていた画像として、犬と食べ物の区別が難しい画像というものがありました。 みなさんは下の画像のどれが犬でどれが食べ物かが判別できるでしょうか? 遠目で見ると判別はかなり難しい感じがします。

20170914153246

画像は以下から引用させていただきました。
左の画像:https://twitter.com/teenybiscuit/status/707727863571582978
右の画像:https://twitter.com/ohmycorgi/status/867745923719364609

これらの画像に我々の料理/非料理判別モデルを適用して、料理画像だけを抽出してもらうとしましょう。

20170914153237

見事に料理画像だけを選び出すことができました! Deep Learningのモデルが適切に料理とそれ以外を区別できていることを伺い知ることができます。

これ以外にも、近い内容の話として 弁護士の柿沼太一先生との対談などもあります。 ご興味があれば是非ご覧ください。

まとめ

クックパッドアプリの料理きろくという機能で用いている料理画像判別技術に関してお伝えしました。 CNN のモデル自体はもの凄く特殊なものを構築しているわけではありませんが、試行錯誤を経たモデルの変遷やその過程で遭遇したタスク特有の問題点などに興味を持っていただけたなら幸いです。 本記事でお伝えしたのは一例であり、クックパッドでは様々なサービスにおいて、発展著しい機械学習の技術をユーザに有益なものへと昇華させる取り組みに日々励んでいます。

いかがでしたでしょうか。 クックパッドでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。 クックパッド株式会社 研究開発部 採用情報

*1:このプロジェクトが始まった段階では私はまだ入社していませんでした

Cookpad Tech Kitchen #10を開催しました。

$
0
0

f:id:tsukasio:20170915160252p:plain

こんにちは、サービス開発部デザイナーの平塚です。

2017年9月13日(水)に、クックパッドオフィスにてデザイナー向けイベント「Cookpad Tech Kitchen #10」を行いました。

f:id:tsukasio:20170915160042j:plain

今回は「自社サービスで取り組むデザイン」をお題に、開発現場での実践事例を交えながら各社の文化やデザイナーの働き方についてご紹介しました。

お集まりいただいた皆さまの熱量も高く、イベントスタート時からQAセッション、懇親会に至るまで、とても充実した時間を過ごさせていただきました。 ご来場いただいた皆さま、本当にありがとうございました。

また今回は多数応募をいただいたため残念ながら抽選に漏れてしまった皆さま、たいへん申し訳ありません。 こういったイベントは今後も開催していきたいと思いますので、またのご参加を心よりお待ちしております!

一部ですが、イベントで行った各プレゼンテーションの概要をご紹介します。

「"料理の追体験"を実現するデザイン」

  • 若月 啓聡(Cookpad/デザイナー)

新機能タイムラインのデザインや、チームのユーザーとの関わり方についてお話を聞くことができました。

f:id:tsukasio:20170915160058j:plain

「はてなブログの世界観になじむ機能デザイン」

  • 松井 沙織(はてな/デザイナー)

はてなブログの新機能やUI変更の中で、書くことを邪魔しないデザインについてお話を聞くことができました。

f:id:tsukasio:20170915160114j:plain

「温度のあるサービスづくり」

  • 木坂 名央(GMOペパボ/デザイナー)

minneの、作家に向き合った温かみのあるデザインについてお話を聞くことができました。

f:id:tsukasio:20170915160133j:plain

「自然さを追求した音楽体験のためのUX」

  • 冨樫 晃己(CyberAgent/デザイナー)

AWAの、現実世界にありえる自然な動きについてお話を聞くことができました。

f:id:tsukasio:20170915160153j:plain

QAセッション

パネルディスカッションでは、

「UIデザイナーとUXデザイナーは兼任していますか? 実際の所UIとUXの兼任は難しいと思うのですがどのように分業や兼業していますか?」

「ドッグフーディングをチーム内で浸透させるのにはどうしていますか?」

といった参加者からの質問について、各社の取り組みについてより深く触れていただきました。

f:id:tsukasio:20170915164720j:plain

これからも楽しいイベントを企画していきます。 今後ともよろしくお願いします!


【クックパッドではデザイナー/エンジニアを積極採用中です】
ユーザー体験に向き合ってサービス開発をしたいデザイナーやエンジニアの方は、下記をご覧ください。

クックパッド採用情報 | UX/UIデザイナー
https://info.cookpad.com/careers/jobs/careers/ux-ui-designer
クックパッド採用情報 | エンジニア
https://info.cookpad.com/careers/jobs/careers/type/engineer

Viewing all 726 articles
Browse latest View live