20200120のPythonに関する記事は30件です。

スマートロック「SESAME mini」とRaspberry Pi Zero WHを使ってWeWorkのオフィスの鍵をICカードで解錠/施錠できるようにした

弊社はWeWorkのプライベートオフィスに入居しています。ビル内のラウンジエリアなどの共用スペースはWeWorkメンバー用のICカードで解錠できるのですが、オフィスの部屋の鍵は物理キーになっているのがほんのり不満でした。部屋もICカードで開けられたら便利ですのに。

何か方法がないかWeWorkのサポートに問い合わせたところ、市販のスマートロックを利用している会社があるという話だったので、弊社もそれに倣うことにしました。

セサミminiを導入

工事不要でドアに取り付け、スマホのアプリで鍵の開け締めができる、いわゆるスマートロックはさまざまなメーカーから発売されています。

ただ、WeWorkのプライベートオフィスの鍵はちょっと特殊な形状をしている(取付可能なエリアの幅が狭い、サムターンのつまみが薄い)ため、どんなスマートロックでも取り付けられるわけではありませんでした。いろいろと調べた結果、セサミminiなら大丈夫そうだと判断し、オンラインショップから購入しました。

決めたポイントは以下の点です。

  1. 幅が57mmとコンパクトなのでプライベートオフィスのドアにも取り付けられる
  2. どんな形状の鍵でも3Dプリンターでアダプターを製作してくれる
  3. 物理キーも引き続き使用できる(夜間の清掃に入ってもらうため必要)
  4. 価格が比較的安く、ランニングコストが一切かからない

特に2点めが他とは異なる決め手になりました。実際に、鍵の写真をメールしたら即レスがあり、アダプターを送付してくれるという神対応でした。

そのおかげで、無事にスマホアプリで解錠/施錠ができるようになりました。
sesame01.gif

色のバリエーションにマットブラックがあったのも良かったですね。ドアの色と合っていていい感じです。

セサミ WiFi アクセスポイントを追加で導入

セサミ単体ではスマホアプリとのBluetooth接続しかできませんが、専用のWiFiアクセスポイントを追加すると、インターネット接続があれば世界中のどこからでもオフィスに取り付けたセサミにアクセスできるようになります。

さらに、セサミはAPIが公開されていますので、独自にカスタマイズすることが可能です。

物理キーではなくスマホアプリで操作できるようになっただけでも、以前よりかなり便利にはなったのですが、やはり理想はICカードのタッチで開け締めできることです。そのためにはWiFiアクセスポイントが欠かせないので追加で購入しました。

ラズパイとICカードリーダーのセットアップ

ICカードを読み取ってAPIを操作する処理にはRaspberry Piを使います。参考になる記事がすでに存在したので、概ねその手順に沿って進めました。

まずは必要な機器を揃えます。すべてAmazonで購入しました。

そして先述の記事の内容に沿ってセットアップを進めたのですが、USBハブやマウス、キーボードは手持ちがなかったので、以下の記事を参考にしてWiFi経由で操作することにしました。

上記の手順はセキュリティのことをあまり考慮されていないので、そこらへんはちゃんとしておきましょう。いろんな人が混在するシェアオフィスですしね。

WeWorkのICカードを読み取ってセサミAPIを操作する

それでまあ、先のラズパイでセサミを操作する記事に従って進めればだいたいOKなのですが、ちょっとそのままでは上手くいかない部分があります。WeWorkのカードはSuicaじゃないのと、記事で使われているAPIのバージョンが古いせいです。

なので、上手くいかない部分を修正して以下のようなコードを完成させました。なお、現行バージョンのAPIではAPIキーが必要になります。APIキーの取得方法はこちらに記載されています。

SesameNFC.py
# -*- coding: utf-8 -*-
import requests
import json
import binascii
import nfc
import time
import traceback
from threading import Thread, Timer

# ICカード待ち受けの1サイクル秒
TIME_cycle = 1.0
# ICカード待ち受けの反応インターバル秒
TIME_interval = 0.2
# タッチされてから次を開始するまでの無効化する秒
TIME_wait = 3

#SESAME API KEY
API_key = "セサミのAPIキー"

# NFC接続リクエストのための準備
# 106A(NFC type A)で設定
target_req_nfc = nfc.clf.RemoteTarget("106A")

