🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
在上一章中,我们了解了如何利用生成对抗网络( GAN ) 来生成逼真的图像。在本章中,我们将学习如何利用 GAN 来处理图像。我们将了解使用 GAN 生成图像的两种变体——有监督和无监督方法。在监督方法中,我们将提供输入和输出对组合以基于输入图像生成图像,我们将在 Pix2Pix GAN 中了解这一点。在无监督方法中,我们将指定输入和输出,但是,我们不会提供输入和输出之间的一一对应关系,而是期望 GAN 学习这两个类的结构,并将图像从一个类到另一个,我们将在 CycleGAN 中学习。
另一类无监督图像处理涉及从随机向量的潜在空间生成图像,并查看图像如何随着潜在向量值的变化而变化,我们将在自定义图像上利用 StyleGAN部分了解这一点。最后,我们将了解如何利用预训练的 GAN – SRGAN,它有助于将低分辨率图像转换为高分辨率图像。
具体来说,我们将了解以下主题:
想象一个场景,我们有成对的相互关联的图像(例如,对象边缘的图像作为输入,对象的实际图像作为输出)。给定的挑战是,我们要在给定对象边缘的输入图像的情况下生成图像。在传统环境中,这将是输入到输出的简单映射,因此是监督学习问题。但是,假设您正在与一个创意团队合作,该团队正试图为产品提供全新的外观。在这种情况下,监督学习没有多大帮助——因为它只能从历史中学习。
在本节中,我们将了解从鞋子的手绘涂鸦(轮廓)生成鞋子图像的架构。我们将从涂鸦中生成逼真图像的策略如下:
让我们编写策略代码:
1.导入数据集并安装相关包:
- try:
- !wget https://bit.ly/3kiuN93
- !mv 3kiuN93 ShoeV2.zip
- !unzip ShoeV2.zip
- !unzip ShoeV2_F/ShoeV2_photo.zip
- except:
- !wget https://www.dropbox.com/s/g6b6gtvmdu0h77x/ShoeV2_photo.zip
- !pip install torch_snippets
- from torch_snippets import *
- device = 'cuda' if torch.cuda.is_available() else 'cpu'
前面的代码下载了鞋子的图片。下载图片示例如下:

对于我们的问题,我们想在给定轮廓(边缘)的情况下绘制鞋子并采样鞋子的补丁颜色。在下一步中,我们将获取给定鞋子图像的边缘。这样,我们可以训练一个模型,在给定鞋子的轮廓和样本补丁颜色的情况下重建鞋子的图像。
2.定义一个函数以从下载的图像中获取边缘:
- def detect_edges(img):
- img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
- img_gray = cv2.bilateralFilter(img_gray, 5, 50, 50)
- img_gray_edges = cv2.Canny(img_gray, 45, 100)
- # invert black/white
- img_gray_edges = cv2.bitwise_not(img_gray_edges)
- img_edges=cv2.cvtColor(img_gray_edges,cv2.COLOR_GRAY2RGB)
- return img_edges
在前面的代码中,我们利用 OpenCV 包中可用的各种方法来获取图像中的边缘(有关 OpenCV 方法如何工作的更多详细信息,请参见第 18 章,使用 OpenCV 实用程序进行图像分析)。
3.定义图像转换管道(preprocess和normalize):
- IMAGE_SIZE = 256
- preprocess = T.Compose([
- T.Lambda(lambda x: torch.Tensor(x.copy())\
- .permute(2, 0, 1).to(device))
- ])
- normalize = lambda x: (x - 127.5)/127.5
4.定义数据集类 ( ShoesData)。该数据集类返回原始图像和带边缘的图像。我们将传递给网络的另一个细节是随机选择的区域中出现的色块。通过这种方式,我们使用户能够拍摄手绘轮廓图像,在图像的不同部分撒上所需的颜色,并生成新图像。此处显示了示例输入(第三张图像)和输出(第一张图像)(最好以彩色查看):

