Gunosyデータ分析ブログ

Gunosyで働くデータエンジニアが知見を共有するブログです。

箱根でデータ分析部開発合宿をしました(小田原・箱根おすすめグルメ情報付き)

こんにちは、データ分析部の久保です。 データ分析部では四半期に一度ぐらい開発合宿を行っています。 参加は任意でもちろん業務としてカウントされます。

合宿編

今回の合宿場所は以前も使用したAirbnbのこちらの部屋を使いました。

www.airbnb.jp

ホストのErikoさんも丁寧に対応してくれるし、部屋はとてもきれいで8~10人の宿泊にはかなり快適です。 ただ1点備え付けのwifiにつながりにくいという欠点があったので、今回は予めwifiレンタルをしていくことにしました。

【国内用】当日発送で高速WiFiをレンタル | WiFi東京RENTALSHOP

初日のチェックイン時刻までは途中小田原に立ち寄って、小田原の商工会議所の会議室を借りて各自作業しました。 f:id:beatinaniwa:20170722144653j:plain

今回各自が取り組んだテーマはいくつか例をあげると下のようなものでした

- 「テキストアナリティクスシンポジウムでの発表に向けた実験データの整理と再実験」
- 「ログデータの異常値検出」
- 「広告CTR予測モデルの作成」

合宿中には異常検知の手法についてディスカッションしていたときに確率分布の話題になり、こんな名言(?)も出ていました。

夜には箱根湯寮で温泉を楽しみ、晩御飯を食べながらメンバーの親交を深めました。

www.hakoneyuryo.jp

グルメ編

さて開発合宿の楽しみといえば現地でのおいしい食事があります。 初日のお昼は小田原の海鮮料理を食べました。

サカナキュイジーヌ・リョウ (SAKANA CUISINE RYO)

海鮮ちらし、とくに生しらすは絶品でした。 f:id:beatinaniwa:20170724155734j:plainf:id:beatinaniwa:20170724155755j:plain

また2日目は箱根から小田原に向かう途中に風祭で途中下車し、うなぎを食べに行きました。

うなぎ亭 友栄

友栄は食べログのうなぎランキング全国3位(7/24現在)にもなっていて、行く前から相当期待が高まっていましたがその期待をまったく裏切ることない味で最高でした。箱根近辺に行った際にはぜひ立ち寄ることをおすすめします。 今まで食べたうなぎの中でも個人的には相当上位でした。

(時間帯によるかもしれませんが30~1時間ぐらいは待つ覚悟をしていったほうが良い気がします)

f:id:beatinaniwa:20170724155811j:plain

開発合宿は日頃なかなか手を付けられない中長期的なタスクに取り掛かるのにはとても良い方法だと思います。 次の開発合宿をどこでやるか、今から楽しみにしています。

Gunosy における AWS 上での自然言語処理・機械学習の活用事例: AWS Summit dev day 2017

はじめに

こんにちは。Gunosyデータ分析部の大曽根(@dr_paradi) です。最近はJOHN TROPEA BAND featuring STEVE GADD etcのライブを観に行きました。 業務では主にニュースパスのユーザ行動分析、記事配信アルゴリズム開発全般を担当しています。

先日開催されました、AWS Dev Day Tokyo 2017において、「Gunosy における AWS 上での自然言語処理・機械学習の活用事例」というタイトルで発表してきましたので、その内容について簡単ですが書きたいと思います。

発表内容

私が発表した内容は下記のスライドにまとまっています。弊社が提供するサービスのニュースドメインのもの(グノシー、ニュースパス)における処理の流れを大まかに話しました。

発表内容では以下の3点を中心に話しました

  • 記事分類
  • 属性推定 + スコアリング
  • 効果測定 (ABテスト)

各項目に関して過去の発表資料を含め簡単に解説します。

記事分類

文書分類に関しては多くの手法がありますが、基本的には文書を(いろいろな方法で) ベクトル化し*1、教師あり多クラス分類問題として解きます。 ナイーブベイズを用いた例に関しては下記の資料にまとまっております。

qiita.com

教師あり分類問題においては、教師データの作成、収集が重要になります。 教師データおよびモデルの管理に関しては以前の勉強会で私が簡単に話しました。

発表資料にある通り、作成したモデルはアプリケーションのdeploy時にS3から取得します。 deployの際に下記のようなスクリプトを実行しています。

# get models
execute "aws s3 cp --region #{s3_region} s3://#{s3_bucket}/#{s3_prefix} #{application_data_dir}/ --recursive"

