いまさら聞けない!?プロダクトマネージャー・ディレクターが機械学習の案件を始めるまで

はじめに

この記事はEnigmo Advent Calendar 2018の25日目です。

BUYMAでプロダクトマネージャー・ディレクターのようなことをしています。 機械学習に関する案件を初めて進めてみようと思い、 プロダクトマネージャー・ディレクター目線で 、やってきたことや学んだことをまとめます。

知識がなくてもプロジェクトや案件は進めれるとは思いますが、ある程度理解があることで、プロジェクトの幅も広がりますし、エンジニアとのコミュニケーションも円滑になりますし、 何より自分も楽しいです

また、もし機械学習に関して知見がない会社でプロジェクトを進めていく場合の参考になればと思います。

この記事の対象者

非エンジニアプロダクトマネージャー・ディレクター  向けの記事です。

  • プロジェクトや案件で機械学習を利用しようと検討しているがどうしたらよいかわからない
  • 自分で機械学習学びたい!せっかくなら、プロジェクトや案件にしたい!(自分はこのタイプです) という方向け。

対象ではない方

  • 技術的な話とかかわらない方
  • 既に機械学習を利用してプロジェクトを推進している方
  • 機械学習に関わるプログラミングを実施している方
  • エンジニアの方

全体の流れ

1.基本知識のインプット 2.実際にコーディングもやってみる 3.プロジェクト・案件にするまで

基本知識のインプット

ゼロからスタートする場合は、何から初めてよいのか?と悩むことも多いと思います。

一番最初にやってみた

まず参考にしたのが下記の記事 【保存版・初心者向け】独学でAIエンジニアになりたい人向けのオススメの勉強方法

網羅されていて、わかりやすいが、プロダクトマネージャー・ディレクターとしては、ここまではいらない。(実際にやってみて途中でいろいろ挫折。時間もかかる)

その中で、やってきたことをプロダクトマネージャー・ディレクター向けに書きます。 下記に出てくる記事や本を読めば、最低限は大丈夫ではないかなと思います。

STEP1:まず機械学習を知る

例に漏れずここから。流し読みでもかなり面白い。 人工知能は人間を超えるか (角川EPUB選書)

読み終わると

  • AIってなんかすごそうってなる
  • なんかすごくAIがわかった気持ちになれる
  • エンジニアが言っていることが何となくわかるようになる

STEP2 : Pythonを知る

実際にコーディングもやることを考えると、ディレクターでもPythonは必須です。 全く触れたことがなかったので、本当に簡単なところから。

ドットインストール

動画を通勤中や移動中に見る。理解しようとせず最初は、流し見でよいと思う。 3回ほど聞くと、なんとなくわかってくる。

終わると

  • 動画なら、なんでも良いと思う
  • Pythonがわかった気になれる
  • Pythonを書いてみたくなる
  • エンジニアの言っていることがわかるようになる

Python3入門ノート

非常にわかりやすかった。 エンジニアではないので、日々コーディングしないと言語は習得が難しいので、なんとなく理解する程度で、読み飛ばす。 その中でも、読んでおくとよさそうのは、 Part3の「Numpyの配列」 ただ、読んでもほぼ忘れる。

あと、読み進める中で気になるところは、写経して実際にコードを書いみると良い。 一番重要なのは、 あとから困ったときに調べるために使うこと 。

読み終わると
  • Pythonがかけて嬉しくなる
  • エンジニアの仲間に入れてた気がする

STEP3(+α):Courseraのmachine-learning

無料 でここまで使える学習ツールはすごい!

機械学習のロジック部分で、実際にはCourseraを全くやっていなくてもコーディングはできる。知っておくとパラメーターチューニングの意味を理解できる面白い。 通勤中などの移動中に聞ける。 英語できなくても、日本語も用意されているのでほぼ問題ない。

こういう記事からも力をもらいながら、完了。

Courseraのmachine-learning

学習を終えると
  • なんか機械学習の理論がわかった気になれる
  • ただ、本当にすぐに忘れる
  • アンドリュー先生(講師の先生)が優しすぎて好きになる

実際にコーディングもやってみる

PandasとMachineLearning

KaggleのLearnがめちゃくちゃ良い。しかも 無料。 英語だけれど、Google翻訳を使えばほぼ問題なく進められる。

機械学習を実際に行う際のベースとなる部分なので、これはやっておくことをおすすめする。ただ、使わないと忘れるので注意。

本を読みながら深掘りしてみる

pythonで始める機械学習 を読んで、気になるところは写経してみる。

実際のプロジェクトをする際は、機械学習のパラーメーターチューニングなどはディレクターには求められていないと思うので、1章から3章くらいまでやれば十分だと思う。

読み終わると
  • 機械学習のコードが書けて、嬉しくなる
  • なんか実際のデータでやってみたくなる

(補足)コーディングに利用するツール

自分で環境構築できる方はぜひそちらで。 環境構築ができない/しない方は、GoogleColaboratoryでやるのがおすすめ。 これも無料で簡単なので、僕もこれを使いました。めっちゃ楽です。

プロジェクト・案件にするまで

はじめに

プロマネとして、機械学習プロジェクトを始めるならこれは必読書です。

仕事ではじめる機械学習

本書では、機械学習やデータ分析の道具をどのようにビジネスに生かしていけば良いのか、また不確実性が高いと言われている機械学習プロジェクトの進め方について 整理しています。 本書はもともと、機械学習の初学者向けに書いた文章からはじまりました。入門者のために書きはじ めたのですが、実際には理論を軽めにしたソフトウェアエンジニア向けの実践的なカタログのような形 になっています。 アルゴリズムの話などは他の書籍でも数多く取り上げられているので、本書ではプロジェクトのはじ め方や、システム構成、学習のためのリソースの収集方法など、読者が「実際どうするの?」と気にな るであろう点を中心にしています

