DeepLearningの学習経過をslackで受け取れるようにする

# モチベーション
DeepLearningの学習って結構時間がかかりますよね? (数時間とか、ヘタしたら数日とか・・・)
手元のPCで学習が回っていて常に状況を確認できるならともかく、リモートのPCで学習は回っていたり、大体時間がかかるからってどっかに出掛けてみたりするじゃないですか。
でも学習が進んでいるかどうかは定期的に知りたい。もっと言えば、エラーで途中終了しているのはすぐにでも知りたい（学習終わってるかな？って思って見てみたら止まってた・・・(泣)みたいなのは避けたい）。
そこで、[Slackのpython-slackclient](https://github.com/slackapi/python-slackclient)を使って学習結果をslackのchannelに定期的に通知する方法を紹介します。
前半は単純にpython-slackclientの簡単な使い方、後半は[chainer](https://github.com/chainer)のExtensionとしてこのSlack通知機能を実装する方法を説明します。

こんな感じを目指します↓
![slack-bot.png](https://qiita-image-store.s3.amazonaws.com/0/43125/69581633-cbaf-66f3-0ec7-e03cef7f1972.png)


# python-slackclientの使い方
## 0. Slackのワークスペースを作成 (Optional)
Slackのアプリ(Android, iPhone, Mac, Windows, Linuxどれでも！)からとりあえず適当にワークスペースを作ります。
ワークスペースの作り方がわからない場合は[こちら](https://get.slack.help/hc/ja/articles/206845317-Slack-%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)の本家のページを見てみてください。

## 1. 通知をするためのslack appを作成する
[ここから](https://api.slack.com/slack-apps)、Create a slack appをクリックして指示にしたがって新しくアプリを作ります。![build-slack-app.png](https://qiita-image-store.s3.amazonaws.com/0/43125/a7953424-e075-8717-b69d-0e073e772833.png)

## 2. 作ったslack appにメッセージの送信を許可する
アプリ作成後、アプリの管理画面から、FeaturesにあるOAuth & Permissionsを選択します。
![manage-app.png](https://qiita-image-store.s3.amazonaws.com/0/43125/3ce32247-3bdd-091a-a6cb-53915d233176.png)
その後、真ん中くらいにあるSelect Permission Scopesを表示し、選択画面から、send messages as [作ったappの名前] chat:write:botを選択、設定を保存します。


## 3. tokenの取得
今回はSlackのWebapiをpythonから使うので、そのためのtokenを取得します。
2と同じく、OAuth & Permissionsから、一番上にあるOAuth Access Tokenをコピーして適当に保存します。
ここではslack_tokenという名前のファイルに保存したとして話を進めます。

## 4. channelにメッセージを送ってみる
ためしに、tokenを使ってメッセージが送れるか確認します。これができたら基本的な設定は終了です。
まずslackのwebapiをpythonから使えるようにするためslackclientを入れます

```bash
pip install slackclient
```

あとは次のお試しコードを使ってメッセージが送信できればokです。（channelは適宜変えてください。とりあえずgeneralに送るようになっています）

```python
from slackclient import SlackClient

def send_message(client, text, channel):
    client.api_call(
        "chat.postMessage",
        channel=channel,
        text=text
    )

def read_text(filename):
    with open(filename, 'r') as f:
        return f.read()

def main():
    token = read_text('slack_token')

    sc = SlackClient(token)
    text = "Hello from Python! :tada:"
    channel = "general"
    send_message(sc, text, channel)


if __name__ == '__main__':
    main()
```

# chainerのExtensionとして実装する
ここまでで、slackにメッセージを送れるようになったので、ログ出力の要領で使えばもうやりたいことはだいたいできるのですが、chainerのExtensionとして実装しておくと、chainerのtrainerを使った時に便利なので、Extensionとして実装する方法を説明します。

## Extensionとは?
chainerの[trainer](https://docs.chainer.org/en/stable/reference/generated/chainer.training.Trainer.html)を使って学習をさせるときに、学習経過に合わせてパラメータを変更したり、ログをとったり、今回のようにメッセージを送ったりするためにtrainerに付け足すことのできる拡張機能のようなものです。
具体的なtrainerとextensionの関係は[chainerのmnistのチュートリアル](https://docs.chainer.org/en/stable/examples/mnist.html#)がわかりやすいので見てみてください。

## Slackにメッセージを送るExtensionを作成する
Extensionの作り方はfunctionを拡張するものやExtensionクラスを継承するものなどいくつかあるようですが、ここでは後者のExtensionを拡張する形で、slackにメッセージを送るExtensionの作り方を説明します。

### extionsion.Extensionを拡張したクラスをつくる
[chainer本体にあるPrintReport](https://github.com/chainer/chainer/blob/v4.3.1/chainer/training/extensions/print_report.py#L9)を参考に、SlackReportというクラスを作ります。(名前は適宜変更してください)
ここでは、entriesで指定したログをslackに送信するために、指定されたkeyを使ってheaderの作成と、実際に出たログの長さに合わせて表示するためのtemplateを作成しています。

```python
from chainer.training import extension
from chainer.training.extensions import log_report as log_report_module
from slackclient import SlackClient

class SlackReport(extension.Extension):
    def __init__(self, token, entries, channel='training report', log_report='LogReport'):
        self._slack_client = None if token is None else SlackClient(token)
        self._entries = entries
        self._channel = channel
        self._log_report = log_report

        # format information
        entry_widths = [max(10, len(s)) for s in entries]

        self._header = '  '.join(('{:%d}' % w for w in entry_widths)).format(
            *entries) + '\n'

        templates = []
        for entry, w in zip(entries, entry_widths):
            templates.append((entry, '{:<%dg}  ' % w, ' ' * (w + 2)))
        self._templates = templates
```

### 学習開始、終了時にメッセージを送信する。
trainerは学習開始と終了時に必ずすべてのextensionのinitialize(trainer), finalize()メソッドを呼ぶようになっているので、これを利用して学習開始、終了をslackに通知します。
特にfinalizeを実装しておくことで、学習が終了したタイミングを知ることができるのでおすすめです。(Exceptionによる異常終了時も含む)
ここで使っているsend_messageは前半部分で説明したslackclientの使い方で書いたものと同じです

```python
    def initialize(self, trainer):
        send_message(client=self._slack_client, text='--------training started--------', channel=self._channel)

    def finalize(self):
        send_message(client=self._slack_client, text='--------training finished--------', channel=self._channel)
```

### 定期的にメッセージを送信する
trainerはextensionを追加したときに指定するtriggerの間隔に合わせて、extensionを呼ぶので、呼ばれた時にslackにログのメッセージを送るようにします。
ポイントはメッセージをひとつのtextとして連結して、send_messageでslackに通知するようにしておくことです。

```python
    def __call__(self, trainer):
        if self._slack_client == None:
            return

        log_report = self._log_report
        if isinstance(log_report, str):
            log_report = trainer.get_extension(log_report)
        elif isinstance(log_report, log_report_module.LogReport):
            log_report(trainer)  # update the log report
        else:
            raise TypeError('log report has a wrong type %s' %
                            type(log_report))

        message = self._header
        observations = log_report.log
        for observation in observations:
            for entry, template, empty in self._templates:
                if entry in observation:
                    message += template.format(observation[entry])
                else:
                    message += empty
            message += '\n'
        send_message(client=self._slack_client, text=message, channel=self._channel)
```

### trainerにExtensionを追加する
最後にtrainerにExtensionを追加して、slackに通知が行くようにしましょう。
追加は簡単でtrainerのextendで先ほど作ったSlackReportを指定するだけです。
このときtriggerを指定しないと毎iterationごとに呼ばれるようになってしまって、通知の嵐になるので、適当に(イテレーション数, 'iteration')や(エポック数, 'epoch')といったように指定して、ある程度学習が進んだタイミングで通知が来るようにしておくと便利です。

```python
    trainer.extend(SlackReport(args.token,
                               entries=['iteration', 'main/loss','validation/main/loss', 'elapsed_time'],
                               channel='general'), 
                               trigger=(100, 'iteration'))
```

# おわりに
いかがでしたでしょうか？簡単に通知が送れるのがわかったかと思います。
みなさんもぜひこのslack通知機能を取り入れて快適なdeeplearningライフを送ってみてはいかがでしょうか。