Tea break

ちょっとした息抜きに

日本語配列のHHKBを買ったけど英語配列にすれば良かったなぁと少し後悔した話

4月の頭に日本語配列のHHKBを購入しました。
家にいる時間が長くモチベーションが死んでいたので、奮発してしまった...

最初は尊師スタイルしたいなぁぐらいの気持ちで買ったのですが、今はこれが無いと物足りないと感じるぐらい素晴らしいものでした。
HHKB最高!HHKB最高!HHKB最高!(皆さんも復唱しましょう)

ただ、使ってて英語配列のを買えば良かったなぁとすこーし後悔することが2点あったので書いておきます。(基本的に満足しています)

1. BackSpaceとEnterキーが遠くて押しにくい

使用方法は画像のようにMBA尊師スタイルなんですが、HHKBってMBAのキーボードより微妙に横に長いんですよ。

f:id:wisteria30:20200511034512j:plain

この微妙に長いというのが肝で、いままでそれほど違和感なく押していたBackSpaceとEnterキーがとても遠く感じて押すのが絶妙に面倒くさいんですよ。
その点、英字配列はその二つのキーが横長なので右小指でそのまま触れていいなぁと思ったりします。 後、日本語配列の特権である矢印キーもホームポジションから遠くてコーディングの時とかは使わないのでいらなかったなぁという感じです。

結局 BackSpace, Enter, 矢印キーはホームポジションで完結するように、Karabainer Elementsでキーバインド自作しました。

2. 英語配列の方がキーキャップのカスタマイズができる

これ購入するまでは完全に盲点だったんですよね...
元々HHKBはキーキャップのカスタマイズがあまりできないっぽいです。(REALFORCE用のを適応させたり色々できるのはできるらしい)

それでも全く無いわけではなく、PFUの公式ページやKBD FANS、AliExpressで売っています。

HHKB オプション|PFUダイレクト

HHKB – KBDfans Mechanical Keyboards Store

ただし、売られているのは英語配列

HHKB用のキーキャップは英語配列のものばっかりで、日本語配列用のものが全然見当たらないんですよね。

f:id:wisteria30:20200511050822j:plain

これとか付けてみたかった...


色々書きましたが、英語配列は触って無いので実は日本語配列の方が性に合っている、、、なんて話になるかもしれません。参考にする方は話半分に見てもらえれば嬉しいです。

まぁ、隣の芝生は青く見えるって事なんですかね。

Pythonでやらかした話とNaNの話

近畿大学 Advent Calendar 2019の10日目 軽い気持ちで始めたアドベントカレンダー、全日埋まってとてもビックリしています。 参加していただいた方々には感謝の気持ちでいっぱいです、ありがとうございます。

この記事では、私がPythonを触っていてやらかしたところを公開します。あと、NaNについてハマった時に調べたものも一応残しておきます。 自分の振り返りと皆さんの反面教師になれればと思います。

==とis

PythonになれてきてからPythonicな書き方を心がけていく中、知り合いに「Pythonではisで等価判定したほうがオシャレ!」と言われ、==isに全て置換したことがあります。 そうすると一見正しそうに動くのですが...

==とisの挙動の違い ==は値が等しいかどうかを判定する isはオブジェクトidが等しいかどうかを判定する

Pythonの文字列やリストは同値でも、オブジェクトidが違う場合があります。(文字列は日本語が入るとisではおかしくなる気がする)なので、isで判定をすると同値でもFalseが返ってきてハマります。

left = "pythonやらかし"
# ==判定
left == "pythonやらかし"
# > True
# is判定
left is "pythonやらかし"
# > False

2次元配列の参照コピー

Pythonは文字列や配列を*で掛けることができます。

"オラ" * 10
# 'オラオラオラオラオラオラオラオラオラオラ'
[0] * 10
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

しかし、これで二次元配列で生成すると参照コピーの罠にハマります。 これ気づかずにめっちゃ時間溶かしました... 横着せずにリスト内包表記などで書くと良いと思います。

two_d_list = [[0] * 10] * 10
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

# [0,0]の値だけ1足したい
two_d_list[0][0] += 1
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

for文

PythonのCなどと違って、for文はシーケンス型を反復する仕組みです。なので、以下のようなことをするとハマります。 1. iのようなターゲットリストを途中で変更しても反映されない 2. ループ中のシーケンスオブジェクトに内部からappendすると無限ループになる

1つ目に関しては諦めるしかないと思います... 2つ目はスライス表記を用いることで解決できます

num_list = list(range(1, 4))
# ターゲットリストを途中でいじっても意味がない
for i in num_list:
  print(i)
  i += 1
# 1
# 2
# 3

# スライスを用いてループ中にappendする
for i in num_list[:]:
  num_list.append(i)
# [1, 2, 3, 1, 2, 3]

Pythonインタプリタ

ちょっとした計算や何か試したいことがあれば、ターミナルからPythonのインタプリンタを呼び出すことがよくあります。 しかし、一度エンターを押してしまうと、後戻りが出来ず、「あっ、変数名間違えた」「あっ、インデント間違えた」といったやらかしがよくあります。なのでipythonを使うようになりました。ipythonはjupyter notebookでおなじみのセルでコードを実行する環境でコマンドで実行できる対話環境としてデフォルトのインタプリンタより優れているのでおすすめです。

バージョン3.4以下のgcd関数のモジュール

これは主にAtCoderに関連する話なのですが、Pythonのバージョンが3.4以下だとgcd関数がmathモジュールにありません(現在のAtCoderのPython3のバージョンは3.4)

mathモジュールにgcd関数が置かれるのは3.5以降で、それ以前のバージョンだとfractionsモジュールにあるのでそれを使用しなければなりません。些細なやらかしかもしれませんが、一分一秒の時間が惜しい競プロでは結構なやらかしでした。

# 3.4以下はこれが使えない
from math import gcd
# 3.4以前はこっちを使う(3.5以降は非推奨)
from fractions import gcd

NaN

a bit pattern of a single-precision or double-precision real number data type that is a result of an invalid floating point operation. 訳: 無効な浮動小数点演算の結果である単精度または倍精度の実数データ型のビットパターン IEEE 754より

PandasでNaNを判定する関数をずっとdf.isnan()だと思っていてハマりました(?) 実際にはdf.isnull()です。個別で判別する場合は、math.isnan(), numpy.isnan()も有効です。

おまけ(むしろ本題)

np.nan == np.nan # is always False! Use special numpy functions instead.