属性推定 + スコアリング

属性推定

属性推定の手法に関してはWebDB Forum 2016の技術報告で発表済みで、下記のエントリにまとまっています。 属性推定でも、記事分類と同様にユーザのクリック履歴をベクトル化し、教師あり多クラス分類問題として解いています。

data.gunosy.io

スコアリング

今回紹介しているニュースのドメインにおいては、記事に鮮度がある*2ので、リアルタイムでのコンテンツ評価が重要になります。 集計および可視化の具体的な方法に関しては下記のエントリが詳しいです。

data.gunosy.io

また、GunosyでのKinesis Analyticsの利用し始めた経緯、詳細については下記のイベントで発表予定です。 AWS Solution Days 2017 ~AWS DB Day~(2017 年 7 月 5 日開催) | AWS

効果測定 (ABテスト)

Gunosyではデータに基づいた意思決定を非常に重要視していますが、ニュースアプリのであるため、時流や季節要因などの影響を受けやく、データに予期しないノイズが乗り、計測が困難になることが多くあります。 そのため、ABテストを用いてできるだけ施策の効果を正確に継続することを重要視しています。

ABテスト実施および測定の際に参考にしたブログは下記のエントリにまとまっています。 data.gunosy.io

マイクロサービスにおけるABテスト実装時のTipsなどは下記の資料にまとまっています。 ABテスト対象であるかどうかのパラメタをログに含めることで、解析を容易にすることができます。

ABテストの重要性に関しては下記の論文発表が詳しいです。

おわりに

AWS Dev Day Tokyo 2017での発表について簡単にまとめました。

機械学習ライブラリの充実により、ある程度の精度のもの(分類器などは)を作成するコストは下がっているように思えますので、今後は実際のサービスで動かすことがより重要になってくるかと思います。 今回の発表が、実サービスで機械学習を応用する際の参考に少しでもなれれば幸いです。

*1:文書のベクトル化についてはそのうち当ブログにて公開される予定です

*2:既知のニュース記事は読まれにくい

プロダクト改善のためにウォッチしておくべき7つの指標

データ分析部でグノシーというニュースアプリのプロダクト改善を担当している @ij_spitz です。

今回はプロダクト改善のためにウォッチしておくべき7つの指標をSQLで算出してみます。 Gunosyではこれらの指標を、プロダクトに異常があった時に検知するため、また施策の効果検証といった主に2つの目的で使用しています。

簡潔にするため、ユーザーとログインの2つのテーブルを使った算出できる指標のみを対象としています。 また、これらの指標をどうやってプロダクト改善に役立てているのかということも少しではありますが、合わせて書いていきたいと思います。

  • DAU
  • WAU(MAU)
  • HAU
  • 積み上げHAU
  • 1ユーザーあたりのログイン回数
  • 登録N日後継続率
  • 登録日別N日後継続率

前提

今回のブログで紹介するSQLはAmazon Redshift上で動くSQLなので、MySQLやGoogle BigQueryでは動かない可能性があります(というか動きません)。 またテーブル定義は以下のようになっています。

logins

Column Type 備考
user_id integer
created_at timestamp without time zone ログイン日時

users

Column Type 備考
user_id integer
created_at timestamp without time zone ユーザーの作成日時

DAU

SELECT
    created_at::date AS date,
    COUNT(DISTINCT user_id) AS dau
FROM
    logins
WHERE
    created_at >= '2017-06-01 00:00:00'
    AND created_at < '2017-07-01 00:00:00'
GROUP BY
    date
ORDER BY
    date

みなさんおなじみだとは思いますが、1日にサービスを利用したユーザーのユニーク数を表すDAUです。 ニュースアプリは毎日使ってもらうのが大事なので、毎日どれくらいのユーザがアプリを起動したかどうかを重要視しています。

DAU = Σ 新規ユーザー数 * 継続率

と分解できるので、DAUを増やすためには新規ユーザー数を増やすことおよび、継続率を上げる(下がった時に異常に早く気付く)ことが重要になってきます。

WAU(MAU)

SELECT
    date_series.date,
    COUNT(DISTINCT user_logins.user_id) AS wau
FROM (
    SELECT
        DISTINCT created_at::date AS date
    FROM
        logins
    WHERE
        created_at >= '2017-06-01 00:00:00'
        AND created_at < '2017-07-01 00:00:00'
) AS date_series
JOIN (
    SELECT
        DISTINCT
            created_at::date AS date,
            user_id
    FROM
        logins
    WHERE
        created_at >= DATEADD(day, -7, '2017-06-01 00:00:00')
        AND created_at < '2017-07-01 00:00:00'
) AS user_logins
ON
    user_logins.date <= date_series.date
    AND user_logins.date > DATEADD(day, -7, date_series.date)
