読者です 読者をやめる 読者になる 読者になる

俺の報告

RoomClipを運営するエンジニアの日報(多分)です。

TensorFlowを使ってRoomClipの部屋診断 - 日報 #159

すごい久しぶりの更新です。
さすがにこんだけブログを書いてないと、かなり敷居が高くなってしまうものなのですが、今日はちょっとお土産ができたのでそれでご勘弁いただこうかと。

巷じゃDeep Leaningってなものが流行っているそうですね。
続々と論文がでたりツールが発表されたりと生き馬の目を抜くリリース合戦が繰り広げられておりますが、 天下のGoogle社からこんなもの(TensorFlow)が登場して、さぁ大変といった模様です。

このDeep Learningですが、
真面目に中身を理解しようとするとなかなか骨が折れます。
「まずはニューラルネットから」しっかりと勉強していく連載を考えているのですが、ここでは軽く概要だけ。

これ乱暴に言いますと、
「何かを入力すると、あるルールにしたがって計算をして、何かを吐き出す」
というまったくもって普通の関数のようなものだと捉えることができます。
特徴的なのは、
「そのルールとやらについて、詳細は何も設定しない」
というところなんですね。
なんとなくの設計、くらいはプログラムするのですが、
詳細な係数の値とか、定数の値とか、その辺は「本当に適当」な数値をいれているのです。
ですが、それではもちろん用をなさないので、
目的のものを吐き出すように「学習」させるんですね。
答えがわかっている入力データを大量に用意して、
それをわざと入力し、結果出力される値と、前もって知っていた答えをみくらべ、
合っていれば放置、間違っていれば詳細の値を「少しだけ」修正する。
これをずーーーーっと繰り返すと、
いつかちゃんと答えを返すような詳細の値、すなわち全体の計算式が確定するわけです。

まぁ分かりづらいですが、
これのいいところは「計算式の詳細」についてもともと知っている必要がないってことなんですね。
例えば、
ある値をいれると、その2倍の値が返ってくるような謎の箱Fを考えるとします。
この謎の箱Fがやることは、数学的に求められます。っていうか2xです。
ですが、敢えてそうせずにものすごーーーい複雑な計算式にして、
その係数などを全部変数にして、ひたっすら色んな値を突っ込んで、
正解がでるようにちょっとずつ変数を調整して、
そして2倍の値が常にでるような超複雑な謎の箱Fを用意するという戦略をとるのがDLなのです。

この例ですとた、ただただ回りくどい方法にみえますが、
「そもそもどういう計算式にしていいかわからないもの」
に関してはものすごい効力を発揮するわけなんですね。

例えば、

とある画像データを入力したら、「犬」が写っていることを認知する謎の箱Fを考えたとして、その中身を前もって数学的に記述するのは相当難しそうです。
なのでその辺はぼやかしておいて、ただひたすら「犬」の写真を学習させる。
最初は不正解だらけだった謎の箱Fも、何度も何度も自身の変更を繰り返すことで、
やがて犬を認識するようになる。
実際中身がどうなっているかは誰も演繹的に説明はできないが、
結果は出せるという、本当の謎の箱Fができあがるわけです。

この辺の具体的な話はまた次回以降にまとめるとして、こんな便利なものないわけですね。しかも、Deep Learningは「画像認識に強い」なんていう論文もいくつも提出されたとあっては、RoomClipとしては放っておける訳ないのです。

さっそく使って遊んでみましょう。

ということで出来たのが、
TensorFlow ✕ RoomClip お部屋スタイル診断ツール

roomclip.jp

です。
冒頭に触れたGoogle社のTensorFlowをつかったツールとなっておりまして、
弊社の優秀なインターンであるM君が、
英語ドキュメントと格闘しながら作り上げてくれました。
本当すごいがんばってくれたので、
ぜひ彼のエンジニアブログも拝読ください。

t8m8.hateblo.jp

(RoomClipはいつでもエンジニアインターンを歓迎しているよ!軽いノリで連絡ください!)

さて使い方はいたって簡単、

RoomClipに登録して!(ここ重要)、
実際にあなたのお部屋の写真を投稿して!(ここも重要)、
そのURLをここに入力すると、
なんと
「あなたのお部屋を合計20個のインテリアスタイルで診断してくれる」
んですね!

判定してくれるスタイルは下記の20通り!

  • シンプル
  • カフェ風
  • 男部屋
  • ホワイトスタイル
  • 北欧風
  • 西海岸系
  • カラフル
  • アンティーク
  • ビンテージ風
  • カリフォルニアスタイル
  • モノトーン
  • インダストリアル
  • 昭和レトロ風
  • ナチュラル系
  • ごちゃごちゃ系
  • レトロスタイル
  • フレンチ風
  • アメリカンスタイル
  • 男前インテリア
  • ジャンク系

これらインテリアスタイルをRoomClipの中にある120万枚の写真から学習し、
判定してくれる謎の箱Fを作ったというわけなんですね。

さっそく、弊社代表の高重の部屋で試してみましょう。

roomclip.jp

まごうことない和風、昭和レトロスタイルです。
さてさて、、、これがどのようになるか、さっそく計算してみます。

f:id:tom_rc:20160330010033p:plain

