
教師なし機械学習「VAE」による連続的な手書き文字の生成
2020.08.13
目次
認識モデルから生成モデルへ
以前まで、手書き文字の認識は、難しいタスクであると考えられてきました。
しかし、ニューラルネットワークの開発が進んだ2020年現在では、比較的、簡単な課題へと変化しました。
手書き文字の認識モデル
このような「認識モデル」では、手書き文字などのデータの分布を考慮せず、与えられたデータそのものから、直接識別境界を引いていきます。

手書き文字の生成モデル
それに対し、「生成モデル」は与えられたデータ群の分布そのものを求めていきます。

生成モデルのメリット
生成モデルでは、分布が分かっているため、任意のデータを生成できます。
これは、機械学習タスクの弱点の一つ、データ不足を補えます。
また、データを連続的に変化させることが可能となり、モーフィングを行うことができるようになります。
Variational AutoEncoder(VAE)を用いて顔画像の表情・向きを変化させる
例えば、映画やアニメなどで、キャラクターが「普通の顔」から「笑顔」に変化していくアニメーションを作成するとしましょう。
このとき、様々な表情を学習した生成モデルを用いれば、イラストレーターなしに、アニメーションの連続的な変化を作成することができます。
VAE で同一人物の顔画像を学習させ変化させた図
以下は、VAE により、同一人物の顔画像を学習させ、連続的な表情・顔の向きを変化させた図です。

