【AI/実践編】生成AI-GANを実装【衝撃簡単④】

生成AI GANを実装

対象読者

今日はGAN(敵対生成ネットワーク)を実装していきます。

やることは、GAN(敵対生成ネットワーク)モデルを使って、MNISTデータに似た画像(偽物)を生成していきます。ノイズからMNISTっぽい画像を生成できるのはなかなか感動ものですよ!(笑)

GAN

p.s.学習には約40分くらいかかります。(1000イテレーションに約2分かかる)

実践の前にササっと見てみてください
AIの基本はこちらからどうぞ

今回の流れ

では、まずはプログラムの流れ・全体像を把握しておきましょう。

  1. データの準備: まず、MNISTデータセットなどの画像データを準備します。各画像は28×28ピクセルのグレースケール画像です。
  2. モデルの定義:
    • 生成器(Generator)モデル: ランダムノイズから画像を生成します。generator_model() 関数で定義されます。
    • 識別器(Discriminator)モデル: 画像が本物か偽物かを判定します。discriminator_model() 関数で定義されます。
    • GANモデル: 生成器と識別器を結合し、生成器がより良い偽の画像を生成できるようにトレーニングします。gan_model() 関数で定義されます。
  3. モデルのコンパイル:
    • 識別器: 二値分類の問題なので、binary_crossentropy 損失関数を使用してコンパイルします。また、Adamオプティマイザを使用します。
    • GAN: 生成器をトレーニングするので、生成器が生成した画像が本物に見えるように識別器を騙すようにします。生成器のみをトレーニングするため、識別器の重みは固定されます。
  4. トレーニング: トレーニングループを実行してGANをトレーニングします。
    • 本物の画像を識別器に渡してラベル1でトレーニングします。
    • 生成器にノイズを入力し、偽の画像を生成します。この偽の画像にはラベル0が付けられます。
    • 生成器が生成した偽の画像とラベル0を識別器に渡してトレーニングします。
    • これらのステップを繰り返し、GANをトレーニングします。
  5. 画像の保存: 一定の間隔で生成器によって生成された画像を保存します。これにより、トレーニング中の生成器の進行状況を視覚的に確認できます。
  6. モデルの保存: トレーニングが完了したら、生成器、識別器、およびGANモデルを保存します。これにより、将来の使用や再トレーニングが容易になります。

この手順に従うことで、GANモデルをトレーニングして新しい画像を生成することができます。

p.s.見ての通り長いので、一気にやろうとせず休憩しながら、ゆっくりやっていきましょう!

今回の全コード

では、ここに今日使ったコードを全部張っておきます。説明が前後することがあるので、不安に感じたらこちらで確認してください。

Python
import matplotlib.pyplot as plt
import numpy as np
import os
from keras.datasets import mnist
from keras.layers import Dense, Flatten, Reshape, LeakyReLU
from keras.models import Sequential
from keras.optimizers import Adam

width = 28
height = 28
channels = 1
shape = (width, height, channels)
noise_dim = 100

def generator_model(shape, noise_dim):
    model = Sequential()
    model.add(Dense(128, input_dim=noise_dim))
    model.add(LeakyReLU(alpha=0.01))
    model.add(Dense(28 * 28 * 1, activation='tanh'))
    model.add(Reshape(shape))
    return model

def discriminator_model(shape):
    model = Sequential()
    model.add(Flatten(input_shape=shape))
    model.add(Dense(128))
    model.add(LeakyReLU(alpha=0.01))
    model.add(Dense(1, activation='sigmoid'))
    return model

def gan_model(generator, discriminator):
    model = Sequential()
    model.add(generator)
    model.add(discriminator)
    return model

discriminator = discriminator_model(shape)
discriminator.compile(loss='binary_crossentropy', optimizer=Adam(), metrics=['accuracy'])
generator = generator_model(shape, noise_dim)
discriminator.trainable = False
gan = gan_model(generator, discriminator)
gan.compile(loss='binary_crossentropy', optimizer=Adam())

losses = []
accuracies = []
iteration_checkpoints = []

