このエントリーをはてなブックマークに追加
ラベル Python の投稿を表示しています。 すべての投稿を表示
ラベル Python の投稿を表示しています。 すべての投稿を表示

2017年2月6日月曜日

配牌からあがれる可能性を予測する!(その5 実際にテストしてみる)

こんにちは、Taroです。
前回の記事の記事の続きになります。
配牌からあがれる可能性を予測する!(その4 勾配を求める際に詰まったこと)
今回は実際にテストしてみたいと思います。

テストに使用した配牌について


Youtubeであがっていた○凰のプレイ動画を1半荘分試しました。

結果


# 第一要素があがれる方、第二要素があがれない方
# 重み
[[-0.09800755  0.09915648]    # 順子
  [ 0.15426824 -0.17342399]    # 対子
  [-0.61190551  0.61323983]]  # 暗刻

# バイアス
[-0.84420447  0.84420447]
この結果だけ見ると、対子の数があがれる方に寄与しているように見えます。
逆に暗刻があるとあがりにくいようです笑
ちょっとリャンメンやカンチャンの形も要素に入れた方が良かったかな、と後悔し始めておりますが、とりあえずこのまま進めたいと思います。

結果を記載しますが、配牌を記載するとおさまりきらないので、「あがれた」か「あがれなかった」を記載します。
結果 あがれる確率 あがれない確率
東1局 あがれない 17% 83%
東2局 あがれない 29% 71%
東3局 あがれない 7% 93%
東3局1本場 あがれない 17% 83%
東4局 あがれない 33% 67%
南1局 あがれる 20% 80%
南1局1本場 あがれない 26% 74%
南2局 あがれない 17% 83%
南3局 あがれる 26% 74%
南4局 あがれる 23% 77%

パッと見、ちゃんと予測できているようには見えないです。
よく考えたら、麻雀は4人のうち1人があがれるゲームなので、単純計算でアガれる確率は25%です。
教師データが足りないのかもしれないと思い、とりあえずプロの対局1局分追加してみました。

結果②


# 第一要素があがれる方、第二要素があがれない方
# 重み
[[ 0.17258578  -0.17396835]    # 順子
  [ 0.20088727 -0.20564398]    # 対子
  [-0.33262621  0.34681625]]  # 暗刻

# バイアス
[-1.07893278  1.07893278]
先ほどとは重みが変わり、順子もアガれる確率に寄与するようになりました。
それではこちらのパラメータをもとに同じテストデータで実験してみます。
比較しやすいよう先ほど求めた結果を①とし、アガれない確率のみ記載します。
結果 結果①のアガれない確率 結果②のアガれない確率
東1局 あがれない 83% 73%
東2局 あがれない 71% 64%
東3局 あがれない 93% 92%
東3局1本場 あがれない 83% 80%
東4局 あがれない 67% 72%
南1局 あがれる 80% 85%
南1局1本場 あがれない 74% 79%
南2局 あがれない 83% 80%
南3局 あがれる 74% 79%
南4局 あがれる 77% 73%

結果の方も変化しました。
10局のうち、6局の「アガれない確率」が下がっています。
つまり「アガれる確率」が上昇しています。
このことから、結果①の時点では、「アガれる」可能性を過少評価していることが考えられます。

おわりに


今後の目標としては、
  • 教師データを増やす
  • レイヤーのノードを追加(リャンメン、カンチャン、ペンチャン)の数

とりあえず、まずは教師データを作成するのが楽になるインターフェースを実装したいと思います。
参考のソースコードはこちらになります。
https://github.com/naoki85/python_mahjong

2017年1月30日月曜日

配牌からあがれる可能性を予測する!(その4 勾配を求める際に詰まったこと)

こんにちは、Taroです。
前回の記事の記事の続きになります。
配牌からあがれる可能性を予測する!(その3 学習の実装)
本日は学習を実装する上で、すぐに理解できなかった勾配算出に関わる部分をまとめたいと思います。

前回の訂正


損失値を算出して、グラフ化しておりましたが、1回ごとの平均値として算出しておりませんでした。
そのため、実際にどれくらいの誤差が生じているか分かりづらかったため、平均値で図をプロットするよう修正しました。
差分はこちらです。
https://github.com/naoki85/python_mahjong/commit

勾配法について


ある重み、バイアスパラメーターのときの、損失関数の勾配(傾き)を求め、傾きが小さくなるようパラメーターを変化させる手法とのことです。
機械学習はこの最適パラメーターを求めるのが1つの命題となっています。
ただ、パラメーターを決めると一口で言っても無限に近い数値の海から探さなければいけません。
ここで本の中で使っていた例えは、思わずへ〜、と頷いてしまいました。
文中ででてくるSGDは、確率的勾配降下法という手法の略です。