print 'ICカードをタッチしてください...'
while True:
    try:
        # USB接続されたカードリーダーをインスタンス化
        clf = nfc.ContactlessFrontend('usb')
        # ICカード待ち受け開始
        # clf.sense( [リモートターゲット], [検索回数], [検索の間隔] )
        target_res = clf.sense(target_req_nfc, iterations=int(TIME_cycle//TIME_interval)+1 , interval=TIME_interval)

        if target_res != None:

            tag = nfc.tag.activate(clf, target_res)

            #IDを取り出す
            idm = binascii.hexlify(tag.identifier).upper()
            print 'NFC detected. ID = ' + idm

            #ICカードのチェック
            if (idm == "tagtool.pyで取り出したID"):
                url_control = "https://api.candyhouse.co/public/sesame/セサミの端末ID"
                head_control = {"Authorization": API_key, "Content-type": "application/json"}
                # get status
                response_control = requests.get(url_control, headers=head_control)
                # 次のリクエストまで間隔が短いとエラーになる?ので数秒待つ
                time.sleep(1)
                res = json.loads(response_control.text)
                stats = res["locked"]

                if (stats == True):
                    # unlock
                    payload_control = {"command":"unlock"}
                    response_control = requests.post(url_control, headers=head_control, data=json.dumps(payload_control))

                else:
                    # lock
                    payload_control = {"command":"lock"}
                    response_control = requests.post(url_control, headers=head_control, data=json.dumps(payload_control))

                print(response_control.text)
                print 'sleep ' + str(TIME_wait) + ' seconds'
                time.sleep(TIME_wait)

            #end if
        #end if

        clf.close()
    #end try

    except KeyboardInterrupt:
        print("Ctrl+Cで停止しました")
        clf.close()
        break

    except:
        clf.close()
        traceback.print_exc()
        pass

#end while

これでWeWorkのICカードでセサミの解錠/施錠ができるようになりました!
sesame02.gif

ケーブルの挿入口の都合でICカードリーダーのマークが天地逆転してしまったため、WeWorkのステッカーを貼ってごまかしています。結果的にオフィシャルアイテムっぽくなっていて素敵なんじゃなかろうか。

ICカードタッチや解錠/施錠、エラー時にLEDを点滅させる

とりあえず動くようにはなったのですが、実はこのコードがしばしばエラーで解錠/施錠に失敗します。どうやらAPIかWiFiアクセスポイントの反応が遅れるときがあるようで、真面目にやるならリトライ処理なんかを組み込むべきなんでしょうが、もう一回タッチすればだいたい上手くいくからまあいいか……と手直しせず放置しています。

その代わりに、イベントごとにLEDを点滅させるようにしました。点滅のパターンによってどの処理が行われたのか判断つきますし、何よりラズパイやるならLチカは王道ですし!

LEDを点滅させる方法は以下の記事を参考にしました。
ラズベリーパイ(Raspberry Pi Zero WH)でPython3 Lチカ(LED点滅)させる方法

そしてLチカを組み込んだコードが以下のとおりです。

SesameNFC_LED.py
# -*- coding: utf-8 -*-
import requests
import json
import binascii
import nfc
import time
import traceback
import RPi.GPIO as GPIO
from threading import Thread, Timer

# ICカード待ち受けの1サイクル秒
TIME_cycle = 1.0
# ICカード待ち受けの反応インターバル秒
TIME_interval = 0.2
# タッチされてから次を開始するまでの無効化する秒
TIME_wait = 3

#SESAME API KEY
API_key = "セサミのAPIキー"

# NFC接続リクエストのための準備
# 106A(NFC type A)で設定
target_req_nfc = nfc.clf.RemoteTarget("106A")

print 'ICカードをタッチしてください...'
while True:
    try:
        # USB接続されたカードリーダーをインスタンス化
        clf = nfc.ContactlessFrontend('usb')
        # ICカード待ち受け開始
        # clf.sense( [リモートターゲット], [検索回数], [検索の間隔] )
        target_res = clf.sense(target_req_nfc, iterations=int(TIME_cycle//TIME_interval)+1 , interval=TIME_interval)

        if target_res != None:

            tag = nfc.tag.activate(clf, target_res)

            #IDを取り出す
            idm = binascii.hexlify(tag.identifier).upper()
            print 'NFC detected. ID = ' + idm

            #ICカードのチェック
            if (idm == "tagtool.pyで読み取ったID"):

                #GPIO番号の指定モードを設定(BCM:役割ピン番号、BOARD:PIN番号)
                GPIO.setmode(GPIO.BCM)
                #23番ピン(緑色LED)を出力として使用するように設定
                GPIO.setup(23, GPIO.OUT)
                # 点滅
                for i in range(3):
                    GPIO.output(23, GPIO.HIGH)
                    time.sleep(0.5)
                    GPIO.output(23, GPIO.LOW)
                    time.sleep(0.5)
                #GPIOの設定をリセット
                GPIO.cleanup()

                url_control = "https://api.candyhouse.co/public/sesame/セサミの端末ID"
                head_control = {"Authorization": API_key, "Content-type": "application/json"}
                # get status
                response_control = requests.get(url_control, headers=head_control)
                # 次のリクエストまで間隔が短いとエラーになる?ので数秒待つ
                time.sleep(1)
                res = json.loads(response_control.text)
                stats = res["locked"]

                if (stats == True):
                    # unlock
                    payload_control = {"command":"unlock"}
                    response_control = requests.post(url_control, headers=head_control, data=json.dumps(payload_control))
                    # 解錠時は緑色LED点灯
                    GPIO.setmode(GPIO.BCM)
                    GPIO.setup(23, GPIO.OUT)
                    GPIO.output(23, GPIO.HIGH)
                    time.sleep(3)
                    GPIO.output(23, GPIO.LOW)
                    GPIO.cleanup()

                else:
                    # lock
                    payload_control = {"command":"lock"}
                    response_control = requests.post(url_control, headers=head_control, data=json.dumps(payload_control))
                    # 施錠時は緑色LED早く点滅
                    GPIO.setmode(GPIO.BCM)
                    GPIO.setup(23, GPIO.OUT)
                    for i in range(6):
                        GPIO.output(23, GPIO.HIGH)
                        time.sleep(0.25)
                        GPIO.output(23, GPIO.LOW)
                        time.sleep(0.25)
                    GPIO.cleanup()

                print(response_control.text)

                print 'sleep ' + str(TIME_wait) + ' seconds'
                time.sleep(TIME_wait)

            #end if
        #end if

        clf.close()
    #end try

    except KeyboardInterrupt:
        print("Ctrl+Cで停止しました")
        clf.close()
        # エラー時赤色LED(22番ピン)を激しく点滅
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(22, GPIO.OUT)
        for i in range(10):
            GPIO.output(22, GPIO.HIGH)
            time.sleep(0.1)
            GPIO.output(22, GPIO.LOW)
            time.sleep(0.1)
        GPIO.cleanup()
        break

    except:
        clf.close()
        GPIO.cleanup()
        traceback.print_exc()
        # エラー時赤色LEDを激しく点滅
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(22, GPIO.OUT)
        for i in range(20):
            GPIO.output(22, GPIO.HIGH)
            time.sleep(0.1)
            GPIO.output(22, GPIO.LOW)
            time.sleep(0.1)
        GPIO.cleanup()
        pass

#end while

……とりあえず動くようにしただけの頭の悪いコードですが、業務に支障が出るわけでもないのでやっぱり手直しせずに放置しています。

デスク上でLEDが点滅しているのがおわかりいただけるでしょうか。このときばかりはWeWorkのガラス壁面をありがたいと思いました。普通の壁だったらICカードリーダーやLEDの設置がもっと面倒だったことでしょう。
sesame03.gif

おわりに

こうして、WeWorkのICカードで部屋の鍵を開け締めしたいという要望を実現することができました。

コードのイケてなさやAPIとWiFiアクセスポイントの反応の遅さのせいで、解錠/施錠にしばしば失敗したり、タッチから解錠/施錠まで数秒かかるという課題はあるものの、利用する人数や頻度がそれほどでもないのでまあよしとしています。ぶっちゃけ、私が快適ならそれでいいです。

ということで、物理キーやスマホ操作から解放され、ICカードさえあれば共用エリアからプライベートオフィスまで自由に行き来できるようになって大変満足しています。めでたしめでたし。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

68日目 【Kaggle入門】ランダムフォレストは単純なやつでした。

前回、性別だけモデルでランダムフォレストで予想したところ、男性全員死亡、女性全員生存というずいぶんな結果になりました。
67日目 【Kaggle入門】ランダムフォレストを使ってみたが?

ランダムフォレストとはいったい何者なのか。

いろいろ実験してみました。

性別&クラスモデルの作成

前回作成した性別だけモデルにPclassを加えます。

21.py
(前略)
#Dataframeを作る
#性別、等級
train_df = train_df.loc[:,['PassengerId','Survived','Sex','Pclass']]
test_df = test_df.loc[:,['PassengerId','Sex','Pclass']]
(以下同文)

その結果Public Score:0.75598・・・下がりました。タイタニック (客船)のWikipediaをみる限りでは、等級ごとに死亡率は大きく異なるはずなのに、不思議です。

生存者と死者の割合のうち、三等船室を利用していた客の死者が多い。三等船室が下部の前方と後方に分断されて配置されており、沈没の際、前方の客室にいた客が脱出するためにはそのまま真上に上がるか、もしくはそのまま船体を突っ切って後方に移動してから真上に上がる2つの方法があった。ところが前者はその真上に一等船室があったためドアが施錠されており、後者の方法だけしかなかったのが、死者が増えた原因だという説もある。

(メモ)三等船室を前室と後室に分けられたら予測をあげられそうです。

クラスだけモデルを確認

22.py
(前略)
#Dataframeを作る
#等級
train_df = train_df.loc[:,['PassengerId','Survived','Pclass']]
test_df = test_df.loc[:,['PassengerId','Pclass']]
(以下同文)

Public Score:0.65550

さらに下がりました。おそらく、性別だけモデルのように、等級だけモデルでは0か1にまとめられている気がします。確かめてみます。

訓練データ

23.py
print(train_df.groupby(['Pclass','Survived']).count())

                 PassengerId
Pclass Survived             
1      0                  80
       1                 136
2      0                  97
       1                  87
3      0                 372
       1                 119

テストデータ

24.py
##確認のため予測結果(submission)にクラスを追加する。
submission['Pclass'] = test_df['Pclass'] 
print(submission.groupby(['Pclass','Survived']).count())
                 PassengerId
Pclass Survived             
1      1                 107
2      0                  93
3      0                 218

訓練データではクラスの中に0と1がありましたが
テストデータ(予測結果)は0か1にまとめられています。

ランダムフォレストは、訓練データの多い方に予測をまとめてしまうようです。

性別&クラスモデルを確認

訓練データ

25.py
print(train_df.groupby(['Sex','Pclass','Survived']).count())
                    PassengerId
Sex Pclass Survived             
0   1      0                  77
           1                  45
    2      0                  91
           1                  17
    3      0                 300
           1                  47
1   1      0                   3
           1                  91
    2      0                   6
           1                  70
    3      0                  72
           1                  72

テストデータ

26.py
#確認のため予測結果(submission)に性別とクラスを追加する。
submission['Sex'] = test_df['Sex'] 
submission['Pclass'] = test_df['Pclass'] 
print(submission.groupby(['Sex','Pclass','Survived']).count())
                     PassengerId
Sex Pclass Survived             
0   1      0                  57
    2      0                  63
    3      0                 146
1   1      1                  50
    2      1                  30
    3      0                  72

訓練データにあったばらつきが、テストデータではまとめられています。
ランダムフォレストは多い方にまとめてしまうようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

68日目 【Kaggle・タイタニック予想】ランダムフォレストは単純なやつでした。

前回、性別だけモデルでランダムフォレストで予想したところ、男性全員死亡、女性全員生存というずいぶんな結果になりました。
67日目 【Kaggle入門】ランダムフォレストを使ってみたが?

ランダムフォレストとはいったい何者なのか。

いろいろ実験してみました。

性別&クラスモデルの作成

前回作成した性別だけモデルにPclassを加えます。

21.py
import pandas as pd
# pandasでCSVを読み込む
train_df = pd.read_csv('train.csv')
test_df = pd.read_csv('test.csv')

#性別を男0女1に変換
train_df.replace({'Sex': {'male': 0, 'female': 1}}, inplace=True)
test_df.replace({'Sex': {'male': 0, 'female': 1}}, inplace=True)

#Dataframeを作る
#性別、等級
train_df = train_df.loc[:,['PassengerId','Survived','Sex','Pclass']]
test_df = test_df.loc[:,['PassengerId','Sex','Pclass']]

(以下同文)

その結果Public Score:0.75598・・・下がりました。タイタニック (客船)のWikipediaをみる限りでは、等級ごとに死亡率は大きく異なるはずなのに、不思議です。

生存者と死者の割合のうち、三等船室を利用していた客の死者が多い。三等船室が下部の前方と後方に分断されて配置されており、沈没の際、前方の客室にいた客が脱出するためにはそのまま真上に上がるか、もしくはそのまま船体を突っ切って後方に移動してから真上に上がる2つの方法があった。ところが前者はその真上に一等船室があったためドアが施錠されており、後者の方法だけしかなかったのが、死者が増えた原因だという説もある。

(メモ)三等船室を前室と後室に分けられたら予測をあげられそうです。

クラスだけモデルを確認

22.py
(前略)
#Dataframeを作る
#等級
train_df = train_df.loc[:,['PassengerId','Survived','Pclass']]
test_df = test_df.loc[:,['PassengerId','Pclass']]
(以下同文)

Public Score:0.65550

さらに下がりました。おそらく、性別だけモデルのように、等級だけモデルでは0か1にまとめられている気がします。確かめてみます。

訓練データ

23.py
print(train_df.groupby(['Pclass','Survived']).count())

                 PassengerId
Pclass Survived             
1      0                  80
       1                 136
2      0                  97
       1                  87
3      0                 372
       1                 119

テストデータ

24.py
print(submission.groupby(['Pclass','Survived']).count())
                 PassengerId
Pclass Survived             
1      1                 107
2      0                  93
3      0                 218

訓練データではクラスの中に0と1がありましたが
テストデータ(予測結果)は0か1にまとめられています。

ランダムフォレストは、訓練データの多い方に予測をまとめてしまうようです。

性別&クラスモデルの確認

訓練データ

25.py
print(train_df.groupby(['Sex','Pclass','Survived']).count())
                    PassengerId
Sex Pclass Survived             
0   1      0                  77
           1                  45
    2      0                  91
           1                  17
    3      0                 300
           1                  47
1   1      0                   3
           1                  91
    2      0                   6
           1                  70
    3      0                  72
           1                  72

テストデータ

26.py
print(submission.groupby(['Sex','Pclass','Survived']).count())
                     PassengerId
Sex Pclass Survived             
0   1      0                  57
    2      0                  63
    3      0                 146
1   1      1                  50
    2      1                  30
    3      0                  72

訓練データにあったばらつきが、テストデータではまとめられています。
ランダムフォレストは多い方にまとめてしまうようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Python3 入門 13日目]7章 文字列(7.1〜7.1.1.1)

7.1 文字列

7.1.1 Unicode

  • コンピュータの記憶の基本単位はバイトで、8個のビットを使って256種類の一意な値を表現できる。

    • ビット:2進数(0と1、バイナリ)で表した数字の桁数。4ビットは4桁、8ビットは8桁の2進数であり、それぞれ2の4乗(16)、2の8乗(256)通りの数字を表せる。
    • バイト:8ビットで1バイト。1バイトを表すのに16進数を使うと分かりやすい。
  • Unicodeは世界の言語の全ての文学と数学、そのほかの分野の記号を定義しようという発展途上の国際標準。

7.1.1.1 Python 3のUnicode文字列

  • Python3の文字列はUnicode文字列であり、バイト列ではない。
  • 文字のUnicode IDまたは名前を知っている場合、Python文字列でそれを使うことができる。
  • Pythonのunicodedataモジュールには双方向ほ変換関数が含まれている。
    • lookup():名前(大文字と小文字を区別しない)を与えると、Unicode文字が返される。
    • name():Unicode文字を与えると、大文字の名前が返される。
>>> def unicode_test(v):
...     import unicodedata
#文字から名前を引き出す
...     name=unicodedata.name(v)
#名前から文字列を引き出す。
...     v2=unicodedata.lookup(name)
...     print("v=%s,name=%s,v2=%s"%(v,name,v2))
... 
>>> unicode_test("A")
v=A,name=LATIN CAPITAL LETTER A,v2=A

#UnicodeのASCII記号
>>> unicode_test("$")
v=$,name=DOLLAR SIGN,v2=$
#Unicodeの通貨記号
>>> unicode_test("\u00a2")
v=¢,name=CENT SIGN,v2=¢
>>> unicode_test("\u20ac")
v=,name=EURO SIGN,v2=
#代替記号の表示
>>> unicode_test("\u2603")
v=,name=SNOWMAN,v2=

>>> place = "cafe"
>>> place
'cafe'
>>> import unicodedata
>>> unicodedata.name('\u00e9')
'LATIN SMALL LETTER E WITH ACUTE'
>>> unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
'é'
#コードにより文字列指定
>>> place = "caf\u00e9"
>>> place
'café'
#名前により文字列指定
>>> place = "caf\N{LATIN SMALL LETTER E WITH ACUTE}"
>>> place
'café'

>>> u="\N{LATIN SMALL LETTER U WITH DIAERESIS}"
>>> u
'ü'

#len()はバイト数ではなく、Unicodeの文字数を数える。
>>> len("&")
1
>>> len("\U0001f47b")
1


感想

エンコーディングやデコーディング、バイト列等聞いた覚えがないような言葉が結構出てきた。一つ一つ調べながらやろう。

参考文献

「Bill Lubanovic著 『入門 Python3』(オライリージャパン発行)」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

S3を使わずにEC2のストレージを利用して、ファイルを保存する

某expertスクールを卒業後、同じようなやり方で簡易的にpythonでapiをアップロードしようとしたら、権限周りで失敗したので備忘録として

EC2の種類

簡易的にpythonのapiを作成するため、S3を使わずストレージ中にapiで出てきたファイルを保存します。

  • tタイプ
  • ubuntu 18.04

EC2ログイン後

スクールのカリキュラムでは、/var/www/を作成して、その中にアプリを入れていましたが、それでは権限周りでエラーが出てしまうので、ログイン後のhome/ubuntu/直下にgit cloneしましょう。

$ ssh -i /path/my-key-pair.pem ubuntu@x.xxx.xxx.xxx(IPv4アドレス)
# ユーザー名部分はubuntuなので注意(ec2-userではない)

(ログイン後)
$ git clone https://github.com/yyuu/pyenv.git ~/.pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

$ pyenv install 3.6.2(ここでpythonのバージョンを選ぶ)
$ pyenv global 3.6.2
$ pyenv rehash

$ apt-get -y update
$ apt-get install -y --fix-missing
$ apt-get build-essential
$ apt-get software-properties-common
$ git clone クローン元のURL

apiをバックグラウンドで起動

通常通りにpython run.appすると、PCを閉じたり、EC2をログアウトした際にアプリもとまってしまうため、下記コマンドでバックグラウンドで起動します。

$ nohup python main.py &
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sparebeatのテストプレイの効率化

Sparebeatの譜面が作りたい!!!

Sparebeat面白いですよね。
めちゃくちゃに遊んでるとなんかそのうちO2Sとかscore makerとか使って譜面作ってみたくなるものです。なりますよね?なれや

現在はSparebeat公式でシミュレータが提供されていて、そこから自作譜面をテストすることができますが、テストプレイって何回もやるのでそのたびいちいちドラッグアンドドロップするのはなかなかめんどくさかったりします。

そこで、ここではシミュレータを介さずに簡単にテストプレイを行う方法を紹介します。

大まかな流れ

  1. 譜面を読み込むhtmlファイルを用意する
  2. そのhtmlファイルがあるディレクトリでlocalhostをたてる
  3. localhostにアクセスする
  4. 譜面(jsonファイル)を更新したらlocalhostを再読込する

順番に追っていきましょうね~

htmlファイルの準備

Sparebeatはブラウザ音楽ゲームなので、プレイする際はhtmlファイルを作ってアクセスすることになります。
とりあえず、こんな感じのindex.htmlを作ってみましょう。

index.html
<html>
    <head>
        <title>Sparebeatのテストプレイ</title>
        <meta charset="UTF-8">
    </head>
    <body style='text-align: center;'>
        <iframe id="sparebeat" width="960" height="640" src="https://sparebeat.com/embed/" frameborder="0"></iframe>
        <script src="https://sparebeat.com/embed/client.js"></script>
        <script>Sparebeat.autoload();</script>
    </body>
</html>

htmlって何?という方もとりあえずメモ帳か何か開いて上記のコードをコピペしてindex.htmlというファイル名でなんかどこかに新しくフォルダを作ってそのどこかのフォルダに保存しておいてください。ここではhtmlを理解する必要はありません。こんなやつで作られてんだな~って思っちゃうくらいで大丈夫です。

index.htmlを置いたフォルダには更に音源となるmp3ファイルをmusic.mp3という名前に、譜面ファイルのjsonファイルをmap.jsonという名前に改名して置いておきます。

そこのフォルダの中身がこんな感じになってればOKです
フォルダ.png

localhostを立ち上げよう

現在の状態でもindex.htmlをダブルクリックするとブラウザが開いてSparebeatっぽい画面が表示されますが、Objectなんたらって言われてプレイすることができません。
ファイルからのアクセスで開いたhtmlでは残念ながらSparebeatはプレイできないらしいのです。
ですが、localhostというものを立ち上げることでプレイすることができます。localhostを立ち上げる方法はググればいくらでも出てくるのですが、今回はPythonを使って立ち上げる方法を紹介します。

Pythonを使ってlocalhostを立ち上げる

Pythonは最近流行りでめっちゃ優秀とウワサのプログラミング言語です。数値計算や機械学習のライブラリが豊富でそっち方面で使っても幸せになりがちなんですが、今回は難しいプログラミングとかはしません。でも、これをきっかけにプログラミングしてない人もプログラミング始めてくれると嬉しいかも?楽しいですよ~

とにもかくにも、まずはPythonをインストールしなければなりません。インストールについてはPython公式のインストール方法に詳しく記載されています。お使いのOSに合わせてPythonをインストールしておいてください。

そしたらさっきindex.htmlを置いたどっかのフォルダをコンソールで開きます。
windowsではさっきのフォルダをshiftキーを押しながら右クリックするとPowerShellウィンドウをここで開くだとかコマンドウィンドウをここで開くなどというのがどっかにあるのでそれを選択してコンソールを開くことができます。cdコマンドで移動しても全然おっけー
ひらく.png

開けましたら以下のコマンドを打ち込みます。

python -m http.server 8080

これだけでlocalhostを立ち上げることができます。8080でなんかダメだったらここの数字を適当なやつに変えてみてください。

ブラウザからlocalhostにアクセスする

ここまでできましたらhttp://localhost:8080/にアクセスしてみましょう。8080を変えていたら8080の部分をそれに対応したものにしてください。
あくせす.png

フォルダに置いた譜面がプレイできるはずです。譜面の更新時、このページをリロードすれば譜面が再読込されて、更新した譜面をプレイすることができると思います。

おわりに

よい譜面制作ライフを~
Python入れたからにはプログラミングするしかないよなあ!?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

書籍「15Stepで踏破 自然言語処理アプリケーション開発入門」をやってみる - 2章Step03メモ

内容

15stepで踏破 自然言語処理アプリケーション入門 を読み進めていくにあたっての自分用のメモです。
今回は2章Step03で、自分なりのポイントをメモります。

準備

  • 個人用MacPC:MacOS Mojave バージョン10.14.6
  • docker version:Client, Server共にバージョン19.03.2

章の概要

MeCabの動作を理解し、チューニングしてみる。
また、MeCab以外の形態素解析器についても確認する。

03.1 MeCab

辞書

MeCabによるわかち書きは、辞書に基づいて行われる。
辞書にはMeCabを使った形態素解析から得られる情報は、辞書にどのような情報が登録されているかに依存し、辞書によって登録されている情報が異なる。

辞書名 内容
IPAdic ・MeCabが公式に推奨している辞書
・IPAコーパスというデータに基づく
UniDic ・UniDicというデータに基づく
・分割される単位が小さく、厳密な「形態素解析」に近い
jumandic ・MeCabとは別のJUMANという形態素解析器で使われている辞書のMeCab移植
・京都コーパスというデータに基づく
・代表表記などのメタ情報が付与されている
ipadic-NEologd ・IPA辞書を元に、単語の数を大幅に拡張
・インターネットから単語をクローリングして語彙を頻繁に拡張しており、新語への対応力がとても高い
正規化を前処理として行うことが推奨されている
unidic-NEologd ・ipadic-NEologd同様、UniDicを元に単語拡張を施した辞書

ipadic-NEologdのインストールと実行

IPAdicとipadic-NEologdの違いとしては、例えば「Deep Learning」(割と新しい単語)という単語の解析が異なる。

  • IPAdic:「Deep」と「Learning」で分割される
  • ipadic-NEologd:「Deep Learning」の1単語で扱われる

MeCabの形態素解析の動作

辞書が保持しているのは実行結果のような形態素に関する情報だけでなく、下記の様々な情報を保持している。

  • 各単語の生起コスト
  • 各単語の左文脈ID
  • 各単語の右文脈ID
  • 文脈IDの各組み合わせの連接コスト

与えられた文章に対して、生起コストと連接コストの組み合わせが最小になる組み合わせを解析結果とする。(下記の例では「東大阪 大好き」と分割した際のコストが(最)小さいので、これが解析結果となる)

例)「東大阪大好き」
# 「東大阪 大好き」と分割する場合
文頭と「東大阪」の連接コスト
「東大阪」の生起コスト
「東大阪」と「大好き」の連接コスト
「大好き」の生起コスト
「大好き」と文末の連接コスト

# 「東大 阪大 好き」と分割する場合
文頭と「東大阪」の連接コスト
「東大」の生起コスト
「東大」と「阪大」の連接コスト
「阪大」の生起コスト
「阪大」と「好き」の連接コスト
「好き」の生起コスト
「大好き」と文末の連接コスト

03.2 MeCab辞書の改造

既存の辞書だけで期待した結果が得られない場合は、自分で辞書をチューニングする。

  • 新語の追加
  • 形態素解析の調整

MeCab辞書のビルド

# ソースファイルのエンコードをUTF-8に変換
$ nkf --overwrite -Ew ./mecab-ipadic-2.7.0-20070801/*

# 辞書をビルド
$ mkdir build
$ $(mecab-config --libexecdir)/mecab-dict-index -d ./mecab-ipadic-2.7.0-20070801 -o build -f utf8 -t utf8
$ cp mecab-ipadic-2.7.0-20070801/dicrc ./build/. # dicrcをコピー

nkfは「Network Kanji Filter」の略。

新語の追加

ソースファイルのディレクトリにcsvファイルを作成する
# 表層形、左文脈ID、右文脈ID、生起コスト、品詞、品詞細分類1、品詞細分類2、品詞細分類3、活用型、活用形、原型、読み、発音

# 自然言語処理を追加したい場合
自然言語処理,1288,1288,0,名詞,固有名詞,一般,*,*,*,しぜんげんごしょり,シゼンゲンゴショリ,シゼンゲンゴショリ

形態素解析の調整

  1. 連接コストを調整する
    1. .csvから対象単語の文脈IDを探す
    2. matrix.defで対象文脈IDの連接コストを修正する
  2. 生起コストを調整する
    1. .csvの対象単語の生起コストを修正する

ただし上記コストを修正する場合は、意図した部分以外の結果にも影響を与えかねないので注意する。

コストを自動調整する手法も用意されているようだが、修正したい部分のコストをピンポイントで手動で調整する方が、影響範囲を小さく抑えられる傾向がありそう。

03.3 様々な形態素解析器

MeCab以外の形態素解析器について概要を把握しておく。

形態素解析器 内容
MeCab ・辞書に基づく
・辞書には、単語と生起コスト、連接コストの情報が含まれる
・実行速度が速い
・辞書が外部ファイル化されているので、必要に応じてカスタマイズできる
JUMAN++ ・ニューラルネットワークを利用した比較的新しい形態素解析器
・文法的な正しさだけでなく、単語の意味を考慮
・ある語の前にある全ての語の情報を考慮
・表記ゆれに対応
・MeCabより優れている点が多いが、実行速度は劣る
KyTea(キューティー) ・ある文字と次の文字の間が単語の区切りであるかどうかを、その前後数文字を元にSVMで予測する
・Pythonラッパーがサードパーティから提供されている
Janome ・Pythonのみで書かれている
・IPA辞書が組み込まれていたり、Pythonから扱えるAPIが提供されている
・実行速度が遅い
・辞書の選択肢が限定される
SudachiPy ・Java用形態素解析SudachiのPythonバインディング
・2019年5月時点では正式リリースはま(もう正式リリースされた?)
Esanpy(Kuromoji) ・KuromojiはJavaで実装された形態素解析器
・Pythonから使う場合、Esanpyを通す
・EsanpyはElasticsearch(全文検索エンジン)を内部で利用するテキスト解析ライブラリ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

学習記録 その25(29日目)

学習記録(29日目)

勉強開始:12/7(土)〜

教材等:
・大重美幸『詳細! Python3 入門ノート』(ソーテック社、2017年):12/7(土)〜12/19(木)読了
・Progate Python講座(全5コース):12/19(木)〜12/21(土)終了
・Andreas C. Müller、Sarah Guido『(邦題)Pythonではじめる機械学習』(オライリージャパン、2017年):12/21(土)〜12月23日(土)読了
Kaggle : Real or Not? NLP with Disaster Tweets :12月28日(土)投稿〜1月3日(金)まで調整
・Wes Mckinney『(邦題)Pythonによるデータ分析入門』(オライリージャパン、2018年):1/4(水)〜1/13(月)読了
・斎藤康毅『ゼロから作るDeep Learning』(オライリージャパン、2016年):1/15(水)〜1/20(月)

『ゼロから作るDeep Learning』

1/20(月) 読み終わり。

8章 ディープラーニング

・7章に出てきたCNNの層を増やし、より深く(ディープ)したものがディープなニューラルネットワーク(ディープラーニング)

・MNISTデータセットような問題、規模ではあまり恩恵が少ないが、より大規模な一般物体認識では認識精度の向上に大きく貢献する。

・アンサンブルやData Augmentation(データ拡張)なども認識精度の向上に貢献している。
 Data Augmentationとは、入力画像に対し、回転や縦横の微小変化を加えることで画像枚数を増やす(画像の嵩増しをする)ことをいう。

・層を深くすることの利点1
 パラメータ数を少なくできる。
 小さなフィルターを重ねてネットワークを深くすることで、ニューロンに変化を生じさせる局所的な空間領域(受容野)を広くカバーできる。

・層を深くすることの利点2
 学習の効率性が高まる。
 ネットワークを深くすることで、学習すべき問題を階層的に分解でき、より単純な問題として取り組むことができる。
 具体的には第1層ではエッジのみ重点を置いて学習、2層以降では、前層で学習した情報をもとに、より高度なパターンを効率良く学習することができる。

・これらの利点がもたらされたのは、層を深くしても正しく学習できるだけの技術や環境(ビッグデータ、GPU等)が整備されてきたというのも大きい。

VGG:畳み込み層とプーリング層から構成される基本的なCNN
 小さなフィルターによる畳み込み層を連続して行ない、プーリング層でサイズを半分に、という処理を繰り返す。
 最後は全結合層を経由して結果を出力する。
 シンプルな構成であり汎用性が高いことから、多くの技術者が用いるベース。

GoogLeNet:縦方向だけでなく、横方向にも深さ(広がり)を持つネットワーク
 サイズの異なるフィルターとプーリングを複数適用し、結果を結合するインセプション構造と呼ばれるものを、一つのビルディングブロックとして使用する。

ResNet:スキップ構造(バイパスやショートカットとも呼ばれる)を導入したネットワーク
 入力データの畳み込み層をまたいで出力に月山する仕組みをとることで、層を深くしても効率よく学習することができる。
 入力データをそのまま流すことで、勾配が小さくなったりする心配がなく、意味のある勾配が伝わっていくことが期待でき、層を深くすることで生じていた勾配消失問題にも軽減効果があるとされている。

・ディープラーニングでは大量の積和演算(もしくは行列計算)を行う必要がある。これを大きく助けているのがGPU
 GPUはグラフィックのための専用ボードとして利用されていたが、大量の並列的演算も得意とするため、ディープラーニングの発展にも多大な影響を与えている。
 (逆にCPUは連続的で複雑な計算を得意としている。)

・実用例1:物体検出
 画像中から物体の位置特定を含めたクラス分類を行う問題

・実用例2:セグメンテーション
 画像に対してピクセルレベルでクラス分類を行う問題
 出力の際には、すべてのピクセルに対してクラス分類を行う。

・実用例3:画像キャプション生成
 NIC(Neural Image Caption)と呼ばれるモデルが有名
 ディープなCNNと自然言語を扱うためのRNN(Recurrent Neural Network)から構成されている。
 画像からCNNによって特徴を抽出し、RNNに渡すことで、この特徴を初期値として、入力テキストを再帰的に生成する。
 本実用例のような複数の種類の情報を組み合わせて処理することをマルチモーダル処理と言う。近年注目を集めている。

・実用例4:自動運転
 構成する技術の中でも、特に重要と言われる周囲を認識する技術で大きく貢献している。
 走路環境を、どのような状況でもロバストに認識できるようになることで、実現に近づく。

・実用例5:Deep Q-Network(強化学習)
 エージェントと呼ばれるものが、環境の状況に応じて行動を選択し、その行動によって環境が変化するというのが基本的な枠組み
 環境の変化によって、エージェントが報酬(観測結果)を得ることで、今後の行動指針も判断していく。
 アルファ碁などもこれにあたる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習でひさ子のギターを自分のギターに持ち替えてもらう -準備編-

CycleGANを使って何かをする

CycleGANの仕組みや詳細はよくわかっていないし、他のいいものを読んでください

馬をしまうまに変えたり、風景画をモネっぽくしたりできるものである。形が同じで色や模様だけ違うものにするのに向いている。そんなにセンスを感じない題材かもしれないけど、個人的に結構嬉しくなりそうなのをやってみよう

サンバーストのジャズマスターをブラックのジャズマスターに塗り替える ことにする。
5.fender_014-1603-300.jpg

s-l300.jpg

最近復活して大騒ぎ、ライブもとっても素敵だった田渕ひさ子さんの使うギターはfenderのジャズマスター、色はサンバースト。私の使ってるギターはCrewsのジャズマスターで色はブラックです。ブランドとかギターの形とかは今回どうでもいいけど、たまたま似ている。

準備編

私の環境

  • macOS High Sierra 10.13.6
  • Anaconda3
  • Python3.7.2
  • Chrome バージョン: 79.0.3945.130(Official Build) (64 ビット)

画像を用意する

google_images_downloadというもので画像を一括ダウンロードできるのでそうしました。
参照:機械学習用の画像集めに便利な「google-images-download」の使い方

pip install google_images_download

インストールして、

googleimagesdownload --keywords "サンバースト ジャズマスター"

などやりました。
デフォで100枚しか持ってこれないけど、100枚あたりが精度的にもちょうどよかった。今回人が一緒に写っているものも欲しかったので、検索ワードを変えながら増やしました。
100枚以上欲しい場合はchromeDriverが必要で、ダウンロードしたら

googleimagesdownload -ri -cd "chromedriver.exe" -l 300  -k "sunburst jazzmaster"

で使えました。-lのあとが枚数。私なんかしばらくうまくいかなくて、結局カレントディレクトリに入れてないあたりで失敗してたみたい?
目的のものじゃないやつも含まれてしまうので、めんどいけど手作業で消しました・・・

サンバーストはめっちゃ使用者いるけど黒は全然いなくて泣いてる
あとで自分のギターを撮りまくるかもしれない

画像のサイズを合わせる

今回のコードは画像サイズを256x256に合わせると良さそう
参考にしたのは以下2サイト

参考にというかパクろうとしたけど、アホでそれすらできずに5億時間かかってしまった…

構造としては、2つ目のサイトをベースに、リサイズの具体的な操作は1つ目のコードを引用した という感じ。
今回は様々なサイズ、比率の画像を256x256の形にしたかったので、長い方に合わせてリサイズしてから余白を追加する方法をとりました。

GitHubにあげている私のコード最終形↓
resize.py

引っかかったとこ

  • ① ディレクトリの相対パスをなんて書けばいいかわからない
  • Downloads
    ├── black
          └── img.jpgがたくさん
    ├── sun
    ├── sun_resize
    └── resize.py
    
    • 元のコードにbundle_resize('dir')とあり、dirを書き換える必要があった
    • Downloads/blackって書いたりblackだけ書いたり迷走した
    • 結局bundle_resize('black/')って書いたらできた
  • ② リサイズしたものを別のフォルダに入れたい

    • os.makedirs(ファイル名, exist_ok=True)でファイル作成
    • out_pathを少し書き換えるだけでよかった

階層構造を作る

cycleGANの方のデータセットを確認すると、

maps
├── trainA
├── trainB
├── testA
└── testB

みたいな感じになっているので、そうする。もうわからないので手作業で適当にやります
フォルダの名前は、色々変えるの難しそうだから同じ名前で作って差し替える

実行編

次回につづく・・・

おまけ

黒いジャズマスターを使ってやっているバンド

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポアソン分布を丁寧に理解してPythonで描画する

はじめに

統計を勉強していると必ず出てくるポアソン分布ですが、例の確率分布の式が中々頭に入ってこなかったので確率分布の導出から丁寧に追って理解しようと考えました。イメージを掴むためにPythonで描画も行います。

参考

ポアソン分布の理解とその分布の描画を行うに当たって下記を参考にさせていただきました。

ポアソン分布の理解

ポアソン分布とは何か

P(X=k) = \frac{\lambda^k \mathrm{e}^{-\lambda}}{k!}

ポアソン分布とは単位時間当たりに平均$\lambda$回起こる事象が丁度$k$回起こる確率を表す確率分布です。ポアソン分布は上記の確率分布に従うとされていますが、式内でネイピア数の累乗が出て来たり、階乗が出て来たりで良く分かりません。何ぜこのような式になるのかを含めて以下で追って行こうと思います。

また、確率変数$X$がパラメータ$\lambda$のポアソン分布に従う時、$X〜Po(\lambda)$と表記されてります。

ポアソン分布に従う事象の例としては下記のようなものがあるとされています。

  • 1時間に特定の交差点を通過する車両の台数
  • 1時間にwebサイトにアクセスされるアクセス数
  • 1日に受け取る電子メールの件数
  • ある一定の時間内の店への来客数

また歴史的には「プロイセン陸軍で馬に蹴られて死亡した兵士数」が最初のポアソン分布の当てはめ例とされているらしく、$1$年間を単位時間として$\lambda = 0.61$のポアソン分布に従うということが示されています。

具体的に1つ確率を計算してみます。
ex)1時間に平均5回アクセスされるサイトが10回アクセスさせる確率($X〜Po(5)$:ポアソン分布に従う)

P(X=10) = \frac{5^{10} \mathrm{e}^{-5}}{10!} \fallingdotseq 0.018

とこのような感じで確率を導き出すことができます。この例の場合だと確率は$1.8\%$と非常に小さいことがわかります。

ポアソン分布の導出(ポアソン極限定理)

ポアソン極限定理の概要

\lim_{\lambda = np, n\to \infty} {}_n \mathrm{C} _kp^{k}(1-p)^{n-k} = \frac{\lambda^k \mathrm{e}^{-\lambda}}{k!}

ポアソン分布はパラメータが$n$と$p=\lambda/n$である二項分布において、$\lambda$の値は一定としながら$n$を無限大に近づけることで、近似的に導出することが可能です。つまりポアソン分布は二項分布の極限であるということです。これをポアソンの極限定理と言います。

$\lambda$の値は一定としながら$n$を無限大に近づけるという操作を行うと、それに従って$p$の値は非常に小さくなります。非常に小さな発生確率にものに適用できる分布ということがわかるかと思います。

ポアソン極限定理の式展開

ポアソン極限定理がどのような式展開をしているか追っていきます。

{\begin{eqnarray}

\lim_{n\to \infty} {}_n \mathrm{C} _kp^{k}(1-p)^{n-k} 
&=& \lim_{n\to \infty}\frac{n!}{(n-k)!k!}p^{k}(1-p)^{n-k} \\
&=&\lim_{n\to \infty}\frac{n(n-1)\cdots(n-k+1)}{k!}(\frac{\lambda}{n})^{k}(1-\frac{\lambda}{n})^{n-k} \\
&=&\lim_{n\to \infty}\frac{n}{n}\frac{n-1}{n}\cdots\frac{n-k+1}{n}(\frac{\lambda^{k}}{k!})(1-\frac{\lambda}{n})^{n}(1-\frac{\lambda}{n})^{-k} \\
&=&\frac{\lambda^{k}}{k!}\lim_{n\to \infty}(1-\frac{\lambda}{n})^{n} \\
&=&\frac{\lambda^{k}\mathrm{e}^{-\lambda}}{k!}

\end{eqnarray}
}

このような式展開でポアソン分布の確率分布を導き出しているのですが、わかりにくい式展開があるので一部下記で補足をします。
まず3行目から4行目の展開のところです。
$\frac{n}{n}\frac{n-1}{n}\cdots\frac{n-k+1}{n}$は$n$を無限大に近づけることで、全て値は$1$として処理できます。
また、$(1-\frac{\lambda}{n})^{-k}$も$n$を無限大に近づけることで$()$の中身が限りなく$1$に近づき、こちらも値は$1$として処理できます。
4行目から5行目の展開はネイピア数の下記定義式を利用しています。

\mathrm{e} = \lim_{x\to \infty}(1+\frac{1}{x})^{\frac{1}{x}}

上記にあてはめるように展開していくと下記のようになります。

{\begin{eqnarray}

\lim_{n\to \infty}(1-\frac{\lambda}{n})^{n} &=& \lim_{n\to \infty}(1-\frac{\lambda}{n})^{-\frac{1}{\frac{\lambda}{n}} (-\lambda)} \\
&=& \mathrm{e}^{-\lambda} 

\end{eqnarray}}

これでポアソン分布の導出をすることができました。

ポアソン分布の性質

E(X) = \lambda  \\
V(X) = \lambda

ポアソン分布の期待値・分散は共に$\lambda$であるという性質があります。
下記のそれぞれの導出過程を記載します。

ポアソン分布の期待値の導出過程

\begin{eqnarray*}E(X)&=&\sum_{k=0}^{n}kP(X=k)\\ &=&\sum_{k=0}^{n}k\frac{λ^{k}\mathrm{e}^{-\lambda}}{k!}\\ &=&\sum_{k=0}^{n}\frac{λ^{k}\mathrm{e}^{-\lambda}}{(k-1)!}\\ &=&λ\sum_{k=0}^{n}\frac{λ^{k-1}\mathrm{e}^{-λ}}{(k-1)!}\\ &=&λ\end{eqnarray*}

期待値と確率分布の性質からまず1行目の式をスタートさせます。
4行目から5行目の式展開は$\sum_{k=0}^{n}\frac{λ^{k-1}\mathrm{e}^{-λ}}{(k-1)!}$が結局はポアソン分布で取りうる全ての確率を足し合わせることになっているため値は$1$と置くことができ、このような式展開が可能になっています。

ポアソン分布の分散の導出過程

\begin{eqnarray*}V(X)&=&E(X^2)-{(E(X))}^2
\end{eqnarray*}

上記分散の性質から、$E(X^{2})$を導き出すことができれば分散も導き出すことができることがわかります。下記が$E(X^{2})$の導出過程になります。

\begin{eqnarray*}E(X^2)&=&\sum_{k=0}^{n}k^{2}P(X=k)\\ &=&\sum_{k=0}^{n}k^{2}\frac{λ^{k}\mathrm{e}^{-λ}}{k!}\\ &=&\sum_{k=0}^{n}(k(k-1)+k)\frac{λ^{k}\mathrm{e}^{-λ}}{k!}\\ 
&=&\sum_{k=0}^{n}k(k-1)\frac{λ^{k}\mathrm{e}^{-λ}}{k!}+\sum_{k=0}^{n}k\frac{λ^{k}\mathrm{e}^{-λ}}{k!}\\
&=&\sum_{k=0}^{n}\frac{λ^{k}\mathrm{e}^{-λ}}{(k-2)!}+λ\\ &=&λ^{2}\sum_{k=0}^{n}\frac{λ^{k-2}\mathrm{e}^{-λ}}{(k-2)!}+λ\\ &=&λ^{2}+λ

\end{eqnarray*}

上記を用いて分散を導出します。

\begin{eqnarray*}V(X)&=&E(X^2)-{(E(X))}^2 \\
&=& λ^{2} + λ - λ^{2} \\
&=& λ
\end{eqnarray*}

こちらでポアソン分布の期待値と分散を導出することができました。

ポアソン分布の描画

ポアソン分布をPythonで描画する

今回は単位時間あたり平均10回発生する事象、平均20回発生する事象、平均30回発生する事象のポアソン分布を重ねて描画してみます。

def poisson(lambda_, k):
    k = int(k)
    result = (lambda_**k) * (math.exp(-lambda_))  / math.factorial(k)
    return result

x =  np.arange(1, 50, 1)
y1= [poisson(10,i) for i in x]
y2= [poisson(20,i) for i in x]
y3= [poisson(30,i) for i in x]

plt.bar(x, y1, align="center", width=0.4, color="red"
                ,alpha=0.5, label="Poisson λ= %d" % 10)

plt.bar(x, y2, align="center", width=0.4, color="green"
                ,alpha=0.5, label="Poisson λ= %d" % 20)

plt.bar(x, y3, align="center", width=0.4, color="blue"
                ,alpha=0.5, label="Poisson λ= %d" % 30)

plt.legend()
plt.show()

ダウンロード.png

このような感じでグラフが描画できます。$λ$の値が大きくなればなるほど確率分布の裾が広がっていく様子が面白いですね。
ちなみにscipyというライブラリを用いると簡単にポアソン分布を描くことができます。

from scipy.stats import poisson

x =  np.arange(1, 50, 1)
y1= [poisson.pmf(i, 10) for i in x]
y2= [poisson.pmf(i, 20) for i in x]
y3= [poisson.pmf(i, 30) for i in x]

plt.bar(x, y1, align="center", width=0.4, color="red"
                ,alpha=0.5, label="Poisson λ= %d" % 10)

plt.bar(x, y2, align="center", width=0.4, color="green"
                ,alpha=0.5, label="Poisson λ= %d" % 20)

plt.bar(x, y3, align="center", width=0.4, color="blue"
                ,alpha=0.5, label="Poisson λ= %d" % 30)

plt.legend()
plt.show()

ダウンロード (1).png

Next

数式を丁寧に追って自分でPythonで描画すると、中々イメージの掴みにくかったポアソン分布を理解することができました。今後も統計関連で学んだことを色々まとめていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder Beginner Contest 042 過去問復習

所要時間

スクリーンショット 2020-01-20 17.01.16.png

感想

ライブラリゲー、D問題初見では厳しそう。

A問題

5,5,7が入ってればYES

answerA.py
x=list(map(int,input().split()))
x.sort()
print("YES" if x==[5,5,7] else "NO")

B問題

ソートすれば辞書順になるので、その後に順番に繋げてしまえば良い。

answerB.py
n,l=map(int,input().split())
s=[input() for i in range(n)]
s.sort()
print("".join(s))

C問題

適当にやったらなぜか通った、テストケースゆるゆるでは?
支払う金額をだんだん増やしていって、その金額を文字列にした時に嫌いな文字が一つも入ってなければその金額を出力してwhile文をbreakします。

answerC.py
n,k=map(int,input().split())
s=set(input().split())
while True:
    l=list(str(n))
    for i in l:
        if i in s:
            break
    else:
        break
    n+=1
print(n)

D問題

このパターンの問題見るの三回目くらいな気がします。
グリッドを辿っていく問題でh,wが極めて大きいので、コンビネーションをmodの逆元を利用して高速化する問題の一種であるとわかります。この問題はPythonのライブラリを用意してないので、いつもC++で解いてます(C++のライブラリもよく理解せずにけんちょんさんのライブラリを利用しているだけですが)。

一つ目のコードは自分で解いた時のコード、二つ目は解答の方法を実装したコードになります。また、コードの直前に貼ってある写真に書いてある方針でそれぞれ実装してあります(ありうる場合を排反かつ十分に取ってくる必要があります。)。

IMG_0098.PNG

answerD.cc
#include<iostream>
#include<vector>
#include<algorithm>
#include<utility>
#include<cmath>
using namespace std;
typedef long long ll;

const ll MAX = 200000;
const ll MOD = 1000000007;

ll fac[MAX], finv[MAX], inv[MAX];

// テーブルを作る前処理
void COMinit() {
    fac[0] = fac[1] = 1;
    finv[0] = finv[1] = 1;
    inv[1] = 1;
    for (ll i = 2; i < MAX; i++){
        fac[i] = fac[i - 1] * i % MOD;
        inv[i] = MOD - inv[MOD%i] * (MOD / i) % MOD;
        finv[i] = finv[i - 1] * inv[i] % MOD;
    }
}

// 二項係数計算
ll COM(ll n,ll k){
    if (n < k) return 0;
    if (n < 0 || k < 0) return 0;
    return fac[n] * (finv[k] * finv[n - k] % MOD) % MOD;
}

signed main(){
  // 前処理
  COMinit();
  ll h,w,a,b;cin >> h >> w >> a >> b;
  ll ans=COM(h+w-2,h-1);
  ll m=min(a,b);
  for(int i=0;i<m;i++){
    ll ans_sub=COM(h-a+b-1,b-i-1)*COM(w-b+a-1,a-i-1)%MOD;
    ans-=ans_sub;
    if(ans<0){
      ans+=MOD;
    }
  }
  cout << ans << endl;
}

IMG_0099.JPG

answerD2.cc
#include<iostream>
#include<vector>
#include<algorithm>
#include<utility>
#include<cmath>
using namespace std;
typedef long long ll;

const ll MAX = 200000;
const ll MOD = 1000000007;

ll fac[MAX], finv[MAX], inv[MAX];

// テーブルを作る前処理
void COMinit() {
    fac[0] = fac[1] = 1;
    finv[0] = finv[1] = 1;
    inv[1] = 1;
    for (ll i = 2; i < MAX; i++){
        fac[i] = fac[i - 1] * i % MOD;
        inv[i] = MOD - inv[MOD%i] * (MOD / i) % MOD;
        finv[i] = finv[i - 1] * inv[i] % MOD;
    }
}

// 二項係数計算
ll COM(ll n,ll k){
    if (n < k) return 0;
    if (n < 0 || k < 0) return 0;
    return fac[n] * (finv[k] * finv[n - k] % MOD) % MOD;
}

signed main(){
  // 前処理
  COMinit();
  ll h,w,a,b;cin >> h >> w >> a >> b;
  ll ans=0;//cout << ans << endl;
  ll m=min(a,b);
  for(int i=1;i<=w-b;i++){
    ll ans_sub=COM(h-a+b+i-2,b+i-1)*COM(w-b+a-i-1,a-1)%MOD;
    ans+=ans_sub;
    ans%=MOD;
  }
  cout << ans << endl;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

QRコードをCNNで解読する

はじめに

畳み込みを使ったCNNは白黒の二次元画像の特徴量を抽出するのが比較的得意です。白黒画像といえばQRコードが挙げられるのでこのQRコードの値をCNNで読み込めるかどうかを試してみます。
本当は白黒のどのビットがどの値かというのはルールベースで読み取れますし、畳み込みを使わないNNでも充分であるわけですが、ここではあえてCNNを使ってみます。

QRコードのあれこれ

QRコードのversionはQRコードのサイズと自ら含むことのできる文字数に依存します。
例えばversion=1では21×21のサイズで下記のように"www.wikipedia.org" と17文字の文字列を含むことができます。E1~E7はエラー補正なので読み取りは必須ではありません。つまり、この場合は各文字は8bitですから計136bitの値を確認すればルールベースでもこのサイズで何が書いてあるかは読めるわけです。
とりあえずこの最小QRコード21×21に書かれた数字を読むことを目的とします。
1280px-QR_Character_Placement.svg.png

コード

Kerasで下記のように書きました。
6桁の数字を文字列になおして最小サイズのQRコードを40000個作成して学習とテストデータとしました。
また、本来のCNNならpoolingを使って画像サイズを半分にしていくべきかも知れませんが、今回の場合は入力サイズが21×21と小さいのでconv2dのみで畳み込み部分を構成しています。

qr.py
import qrcode
import numpy as np
import random
from keras.utils import np_utils
from keras.layers import Input, Conv2D, MaxPooling2D, AveragePooling2D, BatchNormalization, Concatenate
from keras.models import Model

batch_size = 128
num_classes = 10
epochs = 30

X, Y = [], []
sample_list = random.sample(range(10**6), k=40000)
for i in sample_list:
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_H,
        box_size=1, border=0 )

    qr.add_data('%06d' % (i))
    qr.make()
    img = qr.make_image()
    X.append(np.asarray(img))
    Y.append([int(d) for d in format(i, '06d')])

X = np.reshape(np.asarray(X),(-1,21,21,1))/1.0
Y = np.reshape(np_utils.to_categorical(np.asarray(Y)), (-1,1,6,10))
print(X.shape)
print(Y.shape)


inputs = Input((21,21,1))
x = Conv2D(256, (3,3), padding='same', activation='relu')(inputs)
x = BatchNormalization()(x)
x = Conv2D(256, (3,3), padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(256, (3,3), padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(256, (3,3), padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(256, (3,3), padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(512, (3,3), padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(512, (3,3), padding='same', activation='relu')(x)
x = MaxPooling2D(pool_size=(21, 21))(x)
y = [Conv2D(10, (1,1), activation='softmax')(x) for i in range(6)]
y = Concatenate(axis=-2)(y)

model = Model(inputs=inputs, outputs=y)
model.summary()
model.compile(loss='categorical_crossentropy',
              optimizer='Adam',
              metrics=['accuracy'])
history = model.fit(X[:30000], Y[:30000], batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(X[30000:], Y[30000:]))

model.save('qr_model.h5', include_optimizer=False)

ここで

qr.py
y = [Conv2D(10, (1,1), activation='softmax')(x) for i in range(6)]
y = Concatenate(axis=-2)(y)

の部分は下記のように書いても良いです。

qr.py
y1 = Conv2D(10, (1,1), activation='softmax')(x)
y2 = Conv2D(10, (1,1), activation='softmax')(x)
y3 = Conv2D(10, (1,1), activation='softmax')(x)
y4 = Conv2D(10, (1,1), activation='softmax')(x)
y5 = Conv2D(10, (1,1), activation='softmax')(x)
y6 = Conv2D(10, (1,1), activation='softmax')(x)
y = Concatenate(axis=-2)([y1,y2,y3,y4,y5,y6])

この時のコード実行結果は以下のようになりました。

(40000, 21, 21, 1)
(40000, 1, 6, 10)
...
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 21, 21, 1)    0
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, 21, 21, 256)  2560        input_1[0][0]
__________________________________________________________________________________________________
batch_normalization_1 (BatchNor (None, 21, 21, 256)  1024        conv2d_1[0][0]
__________________________________________________________________________________________________
conv2d_2 (Conv2D)               (None, 21, 21, 256)  590080      batch_normalization_1[0][0]
__________________________________________________________________________________________________
batch_normalization_2 (BatchNor (None, 21, 21, 256)  1024        conv2d_2[0][0]
__________________________________________________________________________________________________
conv2d_3 (Conv2D)               (None, 21, 21, 256)  590080      batch_normalization_2[0][0]
__________________________________________________________________________________________________
batch_normalization_3 (BatchNor (None, 21, 21, 256)  1024        conv2d_3[0][0]
__________________________________________________________________________________________________
conv2d_4 (Conv2D)               (None, 21, 21, 256)  590080      batch_normalization_3[0][0]
__________________________________________________________________________________________________
batch_normalization_4 (BatchNor (None, 21, 21, 256)  1024        conv2d_4[0][0]
__________________________________________________________________________________________________
conv2d_5 (Conv2D)               (None, 21, 21, 256)  590080      batch_normalization_4[0][0]
__________________________________________________________________________________________________
batch_normalization_5 (BatchNor (None, 21, 21, 256)  1024        conv2d_5[0][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D)               (None, 21, 21, 512)  1180160     batch_normalization_5[0][0]
__________________________________________________________________________________________________
batch_normalization_6 (BatchNor (None, 21, 21, 512)  2048        conv2d_6[0][0]
__________________________________________________________________________________________________
conv2d_7 (Conv2D)               (None, 21, 21, 512)  2359808     batch_normalization_6[0][0]
__________________________________________________________________________________________________
max_pooling2d_1 (MaxPooling2D)  (None, 1, 1, 512)    0           conv2d_7[0][0]
__________________________________________________________________________________________________
conv2d_8 (Conv2D)               (None, 1, 1, 10)     5130        max_pooling2d_1[0][0]
__________________________________________________________________________________________________
conv2d_9 (Conv2D)               (None, 1, 1, 10)     5130        max_pooling2d_1[0][0]
__________________________________________________________________________________________________
conv2d_10 (Conv2D)              (None, 1, 1, 10)     5130        max_pooling2d_1[0][0]
__________________________________________________________________________________________________
conv2d_11 (Conv2D)              (None, 1, 1, 10)     5130        max_pooling2d_1[0][0]
__________________________________________________________________________________________________
conv2d_12 (Conv2D)              (None, 1, 1, 10)     5130        max_pooling2d_1[0][0]
__________________________________________________________________________________________________
conv2d_13 (Conv2D)              (None, 1, 1, 10)     5130        max_pooling2d_1[0][0]
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 1, 6, 10)     0           conv2d_8[0][0]
                                                                 conv2d_9[0][0]
                                                                 conv2d_10[0][0]
                                                                 conv2d_11[0][0]
                                                                 conv2d_12[0][0]
                                                                 conv2d_13[0][0]
==================================================================================================
Total params: 5,940,796
Trainable params: 5,937,212
Non-trainable params: 3,584
__________________________________________________________________________________________________
Train on 30000 samples, validate on 10000 samples
Epoch 1/30
30000/30000 [==============================] - 66s 2ms/step - loss: 2.7801 - acc: 0.1714 - val_loss: 2.3467 - val_acc: 0.2484
Epoch 2/30
30000/30000 [==============================] - 62s 2ms/step - loss: 1.8426 - acc: 0.3493 - val_loss: 1.6885 - val_acc: 0.3941
Epoch 3/30
30000/30000 [==============================] - 64s 2ms/step - loss: 1.4841 - acc: 0.4555 - val_loss: 1.4549 - val_acc: 0.4547
...
Epoch 28/30
30000/30000 [==============================] - 64s 2ms/step - loss: 0.0401 - acc: 0.9868 - val_loss: 0.3695 - val_acc: 0.9110
Epoch 29/30
30000/30000 [==============================] - 64s 2ms/step - loss: 0.0435 - acc: 0.9853 - val_loss: 0.3403 - val_acc: 0.9184
Epoch 30/30
30000/30000 [==============================] - 63s 2ms/step - loss: 0.0339 - acc: 0.9889 - val_loss: 0.3164 - val_acc: 0.9231

最終精度はacc: 0.9889,val_acc: 0.9231となりました。
epochやモデルは適当なので調整すれば精度はもう少し上がるでしょう。

テスト

検証コードとして下記のように書きます。
結果はQRコードのversion=1という21×21のかなり小さな限定された条件ですが、6桁の数字をある程度予測することが出来ました。

qr2.py
import qrcode
import numpy as np
import random
from keras.models import load_model

X, Y = [], []
sample_list = random.sample(range(10**6), k=10)
for i in sample_list:
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_H,
        box_size=1, border=0 )

    qr.add_data('%06d' % (i))
    qr.make()
    img = qr.make_image()
    X.append(np.asarray(img))
    Y.append(i)

X = np.reshape(np.asarray(X),(-1,21,21,1))/1.0
model = load_model('qr_model.h5')

Y_pred = model.predict(X)

Y_pred_list = []
for i in range(10):
    Y_pred_value = 0
    for j in range(6):
        Y_pred_value += np.argmax(Y_pred[i,0,j])
        Y_pred_value *= 10
    Y_pred_list.append(Y_pred_value//10)
print(Y)
print(Y_pred_list)
[89127, 306184, 427806, 501649, 727976, 232504, 427216, 893062, 127368, 100207]
[89127, 306184, 427806, 501649, 727976, 234506, 431222, 893062, 127378, 100207]

場所の特徴量

一般にMaxPooling2Dでは画像内に特徴量が存在するかは分かりますが、その場所までは後続のレイヤーに伝わりません。CNNといえば画像内のどこかに犬や猫が含まれるといった分類問題に多く使われますが、一方で場所を特定する特徴量抽出が(flattenを使わず)可能なのかと思いましたが、出来ているようです。
これは短距離畳み込みではデータの値、長距離の畳み込みでは左上、右上、左下にある四角のFixed patternとの距離にてデータの場所の特徴量を抽出しているのかもしれません。
データの値(2,4か4,2の8bit)を読み取るだけならConv2dが2層(5,5相当)でも十分な筈ですが、実際にはConv2dが2層のモデルでは精度はそれほど出ませんでした(val_acc:0.5程度)。このためCNNでQRコードを読むなら長距離の畳み込みによる場所の特徴量抽出も必要かと思われます。
(もしくは手っ取り早くモデルにflattenを使った方がいいよって話かもしれませんが…)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新しいData Augmentation? 【GridMix】

はじめに

「GridMix?聞いたことないな」とお思いでしょう。
そりゃそうです。GridMaskとCutMixに感化されて勝手に作った下図のようなAugmentationです。
効果があるかどうかちょっと試してみたので、メモ程度に残しておきます。
mixed_img.png

概要

目的:なにをしたか

  • 上述したGridMixの効果をcifer10で確認した。
  • 同系列のAugmentationであるCutMixと比較した。

結論:どうだったか

 精度:提案手法(GridMix)の方が微妙に優れる
 収束:既存手法(CutMix)が優れる
 チューニング:提案手法(GridMix)の方が手間かも

お遊び程度なので半信半疑ですが、最低限のポテンシャルを確認できました。

背景

GridMaskの紹介

最近発表されたData AugmentationにGridMaskというものがあります。下図のように格子状に画像をマスクするような手法で、従来手法であるCutout等よりも優れているとのことです。
※本記事の主題はこの手法の紹介ではなく、これをMixに展開したものの提案となります
1.png
引用元論文:https://arxiv.org/abs/2001.04086

CutMixの紹介

これについてはQiita等でも既に様々な方が紹介していますので詳細は割愛しますが、ランダムに画像の一部を切り取り、それを他の画像に張り付けて、面積比率でラベルを与える手法になります。
3.png
引用元論文:https://arxiv.org/abs/1905.04899

CutMix ⇒ GridMix へのモチベーション

前からCutMixに少し疑問がありました。
中央近辺の方が情報量が多そうなのに、単純に面積比でラベルを決めて良いものかと。

たとえば、下図は面積比で半分が猫、半分が犬になっているが、これに対してラベルを半々にするのは中々に酷だと思う。私にはワンワンにしか見えない。
2.png

⇒よし、じゃあメッシュ状にミックスしてみよう。
と思ったわけです。

アプローチ

共通のモデルで、cifer10のデータセットに対して以下の3ケースで学習をおこなって精度を比較する。
1. Augmentation無し
2. CutMix Augmentation (既存手法)
3. GridMix (提案手法)

使用するモデル

Conv8層の浅いCNN(not pretrained)
input shape: 32x32x3

GridMix Augmentation

提案手法は、適当なサイズのグリッドで画像をミックスするという、CutMixとGridMaskの合いの子のようなもの。マスクは基本的には市松模様とするが、確率的に網目模様やMix無しが生じるようにした。

下図は左から順番に、市松模様、網目模様、ミックス無し
gridmiximg.png

市松模様のみにしてしまうと、ミックス比率がほぼ0.5で一定となり収束が悪かったのでたまにイージーなケースがはいるようにした。網目模様も入れることで、既存手法であるCutMixに近いものも再現できる。

def grid_mixer(img_1, img_2, interval_h, interval_w, thresh=0.3):
    #make checkerboad
    h, w, _ = img_1.shape
    h_start = np.random.randint(0,2*interval_h)
    w_start = np.random.randint(0,2*interval_w)
    h_grid = ((np.arange(h_start, h_start+h)//interval_h)%2).reshape(-1,1)
    w_grid = ((np.arange(w_start, w_start+w)//interval_w)%2).reshape(1,-1)
    checkerboard = np.abs(h_grid-w_grid)

    #reverse vertical and/or horizontal
    if np.random.rand()<thresh:
        checkerboard += h_grid*w_grid
    if np.random.rand()<thresh:
        checkerboard += (1-h_grid)*(1-w_grid)

    #mix images
    mixed_img = img_1*checkerboard[:, :, np.newaxis]+img_2*(1-checkerboard[:, :, np.newaxis])
    mix_rate = np.sum(checkerboard)/(h*w)
    return mixed_img, mix_rate

h,w,_=img_1.shape
interval_h = h//np.random.uniform(2, 4)
interval_w = w//np.random.uniform(2, 4)                        
img, mix_rate = grid_mixer(img_1, img_m_2, interval_h, interval_w, 0.3)

以下のように、少しパラメータが多いのがネック。

グリッドの間隔幅:
グリッド幅が細かすぎると、浅い層でしか拾えなくなりそうなので(cifer-10のデフォルトサイズは32x32なので)、画像が縦横それぞれ2~4分割になるようにセットした。この辺りはモデルにも依存しそうに感じる。
グリッドのアスペクト比も一応ランダムになるようにしているが、効果は未確認。

市松模様~網目模様の切り替え閾値:
30%確率で横方向マスクを除外、30%確率で縦方向マスクを除外するようにしている。
これによって、49%が市松模様、42%が網目模様、残り9%がミックス無しとなる。
結局のところはCutMixなどで使用されるβ分布などの調整と同じようなことをしている。

学習条件

  • Initial Learning Rate: 0.005
  • Epochs(lr Schedule): 調整パラメータ
  • Optimizer: Adam (beta_1=0.9, beta_2=0.999, decay=0.)
  • Batch Size: 128

結果評価

学習率やスケジュールのパラメータチューニング後、3回実行した平均値を下表に示す。

Case Epochs Val_Accuracy Val_Loss
No Augmentation 25 0.805 0.710
CutMix (beta=alpha=0.7) 32 0.841 0.505
GridMix 45 0.852 0.463

※epoch数はベストパフォーマンスが出るところで調整している

GridMixは収束が遅い…。最初の数エポックは切っておいた方がいいかもしれない。
でも精度はちょっと良くなっている。たかだか1ケースではあるが、僅かな可能性を感じる。

まとめ

結論として、Grid状にCutMixすることで、通常のCutMixよりもよくなる可能性はあります。検証不足ですので、あくまで可能性程度です。
もうすこし色々試さないとなんともいえないところです。もしどなたか気分の乗った人は試してみていただけると泣いて喜びます。もし全然効かなかったら泣いて謝ります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonとBigQueryを使って社内向けピアボーナスツール(集計のみ)を作った

はじめに

社員間で感謝や労いを送り合うツールといえばuniposなんかが有名ですが、
会社規模的にサービス導入は負担が大きいのでとりあえずそれっぽいものを作ってみることにしました。

やりたいこと

slackで専用チャンネルを作り、そこでメンション+メッセージを飛ばすとカウントされる。
月ごとに集計されて、可視化&MHP(モスト・褒められた・パーソン)を出したい。
送られたメッセージもユーザーごとにまとめておきたい

使ったもの

  • slack
    • 全社員が使用しているため採用
  • python slackbot
    • もともと社内で使っていたので採用
  • BigQuery
    • 集計と可視化のために利用
  • GAS
  • google データポータル

作成手順

slackの設定

まずslack apiで新しいBOTを作成します。

  1. Create New App をクリック
  2. App Name を入力して、 Workspace を選択してCreate App
  3. 今回はBOTとして使いたいのでBotsを選択
  4. Always Show My Bot as Online を ON
  5. OAuth & Permissions で Tokenを発行、slackbotから書き込むときに使うのでローカルにコピペしときます
  6. slackで褒め専用チャンネルを作成して、そこに作ったappを追加

これでslack側の準備はほぼ終わりです。お好みでappにアイコン設定したりしてください。

Google Spread SheetとGASの設定

後々非エンジニアが運用するかもしれないことを考え、今回はGET叩いたらSpread Sheetに書き込まれるようにしました。

感謝・労いをした人、された人、した人からのメッセージ、した日付の4カラムを保存します。

こちらのソースを流用させていただきました。

会社のsuiteを使ったため、
Execute the app as:をmeに、
Who has access to the app: を anyone にしないと権限の関係で書き込めませんでした。

できたURLを叩いて各カラムに書き込まれているのを確認して次に進みます。

Bigqueryとデータポータルの設定

先ほど作ったspreadsheetをデータソースとして読みこむプロジェクトを作成。
bigqueryの機能を使って可視化することも考えましたが、
操作が直感的なのでデータポータルを使用。

slackbotの設定

今回一番コーディングらしいコーディングをしたところ。

import requests
import datetime
import re
from slackbot.bot import listen_to
from slackbot.bot import default_reply

pythonのデフォルトライブラリからrequests, datetime, reを、
slackbotからlisten_toとdefault_replyを使用。

@listen_to('<@')
def mention_func1(message):
    api = "https://script.google.com/macros/s/[api]/exec?p1={p1}&p2={p2}&p3={p3}&p4={p4}"

if message.body['channel'] == '[channel ID]' :

        bodytext = re.split('\s+|\s*\n\s*', message.body['text'], maxsplit=1)

        userid = bodytext[0].replace('<@', '').replace('>', '')

        if "さんがチャンネルに参加しました" in userid:
            exit()

        p1 = namelist[userid]
        p2 = bodytext[1]
        p3 = namelist[message.body['user']]
        p4 = datetime.date.today()

        url = api.format(p1=p1, p2=p2, p3=p3, p4=p4)
        r = requests.get(url)

        message.reply('褒めを受け付けました!')

最初、特定のカスタム絵文字+@username で反応するようにしようとしたところ、
カスタム絵文字の使い方がわからない社員がいることがわかり、特定のチャンネルでメンションつけたら動くことにしました。

slackbotが受け取るメッセージでは、した人のIDがSlackのユーザーIDになっていたり、メンション部分が<@USERID> となっています。
運用上表示名で保存したかったので、表示名とユーザーIDのobjectを別に作成、ユーザーIDを表示名に変換しています。

最後にrequestsでURLを叩くよう記載して終わり。

アウトプット

スクリーンショット 2020-01-20 18.26.19.png

slackでメンションつけてメッセージを書き、無事保存されたらbotから返事が来ます。

スクリーンショット 2020-01-20 18.23.36.png

保存されたデータは1月ごとに集計されて、した/された回数やメッセージをデータポータル上で確認できます。
一旦回数とメッセージだけにしましたが、ユーザー毎に作ったり、別の要素で集計したりと言うのが簡単にできるので、今後の運用でデータポータルも変えていければいいなーと思った次第です。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GCP初心者がGCEでマイクラサーバを建ててみた ~サーバ構築編~

はじめに

GCPを初めて半年未満の初心者が勉強のためにマイクラサーバを建ててみたって話です。

目標

  • GCEでマイクラサーバを構築
  • Discordを使いインスタンスの制御
  • コマンドラインからGCPの操作に慣れる

構成

  • VMインスタンス1(マイクラサーバ)
  • VMインスタンス2(discordボット)
  • クラウドストレージ(バックアップ用)
  • サービスアカウント1(discordからの制御用)

構成図

マイクラサーバ構成図.png

手順

1. マイクラサーバ用のインスタンスの作成

なにはともあれマイクラサーバ用のインスタンスを作成しないとなにも始まりませんね。
基本的には以下の公式ドキュメントを参照し作成していきます。
GCPは公式ドキュメントはかなりわかりやすいものが用意されていて初心者には大助かりでした。

Compute Engine での Minecraft サーバーのセットアップ

インスタンスについては私はこのような設定で作成しました。

項目      内容      
インスタンス名 mineserver
リージョン us-central1
ゾーン us-central1-f
シリーズ N1
マシンタイプ n1-standard-2
ブートディスク CentOS 7
プリエンプティブ 有効
ネットワークインタフェース外部IP IPアドレスを作成

プリエンプティブル インスタンスの制限
プリエンプティブルインスタンスを有効にするメリットは、単純にインスタンスの費用が通常の3分の1で済みます。
今回のマイクラサーバについては、ログインしっぱなしなんて状況はほとんど無いと思いましたので、コストを大幅に下げれることを優先しプリエンプティブルインスタンスを採用しました。
目標にも記載しましたが、discordからゲームをする時だけインスタンスをスタートし、終わったらストップするのであまり障害はないものと思っています。

デメリットについては、公式ドキュメントに詳しく記載されているのでそちらを参照してください。

プリエンプティブル インスタンスの制限

永続ディスクの追加
追加ディスクについては公式ドキュメントに記載されている通りに作成します。

項目 内容
ディスク名 minecraft-disk
ディスクタイプ SSD永続タイプ
ソースタイプ 空のタイプ
サイズ 50GB

2. マイクラサーバのインストール・実行

こちらも基本的には上記に記載した公式ドキュメントを元に進めていきますが、
公式はOSがDebianを使用しており、私はCentOSを使用しているのでコマンドが一部ですが違ってきます。
違っている箇所を記載していますので、公式ドキュメントと合わせて読んでいただけると幸いです。

Java ランタイム環境(JRE)をセットアップする

$ sudo yum update

$ sudo yum install -y default-jre-headless

マイクラサーバのダウンロードとインストール
公式ではwgetを使いダウンロードしていますが、centOSにはデフォルトでwgetは入っていないのでインストールします。
ダウンロードリンクについてはminecraftのダウンロードページから最新のリンクをコピーして差し替えてください。

$ sudo yum install wget

$ wget https://launcher.mojang.com/v1/objects/f1a0073671057f01aa843443fef34330281333ce/server.jar

サーバの初回起動
下記のコマンドに「-Xms1G -Xmx3G」とあるが、こちらはメモリの割り当てをしているもので、
マシンスペックに合わせて調整が可能です。

$ java -Xms1G -Xmx3G -d64 -jar server.jar nogui

マイクラサーバを使用するにあったての合意条件
初回起動が終わると同ディレクトリ内にいくつかファイルが生成され、その中にeula.txtというファイルがあるので下記コマンドで開き編集します。
EULA の条件に同意する場合は、eula の値を false から true に変更し、保存して終了します。

$ vi eula.txt

ここに内容を記載

ここまでで最低限必要なセットアップは完了しました。
ですが、このままだとサーバからログアウトするとセッションが切れてマイクラサーバは停止してしまいます。
この問題を解決するためにscreenを使用します。
まずはscreenのインストールからです。

$ yum install screen

インストールしたscreenを使いマイクラサーバを実行します。
スクリーンコマンドの使い方は以下のページを参考にさせていただきました。

Linux screenコマンド使い方

$ screen -S mcs java -Xms1G -Xmx3G -d64 -jar server.jar nogui

クライアントがマイクラサーバにログインできるようにする

公式ドキュメントに書かれているように、マイクラサーバは25565 をデフォルト リスニング ポートとして使用しますのでそちらに合わせたファイアウォールルールを作成します。
ポートを変更したい場合は同ディレクトリにあるserver.propertiesを編集することで変更可能です。
また、server.propertiesでは他にもマイクラサーバの様々な設定を変更することが可能です。

サーバ設定ファイル(server.properties)

3. 定期バックアップの設定・実行

マイクラをプレイしていて突然クラッシュしデータが飛んでしまった。なんて悲しいことは私も何度か経験があります。
そんな時のために定期的にバックアップを取ることは必須ですよね。

基本は公式ドキュメントの「定期バックアップをスケジュールする」の項目通りに進めます。
バックアップ用のスクリプトを作成し、cronを使い定期的に実行します。

cronジョブをスケジュールするには、crontabに書き込む必要があります。

$ crontab -e

//以下crontabの内容
0 */4 * * * /home/minecraft/backup.sh

上記の設定をすると4時間ごとにbackup.shを実行されるようになります。
公式ドキュメントでは4時間でされていますが、私はクラッシュし復旧させる際に4時間も前の状態に戻るのは精神的にきついので1時間ごとにしました(笑)。
またcronについて詳細は以下のページを参考にさせていただきました。

cronの設定方法

バックアップを世代保存する
バックアップを取り続けるだけですと、ファイルが膨大になってしまいますので一定の世代分だけを常に残すように設定します。
CloudStorageではオブジェクトのライフサイクル管理という機能があります。
簡単に説明すると自動的に古いバックアップを削除してくれるものです。

設定については公式ドキュメントの「古いバックアップを自動的に削除する」の項目通りに進めます。
1点公式ドキュメントとは違う設定をした項目は、
[オブジェクト条件の選択] セクションで、[年齢] を選択します。年齢を 7 日間ではなく3日間にしました。
私の場合は1時間ごとにバックアップを取るので1日で24回、7日間も残すとなると168回分のバックアップが残ります。さすがにコストを考慮し3日間にしました。

まとめ

いかがでしたでしょうか、ここまででマイクラサーバの構築編は終了です。
次回は作成したマイクラサーバの操作をdiscordから実行する方法について書いていきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自動で背景を削除 remove.bgを再現してみた

少し前に話題になった「remove.bg」という背景を削除してくれるサービスを使ってみました。その精度の高さに感銘をうけ、興味を持ったため自分でも試してみました。コードはすべてこちらの記事に載せています。

remove.bgとは

  • 自動で画像から背景を削除してくれるサービス
  • 基本無料で使用可能、API取得は有料
  • 髪の毛の一本まで抽出してくれる
    man2.png man.png

Semantic Segmentation

このサービスを使ってみて、まず最初に思いついたのがセグメンテーションの利用です。なのでtorchvisionのDeepLabv3でサクッと試してみました。以下がその結果です。

catt.png
ある程度上手くいっていますが毛の一本一本を抽出することはできていないようです。
セグメンテーションで背景削除ができない原因としては
・ 毛の一本一本に注目してもlossはそんなに変わらない
・そもそもデータセットのターゲット画像が大雑把
・bilinear interpolation でupsamplingしている
という点があると考えられます。
 セグメンテーションと細かな背景削除では目的が違うため上手くいかないのは当然でした、、、

Image Matting

 セグメンテーションが上手くいかなかったため、他の策を調べていると「Image Matting」というタスクがあることが分かりました。Image Mattingは画像やビデオから前景を抽出するタスクです。

画像処理範囲

Image Mattingでは画像の一部のみを処理します。まず画像を"前景"、"背景","そのどちらか"に粗く分割します。(この3クラスに分割したものをtrimapをいいます) 次に"そのどちらか"についてのみ、透明度を示すアルファチャンネルを予測していきます。こうすることで細かい部分の予測漏れの損失を相対的に大きくすることができます。以下の例だと画像中のグレーの部分のみ推論します。

image.png

データセット

有名なデータセットとしてはAdobeが提供しているMatting Datasetがあります。セグメンテーション用のデータセットに比べてかなり細かくなっています。(AdobeのBrian Priceさんにメールでコンタクトをとることで受け取ることができます)

image.png

処理の流れ

  1. マスクの作成
    Image Mattingを実行するためには入力画像とは別に前述のtrimapを準備する必要があります。そのためにまず、セグメンテーションを使用してマスク画像を生成します。
  2. trimapの作成
    生成したマスクからtrimapを作成します。OpenCVの膨張収縮処理を施して作成します。
  3. IndexNet Mattingで推論
    今回はImage MattingのうちIndexNet Mattingというモデルを利用して推論します。理由は、論文の作者による公式実装や学習済みモデルが公開されており試しやすそうだったからです。
    image.png

結果

上記のパイプラインで推論した結果をいくつか載せます。左が元画像、中央が推論結果、右がremove.bgを利用した結果です。  

image.png

まとめ

DeepLearningを用いて背景削除(前景抽出)に取り組みました。
remove.bgの完全再現とはいきませんでしたが、なかなか上手く背景を切り取ることが出来ました!

(この記事で使用した画像は全てぱくたそ様より取得したものです。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cythonを使用して、複数ファイルにまたがったPythonコードから実行ファイルを生成(Mac:成功、Linux:失敗)

記事の内容

Pythonのプロジェクトで、ソースコードを納品しないケースに遭遇しました。
Pyinstallerを使用してバイナリ化する手順を別記事に書きましたが、Cythonも試しましたので手順を記録しておきます。
MacとLinuxで試して、Macは成功、Linuxは失敗という結果になりました。
いずれWindowsでも試す予定です。

サンプルコード

Pythonコードは以下の3ファイルを使用し、foo.pyをバイナリにしたものを起動することにします。

foo.py
from mymod1 import bar
from mymod2 import hoge
import sys

bar("Hello!")
hoge("Hi!")

print("-----------------")
print(sys.path)
print(f"__name__ = {__name__}")
print(f"__file__ = {__file__}")
mymod1.py
def bar(s):
    print(f"bar: {s}")
mymod2.py
import pandas as pd

def hoge(s):
    print(f"hoge: {s}")
    df = pd.DataFrame(index=[])
    print(df)

Mac編(成功の記録)

Python 3.7.4を使用しました。

【Mac編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.so)を生成する

以下のsetup.pyを準備します。

setup.py
from setuptools import setup, Extension
from Cython.Build import cythonize

setup(
    ext_modules=cythonize([
        Extension(
            "mymod1",
            sources=["mymod1.py"],
        ),
        Extension(
            "mymod2",
            sources=["mymod2.py"],
        ),
    ]),
)

端末で以下を実行して、モジュールをインストールします。pipenvを使用しています。

pipenv install setuptools cython pandas

現段階で存在するファイルは以下の通りです。

$ ls
Pipfile
foo.py
mymod1.py
mymod2.py
setup.py

生成されたPipfileは以下の通り。

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
setuptools = "*"
cython = "*"
pandas = "*"

[requires]
python_version = "3.7"

端末で以下を実行して、.soファイルをビルドします。

pipenv run python setup.py build_ext --inplace

現段階で存在するファイルは以下の通りです。マイモジュールの.cと.soが生成されています。

ls
Pipfile
build
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py

生成された共有ライブラリのファイル名がlibで始まっていません。
どうやらこれらの.soファイルは、実行ファイルのビルド時に人がリンクの指定をするものではなく、実行時にdlopen関数で動的にロードされて使われるようです。
最初はそのことに気づきませんでしたが、C言語でdlopen関数を使った遠い記憶があったので、ようやく気づいた(^^;

共有ライブラリ(.so)の動作確認

foo.pyの実行ファイルを生成する前に、先ほど生成された.soを動作確認してみます。
端末で以下を実行します。

mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

pipenv run python foo.py

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のように表示され、正常に動くことが確認できました。

bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/Users/username/PycharmProjects/foo_project', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python37.zip', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/lib-dynload', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/site-packages']
__name__ = __main__
__file__ = foo.py

【Mac編】ステップ2: foo.pyから実行ファイルを生成する

端末で以下を実行して、C言語のソースコードを生成します。
main関数にするために、--embedオプションを使用しています。

pipenv run cython foo.py --embed

現段階で存在するファイルは以下の通りです。foo.cが生成されています。

$ ls
Pipfile
build
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py

foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、探しておきます。

Python.hの場所:

$ find $HOME -type f -name 'Python.h' 2> /dev/null
/Users/username/.pyenv/versions/3.7.4/include/python3.7m/Python.h

ライブラリの場所とライブラリファイル名:

$ cd /Users/username/.pyenv/versions/3.7.4
$ ls
Python.framework
bin
bin.orig
include
lib
share
$ cd lib/
$ ls
lib
libpython3.7m.a
libpython3.7m.dylib
pkgconfig
python3.7
$ pwd
/Users/username/.pyenv/versions/3.7.4/lib

これらの情報を与えて、以下のようにコンパイルします。

gcc foo.c -o foo -I$HOME/.pyenv/versions/3.7.4/include/python3.7m -L$HOME/.pyenv/versions/3.7.4/lib -lpython3.7m

現段階で存在するファイルは以下の通りです。実行ファイルfooが生成されています。

$ ls
Pipfile
build
foo
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py

実行ファイルの動作確認

生成された実行ファイルを動かします。

mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

./foo

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のようにエラーメッセージが表示されました。

Traceback (most recent call last):
  File "foo.py", line 2, in init foo
    from mymod2 import hoge
  File "mymod2.py", line 1, in init mymod2
    import pandas as pd
ModuleNotFoundError: No module named 'pandas'

PYTHONPATHを設定して、やり直します。
pipenvを使用している場合の実行例:

mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

PYTHONPATH=`pipenv --venv`/lib/python3.7/site-packages ./foo

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のように、foo.pyの__name__を表示する行まで成功しました。
__file__を表示する行で落ちていますから、Pythonコードと同じ動きをするとは限らないようです。
ともかく、Macでは成功しました。

bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/Users/username/PycharmProjects/foo_project', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/site-packages', '/Users/username/.pyenv/versions/3.7.4/lib/python37.zip', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7/lib-dynload', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7/site-packages']
__name__ = __main__
Traceback (most recent call last):
  File "foo.py", line 11, in init foo
    print(f"__file__ = {__file__}")
NameError: name '__file__' is not defined

Linux編(失敗の記録)

Ubuntu 18.04、Python 3.7.5を使用しました。

【Linux編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.so)を生成する