def train(iterations, batch_size, sample_interval):
    #前処理
    (X_train, _), (_, _) = mnist.load_data()
    X_train = X_train / 127.5 - 1.0
    X_train = np.expand_dims(X_train, axis=3)
    real_label = np.ones((batch_size, 1))
    fake_label = np.zeros((batch_size, 1))

    #訓練
    for iteration in range(iterations):
        idx = np.random.randint(0, X_train.shape[0], batch_size)
        batch_images = X_train[idx]
        z = np.random.normal(0, 1, (batch_size, noise_dim))
        gene_imgs = generator.predict(z)
        d_loss_real = discriminator.train_on_batch(batch_images, real_label)
        d_loss_fake = discriminator.train_on_batch(gene_imgs, fake_label)
        d_loss, accuracy = 0.5 * np.add(d_loss_real, d_loss_fake)
        discriminator.trainable = False
        g_loss = gan.train_on_batch(z, real_label)
        discriminator.trainable = True

        #データ確認
        if (iteration + 1) % sample_interval == 0:
            losses.append((d_loss, g_loss))
            accuracies.append(100.0 * accuracy)
            iteration_checkpoints.append(iteration + 1)

            print("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (iteration + 1, d_loss, 100.0 * accuracy, g_loss))
            save_images(generator, iteration + 1)

def save_images(generator, iteration, directory='gan_directory', image_grid_rows=4, image_grid_columns=4):
    if not os.path.exists(directory):
        os.makedirs(directory)

    z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, noise_dim))
    gene_imgs = generator.predict(z)
    gene_imgs = 0.5 * gene_imgs + 0.5
    fig, axs = plt.subplots(image_grid_rows, image_grid_columns, figsize=(4, 4), sharey=True, sharex=True)
    cnt = 0
    for row in range(image_grid_rows):
        for col in range(image_grid_columns):
            axs[row, col].imshow(gene_imgs[cnt, :, :, 0], cmap='gray')
            axs[row, col].axis('off')
            cnt += 1

    fig.savefig(f"{directory}/iteration_{iteration}.png")
    plt.close(fig)

iterations = 40000
batch_size = 128
sample_interval = 500
train(iterations, batch_size, sample_interval)

generator.save('generator.keras')
discriminator.save('discriminator.keras')
gan.save('gan_model.keras')

モジュールの準備

必要なモジュールをインポートします。もし、エラーが出たら『pip install ○○』インストールしてください。

Python
import matplotlib.pyplot as plt
import numpy as np
from keras.datasets import mnist
from keras.layers import Dense, Flatten, Reshape
from keras.layers import LeakyReLU
from keras.models import Sequential
from keras.optimizers import Adam
import os

import matplotlib.pyplot as plt
#Matplotlib ライブラリをインポートし、plt という別名で使用できるようにしています。Matplotlib は Python のデータ可視化ライブラリで、グラフや図を描画する際によく使用されます。

import numpy as np
#NumPy ライブラリをインポートし、np という別名で使用できるようにしています。NumPy は Python の数値計算ライブラリであり、多次元配列や行列演算などの高度な数値計算をサポートしています。

from keras.datasets import mnist
#Keras ライブラリの中から、MNIST データセットをダウンロードするための mnist モジュールをインポートしています。MNIST データセットは手書き数字画像データセットであり、機械学習のベンチマークとして広く使用されています。

from keras.layers import Dense, Flatten, Reshape
Keras ライブラリの中から、ニューラルネットワークの層を定義するための Dense、データの平坦化を行う Flatten、データの形状を変更する Reshape 層をインポートしています。

from keras.layers import LeakyReLU
#LeakyReLU は、ニューラルネットワークの中間層や出力層に適用される非線形の活性化関数の一つです。LeakyReLU は、特に深層ニューラルネットワークや生成モデルなどの多くの場面で使用されます。

from keras.models import Sequential
#Keras ライブラリの中から、Sequential モデルを定義するための Sequential クラスをインポートしています。Sequential モデルは、層を直線的に積み重ねて構築されるシンプルなモデルです。

from keras.optimizers import Adam
#Keras ライブラリの中から、Adam 最適化アルゴリズムを定義するための Adam クラスをインポートしています。Adam は勾配降下法の一種であり、ニューラルネットワークの学習時に使用される最適化アルゴリズムの一つです。

import os
#ファイルシステムの操作を行うためのモジュールをインポートしています。


生成画像を定義

ここでは、生成する画像の形状を定義します。今回はMNISTデータをもとに画像を生成するため、生成画像の形状をMNISTに合わせます。

Python
width = 28
height = 28
channels = 1
shape = (width, height, channels)
noise_dim = 100

width=28, height=28, channels=1
#これらの変数は、生成される画像の形状を定義します。MNIST データセットの画像サイズは 28×28 ピクセルで、チャンネル数は 1 です。チャンネルのグレースケールは1、RGB画像は3です。

shape=(width,height,channels)
shape は生成される画像の形状を表すタプルです。

noise_dim=100
#生成器の入力となる潜在空間の次元数を定義します。生成器のの入力はランダムなノイズから構成されるベクトルであり、その次元数を定義します。MNISTのような単純なデータセットでは、一般的には100次元のランダムノイズベクトルでも十分な情報を持つことができます。

p.s.ちなみにdimはdimention(次元)の略です。

生成器(ジェネレータ―)モデルを定義

ここでは、画像を生成する機能を持つ関数を定義していきます。具体的には、ランダムな潜在空間のベクトルを入力として受け取り、それを MNIST のような手書き数字の画像に変換する役割をもつ関数を作っていきます。また、AIにおいてこのような関数を生成器、またはジェネレータと呼びます。