とある通り、 プロジェクト始めたいけど、でどうするの? ということが書いてありますので、実施にプロジェクトを進める方にはすごく学びが大きいかと思います。 実際に、「その作業必要?」のようなリソース判断をすることや、全体像を理解することで手戻りを少なくする一助にもなるかと思います。

この要約がわかりやすかったです。

読み終わると

  • 機械学習のプロジェクトは思っているよりも多くのことがあるのだと知る
  • 実際にプロジェクトをやってみたくなる

実際にやったこと

本書の中で重要だと思ったのは、機械学習を使うことを目的化しないということでした。 機械学習の本なのに、 別に機械学習を使う必要は必ずしもない  と書いてあり驚きました。

実際に案件化していくステップとしては、 1.ユーザーの課題を明確にする 2.機械学習を使わずに解決する方法を考える 3.どうしても必要な場合は機械学習を活用する です。

機械学習の有無にかかわらず、 ユーザーの課題を明確にする ことからスタートして、 機械学習を解決策の一つ として持っておく感覚かと思います。

STEP1:ユーザー課題を明らかにする

プロジェクトを進めるにあたり、課題を明確化します。 実際に僕らが進めた場合は、経営陣やビジネスサイドの方と、

1.経営陣が認識している事業課題 2.現状現場が認識しているサービス課題 3.サービス以外にも、現場メンバーの作業に関する課題

など、様々なレイヤーでサービス課題/業務課題に関するディスカッションする機会を取りました。 課題を俯瞰して考えることで、課題理解も広がり、実際モチベーションも上がりました。

このステップを終えると
  • やっぱり〇〇はすごい解決したいよね〜(サービスの大きな課題)を再認識できます
  • 普段そんな作業をしてるのか。。。という現場のリアルを知れるます
  • 他の部署の人と仲良くなります
  • 実際にやる前提で話をするので、すごく楽しいです。

STEP2:課題の解決策を考えて、進める

初めて案件を進めるとなれば、どのように案件を進めるかが難しいかと思います。

はじめての機械学習関連のプロジェクトということもあり意識したのは、いかに ライトに着地させて一定の効果を出す かでした。

そのための課題設定と解決策を決めます。

本当は、これが理想。 ただ、簡単に見つからないですよね。。。

実際に進めていくなかで、最初の案件として一番しっくりきたのはこういう施策でした。

機械学習を取り入れる理由が明確にあり 、且つ工数も比較的小さいもの。 ここを見つけてプロジェクト化していくことで、進みやすくなるかと思います。

実は、ここで言う開発工数とは、機械学習の基盤を作成する工数は除いて考えています。 機械学習の運用基盤を作成するのは工数がもちろん高いので、

  • できるだけ使い回せるロジックにすること
  • 一度作成すれば、それほどアプリケーション開発の工数が変わらないこと

が重要なので、複数案件化できる状態にしておくことで基盤を作りやすくなるかと思います。

STEP3:巻き込む人を間違えない

前出のまとめ記事にもありましたが、案件を進める上で上記のメンバーは巻き込む必要があります。 そこで間違えると話が進まないので、初めてプロジェクトを進める場合は気にしてみてください。

まとめ/終わりに

ここまでお読みいただきありがとうございました。 はじめての記事でしたが、少しでも参考にしていただければ嬉しい限りです。

  • 基本的なインプットから、コーディング、プロジェクト化など一通りやってみて非常に勉強なりました。
    • 実際にコーディングもプロジェクトもやってみることが一番だと思いました。
  • 案件を考えるときに「機械学習」という選択肢を持てるのは、プロマネとしては強みにな今後なりうるのだろう思います。
    • ただ、選択肢の一つということを意識することは重要
    • 機械学習を使う!と言いながら、やはりいちばん大切なのは、「ユーザーの課題は何か?」という問いに尽きると改めて思いました。
  • 実際に、知見がない中で初めてプロジェクトを進める場合は、関わるメンバーや案件は慎重に進めるとよいと思います。
    • 時代の後押しはあるので、比較的会社も挑戦を応援してくれると思います。

関数型言語、聞いたことあるけど結局何なの?

概要

エニグモ サーバーサイドエンジニアの @gugu です。 この記事はEnigmo Advent Calendar 2018の24日目です。

関数型言語って結局何なの?と思ったので調べてみました。私が疑問に思ったことをベースに調べた内容を記載していこうと思います。

関数型言語として主にHaskellをメインに調べているので、関数型言語すべての話ではない記述があるかもしれません。

対象者

参考書

参考サイト

ありがとうございます!

関数型言語の疑問

「関数型」っていうけど手続き型でもオブジェクト指向でも関数を書くじゃん?

私がはじめに思った疑問です。みなさんもそう思いませんか? 下記のようです。

  • 関数型言語の関数というのは副作用のない純粋な関数のこと。(詳しくは後述)
  • すべてが関数でできている。Haskellだとif文も関数なのでelseの省略は不可。※言語によってそれぞれ例外あり。(そもそも何か値を返すのが「関数」、返さないのが「プロシージャ」と呼ぶ。C言語からプロシージャも含めて「関数」と呼ぶようになったとか。)
  • オブジェクト指向ではクラス内に関数を書く。(お作法的に。なぜこんな話をするのかは後述)

関数型言語の中でもマルチパラダイム言語って両方使えて最強じゃね?

結論から言うとそうでもないらしいです。

その前に関数型言語の種類について説明。。。

オブジェクト指向が備わっているということは副作用が発生する可能性が増えるということ、つまり関数型言語の本来の目的とズレてしまっていることになります。(もちろん適材適所でオブジェクト指向と関数型で使い分けられるメリットはあるかと。)

