深度学习GAN网络之DCGAN
核心要点:
1. 希望能让CNN在无监督学习上,达到与监督学习一样的成功
2. 通过架构约束,构建了深度卷积生成对抗网络(DCGAN)
3. 证明了DCGAN是目前先进的无监督学习网络
4. 证明了DCGAN的生成器和判别器学习到了从物体细节到整体场景的多层次表征
5. 证明了DCGAN判别器提取的图像特征具有很好的泛化性
研究背景:
表征学习:
• 表征(representation)、特征(feature)、编码(code)
• 好的表征
• 具有很强的表示能力,即同样大小的向量可以表示更多信息
• 使后续的学习任务变得简单,即需要包含更高层的语义信息
• 具有泛化性,可以应用到不同领域
• 表征学习的方式
• 无监督表征学习
• 有监督表征学习
模型可解释性:
• 决策树就是一个具有良好可解释性的模型
• 使用特征可视化方法
• 使用数据分析,可以找到数据中一些具有代表性和不具代表性的样本
• NIPS 2017会议上,Yann LeCun:人类大脑是非常有限的,我们没有那么多脑容量去研究所有东西的可解释性
数据集:
LSUN--加州大学伯克利分校发布,包含十个场景类别和二十个对象类别,主要包含了卧室、客厅、教室等场景图像,共计约 100 万张标记图像
SVHN--街景门牌号码数据集,与MNIST数据集相似,单据有更多标签数据(超过六十万的图像),从谷歌街景中收集得到
研究成果:



研究意义:
• 早期的GAN在图像上仅局限MNIST这样的简单数据集中,DCGAN使GAN在图像生成任务上的效果大大提升
• DCGAN几乎奠定了GAN的标准架构,之后GAN的研究者们不用再过多关注模型架构和稳定性,可以把更多的精力放在任务本身上,从而促进了GAN在16年的蓬勃发展
• 开创了GAN在图像编辑上的应用
模型结构:
• 所有的pooling层使用strided卷积(判别器)和fractional-strided卷积(生成器)进行替换
• 使用Batch Normalization
• 移除全连接的隐层,让网络可以更深
• 在生成器上,除了输出层使用Tanh外,其它所有层的激活函数都使用ReLU
• 判别器所有层的激活函数都使用LeakyReLU

训练参数:
• 训练图像的预处理,只做了到 [-1, 1] 的值域缩放
• 使用mini-batch随机梯度下降来训练网络, batch size大小为128
• 采用均值为0 标准差为0.02的正态分布,来对所有权重进行初始化
• 对于LeakyReLU激活函数,leak的斜率设置为0.2
• 优化器使用Adam,而不是此前GAN网络用的momentum
• Adam的学习速率使用0.0002,而非原论文建议的0.001
• Adam的参数momentum term β1,原论文建议的0.9会导致训练震荡和不稳定,将其减少至0.5可以让训练更加稳定
图像生成:
LSUN
• 没有使用 Data Augmentation
• 在LSUN上训练一个3072-128-3072的自编码器,用它从图像中提取128维特征,再经过ReLU层激活后作为图像的语义hash值
• 对生成图像和训练集使用上面的编码器,提取128维的语义hash值,进行重复性检测
• 检测到了大约275000左右数量的重复数据(LSUN数据集大小为300多万)

FACES
• 从DBpedia上获取人名,并保证他们都是当代人
• 用这些人名在网络上搜索,收集其中包含人脸的图像,得到了来自1万人的300万张图像
• 使用OpenCV的人脸检测算法,截取筛选出较高分辨率的人脸,最终得到了大约35万张人脸图像
• 训练时没有使用 Data Augmentation

Imageenet
• 32 × 32 min-resized center crops
• 训练时没有使用 Data Augmentation

无监督表征学习:
CIFAR-10
• 在Imagenet-1k上训练DCGAN
• 使用判别器所有层的卷积特征,分别经过最大池化层,在每一层上得到一个空间尺寸为4*4的特征,再把这些特征做flattened和concatenated,最终得到28672维的向量表示
• 用一个SVM分类器,基于这些特征向量和类别label进行有监督训练

SVHN
• 使用与CIFAR-10实验相同的处理流程
• 使用10000个样本来作为验证集,将其用在超参数和模型的选择上
• 随机选择1000个类别均衡的样本,用来训练正则化线性L2-SVM分类器
• 使用相同的生成器结构、相同的数据集,从头训练有监督CNN模型,并使用验证集进行超参数搜索

模型可视化:
• 在大型图像数据集上训练的有监督CNN模型,可以提取出很好的图像feature
• 希望在大型图像数据集上训练的无监督模型DCGAN,也能学习到不错的层级结构特征
• 使用对判别器的最后一个卷积层使用特征可视化
• 判别器学习到了卧室的典型部分,例如床和窗户
• 使用随机初始化还未经训练的模型来作为对照