わくわく。
計算中です。
TensorFlowはコーディングはわりとシンプルにできるし便利なんですが、
如何せん評価がすごい重たいです。
どなたか簡単にevalできる方法ご存知のかたいらっしゃいますでしょうかね。。。

とにかく待ちます。

...

でました!

f:id:tom_rc:20160330010159p:plain

た、、たしかにシンプルで男部屋だけども、
カフェじゃないよなぁ…
1%だけでも西海岸風の要素があるというのも解せませんね。。。

くっそ〜〜!!
まだまだ画像解析の道のりは険しいぜっ!!
じゃぁみんな、また来週!

(120万枚以上の実際に人が住んでいる部屋写真を有するRoomClipを運営するTunnel社は、積極的にエンジニアの採用を行っております。ご興味ある方は下記リンクまたは、このブログのどこにでもよいので痕跡を残していただければ反応いたします。)

www.wantedly.com

S3のログをLambdaを使ってEC2を踏み台にしてRedshiftへ送る - 日報 #158

前回の記事から数日経って。
やっぱしLambdaのIPを毎回Redshiftに刺すなんて、危険すぎて微妙だったというオチでした。
というのも、やっぱりRevokeのタイミングが難しく、結構securityの項目に残ってしまったりしちゃって、
結局 0.0.0.0/0 を通すよりほか安定しないという感じでした。
いやはや、マズイです。
やっぱりLambdaのIPを固定するのが最善策、ということで、
すっっっっごい苦しいけど「ちょっとだけ」EC2を使うことに。
要はTunnel-SSHを使って、ポートフォワーディングでRedshiftを叩く設計に変えました。

詳しくはコチラ。
https://github.com/hirayama

いやー、、、悔しいですが安定しました。
ただ固定のポートフォワーディングではどうやらエラーが起きるようで
(結構意外だったのですが、Lambdaは恐らくリソースが余っていれば同じインスタンス内で複数のラムダプロセスを叩くみたい。というのも、固定ポートにして何回か連打すると「そのポート使ってますけど?」エラーが生じたりしたので。)
毎回フリーポートをスキャンして使うように設計してあります。

うちでのラムダ利用はもっと増やしていきたいのですが、
なんかいいアイディアないものかね。。。

S3のログをLambdaを使ってRedshiftへ送る - 日報 #157

2015年12月19日 (追記) こっちの方法のほうが安定します。

久しぶり世界。

最近KinesisだのLambdaだのもうなんだの、いっぱいAWS界隈で便利げなものが活発化してきて、
僕も乗り遅れんと必死なわけです。
とはいえ、RoomClipとして「便利」でなければ意味もなく、
仕様こそ知っているけど「うーむ」といって放置しておりました。

が、この度そこそこ合理的な理由を持ってLambdaの使いみちについて考えられたので、
さっそく実装してみることに。

テーマは「S3のバケットログをどうやって回収するか」です。 写真投稿アプリにおいて、S3上においてある写真ファイルが最終的に命なわけでして、
ビジネス上もとても大事なリソースとなります。
そのようなファイルの「アクセスログ」を回収する方法について、ちょっと考えてみます。

S3のログはコンソール上から設定できます。
設定するのは超カンタンなんですが、
結果出力されるログファイルが結構厄介な子です。
ログ用のバケットに「すごい任意のタイミングで」ログファイルを置いていくような仕様なんですね。
これを定期的に回収して整形してなんか見やすい場所に放り投げるcron処理のようなものが必要です。

ほいでまぁ、シンプルにRedshiftに全ログ放り投げたいと思うのは自然な考え方なので、
適当なEC2を作ってcronで1日1回、S3バケットをまるっと舐めて、該当日付のログファイルをつなげてCOPYするのかなぁとか思うわけです。
でもこれ、実は結構面倒なんですよね。
というのもS3のログファイルってかなり適当にきってあって(10分毎とかじゃなくて、結構バラバラ)、
そうなるとSDK使ってもピンポイントで「回収しなければならないファイル」をリスト化するのって難儀なんです。
もちろんできなかないけど。
それにcronだと1日1回っていうのもなんだかかったるいし、そもそもこのためにEC2つくるのも微妙だし、 このために他のcron走ってるEC2使ってもいんだけど、やっぱロギングって影響範囲を小さくしたいから、 できるだけ本丸から切り離していたいものですよね。

ということで、Lambdaをつかうというアイディアにたどり着きました。
S3のログバケットにPUTでオブジェクトが生成されたのをトリガに立ち上がるラムダ、
オブジェクトメタデータだけ取得してCOPYコマンドを生成し、Redshiftにクエリを投げ込む。
という流れです。
キモになるのは、Redshiftとの連携のあたり。
Lambdaは固定IPもなければセキュリティグループももってないので、
Redshiftにセキュアに接続しようとすると中々困難です。
この時点で諦めて、COPYコマンドだけはEC2に任せる、とかNAT使うとかも考えられるのですが、
ここまできたらEC2レスでやりきりたいよね。

というと、別の方法論が考えられます。
1つ目は「毎回LambdaのIPを特定して、Redshiftにセキュリティをセットする」です。
もう1つ目はちょいアクロバティックですが、「LambdaがS3のオブジェクトをもってきて、一行ずつKinesisになげる」です。

