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

pythonによるwavファイル読み込み

PythonのWaveモジュールを使ってwavファイルを編集するの記事の以下のコードをコピペしたら、エラーが出てしまいました。

pywave.py
import wave
import struct
from scipy import fromstring, int16

# ファイルを読み出し
wavf = '/data/input/test.wav'
wr = wave.open(wavf, 'r')

# waveファイルが持つ性質を取得
ch = wr.getnchannels()
width = wr.getsampwidth()
fr = wr.getframerate()
fn = wr.getnframes()

print("Channel: ", ch)
print("Sample width: ", width)
print("Frame Rate: ", fr)
print("Frame num: ", fn)
print("Params: ", wr.getparams())
print("Total time: ", 1.0 * fn / fr)

# waveの実データを取得し、数値化
data = wr.readframes(wr.getnframes())
wr.close()
X = fromstring(data, dtype=int16)

threshold.py:25: DeprecationWarning: The binary mode of fromstring is deprecated, as it behaves surprisingly on unicode inputs. Use frombuffer instead
  X = fromstring(data, dtype=int16)

fromstringはunicode入力のときに何かすごい挙動するからやめて、みたいな事のようです。そして、frombufferを使え、と。
調べてみると、frombufferはnumpyのメソッド(?)のようなので、importにnumpyを入れて、fromstringはnp.frombufferにします。

pywave.py
import numpy as np
import wave
import struct
from scipy import fromstring, int16

# ファイルを読み出し
wavf = 'test.wav'
wr = wave.open(wavf, 'r')

# waveファイルが持つ性質を取得
ch = wr.getnchannels()
width = wr.getsampwidth()
fr = wr.getframerate()
fn = wr.getnframes()

print("Channel: ", ch)
print("Sample width: ", width)
print("Frame Rate: ", fr)
print("Frame num: ", fn)
print("Params: ", wr.getparams())
print("Total time: ", 1.0 * fn / fr)

# waveの実データを取得し、数値化
data = wr.readframes(wr.getnframes())
wr.close()
X = np.frombuffer(data, dtype=int16)

これで無事動きました。

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

初心者が機械学習初心者に向けてアヤメ分類を一から説明してみた

こんにちは。ひろちょんです。

機械学習を始めたばかりの初心者が、様々な記事を参考にしつつアヤメ分類をしてみます。
説明を加えながら書いていくので、是非参考にしてみてください!

↓記事を読む前に筆者について知りたい方へ↓
>>>詳しいプロフィールはコチラ

~目次です~
1. 対象読者とか書いてみる
2. 《Google colaboratory》で進めていきます
3. 色々とimportしていく
  ・オートパイロット状態でimportする奴
  ・scikit-learnの様々な機能をimport!!
4. アヤメのデータセットをじっくり見ていく
  ・インスタンスを生成
  ・アヤメデータはどうなってるの?
5. Pandasを用いてデータを分かりやすくする
  ・配列をDataFrameに変換しよう!
  ・実際にDataFrameを見ていこう!
6. データセットを分割する
  ・説明変数と目的変数とは?
  ・学習用とテスト用のデータはどうするの?
  ・train_test_split関数を使う!
7. データをmatplotlibで可視化する
  ・データを可視化する意味は?《特徴量選択》
  ・散布図をplt.scatterで描いていく!
8. 機械学習アルゴリズムを使っていこう!
  ・特徴量を選択しよう!
  ・モデルを構築⇒学習⇒予測させる
  ・モデルが予想したデータの答え合わせ
  ・2つの結果の違いについて詳しく見る

1. 対象読者とか書いてみる

今回は機械学習アヤメの分類を行っていくことが主題なので、レベルとしてはこんな感じ。

  • 何かしらのNotebook形式でプログラムを実行できる人
  • Python初心者以上
  • 機械学習の流れが何となくわかる人

2. Google Colaboratoryで進めていきます

今回はバージョンとか、仮想環境とか…諸々のしがらみに囚われたくなかったので、Google Colaboratoryにてアヤメ分類をしていきたいと思います。

始め方はすごく簡単で、Google Colaboratoryのサイト左上の《ファイル》から《Python3の新しいノートブック》をクリックしてください。

すると新しいノートブックが生成されるので、左上のUntitled0.ipynbから名前を変更してあげてください!

※僕は名前を『初めてのアヤメ分類』にしておきました。笑

初学者の方はページを見つつ、一から作成することをオススメしますが、一応完成形も↓のGitHubにてノートブックを公開しています。
>>>GitHubへはコチラから

3. 色々importしていく

オートパイロット状態でimportする奴

とりあえず以下をインポートしていきますね。(矢印先は僕のライブラリへの感想です。)

  • Numpy ⇒ 計算機能凄めライブラリ
  • Pandas ⇒ 表を扱いやすいライブラリ
  • Matplotlib ⇒ グラフ可視化ライブラリ
  • warnings ⇒ たまに邪魔な警告を消すライブラリ

そしてプログラム化するとこんな感じ。

importするライブラリ.ipynb
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import warnings
warnings.filterwarnings('ignore')

scikit-learnの色々な機能をimport!!

機械学習ライブラリのscikit-learnには分類を行う以外にも様々な機能が存在しています。今回使用するものをサラっと紹介しますね。(後で深堀りしていきます)

1つ目はscikit-learnに用意されているデータセットです。
scikit-learnのデータセット集一つにアヤメのデータセットがあります。
>>>詳しくはコチラ(UCI Machine Learning Repository: Iris Data Set)

2つ目が学習用とテスト用にデータセットを分ける機能です。
分ける理由としては、《学習用のデータで予測するモデルを作成する》⇒《テスト用データで作成したモデルは良い物か判定する》という機械学習では鉄板の2つの処理を行いたいからです。

3つ目が分類を行うアルゴリズムになります。機械学習には様々な分類のアルゴリズムが存在して、以下のプログラムではアルゴリズムとして『線形のSVM』を用いるので、LinearSVCをインポートしています。
>>>SVM(サポートベクターマシーン)について詳しく知りたい方はコチラ(Qiita)

では上記で紹介した3つをimportしていきます。

scikit-learnの様々な機能をimport.ipynb
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.svm import LinearSVC

4. アヤメのデータセットをじっくり見ていく

まず今回の目的をハッキリさせておきましょう!

どのサイトでも分類の部分を重要視しすぎて、結局何がしたかったのか見失いがちです。

今回の目的は…

アヤメのがく片や花びら幅や長さの数値』を用いて『アヤメ属(花)の種類を分類する』こと

そこで既にアヤメの種類によっての幅や長さのデータを数値を集めてくれているデータセット(先ほどimportした奴)を使って、機械学習により予測していきます。

ayameda.jpg