ここに風変わりな冒険家がいます。彼は広大な乾燥地帯を旅しながら、日々深い谷底を求めて旅を続けています。

~~中略~~

しかも、彼は、厳しい”制約”を2つ自分に課しています。ひとつは地図を見ないこと、もうひとつは目隠しをすることです。

~~中略~~

この困難な状況で重要となってくるのが、地面の「傾斜」です。

~~中略~~

そこで、今いる場所で一番傾斜がきつい方向に進もうというのが、SGDの戦略です。

「ゼロから作るDeep Learning」第6章より引用


勾配を算出する関数について


さて、実際に勾配を算出する段になりまして、少し理解が追いつきませんでした。
例えば、損失関数に対する微分は下記のようになります。(本の中の式)
# xはテストデータ、tは教師データです。
# lossは損失関数です。
def f(W):
    return net.loss(x, t)
ここで関数fはxとtから求める損失関数、f(W)でWに関する関数になります。
(Pythonではlambda式で一行で書けるようですが、あえてこちらで記載します。)
fをWに関して微分して勾配を求めます。

ここで、高校数学程度の知識しかない私はこう思いました。
あれ、fをWに関して微分しても、Wが要素になってないから0なんじゃないか、と。
とりあえずこの疑問は置いておいて、本を読み進めていくと、どう考えても勾配を0として計算しているようには見えませんでした。
Wをどこかで関わらせているのかと思い、勾配を求める関数をちゃんと読んでみました。
# 実際に呼んでいるコード
# net.Wはネットワーククラスが持つ重みの値(クラスプロパティ)
dW = numerical_gradient(f, net.W)

def numerical_gradient(self, f, x):
    h = 1e-4
    grad = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        # ここでプロパティの値を上書きしている!
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)
        
        x[idx] = tmp_val - h
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
         # ここで元に戻している!
        x[idx] = tmp_val
        it.iternext()
        
    return grad
つまり、重みパラメーターをクラスに持たせることで、微分式内で値を暫定的に書き換えて処理をさせています。
恥ずかしながら、ここで引っかかり、いまいち以降の処理を理解できておりませんでした。
もし、こちらの本で勉強していて、同じような場所で詰まっているという方がいらっしゃれば、参考にしていただければ、と思います。

2017年1月23日月曜日

配牌からあがれる可能性を予測する!(その3 学習の実装)



前回の訂正


CSVファイルからの読み込み時、String型になってしまうので、Int型にキャストしなければなりませんでした。
差分はこちらになりますが、その中でリスト内包記述というfor文を1行で書くように修正しております。
# 修正前
tmp_hand = []
for tile in range(0, 12):
tmp_hand.append(row[tile])

# 修正後
tmp_hand = [int(tile) for tile in row]

学習の実装開始


今回より学習機能をつけていきたいと思います。
基本的には、「ゼロから作るディープラーニング」の手順で論理を進めています。
(誤差を求め、その誤差の微分値からパラメータを求める。)
ソースコードはこちらになります。
https://github.com/naoki85/python_mahjong

matplotlib について


その前に、グラフを描画する便利ライブラリであるmatplotlibの準備をしておきます。
まだそこまで複雑なグラフはいらないので、基本的な書き方で大丈夫そうです。
今回はX軸はループの回数、Yは誤差(教師データとの誤差とします。)
# xの値を1ずつ定義
x = range(0, 100)
# ループごとに誤差を格納しておく
y = loss_array
plt.xlabel("x")
plt.ylabel("loss")
plt.plot(x, y)
plt.show()
ためしに作ってみた図はこちらです。


ループ回数を重ねるごとに、誤差がちいさくなっていることが分かります。
ただし、こちらの正当性はまだ確認できていないので、実際に次回は学習後の重み、バイアスを使用してテストしてみます。

学習結果の保存について(pickleモジュール)


学習結果はpickle形式で保存しました。
pickleは「漬物」という意味で、オブジェクトをそのまま保存してくれます。(ピクルスのことか、と覚えました。)
はじめはCSV形式にしようと思ったのですが、いちいち分解して保存するのも、取り出すときに再度辞書型にするのも面倒です。
例えば、下記の結果を保存するとします。
{'W': [1, 2, 3], 'b': [4, 5]}
これを特に前処理なしで下記のように保存(漬物)にしてしまいます。(基本的なファイルの読み書きと同じ感じです。)
with open(pickle_filepath, 'wb') as f:
    writer = pickle.dump(results, f)