NaNはあらゆるものとの数値比較、等価演算でFalseを返すのは特性がありますが(自分自身との比較でFalseを返すのもこの特性から)、参照先で比較するisは数値比較ではないので、この原則に当てはまりません。 しかし、Pythonにはmath.nanとnumpy.nanの二種類のNaNがあり、これらはオブジェクトidが違います。

# isを使ったNaN同士の比較
math.nan is math.nan
# True

# mathモジュールのNaN
id(math.nan)
# 4538852576
# numpyライブラリのNaN
id(np.nan)
# 4569389960

ですが、これらはそれぞれのNaNを別々のライブラリの関数で正しく判定できています。

# それぞれのisnan()関数でNaNが正しく判定できる
math.isnan(np.nan) and np.isnan(math.nan)
# True

どのような実装になっているのか気になったので調べてみました。

math.isnanの実装

math.nanの実装は、cpythonより、Cの実装に従っていると思われます。 そのCの実装はこちらのサイトを参照しました。 - https://ja.cppreference.com/w/c/numeric/math/isnan)

#define Py_IS_NAN(X) isnan(X)

numpy.nanの実装

NumPy core libraries よりC99の実装に従うことがわかる

.. c:function:: int npy_isnan(x) This is a macro, and is equivalent to C99 isnan: works for single, double and extended precision, and return a non 0 value is x is a NaN.

つまりオブジェクトidは違うが元の実装がCで同じなので、同じように動くということでした。なんとなく予想はついていましたが、実際に確認できるとやはり嬉しいですね!

おわりに

アドカレ期限ギリギリになってしまいましたが、これも全てバックアップを阻害して、25G程度のバックアップに3日もかけるようになったiCouldが悪い

Ubuntu18.04にNVIDIA Container Toolkitをインストールする

最初に

nvidia-docker2が非推奨になったそうなので新しく環境構築しました。 手元で上手くいった例を記録として残しているだけで、何が正しいか分かっていない(動けば正義)ので無駄な手順等を行なっている可能性があります、ご了承ください またコマンドと出力が一緒になっている部分はコマンド前に$を付けています

環境

試した環境

  • Docker, nvidia-driver, CUDA未インストール
  • GPUはGTX 1660Tiと1660の二枚刺し
  • OSはUbuntu: 18.04 (詳しくは以下の通り)
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"

nvidia-driverとCUDAをインストールする

以下のサイトを参考にインストール

wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-ubuntu1804.pin
sudo mv cuda-ubuntu1804.pin /etc/apt/preferences.d/cuda-repository-pin-600
sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub
sudo add-apt-repository "deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/ /"
sudo apt-get update
# 自動で合うdriverを入れてくれる
# sudo ubuntu-drivers autoinstallだと435, 以下コマンドだと418のインストールを確認
sudo apt-get -y install cuda-drivers
sudo apt-get -y install cuda

.bashrcにパスを追加

# 以下を追記
export PATH="/usr/local/cuda/bin:$PATH"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"

sudo rebootで再起動、以下で確認

$ nvidia-smi
Fri Nov 15 00:06:50 2019
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.87.01    Driver Version: 418.87.01    CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 166...  On   | 00000000:01:00.0  On |                  N/A |
| 29%   33C    P8     5W / 120W |    110MiB /  5911MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  GeForce GTX 1660    On   | 00000000:03:00.0 Off |                  N/A |
| 28%   31C    P8     3W / 120W |      1MiB /  5914MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    0       953      G   /usr/lib/xorg/Xorg                            39MiB |
|    0      1011      G   /usr/bin/gnome-shell                          69MiB |
+-----------------------------------------------------------------------------+
$ nvcc -V
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2019 NVIDIA Corporation
Built on Sun_Jul_28_19:07:16_PDT_2019
Cuda compilation tools, release 10.1, V10.1.243

Dockerをインストールする

以下のサイトを参考にインストール

sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common -y
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD8
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io -y
# 確認
sudo docker run hello-world

バージョンの確認(NVIDIA Container ToolkitはDockerが19.03以降でないとダメ)

$ docker -v
Docker version 19.03.4, build 9013bf583a

NVIDIA Container Toolkitをインストール

以下のサイトを参考にインストール

distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

動くか確認

$ nvidia-container-cli info
NVRM version:   418.87.01
CUDA version:   10.1

Device Index:   0
Device Minor:   0
Model:          GeForce GTX 1660 Ti
Brand:          GeForce
GPU UUID:       GPU-4dd3cbbf-cb72-be49-a3d0-4625043ce50e
Bus Location:   00000000:01:00.0
Architecture:   7.5

Device Index:   1
Device Minor:   1
Model:          GeForce GTX 1660
Brand:          GeForce
GPU UUID:       GPU-914b0174-e95d-0b8f-cf13-d20ed58f707e
Bus Location:   00000000:03:00.0
Architecture:   7.5

これでGPUコンテナを実行するとき(run)に--gpusオプションを付ければ動く

メモ

dockerコマンドの際に一々sudoを打たなくていいようにする この記事より以下コマンドを入力後、再起動 dockerコマンドをsudoの付与無しに実行できるようにする

sudo gpasswd -a "権限を付与するuser" docker

実際にpytorchを動かしてみる

閑話休題 今回はお手軽にpytorch公式のdockerhubからイメージを持ってくる(動くか確認もしたかった)

# 色々オプションをつけているが、最低限なら-pとか-vはいらない
# -vするなら適当に作業ディレクトリに移動してから行う
docker run -itd --name pytorch -p 8888:8888 -v $PWD/:/workspace --gpus all pytorch/pytorch:1.3-cuda10.1-cudnn7-devel
docker exec -it pytorch /bin/bash

nvidia-smiもちゃんと動くことを確認 学習部分だけいい感じに切り取ってくれている記事があったので利用させてもらう Docker(19.03)でgpu有効化してpytorchで訓練するまでやる(Ubuntu18.04) 無事動くことを確認

おまけ

この記事より丁寧でわかりやすい導入記事() NVIDIA Container Toolkit を使って Docker コンテナで GPU を使う

エイプリルフールに発表される新元号が、偽物かどうかAIだけで判別する物語 【完】

はじめに

エイプリルフールに発表される新元号が、偽物かどうかAIだけで判別する物語の続き
以下ポエム(改めて見直すと、テストになってないのでガバガバ記事ですが、暖かい目で見ていただけると幸いです)

元号は令和

f:id:wisteria30:20200326000942p:plain

