ターミナルからGithubページをシュッと開けるコマンドを実装した

仕事でもプライベートでもコードの管理にGithubを使っています。開発のフローとしては、

  • コードを書く
  • リモートにpushする
  • Pull Requestを立てる
  • レビューをもらう
  • masterへmerge

となるわけですが、Pull Requestを立てるの部分で、いつもChromeを開いて該当のリポジトリを開いてポチポチポチポチとやってると疲れてきてしまうんですね。リモートにpushするの時点でターミナルにいるんだから、このままPR立てるページまで開いて欲しいなあ。。と思ったので、shuttoというコマンドを実装しました。

$ git commit -m "頑張った"
$ git push -u origin hoge-branch
$ shutto pr

使い方は、上記な感じです。サブコマンドであるprを渡してやるとカレントブランチのPR作成ページへ飛ばしてくれます。既にこのブランチでPRが作成済みの場合は、作成されているPRへリダイレクトするようになっています。

GithubページのPull RequestsからどのPRだっけな〜なんて一個ずつ探す必要もなくなって、めっちゃ快適です。 今の所は、カレントブランチのみ対応なので、ブランチで指定できるようにするのが次のステップかなと考えています。シンプルですが痒いところを掻いてくれるそんなコマンドなので、ぜひ〜

github.com

SlackのBotをGKEを使ってデプロイする

先日、Rubotyをk8sで動かす所までやりました。次のステップとしてGoogle Kubernetes Engine(GKE)を使ってクラスタを構築し、ボットをデプロイします。 Google Cloud SDKの使用は初めてでしたのでセットアップから始めました。

ダッシュボートからk8sクラスタGUIで作成します。クラスタ作成には設定値がたくさんありましたが、今回は特にいじらず作成しました。 f:id:keisuke_t:20181209190851p:plain

クラスタをshellからいじるために、接続します。

$ gcloud container clusters get-credentials bot --zone asia-northeast1-a --project your-cluster-name

試しに、podsを表示させようとしますが、まだ何もしてないので表示されません。

$ kubectl get pods
No resources found.

今回、デプロイするのは前回記事で構築したRubotyボットです。前回はDocker hubにimageを置きましたが、今回はGoogle Container Registryへimageをpushし直しました。

KubernetesでRubotyを動かして自動復旧を味わう - keisuke-tの日記

$ gcloud docker -- push asia.gcr.io/your-cluster-name/ruboty:latest

同時に、k8sマニフェストファイルも書き換えています。今回は、Deploymentを定義しています。 Deploymentはアプリケーションデプロイの基本単位となるリソースで、ResplicaSetは同じ仕様のPodのレプリカ数を管理、制御するリソースでしたがDeployementはReplicaSetを管理、制御するリソースです。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ruboty
  labels:
    app: ruboty
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ruboty
  template:
    metadata:
      labels:
        app: ruboty
    spec:
      containers:
        - name: ruboty
          image: asia.gcr.io/kube-test-224908/ruboty:latest
          imagePullPolicy: IfNotPresent

準備はできたので、podsを立ち上げます。

$ kubectl apply -f bot_deploy.yaml
kubectl get deployments
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
ruboty    1         1         1            1           7h
kubectl get pods --watch
NAME                      READY     STATUS    RESTARTS   AGE
ruboty-586bd8888-lg7fg   1/1       Running   0          7h

大成功です。 botも生きてますね。 f:id:keisuke_t:20181209213957p:plain

podsを削除しようとしても、自動復旧で再起動するので落とす時はDeployementごと落としましょう。

$ kubectl delete -f bot_deploy.yml

最後に

デプロイまでのフローがこんなにシンプルだとは思いませんでした。GCPダッシュボードから確認できるノードやポッドの状態もとても分かりやすいですし、この記事では書いてませんが、ポッドのスケーリングやデプロイ時のローリングアップデートも魅力的なので次回は、ウェブサービスのデプロイ、運用に使ってみたいと思いました。

github.com

KubernetesでRubotyを動かして自動復旧を味わう

Kubernetes(以降k8s)の勉強をしたかったので、Rubotyが動くように試行錯誤して構成を整えてみました。 k8sで特に気になっていたのがPodの自動復旧で、障害発生で止まってしまった時にどのように振る舞うのかを手元で実験しました!

Dockerfileの構成はシンプルで、Dockerfileを以下のように定義してDockerhubにあげています。 手元においたファイルをワーキングディレクトリに突っ込んで、bundle exec ruboty --dotenvでrubotyを実行しています。