別に関数型言語でなくてもオブジェクト指向で副作用なく作れば良いのでは?

まずは先に副作用がないとはどういうことか

雑な説明かもしれませんが、変数が途中で変わらず参照透明であることです。

束縛

関数型言語で変数に「代入」することを「束縛」と呼びます。代入と違う点は一度値を入れると変更できません。

x = 1
x = 2 -- error

参照透過性

引数が同じなら返り値も必ず同じになる関数のことを「参照透過性」と言います。

で、オブジェクト指向でも参照透明に書けば良いのでは?

その通りで、オブジェクト指向でも副作用を避けて書くことは大事です。ですが、そもそも言語としてのアプローチが異なります。

関数型言語は副作用が起きづらい?だから?

下記メリットがあります。

  • テストや保守を容易にする
  • バグがおこりずらい
  • 再利用可能な部品を作りやすい
  • (注目)遅延評価

遅延評価

副作用がないので実現可能に。 使用するときに一度だけ計算されキャッシュされます。無駄な処理を省くことができます。

関数型言語ってどうやって書くの?

これが代表例なのかわかりませんが、私が一番なるほどと思ったのはループ文です。 関数型言語は関数がメインなのでループをさせる際はfor文ではなく再帰を使用します。(Haskellだとそもそもforもwhileも無いみたいです。)

Wikipedia(関数型言語)

で、オブジェクト指向関数型言語どちらで書くべき?

すみません、わからないです。。。(汗) 個人的な意見だと、やはり堅牢でテストコードの書きやすい関数型言語がベターなような気がしますが、オブジェクト指向のほうが「オブジェクト」を自然と意識して作れるので設計しやすいような気もします。ただ書き慣れているだけかもしれないのでやはり関数型言語なのでしょうか。

関数型言語の勉強にScalaは向いていないかも。。

関数型言語の勉強でScalaを勉強していたのですが、結局はオブジェクト指向脳を使ってオブジェクト指向で書いてしまうので、なにが関数型言語の特徴なのか理解しづらいです。関数型言語を学びたければ純粋型を学ぶのが良いかと思います。

まとめ

実際私が関数型言語への疑問を中心に書いたので、偏った知識かもしれません。しかし同じような疑問を持っている方に少しでも役にたてばと思って書いてみました。

調べた感じだと関数型言語は良いことづくしなような気もしますが、数学的でやはりとっつきにくいイメージも大きいかと思いました。結局は「関数型言語を知りたければ関数型言語で書くべし!」なんでしょうね。。関数型言語と仲良くなれるようにがんばります!

Codableいいよ!

この記事はEnigmo Advent Calendar 2018の23日目です

こんにちは。iOSチームでエンジニアをやっています。

Codable使ってますか?

iOSチームでは、 Alamofire + Codable で ネットワークまわりの実装を行なっています。

最初はいいのかわからなかったのですが、今ではなくてはならないものになっています。

すごく便利すぎて、Codable無しじゃ開発できない!そんな生活を送っています。

Codableについて軽く説明からの、実際使ってみて、Codableの得意なところと苦手なところを書いていこうと思います。

TL;DR

  • Codable良いから使ってみて!
  • Codableになれると、Enumをたくさん使うようになる

Codableとは

Swift4からFoundationに追加されたtypealiasです。 ( Codableは、プロトコルではありません )

EncodableDecodableの二つプロトコルに準拠します。

https://developer.apple.com/documentation/swift/codable

Codable、何に使うの?

JSONを扱う際の、エンコード / デコード を Codableを使い、簡易に表現します

どうやって使えばいいの?

例えば

itunes.apple.com から取得できるJSONから今リリースしているアプリのVersion知りたい!という時に、使えます。

JSON:

{
    "resultCount": 1,
    "results": [
        {
            ....
            "sellerUrl": "https://www.buyma.com",
            "trackName": "BUYMA(バイマ) - 海外ファッション通販アプリ",
            "currentVersionReleaseDate": "2018-12-12T06:02:14Z",
            "version": "3.3.0",
            "minimumOsVersion": "10.0",
            ...
        }
    ]
}

Codable:

struct AppInfoResponse: Codable {

    let results: [Results]

    struct Results: Codable {
        let version: String
    }
}

すごい簡単ですね。

Codableの良さ

Foundation 純正

  • 純正なので、Swiftのバージョンが上がった際に、オンタイムでアップデートされている
  • サードパーティのライブラリの場合、そのライブラリのアップデート対応が終わるまでXcode/Swiftのバージョンを上げれない

JSONDecoderのカスタムもいける

  • APIのレスポンス内のデータで、Date / DateTimeクラス が 2018-11-4 23:592018-06-24T23:59:59+09:00のように揃っていないケースがあります JSONDecoderdateDecodingStrategy.customにすることで、様々なケースのFormatに対応することができます
    decoder.dateDecodingStrategy = .custom {
        let container = try $0.singleValueContainer()
        let string = try container.decode(String.self)
    
        let formatter = DateFormatter()
    
        /// Date format: ISO_8601
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
        if let date = formatter.date(from: string) {
            return date
        }
        formatter.dateFormat = "yyyy-MM-dd HH:mm"
        if let date = formatter.date(from: string) {
            return date
        }
    
        return Date()
    }
    

Codableの苦手なところ

nilで返さず、空文字で返すと失敗する

  • URLがあるけど、URLになっていない
struct UserResponse: Codable {

    let name: String
    let imageUrl: URL?

    enum CodingKeys: String, CodingKey {
        case name
        case imageUrl = "image_url"
    }
}

成功:

{
    "name": "P",
    "image_url": "https://www.buyma.com/image"
}
</pre>

失敗:

<pre>
{
    "name": "P",
    "image_url": "" // 空文字
}