西暦2019年4月1日11時30分(ちょい過ぎ)から新元号の記者会見が始まった。
そして菅官房長官によって発表された新元号「令和」
典拠は日本最古の歌集「万葉集」の梅の花の歌より

 于時初春令月 氣淑風和
(時に、初春の令月にして、気淑く風和ぎ)

初めて中国古典ではなく、日本古典から採用されたらしい。
予想ガチ勢がAIの学習用辞書データに四書五経とか使っていたら、外れている可能性が高そうである。Wikipediaを辞書データにしてよかった。(中国の詩文集「文選」に似たようなフレーズが存在し、著者はそれを参考にしたという説もあるらしい。つまり中国古典を辞書にしても当たっていた可能性もある。)
後、元号発表がお祭騒ぎだったからか、あまりデマ元号は流れているのを見なかった。

元号判別機は「令和」を元号として判別したのか

グダグダと話したが、「令和」は元号として判定されました、めでたしめでたし。

-----令和-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.1180524155497551
適切な一語目 0.1 <= x <= 0.56, 適切な二語目 0.03 <= y <= 0.49
一語目 平均: 0.1357724815607071, 二語目 平均: 0.1358799934387207
元号かもしれないです!
(MTSHの確認は人がしてください)

せっかくなので精度を見てみる

これ前の記事でやらなきゃいけないこと、、、
令和を除く全ての元号を訓練データにしたので、正直なところテストデータがそれこそ新元号「令和」しかなかった。(勉強不足なので他に良い方法があるのかもしれない)
だからと言ってテストデータ1つに対して正解したので精度100%というのもありえない話なので、この際、判別機を少しいじってでも(前回でいうフィルター)過去の元号にも当てはめてエセ精度を見てみようの回。 以下で行う精度は学習データをテストデータにするエセもエセなので気楽に馬鹿らしく見ていきましょう。(書いてた時は血迷ってました。)

テストで使う元号の総数は243

# 過去の元号 +「令和」のリストをgengoとする
all_gengo_len = len(gengo)
# 243

過去元号フィルターを外す

過去の元号フィルターを外す。
ルール: これまでに元号として用いられたものでないこと
を適用しているが、過去の元号元号っぽいか判別するので当然外す。
外す作業はjudge関数から適宜行なっている。

では精度はどうだろう。

# judge関数はTrueかFalseを返す
unfil_kakogengo_len = len([gen for gen in gengo if judge(gen, model, mecab)])
# 70

# 精度
unfil_kakogengo_len/all_gengo_len*100
# 28.80658436213992

低い、ハサミギロチンの命中と同じレベル。この数字だけ見れば「令和」もヤマカンで当たったように見える。
しかし、1つ1つを見るとMeCabに品詞として発見されているものが多く見える。過去の元号は既存の単語なのだから引っ掛かって当然か。

-----平成-----
既存の品詞のようです
元号ではなさそうです
      ・
      ・
      ・
-----大化-----
既存の品詞のようです
元号ではなさそうです

既存品詞フィルターを外す

既存品詞フィルターを外す。
ルール: 俗用されているものでないこと
を適用していたもの。先ほどの例でも既存の品詞として多くの元号が引っ掛かっていたので、外す。

# 同上
unfil_kakogengo_hinsi_len = len([gen for gen in gengo if judge(gen, model, mecab)])
# 140

# 精度
unfil_kakogengo_hinsi_len/all_gengo_len*100
# 57.61316872427984

精度は倍ほど上がったがそれでもまだ57%、さいみんじゅつと同レベル。
内訳は以下の通り

  1. 元号の可能性がある (57.61316872427984%)
  2. 単語間の距離が適切でない (27.160493827160494%)
  3. 常用漢字ではない (12.345679012345679%)
  4. 単語の意味が元号向きではない (2.880658436213992%)

3番に関してはどうしようもない。過去の元号は読み書きのしやすさなど重視していなかったのだろうが、ここは外せない。
先ほどから精度を出すために一部のフィルターを外しているが、あくまで元号判別の機能を残すことは忘れてはいけない。(全て外せば100%なのは当たり前)
では2番と4番も外せないのだろうか。

1つの仮説

前回の記事の中で、ルール: 国民の理想としてふさわしいようなよい意味を持つものであること
に関して、その漢字の出し方について考察し、そこで以下のような仮説を立てた。(参考: やっぱり最大の難関は「よい意味の判断」)

  • 過去の元号もそれ以前の元号で使われたことのある漢字と関連性がある
  • 元号の一語目は他の元号の一語目と関連性があり、二語目も同様である

重要なのは、この仮説では元号にふさわしい漢字を絞り込んだのではなく、元号の一語目・二語目それぞれにふさわしい漢字を絞り込んだという点である。

ここで1つの仮説が出てきた。
それは 過去の元号は、いい感じの距離感を持つ、いい意味の漢字2文字から成り立っていて、それを基に一語目・二語目それぞれにふさわしい漢字が決まるならば、同様にふさわしい距離感も保証されるというものだ。

それはつまり、元記事の平成の次の元号を、AIだけで決めさせる物語で考察していた「元号として適切な組み合わせ・元号としての組み合わせのバランス」は元号の一語目・二語目それぞれにふさわしい漢字が決まったならば、既に求められているということだ。

はっきり言って、仮説の上に立つ仮説なので正しい保証はどこにもないが、なんだか正しそうな気がする。 (ヤケクソ)

二語の距離感フィルターを外す

上記の仮説に従えば、語の適正フィルターに通りさえすれば、二語の距離感フィルターを通さずとも元号として適切な距離感を持つので、信じて外す。

# 同上
unfil_kakogengo_hinsi_ad_len = len([gen for gen in gengo if judge(gen, model, mecab)])
# 198

# 精度
unfil_kakogengo_hinsi_ad_len/all_gengo_len*100
# 81.48148148148148

精度が80%を超えた。ようやくハイドロポンプの命中率にまで持ってくることができた。 もうこれ以上は外せるフィルターがないので、これで打ち止め。

終わりに

「令和」が無事元号で良かった!
インスタを巧みに使う官庁も流石にエイプリルフールには乗らなかったようで。

ついでにエセ精度も評価してみた。判別機として機能するギリギリのラインで過去の元号を判別すると8割以上の確率で正解を出すことが分かった。
これに関しては、同じデータを使っているので、精度も何もないのだがそこも含めてポエムなのでご愛嬌ということで。

他のテストデータも試してみたいところではあるが、ホイホイ元号が変わるのも面倒なので、しばらくは「令和」が続くことを祈ることにする。

