PytorchでGAN(敵対的生成ネットワーク)を使用し、アニメキャラクターを生成

2020年08月13日

データサイエンス領域では最近、紙面やブログ、動画などでAIが生成した人物の画像を目にすることが多くなりました。著者は、GAN(敵対的生成ネットワーク)が今後、ゲームAIや特殊効果の作成方法を変えていくだろうと考えています。この手法を用いれば、必要に応じて本物のようなテクスチャーやキャラクターを生成できるからです。

そこで、本記事では、AIの画像生成で利用されるGANについて見ていきましょう。PyTorchを使って同じようなアプリケーションを独自に作成、構築する方法をご説明いたします。GANの仕組みや独自モデルの作成方法について理解を深める上でお役に立てれば幸いです。

 

タスク概観

本記事では、Anime Face Datasetを利用して、独自のアニメキャラクターを作成します。Anime Face Datasetは、様々なスタイルの高品質のアニメの顔画像が63,632件含まれているので、今回の目標に適切のスターターデータセットです。

今回のプロジェクトでは、Deep Convolutional Generative Adversarial Networks(DCGAN)を利用します。今回は新しいアニメキャラクターの顔を生成するために利用しますが、DCGANは、現代的なファッションスタイルの作成や一般的なコンテンツ作成、さらに場合によってはデータ拡張にも利用できます。

では、コーディングに入る前に、GANの仕組みについて簡単に見ていきましょう。

 

偽画像を生成するGANの仕組み

GAN(敵対的生成ネットワーク)は通常、二つの競合するニューラルネットワークを使ってコンピューターにデータセットの性質を学習させ、本物のような偽画像を生成します。これらのニューラルネットワークの一つは偽画像を生成し(生成器: Generator)、もうひとつはどの画像が偽画像なのか識別しようとします(識別器: Discriminator)。二つのネットワークが互いに競争することで次第にネットワークが改良されていきます。

例えば、生成器を強盗、識別器を警察官と捉えるとよいでしょう。強盗は盗みを働くにつれ、盗みがうまくなりますが、同時に警察官も強盗を逮捕するのがうまくなります(少なくとも理想的な世界ではそう考えられます)

これらのニューラルネットワークの損失(loss)は主に、他方のネットワークのパフォーマンスの関数になります。

  • 識別器の損失は、生成器の品質の関数です。生成器の偽画像に騙されれば、識別器の損失は大きくなります。
  • 生成器の損失は、識別器の品質の関数です。識別器を騙せなければ、生成器の損失は大きくなります。

トレーニング段階では、識別器と生成器を順番に学習させ、両方のネットワークのパフォーマンスを向上させるつもりです。最終的な目標は、生成器がリアルな画像を作成するのに役立つ重みを見つけることです。最後に、この生成器を使って、ランダムなノイズから高品質の偽画像を生成してみましょう。 

 

生成器のアーキテクチャ

GANで作業する際に直面する主な問題の一つは、学習が安定しないことです。そこで、問題を解くことができ、しかも安定して学習できる生成器のアーキテクチャを構築する必要があります。以下の図は、DCGAN生成器のアーキテクチャを説明した『GAN(敵対的生成ネットワーク)(Deep Convolutional Generative Adversarial Networksを利用した教師なし表現学習)』という論文から引用したものです。

少々わかりにくいかもしれませんが、基本的に生成器とは、正規分布から数値を生成した100次元のベクトルを入力として、画像を出力するブラックボックスと考えることができます。

では、このようなアーキテクチャをどのように作成すればよいのでしょうか。以下の図でおわかりのように、まず、サイズ4x4x1024の密な層(Dense layer)を使用して、100次元のベクトルから密ベクトルに変換します。その後、1024個のフィルタを持つ4×4の画像形状の密ベクトルに再構成します。

ネットワーク自体がトレーニング中に重みを学習するので、現時点では重みについて心配する必要はありません。

1024 4×4マップを取得したら、一連の転置畳み込みを用いてアップサンプリングを行います。アップサンプリングの操作を行うたびに画像サイズが二倍になり、マップの数が半分になります。ただし、最後の段階では、マップの数を半分にしません。出力画像には三つのチャネルが必要なので、各RGBのチャネル用にマップの数を三つに減らします。 

 

