機械学習勉強の備忘録

# 機械学習の学習
趣味でちまちま勉強している機械学習の備忘録。
参考図書に関しては最後に記載しています。

最終的にはサザエさんのじゃんけんの予測がしたいのですが、まずは本に頼らず自力でPythonコードを書こうとして奮闘している中で死にそうなので一度ここでまとめておく。

## 小難しいプログラムよりまずは単純な学習から
最終目標はじゃんけん予測だが、いきなりこれに挑んでも玉砕するだけなのでまずはめっちゃ単純な学習プログラムを自作できるか検討した。

お題は…
　　__複数の分布データの振り分けを学習させる！__

ということで元データとなる分布データを以下のプログラムから作成させた。
### データの用意(自作)
```python:make_data.py
# -*- coding: utf-8 -*-

import sys, os
sys.path.append(os.pardir)
import numpy as np
import matplotlib.pyplot as plt
import csv

x = []
y = []
data = []
data_size = 30000

for i in range(data_size):
    #乱数発生用generator
    generator = np.random.default_rng()
     
    #分布データは以下のように格納する
    #data[0]~[999]→1つ目の分布、data[1000]~data[1999]→2つ目、data[2000]~[2999]→3つ目
    if i < data_size/3:
        #1つ目は(3, 0)を中心にx方向1.0, y方向0.7の標準偏差の分布
        x.append(generator.normal(loc=3, scale=1.0))
        y.append(generator.normal(loc=0, scale=0.7))
    elif i < 2*data_size/3:
        #2つ目は(10, 5)を中心にx方向0.5, y方向1.2の標準偏差の分布
        x.append(generator.normal(loc=10, scale=0.5))
        y.append(generator.normal(loc=5, scale=1.2))
    elif i >= 2*data_size/3:
        #3つ目は(4, 8)を中心にx方向0.8, y方向1.5の標準偏差の分布
        x.append(generator.normal(loc=4, scale=0.8))
        y.append(generator.normal(loc=8, scale=1.5))
    

    data.append([x[i], y[i]])

#分布データをcsv形式で保存
with open('./sample-3distribution.csv', 'w', newline="") as f:
    writer = csv.writer(f)
    writer.writerows(data)

#確認のためにグラフ描画する
# グラフの描画
markers = {'loss': 'o'}
plt.scatter(x, y, s=5, label='sin(x)')
plt.xlabel("x")
plt.ylabel("f(x)")
plt.ylim(-5, 15)
plt.show()
```
作成した分布データはこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611517/bac82b61-d584-7658-dbc9-f3f2c5c6bb71.png)