フィッシャーのアヤメのデータから散布図行列を描く(https://teenaka.at.webry.info/201803/article_12.html )から引用

ちなみに上の図がアヤメになります。この花の長さや幅を測定して数値化したデータセットをimportしました(/・ω・)/

インスタンスを生成

今はアヤメのデータセット機能をインポートしただけなので、まずインスタンスを生成していきます。

アヤメインスタンスを生成する.ipynb
heacet = load_iris()

インスタンスの名前に特に深い意味はないです。単に自サイトの宣伝をしているだけです!笑

もし嫌だという方はコピーするだけではなく、自分で書いていくと理解も深くなるので、ここのインスタンス名を変えて実行してみてください!

アヤメデータはどうなってるの?

さっそくデータセットの中身を見ていきましょう。
↓コチラを実行してください↓

アヤメデータの中身を確認する.ipynb
print("与えられたデータ")
print(heacet.data)
print(heacet.data.shape)
print("-----------------")
print("予測するデータ")
print(heacet.target)
print(heacet.target.shape)
print(heacet.target_names)

実行して出力を見ると、どうやら《数値が書かれた150×4の2次元配列》と《0,1,2と書かれた150×1の1次元配列》が得られたみたいですね。

また0,1,2はそれぞれ『setosa』,『versicolor』,『virginica』対応していることも分かります。

よって与えられた《数値が書かれた150×4の2次元配列》を《データセットを学習用と予想用に振り分け》て、《どれが0,1,2に対応するのか》を《機械学習アルゴリズム》に通して、《予想して》いく流れが見えてきます。

さて、これらのデータをpandasを用いて見やすくしていきましょう(/・ω・)/

5. Pandasを用いてデータを分かりやすくしよう!

今のままでは配列が出力されただけで、何のデータなのかよくわからない状況になっています。

そんな時に表を列ごとに名前を付けて見やすくできたり、平均値や標準偏差などを自動で出してくれるというPandasライブラリを使っていきます。

配列をDataFrameに変換しよう!

今からやっていくことはこんな感じ↓

  1. DataFrameの第一引数にデータセット、第二引数にカラムの名前を与える。
  2. DataFrameの第一引数に目的変数、第二引数にカラムの名前を与える。
  3. 1と2のDataFrameを横に結合したDataFrameを作る

↓なのでプログラムはこんな感じになります↓

Pandasの定義する.ipynb
heacet_data = pd.DataFrame(heacet.data, columns=["がく片の長さ","がく片の幅","花びらの長さ","花びらの幅"])
heacet_target = pd.DataFrame(heacet.target, columns=["花の種類"])
heacet_all = pd.concat([heacet_data,heacet_target], axis=1)

恐らく1行目と2行目のプログラムは大体何をしているか分かると思いますが、3行目に『concatメソッド』を用いて、引数に『1行目のデータと2行目のデータを選択』して、『axis=1』と定義しています。

axis=1ってなに??

『axis』は恐らくよく見かけているのではないでしょうか。今後も使っていくと思うので、しっかりと確認しておきましょう!

【 axisとは軸を指定する引数 】

図で簡単に表すとこんな感じ↓
gyoretsus.jpg

axis=0だと縦の行を示して、axis=1だと横の列を示していることになります。

そこで今回は、DataFrameであるheacet_dataのcolumnsに《花の種類》というcolumnを横に追加したかったので、axis=1と指定したわけですね。

実際にDataFrameを見ていこう!

さてさて、PandasのDataFrameを使ったことで見やすくなったはずなので、見にいきましょう~

headメソッドを使って最初の10行を見てみます。

DataFrameyatsu.JPG

とてもまとまっていて、2次元配列を単に出力した時とは見やすさが大違いですね。笑

またPandasでは平均値などを出してくれるdescribeメソッドもあるので、是非実行してみてください!
describe.JPG

6. データセットを分割する

そもそも論なんですが、予測するデータはすでに答えが存在してしまっているので、自分で学習させるデータと予測するデータを創り出さなければなりません。

冒頭で訓練用とテスト用に分ける機能をインポートしましたよね。やっとここで使います。
ですが少し待ってください。ある用語を説明していなかったので、ここで挟みます。

説明変数と目的変数とは?

ゴリゴリペイントで書いた感のある図で説明していきます!
predictsitame.jpg

上の図のように今まで《がく片の長さ、がく片の幅、花びらの長さ、花びらの幅》といったカラムを付けていた部分を説明変数、《花びらの種類》とカラムを付けていた部分を目的変数と呼びます。
これから多用していくので、是非覚えておいてください!!

そして単純に説明変数目的変数を使って学習をさせていきたいのですが、まだ学習用とテスト用のデータを分割していませんでした!

え、そもそも学習用とテスト用のデータってなに?

tr_te_map.jpg

↑上図のように学習用データを使って学習させてモデルを作成して、テスト用データを使ってモデルが正確に動作しているか確かめるという流れが機械学習にはあります。

この一連の流れをこなす為に《学習用データ》と《テスト用データ》が必要となってきます。

separate.jpg

実はこの『学習用とテスト用(詳しくはvalidation用)のデータを分ける』という段階は深堀りすれば、Kaggler達で様々な議論が行われているような分野みたいです…

この記事はそんな話題には触れず、サラッと進めていきます。笑

てか『データなんてPythonでシンプルに分ければ良いじゃないか!』という話なのですが、先ほどの《花の種類》の目的変数を見て頂けたら分かる通り、0⇒1⇒2の順番で目的変数が並んでいます

ここでもしスライスを使ってデータを半分に分けたりすると、上部分の目的変数が0と1のデータしかモデルは識別できないので、目的変数が2であるデータに対して有効的なモデルを作ることができません

ペイント間満載の図を使っていくと…
modelkun.jpg
↑上では『0のデータ』と『1のデータ』しか学習していません。

↓結果的にこのような事が起こります。
shikibetsu.jpg

図にしてしまえば単純な話ですね!

そこで、
『データを学習用とテスト用に分ける且つシャッフルしてくれるような機能があれば良いなぁ~』
なんて思う訳です。

『あります。train_test_split関数があります!!!』

散々言葉の定義について書いてきましたが、やっとプログラムを書いていきます。笑
ここで冒頭にてimportしたtrain_test_split関数を使います。

train_test_split関数を使う

今回は学習用の説明変数と目的変数、テスト用の説明変数と目的変数を以下のように定義します。

  • 学習用の説明変数 ⇒ setsumei_train
  • 学習用の目的変数 ⇒ mokuteki_train
  • テスト用の説明変数 ⇒ setsumei_test
  • テスト用の目的変数 ⇒ mokuteki_test

これらをプログラム化するとこうです↓

学習用とテスト用の説明変数と目的変数を定義する.ipynb
setsumei_train,setsumei_test,mokuteki_train,mokuteki_test = train_test_split(heacet_data, heacet_target, test_size=0.33)

引数としてtest_sizeというものがあります。これはデータセットをどれだけテスト用に使うかを割合で設定する引数です。

今回は『データセット50個分 = 全体の1/3 = 0.33』をテスト用に使うので、引数を『0.33』としています。

しっかりと分けれているか見ていきます。僕はこうなりました↓
headyatsu.JPG
headyatsu2.JPG
describeyatsu.JPG

良い感じにシャッフルされて、100個50個で分かれていますね。またID番号も同じなので良い感じです( *´艸`)

7. データをmatplotlibで可視化する

次は効果的な特徴量を見つけるために、matplotlibでデータを可視化していきます。

データを可視化する意味は?《特徴量選択》

特徴量とは説明変数のことを指していて、『特徴量 ≠ 説明変数』であることは覚えておいて欲しいのですが、今は同じものとしておきます。

さてデータを可視化することで、効果的な特徴量選択が行えるということを例えを出して説明していきます。

グラフ上にプロットしているの点についてそれぞれで分類したいとします。

このとき↓下の2つの図ではどちらの方が正しく分類できるでしょうか?
graphdayo.jpg
↑これが1つ目の図

graphda.jpg
↑これが2つ目の図

どう見ても2つ目の図のほうが正しく分類できそうですよね。実際にそうです。笑

少し極端な例でしたが、上下の図のように選択する特徴量によって、分類のしやすさが変化することがあります。

今回のアヤメの分類で言えば、《がく片の長さ》《がく片の幅》《花びらの長さ》《花びらの幅》で2つ特徴量を選ぶとすれば、組み合わせによって分類のしやすさが変わってくるということです。

散布図をplt.scatterで描いていく!

matplotlibのscatterメソッドは引数として以下を指定します。

  1. 横軸にしたいデータ
  2. 縦軸にしたいデータ
  3. label=凡例
  4. cmap=カラーマップの種類

カラーマップを用いて色分けしていくのですが、様々な色分けの方法が存在します。
>>>指定できるカラーマップの一覧はコチラ(matplotlib公式リファレンス)

早速matplotlibを使ったプログラムを書いていきたいのですが、花の種類によって視覚的に分類の正確さを測るために目的変数別に色を変えていきます。

目的変数をフィルタリングする方法が僕は少し躓きました。詳しい方がいたらコメントを頂けたら嬉しいですm(__)m

縦軸と横軸データの選択時に上下どちらの引数を使っても大丈夫だったので、実際に左右で使っています。

  • setsumei_train[(mokuteki_train == 0).values]["がく片の長さ"]
  • setsumei_train[mokuteki_train["花の種類"] == 0]["がく片の長さ"]

がく片の長さと幅を使って、プロットしていきます。

pltでプロットする(がく片).ipynb
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==0]["がく片の長さ"],setsumei_train[(mokuteki_train == 0).values]["がく片の幅"],label="setosa",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==1]["がく片の長さ"],setsumei_train[(mokuteki_train == 1).values]["がく片の幅"],label="versicolor",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==2]["がく片の長さ"],setsumei_train[(mokuteki_train == 2).values]["がく片の幅"],label="virginica",cmap="rgb")

## X軸の範囲を指定
plt.xlim(3,9)
## Y軸の範囲を指定
plt.ylim(1,5)

## X軸の名前
plt.xlabel("Length of sepal")
## Y軸の名前
plt.ylabel("Width of sepal")

## グラフのタイトル
plt.title("Relation between length and width of sepal")
## 凡例を出力
plt.legend()

先ほど上下どちらでも良いと言いましたが、目的変数をフィルタリングする方法が一つは『Numpyで2次元でブールインデックス参照をしている?』のと、『Seriesのブール値をDataFrameに入れることによって、フィルタリングしている?』方法があります。
この考えが正しいのか証明できる文献がイマイチ探せなかったので、詳しい方がいたらコメントを頂けたら嬉しいですm(__)m

一応その結果は残しておきます。

zikken.py
>>> mokuteki_train == 0
## ブール値でDataFrameが返ってくる
>>> mokuteki_train["花の種類"] == 0
## ブール値でSeriesが返ってくる
>>> (mokuteki_train == 0).values
## np行列でブール値の2次元配列が返ってくる
>>> (mokuteki_train["花の種類"] == 0).values
## np行列でブール値の1次元配列が返ってくる

# この時setsumei_train[mokuteki_train["花の種類"]==0]と
# setsumei_train[(mokuteki_train == 0).values]が全く同じ値を返します。

また花びらの長さと幅でプロットする例も示しておきます。

pltでプロットする(花びら).ipynb
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==0]["花びらの長さ"],setsumei_train[(mokuteki_train == 0).values]["花びらの幅"],label="setosa",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==1]["花びらの長さ"],setsumei_train[(mokuteki_train == 1).values]["花びらの幅"],label="versicolor",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==2]["花びらの長さ"],setsumei_train[(mokuteki_train == 2).values]["花びらの幅"],label="virginica",cmap="rgb")

plt.xlim(0,8)
plt.ylim(0,4)

plt.xlabel("Length of petal")
plt.ylabel("Width of petal")

plt.title("Relation between length and width of petal")
plt.legend()
そして↓下図ががく片の長さと幅を使ってプロットした結果

plt1.JPG

これは分類しにくそうですね。人間である僕からしても、どこに区別する線を引いたら良いのかわかりません。笑

そして↓下図が花びらの長さと幅を使ってプロットした結果

plt2.JPG

これはそれぞれ目的変数の集合ができているので、簡単に分類する境界線が引けそうですね!
このように選択する特徴量によって、分類のしやすさが変わってくることが視覚的に分かりました。

8. 機械学習アルゴリズムを使っていこう!

ついに機械学習のアルゴリズムに触れることができます。

といってもLinearSVCを用いることを決めてしまっているので、もうそこまでやることはないです。

ハイパーパラメータを決める』といった段階もありますが、今回は『機械学習の一連の流れをつかむ』ことを重きに置いているので触れないでおきます。気になる方は下のリンクで参考文献を貼っておきます。
>>>LinearSVCのハイパーパラメータの詳しい説明はコチラ

特徴量を選択しよう!

では先ほどMatplotlibを使って可視化させた2つのパターン(がく片コンビと花びらコンビ)の特徴量を選択していきます。
新たに2つのDataFrameとして定義すると、《名前による参照メソッドloc》を使って、プログラムは以下になる。

2つの特徴量を作成する.ipynb
## がく片コンビのDataFrameを作成する。
gakuhen_train = setsumei_train.loc[:,["がく片の長さ","がく片の幅"]]

## 花びらコンビのDataFrameを作成する。
hanabira_train = setsumei_train.loc[:,["花びらの長さ","花びらの幅"]]

LinearSVCでモデルを構築⇒学習⇒予測させる

やっとモデル構築までたどり着きました。笑

もう一度ここ付近の話を説明した図を引っ張ってきますと、
kokoyatteta.jpg

今までは『与えられたデータセットがどうなっているか見たり』、『学習用とテスト用でデータを分けたり』と上図での上の方をずっとやっていました。

ですがこれが『機械学習という分野の一種の特徴』だそうで、機械学習は『前処理に時間がとられる』と聞いたことありませんか??

まさにそれを具現化しちゃいましたね。記事の大半を占めてしまっています。

そして次にやる機械学習アルゴリズムを実装させる部分は『機械学習アルゴリズムを理解するのは難しい』けれど、『実装させるのは超簡単』という分野で、すぐに終わります。

kokoyaruyo.jpg

よって冒頭で定義したLinearSVCを使っていきます。プログラムはこんな感じ↓

LinearSVCでモデル構築⇒学習⇒予測する.ipynb
## それぞれモデルを構築
## それぞれモデルを構築
gakuhen_model = LinearSVC()
hanabira_model = LinearSVC()

## それぞれのモデルに学習させる
gakuhen_model.fit(gakuhen_train,mokuteki_train)
hanabira_model.fit(hanabira_train,mokuteki_train)

## それぞれのモデルで予測させて、予測値を代入させる
### モデルが《がく片の長さと幅》を使って学習しているので、予測する時も《がく片の長さと幅》を渡す必要がある。
gakuhen_predict = gakuhen_model.predict(setsumei_test.loc[:,["がく片の長さ","がく片の幅"]])
### モデルが《花びらの長さと幅》を使って学習しているので、予測する時も《花びらの長さと幅》を渡す必要がある。
hanabira_predict = hanabira_model.predict(setsumei_test.loc[:,["花びらの長さ","花びらの幅"]])

ついに答え合わせです!ここで上で立てた仮説を検証できますね。

Matplotlibで可視化させた図では『《がく片の幅と長さ》は正確に分類できなさそう』でした!
>>>がく片の長さと幅を可視化したグラフはコチラ

逆に『《花びらの幅と長さ》は《がく片の幅と長さ》よりかは正確に分類できそう』でしたね!
>>>花びらの長さと幅を可視化したグラフはコチラ

モデルが予想したデータの答え合わせ

では答え合わせできるプログラムをインポートして、実行していきます。

accuracy_scoreを使う.ipynb
## sklearnライブラリからscore算出の関数をimport
from sklearn.metrics import accuracy_score

## gakuhen_scoreとhanabira_scoreにそれぞれに結果を代入
gakuhen_score = accuracy_score(mokuteki_test, gakuhen_predict)
hanabira_score = accuracy_score(mokuteki_test, hanabira_predict)

print('がく片の長さと幅コンビの正解率:{}'.format(gakuhen_score),'花びらの長さと幅コンビの正解率:{}'.format(hanabira_score), sep='\n')

↓僕は出力結果として以下が得られました!↓
syuturyokukekka.JPG

とりあえずどちらも正答率が8割を超えているので、機械学習によってアヤメの種類を分類することはひとまず成功しましたね!

ではでは…

結果について考察していきます。

2つの結果の違いについて詳しく見る

人間の目で見ても《がく片の長さと幅》より《花びらの長さと幅》の方が正確に分類しやすそうでしたが、LinearSVCアルゴリズムにとっても同じように《花びらの長さと幅》の方が正確に分類できるようです。笑

ですが『LinearSVCアルゴリズムにとっても同じように《花びらの長さと幅》の方が正確に分類できる』というのは僕の推論でしかないのです。

実際に確かめるには、どこで境界を作っているのかをMatplotlibを使って可視化していくと良いですよね!!

がく片の長さと幅》と《花びらの長さと幅》でそれぞれ境界線を見たいので、代入できる関数として定義していきます。

分類の境界を可視化する.ipynb
def heacet_border_check(H, M, model, param1, param2, resolution=0.01):
    H1_min, H1_max = H[param1].min()-0.5, H[param1].max()+0.5
    H2_min, H2_max = H[param2].min()-0.5, H[param2].max()+0.5
    H1, H2 = np.meshgrid(np.arange(H1_min, H1_max, resolution),
                           np.arange(H2_min, H2_max, resolution))
    n = np.array([H1.ravel(), H2.ravel()]).T
    Z = model.predict(n)
    Z = Z.reshape(H1.shape)
    plt.contourf(H1, H2, Z, alpha=0.5, cmap="Set2")
    plt.xlim(H1_min, H1_max)
    plt.ylim(H2_min, H2_max)
    plt.xlabel("Length")
    plt.ylabel("Width")
    plt.scatter(H[param1],H[param2], c=M["花の種類"], cmap="brg")

関数が用意できたので、コチラを使って出力すると…
kekka2.JPG
↑まずは上図が《がく片の長さと幅》を使った時の境界図です。
青丸は上手く境界を持てているようですが、赤丸緑丸ごちゃごちゃしていて、微妙なところに境界線が引かれていますね。

そりゃあスコア低くなりますよね…という感じ。

kekka3.JPG
↑続いて上図が《花びらの長さと幅》を使った時の境界図です。
青丸は完璧ですね。赤丸緑丸同士がほんの少し境界を越えているくらいで、ほぼ綺麗に境界線を引けていると思います。

がく片花びらとで場合分けしてきましたが、『データの可視化によって特徴を選択することはとても重要なこと』は伝わったのではないでしょうか?

まとめ

最後まで見て頂きありがとうございました。

普段から技術記事は書いていないもので、少し口語が多かったかもしれません…笑

色々と調べながら初心者だからこその目線で《アヤメ分類》について一から説明してみました。

文書が変だったり、間違っている点などございましたら気軽にコメント頂けると嬉しいです。
もちろん感想やよかった点などでも気軽にコメントください(´艸`*)

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

[機械学習]初心者に向けてアヤメ分類を一から解説してみた

こんにちは。ひろちょんです。

機械学習を始めたばかりの初心者が、様々な記事を参考にしつつアヤメ分類をしてみます。
説明を加えながら書いていくので、是非参考にしてみてください!

↓記事を読む前に筆者について知りたい方へ↓
>>>詳しいプロフィールはコチラ

~目次です~
1. 対象読者とか書いてみる
2. 《Google colaboratory》で進めていきます
3. 色々とimportしていく
  ・オートパイロット状態でimportする奴
  ・scikit-learnの様々な機能をimport!!
4. アヤメのデータセットをじっくり見ていく
  ・インスタンスを生成
  ・アヤメデータはどうなってるの?
5. Pandasを用いてデータを分かりやすくする
  ・配列をDataFrameに変換しよう!
  ・実際にDataFrameを見ていこう!
6. データセットを分割する
  ・説明変数と目的変数とは?
  ・学習用とテスト用のデータはどうするの?
  ・train_test_split関数を使う!
7. データをmatplotlibで可視化する
  ・データを可視化する意味は?《特徴量選択》
  ・散布図をplt.scatterで描いていく!
8. 機械学習アルゴリズムを使っていこう!
  ・特徴量を選択しよう!
  ・モデルを構築⇒学習⇒予測させる
  ・モデルが予想したデータの答え合わせ
  ・2つの結果の違いについて詳しく見る

1. 対象読者とか書いてみる

今回は機械学習アヤメの分類を行っていくことが主題なので、レベルとしてはこんな感じ。

  • 何かしらのNotebook形式でプログラムを実行できる人
  • Python初心者以上
  • 機械学習の流れが何となくわかる人

2. Google Colaboratoryで進めていきます

今回はバージョンとか、仮想環境とか…諸々のしがらみに囚われたくなかったので、Google Colaboratoryにてアヤメ分類をしていきたいと思います。

始め方はすごく簡単で、Google Colaboratoryのサイト左上の《ファイル》から《Python3の新しいノートブック》をクリックしてください。

すると新しいノートブックが生成されるので、左上のUntitled0.ipynbから名前を変更してあげてください!

※僕は名前を『初めてのアヤメ分類』にしておきました。笑

初学者の方はページを見つつ、一から作成することをオススメしますが、一応完成形も↓のGitHubにてノートブックを公開しています。
>>>GitHubへはコチラから

3. 色々importしていく

オートパイロット状態でimportする奴

とりあえず以下をインポートしていきますね。(矢印先は僕のライブラリへの感想です。)

  • Numpy ⇒ 計算機能凄めライブラリ
  • Pandas ⇒ 表を扱いやすいライブラリ
  • Matplotlib ⇒ グラフ可視化ライブラリ
  • warnings ⇒ たまに邪魔な警告を消すライブラリ

そしてプログラム化するとこんな感じ。

importするライブラリ.ipynb
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import warnings
warnings.filterwarnings('ignore')

scikit-learnの色々な機能をimport!!

機械学習ライブラリのscikit-learnには分類を行う以外にも様々な機能が存在しています。今回使用するものをサラっと紹介しますね。(後で深堀りしていきます)

1つ目はscikit-learnに用意されているデータセットです。
scikit-learnのデータセット集一つにアヤメのデータセットがあります。
>>>詳しくはコチラ(UCI Machine Learning Repository: Iris Data Set)

2つ目が学習用とテスト用にデータセットを分ける機能です。
分ける理由としては、《学習用のデータで予測するモデルを作成する》⇒《テスト用データで作成したモデルは良い物か判定する》という機械学習では鉄板の2つの処理を行いたいからです。

3つ目が分類を行うアルゴリズムになります。機械学習には様々な分類のアルゴリズムが存在して、以下のプログラムではアルゴリズムとして『線形のSVM』を用いるので、LinearSVCをインポートしています。
>>>SVM(サポートベクターマシーン)について詳しく知りたい方はコチラ(Qiita)

では上記で紹介した3つをimportしていきます。

scikit-learnの様々な機能をimport.ipynb
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.svm import LinearSVC

4. アヤメのデータセットをじっくり見ていく

まず今回の目的をハッキリさせておきましょう!

どのサイトでも分類の部分を重要視しすぎて、結局何がしたかったのか見失いがちです。

今回の目的は…

アヤメのがく片や花びら幅や長さの数値』を用いて『アヤメ属(花)の種類を分類する』こと

そこで既にアヤメの種類によっての幅や長さのデータを数値を集めてくれているデータセット(先ほどimportした奴)を使って、機械学習により予測していきます。

ayameda.jpg

フィッシャーのアヤメのデータから散布図行列を描く(https://teenaka.at.webry.info/201803/article_12.html )から引用

ちなみに上の図がアヤメになります。この花の長さや幅を測定して数値化したデータセットをimportしました(/・ω・)/

インスタンスを生成

今はアヤメのデータセット機能をインポートしただけなので、まずインスタンスを生成していきます。

アヤメインスタンスを生成する.ipynb
heacet = load_iris()

インスタンスの名前に特に深い意味はないです。単に自サイトの宣伝をしているだけです!笑

もし嫌だという方はコピーするだけではなく、自分で書いていくと理解も深くなるので、ここのインスタンス名を変えて実行してみてください!

アヤメデータはどうなってるの?

さっそくデータセットの中身を見ていきましょう。
↓コチラを実行してください↓

アヤメデータの中身を確認する.ipynb
print("与えられたデータ")
print(heacet.data)
print(heacet.data.shape)
print("-----------------")
print("予測するデータ")
print(heacet.target)
print(heacet.target.shape)
print(heacet.target_names)

実行して出力を見ると、どうやら《数値が書かれた150×4の2次元配列》と《0,1,2と書かれた150×1の1次元配列》が得られたみたいですね。

また0,1,2はそれぞれ『setosa』,『versicolor』,『virginica』対応していることも分かります。

よって与えられた《数値が書かれた150×4の2次元配列》を《データセットを学習用と予想用に振り分け》て、《どれが0,1,2に対応するのか》を《機械学習アルゴリズム》に通して、《予想して》いく流れが見えてきます。

さて、これらのデータをpandasを用いて見やすくしていきましょう(/・ω・)/

5. Pandasを用いてデータを分かりやすくしよう!

今のままでは配列が出力されただけで、何のデータなのかよくわからない状況になっています。

そんな時に表を列ごとに名前を付けて見やすくできたり、平均値や標準偏差などを自動で出してくれるというPandasライブラリを使っていきます。

配列をDataFrameに変換しよう!

今からやっていくことはこんな感じ↓

  1. DataFrameの第一引数にデータセット、第二引数にカラムの名前を与える。
  2. DataFrameの第一引数に目的変数、第二引数にカラムの名前を与える。
  3. 1と2のDataFrameを横に結合したDataFrameを作る

↓なのでプログラムはこんな感じになります↓

Pandasの定義する.ipynb
heacet_data = pd.DataFrame(heacet.data, columns=["がく片の長さ","がく片の幅","花びらの長さ","花びらの幅"])
heacet_target = pd.DataFrame(heacet.target, columns=["花の種類"])
heacet_all = pd.concat([heacet_data,heacet_target], axis=1)

恐らく1行目と2行目のプログラムは大体何をしているか分かると思いますが、3行目に『concatメソッド』を用いて、引数に『1行目のデータと2行目のデータを選択』して、『axis=1』と定義しています。

axis=1ってなに??

『axis』は恐らくよく見かけているのではないでしょうか。今後も使っていくと思うので、しっかりと確認しておきましょう!

【 axisとは軸を指定する引数 】

図で簡単に表すとこんな感じ↓
gyoretsus.jpg

axis=0だと縦の行を示して、axis=1だと横の列を示していることになります。

そこで今回は、DataFrameであるheacet_dataのcolumnsに《花の種類》というcolumnを横に追加したかったので、axis=1と指定したわけですね。

実際にDataFrameを見ていこう!

さてさて、PandasのDataFrameを使ったことで見やすくなったはずなので、見にいきましょう~

headメソッドを使って最初の10行を見てみます。

DataFrameyatsu.JPG

とてもまとまっていて、2次元配列を単に出力した時とは見やすさが大違いですね。笑

またPandasでは平均値などを出してくれるdescribeメソッドもあるので、是非実行してみてください!
describe.JPG

6. データセットを分割する

そもそも論なんですが、予測するデータはすでに答えが存在してしまっているので、自分で学習させるデータと予測するデータを創り出さなければなりません。

冒頭で訓練用とテスト用に分ける機能をインポートしましたよね。やっとここで使います。
ですが少し待ってください。ある用語を説明していなかったので、ここで挟みます。

説明変数と目的変数とは?

ゴリゴリペイントで書いた感のある図で説明していきます!
predictsitame.jpg

上の図のように今まで《がく片の長さ、がく片の幅、花びらの長さ、花びらの幅》といったカラムを付けていた部分を説明変数、《花びらの種類》とカラムを付けていた部分を目的変数と呼びます。
これから多用していくので、是非覚えておいてください!!

そして単純に説明変数目的変数を使って学習をさせていきたいのですが、まだ学習用とテスト用のデータを分割していませんでした!

え、そもそも学習用とテスト用のデータってなに?

tr_te_map.jpg

↑上図のように学習用データを使って学習させてモデルを作成して、テスト用データを使ってモデルが正確に動作しているか確かめるという流れが機械学習にはあります。

この一連の流れをこなす為に《学習用データ》と《テスト用データ》が必要となってきます。

separate.jpg

実はこの『学習用とテスト用(詳しくはvalidation用)のデータを分ける』という段階は深堀りすれば、Kaggler達で様々な議論が行われているような分野みたいです…

この記事はそんな話題には触れず、サラッと進めていきます。笑

てか『データなんてPythonでシンプルに分ければ良いじゃないか!』という話なのですが、先ほどの《花の種類》の目的変数を見て頂けたら分かる通り、0⇒1⇒2の順番で目的変数が並んでいます

ここでもしスライスを使ってデータを半分に分けたりすると、上部分の目的変数が0と1のデータしかモデルは識別できないので、目的変数が2であるデータに対して有効的なモデルを作ることができません

ペイント間満載の図を使っていくと…
modelkun.jpg
↑上では『0のデータ』と『1のデータ』しか学習していません。

↓結果的にこのような事が起こります。
shikibetsu.jpg

図にしてしまえば単純な話ですね!

そこで、
『データを学習用とテスト用に分ける且つシャッフルしてくれるような機能があれば良いなぁ~』
なんて思う訳です。

『あります。train_test_split関数があります!!!』

散々言葉の定義について書いてきましたが、やっとプログラムを書いていきます。笑
ここで冒頭にてimportしたtrain_test_split関数を使います。

train_test_split関数を使う

今回は学習用の説明変数と目的変数、テスト用の説明変数と目的変数を以下のように定義します。

  • 学習用の説明変数 ⇒ setsumei_train
  • 学習用の目的変数 ⇒ mokuteki_train
  • テスト用の説明変数 ⇒ setsumei_test
  • テスト用の目的変数 ⇒ mokuteki_test

これらをプログラム化するとこうです↓

学習用とテスト用の説明変数と目的変数を定義する.ipynb
setsumei_train,setsumei_test,mokuteki_train,mokuteki_test = train_test_split(heacet_data, heacet_target, test_size=0.33)

引数としてtest_sizeというものがあります。これはデータセットをどれだけテスト用に使うかを割合で設定する引数です。

今回は『データセット50個分 = 全体の1/3 = 0.33』をテスト用に使うので、引数を『0.33』としています。

しっかりと分けれているか見ていきます。僕はこうなりました↓
headyatsu.JPG
headyatsu2.JPG
describeyatsu.JPG

良い感じにシャッフルされて、100個50個で分かれていますね。またID番号も同じなので良い感じです( *´艸`)

7. データをmatplotlibで可視化する

次は効果的な特徴量を見つけるために、matplotlibでデータを可視化していきます。

データを可視化する意味は?《特徴量選択》

特徴量とは説明変数のことを指していて、『特徴量 ≠ 説明変数』であることは覚えておいて欲しいのですが、今は同じものとしておきます。

さてデータを可視化することで、効果的な特徴量選択が行えるということを例えを出して説明していきます。

グラフ上にプロットしているの点についてそれぞれで分類したいとします。

このとき↓下の2つの図ではどちらの方が正しく分類できるでしょうか?
graphdayo.jpg
↑これが1つ目の図

graphda.jpg
↑これが2つ目の図

どう見ても2つ目の図のほうが正しく分類できそうですよね。実際にそうです。笑

少し極端な例でしたが、上下の図のように選択する特徴量によって、分類のしやすさが変化することがあります。

今回のアヤメの分類で言えば、《がく片の長さ》《がく片の幅》《花びらの長さ》《花びらの幅》で2つ特徴量を選ぶとすれば、組み合わせによって分類のしやすさが変わってくるということです。

散布図をplt.scatterで描いていく!

matplotlibのscatterメソッドは引数として以下を指定します。

  1. 横軸にしたいデータ
  2. 縦軸にしたいデータ
  3. label=凡例
  4. cmap=カラーマップの種類

カラーマップを用いて色分けしていくのですが、様々な色分けの方法が存在します。
>>>指定できるカラーマップの一覧はコチラ(matplotlib公式リファレンス)

早速matplotlibを使ったプログラムを書いていきたいのですが、花の種類によって視覚的に分類の正確さを測るために目的変数別に色を変えていきます。

目的変数をフィルタリングする方法が僕は少し躓きました。詳しい方がいたらコメントを頂けたら嬉しいですm(__)m

縦軸と横軸データの選択時に上下どちらの引数を使っても大丈夫だったので、実際に左右で使っています。

  • setsumei_train[(mokuteki_train == 0).values]["がく片の長さ"]
  • setsumei_train[mokuteki_train["花の種類"] == 0]["がく片の長さ"]

がく片の長さと幅を使って、プロットしていきます。

pltでプロットする(がく片).ipynb
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==0]["がく片の長さ"],setsumei_train[(mokuteki_train == 0).values]["がく片の幅"],label="setosa",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==1]["がく片の長さ"],setsumei_train[(mokuteki_train == 1).values]["がく片の幅"],label="versicolor",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==2]["がく片の長さ"],setsumei_train[(mokuteki_train == 2).values]["がく片の幅"],label="virginica",cmap="rgb")

## X軸の範囲を指定
plt.xlim(3,9)
## Y軸の範囲を指定
plt.ylim(1,5)

## X軸の名前
plt.xlabel("Length of sepal")
## Y軸の名前
plt.ylabel("Width of sepal")

## グラフのタイトル
plt.title("Relation between length and width of sepal")
## 凡例を出力
plt.legend()

先ほど上下どちらでも良いと言いましたが、目的変数をフィルタリングする方法が一つは『Numpyで2次元でブールインデックス参照をしている?』のと、『Seriesのブール値をDataFrameに入れることによって、フィルタリングしている?』方法があります。
この考えが正しいのか証明できる文献がイマイチ探せなかったので、詳しい方がいたらコメントを頂けたら嬉しいですm(__)m

一応その結果は残しておきます。

zikken.py
>>> mokuteki_train == 0
## ブール値でDataFrameが返ってくる
>>> mokuteki_train["花の種類"] == 0
## ブール値でSeriesが返ってくる
>>> (mokuteki_train == 0).values
## np行列でブール値の2次元配列が返ってくる
>>> (mokuteki_train["花の種類"] == 0).values
## np行列でブール値の1次元配列が返ってくる

# この時setsumei_train[mokuteki_train["花の種類"]==0]と
# setsumei_train[(mokuteki_train == 0).values]が全く同じ値を返します。

また花びらの長さと幅でプロットする例も示しておきます。

pltでプロットする(花びら).ipynb
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==0]["花びらの長さ"],setsumei_train[(mokuteki_train == 0).values]["花びらの幅"],label="setosa",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==1]["花びらの長さ"],setsumei_train[(mokuteki_train == 1).values]["花びらの幅"],label="versicolor",cmap="rgb")
plt.scatter(setsumei_train[mokuteki_train["花の種類"]==2]["花びらの長さ"],setsumei_train[(mokuteki_train == 2).values]["花びらの幅"],label="virginica",cmap="rgb")

plt.xlim(0,8)
plt.ylim(0,4)

plt.xlabel("Length of petal")
plt.ylabel("Width of petal")

plt.title("Relation between length and width of petal")
plt.legend()
そして↓下図ががく片の長さと幅を使ってプロットした結果

plt1.JPG

これは分類しにくそうですね。人間である僕からしても、どこに区別する線を引いたら良いのかわかりません。笑

そして↓下図が花びらの長さと幅を使ってプロットした結果

plt2.JPG

これはそれぞれ目的変数の集合ができているので、簡単に分類する境界線が引けそうですね!
このように選択する特徴量によって、分類のしやすさが変わってくることが視覚的に分かりました。

8. 機械学習アルゴリズムを使っていこう!

ついに機械学習のアルゴリズムに触れることができます。

といってもLinearSVCを用いることを決めてしまっているので、もうそこまでやることはないです。

ハイパーパラメータを決める』といった段階もありますが、今回は『機械学習の一連の流れをつかむ』ことを重きに置いているので触れないでおきます。気になる方は下のリンクで参考文献を貼っておきます。
>>>LinearSVCのハイパーパラメータの詳しい説明はコチラ

特徴量を選択しよう!

では先ほどMatplotlibを使って可視化させた2つのパターン(がく片コンビと花びらコンビ)の特徴量を選択していきます。
新たに2つのDataFrameとして定義すると、《名前による参照メソッドloc》を使って、プログラムは以下になる。

2つの特徴量を作成する.ipynb
## がく片コンビのDataFrameを作成する。
gakuhen_train = setsumei_train.loc[:,["がく片の長さ","がく片の幅"]]

## 花びらコンビのDataFrameを作成する。
hanabira_train = setsumei_train.loc[:,["花びらの長さ","花びらの幅"]]

LinearSVCでモデルを構築⇒学習⇒予測させる

やっとモデル構築までたどり着きました。笑

もう一度ここ付近の話を説明した図を引っ張ってきますと、
kokoyatteta.jpg

今までは『与えられたデータセットがどうなっているか見たり』、『学習用とテスト用でデータを分けたり』と上図での上の方をずっとやっていました。

ですがこれが『機械学習という分野の一種の特徴』だそうで、機械学習は『前処理に時間がとられる』と聞いたことありませんか??

まさにそれを具現化しちゃいましたね。記事の大半を占めてしまっています。

そして次にやる機械学習アルゴリズムを実装させる部分は『機械学習アルゴリズムを理解するのは難しい』けれど、『実装させるのは超簡単』という分野で、すぐに終わります。

kokoyaruyo.jpg

よって冒頭で定義したLinearSVCを使っていきます。プログラムはこんな感じ↓

LinearSVCでモデル構築⇒学習⇒予測する.ipynb
## それぞれモデルを構築
## それぞれモデルを構築
gakuhen_model = LinearSVC()
hanabira_model = LinearSVC()

## それぞれのモデルに学習させる
gakuhen_model.fit(gakuhen_train,mokuteki_train)
hanabira_model.fit(hanabira_train,mokuteki_train)

## それぞれのモデルで予測させて、予測値を代入させる
### モデルが《がく片の長さと幅》を使って学習しているので、予測する時も《がく片の長さと幅》を渡す必要がある。
gakuhen_predict = gakuhen_model.predict(setsumei_test.loc[:,["がく片の長さ","がく片の幅"]])
### モデルが《花びらの長さと幅》を使って学習しているので、予測する時も《花びらの長さと幅》を渡す必要がある。
hanabira_predict = hanabira_model.predict(setsumei_test.loc[:,["花びらの長さ","花びらの幅"]])

ついに答え合わせです!ここで上で立てた仮説を検証できますね。

Matplotlibで可視化させた図では『《がく片の幅と長さ》は正確に分類できなさそう』でした!
>>>がく片の長さと幅を可視化したグラフはコチラ

逆に『《花びらの幅と長さ》は《がく片の幅と長さ》よりかは正確に分類できそう』でしたね!
>>>花びらの長さと幅を可視化したグラフはコチラ

モデルが予想したデータの答え合わせ

では答え合わせできるプログラムをインポートして、実行していきます。

accuracy_scoreを使う.ipynb
## sklearnライブラリからscore算出の関数をimport
from sklearn.metrics import accuracy_score

## gakuhen_scoreとhanabira_scoreにそれぞれに結果を代入
gakuhen_score = accuracy_score(mokuteki_test, gakuhen_predict)
hanabira_score = accuracy_score(mokuteki_test, hanabira_predict)

print('がく片の長さと幅コンビの正解率:{}'.format(gakuhen_score),'花びらの長さと幅コンビの正解率:{}'.format(hanabira_score), sep='\n')

↓僕は出力結果として以下が得られました!↓
syuturyokukekka.JPG

とりあえずどちらも正答率が8割を超えているので、機械学習によってアヤメの種類を分類することはひとまず成功しましたね!

ではでは…

結果について考察していきます。

2つの結果の違いについて詳しく見る

人間の目で見ても《がく片の長さと幅》より《花びらの長さと幅》の方が正確に分類しやすそうでしたが、LinearSVCアルゴリズムにとっても同じように《花びらの長さと幅》の方が正確に分類できるようです。笑

ですが『LinearSVCアルゴリズムにとっても同じように《花びらの長さと幅》の方が正確に分類できる』というのは僕の推論でしかないのです。

実際に確かめるには、どこで境界を作っているのかをMatplotlibを使って可視化していくと良いですよね!!

がく片の長さと幅》と《花びらの長さと幅》でそれぞれ境界線を見たいので、代入できる関数として定義していきます。

分類の境界を可視化する.ipynb
def heacet_border_check(H, M, model, param1, param2, resolution=0.01):
    H1_min, H1_max = H[param1].min()-0.5, H[param1].max()+0.5
    H2_min, H2_max = H[param2].min()-0.5, H[param2].max()+0.5
    H1, H2 = np.meshgrid(np.arange(H1_min, H1_max, resolution),
                           np.arange(H2_min, H2_max, resolution))
    n = np.array([H1.ravel(), H2.ravel()]).T
    Z = model.predict(n)
    Z = Z.reshape(H1.shape)
    plt.contourf(H1, H2, Z, alpha=0.5, cmap="Set2")
    plt.xlim(H1_min, H1_max)
    plt.ylim(H2_min, H2_max)
    plt.xlabel("Length")
    plt.ylabel("Width")
    plt.scatter(H[param1],H[param2], c=M["花の種類"], cmap="brg")

関数が用意できたので、コチラを使って出力すると…
kekka2.JPG
↑まずは上図が《がく片の長さと幅》を使った時の境界図です。
青丸は上手く境界を持てているようですが、赤丸緑丸ごちゃごちゃしていて、微妙なところに境界線が引かれていますね。

そりゃあスコア低くなりますよね…という感じ。

kekka3.JPG
↑続いて上図が《花びらの長さと幅》を使った時の境界図です。
青丸は完璧ですね。赤丸緑丸同士がほんの少し境界を越えているくらいで、ほぼ綺麗に境界線を引けていると思います。

がく片花びらとで場合分けしてきましたが、『データの可視化によって特徴を選択することはとても重要なこと』は伝わったのではないでしょうか?

まとめ

最後まで見て頂きありがとうございました。

普段から技術記事は書いていないもので、少し口語が多かったかもしれません…笑

色々と調べながら初心者だからこその目線で《アヤメ分類》について一から説明してみました。

文書が変だったり、間違っている点などございましたら気軽にコメント頂けると嬉しいです。
もちろん感想やよかった点などでも気軽にコメントください(´艸`*)

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

日本※で一番※ポーカーが上手い※のは誰か計算してみた(TrueSkill)~理論編~

記事へのリンク

  1. 理論編(←本記事)
  2. 実装編(書いてます・・・)
  3. 評価編(書いてます・・・)

本記事におけるお断り

本記事は筆者が自学自習のためにデータ取得から計算までを行ったものです。特定の団体や個人から許可許諾はとっていないため、もし内容に問題があるとお考えの場合にはご連絡いただければ幸いです。
また、タイトルにもある通り、単純に強い、上手いを判断するのが困難であるポーカーにおけるスキルの『推定』がやりたかったことです。
本記事を通じて少しでもポーカーに興味を持ってアミューズメントやオンラインでポーカーを始めたいと思う人が増えれば何よりです。

なんでこんなことしようと思ったのか

pandasを勉強する機会があり、継続学習のために興味がある領域でお勉強&アウトプットしたかったらです。
あと、ポーカーというかなり運の要素が入るゲームにおいて実力を正確に計算する事が出来るのかという点は昔から興味がありました。
昔社交ダンスの試合結果100万件をELOレーティングで計算したりと、何かと順位を元にした実力推定は興味があったのです。

背景の説明

ポーカー

現在世界ではテキサスホールデムという種目が最も主流でありプレイ人口も多いです。
本記事では今後ポーカー=ノーリミットテキサスホールデムという前提で記述します。
5枚引いて、好きな枚数チェンジして、勝負!ではなく、裏向きの自分しか使えない2枚+全員が使える5枚のうち好きな5枚を使って勝負!
という感じのルールです。詳細は以下URLよりぜひご覧ください!
https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%AD%E3%82%B5%E3%82%B9%E3%83%BB%E3%83%9B%E3%83%BC%E3%83%AB%E3%83%87%E3%83%A0

日本のポーカー事情

ご存知のとおり日本において賭博は違法です。そんな中どのように日本のプレイヤーはポーカーを楽しんでいるかというと、大きく3つに分けられます。

  1. アミューズメント施設で遊ぶ
  2. オンラインゲームで遊ぶ
  3. 海外のカジノに行って遊ぶ

それぞれざっくり言うと以下のような特徴があります。

名称 特徴 代表的な場所 データの有無
1.アミューズメント施設 ゲームセンターと同じような仮想チップを使ってゲームを楽しむ。換金は出来ないが大きな大会ではスポンサーから商品が出る事もある。 アキバギルド、東京dePoker、カジノクエスト、パラハetc 全順位データ公開
2.オンラインゲーム モバイルやPCから遊べる。一部サイトではお金を賭けて遊べてしまったりするが、合法か違法かはグレーなので自己責任。 PokerStars、Zynga、PokerPoker 一部開発者のみに限定公開
3. 海外のカジノ カジノが有ってポーカーが遊べる国に行けば合法的にお金を賭けてポーカーが出来る。全てのカジノでポーカーが出来るわけではない(むしろ全体でみると少ない?)ので注意。 ラスベガス、マカオ、韓国、マニラ、バルセロナ、モナコetc 入賞者のみ公開

今回、「1.アミューズメント施設で遊ぶ」という試合結果をもとにして計算を行いました。
なぜかと言うと、データが公開された状態で十分な量存在していることと、結果に対する肌感があるため自分でも計算結果を一定評価出来るからです。

計算手法

今回は「総当たり式ELOレーティング」と「TrueSkill」の2つの方法で計算を行いました

総当たり式ELOレーティング

ELOレーティングはチェスの実力評価のために開発された(恐らく)最古の実力を定量化するためのアルゴリズムです。
https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%AD%E3%83%AC%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

ただし、1vs1の試合結果しか計算出来ないため、将棋・チェス・ボクシング・剣道etcの競技では単純に適用可能なのですが、
ポーカー、ゴルフ、競馬、社交ダンスetcの様な複数のプレイヤーが同時に競技を行い順位がつく競技に適用するためには工夫が必要です。

その工夫が「総当たり式」であり、端的に言えば1試合の参加者を全2人の組み合わせに分解してそれぞれで勝敗を計算してしまおう、という考えです。
例:ABCDEの5人が100m走を走り、CABEDの順位だったとする
- AvsB:Aの勝ち、Bの負け
- AvsC:Cの勝ち、Aの負け
- (略)
- DvsE:Eの勝ち、Dの負け

といった感じでそれぞれの結果を計算し最終的に合算します
計算式については上記Wikipediaリンクに詳細がありますが、抜粋するとこんな感じです。

勝率の計算

Aさん(レーティング1700)とBさん(レーティング1500)が対戦する場合を考えます
まずAさんとBさんの対戦した時の勝率を計算します
Aさんから見た勝率は以下の式より約76%と計算できます

W_{ab} = \frac{1}{10^{(R_B-R_A)/400}+1} 

レーティング変動を計算

上記で算出した勝率をもとに、試合後のレーティング(R')がどの程度変動するか計算します。

R'_A = R_A + K \times W_{BA}
R'_B = R_B - K \times W_{BA}

定数パラメーター

ELOレーティングの計算において、3つのパラメータを決定する必要があります。

  • 初期レーティング:多くの場合1500と設定されていますが好きな数字でOK
  • 初期レーティングに対する勝敗比:多くの場合400と置かれています。初期レーティングに対して乖離しすぎていないければOKの認識
  • 変動率(K):1回の試合結果でどの程度レーティングを変動させるのかのパラメータです。32が一般的と言われていますが、競技の特性によってここの値を適正に変える必要があります。試合頻度が高い、1対1ではなく順位式の競技、運の要素が大きい競技、などの場合にはKの値を小さく設定するのが良いです

計算してみた結果から言うと・・・

とてもではないが使いものになりませんでした。。。
理由はいくつか考えられますが、ポーカーのようなどんな実力者でもかなりの頻度で初心者に負けうる様な競技の場合
レーティングが1回の勝敗に大きく左右されすぎるため、人数が多い試合においては1回の負け/勝ちであまりに大きな変動となってしまう事がわかりました。
例として100人程度の規模である程度の実力者が下位20%くらいの順位だと一発で-2000くらいの変動を食らっていました。
では大きく変動しすぎないようにKを下げれば良いかと言われると、今度は少人数の試合で全く動かなくなるためこれもよろしくない。
では人数にKを反比例させる、等考えられますがそうすると1試合の価値が変わる事になりこれもどうかと。。。
syelo.PNG
ちなみにこちらは私の約80回あまりの試合ごとの変動をプロットしたものです。
見ての通り、概ね1500より高い位置に居ますが、最後の方で大きくマイナスを食らった結果、
残念ながら1500未満の結果となりました。(悲しい)

TrueSkill

TrueSkillはマイクロソフト謹製のスキル推定アルゴリズムです。
XBoxのゲーム内マッチングで利用する目的で開発され、マイクロソフトが特許を持っており商用利用には許諾が必要です
日本語記事だとQiita内では以下2つが大変参考になりました。
数学が苦にならない人は1つめの記事を苦手な方は2番目の記事を読むと大変わかりやすいと思います

誰でも分かるTrueSkill
TrueSkill「まだ Elo レーティングで消耗してるの?」

以下2つめの記事より引用した特徴です

  • 収束が早い。 レーティングに初めて参加するプレイヤーの実力を推定するのに何度も何度も不適当なマッチングで対戦する必要がない。
  • 複数人による対戦に対応している。 勝ちか負けかのみならず順位を定めるようなゲームやチーム戦1のゲームにも使用できる。
  • ゲームへの参加に重み付けができる。 例えばチームメンバーのひとりが回線トラブルにより途中でゲームから抜けた場合でも適用できる。
  • Microsoft の息がかかっている。 特許申請と商標登録がなされており、なんとなく手が出しづらい。 あと一部の狂信的 OSS 主義者が憤死する。

TrueSkillの概要

※ここから先の説明は数学素人の筆者が理解しやすさ重視で記載しているので誤っている箇所があるかもしれません
ELOレーティングは1回スキルの計算が終わると、「今回は良い結果だったからあなたのレーティングは1500⇒1600にアップしました」という様に具体的なスキルの数値を示します。
これを複数回繰り返すことで本来のスキルに収束してゆく、という考え方です。
一方TrueSkillは1回スキルの計算が終わると、「今回は良い結果だったからあなたのレーティングは、試合前は1200~1800の間のどこかで中央の1500である可能性が最も高かったけど、計算後は1400~1700の間のどこかで中央値の1550である可能性が最も高くなるとわかりました」という様にスキルを確率分布で示します。

数学に詳しい方はお気づきかと思いますが、これは正規分布で表現されているという事です。
TrueSkillで計算が進むとは、正規分布の中央の値(μ)が左右に動きその人のスキルに近づいてゆき、更に正規分布の範囲が狭まり(σ)その人のスキルの幅が確定してゆく。
といった過程を経て最終的なスキルが算出されます
sy.png

こちらもELOレーティングと同様に私自身の計算過程である約80試合の計算過程をプロットしたものです。
低くて横に広いグラフが最初の状態、高くて横に狭いグラフが最終的な状態です。
見れば分かる通り私はグラフの中央が右方向に動いているため、一応計算上は強い人、ということになります。
(一応計算上は、です。。。)

次の記事

次の記事では、どのように上記理論を実装したのか簡単に記載したいと思います。
そして最後に計算結果から分析結果をまとめて記載したいと思います。
ちなみに実名(ポーカーの場合はポーカーネームというニックネーム)を公表すると色々問題になりそうなのですが、
どうしようかは現在まだ決めていません。。。

もし現時点で自分の値を知りたい方はTwitterで本人までDMいただければこっそり教えます!

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

【小ネタ】terminalからエラーメッセージをググる

ターミナルで実行する際にエラーが出た時、ブラウザを起動して検索するプロセスでとても億劫になっていたのでターミナルから検索したい文字を入力すると自動でブラウザが開いて検索してくれるsomethingを作りました。
これでエラーが出たときに気負う事なく検索ができるようになります。
小ネタ程度のものなので、コピペして必ず動くとは言えないほどガバなコードです。
フローだけ参考にして、よかったら自作してみてください。

詳細

コマンドライン引数に検索したいワードを入力し、実行することで入力したワードを検索します。
検索結果は、ブラウザが起動して表示されます。
使用したブラウザは、safariです。
数行で書けます。

コード

search.py
import webbrowser, sys

url = 'https://www.google.com/search?client=safari&rls=en&q={}&ie=UTF-8&oe=UTF-8'
search = ' '.join(sys.argv[1:])
webbrowser.open_new_tab(url.format(search))

実行する際の、python search.py hoge の python search.pyを環境変数にします。

terminal
$ echo export google=python\ search.pyのフルパス >> ~/.bash_profile
$ source .bash_profile

これで簡単にターミナルから検索ができるようになりました。

terminal
$ $google hoge

感想

大したものではないですがこの程度のもので効率が上がるならええやん。と思いました。

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

pipを使おうとして「TypeError: 'module' object is not callable」が出た場合の対処方法メモ

正確な発動条件が定かではないですが、ubuntu18.04にpipを手動インストールし、pyenvで環境構築した際にタイトルのエラーが発生したので、解決できた方法をメモします。

現象

上記条件で、pyenvで作成したpython環境でpipを実行しようとすると次のエラーが出て動かない。

>> pip
Traceback (most recent call last):
  File "/home/dev-user/.pyenv/versions/3.6.7/bin/pip", line 11, in <module>
    sys.exit(main())
TypeError: 'module' object is not callable

解決方法

pyenv環境で次のコマンドでpipインストールを再実行したところ解決。

curl -kL https://bootstrap.pypa.io/get-pip.py | python

あとがき

エラーメッセージで検索すると、aptでpipをアップデートしている記事や、pip install pip==18.0などでversionを戻している記事などはあったのですが、直接的な解法にたどり着くのに時間がかかったのでメモを兼ねて記事にしました。

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

【Python】datetimeオブジェクトのタイムゾーンを一行で変更する(JST <=> UTC)

何番煎じかわかりませんが、(タイムゾーン指定のない)シンプルな日時文字列から、入力と出力のタイムゾーンを指定して変換結果を得るコードのメモになります。

utcとの差分9時間を加減すれば処理できるのですが、ちゃんと?、'UTC'、'Asia/Tokyo'などとタイムゾーンを指定して変換したい場合のコードです。

日付関連の用語に慣れていないので、もし誤用などありましたらご指摘ください。

使っているライブラリ

import datetime
import pytz

JSTの日時文字列をUTCに変換する

# 文字列から(タイムゾーン情報を持たないnaiveな)datetimeオブジェクトを作成する
datetime_obj = datetime.datetime.strptime("2019-10-28 13:00:00", "%Y-%m-%d %H:%M:%S")

# 変換(JST -> UTC)
utc_datetime_obj = pytz.timezone('Asia/Tokyo').localize(datetime_obj).astimezone(pytz.timezone('UTC'))

# 結果を確認
print(utc_datetime_obj.strftime("%Y-%m-%d %H:%M:%S"))
# (9時間前の時刻が表示される)
# 2019-10-28 04:00:00

やっていること

datetimeオブジェクトを作成

日時文字列とその書式情報から、タイムゾーン情報を持たないnaiveなdatetimeオブジェクトを作成

datetime_obj = datetime.datetime.strptime("2019-10-28 13:00:00", "%Y-%m-%d %H:%M:%S")

datetimeオブジェクトにタイムゾーン情報を付与

作成したdatetimeオブジェクトに(JSTの)タイムゾーン情報を付与してawareなdatetimeオブジェクトを作成

jst_datetime_obj = pytz.timezone('Asia/Tokyo').localize(datetime_obj)

astimezoneでUTCに変換

utc_datetime_obj = jst_datetime_obj.astimezone(pytz.timezone('UTC'))

(逆) UTCの日時文字列をJSTに変換する

# この文字列をUTCに見立てて処理する
# 文字列から(タイムゾーン情報を持たないnaiveな)datetimeオブジェクトを作成する
datetime_obj = datetime.datetime.strptime("2019-10-28 13:00:00", "%Y-%m-%d %H:%M:%S")

# 変換(UTC -> JST)
jst_datetime_obj = pytz.timezone('UTC').localize(datetime_obj).astimezone(pytz.timezone('Asia/Tokyo'))

# 確認
print(jst_datetime_obj.strftime("%Y-%m-%d %H:%M:%S"))
# (9時間後の時刻が表示される)
# 2019-10-28 22:00:00
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

非同期httpリクエストを使ってスクレイピングする - grequests -

async/awaitを使う

pythonロゴ.jpg

背景

スクレイピングを効率的に行う方法としてmultiprocessingを使ってマルチプロセスを使用する方法がありますが、マルチプロセスでの実行は環境のコア数に依存してしまいます。
リクエストの結果を待ってる時にコアを掴んでおく必要はなく非同期で実行するようにすることで効率的に処理を進める方法を探していたら見つけたので実行方法について書きました。

grequestsとは

geventを使って非同期HTTPリクエストを簡単に実現することができるライブラリです。
geventとは非同期処理をベースとしたネットワーク処理のライブラリです。
bottleやflaskを使用してwebsocketを使う場合によく出てくるものと一緒です。

FlaskとWebSocketを使用してリアルタイム通信を行う

gevent/gevent

環境

$ uname -a
Darwin mbp01 19.0.0 Darwin Kernel Version 19.0.0: Wed Sep 25 20:18:50 PDT 2019; root:xnu-6153.11.26~2/RELEASE_X86_64 x86_64

$ python3 -V
Python 3.7.4

インストール

grequestsはpypiで公開されているのでpipでインストールできます。

$ pip install grequests

grequests 0.4.0

ソースコードはGitHub上で公開されています。

spyoungtech/grequests

使ってみる

非同期httpリクエストを実行する方法はとても簡単です。

app.py
import grequests

urls = [
    'http://www.heroku.com',
    'http://python-tablib.org',
    'http://httpbin.org',
    'http://python-requests.org',
    'http://fakedomain/',
    'http://kennethreitz.com'
]

# 非同期リクエスト用のオブジェクトを生成
rs = (grequests.get(u) for u in urls)

# httpリクエスト/結果を表示
print(grequests.map(rs))

サンプルではhttpのGETを実行していますがこれ以外にも基本的には対応しているようです。

grequests.py
# Shortcuts for creating AsyncRequest with appropriate HTTP method
get = partial(AsyncRequest, 'GET')
options = partial(AsyncRequest, 'OPTIONS')
head = partial(AsyncRequest, 'HEAD')
post = partial(AsyncRequest, 'POST')
put = partial(AsyncRequest, 'PUT')
patch = partial(AsyncRequest, 'PATCH')
delete = partial(AsyncRequest, 'DELETE')

さくっと検証

実際に自分の環境で検証してみたのですがスクレイピングプログラムの実行時間が早くなるのは実感できませんでした。
(そもそもそこまでリクエストを多数実行してるわけでもないので。。。)
せっかくなので非同期を体感できるような検証環境を作って試してみました。

サーバアプリケーション

flaskをuwsgiで多重化して実行。
4プロセスで起動するので4リクエストまでは(環境次第で)同時に処理します。
並列性を確認する簡単な方法として非同期sleepである標準モジュールのsleepを使用します。
返却値としてはリクエストの時間とレスポンスの時間をつめて返します。

ちなみにflaskではデフォルトで起動すると複数のリクエストを同時に処理することができず今回の検証ではuwsgiを使ってます。
ただ起動時にthreaded=Trueオプションを指定することでuwsgiを使わなくても検証することは可能です。

While lightweight and easy to use, Flask’s built-in server is not suitable 
for production as it doesn’t scale well and by default serves only one request at a time. 
Some of the options available for properly running Flask in production are documented here.

Deployment Options

(threadに関しては実際のシステムではWSGIなどを用いることが多いようなのであまり使われないオプションって認識)

サーバ
#!/usr/local/bin/python3
# coding: utf-8

import datetime
import os
import sys
import time
from flask import Flask, jsonify, request

app = Flask(__name__)


def get_date_formatting():
    return str(datetime.datetime.today())[:-7]


@app.route("/", methods=["GET"])
def hello_world():
    try:
        sec = int(request.args.get("sec"))
    except Exception:
        sec = 0

    req_time = get_date_formatting()
    time.sleep(sec)
    res_time = get_date_formatting()
    return jsonify({
        "pid": os.getpid(),
        "req-time": req_time,
        "res-time": res_time
    })


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=5000)

クライアント

同期的に実行
1リクエストごとに処理された時間が追加されていってるのが確認できます。

client.py
import requests

def main():
    urls = [
        "http://localhost:5000?sec=3",
        "http://localhost:5000?sec=5",
        "http://localhost:5000?sec=1",
        "http://localhost:5000"
    ]


    for i in urls:
        print((requests.get(i).text))


if __name__ == "__main__":
    main()
実行結果
{
  "pid": 21577,
  "req-time": "2019-10-28 20:55:22",
  "res-time": "2019-10-28 20:55:25"
}

{
  "pid": 21577,
  "req-time": "2019-10-28 20:55:25",
  "res-time": "2019-10-28 20:55:30"
}

{
  "pid": 21577,
  "req-time": "2019-10-28 20:55:30",
  "res-time": "2019-10-28 20:55:31"
}

{
  "pid": 21577,
  "req-time": "2019-10-28 20:55:31",
  "res-time": "2019-10-28 20:55:31"
}

grequestsを使って実行。
実行時間をみると並列に4リクエスト同時に実行してサーバ側で処理されていることがわかりました。

client_aio.py
import grequests

def main():
    urls = [
        "http://localhost:5000?sec=3",
        "http://localhost:5000?sec=5",
        "http://localhost:5000",
        "http://localhost:5000"
    ]

    rs = (grequests.get(u) for u in urls)

    for r in grequests.map(rs):
        if r is not None:
            print(r.text.rstrip())


if __name__ == "__main__":
    main()
実行結果
{
  "pid": 21577,
  "req-time": "2019-10-28 20:51:31",
  "res-time": "2019-10-28 20:51:34"
}
{
  "pid": 21577,
  "req-time": "2019-10-28 20:51:31",
  "res-time": "2019-10-28 20:51:36"
}
{
  "pid": 21577,
  "req-time": "2019-10-28 20:51:31",
  "res-time": "2019-10-28 20:51:31"
}
{
  "pid": 21577,
  "req-time": "2019-10-28 20:51:31",
  "res-time": "2019-10-28 20:51:31"
}

まとめ

ネットワークIOなどは非同期で処理をしやすいのは間違いないですが、
私が使ってるスクレイピングプログラムだとあまり効果が出ませんでした。大量にリクエストを送るプログラム(テストなど?)の並行実行には強いのではないでしょうか?

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

DeepChemでScaffoldSplitterの使った場合にValueError: No molecule providedが発生した話

はじめに

DeepChemでScaffoldSplitterを普通に使ったところ、掲題のエラーが発生したので調べたときのメモ

環境

  • python 3.6
  • deepchem 2.2.1.dev54
  • rdkit 2019.03.3.0

現象

splitterにScaffoldSplitterを指定して意気揚々とCross-Validationをやろうとしたら、deepchem.data.CSVLoaderでcsvファイルを読み込もうとした時に、以下エラーが発生。

  File "C:\Users\XXX\AppData\Local\conda\conda\envs\yyy\lib\site-packages\rdkit\Chem\Scaffolds\MurckoScaffold.py", line
108, in MurckoScaffoldSmiles
    raise ValueError('No molecule provided')
ValueError: No molecule provided

原因と解決策

エラーログを元に色々見たところ、deepchem.splits.splitters.pyの870行目に以下を発見。

 for ind, smiles in enumerate(dataset.ids):
      if ind % log_every_n == 0:
        log("Generating scaffold %d/%d" % (ind, data_len), self.verbose)
      scaffold = generate_scaffold(smiles)
      if scaffold not in scaffolds:
        scaffolds[scaffold] = [ind]
      else:
        scaffolds[scaffold].append(ind)

dataset.idsからsmilesとってんじゃん。ダメじゃん。そこはdataset.smilesからとるべきでしょうが。
といっても回避策はないので、ScaffoldSplitterを使う場合は、CSVLoaderのid_fieldsは指定しないようにしましょう。
そうすると、id_fieldにsmiles_fieldsが設定されるっぽいので。
id_fieldが何に使われてるか知らんけど、まあ今のところ大丈夫っぽい。

loader = dc.data.CSVLoader(tasks=[args.target_col],
#                              id_field=args.id_col, # ここはコメントオフにしよう。
                               smiles_field=args.smiles_col,
                               featurizer=featurizer)

おわりに

OSSを使う場合は、適度にソースも見ながら、うまく付き合うことが大事ということで。。。

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

DeepChemでScaffoldSplitterを使った場合にValueError: No molecule providedが発生した話

はじめに

DeepChemでScaffoldSplitterを普通に使ったところ、掲題のエラーが発生したので調べたときのメモ

環境

  • python 3.6
  • deepchem 2.2.1.dev54
  • rdkit 2019.03.3.0

現象

splitterにScaffoldSplitterを指定して意気揚々とCross-Validationをやろうとしたら、deepchem.data.CSVLoaderでcsvファイルを読み込もうとした時に、以下エラーが発生。

  File "C:\Users\XXX\AppData\Local\conda\conda\envs\yyy\lib\site-packages\rdkit\Chem\Scaffolds\MurckoScaffold.py", line
108, in MurckoScaffoldSmiles
    raise ValueError('No molecule provided')
ValueError: No molecule provided

原因と解決策

エラーログを元に色々見たところ、deepchem.splits.splitters.pyの870行目に以下を発見。

 for ind, smiles in enumerate(dataset.ids):
      if ind % log_every_n == 0:
        log("Generating scaffold %d/%d" % (ind, data_len), self.verbose)
      scaffold = generate_scaffold(smiles)
      if scaffold not in scaffolds:
        scaffolds[scaffold] = [ind]
      else:
        scaffolds[scaffold].append(ind)

dataset.idsからsmilesとってんじゃん。CSVLoaderでid_fieldを明示的に指定した場合は、そこにはsmilesは格納されないわけだから、そのフィールドをsmilesと勝手に判断し、molに変換したらそりゃー、'No molecule provided'が発生するよ。
といっても、こんなファイル修正しようがないのでScaffoldSplitterを使う場合は、CSVLoaderのid_fieldsは指定しないようにしましょう。
そうすると、id_fieldにsmiles_fieldsの値が設定されるっぽいので、結果オーライです。
id_fieldが何に使われてるか知らんけど、まあ今のところ大丈夫っぽい。

loader = dc.data.CSVLoader(tasks=[args.target_col],
#                              id_field=args.id_col, # ここはコメントオフにしよう。
                               smiles_field=args.smiles_col,
                               featurizer=featurizer)

おわりに

OSSを使う場合は、適度にソースも見ながら、うまく付き合うことが大事ということで。。。 
以上、取り急ぎご報告まで失礼します。

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

[ゼロから作るDeep Learning]学習機能を実装したニューラルネットワークの実装

はじめに

この記事はゼロから作るディープラーニング 5章ニューラルネットワークの学習を自分なりに理解して分かりやすくアウトプットしたものです。
文系の自分でも理解することが出来たので、気持ちを楽にして読んでいただけたら幸いです。
また、本書を学習する際に参考にしていただけたらもっと嬉しいです。

学習機能を実装したニューラルネットワーク

今回は分類問題を解く専用の学習機能付きニューラルネットワークのクラスを実装して行きたいと思います。

今回の実装の流れを簡単に説明します。

1, インスタンス変数でパラメータとハイパーパラメータを実装

2, 予測データを出力するニューラルネットワークの処理を行うクラスメソッドpredictの実装

3, ニューラルネットワークから損失関数までの処理を行うクラスメソッドlossの実装

4, 予測データの正答率を出すメソッドaccuracyの実装

5, 学習を行うための勾配式を実装

1, インスタンス変数でパラメータとハイパーパラメータを実装

class MNST_net:# 中間層は今回一層

    def __init__(self, input_size, hiden_size, output_size, weight_init_std = 0.01):
        self.params = {}#パラメータの初期化
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hiden_size)
        self.params['b1'] = np.zeros(hiden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hiden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

最初のクラスメソッドでハイパーパラメータとパラメータを実装します。

init(self):のカッコの中には、input_size(入力層のニューロン数),hiden_size(中間層のニューロン数),output_size(出力層のニューロン数)、weight_init_std(重みを小さなな値にして過学習を抑える)などの人間が設定しないといけないハイパーパラメータを記述します。

重みやバイアスなどのパラメータの実装(初期化)はparamsと言う辞書をインスタンス変数として作成し、その中に各層ごと各パラメータの種類ごとに分けて実装していきます。

重みは、np.random.randnを使って(入力信号✖︎ニューロン数)のランダムな整数が入った配列を作成し、weight_init_stdをかけることで実装(初期化)できます。

バイアスは初期値をゼロにしないといけないので、np.zerosでニューロン数のゼロが入った一次元配列を作成することで実装(初期化)できます。

2, 予測データを出力するニューラルネットワークの処理を行うクラスメソッドpredictの実装

class MNST_net:# 中間層は今回一層

    def __init__(self, input_size, hiden_size, output_size, weight_init_std = 0.01):
        self.params = {}#パラメータの初期化
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hiden_size)
        self.params['b1'] = np.zeros(hiden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hiden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):# ニューラルネットワークの処理
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']

        a1 = np.dot(x, W1) + b1# 入力層から隠れ層1段の処理
        z1 = sigmoid_function(a1)

        a2 = np.dot(z1, W2) + b2
        y = softmax_function_pro(a2)
        return y

初期化されたパラメータをそれぞれ変数に入れて、各層のニューラルネットワークのニューロンの処理を実装していきます。

3, ニューラルネットワークから損失関数までの処理を行うクラスメソッドlossの実装

class MNST_net:# 中間層は今回一層

    def __init__(self, input_size, hiden_size, output_size, weight_init_std = 0.01):
        self.params = {}#パラメータの初期化
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hiden_size)
        self.params['b1'] = np.zeros(hiden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hiden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):# ニューラルネットワークの処理
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']

        a1 = np.dot(x, W1) + b1# 入力層から隠れ層1段の処理
        z1 = sigmoid_function(a1)

        a2 = np.dot(z1, W2) + b2
        y = softmax_function_pro(a2)
        return y

    def loss(self, x, t):#  NNから損失関数までの処理
        y = self.predict(x)
        return cross_entropy_errors_label(t, y)