前者は「複数のLambdaが立ち上がった時、たまたまセキュリティが通らない奴がいる」可能性があり、
後者は「TimeoutせずにKinesisに送り切ることができるか」と「結局同じオブジェクトがS3にできて、それをKinesisがCOPYする」という問題があります。
んー、つまるところLambda使うのスジ悪そう、、、という風に考えました。

ですが、
まぁせっかくだし、Lambdaが固定IPになる可能性とかVPCリソースへのアクセス可能になる話とかもきてるし、
前者のやり方で組むだけ組んでみようと思ったので、組みました。

プログラムフローは下記の通り。

  1. LambdaのIPを http://checkip.amazonaws.com/ から取得する
  2. そのIPをaws-sdkをつかってRedshiftにセキュリティingressさせる
  3. Redshiftに接続し、COPYコマンドを走らす
  4. 接続を解除して、IPをrevokeする

とても簡単です。
ですが、問題は2,4周りです。
LambdaのIPは必ずしも毎回異なるわけではないので、
仮に接続直前でIP許可があっても、別のプロセスでrevokeされてたら接続不可能となってtimeoutします。
なので、IPをrevokeするのはingressしたプロセスだけにして、
AlreadyExistsだったプロセスはrevokeさせないように運用します。
完璧ではないですが、これで大分マシなはずです。

ということで、コードをgitHubにあげてありますので、気になる方はどうぞ。
Node.jsで実装しました。

https://github.com/hirayama/Lambda-S3Logger

なーんかLambda、Kinesisの良い事例ないかなぁ…

あ!
あとチップスとして、S3ログフォーマットが、空白区切りデータのくせに日付表示に空白を含むという半端じゃない困った子ちゃんなので、
Redshiftにぶっこむときそうとう苦しみます。
Dateでソートができないとか、全く意味無いですもんね。
なのでCOPYコマンドで少し工夫する必要があります。

COPY s3log_table
FROM s3://hogehoge/
CREDENTIALS aws_access_key_id={aws_access_key};aws_secret_access_key={aws_secret_access_key}
delimiter ' '
REMOVEQUOTES
COMPUPDATE OFF
MAXERROR 1000
TIMEFORMAT AS '[DD/MON/YYYY:HH24:MI:SS'
REGION '{S3Region}

こんな感じでTIMEFORMATを指定しておくといいです。
味噌は最初のカギカッコw

Slack上で入力したメッセージを元にRedmineのチケットを自動発行させた話 - 日報 #156

今日は平山じゃないよ!
N氏の日報だよ!


この記事を読んで下さっている皆様の会社では、メッセやタスク管理の環境はどのような体制で組まれているでしょうか?

弊社では、メッセでのコミュニケーションをSlackで行い、タスクに関してはRedmineで回しています。 Slack自体はメッセとして非常に使いやすいのですが、使っていく中で問題もありまして、

例えば、
A君がB君に、とあるチャネルで仕事の依頼をし、
その直後にチャネル内でのやりとりが盛り上がって、200発くらいメッセージがPOSTされてしまったとしましょう。

結果、
A君がB君に依頼したという事を忘れたり見落としたりするという事が起こるかもしれませんし、わりとあるあるだと思います。

この問題は、A君とB君の間における管理体制が良くないというのもありますし、
そもそもフローする事が前提な場所で必ず実行しないといけないような依頼をするのも危なっかしい気もします。
そして、この手の問題が頻発するというのはお互いにとって辛い状態だと思います。

そういった問題を解決するには、

  • Slackでメッセをしている時に依頼内容が発生した時に、
  • 必ず忘れないようにタスク化される

という状態が望ましいのですが、これを自動で行う方法なんて無いんじゃないかと思ってましたが、 Slackのインテグレーション機能の「Outgoing Webhook」というのを使うと実現できそうという事がわかりました。

ですので今回は、Slack上で入力したメッセージを元にRedmineのチケットを自動発行させた話 について説明していきたいと思います。


事前準備
RedmineREST APIキーを取得しておく(省略)

手順

  1. Outgoing WebhookにAPIを登録
  2. RedmineにチケットをPOSTするAPIを作る
  3. 実際にtrigger_wordを打ち込んでみる

1. Outgoing WebhookにAPIを登録

f:id:tom_rc:20151112180429p:plain ・Trigger Word(s) Redmineへのチケット投稿であるかを識別するキーワードを入力する。 今回は、Slack上で、redmineと入力したら発火するようにしました。

・URL(s) ここには、Slackが叩くAPIのURLを入力します。 SlackからAPIが叩かれた時のメッセージの情報はPOST形式で送信されます。

2. RedmineにチケットをPOSTするAPIを作る

Outgoing WebhookからAPIが叩かれた時のPOSTデータは以下となりますので、

<?php
Array
(
    [token] => xxxxxxxxxxxxxx
    [team_id] => xxxxx
    [team_domain] => xxxxxx
    [service_id] => 123456
    [channel_id] => xxxxxxx
    [channel_name] => xxxxxxx
    [timestamp] => 1446199916.000004
    [user_id] => U0XXXXXX
    [user_name] => xxxxx.xxxxxx
    [text] => メッセージ本文だよー
    [trigger_word] => redmine
)

