- 投稿日:2020-07-03T20:19:37+09:00
Tensorflow2(Subclassing API)によるMNIST画像分類の実装~精度評価
はじめに
今回は以下について紹介します。
- 手書き数字のデータセットとして有名なMnistをTensoflowAPIを使わずにダウンロード
Tensorflow Subclass APIによる分類モデルの実装~評価まで一連の流れ
- データのダウンロード・読み込み
- データの前処理およびジェネレータによる出力
- モデルの学習
- モデルの評価
なおここでSubclass APIも含めた機械学習のモデルを構築する3つの方法について網羅的に紹介しているのでご覧ください。
【対象読者】
- 色んなpythonライブラリを活用して分類モデルを実装してみたい人
- TensorFlow2.xのチュートリアルレベルよりスキルをアップグレードしたい人
- 実装した分類モデルを 様々な指標で評価したい人
1. データの準備
1-1) Mnist csvファイルのダウンロード
まず、Mnistデータセットを準備しましょう。
前の記事でも紹介しましたが、
Mnistとは‘Mixed National Institute of Standards and Technology database’の略語で手書き数字画像60,000枚と、テスト画像10,000枚を集めた画像データセットです。
前回同様、tensorflow.keras.datasets.mnist
でMnistデータセットを簡単にロードできますが、
今回はMnistデータセットを私が作りたい形で変更してPickle形式で保存して利用しようと思っているので、
Mnist in CSV(kaggle)でMnistのcsvファイルをダウンロードしました。MnistのCSVファイルを開くと数字の羅列がされたデータなので、どのような意味を持ったファイルなのか画像処理初心者では把握するのが難しい印象です。
しかし、意味が分かれば難しくなく…
- 結論から言うとMnistのCSVファイルの1つの行は1つの画像情報に当たります。
- 各行の先頭の数字はラベルとしてその行のイメージがどんな数字か表示しています。
- それ以外の784個の数字は0~255の間の値(画素値)で縦横28×28(784)を表現しているにすぎません。
2-1) Pickle形式で保存
csvファイルそのまま使用して学習させても大丈夫ですが、Pickle形式のファイルで保存してみましょう。
Pickleはオブジェクトをファイルとして保存するものであり、保存時・読み込み時に特別な設定や処理をすることなくオブジェクトをそのまま読み書きできるという利点があるのでPickle形式で保存することが都合のいい場合があります。
csvファイルをPickle形式で保存するにはpandas.DataFrame
を使うと簡単に作成できます。
( ※ tf.data.Datasetの活用が推奨されているようですが、今回の記事では説明を割愛します。)(1) ライブラリのインポート
ライブラリをインポートします。
import os, sys import csv import numpy as np import pandas as pd import tensorflow as tf
pandas
はPythonのデータ解析用のライブラリで機械学習ではデータの整理、可視化、前処理するため使います。(2) データの読み込み
Mnistデータセットをダウンロード(URLは上記1-1参照)して保存したパスを指定し
pd.read_csv
でcsvファイルを読み込んでいます。file = open("/Mnistデータセットが保存されているパス/mnist_train.csv") data_train = pd.read_csv(file)
read_csv
関数の引数で区切り文字の指定するsep
やインデックスラベルの指定するindex_col
等を設定することができますが、ここではcsvファイルを読み込むだけなので詳しい説明は省略します。(3) イメージとラベルを分離してpandas.DataFrameへの変更
pythonのスライス記法という要素の指定方法でcsvファイルのイメージとラベルを分離します。
x_train = np.array(data_train.iloc[:, 1:]) # イメージのデータ y_train = np.array(data_train.iloc[:, 0]) # ラベルのデータ img_id = list(range(len(x_train))) df = pd.DataFrame({'img_id':img_id,'mnist_img':list(x_train), 'mnist_label':list(y_train)})
pd.DataFrame
は二次元の表形式のデータ(テーブルデータ)を取り扱う、pandasのインスタンスです。
(※ 一次元の表形式はpd.Series
です。)
pd.DataFrame
はまずPythonの辞書型データの定義した後、DataFrameを定義する流れになります。
Pythonの辞書型データの定義
{“キー”:list型のデータ,...}
- 'キー'の数 = 列(Columns)の数
list
型のデータの数 = 行(index)の数作った辞書型データを
pd.DataFrame
メソッドに引数として入力x_tain, y_trainはデータ型は
numpy.ndarray
のため、上記の通りlist
型で変更しなければpandas.DataFrame
で保存できません。display(df) # 確認上記のコードで確認してみると下記の表ように画面で見目よい表示されます。
img_id mnist_img mnist_label 0 0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 5 1 1 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 0 2 2 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 4 ... ... ... ... 59998 59998 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 6 59999 59999 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 8 60000 rows × 3 columns
(4) 指定したパスでPickle形式のファイルを保存
dataset_name = 'ファイルの名' filename = '{}_train_df.pkl'.format(dataset_name) filepath = os.path.join(ファイルまでのパス, filename) df.to_pickle(filepath)テストデータセットのファイルも同じ方法でPickle形式への保存ができますので、上のコードを参考して作成してください。
2. ジェネレータの実装
今回はオリジナルのクラスでジェネレータを実装します。
generator.py
というファイルにジェネレータを実装します。ジェネレータとはイテレータ(;反復可能オブジェクト)の一種であり、1要素を取り出そうとする度に処理を行い、要素をジェネレートするタイプのものです。
AIのプログラムにおいては、モデルに数枚づつ画像を提供し続ける処理(ミニバッチ処理)が必要になる為、ジェネレータでその機能を実現します。イテレータを理解すればジェネレータがどんなものかすぐわかると思いますので話が横道にそれますが、イテレータの起動についてしばらく説明します。
- オブジェクトの
__iter__()
メソッドが呼ばれ、イテレータ実装を返すことが求められます。- この返り値で得られたオブジェクトは
__next__()
というメソッドが呼ばれます。__next__()
メソッドは1つずつ要素を取り出します。__next__()
メソッドはイテレータが尽きている場合、 default が与えられていればそれが返され、 そうでなければ StopIteration が送出されます。下記のコードは私が作ったジェネレータの全体です。上記の内容を参考にコードの動作を考えてみると良いかと思われます。
以降でコードの重要な部分を少し詳しく説明します。class BatchGenerator(): def __init__(self, df, shuffle = False, random_state = None): self.data_list = df.to_dict(orient='records') self.batch_size = 32 self.input_shape = (28, 28, 1) self.img_size = 28 self.shuffle = shuffle if random_state is None: random_state = np.random.RandomState(1234) self.random_state = random_state self._idx = 0 self._reset() def __len__(self): N = len(self.data_list) b = self.batch_size return N // b + bool(N % b) def __iter__(self): return self def __next__(self): if self._idx >= len(self.data_list): self._reset() raise StopIteration() selected_data_list = self.data_list[self._idx:(self._idx + self.batch_size)] img_list = [] label_list = [] batch = {} for data in selected_data_list: img = self.load_img(data) agu_img = self.data_augmentation(img) agu_img = np.expand_dims(agu_img, axis=-1) one_hot_labeled = self.one_hot_labeling(data) img_list.append(agu_img) label_list.append(one_hot_labeled) batch['batch_id'] = np.asarray([i['img_id']for i in selected_data_list]) batch['batch_mnist_img'] = np.asarray(img_list) batch['batch_mnist_label'] = np.asarray(label_list) self._idx += self.batch_size return batch def _reset(self): if self.shuffle: self.data_list = shuffle(self.data_list, random_state=self.random_state) self._idx = 0 def load_img(self, data): img = data['mnist_img'] img = img/255.0 img = img.reshape(28, 28, 1) img = img.astype('float32') return img def one_hot_labeling(self, data): one_hot_label = tf.keras.utils.to_categorical(data['mnist_label'], num_classes=10) return one_hot_label def data_augmentation(self,image): bg_value = np.median(image) angle_1 = np.random.randint(-45, 45, 1) mat_1 = cv2.getRotationMatrix2D((self.img_size/2, self.img_size/2), angle_1[0], 1) affine_img = cv2.warpAffine(image, mat_1, (self.img_size, self.img_size), borderValue=(0, 0, 0)) tr_x = self.img_size*np.random.uniform()-self.img_size/2 tr_y = self.img_size*np.random.uniform()-self.img_size/2 mat_2 = np.array([[1,0,tr_x],[0,1,tr_y]], dtype=np.float32) affine_img_translation = cv2.warpAffine(affine_img, mat_2, (self.img_size, self.img_size)) return affine_img_translation2-1) データをbatch_sizeほど読み込み
__iter__()
メソッドにより__next__()
メソッドが呼ばれます。
__next__()
メソッドでは設定したself.batch_size
だけself.data_list
からデータを取得します。
self.data_list
はdf.to_dict(orient)
が割り当てられていますが、これはpandas.DataFrame
を辞書型(dict)に変換することです。df.to_dict(orient)引数
- orient : 以下の形式を指定することが可能です。
- dict(デフォルト) : keyが列ラベル、valueが行ラベルと値の辞書となる。
{column -> {index -> value}}
- list:keyが列ラベル、valueが値のリストとなる。行名の情報は失われる。
{column -> [values]}
- series:keyが列ラベル、valueが行ラベルと値のpandas.Seriesとなる。
{column -> Series(values)}
- split:keyが'index', 'columns', 'data'となり、それぞれのvalueが行ラベル、列ラベル、値のリストとなる。
{index -> [index], columns -> [columns], data -> [values]}
- records : keyが列ラベル、valueが値となる辞書を要素とするリストとなる。行名の情報は失われる。
[{column -> value}, ... , {column -> value}]
- index:keyが行ラベル、valueが列ラベルと値の辞書となる。
{index -> {column -> value}}
戻り値
dict
、list
またはcollections.abc.Mapping
2-2) データの前処理
load_img
とone_hot_labeling
関数でデータの前処理を行います。Mnistのデータの普通の前処理流れはです。(1) イメージの前処理
def load_img(self, data): img = data['mnist_img'] img = img/255.0 img = img.reshape(28, 28, 1) img = img.astype('float32') return img
データを (600000, 784) から (60000,28,28,1)に
reshape
します。
- 最後の1はチャンネルでグレースケール化するために追加します。
イメージを255.で割り算を行い、0~1に正規化(normalization)します。
- 正規化をすることで、精度がよくなります。
データ型を
float32
で変更する。
- ニューラル ネットワークをトレーニングする時は32ビット整数を使用するのが一般的なため、
float32
で変更します。(2) ラベルの前処理
ラベルはOne-Hotエンコーディングします。One-Hotエンコーディングとはカテゴリカル変数に対して、各要素が該当するなら1、該当しないなら0とするカラムを変数の要素数分作ることです。
Mnistのラベルは[0 ~ 9]の10個の数字が存在するのでone-hot結果の次元数は10になります。
例えば、‘5’である正解ラベルをOne-Hotエンコーディング場合は、[0.,0.,0.,0.,0.,1.,0.,0.,0.,0.]になります。One-hot表現のメリットとデメリットは下記の通りです。
メリット デメリット 変数の全ての値を平等に扱えて、機械学習アルゴリズムの予想(prediction)結果向上 次元数が増えて、メモリ使用量や計算量が爆発的に増加 def one_hot_labeling(self, data): one_hot_label = tf.keras.utils.to_categorical(data['mnist_label'], num_classes=10) return one_hot_label今回は
keras.utils
に実装されたto_categorical
を使います。
( ※ NumPyのeye
またはidentity
等もone-hot表現に変換できます。)keras.utils.to_categorical(y, num_classes=None)クラスベクトル(0からnb_classesまでの整数)を
categorical_crossentropy
とともに用いるためのバイナリのクラス行列に変換します。引数
- y: 行列に変換されるクラスベクトル(0からnum_classesまでの整数)
- num_classes: 総クラス数
戻り値
- 入力のバイナリ行列表現
2-3) DataAugumentation(データ拡張)
DataAugumentationとは学習データ(訓練データ)の画像に対して平行移動、拡大縮小、回転、ノイズの付与等の人為的に処理を加えてデータ水増しするテクニックです。機械学習における普遍的な課題である過学習(Overfitting)を予防する利点があります。
【過学習とは?】
統計学や機械学習において、あるデータ群を基につくったモデルが、そのデータ群に含まれない新しいデータ群に対しては、同水準の予測精度を示すことができない状態DataAugumentationでは
tf.keras.preprocessing.image.ImageDataGenerator
を利用して簡単に色んな処理の実装ができますが、ここでは直接openCV(;画像や動画を処理するのに必要な、さまざま機能が実装されているライブラリ)を使ってイメージをを回転と平行移動の処理を加えてみました。def data_augmentation(self,image): bg_value = np.median(image) # 回転の処理 angle_1 = np.random.randint(-45, 45, 1) mat_1 = cv2.getRotationMatrix2D((self.img_size/2, self.img_size/2), angle_1[0], 1) affine_img = cv2.warpAffine(image, mat_1, (self.img_size, self.img_size), borderValue=(0, 0, 0)) # 平行移動の処理 tr_x = self.img_size*np.random.uniform()-self.img_size/2 tr_y = self.img_size*np.random.uniform()-self.img_size/2 mat_2 = np.array([[1,0,tr_x],[0,1,tr_y]], dtype=np.float32) affine_img_translation = cv2.warpAffine(affine_img, mat_2, (self.img_size, self.img_size)) return affine_img_translationopenCVを使うためにデータ型が
numpy.ndarray
である必要がありますが、これらのイメージは既にnumpy.ndarray
データ型なので型変換はしなくても大丈夫です。
data_augmentation
関数を適用したMnistは下のようになります。左図は普通のMnistイメージで右図は回転と平行移動の処理されたイメージです。openCVを利用するこのように画像で細かいAugmentation処理を加えることができることがわかります。
3. Subclassing APIによるモデルを作成
TendorFlow 2.0以降ではモデルを構築する方法がSequential API、Functional API、Subclassing API 3つあります。各々の詳しい説明は前回の記事を参考してください。ここでは優柔なモデルを構築するため、3つの中で一番優柔性が高いKerasのmodel Subclassing APIを使って
tf.keras
モデルを作ります。3-1) モデルの構築
モデルの構成は25 Million Images! (0.99757) MNISTを参考に作成してみました。モデルの具体的な実装方法を知りたい方は前回の記事をご覧ください。
class ClassificationModel(tf.keras.Model): def __init__(self): super(ClassificationModel, self).__init__() self.conv1_1 = layers.Conv2D(32, 3, activation='relu') self.conv1_2 = layers.BatchNormalization() self.conv1_3 = layers.Conv2D(32, 3, activation='relu') self.conv1_4 = layers.BatchNormalization() self.conv1_5 = layers.Conv2D(32, 5, strides=2, padding='same', activation='relu') self.conv1_6 = layers.BatchNormalization() self.drop1 = layers.Dropout(0.4) self.conv2_1 = layers.Conv2D(64, 3, activation='relu') self.conv2_2 = layers.BatchNormalization() self.conv2_3 = layers.Conv2D(64, 3, activation='relu') self.conv2_4 = layers.BatchNormalization() self.conv2_5 = layers.Conv2D(64, 5, strides=2, padding='same', activation='relu') self.conv2_6 = layers.BatchNormalization() self.drop2 = layers.Dropout(0.4) self.conv3_1 = layers.Conv2D(128, kernel_size = 4, activation='relu') self.conv3_2 = layers.BatchNormalization() self.conv3_3 = layers.Flatten() self.drop3 = layers.Dropout(0.4) self.d3 = layers.Dense(10, activation='softmax') def call(self, x): x = self.conv1_1(x) x = self.conv1_2(x) x = self.conv1_3(x) x = self.conv1_4(x) x = self.conv1_5(x) x = self.conv1_6(x) x = self.drop1(x) x = self.conv2_1(x) x = self.conv2_2(x) x = self.conv2_3(x) x = self.conv2_4(x) x = self.conv2_5(x) x = self.conv2_6(x) x = self.drop2(x) x = self.conv3_1(x) x = self.conv3_2(x) x = self.conv3_3(x) x = self.drop3(x) return self.d3(x)3-2) モデルのビルド
model = ClassificationModel() model.build((32, 28, 28, 1)) model.summary()
model.build()
メソッド : 重みを定義するメソッドmodel.summary()
メソッド : 作成すると作ったモデルの構成を簡単確認ができるメソッドModel: "classification_model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d (Conv2D) multiple 320 _________________________________________________________________ batch_normalization (BatchNo multiple 128 _________________________________________________________________ conv2d_1 (Conv2D) multiple 9248 _________________________________________________________________ batch_normalization_1 (Batch multiple 128 _________________________________________________________________ conv2d_2 (Conv2D) multiple 25632 _________________________________________________________________ batch_normalization_2 (Batch multiple 128 _________________________________________________________________ dropout (Dropout) multiple 0 _________________________________________________________________ conv2d_3 (Conv2D) multiple 18496 _________________________________________________________________ batch_normalization_3 (Batch multiple 256 _________________________________________________________________ conv2d_4 (Conv2D) multiple 36928 _________________________________________________________________ batch_normalization_4 (Batch multiple 256 _________________________________________________________________ conv2d_5 (Conv2D) multiple 102464 _________________________________________________________________ batch_normalization_5 (Batch multiple 256 _________________________________________________________________ dropout_1 (Dropout) multiple 0 _________________________________________________________________ conv2d_6 (Conv2D) multiple 131200 _________________________________________________________________ batch_normalization_6 (Batch multiple 512 _________________________________________________________________ flatten (Flatten) multiple 0 _________________________________________________________________ dropout_2 (Dropout) multiple 0 _________________________________________________________________ dense (Dense) multiple 1290 ================================================================= Total params: 327,242 Trainable params: 326,410 Non-trainable params: 832 _________________________________________________________________3-3) 損失関数・オプティマイザの定義
loss_object = tf.keras.losses.SparseCategoricalCrossentropy() optimizer = tf.keras.optimizers.Adam() train_loss = tf.keras.metrics.Mean(name='train_loss') train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy') test_loss = tf.keras.metrics.Mean(name='test_loss') test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')4. 学習
まず、先ほどの作った
generator.py
を呼び出すために、インポートします。
今回のケースでは./data/にgenerator.py
を配置しています。from data.generator import BatchGeneratorそして、保存したpickle形式のファイルを読み込みます。
train_df = pd.read_pickle("/フォルダのパス/指定したファイルの名_train_df.pkl")インポートしたジェネレータを定義しましょう。
train_gen = BatchGenerator(train_df, config) batch = next(train_gen)学習とテストを行います。
@tf.function def train_step(x, t): with tf.GradientTape() as tape: predictions = model(x, training=True) loss = loss_object(t, predictions) gradients = tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(gradients, model.trainable_variables)) train_loss(loss) train_accuracy(t, predictions) @tf.function def test_step(x, t): test_predictions = model(x) t_loss = loss_object(t, test_predictions) test_loss(t_loss) test_accuracy(t, test_predictions) EPOCHS = 5 for epoch in range(EPOCHS): for batch in tqdm(train_gen): x = batch['batch_mnist_img'] t = batch['batch_mnist_label'] t = np.argmax(t, axis=1).reshape(-1,1) train_step(x, t) #学習 for batch in tqdm(train_gen): x = batch['batch_mnist_img'] t = batch['batch_mnist_label'] t = np.argmax(t, axis=1).reshape(-1,1) test_step(x, t) #テスト template = 'Epoch {}, Loss: {}, Accuracy: {}, test-Loss: {}, test-Accuracy:{}' print(template.format(epoch + 1, train_loss.result(), train_accuracy.result() * 100, test_loss.result(), test_accuracy.result()*100))5. 分類モデルの評価
分類モデルを未知データ(;あるモデルの訓練データに含まれない新しいデータ群)を利用して評価しましょう。そのため、先ほどの作ったpickle形式のテストデータを読み込みます。
test_df = pd.read_pickle("/テストデータが保存されているパス/ファイル名_test_df.pkl") display(test_df)
img_id mnist_test_img mnist_real_label 0 0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 7 1 1 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 2 2 2 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 1 ... ... ... ... 9998 9998 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 5 9999 9999 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 6 10000 rows × 3 columns
今回はジェネレータを利用せずに、評価用のデータを準備します。
img_id = pd.Series.tolist(test_df['img_id']) mnist_test_img = pd.Series.tolist(test_df['mnist_test_img']) mnist_test_label= pd.Series.tolist(test_df['mnist_real_label']) mnist_test_img_list = [] #イメージ mnist_test_label_list = [] #ラベル for i in range(len(img_id)): mnist_test_img_list.append((mnist_test_img[i].reshape(28,28,1))/255.) mnist_test_label_list.append(mnist_test_label[i])
分類モデルの評価に次のような指標を使います。
5-1) 混同配列
まずは、混同配列です。混同行列の主対角成分の要素は、正確にクラス分類されたサンプル数を示し、それ以外の要素は、実際とは違うクラスに分類されたサンプル数を示します。
混同配列を作るにはsklearnライブラリまたはseabornライブラリを利用することができます。
(1) sklearnライブラリを利用する方法
scikit-learnは、分類、回帰、クラスタリング等が実装されているPythonの機械学習ライブラリです。scikit-learnライブラリを読み込む時は
sklearn
と書きます。from sklearn.metrics import confusion_matrix y_true = np.asarray(mnist_test_label_list) y_pred = model(np.asarray(mnist_test_img_list)) y_pred = np.argmax(y_pred, axis=1) #reshape(-1,1) confusion_matrix(y_true, y_pred)array([[ 976, 0, 0, 0, 0, 1, 1, 1, 1, 0], [ 0, 1127, 1, 2, 1, 0, 3, 0, 1, 0], [ 3, 1, 1010, 2, 1, 0, 2, 8, 4, 1], [ 0, 0, 0, 1003, 0, 2, 0, 1, 4, 0], [ 0, 1, 0, 0, 975, 0, 2, 0, 0, 4], [ 2, 0, 1, 12, 0, 862, 5, 0, 4, 6], [ 8, 4, 0, 1, 6, 1, 934, 0, 4, 0], [ 1, 1, 8, 1, 0, 0, 0, 1013, 1, 3], [ 6, 2, 0, 3, 2, 0, 0, 4, 951, 6], [ 3, 3, 0, 2, 13, 1, 0, 2, 4, 981]])(※ 結果は実装したゼネレーターやモデルによって異なります。)
(2) seabornライブラリを利用する方法
seabornはデータの可視化(グラフ作成)を行うPythonライブラリです。sklearnライブラリを利用すると結果は同じですが、sklearnライブラリより視覚的に見えます。インポート文で
seaborn
書くことで使用できます。import seaborn as sn from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt def print_cmx(y_true, y_pred): labels = sorted(list(set(y_true))) cmx_data = confusion_matrix(y_true, y_pred, labels=labels) df_cmx = pd.DataFrame(cmx_data, index=labels, columns=labels) plt.figure(figsize = (12,7)) sn.heatmap(df_cmx, annot=True, fmt='g' ,square = True) plt.show() print_cmx(y_true , y_pred)(※ 結果は実装したジェネレーターやモデル、乱数によって異なります。)
5-2) 精度(正解率)
精度は、すべてのサンプルについて、正確に予測できた割合を示します。
from sklearn.metrics import accuracy_score print('正解率(accuracy):', accuracy_score(y_true, y_pred))正解率(accuracy): 0.9832(※ 結果は実装したジェネレーターやモデル、乱数によって異なります。)
感覚的にはこの指標が一番分かりやすいですが、陽性クラスと陰性クラスでサンプル数に大きな差があるとき、うまく性能を表せない場合があります。それで他の指標で確認する必要があります。
5-3) 適合率
適合率は、陽性であると予測されたものが、実際にどのくらい陽性だったのかを示す指標です。偽陽性と予測することを低く抑えたい場合は、適合率を用います。
from sklearn.metrics import accuracy_score print('正解率(accuracy):', accuracy_score(y_true, y_pred))適合率(precision): 0.9832536618523209(※ 結果は実装したジェネレーターやモデル、乱数によって異なります。)
5-4) 再現率
再現率は、実際に陽性のサンプルのうち、どのくらい陽性と予測されたかを示す指標です。偽陰性と予測することを低く抑えたい場合は、再現率を用います。
from sklearn.metrics import recall_score print('再現率(recall):',recall_score(y_true, y_pred, average='macro'))再現率(recall): 0.9828852947538065(※ 結果は実装したジェネレーターやモデル、乱数によって異なります。)
5-5) F1値
適合率と再現率は重要な指標ですが、片方だけではモデルの全体像が把握できません。この2つの指標をまとめた評価指標がF値で、適合率と再現率の調和平均がF1値です。
from sklearn.metrics import f1_score print('F1値(F1-measure):',f1_score(y_true, y_pred, average='macro'))F1値(F1-measure): 0.9830195547575524(※ 結果は実装したジェネレーターやモデル、乱数によって異なります。)
6. まとめ
前回の記事より多くのpythonライブラリを利用して画像分類モデル実装・学習・評価しました。
またPytorchの書き方に似ているSubclass APIによるモデルを実装したため、Pytorchユーザーにも馴染みやすい書き方だったと思います。
ぜひ試してみてください。以上です。
記事に誤り等ありましたら、ご指摘ください。7. 参考資料
- 投稿日:2020-07-03T16:19:17+09:00
TensorFlow QuantumのMNIST量子機械学習をPyTorch + Blueqatに移植してみた
このスライドはBlueqat Summit 2020夏のために作成しました。
About Me
- 量子計算用ライブラリBlueqatをメインで開発しています
- 最近、未踏ターゲット事業2020での開発もやっています
- 圏論とか定理証明とかにも入門しはじめました
TensorFlow Quantum
https://github.com/tensorflow/quantum
TensorFlow Quantum (TFQ) is a Python framework for hybrid quantum-classical machine learning that is primarily focused on modeling quantum data. TFQ is an application framework developed to allow quantum algorithms researchers and machine learning applications researchers to explore computing workflows that leverage Google’s quantum computing offerings, all from within TensorFlow.
量子機械学習の時代、ついに?
Blueqat開発者にとって、めちゃくちゃ気になる
ということで、とりあえずホワイトペーパー1読んで、必要に応じてソース2を眺めつつ、チュートリアルのMNIST3をPyTorch + Blueqatに移植してみました。
前提
- ここで出てくるのはすべてゲート方式の量子コンピュータの話です
- 実際の量子コンピュータではなく、シミュレータを使っています
- 実機に比べてシミュレータは、ノイズの影響がないため精度がよく、また、量子コンピュータの内部状態を直接取得できるため、小さな回路での期待値計算は実機より高速です
量子コンピュータの理想と現実
- 理想
- 量子コンピュータがあれば、どんな計算でも一瞬で解ける
- 量子的超越により量子コンピュータは既に従来のコンピュータを凌駕した
- 量子機械学習 = シンギュラリティー
- 現実
- そもそも「どんな計算でも一瞬で解ける」は大嘘(既知の物理法則を凌駕する必要がある)
- 「量子的超越」を既に達成したかは微妙4で、また、「量子的超越」は実用的なアプリケーションの有無とは直接関係がない
- 今はハードウェアが未熟で、できることはほとんど何もない
- 量子機械学習は想像を絶するほどショボい!!
- シミュレータは非力だが、実機は(一部用途を除き)シミュレータよりも非力。なのでシミュレータを使ってやっているが、やっぱりショボい
- 実用面でシミュレータを超える実機が出たら状況が変わる? (けれど、それはいつ?)
TensorFlow Quantum (TFQ)は一体何をしてくれるのか?
- 機械学習部分→TensorFlowを使う
- 量子回路部分→Cirqを使う
TFQはそれらを糊付けするライブラリで、
- 量子回路をTFのテンソルに乗せる
- 量子回路テンソルに回路を付け加えるTFのレイヤーを作る
- 量子回路テンソルを、量子回路のシミュレータや実機で動かし、値を取り出すTFのレイヤーを作る
- 値を取り出すレイヤーについて、値を数値微分、あるいは微分を与える別回路によって微分できるようにする
を主にしている。
量子回路テンソル?
- 具体的には、Cirqの量子回路オブジェクトをprotobuf形式にバイナリエンコーディングしている
- PickleとかJSONとかそんなんだと思ってもらえばいい
- それをバイト列としてTFのテンソルに載せている
- とりあえずテンソルに載せられるのはメリット
- けれど、テンソルを(デコード、再エンコードすることなく)回路を直接いじるとかは難しそう
- Blueqatでやる上で、これも真似する必要があるのか、未だに迷っている
- 今回は回路のテンソル化はやらず、floatを入力として、入力値から回路を作ることにした
ということで、MNISTやります
ダウンスケールの衝撃
チュートリアルによると、まず、MNISTのうち3と6だけを取り出して、3か6かを判定する問題にする。(多分、量子ビット数とか計算時間の都合上)
衝撃ポイント1: MNISTはフルではできない衝撃ポイント2: MNISTの画像(28x28 pixel)は大きすぎるので、4x4 pixelに画像縮小
もはや、元の画像の面影がないです。逆にこれで分かるのすごい。その結果、同じデータなのにラベルが違う画像が少し出てきたので、学習用データからそれを削るらしく。
さらに、ピクセルの値が0.5より大きいかどうかで、bool値にしている。
そうすると、だいたいの画像が同じになってしまい、11520サンプルのうち、193枚しか画像が残らず、うち44枚が、3のラベルも6のラベルも両方ついている状況になってしまい、けれどそれを削ると学習には少なすぎるので、このままやるとのこと。
PyTorchでやる
いろいろ思うところはあるけど、やっていきましょう。
PyTorchでも、MNISTデータをリサイズしたけれど、
TensorFlowとはだいぶ違う雰囲気になった。(同じ「3」の画像を使った)
リサイズアルゴリズムの違いだと思うが、詳しくは調べていない。
閾値を0.5でbool値にすると、ほとんどの画像が重複した(全部falseになった?)ので、閾値を0.2に下げた。
そうすると、315枚の画像が残って、重複は54枚だった。
TensorFlowに倣って、これでそのまま学習させる。
モデル回路
TFQの実装に倣った。
- まず、画像(4x4 = 16ビット)サイズに合わせて、16 qubit用意する。各ピクセルのtrueに対応するビットをXゲートで|1>にする
- 測定用量子ビットを1つ付け足す
- 測定用量子ビットと、各ピクセル量子ビットの間にRXXゲートを噛ませる。回転角はパラメータにする(ピクセル数が16なので、全16パラメータ)
- 測定用量子ビットと、各ピクセル量子ビットの間にRZZゲートを噛ませる。回転角はパラメータにする(ピクセル数が16なので、全16パラメータ、RXXと合わせて32パラメータ)
Blueqat + PyTorchでは、入力テンソルとパラメータテンソルから量子回路を作って返す関数を用意し、その関数を「モデル回路」と考えることにした。
数値微分
前進差分法での数値微分をPyTorchに実装する。以前書いた。
Loss
TFQはHingeLossでやっていたが、PyTorchに見つからなかったので作った。
class HingeLoss(torch.nn.Module): def __init__(self): super(HingeLoss, self).__init__() self.loss = torch.nn.MarginRankingLoss(margin=1.) def forward(self, x, y): zeros = torch.zeros_like(x) return self.loss(x, zeros, y)
学習
バッチサイズ16、エポック数3
各バッチごとのloss(赤線)と正解率(青線)。
グラフ用のデータ取りミスって、各エポックごとに変な線が入っているのは気にしない。正解率
test用データで正解率を取ってみると1522/1968 (77.33739837398375 %)の正解。
TFQのチュートリアルでは8割を越えているので、悔しいが、結果が出るのに数時間かかるので、ハイパーパラメータを調整してやり直す気にはなれない。
古典との比較
TFQのチュートリアルで、
- CNNを利用して普通の大きさのMNIST画像で3と6とを識別
- フェアな比較になるように、4x4に縮小したMNIST画像で、パラメータ数も量子の32パラメータと近くなるように37パラメータのモデルを使って3と6とを識別
を試していた。
私の方でも、CNNは面倒で書かなかったが、37パラメータモデルは書いて試した。
学習はすぐに終わり、結果は1814/1968 (92.17479674796748 %)であった。衝撃ポイント3: 速度でも精度でも、圧倒的に古典のほうがいい
まとめ
量子機械学習をやった。
- 現状は何もメリットがない
- 想像を絶するほどショボい
- それでも、今後も量子機械学習はホットトピックだと思うので、PyTorch + Blueqatの方向性で今後も進めていきます
ソースコード(Jupyter Notebook): Gistに置きました。