predictメソッドで予測データを取得して変数yに入れ、それを交差エントロピー誤差の引数に入れてreturnで返します。

4, 予測データの正答率を出すメソッドaccuracyの実装

class MNST_net:# 中間層は今回一層

    def __init__(self, input_size, hiden_size, output_size, weight_init_std = 0.01):
        self.params = {}#パラメータの初期化
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hiden_size)
        self.params['b1'] = np.zeros(hiden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hiden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):# ニューラルネットワークの処理
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']

        a1 = np.dot(x, W1) + b1# 入力層から隠れ層1段の処理
        z1 = sigmoid_function(a1)

        a2 = np.dot(z1, W2) + b2
        y = softmax_function_pro(a2)
        return y

    def loss(self, x, t):#  NNから損失関数までの処理
        y = self.predict(x)
        return cross_entropy_errors_label(t, y)

    def accuracy(self, x, t):# 正答率
        y = self.predict(x)
        y = np.argmax(y, axis = 1)# 一番確率の高いラベルのインデックスを出す
#         t = np.argmax(t, axis = 1)# 正解ラベルのインデックスを出す one-hot法

        accuracy = (y == t).sum() / float(x.shape[0])
        return accuracy

predictで予測データを出して、一番値が大きいindexをnp.argmaxで出し、それと正解データを比べて正解したものの合計 / 全体数 で正答率を出します。

5, 学習を行うための勾配式を実装

class MNST_net:# 中間層は今回一層

    def __init__(self, input_size, hiden_size, output_size, weight_init_std = 0.01):
        self.params = {}#パラメータの初期化
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hiden_size)
        self.params['b1'] = np.zeros(hiden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hiden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):# ニューラルネットワークの処理
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']

        a1 = np.dot(x, W1) + b1# 入力層から隠れ層1段の処理
        z1 = sigmoid_function(a1)

        a2 = np.dot(z1, W2) + b2
        y = softmax_function_pro(a2)
        return y

    def loss(self, x, t):#  NNから損失関数までの処理
        y = self.predict(x)
        return cross_entropy_errors_label(t, y)

    def accuracy(self, x, t):# 正答率
        y = self.predict(x)
        y = np.argmax(y, axis = 1)# 一番確率の高いラベルのインデックスを出す
#         t = np.argmax(t, axis = 1)# 正解ラベルのインデックスを出す one-hot法

        accuracy = (y == t).sum() / float(x.shape[0])
        return accuracy

     def slopeing_grad_net(self, x, t):# 初期パラメータ勾配式
        loss_c = lambda W: self.loss(x, t) # 関数化させないといけなかった 引数Wはダミー

        grads = {}
        grads['W1'] = slopeing_grad(loss_c,self.params['W1'])
        grads['b1'] = slopeing_grad(loss_c,self.params['b1'])
        grads['W2'] = slopeing_grad(loss_c,self.params['W2'])
        grads['b2'] = slopeing_grad(loss_c,self.params['b2'])

        return grads

ニューラルネットワークから損失関数までの処理を変数loss_cに入れて、それと初期化されたパラメータを各層・各種類ごとに前回作成したslopeing_gradで勾配を求めて、それをgradsと言う辞書にまとめてreturnで返します。

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

量子情報理論の基本:フィデリティ

$$
\def\bra#1{\mathinner{\left\langle{#1}\right|}}
\def\ket#1{\mathinner{\left|{#1}\right\rangle}}
\def\braket#1#2{\mathinner{\left\langle{#1}\middle|#2\right\rangle}}
$$

はじめに

量子情報理論の基本の基として、おさえておくべき事項の中で、2つの量子状態の違いについてのお話が残っていました。よく使われる指標として「フィデリティ(fidelity)」と「トレース距離(trace distance)」というものがあります。今回は、フィデリティについて勉強してみます。どのように定義されるのか、また、どんな性質があるのかということを説明した後で、量子計算シミュレータqlazyを使って、その重要な性質について、実際に計算して実感してみたいと思います。

参考にさせていただいたのは、以下の文献です。

  1. ニールセン、チャン「量子コンピュータと量子通信(3)」オーム社(2005年)
  2. 石坂、小川、河内、木村、林「量子情報科学入門」共立出版(2012年)
  3. 富田「量子情報工学」森北出版(2017年)

フィデリティの定義

内積の絶対値

フィデリティ(fidelity)は、日本語では「忠実度」と訳されますが、要は2つの状態の類似度を表す指標です。純粋状態の類似度として、直感的にわかりやすいのは内積です。2つの純粋状態を各々$\ket{\phi},\ket{\psi}$とすると、内積の絶対値は以下のように表現でき、これをフィデリティ$F(\rho,\sigma)$の定義とします。

F(\ket{\phi},\ket{\psi}) = |\braket{\phi}{\psi}|  \tag{1}

通常、純粋状態はノルム1に正規化されているので、純粋状態のフィデリティはヒルベルト空間上の2つのベクトルのなす角のコサインの絶対値であると解釈できます。したがって、両者直交している場合は0(最小値)となり、平行の場合は1(最大値)となります。

混合状態のフィデリティは、これの拡張版として定義されます。前回の記事で説明した「純粋化」をここで登場させます。任意の混合状態は適当な参照系をアンシラとして追加することで、形式的に純粋状態にすることができるというお話でした。いま、2つの混合状態を$\rho,\sigma$とし、各々を純粋化した状態を$\ket{\phi},\ket{\psi}$とします。その内積の絶対値$|\braket{\phi}{\psi}|$をフィデリティと定義します、と単純に言えれば良いのですが、そう簡単にはいきません。純粋化で得られる状態には参照系に関するユニタリの自由度があり、一意には決まらないのでした。そこで、あり得る純粋状態のすべてを考えて、その最大値をフィデリティの定義とします。つまり、フィデリティ$F(\rho,\sigma)$を

F(\rho,\sigma) = \max_{\ket{\phi},\ket{\psi}} |\braket{\phi}{\psi}|  \tag{2}

のように定義してみます。しかし、$max$が含まれている表式では使い勝手がよろしくないので、何とかしたいと思うわけです。そのため、ちょっと回り道をして、以下の2つのことを確認しておきます。

  • ユニタリの自由度を含んだ純粋化の表現
  • 演算子のトレースノルム

ユニタリの自由度を含んだ純粋化の表現

まず、純粋化の結果として得られる純粋状態を、ユニタリの自由度を含んだ形に表現し直します。
前回の記事
で見たように、注目系Aにおける混合状態、

\rho_{A} = \sum_{k=1}^{d} \lambda_{k} \ket{k}_{A} \bra{k}_{A}  \tag{3}

の純粋化は、参照系Rにおける適当な正規直交基底$\{ \ket{k}_{R} \}$を使って、

\ket{\phi} = \sum_{k=1}^{d} \sqrt{\lambda_{k}} \ket{k}_{A} \ket{k}_{R}  \tag{4}

のように表すことができます。これは、以下のように書くこともできます1

\ket{\phi} = (\sqrt{\rho} \otimes I_{R}) \sum_{k=1}^{d} \ket{k}_{A} \ket{k}_{R}  \tag{5}

ところが、ユニタリの自由度があるので、参照系R上で定義されるユニタリ演算子$U_{R}$を用いて、

\ket{\tilde{\phi}} = (\sqrt{\rho} \otimes I_{R}) \sum_{k=1}^{d} \ket{k}_{A} U_{R} \ket{k}_{R}  \tag{6}

と書くこともできます2

この式は、行列$U_{R}^{T}$を行列$U_{A}$とおき、$U_{A}$を注目系Aでのユニタリ演算子と同一視することで、以下のようにも表現できます。

\ket{\tilde{\phi}} = (\sqrt{\rho} \otimes I_{R}) \sum_{k=1}^{d} U_{A} \ket{k}_{A} \ket{k}_{R}  \tag{7}

これはそれほど自明ではないので、以下で証明してみます。

【証明】

\begin{align}
& \sum_{k=1}^{d} \ket{k}_{A} U_{R} \ket{k}_{R} \\
&= \sum_{k=1}^{d} \sum_{i=1}^{d} \ket{k}_{A} \ket{i}_{R} \bra{i}_{R} U_{R} \ket{k}_{R} \\
&= \sum_{k=1}^{d} \sum_{i=1}^{d} (U_{R})_{ik} \ket{k}_{A} \ket{i}_{R} \\
&= \sum_{k=1}^{d} \sum_{i=1}^{d} (U_{R}^{T})_{ki} \ket{k}_{A} \ket{i}_{R} \\
&= \sum_{i=1}^{d} \sum_{k=1}^{d} (U_{R}^{T})_{ik} \ket{i}_{A} \ket{k}_{R} \\
&= \sum_{i=1}^{d} \sum_{k=1}^{d} \bra{i}_{A} U_{A} \ket{k}_{A} \ket{i}_{A} \ket{k}_{R} \\
&= \sum_{k=1}^{d} U_{A} \ket{k}_{A} \ket{k}_{R}  \tag{8}
\end{align}

したがって、

\begin{align}
\ket{\tilde{\phi}} &= (\sqrt{\rho} \otimes I_{R}) \sum_{k=1}^{d} \ket{k}_{A} U_{R} \ket{k}_{R}  \\
&= (\sqrt{\rho} \otimes I_{R}) \sum_{k=1}^{d} U_{A} \ket{k}_{A} \ket{k}_{R}  \tag{9}
\end{align}

となります。(証明終)

演算子のトレースノルム

