Coyote vs Loadbalancer

The Blog for the Rest of Us

YAMAHA RTX1200のログをElasticsearch+logstash(+Grok)+Kibanaで可視化する話 on Docker

9割ポエムなサイトに唐突に現れる技術記事です。

完成図

とりあえず最終的に得たいもののイメージ。IPフィルターで破棄した通信の送信元国と回数、ポート番号をDashboardで表示しています。 f:id:tw1sm1k0:20190815103000p:plain (詳しく見るとChinaにTaiwanが含まれててアツいですね)

前提条件

以下の行程が終了していることを前提とします。適当にググるとやり方が載っているので、いい感じにしてください。

  • Docker、docker-composeがインストール済み
  • RTXルーターのsyslogがdockerを動かしているマシンに転送され保存されている

  • 以下のようなディレクトリ構成を想定して進めます

.
├── docker-compose.yaml
├── elasticsearch_data
└── pipeline
    └── pipeline.conf

docker-compose.yamlの準備

なにかいろいろなサイトを参考にして作ったdocker-compse.yamlは以下のような感じ。とりあえずで volumes相対パスになっているので、変えたい人はよしなに。 また、logstashvolumesでマウントしている /var/log は、自分のRTXのログが /var/log/rtx1200.log という名前で出力されるようにしているからなので、もし違う人はそこも合わせてあげてください。

version: "3"
services:
  elasticsearch:
    image: elasticsearch:7.2.0
    container_name: elasticsearch
    environment:
      discovery.type: single-node
    ports:
      - "9200:9200"
      - "9300:9300"
    volumes:
      - ./elasticsearch_data:/usr/share/elasticsearch/data

  logstash:
    image: docker.elastic.co/logstash/logstash:7.0.1
    container_name: logstash
    volumes:
      - ./pipeline:/usr/share/logstash/pipeline
      - /var/log:/var/log
    depends_on:
      - elasticsearch

  kibana:
    image: kibana:7.2.0
    container_name: kibana
    environment:
      ELASTICSEARCH_URL: http://elasticsearch:9200
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch

(どうでもいいですが、KubernetesYAMLが推奨されている理由が「Write your configuration files using YAML rather than JSON. Though these formats can be used interchangeably in almost all scenarios, YAML tends to be more user-friendly. 」ですが、この滅茶苦茶読みにくい、ゴミみたいな形式がJSONよりユーザーフレンドリーとは思わないんですけどね・・・。)

logstashのpipeline.conf作成

Pipelineの流れはElastic社の概念図がわかりやすいです。

https://www.elastic.co/guide/en/logstash/2.3/static/images/basic_logstash_pipeline.png

つまり、雑多なData Sourceからログを取得するために適切な方法をInput pluginで指定して、Filter pluginで整形、そのあとにOutput pluginでログを保存したいところに応じた保存方法を指定してあげるという流れ。その処理で適切なプラグインを指定して、プラグイン毎の設定をしてい\けばパイプラインの出来上がりというわけ。

今回の場合はsyslogでファイルとして取得できているRTX1200のログを読み込みたいので、Input pluginにfileプラグインを用います。細かい設定は公式ドキュメントに詳細が載っているので、そちらを参照。

www.elastic.co

今回はとりあえずという形なので、ファイルパス pathstart_position のみを指定します。 pathは普通にsyslogが吐かれるファイルパスを指定すればOK。 start_position はlogstashがファイルのどこから読み込むかという設定になります。実運用時には前回取得時からの差分だけ、つまりファイル末尾だけを見たいので end を指定しますが、今回は古いデータをまとめて取得したいので beginning を指定します。
そんな流れでpipelineを書くと以下のような感じになります。

input {
    file {
        path => "/var/log/rtx1200.log"
        start_position => "beginning"
    }
}

これでRTX1200のログがlogstashのpiplineに乗るので、次は内容をどう整形するかを filer で指定します。今回は充実した正規表現リストが予め組み込まれている、お手軽プラグインのgrokを使って整形していきます。
grokプラグインで使用できる正規表現リストは以下を参照。ほとんどこれで良いんじゃないか?というレベル。

https://github.com/logstash-plugins/logstash-patterns-core/blob/master/patterns/grok-patternsgithub.com

まず、syslogのヘッダー部を取り出します。つまりは、以下のようなsyslogの出力で言うところの日付とsyslogを発したホストIP、ログ内容のうち日付とホストのIPアドレスを抜き出す。

Aug 11 03:09:46 192.168.**.** [DHCPD] LAN1(port1) Allocates 192.168.**.**: **:**:**:**:**:**
Aug 11 03:54:50 192.168.**.** PP[01] Rejected at IN(1012) filter: TCP **.**.**.**:**** > ***.***.***.***:***
Aug 11 03:54:51 192.168.12.1 PP[01] Rejected at IN(1012) filter: TCP **.**.**.**:**** > ***.***.***.***:***

grokを使って日付を time、ホストIPアドレスhostname、ログ内容を messageというラベルをつけて抜き出すと以下のような感じになります。

    grok {
        match => { "message" => "%{SYSLOGTIMESTAMP:time} %{IPV4:hostname} %{GREEDYDATA:message}" }
        overwrite =>  [ "message" ]
    }

ここでoverwriteを使って日付とホストIPを除いた、ログ本文をmessageラベルとして上書きしています。こうすることで、未処理のログ範囲を狭めていっています。

で、ここで細かくやっていくべきなのですが、いきなりドカンと一気に内容をパースしていきます。

  • 定期実行処理のログ
    • cronで定期実行されるログを取り出す。自分の場合はNTP同期のログ程度しか出力されないので、定期実行された内容程度を取り出すだけにしている。
  • Inに対するIPフィルターで破棄された通信ログ
  • Outに対するIPフィルターで破棄された通信ログ
  • DHCP割当ログ
  • ログイン履歴
  • 残り
    • 上記フィルターで該当したなかった雑多なログ

