Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

「仕事ではじめる機械学習」を読んだので作者に媚を売る

- はじめに -

以下を読んで、筆者ら (@chezou, @tokoroten, @hagino3000) ともTwitterで相互フォローだし、いっちょ媚び売るために感想記事でも書いとくかみたいな記事。

www.oreilly.co.jp

私は「企業で機械学習プロジェクトをいくつか経験している」「書に載っているアルゴリズムや検定も大体わかる」くらいで本書のターゲットからは少し外れているっぽいのだけれど、知ったことではない。


 

- この本どんな人がターゲット? -

「仕事ではじめる機械学習」というタイトルの通り、「俺は来年から新卒社会人!大学で学んだ知識を活かして機械学習エンジニアとして頑張っていくぞ!」みたいな人が読むとすごく為になる本。
あと、ターゲットとしては「バイトで機械学習経験したい学生」とか「突然上司に機械学習やってくれって言われた!」みたいな人とか。

あと、機械学習を使った時に、プロジェクトの回し方が他の開発とは少し変わってくること、インフラ要求、事業への応え方も結構独特(正確に言うとあまり知られていない)という内容が書かれており、「弊社にもR&Dみたいな機械学習開発やってるあるんだけど、アイツらマジ何考えながらやってんの?」という人もターゲットに入りそう。

機械学習や統計を使って仕事している人の理想が描かれているので、読むと機械学習を仕事でやっている人が実際どうプロジェクトを進めているか、彼らの課題は何かが広く見えてくると思います。


 

- 感想とか -

詳細な内容は@razokuloverが丁寧にまとめてくれてるのでそれで十分だと思います。
razokulover.hateblo.jp

以下はもう自分が気になったこと、考えたことをつらつら書いていくだけです。

 

機械学習を使わない方法を考える」

この書籍では「機械学習を使おうとする前に機械学習を使わない方法を考える」という内容が序盤に数回出てきます。

これは、機械学習や統計を業務で使う上で非常に重要で、エンジニアリング面だけでなくインフラ、アプリ、経営…会社の全ての面に関わってくる非常に大事な文言です。
筆者もTwitterでドヤ顔でネタにしているくらい大事な事です。

これは最終的に機械学習やるやらないに関わらず本当に広まって欲しい内容です。
技術の不安定さや実運用の難しさを丁寧に説いており、現場の機械学習野郎が泣き喚いてるみたいな上司の方は是非本書を熟読して下さい。

 
さて少し話変わって実際問題ですが、無理に機械学習を使うプロジェクトは世の中から減るばかりか増える一方です。実際現場には「それ最頻値、中央値で良くね?」といった状況から、下手すれば「それデータ集めて整理すればif文何個か作ればよくね?」という状況まであります。

書籍に書かれている通り、「機械学習はコストになりやすい技術」です。
それでも使いますか?という疑問符を常に持っておかなければいけません。


私は実際にこういった機械学習コスト度外視プロジェクトが進む主な原因は、現場で機械学習を使わない方法を考えてない」のではなく「機械学習を使わない方法がイマイチわからない」という場合が多いのだろうと考えています ("何でも良いから人工知能使って"と言われたみたいな状況は論外にします)。


例えば、想定として画像認識のタスクをやりたいケースを考えます。
Google先生で「画像認識」で検索するとまずDeep Learningの記事が出てきます。

ここから深みにハマっていくと、やれ「Deepじゃないと出来ないんだ」「複雑な機械学習じゃないと精度の高い物は出来ないんだ」となっていくのではと考えています。実際には解きたい課題はもっと簡単なのに。

まあSNSでもDeepDeep機械学習Deepした案件が拡散されやすいですし、IT企業に属していれば誰しも脳裏に焼き付いてる何かがあると思いますし、心理の働きも多く影響してきますが、一度自分の課題を見返すことが大事です。


機械学習エンジニアの仕事には、こういった場合に「機械学習を使わない方法を提示する事」が入ってくると私は感じています。

例えば「まず色情報だけでヒストグラムを取ると簡易な手法でこれだけ精度が出ます」「既存のパッケージやシステムに割り当てるだけで十分です」「画像をクラウドソーシングに投げてまず運用しましょう」「高度な機械学習は次のフェーズにしましょう」「DeepはAWS上に構築した場合GPU費用もかさむので、独自開発せずGoogleが出してる既存APIを使いましょう」といった説得がここにあたります。

この書籍内では後半に「分析結果を上司に説明する方法」のような項があり、そちらに通ずる部分もありあますが、機械学習エンジニアにとって「機械学習を説明できる事」と同等なレベルで「機械学習を使わない方法を提示できる事」が大事な力であると、この書籍で再認識しました。

上記提案できるためには、インフラの負荷計算やコスパの算出、様々なビジネスの知見、観点、データの可視化などが必要になるため、それらを学ぶ機会があれば大事にしたい所です。


これらは、ちょっとフィルターを通して考えれば、機械学習エンジニア以外でも自分の専門のリスキーさを理解し説得できるという事が働く上で大事な技術という事ではあるのですが、"機械学習エンジニアは特に"という点で書籍内で繰り返し言われている事に良さを感じました。

 

インフラ環境

この書籍では主に機械学習を取り巻くインフラ環境として「マイクロサービス」を取り扱っています。
AWSGoogle Cloud Platform(GCP)を利用し、クラウド上のインスタンスやLambdaのようなコンピューティングサービス、RDBSやRedshiftなどのデータベース等を適切に使い分け、細かく機能ごとにサービス配置を分けるアレです。(最近ではよくピタゴラスイッチと呼ばれているソレです)。

経験上、マイクロサービスは機械学習プロジェクトにかなり適した形だと私も感じています

マイクロサービスは「プロジェクト進行具合やスケーリング等までサービスごとに分けて考えられる」というのが大きなメリットですが、最も機械学習に効いてくるのが「最悪切り離してポイできる」だと思います。

「データはナマモノ」と界隈ではよく言われますが、「時期が変われば今まで使っていた分類器がゴミになる」とか「前処理がちょっと変わって全部パー」みたいな事が多々起こり得るのが機械学習プロジェクトです。
マイクロサービスはその特性によくマッチしています。

機械学習におけるインフラ設計の考え方だけでなく、後半実例も含めながら丁寧に書かれているので、機械学習屋だけでなく機械学習プロジェクト周辺に属するインフラ屋さんにもオススメできる本だと思います。

 
強いて言えば、Workflow辺りに少し触れて欲しかったと思います。

マイクロサービスは良いところも多いですが、既存がもう1つのプロジェクトとして完成している場合が多々あります。
機械学習をその中に持ち込む事で、ピタゴラスイッチが総崩れになり復旧が大変だったという話も聞きます。

またこれらは逆も然りで、切り分けすぎて「アプリ側、バックエンド側の仕様が見えてない」がために「機械学習器の精度が上がらない」といった状況もままあります。


この辺TPOである場合が多いんですが、そのために全体を見通してサービスを管理できるフレームワークが最近徐々に使われるようになってきています。

書籍にある通りログの取得も大事ですが、ワークフローフレームワークのようなデータの取得から前処理、機械学習器を含めたフローの構築、検定以外でKPIに則しているかのテストを、1つのフレームの中で定期的に回せる事がじわじわ効いてきたりします。
もちろん両者良し悪しあるので、この書籍を読み終わった後にWorkflowについて調べてみるのも良いかもしれません。

 
つらつら書いてますが、個人的にワークフローフレームワークは嫌いで機械学習プロジェクトでは使いたくないとまで思っているんですが、まあ便利なので書籍内で取り上げてもらって筆者のプロの方々がどう考えているのか知れれば良かったなあ〜みたいな感じなので、主な理由がゲスです。

 

インフラは機械学習の本質か

インフラやアプリ側の知識、ログも取らないと…という書籍の内容には同意が9割、モヤモヤ1割という感じです。
この疑問はこの書籍に限ったことではなく、常に自分の中にあるものです。

機械学習屋の技術力は、それぞれのアルゴリズムの特性を理解しデータから統計的で適切な処理を施せるところにあると思います。
果たして「AWSの各サービスやスケーリングシステムの把握に毎日数時間使う」「アプリ側のswiftのコードを読むために数日勉強する」といった時間をどの程度取るべきなのでしょうか。それらの設定までやり切るべきでしょうか。

AWSの新しいサービスを触る時間でKaggleをやったほうが良いのでは?

 
もちろんですが、別レイヤを把握することの効能も記事の上でゴリ推ししたので、これらはTPOであり当人が選択すべき点であるという事は言わずもがなだと思います。
会社の状況や自身のキャリアプランに合わせて、その辺りも選んでいかないといけないなと思う次第です。


 

美しい強調フィルタリングからFactorization Machineの流れ

強調フィルタリングやFactorization Machineは、今やレコメンドエンジンを作る上で欠かせない技術です。
Factorization Machineはニューラルネット拡張やら行列の高速計算など発展著しく、Deep、Embedding、Rank学習、生成モデル、バンディット辺りくらいホットです。

書籍『仕事ではじめる機械学習』より、「強調フィルタリングの良し悪し」の説明から美しく「Factorization Machineを使おう!」という流れに持っていけている日本語の書籍は、他にないと思います。

筆者FM好き過ぎでは?

 
レコメンドエンジンを作りたいんじゃいという人は読むと良いです。

 

Excel

途中Excelを使うコーナーがあります。
ところてん氏の書くコーナーですが、機械学習を使わないために様々な分析をExcelで行っています。

実際見てて思ったんですが、Excelはとても高機能でポチポチでビジュアライズできる良いソフトウェアだと思います。

ただ、別にPythonやRに親和性があるわけでもなく、文字コードVBAというキワモノと一緒に生きていくことになるため、やはりMicrosoftという感じ。

 
みんなExcelに変わるやつを心の中で求めているのでは…とただただ思いました。

 

実務案件対応

最後の章では、実際に実務やデータセットを例に、どうやって機械学習の手法を選んだり、プロジェクトを進めていけばよいかが経験を元に書かれています。

私の求めていた部分は結構ここにあって、実務で使った機械学習案件って結構外に出にくいのでこういった所で読めるのはとてもありがたいです。

 
基本的に機械学習における「ドメイン情報」と呼ばれる「データに関する知見」というのは、それだけで会社の機密データや顧客の個人情報につながる場合が多く、そういった背景を理解した機械学習エンジニアの集まる勉強会でさえ詳細をボカされる場合が多いです。