【補足】 リークされた元号候補も判別してみる

コメントで面白そうな情報とリンクをいただいたので判別してみる。

リークされた他の案も調べてみてください! https://www3.nhk.or.jp/news/html/20190402/k10011870221000.html

リークされた「令和」を除いた5つの原案は以下の通り 「英弘」・「久化」・「広至」・「万和」・「万保」

はい、どん!

-----英弘-----
常用漢字ではないようです
元号ではなさそうです
-----久化-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: -0.21727973222732544
単語間の距離が適切ではないようです
元号ではなさそうです
-----広至-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.21013568341732025
適切な一語目 0.1 <= x <= 0.56, 適切な二語目 0.03 <= y <= 0.49
一語目 平均: 0.004297872539609671, 二語目 平均: 0.16898992657661438
単語の意味が元号向きではないようです
元号ではなさそうです
-----万和-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.05686895176768303
単語間の距離が適切ではないようです
元号ではなさそうです
-----万保-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.11570872366428375
適切な一語目 0.1 <= x <= 0.56, 適切な二語目 0.03 <= y <= 0.49
一語目 平均: 0.05171163007616997, 二語目 平均: 0.1425367295742035
単語の意味が元号向きではないようです
元号ではなさそうです

まさかの全て元号ではないという判定。 単語間の距離が適切ではないものに関しては仮説に基づいてフィルターを外してみる。

-----久化-----
適切な一語目 0.1 <= x <= 0.56, 適切な二語目 0.03 <= y <= 0.49
一語目 平均: 0.40043678879737854, 二語目 平均: 0.03221416100859642
元号かもしれないです!
(MTSHの確認は人がしてください)
-----万和-----
適切な一語目 0.1 <= x <= 0.56, 適切な二語目 0.03 <= y <= 0.49
一語目 平均: 0.05171163007616997, 二語目 平均: 0.1358799934387207
単語の意味が元号向きではないようです
元号ではなさそうです

それぞれ考察してみる。

「英弘」

「弘」が常用漢字ではなく、JIS第1水準漢字なため。 JIS第1水準漢字は画数の多い漢字も入っているので、これは致し方ない。 ただし、過去に「弘化」・「弘治」など「弘」を使った元号がいくつかあるので対策を考えるべきかもしれない。

「久化」

元号かもしれない。(元号ではなかったが) 「久」・「化」ともに元号経験のある漢字なので、この中では一番安牌。 むしろ安牌過ぎて選ばれなかった可能性。

「広至」

「広」が元号向きの単語ではないため。 いい意味には見えるけど、個人的にコレジャナイ感はわかる。

「万和」

「万」が元号向きの単語ではないため。 鶴は千年亀は万年、「万」は「亀」と深い関係性があるような気もするがダメみたい。

「万保」

上と同じく、「万」が元号向きの単語ではないため。 一語目に「千」・「百」を比較として入れてみるとこっちはOKだった。

まとめ

令和以外の元号候補は「久化」を除いて全て撃沈する面白い結果がみれた。 AI的に見ても他の候補は元号足り得なかったのか。 はたまた、碌に機能しないオンボロ判別機だったのか。 結果論で言うなら、元号になった原案にのみ正常に機能した素晴らしい判別機だ。(笑) その辺りは見る人の評価に任せたいと思う。

エイプリルフールに発表される新元号が、偽物かどうかAIだけで判別する物語

注意書き

この記事は、@youwht さんがQiitaに投稿されている平成の次の元号を、AIだけで決めさせる物語をとてもとても(パクリといっていいレベルで)参考にしています。 すごく面白い記事なのでおすすめです。 なお、Wikipediaを元にしたchar2vecモデルの作成法などはそちらを参考にしてください。

背景

元号は4月1日に公表、5月1日に改元することが正式に発表されている。 でも発表が4月1日ということで嘘の新元号が発表されるのでは??? とネット上ではもっぱらの噂である。 さすがにそれは冗談だとしてもエイプリルフールに乗じて、 嘘(ネタ)の新元号の情報が大量に流れるのはありそうな話ではある。

そんな時に先の記事を見て、 AIだけで元号予想ができるなら、 AIだけで元号かどうかの判別もできるんじゃね? と思ったのでAIだけで元号判別機を作ってみる。

元号のルール

ルールは以下のようにする。 (おくり名は面倒なので今回はパス)

  1. 国民の理想としてふさわしいようなよい意味を持つものであること
  2. 漢字2字であること
  3. これまでに元号として用いられたものでないこと
  4. 常用漢字であること
  5. 俗用されているものでないこと
  6. M(明治)、T(大正)、S(昭和)、H(平成)とアルファベットのイニシャルが異なること

方針

方針はシンプルに元号のルールで定めたルールに対応したフィルターを作り、 それらのフィルターを全て通過した文字列は元号の可能性があるとする。

逆にフィルターのどこかで引っかかれば、新元号ではないということにしよう。

やっぱり最大の難関は「よい意味の判断」

ルール1. 国民の理想としてふさわしいようなよい意味を持つものであること よい意味ってなんだよ(哲学) 元記事では

発想として、良い意味の漢字=既に元号で使われたことのある漢字、として、 それに似たベクトルを持つ漢字が、元号の候補となる漢字なのではないか?

としてChar2Vecで過去の元号で使われた漢字とコサイン類似度が一定値以上の漢字を列挙。 その中から組み合わせを探していたが、決め打ちの漢字以外は元号として認めない判別機は判別機にあらず! ということで別の手を探す。

ここでさっきの発想を1つ進めて、

  • 過去の元号もそれ以前の元号で使われたことのある漢字と関連性がある
  • 元号の一語目は他の元号の一語目と関連性があり、二語目も同様である

と仮定してみる。

つまり「平成」という漢字は、 一語目の「平」は昭和以前の元号の一語目との 二語目の「成」は昭和以前の元号の二語目との関連性があるのではないか?

そしてこの仮定が正しければ、 全ての元号の一語目・二語目どうしの組み合わせから 元号の一語目・二語目にふさわしい漢字が統計的に絞り込めるはず!

仮説が正しいのか確認するため、 元号の一語目・二語目どうしの組み合わせのコサイン類似度を見る。 (同じ漢字の場合コサイン類似度は 1 なのでそこは省く)

import numpy as np
import matplotlib.pyplot as plt
import random

dimension = 0 # 0 = 一語目のコサイン類似度, 1 = 二語目のコサイン類似度 を測る
distancelist=[]

