AI学習で有名なアヤメ(iris)の分類をPython初心者が実装してみる
はじめに
C言語を半年間学んだ私がPython初心者のまま機械学習を学んでみるという無謀な記事になっています。同じような境遇の方に少しでも参考になれば幸いです。
概要
TesorFlow/Kerasを用いてニューラルネットを作成し、3種類のアヤメの分類を行います。
Google Colabratory (以下colab) を使ってコードを書いていきます。
colabは環境構築が必要ないので、すぐにブラウザ上でPythonを動かすことができます。
実装
まずはPythonの勉強から
さすがにPythonの大まかな書き方を知らなくてはコードは読めないので、以下のYoutubeで勉強しました。
データセットのロードと情報の確認
それではコードを書いていきます。
コードの左上にインタプリタと書かれているものを実行してください。
左上に出力結果と書かれているものはインタプリタで実行した出力結果になります。
まず、sklearn[1]モジュールのdatasetsからload_iris
をインポートします。
そしてiris
にアヤメのデータを格納します。
from sklearn.datasets import load_iris
iris = load_iris()
iris
にどんなデータが格納されたかdir
関数で確認してみます。
dir(iris)
['DESCR',
'data',
'data_module',
'feature_names',
'filename',
'frame',
'target',
'target_names']
DESCR
があるのでiris.DESCR
を出力してデータセットの説明文を見てみます。
print(iris.DESCR)
.. _iris_dataset:
Iris plants dataset
--------------------
**Data Set Characteristics:**
:Number of Instances: 150 (50 in each of three classes)
:Number of Attributes: 4 numeric, predictive attributes and the class
:Attribute Information:
- sepal length in cm
- sepal width in cm
- petal length in cm
- petal width in cm
- class:
- Iris-Setosa
- Iris-Versicolour
- Iris-Virginica
:Summary Statistics:
============== ==== ==== ======= ===== ====================
Min Max Mean SD Class Correlation
============== ==== ==== ======= ===== ====================
sepal length: 4.3 7.9 5.84 0.83 0.7826
sepal width: 2.0 4.4 3.05 0.43 -0.4194
petal length: 1.0 6.9 3.76 1.76 0.9490 (high!)
petal width: 0.1 2.5 1.20 0.76 0.9565 (high!)
============== ==== ==== ======= ===== ====================
:Missing Attribute Values: None
:Class Distribution: 33.3% for each of 3 classes.
:Creator: R.A. Fisher
:Donor: Michael Marshall (MARSHALL%PLU@io.arc.nasa.gov)
:Date: July, 1988
The famous Iris database, first used by Sir R.A. Fisher. The dataset is taken
from Fisher's paper. Note that it's the same as in R, but not as in the UCI
Machine Learning Repository, which has two wrong data points.
This is perhaps the best known database to be found in the
pattern recognition literature. Fisher's paper is a classic in the field and
is referenced frequently to this day. (See Duda & Hart, for example.) The
data set contains 3 classes of 50 instances each, where each class refers to a
type of iris plant. One class is linearly separable from the other 2; the
latter are NOT linearly separable from each other.
.. topic:: References
- Fisher, R.A. "The use of multiple measurements in taxonomic problems"
Annual Eugenics, 7, Part II, 179-188 (1936); also in "Contributions to
Mathematical Statistics" (John Wiley, NY, 1950).
- Duda, R.O., & Hart, P.E. (1973) Pattern Classification and Scene Analysis.
(Q327.D83) John Wiley & Sons. ISBN 0-471-22361-1. See page 218.
- Dasarathy, B.V. (1980) "Nosing Around the Neighborhood: A New System
Structure and Classification Rule for Recognition in Partially Exposed
Environments". IEEE Transactions on Pattern Analysis and Machine
Intelligence, Vol. PAMI-2, No. 1, 67-71.
- Gates, G.W. (1972) "The Reduced Nearest Neighbor Rule". IEEE Transactions
on Information Theory, May 1972, 431-433.
- See also: 1988 MLC Proceedings, 54-64. Cheeseman et al"s AUTOCLASS II
conceptual clustering system finds 3 classes in the data.
- Many, many more ...
大量に出力されましたが注目すべき文は以下の説明です。
:Number of Instances: 150 (50 in each of three classes)
:Number of Attributes: 4 numeric, predictive attributes and the class
:Attribute Information:
- sepal length in cm
- sepal width in cm
- petal length in cm
- petal width in cm
- class:
- Iris-Setosa
- Iris-Versicolour
- Iris-Virginica
この説明からSetosa,Versicolour,Virginicaの3つのクラスがあり、それぞれ50個のデータを持ち、合計で150個のデータがあることがわかります。
またそのクラス一つの中に4つのデータが格納されており、それぞれ
- がく片の長さ(cm)
- がく片の幅(cm)
- 花弁の長さ(cm)
- 花弁の幅(cm)
のデータであることが説明文に書いてあります。
これらの長さや幅を用いてアヤメを分類するAIを作ります。
また分類するアヤメの種類はtarget_names
に格納されており、
list(iris.target_names)
を実行することで確認することもできます。
それでは計測データの構造をshape
メソッドを使って確認します。
iris.data.shape
(150, 4)
150行、4列であることがわかりました。具体的な値を確認します。
iris.data
array([[5.1, 3.5, 1.4, 0.2],
[4.9, 3. , 1.4, 0.2],
[4.7, 3.2, 1.3, 0.2],
[4.6, 3.1, 1.5, 0.2],
・・・・
[6.3, 2.5, 5. , 1.9],
[6.5, 3. , 5.2, 2. ],
[6.2, 3.4, 5.4, 2.3],
[5.9, 3. , 5.1, 1.8]])
各列に何が格納されているかはfeature_names
に入っています。
iris.feature_names
['sepal length (cm)',
'sepal width (cm)',
'petal length (cm)',
'petal width (cm)']
よってiris.data
には、各行ごとに左から
[がく片の長さ(cm)、がく片の幅(cm)、花弁の長さ(cm)、花弁の幅(cm)]
が入っていることがわかりました。
教師データ(ターゲットデータ)はiris.target
に格納されています。
まずはiris.targe
同様、shape
メソッドを使って構造を確認してみましょう。
iris.target.shape
(150,)
教師データは1次元配列で150個の要素を持っていることが確認できました。
具体的な中身を確認します。
iris.target
array([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])
計測データに対応する、0,1,2のそれぞれ3種類の数値が格納されており、それぞれ順番に
- 0:Setosa
- 1:Versicolour
- 2:Virginica
です。
またこのことから最初の50個がSetosa、次の50個がVersicolour、次の50個がVirginicaであることがわかったのでそれを活用して次は散布図を作成します。
データを可視化する
matplotlib
については説明が長くなってしまうので以下の記事を参考にしてください。
また[:50, 0]
といった記述のことをスライスと言いますが、こちらについてもわからない方は以下の記事を参考にしてみてください。
それではコードを書いていきます。
iris.data
には4種類の計測データがありますが、
x軸にはがく片の長さ、y軸にはがく片の幅が来るように散布図を書いてみます。
import matplotlib.pyplot as plt
x = iris.data
y = iris.target
plt.scatter(x[:50, 0], x[:50, 1], color='r', marker='o', label='setosa')
plt.scatter(x[50:100, 0], x[50:100, 1], color='g', marker='+', label='versicolor')
plt.scatter(x[100:, 0], x[100:, 1], color='b', marker='x', label='virginica')
plt.title("Iris Plants Database")
plt.xlabel("sepal length(cm)")
plt.ylabel("sepal width(cm)")
plt.legend()
plt.show
図1. がく片の長さと幅の関係
散布図を見てみると、がく片の長さと幅だけでもそれぞれの特徴を認識することができます。
TensorFlowとKerasのロード
では、これから機械学習を行う上で必要なモジュールであるTensorFlowとKerasをロードします。
import tensorflow as tf
from tensorflow import keras
print(tf.__version__)
tf.random.set_seed(1)
2.7.0
無事インポートできていればTensorFlowのバージョンが出力されるはずです。
また最後の行は乱数のシード値を固定しています。
引数は実数であればなんでもいいです。
ちなみにこのシード値に結果を左右されるのはあまりよろしくありません。
今回は1に設定します。
本当に固定されているのか検証してみた
tf.random.uniform
:一様分布からランダムに値をサンプリングする関数
tf.random.set_seed(0)
print(tf.random.uniform([1]))
tf.random.set_seed(0)
print(tf.random.uniform([1]))
tf.Tensor([0.29197514], shape=(1,), dtype=float32)
tf.Tensor([0.29197514], shape=(1,), dtype=float32)
これによってTensorFlowの重み[2]の設定が固定されました。
固定する理由としては、科学にとって大切な再現性を厳守するためです。
しかしこの行は別に入れても入れなくても好みです。
毎回違う結果を見たいのであれば最後の行は削除していただいても構いません。
データセットの分割
ではデータセットの分割を行う前にデータをランダムに並べ替えます。
import random
random.seed(12345)
Ndata = len(iris.data)
print(f"Ndata={Ndata}")
idxr = [k for k in range(Ndata)]
print(idxr)
random.shuffle(idxr)
print(idxr)
Ndata=150
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149]
[109, 78, 108, 60, 93, 46, 14, 59, 118, 8, 18, 96, 77, 62, 104, 25, 99, 133, 130, 82, 80, 148, 17, 4, 34, 101, 33, 81, 126, 61, 28, 89, 132, 79, 9, 68, 64, 54, 149, 35, 26, 136, 70, 140, 40, 57, 120, 39, 30, 119, 139, 113, 102, 103, 86, 45, 72, 114, 50, 11, 122, 51, 24, 10, 56, 7, 6, 117, 124, 145, 112, 121, 144, 137, 91, 128, 1, 38, 21, 92, 16, 138, 55, 75, 32, 100, 85, 71, 27, 63, 15, 97, 5, 131, 135, 147, 67, 29, 127, 146, 134, 73, 87, 13, 116, 83, 12, 36, 22, 74, 98, 20, 143, 84, 115, 0, 53, 141, 88, 65, 123, 125, 58, 3, 142, 43, 107, 48, 19, 52, 37, 42, 129, 105, 23, 90, 47, 44, 66, 110, 31, 95, 41, 111, 69, 49, 94, 76, 2, 106]
まずrandom.seed(12345)
で再現性のためにseed値を固定します。先ほど同様引数は実数であればなんでも構いません。今回は12345に設定しました。
len関数を使い、Ndata = len(iris.data)
でデータの要素数を調べ、
print(f"Ndata={Ndata}")
で出力しています。
idxr = [k for k in range(Ndata)]
この書き方はPythonを勉強していないと初見ではわからないと思います。これはリスト内包表記[3]と言います。
リスト内包表記を簡単に書いた場合
idxr = []
for k in Ndate
idxr.append(k)
print(idxr)
を実行することでリスト内に0から149までの数値が格納されていることがわかります。
ちなみにidxr
とはインデクサー(indexer)のことでプログラミングではこのように略して書くことが多いようです。
またrandom.shuffle(idxr)
よって先ほどのリストの中身をシャッフルし、
print(idxr)
で中身がランダムに再配置されたか確認しています。
それでは以下のコードでデータの分割を行います。わかりやすくするために今回は訓練データと検証データを半分、半分で分割します。
Ndata_train=int(Ndata*0.5)
print(f"# of training data = {Ndata_train}")
print(f"# of validation data = {Ndata-Ndata_train}")
train_data = iris.data[idxr[:Ndata_train]]
train_labels = iris.target[idxr[:Ndata_train]]
val_data = iris.data[idxr[Ndata_train:]]
val_labels = iris.target[idxr[Ndata_train:]]
# of training data = 75
# of validation data = 75
Ndata_train=int(Ndata*0.5)
でデータ分割の割合を指定しています。
なぜintにキャスト(型変換)しているのか
print(type(Ndata))
print(type(Ndata*0.5))
print(type(int(Ndata*0.5)))
<class 'int'>
<class 'float'>
<class 'int'>
float型からわざわざint型に戻しています。もしint型じゃなければ
train_data = iris.data[idxr[:Ndata_train]]
のスライスを実行する行で
TypeError: slice indices must be integers or None or have an __index__ method
とインタプリタ先生に怒られてしまいます。スライスはintでなければならないようです。
print(f"# of training data = {Ndata_train}")
と
print(f"# of validation data = {Ndata-Ndata_train}")
で
それぞれ訓練データと検証データの数を出力しています。
train_data = iris.data[idxr[:Ndata_train]]
の行では訓練データを、
train_labels = iris.target[idxr[:Ndata_train]]
の行では訓練データの教師ラベルをスライスを使って、
idxr
の0~74番目をそれぞれiris.data
とiris.target
に対応させて、train_data
とtrain_labels
に代入しています。
上と同じくval_data = iris.data[idxr[Ndata_train:]]
の行では検証データを、
val_labels = iris.target[idxr[Ndata_train:]]
の行では検証データの教師ラベルをスライスを使って、
idxr
の75~149番目をそれぞれiris.data
とiris.target
に対応させて、
val_data
とval_labels
に代入しています。
ニューラルネットワークの設計
それではニューラルネットワークの作成をしていきます。
model = keras.Sequential([
keras.layers.Dense(4, activation='relu'),
keras.layers.Dense(10, activation='relu'),
keras.layers.Dense(10, activation='relu'),
keras.layers.Dense(3, activation='softmax')
])
KerasのSequentialというクラスを使ってニューラルネットワークを作成しています。
4次元(がく片の長さ、がく片の幅、花弁の長さ、花弁の幅)のデータから3次元(setosa, versicolor, virginica)のラベルへ層が構成されています。
中間層は2層でユニット数(中間層の次元)は10、Dense
[4]で設定したため全結合層になっています。
model
には、keras.Sequential
から作成したインスタンスが入っており、これを使用してデータを学習させることが可能になります。
機械学習を行う
まずはmodel
インスタンスのcompile
メソッドを使って学習の設定を行います。
model.compile(optimizer='SGD',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
compile
には引数が3つあります。
- optimizer:最適化手法を設定する。今回は
SGD
に設定。 - loss:損失関数を設定する。今回は
sparse_categorical_crossentropy
に設定。 - metrics:評価関数を設定する。基本的にはここに
accuracy
を入れておけば問題ない。自作の評価関数を設定しても良い。
損失関数や評価関数などはこちらのサイトが参考になりました↓
それでは学習を行います。model
インスタンスのfit
メソッドを使うことで学習を行うことができます。
training_history = model.fit(train_data, train_labels,
validation_data=(val_data, val_labels),
epochs=30,
batch_size = Ndata_train//10,
verbose=1)
Epoch 1/30
11/11 [==============================] - 1s 18ms/step - loss: 1.1173 - accuracy: 0.3600 - val_loss: 1.0531 - val_accuracy: 0.3067
Epoch 2/30
11/11 [==============================] - 0s 5ms/step - loss: 1.0454 - accuracy: 0.3467 - val_loss: 1.0474 - val_accuracy: 0.3867
Epoch 3/30
11/11 [==============================] - 0s 4ms/step - loss: 1.0529 - accuracy: 0.3733 - val_loss: 1.0373 - val_accuracy: 0.5067
・・・・
Epoch 28/30
11/11 [==============================] - 0s 6ms/step - loss: 0.4374 - accuracy: 0.8800 - val_loss: 0.4318 - val_accuracy: 0.7200
Epoch 29/30
11/11 [==============================] - 0s 4ms/step - loss: 0.4376 - accuracy: 0.8000 - val_loss: 0.3903 - val_accuracy: 0.9333
Epoch 30/30
11/11 [==============================] - 0s 4ms/step - loss: 0.4205 - accuracy: 0.8800 - val_loss: 0.3966 - val_accuracy: 0.8000
fit
の引数には、訓練データとそれに対応する訓練データのラベルを代入しています。
また今回はオプションとして他に4つの引数を使っています。
- validation_data:検証用データをタプルにして渡す。今回は
(val_data, val_labels)
を設定している。 - epochs:エポック数を設定。今回は30に設定している。デフォルトは1。
- batch_size:バッチサイズを設定。今回ミニバッチのサイズは
Ndata_train//10
によって7つだが、この数字に特に意味はない。デフォルトは32。 - verbose:ログ出力の設定。0だとログが出ない、正の値だと細かいログが出力され、負の値だとepoch数のみ表示される。デフォルトは1。
結果の評価
fitの返り値をtraining_history
に代入したため、
学習の履歴を見るためにtraining_history
の中身を見ていきましょう。
dir(training_history)
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'_chief_worker_only',
'_implements_predict_batch_hooks',
'_implements_test_batch_hooks',
'_implements_train_batch_hooks',
'_keras_api_names',
'_keras_api_names_v1',
'_supports_tf_logs',
'epoch',
'history',
'model',
'on_batch_begin',
'on_batch_end',
'on_epoch_begin',
'on_epoch_end',
'on_predict_batch_begin',
'on_predict_batch_end',
'on_predict_begin',
'on_predict_end',
'on_test_batch_begin',
'on_test_batch_end',
'on_test_begin',
'on_test_end',
'on_train_batch_begin',
'on_train_batch_end',
'on_train_begin',
'on_train_end',
'params',
'set_model',
'set_params',
'validation_data']
大量にでてきたましたが学習の履歴はhistory
に格納されてそうですね。
training_history.history
{'accuracy': [0.36000001430511475,
0.3466666638851166,
0.3733333349227905,
・・・・
0.43177515268325806,
0.3902606964111328,
0.39655089378356934]}
出力結果に'accuracy':
とあることからdict型のリストであることがわかりました。。
辞書のキーに何が入ってるのか確認するためにkeys
メソッドを使います。
training_history.history.keys()
dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])
4つのキーで構成されていることがわかりました。
- loss:訓練データに対する損失関数の値
- accuracy:訓練データに対する精度の値
- val_loss:検証データに対する損失関数の値
- val_accuracy:検証データに対する精度の値
それでは最終的な、訓練データに対する精度の値と検証データに対する精度の値を確認してみましょう。
print("traininig")
print(training_history.history['accuracy'][-1])
print("validation")
print(training_history.history['val_accuracy'][-1])
traininig
0.8799999952316284
validation
0.800000011920929
訓練データに対する精度の値が88%、検証データに対する精度の値が80%であることがわかりました。
最後に損失関数と精度の履歴について可視化したいと思います。
# 訓練データに対する損失関数のプロット
y=training_history.history['loss']
x=range(len(y))
plt.semilogy(x,y,label="loss for training")
# 検証データに対する損失関数のプロット
y=training_history.history['val_loss']
x=range(len(y))
plt.semilogy(x,y,label="loss for validation",alpha=0.5)
plt.legend()
plt.xlabel("Steps")
plt.show()
# 訓練データに対する精度のプロット
y=training_history.history['accuracy']
x=range(len(y))
plt.plot(x,y,label="accuracy for training")
# 検証データに対する精度のプロット
y=training_history.history['val_accuracy']
x=range(len(y))
plt.plot(x,y,label="accuracy for validation")
plt.legend()
plt.xlabel("Steps")
plt.ylim(0,1.1)
plt.show()
図2. アヤメの分類の機械学習の履歴。上が損失関数の履歴で、下が精度の履歴。
損失関数の散布図は対数軸にするためにplt.semilogy
を使っています。
精度の散布図はplt.ylim
を使ってy軸の範囲を0~1.1に設定しています。
結論
最終的な訓練データの正答率は88%、検証データの正答率は80%でした。
アヤメは3種類なので当てずっぽうの33.3%よりは高い精度となりました。
あまり良い精度ではありませんが、ひとまず機械学習成功?と言えるでしょう。
最後の図2では機械学習の目標通り、損失関数が減少すると分類精度は上昇するような負の相関が見られました。
考察
今回のセットアップは初学者用なので変更・改善の余地が多くあります。
- データの分割の仕方、半分半分に分けずに訓練データをもっと多く取ってみる。
- 中間層の数やユニット数を増やしてみる。
- 全結合層だけでなく他の層を試したり、relu関数以外の他の活性化関数も使ってみる。
- 最適化アルゴリズムをSGDではなく、Adamなど別のものに変更してみる。
- 損失関数をsparse_categorical_crossentropyではなく別のものに変更してみる。
- エポック数を増やしたり、バッチサイズを変更してみる。
これらを試したからといって必ずしも精度が上がるわけではありませんが、検証する価値はあると思います。ぜひコードをコピペして遊んでみてください。
感想
長い記事を読んでいただきありがとうございました。
実はコードよりも、そもそもSGDとは何か、交差エントロピーとは何か...といった数学的な問題に時間がかかりました。
微分・線形代数・確率統計など大学数学の知識連発で、高校生の時に理系を目指した自分に感謝しまくりです。
しかし理系と言えど数学が苦手なのでどこまで太刀打ちできるかわかりません。
いずれGANや強化学習もやってみたいので自分の脳スペックと相談しながら気ままに勉強したいと思います。
最後に、初心者が必死こいて殴り書いた記事ですが何かの役に立てばうれしいです。
参考
-
sci-kit learnは機械学習のためのモジュールであり、アヤメ以外にも多くのデータセットが含まれています。 ↩︎
-
https://sinyblog.com/deaplearning/keras_how_to/#:~:text=説明-,Dense,-(keras.layers.Dense) ↩︎
Discussion