精度でさえ詳しい検定結果を示さず「目視したら大体ほとんど正解でしたね〜…」等と適当にボカして言う場合が多いです。
それらをデカい声で公言した事で「あの会社の機械学習器は90%しか精度がない!1割間違えられるサービスに信用はおけない!」みたいな事を言われかねないからです (もちろんその1割をカバーする施策を裏で多くしている筈ですが)。
 
 
直接的な原因がソレという訳ではないですがGoogleが女性とゴリラを誤判定したニュース辺りから、かなりシビアになってきていると肌でも感じます。
書籍内にも「精度が100%の機械学習器は作れない」という話がありますが、本当にその通りで機械学習プロジェクトというのは機械学習単体では成り立たないのです。ただ、周辺のそれらを含めて1つ話をするというのは、とても骨が折れる事だと思います。プロジェクトの全体像を1から話す事になりかねませんからね。


そういった背景からか、結構実務の詳細な話があるというのはとてもレアなのです。
このような実務寄りの内容が外に出てくる事案が1つ増えたという事でとても価値があると思います。


 

- おわりに -

この記事には書いてませんが、実際は機械学習の主な手法をいくつか解説、実装パッケージの紹介、検定の手法の紹介など、基本的な内容もしっかり丁寧に書かれています。
Pythonの知見も溜まります。

また、コラムのようにちょいちょい出てくる機械学習界隈では当たり前になっているネタも拾えるので良さあります。

実際に私自身は序盤に書いたターゲット層から外れていると思うので「凄い本が来た!」とまではいきませんでしたが、「自分もしかしたらターゲットにフィットするかもな」と思えれば、値段もお手頃ですのでPDFで持っておく価値は十二分にある良い書籍だと思いました。


み〜んなMediumじゃん。はてなの時代は終わったのか?


 
追記:


めっちゃTogetterにまとめられてるオジサンだと思ってました。すまんせん…

 
追記2:
Workflowについて言及してくれてた


個人的にワークフローシステムに導入によって機械学習エンジニアの仕事量が明らかに増えると思っているのであまり好きになれない部分があったが、これもまたTPOなのだなと思う。
適切に見極められるようになりたい。


 

Pythonとカーネル密度推定(KDE)について調べたまとめ

- はじめに -

端的にやりたい事を画像で説明すると以下
f:id:vaaaaaanquish:20171029110340p:plain

データ標本から確率密度関数を推定する。
一般的な方法としては、正規分布やガンマ分布などを使ったパラメトリックモデルを想定した手法と、後述するカーネル密度推定(Kernel density estimation: KDE)を代表としたノンパラメトリックな推定手法がある。

本記事ではKDEの理論に加え、Pythonで扱えるKDEのパッケージの調査、二次元データにおける可視化に着目した結果をまとめておく。

 

- カーネル密度推定(KDE)とは -

x1, x2, ..., xn を未知の確率密度関数 f を持つ独立同分布からの標本としたとき、任意のカーネル関数 K、パラメータ hカーネル密度推定量は以下の式で表される。

   \hat{f}(x) = \frac{1}{nh} \sum_{i=1}^n k\bigg(\frac{x-X_i}{h}\bigg)
 
つまるところ、標本を使ったKernelを足し合わせて母集団の分布に寄せたいという話。

カーネル関数 K は Gaussian Kernelが代表的。
Rectangular、Triangular、Epanechnikov等、以下性質を満たすものが使われる。

    \int K(x)dx=1,  \int xK(x)dx=0,  \int x^{2}K(x)dx>0

それぞれの特性については後述する

パラメータ h はBandwidth、バンド幅等と呼ばれる平滑化のためのパラメータで、直感的に標本の幅やデータ数に合わせて調整する必要がある事がわかる。
経験的に調整する方法やcovariance(共分散推定)による調整方法等があり、後述する通りそれらが実装されているPythonパッケージもある。

式を見てわかるように、データ上でカーネル関数を足し合わせているだけなので計算が簡単で、分布に対する仮定も非常に少ない。
また、多変量、多次元への拡張やクロスバリデーションによる最適化も可能である。


Kernelの足し合わせでは、離散コサイン変換(DCT)、高速フーリエ変換(FFT)を利用した高速な計算方法が提案されており、一般的なパッケージではこれらが利用されている。
FFT-Based Fast Bandwidth Selector for Multivariate Kernel Density Estimation - arXiv [A Gramack, stat.CO, 2015]

FFTベースの手法以外では、N個の点のM個の評価(各入力/出力対間の距離計算)であると捉え、一般化N体問題としてkd木(kd tree)やball treeに落とし込んで解く手法がある。
sklearn等ではK近傍法 (K-nearest neighbor) の実装で使われているアルゴリズム
Alexander G. Gray's N-Body Page
survey/kdtree.md at master · komi2/survey · GitHub

kd木のようなデータ構造を使うことで、データポイントを空間的に分離して計算し高速化出来る。
木にする事で各Kernelの相対的な誤差(relative tolerance: rtol)、絶対的な誤差(absolute tolerance: atol)
をパラメータとして設定する必要があるが、パラメータに応じた以下の精度で高速に近似カーネル密度推定値を計算することが可能である事が示されている。
   abs (p-p_{t} ) < atol+p_{t} \cdot rtol
Density Estimation Trees - CMU Statistics - Carnegie Mellon University
Forest Density Estimation - arXiv [H Liu, stat.ML, 2010]


KDEの欠点として、パラメトリックな手法に比べて次元が大きい場合に収束レートが非常に遅い、データ点が多い場合のメモリ量や計算量が大きい等があり、適切なパラメトリックモデルが得られてない場合に利用する手法である。

Wikiが割りと丁寧
カーネル密度推定 - Wikipedia
ネットで先生方の講義資料が沢山あるのでGoogleでも
データ解析 第十回 ノンパラメトリック密度推定法 鈴木大慈
カーネル密度推定法(第12章) - TOKYO TECH OCW 杉山 将
カーネル法入門 カーネル法によるノンパラメトリックなベイズ推論 福水健次 統計数理研究所/総合研究大学院大学
英語だと以下辺り参考
http://www.shogun-toolbox.org/static/notebook/current/KernelDensity.html
Kernel Density Estimation in Python | Pythonic Perambulations
GetDist: Kernel Density Estimation
[1704.03924] A Tutorial on Kernel Density Estimation and Recent Advances


 

- Python KDEパッケージの比較 -