for idxone in range(0, len(gengo)):
   for idxtwo in range(idxone, len(gengo)):
        distance = model.similarity(gengo[idxone][dimension], gengo[idxtwo][dimension])
        if not (distance == 1.0):
            distancelist.append(distance)
        
# データ数が多くてグラフが潰れたのでサンプルを抽出してグラフ化
sample_distance = random.sample(distancelist, 200)

# 折れ線グラフを出力
left = np.array(range(len(sample_distance)))
height = np.array(sample_distance)
plt.plot(left, height)

一語目の組み合わせのコサイン類似度 f:id:wisteria30:20200326000801p:plain

二語目の組み合わせのコサイン類似度 f:id:wisteria30:20200326000806p:plain

なんとなく、距離感を保っている気がする

仮説としては使えそうなので、これでいく!(他の方法が思いつかない) 実際に統計的に使える値を算出する。

m = np.mean(distancelist)
median = np.median(distancelist)
variance = np.var(distancelist)
stdev = np.std(distancelist)
print('平均: {0:.2f}'.format(m))
print('中央値: {0:.2f}'.format(median))
print('分散: {0:.2f}'.format(variance))
print('標準偏差: {0:.2f}'.format(stdev))
-----1語目の値-----
平均: 0.33
中央値: 0.30
分散: 0.06
標準偏差: 0.23

-----2語目の値-----
平均: 0.26
中央値: 0.24
分散: 0.05
標準偏差: 0.23

平均と中央値が近いのでいい感じに見える。 上の結果の平均と標準偏差を使用して漢字の範囲を絞ることにする。

前準備

import

import numpy as np
import MeCab
from gensim.models.word2vec import Word2Vec

mecab = MeCab.Tagger("-Ochasen")
# 後述の作成したモデル
model = Word2Vec.load('mychar2vec_fromWikiALL.model')

モデル作成

今回の判別に必要なchar2vecモデルを作成する。 学習データはWikipediaのダンプデータ。 先の記事の通りにやればできた。 ひとまず動くか確認。

print(*model.most_similar(positive = u'病', topn=10), sep="\n")
('患', 0.8373429775238037)
('罹', 0.7904560565948486)
('肺', 0.7740147113800049)
('癌', 0.7698979377746582)
('医', 0.7508054375648499)
('臓', 0.7485206723213196)
('胃', 0.7131659388542175)
('腫', 0.7036600112915039)
('瘍', 0.6957879662513733)
('症', 0.6940966844558716)

想像以上にヘビーな結果となったが、ちゃんと機能してそうに見える。

必要なデータを集める

必要なデータは2つ

使用する際にはPythonのリスト型にしておく。

必要なフィルター群

ルールに対応したフィルター群

前処理フィルター

タブを全角スペースに置換しておく。

# 前処理「\t」はトリムしておく。全角スペース化
def preprocessing(is_era_name):
    return is_era_name.replace(u'\t', u' ')

2文字フィルター

ルール2. 漢字2字であることを実装。 2文字以外は弾く。

# 2文字か
def check_2word(is_era_name):
    if len(is_era_name) == 2:
        return True
    else:
        return False

常用漢字フィルター

ルール4. 常用漢字であることを実装。 常用漢字のリストをjoyo_kanjiとする。 常用漢字以外なら弾く。

# 常用漢字か
def check_joyo_kanji(is_era_name):
    one = is_era_name[:1]
    two = is_era_name[1:]
    if one in joyo_kanji and two in joyo_kanji:
        return True
    else:
        return False

過去元号フィルター

ルール3. これまでに元号として用いられたものでないことを実装。 過去の元号のリストをgengoとする。 過去の元号なら弾く。

# 過去の元号として使われていないか
def check_kakogengo(is_era_name):
    if is_era_name in gengo:
        return False
    else:
        return True

既存品詞フィルター

ルール5. 俗用されているものでないことを実装。 俗用されているもの = 品詞として存在するもの として元記事にしたがってMeCab形態素解析を行う。

Mecabによる形態素解析によって、 分かれる       = 他の意味は無い。 分かれずに一語と認識 = 他の意味で認識された。

# 品詞として存在しないか
def check_hinsi(is_era_name, mecab):
    wordHinsi = []
    parsed_line = mecab.parse(is_era_name)

    wordsinfo_list = parsed_line.split("\n")

    for wordsinfo in wordsinfo_list:
        info_list = wordsinfo.split("\t")
        if len(info_list) > 2:
            wordHinsi.append((info_list[0], info_list[1], info_list[3]))

    # 分かれたかを判別
    if(len(wordHinsi) == 2):
        return True
    return False

二語の距離フィルター

ルール1. 国民の理想としてふさわしいようなよい意味を持つものであること元号の語の組み合わせについて、 元記事の「AIに「元号として適切な組み合わせ」を作らせる」項の仮説

過去元号の距離感に近づく組み合わせにする

がもっともらしいので適用してみる。 つまり、過去の元号たちの二語のコサイン類似度を計算し、 平均と標準偏差から二語の距離の範囲を絞る。 平均と標準偏差は元記事の値の平均: 0.30、標準偏差: 0.24を使う。

# 語と語の距離感が適切か(平均:0.3, 標準偏差:0.24 以内か)
def check_appropriate_distance(is_era_name, model):
    distance = model.similarity(is_era_name[:1], is_era_name[1:])
    print("適切な語と語の距離感 0.06 <= x <= 0.54")
    print("語と語の距離感: {}".format(distance))
    if(0.30-0.24 < distance and distance < 0.30+0.24):
        return True
    else:
        return False

語の適正フィルター

ルール1. 国民の理想としてふさわしいようなよい意味を持つものであることを実装。

やっぱり最大の難関は「よい意味の判断」で求めた 一語目の平均: 0.33、二語目の平均: 0.26を中心として、 標準偏差: 0.23の範囲内に一語目・二語目が入っていればOKとする。