取り出すときも下記のようにかけば、なんとびっくりそのまま取り出せます。
with open(pickle_filepath, 'rb') as f:
    params = pickle.load(f)

{'W': [1, 2, 3], 'b': [4, 5]}
そのため、重み、バイアスの値は教師データとは異なりpickleで管理することとしました。

おわりに


まだ私自身、微分して最適な重みを求める「勾配法」について、 腑に落ちていないところがあるので、
そちらを理解して、実装を踏まえて書きたいと思います。

2017年1月21日土曜日

配牌からあがれる可能性を予測する!(その2 教師データの作成)

こんにちは、Taroです。
前回の記事の記事の続きになります。
配牌からあがれる可能性を予測する!(その1 推論処理)
次回は学習を実装する、と記載しましたが、その前に教師データを取得する方法を実装したいと思います。
今回はDeep LearningというよりはPythonの勉強といった感じです。

教師データの選定


麻雀のようなギャンブルにおいて、教師になるデータは何なのか?
考え出すとキリがなさそうなので、ひとまずプロの対局からデータを作成しました。
プロであれば、(もちろん技術や個性、状況で異なってくるとは思いますが)あがりやすい配牌は必ず仕上げてくれるはずです。
そのため、Youtubeでプロの対局を見続けました(ある種これが苦行かもしれません笑)。
余談ですが、個人的に好きなプロ雀士は土田浩翔プロです。

CSVファイルの作成


教師データはCSVファイルにまとめます。
今回必要なデータは、 配牌結果 なので、下記のようにまとめました。
tile_1 tile_2 tile_3 tile_4 tile_5 tile_6 tile_7 tile_8 tile_9 tile_10 tile_11 tile_12 tile_13 win loss
11 13 17 21 25 27 29 31 31 31 35 43 47 1 0
このデータを読み込んでいきたいと思います。

CSVファイルの読み込み


Pythonのcsvモジュールを利用して簡単に実装します。
(というよりこれくらいしか分からない。。。)
   def load_trainig_data(self):
        u"""
        CSVファイルから教師データを読み込み、データを返します。
        @return array
        [0]で教師データの配牌、[1]で結果を返します
        """
        with open('csv/training_data.csv', 'r', newline='') as csvfile:
            reader = csv.reader(csvfile)
            header = next(reader)
            hand = []
            results = []
            for row in reader:
                tmp_hand = []
                for tile in range(0, 12):
                    tmp_hand.append(row[tile])

                results.append([row[13], row[14]])
                hand.append(tmp_hand)

        return hand, results
このメソッドをMyHandクラスに追加しました。
from my_hand import *
my_hand = MyHand()
input_data, results = my_hand.load_trainig_data()
print(input_data)
[['47', '26', '17', '21', '47', '12', '42', '43', '34', '15', '45', '37'], ['42', '42', '41', '17', '39', '21', '39', '15', '37', '11', '11', '21'], ['47', '35', '22', '22', '23', '26', '12', '13', '17', '18', '39', '38'], ['22', '24', '24', '25', '28', '31', '32', '33', '13', '13', '14', '16'], ['41', '47', '31', '12', '29', '18', '45', '34', '19', '22', '18', '39'], ['46', '15', '15', '16', '16', '47', '45', '11', '43', '32', '34', '27'], ['41', '41', '43', '44', '44', '17', '14', '13', '13', '23', '23', '33'], ['35', '38', '39', '39', '23', '25', '26', '27', '28', '16', '42', '42'], ['45', '47', '39', '44', '43', '21', '36', '27', '13', '32', '39', '16'], ['21', '28', '32', '35', '21', '23', '35', '12', '18', '37', '45', '14'], ['12', '14', '15', '17', '41', '44', '34', '35', '22', '23', '23', '26'], ['24', '24', '29', '31', '32', '36', '37', '38', '39', '13', '17', '47']]
print(results)
[['0', '1'], ['1', '0'], ['0', '1'], ['0', '1'], ['0', '1'], ['0', '1'], ['1', '0'], ['0', '0'], ['1', '0'], ['0', '0'], ['0', '0'], ['0', '0']]
これだとちょっと分かりづらいので、1つ目の要素だけ抜き出します。
print(input_data[0])
['47', '26', '17', '21', '47', '12', '42', '43', '34', '15', '45', '37']
print(results)
['0', '1']
これで、教師データが読み込めました。
(これで正しいのかは置いておきます笑)

Jupyter Notebookで書いてみたよ!