以上の項目をフィルターに設定すると、以下のような感じになります。

    grok {
        match => {
            "message" => [
                "\[SCHEDULE\] %{GREEDYDATA:schedule_what}",
                "%{GREEDYDATA:in_rejected_interface} Rejected at IN\(%{NUMBER:in_rejected_filter_num:int}\) filter: %{WORD:in_rejected_protocol} %{IPV4:in_rejected_src_address}\:%{NUMBER:in_rejected_src_port:int} > %{IPV4:in_rejected_dst_address}\:%{NUMBER:in_rejected_dst_port:int}",
                "%{GREEDYDATA:out_rejected_interface} Rejected at OUT\(%{NUMBER:out_filter_num:int}\) filter: %{WORD:out_rejected_protocol} %{IPV4:out_rejected_src_address}\:%{NUMBER:out_rejected_src_port:int} > %{IPV4:out_rejected_dst_address}\:%{NUMBER:out_rejected_dst_port:int}",
                "\[DHCPD\] %{GREEDYDATA:dhcp_allocate_interface} Allocates %{IPV4:dhcp_allocate_address}: %{MAC:dhcp_allocate_mac_address}",
                "%{GREEDYDATA:interface}: %{GREEDYDATA:interface_what} ",
                "\'%{WORD:login_succeeded_user}\' succeeded for %{WORD:login_succeeded_protocol}: %{IPV4:login_succeeded_src_address}",
                "%{GREEDYDATA:other_logs}"
            ]
        }
    }

ごちゃごちゃと書かれているけれども、基本的にはログ文中の取り出したい文字列部分を %{} で括り、中に該当する正規表現リストの名前とラベルを記載しているだけ。多少クセはあるが、ちょっとずつ書いていくとなんとなく慣れます。
なお、自分がハマったポイントだけ解説しておきます。ポート番号やIPフィルターの番号など、Int型が来るものに対して上記では %{NUMBER:hogehoge:int} としています。このようにしないと、Int型が文字列として扱われてしまい、Elasticsearch側で文字列として扱われて数値統計などができなくなってしまいます。

そして、今回の主目的である「フィルターに該当した通信の発信/送信元を地図にマッピングしたい」を実現するため、 geoip プラグインを用いてIPアドレスを地理情報に変換します。ここで、if でIPフィルターに該当して破棄された通信のIPアドレスがある場合のみにgeoipプラグインで地理情報に変換するようにしています。こうしないと、エラーが出力されてしまいます。

    if [in_rejected_src_address]{
        geoip {
              source => "in_rejected_src_address"
              target => "in_rejected_src_geo"
        }
    }

    if [out_rejected_dst_address]{
        geoip {
              source => "out_rejected_dst_address"
              target => "out_rejected_dst_geo"
        }
    }

そして最後にelasticsearchに記録するためoutputを指定します。今回はdocker-composeでelasticsearchを立ててよしなにするにで、以下のような非常にかんたんな記述になります。