# 一語目と二語目がそれぞれ元号として適切な語か(一語目平均:0.33, 二語目平均:0.26, 標準偏差:0.23 以内か)
def check_meaning(is_era_name):
    distancelist1=[]
    distancelist2=[]
    mean1 = 0.33
    mean2 = 0.26
    std1 = std2 = 0.23

    # 一語目と過去の元号一語目との距離感を測る
    for i in range(len(gengo)):
        distance = model.similarity(is_era_name[:1], gengo[i][0])
        if not (distance == 1.0):
            distancelist1.append(distance)
      
    # 二語目と過去の元号二語目との距離感を測る      
    for j in range(len(gengo)):
        distance = model.similarity(is_era_name[1:], gengo[j][1])
        if not (distance == 1.0):
            distancelist2.append(distance)
               
    distance1 = np.mean(distancelist1)
    distance2 = np.mean(distancelist2)
    print("適切な一語目 {} <= x <= {}, 適切な二語目 {} <= y <= {}".format(mean1-std1, mean1+std1, mean2-std2, mean2+std2))
    print("一語目 平均: {}, 二語目 平均: {}".format(distance1, distance2))
    
    if(mean1-std1 < distance1 and distance1 < mean1+std1 and mean2-std2 < distance2 and distance2 < mean2+std2):
        return True
    else:
        return False

MTSHフィルター

実は最大の難関はここだった

MeCabを使って読み仮名を取ろうとしたが、訓読みで判別されたりして、上手くいかなかったので断念。 そこは人力でフィルタリングしてもらおう。

# 音読みなので「ガン・ゲン」で出て欲しい
'元\tモト\t元\t名詞-一般\t\t\nEOS\n'

1つの関数にまとめる

ifの深いネストを使って1つの関数にまとめあげる。 引数には ( 元号っぽい文字列, char2vecのモデル, MeCabインスタンス ) を指定する。

def judge(is_era_name, model, mecab):
    # 前処理
    preprocessing(is_era_name)
    # 2文字か
    if check_2word(is_era_name):
        # 過去の元号として使われてないか
        if check_kakogengo(is_era_name):
            # 常用漢字か
            if check_joyo_kanji(is_era_name):
                # 既存の品詞でないか
                if check_hinsi(is_era_name, mecab):
                    # 語と語の距離感が適切か
                    if check_appropriate_distance(is_era_name, model):
                        # 一語目と二語目がそれぞれ元号として適切な語か
                        if check_meaning(is_era_name):
                            print("元号かもしれないです!\n(MTSHの確認は人がしてください)")
                            return True
                        else:
                            print("単語の意味が元号向きではないようです")
                    else:
                        print("単語間の距離が適切ではないようです")
                else:
                    print("既存の品詞のようです")
            else:
                print("常用漢字ではないようです")
        else:
            print("過去に元号として使用されています")
    else:
        print("2語ではないようです")
    print("元号ではなさそうです")
    return False

デモ

ちゃんと動くか試してみる。 元記事でAIが導き出した元号「孝天」で実験。 元記事と評価基準がほぼ変わらないのでちゃんと動けば元号認定されるはず。

# 元記事でAIが導き出した元号
judge("孝天", model, mecab)
-----孝天-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.4474194347858429
適切な一語目 0.1 <= x <= 0.56, 適切な二語目 0.03 <= y <= 0.49
一語目 平均: 0.43369507789611816, 二語目 平均: 0.32817038893699646
元号かもしれないです!
(MTSHの確認は人がしてください)

ちゃんと元号認定された。 ついでに元記事第二候補の「元清」も元号認定されました。

-----元清-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.46765050292015076
適切な一語目 0.1 <= x <= 0.56, 適切な二語目 0.03 <= y <= 0.49
一語目 平均: 0.3887541890144348, 二語目 平均: 0.40423762798309326
元号かもしれないです!
(MTSHの確認は人がしてください)

せっかくなので他にも試してみよう。

  • 予想数TOPランカー 「平和」、「安久」
  • 鈴木洋仁氏の予想 「感永」
  • 相田満氏の予想 「玉英」
  • 藤井青銅氏の予想 「永元」
-----平和-----
既存の品詞のようです
元号ではなさそうです

-----安久-----
既存の品詞のようです
元号ではなさそうです

-----感永-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: -0.050712406635284424
単語間の距離が適切ではないようです
元号ではなさそうです

-----玉英-----
既存の品詞のようです
元号ではなさそうです

-----永元-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.6839832067489624
単語間の距離が適切ではないようです
元号ではなさそうです

-----永元-----
適切な語と語の距離感 0.06 <= x <= 0.54
語と語の距離感: 0.6839832067489624
単語間の距離が適切ではないようです
元号ではなさそうです

全てあえなく撃沈。 ちゃんと機能してないのか??? 単語間の距離が適切ではないと弾かれてるものが多いが、 過去の元号では距離がマイナスなものもそこそこあるので「感永」は結構いい感じかもしれない。

発表された新元号が偽物判定されたらどうしよう

元号が発表された続き

エイプリルフールに発表される新元号が、偽物かどうかAIだけで判別する物語【完】

PythonによるSelenium Tips

マグロ大学(近畿大学)アドベントカレンダー 2018の19日目

はじめに

何番煎じだっていうSeleniumの記事です。 使っていてパーツパーツしてるなと思ったので、各パーツ単位で解説してます。 Seleniumってすごいですね。基本的なHTMLとCSSが分かっていれば(分からなくても)難しいことをせずにデータのスクレイピングができます。機械学習やりたいけど、素材の集め方分からないって人は学習コストほぼなしでデータをそろえることができるのでおすすめです。 後、勝手にブラウザが動き回るのは見てても楽しいし、テンション上がる。

Seleniumとは

所謂ヘッドレスブラウザーができるやつです。コードを書いて自動でブラウザを操作することができます。こいつのすごいところはブラウザを実際に動かしているので、Reactなど動的に要素を生成するホームページでも要素を取得することができます。 ただ、難点としてBeautifulSoupで解析してrequests等で要素を取得する場合に比べて実行速度が遅いです。並列化すれば多少はマシになるかもしれないです。(やってないので知らない)

使うもの

  • python3系
  • selenium
  • Chrome
  • Chromeのドライバー (PATHを通しておくと楽 本記事では通した前提)

Import

import系一覧

# 行動起点 全ての始まり
from selenium import webdriver
# キーの入力を受け付ける
from selenium.webdriver.common.keys import Keys
# スクロール等の動作をする場合必要
from selenium.webdriver.common.action_chains import ActionChains
# 最初にオプションとしてつけることでヘッドレスにできる
from selenium.webdriver.chrome.options import Options
# 必須 サーバー側に配慮のないリクエストはDoS攻撃と同義
import time

基本的な使い方

# ---上記import文達---

# ヘッドレスにする場合、下記の4行を使う
# options = Options()
# options.add_argument('--headless')
# options.add_argument('--disable-gpu')
# driver = webdriver.Chrome(options=options)
driver = webdriver.Chrome()