FROM ruby:2.3.0-alpine

RUN apk update && \
    apk add build-base openssl

RUN mkdir /ruboty
ADD Gemfile /ruboty
ADD Gemfile.lock /ruboty
ADD .env /ruboty
WORKDIR /ruboty

RUN bundle install --path vendor/bundle

CMD ["bundle", "exec", "ruboty", "--dotenv"]

今回は、自動復旧の様子が見たいので、ReplicaSetを使ってyamlを定義しています。 ReplicaSetではテンプレートに従ったPod が、どんな時でも正しい数で動作するよう調整してくれるので、今回の実験にベストフィットです。

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: ruboty
  labels:
    app: ruboty
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ruboty
  template:
    metadata:
      labels:
        app: ruboty
    spec:
      containers:
        - name: ruboty
          image: "keisuketsukamoto/ruboty:v1"
          imagePullPolicy: IfNotPresent

replicas: 1で定義されているように常に1Pod動作してほしいのでこのPodが意図的にdeleteされた場合であっても自動復旧してくれることが予想できます。

$ kubectl apply -f rs.yaml

で構成展開します。

$ kubectl get pods
NAME           READY     STATUS    RESTARTS   AGE
ruboty-cx6qc   1/1       Running   0          9s

Podが立ち上がりました。nameの後ろの値は自動で振り分けられたハッシュ値です。 ruboty自体もちゃんと起動しています。

f:id:keisuke_t:20181201193340p:plain

今回、実験したかったのは不具合で落ちてしまった場合の自動復旧なので、ここでおもむろにPodを削除します。

$ kubectl delete pod ruboty-cx6qc

そして、Podsを表示

$ kubectl get pods
NAME           READY     STATUS              RESTARTS   AGE
ruboty-cx6qc   1/1       Terminating         0          28s
ruboty-qblv9   0/1       ContainerCreating   0          3s

削除されたPodsはStatusがTerminatingになりましたが、ReplicaSetでreplicas: 1としているので1Podが常に動作している状態に戻すために復旧が始まります。

$ kubectl get pods
NAME           READY     STATUS    RESTARTS   AGE
ruboty-qblv9   1/1       Running   0          18s

f:id:keisuke_t:20181201193451p:plain

これは、Podのオートヒーリング機能によるもので、ReplicationManagerがPodの状態を監視して、実際に稼働しているPodとマニフェストで定義したreplicasに差異が出た場合、Podの数を調整してくれるものです。

まとめ

ウェブエンジニアがインフラっぽいことに少しでも手が出せるようなるのでとても興味深い技術ですし、これからスタンダードになっていくので大注目してます。 今回のコードはgithubに公開しています。

github.com

参考

以下のブログを参考にさせていただきました!とても助かりました!

dr-asa.hatenablog.com

repl.info

シンプルにレコメンデーションを実装する

初めに、ここで実装したレコメンデーションはとてもシンプルな実装で、もっと優れた手法は世の中たくさんあるはずなので、ご了承ください。

レコメンデーション

僕はNetflixの「Narcos」というドラマが好きなのですが、見ているうちに「Narcosを見たあなたにオススメな一覧」として怖そうな麻薬戦争物がずらりと並びます。これがレコメンデーションですね。レコメンデーションとはいわゆる「類似度」というやつで、Narcosが好きな人はこんな映画、ドラマも好きですよとオススメしてくれるシステムのことです。

仕組み

ピアソン相関という数式を用いて、二つのデータの関係性を数値化して、-1(相関が薄い)から+1(相関が強い)を返します。 相関が強ければ強いほどレコメンドできるという訳ですね。 数式をみると、見た目とてもしんどそうなのですが、

f:id:keisuke_t:20181111171121g:plain

実態はとてもシンプルで

p = (共分散) / (1つ目のデータの標準偏差) * (2つ目のデータの標準偏差)

というだけです。Pythonで書くと

def pearson(d1, d2):
    d1_cv = d1 - d1.mean()
    d2_cv = d2 - d2.mean()
    return np.sum(d1_cv * d2_cv) / np.sqrt(np.sum(d1_cv ** 2) * np.sum(d2_cv ** 2))

pearson関数にデータを渡すことで、渡したデータの相関を-1から+1で返してくれます。

実験

映画のレコメンデーションシステムを実装するために、706作品の映画データとそれぞれの映画をユーザーがレーティングしたデータ8552件をダウンロードしたので、pandasのpivot_table関数を使って、columnsに映画IDをindexにユーザーIDをとり、ratingsをvalueとしてセットした新しいテーブルデータを作成しました。