GROUP BY
    date_series.date
ORDER BY
    date_series.date

DAUを最大化しようとすると、短期的な最適化に陥ってしまう可能性があります(非常に極端な例ですが1日に10回プッシュを打ってみるなど)。

そういった状況を避け、長期的に見てユーザー数が最大化するためには、DAU以外にWAUやMAUといった指標も確認しておく必要があります。

HAU

SELECT
    EXTRACT(HOUR FROM created_at) AS hour,
    COUNT(DISTINCT user_id) AS hau
FROM
    logins
WHERE
    created_at >= '2017-06-01 00:00:00'
    AND created_at < '2017-06-02 00:00:00'
GROUP BY
    hour
ORDER BY
    hour

HAUもよく使われる指標の1つです。

グノシーでは1日に4回プッシュ通知を打っているので、そのプッシュ通知の効果がどうだったかというのを見るためにHAUも確認しています。

プッシュの開封率も確認していますが、アプリのアイコンを直接タップして起動するユーザーもいるためHAUと両方の数値を見ています。

積み上げHAU

SELECT
    hour_series.hour,
    COUNT(DISTINCT logins.user_id) AS hau
FROM (
    SELECT
        DISTINCT EXTRACT(HOUR FROM created_at) AS hour
    FROM
        logins
    WHERE
        created_at >= '2017-06-01 00:00:00'
        AND created_at < '2017-06-02 00:00:00'
) AS hour_series
JOIN
    logins
ON
    hour_series.hour >= EXTRACT(HOUR FROM created_at)
WHERE
    created_at >= '2017-06-01 00:00:00'
    AND created_at < '2017-06-02 00:00:00'
GROUP BY
    hour_series.hour
ORDER BY
    hour_series.hour

DAUを最大化するという目的に立ち返った時に、HAUばかりを見ていると同じユーザーばかり起動していて最終的にDAUはそれほど伸びていなかったというケースもあり得ます。

その日に初めての起動を促すプッシュの方がDAUへの寄与は高くなるため、積み上げHAUも同時に見ています。

1ユーザーあたりのログイン回数

SELECT
    created_at::date AS date,
    1.0 * COUNT(1) / COUNT(DISTINCT user_id) AS login_num
FROM
    logins
WHERE
    created_at >= '2017-06-01 00:00:00'
    AND created_at < '2017-07-01 00:00:00'
GROUP BY
    created_at::date
ORDER BY
    created_at::date

1ユーザーあたりの◯◯という指標もよく使います。 グノシーの場合だと1ユーザーあたりの読んだ記事数や閲覧したタブの数なども見ています。

登録N日後継続率

SELECT
    rr.num_elapsed,
    100.0 * rr.user_num / inflows.user_num AS retention_rate
FROM (
    SELECT
        DATE_DIFF('DAY', users.created_at::date, actives.date) AS num_elapsed,
        COUNT(DISTINCT actives.user_id) AS user_num
    FROM
        users
    JOIN active_users AS actives
    ON
        users.user_id = actives.user_id
    WHERE
        users.created_at >= '2017-06-01 00:00:00'
        AND users.created_at < '2017-06-08 00:00:00'
        AND actives.date >= '2017-06-01'
   GROUP BY DATE_DIFF('DAY', users.created_at::date, actives.date)
) AS rr
JOIN (
    SELECT
        COUNT(DISTINCT users.user_id) AS user_num
    FROM
        users
    WHERE
        users.created_at >= '2017-06-01 00:00:00'
        AND users.created_at < '2017-06-08 00:00:00'
) AS inflows
ON
    1 = 1
WHERE
    /* 対象登録日の最終日に十分な経過日数がある日だけに絞る */
    rr.num_elapsed  < DATE_DIFF('DAY', '2017-06-08', CURRENT_DATE)
ORDER BY num_elapsed

ユーザーが新規登録してからN日後にログインしている率を継続率と呼びます。

ABテストを行ったときは2つのグループ間でこの継続率を比較して、その施策がどれだけ継続率をリフトアップさせているか(もしくはダウンさせているのか)を確認しています。

DAU = Σ 新規ユーザー数 * 継続率

前述した上記の式からもわかるように、継続率が高いほどDAUの積み上がりも良くなるので、基本的には継続率が良くなる施策 = 良い施策となります(もちろん例外はありますが)。