転置畳み込みとは

簡単に言うと、転置畳み込みは、画像をアップサンプリングする方法を提供します。畳み込みの操作では、4×4の画像を2×2の画像に変換しますが、転置畳み込みの場合、以下の図に示すように、2×2の画像から4×4の画像に畳み込みます。

畳み込みニューラルネットワーク(CNN)では、入力の特徴マップのアップサンプリングでよくアンプーリングが使用されることをご存知の方もいるでしょう。では、ここでは、なぜアンプーリングを利用しないのでしょうか。

その理由は、アンプーリングには学習が含まれないからです。それに対して、転置畳み込みは学習できるので、こちらの方がおすすめです。記事の後半で、生成器がパラメータを学習する仕組みについて触れるつもりです。

 

識別器のアーキテクチャ

生成器のアーキテクチャについてご説明したので、次は識別器をブラックボックスとして見てみましょう。実際のところ、識別器は、偽画像かどうかを予測するための密な層を最後に持つ一連の畳み込み層から構築されています。以下の図をご覧ください。

全ての画像畳み込みニューラルネットワークは、画像を入力として受け取り、一連の畳み込み層を利用して偽画像かどうか予測することによって機能します。

 

データ前処理と視覚化

GANのトレーニングを進める前に、64x64x3の標準サイズにデータを前処理します。画像ピクセルを正規化する必要もあります。以下のコードで手順をご確認ください。わかりやすくするために、コメントをつけておきました。

# Root directory for dataset
dataroot = "anime_images/"
# Number of workers for dataloader
workers = 2
# Batch size during training
batch_size = 128
# Spatial size of training images. All images will be resized to this size using a transformer.
image_size = 64
# Number of channels in the training images. For color images this is 3
nc = 3
# We can use an image folder dataset the way we have it setup.
# Create the dataset
dataset = datasets.ImageFolder(root=dataroot,
                           transform=transforms.Compose([
                               transforms.Resize(image_size),
                               transforms.CenterCrop(image_size),
                               transforms.ToTensor(),
                               transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
                           ]))
# Create the dataloader
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
                                         shuffle=True, num_workers=workers)
# Decide which device we want to run on
device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")
# Plot some training images
real_batch = next(iter(dataloader))
plt.figure(figsize=(8,8))
plt.axis("off")
plt.title("Training Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=2, normalize=True).cpu(),(1,2,0)))

 

このコードによる出力は次の通りです。

 

DCGANの実装

次にDCGANを定義しましょう。このセクションでは、ノイズ生成関数、生成器アーキテクチャ、識別器アーキテクチャを定義します。

 

生成器用ノイズベクトルの生成

正規分布を利用して、以下に示すように生成器アーキテクチャを用いて画像に変換するため、ノイズを生成します。

 

nz = 100
noise = torch.randn(64, nz, 1, 1, device=device)

 

生成器のアーキテクチャ

生成器はGANの最も重要な部分です。ここでは、ノイズベクトルを画像にアップサンプリングするために、いくつか転置畳み込み層を加えて生成器を作成します。この生成器のアーキテクチャは上記リンクのDCGAN論文で提供されているものと同じではありません。 

データにより適したものにするため、アーキテクチャにいくつか変更を加える必要があったからです。つまり、生成器の中間に畳み込み層を加え、密な層を全て取り除いて完全に畳み込みできるようにしました。

また、多数のBatchnorm層や活性化関数leaky ReLUを使用しています。以下のコードブロックは、生成器を作成するために使用する関数を記載しています。 

 

# Size of feature maps in generator
ngf = 64
# Number of channels in the training images. For color images this is 3
nc = 3
# Size of z latent vector (i.e. size of generator input noise)
nz = 100

class Generator(nn.Module):
    def __init__(self, ngpu):
        super(Generator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # input is noise, going into a convolution
            # Transpose 2D conv layer 1.
            nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # Resulting state size - (ngf*8) x 4 x 4 i.e. if ngf= 64 the size is 512 maps of 4x4

            # Transpose 2D conv layer 2.
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # Resulting state size -(ngf*4) x 8 x 8 i.e 8x8 maps

            # Transpose 2D conv layer 3.
            nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # Resulting state size. (ngf*2) x 16 x 16

            # Transpose 2D conv layer 4.
            nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # Resulting state size. (ngf) x 32 x 32

            # Final Transpose 2D conv layer 5 to generate final image.
            # nc is number of channels - 3 for 3 image channel
            nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),

            # Tanh activation to get final normalized image
            nn.Tanh()
            # Resulting state size. (nc) x 64 x 64
        )

    def forward(self, input):
        ''' This function takes as input the noise vector'''
        return self.main(input)

 