Python
def generator_model(shape, noise_dim):
    model = Sequential()
    model.add(Dense(128, input_dim=noise_dim))
    model.add(LeakyReLU(alpha=0.01))
    model.add(Dense(28 * 28 * 1, activation='tanh'))
    model.add(Reshape(shape))
    return model

def generator_model(shape, noise_dim):
#この関数は、Generator モデルを構築するために使用されます。引数として shape(生成される画像の形状)と noise_dim(潜在空間の次元数)を受け取ります。

model=Sequential()
#Sequential クラスのインスタンスを作成し、model 変数に代入しています。これにより、新しいSequentialモデルが作成されます。このモデルは、層を順番に追加していくことで構築されます。

model.add(Dense(128,input_dim=noise_dim))
#最初の層として、全結合層(Dense)を追加します。この層は、入力として潜在空間のベクトルを受け取り、128 個のニューロンを持ちます。
#ニューロンは多いほど、より複雑な関数を学習することができますが、計算コストも高くなります。そのため一般的な選択肢である128を選択しました。

model.add(LeakyReLU(alpha=0.01))
#LeakyReLU 活性化関数を追加します。LeakyReLU は、負の入力に対して微小な勾配を持ち、勾配消失問題を緩和する役割があります。
alpha=0.01は、Leaky ReLU(Rectified Linear Unit)関数の負の領域における勾配の傾きを制御するパラメータです。通常のReLU関数では、負の入力に対して勾配が0になりますが、Leaky ReLUでは負の入力に対して小さな勾配を持ちます。これにより、勾配が0になることを防ぎ、勾配消失問題を緩和します。
#勾配の傾きを0.01に設定することは一般的な選択肢の一つであり、負の領域での勾配がゼロになることを防ぎつつ、その影響を抑えるために小さな値が選ばれます。

model.add(Dense(28*28*1,activation='tanh'))
#全結合層を追加します。Dense層は入力には1次元しか受け取れませんが、出力は何次元でもOK。
28*28*1はMNISTデータセットの画像のサイズ(28×28ピクセル)に合わせた出力層のニューロン数を表しています。
tanh 関数は出力値を [-1, 1] の範囲にスケーリングします。GANではsigmoid(0~1)よりもtanhを使うことが一般的です。

model.add(Reshape(shape))
#Reshape 層を追加して、直前の全結合層の出力を指定された画像形状に変形します。これにより、画像の形状に合わせたデータ構造が得られます。

return model
#構築した Generator モデルを返します。


識別器(ディスクリミネーター)モデルを定義

識別器というものを定義していきます。識別器とは、生成器 が生成した偽の画像と本物の画像を区別する役割を持ちます。

Python
def discriminator_model(shape):
    model = Sequential()
    model.add(Flatten(input_shape=shape))
    model.add(Dense(128))
    model.add(LeakyReLU(alpha=0.01))
    model.add(Dense(1, activation='sigmoid'))
    return model

def discriminator_model(shape)
この関数は、識別器モデルを構築するために使用されます。引数として shape(画像の形状)を受け取ります。

model=Sequential()
#Sequential モデルを初期化します。モデル構築の開始合図だと思ってください。

model.add(Flatten(input_shape=shape))
#入力画像の形状を平滑化する Flatten 層を追加します。画像データは、2次元のグリッド(幅x高さ)なのでDesnse層に通すには1次元に直す必要がある。

model.add(Dense(128))
#全結合層を追加します。この層は、128 個のニューロンを持ちます。

model.add(LeakyReLU(alpha=0.01))
#生成器で説明済み

model.add(Dense(1,activation='sigmoid'))
#sigmoid関数は出力を0から1の範囲にマッピングするため、識別器の出力を確率として解釈することができます。出力が0に近いほど偽物である確率が高く、1に近いほど本物である確率が高いと解釈できます。
#与えられた画像が本物か偽物かを判断することです。これが、生成器ではtanhを使い識別器ではsigmoidを使う理由です。生成器では出力を画像のピクセル値として解釈するためにtanh関数が使用されます。tanh関数は、出力を-1から1の範囲にマッピングするため、生成される画像のピクセル値を適切な範囲に収めるのに適しています。

return model
#構築した Discriminator モデルを返します。

GANモデルの定義

生成器(generator)と識別器(discriminator)を組み合わせて、GAN(Generative Adversarial Network)モデルを構築するための関数を作っていきましょう。

Python
def gan_model(generator, discriminator):
    model = Sequential()
    model.add(generator)
    model.add(discriminator)
    return model

def gan_model(generator, discriminator)
#この関数は、生成器と識別器を組み合わせて GAN モデルを構築するために使用されます。引数として 後に登場するgenerator=generator_model(shape,noise_dim)とdiscriminator=discriminator_model(shape)の値が入ります。

model = Sequential()
#Sequential モデルを初期化します。

model.add(generator)
#Generator モデルを追加します。Generator は偽の画像を生成する役割を持ちます。