次に、回り道の2つ目です。「演算子のトレースノルム」を定義したいのですが、その前に「演算子のノルム(または絶対値)」を定義します。線形演算子$A$のノルム$|A|$は、

|A| \equiv \sqrt{A^{\dagger} A}  \tag{10}

と定義されます。$|A|$と書きますが、行列式ではありません。演算子のノルムは数値ではなく演算子ですので、ご注意ください。$A^{\dagger} A$のスペクトル分解を

A^{\dagger} A = \sum_{i} |\lambda_{i}|^{2} \ket{i} \bra{i}  \tag{11}

とすると、式(10)は、

|A| = \sqrt{A^{\dagger} A} = \sum_{i} |\lambda_{i}| \ket{i} \bra{i}  \tag{12}

と書くことができます。演算子のトレースノルム$||A||$は、これのトレースと定義されます。すなわち、

||A|| \equiv Tr |A| = Tr \sqrt{A^{\dagger} A} = \sum_{i} |\lambda_{i}|  \tag{13}

です。

$A$のトレースノルムは、ユニタリ演算子$V$を用いて、実は、

||A|| = \max_{V} |Tr(AV)|  \tag{14}

のように表すこともできます。以下で証明します。

【証明】

まず、$A$をユニタリ演算子$U$を使って、

A = U|A|  \tag{15}

のように極分解します3

\begin{align}
Tr(AV) &= Tr(U|A|V) = Tr(|A|VU) \\
&= \sum_{i} \bra{i} \sum_{j} |\lambda_{j}| \ket{j} \bra{j} VU \ket{i} \\
&= \sum_{i} \sum_{j} |\lambda_{j}| \delta_{ij} \bra{j} VU \ket{i} \\
&= \sum_{i} |\lambda_{i}| \bra{i} VU \ket{i} \tag{16}
\end{align}

において、$VU$はユニタリ(つまり、ベクトルの長さを変えない演算)なので、

|\bra{i} VU \ket{i}| \leq 1  \tag{17}

が成り立ちます。したがって、式(16)は、

Tr(AV) \leq \sum_{i} |\lambda_{i}| = ||A||  \tag{18}

となり、

||A|| = \max_{V} |Tr(AV)|  \tag{14}

が証明されました。ちなみに、式(17)からわかるように、$VU = I$のときに最大値になります。(証明終)

Uhlmannの定理

さて、それでは、式(2)の$max$を使わない表式を導出する準備が整いました。注目系Aにおける2つの混合状態を$\rho,\sigma$とし、参照系Rを使った純粋化を各々、以下のように書くことにします。ここで、式(7)の表現を使います。

\begin{align}
\ket{\tilde{\phi}} &= (\sqrt{\rho} \otimes I_{R}) \sum_{k} U_{A} \ket{k}_{A} \ket{k}_{R} \\
\ket{\tilde{\psi}} &= (\sqrt{\sigma} \otimes I_{R}) \sum_{k} U_{A} \ket{k}_{A} \ket{k}_{R} \tag{19}
\end{align}

これを使って、式(2)のフィデリティを計算します。

\begin{align}
F(\rho,\sigma) &= \max_{U_{A},V_{A}} |\braket{\tilde{\phi}}{\tilde{\psi}}| \\
&= \max_{U_{A},V_{A}} |\sum_{k,l} \bra{k}_{A} \bra{k}_{R} U_{A}^{\dagger} \sqrt{\rho} \sqrt{\sigma} V_{A} \ket{l}_{A} \ket{l}_{R}| \\
&= \max_{U_{A},V_{A}} |\sum_{k} \bra{k}_{A} U_{A}^{\dagger} \sqrt{\rho} \sqrt{\sigma} V_{A} \ket{k}_{A}| \\
&= \max_{U_{A},V_{A}} |Tr(U_{A}^{\dagger} \sqrt{\rho} \sqrt{\sigma} V_{A})| \\
&= \max_{U_{A},V_{A}} |\sqrt{\rho} \sqrt{\sigma} W)| \tag{20}
\end{align}

ここで、$W=V_{A} U_{A}^{\dagger}$とおきました。式(14)を使うと、

F(\rho,\sigma) = ||\sqrt{\rho} \sqrt{\sigma}||  \tag{21}

となります。これで、maxを使わないフィデリティの表式が得られました。これを「Uhlmannの定理」と言います4

フィデリティの性質

フィデリティの定義がわかったので、次にいくつかの重要な性質について説明します。

(1) 交換について対称

F(\rho,\sigma) = F(\sigma,\rho)  \tag{22}

が成り立ちます。定義より明らかです。

(2) 同じ状態に対するフィデリティは1

\rho = \sigma \Leftrightarrow F(\rho,\sigma) = 1  \tag{23}

が成り立ちます。密度演算子がエルミートでトレースが1という性質を使えば、簡単にわかります。

【証明】

F(\rho,\rho) = ||\sqrt{\rho} \sqrt{\rho}|| = ||\rho|| = Tr|\rho| = Tr(\rho) = 1 \tag{24}

(証明終)

(3) 直交している状態のフィデリティは0

\rho \sigma = 0 \Leftrightarrow F(\rho,\sigma) = 0  \tag{25}

が成り立ちます。定義より明らかです。

(4) フィデリティの取りうる値は0から1

式(2)の右辺の内積の絶対値は、

$0 \leq |\braket{\phi}{\psi}| \leq 1$なので、フィデリティの取りうる値は0から1です。

(5) 直積状態のフィデリティは積

F(\rho_{1} \otimes \rho_{2},\sigma_{1} \otimes \sigma_{2}) = F(\rho_{1},\sigma_{1}) F(\rho_{2},\sigma_{2})  \tag{26}

が成り立ちます。

【証明】

\begin{align}
F(\rho_{1} \otimes \rho_{2},\sigma_{1} \otimes \sigma_{2})
&= ||(\sqrt{\rho_{1}} \otimes \sqrt{\rho_{2}}) (\sqrt{\sigma_{1}} \otimes \sqrt{\sigma_{2}})|| \\
&= ||\sqrt{\rho_{1}} \sqrt{\sigma_{1}} \otimes \sqrt{\rho_{2}} \sqrt{\sigma_{2}}|| \\
&= Tr |\sqrt{\rho_{1}} \sqrt{\sigma_{1}} \otimes \sqrt{\rho_{2}} \sqrt{\sigma_{2}}| \\
&= Tr (\sqrt{\rho_{1}} \sqrt{\sigma_{1}} \otimes \sqrt{\rho_{2}} \sqrt{\sigma_{2}}) \\
&= Tr (\sqrt{\rho_{1}} \sqrt{\sigma_{1}}) Tr(\sqrt{\rho_{2}} \sqrt{\sigma_{2}}) \\
&= Tr |\sqrt{\rho_{1}} \sqrt{\sigma_{1}}| Tr|\sqrt{\rho_{2}} \sqrt{\sigma_{2}}| \\
&= F(\rho_{1},\sigma_{1}) F(\rho_{2},\sigma_{2})  \tag{27}
\end{align}

(証明終)

(6) あらゆる物理過程でフィデリティは減少しない(単調性または非減少性)

ある物理過程を表す量子チャネルを$\Gamma$とすると、

F(\rho,\sigma) \leq F(\Gamma(\rho),\Gamma(\sigma))  \tag{28}

が成り立ちます5

【証明】

$\rho,\sigma$の純粋化を各々$\ket{\phi},\ket{\psi}$とします。環境系Eの適当な状態として$\ket{0}_{E}$をもってきて追加し(テンソル積をとり)、$U$でユニタリ変換して、環境系をトレースアウトしたものが量子チャネルです。全体系の純粋状態に対する変化は、

\begin{align}
\ket{\phi} \ket{0}_{E} & \rightarrow U \ket{\phi} \ket{0}_{E} \\
\ket{\psi} \ket{0}_{E} & \rightarrow U \ket{\psi} \ket{0}_{E} \tag{29}
\end{align}

であり、フィデリティは純粋化した状態の内積の絶対値の最大値だったので、

\begin{align}
F(\Gamma(\rho), \Gamma(\sigma)) &\geq |\bra{\phi} \bra{0}_{E} U^{\dagger} U \ket{\psi} \ket{0}_{E} | \\
&= |\braket{\phi}{\psi}| = F(\rho,\sigma) \tag{30}
\end{align}

です。(証明終)

(7) フィデリティの強凹性

F(\sum_{i} p_{i} \rho_{i}, \sum_{i} q_{i} \sigma_{i}) \geq \sum_{i} \sqrt{p_{i} q_{i}} F(\rho_{i},\sigma_{i})  \tag{31}

が成り立ちます6

【証明】

$\ket{\phi_i},\ket{\psi_i}$は、$F(\rho_{i},\sigma_{i})=\braket{\phi_{i}}{\psi_{i}}$を満たすように選んだ$\rho_{i},\sigma_{i}$の純粋化とします。適当な正規直交系$\{ \ket{i} \}$をもってきて、以下の状態を定義します。

\begin{align}
\ket{\phi} &= \sum_{i} \sqrt{p_{i}} \ket{\phi_{i}} \ket{i} \\
\ket{\psi} &= \sum_{i} \sqrt{q_{i}} \ket{\psi_{i}} \ket{i} \tag{32}
\end{align}

ここで、$\ket{\phi},\ket{\psi}$は各々$\sum_{i} p_{i} \rho_{i}$および$\sum_{i} q_{i} \sigma_{i}$の純粋化なので、

\begin{align}
F(\sum_{i} p_{i} \rho_{i}, \sum_{i} q_{i} \sigma_{i}) &\geq |\braket{\phi}{\psi}| \\
&= \sum_{i} \sqrt{p_{i} q_{i}} \braket{\phi}{\psi} \\
&= \sum_{i} \sqrt{p_{i} q_{i}} F(\rho_{i},\sigma_{i})  \tag{33}
\end{align}

です。(証明終)

シミュレータで確認

それでは、上で示したフィデリティの性質のうち、6番目の「あらゆる物理過程で減少しない」という性質に注目し、それが本当なのかどうかをシミュレータで確認してみます。具体的には、2つの密度演算子をランダムに作成して、ランダムに作った量子チャネル(参照系+環境系を追加して純粋化した状態に対してランダムなユニタリ変換を実施し最後にトレースアウトする、というやり方で定義しました)を通した結果、フィデリティが確かに減少しない(=増加する)ということを見てみます。

全体のPythonコードは以下です。

import random
import numpy as np
from scipy.stats import unitary_group
from qlazypy import QState, DensOp

def random_densop(qnum_tar,qnum_ref,qnum_env):

    dim_pur = 2**(qnum_tar+qnum_ref)
    vec_pur = np.array([0.0]*dim_pur)
    vec_pur[0] = 1.0
    mat_pur = unitary_group.rvs(dim_pur)
    vec_pur = np.dot(mat_pur, vec_pur)

    dim_env = 2**qnum_env
    vec_env = np.array([0.0]*dim_env)
    vec_env[0] = 1.0

    vec_whole = np.kron(vec_pur,vec_env)

    qs = QState(vector=vec_whole)
    de = DensOp(qstate=[qs],prob=[1.0])

    qs.free()
    return de

def random_unitary(qnum):

    dim = 2**qnum
    mat = unitary_group.rvs(dim)

    return mat

if __name__ == '__main__':

    # settings
    qnum_tar = 2  # system A : target system
    qnum_ref = 2  # system R : reference system
    qnum_env = 2  # system E : environment system
    qnum_whole = qnum_tar + qnum_ref + qnum_env

    # two random states in system A+R+E (A+R:set randomly, E:set |0> initialy)
    de1_whole = random_densop(qnum_tar,qnum_ref,qnum_env)
    de2_whole = random_densop(qnum_tar,qnum_ref,qnum_env)

    # two states in system A (trace out R+E)
    de1_ini = de1_whole.partial(id=list(range(qnum_tar)))
    de2_ini = de2_whole.partial(id=list(range(qnum_tar)))

    # fidelity for initial states
    fid_ini = de1_ini.fidelity(de2_ini)

    # unitary transformation for whole system
    U = random_unitary(qnum_whole)
    de1_whole.apply(U)
    de2_whole.apply(U)

    # two states in system A (trace out R+E)
    de1_fin = de1_whole.partial(id=list(range(qnum_tar)))
    de2_fin = de2_whole.partial(id=list(range(qnum_tar)))

    # fidelity for final states
    fid_fin = de1_fin.fidelity(de2_fin)

    # result
    print("* fidelity(ini) =", fid_ini)
    print("* fidelity(fin) =", fid_fin)

    if fid_ini < fid_fin:
        print("OK!")
    else:
        print("NG!")

    # free memory
    de1_whole.free()
    de2_whole.free()
    de1_ini.free()
    de2_ini.free()
    de1_fin.free()
    de2_fin.free()

何をやっているか、順に説明します。

# settings
qnum_tar = 2  # system A : target system
qnum_ref = 2  # system R : reference system
qnum_env = 2  # system E : environment system
qnum_whole = qnum_tar + qnum_ref + qnum_env

まず、量子ビット数を適当に設定します。密度演算子が定義されている注目系Aの量子ビット数を2、それを純粋化するための参照系Rの量子ビットを2、注目系Aの周囲を取り巻く環境系Eの量子ビットを2とします。

# two random states in system A+R+E (A+R:set randomly, E:set |0> initialy)
de1_whole = random_densop(qnum_tar,qnum_ref,qnum_env)
de2_whole = random_densop(qnum_tar,qnum_ref,qnum_env)

全体系に対する純粋状態をランダムに作ります。random_densop関数で実行しています。その中身を見てみます。

dim_pur = 2**(qnum_tar+qnum_ref)
vec_pur = np.array([0.0]*dim_pur)
vec_pur[0] = 1.0
mat_pur = unitary_group.rvs(dim_pur)
vec_pur = np.dot(mat_pur, vec_pur)

dim_env = 2**qnum_env
vec_env = np.array([0.0]*dim_env)
vec_env[0] = 1.0

vec_whole = np.kron(vec_pur,vec_env)

qs = QState(vector=vec_whole)
de = DensOp(qstate=[qs],prob=[1.0])

最初の4行で、注目系Aと参照系Rを合わせた系で定義される $\ket{0}^A\ket{0}^R$ に相当するベクトルを作成し、scipyの関数で作ったランダムなユニタリ演算を実施して、ランダムな純粋状態を作り、変数vec_purに格納します。注目系Aの密度演算子をまず作って純粋化すべきところですが、純粋化したものをランダムに用意することにしました(すみませせん、処理を端折りました。同じことなので)。次に、環境系Eに関して、$\ket{0}^{E}$に相当するベクトルを作成し、変数vec_envに格納します。2つのベクトルのテンソル積(クロネッカー積)をnumpyの関数を使って実行し、変数vec_wholeに格納します。これで、注目系でのランダムな密度演算子に対応した、全体系の純粋状態ができたことになります。これに基づき、密度演算子のインスタンスdeを生成し、リターンします。

main部に戻ります。

# two states in system A (trace out R+E)
de1_ini = de1_whole.partial(id=list(range(qnum_tar)))
de2_ini = de2_whole.partial(id=list(range(qnum_tar)))

全体系の密度演算子から参照系と環境系をトレースアウトして、注目系に注目します。密度演算子に対するpatialメソッドに引数として注目系の量子番号リストを与えることで実行できます。今の場合、引数には[0,1]が入ることになります。これで、2つのランダムな初期密度関数が得られたので、、、

# fidelity for initial states
fid_ini = de1_ini.fidelity(de2_ini)

で、本日のメインイベントであるフィデリティを計算します。qlazypyの密度演算子クラスDensOpにfidelityメソッドを追加しました(v0.0.27)。この1行で終わりです。中ではnumpy.linlagを使っていて、固有値問題を解いて得られた固有値のルートを計算するとか、トレースを計算するとか、やっています。

# unitary transformation for whole system
U = random_unitary(qnum_whole)
de1_whole.apply(U)
de2_whole.apply(U)

random_unitary関数でランダムなユニタリ行列を取得します。関数定義を見ていただければわかる通り、scipyの関数を召喚しています。このユニタリを全体系の密度演算子に施すため、密度演算子クラスDensOpのapplyメソッドを使っています。これで、初期状態がランダムに定義された量子チャネルを通ったことになります。

# two states in system A (trace out R+E)
de1_fin = de1_whole.partial(id=list(range(qnum_tar)))
de2_fin = de2_whole.partial(id=list(range(qnum_tar)))

量子チャネルを通った後の状態に対して、部分トレースをとり、注目系での密度演算子を取得します。

# fidelity for final states
fid_fin = de1_fin.fidelity(de2_fin)

先ほどと同様、フィデリティを計算します。

# result
print("* fidelity(ini) =", fid_ini)
print("* fidelity(fin) =", fid_fin)

if fid_ini <= fid_fin:
    print("OK!")
else:
    print("NG!")

結果を表示します。フィデリティが増加していれば「OK!」、そうでなければ「NG!」を表示します。

さて、実行結果です。

* fidelity(ini) = 0.7344321087768828
* fidelity(fin) = 0.9503724941444357
OK!

というわけで、フィデリティが増加していることがわかります。何度も実行しましたが必ず増加しました。

おわりに

「はじめに」で述べましたが、量子状態の違いを定量化する指標として、今回説明した「フィデリティ」の他に、「トレース距離」というものもあります。これは名前の通り、状態がどれだけ離れているかを表す指標です。なので「フィデリティ」とはちょうど逆の関係にあります。この指標もよく使われるということなので、次回は「トレース距離」について勉強したいと思います。

以上


  1. $\rho=\sum_{k} \lambda_{k} \ket{k}^{A} \bra{k}^{A}$から、容易にわかると思います。 

  2. これは大丈夫ですよね。ユニタリの自由度があるということを素直に表しているだけです。 

  3. 任意の線形演算子$A$に対して、あるユニタリ演算子(正確には等距離演算子)$U$があって、$A = U|A|$と表すことができるという定理があります。詳細は量子情報科学入門の付録をご参照ください。直感的には、$A^{\dagger} A$を計算してみれば、確かにそうだということがわかると思います。任意の複素数$z$を$z=|z|e^{i\theta}$と表すことができますが、それに少し似ています。 

  4. ニールセン、チャン量子情報科学入門では、式(21)をフィデリティの定義として式(2)が成り立つことをもって「Uhlmannの定理」という説明がされていました。今回の記事では、量子情報工学の議論進行に従い、逆の流れで証明しました。その方が直感的にわかりやすいと思ったからです。 

  5. 最初にあった識別可能な特徴をもった2つの状態が、環境系と相互作用しながら次第に平衡状態に達して識別不能になっていくという風に考えれば良いと思います。一瞬、大小関係が逆じゃね?と思った方は速攻誤解を正しましょう(自分のことです、汗)。 

  6. なぜこれを「凹性」というのか?$\space 0 \leq p \leq 1$となる$p$に対し、関数$f(x)$が$f(px_{1} + (1-p)x_{2}) \leq pf(x_{1}) + (1-p)f(x_{2})$という性質を満たすとき、その関数$f$を凸関数と言い(関数のカーブを思い描くと上に凸ですよね)、不等号が逆の場合、凹関数と言います(こっちは逆に下に凸ですね、すなわち上に凹)。このアナロジーで理解しておけば良いと思います。ここでは「強凹性」のみを取り上げましたが、「強」がつかない「凹性」もあります。ニールセン、チャンにいくつかのバリエーションが紹介されています。 

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

pythonで数字当てゲーム : チャンスは6回、範囲は0から10,000まで

shiracamus さん

pythonの勉強をしていく過程で、Qiitaもはじめました。なにか投稿しようと思い、python勉強のために作成したものを投稿します。

はじめに

最初に数字当てゲームを知ったのは
「退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング」
という本でした。

この本には、1から20までの数字を6回までに当てるゲームのコードが、載っていました。

random.randint()で選ばれた数字を、int ( input () )で入力してもらう。というものです。

これをもとに、数字の範囲を広げました。(+9981!!)
が、しかし、当たるわけがありません。:joy:

そこで、自分なりにヒントのコードを加えて、当てやすくしたり、時間計測を追加したりして、ゲームとしてなんとか成り立つようにしました。

初心者なので細かくコメントを入れています。(本来は適切ではない……)

1.ヒントの表示

数字入力時に外した場合

その数字より、大きいか、小さいかを返します。
これは本にも載っていましたが、数字が近かった場合(200以内)、表示を変えるようにしました。

# 解答者が選んだ数字が大きいか、小さいか返す関数
def high_low():
    # 数字が200以内なら文言が変わる
    if (guess < secret_number) and (guess + 200 >= secret_number):
        return ("\nもう少し、大きい数字です。(200以内)")
    if (guess > secret_number) and (guess <= secret_number + 200):
        return ("\nもう少し、小さい数字です。(200以内)")
    # 数字が大きいか、小さいかの文言
    if guess < secret_number:
        return ("\nもっと、大きい数字です。")
    if guess > secret_number:
        return ("\nもっと、小さい数字です。")

数字を外して、「ヒントを使いますか?」で「1:はい」を選んだ場合

外した回数によって、ヒントの内容が変わります。

・ 1回外した場合 → 偶数か奇数か
・ 2回外した場合 → 3で割れるか、5で割れるか(Fizz Buzz)
・ 3回外した場合 → 各桁の数字を足す
・ 4回外した場合 → 下2桁目を表示
・ 5回外した場合 → ±10以内の数字をランダムで表示

「2:いいえ」を選んだ場合は、ヒントは表示されません。

# 各ヒントをまとめた関数
def hint(select):
    # 1回目で外した場合、偶数か奇数かを表示
    if guesses_taken == 1:
        print ( "\n偶数か奇数かを表示する……" )
        # 2で割り、余りがなかったら、True
        if not (secret_number % 2):
            return ("…… 偶数!!")
        # 2で割り、余りがあったら、True
        if (secret_number % 2):
            return ("…… 奇数!")

    # 2回目で外した場合、3で割れるか、5で割れるか、を表示
    if guesses_taken == 2:
        print ( "\n3で割れるか、5で割れるか、表示する……" )
        # 3で割っても5で割っても余りがない
        if not (secret_number % 3) and not (secret_number % 5):
            return ("…… 両方で割れる")
        # 3で割ったら余りがない
        if not (secret_number % 3):
            return ("…… 3で割り切れる(5で割れない)")
        # 5で割ったら余りがない
        if not (secret_number % 5):
            return ("…… 5で割り切れる(3で割れない)")
        return ("…… どちらでも割れない")

    # 3回目で外した場合、各桁の数字を表足す
    if guesses_taken == 3:
        # 答えの数字を文字列に変えて、dに格納
        # dにある文字列を、1文字ずつ数字に変えリスト化
        digits = [int ( d ) for d in str ( secret_number )]
        return ("\n各桁の数字を足す……"
                "\n…… " + str ( sum ( digits ) ))  # 各数字を足して文字列型にする

    # 4回目で外した場合、下2桁目を表示
    if guesses_taken == 4:
        return ("\n下2桁目を表示する……"
                "\n…… " + str ( secret_number )[-2])  # 後ろから2文字目を取得

    # 5回目で外した場合、±10以内のランダム数字を表示
    if guesses_taken == 5:
        return ("\n±10以内の数字をランダムで表示する……"
                "\n…… " + str ( random.randint ( secret_number - 10, secret_number + 10 ) ))

2.計測時間の表示と、使用したヒントの表示

調べたら、time.perf_counter ()が精密らしいので実装しました。

start_time = time.perf_counter ()
"""コード"""
end_time = time.perf_counter ()

# 計測した時間の計算(秒数)
tim = end_time - start_time
# 秒数から分数のみ(秒数なし)を計算
mint = tim // 60
# 秒数から秒数のみ(分数なし)を計算
secd = (tim % 60) - mint
# 分数は小数点以下を非表示(計算の時点で必ず0のため)、秒数は小数点第一位まで表示
print ( "所要時間は {:.0f} 分 {:.1f} 秒".format ( mint, secd ) )

「ヒントを使いますか?」で「1:はい」を選んだ場合に、空の使用リストに格納して表示

# 使用したヒントを表示するための記録用、空リスト
record = []

 # ヒントを、順番に列挙したタプルを用意
    used_ability = ("偶数か奇数か", "3で割れるか、5で割れるか", "各桁の数字を足す", "下2桁目を表示", "±10以内の数字をランダムで表示")
    print ( "\nヒントを使いますか?\n 1:はい / 2:いいえ" )
    yesno = int ( input () )
    # yesnoが1だった場合
    if yesno == 1:
        # 解答数に応じた能力を使う
        ability = hint ( guesses_taken )
        print ( ability )
        # 能力のタプルから、解答回数に応じた能力名を、onebyoneに格納
        onebyone = used_ability[guesses_taken - 1]
        # 順番にrecord = []に格納にしてリスト化
        record.append ( onebyone )
    # yesnoが1以外だった場合pass
    if not yesno == 1:
        pass

# 使用したヒントを表示
print ( "\n使用したヒント" )
print ( record )

コード

guessTheNumber.py
# randomをインポート
import random
# timeをインポート
import time


# 解答者が選んだ数字が大きいか、小さいか返す関数
def high_low():
    # 数字が200以内なら文言が変わる
    if (guess < secret_number) and (guess + 200 >= secret_number):
        return ("\nもう少し、大きい数字です。(200以内)")
    if (guess > secret_number) and (guess <= secret_number + 200):
        return ("\nもう少し、小さい数字です。(200以内)")
    # 数字が大きいか、小さいかの文言
    if guess < secret_number:
        return ("\nもっと、大きい数字です。")
    if guess > secret_number:
        return ("\nもっと、小さい数字です。")


# 各ヒントをまとめた関数
def hint(select):
    # 1回目で外した場合、偶数か奇数かを表示
    if guesses_taken == 1:
        print ( "\n偶数か奇数かを表示する……" )
        # 2で割り、余りがなかったら、True
        if not (secret_number % 2):
            return ("…… 偶数!!")
        # 2で割り、余りがあったら、True
        if (secret_number % 2):
            return ("…… 奇数!")

    # 2回目で外した場合、3で割れるか、5で割れるか、を表示
    if guesses_taken == 2:
        print ( "\n3で割れるか、5で割れるか、表示する……" )
        # 3で割っても5で割っても余りがない
        if not (secret_number % 3) and not (secret_number % 5):
            return ("…… 両方で割れる")
        # 3で割ったら余りがない
        if not (secret_number % 3):
            return ("…… 3で割り切れる(5で割れない)")
        # 5で割ったら余りがない
        if not (secret_number % 5):
            return ("…… 5で割り切れる(3で割れない)")
        return ("…… どちらでも割れない")

    # 3回目で外した場合、各桁の数字を表足す
    if guesses_taken == 3:
        # 答えの数字を文字列に変えて、dに格納
        # dにある文字列を、1文字ずつ数字に変えリスト化
        digits = [int ( d ) for d in str ( secret_number )]
        return ("\n各桁の数字を足す……"
                "\n…… " + str ( sum ( digits ) ))  # 各数字を足して文字列型にする

    # 4回目で外した場合、下2桁目を表示
    if guesses_taken == 4:
        return ("\n下2桁目を表示する……"
                "\n…… " + str ( secret_number )[-2])  # 後ろから2文字目を取得

    # 5回目で外した場合、±10以内のランダム数字を表示
    if guesses_taken == 5:
        return ("\n±10以内の数字をランダムで表示する……"
                "\n…… " + str ( random.randint ( secret_number - 10, secret_number + 10 ) ))


# 使用したヒントを表示するための記録用、空リスト
record = []

# 0 ~ 10,000までの数字をランダムに決め、secret_numberに格納
secret_number = random.randint ( 0, 10000 )
print ( "0から10,000までの数字を当ててください。" )

# 時間の計測開始
start_time = time.perf_counter ()

# 最大6回繰り返す
for guesses_taken in range ( 1, 7 ):
    print ( "\n数字を入力してください。" )
    # 入力してもらった数字をguessに格納
    # input()は文字列を返すので、int()で整数値に変換
    guess = int ( input () )

    if guesses_taken == 6:
        break

    # 数字が当たればbreakでforループから抜け出す
    if guess == secret_number:
        break

    print ( high_low () )

    # ヒントを、順番に列挙したタプルを用意
    used_ability = ("偶数か奇数か", "3で割れるか、5で割れるか", "各桁の数字を足す", "下2桁目を表示", "±10以内の数字をランダムで表示")
    print ( "\nヒントを使いますか?\n 1:はい / 2:いいえ" )
    yesno = int ( input () )
    # yesnoが1だった場合
    if yesno == 1:
        # 解答数に応じた能力を使う
        ability = hint ( guesses_taken )
        print ( ability )
        # 能力のタプルから、解答回数に応じた能力名を、onebyoneに格納
        onebyone = used_ability[guesses_taken - 1]
        # 順番にrecord = []に格納にしてリスト化
        record.append ( onebyone )
    # yesnoが1以外だった場合pass
    if not yesno == 1:
        pass

# 時間の計測終了
end_time = time.perf_counter ()