Jupyterという単語は見たことがあったのですが、ずっとライブラリの1つだと思っていました笑。
実はエディタのようで、データサイエンティストの人などが、実際に実行したプログラムを確認しながら文章を書いたりするもののようです。
Anacondaをインストールすると一緒についてきます。
詳しいことは諸先輩方が記述しておりますので、割愛しますが、本記事も下図のように書きました。
プロジェクト上に配置しておけば、自作のコードも読み込んで実行してくれます。
Python以外の言語もサポートしているようなので、コードと実行結果をブログにのせたいときは使用を検討しても良いのではないでしょうか?

2017.01.21 追記


何も考えずに、Jupyterが生成してくれたソースコードを貼り付けたら、Google bloggerのCSSなどを上書きしてしまったようで、不都合が生じました。
急いで書き直したので、結局普通になってしまいました。。。笑
ブログなどに貼り付けるときは事前に検討が必要です。

おわりに


今回のソースコードは下記になります。
Jupyter Notebookで作成したファイルは.ipynb拡張子で保存されています。
https://github.com/naoki85/python_mahjong
次回は、今度こそ学習ロジックの実装をしたいと思います!

2017年1月16日月曜日

配牌からあがれる可能性を予測する!(その1 推論処理)


2017.01.21 更新
その2を公開しました!
https://nowdeveloping.blogspot.jp/2017/01/blog-post_21.html

こんにちは、Taroです。
日増しに寒くなってきましたね。
今年もセンター試験は大寒波とかニュースでやっていましたが、毎年そうなのでタイミングが悪いんですかね?

ゼロから作るDeep Learningを読んでいます


最近、趣味かつ独学ではありますが、Deep Learningの勉強をしています。
ゼロから作るDeep Learning」は初心者でも分かりやすい本だと思います。
(知識がない私でも頑張ってなんとかついていっています笑)

しかし、(私自身に知識がないためだとは思いますが)いまいちピンときません。
ピンとこないというのは、意味が分からないということではなく、「実際にこれをどうやって使うんだろう」というところです。

あれこれ悩んだ結果、簡単なネットワークを自分で作ってみようと思いつきました。
せっかくなら自分の興味のある麻雀をベースに、
配牌を入力して、あがれるかあがれないか予測する
モデルを作ってみたいと思います。

ソースコードも公開していきますが、Deep LearningどころかPythonも初心者なので、ぜひご指導をお願いしますm(_ _)m
麻雀は知らないけど、Deep Learningとか興味ある、という方でも読んでいただけるようにしていきます。

とりあえず設計


配牌(はじめに配られる手札のようなもの)から、アガリの確率を求めるために必要なファクターは何か、真剣に考えてみました(それはもう、プログラムを書いているとき以上に)。
また、あまりに複雑すぎるファクターだと私が実装できない可能性があり、かつ本筋を見失いかねないので、以下の3つの要素としました。
  1. 配牌時に存在する順子の数
  2. 配牌時に存在する対子の数
  3. 配牌時に存在する暗刻の数
麻雀が分からない方は、トランプで7、8、9と階段で持っているか、7、7、7と同じ数字を複数枚持っているか、どちらがアガリやすいかという風に考えてください。

これら3つの要素を算出し、それぞれに対して重み(W)を掛けて、最終的な総和からアガれるかアガれないか予測します。
ちょっと私の説明が下手なきらいもあるので、図にしたいと思います。



データの扱い

例えば、下記のような配牌があります(一番右の牌は無視してください笑)。
このときの入力値はリスト型で、
input_data = [14, 15, 19, 23, 25, 25, 32, 37, 39, 39, 43, 43, 47]
と渡します。
順子の数は0、対子の数が3つ、暗刻の数が0です。
そのため、
X1 = 0
X2 = 3
X3 = 0
とします。
ここで、それぞれに対して重みWを掛けます。
重みは、それぞれのファクターがどれだけ寄与しているか示す値です。
この重みを今後学習させて、最適な値を探していきます。
今回は適当な値を渡しておきます。

そして、最後にそれらを足し合わせて結論を導きます。
結論を導く際に、バイアスで閾値を設定しますが、こちらも今後の学習で値を決めていくので、今回は適当な値を入れます。
最終的には下記のように出力させたいと思います。
result = [0.50407631, 0.49592369]
第一要素がアガれる確率、第二要素がアガれない確率と考えます。
今回は適当な値を入れたので、フィフティフィフティみたいな中途半端なことになっておりますが、今後の学習で立派な予想屋になってもらいます!

次回は、学習を実装していきたいと思います。
できれば完成までお付き合いください!

最後に、ソースコードはこちらになりますので、よろしければご覧ください。
https://github.com/naoki85/python_mahjong
2017.01.21 更新
その2を公開しました!
https://nowdeveloping.blogspot.jp/2017/01/blog-post_21.html