調べて出てきたパッケージとKDEの実装クラスを以下に挙げる

  • SciPy
  • sklearn
    • class: neighbors.KernelDensity
    • doc
    • 独自実装 [ Github ]
    • Tree algorithmで2種類 ["kd_tree"|"ball_tree"]
    • 木なので距離関数metric、葉数leaf_size、前述のatolとrtolを調整する
    • Metric: 11種類 [“euclidean", “manhattan", “chebyshev", “minkowski", “wminkowski", “seuclidean", “mahalanobis", “haversine", “hamming", “canberra", “braycurtis"] + 自前実装 [ doc ]
    • Kernel: 6種類 ["gaussian", "tophat", "epanechnikov", "exponential", "linear", "cosine"]
    • Bandwidth: 自前で用意する必要があるがGridSearchができる
  • Statsmodels
    • class: api.KDEUnivariate, api.KDEMultivariate
    • doc, tutorial
    • 独自実装 [ Github ]
    • KDEUnivariateが一次元でFFTベースの高速実装
    • KDEMultivariateが多次元でFFT効率悪いので分けられているがこちらはcross-validationが使える
    • Bandwidth: 3種類 ["scott", "silverman", "normal_reference"]
    • Kernel: 7種類 ["biweight", "cosine", "Epanechnikov", "Gaussian.", "triangular", "triweight", "uniform"] + 自前実装
  • PyQt-Fit
    • class: kde
    • doc, tutorial
    • Bandwidth or Covariance: 4種類 ["variance_bandwidth", "silverman_covariance", "scotts_covariance", "botev_bandwidth"] + 自前実装
    • Kernel: 4種類["Gaussian", "Tricube", "Epanechnikov", "Higher Order"] + 自前実装
    • Kernelの中のパラメータやデータポイントごとのbandwidth設定(lambdas)、データポイントごとの重み(weights)、methodではカーネルの足し合わせ方まで選べてわりと高級
    • PyQtなるGUI実装のためのパッケージだが切り離されている
    • 使えるところでDCTかFFTを使ってくれる
  • AstroML
    • class: density_estimation.KDE
    • doc
    • 一応書いたが「sklearn優秀なのでこっちは0.3で廃止するね」と言ってる

scipy, statsmodels, sklearn, pyqt-fitの4つが大体メイン。
実装されているアルゴリズムや方針はそれぞれ。
scipy以外は大体どれも自作のbwやKernelが使える。
入力で言うとsklearn以外はlistそのまま突っ込める。sklearnだけnp.array[:, None]。

 

以下利用するデータセット

後述のスクリプトではscikit-learnで取得できるirisのデータセットを利用する。

pip install numpy
pip install pandas
pip install scikit-learn
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
from sklearn import datasets

# irisデータセットサンプル
iris = datasets.load_iris()
# pandas
data_pd = pd.DataFrame(data=iris["data"], columns=iris["feature_names"])
display(data_pd.head())
# 以降ではirisの"petal length (cm)"カラムを利用する
# list
data_list = [x[3] for x in iris["data"]]
# np.array
data_np = np.array(data_list)

# x軸をGridするためのデータも生成
x_grid = np.linspace(0, max(data_list), num=100)
# データを正規化したヒストグラムを表示する用
weights = np.ones_like(data_list)/float(len(data_list))

f:id:vaaaaaanquish:20171029144025p:plain:w400:h180

 
グラフ描画は基本的にmatplotlib

import matplotlib
import matplotlib.pyplot as plt
plt.style.use('ggplot')

  

pandas

多分一番使われているだろう最も簡単なやつ
中身はscipyでgaussian_kdeが動いている

plt.figure(figsize=(14,7))
data_pd["petal length (cm)"].plot(kind="hist", bins=30, alpha=0.5)
data_pd["petal length (cm)"].plot(kind="kde", secondary_y=True)
plt.show()

Matplotlib内のKDE, secondary_y=TrueでY軸正規化して重ねられるので便利
f:id:vaaaaaanquish:20171029144456p:plain:w400:h200

 

scipy

1.0.0のリリースおめでとうございます。
gaussian_kdeのみだが、default値でかなりよしなにやってくれる。

pip install scipy

最もシンプルに書くと以下

from scipy.stats import gaussian_kde

kde_model = gaussian_kde(data_list)
y = kde_model(x_grid)

plt.figure(figsize=(14,7))
plt.plot(x_grid, y)
plt.hist(data_list, alpha=0.3, bins=20, weights=weights)
plt.show()

f:id:vaaaaaanquish:20171029170718p:plain:w400
 
Bandwidthの推定はdefaultで以下scotts_factor
n**(-1./(d+4))
以下silverman_factorも選べるので比較してみる
(n * (d + 2) / 4.)**(-1. / (d + 4)).

scotts = gaussian_kde(data_list)
y_scotts = scotts(x_grid)
silverman = gaussian_kde(data_list, bw_method='silverman')
y_silverman = silverman(x_grid)

plt.figure(figsize=(14,7))
plt.plot(x_grid, y, label="scotts")
plt.plot(x_grid, y_silverman, label="silverman")
plt.hist(data_list, alpha=0.3, bins=20, weights=weights)
plt.legend()
plt.show()

f:id:vaaaaaanquish:20171029170744p:plain:w400
 
自前の関数でもできる

def kde_bw(obj):
    return obj.n * 0.001

kde_model_origin = gaussian_kde(data_list, bw_method=kde_bw)

f:id:vaaaaaanquish:20171029170801p:plain:w400

 

scikit-learn

他と違ってtree-baseな構造を使っていてかなり高速。
sklearnに親しみがある人が多いと思うし、記述も簡易。

 

基本的なKDE
# -*- coding: utf-8 -*-
from sklearn.neighbors import KernelDensity

# default
kde_model = KernelDensity(kernel='gaussian').fit(data_np[:, None])
score = kde_model.score_samples(x_grid[:, None])
# bw値を調整
bw = 0.1
kde_mode_bw = KernelDensity(bandwidth=bw, kernel='gaussian').fit(data_np[:, None])
score_bw = kde_mode_bw.score_samples(x_grid[:, None])

# マイナスが出るので指数関数かます
plt.figure(figsize=(14,7))
plt.plot(x_grid, np.exp(score), label="default")
plt.plot(x_grid, np.exp(score_bw), label="origin")
plt.hist(data_list, alpha=0.3, bins=20, weights=weights)
plt.legend()
plt.show()

f:id:vaaaaaanquish:20171029154709p:plain:w400
出力からわかるようにBW値の設定は必須である。

 

Using Kernel

sklearnで使えるKernelを全て見てみる。
それぞれのKernelの数式は以下にまとまっている。
2.8. Density Estimation — scikit-learn 0.21.3 documentation

# Kearnels
plt.figure(figsize=(14, 7))
plt.grid(color='white', linestyle='-', linewidth=2)
X_src = np.zeros((1, 1))
x_grid = np.linspace(-3, 3, 1000)
for kernel in ['gaussian', 'tophat', 'epanechnikov',
               'exponential', 'linear', 'cosine']:
    log_dens = KernelDensity(kernel=kernel).fit(X_src).score_samples(x_grid[:, None])
    plt.plot(x_grid, np.exp(log_dens), label=kernel)
plt.legend()
plt.show()

f:id:vaaaaaanquish:20171029154411p:plain:w400

理論通りの結果が見られる。
これらを使いKDE値を計算してplotしてみると以下。

# plot
plt.figure(figsize=(14,7))
for kernel in ['gaussian', 'tophat', 'epanechnikov',
               'exponential', 'linear', 'cosine']:
    kde_modes = KernelDensity(bandwidth=bw, kernel=kernel).fit(data_np[:, None])
    scores = kde_modes.score_samples(x_grid[:, None])
    plt.plot(x_grid, np.exp(scores), label=kernel)
plt.hist(data_list, alpha=0.3, bins=20, weights=weights)
plt.legend()
plt.show()

f:id:vaaaaaanquish:20171029154419p:plain:w400

それぞれのKernelの形を足し合わせて元の分布を表現していることがよくわかる。

 

GridSearch

上記のようなパラメータをGridSearchでクロスバリデーションできる。
ここはsklearnらしさある。

from sklearn.model_selection import StratifiedKFold

grid = GridSearchCV(KernelDensity(),
                    {'bandwidth': np.linspace(0.1, 1.0, 10)},
                    cv=20)
grid.fit(data_np[:, None])
print(grid.best_params_)

kde_model_best = grid.best_estimator_
best_score = kde_model_best.score_samples(x_grid[:, None])

plt.figure(figsize=(14,7))
plt.plot(x_grid, np.exp(best_score), label='bw=%.2f' % kde_model_best.bandwidth)
plt.hist(data_list, alpha=0.3, bins=20, weights=weights)
plt.legend()
plt.show()

f:id:vaaaaaanquish:20171029155227p:plain:w400

GridSearchといえば大変なイメージが強いが、KDE自体が高速なので割りと速くできる。
標本がバラけるような環境で困ったらこれ。

 

statsmodels

pip install statsmodels

見るのはKDEUnivariate。
データはfloat値である必要がある。
sampleがnotebookなのでそちらも参考に[ kernel_density ]

import statsmodels.api as sm

kde_model = sm.nonparametric.KDEUnivariate(data_list)
kde_model.fit()

plt.figure(figsize=(14,7))
plt.plot(kde_model.support, kde_model.density, alpha=0.5, label="statsmodels")
plt.hist(data_list, alpha=0.3, bins=20, weights=weights)
plt.legend()
plt.show()

f:id:vaaaaaanquish:20171029155758p:plain:w400

中身がFFTなだけあってシンプルなデータだとかなり早い。

 

pyqt-fit

インストールでこける

pip install pyqt-fit

 
Python3.6だと以下ERROR

    register_loader_type(importlib_bootstrap.SourceFileLoader, DefaultProvider)
AttributeError: module 'importlib._bootstrap' has no attribute 'SourceFileLoader'

pyenvでPython3.5.xにしたら上記ERRORは消える
 
それでも以下のようにpath.pyのバージョンでERRORになる

cannot import name 'path'

以下参考にpath.pyのバージョン指定してインストールしてnotebookとかPython周り再起動
python - PyQt_Fit: cannot import name path - Stack Overflow

sudo pip install -I path.py==7.7.1

 
インストールできてもimportでエラー

# from matplotlib.backends import _macosx
# RuntimeError: Python is not installed as a framework. The Mac OS X backend will not be able to function correctly if Python is not installed as a framework. 
# See the Python documentation for more information on installing Python as a framework on Mac OS X.
# Please either reinstall Python as a framework, or try one of the other backends.
# If you are using (Ana)Conda please install python.app and replace the use of 'python' with 'pythonw'.
# See 'Working with Matplotlib on OSX' in the Matplotlib FAQ for more information.

これはmatplotlib.pylotのsavefig()の「RuntimeError: Invalid DISPLAY variable」と同じ原因なので、以下のようにmatplotlibのpyplotを読み込む前にbackendをAggに変えて回避する。

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

 
Cythonが入ってないと以下Warningが出る

# Warning, cannot import Cython kernel functions, pure python functions will be used instead

KDEでは不要だが、使いたければpipで入れれば消える。

pip install cython

 
Kernelは4種類で内部パラメータもイジれる
bandwidthかcovarianceも["variance_bandwidth", "silverman_covariance", "scotts_covariance", "botev_bandwidth"]がある。
データポイントごとのbandwidthや重みも扱え、Kernelの足し合わせ方も選択できる。

from pyqt_fit import kde, kde_methods

kde_model1 = kde.KDE1D(data_list, bandwidth=0.1)
kde_model2 = kde.KDE1D(
    data_list,
    method=kde_methods.reflection,
    covariance=kde.variance_bandwidth(0.1, data_list))

plt.figure(figsize=(14,7))
plt.plot(x_grid, kde_model1(x_grid), label='bw={:.3g}'.format(kde_model.bandwidth))
plt.plot(x_grid, kde_model2(x_grid), label='variance bw={:.3g}'.format(kde_model.bandwidth))
plt.hist(data_list, alpha=0.3, bins=20, weights=weights)
plt.legend()
plt.show()

f:id:vaaaaaanquish:20171029160352p:plain:w400

裏で勝手にFFT使ってくれるのでかなり早い。


 

- 速度比較 -

以下で適当にn個作ったデータでKDE計算速度を比較する。
np.random.normalって中身ガウス分布じゃんパラメトリックでやれよ…という話ではあるが、その他の条件で平等な評価が気軽ではないため利用する。

x_t = 50 * np.random.rand() * np.random.normal(0, 10, n)
x_g = np.linspace(min(x_t), max(x_t), 100)

環境はPython 3.5 (3.6ではpyfitがうまくインストールできないため)
Macbook Pro 2.8GHz Core i7, memory 16G

nはデータ数
それぞれdefaultのパラメータで、100回KDE算出した際の平均速度(秒)を計算した。
ついでなのでsklearnのGridSearch(BW値のCross-validation)も追加で行う。

package n=100 1000 10000 100000 1000000
scipy 0.001563 0.003090 0.014148 0.142844 1.876725
statsmodels 0.000650 0.000775 0.002706 0.022177 0.297552
scikit-learn 0.000477 0.001233 0.017090 0.338675 11.294480
pyfit 0.000410 0.001728 0.016702 0.236698 2.491684
sklearn(gridsearch) 0.161637 0.285323 15.952579 None None

gridsearchは計算時間からこれ以上は無理そうなので断念。

シンプルな一次元データという事もあるが、statsmodelsクソ早い。
sklearnはtree-baseで最初早いけど、データ数の増加に弱い。理論通りの結果という感じ。


 

- おわりに -

速度ならstatsmodels、メソッドやパラメータの種類ならpyfit、GridSearchや他クラスの利用やclass拡張ならsklearn、scipyは簡易な利用がメリットだということが分かった。

KDEについてこんなに詳しく調べるつもりはなかったけど、冒頭に出したk-nearest neighborのkd木の実装の論文を読んでると面白くなったので土日を使ってしまった。
次はkd木な構造に関する記事を書く。

英語Wikiは丁寧なのでこちらもオススメ。
k-d tree - Wikipedia

構造的因果モデルの基礎

構造的因果モデルの基礎

 

GASでGithubの自分関連のReviewer情報を定期的にSlackにPostする

- はじめに -

GithubのPull Requestを大体1日以内に処理するルールだったのだが、Repositoryが増えて全然管理できなくなったりしたので、Reviewerに入っていてApprovedしてないものだけSlackに通知しようとこねくり回したGoogle Apps Script。

Lambdaとかを使う方が楽だけど、GASは金が掛からないしポチポチで時間トリガーやSlack Bot化を進められるのが良さ。

GASでSlack botは前に書いたので、Slack投稿までは下記で出来ている前提。

vaaaaaanquish.hatenablog.com


 

- GithubのPersonal Access Tokenの取得 -

Githubをブラウザから見て、SettingsからAccessTokenを取得する。

右上のアイコンからAccount -> Settings

f:id:vaaaaaanquish:20171007161923p:plain:w300:h200

上記画像の赤枠をポチポチすると取得できるので、文字列を控えておく。


 

- GAS -

メインとなるGithub周りのスクリプトを導入する前に日付のフォーマット関連の処理をよしなに処理出来るようにしておく(GASではdatetimeの文字列処理が面倒なため)。

日付周りの処理

新しいスクリプトを追加してそれぞれファイル分けできるのでやる
f:id:vaaaaaanquish:20171007183856p:plain:w350:h220


新しく作ったスクリプトファイルにisodate関数を作っておく。
isodateとして以下コードを引用。
iso8601.js/iso8601.js at master · shumpei/iso8601.js · GitHub

/*
 * JavaScript library for ISO-8601 datetime format.
 * Copyright: 2009, Shumpei Shiraishi (shumpei.shiraishi at gmail.com)
 * License: GNU General Public License, Free Software Foundation
 *          <http://creativecommons.org/licenses/GPL/2.0/>
 * Original code and license is:
 *   Web Forms 2.0 Cross-browser Implementation <http://code.google.com/p/webforms2/>
 *   Copyright: 2007, Weston Ruter <http://weston.ruter.net/>
 *   License: GNU General Public License, Free Software Foundation
 *          <http://creativecommons.org/licenses/GPL/2.0/>
 */
var isodate=function(){function e(e,t){t||(t=2);for(var r=e.toString();r.length<t;)r="0"+r;return r}var t=/^(?:(\d\d\d\d)-(W(0[1-9]|[1-4]\d|5[0-2])|(0\d|1[0-2])(-(0\d|[1-2]\d|3[0-1])(T(0\d|1\d|2[0-4]):([0-5]\d)(:([0-5]\d)(\.(\d+))?)?(Z)?)?)?)|(0\d|1\d|2[0-4]):([0-5]\d)(:([0-5]\d)(\.(\d+))?)?)$/;return{validate:function(e,r){var n=!1,a=t.exec(e);if(!a||!r)return a;if(r=r.toLowerCase(),"week"==r)n=0===a[2].toString().indexOf("W");else if("time"==r)n=!!a[15];else if("month"==r)n=!a[5];else if(a[6]){var s=new Date(a[1],a[4]-1,a[6]);if(s.getMonth()!=a[4]-1)n=!1;else switch(r){case"date":n=a[4]&&!a[7];break;case"datetime":n=!!a[14];break;case"datetime-local":n=a[7]&&!a[14]}}return n?a:null},parse:function(e,t){if(!e)return null;var r=this.validate(e,t);if(!r)return null;var n=new Date(0),a=8;if(r[15]){if(t&&"time"!=t)return null;a=15}else{if(n.setUTCFullYear(r[1]),r[3])return t&&"week"!=t?null:(n.setUTCDate(n.getUTCDate()-(7-n.getUTCDay())+7*(r[3]-1)),n);n.setUTCMonth(r[4]-1),r[6]&&n.setUTCDate(r[6])}return r[a+0]&&n.setUTCHours(r[a+0]),r[a+1]&&n.setUTCMinutes(r[a+1]),r[a+2]&&n.setUTCSeconds(r[a+3]),r[a+4]&&n.setUTCMilliseconds(Math.round(1e3*Number(r[a+4]))),r[4]&&r[a+0]&&!r[a+6]&&n.setUTCMinutes(n.getUTCMinutes()+n.getTimezoneOffset()),n},format:function(t,r){if(!t)return null;r=String(r).toLowerCase();var n="";switch(t.getUTCMilliseconds()&&(n="."+e(t.getUTCMilliseconds(),3).replace(/0+$/,"")),r){case"date":return t.getUTCFullYear()+"-"+e(t.getUTCMonth()+1)+"-"+e(t.getUTCDate());case"datetime-local":return t.getFullYear()+"-"+e(t.getMonth()+1)+"-"+e(t.getDate())+"T"+e(t.getHours())+":"+e(t.getMinutes())+":"+e(t.getSeconds())+n+"Z";case"month":return t.getUTCFullYear()+"-"+e(t.getUTCMonth()+1);case"week":var a=this.parse(t.getUTCFullYear()+"-W01");return t.getUTCFullYear()+"-W"+e(Math.floor((t.valueOf()-a.valueOf())/6048e5)+1);case"time":return e(t.getUTCHours())+":"+e(t.getUTCMinutes())+":"+e(t.getUTCSeconds())+n;case"datetime":default:return t.getUTCFullYear()+"-"+e(t.getUTCMonth()+1)+"-"+e(t.getUTCDate())+"T"+e(t.getUTCHours())+":"+e(t.getUTCMinutes())+":"+e(t.getUTCSeconds())+n+"Z"}}}}();

 

Githubからの情報取得

以下メインのスクリプト
userにReviewerかどうか知りたいユーザ(つまり自分)、TeamはそのRepository製作者やチーム名、githubAccessTokenに上記で取得したアクセストークンを入れる。
pullsNameListにはチェックしたいRepositoryの名前をリストで入れておく感じ。

// GithubのプルリクをチェックしてApprovedしてないやつを確認する
// 各設定
var user = "testersp";
var team = "vaaaaanquish";
var githubAccessToken = "hogehoge";
var baseUrl = "https://api.github.com/repos/" + team + "/{}/pulls";
var pullsNameList = ["pull-request-test"];

// pull requestsのURLを生成
var pullsUrlList = [];
for (var i = 0; i < pullsNameList.length; i++) {
  pullsUrlList.push(baseUrl.replace("{}", pullsNameList[i]));
}

// Httpオプションとヘッダ
var httpOptions = {
    method: "GET",
};
var httpOptionsReviews = {
    method: "GET",
    headers: {"Accept":"application/vnd.github.black-cat-preview+json"}
};

// main
function github_main() {
    // 土日は実行しない
    var today = new Date();
    if (today.getDay() == 0 || today.getDay() == 6) return;
    // GithubAPIを走査しSlack用の文字列allContentsを生成する
    var allContents = '';
    for (var i = 0; i < pullsUrlList.length; i++) {
        allContents += doOneRepository(pullsUrlList[i]);
    }
    return allContents;
}

// 1つのリポジトリから情報習得
var doOneRepository = function(githubUrl) {
    // リポジトリの基本情報を習得(プルリク取得)
    var response = getResponse(githubUrl, httpOptions);
    if (response === null || response.length == 0)
        return "";

    // 1リポジトリ分の文字列を生成  
    var allContents = '';
    for (var i = 0; i < response.length; i++) {
        var r = response[i];
        var createdAt = isodate.parse(r["created_at"]);
        var info = {};
        info.prNumber = r["number"];
        info.prTitle = r["title"];
        info.owner = r["user"]["login"];
        info.hourCreate = r["created_at"];
        // requested_reviewersに入っているユーザ
        // (recviewersに入っていてCommentもApproveもしていない)
        info.notApprovedPeople = getNotApproved(githubUrl+"/"+r["number"]+"/requested_reviewers");
        // 文字列生成
        var content = buildSlackPostingString(info);
        if(content.length > 1){
            allContents += (content + "\n");
        }
    }
    // 文字列を整形して返す
    allContents.replace(/.+\n$/g,"")
    var repository = githubUrl.split("/")[5];
    if(allContents.length > 1){
      return repository + '\n' + allContents + '\n';
    }else{
      return "";
    }
}

// requested_reviewersに入っている人を返す
var getNotApproved = function (GithubUrlReviews) {
    var res = getResponse(GithubUrlReviews, httpOptionsReviews);
    var result = [];
    for (var i = 0; i<res.length; i++){
      result.push(res[i]["login"]);
    }
    Logger.log(result);
    return result;
}

// HTTP GETリクエストを投げて返却値をJSONとして得る
var getResponse = function (url, options) {
    var urlToken = url + "?access_token=" + githubAccessToken;
    var response = UrlFetchApp.fetch(urlToken, options);
    if (response.getResponseCode() != 200)
        return null;
    return JSON.parse(response.getContentText());
};

// Slackに投稿する文字列の生成
var buildSlackPostingString = function (info) {
  if(info.notApprovedPeople.indexOf(user) > -1 ){
      var content = '> •  #' + info.prNumber + ' ' + info.prTitle;
      content += ' - @' + info.owner;
      content += '\n>     > CREATED: *' + info.hourCreate.split("T")[0] + '*';
      return content;
  }else{
    return "";
  }
}


具体的にはRepositoryを走査して、requested_reviewersエッジの情報に自分が入っているかをチェックするというもの。

土日は仕事をすべきではないので通知しないようにしている。

requested_reviewers自体はまだPreviewなので注意。以下Reference。
Review Requests | GitHub Developer Guide


以下のような状態でのみ反応する。
f:id:vaaaaaanquish:20171007184928p:plain:w350:h170

CommentかApproveするとrequested_reviewersには入って来ない。

 
この結果を冒頭に書いた記事内にある、GASとSlackの連携を行ったスクリプトに導入する。
Slack botをGASでつくる方法で一番楽そうなやつ - Stimulator

function postSlack(text){
  var url = "https://hooks.slack.com/services/hogehogehoge~";
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload" : '{"text":"' + text + '"}'
  };
  UrlFetchApp.fetch(url, options);
}