その情報を元に受けとなるAPIを作成していきます。 サンプルコードはこんな感じ

<?php
    //
    // Post From slack
    // (Slackから叩かれる)
    //
    public function actionSlack(){
        $postdata = $_POST;

        // SlackのユーザーIDとRedmineのユーザーIDを付き合わせる
        $user_mappings = array(
            'U02HRMCG8' => 1,    // user01 => [redmine user id]
        );

        // チケットのタイトルと本文
        $subject = $postdata['user_name']. 'さんからタスクが発行されました';
        $body = $postdata['text'];

        // 誰にチケットをアサインするか
        $assigned_to_id = 0;
        foreach($user_mappings as $slack_user_id => $user_id){
            if(strstr($body, $slack_user_id)){
                $assigned_to_id = $user_id;
                break;
            }
        }

        // チケットを所属させるプロジェクトのID
        $project_id = 23;

        // RedmineのREST APIにPOSTするデータ
        $data = array(
            'issue' => array(
                'project_id' => $project_id,
                'tracker_id' => 3,
                'priority_id' => 2,
                'assigned_to_id' => $assigned_to_id,
                'subject' => $subject,
                'description' => $body,
                'start_date' => date('Y-m-d'),
                'due_date' => date('Y-m-d')
            )
        );
        // チケット投稿用URL
        $planio_url = 'https://your.redmine.host/issues.json?key=';

        // REST APIのキー
        $post_url = $planio_url. 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

        // Redmineにチケットを登録する
        $result = $this->post_to_redmine($post_url, $data);

        // レスポンスデータからチケットIDを取り出す
        // (trigger_wordをレスポンスに入れてしまうとSlack上で無限ループするので注意)
        $result = array();
        $result_array = json_decode($result, TRUE);
        if(isset($result_array['issue'])){
            $issue_url = 'https://your.redmine.host/issues/'. $result_array['issue']['id']; 
            $postdata['text'] = "以下のチケットが発行されました\n". $issue_url;
            $result = $postdata;
        }
        header('Content-type: application/json');
        echo json_encode($result);
    }
    //
    // Redmineにチケットを投稿する
    //
    private function post_to_redmine($url, $data){
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        $query = http_build_query($data);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
        
        $response = curl_exec($ch);
        
        if(!$response) {
            return false;
        }else{
            return $response;
        }
    }

3. 実際にtrigger_wordを打ち込んでみる

f:id:tom_rc:20151112180415p:plain

少し遅れてoutgoing-webhookからお返事が届きます。

この仕組み、別にチケット登録に限った事じゃなくて、もっと色々な事に活用できそう。

GoogleAnalyticsをアドブロックされた時の対策例 - 日報 #155

iOS9.0からSafariでアドブロック機能が追加されたとかで、
各所盛り上がりを見せております。

特に問題なのはGoogleAnalyticsによる計測までブロックされている!という衝撃的事件でございまして、
広告以外の分野まで飛び火して大盛り上がりの様子です。
AdBlockの検知jsなどが公開されて、今はいったん沈静化したようですが、
「確認しといて」と言われっぱなされた一部のエンジニアは猛烈にアンニュイな気持ちになっていることでしょう。

ということで、 少し冷静にその辺を整理してみたいと思います。

今回の騒ぎはSafariの「Content Blocking Safari Extensions」というエクステンション機能が公開されたことに端を発しています。
正式なドキュメントはこちら。

https://developer.apple.com/library/prerelease/ios/releasenotes/General/WhatsNewInSafari/Articles/Safari_9.html

これ自体はなんのことはない、chromeでもありそうな(多分あると思います)機能でして、 要は「ある条件をトリガーに、ブロッキングするような動作をする」という条件をjsonにして登録できる機能なんですね。

詳しくはこの辺をご参照。 http://www.toyship.org/archives/2182

んで、このある条件をトリガーというところは、
例えば「特定の名前のクラス名のDOM要素」とか「特定のドメインのjsロード」とか、 まぁそういうことを指定できるわけです。
ほいで、「ブロッキングするような動作」というのは、
「DOM要素を削除する」とか「ロードを禁止する」とかそういう動作のようです。

この機能を使えば、「google-analytics.comというドメインのjsロード」を条件に、
「ロードを禁止する」というブロッキング動作を規定できるようになります。
これは何もiPhoneだけの話ではなく、自分でも簡単にSafariから設定できます。

で、なんと新しいiOS9.0からサードパーティのアプリでadBlockList.jsonを設定して、 iPhoneSafari設定からそのリストをロードできるようになってるっぽいんですね。
まつまり、Crystalのようなad blockアプリをインストールすると、
(恐らく)最新版のリストjsonをアプリがセットして、
それをもとにSafariブロッキング動作をする、
という流れのようです。
OSSでのblockerList.jsonは例えばこんな感じ。
https://github.com/krishkumar/BlockParty/blob/master/RediffBlock/blockerList.json
ちゃんとgoogle-analytics.comがurl-filterにしていされており、action.type = 'block'となっています。

この状態でGA計測のサイトにアクセスするとSafariのコンソールに、

Content blocker prevented frame displaying http://hogehoge.com/ from loading a resource from http://www.google-analytics.com/ga.js

こんなようなログが吐き出されているのがわかります。
恐らく