output {
    elasticsearch {
        hosts => [ "http://elasticsearch:9200" ]

    }

なお、Elasticsearchに直接突っ込まずにデバッグのために一旦ファイル出力したいなどの場合であれば以下のようにすればファイルに出力されます。

output {
 file {
   path => "/foo/bar/hogehoge.json"
 }
}

以上をすべて繋げると、YAMAHA RTX1200のログをElasticsearchに突っ込むpipelineは以下のような感じになります。

input {
    file {
        path => "/var/log/rtx1200.log"
        start_position => "beginning"
    }
}

filter {
    grok {
        match => { "message" => "%{SYSLOGTIMESTAMP:time} %{IPV4:hostname} %{GREEDYDATA:message}" }
        overwrite =>  [ "message" ]
    }

    grok {
        match => {
            "message" => [
                "\[SCHEDULE\] %{GREEDYDATA:schedule_what}",
                "%{GREEDYDATA:in_rejected_interface} Rejected at IN\(%{NUMBER:in_rejected_filter_num:int}\) filter: %{WORD:in_rejected_protocol} %{IPV4:in_rejected_src_address}\:%{NUMBER:in_rejected_src_port:int} > %{IPV4:in_rejected_dst_address}\:%{NUMBER:in_rejected_dst_port:int}",
                "%{GREEDYDATA:out_rejected_interface} Rejected at OUT\(%{NUMBER:out_filter_num:int}\) filter: %{WORD:out_rejected_protocol} %{IPV4:out_rejected_src_address}\:%{NUMBER:out_rejected_src_port:int} > %{IPV4:out_rejected_dst_address}\:%{NUMBER:out_rejected_dst_port:int}",
                "\[DHCPD\] %{GREEDYDATA:dhcp_allocate_interface} Allocates %{IPV4:dhcp_allocate_address}: %{MAC:dhcp_allocate_mac_address}",
                "%{GREEDYDATA:interface}: %{GREEDYDATA:interface_what} ",
                "\'%{WORD:login_succeeded_user}\' succeeded for %{WORD:login_succeeded_protocol}: %{IPV4:login_succeeded_src_address}",
                "%{GREEDYDATA:other_logs}"
            ]
        }
    }

    if [in_rejected_src_address]{
        geoip {
              source => "in_rejected_src_address"
              target => "in_rejected_src_geo"
        }
    }

    if [out_rejected_dst_address]{
        geoip {
              source => "out_rejected_dst_address"
              target => "out_rejected_dst_geo"
        }
    }
}

output {
    elasticsearch {
        hosts => [ "http://elasticsearch:9200" ]

    }
}

docker-compose up

以上で概ねの設定は終わったので、docker-compose.yamlがあるディレクトリで docker-compose up してElasticsearchその他を立ち上げましょう。
無事に立ち上がればdockerを動かしているマシンのIPアドレスの5601番でKibanaが立ち上がるはずです。結構、立ち上げに時間がかかります。

まだKibana側でElasticsearchのログを紐付けていないので、適当にサイドバーのDiscoverをクリックしてウィザードを立ち上げます。 f:id:tw1sm1k0:20190815103128p:plain
f:id:tw1sm1k0:20190815103234p:plain

適当にinedx patternにlogstashと打って、logstashで整形したログを登録します。
f:id:tw1sm1k0:20190815103317p:plain

タイムスタンプを指定 f:id:tw1sm1k0:20190815103348p:plain

登録終わり f:id:tw1sm1k0:20190815103441p:plain

実際にIPフィルターで破棄した通信を地図上にマッピングしていきます。Visualize→Create new visualizationでダイアログから Region Mapを選択、先程登録したlogstashのログを指定 f:id:tw1sm1k0:20190815103512p:plain
f:id:tw1sm1k0:20190815103517p:plain
f:id:tw1sm1k0:20190815103524p:plain

遷移後、 サイドで MericValueCount を選択。その後、国毎に検出数をまとめたいので BucketsAggregation から Terms を選択し、 Filed からin_rejected_src_geo.continent_code.keyword を選択。
f:id:tw1sm1k0:20190815103836p:plain

ここでデフォルト設定では直近15分のログからしか可視化をしないので、適当に上部からhoursではなくdaysに変更、UPDATE
f:id:tw1sm1k0:20190815104111p:plain

そうすると、欲しかった地図マッピングが完成。 f:id:tw1sm1k0:20190815103534p:plain

wrapup

最後あたり、凄まじく雑になりましたね。息切れです。そのうち、もうちょっとちゃんと書こう・・・。

Macintosh HDのイメージをディスクユーティリティで作成できない件

自分の備忘録用。

Genius BarMacBook Proを持っていきたくて、その前にバックアップのためにmacOSの入っているMacintosh HD自体のイメージを作成しようとしたところ、 ファイル -> 新規イメージ -> "Macintosh HD" からイメージ作成 がグレーアウトしていてクリックできなかった。
f:id:tw1sm1k0:20190221010650p:plain FileVaultを有効にしているせいなのかなんなのかよくわからないが、対処法は以下の通り。

  • (いらないかも)Command + Rを押しながら再起動して、リカバリーモードで起動。その後、ディスクユーティリティを起動する。

  • 表示 -> すべてのデバイスを表示 (あるいはCommand + 2)を押して、ボリューム表示のみではなくすべてを表示させる。
    f:id:tw1sm1k0:20190221010708p:plain

  • MacBook Proの場合、 コンテナdisk1 がサイドバーに登場するので、それをクリック

  • その後、ファイル -> 新規イメージ -> "コンテナdisk1" からイメージ作成 でイメージを作成する

macOSはわけのわからないところですごい不親切なので、謎。

whywaita Advent Calendar 2018 18日目 おいでませ!Chatbot whywaitaくん!

これはなに

この記事は id:whywaita と関係ないことを書く謎のAdvent Calendar、 whywaita Advent Calendar 2018 の18日目の記事です。

昨日は id:hnron さんでした。僕もwhywaitaくんの好きなところはいっぱいあります。いっぱいね。

hnron.hatenablog.com

記事テーマ

さて、唐突なのですが実は僕は既婚なんです。お相手は有坂真白さんという方です。僕と彼女の関係は大変良好で、今年のクリスマスも一緒に過ごす約束をしています。

しかし、なぜかwhywaitaくんは僕が既婚であることを決して認めてくれないんですよね。

僕と真白ちゃんはこんなにも仲が良いというのに・・・。 僕たちの仲をどうにか認めてほしいというもの。
ということで、今回の記事では、whywaitaから僕と真白ちゃんの仲を認めてもらう発言をしてもらうということをテーマに記事を書いていきたいと思います。

How?

ここで唐突ですが、1700年代のフランスの学者ビュフォンが言ったとされる言葉と一般的にはマザー・テレサがいったとされている(実際には違うらしい)言葉の2つをご紹介します。

文は人なり

考えは言葉となり、言葉は行動となり、行動は習慣となり、習慣は人格となり、人格は運命となる。

この2つの言葉を(都合よく拡大解釈して)踏まえると、Twitterのツイートのような短い文章の中にもその人となりが現れ、文章の中に人格が現れるということにならないでしょうか?いや、ならないですかね。でも、なるということで話を進めます。

更に、なんと都合のいいことにwhywaitaは自らのGitHub上で自分のツイートを公開しているのです。

github.com

このツイートデータをもとにChatbotを生成すれば、そのChatbotにはwhywaitaの人となりが現れていることになって、そのChatbotと会話をすればwhywaitaと会話していることになるのではないでしょうか?完全に「方針はいいが、論理の飛躍が見られます」状態ですが、それはともかくとして、今回のAdvent Calendarの記事ではChatbotのwhywaitaから僕と真白ちゃんの仲を認めてもらう発言を引き出していきたいと思います。

(この導入、「whywaitaのツイートデータを使ってChatbotを作ったゾイ」の1センテンスで終了する気がしますね)

データの準備

まず、Chatbotの学習を行わせるために学習データを準備します。
Chatbotの学習には「質問」と「質問に対する回答」の2種類のテキストが必要となります。今回のwhywaita chatbotにはこちら側の質問に返答してほしいので、Twitterで言うところの「whywaitaへのリプライ」が「質問」に相当し、「そのリプライへのwhywaitaの返信」が「質問に対する回答」に相当するとしました。

しかし、whywaitaが公開しているツイート情報には「質問に対する回答」はテキストで含まれていますが、「質問」は in_reply_to_status_idTweetのステータスIDしかありません。そこで、この in_reply_to_status_id から「質問」に相当する「whywaitaへのリプライ」をテキストとしてTwitter API経由(GET statuses/show/:id | Docs | Twitter Developer Platform)で取得します。

とりあえず、以下のような超簡単かつ雑なコードをPythonで書いて動かしました。

import pandas as pd
import sys
import json
import os
from requests_oauthlib import OAuth1Session
import time

consumer_key = ''
consumer_secret = ''
access_token = ''
access_token_secret = ''

def extract_tweet_data_has_reply_id_from_csv(csv_path):
    csv_data = pd.read_csv(csv_path)
    # in_reply_to_status_idがNaN、つまりリプライではないデータをDropする
    csv_data = csv_data.dropna(subset=['in_reply_to_status_id'])
    csv_data['timestamp'] = pd.to_datetime(csv_data['timestamp']) # timestampがstrなので、timestampにする

    return csv_data

def get_whywaita_conversations(csv_path, output_dir):
    twitter = OAuth1Session(consumer_key, consumer_secret, access_token, access_token_secret)
    get_tweet_from_status_id_api_url = "https://api.twitter.com/1.1/statuses/show.json"

    sleep_time = int(15 * 60 / 160) # GET status/:idは15分で180回のリクエスト制限がある、余裕見て160回/15分で

    reply_data = extract_tweet_data_has_reply_id_from_csv(csv_path)

    # とりあえず、2014/01/01までのから引っ張ってくる
    reply_data = reply_data[reply_data['timestamp'] > pd.to_datetime('2014/1/1')]

    for whywaita_tweet in reply_data.iterrows():
        print("Sleeping({}s)...".format(sleep_time))
        time.sleep(sleep_time)
        in_reply_to_status_id = int(whywaita_tweet[1]['in_reply_to_status_id'])
        output_path = os.path.join(output_dir, "{}.json".format(in_reply_to_status_id))
        params = {'id': in_reply_to_status_id}
        print(params)

        res = twitter.get(get_tweet_from_status_id_api_url, params=params)

        if res.status_code != 200:
            print("Status code is not 200, actual = {}".format(res.status_code))
            continue

        json_data = json.loads(res.text)

        with open(output_path, 'w') as json_file:
            json.dump(json_data, json_file)


if __name__ == '__main__':
    csv_path = sys.argv[1]
    output_dir = sys.argv[2]
    get_whywaita_conversations(csv_path, output_dir)

とりあえずこれを動かすと雑に出力先ディレクトリに in_reply_to_status_id.json という感じでどんどんデータが溜まって行きます(なんでこんなアホな設計したんだ)。

取得完了後、まずは文書の正規化等を行います。正規化処理は neologdn というライブラリを使用したり、絵文字除去をしたり、テキスト中から @hogehoge とかURLを消去したりとかしています。(URLと絵文字除去の処理周りはどっかから拾ってきましたが、どこだったか失念・・。)
その後、カラムとして questionanswer の2つを持つCSVに吐き出します。この動作をするのが以下のコードです。

import emoji
import neologdn
import re

def remove_emoji(src_str):
    """
    絵文字除去
    """
    return ''.join(c for c in src_str if c not in emoji.UNICODE_EMOJI)

def twitter_specific_normalize_process(text):
    """
    @を消す
    URLを削除
    """

    # @を削除
    text = re.sub(r'@[\w]+', '', text)
    # URLを削除
    text=re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)

    return text