ステップ1はMac編と全く同じ手順で成功します。
以下、動作確認した部分のみ貼り付けておきます。

$ pipenv run python foo.py
/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/site-packages/pandas/compat/__init__.py:85: UserWarning: Could not import the lzma module. Your installed Python is incomplete. Attempting to use lzma compression will result in a RuntimeError.
  warnings.warn(msg)
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/var/www/foo_project', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python37.zip', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/lib-dynload', '/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/lib/python3.7', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/site-packages']
__name__ = __main__
__file__ = foo.py

【Linux編】ステップ2: foo.pyから実行ファイルを生成する

Mac編と同様に、端末で以下を実行して、C言語のソースコードを生成します。

pipenv run cython foo.py --embed

現段階で存在するファイルは以下の通りです。foo.cが生成されています。

$ ls
Pipfile
Pipfile.lock
build
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-x86_64-linux-gnu.so
mymod1.py
mymod2.c
mymod2.cpython-37m-x86_64-linux-gnu.so
mymod2.py
setup.py

foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、探しておきます。

Python.hの場所:

$ find $HOME -type f -name 'Python.h' 2> /dev/null
/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/include/python3.7m/Python.h

ライブラリの場所とライブラリファイル名:

$ cd /home/laradock/.anyenv/envs/pyenv/versions/3.7.5
$ ls
bin
include
lib
share
$ cd lib/
$ ls
libpython3.7m.a
pkgconfig
python3.7
$ pwd
/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/lib

