皆さんこんにちは。今日も引き続きChatGPT先生をお迎えして、「ChatGPTとPythonで学ぶ 変分オートエンコーダー (VAE)」というテーマで雑談したいと思います。それではChatGPT先生、よろしくお願いします。
assistant:
もちろんです、よろしくお願いいたします。変分オートエンコーダー(Variational Autoencoder, VAE)は、深層学習における生成モデルの一種で、画像データなどの高次元データから、そのデータの特性を捉える低次元の潜在空間を学習し、その潜在空間から再び高次元データを生成することができるという機械です。
オートエンコーダー(Autoencoder)の基本的な考え方は、データを一度圧縮(エンコーディング)し、それを再び展開(デコーディング)することで元のデータを再現しようとするものです。一般的に、エンコーディングされた表現は元のデータよりも低次元で、これはデータの「潜在的な」特徴を表現しています。
しかし、オートエンコーダーはあくまで再現性に重きを置いており、エンコーディングされた低次元空間がデータの特性を連続的に捉えているわけではありません。例えば、似たようなデータに対して似たような潜在空間を持つとは限らないです。
それに対し、VAEはエンコーディングされる潜在空間を確率分布としてモデリングし、この確率分布からサンプリングすることで新たなデータを生成することを可能にします。この特性により、VAEは潜在空間がデータの特性を連続的に捉えることができ、似たようなデータに対しては似たような潜在空間を持つという性質を持ちます。これにより、VAEはデータ生成のためのモデルとして広く用いられています。
user:
それでは、VAEについて学ぶためのPyTorchコードを書いてください。
assistant:
もちろんです、まずはVAEの基本的な部分から始めましょう。以下のコードは、MNISTデータセットを用いて、変分オートエンコーダーを学習するためのものです。
PyTorchを使ってVAEを設定します:
import torch
from torch import nn
import torch.nn.functional as F
class VAE(nn.Module):
def __init__(self, x_dim, h_dim1, h_dim2, z_dim):
super(VAE, self).__init__()
# ネットワークの構造を定義
# エンコーダー部分
self.fc1 = nn.Linear(x_dim, h_dim1)
self.fc2 = nn.Linear(h_dim1, h_dim2)
self.fc31 = nn.Linear(h_dim2, z_dim) # mu
self.fc32 = nn.Linear(h_dim2, z_dim) # logvar
# デコーダー部分
self.fc4 = nn.Linear(z_dim, h_dim2)
self.fc5 = nn.Linear(h_dim2, h_dim1)
self.fc6 = nn.Linear(h_dim1, x_dim)
def encoder(self, x):
h = F.relu(self.fc1(x))
h = F.relu(self.fc2(h))
return self.fc31(h), self.fc32(h) # mu, log_var
def sampling(self, mu, log_var):
std = torch.exp(0.5*log_var)
eps = torch.randn_like(std)
return eps.mul(std).add_(mu) # return z sample
def decoder(self, z):
h = F.relu(self.fc4(z))
h = F.relu(self.fc5(h))
return torch.sigmoid(self.fc6(h))
def forward(self, x):
mu, log_var = self.encoder(x.view(-1, 784))
z = self.sampling(mu, log_var)
return self.decoder(z), mu, log_var
ここでx_dim
はインプットデータの次元、h_dim1
とh_dim2
はエンコーダーとデコーダーの中間層の次元、z_dim
は潜在空間の次元を表しています。
再構成ロスとKLダイバージェンスのロスを計算する関数です:
# VAEのloss
def loss_function(recon_x, x, mu, log_var):
BCE = F.binary_cross_entropy(recon_x, x.view(-1, 784), reduction='sum')
KLD = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
return BCE + KLD
この関数は、元のデータと再構成データとの間のBinary Cross Entropy(BCE)と、KLダイバージェンス(KLD)を計算し、それらの合計をロスとして返します。これにより、モデルはデータの再構成と、エンコーダー部分の出力が標準正規分布に近似することを同時に試みます。
user:
ありがとうございます。それではMNISTデータセットを学習するコードを書いてください。
assistant:
学習を行うため、必要なライブラリをインポートし、データローダを設定し、最適化手法を定義します。
import torch.optim as optim
from torchvision import datasets, transforms
# transformを定義し、データをTensorに変換します。
transform=transforms.Compose([
transforms.ToTensor(),
])
# MNISTデータセットをダウンロードし、DataLoaderを作成します。
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
# VAEモデルとoptimizerを定義します
model = VAE(x_dim=784, h_dim1= 512, h_dim2=256, z_dim=2)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz to ./data/MNIST/raw/train-images-idx3-ubyte.gz
0%| | 0/9912422 [00:00<?, ?it/s]
Extracting ./data/MNIST/raw/train-images-idx3-ubyte.gz to ./data/MNIST/raw
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz to ./data/MNIST/raw/train-labels-idx1-ubyte.gz
0%| | 0/28881 [00:00<?, ?it/s]
Extracting ./data/MNIST/raw/train-labels-idx1-ubyte.gz to ./data/MNIST/raw
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz to ./data/MNIST/raw/t10k-images-idx3-ubyte.gz
0%| | 0/1648877 [00:00<?, ?it/s]
Extracting ./data/MNIST/raw/t10k-images-idx3-ubyte.gz to ./data/MNIST/raw
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to ./data/MNIST/raw/t10k-labels-idx1-ubyte.gz
0%| | 0/4542 [00:00<?, ?it/s]
Extracting ./data/MNIST/raw/t10k-labels-idx1-ubyte.gz to ./data/MNIST/raw
訓練ループを定義します。1エポックは、訓練データ全体を1回通過することを意味します。各エポックでは、バッチごとに前進と後退のパスが実行されます。
def train(epoch):
model.train()
train_loss = 0
for batch_idx, (data, _) in enumerate(train_loader):
optimizer.zero_grad()
recon_batch, mu, log_var = model(data)
loss = loss_function(recon_batch, data, mu, log_var)
loss.backward()
train_loss += loss.item()
optimizer.step()
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item() / len(data)))
print('====> Epoch: {} Average loss: {:.4f}'.format(epoch, train_loss / len(train_loader.dataset)))
そして、最後にエポック数分だけ訓練を回します。
for epoch in range(1, 16):
train(epoch)
Train Epoch: 1 [0/60000 (0%)] Loss: 544.960449
Train Epoch: 1 [6400/60000 (11%)] Loss: 200.735519
Train Epoch: 1 [12800/60000 (21%)] Loss: 173.905075
Train Epoch: 1 [19200/60000 (32%)] Loss: 158.961411
Train Epoch: 1 [25600/60000 (43%)] Loss: 160.600220
Train Epoch: 1 [32000/60000 (53%)] Loss: 161.286957
Train Epoch: 1 [38400/60000 (64%)] Loss: 153.828735
Train Epoch: 1 [44800/60000 (75%)] Loss: 160.313507
Train Epoch: 1 [51200/60000 (85%)] Loss: 166.212646
Train Epoch: 1 [57600/60000 (96%)] Loss: 162.009933
====> Epoch: 1 Average loss: 174.9055
Train Epoch: 2 [0/60000 (0%)] Loss: 152.017532
Train Epoch: 2 [6400/60000 (11%)] Loss: 167.504883
Train Epoch: 2 [12800/60000 (21%)] Loss: 169.717300
Train Epoch: 2 [19200/60000 (32%)] Loss: 161.910782
Train Epoch: 2 [25600/60000 (43%)] Loss: 151.685608
Train Epoch: 2 [32000/60000 (53%)] Loss: 156.116104
Train Epoch: 2 [38400/60000 (64%)] Loss: 147.346283
Train Epoch: 2 [44800/60000 (75%)] Loss: 140.935654
Train Epoch: 2 [51200/60000 (85%)] Loss: 165.808350
Train Epoch: 2 [57600/60000 (96%)] Loss: 150.116577
====> Epoch: 2 Average loss: 156.0396
Train Epoch: 3 [0/60000 (0%)] Loss: 145.175140
Train Epoch: 3 [6400/60000 (11%)] Loss: 156.050751
Train Epoch: 3 [12800/60000 (21%)] Loss: 147.653793
Train Epoch: 3 [19200/60000 (32%)] Loss: 154.766663
Train Epoch: 3 [25600/60000 (43%)] Loss: 143.625473
Train Epoch: 3 [32000/60000 (53%)] Loss: 147.786209
Train Epoch: 3 [38400/60000 (64%)] Loss: 156.924423
Train Epoch: 3 [44800/60000 (75%)] Loss: 146.409622
Train Epoch: 3 [51200/60000 (85%)] Loss: 145.634766
Train Epoch: 3 [57600/60000 (96%)] Loss: 152.837051
====> Epoch: 3 Average loss: 151.8099
Train Epoch: 4 [0/60000 (0%)] Loss: 159.430588
Train Epoch: 4 [6400/60000 (11%)] Loss: 144.169434
Train Epoch: 4 [12800/60000 (21%)] Loss: 161.254807
Train Epoch: 4 [19200/60000 (32%)] Loss: 146.043106
Train Epoch: 4 [25600/60000 (43%)] Loss: 144.469330
Train Epoch: 4 [32000/60000 (53%)] Loss: 148.495407
Train Epoch: 4 [38400/60000 (64%)] Loss: 154.691116
Train Epoch: 4 [44800/60000 (75%)] Loss: 149.386795
Train Epoch: 4 [51200/60000 (85%)] Loss: 151.144653
Train Epoch: 4 [57600/60000 (96%)] Loss: 141.956436
====> Epoch: 4 Average loss: 149.2900
Train Epoch: 5 [0/60000 (0%)] Loss: 149.278778
Train Epoch: 5 [6400/60000 (11%)] Loss: 145.342682
Train Epoch: 5 [12800/60000 (21%)] Loss: 152.683258
Train Epoch: 5 [19200/60000 (32%)] Loss: 143.833862
Train Epoch: 5 [25600/60000 (43%)] Loss: 148.930817
Train Epoch: 5 [32000/60000 (53%)] Loss: 145.443558
Train Epoch: 5 [38400/60000 (64%)] Loss: 158.752457
Train Epoch: 5 [44800/60000 (75%)] Loss: 152.087494
Train Epoch: 5 [51200/60000 (85%)] Loss: 143.156555
Train Epoch: 5 [57600/60000 (96%)] Loss: 137.660583
====> Epoch: 5 Average loss: 147.6316
Train Epoch: 6 [0/60000 (0%)] Loss: 149.158096
Train Epoch: 6 [6400/60000 (11%)] Loss: 144.257050
Train Epoch: 6 [12800/60000 (21%)] Loss: 150.488815
Train Epoch: 6 [19200/60000 (32%)] Loss: 146.082809
Train Epoch: 6 [25600/60000 (43%)] Loss: 152.995621
Train Epoch: 6 [32000/60000 (53%)] Loss: 147.834747
Train Epoch: 6 [38400/60000 (64%)] Loss: 143.900940
Train Epoch: 6 [44800/60000 (75%)] Loss: 148.147766
Train Epoch: 6 [51200/60000 (85%)] Loss: 145.505417
Train Epoch: 6 [57600/60000 (96%)] Loss: 150.022522
====> Epoch: 6 Average loss: 146.5265
Train Epoch: 7 [0/60000 (0%)] Loss: 150.356857
Train Epoch: 7 [6400/60000 (11%)] Loss: 147.627655
Train Epoch: 7 [12800/60000 (21%)] Loss: 145.781799
Train Epoch: 7 [19200/60000 (32%)] Loss: 156.221588
Train Epoch: 7 [25600/60000 (43%)] Loss: 154.712860
Train Epoch: 7 [32000/60000 (53%)] Loss: 145.700836
Train Epoch: 7 [38400/60000 (64%)] Loss: 141.720856
Train Epoch: 7 [44800/60000 (75%)] Loss: 139.527451
Train Epoch: 7 [51200/60000 (85%)] Loss: 155.103394
Train Epoch: 7 [57600/60000 (96%)] Loss: 134.978790
====> Epoch: 7 Average loss: 145.6293
Train Epoch: 8 [0/60000 (0%)] Loss: 150.588409
Train Epoch: 8 [6400/60000 (11%)] Loss: 145.222046
Train Epoch: 8 [12800/60000 (21%)] Loss: 143.605988
Train Epoch: 8 [19200/60000 (32%)] Loss: 133.354767
Train Epoch: 8 [25600/60000 (43%)] Loss: 126.767410
Train Epoch: 8 [32000/60000 (53%)] Loss: 147.901352
Train Epoch: 8 [38400/60000 (64%)] Loss: 138.856384
Train Epoch: 8 [44800/60000 (75%)] Loss: 140.390396
Train Epoch: 8 [51200/60000 (85%)] Loss: 151.177948
Train Epoch: 8 [57600/60000 (96%)] Loss: 137.925751
====> Epoch: 8 Average loss: 144.7074
Train Epoch: 9 [0/60000 (0%)] Loss: 142.977615
Train Epoch: 9 [6400/60000 (11%)] Loss: 157.935654
Train Epoch: 9 [12800/60000 (21%)] Loss: 150.101166
Train Epoch: 9 [19200/60000 (32%)] Loss: 134.821213
Train Epoch: 9 [25600/60000 (43%)] Loss: 139.680237
Train Epoch: 9 [32000/60000 (53%)] Loss: 149.321976
Train Epoch: 9 [38400/60000 (64%)] Loss: 146.411697
Train Epoch: 9 [44800/60000 (75%)] Loss: 145.668213
Train Epoch: 9 [51200/60000 (85%)] Loss: 137.100250
Train Epoch: 9 [57600/60000 (96%)] Loss: 139.185669
====> Epoch: 9 Average loss: 144.1786
Train Epoch: 10 [0/60000 (0%)] Loss: 152.859131
Train Epoch: 10 [6400/60000 (11%)] Loss: 156.265884
Train Epoch: 10 [12800/60000 (21%)] Loss: 148.196777
Train Epoch: 10 [19200/60000 (32%)] Loss: 146.295731
Train Epoch: 10 [25600/60000 (43%)] Loss: 148.255280
Train Epoch: 10 [32000/60000 (53%)] Loss: 149.744522
Train Epoch: 10 [38400/60000 (64%)] Loss: 137.817673
Train Epoch: 10 [44800/60000 (75%)] Loss: 147.781601
Train Epoch: 10 [51200/60000 (85%)] Loss: 140.824127
Train Epoch: 10 [57600/60000 (96%)] Loss: 158.608017
====> Epoch: 10 Average loss: 143.8422
Train Epoch: 11 [0/60000 (0%)] Loss: 147.448898
Train Epoch: 11 [6400/60000 (11%)] Loss: 138.838669
Train Epoch: 11 [12800/60000 (21%)] Loss: 157.666992
Train Epoch: 11 [19200/60000 (32%)] Loss: 146.019455
Train Epoch: 11 [25600/60000 (43%)] Loss: 144.304474
Train Epoch: 11 [32000/60000 (53%)] Loss: 145.105927
Train Epoch: 11 [38400/60000 (64%)] Loss: 142.786224
Train Epoch: 11 [44800/60000 (75%)] Loss: 155.832352
Train Epoch: 11 [51200/60000 (85%)] Loss: 144.696350
Train Epoch: 11 [57600/60000 (96%)] Loss: 138.222900
====> Epoch: 11 Average loss: 143.8519
Train Epoch: 12 [0/60000 (0%)] Loss: 139.657349
Train Epoch: 12 [6400/60000 (11%)] Loss: 153.164856
Train Epoch: 12 [12800/60000 (21%)] Loss: 138.502625
Train Epoch: 12 [19200/60000 (32%)] Loss: 136.977814
Train Epoch: 12 [25600/60000 (43%)] Loss: 148.295975
Train Epoch: 12 [32000/60000 (53%)] Loss: 145.024399
Train Epoch: 12 [38400/60000 (64%)] Loss: 139.261826
Train Epoch: 12 [44800/60000 (75%)] Loss: 140.234146
Train Epoch: 12 [51200/60000 (85%)] Loss: 143.436829
Train Epoch: 12 [57600/60000 (96%)] Loss: 139.645813
====> Epoch: 12 Average loss: 143.0847
Train Epoch: 13 [0/60000 (0%)] Loss: 144.253677
Train Epoch: 13 [6400/60000 (11%)] Loss: 143.183167
Train Epoch: 13 [12800/60000 (21%)] Loss: 149.045715
Train Epoch: 13 [19200/60000 (32%)] Loss: 145.835999
Train Epoch: 13 [25600/60000 (43%)] Loss: 142.122086
Train Epoch: 13 [32000/60000 (53%)] Loss: 152.077744
Train Epoch: 13 [38400/60000 (64%)] Loss: 148.320404
Train Epoch: 13 [44800/60000 (75%)] Loss: 143.877884
Train Epoch: 13 [51200/60000 (85%)] Loss: 135.269501
Train Epoch: 13 [57600/60000 (96%)] Loss: 142.408371
====> Epoch: 13 Average loss: 142.6004
Train Epoch: 14 [0/60000 (0%)] Loss: 148.268768
Train Epoch: 14 [6400/60000 (11%)] Loss: 143.789337
Train Epoch: 14 [12800/60000 (21%)] Loss: 142.743225
Train Epoch: 14 [19200/60000 (32%)] Loss: 137.034897
Train Epoch: 14 [25600/60000 (43%)] Loss: 148.237091
Train Epoch: 14 [32000/60000 (53%)] Loss: 144.875534
Train Epoch: 14 [38400/60000 (64%)] Loss: 133.709579
Train Epoch: 14 [44800/60000 (75%)] Loss: 149.002319
Train Epoch: 14 [51200/60000 (85%)] Loss: 149.494446
Train Epoch: 14 [57600/60000 (96%)] Loss: 139.305511
====> Epoch: 14 Average loss: 142.1237
Train Epoch: 15 [0/60000 (0%)] Loss: 151.508453
Train Epoch: 15 [6400/60000 (11%)] Loss: 145.673111
Train Epoch: 15 [12800/60000 (21%)] Loss: 138.324722
Train Epoch: 15 [19200/60000 (32%)] Loss: 144.656693
Train Epoch: 15 [25600/60000 (43%)] Loss: 144.889465
Train Epoch: 15 [32000/60000 (53%)] Loss: 150.992935
Train Epoch: 15 [38400/60000 (64%)] Loss: 149.152267
Train Epoch: 15 [44800/60000 (75%)] Loss: 145.646790
Train Epoch: 15 [51200/60000 (85%)] Loss: 140.412918
Train Epoch: 15 [57600/60000 (96%)] Loss: 143.678726
====> Epoch: 15 Average loss: 141.5515
以上で、Variance AutoEncoderの学習が出来上がります。
このモデルは、z_dim
が2次元で学習しているため、潜在変数を2次元プロットとして視覚化することができます。画像がどのように生成されているかを直観的に理解するための強力なツールです。
user:
学習が終わりました。これを用いて、MNISTデータセットを2次元に次元削減して可視化するコードを書いてくれませんか?
assistant:
はい、もちろんです。
次はエンコーダを使ってMNISTデータセットを2次元に圧縮した状態を可視化するためのコードです。
まず、テストデータをロードします:
test_dataset = datasets.MNIST('./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=10000, shuffle=True)
次に、テストデータからエンコーダの出力を記録し、Matplotlibでプロットします:
import matplotlib.pyplot as plt
import numpy as np
model.eval()
with torch.no_grad():
for i, (data, labels) in enumerate(test_loader):
mu, logvar = model.encoder(data.view(-1, 784))
z = model.sampling(mu, logvar)
z = z.detach().numpy()
plt.figure(figsize=(6, 6))
colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k', 'violet', 'orange', 'purple']
for c in range(10):
plt.scatter(z[labels == c, 0], z[labels == c, 1], c=colors[c], label=str(c))
plt.legend()
plt.grid()
plt.title('Latent space from VAE model')
plt.show()
break #または、1バッチ(10,000画像)だけでストップするために使用されます.
このコードは、各ラベルに基づいて異なる色を使って、データ点を潜在空間にプロットします。これは、VAEがどのようにMNISTの手書き数字を潜在空間にマッピングするかを視覚的に理解するのに役立つ方法です。
user:
VAEは生成モデルなので、生成もできるんですよね。10種類の数字を10個ずつ生成して、10x10のsubplotに可視化するコードを書いてください。
assistant:
もちろんです!VAE は新しいデータを生成するために使用することができます。
通常、この生成プロセスは潜在空間からランダムな点をサンプリングし、それをデコーダに通すことで行います。以下のコードはそれを行い、結果を10x10のサブプロットにプロットします。
model.eval()
with torch.no_grad():
# 標準正規分布からランダムに点をサンプリング
z = torch.randn(100, 2)
# サンプリングした点をデコーダを通じて画像に変換
generated_images = model.decoder(z)
#可視化
fig, axs = plt.subplots(10, 10, figsize=(10, 10))
for i, ax in enumerate(axs.flat):
ax.imshow(generated_images[i].view(28, 28), cmap='gray')
ax.axis('off')
plt.show()
ここでは、ランダムな2次元ベクトル(z)を生成し、それをデコーダに入力して新しい MNIST のような画像を生成します。その後、生成した画像を matplotlib でプロットします。
ただし、この生成された画像は学習に使われたオリジナルのMNISTデータとは必ずしも一致しないことに注意してください。それらは標準正規分布からサンプリングしたランダムなベクトルを基に生成されているため、実際のMNISTデータと比べて若干曖昧または抽象的に見えるかもしれません。しかし、それでも一般的な手書きの数字の形状や特性を捉えていることがわかります。
user:
このモデルは、たとえば「数字の8を描きたい」とか「数字の7を描きたい」とか指定できるんでしょうか?
assistant:
基本的なVAEの設定では、生成される数字を直接制御することはできません。これは、VAEの潜在空間は未ラベルの訓練サンプルから学習され、生成プロセスはランダムにサンプリングされた潜在ベクトルから始まる、という自然な性質によるものです。
ただし、そのような制御を可能にするためにVAEを拡張する方法はあります。その1つは、条件付きVAE (CVAE)と呼ばれるモデルです。CVAEは、エンコーダとデコーダの両方にラベル情報を追加入力として提供します。したがって、特定の数字を生成するためには、その数字のラベルをデコーダに入力として与えればよいのです。
したがって、「数字の8を描きたい」、「数字の7を描きたい」という要求に直接対応するためには、CVAEまたは他の一部の条件付き生成モデルを用いる必要があります。
user:
それでは、MNISTデータセットを学習して、生成したい数字を指定できる条件付きVAE (CVAE)のコードを書いてください。
assistant:
はい、まずはCVAEのモデル定義から始めましょう。CVAEでは、エンコーダとデコーダの両方に条件となるラベル情報が入力されます。
class CVAE(nn.Module):
def __init__(self, x_dim, h_dim1, h_dim2, z_dim, c_dim):
super(CVAE, self).__init__()
# エンコーダ部分
self.fc1 = nn.Linear(x_dim + c_dim, h_dim1)
self.fc2 = nn.Linear(h_dim1, h_dim2)
self.fc31 = nn.Linear(h_dim2, z_dim)
self.fc32 = nn.Linear(h_dim2, z_dim)
# デコーダ部分
self.fc4 = nn.Linear(z_dim + c_dim, h_dim2)
self.fc5 = nn.Linear(h_dim2, h_dim1)
self.fc6 = nn.Linear(h_dim1, x_dim)
def encoder(self, x, c):
inputs = torch.cat([x, c], dim=1)
h = F.relu(self.fc1(inputs))
h = F.relu(self.fc2(h))
return self.fc31(h), self.fc32(h)
def sampling(self, mu, log_var):
std = torch.exp(0.5*log_var)
eps = torch.randn_like(std)
return eps.mul(std).add_(mu)
def decoder(self, z, c):
inputs = torch.cat([z, c], dim=1)
h = F.relu(self.fc4(inputs))
h = F.relu(self.fc5(h))
return torch.sigmoid(self.fc6(h))
def forward(self, x, c):
mu, log_var = self.encoder(x.view(-1, 784), c)
z = self.sampling(mu, log_var)
return self.decoder(z, c), mu, log_var
c_dim
は条件となるラベル(ここでは0から9の数字)の次元を示しています。MNISTでは、これは10になります。ラベルはone-hot encodingされている必要があります。つまり、"4"は[0, 0, 0, 0, 1, 0, 0, 0, 0, 0]と表されます。
トレーニングループは基本的なVAEとほとんど同じで、ただしラベル情報も入力として提供されます。
# モデルと最適化手法を定義
model = CVAE(x_dim=784, h_dim1= 512, h_dim2=256, z_dim=2, c_dim=10)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
def one_hot(labels, class_size):
targets = torch.zeros(labels.size(0), class_size)
for i, label in enumerate(labels):
targets[i, label] = 1
return targets
def train(epoch):
model.train()
train_loss = 0
for i, (data, labels) in enumerate(train_loader):
data = data.cuda()
labels = one_hot(labels, 10).cuda()
optimizer.zero_grad()
recon_batch, mu, log_var = model(data, labels)
loss = loss_function(recon_batch, data, mu, log_var)
loss.backward()
train_loss += loss.item()
optimizer.step()
print('====> Epoch: {} Average loss: {:.4f}'.format(epoch, train_loss / len(train_loader.dataset)))
for epoch in range(1, 16):
train(epoch)
以上で、条件を指定して指定の数字を生成するCVAEの訓練が出来上がります。
user:
次のエラーを修正したコードを書いてください。
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[10], line 29
26 print('====> Epoch: {} Average loss: {:.4f}'.format(epoch, train_loss / len(train_loader.dataset)))
28 for epoch in range(1, 16):
---> 29 train(epoch)
Cell In[10], line 19, in train(epoch)
16 labels = one_hot(labels, 10).cuda()
17 optimizer.zero_grad()
---> 19 recon_batch, mu, log_var = model(data, labels)
20 loss = loss_function(recon_batch, data, mu, log_var)
22 loss.backward()
File /usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
1190 # If we don't have any hooks, we want to skip the rest of the logic in
1191 # this function, and just call forward.
1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
1193 or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194 return forward_call(*input, **kwargs)
1195 # Do not call functions when jit is used
1196 full_backward_hooks, non_full_backward_hooks = [], []
Cell In[9], line 34, in CVAE.forward(self, x, c)
33 def forward(self, x, c):
---> 34 mu, log_var = self.encoder(x.view(-1, 784), c)
35 z = self.sampling(mu, log_var)
36 return self.decoder(z, c), mu, log_var
Cell In[9], line 18, in CVAE.encoder(self, x, c)
16 def encoder(self, x, c):
17 inputs = torch.cat([x, c], dim=1)
---> 18 h = F.relu(self.fc1(inputs))
19 h = F.relu(self.fc2(h))
20 return self.fc31(h), self.fc32(h)
File /usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
1190 # If we don't have any hooks, we want to skip the rest of the logic in
1191 # this function, and just call forward.
1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
1193 or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194 return forward_call(*input, **kwargs)
1195 # Do not call functions when jit is used
1196 full_backward_hooks, non_full_backward_hooks = [], []
File /usr/local/lib/python3.10/site-packages/torch/nn/modules/linear.py:114, in Linear.forward(self, input)
113 def forward(self, input: Tensor) -> Tensor:
--> 114 return F.linear(input, self.weight, self.bias)
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cpu and cuda:0! (when checking argument for argument mat1 in method wrapper_addmm)
assistant:
エラーメッセージから、テンソルが異なるデバイス(CPUとGPU)に存在しているために問題が発生していることがわかります。
この問題を解決するためには、すべてのテンソルが同じデバイスに存在していることを確認する必要があります。つまり、モデルとデータの両方をGPUに移動させる必要があります。
上記のコードでは、データはcudaメソッドを使用してGPUに移動しますが、モデルはCPUにまだ在宅しています。これにより、RuntimeErrorが発生します。
したがって、モデルもGPU上に移動させる修正を行います:
# モデルと最適化手法を定義
model = CVAE(x_dim=784, h_dim1= 512, h_dim2=256, z_dim=2, c_dim=10)
model.to("cuda") # モデルをGPUに移動
optimizer = optim.Adam(model.parameters(), lr=1e-3)
def one_hot(labels, class_size):
targets = torch.zeros(labels.size(0), class_size)
for i, label in enumerate(labels):
targets[i, label] = 1
return targets
def train(epoch):
model.train()
train_loss = 0
for i, (data, labels) in enumerate(train_loader):
data = data.cuda() # データをGPUに移動
labels = one_hot(labels, 10).cuda() # ラベルもGPUに移動
optimizer.zero_grad()
recon_batch, mu, log_var = model(data, labels) # ここでエラーが起きています
loss = loss_function(recon_batch, data, mu, log_var)
loss.backward()
train_loss += loss.item()
optimizer.step()
print('====> Epoch: {} Average loss: {:.4f}'.format(epoch, train_loss / len(train_loader.dataset)))
for epoch in range(1, 16):
train(epoch)
====> Epoch: 1 Average loss: 156.5898
====> Epoch: 2 Average loss: 136.8548
====> Epoch: 3 Average loss: 133.9442
====> Epoch: 4 Average loss: 132.4615
====> Epoch: 5 Average loss: 131.5506
====> Epoch: 6 Average loss: 130.8352
====> Epoch: 7 Average loss: 130.3262
====> Epoch: 8 Average loss: 129.9168
====> Epoch: 9 Average loss: 129.4660
====> Epoch: 10 Average loss: 129.1589
====> Epoch: 11 Average loss: 128.8017
====> Epoch: 12 Average loss: 128.5962
====> Epoch: 13 Average loss: 128.2278
====> Epoch: 14 Average loss: 127.9742
====> Epoch: 15 Average loss: 127.7249
user:
学習が終わりました。これを用いて、MNISTデータセットを2次元に次元削減して可視化するコードを書いてくれませんか?
assistant:
はい、もちろんです。
次はCVAEのエンコーダーを使ってMNISTデータセットを2次元に圧縮した状態を可視化するためのコードです。
まず、テストデータをロードします:
test_dataset = datasets.MNIST('./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=10000, shuffle=True)
次に、テストデータからエンコーダの出力を記録し、Matplotlibでプロットします:
import matplotlib.pyplot as plt
import numpy as np
model.eval()
with torch.no_grad():
for i, (data, labels) in enumerate(test_loader):
data = data.cuda()
labels = one_hot(labels, 10).cuda()
mu, logvar = model.encoder(data.view(-1, 784), labels)
z = model.sampling(mu, logvar)
z = z.cpu().numpy()
plt.figure(figsize=(6, 6))
colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k', 'violet', 'orange', 'purple']
for c in range(10):
plt.scatter(z[labels.cpu().numpy()[:, c] == 1, 0],
z[labels.cpu().numpy()[:, c] == 1, 1],
c=colors[c], label=str(c))
plt.legend()
plt.grid()
plt.title('Latent space from CVAE model')
plt.show()
break #再び、1バッチ(10,000画像)だけでストップするために使用されます。
このコードは、各ラベルに基づいて異なる色を使って、データ点を潜在空間にプロットします。これは、CVAEがMNISTの手書き数字を潜在空間にどのようにマッピングするかを視覚的に理解するのに役立つ方法です。
user:
10種類の数字を10個ずつ生成して、10x10のsubplotに可視化するコードを書いてください。
assistant:
VAEと同様に、CVAEでも指定した条件で数字を生成できます。ここでは、それぞれの数字に対して10個ずつ、合計100個の画像を生成してみましょう。
import numpy as np
model.eval()
with torch.no_grad():
z = torch.randn(100, 2).to(device)
conditions = torch.eye(10).to(device)
# 各条件(数字)を10回繰り返し
c = conditions.repeat(10, 1)
generated_images = model.decoder(z, c)
fig, axes = plt.subplots(10, 10, figsize=(10, 10))
for i, ax in enumerate(axes.flat):
ax.imshow(generated_images[i].view(28, 28).cpu(), cmap='gray')
ax.axis('off')
plt.show()
このコードでは、まず100個の2次元潜在ベクトルをランダムに生成します。次に10クラスそれぞれについて10個の条件を生成します。これらがデコーダへの入力となります。
デコーダはそれぞれのzとcの組み合わせから画像を生成し、その結果は100個の28x28の画像となります。
最後に、各画像を10x10のグリッドにプロットします。結果として、各行が特定の数字を表し、各列がその数字の異なる生成例を表すようなグリッドが得られます。
user:
次のエラーを修正したコードを書いてください。
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[14], line 6
3 model.eval()
5 with torch.no_grad():
----> 6 z = torch.randn(100, 2).to(device)
7 conditions = torch.eye(10).to(device)
9 # 各条件(数字)を10回繰り返し
NameError: name 'device' is not defined
assistant:
エラーの報告によれば、device
という名前が定義されていないためNameError
が起きています。これは、torch.randn(100, 2).to(device)
のdevice
が、CPUかGPUどちらを使用するかを指定する変数で、これが存在していないためエラーが出ています。
CUDA
が使用可能な場合にはdevice
をGPUに、そうでない場合にはCPUに設定します。全体のコードでdevice
がどう定義されているか確認してみてください。もし見当たらなければ、以下のように設定できます:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.eval()
with torch.no_grad():
z = torch.randn(100, 2).to(device)
conditions = torch.eye(10).to(device)
# 各条件(数字)を10回繰り返し
c = conditions.repeat(10, 1)
generated_images = model.decoder(z, c)
fig, axes = plt.subplots(10, 10, figsize=(10, 10))
for i, ax in enumerate(axes.flat):
ax.imshow(generated_images[i].view(28, 28).cpu().data.numpy(), cmap='gray')
ax.axis('off')
plt.show()
上記のようにエラーが起きた箇所でdevice
を明示的に定義することでエラーを解消できます。なお、デバイスの設定は一般的に、プログラムの開始部分で行われることが一般的です。
user:
へー。不思議ですね。VAEでは、潜在空間上で文字がなんとなく分かれているけれども、文字を指定して生成させることはできない一方で、CVAEでは、潜在空間上で文字が分かれているように見えないけれども、文字を指定して生成させることができるんですね。
assistant:
はい、その通りです。これは、潜在空間の解釈と生成プロセスの違いからくる興味深い違いで、VAEとCVAEの主な違いの1つです。
VAEでは、ネットワークが潜在空間にデータをどのようにマッピングするかを自動的に学習します。その結果、同じクラスの画像は潜在空間上で近い位置にマッピングされ、異なるクラスの画像は潜在空間上で離れた位置にマッピングされる傾向があります。しかし、特定の条件を満たすデータを明示的に生成する機能はありません。
一方、CVAEでは、ネットワークがデータをエンコードするときに条件ラベルを使用します。これにより、ネットワークは潜在空間をより効率的に利用でき、特定の条件を満たすデータを生成する能力が付くため、具体的な条件(ここでは特定の数字)を指定してデータを生成することができます。
言いかえれば、VAEは潜在空間を「自己組織化」し、似たようなデータは近い位置にマッピングされます。一方、CVAEは「指導あり」の方法で潜在空間を学習し、我々が指定した条件に基づいてデータを生成する能力があります。
user:
今回のCVAEは条件ラベルとして分類クラスの情報を用いましたが、ひょっとして、条件ラベルとして回帰モデルのような連続的な数字を取り扱えたりするでしょうか?
assistant:
はい、その通りです。条件として用いるラベルは分類クラスだけでなく、実は任意の情報をラベルとして用いることができます。
例えば、あるデータセットの中で各データが持つ連続的な属性(物のサイズや色の強さなど)を条件として使ってCVAEを訓練することも可能です。その結果、その属性を任意に指定して新しいデータを生成することが可能となります。
ただし、このような条件付き生成はモデルの訓練が難しくなったり、必要なデータ量が増えるなどの課題も伴うことに注意が必要です。件の属性が何を意味するか、またはその属性と生成したいデータとの関係性が明確である場合にのみ、有用な結果が得られるでしょう。そのため、属性を選択する際や、モデルの訓練を進める際には、その辺りの状況を考慮に入れることが重要です。
user:
ラベルとして連続的な属性を用いるCVAEのクラスの実装は、先ほど教えていただいた、クラス情報を用いるCVAEの実装と同じなんでしょうか?
assistant:
はい、基本的には同じです。主な違いは、ラベルの形状(入力となるテンソルの形状)とその処理方法です。ラベルが連続的な属性を表す場合でも、それらは通常、ネットワークに対する追加の入力として扱われます。
具体的には、もしデータセットのそれぞれのアイテムに2つの連続する属性(例えば、サイズと色の強さ)がある場合、ラベルc
は長さ2のベクトルになる可能性があります。そして、そのラベルはエンコーダとデコーダに入力として供給されるはずです。
ですから、CVAEの基本的な設計は変わらず、連続する属性がある場合でもその設計はそのまま利用できます。ただし、ラベルの扱いや、そのラベルが表す実際の属性についての理解の明確さなどには注意が必要です。また、その属性が何を意味するのか、どのようにデータに影響を与えるのかなど、具体的な応用ケースに合わせてモデルの訓練方法を最適化する可能性があります。
user:
VAEやCVAEは、生成モデルとしての性能は分かりましたが、次元削減モデルとしては、分離能が低いように感じました。潜在空間zの次元を上げれば、分離能は上がるのでしょうか?
assistant:
潜在空間の次元数を増やすと、モデルはより多くの情報を潜在空間にエンコードでき、より複雑なデータ表現を学ぶことが可能となります。したがって、一般的には潜在空間の次元数を増やすことで、分離能やデータ表現の能力は向上する可能性があります。
ただし、いくつかの重要な注意点があります:
-
次元数が多すぎると、学習が困難になる可能性があります。特に、VAEやCVAEのような生成モデルでは、潜在空間が高次元になると、必要なデータの量も増えて訓練が難しくなります。
-
潜在空間が高次元になると、可視化や解釈が難しくなります。2次元または3次元の潜在空間では、簡単に可視化できますが、それ以上の次元数では、直感的な理解や可視化が難しくなります。
-
最後に、次元数を増やすことが必ずしもモデルの性能を向上させるとは限りません。適切な次元数は、問題や利用可能なデータに依存します。ある問題では低次元の潜在空間が適している場合もあれば、別の問題では高次元の潜在空間が必要な場合もあります。
したがって、潜在空間の次元数を選択するときは、これらの考慮事項を考えながら、試行錯誤を繰り返すことが一般的です。
user:
中間層の次元を増やすことの利点を欠点を詳しく教えてください。
assistant:
ニューラルネットワーク、特に深層学習モデルにおいて中間層(隠れ層)の次元(ノード数や層の数)を増やすことによる一般的な利点と欠点は以下のとおりです。
利点:
-
表現力の向上:多くの中間層やノードを持つネットワークは、高次元の複雑な関数を表現できます。これは、ネットワークがより複雑な特徴を学習し、より高度な予測タスクを達成できることを可能にします。
-
性能の向上:適切に訓練され、適切に正則化された場合、ネットワークが持つ情報をよりよく利用し、予測の精度を高めることができます。
欠点:
-
過学習リスク:ネットワークの表現力が高すぎると、訓練データに過度に適合する傾向があり、新しい未知のデータに対する予測性能が低下します。これを過学習(overfitting)と呼びます。
-
計算負荷:中間層の次元が増えると、訓練と予測に必要な計算量が増えます。これは、訓練時間の増加や、大きなメモリ容量の必要性という形で表れます。
-
学習の難易度:深いネットワークや大きな中間層を持つネットワークは訓練が難しいとされています。特に勾配消失問題など、深いネットワークを適切に学習するためには高度な技術が必要です。
これらの利点と欠点は互いにトレードオフの関係にあるため、設計時には適切なバランスを見つける必要があります。また、適切な正則化技術やネットワーク構造(例:残差接続など)を採用することで上記の関係を緩和することも可能です。
user:
エンコーダーの中間層を変更することの効果と、デコーダーの中間層を変更する効果について、もし違いがあれば説明してください。
assistant:
エンコーダとデコーダの中間層を変えることで得られる効果は、大まかには同じで、それぞれの部分の学習能力を向上させることが期待できます。ただし、エンコーダかデコーダかによって、それぞれの役割と効果に違いが出てきます。
-
**エンコーダの中間層を変更:**エンコーダは入力データを潜在表現に変換する役割を持つので、その中間層の次元を増やすと、データから抽出できる情報量が増え、潜在空間の表現能力が向上します。これは、より複雑なデータ構造を捉えられるようになることを意味します。しかし、層を増やしすぎると過学習のリスクが高まり、学習の難易度が上がる可能性があります。
-
**デコーダの中間層を変更:**デコーダは潜在表現を元のデータ空間に戻す役割を持つので、その中間層の次元を増やすと、生成性能が向上します。つまり、よりリアルな、またはより多様なデータを生成できるようになる可能性があります。しかし、こちらも層を増やしすぎると過学習のリスクが高まります。
以上の通り、エンコーダとデコーダの役割の違いにより、その中間層の設計がもたらす具体的な効果も異なると考えられます。問題の性質やデータの特性により、どちらの部分に重点を置くべきかは変わるため、適切なネットワーク構造を見つけるためには、異なる設定での実験や検証が必要となります。
user:
再構成ロスとKLダイバージェンスのロスについても詳しく教えてください。
assistant:
VAE(変分オートエンコーダ)の損失関数は、2つの部分から成っています。1つは「再構成ロス」、もう一つは「KLダイバージェンスのロス」です。
-
再構成ロス(Reconstruction Loss):これは、元の入力データとデコーダにより再構成されたデータとの間の距離(または不一致度)を測る損失です。再構成ロスが低いほど、VAEは正確に元のデータを再構成できていると言えます。再構成ロスの計算方法は、問題の性質によって異なります。例えば、二値のピクセル値(0か1)を持つ画像データを対象とする場合、ベルヌーイ分布を使用して再構成ロスを計算します。それに対して、連続値を持つデータでは、ガウス分布を仮定して二乗損失などを用いることが一般的です。
-
KLダイバージェンスのロス(KL Divergence Loss):これは、エンコーダが学習する潜在変数の分布と事前分布(通常は標準正規分布)との間の距離を測る損失です。KLダイバージェンスのロスが低いほど、潜在変数の分布は事前分布に近いと言えます。この部分のロスは、VAEがエンコーダが出力する潜在変数の分布(事後分布)を事前分布に近づける正則化項の役割を果たします。
総合的にみると、VAEはこれら2つのロスを最小化するように訓練されます。再構成ロスがデータの再構成精度を担当し、KLダイバージェンスのロスが潜在変数の分布を制御する役割を持っています。
user:
KLダイバージェンスのロスは、いろんなところに出てくる気がしますが、VAEやCVAE以外に何がありましたでしょうか?
assistant:
KLダイバージェンスは、情報理論における2つの確率分布間の「距離」を測るための尺度なので、多くの機械学習や統計モデリングのタスクでも使用されます。
以下に、VAEやCVAE以外でKLダイバージェンスが使用される主な例をいくつか挙げてみます:
-
GAN(Generative Adversarial Networks):GANの観点から見ると、KLダイバージェンスはディスクリミネータによる真のデータ分布と生成データ分布の間の非類似性を測定する一つの手段となる可能性があります。
-
強化学習:方策勾配法では、新旧方策の間のKLダイバージェンスを制約やペナルティとして用いることがあります。これは、方策の更新が急激に大きくなりすぎるのを防ぎ、学習の安定性を向上させる目的があります。
-
トピックモデリング:文書のトピックモデリングに使われるLatent Dirichlet Allocation(LDA)のような手法では、KLダイバージェンスは文書間の類似性を計算するために使われます。
以上のように、KLダイバージェンスは統計的・情報理論的な距離尺度として様々な分野で幅広く利用されています。それぞれの問題設定において、最適な確率分布を推定し、その分布がどれだけ目標とする分布と近いかを評価するのに役立ちます。
user:
ありがとうございました。それでは以上の議論を踏まえて、VAEとCVAEについて詳しいまとめを書いてください。
assistant:
はい、以下にVAEとCVAEの詳細なまとめを書いてみます。
-
変分オートエンコーダ(VAE):VAEは生成モデルの一つで、データの隠れた潜在的な表現を学習するために使用されます。VAEはエンコーダとデコーダの2つのパートで構成されています。エンコーダは入力データを潜在空間にエンコードし、デコーダはその潜在表現から元の入力データを再構成(生成)します。VAEの損失関数は、デコーダによる再構成のエラー(再構成ロス)と、エンコーダが出力する潜在変数の分布と事前分布との間のKLダイバージェンス(KLダイバージェンスのロス)の2つから成ります。
-
条件付き変分オートエンコーダ(CVAE):CVAEはVAEの拡張で、ラベル情報を入力データと一緒にモデルに供給します。これにより、特定の条件下でデータを生成する能力を持つモデルを訓練できます。ラベルは連続的な属性でも良く、その場合は通常の入力の一部としてモデルに与えられます。
-
中間層の次元とモデルの性能:エンコーダやデコーダの中間層の次元(ノード数や層数)を増やすことで、モデルの表現力や性能を高めることができますが、計算負荷の増加や過学習のリスクが伴います。また、エンコーダとデコーダの役割が異なるため、その中間層の設計がもたらす具体的な効果も異なると考えられます。調整と実験が必要です。
-
KLダイバージェンス:KLダイバージェンスは、2つの確率分布間の「距離」を測る尺度で、VAEだけでなく、GANや強化学習、トピックモデリングなど、多くのマシンラーニングのタスクでも使用されます。
以上がVAEとCVAEの詳細な特徴とその重要性についてのまとめです。どちらのモデルも、データの潜在的な表現を学び、新しいデータを生成する能力を持つことが特徴的であり、その能力は多くの実問題に応用することができます。
user:
最後に、以上の議論を踏まえてVAEとCVAEをテーマとした落語を創作してください。
assistant:
舞台は今から数百年後の未来、機械が人間のために作業を担当する時代です。
主人公は、老舗の落語家、三代目桂デーヴェです。「デーヴェ」は「VAE」から取ったのだとか。デーヴェは、データを見て表現力豊かな物語をエンコードし、デコードする能力に長けています。
しかしある日、デーヴェはライバルの落語家、初代林家コンディショナルが登場するのを目撃します。「コンディショナル」は「CVAE」から取られています。コンディショナルが登場して以来、その人気は頂点に達しました。なぜなら、彼は物語をエンコードするだけでなく、特定の条件下で独自の物語をデコードすることができたからです。
このライバルの登場に、デーヴェはますます焦り始めます。デーヴェはしばしば考えます。「中間層を増やして自分の表現力を高めればいいのだろうか? それとも違うアプローチが必要だろうか?」と。
そんなある日、デーヴェが深く思い悩んでいると、師匠の大先輩から一つの助言が与えられます。「デーヴェ、お前の持ち味は、物語の骨組みをエンコードすること。そしてそれを賑やかに装飾してデコードすることだ。それがお前のKLダイバージェンスだ。だから、無理して他の人と同じことをしようとせんでもいい。自分らしくいることが大事だ。」
最後に、デーヴェは師匠の言葉を心に刻み、自分の特徴を活かして新たな一篇の落語を作り上げることができました。物語は新しいものでしたが、それでもやはりデーヴェの特長が生かされていて、観客は大いに喜びました。
これが、「デーヴェとコンディショナル」、新しい世界の古典的な物語です。新しい技術を学ぶことも大切ですが、それを適用する時には、自分自身の特性とどのように最も効果的に組み合わせるかを考えることが大切だという教訓を、私たちはこの物語から学び取ることができます。