function test(){
  s = github_main();
  postSlack("未読プルリク情報\n" + s);
}

以下のようにSlackに通知される。
f:id:vaaaaaanquish:20171007185438p:plain

GASの設定で適当に1日数回、時間トリガーで発火するようにしておけば大体大丈夫。
もしくはSlackのbotにして応答させれば良い。


 

- おわりに -

かつてrequested_reviewersが動かなかった頃は、review_comments_urlからcommentをそれぞれ見に行ってcommentしているかチェックしたりしていたんですが、Reviewer機能が実装されたことでこの辺のチェックも本当に簡単になりました。
良かったです。


GAS便利だけどちょっと書くのがしんどいです。

GASエディタもStylishCSS書き換えで少し見やすくしているけど、それでもやっぱりローカル開発環境で十二分に色々やりたいという気持ちになってしまう。
Stylish - ウェブサイト用カスタムテーマ - Chrome ウェブストア
Google Apps Script Dark - Monokai | Userstyles.org


出来ることなら使わずに金を払って既存サービスに任せたい所です。

本末転倒ですが以上です。


 

Workplace by Facebookを使いやすくするTips

- はじめに -

業務で社内SNSとしてWorkplaceを使っていた。

デフォルトのWorkplaceは「人気の投稿」や「チャット」が常に画面内に表示されており、見辛く辛い部分が多いので、それらを解決するTipsを書いておく。

