Pythonの機械学習(keras)でマイヤーの三角形の判定をやってみました


# 1 はじめに
テストの本でマイヤーの三角形のテストを見てた時に
これってkerasで学習させることってできるんじゃない？
って思ってやってみました

マイヤーの三角形  
https://qiita.com/yuji38kwmt/items/26d9e3f63383b40d5554

keras  
https://keras.io/ja/

## 1.1 この記事の目標  
文字認識のサンプルは写経したけど、もう少し機械学習の基本的な使い方に触れてみたいという人に向けて書きました  

”訓練データの整理”と
”訓練モデルのチューニング”と
”訓練モデルの保存と復元”と
”機械学習を進めるうえで詰まった課題と解決策の紹介”を通じて
機械学習のざっくりとした流れの経験を身近な三角形の判定を題材に
機械学習モデル構築の疑似体験が提供できたらいいなと思っています

# 2 学んだこと

## 2.1 訓練データの整理

### 2.1.1 2辺の和の判定  

#### 課題
z>x+y　といった2辺の和の判定条件が安定しませんでした

#### 原因  
三角形の3辺の長さとその判定結果を訓練データとして
モデルに与えてました
そうすると　z>x+y　の判定条件が安定しませんでした
どうやら、x+yなどの複合条件の判定は学習回数がすくないのか知らないけど、
訓練にならないようでした

#### 対策    
そこで、2辺の和を訓練データとして与えたところ、
z>x+y　などの2辺の和の判定が安定するようになりました  

#### 最終的な訓練データ

| 引数 | 内容 |
| - | - |
| 1 | 辺1の長さ |
| 2 | 辺2の長さ |
| 3 | 辺3の長さ |
| 4 | 辺1+辺2の長さ |
| 5 | 辺2+辺3の長さ |
| 6 | 辺3+辺1の長さ |
| 7 | 三角形か否か　|
| 8 | 直角三角形か否か |
| 9 | 2等辺三角形か否か |
| 10 | 正三角形か否か |

### 2.1.2 三角形の判定結果

#### 課題  
辺の長さが3,4,5だと”三角形である”になり、
3,5,4だと”三角形ではない”になり、
同じ形状の三角形の判定結果が違って悩んだ

#### 原因  
3,4,5と3,5,4が別の訓練データとして扱われるのために判定が割れたためであった

#### 対策1  
訓練データをモデルに読み込ませるときに辺の長さでソートした
※3,4,5も3,5,4もモデルには同じ訓練データとして渡すようになる  

#### 対策2  
辺の長さでソートして予測データを入力した
※3,4,5も3,5,4もモデルには同じ予測データとして渡すようになる  

#### 結果  
判定結果が一致するようになった

### 2.1.3 直角三角形と正三角形の判定結果

#### 課題  
直角三角形と正三角形の判定結果がずっとNGになり悩んだ  

#### 原因  
直角三角形と正三角形の訓練データが少なかった
ためでであった  

#### 対策  
訓練データを読み込むときに訓練データを増殖させた

#### 結果  
一気に判定精度が上がった  
※テンション爆上がりした

## 2.2 モデルの調整

### 2.2.1 活性化関数

#### 課題  
辺の長さが0やマイナスの時の判定結果が
”三角形である”になり悩んだ

#### 原因  
隠れ層の活性化関数にReLUを採用していた  
※ReLUは0以下を切り捨てるので当たり前だ  

ReLU  
https://keras.io/ja/activations/

#### 対策  
PReLUを採用した

PReLU  
https://keras.io/ja/layers/advanced-activations/

#### 結果  
辺の長さが0以下の場合の判定結果が"三角形ではない"と判定してくれるようになった

## 2.3 pythonの実装

### 課題  
kerasの保存した重みを読み込みすると  
~~~
'str' object has no attribute 'decode'  
~~~
のエラーが出て途方にくれた  

### 解決方法  
https://qiita.com/Hiroki-Fujimoto/items/b078bfb680fb710c38c1

※偉大な先人の知見に感謝です  

# 3 環境構築

## 3.1 anaconda

pythonのアナコンダのインストール説明が丁寧なので参照しながらお願いします  
https://www.python.jp/install/anaconda/index.html

## 3.2 h5pyのインポート

~~~
pip3 show h5py
sudo pip3 uninstall h5py
sudo pip3 install h5py==2.10.0
~~~

## 3.3 tensorflowとkerasのインポート

~~~
conda update -n base -c defaults conda
conda update -all
conda install tensorflow
conda install keras
~~~