これらの情報を与えて、以下のようにコンパイルします。
リンクエラーが出たら、適宜必要なライブラリを加えます。

gcc foo.c -o foo -I$HOME/.anyenv/envs/pyenv/versions/3.7.5/include/python3.7m -L$HOME/.anyenv/envs/pyenv/versions/3.7.5/lib -lpython3.7m -lm -lpthread -ldl -lutil

現段階で存在するファイルは以下の通りです。実行ファイルfooが生成されています。

$ ls
Pipfile
Pipfile.lock
build
foo
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-x86_64-linux-gnu.so
mymod1.py
mymod2.c
mymod2.cpython-37m-x86_64-linux-gnu.so
mymod2.py
setup.py

実行ファイルの動作確認

生成された実行ファイルを動かします。

mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

PYTHONPATH=`pipenv --venv`/lib/python3.7/site-packages ./foo

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のようにエラーメッセージが表示されました。

$ ./foo
Traceback (most recent call last):
  File "foo.py", line 1, in init foo
    from mymod1 import bar
ImportError: /var/www/foo_project/mymod1.cpython-37m-x86_64-linux-gnu.so: undefined symbol: PyExc_SystemError

このエラーを回避する方法はまだ発見できておりません(誰か教えて)。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

書籍「15Stepで踏破 自然言語処理アプリケーション開発入門」をやってみる - 2章Step02メモ