登録日別N日後継続率

SELECT
    rr.created_date AS created_date,
    100.0 * rr.user_num / inflows.user_num AS retention_rate
FROM (
SELECT
    users.created_at::date AS created_date,
    DATE_DIFF('DAY', users.created_at::date, logins.created_at::date) AS num_elapsed,
    COUNT(DISTINCT logins.user_id) AS user_num
FROM
    users
JOIN
    logins
ON
    logins.user_id = users.user_id
WHERE
    users.created_at >= '2017-06-01 00:00:00'
    AND users.created_at < '2017-07-01 00:00:00'
    AND logins.created_at >= '2017-06-01 00:00:00'
GROUP BY
    created_date,
    logins.created_at::date
) AS rr
JOIN (
    SELECT
        created_at::date AS date,
        COUNT(DISTINCT users.user_id) AS user_num
    FROM
        users
    WHERE
        created_at >= '2017-06-01 00:00:00'
        AND created_at < '2017-07-01 00:00:00'
    GROUP BY
        date
) AS inflows
ON
    rr.created_date = inflows.date
WHERE
    rr.num_elapsed = 1
ORDER BY
    created_date

上のクエリはN = 1となっています。 これも継続率の1種ですが、先ほどの登録N日後継続率とは少し用途が異なります。

登録N日後継続率では主に異なる2つのグループ間での継続率の差を見るときに使用しているのですが、こちらの登録日別N日後継続率だと時系列で継続率の推移が確認できるので、異常検知に使用したり(例えば特定の広告で獲得したユーザーの継続率が低かったとか)中長期的に見て継続率がどんな傾向をしているのかを見るために使用しています。

弊社でBIツールとして主に用いているRedashでの可視化のアウトプットを見ると両者の違いがわかりやすいと思います(細かい数値は載せられないので軸が見えていない点はご了承ください)。

  • 登録N日後継続率(縦軸が継続率、横軸が登録後経過日数) f:id:ishitsukajun:20170703205354p:plain

  • 登録日別N日後継続率(縦軸が継続率、横軸が登録日) f:id:ishitsukajun:20170703205138p:plain

Redashについての記事はこちらをご覧ください。

data.gunosy.io

data.gunosy.io

おわりに

以上で終わりになります。 少し複雑なSQLもありましたが、2つのテーブルとSQLだけでこれだけの有用な指標を算出することができます。 SQLを叩く環境がもし社内に整っているなら、エンジニアでなくても、ぜひSQLをマスターして色々なデータを眺めてみることをおすすめします。 今後、アプリやWebサービスの業界では社内にSQLを叩ける環境があるということがスタンダードになってくるはずなので、覚えておいて損はないスキルだと思います(弊社ではエンジニア以外の社員もSQLを叩いて自分の見たい数値を見ています)。

Gunosyでは数値に基づいたプロダクト改善を日々行っているということを少しでも実感していただければ幸いです。

「これからの強化学習」1章の内容で三目並べ

こんちくわ。データ分析部兼サウンドエンジニアの大曽根です。最近は吾妻光良&The Swingin Buppersのライブに行きました。

今回は4/12に開催した「これからの強化学習」の輪読会の1.3節で紹介した価値反復法のアルゴリズムを、教科書とは異なる例で実装してみました。

開催報告については下記のブログをご覧ください。

data.gunosy.io

メジャーなゲームである三目並べを、1.3節にて紹介されているSarsaを用いて学習しました。 教科書とは別の例で実装することで少しでも理解が深まればと思います。

価値反復に基づくアルゴリズム

マルコフ決定過程において価値関数を特定の更新式に従って更新する手法です。(今回はSarsaで試しました。) 発表の際には、tの状態の更新式に次の状態 t+1が含まれているところなどがわかりづらいとの質問を受けました。 価値反復に基づくアルゴリズムでは過去の事例(エピソード)における、次の状態を参考に現在の行動価値関数を変更していくのでその点がわかりづらいかもしれません。

実装

エージェントと対戦相手が特定の座標に交互に手を打っていく形式になります。 (こういう類のシミュレーションでは)先行と後攻を固定する場合もありますが、今回は先行と後攻を対戦ごとに交代する形式の実装にしています。 また、すでに自身もしくは対戦相手が打った場所には手を打たない制御を入れています。

環境

盤面の座標および○×がどこに置かれているか 実際のコードでは ○: 1、×: - 1、空白: 0の3つの値を持つlistで表現しました