model.add(discriminator)
#Discriminator モデルを追加します。Discriminator は生成された偽の画像と本物の画像を区別する役割を持ちます。

return model
#構築した GAN モデルを返します。このモデルは、Generator と Discriminator を組み合わせています。

モデルのコンパイル

では、モデルをコンパイルしていきましょう。コンパイルとはモデルの損失関数、最適化アルゴリズム評価指標などを設定する作業のことです。

Python
discriminator = discriminator_model(shape)
discriminator.compile(loss='binary_crossentropy', optimizer=Adam(), metrics=['accuracy'])
generator = generator_model(shape, noise_dim)
discriminator.trainable = False
gan = gan_model(generator, discriminator)
gan.compile(loss='binary_crossentropy', optimizer=Adam())

discriminator = discriminator_model(shape)
#Discriminator モデルを構築します。discriminator_model 関数は、画像の形状 shape を引数として受け取り、それに基づいて Discriminator モデルを構築します。

discriminator.compile(loss='binary_corssentropy', optimizer=Adam(), metrics=['accuracy'])
#Discriminator モデルをコンパイルします。ここで、損失関数として binary_crossentropy を指定し、最適化アルゴリズムとして Adam を使用します。また、精度(accuracy)を評価指標として指定します。
binary_crossentropyは2つのクラス(本物または偽物)の確率分布間の距離を測定するための損失関数です。GANでは一般的な選択肢です。
Adamは高速な収束を実現するため、GANのような大規模かつ複雑なモデルのトレーニングに適しています。逆に小さなデータセットでは過剰適合のリスクが高くなってしまう。MNISTは60000枚の画像なので、Adamが適していると言える。
accuracyは二値分類のモデルの性能を簡潔に評価するための良い指標です。識別器は、本物のサンプルと生成されたサンプルの2つのクラスを分類するタスクを行うため適していると言えます。

generator = generator_model(shape, noise_dim)
#Generator モデルを構築します。generator_model 関数は、画像の形状 shape と潜在空間の次元数 noise_dim を引数として受け取り、それに基づいて Generator モデルを構築します。

discriminator.trainable = False
trainable属性を使用して、特定のモデルまたはレイヤーのトレーニング可能なパラメータを無効にする操作です。識別器のすべてのトレーニング可能なパラメータ(重み)を「フリーズ」することを意味します。つまり、この行以降で行われる識別器へのバックプロパゲーションによる更新は、識別器の重みには反映されません。この操作により、生成器のみが更新され、識別器の重みは固定された状態でトレーニングが行われます。

gan = gan_model(generator, discriminator)
#Generator と Discriminator を組み合わせて GAN モデルを構築します。gan_model 関数は、Generator モデルと Discriminator モデルを引数として受け取り、それらを組み合わせて GAN モデルを構築します。

gan.compile(loss='binary_crossentropy', optimizer=Adam())
#GAN モデルをコンパイルします。損失関数として binary_crossentropy を指定し、最適化アルゴリズムとして Adam を使用します。

p.s.識別機はコンパイルするのに、生成器はコンパイルしない理由は、GANの学習プロセスにおいて、生成器の更新はGANモデル全体で行われるためです。

モデルの訓練

指定した反復回数だけ 生成器 と 識別器 を訓練し、一定の間隔でサンプル画像を生成して訓練の進行状況を表示していく準備をしましょう。

Python
losses=[]
accuracies=[]
iteration_checkpoints=[]

def train(iterations, batch_size,sample_interval):
    (X_train,_),(_,_)=mnist.load_data()

    X_train=X_train/127.5-1.0
    X_train=np.expand_dims(X_train,axis=3)

    real_label=np.ones((batch_size,1))

    fake_label=np.zeros((batch_size,1))
    for iteration in range(iterations):
        idx=np.random.randint(0,X_train.shape[0],batch_size)
        batch_images=X_train[idx]

        z=np.random.normal(0,1,(batch_size,noise_dim))
        gene_imgs=generator.predict(z)

        d_loss_real=discriminator.train_on_batch(batch_images,real_label)
        d_loss_fake=discriminator.train_on_batch(gene_imgs,fake_label)
        d_loss,accuracy=0.5*np.add(d_loss_real,d_loss_fake)

        z=np.random.normal(0,1,(batch_size,100))
        gene_imgs=generator.predict(z)

        g_loss=gan.train_on_batch(z,real_label)

        if(iteration+1)%sample_interval==0:

            losses.append((d_loss,g_loss))
            accuracies.append(100.0*accuracy)
            iteration_checkpoints.append(iteration+1)

            print("%d [D loss; %f, acc.; %.2f%%][G loss: %f]"%
                  (iteration+1,d_loss,100.0*accuracy,g_loss))
            
            save_images(generator, iterations)

losses=[], accuracies=[], iteration_checkpoints=[]
#損失と精度の履歴を保存するための空のリストを定義します。