import pandas as pd

ratings = pd.read_csv('ratings.csv')
ratings.drop(['timestamp'], axis=1, inplace=True)
table = ratings.pivot_table(index=['userId'], columns=['movieId'], values='rating')

試しに、ToyStoryとジュマンジにどれくらいの相関があるかを調べると、以下の値が返されました。

0.20285118554433546

ジャンルがわりと似ていたのでもっと高いかと思ったんですが、値的にはやや相関がある程度でしたね。

ToyStoryとNow and thenを比べると、、、

-0.0045909827988617324

マイナスの値になったので、相関は低いことがわかります。

作ったテーブルを元に、movie_idから相関性の高い映画をレコメンドしてくれる関数を実装しました。 受け取ったmovie_idとそれぞれのmovie_idとの相関性を計算したデータをrecommendation_ratingsに配列のtupleとして保存し、うちのトップn件を返しています。

def recommends(movie_id, table, limit):
    recommendation_ratings = []
    for target_id in table.columns:
        if target_id != movie_id:
            correlation = pearson(table[movie_id], table[target_id])
            if np.isnan(correlation):
                continue
            else:
                recommendation_ratings.append((target_id, correlation))
    return sorted(recommendation_ratings, key=lambda x: x[1], reverse=True)[:limit]

実際に、ToyStoryを見た人へおすすめのトップ10を計算させてみると、、、

Finding Nemo (2003)
Monsters, Inc. (2001)
Aladdin (1992)
Toy Story 2 (1999)
Bug's Life, A (1998)
Who Framed Roger Rabbit? (1988)
Honey, I Shrunk the Kids (1989)
Ratatouille (2007)
Beauty and the Beast (1991)
Home Alone (1990)

ピクサーの面々が抽出されました!確かにToyStoryとの相関は高いといえそうです! 今回実装したレコメンデーションはgithubにも公開しているので気になる点があったらぜひ、つっこんでください github.com

参照

相関係数について、勉強になりました! www.sekkachi.com

シンプルにTFIDFを考える

Webチームの同僚がESの社内勉強会で検索の色々を紹介してくれたのですが、「複数のドキュメントがあるときに、このドキュメントを特徴つける単語とは?」を知るためにTFIDFというアルゴリズムが使われていることを知って、どうも気になっていたので色々調べました。

TFIDFとは?

TFIDFは、TF(Term Frequency)とIDF(Inverse Data Frequency)の二つが合わさったアルゴリズムです。

  • TF: それぞれのドキュメント内での単語の出現頻度を表現
  • IDF: それぞれの単語がいくつのドキュメント内で共通して使われているかを表現

TFIDFはそれぞれの積をとることで算出することができます。

難しそうな数式は正直さっぱりなので、数式を日本語におこして解釈すると以下のようにして求めることができそうです。

  • tf = ドキュメントに単語が現れた回数 / ドキュメントの総単語数
  • idf = log(ドキュメントの数 / その単語を含むドキュメント数)
  • tfidf = tf * idf

実際に計算してみる

ここで、例文を使ってどういうこと?を探っていきます。

  • Document A: The rabbit bit my finger.
  • Document B: The dog bit my bacon.

という2つのドキュメントがある時、bitmyみたいな単語は2つのドキュメントで共通に使われている単語なので特に特徴づける単語とはいえないと思いますが、rabbit, dogのような単語は2つのドキュメントを特徴づける単語の一つといえそうです。

DocumentAを例にとって計算をしてみました。

Word TF IDF TFIDF
The 1/7 log(2/2) = 0 0
Rabbit 1/7 log(2/1) = 0.69 0.13
Bit 1/7 log(2/2) = 0 0
My 1/7 log(2/2) = 0 0
Finger 1/7 log(2/1) = 0.69 0.13
Dog 0 log(2/1) = 0.69 0
Bacon 0 log(2/1) = 0.69 0

TFIDFで値が0の単語はDocumentAを特徴づける単語とは確かにいえなそうですが、rabbit, fingerのような単語は0以上の値がセットされているのでDocumentAを確かに特徴づける単語といえそう!という結果になりました。

pythonで実装

Pythonで実装したものをJupyter notebookにしてgithubに公開しています。おかしなところありましたら、レビューお待ちしてます。

https://github.com/garigari-kun/til/blob/master/src/ml/tfidf/tfidf%20notebook.ipynb

github.com