対応策

  • image_urlを 空文字ではなく、nilにしてもらう
  • init(from decoder: Decoder) を実装する

e.g.:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    imageUrl = try? container.decode(URL.self, forKey: .imageUrl)
}

Arrayの中に、様々な Classが混ざっているケース

"topics"の各Objectの"type"を見ないといけない:

{
    "topics": [
        {
          "type": "sale",
          "title": "夏のセール開催中!",
          "products": [
          ]
        },
        {
            "type": "news",
            "title": "夏のセール開催中!",
            "image_url": "https://www.buyma.com/image",
            "link": "https://www.buyma.com/"
        },
        {
          "type": "topic",
          "title": "韓国ブランド集めました!",
          "search_url": "https://www.buyma.com/search"
        }
    ]
}

対応策

  • type見るCodableを使って、一度どのtypeになるのかを判定してから再度デコードする

チームでCodableをどうやって使っているか

開発フロー

  • APIcURLで叩いてjsonを取得
  • Codable準拠したstruct Responseを作成する
  • cURLで取得したjsonファイルを使用してstruct Responseにデコードされるかテストを実装する

サンプルコード

ネットワーククライアントのデコード処理:

func decode(_ type: T.Type, from data: Data) -&gt; T? {
    do {
        return try decoder.decode(type, from: data)
    } catch {
        print("---- API Parse Error ---")
        print(String(bytes: data, encoding: .utf8) ?? "")
        print("Error Description: \(error)")
        return nil
    }
}

Codableができているかどうかのテスト:

class ResponseTests: TestCase {

    func testDecodeResponse() {
        guard let path = Bundle(for: type(of: self)).path(forResource: "ResponseSample/sample", ofType: "json"), let fileHandle = FileHandle(forReadingAtPath: path) else {
            fatalError()
        }

        guard let response = decoder.decode(SampleResponse.self, from: fileHandle.readDataToEndOfFile()) else {
            fatalError()
        }

        XCTAssertNotNil(response)
    }

}

まとめ

  • Codableにすることで、CodingKeyに準拠したCodingKeysを書かないといけない手間はありますが、それを書いてもメリットが大きいです
  • 苦手はありますが、API設計に起因する部分が多いと思うので、チームでAPIを相談する際に、何が苦手なのかを伝えるといいと思います
  • SwiftyJson, ObjectMapperと使っていましたが、ほぼCodableに移せました

LiNGAM入門。気軽に因果関係を推定する(統計的因果探索)

この記事はEnigmo Advent Calendar 2018の22日目です。

はじめに

https://atarimae.biz/archives/7374 交番と犯罪件数が正の相関があるからといって、交番を減らして犯罪件数は減らないですよね。

さて、データ分析を行う上では、相関関係と因果関係を切り分けることが重要になることがあります。 例えば、KPIとある数値xが相関しているとします。 x → KPI という因果関係であれば、xの操作でKPI向上の施策を検討することができます。 逆に、KPI → x という因果関係であれば、xを操作してもKPIは変化しません。 y → x なのか、x → y なのか、xとyの相関関係の有無だけでは、因果関係は分かりません。

この記事では、機械学習ブロフェッショナルシリーズ、統計的因果探索を参考にしています。

なお、、z → x , z → y という未観測の共通原因(交絡因子とも呼ばれる)zが存在する場合についても、書籍では扱われていますが、今回の記事では未観測の共通原因については扱わないことにさせていただきます。

LiNGAM

x_{1}, x_{2}の2変数の関係bを以下のように構造方程式とう呼ばれるモデルであると仮定します。

x_{1} = b_{12}x_{2} + e_{1}

x_{2} = b_{21}x_{1} + e_{2}

x_{1}, x_{2}の2変数で現れ、e_{1}, e_{2}は外生変数がノイズ項です。 なお、LiNGAMでは非ガウス分布として扱います。 因果関係の推定の結果、x_{1} -> x_{2}という因果関係がある場合は、b_{12} = 0となり下記のようになります。

x_{1} = e_{1}

x_{2} = b_{21}x_{1}+e_{2}

ここで、LiNGAMは以下の方法があります

  • (1)独立成分によるによるアプローチ
  • (2)回帰分析と独立性によるアプローチ

独立成分によるによる推定

既にPythonで実装している方がいらっしゃるので、このアプローチに関してはそちらを参考にさせていただきます。

https://github.com/ragAgar/LiNGAM

モジュールとともに、lingam.pyをimportします。

import pandas as pd
import numpy as np
from lingam import LiNGAM

scikit-learn付属のボストン不動産データセットを使います。 価格をyにするのが自然ですが、敢えて 価格:x 部屋数:y とします。

boston = load_boston()
df = pd.DataFrame(boston.data, columns=boston.feature_names)
x = boston.target
y = df.RM.values

推定を実行します。

data = pd.DataFrame(np.asarray([x, y]).T, columns=['target', 'rooms'])
lg = LiNGAM()
lg.fit(data)

推定結果

rooms ---|9.102|---&gt; target

部屋数が上がると、価格が上がる。という正しそうな因果関係を推定することができました。

回帰分析と独立性によるアプローチ

本をPythonで実装しました。ここでは、因果の向きのみの推定を行います。 なお、エントロピーは近似であることが本では述べられており、近似式の導出については、参考文献を読む必要があります。

def calc_r(x, y):
    return ((x - np.mean(x * y) - np.mean(x)*np.mean(y)) / np.var(y) * y,
            (y - np.mean(x * y) - np.mean(x)*np.mean(y)) / np.var(x) * x)

def normalize(x):
    return (x - np.mean(x)) / np.std(x)