train(iterations, batch_size, sample_interval)
iterations: 1つのイテレーションは、生成器と識別器の両方が1度ずつトレーニングされるサイクル
batch_size: バッチサイズ
sample_interval: 何イテレーションごとに損失と精度が記録されるかを示します。この間隔に基づいて、トレーニング中の進行状況が監視され、結果が記録されます。

(X_train,_),(_,_)=mnist.load_data()
# mnistデータセットからトレーニング用の画像データをロードします。mnist.load_data()は、トレーニング用とテスト用の画像データを返しますが、ここではトレーニング用のデータのみを利用しています。ロードされたデータはX_trainに格納されます。(_, _)は、ラベルデータは使用しないことを示します。

X_train=X_train/127.5-1.0
#画像のピクセル値を[-1, 1]の範囲に正規化します。確認してみると、255÷127.5-1.0=1になり0.000001(≒0)÷127.5-1=-0.99999~(≒-1)になりますよね。
#データセットの平均を0にすることで、モデルの学習を安定化させ、収束を高速化することが期待できます。

np.expand_dims(X_train, axis=3)
#画像データの次元を変更します。元々の画像データは(num_samples, width, height)の形状をしていますが、これを(num_samples, width, height, channels)の形状に変更します。ここでchannelsは画像のチャンネル数で、グレースケール画像の場合は1です。
#要するに画像データの次元を画像の情報を保持したまま拡張しています。
#ニューラルネットワークは画像を入力する際には、チャンネルの情報が必要です。しかし、画像データはチャンネル数を持ちません。そのため、次元を追加します。ニューラルネットワークは入力データの次元数からチャンネル数を判断するので、次元を増やせばチャンネルを表現できるのです。これは、グレースケール画像に限らず、RGB画像でも同じことです。

p.s. num_samplesは枚数を表しているよ!