これで、生成クラスを用いてモデルをインスタンス化できます。論文では平均値0と標準偏差0.2を用いて重みを初期化するよう記載されていますが、ここではPyTorchのデフォルトの重み初期化方法を維持します。私たちのプロジェクトでは、PyTorchのデフォルトの重み初期化方法で十分だからです。 

 

# Create the generator
netG = Generator(ngpu).to(device)

# Handle multi-gpu if desired
if (device.type == 'cuda') and (ngpu > 1):
    netG = nn.DataParallel(netG, list(range(ngpu)))

# Print the model
print(netG)

 

最終的な生成器モデルは次のようになります。

 

識別器のアーキテクチャ

以下が識別器のアーキテクチャになります。一連の畳み込み層の最後に密な層を加えて、偽画像かどうかを予測できるようにしています。

# Number of channels in the training images. For color images this is 3
nc = 3
# Size of feature maps in discriminator
ndf = 64

class Discriminator(nn.Module):
    def __init__(self, ngpu):
        super(Discriminator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # input is (nc) x 64 x 64
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
       )

def forward(self, input):
return self.main(input)

 

次に、生成器で行ったのと全く同じように、識別器をインスタンス化します。

# Create the Discriminator
netD = Discriminator(ngpu).to(device)

# Handle multi-gpu if desired
if (device.type == 'cuda') and (ngpu > 1):
    netD = nn.DataParallel(netD, list(range(ngpu)))

# Print the model
print(netD)

 

識別器のアーキテクチャは次のようになります。

 

トレーニング

GANにおけるトレーニングの仕組みを理解することは非常に重要です。また、トレーニングによって、生成器と識別器が同時に改良されていくのを見るのは、興味深いことでもあります。

生成器と識別器がそろったところで、オプティマイザをそれぞれ別々に初期化する必要があります。

# Initialize BCELoss function
criterion = nn.BCELoss()

# Create batch of latent vectors that we will use to visualize
# the progression of the generator
fixed_noise = torch.randn(64, nz, 1, 1, device=device)

# Establish convention for real and fake labels during training
real_label = 1.
fake_label = 0.

# Setup Adam optimizers for both G and D

# Learning rate for optimizers
lr = 0.0002

# Beta1 hyperparam for Adam optimizers
beta1 = 0.5

optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))

 

トレーニングループ

これは重要な部分なので、作成したブロックがどのように連携して機能するのかを理解する必要があります。

複雑に見えるかもしれませんが、このセクションでは上記のコードを順を追ってご説明しましょう。トレーニング全体の主なステップは次のようになっています。

# Lists to keep track of progress/Losses
img_list = []
G_losses = []
D_losses = []
iters = 0

# Number of training epochs
num_epochs = 50
# Batch size during training
batch_size = 128