内容

15stepで踏破 自然言語処理アプリケーション入門 を読み進めていくにあたっての自分用のメモです。
今回は2章Step02で、自分なりのポイントをメモります。

準備

  • 個人用MacPC:MacOS Mojave バージョン10.14.6
  • docker version:Client, Server共にバージョン19.03.2

章の概要

前章で簡単な対話エージェントを作成したが、類似した文章を同様に扱えなかったり、本来は重要でない単語(助詞など)や差異(アルファベットの大文字小文字)を特徴として扱ってしまっている。下記のテクニックについて学び、対話エージェントに適用させる。

  • 正規化
    • neologdnによる正規化
    • アルファベットの小文字化
    • Unicode正規化
  • 品詞によるストップワード除去
  • 見出し語化

02.1 前処理とは

テキスト分類の処理に入る前に、テキストを適切に整形することである。

# 同一の文章として扱えない
Pythonは好きですか
Pythonは好きですか

# 助詞や助動詞の共通性を特徴にしてしまう
# ラベル, 文章
0, あなたが好きです
1, ラーメン好き!
# ↓
# 「ラーメンが好きです」という文章をラベル=0と判断してしまうかも
# 意味的にはラベル=1と判断したい

02.2 正規化

表記のゆれを吸収し、ある一定の表記に統一する処理を文字列の正規化と呼ぶ。
表記のゆれがあっても、同じわかち書きの結果を得て、同じBoWを得ることが目標である。
およその正規化はneologdnで行い、neologdnで足りない正規化(小文字化やUnicode正規化)を個別に対応する。

neologdn

複数の正規化処理をまとめたneologdnというライブラリが用意されている。
これはMeCab辞書の一種のNEologdのデータを生成する時に使われている正規化処理である。
neologdnは関数1つに正規化処理がまとまっていて使い勝手が良く、C言語で実装されているので高速であることが利点である。

使用例
import neologdn

print(neologdn.normalize(<文章>))

小文字化と大文字化

neologdn.normalizeにはアルファベットの小文字・大文字変換は含まれていない。
よって、表記ゆれを吸収するにはPythonのstr型の組み込みメソッドである.lower()や.upper()を使って小文字または大文字に表記を統一する。

ただし、固有名詞などではアルファベットの小文字・大文字の区別が大事なこともあるため、必要に応じて対応する。

Unicode正規化の概要

Unicodeは現在、文字コードの事実上の標準といえるほど広く使われている。
「㈱」と「(株)」や、同じ「デ」でも一文字の「デ」と「テと"を合成したデ」は、そのままではそれぞれ別々の文字として扱われるため当然Bowの結果も異なってしまう。

Unicode正規化の詳解

Unicodeでは文字をコードポイントで表す。(16進表記)
それぞれord()とchr()というPythonの組込関数を用いて相互変換できる。

Unicodeとコードポイントの例
>>> hex(ord('あ'))
'0x3042'
>>> chr(0x3042)
'あ'

# ちなみに、10進表記でも可能
>>> ord('あ')
12354
>>> chr(12354)
'あ'

次に「デ」という文字について、一文字の場合と結合文字列(基底文字と結合文字)の場合でコードポイントを確認する。

デのコードポイント確認
# 一文字
>>> chr(0x30C7)
'デ'

# 結合文字列
>>> chr(0x30C6)
'テ'
>>> chr(0x3099)
'゙'
>>> chr(0x30C6) + chr(0x3099)
'デ'

上記のように、同じ文字に複数の表現方法が存在することになるこの問題に対してUnicodeは、「同じ文字として扱うべきコードポイントの組を定義する」という方法で対応した。これをUnicodeの等価性といい、下記の2つがある。

  • 正準等価性
    • 見た目も機能も同じ文字を等価とみなす
    • 「デ」と「テ」+「"」
  • 互換等価性
    • 見た目や機能が異なる可能性はあるが、同じ文字が元になっているものを等価とみなす
    • 正準等価性を含む
    • 「テ」と「テ」

Unicode正規化は、この等価性に基づき合成済み文字を分解したり合成したりすることであり、下記の4つがある。Canonicalが正準、Compatibilityが互換という意味である。

  • NFD(Normalization Form Canonical Decomposition)
    • 正準等価性による分解
  • NFC(Normalization Form Canonical Composition)
    • 正準等価性による分解 → 正準等価性による合成
  • NFCD(Normalization Form Compatibility Decomposition)
    • 互換等価性による分解
  • NFKC(Normalization Form Compatibility Composition)
    • 互換等価性による分解 → 互換等価性による合成

実際にUnicode正規化を行う際は、アプリケーションが扱う問題やデータの性質に合わせて、どの正規化を用いるのかを決定する必要がある。

02.3 見出し語化

活用などによる語形の変化を補正し、辞書の見出しに載っている形に直すことを見出し語化と呼ぶ。ただし、この時点では「本 を 読む だ」と「本 を 読む ます た」ではまだ同じ特徴を抽出できていない。
次節のストップワードも対応することによって、同じ特徴として扱うことができる。

本を読んだ
本を読みました

↓わかち書き + 見出し語化

本 を 読む だ
本 を 読む ます た

実装する際の話

表記ゆれの吸収という観点では前述の正規化と似ているが、わかち書きを補正するためにわかち書きの処理と合わせて記載することも多い。

MeCabのparseToNodeから得られたnode.featureを用いた場合は、カンマ区切りの6番目の要素から原形を得ることができる。

ただし、原形が登録されていない単語は表層形を使う。

BOS/EOSはMeCabの結果として、文の先頭と末尾を表す擬似的な単語なので、わかち書きの結果には含まないようにする。

02.4 ストップワード

前節では、わかち書きした結果「本 を 読む」までは同じ単語だが、その後が「だ」と「ます た」で異なるのでBoWも異なってしまう。
文意に大きな影響も与えず、語彙に含めるとメモリやストレージ効率の観点からも望ましくない。

辞書ベースのストップワード除去

下記のようにあらかじめ除外用の単語のリストを用意しておいて、if文で判定する。
slothlibなど、ネット上から必要なストップワードリストを用意できることもある。

~~
stop_words = ['て', 'に', 'を', 'は', 'です', 'ます']

~~
if token not in stop_words:
  result.append(token)

品詞ベースのストップワード除去

助詞や助動詞は文章を書く上で重要な品詞だが、文意を表現(対話エージェントでは、クラスID分けに必要な特徴を取得)する上では不要である。

~~

if features[0] not in ['助詞', '助動詞']:
~~

02.5 単語置換

前節と同じく、文章としては重要だが、文意を表現する上では「数値や日時」などは大した意味がないことがあるので、特定の文字列に置換する。

# 変換前
卵を1個買った
卵を2個買った
卵を10個買った

# 変換後
卵を SOMENUMBER 個買った
卵を SOMENUMBER 個買った
卵を SOMENUMBER 個買った
  • 個数の情報は失うが、「卵を買った」という文意はそのままで、個数の差異を統一できている
  • 「SOMENUMBER」の前後に半角スペースを含め、わかち書きで前後の文字と結合されることを防ぐ
  • 「SOME NUMBER」と半角スペースを含めても同一の結果が得られるが、次元数が無駄に1つ増えてしまうので避ける

02.6 対話エージェントへの適用

冒頭でも述べたとおり、この章で学んだ下記のテクニックを対話エージェントに適用させる。

  • 正規化
    • neologdnによる正規化
    • アルファベットの小文字化
    • Unicode正規化
  • 品詞によるストップワード除去
  • 見出し語化
~~

# _tokenize()の改良
    def _tokenize(self, text):
        text = unicodedata.normalize('NFKC', text)  # Unicode正規化
        text = neologdn.normalize(text)  # neologdnによる正規化
        text = text.lower()  # アルファベットの小文字化

        node = self.tagger.parseToNode(text)
        result = []
        while node:
            features = node.feature.split(',')

            if features[0] != 'BOS/EOS':
                if features[0] not in ['助詞', '助動詞']:  # 品詞によるストップワード除去
                    token = features[6] \
                            if features[6] != '*' \
                            else node.surface  # 見出し語化
                    result.append(token)

            node = node.next

        return result
実行結果
0.43617021

正解率は前章の37%より向上したが、まだ43%しかない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonゴルフテク(AtCoder)

はじめに

AtCoderで今まで培ってきたPython3の中で短く書くテクニックの紹介となります。
なお、AtCoderのPython3のバージョンは3.4.3で、例えばf-stringsやセイウチ演算子など、短くなりそうな機能が使えなかったりします。
あくまでAtCoder上でのテクニックということになります(他のところで使えるかは知らない)。
思いついたことを書いていくのでとっ散らかってると思います。すいません。

標準入力

Pythonの標準入力と言えばinput()を思い浮かべると思いますが、短く書くときは、input()よりもopen(0)が使われがちです。
openはファイルを開く関数ですが、第一引数に0を指定することで、標準入力から読み込んでくれます。
例えば、

4
1
2
3
4

のような入力が与えられ、最初の行を変数nに、残りの行をリストaに格納したい場合、

n,*a=map(int,open(0))

と書くことができます。また、

5 4
1 2 3 4 5

のような入力が与えられ、最初の行をnとk、2行目をリストaに格納したい場合、

n,k,*a=map(int,open(0).read().split())

と書くことができます。

evalを使うことが有効となるケースもあります。
evalは引数で与えられた文字列を式として実行してくれます。なので入力の文字列を適切に加工してevalに渡すことで、短くなることがあります。
例えば、

10 20

のように1行に2つの数値が与えられ、その積を出力せよという問題の場合、

print(eval(input().replace(' ','*')))

と書くことができます。この場合、
入力の文字列の' 'を'*'に置換することで、

10*20

という文字列になり、これを式として実行することで解が得られるということになります。
また、制約で2つの数値が100未満である場合、

s=input()
print(int(s[:2])*int(s[2:]))

と書くこともできます。

出力

あるリストaの中身を空白区切りで出力したい場合、

print(*a)

と書くことができます。
これを改行区切りにしたい場合、

print(*a,sep='\n')

とすれば良いです。
AtCoderでは、解を改行区切りで出力せよという場合でも、空白区切りで出力して通ることが良くあります。

YNeos

Pythonゴルフと聞いてこれを思い浮かべた人は多いと思います。
ある条件を満たすとき'Yes'を、そうでないとき'No'を出力せよというような問題で、

print('YNeos'[条件式::2])

などと書くことができます。
なお、条件式がTrueになるときに'No'を出力するので、無理に条件をひっくり返して長くなるくらいなら、

print('NYoe s'[条件式::2])

のようにしても良いです。
この場合、条件式がFalseになるとき'No '(末尾に空白)が出力されるのですが、AtCoderはこのような場合でも通ることが多いです。

セミコロン

Pythonでは改行以外にセミコロンも文の区切りに使えます。
例えばfor文で複数の処理を書くとき、

for _ in'_'*n:hoge;fuga;piyo

などと書くことができます。
また、このような同じ処理をn回繰り返すようなものの場合、execという引数で与えられた文字列を式として実行してくれる関数を使って、

exec('hoge;fuga;piyo;'*n)

と書くことができます。

boolの演算

boolはintのサブクラスなのでintに対して演算を行えます。
例えば、ある条件を満たしたときaに、満たしてないときにbに1を加算したい場合、

f=条件式
a+=f
b+=1-f

などと書くことができます。

リストへのappend

あるリストaに対して、末尾に0を追加したいとき、