Workplaceユーザ向け。


 

- Chrome拡張を導入する -

以下のChrome拡張によって、Workplaceの視認性が向上する。

chrome.google.com


以下GithubのReadmeに解説が載っている。

github.com


また以下記事の筆者が作成している。

qiita.com


導入するだけで変化が見られるので楽。
Workplaceを社内で使うなら導入しておいた方が生産性が100倍高い。

- Chrome拡張でさらにCSSを触る -

Chrome拡張によってブラウザで見ているWebサイトの見た目を書き換える方法はいくつかある。

私は適当にStylishを使っているので以下。

chrome.google.com

導入後は拡張から「スタイルを作る」

f:id:vaaaaaanquish:20171004114948p:plain:w400:h300

スタイル設定画面で見るべきは以下赤枠の4つ

f:id:vaaaaaanquish:20171004115308p:plain

左上から

  • スタイルの名前の設定と保存
  • CSSコード記述フォーム
  • 適応先
  • セクションの追加

まずスタイルの名前を設定し、適応先を「ドメイン上のURL:facebook.com」とする。

私がfacebook.comドメイン上に適応しているCSSは以下
ありとあらゆる情報を消して、自分が「お気に入り」に登録しているグループ以外表示しない設定。

._3pk8 {display: none;}
.sidebarMode .fbChatSidebar{display:none;}
#contentArea {
    width: 100%  !important;
    min-width: 502px;
}
#headerArea{
    width: 100% !important;
    min-width: 502px;
}
.sidebarMode #globalContainer{padding-right: 0px;}
._2yq ._2t-d, ._2xk0 ._2t-d{width: calc(100% - 100px);}
.timelineLayout #contentArea {
    width: calc( 100% - 330px) !important;
    min-width: 502px;
}
._5nb8 {
    width: calc(100% - 345px);
    min-width: 512px;
}
li._4lh .fbTimelineTwoColumn[data-type="s"] {
    width: calc(100% - 345px);
    min-width: 512px;
}
.timelineLayout #contentArea {
    max-width: 100% !important;
    width: calc( 100% - 150px)!important;
}
._4_7u {
    max-width: 100% !important;
    width: calc( 100% - 350px)!important;
}
.fbTimelineUnit {
    max-width: 100% !important;
    width: 100% !important;
}
.fbTimelineTwoColumn {
    max-width: 100% !important;
    width: 100% !important;
}
.timelineUnitContainer {
    max-width: 100% !important;
    width: calc( 100% - 25px)!important;
}
.fbTimelineUFI {
    max-width: 200% !important;
    width: calc( 100% + 20px)!important;
}
.letterboxedImage {
    max-width: 200% !important;
    width: 100% !important;
}
@media screen and (max-width: 1400px) {
    #globalContainer {
        width: calc( 100% - 100px) !important;
        min-width: 500px;
    }
}
@media screen and (min-width: 1401px) {
    #globalContainer {
        width: 1200px !important;
    }
}
@media screen and (max-width: 2000px) {
    .sidebarMode ._50tj{
        padding-right: 20%;
        padding-left: 20%;
    }
}
@media screen and (max-width: 1600px) {
    .sidebarMode ._50tj {
        padding-right: 10%;
        padding-left: 10%;
    }
}
@media screen and (min-width: 2001px) {
    .sidebarMode ._50tj{
        padding-right: 25%;
        padding-left: 25%;
    }
}
h4.navHeader {
    background-color: #999;
    padding: 3px;
}
h4.navHeader .sectionDragHandle {color: #fff;}
._1cwg {
    color: #fff;
    font-size: 9px;
}
._5vb_,
._5vb_ #contentCol {background-color: #ccc;}
h4.navHeader {padding: 3px;}
._1cwg {font-size: 9px;}
._bui .sideNavItem .imgWrap .img {display: none;}
#leftCol{position: fixed;}
._wmx .homeSideNav .navHeader .sectionDragHandle{color: black;}
._2xk0._5vb_.hasLeftCol #leftCol{width: 240px;}
html ._2xk0._5vb_.hasLeftCol #contentCol{margin-left: 240px;}
._wmx ._bui ._5afe .linkWrap{
    margin-left: 10px;
    max-width: 228px;
}
._55y4 ._bui .sideNavItem .linkWrap {margin-right: 2px;}
._wmx ._bui ._5afe .linkWrap.hasCount{max-width: 220px;}
._wmx .homeSideNav#pinnedNav ._bui ._5afe{padding-left: 20px;}
#pagelet_company_logo{display:none;}
#appsNav {display: none;}
#navItem_230259100322928{display: none;}
#navItem_4748854339{display: none;}
#navItem_1434659290104689{display: none;}
#navItem_2344061033{display: none;}
#workGroupsTeamNav{display: none;}
#workGroupsTeamNav{display: none;}
#workGroupsAnnouncementNav{display: none;}
#workGroupsFeedbackNav{display: none;}
#workGroupsSocialNav{display:none;}
._wmx._wmx .homeSideNav .navHeader{display:none;}
._55y4 ._bui .sideNavItem .linkWrap{font-size: 13px;}
._wmx._wmx ._bui ._5afe .linkWrap {max-width: 100%;}
#rightCol ._5zny{display:none;}
div._4-u3._5dwa._5dwb{
    display:none;
    padding:0px;
}
#rightCol ._i7m{display:none;}
.uiBoxWhite{border:0px;}

 
上記CSSだけでは所謂本家のFacebookページに弊害が出たり、グループページで適応されない場合があるので、グループページに対してのみ以下を設定する。

先程の「他のセクションを追加」して、以下のようにコード2を設定。

f:id:vaaaaaanquish:20171004120326p:plain:w400:h300

適応先は「正規表現に一致するURL:.*facebook.com/groups/.*」とし、グループの情報ページに適応させている。

#rightCol ._4-u2._3-96._4-u8:nth-of-type(n+2) {display: none;}
#rightCol {
    width: 100% !important;
    min-width: 502px;
    float:left;
    display:block;
}
#pagelet_rhc_footer {display: none;}
.fixed_elem {
    position: absolute !important;
    top: 0px !important;
}
._5ks6{
    height:40px;
    width:40px;
    padding:0px;
}
#groupsRHCMembersFacepile,
.profileBrowserGrid,
._5ks4{
    height:40px;
    width:100%;
}
#groupsNewMembersLink div:nth-of-type(3){display:none;}
#groupsNewMembersLink{display:none;}

 
上記に加えてグループフィードの中でも適度にCSSを書き換えて閲覧。
新しいセクションを作り、適応先を「正規表現に一致するURL:.*facebook.com/(?!.*groups).*」とすることで、グループフィードのページにCSSを適応。

#rightCol{display:none;}
.timelineLayout #contentArea{width:100% !important;}
._4gt0{display:none;}
.fbTimelineStickyHeader .stickyHeaderWrap{display:none;}


ほとんどの情報が消えるので気になるなら所々display:noneにしてあるところを外していく。


 

- おわりに -

これで大体Workplaceでやっていたことが記事にできた。

Workplaceは、デフォルトだとFacebookみたいで本当に見にくいサービスだが徐々に改善が見られるので今後に期待という感じである。

日々機能が増えていくのはありがたいが、UI/UXをまず見直してほしい…


APIでの操作の記事も書いてある。

vaaaaaanquish.hatenablog.com

 

Workplace by FacebookのGraph APIによる投稿、情報取得、DMの操作メモ

- はじめに -

弊社では、WorkplaceなるFacebookを模した社内SNSを利用している。
1年弱使ったが、非常に出来が良くなりつつある社内ツールである。

見やすく扱いやすくするTipsも書いたくらい使っている。
vaaaaaanquish.hatenablog.com

 
WorkplaceにはGraph APIが用意されており、それによる様々な自動化が可能である。

本記事ではPython 3.xを用いながら、WorkplaceAPI周りのメモをまとめておく。

Workplaceリファレンスは以下だが、現状あまり親切なリファレンスではない。
Reference - Workplace - ドキュメンテーション - 開発者向けFacebook

WorkplaceのGraph APIは多分実装をFacebookからそのまま持ってきており、FacebookAPIと同じ使い方が動く場合が多々あるため以下のリファレンスが時に参考になる。
Graph API Reference - Documentation - Facebook for Developers

 
Pythonではrequestsモジュールを基本的に使う。入って無ければpipで導入。

sudo pip install requests

 

- アクセストークンの取得と利用 -

アクセストークンは管理者権限のあるアカウントによって取得する。
例えば社内であれば偉い人が持っている。

私自身は管理者になった経験がないが、多分Facebookと同様にDeveloperページの右上からログインし、社内ダッシュボードから外部アプリを発行してもらい、そのアクセストークンとコミュニティIDを使う。

f:id:vaaaaaanquish:20170929062208p:plain:w250:h150

 
Graph APIには、App Access Token(access_token)とMember Access Token(impersonate_token)が存在する。

Appのトークンは、「Permission情報の取得、変更」や「管理者の取得」「グループ情報の取得」「メンバー情報の取得」「Memberのトークンの取得」などが行えるものである。一般的なSNSAPIとして、投稿や削除を行うには「Memberのトークンを取得」しMemberのトークンによってMemberのアカウントを操作する。

