対象読者
前回は40×40のカラー画像を生成しました。しかし、今回は144×144の少し大き目な画像を生成していきましょう!あいかわらず、低品質ですが流れの大枠は正しいはずです。
p.s.もはや抽象画の域ですね。これ(笑)。まぁでも、流れは掴んだんでよしとしましょう!
今回の全コード
かなり、以前までのコードを多く使っているので、かなり解説は割愛させていただきます。というか、だんだん慣れてきましたよね。
import matplotlib.pyplot as plt
import numpy as np
from keras.datasets import mnist
import os
from keras.layers import Dense, Flatten, Reshape, LeakyReLU, Dropout
from keras.models import Sequential
from keras.optimizers import Adam
from keras.layers import BatchNormalization, Activation
from keras.layers import Activation, BatchNormalization
from keras.layers import Conv2D, Conv2DTranspose
import cv2
width = 144
height = 144
channels = 1
shape = (width, height, channels)
noise_dim = 100
def generator_model(noise_dim):
model = Sequential()
model.add(Dense(256 * 18 * 18, input_dim=noise_dim))
model.add(Reshape((18, 18, 256)))
model.add(Conv2DTranspose(128, kernel_size=3, strides=2, padding='same'))
model.add(LeakyReLU(alpha=0.01))
model.add(Conv2DTranspose(64, kernel_size=3, strides=1, padding='same'))
model.add(LeakyReLU(alpha=0.01))
model.add(Conv2DTranspose(32, kernel_size=3, strides=2, padding='same'))
model.add(LeakyReLU(alpha=0.01))
model.add(Conv2DTranspose(1, kernel_size=3, strides=2, padding='same'))
model.add(Activation('tanh'))
return model
def discriminator_model(shape):
model = Sequential()
model.add(Conv2D(32, kernel_size=3, strides=2, input_shape=shape, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Conv2D(64, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(128, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(256, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(512, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(1024, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Flatten())
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(lr=0.0001, beta_1=0.5),#0.0001→0.0002,
metrics=['accuracy'])
generator = generator_model(noise_dim)
generator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0007, beta_1=0.5))#0.0003→0.0004
discriminator.trainable = False
gan = gan_model(generator, discriminator)
gan.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0007, beta_1=0.5))
losses = []
accuracies = []
iteration_checkpoints = []
def train(iterations, batch_size, sample_interval):
X_train = load_images("./gray_144_face")
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, noise_dim))
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, iteration + 1)
def save_images(generator, iteration, directory='144x144x_face_gray_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))
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)
def load_images(directory):#グレースケール用に変換する
images = []
for filename in os.listdir(directory):
img = cv2.imread(os.path.join(directory, filename))
if img is not None:
img = cv2.resize(img, (width, height))
if len(img.shape) == 3: # チャンネルが3の場合はグレースケールに変換
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
images.append(img)
else:
print(f"Warning: Failed to load image {filename}")
if len(images) == 0:
print("Error: No images loaded")
return None
else:
return np.array(images)
iterations = 20000
batch_size = 128
sample_interval = 1
train(iterations, batch_size, sample_interval)
generator.save('Face-gene.keras')
discriminator.save('Face-dis.keras')
gan.save('Face-gan.keras')
カラー画像の収集+前処理
では、早速やっていきましょう。まずは画像収集と前処理です。具体的なコードはこちらでやってみてください。↓
ここのタスクで私がやったことをまとめます。
- 独断と偏見から綺麗だと思う女性画像を100枚ずつ計400枚集める
(有村架純・長澤まさみ・橋本環奈・パクジヒョ)
集めれるならもっと多い方がいいです。 - 画像サイズを144×144に変更する
- ファイル名を変更する
この3ステップをここではやりました。また、先ほど提供した記事でこれらすべてのプログラムコードをのせているので、誰でも簡単にできますよ。
モジュール準備
import matplotlib.pyplot as plt
import numpy as np
from keras.layers import Dense, Flatten,Reshape, LeakyReLU
from keras.models import Sequential
from keras.optimizers import Adam
import os
from keras.layers import Activation,Dropout,BatchNormalization
from keras.layers import Conv2D
from keras.layers import UpSampling2D
import cv2
#上記↑は『衝撃簡単7』と同じ
from keras.layers import Conv2DTranspose
from keras.layers import Conv2DTranspose
#Conv2DTransposeは畳み込みの逆操作を行い、入力データをより大きな空間に逆畳み込みするために使用される。
#通常の畳み込み層は、入力としてデータを受け取り、それをフィルター(カーネル)と畳み込んで、出力を生成します。しかし、Conv2DTranspose
は逆の操作を行います。つまり、入力を受け取り、その情報を使用してより大きな出力を生成します。
前回は畳み込み層としてUpsampling2Dを使いました。では、ここで一旦、Conv2DTranspose
とUpsampling2Dの違いをまとめておきましょう。
Conv2DTranspose
:
- 畳み込みの逆操作を行う
- 入力をより大きな空間に逆畳み込みする
- フィルターを使用して入力に重みを適用し、出力を生成する
- フィルターサイズやストライドなどのパラメータを調整することで、出力のサイズや形状を制御することができる
UpSampling2D
:
- 入力データのサイズを拡大する
- 重みはもたない
- 単純に入力の各ピクセルの間に新しいピクセルを挿入し、それらの値を元のピクセルの値と同じにする
- 補間法(nearest neighborやbilinearなど)を使用して、新しいピクセルの値を計算する
- 主なパラメータは拡大する倍率のみ。通常、2の累乗倍率を使用する
要するに、画像生成などのタスクではConv2DTranspose
が使用され、データの拡大だけが必要な場合はUpSampling2D
が選択される。
生成画像の型を定義
生成する画像の型を定義します。幅と高さを自分の生成したい大きさに調整します。また、チャンネルはグレースケールなので1にします。
width = 144
height = 144
channels = 1
shape = (width, height, channels)
noise_dim = 100
生成器(ジェネレーター)モデルを定義
def generator_model(noise_dim):
model = Sequential()
model.add(Dense(256 * 18 * 18, input_dim=noise_dim))
model.add(Reshape((18, 18, 256)))
model.add(Conv2DTranspose(128, kernel_size=3, strides=2, padding='same'))
model.add(LeakyReLU(alpha=0.01))
model.add(Conv2DTranspose(64, kernel_size=3, strides=1, padding='same'))
model.add(LeakyReLU(alpha=0.01))
model.add(Conv2DTranspose(32, kernel_size=3, strides=2, padding='same'))
model.add(LeakyReLU(alpha=0.01))
model.add(Conv2DTranspose(1, kernel_size=3, strides=2, padding='same'))
model.add(Activation('tanh'))
return model
model.add(Dense(256 * 18 * 18, input_dim=noise_dim))
#18はConv2DTransposeの数、ストライドから導く。
#ストライド2が3あるから、18x2x2x2=144(画像サイズ)となるようにする。
model.add(Conv2DTranspose(128, kernel_size=3, strides=2, padding=’same’))
#128個のフィルターを使用し、カーネルサイズが3×3で、ストライドが2の畳み込みの逆操作を行う。これにより、18×18の入力が36×36に拡大される。
#ストライド:畳み込み演算においてフィルター(カーネル)が入力データをどのくらいのステップで移動するかを制御するパラメータ。
#例:ストライドが1の場合→フィルターは1つのピクセルごとに移動→出力画像は入力と同じサイズ
#例:ストライドが2の場合→フィルターは2つのピクセルごとに移動→出力は入力の2倍になる
このようにして、畳み込みの逆操作を行い、より大きな出力を生成することができる。
model.add(Conv2DTranspose(64, kernel_size=3, strides=1, padding=’same’))
#stridesを1にしているため、画像サイズはキープされる。
*以降の解説は前回までで沢山しているので割愛します。
識別器(ディスクリミネーター)を定義
def discriminator_model(shape):
model = Sequential()
model.add(Conv2D(32, kernel_size=3, strides=2, input_shape=shape, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Conv2D(64, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(128, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(256, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(512, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(1024, kernel_size=3, strides=2, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
return model
このdiscriminatorの調整には理論はありません。
『実行しては修正して』を繰り返したら、こうなりました。これが正解ではないので、皆さんも増やしたり、減らしたり、カスタマイズして最適解を導いてみてください。
p.s.以下に私の経験則や一般論を置いておきます。微力ですがカスタマイズで困ったらどうぞ。
DCGANモデルの定義とコンパイル
生成器と識別器を結合してDCGANモデルを作成しましょう!
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(lr=0.0001, beta_1=0.5),metrics=['accuracy'])
generator = generator_model(noise_dim)
generator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0007, beta_1=0.5))
discriminator.trainable = False
gan = gan_model(generator, discriminator)
gan.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0007, beta_1=0.5))
こちらも、トライアンドエラーの繰り返しです。強いて言うなら、1つずつ変えてみてください。複数を同時に変更すると、何が影響を与えたか判断できなくなってしまうので。
モデルの訓練
losses = []
accuracies = []
iteration_checkpoints = []
def train(iterations, batch_size, sample_interval):
X_train = load_images("./gray_144_face")
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, noise_dim))
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, iteration + 1)
idx_real = np.random.randint(0, X_train.shape[0] , batch_size)
#batch_size
の数だけデータをランダムに抽出する
#np.random.randint(a, b, size)
関数は、a
以上 b
未満の範囲からランダムに整数を size
個生成します。z = np.random.normal(0, 1, (batch_size, noise_dim))
#引数として、平均(0)、標準偏差(1)、および形状を指定しています。ここで、batch_size
は生成される行列の行数(サンプル数)、noise_dim
は列数(ノイズの次元数)を示しています。
d_loss_real = discriminator.train_on_batch(batch_images_real, real_label)
d_loss_fake = discriminator.train_on_batch(gene_imgs, fake_label)
#Discriminatorを訓練。本物の画像と偽物の画像をそれぞれ使用して損失を計算。
#train_on_batch
メソッドは、バッチで訓練するために使用されます。このメソッドは、入力データと対応する正解ラベル(または目標値)を受け取り、モデルの重みを更新します。以下に、このメソッドの引数と使い方を説明します。
g_loss = gan.train_on_batch(z, real_label)
#GANモデルを訓練。GANは、生成された画像を本物と誤認させるように学習します。
#real_label
は、生成器(Generator)の訓練時に使用される目標値。目標値として本物の画像に対応するラベルが与えられます。
*他は何回も解説していることなので、割愛します。
画像ロードの関数を定義する
今回は独自の画像データを使うので、モデルに画像を読み込ますためのロード関数が必要になります。ここでは、それを定義していきましょう!
def load_images(directory):#グレースケール用に変換する
images = []
for filename in os.listdir(directory):
img = cv2.imread(os.path.join(directory, filename))
if img is not None:
img = cv2.resize(img, (width, height))
if len(img.shape) == 3: # チャンネルが3の場合はグレースケールに変換
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
images.append(img)
else:
print(f"Warning: Failed to load image {filename}")
if len(images) == 0:
print("Error: No images loaded")
return None
else:
return np.array(images)
これでモデルに独自の画像データを読み込むことができます。
生成画像の保存
画像を保存して目視できるようにしましょう!
def save_images(generator, iteration, directory='144x144x_face_gray_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))
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)
引数のdirectory=を変えないと、以前の保存画像に上書きされてしまうので、これは変えておきましょう!
また、12行目のaxs[row, col].imshow(gene_imgs[cnt, :, :, 0], cmap=’gray’)はグレースケール用に変えています。
p.s.今まで、使ってきたメソッドなので細かい解説は割愛します。
モデルの実行&保存
バッチサイズを変えると、学習に影響が出ます。それ以外のパラメーターはお好みでどうぞ!
iterations = 20000
batch_size = 128
sample_interval = 100
train(iterations, batch_size, sample_interval)
generator.save('144Face-gene.keras')
discriminator.save('144Face-dis.keras')
gan.save('144Face-gan.keras')
ファイルの実行
では、実行していきましょう。実行方法は人それぞれですが、私の場合はCMDでファイル名を入力することで実行しています。
ファイル名.py
これで、いい感じの出力がされたら完了です!
学習モデルで1枚ずつ確認してみよう!
今までは、4×4で画像を確認しましたが、サイズを大きく確認したいので1枚で出力されるようにしましょう。
import numpy as np
import matplotlib.pyplot as plt
from keras.models import load_model
import os
noise_dim=100
def generate_and_save_images(generator, noise_dim, save_directory='generated_images', num_images=10):
if not os.path.exists(save_directory):
os.makedirs(save_directory)
for i in range(num_images):
noise = np.random.normal(0, 1, (1, noise_dim))
generated_image = generator.predict(noise)
generated_image = 0.5 * generated_image + 0.5 # 画像のスケーリングを元に戻す
generated_image = np.squeeze(generated_image) # 不要な次元を削除
plt.imshow(generated_image, cmap='gray')
plt.axis('off')
plt.savefig(f"{save_directory}/generated_image_{i+1}.png")
plt.close()
# 生成器モデルをロードする
generator = load_model('144Face-gene.keras')
# 10枚の画像を生成して保存する
generate_and_save_images(generator, noise_dim)
これで、大きなサイズで画像を確認できるようになりました。
おわりに
今回は大きなサイズの生成方法を紹介しました。もっと、質を高めたい方は、層を複雑にしたりパラメーターの値をカスタマイズしてみてください。