これでインストール完了です  

# 4 訓練データ準備

## 4.1 解説

三角形の訓練データを作成します  
detaフォルダにkeras_triangle.csvを作成します  

### 4.1.1 訓練の流れ

モデルに入力データを入れると判定結果を出力します  

例）  
入力データの辺1,辺2,辺3の長さがそれぞれ3,4,5だと、
直角三角形なので、
判定結果は
1,1,0,0
となります

入力データ  

| 引数 | 内容 |
| - | - |
| 1 | 辺1の長さ |
| 2 | 辺2の長さ |
| 3 | 辺3の長さ |
| 4 | 辺1+辺2の長さ |
| 5 | 辺2+辺3の長さ |
| 6 | 辺3+辺1の長さ |

判定結果  

| 引数 | 内容 |
| - | - |
| 1 | 三角形か否か　|
| 2 | 直角三角形か否か |
| 3 | 2等辺三角形か否か |
| 4 | 正三角形か否か |

## 4.2 実行

~~~
python make_data.py
~~~

## 4.3 コード

~~~python:make_data.py
import random
import sys, csv, operator

def judge(x,y,z):
  if x <= 0 or y <= 0 or z <= 0:
    return 0.0,0.0,0.0,0.0
    
  if (x >= (y + z)) or (y >= (z + x)) or (z >= (x + y)):
    return 0.0,0.0,0.0,0.0
  else:
    Righttriangle = 0.0
    if (x*x == (y*y + z*z)) or (y*y == (z*z + x*x)) or (z*z == (x*x + y*y)):
      Righttriangle = 1.0
  
    Isoscelestriangle = 0.0
    if x == y or y == z or z == x:
      Isoscelestriangle = 1.0
  
    Equilateraltriangle = 0.0
    if x == y and y == z:
      Equilateraltriangle = 1.0
  
    return 1.0,Righttriangle,Isoscelestriangle,Equilateraltriangle

def normalize(in_v):
  out = in_v
  #out = out / 15.0
  return out

def write_data(writer,x,y,z):
  list_ = [x,y,z]
  ll = list(r)
  list_ = list_ + ll
  print(list_)
  writer.writerow(list_)

  list_ = [x,z,y]
  ll = list(r)
  list_ = list_ + ll
  print(list_)
  writer.writerow(list_)

  list_ = [y,x,z]
  ll = list(r)
  list_ = list_ + ll
  print(list_)
  writer.writerow(list_)

  list_ = [z,x,y]
  ll = list(r)
  list_ = list_ + ll
  print(list_)
  writer.writerow(list_)

  list_ = [y,z,x]
  ll = list(r)
  list_ = list_ + ll
  print(list_)
  writer.writerow(list_)

  list_ = [z,y,x]
  ll = list(r)
  list_ = list_ + ll
  print(list_)
  writer.writerow(list_)

with open('data/keras_triangle_org.csv', 'w', newline="") as f:
  writer = csv.writer(f)
  #writer.writerow([11, 1, 2])
  
  for i in range(50):
    x = random.randint(-3,10)
    y = random.randint(-3,10)
    z = random.randint(-3,10)
    r = judge(x,y,z)
    
    x = normalize(x)
    y = normalize(y)
    z = normalize(z)
    
    write_data(writer,x,y,z)
  
  for i in range(-1,11):
    for j in range(-1,11):
      for k in range(-1,11):
        x = i
        y = j
        z = k
        r = judge(x,y,z)

        x = normalize(x)
        y = normalize(y)
        z = normalize(z)

        list_ = [x,y,z]
        ll = list(r)
        list_ = list_ + ll
        print(list_)
        writer.writerow(list_)

mycsv = csv.reader(open('data/keras_triangle_org.csv'),delimiter=',')
result = sorted(mycsv, key=operator.itemgetter(3), reverse=True)
print(result)

with open("data/keras_triangle.csv", "w", newline="") as f:
  data = csv.writer(f, delimiter=',')
  for r in result:
    data.writerow(r)
~~~

# 5 訓練実施

## 5.1 モデル

| Layer (type) | Output Shape | Param # |
| -            | -            | -       |
| dense (Dense) | (None, 6) | 42 |
| dense_1 (Dense) | (None, 64) | 448 |
| dense_2 (Dense) | (None, 64) | 4160 |
| p_re_lu (PReLU) | (None, 64) | 64 |
| dense_3 (Dense) | (None, 4)  | 260 |