然而,我们在步骤 1 中获得的输入图像只是鞋子(第一张图像)——我们将使用它来提取鞋子的边缘(第二张图像)。此外,我们将在下一步中撒上颜色以获取前面图像的输入(第三张图像)-输出(第一张图像)组合。
在下面的代码中,我们将构建获取轮廓图像的类,洒上颜色,并返回一对颜色洒上的图像和原始鞋子图像(生成轮廓的图像):
- class ShoesData(Dataset):
- def __init__(self, items):
- self.items = items
- def __len__(self): return len(self.items)
- def __getitem__(self, ix):
- f = self.items[ix]
- try: im = read(f, 1)
- except:
- blank = preprocess(Blank(IMAGE_SIZE, \
- IMAGE_SIZE, 3))
- return blank, blank
- edges = detect_edges(im)
- im, edges = resize(im, IMAGE_SIZE), \
- resize(edges, IMAGE_SIZE)
- im, edges = normalize(im), normalize(edges)
- self._draw_color_circles_on_src_img(edges, im)
- im, edges = preprocess(im), preprocess(edges)
- return edges, im
- def _draw_color_circles_on_src_img(self, img_src, \
- img_target):
- non_white_coords = self._get_non_white_coordinates\
- (img_target)
- for center_y, center_x in non_white_coords:
- self._draw_color_circle_on_src_img(img_src, \
- img_target, center_y, center_x)
-
- def _get_non_white_coordinates(self, img):
- non_white_mask = np.sum(img, axis=-1) < 2.75
- non_white_y, non_white_x = np.nonzero(non_white_mask)
- # randomly sample non-white coordinates
- n_non_white = len(non_white_y)
- n_color_points = min(n_non_white, 300)
- idxs = np.random.choice(n_non_white, n_color_points, \
- replace=False)
- non_white_coords = list(zip(non_white_y[idxs], \
- non_white_x[idxs]))
- return non_white_coords
-
- def _draw_color_circle_on_src_img(self, img_src, \
- img_target, center_y, center_x):
- assert img_src.shape == img_target.shape
- y0, y1, x0, x1 = self._get_color_point_bbox_coords(\
- center_y, center_x)
- color = np.mean(img_target[y0:y1, x0:x1],axis=(0, 1))
- img_src[y0:y1, x0:x1] = color
-
- def _get_color_point_bbox_coords(self, center_y,center_x):
- radius = 2
- y0 = max(0, center_y-radius+1)
- y1 = min(IMAGE_SIZE, center_y+radius)
- x0 = max(0, center_x-radius+1)
- x1 = min(IMAGE_SIZE, center_x+radius)
- return y0, y1, x0, x1
-
- def choose(self): return self[randint(len(self))]
5.定义训练和验证数据对应的数据集和数据加载器:
- from sklearn.model_selection import train_test_split
- train_items, val_items = train_test_split(\
- Glob('ShoeV2_photo/*.png'), \
- test_size=0.2, random_state=2)
- trn_ds, val_ds = ShoesData(train_items), ShoesData(val_items)
-
- trn_dl = DataLoader( trn_ds, batch_size=32, shuffle=True)
- val_dl = DataLoader(val_ds, batch_size=32, shuffle=True)
6.定义生成器和鉴别器架构,它们利用权重初始化 ( weights_init_normal)UNetDown和UNetUp架构,就像我们在第 9 章,图像分割和第 10 章,对象检测和分割的应用中所做的那样,来定义GeneratorUNet和Discriminator架构。
- def 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 UNetDown(nn.Module):
- def __init__(self, in_size, out_size, normalize=True, \
- dropout=0.0):
- super(UNetDown, self).__init__()
- layers = [nn.Conv2d(in_size, out_size, 4, 2, 1, \
- bias=False)]
- if normalize:
- layers.append(nn.InstanceNorm2d(out_size))
- layers.append(nn.LeakyReLU(0.2))
- if dropout:
- layers.append(nn.Dropout(dropout))
- self.model = nn.Sequential(*layers)
-
- def forward(self, x):
- return self.model(x)
-
- class UNetUp(nn.Module):
- def __init__(self, in_size, out_size, dropout=0.0):
- super(UNetUp, self).__init__()
- layers = [
- nn.ConvTranspose2d(in_size, out_size, 4, 2, 1, \
- bias=False),
- nn.InstanceNorm2d(out_size),
- nn.ReLU(inplace=True),
- ]
- if dropout:
- layers.append(nn.Dropout(dropout))
-
- self.model = nn.Sequential(*layers)
-
- def forward(self, x, skip_input):
- x = self.model(x)
- x = torch.cat((x, skip_input), 1)
-
- return x
- class GeneratorUNet(nn.Module):
- def __init__(self, in_channels=3, out_channels=3):
- super(GeneratorUNet, self).__init__()
-
- self.down1 = UNetDown(in_channels,64,normalize=False)
- self.down2 = UNetDown(64, 128)
- self.down3 = UNetDown(128, 256)
- self.down4 = UNetDown(256, 512, dropout=0.5)
- self.down5 = UNetDown(512, 512, dropout=0.5)
- self.down6 = UNetDown(512, 512, dropout=0.5)
- self.down7 = UNetDown(512, 512, dropout=0.5)
- self.down8 = UNetDown(512, 512, normalize=False, \
- dropout=0.5)
-
- self.up1 = UNetUp(512, 512, dropout=0.5)
- self.up2 = UNetUp(1024, 512, dropout=0.5)
- self.up3 = UNetUp(1024, 512, dropout=0.5)
- self.up4 = UNetUp(1024, 512, dropout=0.5)
- self.up5 = UNetUp(1024, 256)
- self.up6 = UNetUp(512, 128)
- self.up7 = UNetUp(256, 64)
-
- self.final = nn.Sequential(
- nn.Upsample(scale_factor=2),
- nn.ZeroPad2d((1, 0, 1, 0)),
- nn.Conv2d(128, out_channels, 4, padding=1),
- nn.Tanh(),
- )
-
- def forward(self, x):
- d1 = self.down1(x)
- d2 = self.down2(d1)
- d3 = self.down3(d2)
- d4 = self.down4(d3)
- d5 = self.down5(d4)
- d6 = self.down6(d5)
- d7 = self.down7(d6)
- d8 = self.down8(d7)
- u1 = self.up1(d8, d7)
- u2 = self.up2(u1, d6)
- u3 = self.up3(u2, d5)
- u4 = self.up4(u3, d4)
- u5 = self.up5(u4, d3)
- u6 = self.up6(u5, d2)
- u7 = self.up7(u6, d1)
- return self.final(u7)
- class Discriminator(nn.Module):
- def __init__(self, in_channels=3):
- super(Discriminator, self).__init__()
-
- def discriminator_block(in_filters, out_filters, \
- normalization=True):
- """Returns downsampling layers of each
- discriminator block"""
- layers = [nn.Conv2d(in_filters, out_filters, \
- 4, stride=2, padding=1)]
- if normalization:
- layers.append(nn.InstanceNorm2d(out_filters))
- layers.append(nn.LeakyReLU(0.2, inplace=True))
- return layers
-
- self.model = nn.Sequential(
- *discriminator_block(in_channels * 2, 64, \
- normalization=False),
- *discriminator_block(64, 128),
- *discriminator_block(128, 256),
- *discriminator_block(256, 512),
- nn.ZeroPad2d((1, 0, 1, 0)),
- nn.Conv2d(512, 1, 4, padding=1, bias=False)
- )
-
- def forward(self, img_A, img_B):
- img_input = torch.cat((img_A, img_B), 1)
- return self.model(img_input)
7.定义generator和discriminator模型对象并获取摘要:
- generator = GeneratorUNet().to(device)
- discriminator = Discriminator().to(device)
- !pip install torch_summary
- from torchsummary import summary
- print(summary(generator, torch.zeros(3, 3, IMAGE_SIZE, \
- IMAGE_SIZE).to(device)))
- print(summary(discriminator, torch.zeros(3, 3, IMAGE_SIZE, \
- IMAGE_SIZE).to(device), torch.zeros(3, 3, \
- IMAGE_SIZE, IMAGE_SIZE).to(device)))
生成器架构总结如下:
判别器架构总结如下:
8.定义训练判别器的函数 ( discriminator_train_step):
- def discriminator_train_step(real_src, real_trg, fake_trg):
- d_optimizer.zero_grad()
- prediction_real = discriminator(real_trg, real_src)
- error_real = criterion_GAN(prediction_real, \
- torch.ones(len(real_src), 1, 16, 16)\
- .to(device))
- error_real.backward()
- prediction_fake = discriminator(real_src, \
- fake_trg.detach())
- error_fake = criteria_GAN(prediction_fake, \
- torch.zeros(len(real_src), 1, \
- 16, 16).to(device))
- error_fake.backward()
- d_optimizer.step()
- return error_real + error_fake
9.定义函数来训练生成器generator_train_step(fake_trg
- def generator_train_step(real_src, fake_trg):
- g_optimizer.zero_grad()
- prediction = discriminator(fake_trg, real_src)
-
- loss_GAN = criteria_GAN(prediction, torch.ones(\
- len(real_src), 1, 16, 16)\ .to
- (device))
- loss_pixel = criteria_pixelwise(fake_trg, real_trg)
- loss_G = loss_GAN + lambda_pixel * loss_pixel
-
- loss_G.backward()
- g_optimizer.step()
- return loss_G
请注意,在前面的代码中,除了生成器损失之外,我们还获取与loss_pixel给定轮廓的生成图像和真实图像之间的差异相对应的像素损失 ( ):
- denorm = T.Normalize((-1, -1, -1), (2, 2, 2))
- def sample_prediction():
- """Saves a generated sample from the validation set"""
- data = next(iter(val_dl))
- real_src, real_trg = data
- fake_trg = generator(real_src)
- img_sample = torch.cat([denorm(real_src[0]), \
- denorm(fake_trg[0]), \
- denorm(real_trg[0])], -1)
- img_sample = img_sample.detach().cpu()\
- .permute(1,2,0).numpy()
- show(img_sample, title='Source::Generated::GroundTruth', \
- sz=12)
10.将权重初始化 ( weights_init_normal) 应用于生成器和判别器模型对象:
- generator.apply(weights_init_normal)
- discriminator.apply(weights_init_normal)
11.指定损失标准和优化方法(criterion_GAN和criterion_pixelwise):
- criterion_GAN = torch.nn.MSELoss()
- criterion_pixelwise = torch.nn.L1Loss()
-
- lambda_pixel = 100
- g_optimizer = torch.optim.Adam(generator.parameters(), \
- lr=0.0002, betas=(0.5, 0.999))
- d_optimizer = torch.optim.Adam(discriminator.parameters(), \
- lr=0.0002, betas=(0.5, 0.999))
12.训练模型超过 100 个 epoch:
- epochs = 100
- log = Report(epochs)
- for epoch in range(epochs):
- N = len(trn_dl)
- for bx, batch in enumerate(trn_dl):
- real_src, real_trg = batch
- fake_trg = generator(real_src)
- errD = discriminator_train_step(real_src, real_trg, \
- fake_trg)
- errG = generator_train_step(real_src, fake_trg)
- log.record(pos=epoch+(1+bx)/N, errD=errD.item(), \
- errG=errG.item(), end='\r ')
- [sample_prediction() for _ in range(2)]
13.在样本手绘轮廓上生成:
[sample_prediction() for _ in range(2)]
上述代码生成以下输出:
请注意,在前面的输出中,我们生成了与原始图像颜色相似的图像。
在本节中,我们学习了如何利用图像的轮廓来生成图像。但是,这需要我们成对提供输入和输出,这有时可能是一个乏味的过程。在下一节中,我们将了解未配对的图像翻译,其中网络将计算翻译,而无需我们指定图像的输入和输出映射。
想象一个场景,我们要求您执行从一个类到另一个类的图像转换,但不提供输入和相应的输出图像来训练模型。但是,我们在两个不同的文件夹中为您提供这两个类别的图像。CycleGAN 在这种情况下就派上用场了。
在本节中,我们将学习如何训练 CycleGAN 将苹果的图像转换为橙子的图像,反之亦然。CycleGAN 中的循环是指我们将图像从一个类转换(转换)到另一个类并返回到原始类的事实。
在高层次上,我们将在此架构中具有三个单独的损失值(此处提供了更多详细信息):
在这里,我们将从高层次了解构建 CycleGAN 的步骤:
1.导入和预处理数据集
2.构建生成器和判别器网络 UNet 架构
3.定义两个生成器:
4.定义身份损失:
5.定义GAN 损失:
6.定义循环 损失:
7.优化三个损失的加权损失。
现在我们了解了这些步骤,让我们编写代码以将苹果转换为橙子,反之亦然,如下所示:
导入相关数据集和包:
1.下载并提取数据集:
- !wget https://www.dropbox.com/s/2xltmolfbfharri/apples_oranges.zip
- !unzip apples_oranges.zip
我们将处理的图像示例:
请注意,苹果和橙色图像之间没有一一对应的关系(与我们在利用 Pix2Pix GAN部分了解的轮廓到鞋子生成用例不同)。
- !pip install torch_snippets torch_summary
- import itertools
- from PIL import Image
- from torch_snippets import *
- from torchvision import transforms
- from torchvision.utils import make_grid
- from torchsummary import summary
2.定义图像转换管道 ( transform):
- IMAGE_SIZE = 256
- device = 'cuda' if torch.cuda.is_available() else 'cpu'
- transform = transforms.Compose([
- transforms.Resize(int(IMAGE_SIZE*1.33)),
- transforms.RandomCrop((IMAGE_SIZE,IMAGE_SIZE)),
- transforms.RandomHorizontalFlip(),
- transforms.ToTensor(),
- transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
- ])
3.定义数据集CycleGANDataset类apple(orange
- class CycleGANDataset(Dataset):
- def __init__(self, apples, oranges):
- self.apples = Glob(apples)
- self.oranges = Glob(oranges)
-
- def __getitem__(self, ix):
- apple = self.apples[ix % len(self.apples)]
- orange = choose(self.oranges)
- apple = Image.open(apple).convert('RGB')
- orange = Image.open(orange).convert('RGB')
- return apple, orange
-
- def __len__(self): return max(len(self.apples), \
- len(self.oranges))
- def choose(self): return self[randint(len(self))]
-
- def collate_fn(self, batch):
- srcs, trgs = list(zip(*batch))
- srcs=torch.cat([transform(img)[None] for img in srcs]\
- , 0).to(device).float()
- trgs=torch.cat([transform(img)[None] for img in trgs]\
- , 0).to(device).float()
- return srcs.to(device), trgs.to(device)
4.定义训练和验证数据集和数据加载器:
- trn_ds = CycleGANDataset('apples_train', 'oranges_train')
- val_ds = CycleGANDataset('apples_test', 'oranges_test')
-
- trn_dl = DataLoader(trn_ds, batch_size=1, shuffle=True, \ collate_fn=
- trn_ds.collate_fn)
- val_dl = DataLoader(val_ds , batch_size=5, shuffle=True, \
- collate_fn=val_ds.collate_fn)
5.定义网络的权重初始化方法 ( weights_init_normal) 如前几节所定义:
- def weights_init_normal(m):
- classname = m.__class__.__name__
- if classname.find("Conv") != -1:
- torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
- if hasattr(m, "bias") 和 m.bias 不是 None:
- torch.nn.init.constant_(m.bias.data, 0.0)
- 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)
6.定义残差块网络 ( ResidualBlock),在本例中,我们将利用 ResNet:
- class ResidualBlock(nn.Module):
- def __init__(self, in_features):
- super(ResidualBlock, self).__init__()
-
- self.block = nn.Sequential(
- nn.ReflectionPad2d(1),
- nn.Conv2d(in_features, in_features, 3),
- nn.InstanceNorm2d(in_features),
- nn.ReLU(inplace=True),
- nn.ReflectionPad2d(1),
- nn.Conv2d(in_features, in_features, 3),
- nn.InstanceNorm2d(in_features),
- )
-
- def forward(self, x):
- return x + self.block(x)
7.定义生成器网络 ( GeneratorResNet):
- class GeneratorResNet(nn.Module):
- def __init__(self, num_residual_blocks=9):
- super(GeneratorResNet, self).__init__()
- out_features = 64
- channels = 3
- model = [
- nn.ReflectionPad2d(3),
- nn.Conv2d(channels, out_features, 7),
- nn.InstanceNorm2d(out_features),
- nn.ReLU(inplace=True),
- ]
- in_features = out_features
- # Downsampling
- for _ in range(2):
- out_features *= 2
- model += [
- nn.Conv2d(in_features, out_features, 3, \
- stride=2, padding=1),
- nn.InstanceNorm2d(out_features),
- nn.ReLU(inplace=True),
- ]
- in_features = out_features
-
- # Residual blocks
- for _ in range(num_residual_blocks):
- model += [ResidualBlock(out_features)]
-
- # Upsampling
- for _ in range(2):
- out_features //= 2
- model += [
- nn.Upsample(scale_factor=2),
- nn.Conv2d(in_features, out_features, 3, \
- stride=1, padding=1),
- nn.InstanceNorm2d(out_features),
- nn.ReLU(inplace=True),
- ]
- in_features = out_features
-
- # Output layer
- model += [nn.ReflectionPad2d(channels), \
- nn.Conv2d(out_features, channels, 7), \
- nn.Tanh()]
- self.model = nn.Sequential(*model)
- self.apply(weights_init_normal)
- def forward(self, x):
- return self.model(x)
8.定义鉴别器网络 ( Discriminator):
- class Discriminator(nn.Module):
- def __init__(self):
- super(Discriminator, self).__init__()
-
- channels, height, width = 3, IMAGE_SIZE, IMAGE_SIZE
-
- def discriminator_block(in_filters, out_filters, \
- normalize=True):
- """Returns downsampling layers of each
- discriminator block"""
- layers = [nn.Conv2d(in_filters, out_filters, \
- 4, stride=2, padding=1)]
- if normalize:
- layers.append(nn.InstanceNorm2d(out_filters))
- layers.append(nn.LeakyReLU(0.2, inplace=True))
- return layers
-
- self.model = nn.Sequential(
- *discriminator_block(channels,64,normalize=False),
- *discriminator_block(64, 128),
- *discriminator_block(128, 256),
- *discriminator_block(256, 512),
- nn.ZeroPad2d((1, 0, 1, 0)),
- nn.Conv2d(512, 1, 4, padding=1)
- )
- self.apply(weights_init_normal)
-
- def forward(self, img):
- return self.model(img)
- @torch.no_grad()
- def generate_sample():
- data = next(iter(val_dl))
- G_AB.eval()
- G_BA.eval()
- real_A, real_B = data
- fake_B = G_AB(real_A)
- fake_A = G_BA(real_B)
- # Arange images along x-axis
- real_A = make_grid(real_A, nrow=5, normalize=True)
- real_B = make_grid(real_B, nrow=5, normalize=True)
- fake_A = make_grid(fake_A, nrow=5, normalize=True)
- fake_B = make_grid(fake_B, nrow=5, normalize=True)
- # Arange images along y-axis
- image_grid = torch.cat((real_A,fake_B,real_B,fake_A), 1)
- show(image_grid.detach().cpu().permute(1,2,0).numpy(), \
- sz=12)
9.定义训练生成器的函数 ( generator_train_step):
def generator_train_step(Gs, optimizer, real_A, real_B):
G_AB, G_BA = Gs
optimizer.zero_grad()
- loss_id_A = criterion_identity(G_BA(real_A), real_A)
- loss_id_B = criterion_identity(G_AB(real_B), real_B)
-
- loss_identity = (loss_id_A + loss_id_B) / 2
- fake_B = G_AB(real_A)
- loss_GAN_AB = criterion_GAN(D_B(fake_B), \
- torch.Tensor(np.ones((len(real_A), 1, \
- 16, 16))).to(device))
- fake_A = G_BA(real_B)
- loss_GAN_BA = criterion_GAN(D_A(fake_A), \
- torch.Tensor(np.ones((len(real_A), 1, \
- 16, 16))).to(device))
-
- loss_GAN = (loss_GAN_AB + loss_GAN_BA) / 2
- recov_A = G_BA(fake_B)
- loss_cycle_A = criteria_cycle(recov_A, real_A)
- recov_B = G_AB(fake_A)
- loss_cycle_B = criteria_cycle(recov_B, real_B)
-
- loss_cycle = (loss_cycle_A + loss_cycle_B) / 2
- loss_G = loss_GAN + lambda_cyc * loss_cycle + \
- lambda_id * loss_identity
- loss_G.backward()
- optimizer.step()
- return loss_G, loss_identity, loss_GAN, loss_cycle, \
- loss_G, fake_A, fake_B
10.定义训练判别器的函数 ( discriminator_train_step):
- def discriminator_train_step(D, real_data, fake_data, \
- optimizer):
- optimizer.zero_grad()
- loss_real = criterion_GAN(D(real_data), \
- torch.Tensor(np.ones((len(real_data), 1, \
- 16, 16))).to(device))
- loss_fake = criterion_GAN(D(fake_data.detach()), \
- torch.Tensor(np.zeros((len(real_data), 1, \
- 16, 16))).to(device))
- loss_D = (loss_real + loss_fake) / 2
- loss_D.backward()
- optimizer.step()
- return loss_D
11.定义生成器、鉴别器对象、优化器和损失函数:
- G_AB = GeneratorResNet().to(device)
- G_BA = GeneratorResNet().to(device)
- D_A = Discriminator().to(device)
- D_B = Discriminator().to(device)
-
- criterion_GAN = torch.nn.MSELoss()
- criterion_cycle = torch.nn.L1Loss()
- criterion_identity = torch.nn.L1Loss()
-
- optimizer_G = torch.optim.Adam(
- itertools.chain(G_AB.parameters(), G_BA.parameters()), \
- lr=0.0002, betas=(0.5, 0.999))
- optimizer_D_A = torch.optim.Adam(D_A.parameters(), \
- lr=0.0002, betas=(0.5, 0.999))
- optimizer_D_B = torch.optim.Adam(D_B.parameters(), \
- lr=0.0002, betas=(0.5, 0.999))
-
- lambda_cyc, lambda_id = 10.0, 5.0
12.在越来越多的时期训练网络:
- n_epochs = 10
- log = Report(n_epochs)
- for epoch in range(n_epochs):
- N = len(trn_dl)
- for bx, batch in enumerate(trn_dl):
- real_A, real_B = batch
-
- loss_G, loss_identity, loss_GAN, loss_cycle, \
- loss_G, fake_A, fake_B = generator_train_step(\
- (G_AB,G_BA), optimizer_G, \
- real_A, real_B)
- loss_D_A = discriminator_train_step(D_A, real_A, \
- fake_A, optimizer_D_A)
- loss_D_B = discriminator_train_step(D_B, real_B, \
- fake_B, optimizer_D_B)
- loss_D = (loss_D_A + loss_D_B) / 2
-
- log.record(epoch+(1+bx)/N, loss_D=loss_D.item(), \
- loss_G=loss_G.item(), loss_GAN=loss_GAN.item(), \
- loss_cycle=loss_cycle.item(), \
- loss_identity=loss_identity.item(), end='\r')
- if bx%100==0: generate_sample()
-
- log.report_avgs(epoch+1)
13.训练模型后生成图像:
generate_sample()
上述代码生成以下输出:
从前面可以看出,我们成功地将苹果转换为橙子(前两行)和橙子转换为苹果(最后两行)。
到目前为止,我们已经通过 Pix2Pix GAN 了解了成对的图像到图像的翻译,以及通过 CycleGAN 了解了不成对的图像到图像的翻译。在下一节中,我们将学习如何利用 StyleGAN 将一种风格的图像转换为另一种风格的图像。
我们先来了解一下 StyleGAN 发明之前的一些历史发展。正如我们所知,从前一章生成假人脸涉及到 GAN 的使用。研究面临的最大问题是可以生成的图像很小(通常为 64 x 64)。任何生成更大尺寸图像的努力都会导致生成器或鉴别器陷入局部最小值,这将停止训练并产生乱码。生成高质量图像的主要飞跃之一涉及一篇名为 ProGAN(Progressive GAN 的缩写)的研究论文,其中涉及一个巧妙的技巧。
生成器和鉴别器的大小都在逐渐增加。在第一步中,您创建一个生成器和鉴别器,以从潜在向量生成 4 x 4 图像。在此之后,额外的卷积(和放大)层被添加到经过训练的生成器和鉴别器中,它们将负责接受 4 x 4 图像(由步骤 1 中的潜在向量生成)并生成/鉴别 8 x 8 图像。一旦这一步也完成了,在生成器和鉴别器中再次创建新的层,以训练生成更大的图像。一步一步(逐步),图像大小以这种方式增加。逻辑是向已经运行良好的网络添加新层比尝试从头开始学习所有层更容易。以这种方式,https://arxiv.org/pdf/1710.10196v3.pdf):
StyleGAN 使用类似的训练方案,逐步生成图像,但每次网络增长时都会添加一组潜在输入。这意味着网络现在以生成的图像大小的规则间隔接受多个潜在向量。在生成阶段给出的每个潜在值都决定了在该网络的那个阶段将要生成的特征(样式)。让我们在这里更详细地讨论 StyleGAN 的工作细节:
在上图中,我们可以对比传统的图像生成方式和基于样式的生成器。在传统的生成器中,只有一个输入。但是,在基于样式的生成器中有一种机制。让我们在这里了解详细信息:
1.创建大小为 1 x 512 的随机噪声向量z。
2.将其馈送到称为样式网络(或映射网络)的辅助网络,该网络创建大小为 18 x 512 的张量w。
3.生成器(合成)网络包含 18 个卷积层。每一层都将接受以下输入:
请注意,噪声 ('B') 仅用于正则化目的。
前面三个组合将创建一个管道,该管道接收一个 1 x 512 向量并创建一个 1024 x 1024 图像。
现在让我们了解从映射网络生成的 18 x 512 向量中的 18 个 1 x 512 向量中的每一个如何对图像的生成做出贡献。在合成网络的前几层添加的 1 x 512 向量有助于图像中存在的整体姿势和大规模特征,例如姿势、面部形状等,(因为它们负责生成4 x 4、8 x 8 的图像,等等——这是前几张图像,将在后面的层中进一步增强)。中间层添加的向量对应于小尺度特征,例如发型、睁眼/闭眼(因为它们负责生成 16 x 16、32 x 32 和 64 x 64 图像). 最后几层添加的向量对应于图像的颜色方案和其他微观结构。当我们到达最后几层时,图像结构被保留,面部特征被保留,但只有图像级别的细节(例如照明条件)发生了变化。
在本节中,我们将利用预训练的 StyleGAN2 模型来定制我们感兴趣的图像以具有不同的风格。
为了我们的目标,我们将使用 StyleGAN2 模型执行风格迁移。在高层次上,以下是面部风格转换的工作原理(通过代码结果,以下内容会更加清晰):
了解了风格转移是如何完成的,现在让我们了解如何使用 StyleGAN2 在自定义图像上执行风格转移:
通过这一步,我们有两个图像——我们的自定义对齐图像和 StyleGAN2 网络生成的图像。我们现在想将自定义图像的一些特征转移到生成的图像中,反之亦然。
让我们编写前面的策略。
请注意,我们正在利用从 GitHub 存储库中获取的预训练网络,因为训练这样的网络需要几天甚至几周的时间:
1.克隆存储库,安装需求,并获取预训练的权重:
- import os
- if not os.path.exists('pytorch_stylegan_encoder'):
- !git clone https://github.com/jacobhallberg/pytorch_stylegan_encoder.git
- %cd pytorch_stylegan_encoder
- !git submodule update --init --recursive
- !wget -q https://github.com/jacobhallberg/pytorch_stylegan_encoder/releases/download/v1.0/trained_models.zip
- !unzip -q trained_models.zip
- !rm trained_models.zip
- !pip install -qU torch_snippets
- !mv trained_models/stylegan_ffhq.pth InterFaceGAN/models/pretrain
- else:
- %cd pytorch_stylegan_encoder
-
- from torch_snippets import *
2.加载预训练的生成器和合成网络,映射网络的权重:
- from InterFaceGAN.models.stylegan_generator import StyleGANGenerator
- from models.latent_optimizer import PostSynthesisProcessing
-
- synthesizer=StyleGANGenerator("stylegan_ffhq").model.synthesis
- mapper = StyleGANGenerator("stylegan_ffhq").model.mapping
- trunc = StyleGANGenerator("stylegan_ffhq").model.truncation
3.定义从随机向量生成图像的函数:
- post_processing = PostSynthesisProcessing()
- post_process = lambda image: post_processing(image)\
- .detach().cpu().numpy().astype(np.uint8)[0]
-
- def latent2image(latent):
- img = post_process(synthesizer(latent))
- img = img.transpose(1,2,0)
- return img
4.生成一个随机向量:
rand_latents = torch.randn(1,512).cuda()
在前面的代码中,我们通过映射和截断网络传递随机的 1 x 512 维向量以生成 1 x 18 x 512 的向量。这些 18 x 512 的向量决定了生成图像的样式。
5.从随机向量生成图像:
show(latent2image(trunc(mapper(rand_latents))), sz=5)
上述代码生成以下输出:

至此,我们已经生成了一张图片。在接下来的几行代码中,您将了解如何在前面生成的图像和您选择的图像之间执行样式转换。
6.获取自定义图像 ( MyImage.jpg) 并对齐它。对齐对于生成适当的潜在向量很重要,因为 StyleGAN 中生成的所有图像都以人脸为中心并且特征明显可见:
- !wget https://www.dropbox.com/s/lpw10qawsc5ipbn/MyImage.JPG\
- -O MyImage.jpg
- !git clone https://github.com/Puzer/stylegan-encoder.git
- !mkdir -p stylegan-encoder/raw_images
- !mkdir -p stylegan-encoder/aligned_images
- !mv MyImage.jpg stylegan-encoder/raw_images
7.对齐自定义图像:
- !python stylegan-encoder/align_images.py \
- stylegan-encoder/raw_images/ \
- stylegan-encoder/aligned_images/
- !mv stylegan-encoder/aligned_images/* ./MyImage.jpg
8.使用对齐的图像生成可以完美再现对齐图像的潜在值。这是一个识别潜在向量组合的过程,该组合使对齐图像与从潜在向量生成的图像之间的差异最小化:
- from PIL import Image
- img = Image.open('MyImage.jpg')
- show(np.array(img), sz=4, title='original')
-
- !python encode_image.py ./MyImage.jpg\
- pred_dlatents_myImage.npy\
- --use_latent_finder true\
- --image_to_latent_path ./trained_models/image_to_latent.pt
-
- pred_dlatents = np.load('pred_dlatents_myImage.npy')
- pred_dlatent = torch.from_numpy(pred_dlatents).float().cuda()
- pred_image = latent2image(pred_dlatent)
- show(pred_image, sz=4, title='synthesized')
上述代码生成以下输出:

Python 脚本encode_image.py在高层次上执行以下操作:
现在我们有了与感兴趣的图像相对应的潜在向量,让我们在下一步中执行图像之间的风格转换。
9.执行风格转移:
如前所述,风格转移背后的核心逻辑实际上是风格张量的部分转移,即 18 x 512 风格张量中的 18 个子集。在这里,我们将在一种情况下传输前两行(18 x 512),在一种情况下传输 3-15 行,在一种情况下传输 15-18 行。由于每组向量负责生成图像的不同方面,因此每组交换的向量交换图像中的不同特征:
- idxs_to_swap = slice(0,3)
- my_latents=torch.Tensor(np.load('pred_dlatents_myImage.npy', \
- allow_pickle=True))
-
- A, B = latent2image(my_latents.cuda()), latent2image(trunc(mapper(rand_latents)))
- generated_image_latents = trunc(mapper(rand_latents))
-
- x = my_latents.clone()
- x[:,idxs_to_swap] = generated_image_latents[:,idxs_to_swap]
- a = latent2image(x.float().cuda())
-
- x = generated_image_latents.clone()
- x[:,idxs_to_swap] = my_latents[:,idxs_to_swap]
- b = latent2image(x.float().cuda())
-
- subplots([A,a,B,b], figsize=(7,8), nc=2, \
- suptitle='Transfer high level features')
前面的代码生成这个:
这是分别带有idxs_to_swapasslice(4,15)和的输出slice (15,18)。
10.接下来,我们推断一个样式向量,这样新的向量只会改变我们自定义图像的笑脸。为此,您需要计算移动潜在向量
的正确方向。我们可以通过首先创建大量假图像来实现这一点。然后使用 SVM 分类器来训练并找出图像中的人是否微笑。因此,这个 SVM 创建了一个超平面,将笑脸与非笑脸分开。移动
所需的方向将垂直于这个超平面,表示为stylegan_ffhq_smile_w_boundary.npy。实现细节可以在InterfaceGAN/edit.py代码本身中找到:
- !python InterFaceGAN/edit.py\
- -m stylegan_ffhq\
- -o results_new_smile\
- -b InterFaceGAN/boundaries/stylegan_ffhq_smile_w_boundary.npy\
- -i pred_dlatents_myImage.npy\
- -s WP\
- --steps 20
-
- generated_faces = glob.glob('results_new_smile/*.jpg')
-
- subplots([read(im,1) for im in sorted(generated_faces)], \
- figsize=(10,10))
以下是生成的图像的外观:
总之,我们已经了解了这项研究在使用 GAN 生成非常高分辨率的人脸图像方面的进展情况。诀窍是在增加分辨率的步骤中增加生成器和判别器的复杂性,以便在每一步中,两个模型都能很好地完成任务。我们了解了如何通过确保每个分辨率的特征由称为样式向量的独立输入来控制生成图像的样式。我们还学习了如何通过将样式从一张图像切换到另一张来操作不同图像的样式。
现在我们已经了解了如何利用预训练的 StyleGAN2 模型来执行样式迁移,在下一节中,我们将利用预训练的超分辨率 GAN 模型生成高分辨率图像。
在上一节中,我们看到了一个场景,我们利用预训练的 StyleGAN 生成给定样式的图像。在本节中,我们将更进一步,了解如何利用预训练模型来执行图像超分辨率。在将其应用于图像之前,我们将了解超分辨率 GAN 模型的架构。
首先,我们将了解为什么 GAN 是超分辨率任务的良好解决方案。想象一个场景,给您一张图像并要求您提高其分辨率。直观地说,您会考虑使用各种插值技术来执行超分辨率。这是一个示例低分辨率图像以及各种技术的输出(图像来源:https ://arxiv.org/pdf/1609.04802.pdf ):
从上图中我们可以看出,在从低分辨率(原始图像的 4 倍缩小图像)重建图像时,双三次插值等传统插值技术并没有太大帮助。
虽然基于 ResNet 的超分辨率 UNet 可以在这种情况下派上用场,但 GAN 可能更有用,因为它们可以模拟人类感知。鉴于判别器知道典型的超分辨率图像的外观,它可以检测生成的图像具有不一定看起来像高分辨率图像的属性的场景。
随着对 GAN 的超分辨率需求的确立,让我们了解并利用预训练模型。
虽然可以从头开始编码和训练超分辨率 GAN,但我们将尽可能利用预训练模型。因此,在本节中,我们将利用由 Christian Ledig 及其团队开发并发表在题为“使用生成对抗网络的 Photo-Realistic Single Image Super-Resolution ”的论文中的模型。
SRGAN的架构如下(图片来源:https ://arxiv.org/pdf/1609.04802.pdf ):

从上图中,我们看到判别器将高分辨率图像作为输入来训练预测图像是高分辨率图像还是低分辨率图像的模型。生成器网络将低分辨率图像作为输入,生成高分辨率图像。在训练模型时,内容损失和对抗性损失都被最小化了。要详细了解模型训练的细节以及比较用于生成高分辨率图像的各种技术的结果,我们建议您阅读本文。
凭借对模型构建方式的高层次理解,我们将编写利用预训练 SRGAN 模型将低分辨率图像转换为高分辨率图像的方法。
以下是加载预训练的 SRGAN 并进行预测的步骤:
1.导入相关包和预训练模型:
- import os
- if not os.path.exists('srgan.pth.tar'):
- !pip install -q torch_snippets
- !wget -q https://raw.githubusercontent.com/sizhky/a-PyTorch-Tutorial-to-Super-Resolution/master/models.py -O models.py
- from pydrive.auth import GoogleAuth
- from pydrive.drive import GoogleDrive
- from google.colab import auth
- from oauth2client.client import GoogleCredentials
-
- auth.authenticate_user()
- gauth = GoogleAuth()
- gauth.credentials = \
- GoogleCredentials.get_application_default()
- drive = GoogleDrive(gauth)
-
- downloaded = drive.CreateFile({'id': \
- '1_PJ1Uimbr0xrPjE8U3Q_bG7XycGgsbVo'})
- downloaded.GetContentFile('srgan.pth.tar')
- from torch_snippets import *
- device = 'cuda' if torch.cuda.is_available() else 'cpu'
2.加载模型:
- model = torch.load('srgan.pth.tar', map_location='cpu')['generator'].to(device)
- model.eval()
3.获取要转换为高分辨率的图像:
!wget https://www.dropbox.com/s/nmzwu68nrl9j0lf/Hema6.JPG
4.定义函数preprocess和postprocess图像:
- preprocess = T.Compose([
- T.ToTensor(),
- T.Normalize([0.485, 0.456, 0.406],
- [0.229, 0.224, 0.225]),
- T.Lambda(lambda x: x.to(device))
- ])
-
- postprocess = T.Compose([
- T.Lambda(lambda x: (x.cpu().detach()+1)/2),
- T.ToPILImage()
- ])
5.加载图像并对其进行预处理:
- image = readPIL('Hema6.JPG')
- image.size
- # (260,181)
- image = image.resize((130,90))
- im = preprocess(image)
请注意,在前面的代码中,我们对原始图像执行了额外的调整大小以进一步模糊图像,但这只是为了说明 - 因为当我们缩小图像时改进更加明显。
6.通过模型的加载model和postprocess输出传递预处理图像:
- sr = model(im[None])[0]
- sr = postprocess(sr)
7.绘制原始图像和高分辨率图像:
- subplots([image, sr], nc=2, figsize=(10,10), \
- titles=['Original image','High resolution image'])
前面的代码产生以下输出:
从上图中,我们可以看到高分辨率图像捕捉到了原始图像中模糊的细节。
请注意,如果原始图像模糊,则原始图像和高分辨率图像之间的对比度会很高。但是,如果原始图像不模糊,对比度不会那么高。我们鼓励您使用不同分辨率的图像。
在本章中,我们学习了使用 Pix2Pix GAN 从给定轮廓生成图像。此外,我们了解了 CycleGAN 中用于将一类图像转换为另一类图像的各种损失函数。接下来,我们了解了 StyleGAN 如何帮助生成逼真的面孔,并根据生成器的训练方式将风格从一张图像复制到另一张图像。最后,我们了解了如何利用预训练的 SRGAN 模型生成高分辨率图像。
在下一章中,我们将转向学习如何基于很少(通常少于 20 张)图像训练图像分类模型。