ターミナルから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だっけな〜なんて一個ずつ探す必要もなくなって、めっちゃ快適です。 今の所は、カレントブランチのみ対応なので、ブランチで指定できるようにするのが次のステップかなと考えています。シンプルですが痒いところを掻いてくれるそんなコマンドなので、ぜひ〜
SlackのBotをGKEを使ってデプロイする
先日、Rubotyをk8sで動かす所までやりました。次のステップとしてGoogle Kubernetes Engine(GKE)を使ってクラスタを構築し、ボットをデプロイします。 Google Cloud SDKの使用は初めてでしたのでセットアップから始めました。
ダッシュボートからk8sクラスタをGUIで作成します。クラスタ作成には設定値がたくさんありましたが、今回は特にいじらず作成しました。
クラスタを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も生きてますね。
podsを削除しようとしても、自動復旧で再起動するので落とす時はDeployementごと落としましょう。
$ kubectl delete -f bot_deploy.yml
最後に
デプロイまでのフローがこんなにシンプルだとは思いませんでした。GCPのダッシュボードから確認できるノードやポッドの状態もとても分かりやすいですし、この記事では書いてませんが、ポッドのスケーリングやデプロイ時のローリングアップデートも魅力的なので次回は、ウェブサービスのデプロイ、運用に使ってみたいと思いました。
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自体もちゃんと起動しています。
今回、実験したかったのは不具合で落ちてしまった場合の自動復旧なので、ここでおもむろに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
これは、Podのオートヒーリング機能によるもので、ReplicationManagerがPodの状態を監視して、実際に稼働しているPodとマニフェストで定義したreplicasに差異が出た場合、Podの数を調整してくれるものです。
まとめ
ウェブエンジニアがインフラっぽいことに少しでも手が出せるようなるのでとても興味深い技術ですし、これからスタンダードになっていくので大注目してます。 今回のコードはgithubに公開しています。
参考
以下のブログを参考にさせていただきました!とても助かりました!
シンプルにレコメンデーションを実装する
初めに、ここで実装したレコメンデーションはとてもシンプルな実装で、もっと優れた手法は世の中たくさんあるはずなので、ご了承ください。
レコメンデーション
僕はNetflixの「Narcos」というドラマが好きなのですが、見ているうちに「Narcosを見たあなたにオススメな一覧」として怖そうな麻薬戦争物がずらりと並びます。これがレコメンデーションですね。レコメンデーションとはいわゆる「類似度」というやつで、Narcosが好きな人はこんな映画、ドラマも好きですよとオススメしてくれるシステムのことです。
仕組み
ピアソン相関という数式を用いて、二つのデータの関係性を数値化して、-1(相関が薄い)から+1(相関が強い)を返します。 相関が強ければ強いほどレコメンドできるという訳ですね。 数式をみると、見た目とてもしんどそうなのですが、
実態はとてもシンプルで
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つのドキュメントがある時、bit
やmy
みたいな単語は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