s = [ 0, 0, 0, 0, 0, 0, 0, 0, 0]

単純に考えると39通りの状態が考えられ、3目並べ程度のゲームで意外に環境の取りうる範囲は広いことがわかるかと思います。 (実際には存在しない手もあるのでもっと少なくなります。)

行動:

自身の手順の際にs の配列のどこを変更するかを行動としています。 上述の通り環境sの0 (○×のいずれも打たれていない場所) にしか打たない制御をしています。

A = [ 0, 1, 2, 3, 4, 5, 6, 7, 8]

報酬: 勝敗

報酬は勝敗が決定した際のみに振り分けられるものとし、勝利した際(○が3つ並んだ場合)にはプラスの報酬、負けた際(×が3つ並んだ場合)にはマイナスの報酬を振り分けられるようにしました。

f:id:dr_paradi:20170613183916j:plain

また、引き分けの際には報酬なしに設定しました(ここのルール設定によって学習の挙動が異なるかもしれません)。

学習のイメージ

f:id:dr_paradi:20170608015134p:plain

下記の例の場合には正の報酬を得ることができます。

s = [1, 1, 1, 0, -1, 0, 0, -1, 0]

ですので

sn = [ 1, 1, 0, 0, -1, 0, 0, -1, 0]

の際の

an = 2

の場合は勝利することができる(報酬が獲得できる)ため、 Q[sn][an] の価値関数が高くなれば学習が成功していると言えます。

これを繰り返して行くと次の状態価値関数を別のエピソードでフィードバックすることができます。

f:id:dr_paradi:20170613183920j:plain

方策

どの手を打つかの戦略を方策としています。Q値が高いものを打つパターンと、一定確率でランダムの戦略を打つ場合(ε-greedy)の二つを用意しました。

結果

学習に関するパラメータはα = 0.2、γ = 0.8で設定しトータルで10,000ゲームを行い、

  • ベースラインとして用意した完全ランダムのアルゴリズム
  • sarsa
  • sarsa ε-greedy (ε = 10%)

の三種類のアルゴリズムで100試合ごとの勝率をplotしました。 完全ランダムの場合と比較しますると、sarsaのアルゴリズムの場合には徐々に勝率が高くなっていることがわかるかと思います。 今回の例ではあまりgreedyとε-greedyとの間に大きな差はありません。 (plotをみる限りは学習(収束)が割とはやいので探索に対するメリットがないのかもしれません)

f:id:dr_paradi:20170529013128p:plain

考察

初期の盤面のQ値をあまり学習できていなかったので、

  • 初期の盤面のみルールを与える
  • リーチの時点で少し報酬をあたえるなど

なども試してみると面白いかと思いました。

感想

非常にシンプルなゲームですが、ターンの概念や勝敗判定などゲームの基礎的な部分の実装に以外と時間がかかってしまいました。 強化学習は環境の設定と報酬の設定など、本で読むより実装すると気づく部分も多いかと思います。全てを実装する必要はないかと思いますが、本を読みつつ実装してみるのも学びを進める上では重要かと思います。

今後

他のゲームの実装や、学習過程の可視化なども試して行きたいです。

Gunosyデータ分析サマーインターン募集のお知らせ

こんにちは、データ分析部の久保です。 今日はサマーインターンの募集を開始したのでそのお知らせです。

データ分析コース|Gunosy Summer Internship 2017

詳しくはリンク先を見ていただくとして、実際のGunosyプロダクトで使われるような「生きた」データ分析を体験できるインターンとなっています。

期間は3日間と短くなってはいますが、出題される課題を通じていろんなことが学べるように私達も準備しています。

当日はデータ分析部メンバーを始め、現在Gunosyで深くデータに関わっているメンバーがメンターとして適宜アドバイスも行うので、データ分析という仕事に興味はあるけど、実際どんなものなのか体験したいという学生の皆さんはぜひ応募してみてください(ちなみに今回のサマーインターンとは別に研究開発、データ分析アルバイトは通年でも募集しています)。

3日間六本木でおいしいご飯を食べられる、という特典ももれなくついてきます。

またこれも宣伝ですが先日、最近始めた会社のポッドキャストに出演して、データ分析部とはどういう雰囲気で仕事をしているか、というのも話しているので興味があればぜひこっちも聴いてみてください!

#2 ざっくりわかる、データ分析部 by gunosy.fm | Gunosy Fm | Free Listening on SoundCloud

iTunesでの購読はこちらから

それでは夏休みに皆さんと会えるのを楽しみにしています!