例えば、
#2x2のグレースケール画像
[[[10, 20],
  [30, 40]],

これだとチャンネル数を表現できないので、

[[[[10],
   [20]],

  [[30],
   [40]]],
   
このように次元を増やすことが、ニューラルネットワークのチャンネル数の判断に使われるのです。


#RGBでも同様で3x3のRGB画像の場合は
[[[255, 0, 0], [0, 255, 0], [0, 0, 255]],
 [[255, 255, 0], [255, 0, 255], [0, 255, 255]],
 [[128, 128, 128], [0, 0, 0], [255, 255, 255]]]
 
 次元数を増やして、ニューラルネットワークにチャンネル数の判断材料を与えてあげる。
 
 [[[[255], [0], [0]], [[0], [255], [0]], [[0], [0], [255]]],
 [[[255], [255], [0]], [[255], [0], [255]], [[0], [255], [255]]],
 [[[128], [128], [128]], [[0], [0], [0]], [[255], [255], [255]]]]

real_label=np.ones((batch_size,1))
#指定されたサイズの要素が全て1の配列を生成しています。具体的には、realはバッチサイズ分の要素を持つ列ベクトルで、各要素が1です。
#識別器が本物の画像と偽物の画像を区別するために使用されます。ここで生成されたreal_labelは、識別器に対して本物の画像を表すためのラベルとして使用されます。
#要するに本物の画像に対するラベルを作成しています。本物の画像に対しては1を、偽の画像に対しては0を表すラベルをGANでは用います。

fake_label=np.zeros((batch_size,1))
#指定されたサイズの要素が全て0の配列を生成しています。具体的には、fake_labelはバッチサイズ分の要素を持つ列ベクトルで、各要素が0です。
#ここで生成されたfake_labelは、識別器に対して偽の画像を表すラベルとして使用されます。
#要するに偽の画像に対するラベルを作成しています。本物の画像に対しては1を、偽の画像に対しては0を表すラベルをGANでは用います。

for iteration in range(iterations):
#指定されたイテレーション数(学習の反復回数)の範囲でループを開始します。これにより、モデルは複数回の学習を行います。

idx=np.random.randint(0,X_train.shape[0],batch_size)
#ランダムにX_trainからバッチサイズ分のインデックスを選択します。
#np.random.randint(下限値、上限値、取得数)を表すから、0以上X_trainの枚数未満の中からbatch_sizeの数だけランダムにとりだす。
#要するにトレーニングデータからランダムにバッチを取得します。

batch_images=X_train[idx]
#上のコードで、選択されたインデックスを使用して、X_trainから対応する本物の画像のバッチを取得します。

z=np.random.normal(0,1,(batch_size,noise_dim))
#平均が0で標準偏差が1の正規分布(標準正規分布)からランダムな値を持つノイズベクトルを生成しています。生成されたノイズベクトルの形状は(batch_size, noise_dim)であり、batch_sizeは1つのバッチ内のノイズベクトルの数を表し、noise_dimは1つのノイズベクトルの次元数を表します。
標準正規分布は最も一般的で汎用的な確率分布の1つであり、さまざまなデータセットや問題に適用できます。また、パラメータが少なく単純な形をしており、扱いやすいです。そして、ランダムなノイズが標準正規分布に従うことで、生成される画像の多様性が確保されるという特性を持ちます。
#要するに正規分布からランダムなノイズを生成しています。で、このノイズは生成器に入力され、偽の画像を生成する際に使用されます。

gene_imgs=generator.predict(z)
#生成器にノイズベクトル z を入力として与え、生成器がその入力を元に偽の画像を生成する操作を行います。

d_loss_real=discriminator.train_on_batch(batch_images,real_label)
#識別器モデルを1バッチ分の実際の画像データ batch_images とそれに対応する正解ラベル real_label でトレーニングする操作を行います。
train_on_batchにおける第一引数は入力データのバッチです。Numpy配列またはリストの形式で指定します。第二引数は入力データに対応する目標(正解ラベル)のバッチです。Numpy配列またはリストの形式で指定します。通常、教師あり学習の場合に使用されます。
#要するに、識別器を使用して、本物の画像のバッチに対する損失と精度を計算しています。train_on_batchメソッドは、入力データ(batch_images)と正解ラベル(real_lbel)を使用して、1つのバッチに対する損失と精度を計算しています。そして、d_loss_realにはこのトレーニングの結果得られる損失が格納されます。

d_loss_fake=discriminator.train_on_batch(gene_imgs,fake_label)
#上のコードと同様の流れで、識別器を使用して、生成された偽の画像のバッチに対する損失と精度を計算します。そして、d_loss_fakeにはこのトレーニングの結果得られる損失が格納されます。

d_loss, accuracy=0.5*np.add(d_loss_real, d_loss_fake)
np.add(d_loss_real, d_loss_fake)は、本物の画像と偽の画像の損失を要素ごとに加算します。つまり、各バッチでの本物の画像と偽の画像に関する損失を合計します。
0.5 * np.add(d_loss_real, d_loss_fake)は、合計された損失を0.5倍しています。これは、本物の画像と偽の画像の損失を等しく重み付けするためです。GANのトレーニングでは、識別器の目標は本物の画像と偽の画像を区別することであり、両方のタスクが均等に重要であるため、損失を均等に重み付けすることが一般的です。
#要するに本物の画像と偽の画像の両方に対する損失と精度を組み合わせて、識別器の全体的な損失と精度を計算します。

discriminator.trainable = False
#生成器の訓練を行うため、識別器の訓練を一時停止します。これにより、生成器が訓練される間、識別器の重みが固定されます。
g_loss = gan.train_on_batch(z, real_label)

#生成器の訓練を行います。生成器は、生成された偽の画像を本物と見なすように学習します。
discriminator.trainable = True

#生成器の訓練が終了した後、識別器の訓練を再開します。これにより、識別器のパラメータが更新され、次のイテレーションで使用されます。

if(iteration+1)%sample_interval==0
#イテレーション数がsample_intervalの倍数の場合に、以下の処理を実行します。つまり、指定された間隔ごとにこのブロックの中の処理が実行されます。
#iterationは0から始まるので1を足しています。

losses.append((d_loss,g_loss))
#識別器と生成器の損失をlossesリストに追加します。これにより、各イテレーションでの損失の推移を記録することができます。
#このようにして、トレーニング中に損失を記録することで、モデルの収束やトレーニングの安定性を評価したり、後で損失の推移を可視化することができます。

accuracies.append(100.0*accuracy)

#識別器の精度をaccuraciesリストに追加します。この精度はパーセンテージで表され、識別器が正確に本物と偽物を区別できる割合を示します。

iteration_checkpoints.append(iteration+1)

#現在のイテレーション数をiteration_checkpointsリストに追加します。これにより、どのイテレーションで何が行われたかを追跡することができます。イテレーション数に1を加えているのは、イテレーション数が0から始まるためです。

print(“%d [D loss; %f, acc.; %.2f%%][G loss: %f]”%( iteration+ 1 ,d _ loss , 100.0* accuracy,g_loss))

#現在のイテレーション数、識別器の損失と精度、生成器の損失を表示します。これにより、コンソールに学習の進行状況が表示されます。
%d%fとかは文字列フォーマットです。
%d : 整数を埋め込むための指定子です。この場合、iteration + 1 の値が整数として挿入されます。
%f : 浮動小数点数を埋め込むための指定子です。この場合、d_lossaccuracyg_loss の値が浮動小数点数として挿入されます。
%.2f のように .2 のような小数点以下の桁数を指定することもできます。これにより、表示される浮動小数点数の桁数が制限できます。

save_images(generator, iterations+1)
#生成器を使用して、指定されたイテレーションでの生成された画像を保存します。これにより、生成された画像の品質を確認することができます。
#save_images関数は以下で定義します。


生成画像の保存

Generator を使用して生成された画像を指定されたサイズのグリッドに保存していきます。デフォルトでは、4×4 のグリッドに画像が配置されます。

Python
def save_images(generator, iteration, directory='gan_images', image_grid_rows=4, image_grid_columns=4):
    if not os.path.exists(directory):
        os.makedirs(directory)

    for i in range(iterations):
        z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, noise_dim))
        gene_imgs = generator.predict(z)
        gene_imgs = 0.5 * gene_imgs + 0.5
        fig, axs = plt.subplots(image_grid_rows, image_grid_columns, figsize=(4, 4), sharey=True, sharex=True)
        cnt = 0
        for row in range(image_grid_rows):
            for col in range(image_grid_columns):
                axs[row, col].imshow(gene_imgs[cnt, :, :, 0], cmap='gray')
                axs[row, col].axis('off')
                cnt += 1

        plt.savefig(f"{directory}/gan_generated_image_{i}.png")
        plt.close()