print("Starting Training Loop...")
# For each epoch
for epoch in range(num_epochs):
    # For each batch in the dataloader
    for i, data in enumerate(dataloader, 0):
        ############################
        # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
        # Here we:
        # A. train the discriminator on real data
        # B. Create some fake images from Generator using Noise
        # C. train the discriminator on fake data
        ###########################
        # Training Discriminator on real data
        netD.zero_grad()
        # Format batch
        real_cpu = data[0].to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), real_label, device=device)
        # Forward pass real batch through D
        output = netD(real_cpu).view(-1)
        # Calculate loss on real batch
        errD_real = criterion(output, label)
        # Calculate gradients for D in backward pass
        errD_real.backward()
        D_x = output.mean().item()

        ## Create a batch of fake images using generator
        # Generate noise to send as input to the generator
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        # Generate fake image batch with G
        fake = netG(noise)
        label.fill_(fake_label)

        # Classify fake batch with D
        output = netD(fake.detach()).view(-1)
        # Calculate D's loss on the fake batch
        errD_fake = criterion(output, label)
        # Calculate the gradients for this batch
        errD_fake.backward()
        D_G_z1 = output.mean().item()
        # Add the gradients from the all-real and all-fake batches
        errD = errD_real + errD_fake
        # Update D
        optimizerD.step()

        ############################
        # (2) Update G network: maximize log(D(G(z)))
        # Here we:
        # A. Find the discriminator output on Fake images
        # B. Calculate Generators loss based on this output. Note that the label is 1 for generator.
        # C. Update Generator
        ###########################
        netG.zero_grad()
        label.fill_(real_label) # fake labels are real for generator cost
        # Since we just updated D, perform another forward pass of all-fake batch through D
        output = netD(fake).view(-1)
        # Calculate G's loss based on this output
        errG = criterion(output, label)
        # Calculate gradients for G
        errG.backward()
        D_G_z2 = output.mean().item()
        # Update G
        optimizerG.step()

        # Output training stats every 50th Iteration in an epoch
        if i % 1000 == 0:
            print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f'
                  % (epoch, num_epochs, i, len(dataloader),
                     errD.item(), errG.item(), D_x, D_G_z1, D_G_z2))

        # Save Losses for plotting later
        G_losses.append(errG.item())
        D_losses.append(errD.item())

        # Check how the generator is doing by saving G's output on a fixed_noise vector
        if (iters % 250 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)):
            #print(iters)
            with torch.no_grad():
                fake = netG(fixed_noise).detach().cpu()
            img_list.append(vutils.make_grid(fake, padding=2, normalize=True))

        iters += 1

 

 

1. データセットから正規化された画像のバッチをサンプリングします。

for i, data in enumerate(dataloader, 0):

 

2. 生成器による画像(偽画像)と本物の正規化画像(本物の画像)、そしてアノテーションを用いて識別器に学習させます。

# Training Discriminator on real data
        netD.zero_grad()
        # Format batch
        real_cpu = data[0].to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), real_label, device=device)
        # Forward pass real batch through D
        output = netD(real_cpu).view(-1)
        # Calculate loss on real batch
        errD_real = criterion(output, label)
        # Calculate gradients for D in backward pass
        errD_real.backward()
        D_x = output.mean().item()
## Create a batch of fake images using generator
        # Generate noise to send as input to the generator
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        # Generate fake image batch with G
        fake = netG(noise)
        label.fill_(fake_label)

        # Classify fake batch with D
        output = netD(fake.detach()).view(-1)
        # Calculate D's loss on the fake batch
        errD_fake = criterion(output, label)
        # Calculate the gradients for this batch
        errD_fake.backward()
        D_G_z1 = output.mean().item()
        # Add the gradients from the all-real and all-fake batches
        errD = errD_real + errD_fake
        # Update D
        optimizerD.step()

 

3. 識別器を学習できない状態に固定しておいてターゲットを1に設定し、偽画像を入力します。そして、識別器の出力から収集された損失を計算して生成器で誤差逆伝播を実行します。生成器が識別器を騙すことができない場合、損失は大きくなります。識別器が偽画像に対して0を出力すると、損失は大きくなる(BCELoss(0,1))ので、ご確認ください。

netG.zero_grad()
        label.fill_(real_label)
        # fake labels are real for generator cost
        output = netD(fake).view(-1)
        # Calculate G's loss based on this output
        errG = criterion(output, label)
        # Calculate gradients for G
        errG.backward()
        D_G_z2 = output.mean().item()
        # Update G
        optimizerG.step()

 

forループを利用してこのステップを繰り返すと、最終的に優れた識別器と生成器が構築できます。

 

結果

最終的な生成器の出力は以下でご覧ください。GANによって、コンテンツエディタで編集可能な画像が生成されました。