# 当たった場合とハズレた場合で文言を変える
if guess == secret_number:
    print ( "\nグッド! " + str ( '{:,}'.format ( guesses_taken ) ) + "回で当たり!" )
if guess != secret_number:
    print ( "\n結果だけを求めていると、人は近道をしたがるものだ……………"
            "\n近道した時、真実を見失うかもしれない"
            "\nやる気もしだいに失せていく"
            "\n大切なのは『真実に向かおうとする意志』だと思っている"
            "\n……正解は " + str ( '{:,}'.format ( secret_number ) ) )

# 使用したヒントを表示
print ( "\n使用したヒント" )
print ( record )

# 計測した時間の計算(秒数)
tim = end_time - start_time
# 秒数から分数のみ(秒数なし)を計算
mint = tim // 60
# 秒数から秒数のみ(分数なし)を計算
secd = (tim % 60) - mint
# 分数は小数点以下を非表示(計算の時点で必ず0のため)、秒数は小数点第一位まで表示
print ( "所要時間は {:.0f} 分 {:.1f} 秒".format ( mint, secd ) )

おわりに

ちょっとした気持ちで、範囲を0から10,000にしたら、あれもつけたい、これもつけたいで、想像以上に時間を取られました。

本にも書いてありましたが、必勝法は二分探索(binary search)
二分探索で迫れば、最後のヒントを見なくても、結構当てられます。

pythonもQiitaも、まずは1歩

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

Python3+OpenCVで顔認証-Part0 準備編

前提

このシリーズは、以下の環境で動作確認などを行っています。環境によっては細かい部分が異なることがありますが、わからないことはコメントへどうぞ。

環境
OS: Windows 10 Pro (1809, 17763.737)
CPU: Intel(R) Core(TM) i7-8500Y@1.50GHz x4
RAM: 8GB

準備するもの

インストールとか準備

Visual Studio Code

自分の環境にあったものを落としましょう。今回は最新版の1.39.2 を使います。
ダウンロードしたら、"VSCodeUserSetup-x64-1.39.2.exe"みたいなファイルがあると思います。(環境によってちょっとずつ違います)これを実行しましょう。そして、ダイアログに従ってインストールします。

プラグイン

拡張機能です。便利なので入れときましょう。(というか必要)
赤枠のところをポチッとして、検索すれば入ります。
text22.png

日本語パック - https://marketplace.visualstudio.com/items?itemName=MS-CEINTL.vscode-language-pack-ja
Python - https://marketplace.visualstudio.com/items?itemName=ms-python.python
Indent-Rainbow - https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow
Bracket Pair Colorizer 2 (Beta) - https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2

Python3.8.0

これも同じです。自分の環境のものを落として実行します。僕の場合は"python-3.8.0-amd64.exe"でした。
そして、コマンドプロンプトで動作確認。

実行結果
C:\Users\xxx> py
 Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:37:50) [MSC v.1916 64 bit (AMD64)] on win32
 Type "help", "copyright", "credits" or "license" for more information.
>>>

OSが違っても、表示される内容はほぼ同じです。バージョンがあっていて起動できればOK。
(Linux, MacOSXはpython3だったりpythonだったりします。)

OpenCV

py -m pip install opencv-python
以上。かーんたん♪

動作確認
C:\Users\xxx> py
 Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:37:50) [MSC v.1916 64 bit (AMD64)] on win32
 Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>>

何も表示されなければOK。

次回

次回は、顔認証の学習に必要な画像を作るためのプログラムです。お楽しみに~

GitHub

自分で全部コードを打つのはちょっと・・・
というめんどくさがり屋さんのためにGitHubにソースなど一式を上げときます。
記事の更新とほぼ同時にあげます。
Hiro527/OpenCV-Py3-Face - https://github.com/Hiro527/OpenCV-Py3-Face

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

【ベイズ深層学習】Pyroでベイズニューラルネットワークモデルの近似ベイズ推論の実装

今回は,確率的プログラミング言語『Pyro』を使って2層ベイズニューラルネットワークモデルに対して変分推論(平均場近似),ラプラス近似,MCMC(NUTS)の3つの手法を試してみました.
『ベイズ深層学習』第5章5.1節の図5.2のデータを使います.

環境

Python 3.7.5
PyTorch 1.3.0
Pyro 0.5.1

ソースコード

今回のソースコードはGitHub上(こちら)に上げました.

ベイズニューラルネットワークモデル

入力の次元を$H_{0}$, 出力の次元を$D$とするデータ集合$\mathcal{D} = \{ \mathbf{x}_n, \mathbf{y}_n \}_{n = 1}^{N}$が与えられたとします.ただし,データは$\mathcal{i.i.d}$であると仮定します.

この時,入力$\mathbf{x}_n \in \mathbb{R}^{H_{0}}$に対する出力$\mathbf{y}_n \in \mathbb{R}^{D}$を予測する回帰問題を考えます.
ここで,回帰モデルとして,次のベイズニューラルネットワークモデルを仮定しようと思います.
観測モデルは, $$ p\left( \mathbf{y}_n | \mathbf{x}_n, \mathbf{W} \right) = \mathcal{N}\left(
\mathbf{y}_n | \mathbf{f}(\mathbf{x}_n ; \mathbf{W}), \sigma_y^2\mathbf{I} \right) $$ ここで,$\mathbf{f}(\mathbf{x}_n ; \mathbf{W})$はパラメータを$\mathbf{W}$としたニューラルネットワークです.
パラメータ$\mathbf{W}$の事前分布は, $$ p(\mathbf{W}) = \prod_{l=1}^{L}\prod_{i=1}^{H_{l}}\prod_{j=1}^{H_{l-1}}\mathcal{N}(w_{i,j}^{(l)} | 0, \sigma_w^2) $$
ここで,$L$はニューラルネットワークの層の数,$H_l$は第$l$層のユニット数で,$H_L = D$です.
以上より,入力データ$ \mathbf{X} = \{ \mathbf{x}_1, \ldots, \mathbf{x}_N \} $が与えられたもとでの,観測データ$ \mathbf{Y} = \{ \mathbf{y}_1, \ldots, \mathbf{y}_N \} $とパラメータ$\mathbf{W}$の同時分布は,

\begin{align}
p(\mathbf{Y}, \mathbf{W} | \mathbf{X}) 
  &= p(\mathbf{W})\prod_{n=1}^{N}p\left( \mathbf{y}_n | \mathbf{x}_n, \mathbf{W} \right) \\
  &= \left\{\prod_{l=1}^{L}\prod_{i=1}^{H_{l}}\prod_{j=1}^{H_{l-1}}\mathcal{N}(w_{i,j}^{(l)} | 0, \sigma_w^2)\right\}\prod_{n=1}^{N}\mathcal{N}\left( 
\mathbf{y}_n | \mathbf{f}(\mathbf{x}_n ; \mathbf{W}), \sigma_y^2\mathbf{I} \right)
\end{align}

となります.

ニューラルネットワークの仮定

今回は,$L = 2$,$D = 1$,活性化関数$\phi$を双曲線正接関数としたニューラルネットワークを考えることにします.

f(\mathbf{x}_n ; \mathbf{W}) = \sum_{h_{1}=1}^{H_{1}} w_{h_{1}}^{(2)} {\rm Tanh} \left( \sum_{h_{0}=1}^{H_{0}} w_{h_{1}, h_{0}}^{(1)} x_{n, h_{0}} \right)

実装

確率的プログラミング言語『Pyro』を利用して実装を行います.
Pyroについて全く知識のない方は,公式チュートリアルHELLO CYBERNETICSさんの記事等をご覧になると良いと思います.

以下のコードでは自作クラスを用いているため,selfがたくさん出てきて見づらいかもしれませんがご容赦ください.BNNクラスがベイズニューラルネットワークモデルのクラスとなっています.

コードの説明が長々と続くため,結果だけ見たい方はこの節は飛ばしてください.

共通事項

各層の次元

データは入出力共に1次元のものを使いますが,バイアス項を導入するために,入力ベクトルは$(x, 1)^{\top}$と,2次元に拡張します.

H_0 = 2  # 入力次元
H_1 = 4  # 中間層のユニット数
D = 1  # 出力次元

訓練データセット

『ベイズ深層学習』第5章5.1節の図5.2のデータを読み取って使うことにします.

# data
data = torch.tensor([[-4.5, -0.22],
                     [-4.4, -0.10],
                     [-4.0, 0.00],
                     [-2.9, -0.11],
                     [-2.7, -0.33],
                     [-1.5, -0.20],
                     [-1.3, -0.08],
                     [-0.8, -0.21],
                     [0.1, -0.34],
                     [1.5, 0.10],
                     [2.0, 0.11],
                     [2.1, 0.14],
                     [2.6, 0.21],
                     [3.5, 0.23],
                     [3.6, 0.38]])
x_data = data[:, 0].reshape(-1, 1)
x_data = torch.cat([x_data, torch.ones_like(x_data)], dim=1) # biasごと入力に含ませる
y_data = data[:, 1]

plotしてみます.

data.png

ハイパーパラメータ

比較のため,ハイパーパラメータは各手法で以下の共通の値とします.

w_sigma = torch.tensor(0.75)
y_sigma = torch.tensor(0.09)

変分推論

まず,変分推論によるベイズ推論をベイズニューラルネットワークモデルに適用してみます.
今回は,変分推論の中でも最もシンプルな,平均場近似を採用してみます.

ライブラリのimport

必要なライブラリをimportします.

import matplotlib.pyplot as plt
import torch
import pyro
from pyro.distributions import Normal, Delta
from pyro.infer.autoguide.guides import AutoDiagonalNormal
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam
from pyro.infer.predictive import Predictive

生成モデル

確率的プログラミング言語であるPyroのフレームワークでモデルの記述を行うことで,SVIクラスを利用して変分推論を簡単に行うことができます.

BNNクラス
    def model(self, x_data, y_data):
        # パラメータの生成
        with pyro.plate("w1_plate_dim2", self.hidden_size):
            with pyro.plate("w1_plate_dim1", self.input_size):
                w1 = pyro.sample("w1", Normal(0, self.w_sigma))
        with pyro.plate("w2_plate_dim2", self.output_size):
            with pyro.plate("w2_plate_dim1", self.hidden_size):
                w2 = pyro.sample("w2", Normal(0, self.w_sigma))

        f = lambda x: torch.mm(torch.tanh(torch.mm(x, w1)), w2)
        # 観測データの生成
        with pyro.plate("map", len(x_data)):
            prediction_mean = f(x_data).squeeze()
            pyro.sample("obs", Normal(prediction_mean, self.y_sigma), obs=y_data)
            return prediction_mean

Pyroでは,自分の定めた確率的生成モデルをmodelという関数にて記述します.
記述の仕方としては,各確率変数の従う分布からサンプルを生成していくように記述していきます.
サンプルの生成には,pyro.sample(site_name, distribution)を使います.
確率変数の名前をsite_nameで,確率変数の従う分布をdistributionで指定することで,サンプルが生成されます.
また,pyro.plateというコンテキストマネージャが存在します.このwithステートメント内では独立にサンプルが生成されることになります.したがって,独立性を仮定している場合はpyro.plateを使いましょう.

変分モデル

Pyroで変分推論を行う場合,変分モデルも記述する必要があります.modelと同様に各確率変数に関して近似分布を記述してサンプルを生成させても良いのですが,pyro.infer.autoguide.guides.AutoGuideクラスを使うことで,典型的な変分モデルであれば自動的に用意してくれます.

self.guide = AutoDiagonalNormal(self.model)

今回はパラメータの近似分布としてAutoDiagonalNormal,つまり対角ガウス分布を使います.これによって,すべてのパラメータが完全独立分解近似されます.従って,平均場近似を行っていることになります.

推論

生成モデル(model)と変分モデル(guide)を定義したので,準備は完了です.
実際に変分推論をしていきます.

BNNクラス
    def VI(self, x_data, y_data, num_samples=1000, num_iterations=30000):
        self.guide = AutoDiagonalNormal(self.model)
        optim = Adam({"lr": 1e-3})
        loss = Trace_ELBO()
        svi = SVI(self.model, self.guide, optim=optim, loss=loss)

        # train
        pyro.clear_param_store()
        for j in range(num_iterations):
            loss = svi.step(x_data, y_data)
            if j % (num_iterations // 10) == 0:
                print("[iteration %05d] loss: %.4f" % (j + 1, loss / len(x_data)))

        # num_samplesだけ事後分布からサンプルを生成
        dict = {}
        for i in range(num_samples):
            sample = self.guide()  # sampling
            for name, value in sample.items():
                if not dict.keys().__contains__(name):
                    dict[name] = value.unsqueeze(0)
                else:
                    dict[name] = torch.cat([dict[name], value.unsqueeze(0)], dim=0)
        self.posterior_samples = dict

まずはじめに,変分推論をどのような設定で行うかを定めるために,SVIクラスのインスタンスを生成します(SVI(model, guide, optim, loss, ...)).引数としては,生成モデルmodel,変分モデルguide,最適化手法optim,損失関数lossを渡す必要があります.
最適化手法はAdamを,損失関数は変分下界ELBO(の-1倍)を利用します.

後は,svi.step(x_data, y_data)によって近似分布を真の事後分布に近づけていきます.

近似事後分布が求まったら,num_samplesだけ事後分布からパラメータをサンプリングして,self.posterior_samplesにサンプルの辞書を格納します.

予測

事後分布の推論が完了したので,事後予測分布を推論してみます.
Pyroでは,事後予測分布からのサンプルを生成することもできます.

BNNクラス
    def predict(self, x_pred):
        def wrapped_model(x_data, y_data):
            pyro.sample("prediction", Delta(self.model(x_data, y_data)))

        predictive = Predictive(wrapped_model, self.posterior_samples)
        samples =  predictive.get_samples(x_pred, None)
        return samples["prediction"], samples["obs"]

ここで,$y$の予測分布だけでなく,$y$の平均,つまりニューラルネットワークの出力$f$の予測分布も取得してみることにしましょう.
modelで返り値としていた$y$の平均prediction_meanを,その点でのみ$\infty$の確率密度を持つ分散$0$の分布に従う確率変数として,modelをラッピングしたwrapped_modelを新たに定義しました.
これによって,$y$の平均も確率変数として扱うことができるようになり,予測分布を求めることができます.

Predictive(model, posterior_samples).get_samples(x_pred, None)で,事後予測分布からのサンプルを得ることができます.
今回は,$y$("obs")とその平均("prediction")の事後予測分布からのサンプルを取得しています.

結果の図示

それでは,予測結果を図示してみましょう.

VIforBNN.png

左は$y$の平均,つまりニューラルネットワークの出力$f$の予測分布からのサンプルです.
右は$y$の予測分布からのサンプルです.$y$の分散を考慮しているので左に比べて分散が大きくなっていることがわかります.
どちらも緩やかな予測曲線を描いていますね.

ラプラス近似

次は,ラプラス近似を適用してみます.
pyro.infer.autoguide.guidesにはラプラス近似を行えるAutoLaplaceApproximationクラスが存在しますが,使い方が間違っているのか望ましい結果が出せなかったので,MAP推定をPyroで行い,そこからはPyTorchの自動微分を利用して実装を行いました.

ライブラリのimport

必要なライブラリをimportします.

import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import pyro
from pyro.distributions import Normal
from pyro.infer.autoguide.guides import AutoDelta
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam

生成モデル

後に利用するため,PyTorchのニューラルネットワークモデルをまず用意します.

# バイアス項なし全結合Layerを定義
class NonBiasLinear(nn.Module):
    def __init__(self, input_size, output_size):
        super(NonBiasLinear, self).__init__()
        self.weight = nn.Parameter(data=torch.randn(input_size, output_size), requires_grad=True)

    def forward(self, input_tensor):
        return torch.mm(input_tensor, self.weight)


# 2層ニューラルネットワークモデル
class Net(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Net, self).__init__()
        self.fc1 = NonBiasLinear(input_size, hidden_size)
        self.fc2 = NonBiasLinear(hidden_size, output_size)

    def forward(self, x):
        output = self.fc1(x)
        output = torch.tanh(output)
        output = self.fc2(output)
        return output

Pyroでは,PyTorchのnn.Moduleクラスで記述した決定論的なモデルをpyro.random_moduleによって確率的生成モデルへと"lift"することができます.ただし,決定論的なモデルから確率的生成モデルにするために,各確率変数の分布を記述する必要があることには注意してください.

BNNクラス
    def model(self, x_data, y_data):
        # 事前分布
        w1_size = (self.input_size, self.hidden_size)
        w2_size = (self.hidden_size, self.output_size)
        w1_prior = Normal(torch.zeros(size=w1_size), self.w_sigma * torch.ones(size=w1_size))
        w2_prior = Normal(torch.zeros(size=w2_size), self.w_sigma * torch.ones(size=w2_size))
        priors = {'fc1.weight': w1_prior, 'fc2.weight': w2_prior}
        # lift
        lifted_module = pyro.random_module("module", self.net, priors)
        lifted_bnn_model = lifted_module()
        with pyro.plate("map", len(x_data)):
            prediction_mean = lifted_bnn_model(x_data).squeeze()
            pyro.sample("obs", Normal(prediction_mean, self.y_sigma), obs=y_data)
            return prediction_mean

推論

ラプラス近似では,まずMAP推定値を計算し,それから後述する式を計算して近似事後分布を求めます.
ラプラス近似によるベイズ推論の理論の詳細に関しては,『ベイズ深層学習』4.2.3節や,5.1.2節を参照してください.

まず,MAP推定値を求めます.AutoDeltaクラスを利用すれば,Pyroの変分推論の枠組みでMAP推定値を得ることができます.

BNNクラス
    # MAP推定
    def MAPestimation(self, x_data, y_data, num_iterations=10000):
        guide = AutoDelta(self.model)
        svi = SVI(self.model, guide, Adam({"lr": 1e-3}), loss=Trace_ELBO())

        # train
        pyro.clear_param_store()
        for j in range(num_iterations):
            loss = svi.step(x_data, y_data)
            if j % (num_iterations // 10) == 0:
                print("[iteration %05d] loss: %.4f" % (j + 1, loss / len(x_data)))

        # MAP推定値を取得
        param_dict = {}
        for name, value in pyro.get_param_store().items():
            param_dict[name] = value.data
        w1_MAP = param_dict['auto_module$$$fc1.weight']
        w2_MAP = param_dict['auto_module$$$fc2.weight']
        self.net.fc1.weight.data = w1_MAP
        self.net.fc2.weight.data = w2_MAP
        return w1_MAP, w2_MAP

求まったMAP推定値$\mathbf{W}_{\rm MAP}$に対して,ラプラス近似によるパラメータ$\mathbf{W}$の近似事後分布は,

q(\mathbf{W}) = \mathcal{N}(\mathbf{W} | \mathbf{W}_{\rm MAP}, \left\{ \mathbf{\Lambda}(\mathbf{W}_{\rm MAP}) \right\}^{-1})

となります.ただし,$\mathbf{W}, \mathbf{W}_{\rm MAP}$は重みパラメータを1列に並べた列ベクトルの形になっているものとします.
ここで,精度行列$\mathbf{\Lambda}$は,

\begin{align}
\mathbf{\Lambda} 
  &= - \nabla_{\mathbf{W}}^2 \ln{p(\mathbf{W} | \mathbf{Y}, \mathbf{X})} \\
  &= \frac{1}{\sigma_{y}^{2}} \mathbf{I} + \frac{1}{\sigma_{w}^{2}} \nabla_{\mathbf{W}}^2 E(\mathbf{W}) \\
  &\approx \frac{1}{\sigma_{y}^{2}} \mathbf{I} + \frac{1}{\sigma_{w}^{2}} \sum_{n=1}^{N} \left( \nabla_{\mathbf{W}}\mathbf{a}_{n}^{(L)} \right) \left( \nabla_{\mathbf{W}}\mathbf{a}_{n}^{(L)} \right)^\top
\end{align}

と近似します.ここで,ヘッセ行列の計算に『ベイズ深層学習』p.34 式(2.58)の近似を用いることにしました.

BNNクラス
    # ヘッセ行列の計算
    def _compute_hessian(self, x_data, hessian_size):
        hessian_matrix = torch.zeros(size=(hessian_size, hessian_size))
        for x in x_data:
            x.unsqueeze_(0)
            f = self.net.forward(x)
            f.backward(retain_graph=False)
            with torch.no_grad():
                grad_w1 = self.net.fc1.weight.grad
                grad_w2 = self.net.fc2.weight.grad
                grad_f = torch.cat([grad_w1.reshape(-1, 1), grad_w2.reshape(-1, 1)], dim=0)  # 勾配(列ベクトル)の形に整形
                hessian_matrix += torch.mm(grad_f, torch.t(grad_f))
            self.net.zero_grad()  # 勾配を0に戻す
        return hessian_matrix

    # ラプラス近似分布の計算
    def LaplaceApproximation(self, x_data, y_data):
        # 平均ベクトルについて
        w1_MAP, w2_MAP = self.MAPestimation(x_data, y_data)
        W_MAP_vector = torch.cat([w1_MAP.reshape(-1, 1), w2_MAP.reshape(-1, 1)], dim=0)
        # 共分散行列について
        M = W_MAP_vector.shape[0]
        hessian_matrix = self._compute_hessian(x_data, hessian_size=M)
        lambda_matrix = (self.w_sigma ** (-2)) * torch.eye(M) + (self.y_sigma ** (-2)) * hessian_matrix
        self.lambda_mat_inv = torch.inverse(lambda_matrix)

予測

パラメータ$\mathbf{W}$の事後分布が求まったら,新規入力点$\mathbf{x}_{\ast}$に対する出力$y_{\ast}$の事後予測分布を求めます.
この事後予測分布を,

p(y_{\ast} | \mathbf{x}_{\ast}, \mathbf{Y}, \mathbf{X}) 
  \approx \mathcal{N}(y_{\ast} | f(\mathbf{x}_{\ast} ; \mathbf{W}_{\rm MAP}), \sigma_y^2+\mathbf{g}^\top \left\{ \mathbf{\Lambda}(\mathbf{W}_{\rm MAP}) \right\}^{-1} \mathbf{g})

と近似します.ただし,$ \mathbf{g} = \nabla_{\mathbf{W}} f(\mathbf{x}_{\ast} ; \mathbf{W}) \mid _{\mathbf{W} = \mathbf{W}_{\rm MAP}} $と置いています.

BNNクラス
    # 事後予測分布の計算
    def predict(self, x_pred):
        f_pred = self.net.forward(x_pred)
        f_pred.backward(retain_graph=False)
        with torch.no_grad():
            grad_w1 = self.net.fc1.weight.grad
            grad_w2 = self.net.fc2.weight.grad
            g = torch.cat([grad_w1.reshape(-1, 1), grad_w2.reshape(-1, 1)], dim=0)
            y_pred_sigma2 = self.y_sigma ** 2 + torch.mm(torch.t(g), torch.mm(self.lambda_mat_inv, g))  # 予測分散
        self.net.zero_grad()  # 勾配を0に戻す
        return f_pred, torch.sqrt(y_pred_sigma2)

結果の図示

それでは,こちらも予測結果を図示してみましょう.

LaplaceApproximationforBNN.png

こちらは解析的な計算に基づいて$y$の分布のパラメータを求めていますので,平均の曲線と分散の2倍の予測区間を図示しました.
先ほどの平均場近似に比べて,複雑度の高い予測曲線となっているように思われます.

MCMC

最後にMCMCを適用してみましょう.

ライブラリのimport

必要なライブラリをimportします.

import matplotlib.pyplot as plt
import torch
import pyro
from pyro.distributions import Normal, Delta
from pyro.infer.mcmc.api import MCMC
from pyro.infer.mcmc.nuts import NUTS
from pyro.infer.mcmc.util import predictive

生成モデル

生成モデルは先ほどの変分推論の時と同じですので省略します.

推論

MCMCの手法のうち,今回はHMCの発展版であるNUTSを使います.

BNNクラス
    def nuts_sampling(self, x_data, y_data, num_samples, warmup_steps):
        nuts_kernel = NUTS(self.model, target_accept_prob=0.99)
        mcmc = MCMC(nuts_kernel, num_samples=num_samples, warmup_steps=warmup_steps)
        mcmc.run(x_data, y_data)
        self.posterior_samples = mcmc.get_samples()

MCMC(kernel, num_samples, warmup_steps, ...)としてインスタンスを生成し,runメソッドを呼び出すことでMCMCサンプリングが行われます.
その後,get_samplesメソッドを呼び出すことで生成したサンプルを取得できます.

予測

BNNクラス
    def predict(self, x_pred):
        def wrapped_model(x_data, y_data):
            pyro.sample("prediction", Delta(self.model(x_data, y_data)))

        samples = predictive(wrapped_model, self.posterior_samples, x_pred, None)
        return samples["prediction"], samples["obs"]

MCMCによる事後分布の推論をした後の予測分布の求め方は,変分推論の時とほぼ同じです.おそらく正式リリースの時には統合されていると思いますが,現在,MCMCではPredictiveではなくpredictiveを使います.

結果の図示

こちらも結果を図示してみましょう.

MCMCforBNN.png

変分推論の時と同様,左は$y$の平均,右は$y$の予測分布です.
複雑度の高い予測分布が得られています.

比較

最後に,3つの手法の比較を行います.

実行速度

変分推論(VI),ラプラス近似(LA),MCMCそれぞれに対して,推論にかかった時間を測定しました.(各1回しか測定してないので目安程度ですが)

VI
time: 93.1693[sec]
LA
time: 19.8220[sec]
MCMC
sample: 100%|██████████| 1500/1500 [25:57,  1.04s/it, step size=9.50e-03, acc. prob=0.984]

MCMCに関しては,PyroでMCMCを走らせることで上記のように表示が出ます.25分57秒かかっているようです.

最適化による推論アルゴリズムである変分推論とラプラス近似が非常に高速であることがわかります.変分推論よりラプラス近似が速いのは,最適化ステップ数が少ないためです.

精度

もう一度,各手法の予測分布の図を載せます.

変分推論
VIforBNN.png
ラプラス近似
LaplaceApproximationforBNN.png
MCMC
MCMCforBNN.png

MCMCは,理論的には真の事後分布からのサンプリングが可能であり,図をみても最も正確に予測分布からのサンプリングができていそうです.一方,変分推論やラプラス近似は,近似によって表現能力が制限されていることが見て取れます.特に,変分推論では平均場近似をしたので,とても制限されたものになっています.

まとめ

今回扱ったモデルは,たった2層のベイズニューラルネットワークモデルであり,隠れユニット数も4つと,ニューラルネットワークとしては単純なモデルです.データサイズもたった15です.それでもMCMCは収束までに結構時間がかかりました.より複雑なベイズモデルを学習させるのには,ミニバッチを利用するなどの計算時間削減の工夫が必須であることを体感できました.

参考文献

『ベイズ深層学習』
『Pyro Documentation』(Pyroの公式ドキュメント)
『Welcome to Pyro Examples and Tutorials!』(Pyroの公式チュートリアル)
『確率的プログラミング言語Pyroと変分ベイズ推論の基本』(HELLO CYBERNETICSさんの記事)

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

Ansibleでサーバ情報を取得する

はじめに

前回Ansibleの基礎を学んだので今回はAnsibleを使用してサーバ情報を取得しようと思う。
色々試したが結果の成形等で悩んで結局、各サーバでcommandを実行⇒pythonで情報を成形という方法を試した。

Ansibleの設定

ディレクトリ構成は以下のようになっている。

ディレクトリ構成
ansible
├── inventory
│   └── hosts
│   
├── json_ansible.cfg
├── json_change.py
├── server_info_get.yml
└── result.json

hostsファイルの中身は以下。

hosts
[targets]
192.168.163.130
192.168.163.131

次にPlay-book(server_info_get.yml)の中身は以下。

server_info_get.yml
- hosts: targets
  user: root
  gather_facts: false
  tasks:
        - command: "{{ item }}"
          with_items:
            - 'hostname'
            - 'cat /etc/redhat-release'
            - 'uname -a'
            - 'yum list installed'

with_items配下に取得したいサーバ情報を書き出した。今回はCentOSサーバが対象だが、windowsサーバなどが対象に入った状態で1つのplay-bookで実行しようとした場合はもう少し工夫が必要そう。

次にansibleで取得した情報をjson形式で保存したいので設定ファイル(json_ansible.cfg)を作成する。

json_ansible.cfg
[defaults]
stdout_callback = json

これでAnsible側の設定は完了した。

Ansibleの実行

それではAnsibleを実行してみようと思う。上記で作成した設定ファイルを読み込ませて実行する。

Ansible_server
[root@server ansible]# ANSIBLE_CONFIG=/ansible/json_ansible.cfg ansible-playbook -i inventory/hosts server_info_get.yml > result.json

ANSIBLE_CONFIGで設定ファイルを指定してAnsibleを実行、実行結果をresult.jsonに掃き出した。

それでは中身を見てみようと思う。内容が多いのでlessで

Ansible_server
[root@server ansible]# less result.json
result.json
{
    "plays": [
        {
            "play": {
                "id": "000c29a4-5724-73ef-251b-000000000008",
                "name": "targets"
            },
            "tasks": [
                {
                    "hosts": {
                        "192.168.163.130": {
                            "changed": true,
                            "msg": "All items completed",
                            "results": [
                                {
                                    "_ansible_ignore_errors": null,
                                    "_ansible_item_result": true,
                                    "_ansible_no_log": false,
                                    "_ansible_parsed": true,
                                    "changed": true,
                                    "cmd": [
                                        "hostname"
                                    ],
                                    "delta": "0:00:00.008092",
                                    "end": "2019-10-28 16:38:35.390429",
                                    "failed": false,
                                    "invocation": {
                                        "module_args": {
                                            "_raw_params": "hostname",
                                            "_uses_shell": false,
                                            "chdir": null,
                                            "creates": null,
                                            "executable": null,
                                            "removes": null,
                                            "stdin": null,
                                            "warn": true
                                        }
                                    },
                                    "item": "hostname",
                                    "rc": 0,
                                    "start": "2019-10-28 16:38:35.382337",
                                    "stderr": "",
                                    "stderr_lines": [],
                                    "stdout": "test1",
                                    "stdout_lines": [
                                        "test1"
~~~~~~~~~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~~~~~~~~
                                    ]
                                }
                            ],
                            "warnings": [
                                "Consider using yum module rather than running yum"
                            ]
                        }
                    },
                    "task": {
                        "id": "000c29a4-5724-73ef-251b-00000000000a",
                        "name": ""
                    }
                }
            ]
        }
    ],
    "stats": {
        "192.168.163.130": {
            "changed": 1,
            "failures": 0,
            "ok": 1,
            "skipped": 0,
            "unreachable": 0
        },
        "192.168.163.131": {
            "changed": 1,
            "failures": 0,
            "ok": 1,
            "skipped": 0,
            "unreachable": 0
        }
    }
}

うまく実行されたみたいだ。

JSONファイルの成形

json形式で情報は取得できたが、いらない情報も多くあって正直見にくい。なのでjsonファイルの情報をpythonを使って人の目でみて見やすいようしようと思う。

以下のようなプログラムを作成した。

json_change.py
#coding:utf-8
import json
import sys
import pprint

# コマンドライン引数を取り込む。
args = sys.argv
# コマンドライン引数の2番目を取得。
jsonfile = args[1]

f = open(jsonfile,'r')
jsonfile = json.load(f)

for i in json_dict['plays']:
    for j in i['tasks']:
        for m in j['hosts'].items():
          print(m[0])
          data=m[1]["results"]
          print(data[0]["stdout"])
          print(data[1]["stdout"])
          print(data[2]["stdout"])
          print(data[3]["stdout"])

これを実行する。

Ansible_server
[root@server ansible]# python3 json_change.py result.json

192.168.163.130
test1
CentOS Linux release 7.6.1810 (Core)
Linux test1 3.10.0-957.el7.x86_64 #1 SMP Thu Nov 8 23:39:32 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
読み込んだプラグイン:fastestmirror
インストール済みパッケージ
GeoIP.x86_64                          1.5.0-13.el7                     @anaconda
NetworkManager.x86_64                 1:1.12.0-6.el7                   @anaconda
NetworkManager-libnm.x86_64           1:1.12.0-6.el7                   @anaconda
~~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~
xz-libs.x86_64                        5.2.2-1.el7                      @anaconda
yum.noarch                            3.4.3-161.el7.centos             @anaconda
yum-metadata-parser.x86_64            1.1.4-10.el7                     @anaconda
yum-plugin-fastestmirror.noarch       1.1.31-50.el7                    @anaconda
zlib.x86_64                           1.2.7-18.el7                     @anaconda
192.168.163.131
test2
CentOS Linux release 7.6.1810 (Core)
Linux jtf-docker 3.10.0-957.27.2.el7.x86_64 #1 SMP Mon Jul 29 17:46:05 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
読み込んだプラグイン:fastestmirror
インストール済みパッケージ
GeoIP.x86_64                         1.5.0-13.el7                   @anaconda
NetworkManager.x86_64                1:1.12.0-10.el7_6              @updates
NetworkManager-libnm.x86_64          1:1.12.0-10.el7_6              @updates
~~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~
yum-metadata-parser.x86_64           1.1.4-10.el7                   @anaconda
yum-plugin-fastestmirror.noarch      1.1.31-50.el7                  @anaconda
yum-utils.noarch                     1.1.31-50.el7                  @base
zlib.x86_64                          1.2.7-18.el7                   @anaconda

お!ちょっと見にくいがほしい情報だけが表示された!

次の課題

情報取得、jsonファイルの成形はできたがまだ見にくい。というかpythonのネストが深くて個人的に微妙・・・。
なので次回は取得した情報をツールを使ってビジュアライズ化しようと思う。(ansibleからは離れるが)

参考

https://go-journey.club/archives/4830

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

[機械学習]サポートベクトルマシン(SVM)について、できるだけ分かりやすくまとめていく④~ソフトマージンとハードマージンの実装~

はじめに

今回の記事は前回の記事の続きになっています。

よろしければ以下の記事もご覧ください。

[機械学習]サポートベクトルマシン(SVM)について、できるだけ分かりやすくまとめていく①~理論と数式編~

[機械学習]サポートベクトルマシン(SVM)について、できるだけ分かりやすくまとめていく②~ラグランジュの未定乗数法~

[機械学習]サポートベクトルマシン(SVM)について、できるだけ分かりやすくまとめていく③~カーネル法について~

分類問題:ハードマージン

線形分離可能なデータを分離するsvmを実装していきます。

用いるデータはiris(アヤメ)データセットです。

iris(アヤメ)データセットについて

irisデータは、アヤメという花の品種のデータです。

アヤメの品種であるSetosaVirginicaVirginicaの3品種に関するデータが50個ずつ、全部で150個のデータです。

実際に中身を見ていきましょう。

from sklearn.datasets import load_iris
import pandas as pd

iris = load_iris()
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)

print(iris_df.head())

sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2

iris.feature_namesに各々のカラム名が格納されているので、それをpandasのDataframeの引数に渡すことで上のようなデータを出力できます。

Sepal Lengthはがく弁の長さが、Sepal Widthにはがく弁の幅が、Petal lengthには花びらの長さが、Petal Widthには花びらの幅のデータが格納されています。

以下のようにすれ正解ラベルを表示できます。

print(iris.target)

[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2]

このように、アヤメの品種であるsetosaversicolorvirginicaをそれぞれ0, 1, 2としています。

アヤメのデータについての説明はここまでです。

実装

以下のコードでデータセットを作成しましょう。

import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.svm import LinearSVC
from sklearn.datasets import load_iris
import mglearn

iris = load_iris()
X = iris.data[:100, 2:]
Y = iris.target[:100]
print(X.shape)
print(Y.shape)

(100, 2)
(100,)

今回はsetosaversicolorpetal lengthpetal widthのデータを用いて分類を行います。

以下のコードでデータの描画を行います。

mglearn.discrete_scatter(X[:, 0], X[:, 1], Y)
plt.legend(['setosa', 'versicolor'], loc='best')
plt.show()

image.png

mglearn.discrete_scatter(X[:, 0], X[:, 1], Y)のコードは第一引数をX軸、第二引数にY軸、第三引数に正解ラベルをとって、scatterプロットを行います。

loc='best'により、凡例がグラフの邪魔にならない位置にくるように調整しています。

上のデータから、明らかに直線で分離できることが分かりますね。むしろ簡単すぎるくらいです。

次のコードでモデルを作成しましょう。

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, stratify=Y, random_state=0)
svm = LinearSVC()
svm.fit(X_train, Y_train)