<script src="http://google-analytics.com/ga.js">

が引っかかったんでしょう。
なるほど、よくわかりましたと。

さて、ここまでわかれば色々と対策を打てそうなものですね。

対策1 GoogleAnalyticsをブロックさせない追記:下記の手法では厳しいです…ごめんなさい。

追記! 下記の方法ではgoogle-analytics.comへの全ての通信を代替できないので、ブロックさせないのは無理でした。申し訳ありません…
下記のようなアイディアとしてgoogle-analytics.comの手前に自前のproxyを置くなどが考えられます。

Googleのポリシー上OKならこのやり方がベストなんじゃないかなぁと思いつつ、
これはやっていいのかどうなのか不明なので、
とりあえずやり方だけ書いて置きます。

単純にgoogle-analytics.comがフィルタ対象なのであれば、
別のドメインからga.jsを読めばいいだけの話です。

<script type='text/javascript'>
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXXX-X']);
  _gaq.push(['_setDomainName', 'hoge.hoge']);
  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    var protocol = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www');
    var src = protocol + '.google-analytics.com/ga.js';
    ga.src = '/ga_js?p=' + encodeURI(protocol);
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>

ここで /ga_js というページを自身のドメインに用意して、

<?php
  $p = isset($_GET['p']) ? $_GET['p'] : 'http://www';
  $base_analytics_uri = '.google-analytics.com/ga.js';
  $url = $p . $base_analytics_uri;
  echo file_get_contents($url);
?>

こんな感じで google-analytics.com/ga.js を読み込んで返してあげれば、
ブロックされないでしょう。(1回だけテストしたらブロックされませんでした)
(ですが、ずーっとこのやり方でいけるのかは分かりません。)

これがOKなやり方なのか非常に疑問なので、まだ実装はしていませんが、
なんか、、、大丈夫だと自信を持って言える方をお待ちしております。

対策2 対策しないけど、blockを計測する

被害の規模で行動を決めるというは当然のことですね。
やっぱりまだまだAdBlockアプリは有料も多いし皆そんなにいれてないだろう、
とかいう予想が本当なのかチェックしましょう。

とうことで、
http://www.google-analytics.com/ga.js
を読み込みます。
http://jsbeautifier.org/
上記サイトで難読化されたga.jsを綺麗にして眺めますと、

// 2153行目あたり
var Z = new Y; 

// 850行目あたり
Y = function() {
  T(Y.prototype,"push", Y.prototype.push, 5);
  T(Y.prototype, "_getPlugin", Pc, 121);
  T(Y.prototype, "_createAsyncTracker", Y.prototype.Sa, 33);
  T(Y.prototype, "_getAsyncTracker", Y.prototype.Ta, 34);
  this.I = new nf;
  this.eb = []
 };

// 83行目あたり
var nf = function() {
  this.prefix = "ga.";
  this.values = {}
};

// 729行目あたり
var W = window,

// 2154行目あたり
    (function(a) {
        if (!Fe(a)) {
            H(123);
            var b = !1,
                c = function() {
                    if (!b && Fe(a)) {
                        b = !0;
                        var d = J, 
                            e = c; 
                        d.removeEventListener ? d.removeEventListener("visibilitychange", e, !1) : d.detachEvent && d.detachEvent("onvisibilitychange", e)
                    }    
                };   
            Ga(J, "visibilitychange", c)
        }    
    })(function() {
        var a = W._gaq,
            b = !1;
        if (a && Ba(a.push) && (b = "[object Array]" == Object.prototype.toString.call(Object(a)), !b)) {
            Z = a; 
            return
        }    
        W._gaq = Z; 
        // ここ!!! console.log(W._gaq.I.prefix);
        b && Z.push.apply(Z, a)
    });

上記をよく読めばわかりますが、
まず 2153行目あたりで、変数ZにYをnewしています。
Yのオブジェクトは、850行目あたりで、this.Iにnfというオブジェクトをnewしています。
nfオブジェクトは83行目あたりで、

nf.prefix = 'ga.';

しています。
ほいで、2154行目あたりにある即時関数で、
window._gaqにZを代入しています。

つまり、window._gaqの要素名Iにおいて、prefix='ga.'が代入されているはずです。
これが入っていればきっと正しくga.jsが起動していると思われます。
(まぁ仕様変更されたら辛いですが)

なので、
各ページのjsにおいて、