# ドライバが設定されるまでの待ち時間を設定する。
driver.implicitly_wait(10)
# リンクに転移
driver.get("http://www.python.org")
# 要素の取得
element = driver.find_element_by_name("q")
# 要素に対しての操作
element.clear()
element.send_keys("pycon")
element.send_keys(Keys.RETURN)
# ドライバ終了
driver.close()

pyconって文字がsearch欄に入力されて一瞬でブラウザ消えたのが見えれば動いてます。 使えるメソッド関連は下記の記事がわかりやすく、まとまっています。 Selenium webdriverよく使う操作メソッドまとめ

リンクを新規タブで開く

  • Chromeデフォルトの機能 Ctl+Enter(MacはCommand+Enter)で新規タブを開く
  • JavaScriptを使う

上記2つの方法が使えます。 ただし、1つ目の方法はあくまでもリンクを踏んだ時にしか使えません。

# リンク要素
link_element = ... #driver.find_element

# ---1つ目の方法---
# デフォルト機能で新規タブを開く (MacならCONTROLをCOMMANDにする)
link_element.send_keys(Keys.CONTROL, Keys.ENTER)
# ---2つ目の方法---
# javascriptで新規タブを開く
driver.execute_script("window.open(arguments[0], 'newtab')", link_element)

# タブの変更
driver.switch_to.window(driver.window_handles[1])
# 何らかの処理
...
# タブの消去
driver.close()
# 元のタブに戻る
driver.switch_to.window(driver.window_handles[0])

特定の要素までスクロール

ActionChainsを使うと特定の要素までスクロールできます。

# スクロールする目的地の要素
scroll_element = ... #driver.find_element
# 初期化
actions = ActionChains(driver)
# 動作の決定
actions.move_to_element(scroll_element)
# 実行
actions.perform()

リストなど同系の要素を全て取得する

基本的に複数の要素をまとめて取得するときはfind_elementsメソッドを使ってリストで取得するのが普通です。 しかし、取得したい要素が他のクラス名と被っていて余計なものまで取得してしまうような場合には、少し手間ですがX-Pathを使うのがおすすめです。 基本的にまとめて取得したいものは要素の形がほぼ一緒で番号だけが違うと言うことが多いのでそこを利用して、format()メソッドで部分的に変更して要素を取得していきます。

# ---普通---
elements = driver.find_elements_by_class_name("・・・")
# ---X-Pathを使う例---
elements = []
# format()メソッドで使う添字
i=1
While True:
    try:
        # X-Pathは各自で解析してformat()で変えるべきところを探してください
        elements.append(driver.find_element_by_xpath('//*[@id="content"]/div/section/div[1]/div[{}]'.format(i))
        # X-Pathの要素をずらしていく
        i+=1
    except:
        break

画像を保存する

画像を保存するのはrequestsを使う方が簡単 スクリーンショットでいいならsave_screenshot(export_file_name)メソッドを要素の後につけるだけで撮れます。

# 入ってない場合はpipでinstall
import requests
# 高水準ファイルライブラリ
import shutil

# ファイル名
export_file_name = ...
# imageタグのsrcやcssのbackground-image等、画像のパスを取得
img = icon.value_of_css_property("background-image")
# 画像を取得
res = requests.get(img, stream=True)
# 画像を保存
with open(export_file_name, "wb") as f:
    shutil.copyfileobj(res.raw, f)

最後に

情報が結構バラバラに点在してたので自分が使った分だけでも、まとめてみました。 基本的にSeleniumは情報量が多いので、ググれば結構親切に出てきてそういった面でも学習コストは低いと思います。 例外処理とtime.sleep()だけ忘れずにスクレピングを楽しみましょう。

ChainerRLでブロック崩しを学習する

CSGAdventCalendar最終日です。 ChainerRLを使ってブロック崩しの学習をさせるチュートリアルをやりました。 実装はGoogleColaboratoryを使いました。

ylt2p-aihop.gif

ChainerRLとは

Chainerを使って実装していた深層強化学習アルゴリズムを”ChainerRL”というライブラリとしてまとめて公開したもの。 以下のような最近の深層強化学習アルゴリズムを共通のインタフェースで使えるよう実装している。

  • Deep Q-Network (Mnih et al., 2015)
  • Double DQN (Hasselt et al., 2016)
  • Normalized Advantage Function (Gu et al., 2016)
  • (Persistent) Advantage Learning (Bellemare et al., 2016)
  • Deep Deterministic Policy Gradient (Lillicrap et al., 2016)
  • SVG(0) (Heese et al., 2015)
  • Asynchronous Advantage Actor-Critic (Mnih et al., 2016)
  • Asynchronous N-step Q-learning (Mnih et al., 2016)
  • Actor-Critic with Experience Replay (Wang et al., 2017)
  • etc.

準備

ChainerRLのインストール

!apt-get -qq -y update
# Install Chainer and CuPy!
!apt-get -qq -y install libcusparse8.0 libnvrtc8.0 libnvtoolsext1 > /dev/null
!ln -snf /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so.8.0 /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so
!pip install cupy-cuda80 chainer
# Install ChainerRL and OpenAI Gym
!apt-get -qq -y install xvfb freeglut3-dev ffmpeg cmake swig zlib1g-dev> /dev/null
!pip -q install chainerrl atari-py gym 'gym[atari]' 'gym[box2d]'  pyglet pyopengl pyvirtualdisplay

Google driveとの接続

atari のゲームを DQNで学習させると、とても時間がかかります。そのため、Google drive に経過を保存できるようする。 次のコードセルを実行し、以下の手順で Google アカウントの認証を行います。

  • URLが表示されるのでそれをクリック
  • Google アカウントにログイン
  • 表示されるトークンをコピー
  • このノートに戻って、テキストボックスにそのトークンを貼り付け
  • 再度URLが表示されるのでそれをクリック
  • このノートに戻って、テキストボックスにそのトークンを貼り付け
!apt-get install -y -qq software-properties-common python-software-properties module-init-tools > /dev/null
!add-apt-repository -y ppa:alessandro-strada/ppa 2>&1 > /dev/null
!apt-get update -qq 2>&1 > /dev/null
!apt-get -y install -qq google-drive-ocamlfuse fuse > /dev/null

from google.colab import auth
auth.authenticate_user()

# Generate creds for the Drive FUSE library.
from oauth2client.client import GoogleCredentials
creds = GoogleCredentials.get_application_default()
import getpass
!google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret} < /dev/null 2>&1 | grep URL
vcode = getpass.getpass()
!echo {vcode} | google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret}

!mkdir -p drive
!google-drive-ocamlfuse drive