モデルの作成自体はこのコードで終わりです。簡単ですね。

以下のコードでモデルがどのような形になったのかを図示しましょう。

plt.figure(figsize=(10, 6))
mglearn.plots.plot_2d_separator(svm, X)
mglearn.discrete_scatter(X[:, 0], X[:, 1], Y)
plt.xlabel('petal length')
plt.ylabel('petal width')
plt.legend(['setosa', 'versicolor'], loc='best')
plt.show()

image.png

しっかりとデータを分ける境界線が作成されていることが確認できますね。

mglearn.plots.plot_2d_separator(svm, X)の部分は少し分かりにくいと思うので解説します。定義となるコードを確認しましょう。

plot_2d_separator(classifier, X, fill=False, ax=None, eps=None, alpha=1,cm=cm2, linewidth=None, threshold=None,linestyle="solid"):

第一引数に分類モデルを渡して、第二引数に元のデータを渡すと境界線を引いてくれる関数ですね。

ここまでで、線形分離可能な問題におけるsvmのモデルの実装は終了です。

分類問題: ソフトマージン

今回はソフトマージンの問題について取り扱います。

import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.svm import LinearSVC
from sklearn.datasets import load_iris
import mglearn

iris = load_iris()

X = iris.data[50:, 2:]
Y = iris.target[50:] - 1

mglearn.discrete_scatter(X[:, 0], X[:, 1], Y)
plt.legend(['versicolor', 'virginica'], loc='best')
plt.show()

image.png

今度はversicolorverginicapetal lengthpetal widthについてのデータをプロットしています。

完全に線形分離することは不可能な問題ですね。

ここでソフトマージンの式を復習です。導出はこちらの記事を参考にしてください。

min_{W, \xi}\Bigl\{\frac{1}{2}||W||^2 + C\sum_{i=1}^{N} \xi_i\Bigr\} \quad \quad
t_i(W^TX_i + b)\geq 1 - \xi_i\\
\xi_i = max\Bigl\{0, M - \frac{t_i(W^TX_i + b)}{||W||}\Bigr\}\\
i = 1, 2, 3, ... N

データがマージンの内側に入り込んでしまうので、$ C\sum_{i=1}^{N} \xi_i$の項により制限を緩めているのでしたね。

このCの値はskleaarnにおいて、デフォルトで1.0になっています。この数値を変化させて、図がどう変わるのか確認してみましょう。以下のコードで、引数に与えたモデルの境界線をプロットする関数を定義します。

def make_separate(model):
    mglearn.plots.plot_2d_separator(svm, X)
    mglearn.discrete_scatter(X[:, 0], X[:, 1], Y)
    plt.xlabel('petal length')
    plt.ylabel('petal width')
    plt.legend(['setosa', 'versicolor'], loc='best')
    plt.show()

以下のコードで図を描画しましょう。C=0.1とします。

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, stratify=Y, random_state=0)
svm = LinearSVC(C=0.1)
svm.fit(X_train, Y_train)
make_separate(svm)
print(svm.score(X_test, Y_test))

0.96

image.png

次はC=1.0です。

svm = LinearSVC(C=1.0)
svm.fit(X_train, Y_train)
make_separate(svm)
print(svm.score(X_test, Y_test))

1.0

image.png

次はC=100です。

svm = LinearSVC(C=100)
svm.fit(X_train, Y_train)
make_separate(svm)
print(svm.score(X_test, Y_test))

1.0

image.png

適切なCを設定するのが大切ですね。色々変えながら様子を見ていくのがよさそうです。

ここまででソフトマージンの実装は終了です。

終わりに

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

【分散学習】 TensorflowにおけるCPU/GPU使い分け

TensorflowにおけるプロセスごとのCPU/GPU使い分けについて記述します。

背景

Ape-x,DISTRIBUTEDPRIORITIZEDEXPERIENCEREPLAY
R2D2,Recurrent Experience Replay in Distributed Reinforcement Learning

強化学習において、Ape-x、R2D2のように経験の獲得はCPUを用いて並列化し、その経験を中央のGPUが学習する手法が提案されています。
CPUは並列化しやすいため、学習効率の改善が期待できます。

環境

Ubuntu18.04
Python 3.6.8
Tensorflow 1.12.0
CUDA 10.1

CPU/GPU使い分け

解決策

Ape-x,R2D2ではマルチプロセスを用い、GPU計算をするLearnerと、CPU計算をするActorを生成します。
このとき、プロセス間のCPUとGPUの使い分けは下記コードで実現出来ます。

import os,multiprocessing

def hoge_cpu():
    #CPU計算をしたい
    os.environ["CUDA_VISIBLE_DEVICES"] = "" #""には何も書かない
    ...

def hoge_gpu():
    #GPU計算をしたい
    ...

multiprocessing.Process(target=hoge)
multiprocessing.Process(target=hoge_gpu)

os.environ["CUDA_VISIBLE_DEVICES"]がポイントです。これを書くだけです。

別の実装

他の方の実装では下記のコードがありました。

import tensorflow as tf
with tf.device("/gpu:0"):
    multiprocessing.Process(target=hoge_gpu)
with tf.device("/cpu:0"):
    multiprocessing.Process(target=hoge_cpu)

しかしこの実装ではCPU側の実行にGPUのメモリ領域が確保されていました。プロセス数に余裕があってもGPUのメモリサイズが限界となり、並列化の数が小さくなってしまいます。

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

Pipenvのscriptsで複数のコマンドを実行させる

きっかけ

Djangoではmigrateファイル生成とマイクレートの実行は別々のコマンドのためマイグレーション時には次の2つを打つ必要性が有ります。

$ pipenv run python manage.py makemigrations
$ pipenv run python manage.py migrate

そこでPipenvにあるオプションであるscriptsを使って一つにまとめようと次の内容を記述しました。

(前略)
[scripts]
dev = "python manage.py runserver"
migrate = "python manage.py makemigrations && python manage.py migrate"

実行すると次のようなエラーが出ます。

% (*'-')<3 pipenv run migrate   

No installed app with label 'python'.
No installed app with label 'manage.py'.
No installed app with label 'migrate'.

解決策

bashのCオプションをつかってコマンドを流し込みます。cオプションは引数の文字列からコマンドを実行してくれるというものになっています。今回書いたものは次にのものになります。

(前略)

[scripts]
dev = "python manage.py runserver"
migrate = "bash -c 'python manage.py makemigrations && python manage.py migrate'"

これで実行してみます。

% (*'-')<3 pipenv run migrate                                                                                                                 [~/dev/socPlProject/yebisu][add/yebsu]
No changes detected
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  No migrations to apply.

問題なく実行されました。

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

PythonのSeleniumラッパーSeleneの使い方

概要

JavaのSeleniumWebDriverラッパーとしてはSelenideが有名ですが、PythonではSeleneがメジャーなようです。

公式のドキュメント等見てもイマイチわからないところが多かったので、自分で調べてみたついでにわかったことを残しておきます。

※色々調べてみた感想としては、Seleneの中身読むのが一番わかりやすかったです。

そもそもSelenとは

  • Effective web test automation toolである
    • 簡潔なAPI
    • 要素等の検索を待つ
    • アサートを待つ
    • 動的な要素にも対応
  • UIテストのロジックを自動化するもの

準備

手元の環境

  • Windows10Pro
  • Python 3.7.2
  • Selenium WebDriver 3.141.0
  • pytest 5.0.0
  • GoogleChrome 77
  • FireFox 70

環境構築

SeleniumWebDriverとPythonで自動テストを書ける状態に加えて、Seleneのインストールが必要です。

インストール手順などは公式に書いてあります。

yashaka/selene: Concise UI tests in Python + Ajax support + PageObjects

が、バージョンごとに複数手順があるようなので、今回は

latest published pre-release version (currently this is recommended option unless selene 1.0 will be released):

と書いてある最新のプレリリース版を使おうと思います。

$ pip install selene --pre

使い方

画面の要素を取得する

browser.element('#new-todo')のように書くことができますが、基本はより短いsを使います。

find_element_by_id("hoge")と書くかわりに、

  • s('#hoge')
  • s(by.id('hoge'))

のいずれかの書き方ができます。

一つめのほうは、sの引数にそのままCSSセレクタを書くパターン。もうひとつのほうは、byを使ってidなりxpathなりを使う、生のWebDriverに近いパターン。

いずれにせよ、行は減らないものの文字数はだいぶ減っています。ただ、読んで理解しやすいかというと怪しいかもしれませんね。

sは"search element"のsだそうです。

find_elements_by_hogeを行いたいときには、ssを使います。

ss('#todo-list>li')

こちらは、browser.all('#todo-list>li')とも書けますが、やはり短いssのほうを公式も勧めています。

スクリーンショット取得

生のWebDriverだとsave_screenshot()メソッドを使うところ、Seleneの場合はtake_screenshot()になるようです。

take_screenshot()はパスとファイル名の二つの引数をとり、どちらも書かなくても動くようにはなっています。

第一引数で指定するパスはスクリーンショットの保存先のパス、第二引数で指定するファイル名はスクリーンショットのファイル名です。

たとえば、

browser.take_screenshot(".\\capture")

とだけ書いた場合。この場合は、スクリプトと同階層にcaptureというフォルダができ、そこにスクリーンショットがどんどん追加されていきます。
このときのファイル名は過去のものと重複しないようにSeleneが付けてくれます。screen_1571995717156.pngのような名前になります。

引数に何も指定しないと、デフォルトのレポート保存先に"screenshots"というフォルダを作って保存されます。

私の手元の例は↓です。

C:\Users\yoshiki.itou\.selene\screenshots\1572231343309\screen_1572231343310.png

毎回保存先を指定するのが面倒な場合は、引数指定なしの場合の保存先を設定変更することができます。

config.reports_folder = ".//screenshot/"

と書くと、実行したテストスクリプトと同じ階層にscreenshotsというフォルダを作り、その中にスクリーンショットを保存していきます。

タイムアウトの設定

例えば
python
s("#new-todo").should_be(enabled)

とだけ書いた場合、デフォルトでタイムアウトは4秒。

s("#new-todo").should_be(enabled, timeout=10)

と書けば、この場合のタイムアウトが10秒になります。

もしくは

config.timeout = 10

と書いておけば、以降のタイムアウトが10秒になります。

実行するブラウザの指定

SeleneはAutomatic driver managementというのをやってくれるので、自分でダウンロードしてきたhogedriver.exeのパスを指定or環境変数PATHを通して・・・という作業をしなくて済むようになっています。

このへんはSergeyPirogov/webdriver_managerを利用しているようです。

デフォルトではChromeが起動するようになっているようですが、明示的に指定する場合は以下のように書きます。

from selene import config
from selene.browsers import BrowserName

config.browser_name = BrowserName.CHROME

FireFoxで動かしたい場合、CHROMEのところを単純に
Python
config.browser_name = BrowserName.FIREFOX

にすればうまくいくだろうと思ったのですが、私の環境ではエラーで動作せず。

中身見てみたところ、どうも

config.browser_name = BrowserName.MARIONETTE

だと動作するようでした。

※ここについては、みんなそうだという自信はないので、FIREFOXでダメなときはお試しください。

selene/browsers.py at master · yashaka/seleneあたりを見てみても、FIREFOX自体は設定できるので、ちょっと謎です。わかる方教えてください。

アサート

アサートに使えるものは

  • should
  • assure
  • should_be
  • should_have
  • should_not
  • assure_not
  • should_not_be
  • should_not_have

がありますが、内部的には

  • should
  • should_not

の2つで、他は上記いずれかのエイリアスです。

アサーションのためにshouldあるいはそのエイリアスを使うにあたって、PASS時は特に困ることはなさそうです。

一方FAIL時は多少厄介で、TimeOutExceptionが出てきてしまいました。

たとえば、以下がFAIL時の表示の例です。画面に51000と表示されるべきところ、わざと「41000と表示されるか」をテストして失敗させています。

コード(抜粋)

s(by.id("price")).should(have.text('41000'))

結果(抜粋)

E           selenium.common.exceptions.TimeoutException: Message:
E                       failed while waiting 4 seconds
E                       to assert Text
E                       for first_by('id', 'price')
E
E                       reason: ConditionMismatchException: condition did not match
E                               expected: 41000
E                                 actual: 51000
E                       screenshot: file://.//screenshot/screen_1572244618378.png

..\..\..\..\appdata\local\programs\python\python37\lib\site-packages\selene\elements.py:232: TimeoutException

ConditionMismatchExceptionなのでそこ見ればわかるだろと言われればそうなのですが、エラーメッセージの最後の最後でTimeoutExceptionで出てきてしまうのがうーん。。

とはいえ、比較のときには標準のassert使う、だとラッパーライブラリのうまみ半減なので、難しいところですね。

また、標準のassertだと条件式がFalseのときだけ表示するメッセージを第三引数に書いておけますが、Seleneのアサートだとそれが無さそうです。

参考

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

Python3のmap/filterがiteratorを返してることの意味

Python3でmap/filterがイテレータを返すことは知っていたが、トラップにはまってしまったのでメモ

lst = [1,2,3,4,5]

flist = filter(lambda i: i%2==0, lst)
print(flist)

for i in flist:
    print(i)

for i in flist:
    print(i)

python2系で実行すると

[2, 4]
2
4
2
4

python3系で実行すると

<filter object at 0x7f1d6636e518>
2
4

イテレータを一度ループさせて終端まで行ってるのでもう一度適用しても何も残ってないですよ、というお話

flist = list(filter(lambda i: i%2==0, lst))

と書かないといけない

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

numpyを使わずに列と行を入れ替える

python
>>> arr
[[2, 3, 603], [1, 1, 286], [4, 4, 882]]

>>> [list(x) for x in list(zip(*arr))]
[[2, 1, 4], [3, 1, 4], [603, 286, 882]]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python入門 5回目 Pythonによる科学計算

index

概要

ここでは色々な計算テクニックを身に付けるために、Numpyのインデックス参照やブロードキャストについて、Scipy(サイパイ)の線形代数や積分計算、最適化計算についてみて行く。

今回使うライブラリのインポート

import numpy as np
import numpy.random as random
import scipy as sp

import matplotlib.pyplot as plt
import matplotlib as mpl

Numpyを使った計算の応用

インデック参照

sample_array = np.arange(10)
print('sample_array:',sample_array)
出力
sample_array: [0 1 2 3 4 5 6 7 8 9]

上の結果からわかるようにsample_arrayは0から9までの数字(配列)を作成している。
連番や等差数列を生成するnumpy.arange関数の使い方

次にスライス操作を行ってみる。

#元のデータ
print(sample_array)

# 前から数字を5つ取得して、sample_array_sliceに入れる(スライス)
sample_array_slice = sample_array[0:5]
print(sample_array_slice)
出力
[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4]

次は、sample_array_sliceの先頭から3つを10という値に置き換える。
この時、元の変数であるsample_arrayの値も変わっていることに注意

# sample_array_sliceの3文字目までは、10で置換
sample_array_slice[0:3] = 10
print(sample_array_slice)

# スライスの変更はオリジナルのリストの要素も変更されていることに注意
print(sample_array)
出力
[10 10 10  3  4]
[10 10 10  3  4  5  6  7  8  9]

データのコピー

先ほどの代入元の変数の値も変わってしまわないように元のデータを参照せず、元のデータをコピーした物を参照させるようにすれば良い。

# copyして別のobjectを作成
sample_array_copy = np.copy(sample_array)
print(sample_array_copy)

sample_array_copy[0:3] = 20
print(sample_array_copy)

# 元のリストの要素は変更されていない
print(sample_array)
出力
[10 10 10  3  4  5  6  7  8  9]
[20 20 20  3  4  5  6  7  8  9]
[10 10 10  3  4  5  6  7  8  9]

ブールインデックス参照

bool(True or False)によって、どのデータを取り出すかを決める機能。
sample_namesは、「a」「b」「c」「d」「a」という値を要素として持つ要素数5の配列、dataは、標準正規乱数からなる5×5の配列を作成。

sample_names = np.array(['a','b','c','d','a'])
random.seed(0) # 発生する乱数の種を固定
data = random.randn(5,5)

print(sample_names)
print(data)
出力
['a' 'b' 'c' 'd' 'a']
[[ 1.764  0.4    0.979  2.241  1.868]  #a
 [-0.977  0.95  -0.151 -0.103  0.411]  #b
 [ 0.144  1.454  0.761  0.122  0.444]  #c
 [ 0.334  1.494 -0.205  0.313 -0.854]  #d
 [-2.553  0.654  0.864 -0.742  2.27 ]] #a

要素の値が「'a'」である部分だけTrueになる結果を取り出す

sample_names == 'a'
出力
array([ True, False, False, False,  True])

data変数の[]の中に条件として指定すると、Trueになっている箇所のデータだけが取り出せる

data[sample_names == 'a']
出力
array([[ 1.764,  0.4  ,  0.979,  2.241,  1.868],
       [-2.553,  0.654,  0.864, -0.742,  2.27 ]])

条件制御

numpy.where(条件の配列, Xのデータ, Yのデータ)
これは、条件の配列がTrueのときは ? のデータ、そうでなければ ? のデータが取り出される。

cond_data = np.array([True,True,False,False,True])

x_array= np.array([1,2,3,4,5])

y_array= np.array([100,200,300,400,500])

print(np.where(cond_data,x_array,y_array))
出力
[  1   2 300 400   5]

Numpyの演算処理

重複の削除

uniqueを使うことで、要素の重複を削除できる

cond_data = np.array(['ばなな','りんご','パイナップル','ばなな','パイナップル'])
print(cond_data)

# 重複削除
print(np.unique(cond_data))
出力
['ばなな' 'りんご' 'パイナップル' 'ばなな' 'パイナップル']
['ばなな' 'りんご' 'パイナップル']

ユニバーサル関数

全ての要素に関数を適用できる

sample_data = np.arange(10)
print('元のデータ:', sample_data)
print('すべての要素の平方根:',np.sqrt(sample_data))
print('すべての要素のネイピア指数関数:',np.exp(sample_data))
出力
元のデータ: [0 1 2 3 4 5 6 7 8 9]
すべての要素の平方根: [0.    1.    1.414 1.732 2.    2.236 2.449 2.646 2.828 3.   ]
すべての要素のネイピア指数関数: [1.000e+00 2.718e+00 7.389e+00 2.009e+01 5.460e+01 1.484e+02 4.034e+02
 1.097e+03 2.981e+03 8.103e+03]

最小、最大、平均、合計の計算

Pandansでもできるが、Numpyでもできる

data = np.arange(9).reshape(3,3)

print(data)

print('最小値:',data.min())
print('最大値:',data.max())
print('平均:',data.mean())
print('合計:',data.sum())

# 行列を指定して合計値を求める
print('行の合計:',data.sum(axis=1))
print('列の合計:',data.sum(axis=0))
出力
[[0 1 2]
 [3 4 5]
 [6 7 8]]
最小値: 0
最大値: 8
平均: 4.0
合計: 36
行の合計: [ 3 12 21]
列の合計: [ 9 12 15]

axis = 0, axis=1 がわからなくなった人へ

真偽値の判定

anyallを使うと、要素の条件判定ができる。

any:いずれか少なくとも1つ満たすものがあればTrue
all:全て満たす場合にTrue

cond_data = np.array([True,True,False,False,True])

print('Trueが少なくとも1つあるかどうか:',cond_data.any())
print('すべてTrueかどうか:',cond_data.all())
出力
Trueが少なくとも1つあるかどうか: True
すべてTrueかどうか: False

条件を指定してからsumを指定すると、条件に合致する要素の個数を調べられる。

data2 = np.arange(9).reshape(3,3)
print(data2)
print('5より大きい数字がいくつあるか:',(data2>5).sum())
出力
[[0 1 2]
 [3 4 5]
 [6 7 8]]
5より大きい数字がいくつあるか: 3

対角成分の計算

対角成分(行列の左上から右下にかけての対角線上に並ぶ成分)

data3 = np.arange(9).reshape(3,3)
print(data3)

print('対角成分:',np.diag(data3))
print('対角成分の和:',np.trace(data3))
出力
[[0 1 2]
 [3 4 5]
 [6 7 8]]
対角成分: [0 4 8]
対角成分の和: 12

配列操作とブロードキャスト

再形成

sample_array = np.arange(10)
sample_array
出力
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

reshape(2, 5)で2行5列の行列に再形成

sample_array2 = sample_array.reshape(2,5)
sample_array2
出力
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])

5行2列の行列を再形成

sample_array2.reshape(5,2)
出力
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7],
       [8, 9]])

データの結合

concatenateを使うと、データの結合が可能

sample_array3 = np.array([[1,2,3],[4,5,6]])
sample_array4 = np.array([[7,8,9],[10,11,12]])

# 行方向に結合。パラメータのaxisに0を指定
np.concatenate([sample_array3,sample_array4],axis=0)
出力
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12]])

行方向の結合は、vstackでも可能

np.vstack((sample_array3,sample_array4))
出力
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12]])

列方向の結合はaxisに1を設定する。

np.concatenate([sample_array3,sample_array4],axis=1)
出力
array([[ 1,  2,  3,  7,  8,  9],
       [ 4,  5,  6, 10, 11, 12]])

列方向の結合は、hstackでも可能。

np.hstack((sample_array3,sample_array4))
出力
array([[ 1,  2,  3,  7,  8,  9],
       [ 4,  5,  6, 10, 11, 12]])

配列の分割

