フェイク画像の生成

このページでは、連続時間拡散モデルを使って画像を生成する実験をしてみる(ここは、これから)

このページの制作にあたり、 「ろーる きゃべつ」さんの記事を参考にさせていただきました。 ただし、アルゴリズムやコードは異なります。

漢字のビットイメージデータの作成

漢字は象形文字から発展したという由来もあって、一種の図形として解釈したり操作できる余地が大きい。 ここでは、漢字を画像として拡散モデルに学習させ、「新しい」文字を生成する実験をすることにしよう。

それを行うには、まず、漢字のビットマップイメージデータが必要となる。 以下は、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

上記がうまくいかない場合は、無償で配布されている東雲フォントから作成した データファイル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ステップ目、時刻TNTステップ目に対応させる。

# 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)、より画数の大きな文字がサンプルされる。

(書きかけ)