def save_images(generator, iteration, directory='gan_images', image_grid_rows=4, image_grid_columns=4):
generator は、画像を生成するために使用される Generator モデルです。
iterations は、保存される画像の数を決定します。各イテレーションごとに1つの画像が保存されます。
directory= は、画像が保存されるディレクトリのパスを指定します。デフォルトは ‘gan_images’ 。お好きな名前でどうぞ!
image_grid_rows は、保存される画像のグリッドの行数を決定します。デフォルトは 4 です。
image_grid_columns は、保存される画像のグリッドの列数を決定します。デフォルトは 4 です。

if not os.path.exists(directory):
 os.makedirs(directory)

#指定されたディレクトリが存在しない場合は、デフォルトで設定したディレクトリを作成します。このディレクトリは、生成された画像が保存される場所です。

z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, noise_dim))
#0:正規分布の平均を指定します。
1:正規分布の標準偏差を指定します。
#正規分布からランダムなノイズを生成します。これは、生成器に入力する際に、画像を生成する際のランダム性を与えます。

gene_imgs = generator.predict(z)
#生成器にノイズを入力し、画像を生成します。

gene_imgs = 0.5 * gene_imgs + 0.5
#生成された画像のピクセル値を[-1, 1]から[0, 1]に変換します。これにより、画像が正規化されます。
#以下で用いるmatplotlib の imshow 関数を使用する場合、ピクセル値が [0, 1] の範囲にあることが前提とされます。

fig, axs = plt.subplots(image_grid_rows, image_grid_columns, figsize=(4, 4), sharey=True, sharex=True)
#画像を表示するための図(figure)と軸(axes)を作成します。
plt.subplots() 関数は、指定された行数と列数のグリッド状の図を作成します。
image_grid_rows:グリッドの行数を指定します。これは画像のグリッドの縦のセル数を表します。
image_grid_columns:グリッドの列数を指定します。これは画像のグリッドの横のセル数を表します。
figsize=(4, 4):生成される図のサイズを指定します。ここでは、幅と高さがともに 4 インチの正方形に設定されています。
sharey=True:すべてのサブプロットが縦軸を共有するように設定します。つまり、すべてのサブプロットが同じ縦軸のスケールを持ちます。複数のサブプロット間でデータを比較したり、パターンを識別したりする場合に便利です。
sharex=True:すべてのサブプロットが横軸を共有するように設定します。あとは、shareyと同じ。
fig, axsplt.subplots() 関数は、作成された図とそれに含まれるすべてのサブプロットを返します。このコードでは、fig は図全体を表し、axs はすべてのサブプロットを含む配列です。
#要するにこの行の結果として、fig には指定されたサイズと行数・列数に基づいてグリッド状に配置されたサブプロットが含まれ、axs にはそれらのサブプロットへの参照が格納されます。これらのサブプロットは、後で画像を描画するために使用されます。


cnt=0
#表示する画像のカウンターを初期化します。
#この行は、サブプロットのインデックスを追跡するための変数 cnt を初期化しています。通常、画像のグリッドなどの複数のサブプロットを配置する場合、各サブプロットに対してループを行い、それぞれのサブプロットに対して特定の操作を行います。この変数 cnt は、そのようなループ内で各サブプロットのインデックスを追跡するために使用されます。各サブプロットのインデックスを cnt で表現することで、そのインデックスに基づいてサブプロットに対する操作を行うことができます。

for row in range(image_grid_rows):
 for col in range(image_grid_columns):

 axs[row, col].imshow(gene_imgs[cnt, :, :, 0], cmap='gray')
 axs[row, col].axis(‘off’)
 cnt += 1