a+=[0]

のように大きさ1のリストを足せば良く、さらに、

a+=0,

ようにタプルを足すこともできます。
ある条件を満たしたときのみ追加したい場合は

a+=[0]*条件式

のように書くことができます。

import

例えば

from numpy import*

のように書くことでNumPyの関数がnp.など付けずに使えるようになります。

色々な演算

nをmで割る(切り上げ)

0--n//m

(n+1)*2

n+1<<1

もしくは

-~n*2

(n-1)*2

~-n*2

おわりに

なにか思いついたら追記していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

__init__.py を省略してはいけない

Python 3.3 から __init__.py を省略して良いと思っている人が多いですが、 省略しないでください。

なぜ勘違いが起こったのか

Python 3.3 から、 PEP 420 で Implicit namespace package が追加されました。

Namespace package とは普通の package ではありません。 特殊な用途のもので、ほとんどの人にとっては 知る必要すらない ものです。

どうしても知りたければ、上の PEP 420 と packaging guide を読んでください。

__init__.py を省略する弊害

普通の package で Implicit namespace package を乱用すると弊害があります。

import が遅くなる

通常の package とは違うので import が package 内のモジュールを探すのが遅くなる可能性があります。
また、確率は低いですがその探索順序の違いによってなにか問題が起こる可能性もあります。

ツールが対応していない