def normalize(text):
    text = twitter_specific_normalize_process(text)
    text = remove_emoji(text)
    text = neologdn.normalize(text)

    return text
from normalize import normalize
import pandas as pd
import sys
import os
import json

def extract_tweet_data_has_reply_id_from_csv(csv_path):
    """
    whywaitaがGithubで公開しているCSVからin_reply_toのあるデータのみを取り出す
    (+timestampカラムがpandas上でstr扱いされるので、timestamp型に変換)
    """
    csv_data = pd.read_csv(csv_path)
    # in_reply_to_status_idがNaN、つまりリプライではないデータをDropする
    csv_data = csv_data.dropna(subset=['in_reply_to_status_id'])
    csv_data['timestamp'] = pd.to_datetime(csv_data['timestamp']) # timestampがstrなので、timestampにする

    return csv_data


if __name__ == '__main__':
    whywaita_github_tweet_csv_path = sys.argv[1]
    crawled_in_reply_to_json_dir = sys.argv[2]
    generated_dataset_csv_output_path = sys.argv[3]

    whywaita_github_tweet_csv_data = extract_tweet_data_has_reply_id_from_csv(whywaita_github_tweet_csv_path)

    dataset_data = pd.DataFrame({'question':[], 'answer':[]})

    for _, row in whywaita_github_tweet_csv_data.iterrows():
        # CSV上でのwhywaitの発言はin_reply_to_status_idのツイートへの返信なので、`answer`
        answer = row['text']

        # in_reply_to_status_idを取り出し、クローリングしてきたJSONファイルを読み込み
        in_reply_to_status_id = int(row['in_reply_to_status_id'])
        crawled_json_file_path = os.path.join(crawled_in_reply_to_json_dir, "{}.json".format(in_reply_to_status_id))
        if not os.path.exists(crawled_json_file_path):
            # 対応するJSONがない場合、スキップ
            #   -> アカウントがない、ツイ消し、鍵垢
            continue
        json_data = None
        with open(crawled_json_file_path) as json_file:
            json_data = json.load(json_file)
        # in_reply_to_status_idのツイートにwhywaitaが返信するので、`question`
        question = json_data['text']

        # 正規化
        answer = normalize(answer)
        question = normalize(question)

        dataset_data = dataset_data.append(pd.Series({'question':question, 'answer':answer}), ignore_index=True)

    dataset_data.to_csv(generated_dataset_csv_output_path, index=False)

これでとりあえずデータの準備は終わりました。

がくしゅー

さて、データが集まりましたのでモデルの構築をして学習を行わせたいと思います。Chatbotといえばseq2seqという感じですね。ということでKerasかChainerで実装しようと思ったんですが、時間がなかったので今回はTensor2Tensorというとても便利なライブラリを使用することにしました

What is Tensor2Tensor

Library of deep learning models and datasets designed to make deep learning more accessible and accelerate ML research.

github.com