Appのトークンは無制限に使えるが、Memberのトークンは24時間でAppトークンによって再発行する必要がある。

f:id:vaaaaaanquish:20170929070101p:plain:w500:h350
トークンの扱い

上記の図では、社内の特定人物のimpersonate_tokenを自由に取得して色々できるように(例えば社長のアカウントから投稿できるように)見えるが、その点は管理者によるaccess_tokenの権限設定によってコントロールするのがWorkplaceAPIの思想だと思われる。
多分、中央管理する管理者がコントロールしやすいように。

 
前述した通り、Pythonではrequestsモジュールを基本的に使う。入って無ければpipで導入。

sudo pip install requests

参加している全ユーザの名前とidの取得

impersonate_tokenを取得せず、その企業に参加しているユーザのidと名前、Adminかどうか取得する。
これらはAppのaccess_tokenだけでもできる。

import requests
import json

TOKEN = "管理者より得たAppのAccessトークン"
GRAPH_URL_PREFIX = "https://graph.facebook.com/"
COMMUNITY_ID = "管理者より得たコミュニティID"

headers = {'Authorization': 'Bearer ' + TOKEN}
graph_url = GRAPH_URL_PREFIX + COMMUNITY_ID + "/members"
result = requests.get(graph_url, headers=headers)
result_json = json.loads(result.text)

この状態で25件までユーザ情報の取得が可能。

それ以上の情報を得るには返却された結果の['paging']['next']に入っているURLを叩く。
以下のようなコードを追記すればよい

while(1):
    result = requests.get(result_json['paging']['next'], headers=headers)
    result_json = json.loads(result.text)
    if "next" not in result_json['paging'].keys():
        break
    print(result_json['paging']['next'])

また、一回で取得できる量や、内容はパラメータによって設定できる。
例えばユーザが1000人程でidのみ欲しい場合は、whileで回さず以下のようなURLを叩くと一発で返ってくる。

graph_url = GRAPH_URL_PREFIX + COMMUNITY_ID + "/members?fields=id&limit=1500"

 

メンバーのimpersonate_tokenの取得

ユーザのidは上記方法で取得するか、ユーザのプロフィールページのURLにidというパラメータで記載されている。
例:https://hogehoge.facebook.com/profile.php?id=100000000000081

そのidに対してimpersonate_tokenを取得するには以下のURLをフィードの取得と同じようにgetで叩く。

import requests
import json

TOKEN = "管理者より得たAppのAccessトークン"
GRAPH_URL_PREFIX = "https://graph.facebook.com/"

headers = {'Authorization': 'Bearer ' + TOKEN}
graph_url = GRAPH_URL_PREFIX + "100000000000081" + "?fields=impersonate_token"
result = requests.get(graph_url, headers=headers)
result_json = json.loads(result.text)

print(result_json["impersonate_token"])

このimpersonate_tokenを利用してユーザに成りすます形で投稿や情報取得を行う事ができる。

 

メンバーの情報の取得

上記のidには、impersonate_token以外にEmailや電話番号、姓名、部署やプロフィール、ヘッダ画像といった情報も紐付いており、それらもAppのaccess_tokenによって取得が可能である。
詳細なフィールドは以下のReferenceのFieldsを参考に。
Member - Workplace - ドキュメンテーション - 開発者向けFacebook

 
例えば参加しているユーザの「Email」「名字」「impersonate_token」「画像URL」を取得したい場合は、上記のimpersonate_tokenを取得する時のコードを参考にURLのフィールドを以下のように変更する。

graph_url = GRAPH_URL_PREFIX + "100000000000081" + "?fields=impersonate_token,email,first_name,picture"

これによりユーザの情報取得が可能である。

上記参考にしているReferenceのURLのEdges 以降はimpersonate_tokenが必要になってくる。


 

- impersonate_tokenによる情報取得 -

impersonate_tokenを取得する事で、そのユーザに成りすます形でWorkplace上の情報の取得編集が可能である。

以降は上記したimpersonate_tokenを取得する方法24時間以内に取得したimpersonate_tokenがある状態で進める。

 

ユーザプロフィール画面のフィードの投稿取得

WorkplaceにはFacebook同様、自分のフィードが用意されている。

f:id:vaaaaaanquish:20170929104457p:plain:w500:h300

最もシンプルに、id=100000000000081のフィードを取得するには以下

import requests
import json

TOKEN = "取得したimpersonate_token"
GRAPH_URL_PREFIX = "https://graph.facebook.com/"

headers = {'Authorization': 'Bearer ' + TOKEN}
graph_url = GRAPH_URL_PREFIX + "100000000000081/feed"
result = requests.get(graph_url, headers=headers)
print(json.loads(result.text))

上記コードにより、以下のような結果が返ってくる。

{'data':
  [
    {'message': 'これはテストです',
     'created_time': '2017-09-29T01:36:19+0000',
     'id': 'この投稿のid'}]}

 
このPostには様々なFieldが付随している。詳細は以下。
Post - Workplace - ドキュメンテーション - 開発者向けFacebook

例えば投稿に付随する「permalink_url」や「画像」の情報を取得するには以下のようにfieldsパラメータをつけたURLを叩く。

graph_url = GRAPH_URL_PREFIX + "100000000000081/feed?fields=permalink_url,image"

 
上記方法では25件まで投稿の取得が可能である。
limitは上記access_tokenによる情報取得の時同様limitパラメータを付ける。
また、Feed全般における時系列情報についてはsince等のパラメータの利用も可能である。

以下は10日以内の情報を最大100件取得できるURLを生成している例である。

import datetime

DAYS = 10
SINCE = datetime.datetime.now() - datetime.timedelta(days=DAYS)

graph_url = GRAPH_URL_PREFIX + "100000000000081/feed?fields=permalink_url,image&limit=100&since="
graph_url += SINCE.strftime("%S")

 
パラメータについてはWorkplaceのページには一切解説がなく、FacebookのGraph APIを参考に試行錯誤していくしかない状態である。
Graph API Reference Post /post - ドキュメンテーション - 開発者向けFacebook

中には機能的に動作しないパラメータもあるので注意が必要。

 

ユーザのプロフィール画面の投稿に対するコメントの取得

WorkplaceではFacebook同様、投稿に対するコメントと、そのコメントに対するコメントの二種類がある。

f:id:vaaaaaanquish:20170929111514p:plain:w500:h200

面倒だが、現状「投稿の取得」「投稿のコメントの取得」「投稿のコメントへのコメントの取得」はそれぞれAPIを叩いて行う必要がある。


投稿にはそれぞれ独自のidが振られている。
上記のフィード取得の方法ではレスポンスに['data']['id']が含まれているのでそれ。

また投稿の時間部分のリンクをクリックすると投稿のページに飛べるが、このリンクには以下のようにstory_fbidとidパラメータがあり、こちらをアンダースコアで繋ぎ合わせると投稿のidが作成できる。

f:id:vaaaaaanquish:20170929112642p:plain
例:https://hogehoge.facebook.com/permalink.php?story_fbid=300000000000003&id=100000000000001&pnref=story
上記例における投稿のid:100000000000001_300000000000003

この投稿のコメントを取得するには以下

import requests
import json

TOKEN = "取得したimpersonate_token"
GRAPH_URL_PREFIX = "https://graph.facebook.com/"

headers = {'Authorization': 'Bearer ' + TOKEN}
graph_url = GRAPH_URL_PREFIX + "100000000000001_300000000000003" + "/comments"
result = requests.get(graph_url, headers=headers)
print(json.loads(result.text))

結果としてはこんな感じ

{'data': 
  [
    {'created_time': '2017-09-29T02:11:13+0000',
     'from': {'name': 'ユーザ名', 'id': '投稿したユーザのID'},
     'message': 'テストに対するコメントに対するコメントです', 'id': 'コメントのid'}],
     'paging': {'cursors': {'before': 'WT~', 'after': 'WT~'}, 'next': URL}}

返却されたデータの中に「コメントの投稿id」があるため、それを利用して「コメントへのコメント」を取得する形になる。
やり方は投稿のコメントの取得方法と同じ。

上限は25件のため、アクセストークンによる情報取得と同じようにlimit上限をあげたり、pagingのnextに書いてあるURLを利用して上限の次の情報を取得する。

 

グループ情報の取得

Workplaceにはプロジェクトやチーム、部署ごとにGroupを作って、その中にスレッドを立てて投稿できる機能がある。

f:id:vaaaaaanquish:20170929114735p:plain:w300:h320


グループの名前やidはimpersonate_tokenなしでもaccess_tokenのみで取得できる。
しかし、Workplaceには「参加した人しか見れない非公開グループ(CLOSED)」「グループ一覧にも表示されない秘密のグループ(PRIVATE)」を設定する事が可能で、それらはaccess_tokenでは取得できず、そのグループに参加しているユーザのimpersonate_tokenのみで取得が可能となる。


全てのグループ情報を取得するには、フィードの取得同様getで以下のURLを叩く。

GRAPH_URL_PREFIX = "https://graph.facebook.com/"
COMMUNITY_ID = "管理者より得たコミュニティID"
graph_url = GRAPH_URL_PREFIX + COMMUNITY_ID + "/groups"
||< 

以下のような、グループの名前とグループid、そのグループの状態を含めた結果が帰ってくる。
>|json|
{'data':
  [
    {'name': 'test',
     'privacy': 'OPEN',
     'id': '1000000000000006'},
    {'name': 'hogehoge',
     'privacy': 'CLOSED',
     'id': '1000000000000001'},
    {'name': 'TeamA',
     'privacy': 'SECRET',
     'id': '1000000000000003'}],
 'paging': {'cursors': {'before': 'QV', 'after': 'QV'}, 'next': URL}}

アクセストークンによる情報取得と同様、デフォルトでは一度に取得できる件数が25なので、limitを上げるかnextを見てさらに追加で情報を取得する。

 
グループ情報に付随するFieldにはiconやDescription、グループ管理者等があり、以下を参考にフィールドを追加する形で取得が可能である。
Group - Workplace - ドキュメンテーション - 開発者向けFacebook
 