入力層の要素  
x,y,z,x+y,y+z,z+xの6個

出力層の要素  
三角形,直角三角形,二等辺三角形,正三角形の4個

## 5.2 コード

### 5.2.1 実行

~~~
python keras_test.py
~~~

### 5.2.2 解説

#### 5.2.2.1 判定結果がいまいちのとき

~~~
model.fit(X_train, y_train, epochs=100, batch_size=32)
~~~

epochsを大きくしてください  
学習回数が増えます  


#### 5.2.2.2 判定結果がいまいちのとき2

~~~
  model.add(Dense(units=6, activation='tanh'))
  model.add(Dense(units=64, activation='tanh'))
  model.add(Dense(units=64))
  model.add(PReLU())
~~~

Denseを追加したり、活性化関数を変更してください  

#### 5.2.2.3 モデルを変更したときや、学習をやりなおしたいとき

'cnn_model.json'  
'cnn_model.yaml'  
'cnn_model_weights.hdf5'  
上記3つのモデルと重みのファイルを削除してください  

### 5.2.3 コード全体

~~~python:keras_test.py
# -*- coding: utf-8 -*-
import numpy as np
import os.path
from tensorflow.keras.models import Sequential, model_from_json
from tensorflow.keras.layers import Dense, Activation, PReLU

X_train = []
y_train = []

model_filename_json = 'cnn_model.json'
model_filename_yaml = 'cnn_model.yaml'
weights_filename = 'cnn_model_weights.hdf5'

import csv

def normalize(in_v):
  out = in_v
  out = out / 10.0
  return out

def secification(x,y,z):
  
  in_data = [x,y,z]
  in_data.sort()
  
  in1 = normalize(in_data[0])
  in2 = normalize(in_data[1])
  in3 = normalize(in_data[2])
  
  d1 = in1 + in2
  d2 = in2 + in3
  d3 = in3 + in1

  return in1,in2,in3,d1,d2,d3

def print_judge(in_d):
  if in_d[0,0] > 0.9:
    if in_d[0,1] > 0.9:
      # 直角三角形
      print('Righttriangle')
      return
    if in_d[0,2] > 0.9:
      if in_d[0,3] >0.9:
        # 正三角形
        print('Equilateraltriangle')
      else:
        # 二等辺三角形
        print('Isoscelestriangle')
      return
    # ただの三角形
    print('triangle')
  else:
    # 三角形ではない
    print('****')

with open('data/keras_triangle.csv') as f:
  reader = csv.reader(f)
  for row in reader:
    r0 = float(row[0])
    r1 = float(row[1])
    r2 = float(row[2])

    if float(row[5]) != 0.0 or float(row[6]) != 0.0:
      # 二等辺三角形 正三角形
      for i in range(100):
        X_train.append(secification(r0,r1,r2))
        y_train.append([float(row[3]),float(row[4]),float(row[5]),float(row[6]) ])
    elif float(row[4]) !=0.0:
      # 直角三角形
      for i in range(1000):
        X_train.append(secification(r0,r1,r2))
        y_train.append([float(row[3]),float(row[4]),float(row[5]),float(row[6]) ])
    else:
      X_train.append(secification(r0,r1,r2))
      y_train.append([float(row[3]),float(row[4]),float(row[5]),float(row[6]) ])

if True == os.path.isfile(model_filename_json) and True == os.path.isfile(weights_filename):
  print('load model')
  json_string = open(model_filename_json).read()
  model = model_from_json(json_string)
  print(type(model))
  print('load weights')
  model.load_weights(weights_filename, by_name=False)
  model.compile(optimizer='sgd', loss='mean_squared_error')

else:
  model = Sequential()
  model.add(Dense(units=6, activation='tanh'))
  model.add(Dense(units=64, activation='tanh'))
  model.add(Dense(units=64))
  model.add(PReLU())
  model.add(Dense(units=4, activation='sigmoid'))

  model.compile(optimizer='sgd', loss='mean_squared_error')

model.fit(X_train, y_train, epochs=100, batch_size=32)

loss_and_metrics = model.evaluate(X_train, y_train, batch_size=32)
print('Test loss:', loss_and_metrics)

# 学習結果を保存する
print('save model')
json_string = model.to_json()
open(model_filename_json, 'w').write(json_string)
yaml_string = model.to_yaml()
open(model_filename_yaml, 'w').write(yaml_string)
print('save weights')
model.save_weights(weights_filename)
print(type(model))