画像は少々粗いですが、何と言っても、このプロジェクトはGAN探索の出発点です。この分野では、より複雑で優れたGANアーキテクチャが次々と生み出されているので、これらのアーキテクチャの画像品質もさらに向上していくでしょう。また、これらの画像がノイズベクトルだけから生成されていることもお忘れなく。ノイズを入力して、画像が出力されているのですから、驚くばかりです。

1. トレーニング期間全般にわたる損失

以下は損失に関するグラフです。ステップを繰り返すと、GANの損失は平均して小さくなっており、分散も小さくなっています。そのため、トレーニングの回数をもっと増やすと、より良い結果が得られる可能性があります。 

plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()

2. Jupyterノートブックで250回反復するごとに画像をアニメーション化

以下のコードを用いて、出力をアニメーションとして見ることを選択できます。

#%%capture
fig = plt.figure(figsize=(8,8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)

HTML(ani.to_jshtml())

友達に送信したければ、GIFとしても保存できます。

ani.save('animation.gif', writer='imagemagick',fps=5)
Image(url='animation.gif')

3. 200回反復するごとに画像生成

以下は、特定のトレーニング段階で画像を生成するためのコードです。画像ではあまりはっきりわからないかもしれませんが、画像の品質はトレーニングのステップ数が増えるにつれ、改良されていきます。

# create a list of 16 images to show
every_nth_image = np.ceil(len(img_list)/16)
ims = [np.transpose(img,(1,2,0)) for i,img in enumerate(img_list)if i%every_nth_image==0]
print("Displaying generated images")
# You might need to change grid size and figure size here according to num images.
plt.figure(figsize=(20,20))
gs1 = gridspec.GridSpec(4, 4)
gs1.update(wspace=0, hspace=0)
step = 0
for i,image in enumerate(ims):
    ax1 = plt.subplot(gs1[i])
    ax1.set_aspect('equal')
    fig = plt.imshow(image)
    # you might need to change some params here
    fig = plt.text(7,30,"Step: "+str(step),bbox=dict(facecolor='red', alpha=0.5),fontsize=12)
    plt.axis('off')
    fig.axes.get_xaxis().set_visible(False)
    fig.axes.get_yaxis().set_visible(False)
    step+=int(250*every_nth_image)
#plt.tight_layout()
plt.savefig("GENERATEDimage.png",bbox_inches='tight',pad_inches=0)
plt.show()

 

以下は、段階を追ってGANの結果を示しています。

この記事では、本物に近い偽画像を作成するためのGANの基本についてご説明いたしました。DCGANの生成器および識別器のアーキテクチャーとゼロからアニメ画像を生成する方法について理解を深める上でお役に立てたでしょうか。

このモデルは最も完成されたアニメ顔画像生成器ではありませんが、敵対的生成ネットワークの基本を理解するためのベースとして利用しました。今後、より複雑で興味深いGANを構築するための足がかりになることを期待しています。教師データが手元にありさえすれば、必要に応じて本物のように見えるテクスチャーやキャラクターを作り出せる能力を取得できたと考えると、これはなかなかの成果です。

この記事のコードの詳細は、こちらのGitHubリポジトリをご覧ください。技術的な観点から考察した機械学習に関する記事に興味があれば、以下の関連記事をご覧ください。また、機械学習に関する記事を直接受信できるよう、ぜひライオンブリッジのメールマガジンにご登録ください。 

 

Lionbridge AIについて

当社は教師データの作成やアノテーションサービスを提供し、機械学習の研究開発を支援しております。100万人の認定コントリビューターが登録されており、20年に渡るAIプロジェクトの実績がございます。無料トライアルやお見積は、こちらからお問い合わせ下さい。

※ 本記事は2020年8月7日、弊社英語ブログに掲載された寄稿記事に基づいたものです

AI向け教師データの作成やアノテーションサービスを提供し、研究開発を支援いたします。

メディア掲載結果

    AI・機械学習の最新情報をお届けします!

    Lionbridge AIのブログで紹介している事例記事やトレンドニュースといったビジネスに役立つ情報はもちろん、オープンデータセット集なども合わせてメール配信しております。