リポジトリにある通り、DeepLearningをより身近に、そして機械学習の研究を加速化させることを目的としたDeep Learningのモデルとデータセットのライブラリです(意訳しただけでは)。正直、謳い文句はどうでもよくて「どれだけ楽に使えるの」というところが大事であるんですが、本当に楽に使えます。MNIST(手書き文字のデータセット機械学習のHello, world的なもの)を使った手書き認識モデルの学習であれば、以下のコマンドを叩くだけです。

t2t-trainer \
  --generate_data \
  --data_dir=~/t2t_data \
  --output_dir=~/t2t_train/mnist \
  --problem=image_mnist \
  --model=shake_shake \
  --hparams_set=shake_shake_quick \
  --train_steps=1000 \
  --eval_steps=100

すっごい楽。既存モデルとデータセットを使うだけなら、コードを書く必要もなし。ここまで楽なのも正直どうかとは思いますが、ツールとしてDeepLearningを使う人(僕)にはとてもありがたい限りです。

今回はこのTensor2Tensorの lstm_seq2seq_attention_bidirectional_encoder というモデルを使用した上で、自作のデータセットを食わせて学習を行わせてみます。

自作データセットで学習させる(前準備)

Tensor2Tensor側が用意している Train on Your Own Data というドキュメントを読めばだいたいわかる(丸投げ)。

tensorflow.github.io

ドキュメントではText2TextProblemというテキストからテキストという問題を解かせる場合が記述されています。今回の入力として質問文を、回答として質問への答えを返すという問題も Text2TextProblem に相当しています。ということで、今回はこのドキュメントのサンプルコードをほぼそのまま使い、以下のようなファイル whywaita.py を作成しました。

import pandas as pd
import os

from tensor2tensor.data_generators import problem
from tensor2tensor.data_generators import text_problems
from tensor2tensor.utils import registry

@registry.register_problem
class Whywaita(text_problems.Text2TextProblem):
    """
    whywaitaっぽい返答をするChatbot
    """

    @property
    def approx_vocab_size(self):
        return 2**13  # ~8k

    @property
    def is_generate_per_split(self):
        # generate_data will shard the data into TRAIN and EVAL for us.
        return False

    @property
    def dataset_splits(self):
        """Splits of data to produce and number of output shards for each."""
        # 10% evaluation data
        return [{
            "split": problem.DatasetSplit.TRAIN,
            "shards": 9,
            }, {
            "split": problem.DatasetSplit.EVAL,
            "shards": 1,
            }]

    def generate_samples(self, data_dir, tmp_dir, dataset_split):
        del tmp_dir
        del dataset_split

        # whywaitaの会話を"""正規化"""したCSVデータ(ここでは正規化は行わない)
        csv_path = os.path.join(data_dir, 'conversations.csv')
        csv_data = pd.read_csv(csv_path)
        csv_data = csv_data.dropna()

        for _, row in csv_data.iterrows():
            question = row['question']
            answer = row['answer']
            answer = answer.strip() # 改行を抜く
            question = question.strip()

            yield {
                'inputs': question,
                'targets': answer
            }

approx_vocab_size とかはもう少し調整するといいのかもしれないですが、今回は時間がなかったのでデフォルト値を使用しています。

更に、上記のファイルをimportするだけのファイル __init__.py を同じディレクトリに置いておきます。

from whywaita import Whywaita

この2つのファイルを usr_dir 等、任意のディレクトリに置いておきます。

自作データセットで学習させる(Tensor2Tensor用のデータを生成)

さて、データセットをTensor2Tensorで使える形にするために以下のように t2t-datagen コマンドを実行していきます。(なお、このときデータ準備で作成したデータをdata_dir という名前のディレクトリに置いています)

t2t-datagen \
--data_dir=data_dir \
--tmp_dir=tmp_dir \
--problem=whywaita \
--t2t_usr_dir=./

オプションの説明を簡単にすると

  • --data_dir
  • --t2t_usr_dir
  • --tmp_dir
  • --problem
    • whywaita.py でいう text_problems.Text2TextProblem を継承したクラスである Whywaita を指定する。ただし、クラス名の大文字はすべて小文字になり、キャメルケースだった場合にはスネークケースにTensor2Tensor内で変換されているため注意が必要。

      PROBLEM is the name of the class that was registered with @registry.register_problem, but converted from CamelCase to snake_case.

実行後、しばらく待つとTensor2Tensor用のデータ生成が終了します。

学習

以上で学習に必要なデータの準備が終わったので、さっそく学習をしていきたいと思います。すでに書いたように、Tensor2Tensorはディレクトリと使用するモデルとハイパーパラメータを引数として指定してコマンドを実行するだけで学習が進んでいきます。

今回は以下のようなコマンドを実行しました。

t2t-trainer \
        --data_dir=./data_dir \
        --problem=whywaita \
        --model=lstm_seq2seq_attention_bidirectional_encoder \
        --hparams_set=lstm_luong_attention_multi \
        --output_dir=./train_dir \
        --t2t_usr_dir=./

オプションはほぼ t2t-datagen と同じです。 --model--hparams_stには学習時に使用するモデルとハイパーパラメータを指定しています。これらの一覧は t2t-trainer --registry_help を実行すると表示されます。・・・表示はされるんだけど、あまり詳細な情報はなくて困る。更にググってもいまいち有益な情報は出てこない。すごい困る。現状ではトライ・アンド・エラーかなぁ・・・という状態。もし、ドキュメントがあったら教えてください・・・。

学習の間にはlossとval_lossは適宜標準出力に表示されますし、 output_dir で指定したディレクトリにcheckpoint毎のモデルが出力されます。また、 Tensorboard に logdir として --output_dir で指定したディレクトリを渡すとTensorboardで可視化されます。

会話をしてみるぞ!