classes = model.predict(X_train, batch_size=32)
print(classes)

# 表示を小数点3桁にする
np.set_printoptions(precision=3)

# 学習結果を判定する
print('------------------------')
print("ok 1 0 0 0 e=tri")
v_test = [secification(6.0,4.0,5.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ok 1 1 0 0 e=Right")
v_test = [secification(3.0,4.0,5.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ok 1 1 0 0 e=Right")
v_test = [secification(6.0,8.0,10.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ok 1 0 1 0 e=Iso")
v_test =[secification(8.0,8.0,10.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ok 1 0 1 1 e=Equ")
v_test =[secification(8.0,8.0,8.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ng")
v_test = [secification(4.0,4.0,9.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ng")
v_test = [secification(2.0,2.0,5.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ng")
v_test = [secification(4.0,0.0,5.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

print('------------------------')
print("ng")
v_test = [secification(-1.0,4.0,5.0)]
print(v_test)
classes = model.predict(v_test)
print(classes)
print_judge(classes)

model.summary()
~~~

# 6 判定結果

いろんなパターンを確認してみた  
誤答がないので学習は成功していると思う  

------------------------
## 6.1 三角形の判定
~~~
ok 1 0 0 0 e=tri
[(0.4, 0.5, 0.6, 0.9, 1.1, 1.0)]
[[1.000e+00 2.769e-01 7.428e-04 2.564e-07]]
triangle
~~~

各辺が4,5,6なので、ただの三角形として判定されている
Good！！

------------------------
## 6.2 小さい直角三角形の判定

~~~
ok 1 1 0 0 e=Right
[(0.3, 0.4, 0.5, 0.7, 0.9, 0.8)]
[[1.000e+00 9.831e-01 3.953e-05 1.107e-07]]
Righttriangle
~~~

各辺が3,4,5なので、直角三角形として判定されている
Good！！

------------------------
## 6.3 大きい直角三角形の判定

~~~
ok 1 1 0 0 e=Right
[(0.6, 0.8, 1.0, 1.4, 1.8, 1.6)]
[[1.000e+00 9.913e-01 1.544e-07 1.128e-11]]
Righttriangle
~~~

各辺が6,8,10なので、直角三角形として判定されている
Good！！

------------------------
## 6.4 二等辺三角形の判定

~~~
ok 1 0 1 0 e=Iso
[(0.8, 0.8, 1.0, 1.6, 1.8, 1.8)]
[[1.000e+00 2.271e-06 9.684e-01 2.313e-06]]
Isoscelestriangle
~~~

各辺が8,8,10なので、二等辺三角形として判定されている
Good！！

------------------------
## 6.5 正三角形の判定

~~~
ok 1 0 1 1 e=Equ
[(0.8, 0.8, 0.8, 1.6, 1.6, 1.6)]
[[1.000e+00 1.083e-12 1.000e+00 9.782e-01]]
Equilateraltriangle
~~~

各辺が8,8,8なので、正三角形として判定されている
Good！！

------------------------
## 6.6 三角形ではない判定1

~~~
ng
[(0.4, 0.4, 0.9, 0.8, 1.3, 1.3)]
[[6.076e-03 7.958e-21 1.566e-02 1.355e-10]]
****
~~~

各辺が4,4,9なので、三角形ではないと判定されている
Good！！

------------------------
## 6.7 三角形ではない判定2

~~~
ng
[(0.2, 0.2, 0.5, 0.4, 0.7, 0.7)]
[[1.515e-04 1.183e-17 3.476e-03 1.601e-07]]
****
~~~

各辺が2,2,5なので、三角形ではないと判定されている
Good！！

------------------------
## 6.8 三角形ではない判定3

~~~
ng
[(0.0, 0.4, 0.5, 0.4, 0.9, 0.5)]
[[6.118e-05 1.814e-08 2.491e-09 2.896e-09]]
****
~~~

各辺が0,4,5なので、三角形ではないと判定されている
Good！！

------------------------
## 6.9 三角形ではない判定4

~~~
ng
[(-0.1, 0.4, 0.5, 0.30000000000000004, 0.9, 0.4)]
[[2.748e-06 5.327e-10 5.782e-10 9.454e-09]]
****
~~~

各辺が-1,4,5なので、三角形ではないと判定されている
Good！！

# 7 おわりに

kerasって簡単に層の追加できるし、
活性化関数の種類がいっぱいあるし、
いろんなものの学習させて試したくなります  
想像を刺激しますね