splitを使うと配列を分割できる。
splitに[1,3]というパラメータを指定することによって以下のように分割させる。
①1の手前までの行(0番目の行)
②1から3の手前までの行(1番目と2番目の行)
③3以降全ての行(3番目の行)

# sample_array_vstackを3つに分割し、first、seocnd、thirdという3つの変数に代入
first,second,third=np.split(sample_array_vstack,[1,3])
print(first) #出力: [[1 2 3]]

print(second) #出力: [[4 5 6]
              #      [7 8 9]]

print(third) #出力: [[10 11 12]]

繰り返し処理

repeatを使うと、それぞれの要素を繰り返し生成できる

data4 = np.arange(5)
print(data4)

data4.repeat(3)
出力
[0 1 2 3 4]
array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4])

ブロードキャスト

配列の大きさが異なっている時、自動的に要素をコピーして、対象の大きさを揃える機能。
NumPyのブロードキャストのメモ
機械学習の Python との出会い
公式

sample_array = np.arange(10)
print(sample_array)

sample_array + 3
出力
[0 1 2 3 4 5 6 7 8 9]
array([ 3,  4,  5,  6,  7,  8,  9, 10, 11, 12])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BERTの軽量版,ALBERTとは?

 BERT(Bidirectional Transformers for Language Understanding)とは,2018年9月11日にarXivに公開された論文のモデルです.(BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding)このBERTが出た当時,NLP界隈ではかなり騒がれていました.なんでも,転移学習が可能で,様々なタスクにおいてSOTAと達成したとのこと.しかも,RNNベースではなく,Attentionベースなため並列計算ができ,学習速度も速い.(モデルの大きさにもよりますが)
 しかし,学習速度がある程度早く,かつ高精度なBERTですが,欠点を上げるとすればモデルがかなり大きいことでしょう.標準のBERTでもTransformerが12層も積み重なっています.
 そこで,BERTの軽量版,ALBERTA Lite BERT)が2019年10月23日(ver.2)にarXivで公開されました.(ALBERT: A Lite BERT for Self-supervised Learning of Language Representations
 本記事ではこのALBERTとはなんぞや,というところを論文を見ながら書いていきます.

*注:筆者はNLP初学者なので間違えている部分があればお教えいただけると嬉しいです.

要約

Increasing model size when pretraining natural language representations often results in improved performance on downstream tasks. However, at some point
further model increases become harder due to GPU/TPU memory limitations,
longer training times, and unexpected model degradation. To address these
problems, we present two parameter-reduction techniques to lower memory consumption and increase the training speed of BERT (Devlin et al., 2019). Comprehensive empirical evidence shows that our proposed methods lead to models that scale much better compared to the original BERT. We also use a selfsupervised loss that focuses on modeling inter-sentence coherence, and show
it consistently helps downstream tasks with multi-sentence inputs. As a result,
our best model establishes new state-of-the-art results on the GLUE, RACE, and
SQuAD benchmarks while having fewer parameters compared to BERT-large.
The code and the pretrained models are available at https://github.com/
google-research/google-research/tree/master/albert.

 書いてあることをGoogle翻訳にかけ(),まとめると,

  • 自然言語処理において事前学習時にモデルを大きくすると,大抵パフォーマンスが向上する
  • しかし,GPUやTPUのメモリ制限,学習時間の増加,またその他の予期せぬトラブルのため,普通はモデルを大きくするのは難しい
  • そこで,BERTの軽量化し,かつ,学習を高速化させる2つの手法を紹介する
  • また,その手法を用いたモデル(ALBERT)はGLUE,RACE,SQuADのタスクでBERT-largeを超えて,SOTAをたたき出した

というようなことを言っています.
 つまり,軽量で高速で高精度というとんでもないモデルということになりますね.

 ちなみに、ソースコードと事前学習済みモデルはここにあります。

ALBERTのメインポイント

 Introductionの前半を簡単にまとめると、「単純にモデルのサイズを上げれば精度が上がるというわけではなく、逆に精度が下がる場合もあるよ。しかもモデルのサイズを上げるのは、分散処理するにしてもオーバーヘッド(余分な処理)がモデルのパラメータ数に依存するから、あまり良くない」ということを言っていますね。
 この論文では、BERT-largeと、そのパラメータ数を2倍にしたBERT-xlargeでRACEタスクにおいて比較をしています。(以下を参照)
Screenshot from 2019-10-26 00-52-41.png
Screenshot from 2019-10-26 00-52-26.png
 そして、本題の後半ですが、ALBERTの2つのメモリ削減の手法について解説しています。
 まず1つ目は、「factorized embedding parameterization」(因数分解埋め込みパラメータ?)です。

因数分解埋め込みパラメータ

 ざっくりいうと、埋め込みベクトルの次元Eと隠れ層の次元Hを分けて小さくしよう、ということらしいです。一般的に、BERTの埋め込みベクトルの次元Eと隠れ層の次元Hを同じで、BERT-baseの場合は768次元となっています。
 普通の方法だと、計算量は
$$O(V\times H)\qquad V=size(vocabulary)$$
$$
M_{prams}=
\begin{bmatrix}
a_{11} & \cdots & a_{1n} & \cdots & a_{1h}\\
\vdots & \ddots & & & \vdots \\
a_{n1} & & a_{nn} & & a_{nh} \\
\vdots & & & \ddots & \vdots \\
a_{v1} & \cdots & a_{vn} & \cdots & a_{vh}
\end{bmatrix}\qquad (V\times H)
$$
ですが、因数分解を利用してEとHを分けた場合は、
$$O(V\times E+E\times H)$$
$$
M_{prams}=
\begin{bmatrix}
a_{11} & \cdots & a_{1n} & \cdots & a_{1e} \\
\vdots & \ddots & & & \vdots \\
a_{n1} & & a_{nn} & & a_{ne} \\
\vdots & & & \ddots & \vdots \\
a_{v1} & \cdots & a_{vn} & \cdots & a_{ve}
\end{bmatrix}
\times
\begin{bmatrix}
a_{11} & \cdots & a_{1n} & \cdots & a_{1h} \\
\vdots & \ddots & & & \vdots \\
a_{n1} & & a_{nn} & & a_{nh} \\
\vdots & & & \ddots & \vdots \\
a_{e1} & \cdots & a_{en} & \cdots & a_{eh}
\end{bmatrix}\qquad (V\times E)\times (E\times H)
$$
となります。これはEを小さくするほど、モデルのサイズ低下に貢献します。論文を見ると、埋め込みベクトルの部分のみこの方法を使っている読み取れます。体感的にはあまり効果がなさそうに見えますが、実際問題、語彙の大きさは一般的に30,000近くあるため、ここがパラメータのかなりの部分を占めます。

実例を出すと、
$$
V=32000,H=1024,E=128 \\
V*H=32,768,000 \\
V*E+E*H=4,227,072
$$
約88%ものパラメータ削減(Embeddingのみ)となります!

 次に、2つ目「Cross-layer parameter sharing(クロスレイヤーパラメータ共有)」です。(日本語訳これでいいかな?)

クロスレイヤーパラメータ共有

 普通のパラメータ共有といえば、FFNレイヤーのみや、Attentionレイヤーのみなどですが、ALBERTは基本的に、すべてのacross layer(隣のレイヤ)においてパラメータを共有するようです。
 また、L2距離とコサイン類似度をもちいて、各レイヤーの入力と出力の類似度を求める実験をしていますが、DQE(Deep Equilibrium Models)を理解していないので、間違っているかもしれません。(不勉強で申し訳ない。。。)
 以下の図は、各レイヤーの入力と出力のベクトルの差をL2距離(左)、コサイン類似度(右)を用いて測定しています。
Screenshot from 2019-10-28 00-08-43.png
 BERTは赤い点線の方ですが、各レイヤーでかなり振動しているように見えます。それとは対象的に、ALBERTはレイヤーが進むにつれて収束していっているようです。結果として、重み共有はネットワークの安定化の効果があるようです。

 上記の2つのパラメータ削減の手法を用いたALBERTはBERTと比べるとかなりパラメータを削減できます。ぱっと見た感じではモデルが大きくなればなるほどその効果は絶大なものとなっているようです。詳細は以下の表をご覧ください。
Screenshot from 2019-10-28 00-29-09.png
 また、パラメータ数が減るということは、計算量が減り学習速度が上がるということでもあります。BERT-largeとALBERT-largeを比較すると、パラメータ数は約1/18倍、学習速度は1.7倍となったそうです。

 また、ALBERTにはもう一つBERTと違う点があります。それが「Sentence-Order Prediction(SOP)」です。

Sentence-Order Prediction

 事前知識として、BERTの事前学習には「MLM(Masked Language Model)」と「NSP(Next Sentence Prediction)」があります。MLMの方がBERTの事前学習のメインなのですが、NSPは実はタスクとして簡単すぎるのではないかと言われています。理由としては、トピック予測とコヒーレンス(一貫性)予測がコンフリクトしていて、トピック予測のほうが簡単なタスクとなってしまっているかららしいです。(?)つまり、本来文章の一貫性がほしいところが、トピックでも解けてしまうということですかね?
 そこで、Sentence-Order Predictionという新しい事前学習タスクを提案しています。
 内容としては、正解のデータセットはそのままに、不正解のデータセットは、正解例の順序を逆にしたものを使用します。それによって、文章のトピックのみで判断するのではなく、一貫性で予測してくれるようになるらしいです。

ALBERTの精度比較

 そして、気になる精度の話ですが、以下の表を見ていただければ解ると思います。
Screenshot from 2019-10-28 10-31-53.png
 ALBERT-xxlargeが、BERTを越しSOTAとなっています。しかし、ALBERT-base、ALBERT-large注目していただきたいです。ALBERT-largeはBERT-base以上のスコアでパラメータ数は1/6になっています。この論文の売りは精度ではなく、モデルのサイズを圧縮するかというところなので、この結果はかなりいい感じなのではないでしょうか。

感想

 近年、モデルのサイズはSOTAのスコアとともに増加しつつあります。その中で、この論文はスコアを上げるとともにモデルの軽量化まで達成しているという点がとても素晴らしいと思います。また、モデルの軽量化の手法が2つともかなりシンプルな点もとてもすばらしいです。ただ、この論文のとおりだと推論時は特に速度が変わらないので、そこのところも改善できれば実用的にも強いと思います。

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

FBX SDK の pScene と lScene の違い

FBX SDKの pScene と lScene の違い

FBX SDK のサンプルコードを見ていると、C++版でもPython版でも、 pScenelScene という変数をよく見かけます。
どちらも、 FbxScene クラスのインスタンスなのですが、この pl の違いが気になったので、調べてみました。

pScene は引数で lScene はローカル変数だった

公式ドキュメントの以下のページに説明がありました。
Autodesk FBX / Information and technical support

なんと、pScene は引数で lScene はローカル変数でしたw

まとめると、以下のようになります。

接頭辞 意味
Fbx FBX SDKが用意したクラスの接頭辞 FbxScene
p クラスのメンバー関数(メソッド)の引数の接頭辞 pScene
l ローカル変数の接頭辞 lScene
g グローバル変数の接頭辞 gScene
m クラスのメンバー変数の接頭辞 mScene

ここまで命名を細かく分けるのは、 C++ っぽいなぁと思いました。

さいごに

意味がわかってスッキリしました!

ちなみに、上記を調べるまでは、勝手にシステムハンガリアン記法だと思い込んでいました。
システムハンガリアンだと、

  • p はポインタ型
  • llong

です。

ちゃんと調べてよかったですw

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

HerokuでServer Error 500 が出て数日悪戦苦闘したが解決したケアレスミス

背景

DjangoでWEBアプリを開発していました。
ローカルで仮想環境を作って、Herokuにデプロイしていましたが、ある日突然サーバーエラー500。
特定のページだけだったのですが、どうにもこうにも原因が分からず、1,2週間ほど放置してしまいました。。

どこかの記事で、settigns.pyの中で、

DEBUG = TRUE

で一度でもデプロイをしたらサーバーエラーが出続けるというのを見つけたのですが、それが事実であれば、データベースの内容を移行させなくてはならなくなり、面倒です。。。

Herokuのサーバーエラー500は厄介

何が厄介かというと、エラーの種類によっては、
- ローカルではエラーにならない
- heroku のログを見てもserver error 500 以外のことが分からない
からなのです。

原因は

原因はいたって簡単。CSSやJSを読み込むところがリンクエラーになっていました。

<link rel="stylesheet" href="{% static 'css/●●●.css' %}">
<script src="{% static 'js/●●●.js' %}"></script>

↑です。

エラーになったページをローカルで開いて、ソースを読み込んだらすぐにわかりました。

はまってしまったので、備忘録として残しておきます。

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

C# (LINQ) で Python (list, numpy) っぽくリスト操作

はじめに

Python のリストや numpy ndarray の基本操作に対応する処理を、C# でどう書けばよいか?
毎回ウェブをあちこち探すのはとても時間がかかります。

普段 Python を使っていてたまに C# で書くとき、すぐに対応が見つかるように、一通りまとめておきます。

バージョン

Python 3、C# 6.0 (.NET Framework 4.6) を前提にします。.NET Framework 3.5 でも一部を除き対応していると思われます。

配列とリスト

  • Python は標準リストと numpy.ndarray を扱います。
  • C# はリストを主に扱います。

C# の配列とリストはどちらも IEnumerable を実装しているので、LINQ の説明の多くは配列でも使えます。

(Python 標準ライブラリの array.array はあまり使われないと思いますので省略します。)

サンプルコードでの配列名とリスト名

オブジェクト名に以下の文字を入れておきます。
宣言や宣言時の型は、誤解がない範囲で省略します。

  • Python
    • 標準リスト: lst
    • 配列: arr
  • C#
    • リスト: list
    • 配列: array
Python
# 以下が import されていると仮定します
import numpy as np  # for numpy.ndarray

lst = [1, 2, 3]  # 標準リスト
arr = np.array([1, 2, 3])  # numpy.ndarray
C#
// 以下が using されていると仮定します
using System;
using System.Collections.Generic;  // for List
using System.Linq;

List<int> list = new List<int> {1, 2, 3};  // リスト (Collections.Generic)
int[] array = new int[] {1, 2, 3};  // 配列

おことわり

この記事は、数値計算や機械学習の置き換え、パフォーマンスを目的とはしていません。あくまで、Python のリストや numpy.ndarray の基本操作に類似した操作を C# で書く場合の参考です。

本来の numpy 的用途(速度重視)なら、BLAS や MKL など専用演算ライブラリがベースにあるモジュールやパッケージを使った方がもちろん高速なはず。たとえば Math.NET Numerics (MKL版) とか。

Python 基本操作

range

Python
# range(stop) / range(start, stop[, step])
range(5)  # 0, 1, 2, 3, 4  (= range(0, 5))
range(1, 5)  # 1, 2, 3, 4
# 1つ飛ばし
range(3, 10, 2)  # 3, 5, 7, 9 (3-9の奇数)

for val in range(1, 5):
    print(val)
C#
// IEnumerable<int> Range (int start, int count);
Enumerable.Range(0, 5)  // 0, 1, 2, 3, 4
Enumerable.Range(1, 4)  // 1, 2, 3, 4 (第2引数は要素数)
// 1つ飛ばし
Enumerable.Range(0, 4).Select(i => i * 2 + 3)  // 3, 5, 7, 9 (*1)
Enumerable.Range(3, 7).Where(i => i % 2 == 1)  // 3, 5, 7, 9 (*2)

foreach (var val in Enumerable.Range(1, 4))
{
  Console.WriteLine(val);
}

Python3 の range (Python2 だと xrange)と C# の Enumerable.Range はどちらも遅延評価されます。

range/Range もそうですが、Python では (開始値、終了値+1) とするのに対し、C# では (開始値、要素数) とするメソッドが多いようです。

Python の range に合わせる

Python の range(start, stop, step) に合わせた実装を考えてみます。

C#でrange(python)に近いもの
// (*1) を拡張してみる(負の数や step < 0 にも対応.step = 0 のエラー処理省略)
Enumerable.Range(0, (int)Math.Ceiling((stop-start)/(double)step))
    .Select(i => i * step + start)

// (*2) を拡張してみる(これだと負の数には対応できないし効率悪そう)
// Enumerable.Range(start, stop-start).Where(i => i % step == start % step)

// いっそのこと for ループで静的メソッドを定義してしまう方が分かりやすいかも
public static IEnumerable<int> MyRange(int start, int stop, int step)
{
    if (step > 0) for (int i = start; i < stop; i += step) yield return i;
    else if (step < 0) for (int i = start; i > stop; i += step) yield return i;
    else yield break;
}

遅延評価せずリストや配列にする

Python
lst = list(range(0, 5))
arr = np.array(range(0, 5))
C#
list = Enumerable.Range(0, 5).ToList();
array = Enumerable.Range(0, 5).ToArray();

numpy.arange

Python
np.arange(0, 1, 0.1)  # 0, 0.1, ..., 0.9
C#
Enumerable.Range(0, 10).Select(i => i * 0.1);  // 0, 0.1,..., 0.9

一般には、先の「(*1)を拡張してみる」の実装で、小数の場合でも行けるのではと思います。

一方、先の MyRange を double 版の実装にして 0.1 ずつインクリメントなどしてしまうと、丸め誤差で大小比較の判定がうまくいかないです。decimal 版なら大丈夫でしょう。

内包表現や各要素に対する演算

Python
lst2 = [x * x for x in lst1]  # もしくは [x ** 2 for x in lst1]
arr2 = arr1 * arr1  # もしくは arr1 ** 2 (powerは遅いはず)
C#
list2 = list1.Select(x => x * x).ToList();

内包表現 (list comprehension) を使っていたところは、LINQ の Select で何とかなることが多いです。あとは、文脈をよく考えて、遅延評価をするかしないか判断が必要になります。
遅延評価した方がよいなら ToList() せず IEnumerable のままにします。

例:要素の総和で正規化

要素の総和が1になるように正規化する方法をまとめておきます。確率計算でもよく使います。

Python
lst2 = [x / sum(lst1) for x in lst1]
arr2 = arr1 / sum(arr1)  # もしくは arr1 / arr1.sum()
C#
list2 = list1.Select(x => x / list1.Sum()).ToList();

map, filter

Python では、イテレータが欲しい、遅延評価したい、という場合はリスト内包表現ではなく mapfilter を使えますが、これは LINQ だとそのまま SelectWhere に対応しそうです。

Python
for x in map(lambda x: x * x, lst):  # 各要素の二乗を列挙して表示
    print(x)
for x in filter(lambda x: x < 4, lst):  # 4未満の要素を表示
    print(x)

# 組み合わせ:二乗した要素が10未満なら二乗した値を表示
for x in filter(lambda x : x < 10, map(lambda x: x * x, lst)):
    print(x)
C#
foreach(var x in list.Select(x => x * x))   // 各要素の二乗を列挙して表示
    Console.WriteLine(x);
foreach(var x in list.Where(x => x < 4))  // 4未満の要素を表示
    Console.WriteLine(x);

// 組み合わせ:二乗した要素が10未満なら二乗した値を表示
foreach(var x in list.Select(x => x * x).Where(x => x < 10))
    Console.WriteLine(x);

filtermap が組み合わさってくると、LINQ のようにメソッドチェーンで書く方が分かりやすいですね。

enumerate

リストや配列の値だけでなくそのインデックスも欲しい場合、Python では enumerate ですが、C# では Select を使えます。

Python
lst1 = ['foo', 'bar']
lst2 = ['[{0}] {1}'.format(i, s) for i, s in enumerate(lst1)]
C#
var list1 = new List<string> {"foo", "bar"};
var list2 = list1.Select((s, i) => string.Format("[{0}] {1}", i, s)).ToList()

LINQ の Select は、value, index の順序が Python の enumerate と逆なので注意が必要です。

zip および要素ごとの演算

Pythonで2つのリストをzip
lst3 = [x - y for x, y in zip(lst1, lst2)]  # 要素ごとの引き算
sum([x * y for x, y in zip(lst1, lst2)])  # 内積計算
NumPyで要素ごとの演算
arr3 = arr1 - arr2  # 要素ごとの引き算
arr1.dot(arr2) # もしくは np.dot(arr1, arr2)  # 内積計算
C#
list3 = list1.Zip(list2, (x, y) => x - y).ToList();  // 要素ごとの引き算
list1.Zip(list2, (x, y) => x * y).Sum();  // 内積計算
// もしくは
list3 = Enumerable.Zip(list1, list2, (x, y) => x - y).ToList();  // 要素ごとの引き算
Enumerable.Zip(list1, list2, (x, y) => x * y).Sum();  // 内積計算

3つ以上のリスト(配列)を組み合わせる

Python
lst4 = [x + y + z for x, y, z in zip(lst1, lst2, lst3)]  # 要素ごとの足し算
arr4 = arr1 + arr2 + arr3  # 要素ごとの足し算
C#で3つのリストをZip
list4 = list1.Zip(list2, (x, y) => x + y)
    .Zip(list3, (zip1, z) => zip1 + z).ToList(); // 要素ごとの足し算

LINQ の Zip は3つ以上のリストをとれないので、Zip を繰り返して使うことになるようです。

一段目の Zip でペアをそのまま次段へ匿名型で渡せば、二段目の Zip でまとめることもできます。

C#で3つのリストをZip(匿名型でつなげる)
list4 = list1.Zip(list2, (x, y) => new {x, y})
    .Zip(list3, (zip1, z) => zip1.x + zip1.y + z).ToList(); // 要素ごとの足し算

初期化

0 で初期化

Python
lst = [0] * 10  # 1次元ならこれでOK (入れ子など参照が絡む場合は落とし穴)
lst = [0 for _ in range(10)]  # これもOK (入れ子など参照が絡んでくるならこちら)
arr = np.zeros(10)  # 0 で初期化された要素10の ndarray
C#
int[] array = new int[10];  // 0 で初期化された要素10の配列
List<int> list = new int[10].ToList();
List<int> list = new List<int> (new int[10]);  // コンストラクタの引数に配列を渡す

同じ値で埋めて初期化

Python
# False で埋めた配列
lst = [False] * 10  # 1次元ならこれでOK (入れ子など参照が絡む場合は落とし穴)
lst = [False for _ in range(10)]  # 入れ子リストなど参照が絡んでくるならこちら
arr = np.full(10, False)  # False で初期化された要素10の ndarray
C#
List<bool> list = Enumerable.Repeat(false, 10).ToList();  // こちら落とし穴あり(*1)
List<bool> list = Enumerable.Range(0, 10).Select(_ => false).ToList();
List<bool> list = new bool[10].Select(_ => false).ToList();
// いずれも ToList() を ToArray() にすれば配列に

(*1) 参照型でRepeat だと、ひとつ変えるとすべて変わる.この落とし穴は Python の [値] * 要素数 と似ている.
https://qiita.com/yosizo@github/items/1adcff1fc974cde5256a

連番の値で初期化

「遅延評価せずリストや配列にする」の通り、RangeToList()ToArray() すればよい。 Python で range から listnp.array を作るのと同様.

乱数で初期化

整数乱数で初期化

Python
import random  # 標準ライブラリ
# randint(a,b)は randrange(a,b+1)のエイリアス
lst = [random.randrange(0, 10) for _ in range(5)]  # 0以上9以下で5個
lst = [random.randint(0, 9) for _ in range(5)]  # 0以上9以下で5個
#  lst = random.sample(range(10), k=5))  # 0以上9以下で5個(重複無しなので上とは異なる)

arr = np.random.randint(0, 10, 5)  # 0以上9以下で5個

numpy の random.randint は、標準ライブラリの random.randint とは異なり(むしろ randrange と同じで)指定値未満の乱数を返すので注意!

C#
System.Random rg = new System.Random();  // random number generator (usingしておいてもよい)
int[] rndarray = Enumerable.Range(0, 5).Select(_ => rg.Next(0, 10)).ToArray();  // 0以上9以下で5個
List<int> rndlist = Enumerable.Range(0, 5).Select(_ => rg.Next(0, 10)).ToList();  // 0以上9以下で5個

Next()の第一引数(最小値)は省略すると0以上となる。

実数乱数で初期化

Python
import random  # 標準ライブラリ
lst = [random.random() for _ in range(5)]  # 0以上1未満で5個

arr = np.random.rand(5)  # 0以上1未満で5個
C#
double[] array = Enumerable.Range(0, 5).Select(_ => rng.NextDouble()).ToArray();  // 0以上1未満で5個
List<double> list = Enumerable.Range(0, 5).Select(_ => rng.NextDouble()).ToList();  // 0以上1未満で5個

乱数生成アルゴリズム自体を変える方法はこちらで解説されている。

値をコピーして初期化

Python
lst1 = [3, 1, 4]  # コピー元を適当に作っておく
lst2 = list(lst1)  # lst1, lst2 のオブジェクトIDは異なる
lst2 = lst1[:]  # これも可 (lst1, lst2 のオブジェクトIDは異なる)
# lst2 = lst1  # lst1, lst2 のオブジェクトIDは一致 (コピーではない)
import copy
lst2 = lst1.copy()  # これも可 (lst1, lst2 のオブジェクトIDは異なる)

arr1 = np.array([3, 1, 4])  # コピー元を適当に作っておく
arr2 = np.copy(arr1)  # lst1, lst2 のオブジェクトIDは異なる
arr2 = arr1.copy()  # これも可 (lst1, lst2 のオブジェクトIDは異なる)
# arr2 = arr1  # arr1, arr2 のオブジェクトIDは一致 (コピーではない)

C#
var list2 = new List<int>(list1);  // コピーコンストラクタで初期化
var list2 = list1.ToList();  // これも可

var array2 = new int[array1.Length];  // あらかじめ領域確保
array1.CopyTo(array2, 0);  // 第二引数はコピー先の開始 index
Array.Copy(array1, array2, array1.Length);  // これも可

var array2 = array1.ToArray();  // これも可
var array2 = array1.Clone() as int [];  // これも可 (キャスト必要)

要素へのアクセスやインデックス取得

スライス (Slice)

Python(numpy)
array[0:5]  # 0-4番目の5要素
array[5:]  # 0-4の5要素を飛ばして 5-
array[3:5]  # 0-2を飛ばして 3-4 の2要素
C#
array.Take(5)  // 0-4番目の5要素
array.Skip(5)  // 0-4の5要素を飛ばして 5-
array.Skip(3).Take(2)  // 0-2を飛ばして 3-4 の2要素

list.GetRange(3, 2)  // Listならこれも可

条件を満たす要素のインデックスを取得

Python(numpy)
idx_lst = np.where(lst == val)
C#
var indexList = list.Select((x, i) => new { x, i })
    .Where(xi => xi.x == val).Select(xi => xi.i);

最小(最大)の値を取る要素のインデックスを取得

Python(numpy)
arr = np.array([2, 1, 8, 4, 0, 8])  # 空でないとする

idx = np.argmin(arr)  # 最小値をとる最初の要素(0)のインデックス (4)
idx = np.argmax(arr)  # 最大値をとる最初の要素(8)のインデックス (2)
C#
var list = new List<int> { 2, 1, 8, 4, 0, 8 };
// var list = new List<int>();  // 空のケースも考慮するならコメントアウトした方

var argmin = list.Select((x, i) => new { x, i })
    .Aggregate((min, xi) => xi.x < min.x ? xi : min).i;  // 4
//    .Aggregate(new { x = int.MaxValue, i = -1 }, (min, xi) => xi.x < min.x ? xi : min).i;  // 4

var argmax = list.Select((x, i) => new { x, i })
    .Aggregate((max, xi) => xi.x > max.x ? xi : max).i;  // 2
//    .Aggregate(new { x = int.MinValue, i = -1 }, (max, xi) => xi.x > max.x ? xi : max).i;  // 2

空のリストにも対応するなら Aggregate の第一引数にシードを与えます。

「特定のキーで Min()Max() すると、そのキーの最小(最大)値だけが戻ってきてしまうが,要素のインデックスもしくは(キーを含む)オブジェクトそのものを取得したい」という質問は昔から StackOverflow でも何度も出ているようです。(Qiita でも。)

別途実装せず LINQ でシンプルに書くには、例えば以下のやり方があります。

  1. Aggregate を使う(上のコード) -> O(n) なのでよいが可読性やや難あり
  2. OrderByOrderByDescending して FirstTake -> ソートで O(n log(n)) になる
  3. MinMax で取得した最小(最大)値で Where -> 2回走査してしまう

n が小さければ、可読性重視で 2, 3 もありかも。
再利用性も考え、素直にループ(イテレータ)で実装しておくか、すでにある実装を使うのもよさそう(MoreLINQMinByMaxBy など)。

リスト操作

Python(標準リスト)
lst.append(val)  # 末尾へ値を追加
lst.extend(lst2)  # 末尾へ別のリストを追加
lst.insert(idx, val)  # idx へ値を挿入
lst[idx:idx] = lst2  # idx位置に lst2の要素すべてを挿入

lst.clear()  # 要素をすべて削除
lst.remove(val)  # 最初に見つかった val の値をもつ要素削除
del lst[idx:idx+len]  # idx位置から len個の要素を削除
del lst[-1]  # 末尾を削除
C#
list.Add(val);  // 末尾へ値を追加
list.AddRange(list2);  // 末尾へ別のリストを追加
list.Insert(idx, val);  // idx へ値を挿入
list.InsertRange(idx, list2);  // idx位置に list2の要素すべてを挿入

list.Clear();  // 要素をすべて削除
list.Remove(val);  // 最初に見つかった val の値をもつ要素削除
list.RemoveRange(idx, len);  // idx位置から len個の要素を削除
list.RemoveAt(list.Count - 1);  // 末尾を削除

// その他の操作
list.RemoveAll(v => v == val);  // ラムダ式で削除する要素の条件を指定 (左例は val 値をもつ要素全て削除)

文字列操作

リストが絡む文字列操作に限定して取り上げます。

split, join

Python
str1 = 'pen-pineapple-apple-pen'
str1_list = str1.split('-')

str2_list = ['pen', 'pineapple', 'apple', 'pen']  # str1_list
str2 = '-'.join(str2_list)  # str1
C#
string str1 = "pen-pineapple-apple-pen";
string[] str1Array = str1.Split('-');  // Split は配列を返す
List<string> str1List = str1Array.ToList();  // リストが必要なら変換

List<string> str2List = new List<string> {"pen", "pineapple", "apple", "pen"};
string str2 = string.Join("-", str2List);  // 可変長引数で文字列を渡すことも可能
// string str2 = str2List.Aggregate((x, y) => x + "-" + y);  // 空リストだとInvalidOperation
// string str2 = str2List.Aggregate(string.Empty, (x, y) => x + "-" + y).TrimStart('-');

しばらく別の言語で書いていると、Python の join は書き方に迷います。

C# の Split では、セパレータに char を渡すときは単純ですが、文字列を渡す場合は第二引数のオプション指定(StringSplitOptions.None など)が必要です。
Join は基本文字列を渡します・・・ややこしいですね。

string.Join の代わりに Aggregate を使うとメソッドチェーンで書けますが、こちらの記事で議論されているようにいくつか問題があります。
まず、空のリストが渡されると例外が投げられます。初期値として空文字を渡すと例外を回避できますが、今度は先頭にもセパレータがついてしまうため、これを削除(上の例では TrimStart)する必要があります。

StackOverflow では以下のような書き方も紹介されていました。パフォーマンスも string.Join 並みによいということです。

C#/LINQのAggregateで文字列を結合
string strJoin = strList.Aggregate(new StringBuilder(),
    (x, y) => x.Append(x.Length==0 ? "" : "-").Append(y)).ToString();

ただ、いくつかの記事やスレッドを追った感じでは、あえて Aggregate にせず、素直に string.Join する方がよい、という結論が多いようです。

print とフォーマット指定

Python
lst = [1/2, 3/4, 5/6]
print(lst)  # [0.5, 0.75, 0.8333333333333334]
print("[" + ", ".join("%.2f" % x for x in lst) + "]")  # [0.50, 0.75, 0.83]

arr = np.array([1/2, 3/4, 5/6])
print(arr)  # [0.5        0.75       0.83333333]
print(np.array_str(arr, precision=2))  # [0.5  0.75 0.83]
print(np.array2string(arr, precision=2, floatmode='fixed'))  # [0.50  0.75 0.83]
# array2string はオプション豊富.小数点以下の桁数を固定して0埋めできる
# array_str の中身は array2string とのこと

# numpy は以下でも可(一時的に使うなら元の設定に戻しておく)
orig_printopt = np.get_printoptions()  # 現在の設定を辞書へ
np.set_printoptions(precision=2, floatmode='fixed')
print(arr)  # [0.50  0.75 0.83]
np.set_printoptions(**orig_printopt)  # 設定を戻す(辞書を展開して引数へ)
C#
var list_print = new List<double> {1.0/2, 3.0/4, 5.0/6};
Console.WriteLine("[" + string.Join(", ", list) + "]");  // [0.5, 0.75, 0.833333333333333]
Console.WriteLine("[" + string.Join(", ", list.Select(x => x.ToString("0.00"))) + "]");  // [0.50, 0.75, 0.83]

ソート

昇順・降順ソートと安定性

Python
sorted_lst = sort(lst)  # 昇順ソートされたリストが返る
lst.sorted()  # lstそのものが昇順ソートされる (in-place)

sorted_data = np.sort(arr)  # 昇順ソート
sorted_data = np.sort(arr)[::-1]  # 降順ソート
C#
Array.Sort(array);  // arrayそのものが昇順ソートされる (inplace)
Array.Reverse(array);  // 昇順ソート後にひっくり返して逆順へ

list.Sort();  // listそのものが昇順ソートされる
list.Reverse();  // 昇順ソート後にひっくり返して逆順へ

// ラムダ式を使うと逆順ソートも一回で
Array.Sort(array, (x, y) => y - x);  // arrayそのものが逆順ソートされる
list.Sort((x, y) => y - x);  // listそのものが逆順ソートされる

// LINQを使ってもソートできる(特に複数のキーがある場合は便利.リストでも利用可)
sorted_array = array.OrderBy(x => x);  // 昇順ソート(戻り値で受け取る)
sorted_array = array.OrderByDescending(x => x);  // 降順ソート(戻り値で受け取る)

List の内部は配列なのでソートのアルゴリズムは同じとか。
ソートアルゴリズムの安定性について:Reverse() はちょっと強引ですが、もともと Array.Sort() も安定ソートではないので、同じ値のデータの順序を気にしない場合はありでしょう。
一方で、LINQ の OrderBy() は安定ソートとのこと(Stack Overflow より)。安定なソートが欲しい場合は LINQ が手っ取り早いかも。

ちなみに Python では list のソートは安定で、numpy でも 'stable' をオプションで渡すと安定性が保証されます。

ソートされたデータの添え字

Python
idx = np.argsort(arr)
C#(array1とインデックスのソート結果をarray2とidxへ)
// Array.Sort で
int[] array2 = new int[array1.Length];  // ソート結果を別配列にするための準備
array1.CopyTo(array2, 0);  // array1 を array2 にコピー
int[] idx = Enumerable.Range(0, array2.Length).ToArray();  // 連番の配列 0,1,...
Array.Sort(array2, idx);  // 別配列も一緒に並べ替えるオーバーロードがあるのでこれを使う
for(var i=0; i < array2.Length; i++)  // ついでに表示
{
    Console.WriteLine($"key: {array2[i]}, idx: {idx[i]}");
}

// LINQ で
var sortedPair = array1
    .Select((x, i) => new KeyValuePair<int, int>(x, i))
    .OrderBy(x => x.Key);
var array2 = sortedPair.Select(x => x.Key).ToList();
var idx = sortedPair.Select(x => x.Value).ToList();

// 分かりやすい名前でタプル(ペア)を作りそのまま使ってしまうのもあり
var sortedPair = array1
    .Select((x, i) => (Key: x, Index: i))  // C# 7 (.NET Framework 4.7 など)
    .OrderBy(x => x.Key)
    .ToList();
sortedPair.ForEach(x => {  // ついでに表示
    Console.WriteLine($"key: {x.Key}, idx: {x.Index}");
});

この Stack Overflow の記事を参考にしました。

二つの系列をまとめてソート

C#
var list1 = new List<int> { 3, 4, 1, 2 };
var list2 = new List<int> { 6, 7, 8, 9 };
var zipped = list1.Zip(list2, (x1, x2) => new {key1 = x1, key2 = x2});
var sorted = zipped.OrderBy(x => x.key1).Select(x => x.key2);  // 8, 9, 6, 7

基本的な統計量や数学関数

基本的な統計量

Python
np.mean()
np.std()
np.max()
np.min()
np.argmax()
C#
double mean = data.Average();
double varP = data.Select(x => (x - mean) * (x - mean)).Sum() / data.Length;
double stdevP = Math.Sqrt(var);

そろそろ Math.NET などパッケージを入れたほうがよいかもしれません。

bool型の統計量

Python
lst = [True, False, True]
sum(lst)  # 2
any(lst)  # True
all(lst)  # False
C#
var list = new bool[] { true, false, true };
list.Count()  // Sum() ではない
list.Any(v => v)  // true
list.All(v => v)  // false

数学関数

Python
np.round()
C#
Math.Round()

多次元配列

以下書きかけです。リストのリストを矩形行列としてみた操作(つまり内側のリストはすべて同じ長さと仮定)。

初期化

Python
mat = np.zeros((NROWS, NCOLS))
C#
List<List<double>> mat = Enumerable.Range(0, NROWS).Select(_ => new double[NCOLS].ToList()).ToList();

Repeat ではなくそれぞれ実体化していく。

行や列をコピー

Python
arr = mat[i,:].copy()
arr = mat[:,j].copy()
arr = mat.flatten()
C#
var list = mat[i];
var list = mat.Select(r => r[j]).ToList();
var list = mat.SelectMany(r => r).ToList();  // flatten

列ごとの平均

Python
mat.mean(0)
C#
var ret = mat.SelectMany(r => r.Select((val, j) => new {val, j}))
     .GroupBy(x => x.j, (key, y) => {return y.Average(z => z.val);});

いったん {val, j} を要素とする x へ平坦化したあとに、インデックス j でグルーピングして y とし、それぞれのグループで平均を計算している。

参考: https://teratail.com/questions/76764

その他(未テスト)

DeepCopy

転置

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

Pythonの正規表現で特殊記号をすべて闇に葬り去りたいとき

記号がいらない

/ * # %
など、スクレイピング・自然言語処理においては記号はいらない場合があります。

Pythonのreモジュールで一括削除をします。

import re
code_regex = re.compile('[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】&*・()$#@。、?!`+¥%]')

txt = input().rstrip()
cleaned_text = code_regex.sub('', txt)
print(cleaned_text)

[]に入っている記号のどれかに一致してしたとき削除してくれるため、この中にいらない文字を入れて削除をします。
全角の記号も強引に打ち込んで闇に葬り去りましょう。

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

SimPyで離散事象シミュレーション(5) エージェントを組み込もう

はじめに

SimPyというPythonの離散事象シミュレーション用のパッケージを見つけて試してみたら気に入ったので自分用の備忘録も兼ねて使い方をまとめていく.

今回は最終回として,これまでの回で何度かとりあげた在庫管理の例を題材にして,簡単なテーブル型のQ学習で行動政策を獲得していく在庫管理者エージェントを導入してみよう.

サンプルコードと概説

さっそくサンプルコードをみていこう.第2回や第4回にとりあげたのとほぼ同じ在庫管理モデルのシミュレーションである.Containerリソースを継承したCustomContainerクラスで対象システムをモデル化している.

今回新たに追加したのはコストの側面である.在庫管理者が発注を出すたびに発注コスト(ORDER_COST)がかかり,品物を保管しておくために1個1期あたり所定の在庫保管コスト(STOCK_COST)がかかる.さらに,顧客を待たせた場合は1個1期あたり所定のペナルティコスト(PENALTY)が必要になる.

在庫管理者はこれらのコスト(の割引現在価値)がなるべく小さくなるように,発注のタイミングと量を決められるように行動政策を学習したい.

配送業者のプロセス(deliverer())と顧客のプロセス(customer())は第2回や第4回のものとほぼ同じであり,recorder()プロセスは描画のためのデータを記録するためのもの,main()関数の後半は描画のためのコードなので,それらの詳細は省略する.

以下では,在庫管理者エージェント(Agentクラス)に主に注目する.

import numpy as np
import matplotlib.pyplot as plt
import simpy
import math

STATES = 25  # state in ((,-1], [0, 1], [2, 3], ..., [46,))
ACTIONS = 10  # action in (0, 3, 6, 9, 12, 15, 18, 21, 24, 25)
ORDER_COST = 1
STOCK_COST = 0.025
PENALTY = 0.1
DISCOUNT = 0.9
LEARNING = 0.2

class CustomContainer(simpy.Container):
    def __init__(self, env, capacity=float('inf'), init=0):
        self.env = env
        self.orders = []  # list of back orders
        super(CustomContainer, self).__init__(env, capacity, init)

    @property
    def shortage(self):  # total amount requested by waiting customers
        num = 0
        for customer in self.get_queue:
            num += customer.amount
        return num

    def get_state(self):  # return encoded state number
        total = self.level +sum(self.orders) -self.shortage
        return max(0, min(math.floor(total /2) +1, STATES -1))

    def get_reward(self, action):  # negative reward or cost
        reward = self.level *STOCK_COST +self.shortage *PENALTY
        if action > 0:
            reward += ORDER_COST
        return reward

class Agent:
    def __init__(self, env):
        self.env = env
        self.epsilon = 0.1
        self.recent_rewards = [0] *100
        self.Q = np.random.rand(STATES *ACTIONS).reshape(STATES, ACTIONS) *10
        self.Q[0][0] = math.inf
        for a in range(1, ACTIONS):
            self.Q[STATES -1][a] = math.inf

    @property
    def average_reward(self):
        return sum(self.recent_rewards) /len(self.recent_rewards)

    def e_greedy(self, state):
        if state == 0:  # you should order at least some amount
            if self.epsilon <= np.random.rand():
                return self.Q[state].argmin()  # greedy
            else:
                return np.random.randint(1, ACTIONS)  # random
        elif state == STATES -1:  # you cannot order
            return 0
        elif self.epsilon <= np.random.rand():
            return self.Q[state].argmin()  # greedy
        else:
            return np.random.randint(0, ACTIONS)  # random

    def move(self):
        state_to = self.env.model.get_state()
        while True:
            state_from = state_to
            action = self.e_greedy(state_from)
            if action > 0:  # when you order
                self.env.model.orders.append(action *3)
                self.env.process(deliverer(self.env))  # activate deliverer
                if not self.env.record.triggered:
                    self.env.record.succeed()  # record log
            yield self.env.timeout(1)  # periodic inventory check
            state_to = self.env.model.get_state()
            reward = self.env.model.get_reward(action)
            self.recent_rewards.append(reward)
            self.recent_rewards.pop(0)
            self.update_Q(state_from, action, state_to, reward)

    def update_Q(self, state_from, action, state_to, reward):
        Q_to_min = self.Q[state_to].min()
        self.Q[state_from][action] += LEARNING *(reward +DISCOUNT *Q_to_min -self.Q[state_from][action])

    def update_epsilon(self):
        self.epsilon *= 0.9

def deliverer(env):
    yield env.timeout(3)  # delivery lead time = 3
    env.model.put(env.model.orders.pop(0))

def customer(env):
    while True:
        time_to = np.random.exponential(1)
        yield env.timeout(time_to)
        how_many = np.random.randint(1, 8)  # mean = 4
        env.model.get(how_many)
        if not env.record.triggered:
            env.record.succeed()  # record log

def recorder(env):  # process for recording log for visualization
    _t = []
    env.record = env.event()
    while True:
        yield env.record
        _t.append(env.now)
        env.y11.append(env.model.level)
        env.y12.append(sum(env.model.orders))
        env.y13.append(env.model.shortage)
        env.t.append(env.now)
        env.z.append(env.agent.average_reward)
        if env.now > 200:
            t_min = env.now -200
            env.y11 = [
                env.y11[i] for i in range(len(_t)) if _t[i] > t_min
                ]
            env.y12 = [
                env.y12[i] for i in range(len(_t)) if _t[i] > t_min
                ]
            env.y13 = [
                env.y13[i] for i in range(len(_t)) if _t[i] > t_min
                ]
            _t = [_t[i] for i in range(len(_t)) if _t[i] > t_min]
            env.x = [_t[i] -max(_t) +200 for i in range(len(_t))]
        else:
            env.x = _t
        env.record = env.event()

def main():
    env = simpy.Environment()
    env.model = CustomContainer(env)
    env.agent = Agent(env)
    env.process(recorder(env))
    env.process(customer(env))
    env.process(env.agent.move())
# ---------- code for visualization ----------
    env.x = []
    env.y11 = []
    env.y12 = []
    env.y13 = []
    env.t = []
    env.z = []

    fig = plt.figure(1, figsize=(12, 8))

    ax1 = fig.add_subplot(221)
    ax1.set_xlabel('time')
    ax1.set_ylabel('cost')
    ax1.set_xlim(0, 50000)
    ax1.set_ylim(0, 2)
    line1, = ax1.plot(env.t, env.z, label='average cost')
    ax1.legend()
    ax1.grid()

    ax2 = fig.add_subplot(222)
    ax2.set_xlabel('time')
    ax2.set_ylabel('number')
    ax2.set_xlim(0, 200)
    ax2.set_ylim(0, 60)
    line21, = ax2.plot(env.x, env.y11, label='at hand')
    line22, = ax2.plot(env.x, env.y12, label='ordered')
    line23, = ax2.plot(env.x, env.y13, label='shortage')
    ax2.legend()
    ax2.grid()

    ax3 = fig.add_subplot(223)

    for t in range(1, 1000):
        env.run(until=t*50)  # stepwise execution
        if t % 50 == 0:
            env.agent.update_epsilon()
            print('epsilon = {}'.format(env.agent.epsilon))
        line1.set_data(env.t, env.z)
        line21.set_data(env.x, env.y11)
        line22.set_data(env.x, env.y12)
        line23.set_data(env.x, env.y13)
        heatmap = ax3.imshow(env.agent.Q, vmin=2, vmax=10, cmap='jet', aspect=0.25)
        bar = plt.colorbar(heatmap, ax=ax3)
        plt.pause(0.1)
        bar.remove()
    plt.show()
# ---------- ---------- ---------- ----------

if __name__ == "__main__":
    main()

Agentクラスは,在庫管理者の行動政策を標準的なテーブル型のモデルフリーQ学習で獲得していくエージェントになっている.状態は,現在の在庫量(level)とバックオーダ(oeders)の総量の和から,待たされている顧客の要求量の総和(shortage)を減算したスカラー値を離散化したもの,行動は,簡単のため,3の倍数に限定した発注量である.

Q値を乱数で初期化し,ε-greedyで行動しながら,標準的な方法でQ値を更新していっていることがわかる.この記事は強化学習(Q学習)の解説を意図したものではないので,学習の詳細にはこれ以上踏み込まないことにする.

ここで,注目してほしいことは,このε-greedyに従った在庫管理者エージェントの行動政策が,SimPyのプロセスとしてコード化されており,それが,main()の中で

env.process(env.agent.move())

とするだけで,簡単にシミュレーションに統合できていることである.

まとめ

ここまで5回に渡ってPythonの離散事象シミュレーション用のパッケージSimPyについてまとめてきた.もし最終回まで目を通してくださった方がいたとしたら有り難いと思う.何かの参考になれば幸いです.

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

Djangoレッスン(2) モデルを使おう

前回はDjangoプロジェクトの作成から、自分の書いたHTMLファイルをブラウザで表示するところまで行いました。今回はDjangoのアーキテクチャ(全体像)を説明し、「モデル」と呼ばれるデータを管理する機能について解説します。

Djangoのアーキテクチャ

前回はあらかじめ用意したHTMLファイルを表示するのみでしたが、実際のアプリケーションでは条件に応じて表示するものを出し分けることが要求されます。HTMLファイルのように内容が変わらない情報のことを静的コンテンツと呼ぶのに対し、ユーザのリクエストに応じて内容が変わる情報を動的コンテンツと呼びます。動的コンテンツを配信する仕組みをこれから作っていくわけです。

多くのアプリケーションでは、情報をデータベースと呼ばれるデータの保管場所に保存し、必要に応じてデータを取り出して利用します。HTMLにはデータベースを操作する機能がないため、webページとデータベースを仲介する機能が必要になります。

スクリーンショット 2019-10-28 0.46.35.png

この仕組みをDjangoでは、モデル テンプレート ビュー URLディスパッチャというコンポーネントに分割して実現します。ユーザのリクエストを受け取ってからレスポンスを返す流れを説明します。

スクリーンショット 2019-10-28 0.47.41.png

  • ユーザからのリクエストをURLディスパチャが受け取り、その情報をビューに渡す
  • ビューは、リクエストの内容に応じてモデルを呼び出す
  • モデルは、ビューからの命令に従って、データベースに接続し、データの挿入/抽出及びデータの整形を行い結果をビューに返す
  • ビューはその結果をテンプレートに渡す
  • テンプレートは渡されたデータを所定の枠にはめ込むことでページを作成し、URLディスパッチャに返す
  • URLディスパッチャは出来たページをユーザにレスポンスする

ざっくり言うと、モデルはデータを管理する担当、テンプレートはページを作成する担当、ビューはモデルとテンプレートを繋げる担当です。このようなDjangoのアーキテクチャは、Model、Template、Viewの頭文字をとってMTVモデルと呼ばれます。

メモアプリ

このチュートリアルではメモアプリを作ることを最初の目標にして進めていきます。メモアプリに必要な以下の機能を実装します。

  • メモを作成する
  • メモを一覧表示する
  • メモの詳細を表示する
  • メモを編集する
  • メモを削除する

今回はまずメモデータを保存するためのモデルを作成します。

モデルの定義

memo_app/models.pyを編集します。

memo_app/models.py
from django.db import models

class Memo(models.Model):
    id = models.AutoField(primary_key=True)
    text = models.TextField(null=False)

    class Meta:
        db_table = 'memos'

Memoクラスを定義しました。models.Modelを継承することでDjangoのモデルとして機能するようになります。このクラスをMemoモデルと呼ぶことにしましょう。このモデルはidtextの2つの属性を持ちます。”Memo”は”id”と”text”の値を持っている、という概念を作ると言ってもいいでしょう。Memoモデルに属する1つのデータをMemoモデルのオブジェクトと呼びます。ある1つのオブジェクトはidとtextの2つの値を持つことになります。
下図に名称をまとめてみました。定義したモデルに従ってデータベースにテーブルが作成されるため、対応関係も載せました。
スクリーンショット 2019-10-28 2.10.34.png

idはMemoモデルの中の1つ1つのデータに対して振られる連番のことです。id = models.AutoField(primary_ley=True)はどんなモデルにも付けるのでテンプレだと思ってください。こう定義することで勝手にidを振ってくれます。

textはメモの本文である文字列データのことです。TextFieldにすることで(長い)文字列として扱ってくれます。null=Falseはtextの値を空白にすることを禁止するオプションです。

下のdb_table='memos'ではデータベースにテーブルを作成する際のテーブル名を指定しています。指定しなくてもテーブル名は自動で付きます(が少々気に入らないのでいつも指定しています)。

models.AutoFieldやmodels.TextFieldのようなフィールドや、(null=False)のようなフィールドオプションは他にもまだあるので、必要に応じて調べて使うようにしましょう。

Django: モデルフィールドリファレンスの一覧 - Qiita

モデルの作成

memo_app/models.pyのモデル定義に従って、データベースにテーブルを作成します。データベースを操作する必要はなく、Djangoが提供するコマンドを打つことで行います。2ステップあります。

マイグレーションファイルの生成

$ python manage.py makemigrations

Migrations for 'memo_app':
  memo_app/migrations/0001_initial.py
    - Create model Memo

このコマンドを実行すると、モデルファイルが読み込まれ、マイグレーションファイルというファイルが生成されます。今、memo_app/migrations/0001_initial.pyが生成されました。
この時点ではまだデータベースは変更されていません。次のコマンドを打つと変更されます。

マイグレート

$ python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, memo_app, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying memo_app.0001_initial... OK
  Applying sessions.0001_initial... OK

このコマンドを打つことで、マイグレーションファイルの内容を実際にデータベースに反映させます。最初の実行時にはDjangoがデフォルトで提供するユーザや権限などに関する内容も反映されます。

管理画面

Memoモデルが作成されたことを確認する方法はいくつかあるのですが、管理画面を見るのが分かりやすいかなと思います。

memo_app/admin.pyを編集します。

memo_app/admin.py
from django.contrib import admin
from .models import Memo

admin.site.register(Memo)

サーバを起動してブラウザでlocalhost:8000/adminを開くと、ログイン画面が出てきます。

$ python manage.py runserver

まだユーザを作っていないのでログインできません。管理者用ユーザを作成しましょう。一回サーバを停止して以下のコマンドを打ちます。ユーザ名、メールアドレス、パスワードを入力しましょう。

$ python manage.py createsuperuser
# ユーザ名、メールアドレス、パスワードを入力する

再度サーバを起動し、ユーザ名とパスワードを入力してログインしましょう。

スクリーンショット 2019-10-28 1.50.12.png

これが管理画面です。Djangoが勝手に作ってくれています。Memosがadmin.pyで登録したMemoモデルです。Memosをクリックし、'MEMOを追加'ボタンを押すとフォーム画面が出てきます。Textフォームに適当に文字を入力して保存すると、オブジェクトが作成されます。編集や削除もできます。試しにオブジェクトを3つくらい作成しておきましょう。

メモ一覧ページの作成

では、Memoモデルに保存されているオブジェクトの一覧を表示するページを作成していきましょう。まず、memo_app/views.pyに新たな関数を追加します。

memo_app/views.py
from django.shortcuts import render
from .models import Memo

def index(request):
    return render(request, 'index.html', {'text': 'This page is a memo_app/index'})

# ↓追加
def list_view(request):
    return render(request, 'memo_list.html', {'memos': Memo.objects.all()})

render関数は前回の記事で出てきましたね。第2引数にテンプレート、第3引数にテンプレートに渡す辞書を指定します。
Memo.objects.all()Memoモデルから全てのオブジェクトを取得することを意味します。この書き方は後ほど詳しく見ることにして、保存してあるオブジェクトのリストが返されると思ってください。

次にmemo_list.htmlを作成します。

memo_app/templates/memo_list.html
<!DOCTYPE html>
<html lang=“ja”>
<head>
  <meta charset=“utf-8”>
  <title>memo_app</title>
</head>
<body>
  <h1>Memo List Page</h1>
  {% for memo in memos %}
      <p>{{ memo.id }} {{ memo.text }}</p>
  {% endfor %}
</body>
</html>

このテンプレートではビューからmemosという変数名でMemoモデルのオブジェクトリストを受け取っています。これを表示するのですが、Djangoにはhtmlファイルでpython風なコードを書くことができるテンプレートエンジン(Jinja2)という機能が備わっています。ただしpythonのようにいろいろな処理はできず、決められた書き方に従う必要があるので注意です。memosはリスト(正確にはQuerySet)なのでfor文を回し、各オブジェクトのidとtextの値をpタグで表示します。

最後にmemo_app/urls.pyでurlを定義します。

memo_app/urls.py
from django.urls import path
from . import views

app_name ='memo_app'
urlpatterns = [
    path('index/', views.index, name='index'),
    path('memo_list/', views.list_view, name='memo_list')  # 追加
]

サーバを起動し、localhost:8000/memo_app/memo_listを開いてみましょう。一覧で表示されたでしょうか。

スクリーンショット 2019-10-28 2.20.07.png

データベース操作

データベースにデータを挿入したり抽出したりするためには、本来はSQLという言語を使う必要があります。しかし開発する際にSQLは扱いづらいため、データベースの操作を簡単に実行できる仕組みがよく導入されます。これをORマッパー(Object-relational mapping)と呼びます。
DjangoでもモデルマネージャーというORマッパーが用意されています。

ORマッパーを試すために、djangoシェルという機能を使いましょう。以下のコマンドを打つとシェルが起動します。

$ python manage.py shell

ここではpythonのインタラクティブモードと同じようにpythonを実行することができます。試しに以下を実行してみましょう。

スクリーンショット 2019-10-28 2.59.49.png

In[1]ではMemoモデルをインポートしています。
In[2]はviews.pyで書きましたね。.all()は全てのオブジェクトが返ってきます。.objectsは決まり文句ですので気にしなくてOKです。返り値がQuerySetになっていますが、とりあえずはリストだと思ってもらって大丈夫です。
In[3]ではリストのように[0]で1つ目の要素を取り出せていますね。各要素はオブジェクトなので、オブジェクト.属性で各値を取り出すことができます(In[4], In[5])。
In[6]は.first()で一番最初の要素を取り出しています。これはIn[3]と同じ結果になります。

スクリーンショット 2019-10-28 3.04.50.png

ある条件を満たすオブジェクトのみを抽出したい時は、.filter(属性=value)を使います。
In[8]ではidが3であるオブジェクトを取得しています。結果がQuerySetであることに要注意です。QuerySetはリストのことなので、In[9]のようにリスト.属性ではエラーになります。In[10]では.first()で最初の要素を取り出すことでオブジェクト.属性を実行することができます。

他にも様々な操作ができるので、ぜひいろいろ試してみてください。
クエリを作成する | Django ドキュメント | Django
QuerySet API reference | Django ドキュメント | Django


今回は最初にDjangoのアーキテクチャ(全体像)を説明し、モデルの作成、管理画面の表示、モデルオブジェクトの表示を行いました。次回はモデルオブジェクトの追加、編集、削除を行うページの作成を行います。

今回のキーワード:
  - MTVモデル(モデル、テンプレート、ビュー)
  - モデルの属性とオブジェクト
  - マイグレート
  - 管理画面
  - テンプレートエンジン
  - ORマッパー
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む