さて、学習がある程度進んだところで、会話をしてみることにします。 t2t-decoder というコマンドから学習したモデルを使用することができます。
--data_dir, --output_dir, --model, --problem, --hparams_st, --t2t_usr_dir は学習時に使ったものと同じものを指定しておきます。また、--decode_hparamsはなにか適当な値を指定しました。この辺もいまいち情報がなくて困る・・・。

DATA_DIR=./data_dir
PROBLEM=whywaita
MODEL=lstm_seq2seq_attention_bidirectional_encoder
TRAIN_DIR=$1
BEAM_SIZE=4
ALPHA=0.6
HPARAMS=lstm_luong_attention_multi


t2t-decoder \
   --data_dir=$DATA_DIR \
   --problem=$PROBLEM \
   --model=$MODEL \
   --hparams_set=$HPARAMS \
   --output_dir=$TRAIN_DIR \
   --decode_hparams="beam_size=$BEAM_SIZE,alpha=$ALPHA" \
   --decode_interactive=true \
   --t2t_usr_dir=./

あとはインタラクティブに会話ができるので、DeepLearningの世界に顕現したwhywaitaとの会話をしてみましょう。

茶番

f:id:tw1sm1k0:20181216215201p:plain
だいたいいつもの感じですね。

f:id:tw1sm1k0:20181216215241p:plain whywaitaっぽい。

f:id:tw1sm1k0:20181216215335p:plain 食べないで!

f:id:tw1sm1k0:20181216215951p:plain 会話になってない

本番

ちょっと怪しい感じですが、僕とましろちゃんの仲を認めてもらいましょう。

f:id:tw1sm1k0:20181216220038p:plain

は〜〜〜〜〜〜〜〜〜?????????????

これはだめそうですね、データセットを増やしてパラメータを調整して更に学習をさせてみました。

f:id:tw1sm1k0:20181216220809p:plain

ブチ切れた

締め

ということで、Chatbotのwhywaitaからでさえ認めてもらうことができませんでしたが、そんなことで揺らぐ僕と真白ちゃんの仲ではないので大丈夫です。

実を言うと、わりとlossとval_lossがかなりアレで過学習気味です。データセットの規模が小さいせいなのか、それともTwitter上のツイートにかなりノイズが乗っているのか、モデル/ハイパーパラメータが適切ではないのかなど様々検証すべき点はあります。あと、そもそも t2t-decoder で入力したこちら側からの質問をデータセットと同様の前処理(正規化)をしていない段階でだいぶお察しです。
今回はwhywaita Advent Calendar用のネタだったので深追いをしませんでしたが、個人的にTensor2Tensorは便利だなぁと思ったので、そのへんを他のデータセット等を使って調査してみたいなと思います。

なにはともあれ、来年もきっと id:masawada さんが伝統としてwhywaita Advent Calendarを作ってくれるはずなので楽しみにまっています。

それでは、明日は id:yu_ki_kun_0 さんです。弊社でのお仕事は楽しいですか?

masawada Advent Calendar 2018 14日目 ~masawadaの婚期を遥か遠くに吹きとばせ~

Introductory chapter

これは何

この記事は masawada Advent Calendar 2018 - Adventar 14日目の記事です。

昨日は id:mazco さんの「マサワパピック」でした。

mazco.hatenablog.jp

すごい!!!!殴ったときの効果音が大変好きです。
今年のmasawada Advent Calendar、クオリティがやばいですね・・・。自分はそんな高クオリティな記事を書けないのですごい心配してたんですが、主催者自らも心配してたのでちょっと安心しました(?)

自己紹介

さて、Advent Calendarのページを眺めると僕の知らない人が多いということは、つまりは読む人も僕のことを知らないということだと思うのでまずは自己紹介をば。

どうも id:tw1sm1k0です。大学院で機械学習をやったり、バイトで画像処理をしたりしてる、可愛い女の子と恋愛をするPCゲームが好きな大学院生です。id:masawadaさんとはサークルの先輩・後輩の関係で、休日の大学でシャボン玉をする会をやったり、LTイベントに行って老害っぷりを発揮したり、直近では五島列島旅行に一緒に行ったりする仲です。
あと、「秋葉原肉の万世まで焼き肉に行きましょう!」と言われて焼き肉をして酒を飲んだ結果、唐突に「よっしゃ!MacBook Pro買うぞ!!!」となって酔った頭でオリエントコーポレーションのローン申し込み用紙を一緒に書く仲です。流石にあのときは、途中で「今、なにか重大なことを『ちょっとそこのコンビニ行こうぜ!』みたいなノリでやってないか・・・?」と冷静になって震えだしたのを覚えています。

参加経緯と記事テーマ決め

さて、自己紹介はこんなものにして。今回、なぜ記事を書くことになったのかというと、サークルのSlackにある限界オタクSlackチャンネル(同期命名)で以下のような雑な会話がなされた結果なのです。
f:id:tw1sm1k0:20181212010726p:plain

雑ですね。完全に何を書くのかということを考えていないのに元気よくお返事をしてしまったので、記事を書く段階(今)になって「どうしよう・・・」と頭を抱えている状態です。元気よくお返事はできたので満点にしてほしいところですが、とりあえず参考にサークルの同期と後輩の記事を見てみることにしましょう。

benevolent0505.hatenadiary.com

hogashi.hatenablog.com

whywaita.hateblo.jp

よっしゃ!!!雑に自分の書きたい文章書くぞ!!!!!

記事テーマ: masawadaに今、僕が一番買ってほしい婚期の遠のくモノ

ということで、本編に行きましょう。(強引な場面展開)

closing chapter

婚期が遠のくモノ、といって皆さんは何を思いつくでしょうか。

婚期が遠のくモノを「結婚(候補)が受け入れられないもの」としてみましょう。結婚というのは自分ひとりで出来るものではなく、必ずお相手がいるものなのでお相手の趣味・嗜好次第ではあると思います。ですが、あえて「結婚(候補)が受け入れられないもの」を上げるとすればギャンブルやアダルティーなものでしょうか。あるいは、オタク系が苦手な子であればアニメや漫画も場合によっては該当してしまうかもしれません。