def entropy(u):
    Hv = (1 + np.log(2 * np.pi)) / 2
    k1 = 79.047
    k2 = 7.4129
    gamma = 0.37457
    return Hv - k1*(np.mean(np.log(np.cosh(u))) - gamma)**2 - k2 * (np.mean(u * np.exp(-1 * u**2 /2)))**2

def lingam_reg(x, y, columns):

    xr, yr = calc_r(x, y)
    m = entropy(normalize(x)) + entropy(normalize(xr) / np.std(xr)) \
        - entropy(normalize(y)) - entropy(normalize(yr) / np.std(yr))

    if m &gt;= 0:
        print('{0} ---&gt; {1}'.format(columns[0], columns[1]))
    else:
        print('{0} ---&gt; {1}'.format(columns[1], columns[0]))

推定を実行します。

data = pd.DataFrame(np.asarray([x, y]).T, columns=['target', 'rooms'])
lg = LiNGAM()
lg.fit(data)

推定結果

rooms ---&gt; target

こちらも正しそうな推定結果を偉ました。

部屋数以外の変数でもやってみる

  • INDUS: 小売業以外の商業が占める面積の割合
y = df.INDUS
X = pd.DataFrame(np.asarray([x, y]).T, columns=['target', 'INDUS'])
lg = LiNGAM()
lg.fit(X)
lingam_reg(x, y, columns=['target', 'INDUS'])

推定結果

INDUS ---|-0.648|---&gt; target
INDUS ---&gt; target

小売業以外の商業、つまりオフィスの面積の割合が増えると、不動産価値は上がる。正しい気がする。

  • 税率
y = df.TAX
X = pd.DataFrame(np.asarray([x, y]).T, columns=['target', 'TAX'])
lg = LiNGAM()
lg.fit(X)
lingam_reg(x, y, columns=['target', 'TAX'])

推定結果

target ---|-8.586|---&gt; TAX
target ---&gt; TAX

不動産価値が高いと、税率が上がる。今までは逆の因果関係だが、この場合は正しそう。

最後に

ごく簡単な例ですが、統計的因果探索であるLiNGAMを体験することができました。 前述の構造方程式の通り、変数間の関係が線形である等、仮定があることは注意しなければいけませんが、あくまで因果関係の向きを推定する上では、その仮定の上でも議論が進められると思います。

また、こちらの取り組みについても興味深いです。 https://qiita.com/MorinibuTakeshi/items/402cb905e70655724d35

未観測の共通要因については今回割愛してしまいましたが、LiNGAMは因果推論という枠組みの一つの取り組みであり、傾向スコアやグレンジャー因果性といった取り組みについても今後学習を進めていきたいと思います。

LINE Flex Messageを検討してみた

エニグモの @takurokamiyoshi です。 この記事は Enigmo Advent Calendar 2018 の21日目の記事です。

私は主にフロントエンド周りの実装やディレクション業務を行っています。 弊社ではファッションECサイトであるBUYMAを運営しており、 利用者とのコミュニケーションツールとしてメールやアプリ、そしてLINEも重要なツールだと考えています。

BUYMA LINEアカウントについて

https://www.buyma.com/contents/line/ これからLINEアカウントの友だち数も増やして、LINEアカウント内でいろんな施策を実施したいと思っています。

LINE Messaging APIとは?

LINE社が提供するLINEカウントを通じて、ユーザとの双方向コミュニケーションを実現するAPIです。 メール施策で実施しているシナリオ配信のような配信に利用にしたり、ボットとして活用しているLINEアカウントも多いようです。

※とりあえずLINE Messaging API試してみたい人は無料で試せるLINE Developer Trialプランがあります。LINEアカウントを作成いただいてcurlでLINE Messaging APIをコールしてもらえればと思います。

Flex Messageとは?

LINE Messaging APIではテキスト、画像、動画、カルーセルといった様々なメッセージタイプでメッセージを配信することができます。 Flex Messageは複数の要素を組み合わせてレイアウトを自由にカスタマイズできるメッセージです。 html、cssに近い感覚でメッセージレイアウトを考えることができます。

Simulator

Flex Message Simulator LINE社が提供しているものになります。 Simulatorなしでゼロから作成するのはかなり困難で Flex Message Simulatorではサンプルがいくつか準備されているものでこれをベースに考えると良いと思います。

Flex Messageはレイアウトの自由度が高いのでいろいろ検討が必要だと思い今回の記事を作成しようと思いました。

レイアウト案

いくつかの案を記載していきます。 ※curlの{XXX} の部分は各環境で置き換えてください。 ※curlは長くなったので一部改行、空白なしにしています。

縦積みパターン

とりあえず何かに使えそうだと思います。

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type:application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d '{
    "to": "{userId}",
    "messages":[
        {  
          "type": "flex",
          "altText": "this is a flex message",
          "contents": {"type":"bubble","body":{"type":"box","layout":"vertical","spacing":"none","margin":"none","contents":[{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}}]}}
        }
    ]
}'

商品一覧パターン

スマホサイトの商品一覧のような形のレイアウトです。 もっと長くしてもよいかなとも思いました。

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type:application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d '{
    "to": "{userId}",
    "messages":[
        {  
          "type": "flex",
          "altText": "this is a flex message",
          "contents": {"type":"bubble","hero":{"type":"image","url":"{url}","size":"full","aspectRatio":"10:3","aspectMode":"cover","action":{"type":"uri","uri":"{uri}"}},"body":{"type":"box","layout":"horizontal","spacing":"md","contents":[{"type":"box","layout":"vertical","flex":1,"contents":[{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","size":"xxl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}}]},{"type":"box","layout":"vertical","flex":1,"contents":[{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","size":"xxl","gravity":"bottom","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}}]}]},"footer":{"type":"box","layout":"horizontal","contents":[{"type":"button","action":{"type":"uri","label":"More","uri":"{uri}"}}]}}
        }
    ]
}'