環境の準備

import chainer
from chainer import functions as F
from chainer import links as L
import chainerrl
from chainerrl.envs import ale
import numpy as np

chainerrl.experiments.prepare_output_dir ではログ出力のディレクトリを設定します。 ale.ALEで環境を作ります。学習用の環境と、バリデーション用の環境を作ります。 また、学習用の環境は、報酬を -1〜1の範囲にクリップするため、chainerrl.misc.env_modifiers.make_reward_clippedを呼び出します。

outdir = chainerrl.experiments.prepare_output_dir(None, "drive/dqn_out")

ROM = "breakout"
TRAIN_SEED = 0
TEST_SEED = 2 ** 16 - 1 - TRAIN_SEED

env = ale.ALE(ROM, use_sdl=False, seed=TRAIN_SEED)
chainerrl.misc.env_modifiers.make_reward_clipped(env, -1, 1)
eval_env = ale.ALE(ROM, use_sdl=False,
                       treat_life_lost_as_terminal=False,
                       seed=TEST_SEED)

n_actions = env.number_of_actions

q_func = chainerrl.links.Sequence(
            chainerrl.links.NatureDQNHead(),
            L.Linear(512, n_actions),
            chainerrl.action_value.DiscreteActionValue)

# Use the same hyper parameters as the Nature paper's
optimizer = chainer.optimizers.RMSpropGraves(lr=2.5e-4, alpha=0.95, momentum=0.0, eps=1e-2)

optimizer.setup(q_func)

rbuf = chainerrl.replay_buffer.ReplayBuffer(10 ** 6)

explorer = chainerrl.explorers.LinearDecayEpsilonGreedy(
        1.0, 0.1,
        10 ** 6,
        lambda: np.random.randint(n_actions))

# In testing DQN, randomly select 5% of actions
eval_explorer = chainerrl.explorers.ConstantEpsilonGreedy(5e-2, lambda: np.random.randint(n_actions))

def dqn_phi(screens):
    assert len(screens) == 4
    assert screens[0].dtype == np.uint8
    raw_values = np.asarray(screens, dtype=np.float32)
    # [0,255] -> [0, 1]
    raw_values /= 255.0
    return raw_values

agent = chainerrl.agents.DQN(q_func, optimizer, rbuf, gpu=0, gamma=0.99,
                explorer=explorer, replay_start_size=5 * 10 ** 4,
                target_update_interval=10 ** 4,
                clip_delta=True,
                update_interval=4,
                batch_accumulator='sum', phi=dqn_phi)

学習

import sys
STEPS = 10 ** 7
def step_hook(env, agent, step):
  sys.stdout.write("\r{} / {} steps.".format(step, STEPS))
  sys.stdout.flush()
    
chainerrl.experiments.train_agent_with_evaluation(
    agent=agent, env=env, steps=STEPS,
    eval_n_runs=10, eval_interval=10 ** 5,
    outdir=outdir, eval_explorer=eval_explorer,
    eval_env=eval_env, step_hooks=[step_hook])

モデルのリロード

import pandas as pd
import glob
import os
model_files = glob.glob("drive/dqn_out/*/*/model.npz")
model_files.sort(key=os.path.getmtime)
last_model_dir = os.path.dirname(model_files[-1])
last_model_dir

import chainer
from chainer import functions as F
from chainer import links as L
import chainerrl
from chainerrl.envs import ale
import numpy as np

ROM = "breakout"
TRAIN_SEED = 0
TEST_SEED = 2 ** 16 - 1 - TRAIN_SEED

env = ale.ALE(ROM, use_sdl=False, seed=TRAIN_SEED)
chainerrl.misc.env_modifiers.make_reward_clipped(env, -1, 1)
eval_env = ale.ALE(ROM, use_sdl=False,
                       treat_life_lost_as_terminal=False,
                       seed=TEST_SEED)

n_actions = env.number_of_actions
q_func = chainerrl.links.Sequence(
            chainerrl.links.NatureDQNHead(),
            L.Linear(512, n_actions),
            chainerrl.action_value.DiscreteActionValue)

# Use the same hyper parameters as the Nature paper's
optimizer = chainer.optimizers.RMSpropGraves(lr=2.5e-4, alpha=0.95, momentum=0.0, eps=1e-2)

optimizer.setup(q_func)

rbuf = chainerrl.replay_buffer.ReplayBuffer(10 ** 6)

explorer = chainerrl.explorers.LinearDecayEpsilonGreedy(
        1.0, 0.1,
        10 ** 6,
        lambda: np.random.randint(n_actions))

def dqn_phi(screens):
    assert len(screens) == 4
    assert screens[0].dtype == np.uint8
    raw_values = np.asarray(screens, dtype=np.float32)
    # [0,255] -> [0, 1]
    raw_values /= 255.0
    return raw_values

agent = chainerrl.agents.DQN(q_func, optimizer, rbuf, gpu=0, gamma=0.99,
                explorer=explorer, replay_start_size=5 * 10 ** 4,
                target_update_interval=10 ** 4,
                clip_delta=True,
                update_interval=4,
                batch_accumulator='sum', phi=dqn_phi)
agent.load(last_model_dir)

学習結果の確認

import pandas as pd
import glob
import os
score_files = glob.glob("drive/dqn_out/*/scores.txt")
score_files.sort(key=os.path.getmtime)
score_file = score_files[-1]
df = pd.read_csv(score_file, delimiter='\t' )
df

結果が下記です。 f:id:wisteria30:20200326000052p:plain グラフ化します。

df[["max", "median", "mean", "stdev", "min"]].plot()

f:id:wisteria30:20200326000104p:plain

実行結果の確認

frames = []
for i in range(10):
    obs = env.reset()
    done = False
    R = 0
    t = 0
    while not done:
        action = agent.act(obs)
        obs, r, done, _ = env.step(action)
        frames.append(env.ale.getScreenRGB())
        R += r
        t += 1
    print('test episode:', i, 'R:', R)
    agent.stop_episode()

アニメーションの作成 最初のやつです。

import matplotlib.pyplot as plt
import matplotlib.animation
import numpy as np
from IPython.display import HTML

fig = plt.figure(figsize=(5, 5))
plt.axis('off')

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig, images, interval=30, repeat_delay=1)

HTML(ani.to_jshtml())

ylt2p-aihop.gif

終わりに

チュートリアルそのまま行うだけで簡単にDQNで学習とわかりやすい形で結果の可視化ができました。 色々いじって遊びたいと思います。