フェイク画像の生成
このページでは、連続時間拡散モデルを使って画像を生成する実験をしてみる(ここは、これから)
このページの制作にあたり、 「ろーる きゃべつ」さんの記事を参考にさせていただきました。 ただし、アルゴリズムやコードは異なります。
漢字のビットイメージデータの作成
漢字は象形文字から発展したという由来もあって、一種の図形として解釈したり操作できる余地が大きい。 ここでは、漢字を画像として拡散モデルに学習させ、「新しい」文字を生成する実験をすることにしよう。
それを行うには、まず、漢字のビットマップイメージデータが必要となる。 以下は、Linuxにインストールされているアウトラインフォント(TrueType形式)を読み込んで、ビットマップデータに変換し、NumPyで読むことのできる形式のファイルとして保存するスクリプトの例である。
font_path = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
の箇所は、各自で使用しているOSで実際に使用している日本語フォントデータへのパス名に変更のこと。
これを実行すると、作業ディレクトリにkanji-32x32.npz
という名前のファイルが作成される。
from PIL import Image, ImageFont, ImageDraw from fontTools.ttLib import TTFont import numpy as np def generate_kanji_string(font_path): font = TTFont(font_path) cmap = font.getBestCmap() kanji_string = '' codes = [] for code, glyph in sorted(cmap.items()): if (code >= 0x3402 and code <= 0x9FA2) or (code >= 0xF91D and code <= 0xFA6A) or (code >= 0x2000B and code <= 0x2F8ED): character = chr(code) kanji_string += character codes.append(code) # print(f"U+{code:04X}: {glyph} ({character})") return kanji_string, codes def create_bitmaps_from_font(font_path, characters, size): font = ImageFont.truetype(font_path, size) bitmaps = [] for char in characters: if not font.getmask(char).getbbox(): print(f'Skipping undefined character: {char}') continue image = Image.new('1', (size,size), 0) draw = ImageDraw.Draw(image) draw.text((0, 0), char, font=font, fill=1) bitmap_array = np.array(image) bitmaps.append(bitmap_array) return np.array(bitmaps) font_path = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf' characters, codes = generate_kanji_string(font_path) # print(characters) size = 32 bitmaps = create_bitmaps_from_font(font_path, characters, size) # print(bitmaps.shape) codes = np.array(codes) # print(codes.shape) np.savez('kanji-32x32.npz', bitmaps=bitmaps, codes=codes)
上記がうまくいかない場合は、無償で配布されている東雲フォントから作成した データファイルshinonome-32x32.npzを用意しておいたので、ダウンロードして使うこと。
データの読み込みとスコア関数の学習
漢字のビットマップデータを読み込んで、ディープニューラルネットでスコア関数を学習するコードを、以下のように構成する。
フォントデータの読み込み
漢字のビットマップデータをNumPyの配列に読み込む:
data = np.load('kanji-32x32.npz') images = data['bitmaps'] unicodes = data['codes']
東雲フォントのデータをダウンロードした場合は、代わりに以下の4行を加える
data = np.load('shinonome-32x32.npz') images = data['bitmaps'] jiscodes = data['codes'] images = images[jiscodes >= 0x3021] # choose KANJI only
画像データの前処理
データを、チャネル数1のグレイスケール画像として処理するための前処理を行う。
画像データ配列名をx0s
と置き直す。
images = images.astype("float32") images = np.expand_dims(images, axis=-1) print("images shape:", images.shape) NSAMPLE= images.shape[0] print(NSAMPLE, "train samples") print("mean_val=",np.mean(images)) x0s = images
パラメータの設定
連続時間の拡散モデルのパラメータを設定する。パラメータの意味は前ページを参照のこと。
T
は拡散の終了時間、DT
はステップ幅である。
R=1.0 B=1.0 DT=1.0/128 T=1.0 R2=math.sqrt(R) D=R/2
時間ステップの設定
拡散の時間ステップをあらかじめ配列 time_course
に設定しておく。
時刻0を0ステップ目、時刻T
をNT
ステップ目に対応させる。
# define time steps time_course = [] t=0 while t<T: time_course.append(t) t += DT time_course.append(t) NT = len(time_course)-1 time_course = np.array(time_course)
時間情報の埋め込み
画像と共に、時間情報を埋め込むため、時間を32x32ピクセルのグレースケール画像に変換する関数を定義しておく。
# 0<=tim<=1 def time_encode(tim): a = np.empty((32,32)) for i in range(32): p = (math.sin(math.pi*i*tim)+1)/2 for j in range(32): q = (math.cos(math.pi*j*tim)+1)/2 a[i,j] = p*q return a
スコアの学習モデルの定義
漢字のグレースケール画像と時刻を埋め込んだ画像(32 x 32ピクセル、チェネル数2)を入力に、画素毎のスコア関数値(shape=(32,32,1))を出力とするニューラルネットのモデルを定義する。 ここで用いるのは、画像用の深層学習モデルとして使われ、U-Net と呼ばれているモデルである。
# # MODEL: simple U-Net # input_img = Input(shape=(32, 32, 2)) (以下、省略)
モデルのトレーニング
計算時間を節約するため、それぞれの画像x0s[i]
について、拡散時間(ステップ)k
をランダムに選びながら、トレーニング用データを生成する。
i
番目の画像について、ランダムに選んだステップ数k
まで順方向に拡散させた画像を、
前のセクションで述べた(5)式に基づき、正規乱数で生成する。
そのときの$\frac{\partial \log p}{\partial x}$(の分子に相当する値)を求め、教師データとして配列dlogp_train
にセットする。
また、拡散された画像に加え、拡散時間をエンコードした画像を合わせて、入力データとする。
こうしてモデルのフィッティングを行いつつ、全てのサンプル画像について、上記の計算を10回繰り返す。
# # training score # n_train=0 while n_train<10: print("train#",n_train) xt = np.empty((NSAMPLE,32,32,1)) x_train = np.empty((NSAMPLE,32,32,2)) dlogp_train = np.empty((NSAMPLE,32,32,1)) for i in range(NSAMPLE): k = 1+np.random.randint(NT-1) t = time_course[k] std = math.sqrt( (D*(1-math.exp(-2*B*t)))/B ) mean = x0s[i]*math.exp(-B*t) xt[i] = np.random.normal(mean,std,size=(32,32,1)) dlogp_train[i] = - (xt[i] - mean) x_train[i,:,:,:1] = xt[i] x_train[i,:,:,1] = time_encode(t) nepoch=5 model.fit(x_train, dlogp_train, epochs=nepoch, verbose=1) n_train += 1
トレーニングデータを生成する際に、O-U過程から得られる $$ \frac{\partial \log p(x,\tau)}{\partial x} = -\frac{B (x - x_0 e^{-B (T-\tau)})}{D \left( 1 - e^{-2 B (T-\tau)}\right)} $$ をそのまま適用すると、$T-\tau$が小さなときに値が非常に大きくなって、データの均斉が破れてしまう。 そのため、ここの計算例では、分子の部分のみを学習させ、逆拡散過程で、分母の部分を補うようにした。
モデルと荷重の保存
# # save the trained model # model.save("model-kanji.h5")
コードの全体は以下のとおりである:
import numpy as np import matplotlib.pyplot as plt import random import math import tensorflow as tf from keras.models import Model from keras.layers import Input,Reshape,Flatten, Dense, Activation, BatchNormalization, Conv2D, Conv2DTranspose, MaxPooling2D, concatenate from keras import optimizers import sys data = np.load('kanji-32x32.npz') images = data['bitmaps'] unicodes = data['codes'] ## 東雲フォントのデータを使う場合は下記の4行のコメントを外す # data = np.load('shinonome-32x32.npz') # images = data['bitmaps'] # jiscodes = data['codes'] # images = images[jiscodes >= 0x3021] # choose KANJI only # setup data images = images.astype("float32") images = np.expand_dims(images, axis=-1) print("images shape:", images.shape) NSAMPLE= images.shape[0] print(NSAMPLE, "train samples") print("mean_val=",np.mean(images)) x0s = images # parameters R=1.0 B=1.0 DT=1.0/128 T=1.0 R2=math.sqrt(R) D=R/2 # define time steps time_course = [] t=0 while t<T: time_course.append(t) t += DT time_course.append(t) NT = len(time_course)-1 time_course = np.array(time_course) # 0<=tim<=1 def time_encode(tim): a = np.empty((32,32)) for i in range(32): p = (math.sin(math.pi*i*tim)+1)/2 for j in range(32): q = (math.cos(math.pi*j*tim)+1)/2 a[i,j] = p*q return a # # MODEL: simple U-Net # input_img = Input(shape=(32, 32, 2)) # encoder conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img) conv1 = BatchNormalization()(conv1) conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(conv1) conv1 = BatchNormalization()(conv1) pool1 = MaxPooling2D(pool_size=(2, 2))(conv1) conv2 = Conv2D(128, (7, 7), activation='relu', padding='same')(pool1) conv2 = BatchNormalization()(conv2) conv2 = Conv2D(128, (7, 7), activation='relu', padding='same')(conv2) conv2 = BatchNormalization()(conv2) pool2 = MaxPooling2D(pool_size=(2, 2))(conv2) # decoder up1 = Conv2DTranspose(128, (7, 7), strides=(2, 2), activation='relu', padding='same')(pool2) up1 = BatchNormalization()(up1) merge1 = concatenate([up1, conv2], axis=3) conv3 = Conv2D(128, (7, 7), activation='relu', padding='same')(merge1) conv3 = BatchNormalization()(conv3) up2 = Conv2DTranspose(32, (3, 3), strides=(2, 2), activation='relu', padding='same')(conv3) up2 = BatchNormalization()(up2) merge2 = concatenate([up2, conv1], axis=3) conv4 = Conv2D(32, (3, 3), activation='relu', padding='same')(merge2) conv4 = BatchNormalization()(conv4) # output output = Conv2D(1, (1, 1), activation='linear', padding='same')(conv4) model = Model(inputs=input_img, outputs=output) model.summary() model.compile(loss='mean_squared_error', optimizer='Adam') # # training score # n_train=0 while n_train<10: print("train#",n_train) xt = np.empty((NSAMPLE,32,32,1)) x_train = np.empty((NSAMPLE,32,32,2)) dlogp_train = np.empty((NSAMPLE,32,32,1)) for i in range(NSAMPLE): k = 1+np.random.randint(NT-1) t = time_course[k] std = math.sqrt( (D*(1-math.exp(-2*B*t)))/B ) mean = x0s[i]*math.exp(-B*t) xt[i] = np.random.normal(mean,std,size=(32,32,1)) dlogp_train[i] = - (xt[i] - mean) x_train[i,:,:,:1] = xt[i] x_train[i,:,:,1] = time_encode(t) nepoch=5 model.fit(x_train, dlogp_train, epochs=nepoch, verbose=1) n_train += 1 # # save the trained model # model.save("model-kanji.h5")
学習済みモデル:model-kanji.h5
作業ディレクトリに漢字のイメージデータ(kanji-32x32.npz または shinonome-32x32.npz)が保存されている状態でこのコードを実行すると、
ファイル model-kanji.h5
が作成される。
終了までにはそれなりの時間を要する。
画像の生成
学習済みのモデルと荷重を用いて、スコアを推定しながら、逆向きに拡散させることで、「漢字のような」画像を生成することができる。
以下は、ガウス分布に従う乱数で生成した画像から出発して、逆方向に拡散プロセスを進行させ、得られた画像を100枚表示するコードの例である。
import numpy as np import matplotlib.pyplot as plt import random import math import tensorflow as tf from tensorflow.keras.models import load_model import sys limit_vals = lambda arr: np.where(arr <= 0, 0, np.where(arr >= 1, 1, arr)) R=1.0 B=1.0 DT=1.0/128 T=1.0 R2=math.sqrt(R) D=R/2 # define time steps time_course = [] t=0 while t<T: time_course.append(t) t += DT time_course.append(t) NT = len(time_course)-1 time_course = np.array(time_course) print(NT) # 0<=tim<=1 def time_encode(tim): a = np.empty((32,32)) for i in range(32): p = (math.sin(math.pi*i*tim)+1)/2 for j in range(32): q = (math.cos(math.pi*j*tim)+1)/2 a[i,j] = p*q return a model = load_model('model-kanji.h5') model.summary() NGEN=100 s2 = (1/2)*(1-math.exp(-2*B*1)) ys = np.random.normal(0.12, math.sqrt(s2),size=(NGEN,32,32,1)) k = NT-1 while k>0: t = time_course[k] yp = np.empty((NGEN,32,32,2)) yp[:,:,:,:1] = ys yp[:,:,:,1] = time_encode(t) score = model.predict(yp, verbose=0) * B/(D*(1-math.exp(-2*B*t))) dt = time_course[k] - time_course[k-1] for i in range(NGEN): # dy = (B*ys[i] + R*score[i])*dt + R2*np.random.normal(0,1,size=(32,32,1))*math.sqrt(dt) dy = (B*ys[i] + 0.5*R*score[i])*dt ys[i] = ys[i] + dy k = k - 1 figs = plt.figure() for i in range(10*10): figs.add_subplot(10,10,i+1) plt.imshow(limit_vals(ys[i])) plt.savefig('gen-images.png') plt.show()
以下は実行結果の例である。
このコードで、初期条件は
NGEN=100 s2 = (1/2)*(1-math.exp(-2*B*1)) ys = np.random.normal(0.12, math.sqrt(s2),size=(NGEN,32,32,1))
の箇所で生成しているが、平均(ここでは0.12としている)と分散(変数s2
)を調整することで、最終的な結果が大きく異なる。
例えば、平均値を大きくすると(例えば 0.3)、より画数の大きな文字がサンプルされる。
(書きかけ)