細いメッセージパターン

意外と需要がありそうです。 リッチメニューを出しているとメッセージが見える範囲が狭くなりますし、画像作成は必要ありません。

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type:application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d '{
    "to": "{userId}",
    "messages":[
        {  
          "type": "flex",
          "altText": "this is a flex message",
          "contents": {"type":"bubble","body":{"type":"box","layout":"horizontal","contents":[{"type":"button","style":"primary","action":{"type":"uri","label":"リンク1","uri":"{uri}"}},{"type":"button","style":"secondary","action":{"type":"uri","label":"リンク2","uri":"{uri}"}}]}}
        }
    ]
}'

まとめ・感想

自由度が高いので細かいこだわりを反映できるメッセージですがやはり複雑なものを考えるのは難しいです。 Flex Messageをゼロから作成するのは結構大変なので、他の人が作成したものを共有していけるようにしていければ良いなと思いました。 ざっと触ってみてちょっとだけ慣れてきました。慣れてくれば、Simulatorなしでも作成できるようになるのだろうか。。 また以前はマルチキャスト /v2/bot/message/multicastFlex Messageを配信できなかったですが使用できるようになったので大量配信にも使えそうです。 今回調べたことを実際の施策の中で活かしていきたいと思います。

参考

LINE Developers ドキュメント

Gitlab CIを利用したGCP(GKE)への自動デプロイ

この記事は Enigmo Advent Calendar 201820日目の記事です。

概要

GCP(GKE)を利用してログからユーザ属性を機械学習により予測し、 その結果をAPIで返却するシステムを構築しました。 運用していく上でCI/CDツールは何にしようかなぁというところで、 今後社内でGitlabを使っていくという流れがあったのと GKEとの相性も良さそうということでGitlab CIを利用することにしました。1

今回はGitlabとGCP(GKE)環境を連携する方法と、 Gitlab CIのキモとなる.gitlab-ci.ymlの内容についてまとめたいと思います。

システム構成

簡単にデプロイ対象のシステム構成を紹介します。 赤い枠で示したものが今回の作業対象になります。

環境条件

今回は下記の環境を使用しています。 Gitlabについては毎月バージョンが更新されるので、旧バージョンを利用されている方などは若干UI等が変わっているかもしれません。

  • Gitlab
    GitLab Community Edition 11.5.3
  • GKE
    クラスタバージョン 1.10.9-gke.5

CI/CDのフロー

CI/CDは下記のフローで行います。

テスト環境

  • developブランチへのcommit&push

以降は自動

  • dockerイメージのbuild
  • dockerイメージをGCSへpush
  • GKE(CronJon)へのデプロイ
  • GKE(Deployment)へのデプロイ

本番環境

本番はすべて手動

  • developブランチからmasterブランチへのMR作成
  • マージ
  • GKE(CronJon)へのデプロイ
  • GKE(Deployment)へのデプロイ

設定手順

Gitlab CIを利用するにあたり必要な設定を行っていきます。

サービスアカウント作成

GCP(GKE)とGitlab CIとの連携のためにGCPコンソールからサービスアカウントを作成します。

  • ここでは仮に「Gitlab CI」という名前のサービスアカウントを作成します。
  • 役割(role)に下記の2つを付与してください。
    • Kubernetes Engine 開発者
    • ストレージ管理者
  • キーのタイプでJSONを選択し、キーを作成し、ローカルにキーファイルをdownloadします。

変数の設定

GKEへの認証情報をGitlabに登録します。 対象のプロジェクト(リポジトリ)のSettings -> CI/CD -> 変数にて、 キーに「SERVICE_ACCOUNT_KEY」を入力し、 値に先ほどdownloadしたキーファイルの中身をそのままコピペします。

下記の例では「SERVICE_ACCOUNT_KEY_PROD」というキーもありますが、 これは本番とテストでプロジェクトを分けているためです 1つのプロジェクト配下の場合は1つのキーで問題ないと思います。

Specific Runnersの登録

下記コマンドにてSpecific Runnerを登録します。 今回はdocker in docker方式で行います。 shell executorを利用する場合はこちらを参考にしてください。

$ sudo gitlab-runner register -n \
> --url [URL] \
> --registration-token [TOKEN] \
> --executor docker \
> --description "docker-runner" \
> --tag-list "gke" \
> --docker-image "docker:stable" \
> --docker-privileged

※指定するURLとTOKENについてはGitlabのSettings -> CI/CD -> Runnerの項目を参照してください。

今回はオンプレ版のGitlab環境を使用していますが、 gitlab.comでShared Runnerを利用する場合には こちらの手順は不要です。

.gitlab-ci.ymlの作成

今回はCronjobへのデプロイ定義を例に説明します。 Deploymentへのデプロイ定義も基本的に同じです。

  • 共通設定
    • テスト/本番環境など、デプロイ対象の環境が複数ある場合にはどうしても定義が冗長的になります。
    • それを解消するために、下記ではYAMLのアンカー/エイリアスとGitlabのextends(v11.3で追加)という機能を利用して、共通的な定義はまとめるようにしています。
.auth_gke: &amp;auth_gke |
  gcloud auth activate-service-account --key-file=key.json
  gcloud config set project ${PRJ_ID}
  gcloud config set container/cluster ${CLUSTER_NAME}
  gcloud config set compute/zone ${CLUSTER_ZONE}
  gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${CLUSTER_ZONE}

.setting_dev:
  environment: develop
  variables:
    CI_DEBUG_TRACE: 'true'
    PRJ_ID: 'XXXXX-dev'
    CLUSTER_NAME: 'dev-XXXX'
    CLUSTER_ZONE: 'asia-northeast1-a'
    GCS_KEY: 'XXXX.json'
    ENV_CONFIG: '/PATH/conf/settings_dev.json'