逆に「これがあるから結婚しなくて良くない?」というのも婚期が遠のくモノかもしれませんね。それこそ可愛い女の子と恋愛ができるPCゲームとかですかね。いや、俺はましろちゃんと結婚しているが?????(急に暴れるのはいつものクセなので気にしないでください)。


このように様々な"""婚期が遠のくモノ”””がある中、今一番僕がmasawadaさんに買ってほしいものは

抱きまくら

です。

抱きまくらといっても、以下のような普通の人が使うようなものではありません。

こういう、なぜか無地で業界のデファクトスタンダードな160cm x 50cmサイズの高級なやつです。

こういう高級な抱きまくらを僕もいくつか購入していて、そのうち1本を愛用しています。驚くほどふかふか&&もっちりしていて、1本持っているだけで人生が変わるレベルです。どれだけ変わるかというと、もしvim.orgに抱きまくらという項目があったら「Life Changing」に爆速でチェックを入れてRateするレベルで人生が変わります( https://www.vim.org/scripts/script.php?script_id=3396 )

このような高級抱きまくらには細かくてうるさい大変こだわりを持つユーザーが多くいるので、メーカー側もこだわりを持って作っています。実際に、「マシュマロ改 エキスパートエディション 熟練者用」の商品紹介には、ちょっと僕には解析できない難しい言語でこだわりが書かれていたりします。(そもそも熟練者ってなに)

キャラクターグッズとしての抱き枕本体は人に例えると筋肉や脂肪で、抱き枕カバーは皮膚と考えます。
重要なポイントは恋人や妻としての抱き枕がどれだけ理想の女の子の体っぽいのかという事。
(あくまで理想の女の子で有って実在の女性の感触を100%再現すれば良いわけではない)
・抱き枕が折れずに立つ⇒女の子は折れないよね、折れたら死ぬからリアルじゃ無い
・カバーと本体がこすれて異音が出ない⇒女の子はキュッキュツ鳴らない
・ズレない⇒10代から30代位でズレる人はまずいないよね
・柔らかい⇒胸の感触が一番重要
・直ぐ戻る⇒揉んだら直ぐに元に戻る!直ぐに戻らないなんて老人だよ

マシュマロ改EE(エキスパートエディション 熟練者用)はマシュマロ改・マシュマロ改二で評価頂いている点を極力残しつつ一体感を上げる形で検討を行いました。


さて、勘のいい皆様はお気づきだと思いますが、高級抱きまくらは総じて無地なんですね。なぜか。こんな真っ白だとカバーをかけたくなるのが人情といったものです。

そこでカバーのことを考えましょう。カバーというのも無地から柄付きなものまで多種多少です。そんな中で、masawadaさんに是非オススメしたいのがアニメ系のものです。

masawadaさんは電気通信大学というオタク大学の学生だったと伝え聞いていますので、きっとアニメ系コンテンツが好きなことだと思います。そうに違いありませんので、その方向で話を進めます。

アニメ系のカバーはアニメイトや何かのブランド直販サイトなどでも売られています。結構気軽に買えます。

「抱き枕」検索結果 | アニメイト

気軽に買えますが、ここで注意してほしいのがカバーの材質です。一部業界の1万円台のカバーは基本的に気にしなくても高品質な素材を使っていますが、アニメやラノベ系のものの場合には高価な場合にも雑な材質を使っていることがあります。そのときには材質が「ライクトロン」(トリニトロンじゃないよ!)または「アクアライクラ・アクアプレミア」であるものを選択した方が幸せになれます。


さて、以上で紹介したかわいい女の子が描かれたすべすべなカバーをふっかふかな抱きまくら本体にかけて一緒に寝れば、まるで好きなあの子と一緒に寝ているようで(難しい漢字で同衾と書くやつ)、とても幸せになれます。つまりは、「これがあるから結婚しなくて良くない?」という状態になるわけですね。更に、そうやって寝ている状態(+カバーを本体にかけているところ)を結婚(候補)の人が見たら「結婚(候補)が受け入れられないもの」と分類されること請け合い。これで完璧に婚期が遠のきますね!ぜひやりましょう!!

「でも、宅配時にデカイ箱で来たら恥ずかしいじゃん」という意見もあるかと思います。

ご安心ください。上で紹介した抱きまくら本体は業務用の装置で圧縮されて送られてくるため、そこまで大きな箱で来ません。もとに戻すのも、封を開けてゆっくり空気を吸わせるだけです。

ちなみに、僕はこのときの空気を吸ってでかくなるときの「すぅ〜」みたいな音を「婚期が遠のく音」と呼んでいますので、皆様もぜひお使いください。

coda

さて、「masawadaに今、僕が一番買ってほしい婚期の遠のくモノ」でしたがいかがでしたでしょうか?僕は読んだ人がドン引きするのを下書きの段階で感じています。

ところで、なぜ抱きまくらをご紹介したのかというと、ベッドに行きたくなる理由を作ってあげようと思ったからなんですね。五島列島に旅行に行っているときにmasawadaさんが布団ではなく床の上で寝てしまうことがある〜という話をしていたので、健康のために〜と思ったわけです。婚期云々は後付です。本当です。ただ書き終わってから思いましたが、今は冬なので流石に床で寝ることはなさそうですね・・・。

ともあれ、masawadaさんには今後共、一緒にオリエントコーポレーションのローン申込用紙を書いていける仲でいたいなぁと思います。あと、どっちが先に婚姻届を役所に出せるのかというのも勝負したいところではあります。

最後に。今僕が使っているDHR7000H(SONYのヘッドホンの型番みたいだね!)が佐川急便で送られたときの荷札を共有しておきます。

それでは、こんなやばい記事の次になってしまって大変申し訳無いのですが、明日の担当は id:papix さんです。お会いしたことはないですが、もしお会いする機会があればまず謝罪から入りたいと思います!

私信

masawadaさんへ。sprite storeの抱きまくら予約注文締切が間近で、これを逃すともう手に入れられないです。なので、僕の嫁の真白ちゃん以外の抱き枕カバーを急いで予約注文することをおすすめします。ちなみに僕は真白ちゃんのカバーを各種、最低2枚ずつ買いました。

Aruba AP-207でridiculously expensiveなコンソールケーブルを買わずに設定がしたい

自分用の備忘録。

ArubaのAP-207(と特定の機種)ではコンソールポートがRJ45ではなく、特殊なポートになっている。そして純正品のコンソールケーブルが異常なほど高い。

フォーラムを探すと普通のTTL-RS232 or TTL-USBのアダプタを使えば行けるという情報がある。

community.arubanetworks.com

ピンアサイン情報もあるので大変便利。

ただ、見づらいので自分用に画像を置いておく。

f:id:tw1sm1k0:20181213004001p:plain

f:id:tw1sm1k0:20181213004029p:plain

コンソールポートの上下に各々サイズ(横幅)が違う突起があるが、短い方を下にしたとき、左から順にGND、RX、TXだ。


ここで、まだ検証していないがCP210X系のチップが載ったものだとエラーが出るらしい?初期不良なのかもしれないが、確かに自分の場合も出た。

https://community.arubanetworks.com/t5/Wireless-Access/How-to-make-Aruba-AP-207-work-Only-APboot-no-nand-flash/m-p/455394/highlight/true#M83685community.arubanetworks.com

Device nand0 not found!
Error initializing mtdparts!
Error, no UBI device/partition selected!

しかし、エラー的にはNANDがないというエラーなので、果たしてコンソールケーブルが関係するのか・・・

Tensorflowをコードからコンパイルしてインストール(Tensorflow 1.12.0)

タイトル通り。
古いCPUを使っているマシンでは、Tensorflow 1.8.0あたりからimportしようとするとCore dumpして落ちてしまう。その場合には、コードからコンパイルする必要がある。今回シュッとやったので、備忘録的に残しておく。単純にTensorflowがGPUで回せれればOKの精神なので、configureはかなり適当。

基本的には公式ページをたどればOK

Build from source  |  TensorFlow

検証環境

  • Ubuntu 16.04
  • GTX 1070
    • CUDA SDK 9.0
    • Driver Version 390.30
    • cudnn 7.0.5.15
  • Python 3.6.2
    • miniconda3-4.3.27

下準備

apt/pipでパッケージをインストール

  • (既に入っていたので飛ばしたが)、適宜必要なパッケージをapt-getで入れる
sudo apt install python-dev python-pip # or python3-dev python3-pip
  • pipで関連パッケージを入れる。keras関連を入れるのを忘れないこと。(kerasはTensorflowのラッパーというイメージがあったので、Tensorflowのインストール時にはいらんだろと思ってスキップしたらコンパイルが失敗した)
pip install -U pip six numpy wheel mock
pip install -U keras_applications==1.0.5 --no-deps
pip install -U keras_applications==1.0.5 --no-deps

Bazelをインストール

  • BazelはGoogleが開発するビルドツール。自分の環境には入ってなかったので入れた。
  • 基本的にはBazelのインストールガイドを見ればOK。ローカル環境下に入れられるので便利

本題

  • TensorflowのGithubページからコードをClone
git clone https://github.com/tensorflow/tensorflow.git
cd tensorflow
  • configureを実行すると。いろいろ聞かれる。が、基本的にTensorflowがGPUで回せれればいいやという意思から、ほぼEnterを押し続けてDefault設定を利用した。
    • ただし、Defaultでは(確か)CUDAはDisable状態なはずなので、そこだけ注意
    • また、途中でCUDAのCompute Capabilityを聞かれるので、以下のNVIDIAのサイトで確認する(今回のGTX1070の場合は6.1) developer.nvidia.com
  • configureに成功したら、ビルドをしていくが、Bazel側の設定を適当にするとリソースを食いつぶすっぽいので使用するメモリとCPUコア数を確認しておく。(ref: https://qiita.com/Guwashi/items/d26f06ed45f9d740ffa2 )
    • free -h
    • /usr/bin/getconf _NPROCESSORS_ONLN
  • 環境に応じてBazelを実行(//tensorflowあたりに違和感があるが、問題ない)
    • 1-2時間程度かかる。めっちゃ時間がかかるぞ!
bazel build --local_resources=2048,6,1.0 --verbose_failures --config=opt --config=cuda //tensorflow/tools/pip_package:build_pip_package
  • コンパイルが無事終了したら、pipでインストールするように以下のコマンドでwhlパッケージを作成
./bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
  • /tmp/tensorflow_pkg以下にwhlが図れるので、pipでインストール
    • 適宜、python -m venv hogehoge 等で仮想環境を作っておいて、現状の環境を汚さないようにすると吉
  • ipythonで試しにimportしてみてうまく行けばOK
    • ただし、カレントディレクトリがgit cloneしてきたフォルダでやるとエラーが出るのでそれ以外のパスでやること

五島列島旅行記(2018/09/21-2018/09/26)

DSC02137

9月21日から26日にかけて、長崎県五島列島に旅行に行ってきました。きっかけはもちろん、蒼の彼方のフォーリズム(舞台が五島列島がモチーフになった四島列島)なわけで。ゲームをやって一度行きたいなぁと思っていたので、念願のという感じです。

デッカード「島を5つくれ」

主人「4つで十分ですよ」「わかってくださいよ」

実のところ、21日から22日は大阪や福岡周辺を回っていたので、実質的には4日間だけ五島列島にいたわけなのですが。まぁ、そのへんも含めて旅行記としたいと思います。
(主に一眼レフで写真を撮ったのですが、基本的に現像が面倒だったのでLightroomGoogle Photosの自動で適当にやってるので、そのあたりは目をつぶってもらえれば〜と思います)

続きを読む