例えば標準ライブラリの unittest で test モジュールを自動で探す機能は __init__.py がないディレクトリの中を探しに行きません。 (https://bugs.python.org/issue29642 を参照)

もし対応しようとしたら、 __init__.py がないディレクトリも全部再帰的に探索しないといけなくなります。そのディレクトリは node_modules で数十万のファイルやディレクトリが入っているかもしれません。そのディレクトリがネットワークマウントされていたりしたらどれだけ遅くなるでしょうか。

Implicit namespace package を通常の package として 乱用するユーザーのためにそんな速度低下は到底受け入れられません。

同じ理由で lint 等のツールでも、自動でパッケージやモジュールを探すような機能が Implicit namespace package を探してくれると期待してはいけません。

背景を知らずに要望を受け入れて対応しているツールもあるかもしれませんが、「対応するべき」「対応しろ」というIssueやPull Requestを送るのはメンテナや他のユーザーに迷惑なのでやめましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】Pythonによる簡単な文書作成

Pythonを業務自動化するために使いたいという方は多いのではないでしょうか。
そこで、Pythonによって文書を作成する方法を具体例と共に初心者向けに解説しました。
私が試行錯誤して苦労した点をわかりやすく説明したいです。
例としてはリストから文書作成

 Python3系で動作します。

例1:リストから文書作成

■■やりたいこと■■

list.py
 Name = [Anago, Ikura, Unagi]

みたいなリストから

text0.txt
 Hello, I am Anago
text1.txt
 Hello, I am Ikura
text2.txt
 Hello, I am Unagi

っていう感じのファイルを作りたい。

■■実装■■
次のように書けます。

say_hi.py
 Name = ["Anago", "Ikura", "Unagi"]
 for i, name in enumerate(Name):
     with open(f"text{i}.txt", "w") as file:
         file.write(f"Hello, I am {name}")

■■解説■■
■1行目 Name = ["Anago", "Ikura", "Unagi"]
 ただの文字のリストです。

■2行目 for i, name in enumerate(Name):
 enumerateという組み込み関数(importなしに使える関数)を使っていて、
これはリストをあたえると、何番目の要素であるかを表すインデックスと要素を返してくれます。
インデックス、要素の順です。
ループ内でi, nameがそれぞれインデックス、要素を表す変数になります。

■3行目 with open(f"text{i}.txt", "w") as file:
 ここは複雑です。
まず、with構文を使っています。
本当はファイルをopenをしたらcloseをしなければいけないのですが、これを省略することができます。
open(f"text{i}.txt", "w") as file
はopenでファイルを開く処理をして、得られたファイルオブジェクト(ファイルを表す変数のようなもの)というものにfileという名前をつけています。
openは2つの引数を取っています。
1つめはファイル名を表しています。
f"text{i}.txt"はf-stringsというもので、""でくくった文字列中の{i}で変数iが代入されます。これによって、ループによって異なるファイル
text0.txt
text1.txt
text2.txt
に書き込むことができます。
2つめの"w"は、1つめの引数で指定したファイル名のファイルがなければ、新規作成、あったら上書き保存することを表しています。

■4行目 file.write(f"Hello, I am {name}")
file.write
は3行で得たファイルオブジェクトfileに対して書き込むことを表しています。
中身のf"Hello, I am {name}"はこれもf-stringsで{name}によってnameが代入されます。

参考文献

参考にさせていただいたサイト

ファイル読み書き file open read write

pythonでファイルの読み書き

python for文を初心者向けに解説!for文基礎はこれで完璧

f-strings の使用例

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flask-Python实现简单网页

例子1:最简单的web启动

最简单实现helloworld

hello.py
# start source block
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello World!'

def main():
    # 开发用web服务器启动
    app.run(host='127.0.0.1', port=5678, debug=False)


if __name__ == '__main__':
    # 调用main启动函数
    main()
# end source block

上面代码保存为hello.py
命令行运行 $ python hello.py
在浏览器访问地址:http://localhost:5678
正常页面显示 Hello World!

例子2:Blueprint启动views目录下的py文件和html模板

目录结构

hello/
--app.py
--views/
----init.py # 作为软件包定义
----view_hello.py
--templates/
----view_hello.html

主启动app.py文件

app.py
# start source block
from flask import (
    Flask,
    Blueprint,
)

# 导入目标页面的view
from views import view_hello

app = Flask(__name__)

# 注册目标页面的子app 所有对象页面都会在根地址root_url/hello/出现
app.register_blueprint(view_hello.app, url_prefix='/hello')


def main():
    # 开发用web服务器启动
    app.run(host='127.0.0.1', port=5678, debug=False)


if __name__ == '__main__':
    # 调用main启动函数
    main()
# end source block

html数据先是用服务器端处理view_hello.py

view_hello.py
# start source block
from flask import (
    Flask,
    Blueprint,
    render_template,
)

app = Blueprint(
    'views.hello',
    __name__,
    template_folder='templates',
)


@app.route('/sayhello')
def say_hello():
    page_title = 'hello world'
    page_html = 'view_hello.html'

    res = {
        'page_title': page_title,
        # 'data_lst': data_lst, 其他页面表示数据
    }

    return render_template(
        page_html,
        res=res,
    )
# end source block

__init__.py
# start source block
# package
# 只有注释
# end source block

前端表示用view_hello.html

view_hello.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <title>{{res.page_title}}</title>

  </head>
  <body >
你好 世界!   
  </body>
</html>

命令行运行 $ python app.py
在浏览器访问地址:http://localhost:5678/hello/sayhello
正常页面显示 标题:hello world! 页面内容:你好 世界!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder Beginner Contest 121 過去問復習

所要時間

スクリーンショット 2020-01-20 14.39.41.png

感想

D問題がXORだったので無理だと思ってたらそこまで難しくなく、気合いが足りないと感じました。

A問題

かぶるところの数え上げだけ注意。

answerA.py
H,W=map(int,input().split())
h,w=map(int,input().split())
print(H*W-h*W-w*H+h*w)

B問題

全問題に対してそれぞれの評価式を行えば良い。

answerB.py
n,m,c=map(int,input().split())
b=list(map(int,input().split()))
ans=0
for i in range(n):
    a=list(map(int,input().split()))
    k=0
    for i in range(m):
        k+=a[i]*b[i]
    ans+=(k+c>0)
print(ans)

C問題

昇順ソートで取れるだけ取る。
どこまで取れるかを正確に判断する必要がある。

answerC.py
n,m=map(int,input().split())
ab=[list(map(int,input().split())) for i in range(n)]
ab.sort()
ans=0
for i in range(n):
    if m>ab[i][1]:
        ans+=ab[i][0]*ab[i][1]
        m-=ab[i][1]
    else:
        ans+=ab[i][0]*m
        print(ans)
        break

D問題

XORの計算ではそれぞれのビットを独立に計算することができ、整数$c_1$~$c_n$のXORを考える時、二進数に直してそれぞれのビットについて1がいくつあるかを数えることで、その1の個数が奇数の時は1で偶数の時は0と求めることができます。つまり、この問題でもA~Bのそれぞれのビットに注目して独立に計算をすれば良いことがわかります。
ここで、A~Bを全て二進数に直し、昇順に並べたときのことをまずは考えます。このとき下図のようになることがわかります。

IMG_0096.JPG

0と1がそれぞれのビットでいくつあるかに注目して考えると、それぞれのビットで周期的に0と1を繰り返していることがわかります。この周期の長さはkビット目(0-indexed)について$2^{k}$になることは少し考えれば(考えなくても?)明らかです。また、k>=1の時は1周期の長さが偶数なのでXORの計算をすると1周期分は0になるので無視してよく、AとBの両端にいくつ1が連続しているかを数えれば良いです(半端に残ってしまっている部分を数えるというイメージ、k=1の時は0と1を繰り返すだけなので簡単に1の数は求められます。)。また、あるビットについてAとBが(0,1)or(1,0)となる時は単純に片側の1から1がいくつあるのかを計算すれば良いのですが、AとBがともにそのビットで1の場合はA~Bで全て1になる際にダブルカウントしてしまうので注意が必要です。また、端からどこまで1が続くかを考える場合は下図のようにして考えれば差を考えることで容易に1が何個あるかを考えることができます(境界を考えました。)。

IMG_0095.JPG

以上を実装すると以下のようになります。

また、少しまとまってないので思考のフローを以下で整理します。

(1)XORなのでそれぞれのビットにいくつ1があるかを数えれば良い

(2)(連続する)0と1の周期が存在する

(3)(周期の長さが偶数なので、)端から1が何個あるかを数えれば良い

(4)周期が切り替わる境界の数が何かを考えれば(3)がわかる

また、b=0の時はlog2の引数が0になってしまいREになるので注意が必要です。僕は引っかかりました。

answerD.py
import math
import sys
a,b=map(int,input().split())
if b==0:
    print(0)
    sys.exit()
n=math.floor(math.log2(b))+1
ans=[0]*n
form="0"+str(n)+"b"
sa=format(a,form)
sb=format(b,form)
for i in range(n):
    if i==n-1:
        if (b-a+1)%2==0:
            ans[i]=((b-a+1)//2)%2
        else:
            if sa[i]=="1":
                ans[i]=((b-a+1)//2+1)%2
            else:
                ans[i]=((b-a+1)//2)%2
        break
    if sa[i]=="1" and sb[i]=="0":
        s_compa=sa[:i]+"1"*(n-i)
        cmpa=int(s_compa,2)
        ans[i]=(cmpa-a+1)%2
    elif sa[i]=="0" and sb[i]=="1":
        s_compb=sb[:i]+"1"+"0"*(n-i)
        cmpb=int(s_compb,2)
        ans[i]=(b-cmpb+1)%2
    elif sa[i]=="1" and sb[i]=="1":
        s_compa=sa[:i]+"1"*(n-i)
        cmpa=int(s_compa,2)
        s_compb=sb[:i]+"1"+"0"*(n-i)
        cmpb=int(s_compb,2)
        if cmpa>a:#cmpb<b
            ans[i]=((b-cmpb+1)+(cmpa-a+1))%2
        else:
            ans[i]=(b-a+1)%2
cnt=0
for i in range(n):
    cnt+=(ans[i]*2**(n-i-1))
print(cnt)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C++ で Python を実行 on Visual Studio 2017

背景

お仕事()でC++でPythonを実行したい、しなくてはいけない状況になりました。
その際に環境構築にかなり手間取ったので一度まとめたいと思います。
いくつかインストールします。
時間もかかります。

開発環境

・ノートパソコン
・OS:window 10 64 bit
・CPU:へっぽこ
・GPU:なし
・Visual Studio 2017
・C++
・Python 3.7.4

インストール内容

・Visual Studio 2017
・Python 3.7.4
・boost 1.70.0

インストール:Visual Studio 2017

下のURLにいき、「Download Community 2017」をダウンロードします。
こちらは個人等であれば無料で使えます。
あとMicrosoftのアカウントを作成しておくことをお勧めします。
1か月ほど使用しているとアカウントを作ってログインしないとつかわせないぞ~、と言われますよ(恐怖)。

URL
https://docs.microsoft.com/ja-jp/visualstudio/releasenotes/vs2017-relnotes

image.png

次にインストールします。
左側の「C++によるデスクトップ開発」と「右側のWINDOWS 10 SDK」のそれぞれにチェックを入れます。
Pythonはここではいれません
今回は純正Pythonを入れます。 
Visual studio 2017によってPython(またはanaconda)をいれてもいいんですが、
個人的にVisual studio 2017にPythonを依存させてしまうのが怖いので、
互いに独立するように環境を構築します。

image.png

下のようにインストールが始まります。
image.png

インストール:Python 3.7.4

Pythonのインストール方法はたくさんのサイトで確認できますが、確認用に載せます。

URL
https://www.python.org/downloads/release/python-374/

URLの下にスクロールするとFilesがあります。
OSがwindosn 64bitなのでwindos x86-64 executable installerをダウンロードします。

image.png

Add Python 3.7 to PATH にチェックを入れて、自身のPC内の環境変数パスを通します。

image.png

インストール:boost 1.70.0

以下のURLからダウンロードします。

URL
https://www.boost.org/users/history/version_1_70_0.html

boostはC++とPythonをつなげるライブラリの要素もあります。
最新バージョンもありますが、1.70.0は以前にも使用できることを確認できているので、今回はこちらを指定しました。

image.png

ダウンロードして、展開し、Cドライブの直下に置いてください。
注意してほしいのは、boostの設定はかなり時間がかかります。
恐ろしいほど時間がかかります。
なのでファイルを操作しているときは、気長に待ちましょう。
私はzipファイルの展開に30分かかっています。
ディレクトリの位置に注意してください。

image.png

次にコマンドプロンプト(cmd)でboostをインストールします。
○○はユーザーネームです。

cmd
C:\Users\○○ > cd C:\boost_1_70_0
C:\boost_1_70_0>bootstrap.bat

bootstrap.batの実行結果。

cmd
Building Boost.Build engine

Generating Boost.Build configuration in project-config.jam for msvc...

Bootstrapping is done. To build, run:

    .\b2

To adjust configuration, edit 'project-config.jam'.
Further information:

    - Command line help:
    .\b2 --help

    - Getting started guide:
    http://boost.org/more/getting_started/windows.html

    - Boost.Build documentation:
    http://www.boost.org/build/

1でVisual Studio 2017をインストールしました。
boostをインストールするときにそれぞれのバージョンに注意してください。
・Visual Studio 2017 はmscv-14.1
・64bit であること x64
またディレクトリの移動せずに以下を入力してboostをインストール。

cmd
b2.exe toolset=msvc-14.1 link=static runtime-link=static,shared --build-dir=build/x64 address-model=64 -j5 install --includedir=C:\boost_1_70_0\include --libdir=C:\boost_1_70_0\stage\lib\x64

※Visual Studio 2017 以外の2019, 2015だとmscv-14.1ではないのでご注意を。
 Visual Studio 2019 のバージョンはmscv-14.2(確認済み)
 Visual Studio 2015 のバージョンはmscv-14.0(未確認)

サンプルプログラムの実行

準備は整いました。
boostに時間がかけられたかもしれませんが、めげずに行きましょう(汗)。

新規プロジェクトを作成

Visual Studio 2017を起動して、新規のプロジェクトを起動します。
C++でコンソールアプリを選択。
今回のプロジェクト名をtest_Cplus2_Pythonとしました。
image.png

プロジェクトのプロパティを設定

プロジェクトを右クリくして、プロパティを選択。
設定項目は4つです。
1. ソリューション構成とソリューションプラットホームの変更。
プロパティページとメイン画面の両方で設定する。
この設定変更を忘れがちです。
2. boostとPythonをインクルード。
3. マルチスレッドに設定。
4. boostとPythonの.libファイルをリンク。

プロパティページの設定
1. ソリューション構成とソリューションプラットホーム
Debug → Release に変更
x86 → x64

2. C++ → 全般 → 追加のインクルード
C:\boost_1_70_0
C:\Users\○○\AppData\Local\Programs\Python\Python37\include

3. C++ → コード生成 → ランタイムライブラリで 
マルチスレッド(/MT)に変更

4. リンカー → 全般 → 追加のライブラリディレクトリ
C:\boost_1_70_0\stage\lib\x64
C:\Users\○○\AppData\Local\Programs\Python\Python37\libs

設定項目を赤い外枠で囲みました。
○○はユーザー名です。
image.png

一度だけ実行

プロジェクトのプロパティ設定ができたら、一度プログラムを実行します。
実行することによってプロジェクトが動くことの確認と.exeファイルを作成ができます。
Hello world! がでれば大丈夫です。

C++とPythonのサンプルコード

C++でPythonとPythonのファイル(.py)を呼ぶので、それぞれにコードを書き込まなくてはいけません。
C++はVisual Studio上に書き込み、Pythonはテキストを新規で作成して、下のテストコードを書く込んでください。

C++に関しては、pythonの名前空間を定義し、関数を

test_Cplus2_with_Python.cpp
#define BOOST_PYTHON_STATIC_LIB

#include <iostream>
#include <boost/python.hpp>

int main()
{
    //名前空間
    namespace py = boost::python;
    //初期化
    Py_Initialize();
    //出力
    std::cout << "Hello World! from C++ \n";

    //Pythonのファイル(test_py.py)をインポート
    py::object module_ns = py::import("test_py").attr("__dict__");
    //インポートしたファイル内の関数を定義
    py::object get_and_return = module_ns["hello_from_python"];
    //インポートした関数を実行
    auto return_nd_array = get_and_return();
}

呼び込むPythonファイルを以下のようにシンプルな処理にします。

test_py.py
def hello_from_python():
        print("Hello World! from Python")

フォルダ構成

先程作成したC++のコードは
test_Cplus2_with_Python→test_Cplus2_with_Python.cppへ書き込み、
Pythonのコードはx64→Release→test_py.pyを配置します。

フォルダ構成
test_Cplus2_with_Python(自作フォルダ)
 ├── test_Cplus2_with_Python
 │  ├── test_Cplus2_with_Python.cpp 〇
 │  ├── test_Cplus2_with_Python.vcxproj
 │  ├── test_Cplus2_with_Python.vcxproj.filters
 │  ├── test_Cplus2_with_Python.vcxproj.user
 │  └── x64
 │  
 ├── x64 
 │  └── Release
 │       ├── test_Cplus2_with_Python.exe
 │       ├── test_Cplus2_with_Python.iobj
 │       ├── test_Cplus2_with_Python.ipdb
 │       ├── test_Cplus2_with_Python.pdb
 │       └── test_py.py 〇
 │ 
 └── test_Cplus2_with_Python.sln

実行結果

C++とPythonのそれぞれの文字列を出力できたら成功です。

image.png

できない場合は、
・プロジェクトページでの設定が間違っている
・Pythonファイルのコードが間違っている
・Pythonファイルの配置位置は間違っている
・boostのインストールが間違っている
などがあげられます。

反省

C++でPythonを呼ぶことなんて、そうそうあることではないと思います。
活用方法としては、C++でPythonの深層学習ツールを使用するときに有効です。
後輩を含め、様々な人のお役に立てることを心からお祈りしています。
余裕があったら、C++にPythonの深層学習を用いた物体検出手法(YOLOv3)の実装を載せたいと思います。

Github

ご参考にどうぞ。

URL
https://github.com/yusa0827/200121_Cplus2_with_Python
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OpenCVのSIFTで特徴抽出してみた

1. SIFTとは

  • SIFT(Scale-Invariant Feature Transform)
  • 特徴点の検出と特徴量の記述を行います。
  • 特徴: 拡大縮小に強い、回転に強い、照明変化に強い。

SIFTのアルゴリズム

【特徴点の検出】

1-1. 特徴点となる候補点の探索
1-2. 候補点の絞り込み

【特徴量の記述】

2-1. 各特徴点の勾配を検出
2-2. 各特徴点の勾配方向ヒストグラム計算

1-1. 特徴点となる候補点の探索

スケール方向の差分画像(DoG画像)において、極値を取る点を特徴点とする。簡単にまとめると、2次元(x,y)である画像に、スケールという次元を加えて3次元にする。スケールはどうやって決めるかというと画像(x,y)を、ある量σだけ平滑化した(x,y,σ)を考える。平滑化にはガウシアンフィルタを使う。このデータを使って、特徴点となる候補を洗い出す。変化量が大きいってことは、情報量が多い。

1-2. 候補点の絞り込み

DoG画像の出力値は、(x,y,σ)を変数とする関数なので、それを特徴点周りで近似してやり、その近似式の導関数とかを使って、極値をとる点を算出し直すってこと。エッジ上の点は除外。DoG出力値が小さいものを除外。

2-1. 各特徴点の勾配を検出

  • 特徴点の周りで輝度勾配ヒストグラムを作成
  • 方向・強度の計算はHoGとほぼ同じ
  • 方向は36方向

ポイント
強度は、特徴点のスケールのガウシアンフィルタで重み付け。
⇒スケール変化に強くなる。

  • 求めたヒストグラムにて、強度が最大値の80%を超える方向を、この特徴点の方向と定める。

2-2. 各特徴点の勾配方向ヒストグラム計算

  • 特徴点の方向を基準の方向として、再度、輝度勾配ヒストグラムを作成する(回転に強い)
  • 方向は8方向(45度ずつ)
  • 4x4x8 = 128次元の特徴量
  • 正規化もする(これにより照明変化に強くなる)

SIFTのコード

サンプルコードです。

import cv2
import numpy as np

img = cv2.imread('dog.jpg')
sift = cv2.xfeatures2d.SIFT_create()
keypoints, descriptors = sift.detectAndCompute(img, None)
img_sift = cv2.drawKeypoints(img, keypoints, None, flags=4)
cv2.imwrite("sift_img.jpg",img_sift)

  • image : 入力画像
  • keypoints : 入力画像から得られたキーポイント
  • flags : 図形描画機能の識別設定

入力画像
dog.jpeg

出力画像

sift_img.jpg

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonで隣接行列から2点間の全てのパスを見つける

はじめに

2頂点対最短経路問題を解いている時に、最短ではないが条件を満たすパスの見つけ方を調べたので、メモとして残しておきます。

手順

1. 隣接行列の作成

今回のデータは.csv fileに保存されているのでpandasを使って読み取り、numpy arrayに変換します。

file = pd.read_csv('Data1.csv', header=None, delimiter=',')  
data = np.array(file)

2. NetworkXでグラフを作成する。

NetworkXは、グラフ/ネットワーク理論系の計算を行うためのPythonのパッケージです。

G=nx.from_numpy_matrix(data)

3. 指定した2点間をつなぐ全てのパスを探し、プリントする。

使用した関数はnx.all_simple_paths()。この関数は先ほど作成したグラフGにおいて、スタート地点(= source)とゴール地点(= target)を指定することで、指定した2点間をつなぐ全てのパスを探し出します。ただし、有向グラフでない場合、特に巨大なグラフの場合は何万通りものパスを見つけてしまうので、cutoffでパスの深さを限定する必要があります。

for path in nx.all_simple_paths(G,source=1,target=100, cutoff=13): 
    print(path)

結果はこの通りです、
読み込んだデータは8000ノードある巨大なものなので、パスの深さを13に限定しました。

[1, 2587, 808, 211, 188, 6510, 6216, 3057, 5276, 1512, 3826, 5203, 100]
[1, 2587, 808, 4675, 1671, 5411, 3983, 3714, 202, 3064, 644, 2182, 100]
[1, 2587, 808, 4675, 1671, 5411, 3983, 6512, 3590, 7915, 2182, 100]
[1, 2587, 808, 4675, 5958, 4521, 6562, 7430, 4083, 6189, 3089, 3826, 5203, 100]
[1, 2587, 6562, 4521, 5958, 4675, 1671, 5411, 3983, 6512, 3590, 7915, 2182, 100]
[1, 2587, 6562, 6046, 4846, 324, 7374, 1141, 3651, 1963, 888, 5203, 100]
[1, 2587, 6562, 7430, 4083, 6189, 3089, 3826, 5203, 100]
[1, 3346, 4986, 4968, 2652, 1750, 5373, 6453, 6667, 5505, 7572, 7915, 2182, 100]
[1, 3346, 4986, 4968, 2652, 2154, 5437, 656, 3987, 2058, 6764, 6790, 2182, 100]
[1, 3346, 4986, 4968, 2652, 2800, 2448, 3575, 1141, 3651, 1963, 888, 5203, 100]
[1, 3346, 4986, 4968, 4197, 7116, 6733, 869, 4783, 6468, 7620, 6790, 2182, 100]
[1, 3346, 4986, 4968, 5331, 1379, 6246, 6214, 40, 2579, 3590, 7915, 2182, 100]
[1, 3346, 4986, 4968, 5331, 7417, 7969, 6865, 2863, 868, 6764, 6790, 2182, 100]

最後に

コード自体はとてもシンプルですが、一度引っかかったポイントとしてはパスの深さを限定しないでいるとプログラムが終わらないという点です。同じような問題に取り組んでいる方の参考になればと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pyinstallerを使ってPythonコードから生成した実行ファイルについて、実行時エラーModuleNotFoundErrorを回避

記事の内容

Mac及びWindows10で、Pyinstallerを使ってPythonコードから生成した実行ファイルを動かしたら、ModuleNotFoundErrorが発生しました。それを回避した時の記録です。

手順

以下のように、Pyinstallerを使ってPythonコードから実行ファイルを生成しました。pipenvを使用しています。

pipenv run pyinstaller foo.py --onefile

実行ファイルがdistディレクトリ配下に生成されましたので、以下のように動かしました。

./dist/foo

実行時エラーが発生しました。
Macでのエラーメッセージ:

Traceback (most recent call last):
  File "site-packages/PyInstaller/loader/rthooks/pyi_rth_pkgres.py", line 13, in <module>
  File "/Users/username/.local/share/virtualenvs/foo_project-dA2KfCBr/lib/python3.7/site-packages/PyInstaller/loader/pyimod03_importers.py", line 623, in exec_module
    exec(bytecode, module.__dict__)
  File "site-packages/pkg_resources/__init__.py", line 86, in <module>
ModuleNotFoundError: No module named 'pkg_resources.py2_warn'
[5125] Failed to execute script pyi_rth_pkgres

Windowsでのエラーメッセージ:

Traceback (most recent call last):
  File "site-packages\PyInstaller\loader\rthooks\pyi_rth_pkgres.py", line 13, in <module>
  File "c:\users\username\.virtualenvs\foo_project-uuzswlzx\lib\site-packages\PyInstaller\loader\pyimod03_importers.py", line 623, in exec_module
    exec(bytecode, module.__dict__)
  File "site-packages\pkg_resources\__init__.py", line 86, in <module>
ModuleNotFoundError: No module named 'pkg_resources.py2_warn'
[14256] Failed to execute script pyi_rth_pkgres

対処方法です。
まず、最初にpyinstallerを動かしたときに.specファイルも生成されていますので、それをテキストエディタで編集します。
ModuleNotFoundErrorと言われてしまったモジュールを、以下のようにhiddenimportsに書き加えます。

foo.spec
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['foo.py'],
             pathex=['/Users/username/PycharmProjects/n/foo_project'],
             binaries=[],
             datas=[],
-              hiddenimports=[],
+              hiddenimports=['pkg_resources.py2_warn'],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='foo',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True )

以下のように、編集した.specファイルを使用して再びpyinstallerを起動します。

pipenv run pyinstaller foo.spec --onefile

実行ファイルが生成されましたので、以下のように動かしたら、うまく動きました。

./dist/foo

エラーメッセージの意味(想像)

エラーメッセージ内に、以下の内容が含まれていました。

exec(bytecode, module.__dict__)

私の想像ですが、pyinstallerが生成した実行ファイルでは、人がpipenv installでインストールしたモジュールが、exec関数が動かすコードに引き継がれないのだと思います。
exec関数が動かすコード用に、モジュールを.specファイルのhiddenimportsに指定しておく必要があるのでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rust/Python] 周期境界条件

実数 $\mathbb{R}$ に周期 $b > 0$ の周期境界条件を課す ($\mathbb{R} / b \mathbb{R}$) ことを考えます. つまり, ふたつの数 $x$ と $x+b$ を同一視します. このとき任意の数 $x \in \mathbb{R}$ に対応する座標の一意的な表現として
* $0 \leq x < b$
* $-\frac{b}{2} \leq x < \frac{b}{2}$
という二通りの表現方法が可能です. 本記事では任意の実数 $x$ が与えられたときにこれらの制約を満足する値へと変換するコードを確認します.

Rust 実装

コード全体と動作確認はこちら: PlayGround

0 ≦ x < b

Rust では剰余 x%bx と同符号になります. 従って x が正ならば x%b でよいのですが, そうでなければ b を足す必要があります.

#[inline]
fn pbc1(x: f64, b: f64) -> f64 {
    let dx = x%b;
    if dx >= 0. { dx } else { dx + b }
}

最初 f64::is_sign_negative を使っていたのですが, -0.0 の扱いで困った結果, 不等号を使うべきだという結論に到達しました.

-b/2 ≦ x < b/2

この場合 (x/b + 0.5).floor() を計算すると x を欲しい区間に移動するために何回 b で動かせばよいのかがわかります.

#[inline]
fn pbc2(x: f64, b: f64) -> f64 {
    x - (x/b + 0.5).floor()*b
}

Python 実装

以下のコードは入力 x が float でも np.ndarray でも使えます. 動作確認はこちら: Paiza

0 ≦ x < b

Python では剰余は $0 \leq x\%b < b$ を満たすようになっている (いま $b > 0$ としています) ので一発です.

def pbc1(x, b):
    return x%b

-b/2 ≦ x < b/2

これは上の Rust 実装をそのまま Python に書き換えるだけです.

def pbc2(x, b):
    return x - np.floor(x/b + 0.5)*b
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

独自例外の作成

class UppercaseError(Exception):
    pass

def check():
    words = ['APPLE', 'orange', 'banana']
    for word in words:
        if word.isupper():
            raise UppercaseError

try:
    check()
except UppercaseError as e:
    print('独自に作成したエラーです。')
実行結果
独自に作成したエラーです。

Exceptionをスーパークラスとして継承した
サブクラスUppercaseErrorを作成。

UppercaseErrorの中身はpass即ち、
Exceptionと同じ。

check関数を作成。
check関数の中身はwordsリストの中身を1つ1つ
大文字で記載したものがないかチェックし、
あった場合はraise UppercaseError
つまり、UppercaseErrorとする というもの。

try以下は、
check関数を実行して、
UppercaseErrorが発生した場合は、
except UppercaseErrorブロックが実行されるというもの。

なので、
実行結果に、
独自に作成したエラーです。と出力される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

強化学習 今日から学習

あなたにはこれから、北海道の札幌から沖縄の那覇まで旅行していただきます。強化学習で。

この日本縦断旅行には、以下の2つの目標があります。

  • 目標1:ゴールにたどり着く。 まずはゴールにたどり着かないことにはどうにもなりません。
  • 目標2:最短経路を発見する。 ゴールまでの経路を発見したら、その中で最短のものを発見します。

そして、以下のルールを適用したいと思います。

  • 一度訪れた都市は二度と訪問しない。間違えて二度同じ都市を訪れたらゲームオーバー。

グラフ探索手法を使えばいいんじゃない?

ええ、その通りです。グラフ探索手法を使えば簡単に求まります。詳しくは

を見てください。でもここでは、強化学習について今日から学習していただくのが目的です。

日本の県庁所在地データ

グラフ理論の基礎グラフ理論の基礎をmatplotlibアニメーションでで用いた以下のデータを用います。

県庁所在地の緯度・経度

県庁所在地の緯度・経度データをダウンロードします。

import urllib.request
url = 'https://raw.githubusercontent.com/maskot1977/ipython_notebook/master/toydata/location.txt'
urllib.request.urlretrieve(url, 'location.txt') # データのダウンロード
('location.txt', <http.client.HTTPMessage at 0x110e78ba8>)

ダウンロードしたデータを読み込みます。

import pandas as pd
location = pd.read_csv('location.txt').values
pd.DataFrame(location)
0 1 2
0 Sapporo 43.0642 141.347
1 Aomori 40.8244 140.74
2 Morioka 39.7036 141.153
3 Sendai 38.2689 140.872
... ... ... ...
44 Miyazaki 31.9111 131.424
45 Kagoshima 31.5603 130.558
46 Naha 26.2125 127.681

47 rows × 3 columns

読み込んだデータを可視化しましょう。

%matplotlib inline
import matplotlib.pyplot as plt
plt.figure(figsize=(11, 9))
plt.scatter(location[:, 2], location[:, 1])
for city, y, x in location:
    plt.text(x, y, city, alpha=0.5, size=12)
plt.grid()

output_4_1.png

県庁所在地間の徒歩移動

県庁所在地間を徒歩で移動すると何時間かかるか、グーグルマップで調べてみたデータがあります(徒歩で移動できない場合はフェリーを使うものとします)。これをダウンロードします。

url = 'https://raw.githubusercontent.com/maskot1977/ipython_notebook/master/toydata/walk.txt'
urllib.request.urlretrieve(url, 'walk.txt')
('walk.txt', <http.client.HTTPMessage at 0x1220b8a58>)

ダウンロードしたデータを読み込みます。

walk = pd.read_csv('walk.txt', sep='\s+')
walk
Town1 Town2 Hour Ferry
0 Sapporo Aomori 55 True
1 Akita Aomori 36 NaN
2 Akita Sendai 45 NaN
3 Akita Niigata 52 NaN
... ... ... ... ...
96 Nagasaki Kumamoto 18 NaN
97 Nagasaki Saga 20 NaN
98 Kagoshima Naha 26 NaN

99 rows × 4 columns

あれ?鹿児島-那覇間がフェリーになってないことに今気づいた...まあいいや、今回のデータ解析に影響はない。ここで得られた都市間の関係を可視化しましょう。

plt.figure(figsize=(11, 9))
for city, y, x in location:
    plt.text(x, y, city, alpha=0.4, size=8)

for w in walk.values:
    t1 = np.where(location[:, 0] == w[0])[0][0]
    t2 = np.where(location[:, 0] == w[1])[0][0]
    n1, y1, x1 = location[t1]
    n2, y2, x2 = location[t2]
    plt.plot([x1, x2], [y1, y2])

plt.scatter(location[:, 2], location[:, 1])
plt.grid()

output_12_0.png

この県庁所在地の都市を「頂点」、都市と都市の間の線を「辺」と呼び、頂点と頂点が辺で直接結ばれていることを隣接していると呼ぶことにします。せっかく都市間の徒歩移動時間のデータがあるのですが、解釈を簡単にするため、以降の解析では「辺」の情報は用いますが、徒歩移動時間のデータは使いません。

県庁所在地間の距離行列

県庁所在地の緯度・経度から、県庁所在地間の距離行列を求めましょう。

import numpy as np
from scipy.spatial import distance

dist_mat = distance.cdist(location[:, 1:], location[:, 1:], metric='euclidean') # ユークリッド距離

行列をカラーマップで可視化する関数を作ります。

import pandas as pd
def visualize_matrix(matrix):
    plt.figure(figsize=(12, 10))
    plt.imshow(matrix, interpolation='nearest', cmap=plt.cm.coolwarm)
    plt.colorbar()
    tick_marks = np.arange(len(matrix))
    plt.xticks(tick_marks, matrix.columns, rotation=90)
    plt.yticks(tick_marks, matrix.columns)
    plt.tight_layout()

得られた距離行列を可視化すると

visualize_matrix(pd.DataFrame(dist_mat, columns=location[:, 0]))

output_8_0.png

この距離行列を、今回の解析では、2つの用途に使います。

  • ひとつは、現在地から、目的地である那覇までの距離の計算。
  • もうひとつは、出発地である札幌から、現在地までの総移動距離の計算。

エージェント

強化学習では、学習する「頭脳」にあたるものを「エージェント」と呼びます。エージェントは、取れる行動の選択肢を把握し、学習を通じて、その行動の判断の重みを調整して行きます。

取れる行動の選択肢シータ

変数シータ(θ, theta)は、そのとき取れる行動の選択肢を表すものとします。グラフ理論で言えば、「辺」(頂点と頂点の接続関係)を表します。計算してみましょう。

import numpy as np
theta_zero = np.full((len(location), len(location)), np.nan)

for w in walk.values:
    i = np.where(location[:, 0] == w[0])[0][0]
    j = np.where(location[:, 0] == w[1])[0][0]
    theta_zero[i, j] = 1
    theta_zero[j, i] = 1

以上のようにすれば、都市 $i$ と隣接している都市の番号は theta_zero[i] で得られます。

取る行動の確率パイ

変数パイ(π, pi)は、そのとき取る行動の確率を表すものとします。グラフ理論で言えば、「辺の重み」を表します。強化学習では、この pi「ポリシー」(方策)と呼び、これを最適化することになります。

まずは初期値として、選択肢が複数ある場合に、その中から1つを等しい確率でランダムに選択する場合を考えます。

def normalize_pi(theta):
    [m, n] = theta.shape
    pi = np.zeros((m, n))
    for i in range(0, m):
        pi[i, :] = theta[i, :] / np.nansum(theta[i, :])

    return np.nan_to_num(pi)

このようにして、 pi の初期値 pi_zero を求めます。

pi_zero = normalize_pi(theta_zero)

pi_zero を可視化しましょう。

visualize_matrix(pd.DataFrame(pi_zero, columns=location[:, 0]))

output_19_0.png

ここで注意すべきは、 pi_zero が対称行列ではないことです。たとえば、札幌から青森に向かう辺と、青森から札幌に向かう辺がありますが、その重みは一緒ではありません。なぜなら、このグラフにおいては、札幌から次の都市へ向かう選択肢は1つしかないのに対し、青森から次の都市へ向かう選択肢は複数あるからです。

「行動」と「状態」について

強化学習では、「行動」と「状態」を区別して取り扱います。ある状態において、ある行動を選択した結果、異なる状態へ遷移します。

この記事で取り扱う例は、「行動」と「状態」を区別しなくても良い特殊な例です。つまり、「状態」は「そのときいる都市」を指し、「行動」は「次に行く都市」を指します。行動と状態を区別しなくても良い分、コードが簡単になります。

ランダムウォーク

まずは、このポリシー pipi_zero のままの状態で日本縦断旅行を試みましょう。ある都市に着いたら、次にどの都市に行くかをランダムに決定するランダムウォークになります。

次の行動を確率的に選ぶ

ポリシー pi が示す確率に従って、次の都市を選ぶ関数を作ります。

def get_next(town, pi):
    return np.random.choice(range(len(pi)), p=pi[town, :])

たとえば、 pi_zero に従って、東京(12番)の次の都市は次のように求められます。

get_next(12, pi_zero)
13

ランダムに決定しているため、当然、実行のたびに結果は変わります。

ランダムウォークによる探索

それでは、ランダムウォークによる探索を行いましょう。

def explore(pi, start=0, goal=46): # スタートは0(札幌)、ゴールは46(那覇)
    route = [start] # 旅行履歴を入れるリスト
    town = start
    while True:
        next_town = get_next(town, pi) # 次の都市を決定する
        if next_town in route: # 同じ都市を二度訪問したら
            break # ゲームオーバー

        route.append(next_town) # 次の都市を履歴に入れる

        if next_town == goal: # ゴールに到着
            break # おめでとうございます
        else: # まだゴールじゃない
            town = next_town # 「次の都市」を「現在位置」とする

    return route

pi_zero に従ってランダムウォークを開始

route = explore(pi_zero)
route
[0, 1, 2, 3, 4, 5]

ランダムなので、もちろん実行のたびに結果が変わります。都市の番号だけだとイメージしにくい場合は、次のようにして都市名に変換したらイメージしやすいでしょう。

[location[i, 0] for i in route]
['Sapporo', 'Aomori', 'Morioka', 'Sendai', 'Akita', 'Yamagata']

この例では、札幌から山形まで歩いて、その次の行き先で間違えて訪問済みの都市を選んでしまったのでゲームオーバーです。

さて、こんなランダムウォークで、果たして札幌から那覇まで辿り着けるでしょうか?

探索結果の評価指標

探索結果の良し悪しを評価する方法を作りましょう。現在地から目的地(那覇)までの直線距離と、出発地(札幌)から現在地までの総移動距離を計算します。

def evaluate(route, goal=46):
    dist_goal = dist_mat[route[-1], goal]
    len_route = sum([dist_mat[route[i], route[i + 1]] for i in range(len(route) - 1)])
    return [dist_goal, len_route]

dist_goal は「現在地から目的地(那覇)までの直線距離」、 len_route は「出発地(札幌)から現在地までの総移動距離」です。

では、実行例です。

route = explore(pi_zero)
[location[i, 0] for i in route], evaluate(route)
(['Sapporo', 'Aomori'], [19.597025248636594, 2.3205099949149073])

この例では、札幌から青森へ行き、青森から札幌へ戻る道を選択してしまってゲームオーバー。 現在地(青森)から目的地(那覇)までの距離は 19.597025248636594 で、出発地(札幌)から現在地(青森)までの距離は 2.3205099949149073 というわけです。

日本縦断旅行の目標として、この dist_goal がゼロになる経路を探し、その中で len_route が最小のものを探すことになります。 len_route が短ければ良いというものではありません。

今から、札幌から那覇まで移動するゲームを何度もトライしますが、ある結果が得られた時に、これまでの中で最も良いものなのかどうかを判定する関数を作りましょう。

best_dist_goal = 1000000 # ありえないほど大きな値を初期値にする
best_len_route = 1000000 # ありえないほど大きな値を初期値にする

def is_best_ever():
    if best_dist_goal >= dist_goal: # ベスト dist_goal 値と同じかそれより小さいなら
        if best_dist_goal > dist_goal: # 過去のベスト dist_goal 値より小さいなら
            return True # これまでの中で最も良い
        elif best_len_route > len_route: # 過去のベスト len_route 値より小さいなら
            return True # これまでの中で最も良い
        else:
            return False # 最良ではない
    else:
        return False # 最良ではない

探索実行

では、準備は出来上がったので、ランダムウォークによる探索を5万回繰り返します。

%%time # 実行時間を計測
pi = pi_zero.copy() # pi の初期化
theta = theta_zero.copy() # theta の初期化

best_dist_goal = 1000000 # ありえないほど大きな値を初期値にする
best_len_route = 1000000 # ありえないほど大きな値を初期値にする

dist_goal_history0 = [] # dist_goal の履歴
len_route_history0 = [] # len_route の履歴
best_route0 = [] # ベスト経路

for itera in range(50000): # 5万回繰り返す
    route = explore(pi) # 探索
    dist_goal, len_route = evaluate(route) # 評価
    dist_goal_history0.append(dist_goal) # 記録
    len_route_history0.append(len_route) # 記録

    if is_best_ever(): # 過去のベストなら
        best_dist_goal = dist_goal # ベスト dist_goal
        best_len_route = len_route # ベスト len_route
        best_route0 = route # ベスト経路
CPU times: user 10 s, sys: 118 ms, total: 10.2 s
Wall time: 11.8 s

探索結果

結果を可視化する関数を作ります。まずは、得られた dist_goal len_route の分布を可視化する関数

def draw_histgrams(dist_goal_history, len_route_history):
    plt.figure(figsize=(max(len_route_history) / 4, max(dist_goal_history) / 4))
    x_max = max(dist_goal_history + len_route_history)
    ax = plt.subplot(2,1,1)
    ax.hist(dist_goal_history, label='dist_goal', bins=20)
    ax.set_xlim([0, x_max])
    ax.grid()
    ax.legend()
    ax = plt.subplot(2,1,2)
    ax.hist(len_route_history, label='len_route', bins=20)
    ax.set_xlim([0, x_max])
    ax.grid()
    ax.legend()
draw_histgrams(dist_goal_history0, len_route_history0)

output_35_0.png

札幌からスタートして那覇へはなかなかたどり着きません。運が良ければたどり着くことはありますが。探索の初期でゲームオーバーになるケースが多いことが分かります。

次に、 dist_goal len_route の関係を見てみましょう。

def draw_scatter(dist_goal_history, len_route_history):
    plt.figure(figsize=(max(len_route_history) / 4, max(dist_goal_history) / 4))
    plt.scatter(len_route_history, dist_goal_history, alpha=0.5)
    plt.ylabel('dist_goal')
    plt.xlabel('len_route')
    plt.ylim([0, max(dist_goal_history) + 1])
    plt.xlim([0, max(len_route_history) + 1])
    plt.grid()
    plt.show()
draw_scatter(dist_goal_history0, len_route_history0)

output_37_0.png

那覇までたどり着けていないこと、そして、ゲームオーバーになった場所が同じでも、そこに至るまでの移動距離には幅がある(いろんな経路を選択している)ことが分かります。

次に、 dist_goal len_route がどのように推移していったか見てみましょう。

def visualize_history(history, interval=50, window=50):
    plt.plot(range(0, len(history), interval),  # 全体の最大値
             [np.array(history)[:i].max() for i in range(len(history)) if (i%interval) == 1], label='max')
    plt.plot(range(window, len(history)+window, interval), 
             [np.array(history)[i:i + window].mean() for i in range(len(history)) if (i%interval) == 1], 
             label='mean(recent)') # 最近 interval 回の平均値
    plt.plot(range(0, len(history), interval), 
             [np.array(history)[:i].mean() for i in range(len(history)) if (i%interval) == 1], label='mean') # 全体の平均値
    plt.plot(range(0, len(history), interval), 
             [np.array(history)[:i].min() for i in range(len(history)) if (i%interval) == 1], label='min') # 全体の最小値
    plt.legend()
    plt.grid()
    plt.show()
visualize_history(dist_goal_history0)

output_39_0.png

dist_goal は、たまに最小値を更新することはありますが、那覇まではたどり着かず。そして、何度探索を繰り返しても、その平均値が改善されることはありません。何も学習していないから当然です。

visualize_history(len_route_history0)

output_40_0.png

同様に len_route も、たまに最大値を更新することはありますが、那覇まではたどり着かず、何度探索を繰り返しても、その平均値が改善されることはありません。何も学習していないから当然です。

では最後に、このランダムウォークで見出したベストな経路を表示してみましょう。

def draw_route(route):
    plt.figure(figsize=(11, 9))
    for i in route:
        plt.text(location[i, 2], location[i, 1], location[i, 0], alpha=0.4, size=12)
    plt.grid()
    plt.plot([location[i][2] for i in route], [location[i][1] for i in route])
    plt.ylabel('latitude')
    plt.xlabel('longitude')
    plt.show()
draw_route(best_route0)

output_42_0.png

めっちゃ頑張ったのに、佐賀までしか辿り着けませんでした。運が良ければ、那覇にたどり着くことはあります。でもそれを学習することはありません。

方策勾配法

それでは、いよいよ 強化学習 の開始です。強化学習の手法は大きく 方策勾配法 (Policy gradient method)価値反復法 (Value iteration method) の2つに分けられるようです。まずは方策勾配法をやりましょう。

ポリシー pi を更新する

得られた経路 route は、「その経路の終点がゴールにどのくらい近いか」という視点で、良し悪しを評価できます。ゴールまでの距離が小さいほど、今後の探索で、その経路上の辺を選ぶ確率が高くなるように、ポリシー pi を更新しましょう。

def update_pi(pi, route, goal=46, eta=0.1): # イータ(η, eta) は学習率
    new_pi = pi.copy() # numpy array をコピー
    for i in range(len(route) - 1): # 経路上の各辺に対して
        town1 = route[i] # 辺iの起点の都市
        town2 = route[i + 1] # 辺iの終点の都市
        new_pi[town1, town2] +=  eta / (dist_mat[route[-1], goal] + 1)
        # 経路の終点がゴールに近ければ近いほど高い得点が得られるようにする

    return normalize_pi(new_pi) # pi を更新する

最後に normalize_pi を用いるのは、行の値の和が 1.0 になるように調整するためです。

探索実行

では、探索を開始しましょう。

%%time
pi = pi_zero.copy()

best_dist_goal = 1000000
best_len_route = 1000000

dist_goal_history1 = []
len_route_history1 = []
best_route1 = []
for itera in range(50000):
    route = explore(pi)
    dist_goal, len_route = evaluate(route)
    dist_goal_history1.append(dist_goal)
    len_route_history1.append(len_route)

    pi = update_pi(pi, route)

    if is_best_ever():
        best_dist_goal = dist_goal
        best_len_route = len_route
        best_route1 = route
CPU times: user 59.9 s, sys: 340 ms, total: 1min
Wall time: 1min 1s

探索結果

以下が結果の一例です。結果は実行のたびに異なり、場合によっては、5万回学習を繰り返しても目的地の那覇にたどり着かないことがあります。

得られた dist_goal len_route の分布は、ランダムウォークによる分布と大きく異なります。目的地の那覇に到着する回数が圧倒的に多くなりました。

draw_histgrams(dist_goal_history1, len_route_history1)

output_46_0.png

dist_goal len_route の関係は次のようになりました。

draw_scatter(dist_goal_history1, len_route_history1)

output_47_0.png

dist_goal の履歴です。ランダムウォークと比べて圧倒的に早い時期に目的地の那覇に到達し、しかも、その後は目的地に簡単に到着できるようになりました。

visualize_history(dist_goal_history1)

output_48_0.png

dist_goal の履歴のうち、最初の5000回です。

visualize_history(dist_goal_history1[:5000])

output_49_0.png

同様に、len_route の履歴です。

visualize_history(len_route_history1)

output_50_0.png

len_route の履歴の最初の5000回です。

visualize_history(len_route_history1[:5000])

output_51_0.png

ベストの経路として出力されたのがこちらです。最短経路に近いものが選ばれているのが分かります。ひとつだけ、惜しい。福岡から佐賀を経由して熊本に行っています。ここは、佐賀を経由せず、福岡から直接熊本に向かうべきでした。

draw_route(best_route1)

output_52_0.png

得られたポリシー pi を、別名pi_pgとして保存しておきましょう。

pi_pg = pi.copy()

そのポリシーpi_pgを可視化

visualize_matrix(pd.DataFrame(pi_pg, columns=location[:, 0]))

output_54_0.png

絶対的に重要な経路は赤くなっています。たとえば、鹿児島に到着したら、次は那覇しかあり得ません。それ以外(熊本、宮崎)を選択しては絶対にダメです。それをよく表しています。

初期値と比べてどう変わったかはこのようにして確認できます。

visualize_matrix(pd.DataFrame(pi_pg - pi_zero, columns=location[:, 0]))

output_55_0.png

重要ではない場所はほとんど変化していません。たとえば、四国地方や関東地方はあまり重要ではないようです。

Epsilon-Greedy法

上記の「方策勾配法」に対して、ここで紹介するEpsilon-Greedy法 と、その次に紹介するQ学習は「価値反復法」(Value Iteration Method)と呼ばれます。

価値反復法では、ポリシーはなぜか「π」ではなく「Q」と呼ばれるようです。piが完全な均等確率からスタートするのに対し、Qは、均等ではないランダムな確率からスタートします。

ポリシーQ

def generate_Q(theta): # 取れる選択肢 theta から、均等ではないランダム確率を生成
    [m, n] = theta.shape
    Q = np.zeros((m, n))
    rand = np.random.rand(m, n)
    for i in range(m):
        for j in range(n):
            if theta[i, j] == 1:
                Q[i, j] = rand[i, j]

    return normalize_pi(Q)

均等ではないポリシーQの初期値Q_zeroを生成します

Q_zero = generate_Q(theta_zero)

生成したQ_zeroを可視化します。

visualize_matrix(pd.DataFrame(Q_zero, columns=location[:, 0]))

output_59_0.png

Q_zeropiの違いはこのようになります。

visualize_matrix(pd.DataFrame(Q_zero - pi_zero, columns=location[:, 0]))

output_60_0.png

次の行動の選び方

方策勾配法では、次の行動get_nextはポリシーpiの示す確率に従いランダムに選択しました。価値反復法では、次の行動get_nextを次のように書き換えます。

def get_next(town, Q, epsilon=0.1):
    if np.random.rand() < epsilon:
        return np.random.choice(range(len(Q)), p=Q[town, :])
    else:
        return np.nanargmax(Q[town, :])

つまり、新たなパラメータepsilonを導入し、ポリシーQの示す確率に従いランダムに行動を選択する場合と、ポリシーQが最大値をとる行動を選択する場合とを生じさせるのです。最初はepsilonは大きめの値を設定します。つまり、ランダムな選択をする機会を多くします。その後、次第にepsilonを小さくし、ランダムな選択をする機会を少なくしていきます。

報酬

「報酬」(reward)という概念も導入し、目的を達成したら(ゴールに着いたら)1点、失敗したら(同じ都市を二度訪問しゲームオーバーになったら)-1点、途中経過は0点とします。

次に示す sarsa という方法では、ある行動(経路上の辺)に対するQ値を更新する際に、その次の行動(経路上の次の辺)のQ値も参照しながら更新します。このとき、時間割引率gammaをかけます。

def sarsa(town, Q, prev_t, next_t, reward, eta=0.1, gamma=0.9, goal=46):
    if reward == 1: #dist_mat[town, goal] == 0:
        Q[prev_t, town] = Q[prev_t, town] + eta * (reward - Q[prev_t, town])
    elif reward >= 0:
        Q[prev_t, town] = Q[prev_t, town] + eta * (reward - Q[prev_t, town] + gamma * Q[town, next_t])
    else:
        Q[prev_t, town] = Q[prev_t, town] - eta * Q[prev_t, town]

    return normalize_pi(Q)

このようにすることで、目的地で得られた報酬が、目的地の手前のほうの行動の選択に影響を及ぼすようにします。ゲームオーバーになったときの「負の報酬」も、そこに到るまでの過去の行動の選択に負の影響を及ぼすようにします。

def explore_epsilon_greedy(Q, epsilon=0.1, eta=0.1, gamma=0.9, start=0, goal=46, min_epsilon=0.01):
    epsilon = max(epsilon, min_epsilon)
    route = [start]
    town = get_next(start, Q, epsilon)
    prev_t = start
    while True:
        if town in route:
            reward = -1
            Q = sarsa(town, Q, prev_t, next_t, reward, eta, gamma)
            break
        elif town == goal:
            reward = 1
            route.append(town)
            Q = sarsa(town, Q, prev_t, next_t, reward, eta, gamma)
            break
        else:
            reward = 0
            route.append(town)
            next_t = get_next(town, Q, epsilon)
            Q = sarsa(town, Q, prev_t, next_t, reward, eta, gamma)
            prev_t = town
            town = next_t

    return [route, Q]

探索実行

では、探索を開始しましょう。

%%time
eta = 0.1 # 学習率
gamma = 0.9 # 時間割引率
epsilon = 0.5
Q = Q_zero.copy()

best_dist_goal = 1000000
best_len_route = 1000000

best_route2 = []
dist_goal_history2 = []
len_route_history2 = []
for itera in range(50000):
    epsilon = epsilon * 0.99
    route, Q = explore_epsilon_greedy(Q, epsilon, eta, gamma)
    dist_goal, len_route = evaluate(route)
    dist_goal_history2.append(dist_goal)
    len_route_history2.append(len_route)

    if is_best_ever():
        best_dist_goal = dist_goal
        best_len_route = len_route
        best_route2 = route
CPU times: user 7min 50s, sys: 948 ms, total: 7min 50s
Wall time: 7min 53s

探索結果

Epsilon-Greedy法による探索結果の一例です。実は、結果はあまり安定せず、実行するたびに結果がけっこう大きく変わります(試してみてください)。

まずはdist_goallen_routeの分布

draw_histgrams(dist_goal_history2, len_route_history2)

output_65_0.png

dist_goallen_routeの関係

draw_scatter(dist_goal_history2, len_route_history2) 

output_66_0.png

dist_goalの履歴

学習率 eta を大きくすると収束が早くなりますが、局所解に陥る可能性が高くなります。小さくすると、収束は遅くなりますが、局所解に陥る可能性は低くなります。

visualize_history(dist_goal_history2)

output_67_0.png

dist_goalの履歴の最初の5000回

visualize_history(dist_goal_history2[:5000])

output_68_0.png

len_routeの履歴

visualize_history(len_route_history2)

output_69_0.png

len_routeの履歴の最初の5000回

visualize_history(len_route_history2[:5000])

output_70_0.png

Epsilon-Greedy法により得られた最短経路

ここでも、真の最短経路とは少し違う、惜しい結果になりました。方策勾配法で得られた結果と比べてみてください。

draw_route(best_route2)

output_71_0.png

方策勾配法では、目的地である那覇までの距離をpi値の更新に用いていたため、「目標1:ゴールにたどり着く」と「目標2:最短経路を発見する」を同時に満たす経路を学習していました。このため、何度実行しても、最短経路に近い結果が得られます。

今回のEpsilon-Greedy法では、与えた「報酬」は「ゴールにたどり着いたかどうか」と「ゲームオーバーになったか(同じ都市を二度訪問してしまったか)」だけを反映しています。そのため、「目標1:ゴールにたどり着く」ための方法は学習しますが、「目標2:最短経路を発見する」ための学習はしません。このため、最短経路に近い結果がたまたま得られることはありますが、最短経路とは程遠い結果に収束することも実は多いのです(試してみてください)。

「目標2:最短経路を発見する」ための学習もEpsilon-Greedy法で行えるようにはどうすればよいか、ぜひ考えてみましょう。

Epsilon-Greedy法で学習後のQ値をQ_egとして保存します。

Q_eg = Q.copy()

その値は次のようになります。

visualize_matrix(pd.DataFrame(Q_eg, columns=location[:, 0]))

output_73_0.png

方策勾配法により得られたpi_pgと比較してみましょう。

Q学習

価値反復法のもうひとつの方法として、「Q学習」(Q-learning)が有名です。基本的には Epsilon-Greedy 法と似ていますが、大きな違いは、「次の行動」を選択するときに生じるランダム性が入らないことです。その分、収束が早くなると言われています。

ですが、私が以下のコードを何度か実行した限りでは、収束が早くなることはありますが必ずそうなるわけでもなく、局所解に陥る(目的地である那覇にたどり着けない)ことが多くなってしまう印象があります。

def Q_learning(town, Q, prev_t, reward, eta=0.1, gamma=0.9, goal=46):
    if reward == 1: #dist_mat[town, goal] == 0:
        Q[prev_t, town] = Q[prev_t, town] + eta * (reward - Q[prev_t, town])
    elif reward >= 0:
        Q[prev_t, town] = Q[prev_t, town] + eta * (reward - Q[prev_t, town] + gamma * np.nanmax(Q[town, :]))
    else:
        Q[prev_t, town] = Q[prev_t, town] - eta * Q[prev_t, town]

    return normalize_pi(Q)
def explore_Q_learning(Q, epsilon=0.1, eta=0.1, gamma=0.9, start=0, goal=46):
    prev_t = start
    route = [start]
    town = get_next(start, Q, epsilon)
    while True:
        if town in route:
            reward = -1
            Q = Q_learning(town, Q, prev_t, reward, eta, gamma)
            break
        elif town == goal:
            reward = 1 
            route.append(town)
            Q = Q_learning(town, Q, prev_t, reward, eta, gamma)
            break
        else:
            reward = 0
            dist_goal, len_route = evaluate(route)
            if best_dist_goal > dist_goal:
                reward = 1
            route.append(town)
            next_t = get_next(town, Q, epsilon)
            Q = Q_learning(town, Q, prev_t, reward, eta, gamma)
            prev_t = town
            town = next_t

    return [route, Q]

探索実行

%%time
eta = 0.1 # 学習率
gamma = 0.9 # 時間割引率
epsilon = 0.5
Q = Q_zero.copy()

best_dist_goal = 1000000
best_len_route = 1000000

best_route3 = []
dist_goal_history3 = []
len_route_history3 = []
for itera in range(50000):
    epsilon = epsilon * 0.99
    route, Q = explore_Q_learning(Q, epsilon, eta, gamma)
    dist_goal, len_route = evaluate(route)
    dist_goal_history3.append(dist_goal)
    len_route_history3.append(len_route)

    if is_best_ever():
        best_dist_goal = dist_goal
        best_len_route = len_route
        best_route3 = route
CPU times: user 9min 50s, sys: 1.41 s, total: 9min 52s
Wall time: 9min 54s

探索結果

Q学習による探索結果の一例です。Epsilon-Greedy法と同様、結果はあまり安定せず、実行するたびに結果がけっこう大きく変わります(試してみてください)。

dist_goallen_route の分布

draw_histgrams(dist_goal_history3, len_route_history3)

output_81_0.png

dist_goallen_route の関係

draw_scatter(dist_goal_history3, len_route_history3) 

output_82_0.png

dist_goalの履歴

visualize_history(dist_goal_history3)

output_83_0.png

dist_goalの履歴の最初の5000回

visualize_history(dist_goal_history3[:5000])

output_84_0.png

len_routeの履歴

visualize_history(len_route_history3)

output_85_0.png

len_routeの履歴の最初の5000回

visualize_history(len_route_history3[:5000])

output_86_0.png

Q学習によりにより得られた最短経路

draw_route(best_route3)

output_87_0.png

この計算例ではっきり分かるように、これは真の最短経路とは異なります。理由は、Epsilon-Greedy法の時に述べたのと同様、「報酬」の設計の問題上、「目標1:ゴールにたどり着く」ための学習は行なっているが、「目標2:最短経路を発見する」ための学習は行なっていないためです。

また上に述べたように、私の観測範囲内において、Q学習は収束が早くなることはあるかも知れませんが、5万回計算しても目的地の那覇にたどり着かないことも多く、「Q学習により得られた最短経路」は「Epsilon-Greedy法により得られた最短経路」より長くなってしまう傾向があるように思います。理由は、「次の行動」を選択するときのランダム性を抑えてあるため、たまたま選んだ経路でゴールまでたどり着いた場合、その経路が長いものであっても、それが変更される機会がEpsilon-Greedy法と比べて少なくなってしまうからではないかと思います。

Q_qlearn = Q.copy()
visualize_matrix(pd.DataFrame(Q_qlearn, columns=location[:, 0]))

output_89_0.png

と、いうわけで。強化学習については以上です。いやあ、沖縄は遠い。

これに深層学習をミックスした深層強化学習というものがありますが、それはまた別の機会に。チャオ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む