隐空间分析:
隐变量空间漫游
•在latent space上walking可以判断出模型是否是单纯在记住输入(如果生成图像过渡非常sharp),以及模式崩溃的方式
• 如果在latent space中walking导致生成图像的语义变化(例如添加或删除了对象),我们可以推断模型已经学习到了相关和有趣的表征


去除特定的对象
• 为了研究模型是如何对图像中的特定物体进行表征的,尝试从生成图像中把窗口进行移除
• 选出150个样本,手动标注了52个窗口的bounding box
• 在倒数第二层的conv layer features中,训练一个简单的逻辑回归模型,来判断一个feature activation是否在窗口中
• 使用这个模型,将所有值大于0的特征(总共200个),都从空间位置中移除

人脸样本上的矢量运算
• vector("King")-vector("Man")+vector("Woman")的结果和向量Queen很接近
• 对单个样本进行操作的结果不是很稳定,而如果使用三个样本的平均值,结果就会好很多



总结展望:
• 提出了一套更稳定的架构来训练生成对抗性网络
• 展示了对抗性网络可以很好的学习到图像的表征,并使用在监督学习和生成式的建模上
• 模式崩溃问题仍然存在
• 可以再延伸应用到其他领域,例如视频(做帧级的预测)和声频 (用于语音合成的预训练特征)
• 可以对latent space进行更进一步的研究
关键点:
• 将CNN网络的最新成果应用到GAN
• 精细的超参数调节尝试
• 对latent space的多维度分析
创新点:
• 将GAN应用到表征学习上
• 对生成图像中的特定对象进擦除
• 对生成图像进行矢量运算
代码:
一、使用MNIST数据集生成数字:
import argparse
import os
import numpy as np
import mathimport torchvision.transforms as transforms
from torchvision.utils import save_imagefrom torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variableimport torch.nn as nn
import torch.nn.functional as F
import torchos.makedirs("images", exist_ok=True)parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=200, help="number of epochs of training")
parser.add_argument("--batch_size", type=int, default=64, help="size of the batches")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the latent space")
parser.add_argument("--img_size", type=int, default=32, help="size of each image dimension")
parser.add_argument("--channels", type=int, default=1, help="number of image channels")
parser.add_argument("--sample_interval", type=int, default=400, help="interval between image sampling")
opt = parser.parse_args()
print(opt)cuda = True if torch.cuda.is_available() else Falsedef weights_init_normal(m):classname = m.__class__.__name__if classname.find("Conv") != -1:torch.nn.init.normal_(m.weight.data, 0.0, 0.02)elif classname.find("BatchNorm2d") != -1:torch.nn.init.normal_(m.weight.data, 1.0, 0.02)torch.nn.init.constant_(m.bias.data, 0.0)class Generator(nn.Module):def __init__(self):super(Generator, self).__init__()self.init_size = opt.img_size // 4self.l1 = nn.Sequential(nn.Linear(opt.latent_dim, 128 * self.init_size ** 2))self.conv_blocks = nn.Sequential(nn.BatchNorm2d(128),nn.Upsample(scale_factor=2),nn.Conv2d(128, 128, 3, stride=1, padding=1),nn.BatchNorm2d(128, 0.8),nn.LeakyReLU(0.2, inplace=True),nn.Upsample(scale_factor=2),nn.Conv2d(128, 64, 3, stride=1, padding=1),nn.BatchNorm2d(64, 0.8),nn.LeakyReLU(0.2, inplace=True),nn.Conv2d(64, opt.channels, 3, stride=1, padding=1),nn.Tanh(),)def forward(self, z):out = self.l1(z)out = out.view(out.shape[0], 128, self.init_size, self.init_size)img = self.conv_blocks(out)return imgclass Discriminator(nn.Module):def __init__(self):super(Discriminator, self).__init__()def discriminator_block(in_filters, out_filters, bn=True):block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1), nn.LeakyReLU(0.2, inplace=True), nn.Dropout2d(0.25)]if bn:block.append(nn.BatchNorm2d(out_filters, 0.8))return blockself.model = nn.Sequential(*discriminator_block(opt.channels, 16, bn=False),*discriminator_block(16, 32),*discriminator_block(32, 64),*discriminator_block(64, 128),)# The height and width of downsampled image
ds_size = opt.img_size // 2 ** 4self.adv_layer = nn.Sequential(nn.Linear(128 * ds_size ** 2, 1), nn.Sigmoid())def forward(self, img):out = self.model(img)out = out.view(out.shape[0], -1)validity = self.adv_layer(out)return validity# Loss function
adversarial_loss = torch.nn.BCELoss()# Initialize generator and discriminator
generator = Generator()
discriminator = Discriminator()if cuda:generator.cuda()discriminator.cuda()adversarial_loss.cuda()# Initialize weights
generator.apply(weights_init_normal)
discriminator.apply(weights_init_normal)# Configure data loader
os.makedirs("../../data/mnist", exist_ok=True)
dataloader = torch.utils.data.DataLoader(datasets.MNIST("../../data/mnist",train=True,download=True,transform=transforms.Compose([transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]),),batch_size=opt.batch_size,shuffle=True,
)# Optimizers
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor# ----------
# Training
# ----------for epoch in range(opt.n_epochs):for i, (imgs, _) in enumerate(dataloader):# Adversarial ground truths
valid = Variable(Tensor(imgs.shape[0], 1).fill_(1.0), requires_grad=False)fake = Variable(Tensor(imgs.shape[0], 1).fill_(0.0), requires_grad=False)# Configure input
real_imgs = Variable(imgs.type(Tensor))# -----------------# Train Generator# -----------------optimizer_G.zero_grad()# Sample noise as generator input
z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))# Generate a batch of images
gen_imgs = generator(z)# Loss measures generator's ability to fool the discriminator
g_loss = adversarial_loss(discriminator(gen_imgs), valid)g_loss.backward()optimizer_G.step()# ---------------------# Train Discriminator# ---------------------optimizer_D.zero_grad()# Measure discriminator's ability to classify real from generated samples
real_loss = adversarial_loss(discriminator(real_imgs), valid)fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)d_loss = (real_loss + fake_loss) / 2d_loss.backward()optimizer_D.step()print("[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
% (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item()))batches_done = epoch * len(dataloader) + iif batches_done % opt.sample_interval == 0:save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)
结果展示:

二、使用微笑数据集生成笑脸:
#!/usr/bin/env python
# coding: utf-8# 目录
#
# 1. [导入所需包](#导入所需包)
#
# 2. [基本参数配置](#基本参数配置)
#
# 3. [导入数据集](#导入数据集)
#
# 4. [定义生成器与判别器](#定义生成器与判别器)
#
# 5. [初始化生成器和判别器](#初始化生成器和判别器)
#
# 6. [定义损失函数](#定义损失函数)
#
# 7. [开始训练](#开始训练)
#
# 8. [绘制损失曲线](#绘制损失曲线)
#
# 9. [真假对比](#真假对比)
# # 本项目使用 DCGAN 模型,在自建数据集上进行实验。
#
# 本项目使用的数据集是人脸嘴巴区域——微笑表情的数据集
#
# 
#
#
#
# 数据集文件夹结构如下,图片供 4357 张
#
# ```cmd
# ├─mouth
# │ └─smile
# ├─1smile.jpg
# ├─2smile.jpg
# ├─3smile.jpg
# └─....
# ```
#
# 同时,创建一个 `out` 文件夹来保存训练的中间结果,主要就是看 DCGAN 是如何从一张噪声照片生成我们期待的图片# In[1]:import os, sys
import time
import shutil
if os.path.exists("out"):print("移除现有 out 文件夹!")if sys.platform.startswith("win"):shutil.rmtree("./out")else:os.system("rm -r ./out") time.sleep(1)
print("创建 out 文件夹!")
os.mkdir("./out")# 1. 在加载该 NoteBook 文件时,会自动加载数据集至 `./download/mouth` 文件夹下。若没有自动加载数据集,则需要手动加载,手动加载方式如下:
#
# **点击本页面左方 `天池` 按钮(需要在 CPU 环境下),点击 `mouth` 旁边的下载按钮,就会自动加载数据集了!**
# 
#
#
# 运行下面代码,对数据集进行解压。
#
# 由于图片数量多,解压需要一定时间
# # In[1]:#get_ipython().system('unzip mouth.zip -d ./mouth')
#print("解压完毕!")# # 导入所需包# In[2]:#from __future__ import print_function
#%matplotlib inline
import argparse
import os
import random
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.backends.cudnn as cudnn
import torch.optim as optim
import torch.utils.data
import torchvision.datasets as dset
import torchvision.transforms as transforms
import torchvision.utils as vutils
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTMLos.environ['KMP_DUPLICATE_LIB_OK'] = 'True'# # 基本参数配置# In[3]:# 设置一个随机种子,方便进行可重复性实验
manualSeed = 999
print("Random Seed: ", manualSeed)
random.seed(manualSeed)
torch.manual_seed(manualSeed)# 数据集所在路径
dataroot = "mouth/"
# 数据加载的进程数
workers = 0
# Batch size 大小
batch_size = 64
# Spatial size of training images. All images will be resized to this
# size using a transformer.
# 图片大小
image_size = 64# 图片的通道数
nc = 3
# Size of z latent vector (i.e. size of generator input)
nz = 100
# Size of feature maps in generator
ngf = 64
# Size of feature maps in discriminator
ndf = 64
# Number of training epochs
num_epochs = 10
# Learning rate for optimizers
lr = 0.0003
# Beta1 hyperparam for Adam optimizers
beta1 = 0.5
# Number of GPUs available. Use 0 for CPU mode.
ngpu = 1# Decide which device we want to run on
device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")# # 导入数据集# In[4]:# We can use an image folder dataset the way we have it setup.
# Create the dataset
dataset = dset.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)# 简单看一下我们的原始数据集长啥样# In[5]:# 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)))
# plt.show()# # 定义生成器与判别器# In[6]:# 权重初始化函数,为生成器和判别器模型初始化
def weights_init(m):classname = m.__class__.__name__if classname.find('Conv') != -1:nn.init.normal_(m.weight.data, 0.0, 0.02)elif classname.find('BatchNorm') != -1:nn.init.normal_(m.weight.data, 1.0, 0.02)nn.init.constant_(m.bias.data, 0)# Generator Code
class Generator(nn.Module):def __init__(self, ngpu):super(Generator, self).__init__()self.ngpu = ngpuself.main = nn.Sequential(# input is Z, going into a convolution
nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),nn.BatchNorm2d(ngf * 8),nn.ReLU(True),# state size. (ngf*8) x 4 x 4
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),nn.BatchNorm2d(ngf * 4),nn.ReLU(True),# state size. (ngf*4) x 8 x 8
nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),nn.BatchNorm2d(ngf * 2),nn.ReLU(True),# state size. (ngf*2) x 16 x 16
nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),nn.BatchNorm2d(ngf),nn.ReLU(True),# state size. (ngf) x 32 x 32
nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),nn.Tanh()# state size. (nc) x 64 x 64
)def forward(self, input):return self.main(input)class Discriminator(nn.Module):def __init__(self, ngpu):super(Discriminator, self).__init__()self.ngpu = ngpuself.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)# # 初始化生成器和判别器# In[7]:# 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)))# Apply the weights_init function to randomly initialize all weights
# to mean=0, stdev=0.2.
netG.apply(weights_init)
# Print the model
print(netG)# 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)))# Apply the weights_init function to randomly initialize all weights
# to mean=0, stdev=0.2.
netD.apply(weights_init)# Print the model
print(netD)# # 定义损失函数# In[8]:# Initialize BCELoss function
criterion = nn.BCELoss()# # 开始训练# In[9]:# 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.0
fake_label = 0.0# Setup Adam optimizers for both G and D
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))# Training Loop# Lists to keep track of progress
img_list = []
G_losses = []
D_losses = []
iters = 0print("Starting Training Loop...")
# For each epoch
for epoch in range(num_epochs):import timestart = time.time()# 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)))############################# Train with all-real batch
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 all-real batch
errD_real = criterion(output, label)# Calculate gradients for D in backward pass
errD_real.backward()D_x = output.mean().item()## Train with all-fake batch# Generate batch of latent vectors
noise = torch.randn(b_size, nz, 1, 1, device=device)# Generate fake image batch with G
fake = netG(noise)label.fill_(fake_label)# Classify all fake batch with D
output = netD(fake.detach()).view(-1)# Calculate D's loss on the all-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)))###########################
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
if i % 50 == 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 fixed_noise
if (iters % 20 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)):with torch.no_grad():fake = netG(fixed_noise).detach().cpu()img_list.append(vutils.make_grid(fake, padding=2, normalize=True))i = vutils.make_grid(fake, padding=2, normalize=True)fig = plt.figure(figsize=(8, 8))plt.imshow(np.transpose(i, (1, 2, 0)))plt.axis('off') # 关闭坐标轴
plt.savefig("out/%d_%d.png" % (epoch, iters))plt.close(fig)iters += 1print('time:', time.time() - start)# # 绘制损失曲线# In[10]: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()
plt.savefig('Loss-curve.jpg', bbox_inches='tight')# # 真假对比# In[14]:# Grab a batch of real images from the dataloader
# real_batch = next(iter(dataloader))# Plot the real images
plt.figure(figsize=(15,15))
plt.subplot(1,2,1)
plt.axis("off")
plt.title("Real Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0)))# Plot the fake images from the last epoch
plt.subplot(1,2,2)
plt.axis("off")
plt.title("Fake Images")
plt.imshow(np.transpose(img_list[-1],(1,2,0)))
plt.show()
plt.savefig('Real_Images-VS-Fake_Images.jpg', bbox_inches='tight')# In[ ]:结果展示:

越往后面生成的图像越真实
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