次に用意した上記の分布データを用いて、機械学習を実装し、3つの分布を分けられるか試してみる。
### 分布データの振り分けプログラム
```python:3distribution_learning.py
# -*- coding: utf-8 -*-

import sys, os
sys.path.append(os.pardir)
import csv
import numpy as np
import matplotlib.pyplot as plt

#ニューラルネットワーク生成クラスを用意　↓ここからデータ読出し関数まで(Mainの前)
class NuralNetwork:
    # 重みパラメーター
    W = []
    b = []
    
    def __init__(self, input_size, hidden_sizeList, output_size):
        I, O = input_size, output_size
        Hlist = hidden_sizeList

        global W
        global b
    
        #パラメーターの初期化
        self.W.append(np.array(np.random.randn(input_size, Hlist[0])))
        self.b.append(np.zeros(Hlist[0]))
        
        k=0
        if len(Hlist) > 1:
            for i in range(len(Hlist)-1):
                k = i + 1
                self.W.append(np.random.randn(Hlist[i], Hlist[k]))
                self.b.append(np.zeros(Hlist[k]))
        self.W.append(np.random.randn(Hlist[k], output_size))
        self.b.append(np.zeros(output_size))        

    #学習時使用する計算(関数)の記載
    #シグモイド関数
    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))

    #ソフトマックス関数
    def softmax(self, a):
        c = np.max(a)
        exp_a = np.exp(a - c)
        sum_exp_a = np.sum(exp_a)
        y = exp_a / sum_exp_a
        return y

    # 2乗和誤差
    def mean_squared_error(self, y, t):
        return 0.5 * np.sum((y-t)**2)

    # 交差エントロピー誤差
    def cross_entropy_error(self, y, t):
        delta = 1e-7
        return -np.sum(t * np.log(y + delta))

    # 損失関数
    def loss(self, x, t):
        return self.cross_entropy_error(x, t)

    # 推論実行関数
    def predict(self, x):
        global W
        global b
        a, z = [], []

        k = 0
        a.append(np.dot(x, self.W[k]) + self.b[k])
        z.append(self.sigmoid(a[k]))
        if len(self.W) > 2:
            for i in range(len(self.W)-2):
                k = i + 1
                a.append(np.dot(z[i], self.W[k]) + self.b[k])
                z.append(self.sigmoid(a[k]))
        
        a.append(np.dot(z[k], self.W[k+1]) + self.b[k+1])
        y = self.softmax(a[k+1])
        return y

    # Wパラメーターの勾配計算
    def gradient_W(self, x, t):
        global W
        global b
        
        h = 1e-4
        tmp_grad = []
        tmp2_grad = []
        grad_W = []

        for i in range(len(self.W)):
            for j in range(len(self.W[i])):
                for k in range(len(self.W[i][j])):
                    tmp_original = self.W[i][j][k]
                    self.W[i][j][k] = tmp_original + h
                    fxh1 = self.loss(self.predict(x), t)

                    self.W[i][j][k] = tmp_original - h
                    fxh2 = self.loss(self.predict(x), t)

                    tmp_grad.append((fxh1 - fxh2) / (2*h))
                    self.W[i][j][k] = tmp_original
                    
                tmp2_grad.append(tmp_grad)
                tmp_grad = []
            grad_W.append(tmp2_grad)
            tmp2_grad = []

        return grad_W

    # bパラメーターの勾配計算(ほぼWと一緒)
    def gradient_b(self, x, t):
        global W
        global b
        
        h = 1e-4
        tmp_grad = []
        grad_b = []

        for i in range(len(self.b)):
            for j in range(len(self.b[i])):
                tmp_original = self.b[i][j]
                self.b[i][j] = tmp_original + h
                fxh1 = self.loss(self.predict(x), t)

                self.b[i][j] = tmp_original - h
                fxh2 = self.loss(self.predict(x), t)

                tmp_grad.append((fxh1 - fxh2) / (2*h))
                self.b[i][j] = tmp_original
                    
            grad_b.append(tmp_grad)
            tmp_grad = []

        return grad_b

# 分布データの読み出し
def get_data():
    csv_file = open("./sample-3distribution.csv", "r", encoding="ms932", errors="", newline="")
    f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)

    data = []
    for row in f:
        data.append(row)
    
    #分布の1~3つ目のどれかという教師データ(正解)の用意
    distributionType = []
    for i in range(len(data)):
        if i < len(data)/3:
            #data[0]~[999]→1つ目→(1,0,0)
            distributionType.append([1,0,0])
        elif i < 2*len(data)/3:
            #data[1000]~[1999]→2つ目→(0,1,0)
            distributionType.append([0,1,0])
        elif i >= 2*len(data)/3:
            #data[2000]~[2999]→3つ目→(0,0,1)
            distributionType.append([0,0,1])
            
    return data, distributionType

# 最終分布の振り分け判断用
def judge(p):
    for i in range(len(p)):
        if max(p) == p[i]:
            return i

#===========  ここからMain処理  ==============================================
max_epoch = 10000
learning_rate = 0.01
train_loss_list =[]
#csvファイルから分布データ読出し
rawStrdata1, rawStrdata2 = get_data()

rx, rt = [], []
for i in range(len(rawStrdata1)):
    rx.append([float(rawStrdata1[i][0]), float(rawStrdata1[i][1])])
    rt.append([float(rawStrdata2[i][0]), float(rawStrdata2[i][1]), float(rawStrdata2[i][2])])


#====== ニューラルネットワークの構築 ============

inputData_size = 2
hidden_sizeList = np.array([5, 4])
model = NuralNetwork(inputData_size, hidden_sizeList, output_size=3)

for En in range(max_epoch):
    idx = int(np.random.uniform(0, len(rx)))
    #分布データ
    x = np.array([rx[idx][0], rx[idx][1]])
    #教師データ(正解)
    t = np.array([rt[idx][0], rt[idx][1], rt[idx][2]])

    # 推論実行
    y = model.predict(x)
    # 勾配取得
    grad_W = model.gradient_W(x, t)
    grad_b = model.gradient_b(x, t)

    #パラメーター更新
    for i in range(len(model.W)):
        for j in range(len(model.W[i])):
            for k in range(len(model.W[i][j])):
                model.W[i][j][k] -= learning_rate * grad_W[i][j][k]

    for i in range(len(model.b)):
        for j in range(len(model.b[i])):
            model.b[i][j] -= learning_rate * grad_b[i][j]

    # 損失の計算
    ls = model.loss(model.predict(x), t)
    
    # グラフ描画用にデータ格納&学習進捗確認用output
    if En % 100 == 0:
        train_loss_list.append(ls)
        print("i=", int(En/100), "\t| loss=", ls)

# グラフの描画
markers = {'loss': 'o'}
plt.plot(train_loss_list, label='loss')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 2)
plt.legend(loc='lower right')
plt.show()


xx = np.arange(-5, 15, 0.1)
yy = np.arange(-5, 15, 0.1)
X, Y = np.meshgrid(xx, yy)
temp, Ddata = [], []

for i in range(len(yy)):
    for j in range(len(xx)):
        p = model.predict([xx[j], yy[i]])
        temp.append(judge(p))
    Ddata.append(temp)
    temp = []

plt.contourf(X, Y, Ddata)

x, y = [], []
for i in range(len(rawStrdata1)):
    x.append(float(rawStrdata1[i][0]))
    y.append(float(rawStrdata1[i][1]))
markers = ['o', 'x']
plt.scatter(x, y, s=5, label='confirm')
plt.show()
```

上記プログラムでの学習の様子は以下の通り。
__ちゃんと損失が減少し学習が進んでいるのが分かる！(感動)__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611517/56dce4ab-b83c-4e61-ab35-ed10f998c17f.png)

(…時々損失が大きくなる突発的なピークは何なんだろうか)

それで3つの分布の振り分けの様子は以下の通り。
ちゃんと学習し、分布を分けることに成功した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611517/8b58d860-4e5a-ffa1-d7ea-df59d0f80c19.png)

## 最後に
ただこれ、何度か実行するとたまに学習できない時もあったりと、もう少し改良の余地がありそうです。。
というか、愚直に実装したため誤差逆伝搬とかそういうのも取り入れられていないので、まずはそういうのから実装していきたい。。。
まぁ今回は備忘録ということで、まずはここまで！

じゃんけん予測できるようになるかなぁ…

## 参考図書
・「ゼロから作るDeep Learning」 斎藤康毅 著　－オライリージャパン