特定の1グループに関する周辺情報を取得するには、グループidに対してリクエストを叩く。
グループIDは上記のグループリストのような形で取得する他、グループページのURLからも判断できる。
例:https://hogehoge.facebook.com/groups/1000000000000006/
例のURLのグループID:1000000000000006

例えば上のグループには以下のようにfieldsを指定したURLを叩く。

GROUP_ID = "1000000000000006"
graph_url = GRAPH_URL_PREFIX + GROUP_ID + "?fields=cover,description,name"

上記例ではカバー画像のURLやグループの名前、説明を取得している。
レスポンスが大きくなりそうな情報(メンバーのリストやファイル情報)については、Edge情報として下記のやり方で取得する。

 

グループの関連情報の取得

上記のグループの情報において、Edgeになる情報は別の形で取得する。
以下のEdgeを参考。
Group - Workplace - ドキュメンテーション - 開発者向けFacebook

取得するにはURLの末尾を以下欲しい情報に合わせて叩く

  • /admins グループ管理者
  • /albums グループに投稿された画像群
  • /docs グループに投稿された文書
  • /events グループで開催されているイベント
  • /files グループに投稿された上記以外のファイル
  • /member_requests 現在のメンバーリクエスト(鍵グループのみ)
  • /members メンバーのリスト
  • /moderators グループ作成者 
  • /feed (後述)

例えばGROUP_ID = "1000000000000006"に対してグループの管理者が知りたい場合は、以下のようなURLを叩く。

GROUP_ID = "1000000000000006"
graph_url = GRAPH_URL_PREFIX + GROUP_ID + "/admins"

一回のリクエストにおける情報取得件数のデフォルト値は25件であり、アクセストークンによる情報取得と同様に、大きなlimitなどを利用し情報を取得する。

 

グループのフィードへの投稿の取得

グループフィードへ投稿する。
以下のReferenceのEdge情報を参考にする。
Group - Workplace - ドキュメンテーション - 開発者向けFacebook


グループへの投稿のidは、上記グループフィードの情報取得からも得られるが、URLからも判別できる。
例:https://hogehoge.facebook.com/groups/1972434329706466/permalink/1000000000000006
例のURLのグループID:1000000000000006


基本的には個人フィードへの投稿の取得と同じである。
グループのフィードの投稿はidに対して/feedを付ければ取得できる。

例としては以下

GROUP_ID = "1000000000000006"
graph_url = GRAPH_URL_PREFIX + GROUP_ID + "/feed"

個人フィードへの投稿の取得と同様に、デフォルトでは25件まで投稿の取得が可能である。
limitパラメータ、sinceパラメータの利用も可能である。

  

投稿に対するコメントの取得

ユーザのプロフィール画面の投稿に対するコメントの取得と同様にフィードの投稿等のコメントも取得可能であるが、グループへの投稿と個人フィードの投稿に付くIDの形式が違うので注意する必要がある。


取得の方法は同じで、投稿ID + "/comments"のURLを叩く。

graph_url = GRAPH_URL_PREFIX + "100000000000001" + "/comments"

コメントのコメントの取得の方法もユーザのプロフィール画面の投稿に対するコメントの取得に書いてある方法と同様の形で取得できる。

 

投稿に対するLike数やメディア情報の取得

WorkplaceにはFacebookと同じくコメントに「ライク」「リアクション」を付ける事ができる。
また、投稿に対して動画や画像、ワードファイル等を直接貼り付ける事ができる。


コメントに付随するそれらの情報を取得するには、上記の投稿へのコメントの取得と同じように投稿idに対するEdgeにそれぞれ欲しい情報の名前を付けたURLを叩く。

以下のReferenceのEdge情報を参考にする。
Group - Workplace - ドキュメンテーション - 開発者向けFacebook


例えば、投稿idが100000000000001の投稿に対してライクした人を知りたければ以下のURLを叩く。
(グループへの投稿と個人フィードの投稿に付く投稿idの形式がかなり違うので戸惑うがやり方は同じ)

graph_url = GRAPH_URL_PREFIX + "100000000000001" + "/likes"

これでLikeしたユーザの名前とidのリストを取得できる。

リアクションした人のリストは/reactions、動画や文書添付情報は/attachmentsを叩く。

 

ユーザのDMの取得

ユーザのDMの取得には、ユーザIDとimpersonate_tokenを利用する。
一応Referenceには、impersonate_tokenが示す当人のDMしか取得できないことになっている。
Graph API Reference: User conversations - Documentation - Facebook for Developers


ユーザIDはユーザのimpersonate_tokenの取得時にも出てきた個人のID。

GRAPH_URL_PREFIX + USER_ID  +"/conversations?fields=messages{message,attachments}

上記にはfieldsパラメータを付与しているが、現時点ではそれらが無いと取得できなかった。

リファレンスでは、Creating、Updating、Deletingも「You can't perform this operation on this endpoint.」となっているため、現状DMの操作は取得だけでDMを送る、更新する、削除するといった操作は記事執筆時点ではできない


 

- impersonate_tokenによる投稿 -

impersonate_tokenを取得する事で、そのユーザに成りすます形でWorkplace上への投稿が可能である。

以降は上記したimpersonate_tokenの取得の方法で24時間以内に取得したimpersonate_tokenがある状態で進める。

 

プロフィール画面のフィードへの投稿

requestのPOSTメソッドを使ってユーザとして投稿する。


ユーザIDが100000000000081のユーザのフィードに投稿するには以下のようにIDのfeedエッジに対してPOSTを叩く

import requests
TOKEN = "取得したimpersonate_token"
GRAPH_URL_PREFIX = "https://graph.facebook.com/"
headers = {'Authorization': 'Bearer ' + TOKEN}

USER_ID = "100000000000081"
graph_url = GRAPH_URL_PREFIX + USER_ID + "/feed"
data={
    "message":"hello",
    "link":"https://developers.facebook.com/docs/workplace/custom-integrations/apps"}
requests.post(graph_url, headers=headers, data=data)

f:id:vaaaaaanquish:20170929145931p:plain:w400:h280

投稿者名の隣にtool_appなどApp名が表示されるので、APIから投稿しているかどうかがわかる。

また、POSTには以下のような情報を付与できる。
Post - Workplace - ドキュメンテーション - 開発者向けFacebook

例えば以下

  • message 本文
  • formatting 本文をマークダウンにするか普通のテキストにするか
  • link リンク情報
  • permalink_url 投稿の固定リンク
  • picture 添付する画像のURL
  • place 位置情報
  • poll 投票
  • properties 動画やドキュメントの詳細
  • type 添付ファイルのタイプ
  • to 投稿に紐づけるユーザ
  • with_tags ポストにつくタグ

よしなにdataの中身に付ければつく。

 

グループへの投稿

上記のフィードへの投稿のUSER_IDがGROUP_IDに変わるだけ

graph_url = GRAPH_URL_PREFIX + GROUP_ID + "/feed"

 

投稿へコメントする

全ての投稿には投稿IDが付いており、そのIDのcommentsエッジに対してポストする。
投稿IDは、上記プロフィールフィードのコメント取得グループフィードのコメント取得を参照。


上記のフィードへの投稿を参考にポストする。

graph_url = GRAPH_URL_PREFIX + 投稿のID + "/comments"
requests.post(graph_url, headers=headers, data={"message":"hello"})

f:id:vaaaaaanquish:20170929151258p:plain:h220:w450

投稿の投稿にコメントする場合も同様に、投稿idのcommentsエッジに対してpostする。

 

投稿のフォーマット

最近やっとAPI経由での投稿でMarkdown形式やユーザへのリプライが可能になった。
Workplace公開当初、いち早く弊社は導入しAPIも真っ先に触ったがこの辺りが真っ当に動かず本当にアレだった。


フィードへの投稿を参考に、message内で@[ユーザID]とする事でリプライ、formattingにMARKDOWNを指定する事でマークダウン形式での投稿ができる。

使える構文は以下のReferenceページが参考になる。
Post - Workplace - ドキュメンテーション - 開発者向けFacebook

例を示すとこんな感じ

data = {
    "message":"[@100013051545981]\n``` code block ```",
    "formatting":"MARKDOWN"}
requests.post(graph_url, headers=headers, data=data) 

また、最近ではpoll情報をdataに追加することで投票投稿なども作れるようになった。

f:id:vaaaaaanquish:20170929152545p:plain:h200:w400


 

- おわりに -

WorkplaceのGraphAPIは公開当初こそ使い物にならないレベルだったが、最近はよくなっている。

通知の取得とかDM操作ができない、Likeやリアクションができない、ビデオ配信機能もあるのに取得できない…みたいな不満は多々あって、一応Workplaceの公式ユーザページで文句は出ているが・・・


以前技術ブログじゃないほうで社内SNSのススメという記事を書いたが、この時よりは幾分かマシになっていると思う。

vaaaaaanquish.hatenadiary.jp


更新にもスピード感があるし、それだけエンジニアが投入されているという証だろうか。


 

UbuntuにPythonのWebスクレイピングと自然言語処理環境を作るメモ

- はじめに -

Webから文章を取得して、自然言語処理かけた後に機械学習にかけるみたいな事はままある。

大体Docker使えば良いんだけど、そうじゃないんだよなという時のための個人的なメモ。

Ubuntu 16.04でPython3.xなら大体インストールできるはず。


 

- スクレイピング周り -

lxmlなるHTML解析パーサに関連したパッケージを入れた後に下記を導入する

  • joblib 並列稼働
  • selenium ブラウザ操作
  • cchardet 文字コード推定
  • requests HTTP通信
  • BeautifulSoup4, lxml HTML解析
sudo apt-get install -y libxml2-dev libxslt1-dev
sudo pip install joblib selenium cchardet requests BeautifulSoup4 lxml

 
以下参考に使いたいheadlessブラウザを導入し動作チェックをしておく。

・PhontomJS:
PythonでWebスクレイピングする時の知見をまとめておく - Stimulator

wget -O /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
cd /tmp
bzip2 -dc /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar xvf -
sudo mv /tmp/phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin/
phantomjs --version

 
・headless Chrome:
headless chromeをPythonのseleniumから動かして引数を考えた (Ubuntu 16.04) - Stimulator

wget https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip
unzip chromedriver_linux64.zip
sudo mv chromedriver /usr/local/bin/
sudo apt-get install libappindicator1
sudo touch /etc/default/google-chrome
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb

 
・headless Firefox:
Firefox headlessモードをUbuntuとPythonとSelenium環境で動かす - Stimulator