AutoEncoder と Variational AutoEncoder(VAE)
前回、AutoEncoder を用いた次元削減について話しましたが、AutoEncoder では、データと潜在変数の関係を \(1:1\) として次元埋め込みをしていました。
「Variational AutoEncoder(VAE)」では、潜在変数に確率的なブレを与えることで、与えられたデータ群の分布を推定し、連続的な画像生成を可能にします。
つまり、VAE は、生成モデルの一つとなります。
AutoEncoder による次元削減の記事
なお、AutoEncoder を用いた次元削減については、以下の記事で解説しています。
こちらの記事もオススメ!
Variational AutoEncoder(VAE) の理論
ネットワーク構造
さて、「生成モデルは、データの分布を求めること」と述べました。
今後は、データを
$$X = { x_{1} , x_{2} , \ldots , x_{n} | x_{i} は i 番目のピクセルの画素値 }$$
として、その確率分布を \(P(X)\) とします。
そして、AutoEncoder の時にもお話したように、画像のような高次元データは、ほとんどの画素値が冗長であり、周りの画素値で補完できます。
そのため、実際には、データはより低次元に分布するはずです。
「0~9」の手書き文字画像を3次元の潜在変数に次元削減する
以下は、AutoEncoder により、「0~9」の手書き文字画像を3次元の潜在変数に次元削減したときの例です。
このような低次元の潜在変数を \(z\) とし、その確率分布を \(P(z)\) とします。
潜在変数 z をニューラルネットワークで求める
AutoEncoder 同様、潜在変数 \(z\) をニューラルネットワークによって求めていきます。
しかし、AutoEncoder のように、直接 \(z\) を求めるわけではありません。
分布 \(P(z|X)\) が正規分布に従うとし、ニューラルネットワークは、\(z\) をサンプリングするため、正規分布の平均 \(μ(X)\) と、分散 \(σ(X)\) を出力します。
(詳しくは後で解説します)
ニューラルネットワークの出力から平均と分散を求め、\(N(μ(X),σ(X))\) から \(z\) をサンプリングし、その \(z\) から入力データ \(X\) の復元を行います。
そもそもの目的
ネットワーク構造は、比較的簡単にお話しできましたが、損失関数の理解は難しいです。
VAE を実装するにあたって、私たちが考えるべき損失関数とは何でしょうか。
生成モデルの目的は、データの分布、すなわち \(P(X)\) を求めることでした。
尤度(ゆうど)の最大化をすることで確率分布を推定する
確率分布を求める方法として、尤度の最大化があげられます。
確率分布 \(P(X)\) が、何らかのパラメータ \(θ\)(普通は平均とか分散をあらわす)で表されているとします。
最尤推定で確率分布を求める際の尤度関数は、同時確率分布に等しく、以下のようになります。
$$P_{\theta}(X) = \prod_{i = 1}^n P_{\theta} (x_{i})$$
また、尤度関数は、普通対数を取ります。
尤度が確率の掛け算であるため、対数を取ることで、微分の計算が和で済むからです。
$$\log (P_{\theta} (X)) = \sum_{i = 1}^n \log (P_{\theta} (x_{i})) \dots ①$$
この尤度関数を最大化するようなパラメータ\(θ\)を求めることで、最もデータ\(X\)の生成分布に近い分布が得られます。
$$ar g \max_{\theta} \sum_{i =1}^n \log (P_{\theta}(x_{i}))$$
損失関数
対数尤度関数の最大化が目的となったので、さらに式を変形します。
観測データは、潜在変数により生成されたと考えることもできるため、\(z\) により \(P(X)\) を周辺化し、式変形すると以下のようになります。
$$\begin{eqnarray}
\log(P_{\theta}(X)) &=& \sum_{i = 1}^n \log(P_{\theta}(x_{i})) \dots ① \\
&=& \sum_{i = 1}^n \log \begin{pmatrix} \displaystyle \int P_{\theta} (x_{i} , z) dz \end{pmatrix} \\
&=& \sum_{i = 1}^n \log \begin{pmatrix} \displaystyle \int \frac{P_{\theta} (z|x_{i})P_{\theta}(x_{i} , z)}{P_{\theta}(z|x_{i})} dz \end{pmatrix} \\
\end{eqnarray}$$
ここで \(P(X|z)\) は、潜在変数 \(z\) が与えられたときの復元データの分布です。
そのため、ニューラルネットワークでいう Decoder 部は、この分布に基づいてサンプリングされたものが出力されます。
また逆に、\(P(z|X)\) はデータが与えられたときの潜在変数の分布であり、Decoder 同様、ネットワークの Encoder 部分は、この分布に基づいて \(z\) が観測されます。
AutoEncoder では、データ \(X\) と潜在変数 \(z\) の関係を点で求めていました。
しかし、VAE では、 \(P(z|X)\) を求めることでデータの低次元な分布を得ることができます。
そして、この\(P(z|X)\)を近似することで、対数尤度の最大化を解いていきます。
近似分布の平均と分散を推定する
近似した分布 \(q(z|X)\) は、正規分布に従うとし、先ほど述べたように、その平均と分散パラメータを Encoder により推定していきます。
\(q(z|X)\) で分布を仮定したら、イェンセンの不等式から対数尤度の下限を求めます。
$$\begin{eqnarray}
\displaystyle \sum_{i = 1}^n \log \begin{pmatrix} \displaystyle \int \frac{P_{\theta}(z|x_{i})P_{\theta}(x_{i},z)}{P_{\theta}(z|x_{i})}dz\end{pmatrix} &\approx& \displaystyle \sum_{i = 1}^n \log \begin{pmatrix} \displaystyle \int \frac{q_{\varphi}(z|x_{i})P_{\theta}(x_{i},z)}{q_{\varphi}(z|x_{i})}dz \end{pmatrix}\\
&≧& \displaystyle \sum_{i = 1}^n \displaystyle \int q_{\varphi}(z|x_{i}) \log \begin{pmatrix} \frac{P_{\theta}(x_{i} , z)}{q_{\varphi}(z|x_{i})}\end{pmatrix}dz\\
&=& \displaystyle \sum_{i = 1}^n L(x_{i},\theta,\varphi) \dots ②
\end{eqnarray}$$
②の下限は、変分下限(Variational Lower Bound)と言われており、VAE がそう呼ばれることの根拠となっています。
対数尤度の最大化をするには、この下限を押し上げればよいということになります。
対数尤度①と変分下限②の差を計算
ここで、対数尤度①と、右辺に出てきた変分下限②の差を計算してみましょう。
$$\begin{eqnarray}
&\sum_{i = 1}^n& \log (P_{\theta}(x_{i})) - L(x_{i},\theta,\varphi)\\
&=& \sum_{i = 1}^n \log (P_{\theta}(x_{i}))\displaystyle \int q_{\varphi}(z|x_{i})dz - \displaystyle \int q_{\varphi}(z|x_{i}) \log \begin{pmatrix} \frac{P_{\theta}(x_{i} , z)}{q_{\varphi}(z|x_{i})} \end{pmatrix}dz\\
&=& \sum_{i = 1}^n \displaystyle \int q_{\varphi}(z|x_{i})\begin{pmatrix} \log (P_{\theta}(x_{i})) - \log (P_{\theta}(x_{i},z)) + \log (q_{\varphi}(z|x_{i}))\end{pmatrix}dz\\
&=& \sum_{i = 1}^n \displaystyle \int q_{\varphi}(z|x_{i})\begin{pmatrix} \log (P_{\theta}(x_{i})) - \log (P_{\theta}(z|x_{i})) - \log (P_{\theta}(x_{i})) + \log(q_{\varphi}(z|x_{i}))\end{pmatrix}dz\\
&=& \sum_{i = 1}^n \displaystyle \int q_{\varphi}(z|x_{i})\begin{pmatrix} \log (q_{\varphi}(z|x_{i})) - \log (P_{\theta}(z|x_{i}))\end{pmatrix}dz\\
&=& \sum_{i = 1}^n KL[q_{\varphi}(z|x_{i})||P_{\theta}(z|x_{i})]
\end{eqnarray}$$
最後の式は、KL ダイバージェンスと呼ばれるもので、分布間の距離を表す指標です。
KL ダイバージェンスは、2つの分布が全く同じであるならば「0」を示します。