#ループを使用して、生成された画像をグリッド状に表示します。imshowメソッドを使用して画像を表示し、axis('off')を使用して軸を非表示にします。
rowcol のインデックスを使用して、指定された行と列の位置にあるサブプロットに画像を表示するために imshow() 関数を呼び出しています。
#axs[row, col]axsplt.subplots() 関数で生成されたサブプロットの配列です。rowcol のインデックスを使用して、特定の行と列の位置にあるサブプロットにアクセスしています。
#.imshow(gene_imgs[cnt, :, :, 0], cmap='gray')imshow() 関数は、画像を表示するための Matplotlib の関数です。指定された画像をサブプロットに表示します。
#gene_imgs[cnt, :, :, 0]gene_imgs は生成された画像の行列です。cnt は、現在の画像のインデックスを表します。:, :, :, 0 は、RGB チャンネルがある場合でも、最初のチャンネル(グレースケール画像の場合は唯一のチャンネル)を取得します。
#cmap='gray':画像のカラーマップを指定します。'gray' は、グレースケールの画像を表示するためのカラーマップです。
#Matplotlib の axis() 関数は、軸の表示を制御するために使用されます。'off' を指定することで、軸を非表示にします。これにより、画像が表示されるだけで、周囲に軸が表示されなくなります。一般的に、画像を表示する際には軸を非表示にすることが多いため、この設定が使用されます。
#cnt += 1:サブプロット内で処理される画像のインデックスを更新するために、cnt 変数に 1 を加算しています。これにより、画像が正しい順序でグリッドに配置されることが保証されます。

fig.savefig(f”{directory}/iteration_{iteration}.png”)
#生成された図を指定されたディレクトリに保存します。イテレーション番号が含まれたファイル名で保存されます。
savefig() 関数は、現在の図を指定されたファイル名で保存します。

plt.close(fig)
#生成した図を閉じます。これにより、メモリの使用量が減少し、プログラムが効率的に実行されます。
close() 関数は、現在の図を閉じてメモリを解放します。図を閉じることで、プログラムがメモリを効率的に管理し、リソースの無駄な使用を防ぎます。→メモリリークを防ぐ

p.s.メモリリークとは使い終わった不要なメモリを開放しないことで、メモリの使用量が増加し続ける現象を指します。


モデルの実行

GANモデルは時間がかかります。自分の時間と相談しながら、パラメーターを設定してください。

p.s.以下の設定だと10時間以上はかかります。

iterations = 20000
batch_size = 128
sample_interval = 1000
train(iterations, batch_size, sample_interval)

iterations=20000
#訓練のイテレーション数を指定します。生成モデルと識別モデルの訓練プロセスが 20000 回の反復で行われることを示しています。

batch_size=256
#バッチサイズを指定します。一度にモデルに渡される訓練サンプルの数です。
#GPUを使用する場合はバッチサイズの選択肢は、32、64、128、256などの2の冪乗にするのが一般的です。CPUの場合はあまり関係ありません。(この実践はCPUでやっています。)

sample_interval=1000
#進捗を表示するために、途中経過を確認する頻度を示しています。何回のイテレーションごとに進捗を表示するかを示します。
#この場合、1000イテレーションごとに損失と精度を記録し、画像を保存します。

train(iterations,batch_size,sample_interval)
train関数を呼び出して、GANの訓練を開始します。指定された反復回数(iterations)、バッチサイズ(batch_size)、サンプル間隔(sample_interval)で訓練が行われます。

モデルの保存とロード方法

それでは、モデルを保存しましょう。

generator.save('gene_mod.keras')
discriminator.save('disc_mod.keras')
gan.save('gan_model.keras')

generator.save(‘gene_mod.keras’)
#訓練が完了した後、生成器のモデルをファイルに保存します。ここでは、gene_mod.kerasという名前で保存されます。
discriminator.save(‘disc_mod.keras’)

#訓練が完了した後、識別器のモデルをファイルに保存します。ここでは、disc_mod.kerasという名前で保存されます。
gan.save(‘gan_mode.keras’)

#訓練が完了した後、GANモデル全体をファイルに保存します。ここでは、gan_model_りおちゃん.kerasという名前で保存されます。

p.s.前回まではh5を使っていましたがKeras 形式を使用すると、モデルの保存とロードがより柔軟で効率的になることが判明したのでこうしました。
#HDF5 形式では、モデルのアーキテクチャ、重み、トレーニング設定などが単一のファイルに保存されます。


モデルのロード

モデルをロードする時はh5と同じ流れです。

  from keras.models import load_model
  
  # 保存されたモデルをロード
  loaded_model = load_model('generator_model.keras')

これで、学習したモデルを再利用できます。

おわりに

ふーっ。やっと終わりました。ちょっと長かったですね。

偽画像が生成されるのは感動したけど、思ったより精度は高くなかったですよね。

次回はこの課題を克服していきます。層を少しだけ変えてやるだけで精度を劇的に高められます。また、そこ以外は今回と同じコードを使用するので、労力は少ないです。今日頑張ったので次回は楽に実装していきましょう!

本物と見分けがつかない偽物をつくろう!

タイトルとURLをコピーしました