6.1.4 Momentum
式(6.3)中有 αv 这一项。在物体不受任何力时,该项承担使物体逐渐减速的任务(α 设定为 0.9 之类的值),对应物理上的地面摩擦或空气阻力。下面是 Momentum 的代码实现
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
params[key] += self.v[key]
实例变量 v 会保存物体的速度。初始化时,v 中什么都不保存,但当第一次调用 update() 时,v 会以字典型变量的形式保存与参数结构相同的数据。
6.1.5 AdaGrad
在关于学习率的有效技巧中,有一种被称为学习率衰减(learning ratedecay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。而 AdaGrad 进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。AdaGrad 会为参数的每个元素适当地调整学习率,与此同时进行学习(AdaGrad 的 Ada 来自英文单词 Adaptive,即“适当的”的意思)。
现 在 来 实 现 AdaGrad。AdaGrad 的 实 现 过 程 如 下 所示
class AdaGrad:
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
这里需要注意的是,最后一行加上了微小值 1e-7。这是为了防止当self.h[key] 中有 0 时,将 0 用作除数的情况。在很多深度学习的框架中,这个微小值也可以设定为参数,但这里我们用的是 1e-7 这个固定值。
6.1.6 Adam
它的理论有些复杂,直观地讲,就是融合了 Momentum 和 AdaGrad 的方法。通过组合前面两个方法的优点,有望实现参数空间的高效搜索。此外,进行超参数的“偏置校正”也是 Adam 的特征。
6.2 权重的初始值
6.2.1 可以将权重初始值设为 0 吗
后面我们会介绍抑制过拟合、提高泛化能力的技巧——权值衰减(weightdecay)。简单地说,权值衰减就是一种以减小权重参数的值为目的进行学习的方法。通过减小权重参数的值来抑制过拟合的发生。如果想减小权重的值,一开始就将初始值设为较小的值才是正途。实际上,在这之前的权重初始值都是像 0.01 * np.random.randn(10, 100) 这样,使用由高斯分布生成的值乘以 0.01 后得到的值(标准差为 0.01 的高斯分布)。
为什么不能将权重初始值设为 0 呢?严格地说,为什么不能将权重初始值设成一样的值呢?这是因为在误差反向传播法中,所有的权重值都会进行相同的更新。比如,在 2 层神经网络中,假设第 1 层和第 2 层的权重为 0。这样一来,正向传播时,因为输入层的权重为 0,所以第 2 层的神经元全部会被传递相同的值。第 2 层的神经元中全部输入相同的值,这意味着反向传播时第 2 层的权重全部都会进行相同的更新。
因此,权重被更新为相同的值,并拥有了对称的值(重复的值)。这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”(严格地讲,是为了瓦解权重的对称结构),必须随机生成初始值。
6.2.2 隐藏层的激活值的分布
观察隐藏层的激活值 A(激活函数的输出数据)的分布,可以获得很多启发。这里,我们来做一个简单的实验,观察权重初始值是如何影响隐藏层的激活值的分布的。这里要做的实验是,向一个 5 层神经网络(激活函数使用sigmoid 函数)传入随机生成的输入数据,用直方图绘制各层激活值的数据分布。
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.random.randn(1000, 100) # 1000 个数据
node_num = 100 # 各隐藏层的节点(神经元)数
hidden_layer_size = 5 # 隐藏层有 5 层
activations = {} # 激活值的结果保存在这里
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
w = np.random.randn(node_num, node_num) * 1
z = np.dot(x, w)
a = sigmoid(z) # sigmoid 函数
activations[i] = a
这里假设神经网络有 5 层,每层有 100 个神经元。然后,用高斯分布随机生成 1000 个数据作为输入数据,并把它们传给 5 层神经网络。激活函数使用 sigmoid 函数,各层的激活值的结果保存在 activations 变量中。这个代码段中需要注意的是权重的尺度。虽然这次我们使用的是标准差为 1 的高斯分布,但实验的目的是通过改变这个尺度(标准差),观察激活值的分布如何变化。现在,我们将保存在 activations 中的各层数据画成直方图。
# 绘制直方图
for i, a in activations.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
从图 6-10 可知,各层的激活值呈偏向 0 和 1 的分布。这里使用的 sigmoid函数是 S 型函数,随着输出不断地靠近 0(或者靠近 1),它的导数的值逐渐接近 0。因此,偏向 0 和 1 的数据分布会造成反向传播中梯度的值不断变小,最后消失。这个问题称为梯度消失(gradient vanishing)。层次加深的深度学习中,梯度消失的问题可能会更加严重。
将权重的标准差设为 0.01,进行相同的实验。这次呈集中在 0.5 附近的分布。因为不像刚才的例子那样偏向 0 和 1,所以不会发生梯度消失的问题。但是,激活值的分布有所偏向,说明在表现力
上会有很大问题。为什么这么说呢?因为如果有多个神经元都输出几乎相同的值,那它们就没有存在的意义了。比如,如果 100 个神经元都输出几乎相同的值,那么也可以由 1 个神经元来表达基本相同的事情。因此,激活值在分布上有所偏向会出现“表现力受限”的问题。
Xavier 的论文中,为了使各层的激活值呈现出具有相同广度的分布,推导了合适的权重尺度。推导出的结论是,如果前一层的节点数为 n,则初始值使用标准差为 的分布。
使用 Xavier 初始值后,前一层的节点数越多,要设定为目标节点的初始值的权重尺度就越小。
6.2.3 ReLU 的权重初始值
观察实验结果可知,当“std = 0.01”时,各层的激活值非常小 A。神经网络上传递的是非常小的值,说明逆向传播时权重的梯度也同样很小。这是很严重的问题,实际上学习基本上没有进展。
接下来是初始值为 Xavier 初始值时的结果。在这种情况下,随着层的加深,偏向一点点变大。实际上,层加深后,激活值的偏向变大,学习时会出现梯度消失的问题。而当初始值为 He 初始值时,各层中分布的广度相同。由于即便层加深,数据的广度也能保持不变,因此逆向传播时,也会传递合适的值。总结一下,当激活函数使用 ReLU 时,权重初始值使用 He 初始值,当激活函数为 sigmoid 或 tanh 等 S 型曲线函数时,初始值使用 Xavier 初始值。这是目前的最佳实践。
6.3 Batch Normalization
Batch Normalization(下 文 简 称 Batch Norm)是 2015 年 提 出 的 方 法。Batch Norm 虽然是一个问世不久的新方法,但已经被很多研究人员和技术人员广泛使用。实际上,看一下机器学习竞赛的结果,就会发现很多通过使用这个方法而获得优异结果的例子。
可以使学习快速进行(可以增大学习率)。
不那么依赖初始值(对于初始值不用那么神经质)。
抑制过拟合(降低 Dropout 等的必要性)。
6.4 正则化
机器学习的问题中,过拟合是一个很常见的问题。过拟合指的是只能拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。机器学习的目标是提高泛化能力,即便是没有包含在训练数据里的未观测数据,也希望模型可以进行正确的识别。
6.4.1 过拟合
发生过拟合的原因,主要有以下两个。
• 模型拥有大量参数、表现力强。
• 训练数据少。
这 里,我 们 故 意 满 足 这 两 个 条 件,制 造 过 拟 合 现 象。为 此,要 从MNIST 数据集原本的 60000 个训练数据中只选定 300 个,并且,为了增加网络的复杂度,使用 7 层网络(每层有 100 个神经元,激活函数为 ReLU)。
首先是用于读入数据的代码。
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 为了再现过拟合,减少学习数据
x_train = x_train[:300]
t_train = t_train[:300]
接着是进行训练的代码。和之前的代码一样,按 epoch 分别算出所有训练数据和所有测试数据的识别精度。
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100,
100, 100, 100], output_size=10)
optimizer = SGD(lr=0.01) # 用学习率为 0.01 的 SGD 更新参数
max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0
for i in range(1000000000):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
grads = network.gradient(x_batch, t_batch)
optimizer.update(network.params, grads)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
6.4.2 权值衰减
权值衰减是一直以来经常被使用的一种抑制过拟合的方法。该方法通过在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合原本就是因为权重参数取值过大才发生的。
6.4.3 Dropout
作为抑制过拟合的方法,前面我们介绍了为损失函数加上权重的 L2 范数的权值衰减方法。该方法可以简单地实现,在某种程度上能够抑制过拟合。但是,如果网络的模型变得很复杂,只用权值衰减就难以应对了。在这种情况下,我们经常会使用 Dropout方法。
Dropout 是一种在学习的过程中随机删除神经元的方法。训练时,随机选出隐藏层的神经元,然后将其删除。被删除的神经元不再进行信号的传递。训练时,每传递一次数据,就会随机选择要删除的神经元。然后,测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出。
下面我们来实现 Dropout。这里的实现重视易理解性。不过,因为训练时如果进行恰当的计算的话,正向传播时单纯地传递数据就可以了(不用乘以删除比例),所以深度学习的框架中进行了这样的实现。
class Dropout:
def __init__(self, dropout_ratio=0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flg=True):
if train_flg:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask
这里的要点是,每次正向传播时,self.mask 中都会以 False 的形式保存要删除的神经元self.mask 会随机生成和 x 形状相同的数组,并将值比dropout_ratio 大的元素设为 True。反向传播时的行为和 ReLU 相同。也就是说,正向传播时传递了信号的神经元,反向传播时按原样传递信号;正向传播时没有传递信号的神经元,反向传播时信号将停在那里。
6.5 超参数的验证
神经网络中,除了权重和偏置等参数,超参数(hyper-parameter)也经常出现。这里所说的超参数是指,比如各层的神经元数量、batch 大小、参数更新时的学习率或权值衰减等。如果这些超参数没有设置合适的值,模型的性能就会很差。虽然超参数的取值非常重要,但是在决定超参数的过程中一般会伴随很多的试错。
6.5.1 验证数据
调整超参数时,必须使用超参数专用的确认数据。用于调整超参数的数据,一般称为验证数据(validation data)。我们使用这个验证数据来评估超参数的好坏。
根据不同的数据集,有的会事先分成训练数据、验证数据、测试数据三部分,有的只分成训练数据和测试数据两部分,有的则不进行分割。在这种情况下,用户需要自行进行分割。如果是 MNIST 数据集,获得验证数据的最简单的方法就是从训练数据中事先分割 20% 作为验证数据,代码如下所示。
(x_train, t_train), (x_test, t_test) = load_mnist()
# 打乱训练数据
x_train, t_train = shuffle_dataset(x_train, t_train)
# 分割验证数据
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]
6.5.2 超参数的最优化
进行超参数的最优化时,逐渐缩小超参数的“好值”的存在范围非常重要。所谓逐渐缩小范围,是指一开始先大致设定一个范围,从这个范围中随机选出一个超参数(采样),用这个采样到的值进行识别精度的评估;然后,多次重复该操作,观察识别精度的结果,根据这个结果缩小超参数的“好值”的范围。通过重复这一操作,就可以逐渐确定超参数的合适范围。
超参数的范围只要“大致地指定”就可以了。所谓“大致地指定”,是指像 0.001(10−3)到 1000(103)这样,以“10 的阶乘”的尺度指定范围(也表述为“用对数尺度(log scale)指定”)。在超参数的最优化中,要注意的是深度学习需要很长时间(比如,几天或几周)。因此,在超参数的搜索中,需要尽早放弃那些不符合逻辑的超参数。于是,在超参数的最优化中,减少学习的 epoch,缩短一次评估所需的时间是一个不错的办法。以上就是超参数的最优化的内容,简单归纳一下,如下所示。
步骤 0
设定超参数的范围。
步骤 1
从设定的超参数范围中随机采样。
步骤 2
使用步骤 1 中采样到的超参数的值进行学习,通过验证数据评估识别精度(但是要将 epoch 设置得很小)。
步骤 3
重复步骤 1 和步骤 2(100 次等),根据它们的识别精度的结果,缩小超参数的范围。
6.6 小结
我们介绍了神经网络的学习中的几个重要技巧。参数的更新方法、权重初始值的赋值方法、Batch Normalization、Dropout 等,这些都是现代神经网络中不可或缺的技术。
CycleGAN的实现
1. CycleGAN 简介
CycleGAN(Cycle-Consistent Generative Adversarial Networks)是一种无监督图像到图像转换(Image-to-Image Translation)模型,能够在不需要成对训练数据的情况下实现高质量的风格转换。该方法由 Jun-Yan Zhu 等人在 2017 年提出,主要解决未配对数据(Unpaired Data)的图像转换问题。
2. CycleGAN 主要结构
CycleGAN 由两个生成器和两个判别器组成:
生成器 G(X → Y):将域 X(如马的图片)转换为域 Y(如斑马的图片)。
生成器 F(Y → X):将域 Y 的图像转换回域 X。
判别器 D_X:判断给定的 X 域图像是真实的还是由 F 生成的。
判别器 D_Y:判断给定的 Y 域图像是真实的还是由 G 生成的。
3. CycleGAN 的应用场景
风格转换:如将照片转换为油画风格,或者将真实图像转换为动漫风格。
图像增强:如提高医学影像的质量或将黑白照片转换为彩色。
域适应:用于将数据从一个领域(Domain)映射到另一个领域。
图像修复:在去雾、去噪声等任务中表现良好。
4. CycleGAN 的优缺点
优点:
无需成对数据,适用于无监督图像转换任务。
效果自然,生成图像更加逼真。
结构简单,训练方法类似于标准 GAN。
缺点:
容易模式崩溃(Mode Collapse),导致生成的图像缺乏多样性。
训练不稳定,对超参数(如学习率、循环损失权重等)敏感。
转换不一定完全准确,对于复杂场景可能生成伪影(Artifacts)。
5. 总结
CycleGAN 依靠生成器和判别器之间的对抗训练、循环一致性损失和恒等映射损失,实现了跨域图像转换。在实际操作中,若发现生成器输出越来越接近原图,可能是因为损失权重设置不合理,或对抗性损失信号不足、判别器结构太弱。通过逐步调整这些参数和结构,结合实验结果,可以使 CycleGAN 学会真正的跨域风格转换。
6.实现过程
(1)导入必要的库
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
from PIL import Image
import os
import glob
import random
(2)定义数据集类
class ImageDataset(Dataset):
def __init__(self, root, transforms_=None, unaligned=False, mode="train"):
self.transform = transforms.Compose(transforms_)
self.unaligned = unaligned
self.files_A = sorted(glob.glob(os.path.join(root, "%sA" % mode) + "/*.*"))
self.files_B = sorted(glob.glob(os.path.join(root, "%sB" % mode) + "/*.*"))
def __getitem__(self, index):
image_A = Image.open(self.files_A[index % len(self.files_A)])
if self.unaligned:
image_B = Image.open(self.files_B[random.randint(0, len(self.files_B) - 1)])
else:
image_B = Image.open(self.files_B[index % len(self.files_B)])
if image_A.mode != "RGB":
image_A = image_A.convert("RGB")
if image_B.mode != "RGB":
image_B = image_B.convert("RGB")
item_A = self.transform(image_A)
item_B = self.transform(image_B)
return {"A": item_A, "B": item_B}
def __len__(self):
return max(len(self.files_A), len(self.files_B))
(3)定义生成器和判别器
生成器
class Generator(nn.Module):
def __init__(self, input_nc, output_nc, n_residual_blocks=9):
super(Generator, self).__init__()
# Initial convolution block
model = [nn.ReflectionPad2d(3),
nn.Conv2d(input_nc, 64, 7),
nn.InstanceNorm2d(64),
nn.ReLU(inplace=True)]
# Downsampling
in_features = 64
out_features = in_features * 2
for _ in range(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
out_features = in_features * 2
# Residual blocks
for _ in range(n_residual_blocks):
model += [ResidualBlock(in_features)]
# Upsampling
out_features = in_features // 2
for _ in range(2):
model += [nn.ConvTranspose2d(in_features, out_features, 3, stride=2, padding=1, output_padding=1),
nn.InstanceNorm2d(out_features),
nn.ReLU(inplace=True)]
in_features = out_features
out_features = in_features // 2
# Output layer
model += [nn.ReflectionPad2d(3),
nn.Conv2d(64, output_nc, 7),
nn.Tanh()]
self.model = nn.Sequential(*model)
def forward(self, x):
return self.model(x)
class ResidualBlock(nn.Module):
def __init__(self, in_features):
super(ResidualBlock, self).__init__()
self.conv_block = nn.Sequential(
nn.Conv2d(in_features, in_features, 3, padding=1),
nn.InstanceNorm2d(in_features),
nn.ReLU(inplace=True),
nn.Conv2d(in_features, in_features, 3, padding=1),
nn.InstanceNorm2d(in_features)
)
def forward(self, x):
return x + self.conv_block(x)
判别器
class Discriminator(nn.Module):
def __init__(self, input_nc):
super(Discriminator, self).__init__()
# A bunch of convolutions one after another
model = [nn.Conv2d(input_nc, 64, 4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True)]
model += [nn.Conv2d(64, 128, 4, stride=2, padding=1),
nn.InstanceNorm2d(128),
nn.LeakyReLU(0.2, inplace=True)]
model += [nn.Conv2d(128, 256, 4, stride=2, padding=1),
nn.InstanceNorm2d(256),
nn.LeakyReLU(0.2, inplace=True)]
model += [nn.Conv2d(256, 512, 4, padding=1),
nn.InstanceNorm2d(512),
nn.LeakyReLU(0.2, inplace=True)]
# FCN classification layer
model += [nn.Conv2d(512, 1, 4, padding=1)]
self.model = nn.Sequential(*model)
def forward(self, x):
x = self.model(x)
return F.avg_pool2d(x, x.size()[2:]).view(x.size(0), -1)
(4)定义损失函数和优化器
criterion_GAN = nn.MSELoss()
criterion_cycle = nn.L1Loss()
criterion_identity = nn.L1Loss()
optimizer_G = optim.Adam(itertools.chain(netG_A2B.parameters(), netG_B2A.parameters()), lr=0.0002, betas=(0.5, 0.999))
optimizer_D_A = optim.Adam(netD_A.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_D_B = optim.Adam(netD_B.parameters(), lr=0.0002, betas=(0.5, 0.999))
(5)训练模型
for epoch in range(num_epochs):
for i, batch in enumerate(dataloader):
real_A = batch['A'].to(device)
real_B = batch['B'].to(device)
# Adversarial ground truths
valid = torch.ones(real_A.size(0), *patch, requires_grad=False).to(device)
fake = torch.zeros(real_A.size(0), *patch, requires_grad=False).to(device)
# ------------------
# Train Generators
# ------------------
optimizer_G.zero_grad()
# GAN loss
fake_B = netG_A2B(real_A)
loss_GAN_A2B = criterion_GAN(netD_B(fake_B), valid)
fake_A = netG_B2A(real_B)
loss_GAN_B2A = criterion_GAN(netD_A(fake_A), valid)
# Cycle loss
recovered_A = netG_B2A(fake_B)
loss_cycle_ABA = criterion_cycle(recovered_A, real_A) * 10.0
recovered_B = netG_A2B(fake_A)
loss_cycle_BAB = criterion_cycle(recovered_B, real_B) * 10.0
# Total loss
loss_G = loss_GAN_A2B + loss_GAN_B2A + loss_cycle_ABA + loss_cycle_BAB
loss_G.backward()
optimizer_G.step()
# -----------------------
# Train Discriminator A
# -----------------------
optimizer_D_A.zero_grad()
# Real loss
loss_real = criterion_GAN(netD_A(real_A), valid)
# Fake loss (on batch of previously generated samples)
fake_A_ = fake_A_buffer.push_and_pop(fake_A)
loss_fake = criterion_GAN(netD_A(fake_A_.detach()), fake)
# Total loss
loss_D_A = (loss_real + loss_fake) / 2
loss_D_A.backward()
optimizer_D_A.step()
# -----------------------
# Train Discriminator B
# -----------------------
optimizer_D_B.zero_grad()
# Real loss
loss_real = criterion_GAN(netD_B(real_B), valid)
# Fake loss (on batch of previously generated samples)
fake_B_ = fake_B_buffer.push_and_pop(fake_B)
loss_fake = criterion_GAN(netD_B(fake_B_.detach()), fake)
# Total loss
loss_D_B = (loss_real + loss_fake) / 2
loss_D_B.backward()
optimizer_D_B.step()
# Print log
print(f"[Epoch {epoch}/{num_epochs}] [Batch {i}/{len(dataloader)}] [D loss: {loss_D_A.item() + loss_D_B.item()}] [G loss: {loss_G.item()}]")
(6)评估和保存模型
# Save the model checkpoints
torch.save(netG_A2B.state_dict(), 'netG_A2B.pth')
torch.save(netG_B2A.state_dict(), 'netG_B2A.pth')
torch.save(netD_A.state_dict(), 'netD_A.pth')
torch.save(netD_B.state_dict(), 'netD_B.pth')