.setting_prod:
  environment: production
  variables:
    PRJ_ID: 'XXXXX-prod'
    CLUSTER_NAME: 'XXXXX'
    CLUSTER_ZONE: 'asia-northeast1-a'
    GCS_KEY: 'XXXX.json'
    ENV_CONFIG: '/PATH/conf/settings_prod.json'

services:
  - docker:dind

stages:
  - build
  - deploy
  • ジョブ定義①
    ここではdockerイメージのbuildと、GCRへのコンテナイメージのpushを行っています。
    • extendsで先程定義した内容を継承しています。
    • only:changesで該当のファイルに変更があった場合のみジョブが実行されるようになります
docker build_base:
  extends: .setting_dev
  image: 'docker:stable'
  stage: build
  tags:
    - gke
  script:
    - docker info
    # Build the image
    - docker build --cache-from ${IMAGE_TAG} -t ${IMAGE_TAG}:${IMAGE_VER} .
    # Log in to Google Container Registry
    - echo "$SERVICE_ACCOUNT_KEY" &gt; key.json
    # - docker login -u _json_key --password-stdin https://asia.gcr.io &lt; key.json
    - docker login -u _json_key -p &quot;$SERVICE_ACCOUNT_KEY&quot; https://asia.gcr.io
    # Push the image
    - docker push ${IMAGE_TAG}:${IMAGE_VER}
  only:
    refs:
      - develop
    changes:
      - Dockerfile
      - bin/*
      - conf/*
      - requirements.txt
  • ジョブ定義②
    ここではテスト/本番環境に対する2つのCronJobリソースへのデプロイを行います。
    • 本番環境へはwhen: manualを定義して手動でデプロイするようにしています。
deploy_model_builder_dev:
  extends: .setting_dev
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    # Authenticate with GKE
    - echo "$SERVICE_ACCOUNT_KEY" &gt; key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    refs:
      - develop
    changes:
      - .gitlab-ci.yml
      - kubernetes/XXXX-job.yaml

deploy_bulk_judgement_dev:
  extends: .setting_dev
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    - echo "$SERVICE_ACCOUNT_KEY" &gt; key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    refs:
      - develop
    changes:
      - .gitlab-ci.yml
      - kubernetes/XXXX-job.yaml

deploy_model_builder_prod:
  extends: .setting_prod
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    # Authenticate with GKE
    - echo "$SERVICE_ACCOUNT_KEY_PROD" &gt; key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    - master
  when: manual

deploy_bulk_judgement_prod:
  extends: .setting_prod
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    # Authenticate with GKE
    - echo "$SERVICE_ACCOUNT_KEY_PROD" &gt; key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    - master
  when: manual

ロールバック

せっかくなのでロールバックの手順についても記載しておきます。 Gitlabの左メニューから運用 -> 環境とたどっていくと、下記のような画面が表示されます。

上記で記載した.gitlab-ci.ymlにenvironmentという定義があると思いますが、 そこで定義した文字列が環境のところに表示されているのがわかると思います。 たとえばここでproductionをクリックします。 すると下記の画面に遷移します。

この一覧はproductionに対するコミットの一覧です。 たとえば直前のコミットにロールバックしたい場合は赤枠のところをクリックすればOKです。 もちろん、この画面から任意のコミットを選択して再デプロイすることも可能です。

environmentの定義があることで環境毎のコミット履歴が一覧で見れるかつ、 そこからのデプロイ、ロールバックも簡単に行えます。 複数環境がある場合にはぜひ設定することをおすすめします。

まとめ

  • 今回、Gitlab CIを初めて利用してみましたが、特に癖もなく使いやすかったです。
  • Auto DevOpsや、先日発表されたGitLab Serverlessなど、まだまだアツい機能があるので、時間を見つけて検証してみようと思います。

  1. 当初はGitlabのAuto Devopsの利用を考えていましたが、検討時点では利用しているGitlabのバージョンが古かったため、ひとまずGitlab CIを利用することにしました。 

日報をword cloudで可視化して2018年を振り返る

この記事は Enigmo Advent Calendar 2018 の19日目の記事です。

はじめに

ネタ何にしようかなぁと思って、

  • カジュアルな感じでかつ単発で終わるようなもの
  • 検索、自然言語処理関連で何か
  • 年末的な何か

ということを踏まえて、 Qiitaチームに日々挙げている自分の作業日報を可視化して2018年の振り返りをしてみることにしました。

私がエニグモに入社したのが今年の2月なので、正確には1年ではないですが、 細かいことは気にせずいきたいと思います。

作業フロー

作業フローとしては、

  • Qiita API v2を利用して自分の作業日報の本文と作成日時を取得
  • 取得した本文を形態素解析器にかけてキーワードのみを抽出
  • 抽出したキーワードをword cloudを利用して可視化
  • 可視化したものを見て思い出に浸る

という感じで進めようと思います。

Qiita APIで日報の本文を取得

  • アクセストークンの発行 Qiitaチームからデータを取得するためアクセストークンを発行します
  • アクセストークンの利用
    • 取得したアクセストークンはAuthorizationリクエストヘッダに指定して利用します。
    • 今回はpythonで実装しましたが、curlの場合は下記のような感じで取得できます。
$ curl https://qiita.com/api/v2/authenticated_user/items \
-H "Authorization: Bearer [アクセストークン]"
  • 下記のメソッドを定義して日報データを取得します
    • APIを利用して20件ずつデータを取得
    • 取得したデータはDataframeに突っ込み、不要なデータは取り除く
    • 取得した作成日時をDatetimeIndexに設定して、月単位でデータが扱えるようにしておく
def get_report():
    BASE_URL = 'https://XXXX.qiita.com/api/v2/items?'
    PER_PAGE = 20
    # tagで絞り込み
    QUERY = 'tag%3A%E6%97%A5%E5%A0%B1%2F%E4%BC%8A%E8%97%A4%E6%98%8E%E5%A4%A7'
    curr_page = 1
    with open('qiita_access_token.txt') as f:
        qiita_access_token = f.read().strip()
    header = {'Authorization': 'Bearer {}'.format(qiita_access_token)}
    
    
    # レスポンスヘッダを見てページング処理
    df_list = []
    total_cnt = 0
    while(True):
        target_url = '{0}page={1}&amp;per_page={2}&amp;query={3}'.format(BASE_URL, curr_page, PER_PAGE, QUERY)
        req = urllib.request.Request(target_url, headers=header)
        res = urllib.request.urlopen(req)
        if curr_page == 1:
            # ヘッダから全投稿数を取得
            total_cnt = int(res.info()['Total-Count'])
            # 最終ページの計算
            if total_cnt % PER_PAGE == 0:
                last_page = total_cnt // PER_PAGE
            else:
                last_page =  total_cnt // PER_PAGE + 1
        res_body = res.read()
        res_dict = json.loads(res_body.decode('utf-8'))
        
        # dataframeに変換
        df_list.append(pd.io.json.json_normalize(res_dict))
    
        # 最終ページチェック
        if curr_page == last_page:
            break
        curr_page+=1
        
    # df_listを結合
    report_df = pd.concat(df_list, ignore_index=True)
    
    # 必要なデータのみ抽出
    report_df = report_df[['body', 'created_at']]
    
    # created_atの文字列をdate型に変換
    report_df['created_at'] = pd.to_datetime(report_df['created_at'])
    
    # DatetimeIndexに変換
    report_df.set_index('created_at', inplace=True)
    
    return report_df

形態素解析器でtokenize&前処理

  • janome(辞書はneologd)を利用し、前処理として下記を実施します。
    • unicode正規化
    • 改行コード削除
    • 大文字→小文字に統一
    • 名詞のみ抽出(数字は対象外)
def analyze():
    
    # charfilter
    ## unicode正規化
    ## 改行コード削除
    charfilters = [UnicodeNormalizeCharFilter(),
                           RegexReplaceCharFilter(u'\r\n', u'')]

    # tokenizer
    tokenizer = Tokenizer(mmap=True)

    # tokenfilter 
    ## 大文字→小文字
    ## 名詞のみ抽出(数は除去)
    tokenfilters = [POSKeepFilter('名詞'), POSStopFilter(['名詞,数']), LowerCaseFilter()]
    # analyzer
    analyzer = Analyzer(char_filters=charfilters, tokenizer=tokenizer, token_filters=tokenfilters)
    
    return analyzer


def get_word_list(report_list, analyzer):
    word_list = []
    for report in report_list:
        word_list.append(" ".join( analyzed_line.base_form for analyzed_line in analyzer.analyze(report)))
    return word_list

可視化処理

word cloudを利用して可視化するためのメソッドを定義します

def draw_wc(vecs_dic, fig_title):
    font_path = '/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc'
    wordcloud = WordCloud(background_color='white',
                          font_path = font_path,
                          min_font_size=15,
                          max_font_size=200,
                          width=1000,
                          height=1000)
                          
    wordcloud.generate_from_frequencies(vecs_dic)
    plt.figure(figsize=[20,20])
    plt.imshow(wordcloud,interpolation='bilinear')
    plt.axis("off")
    plt.title(fig_title,fontsize=25)
    plt.show()

指定した月の日報を可視化する

では実際にやっていきます。 まず、私が入社した月の日報を見てみましょう。

  • 1年間分の日報データを取得
df_reports = get_report()
  • 可視化したい月を指定
target_month = '2018-02'
my_analyzer = analyze()
words = get_word_list(df_reports[target_month].body.tolist(), my_analyzer)
  • TF-IDFで各単語を重み付け
vectorizer = TfidfVectorizer(use_idf=True, token_pattern=u'(?u)\\b\\w+\\b')
vecs = vectorizer.fit_transform(words)
words_vectornumber = {}
for k,v in sorted(vectorizer.vocabulary_.items(), key=lambda x:x[1]):
    words_vectornumber[v] = k
vecs_array = vecs.toarray()
all_reports = []
vecs_dic = {}

for vec in vecs_array:
    words_report = []
    vector_report = []
    for i in vec.nonzero()[0]:
        vecs_dic[words_vectornumber[i]] = vec[i]
  • 可視化
draw_wc(vecs_dic, target_month)

「入社」というキーワードでフレッシュ感がほのかに漂っています。 その他でみると「性能」、「分析」、「検証」、「測定」とかが大きくでてますね。 そういえば、この月は検索システムの性能改善をメインでやっていた気がします。

続いて6月の日報を見てみます。

「韓国」と大きく出てますが、これな何でしょうかね。全く覚えがありません(笑) 「chef」、「レシピ」、「zero」とかはこのあたりでchef zeroで何かの検証をしていたんでしょう。きっと。。 「パーソナライズド」、「fasttext」があるので、この時期あたりからパーソナライズサーチの検証をしてたんだと思います。

最後に11月

他人の日報なんて誰も興味ないと思うので、これで最後にします。 せっかくなのでクリスマスっぽくしてみました。

先月の内容なので、特に振り返ることもありませんが、 Gitlab CI周りの調査/検証やAPIの開発をメインで実施していた時期でした。

おわりに

今回初めてword cloudを使ってみましたが、 視覚的にデータを見るのって新たな気付きがあったりして楽しいですね。 来年はフォントとか画像をもうちょっとオサレな感じにして日報Tシャツ作ってヘビロテしようと思います。