sudo apt-add-repository ppa:mozillateam/firefox-next
sudo apt-get update
sudo apt-get install firefox
wget https://github.com/mozilla/geckodriver/releases/download/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz
tar -zxvf geckodriver-v0.18.0-linux64.tar.gz
sudo cp ./geckodriver /usr/local/bin


 

- 自然言語処理周り -

Mecab

事前にhttp://taku910.github.io/mecab/#download から最新リリースのmecab-hoge.tar.gzをダウンロードしてscpしておく

tar xvf mecab-*.tar.gz
cd mecab-*
 ./configure --prefix=$HOME/local --with-charset=utf8 --enable-utf8-only
make
make install

bashrcに書く

export PATH="$HOME/local/bin:$PATH"

 
・IPAdic

事前にhttp://taku910.github.io/mecab/#download から最新リリースのmecab-ipadic-hoge.tar.gzダウンロードしてscpしておく

tar xzvf mecab-ipadic-*.tar.gz
cd mecab-ipadic-*/
./configure --with-charset=utf8 --with-dicdir=$HOME/local/lib/mecab/dic/ipadic
make
make install

 
・neologdなるIPAdicの拡張辞書

git clone https://github.com/neologd/mecab-ipadic-neologd.git
cd mecab-ipadic-neologd/
sudo bin/install-mecab-ipadic-neologd -n -u


 

- Python周り -

MeCabPythonバインディングや前処理モジュールのneologdn、正規化の分散表現にしたい場合やBoWとかも使いたいだろうからgensim、Word2vec、sklearnを入れてひとまず下準備おわり。

sudo pip install mecab-python3
sudo pip install neologdn
sudo pip install numpy
sudo pip install scipy
sudo pip install gensim
sudo pip install cython
sudo pip install word2vec
sudo pip install scikit-learn
sudo pip install git+https://github.com/miurahr/pykakasi

厄介なのはpykakasiくらい。
(pipで普通にインストールするとビルドで死ぬ)


neologdn参考:mecab-neologd 前処理用 Python モジュール neologdn 公開しました - Debug me
pykakasi参考:Django Python 漢字・ひらがな・カタカナをローマ字に変換する~pykakasi - Djangoroidの奮闘記


 

- おわりに -

多分これでなんとかなると思います(自分宛て)。


 

Slack botをGASでつくる方法で一番楽そうなやつ

- はじめに -

正直今時AWS LambdaがSlackサポートしていてポチポチやってスクリプト数行でbotが出来るし、フレームワークも充実しているので、何故今更GASなのかと思ったらブラウザバックした方が良い。
hubotもAWSも実質サーバ代がかかるけど `GASは無料` で `Google Driveの中身を触れる` くらいしかメリットがない。

投稿するだけなら以下の記事のようにIncoming Webhooksだけ設定して適当な所からPOSTするので良い
vaaaaaanquish.hatenablog.com


 
それでもGoogle Apps Scriptで簡単に応答するbotが作りたいんじゃいという記事。

Slackにbotもどきを導入する方法はいくつかあるが、今回目指すのは以下のようにbotがリプライできて、リプライ内容に対してbotが返信してくれる状態。
f:id:vaaaaaanquish:20170927155852p:plain:h170:w300

結論からいうとSlackにAppでbot追加するのと、Outgoing WebHooksを組み合わせるのが現状最善手っぽい。


 

- Slack側の色々 -

Slackにbotらしきものを導入する方法はいくつかある。

Custom Integrations には Bots や Outgoing WebHooks、Incoming Webhooksが用意されているし、公開AppにはHubot連携等のbot作成フレームワークとの連携Appがいくつかある。

本記事では簡単に「独自App追加」と「Outgoing WebHooks」だけでSlack botを実現する。

 

Slackにbotを追加して投稿する設定

SlackのAppページから新しいAppを作成
https://api.slack.com/apps

f:id:vaaaaaanquish:20170927162923p:plain:h200:w400

アプリ名と登録したいチャンネルを選んでCreate App
f:id:vaaaaaanquish:20170927163027p:plain:h200:w200

画面が遷移しApp管理画面に移る
 
左タブからBot Userを選択、適当な名前でbotを追加し、Always Show My BotをOnにしてSave。
f:id:vaaaaaanquish:20170927163157p:plain


このBotから投稿できるよう、左タブからIncoming Webhooksを選択。
f:id:vaaaaaanquish:20170927163716p:plain:h300:w300

認証画面が出るのでここで投稿したいチャンネルを選んでAuthorize。
f:id:vaaaaaanquish:20170927163815p:plain:h200:w250

 
Botの投稿URLが生成されるのでコピーしておく。
f:id:vaaaaaanquish:20170927164040p:plain:h250:w300


ちなみにこの時点でコンソール等からURLに対してPOSTしてやれば試せる(まあ試さなくても大体できてる)。

curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/~~~

あとこの時点で、botデフォルトのicon等が設定でき、左タブのSettings>Basic Informationに飛んで下の方にスライドしていくと設定する箇所がある。

 

GASからSlackにPOST

GASが入ってない場合は新規でアプリの追加すれば、Google Drive上から右クリックメニュー>その他>Google Apps Scriptが選択出来るようになる。
f:id:vaaaaaanquish:20170927165029p:plain:w300:h300

GASでSlackする記事を見ると、大抵soundTrickerさんが作ったLibraryを使った方法が出て来るが、Slack APIのTokenが必要な事と拡張できないので使わない。

もしライブラリを使うなら以下が導入で分かりやすいかも。
Slack BotをGASでいい感じで書くためのライブラリを作った
GASとSlackではじめるチャットボット〜初心者プログラマ向け〜
初心者がGASでSlack Botをつくってみた - CAMPHOR- Tech Blog

 
先程取得したbotのURLを指定してGASのコードをコピペして、test関数をGASエディタの上部バーの再生ボタンっぽいやつで実行。

function postSlack(text){
  var url = "https://hooks.slack.com/services/T~~~~";
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload" : '{"text":"' + text + '"}'
  };
  UrlFetchApp.fetch(url, options);
}

function test(){
  postSlack("これはテストです");
}


f:id:vaaaaaanquish:20170927165343p:plain
注意するとしたらpayloadは {"text": "hoge"} という内容のStringであるという事くらいか。

Slackの投稿フォームで@を押した際に出るサジェストにbotが追加されている事も確認する。

 

SlackからGASでリプライを受け取る

本来なら上記のAppでリプライを受け取れたり出来れば良いのだが、現状できなさそう。
一般的な Browse Apps > Custom Integrations > Outgoing WebHooks を使って受け取りを行う。

 

GAS側の設定


GASはdoPost()なるイベントハンドラがデフォルトで用意されているので、doPostを適当に書けばHTTPメソッドでPOSTされた内容を受け取る事ができる。GETされる場合はdoGETもある。
先程のコードに以下を追記する。

function doPost(e) {
  var message = "こんにちは " + e.parameter.user_name + "さん";
  postSlack(message)
}

Slackから受け取った内容からuser_nameを見て返すというシンプルなコード。
Slackの場合だとe.parameter.textで投稿内容を取得できますが今回はスルー。

 
上記を追記し保存したら、GASをWebアプリケーションとして公開する。

GASエディタ画面の上部から公開を選択、ウェブアプリケーションとして導入する。
f:id:vaaaaaanquish:20170927172638p:plain:h200:w200

プロジェクトバージョンを新規作成して、アプリケーションの実行者を自分に、全員が実行できるよう設定して公開する。
f:id:vaaaaaanquish:20170927173148p:plain:h200:w200

最初だけ許可の確認があるかもしれないので許可。

こんな感じで公開されたURLが出て来るので保存しておく。
f:id:vaaaaaanquish:20170927173413p:plain


GASの一番面倒くさいところは、この「プロジェクトとして公開する」ところにある。

ウェブアプリケーションとしてAPIのような形で公開していく場合、スクリプトの保存に加えて、上記の「ウェブアプリケーションとして導入」「プロジェクトバージョンを新しく作成」「公開」の手順を毎回踏む必要がある。
(スクリプトの保存だけして新バージョンとして公開してなかった〜という事が多々発生する)

僕は適当にバージョン1、バージョン2、…と命名しているけど、大体どっかで破綻してしまうので何かいい方法が欲しい…

 

Slack側の設定

自分のSlackチーム名を確認してappsに飛ぶ

https://自分のSlackチームドメイン.slack.com/apps

検索窓があるので Outgoing WebHooks を検索。
f:id:vaaaaaanquish:20170927171320p:plain

Add ConfigurationするとWebhock Integrationボタンがある画面に遷移するのでIntegration。

f:id:vaaaaaanquish:20170927174105p:plainf:id:vaaaaaanquish:20170927174139p:plain

 
それっぽい設定画面に遷移したら少し下にスクロールして、Integration Settingを行いSaveする。
f:id:vaaaaaanquish:20170927180319p:plain:h200:w350

URLは上部GAS側の設定時に取得したウェブアプリケーションのURL。

Trigger Wordは、Slack上の投稿の先頭を見て引っ掛ける単語で、カンマ区切りで設定できる。

test,bot,@,&

みたいに複数設定できるのだが、例えば@だと

@hoge こんにちは

が取得できない。
理由は@hogeとした場合、リプライやコマンド形式となり、リンクになるから。

リプライをcatchしたい時は、実際Botへリプライして右クリックでリンク取得し、URL末尾のIDを<@ >で挟む。
f:id:vaaaaaanquish:20170927181412p:plain

上記だとhttps://hogehoge.slack.com/team/U78NAL1QQ になるので <@U78NAL1QQ> をtrigger wordに設定しておけば良い。


詳しくは以下。
https://api.slack.com/docs/message-formatting


ちゃんとSaveする。


 
上記設定した上でBotに向けてリプライを飛ばせば返答が得られる。
f:id:vaaaaaanquish:20170927181858p:plain



 

- おわりに -

多分GAS使うならこれが一番楽です。

App上でOutgoing WebHooksも作れたら本当はもっと楽ですが…

一番ラクなのはGASを使わずにAWSに課金してLambdaでポチポチやる事だと思います(Python書けるし…)

あと少しだけGASで運用してたbotに関する記事を投稿すると思います。