$(document).ready(function() {

  // 例えばこんな感じ。
  // 例えば1秒後にwindow._gaq.I.prefixが格納されていればgoogle-analytics.com/ga.jsはブロックされなかったみたいな
  setTimeout(function () {
    if (window._gaq != undefined) {
      if (window._gaq.I != undefined) {
        if (window._gaq.I.prefix != 'ga.') {
          // ここでロードされているか判定
        }
      }
    }
  }, 1000);

}

こんなようなことをしておけば、「こいつアナリティクスブロックしてきた」とわかります。
これをカウンティングしておけば、大体のPVに対してどの程度ロストしたのかを測ることができると思います。

以上ですが、
google-analytics.comをフィルタしてくるってどういう意図なんでしょうかね。
ちょっとそこは詳しく聞いてみたいところです。

さて、日々生じる外敵脅威と闘いながらも、
常に対策し切る頑強なエンジニアを弊社は募集しております。
なにとぞ、よろしくお願いいたします。

www.wantedly.com
www.wantedly.com

Codeigniterで複数アプリケーション - 日報 #154

今日はサクッと。

PHPフレームワークベンチマークを久々調べてみましたら、
http://blog.a-way-out.net/blog/2015/03/26/php-framework-benchmark/
こんな素晴らしいサイトが。

CodeIgniterは非常に優秀な成績を納めておりまして、
嬉しい限りです。
Laravelのような新しいフレームワークも触ってみたいですが、
あくまで軽さ重視でいきたい今日此の頃です。

さて、
CodeIgniterのコア周りで僕らが加えた大きな変更について今日は少し触れたいと思います。
CIで真っ先に困ったのは、以下2点です。

  1. 複数アプリケーションを作り、ライブラリなどは共有したい
  2. ユニットテストに対応したい

2番に関しては、言わずと知れたkenjiさん(http://d.hatena.ne.jp/Kenji_s/20110822/1314003400)の記事が有名ですが、
1番に関してはモジュールを使うとか、なんかあまり歯切れのいいものになっていない感じがします。
例えばAPIとWEBでCodeIgniterのコアやライブラリは共有したいけど、
コントローラーとビューは別にしたい、なんてことがあると思います。
その場合、どうすりゃいいのか?

結論から言うと、
applicationのlibray,model,helper,config,coreのロードの先に、 commonのようなパスを追加していけばいいという感じです。
CI_Loaderを直接いじると面倒なので、extendして使用します。
public/index.phpに対して

define('COMMONPATH', (dirname(__FILE__) . '/../../hoge/common/'));
  

のように例えばCOMMONPATHを2階層上につくっておき、
その配下にcommon/application/libraryのようなものを作成しておく。
さらに、Codeigniter.phpもcommon/application/system/coreとかに置いておき、
ど頭あたりで、

    if (defined('COMMONPATH')) {
        $base_paths[] = COMMONPATH. 'application/';
    }

のよーにcommon配下のアプリケーションをbase_pathとして追加しておく。
こうすることで、Codeigniter.phpがcommon配下のcoreディレクトリなどを参照してくれるようになる。

この時点でCodeigniter.phpをいじるという大罪を覚悟せねばならないわけですが、
まぁしゃぁなし。

ほんで、このおかげでcommon/application/coreの配下に例えばRC_Loader.phpなるクラスファイルを作成し、
CI_Loaderをextendしておけば、とりあえずCI_Loaderを拡張することができるわけだ。

最後の仕上げに、RC_Loaderにて、

    public function __construct() {
        parent::__construct ();
        $this->_ci_library_paths [] = COMMONPATH . 'application/';
        $this->_ci_helper_paths [] = COMMONPATH . 'application/';
        $this->_ci_model_paths [] = COMMONPATH . 'application/';
        $CI =& get_instance();
        $CI->config->_config_paths[] = COMMONPATH . 'application/';
    }

こんな感じでCOMMONPATH配下のapplicationを参照するように追加しておく。
(ちなみに各loadメソッド、例えばlibraryなどのロードクラスにおいてsubclass_prefixがついている状態でcommonからロードするように一部書き換えは必要。なのでRC_Loaderでhelperメソッドやらlibraryメソッドをオーバーライドする必要が厳密にはあります。)

    public function helper($helpers = array())
    {
            // ...
            // Try to load the helper
            foreach ($this->_ci_helper_paths as $path)
            {
                $helper_file_path_list = array(
                    $path.'helpers/'.$helper.'.php',
                    $path.'helpers/'.config_item('subclass_prefix'). $helper.'.php'
                );
                foreach($helper_file_path_list as $helper_file_path){
                    if (file_exists($helper_file_path))
                    {
                        include_once($helper_file_path);

                        $this->_ci_helpers[$helper] = TRUE;
                        log_message('debug', 'Helper loaded: '.$helper);
                        break;
                    }
                }
            }
    }

例えばこんな上記の様な感じ。
こうしておくと、load先がcommon配下をみてくれるようになるので、
あとはどっからでもcodeigniterっぽくフレームワークを開始するけど、library,helper,model,coreなどを共有してコントローラーとビューだけかけばよい、というハッピーな状況にもっていけます。

まぁ色んなやり方があると思いますが、取り急ぎこうすれば複数アプリいけまっせという、
小ネタの話でした。

RoomClipはPHP(欲を言えばとくにCodeigniterを使える)エンジニアを募集しております!
PHPフレームワークにつかれた方、本気で魔改造してみませんか? www.wantedly.com
www.wantedly.com

ベイズの定理の簡単な説明 - 日報 #153

機械学習周りの調査をしていると、
ベイズ推定の話がしょっちゅうでてきます。
(と同時に最尤法の話もでてきます。)
前職時代にMCMCを使ってガリガリベイジアン推定をしていたこともあり、
この手の話には馴染みがあるのですが、
しっかりと整理したことが実はなかったと思いだしたので、
改めてここでこの辺の話をまとめてみます。

まず、「何に興味をもてばいいのか」を明確にしないと、
すごいごちゃごちゃしちゃう話だと思うので、そこだけ最初に明言します。
よく数学の問題などで、
「コインをなげて最初に表がでて、次に裏が出る確率はいくつか?」
みたいな話があります。
賢明な人ならば、最初に表が出る確率は1/2で、続いて裏がでるのは1/2だから、

1/2 ✕ 1/2 = 1/4

と計算したり、
はたまた、コインを二回投げてでる「場合の数」を全て列挙し、

表表
表裏
裏表
裏裏

の4つの場合の数うち、表裏は1個なので、 1/4 と結論を出したりします。
ところがこの考え方、両方共ある数学特有の前提にのっとって考えています。
それは、「表も裏も出る確率は同じ、1/2」という前提です。
コインに細工がない、みたいな表現で簡易にこの前提を表現したりしています。

これ、現実的にどうなんでしょうかね。
例えば人情として「表表表表表裏表表表」みたいな結果を観測したら、
「ゆがんでんじゃね?このコイン」
って思いますよね。
これを「主観でしょ。無限回やれば偏りは消えるから」って考えるのも結構なことですが、
逆に「コインに歪みがあるかどうかを検定してくれ」って言われている職場の人だったら、
自信をもってそんなこといえませんよね。
ゆがんでんじゃねぇの?ってのは確かに主観ですが、すごい妥当な推測でもあるわけです。

これを考えるのが、ベイズであります。
「何回か観測したら、事前に想定してた確率(この場合表が出る確率1/2)が変化している」という現象を数式に表すということです。
これが僕らがもっとも興味を持っている対象です。

んなことできんの?って思いますが、できるからすげぇ話なんですね。

ではまず、簡単にベイズの式を導出するところから始めます。
確率の話は、基本「場合の数」で考えるとすごい話が楽になるので、
まずそれで考えていきます。

今すべての事象を列挙したらW個ある試行があったとします。
その内、aという結果がでるのがA個、
bという結果がでるのがB個あったとします。
また、aでかつb(a∩b)という結果がでるのがC個あるとします。
この時、それぞれの確率は、

P(a) = A/W
P(b) = B/W
P(a∩b) = C/W

超簡単です。
では続いて、bである前提で、aである確率は?
つまり世界がまずb(B個)になって、さらにaとなる(A∩B個=C個)確率をP(a|b)とかくとそれは、

P(a | b) = C/B

とかけます。
同様に、

P(b | a) = C/A
⇔ C = A ✕ P(b | a)

とかけます。
すると上の二式からCを消すと、

P(a | b) = A ✕ P(b | a) / B
⇔ P(a | b) = A/W ✕ P(b | a) / B/W
⇔ P(a | b) = P(a) ✕ P(b | a) / P(b)

となります。
この

P(a | b) = P(a) ✕ P(b | a) / P(b)

これがベイズの式、と言われます。
ものっっっすごい簡単な式変換でもとまるんですね。
んで、これが超インパクトのある式なわけです。

コインの話にもどりましょう。
いま、コインの表が出る確率がxだと仮定しましょう。
すると必然的に裏の出る確率は1-xになります。

P(表) = x
P(裏) = 1-x

その前提で、2回トスしたとき、[表裏]という結果になったとします。
(結果には[ ]をつけていきます)
このとき、

P( [表裏] ) = x(1-x)

ですね。
ただし、これは不十分な表現です。
あくまで、表が出る確率をxとした前提での確率なので、

P( [表裏] | 表が出る確率=x ) = x(1-x)

と書くのが厳密には正しいです。
この「表がでる確率=x」は長いので「B=x」と書くこととします。
すると

P( [表裏] | B=x ) = x(1-x)

こうかけますね。
これは観測の結果です。
僕らがいま最も関心があるのは、
xの値として「もっとも妥当性があるのはいくつか?」です。
これをもっと確率の問題っぽくいえば、
「コインを2回トスして、[表裏]という観測をしたあとに、そもそもコインの表がでる確率はいくつが妥当だと考えればよいのか?」
ということです。
直感的には「そりゃ表裏と平等にでたんだから、今のところコインはゆがんでなさそうだ。表が出る確率として妥当性が高いのは、1/2でしょう」と思うところです。
これを計算してみましょう。

僕らが知りたいのは [表裏]を観測した前提で、B=xとなる確率です。
つまり

P ( B=x | [表裏] )

を知りたいわけです。
ここでベイズの式が出てくるわけですね。

P(a | b) = P(a) ✕ P(b | a) / P(b)

これは P ( a | b ) という事前にbが起きたあとで a が起こる確率を、
その逆にあたる P ( b | a ) で表現できている、というのが味噌です。

僕らは

P( [表裏] | B=x ) = x(1-x)

これを知っていて、

P ( B=x | [表裏] )

これを求めたい。
([表裏]を観測した世界で、表が出る確率がxとなる確率のこと)
ベイズの式を使えば、二番目の式は、一番目の式で表現できそうです。
やってみましょう。

P ( B=x | [表裏] ) = P( B=x ) ✕ P( [表裏] | B=x ) / P( [表裏] )
⇔ P ( B=x | [表裏] ) = x(1-x) ✕ P( B=x ) / P( [表裏] )

さて、困ったことが2つあります。
P( B=x ) と
P( [表裏] ) を求めないといけないのです。
でも後者はすごく楽です。
これは「コインがゆがんでようとなかろうと、関係なく、[表裏]がでる確率」のことを言っているので、
単純に場合の数から計算して1/4となります。
というかもうこれは絶対に定数になるのでαとかおいちゃいましょう。
問題は前者です。
P( B=x )
これの意味は「B=xとなる確率」をいってます。
もっと言うと、
「表がでる確率がxとなる確率」ということです。
恐怖ですね。何言ってっか分かりません。
xに適当な数値をいれてみます。

「表がでる確率が1となる確率」=「毎回表になる歪みまくってるコインである確率」
「表がでる確率が1/2となる確率」=「なんの歪みもないコインである確率」
「表が出る確率が0となる確率」=「毎回裏がでるように完全に歪んだコインである確率」

こんな設定どこにもしてません。
普通に誠意ある人が作ったコインなら、何の歪みも無いコインである確率が90%くらいで、
たまたまミスって表しかでない、裏しかでないようなコインになっちゃうのが1%くらいで、、、
と主観で決めるしかありません。
これがベイズ推定の弱点です。
ここがまったくもって主観なんですね。
今回はたまたま「どんなコインに仕上がるかはすべて同確率=βとする」としちゃいましょう。
つまり、マジで適当な奴が作ったコインなんで、どんなコインに仕上がるかすべて同確率ということです。

こうすると

P( B=x ) = β

とxによらない値(定数)になるので、これでグッと楽になりました。
(もっとちゃんとしたい人は確率密度関数を参照)
ベイズの式に戻ると、

P( B=x | [表裏] ) = x(1-x) ✕ P( B=x ) / P( [表裏] )
P( B=x | [表裏] ) = x(1-x) ✕ β/α P( B=x | [表裏] ) = x(1-x) ✕ η

となるわけです。
これは嬉しい式が求まりました。
左辺 P( B=x | [表裏] ) とは「[表裏]を観測したあとに、B=xである確率」を示しており、
右辺ではそれが x(1-x) ✕ η となると言ってるわけです。
この右辺は x = 1/2 で極大値を取ります。
よって、 B=1/2 となる確率がもっとも大きい、すなわち、
「表が出る確率が1/2となるのがもっともらしい!」
といえるわけです。
[表裏]という事実を観測したので、このコインは表が出る確率が1/2っぽいな。と思う。
直感に沿ってますね。

では、話を極端な方に持って行きましょう。
続いてまたコインをトスしたら今度は[表]となりました。
つまり全部で[表裏表]となったんです。

いま、同様に計算すると、

P( B=x | [表裏表] ) = x^2(1-x) ✕ P( B=x ) / P( [表裏表] )

となります。
ここでベイジアン推定という極めて突飛な方法が飛び出ます。
この右辺のP( B=x ) を前回と同じように「全て同じ値β」とはみなさないのです。
なぜなら、すでに僕らはP( B=x )についての妥当な推測 P( B=x | [表裏] ) = x(1-x) ✕ η を知っているからです。
そもそも主観で決めていたP( B=x )なのだから、より妥当な推測値を当てはめることに何の抵抗もありません。
よって、P( B=x )に x(1-x) ✕ ηを代入します。

P( B=x | [表裏表] ) = x^2(1-x) ✕ x(1-x) ✕ η / P( [表裏表] )
P( B=x | [表裏表] ) = x^3(1-x)^2 ✕ θ

これは x = 0.6 くらいで極大になります。 ( 0 < x < 1 の範囲で )
つまり、表が出る確率が少し高いコインだ、という意識に「更新」されたわけです。

もっと行きましょう。
さらにもう一回コイントスをしたらまた[表]がでたとします。

P( B=x | [表裏表表] ) = x^3(1-x) ✕ P( B=x ) / P( [表裏表表] )

P( B=x )としてさっき学んだ、P( B=x | [表裏表] ) = x3(1-x)2 ✕ θ を代入します。

P( B=x | [表裏表表] ) = x^3(1-x)^2 ✕ x^3(1-x)^2 ✕ γ
P( B=x | [表裏表表] ) = x^6(1-x)^4 ✕ γ

これの極大値は大体 x = 0.67 くらいになります。
さらに表が出る確率が少し高いコインだ、という意識が濃くなりました。
さらに表がでたら、、、とどんどん「更新」を繰り返していくことで、
このコインが「表ばっかりでるやつなのか。そうでないのか。」という検証に定量的評価を与えることができるようになります。

最後に用語のはなしですが、
この P(B=x)のことを事前確率とよび、
P( B=x | [表裏表表] ) のことを事後確率と呼びます。
ベイズの考え方は、
事前確率を事後確率で更新していく、
ということを表現しています。
もっといえば、事後確率によって、主観的であった事前確率を妥当なものに更新していく、と言った感じでしょうか。
式をみてもわかるように、 P(B=x)にある値をかけた値を事後確率とよんでいます。
(ちなみにこのある値のことを尤度などと呼びますが、もう面倒くせぇので説明しねぇ)

あぁなんか疲れた。
なんでこんな話したのか忘れたけど、
要は「そんな難しい話じゃねぇよ。計算がちょう面倒なだけで」ってことをいいたかったんです。
そんな俺と一緒に働いてくださるイケメンを目下大募集中ですよ!

www.wantedly.com
www.wantedly.com