つまり、対数尤度は、以下の2つの項で表されることが分かりました。
$$\sum_{i = 1}^n \log (P_{\theta}(x_{i})) = \sum_{i = 1}^n L(x_{i},\theta,\varphi) + \sum_{i = 1}^n KL[q_{\varphi}(z|x_{i})||P_{\theta}(z|x_{i})]$$
第2項目は、近似精度がよくなれば、おのずと「0」に近づく非負値です。
そのため、対数尤度の最大化を行うには、変分下限の最大化を行えばよいことになります。
$$ar g \max_{\theta} \sum_{i =1}^n \log (P_{\theta}(x_{i})) = ar g \max_{\theta,\varphi} \sum_{i =1}^n L(x_{i},\theta,\varphi)$$
変分下限を式変形していくと、ようやくお目当ての損失関数が得られます。
$$\small \begin{eqnarray}
&\sum_{i = 1}^n& \log (P_{\theta}(x_{i}))-\displaystyle \int q_{\varphi}(z|x_{i})\begin{pmatrix} \log(q_{\varphi}(z|x_{i}))-\log(P_{\theta}(z))-\log(P_{\theta}(x_{i}|z))+\log(P_{\theta}(x_{i})) \end{pmatrix}dz\\
&=& \sum_{i = 1}^n - \displaystyle \int q_{\varphi}(z|x_{i})\begin{pmatrix} \log(q_{\varphi}(z|x_{i})) - \log(P_{\theta}(z)) -\log(P_{\theta}(x_{i}|z)) \end{pmatrix}dz\\
&=& \sum_{i = 1}^n \displaystyle \int q_{\varphi}(z|x_{i})\log(P_{\theta}(x_{i}|z))dz - KL[q_{\varphi}(z|x_{i})||P_{\theta}(z)]
\end{eqnarray}$$
この式を最大化することで、VAE は学習を進めていきます。
実装上の損失関数
損失関数は、このままでは積分計算が入っているため、積分が入らない形に変形します。
1項目の KL ダイバージェンスについて
まずは、1項目の KL ダイバージェンスについて説明します。
正規分布同士の KL ダイバージェンスは、以下のように計算されます。
$$\frac{1}{2}\sum_{j = 1}^J(1 + \log(\sigma_{j}(X))-\mu_{j}(X)^2 - \sigma_{j}(X)^2$$
\(J\) は、潜在変数の次元数です。
積分計算をゴリゴリやれば出るので、式は割愛します。
2項目の積分はサンプリング近似を実行
2項目に関して、積分計算を解くのが難しいので、サンプリング近似を行います。
$$\displaystyle \int q_{\varphi}(z|x_{i})\log(P_{\theta}(x_{i}|z))dz \approx \frac {1}{L} \sum_{i = 1}^L \log (P_{\theta}(x_{i}|z_{i}))$$
これは、確率分布 \(q(z|X)\) に対する期待値計算であるため、有限個のサンプルの平均で近似してしまおうというものです。
また、mnist データは、普通「0, 1」 のデータで表されているため、分布には、ベルヌーイ分布を仮定します。
さらに、学習時のバッチサイズが大きければ、\(L=1\) で十分です。
以上を元に、近似した値を求めていくと、以下のようになります。
$$\begin{eqnarray}
\log(P_{\theta}(x_{i}|z_{i})) &=& \log(f(z_{i})^{x_{i}}(1-f(z_{i})^{1-x_{i}}))\\
&=& x_{i}\log(f(z_{i}))+(1-x_{i})\log(1-f(z_{i}))
\end{eqnarray}$$
ただし、\(f\)は、潜在変数を入力とする Decoder の関数、つまりは、再構成後の画素値を出力します。
以上から、第2項の損失関数は、以下のように表されます。
$$\sum_{i = 1}^n x_{i} \log (f(z_{i}))+(1-x_{i}) \log(1-f(z_{i}))$$
実験結果
画像の再構成
AutoEncoder と同様に、再構成をしてみました。
元画像

再構成結果

潜在変数が二次元なので、間違えている部分もありますね。
VAE だからと言って、AutoEncoder よりも表現力が上がるわけではなさそうです。
それに画像がぼやけています。
※ VAE に限らず、ピクセル単位で誤差を計算するモデルは、すべて画像がぼけやすいです。
潜在変数の分布の可視化
潜在変数の散らばりも見てみましょう!
AutoEncoder のように無秩序ではなく、分布が正規分布に引き寄せられるようになりました。
この分布であれば、本来は、非線形であるそれぞれの数字の分布を線形分離することもできそうですね。
連続的な数字画像の生成
では今度は、この二次元に落とし込んだ潜在変数上で、画像を滑らかに変化させてみましょう!
潜在変数の値を徐々に変化させ、その値を Decoder に通して画像を生成しました。

「2」⇒「4」⇒「9」⇒「3」⇒「5」⇒「3」⇒「8」と、左上から右下にジグザグに見ていくことで、数字が徐々に変化していく様子が分かります。
さいごに
確率モデルを扱う VAE を用いることで、画像を連続的に変化をさせることが可能になりました。
今回は、数字のみで実験しましたが、VAE を用いれば、顔画像であろうと衣類であろうと様々なものを連続的に変化させ、モーフィングさせることができます。
面白いので、ぜひ、ご自身の手でも確かめてみてくださいね!
(株)ライトコードは、WEB・アプリ・ゲーム開発に強い、「好きを仕事にするエンジニア集団」です。
機械学習でのシステム開発依頼・お見積もりはこちらまでお願いします。
また、機械学習系エンジニアを積極採用中です!詳しくはこちらをご覧ください。
※現在、多数のお問合せを頂いており、返信に、多少お時間を頂く場合がございます。
こちらの記事もオススメ!
ソースコード全体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | import torch import torchvision import torchvision.datasets as dset from torch import nn from torch.autograd import Variable from torch.utils.data import DataLoader from torchvision import transforms from torchvision.utils import save_image from mpl_toolkits.mplot3d import axes3d from torchvision.datasets import MNIST import torch.nn.functional as F import os import pylab import math import matplotlib.pyplot as plt #gpuデバイスがあるならgpuを使う #ないならcpuで device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") #カレントディレクトリにdc_imgというフォルダが作られる if not os.path.exists('./dc_img'): os.mkdir('./dc_img') #カレントディレクトリにdataというフォルダが作られる if not os.path.exists('./data'): os.mkdir('./data') num_epochs = 5#エポック数 batch_size = 30 #バッチサイズ learning_rate = 1e-3 #学習率 train =True#Trueなら訓練用データ、Falseなら検証用データを使う pretrained =False #学習済みのモデルを使うときはここをTrueに latent_dim = 2 #最終的に落とし込む次元数 save_img = True #元画像と再構成画像を保存するかどうか、バッチサイズが大きいときは保存しない方がいい def to_img(x): x = x x = x.clamp(0, 1) return x #画像データを前処理する関数 transform = transforms.Compose([ transforms.RandomResizedCrop(32, scale=(1.0, 1.0), ratio=(1., 1.)), transforms.ToTensor(), ]) #このコードで自動で./data/以下にm-nistデータがダウンロードされる trainset = torchvision.datasets.MNIST(root='./data/', train=True, download=True, transform=transform) #このコードで自動で./data/以下にm-nistデータがダウンロードされる testset = torchvision.datasets.MNIST(root='./data/', train=False, download=True, transform=transform) #学習時なら訓練用データを用いる if train: dataloader = DataLoader(trainset, batch_size=batch_size, shuffle=True) #テスト時なら検証用データを用いる else: dataloader = DataLoader(testset, batch_size=batch_size, shuffle=True) #ネットワーク定義 class VAE(nn.Module): def __init__(self, z_dim): super(VAE, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d(1, 64, 3, stride=1, padding=1), # b, 64, 32, 32 nn.BatchNorm2d(64), nn.LeakyReLU(0,True), nn.MaxPool2d(2) # b, 64, 16, 16 ) self.conv2 = nn.Sequential( nn.Conv2d(64, 128, 3, stride=1, padding=1), # b, 128, 16, 16 nn.BatchNorm2d(128), nn.LeakyReLU(0,True), nn.MaxPool2d(2) # b, 128, 8, 8 ) self.conv3 = nn.Sequential( nn.Conv2d(128, 256, 3, stride=1, padding=1), # b, 256, 8, 8 nn.BatchNorm2d(256), nn.LeakyReLU(0,True), nn.MaxPool2d(2) # b, 256, 4, 4 ) self.conv4 = nn.Sequential( nn.Conv2d(256, 512, 4, stride=1, padding=0), # b, 512, 1, 1 nn.BatchNorm2d(512), nn.LeakyReLU(0,True), ) self.mean = nn.Sequential( nn.Linear(512,latent_dim),# b, 512 ==> b, latent_dim ) self.var = nn.Sequential( nn.Linear(512,latent_dim),# b, 512 ==> b, latent_dim ) self.decoder = nn.Sequential( nn.Linear(latent_dim,512),# b, latent_dim ==> b, 512 nn.BatchNorm1d(512), nn.LeakyReLU(0,True), ) self.convTrans1 = nn.Sequential( nn.ConvTranspose2d(512, 256, 4, stride=2,padding = 0), # b, 256, 4, 4 nn.BatchNorm2d(256), nn.LeakyReLU(0,True), ) self.convTrans2 = nn.Sequential( nn.ConvTranspose2d(256, 128, 4, stride=2,padding = 1), # b, 128, 8, 8 nn.BatchNorm2d(128), nn.LeakyReLU(0,True), ) self.convTrans3 = nn.Sequential( nn.ConvTranspose2d(128, 64, 4, stride=2,padding = 1), # b, 64, 16, 16 nn.BatchNorm2d(64), nn.LeakyReLU(0,True), ) self.convTrans4 = nn.Sequential( nn.ConvTranspose2d(64, 1, 4, stride=2,padding = 1), # b, 3, 32, 32 nn.BatchNorm2d(1), nn.Sigmoid() ) #Encoderの出力に基づいてzをサンプリングする関数 #誤差逆伝搬ができるようにreparameterization trickを用いる def _sample_z(self, mean, var): std = var.mul(0.5).exp_() eps = Variable(std.data.new(std.size()).normal_()) return eps.mul(std).add_(mean) #Encoder def _encoder(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) x = self.conv4(x) x = x.view(-1,512) mean = self.mean(x) var = self.var(x) return mean,var #Decoder def _decoder(self, z): z = self.decoder(z) z = z.view(-1,512,1,1) x = self.convTrans1(z) x = self.convTrans2(x) x = self.convTrans3(x) x = self.convTrans4(x) return x def forward(self, x): # xは元画像 mean,var = self._encoder(x) #Decoderの出力はlog σ^2を想定 z = self._sample_z(mean, var) #潜在変数の分布に基づいてzをサンプリング x = self._decoder(z) #サンプリングしたzに対して画像を再構成 return x,mean,var,z def loss(self, x): mean, var = self._encoder(x) #Decoderの出力はlog σ^2を想定 KL = -0.5 * torch.mean(torch.sum(1 + var- mean**2 - var.exp())) #KLダイバージェンス z = self._sample_z(mean, var) #潜在変数の分布に基づいてzをサンプリング y = self._decoder(z) #サンプリングしたzに対して画像を再構成 delta = 1e-7 #logの中身がマイナスにならないように微小な値を与える reconstruction = torch.mean(torch.sum(x * torch.log(y+delta) + (1 - x) * torch.log(1 - y +delta))) #再構成誤差 lower_bound = [-KL, reconstruction] return -sum(lower_bound),y,mean,var,z def main(): #ネットワーク宣言 model = VAE(latent_dim).to(device) #事前に学習したモデルがあるならそれを使う if pretrained: param = torch.load('./conv_Variational_autoencoder_{}dim.pth'.format(latent_dim)) model.load_state_dict(param) #最適化法はAdamを選択 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5) for epoch in range(num_epochs): itr = 0 print(epoch) for data in dataloader: itr+=1 img, num = data #img --> [batch_size,1,32,32] #num --> [batch_size,1] #imgは画像本体 #numは画像に対する正解ラベル #ただし、学習時にnumは使わない #imgをデバイスに乗っける img = Variable(img).to(device) # ===================forward===================== #outputが再構成画像、latentは次元削減されたデータ if train == False: output,mu,var,latent = model(img) #学習時であれば、ネットワークパラメータを更新 if train: #lossを計算 #元画像と再構成後の画像が近づくように学習 loss,output,mu,var,latent = model.loss(img) # ===================backward==================== #勾配を初期化 optimizer.zero_grad() #微分値を求める loss.backward() #パラメータの更新 optimizer.step() print('{} {}'.format(itr,loss)) # ===================log======================== #データをtorchからnumpyに変換 z = latent.cpu().detach().numpy() num = num.cpu().detach().numpy() #次元数が3の時のプロット if latent_dim == 3: fig = plt.figure(figsize=(15, 15)) ax = fig.add_subplot(111, projection='3d') ax.scatter(z[:, 0], z[:, 1], z[:, 2], marker='.', c=num, cmap=pylab.cm.jet) for angle in range(0,360,60): ax.view_init(30,angle) plt.savefig("./fig{}.png".format(angle)) #次元数が2の時のプロット if latent_dim == 2: plt.figure(figsize=(15, 15)) plt.scatter(z[:, 0], z[:, 1], marker='.', c=num, cmap=pylab.cm.jet) plt.colorbar() plt.grid() plt.savefig("./fig.png") #元画像と再構成後の画像を保存するなら if save_img: value = int(math.sqrt(batch_size)) pic = to_img(img.cpu().data) pic = torchvision.utils.make_grid(pic,nrow = value) save_image(pic, './dc_img/real_image_{}.png'.format(epoch)) #元画像の保存 pic = to_img(output.cpu().data) pic = torchvision.utils.make_grid(pic,nrow = value) save_image(pic, './dc_img/image_{}.png'.format(epoch)) #再構成後の画像の保存 #もし学習時ならモデルを保存 #バージョン管理は各々で if train == True: torch.save(model.state_dict(), './conv_Variational_autoencoder_{}dim.pth'.format(latent_dim)) if __name__ == '__main__': main() |
ライトコードよりお知らせ






一緒に働いてくれる仲間を募集しております!
ライトコードでは、仲間を募集しております!
当社のモットーは「好きなことを仕事にするエンジニア集団」「エンジニアによるエンジニアのための会社」。エンジニアであるあなたの「やってみたいこと」を全力で応援する会社です。
また、ライトコードは現在、急成長中!だからこそ、あなたにお任せしたいやりがいのあるお仕事は沢山あります。「コアメンバー」として活躍してくれる、あなたからのご応募をお待ちしております!
なお、ご応募の前に、「話しだけ聞いてみたい」「社内の雰囲気を知りたい」という方はこちらをご覧ください。
ライトコードでは一緒に働いていただける方を募集しております!
採用情報はこちら書いた人はこんな人

ITエンタメ2021.01.12【スティーブ・ウォズニアック】アップルコンピューターのもう一人の創業者!
IT技術2021.01.11React Hooks登場でコンポーネントはどう変わった?
IT技術2021.01.05【Unity】Rigidbodyの基本
IT技術2021.01.04【Unity】ARkit3を使ったARアプリを開発する方法を解説(AR foundation,iOS)