banner
Nagi-ovo

Nagi-ovo

Breezing
github

LLM演进史(三):批归一化——激活与梯度的统计调和

本节的重点在于,要对于训练过程中神经网络的激活,特别是向下流动的梯度有深刻的印象和理解。理解这些结构的发展历史是很重要的,因为 RNN (循环神经网络),作为一个通用逼近器 (universal approximator),它原则上可以实现所有的算法,但不太容易用一些梯度为主的技术作优化。其不易优化的原因是一个理解重点,可以通过观察激活和梯度在训练中的表现来的出结论。我们也会见到很多试图改善这种情况的变体。

起点:MLP#

来重新温习一下上一章的 MLP 模型,也就是我们当前的起点吧:
在上一章的代码基础上修改,不再做硬编码,方便后面调整。

# MLP revisited
n_embd = 10 # 字符嵌入向量的维度
n_hidden = 200 # 隐藏层中隐藏单元的数量 
# vocab_size = 27, block_size = 3

g = torch.Generator().manual_seed(2147483647) # for reproducibility
C = torch.randn((vocab_size,n_embd),              generator=g)
W1 = torch.randn((n_embd * block_size, n_hidden), generator=g)
b1 = torch.randn(n_hidden,                        generator=g)
W2 = torch.randn((n_hidden, vocab_size),          generator=g)
b2 = torch.randn(vocab_size,                      generator=g)

paramters = [C, W1, b1, W2, b2]
print(sum(p.nelement() for p in paramters)) # 总参数量 
for p in paramters:
    p.requires_grad_()

总参数量和上章一样是 11897

神经网络的训练部分也做这样不改变功能的修改:

# 和上一章作同样的mini-batch优化

max_steps = 200000
batch_size = 32
lossi = []

for i in range(max_steps):

    # mini-batch
    ix = torch.randint(0,Xtr.shape[0],(batch_size,), generator=g)
    Xb, Yb = Xtr[ix], Ytr[ix] # batch X, Y

    # forward pass
    emb = C[Xb] # embedding 
    embcat = emb.view(emb.shape[0], -1) # 拼接所有嵌入向量
    hpreact = embcat @ W1 + b1 # 隐藏层预处理
    h = torch.tanh(hpreact) # 隐藏层
    logits = h @ W2 + b2 # 输出层 
    loss = F.cross_entropy(logits, Yb) # 损失函数

    # backward pass
    for p in parameters:
        p.grad = None
    loss.backward()

    # update
    lr = 0.1 if i < 100000  else 0.01
    for p in parameters:
        p.data += -lr * p.grad 
    
    # tracks stats
    if i % 10000 == 0:
        print(f'{i:7d}/{max_steps:7d}: {loss.item():.4f}')
    lossi.append(loss.log10().item())

plt.plot(lossi)

Screenshot 2024-02-23 at 01.10.08

注意这里最初的损失函数高达 27,然后马上就降到很低了,可以猜测一下原因

Screenshot 2024-02-17 at 17.57.25

像一个曲棍球棍 (a hockey stick)

损失函数可视化也做了一个按索引分割不同部分的损失功能:

@torch.no_grad() # 这个装饰器可以取消跟踪这个函数内的所有梯度
def split_loss(split):
    x,y = {
        'train': (Xtr, Ytr),
        'val': (Xdev, Ydev),
        'test': (Xte, Yte),
    }[split]
	# forward pass
    emb = C[x] # (N, block_size, n_embd)
    embcat = emb.view(emb.shape[0], -1) # 拼接为 (N, block_size * n_embd)
    h = torch.tanh(embcat @ W1 + b1) # (N, n_hidden)
    logits = h @ W2 + b2 # (N, vocab_size)
    loss = F.cross_entropy(logits, y)
    print(split, loss.item())

split_loss('train')
split_loss('val')

这个装饰器的作用就相当于对每个 tensor 都加了一个requires_grad = False,不会调用backward(),所以不需要底层维护影响效率的图表

这里的损失为:
train 2.2318217754364014
val 2.251192569732666

# sample from the model
g = torch.Generator().manual_seed(2147483647 + 10)

for _ in range(20):
    out = []
    context = [0] * block_size # initialize with all ...
    while True:
        # 前向传播
        emb = C[torch.tensor([context])]  # (1, block_size, n_embd)
        h = torch.tanh(emb.view(1, -1) @ W1 + b1)
        logits = h @ W2 + b2
        probs = F.softmax(logits, dim=1)
        # 从分布中采样
        ix = torch.multinomial(probs, num_samples=1, generator=g).item()
        # 移动上下文窗口并跟踪样本
        context = context[1:] + [ix]
        out.append(ix)
        # 若采样到特殊的'.'终止符则break
        if ix == 0:
            break

    print(''.join(itos[i] for i in out))

采样的结果:

carlah. amorilli. kemri. rehty. sacessaeja. huteferniya. jareei. nellara. chaiir. kaleig. dham. jore. quint. sroel. alian. quinaelon. jarynix. kaeliigsat. edde. oia.

表现还不是很好,但总比 Bigram 中的强点了。

初始化修正#

首先,正如上面提到的初始损失过大,我们的神经网络在初始化步骤有问题。

我们在初始化的时候想要的是什么?

要预测的下一个字符有 27 种可能,我们没有理由说其中的任何一个字符要比其他字符可能性大。所以我们期望最初的概率分布是均匀分配的(1/27)。这里手动计算一下正确的损失是多少:

Screenshot 2024-02-23 at 01.15.41

比 27 小很多

这是因为一开始神经网络随机分配导致字符间的概率分布差距很大导致的,可以打断点来检查:

Screenshot 2024-02-23 at 01.22.00

logits 中应该都大概为 0,而现在这样非常极端的分布,会导致错误的自信,让损失变的很大。

由于 logits 被定义为h @ W2 + b2,所以把这些参数变小:

W2 = torch.randn((n_hidden, vocab_size),          generator=g) * 0.01
b2 = torch.randn(vocab_size,                      generator=g) * 0

Screenshot 2024-02-23 at 01.33.56

logits 中都接近 0 了

非常接近我们想要的损失 (上面提到的 3.2985)

那我们能不能把W2设为 0 呢,这样不就能得到最低的损失了吗?

你不会想去把神经网络中的权重参数设置为 exactly 0 的,这会导致对称性破坏问题,阻碍网络学习到有用的特征,所以应该只会设一个很小的数字。

现在的损失还是有一些,这被称为symmetry breaking

Screenshot 2024-02-23 at 01.32.37

来看看devv.ai对个名词的解释,顺便推荐一下这个网站,好用的很。

去掉 break,验证我们的优化,得到了期待的结果:

0/ 200000: 3.3221 
10000/ 200000: 2.1900 
...
190000/ 200000: 1.9368

Screenshot 2024-02-23 at 01.37.32

train 2.123051404953003
val 2.163424015045166

现在损失函数的初始值很正常,图像不再像一个hocky stick了,最终的性能也更好了。性能提升的原因是我们用了更多的周期去优化神经网络,而不是用最初的几千轮迭代用在压缩过高的初始权重

梯度消失#

对 h 和 hpreact 进行可视化:

Screenshot 2024-02-24 at 14.46.41

可以看到隐藏层中大量激活值分布在 ±1,这是由 pre-action 部分分布在过大的范围导致的(对于绝对值较大的输入,tanh 的输出会饱和到 ±1)。

这其实是很不好的。可以回想一下我们在 micrograd 中实现的 tanh 及其反向传播,tanh 永远只会按一定比例减小梯度。在经过 tanh 单元t=±1t=±1时,我们的梯度就消失了。因为 tanh 在输出靠近 ±1 时,处于平缓的尾部,对损失影响很小,梯度就是 0。而如果 t 是 0 的话,神经元就不活跃了(梯度原样输出)。

def tanh(self):
		# ... forward pass code
        def _backward():
            self.grad += (1 - t**2) * out.grad
        out._backward = _backward

        return out

如果一个神经元中所有例子都是 ±1 的话,那这个神经元就完全没有在学习,可以说是 “死神经元”。

观察到并没有一个神经元中完全都是白色 (绝对值> 0.99=True),神经元还是会学习的,但我们肯定还是希望在初始化上不要有这么多预激活是 ±1 的情况。

这对于其他神经网
Screenshot 2024-02-24 at 14.54.06
络中使用的非线性激活函数也是适用的,如Sigmoid这个同样起squashing neuron(挤压作用的)函数;对于ReLU,则是在pre-action是负的时候发生梯度消失,反向传播时梯度归零。除了在初始化中可能出现,如果学习率设置得过高,那么在反向传播过程中,某些神经元可能会接收到非常大的梯度更新。这种大的梯度更新可能会导致神经元的权重被更新到一个极端值,使得这个神经元在之后的前向传播过程中,对于所有的输入数据都不再产生激活(即输出一直为 0)。这种现象被形象地称为神经元被 “敲昏了”(knocked out),变成死神经元,就像是永久性脑损伤一样;比较常用的ELU也有这个问题。

Leaky ReLU则不会有这个问题,可以看到它并没有平坦的尾部,总是会得到梯度。

权重初始化#

问题的源头hpreact是嵌入后乘以 w1+b1,离 0 太远了。而我们想要的和我们之前对 logits 的期望非常相似:

W1 = torch.randn((n_embd * block_size, n_hidden), generator=g) * 0.1
b1 = torch.randn(n_hidden,                        generator=g) * 0.01

Screenshot 2024-02-24 at 15.19.03

Screenshot 2024-02-24 at 15.26.30

现在的直方图就好多了,这是因为在 pre-action 时,分布范围减少到了 (-2,1.5)

Screenshot 2024-02-24 at 15.18.11

可以看到现在不再有在 0.99 以上的神经元。

将 W1 乘的系数增大到 0.2,得到了比较满意的效果:

Screenshot 2024-02-24 at 15.29.54

  • 原始:
    train 2.2318217754364014
    val 2.251192569732666

  • 修复了 softmax 过度自信的问题:
    train 2.123051404953003
    val 2.163424015045166

  • 修复了初始化时 tanh 层过度饱和问题:
    train 2.035430431365967
    val 2.1064531803131104

可以看到,尽管初始化很糟糕,但这个网络还是学到了一定的特征。但这只是因为这个单层 MLP 网络很浅,优化问题很简单,对于这些错误比较宽容。面对更深层的网络时, 这些问题就会影响严重了。

那对于又大又深的神经网络,我们怎么设置这些调整缩放 scale 呢?

初始化策略#

我们来观察一下预激活时,对于两个高斯分布相乘后均值和标准差会发生什么变化呢?

Screenshot 2024-02-24 at 16.23.43

均值还是接近 0,因为这是对称运算,但是标准差翻了三倍,所以这个高斯分布是在延展的。

在神经网络中,我们希望在初始化时,每层的激活分布不要有太大的差异,以避免梯度消失或梯度爆炸问题。如果每层的激活值分布得太广或太狭,会导致网络在训练时的不稳定。理想情况下,网络在初始时应该具有良好的激活分布,这样可以确保信息和梯度能够有效地在网络中传播。

这里对应着一个概念:内部协变量偏移(Internal Covariate Shift

那缩放的比例设置为多少合适呢?

w = torch.randn(10, 200) * 0.3
'''
tensor(-0.0237) tensor(1.0103) 
tensor(-0.0005) tensor(0.9183)
已经接近了,那如何正好是标准正态分布呢?
'''

神经网络的权重初始化过程中,通常会将权重的初始值标准化,使得每个神经元的权重都除以其输入连接数(fan-in)的平方根。

x = torch.randn(1000, 10)
w = torch.randn(10, 200) / 10**0.5 # 输入元素数量是10

Screenshot 2024-02-24 at 16.44.42

在这个问题上引用较多的是Delving Deep into Rectifiers: Surpassing Human-Level Performance on Image Net Classification。这篇论文在研究卷积神经网络中,特别研究了 ReLU 和 P-ReLU 的非线性 (nonlinearity)。前面说过 ReLU 会将负的激活全部归零,相当于扔掉了一半的分布,因此你需要在神经网络的前向传播中对此做补偿。研究发现权重初始化必须用标准差是2/nl\sqrt{2/n_l}zero-mean Gaussian,而我们这里使用的1/nl\sqrt{1/n_l},这正是因为 ReLU 丢弃了一半的分布。

这一点在 PyTorch 中是集成了的,见链接torch.nn.init:

torch.nn.init.kaiming_normal_(tensor, a=0mode='fan_in'nonlinearity='leaky_relu'generator=None)

这个函数的参数意义如下:

  • tensor: 要初始化的张量。
  • a: 正态分布的均值,默认为 0。
  • mode: 计算标准差的模式,可选值为 'fan_in' 和 'fan_out',默认为 'fan_in'。'fan_in' 用于输入,'fan_out' 用于输出。
  • nonlinearity: 非线性激活函数的类型,可选值为 'torch.nn.functional' 中的激活函数,默认为torch.nn.functional.relu

Screenshot 2024-02-26 at 00.03.15

根据文章的结论,对于不同的nonlinearity激活函数,对应的增益也不同:

Screenshot 2024-02-26 at 00.02.03

这里的 ReLU 的增益对应着上面提到的2/nl\sqrt{2/n_l}

我们使用的 tanh 也需要这样的增益,原因在于就像 ReLU 是将负的挤压为 0,tanh 是将尾部挤压,因此需要防止这个挤压操作的增益,让分布回到标准正态分布。

多年前的研究中,神经网络在初始化问题上十分脆弱。然而现代技术创新,如残差网络、多种归一化层和比随机梯度下降更好的优化器(Adam 等), 使得 “保证初始化完全正确” 相对不再那么重要。

实践中,我们修改先前的增益,根据公式:std=5/3fan_instd=\frac{5/3}{fan\_in}

W1 = torch.randn((n_embd * block_size, n_hidden), generator=g) * (5/3)/((n_embd * block_size)**0.5) # 0.2

重新训练,得到了相近的结果。

batch norm#

批处理归一化

影响深远的Batch Normalization是谷歌的一个团队在 2015 年提出的,它让训练深度神经网络成为可能
文章提出的核心观点是:你可以把隐藏层状态 (对应我们的hpreact) 直接修正为标准正态分布。

Screenshot 2024-02-26 at 00.35.35

hpreact = embcat @ W1 + b1 # 隐藏层预处理
hpreact = (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) # batch normalization

这确保了即使是在深层网络中,每个神经元的 firing rate(即激活值)在初始化阶段都能保持在有利于梯度下降优化的近似高斯分布。但是在后面,我们需要让反向传播告诉我们这个分布应该如何变换,变的更尖锐还是更分散,对应的这会让一些神经元变的更容易 trigger happy(激活) 还是更难被激活。

因此,我们还有一个scale and shift步骤,对标准化后的分布增益并加上偏差,来得到这一层的输出。

# 添加参数
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))

parameters = [C, W1, b1, W2, b2, bngain, bnbias]

# scale and shift
hpreact = bngain * (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) + bnbias # batch normalization

在初始化时,gain 和 bias 分别是 1 和 0,此时分布是我们想要的标准正态分布。因为它们都是可微的,在后面的优化过程中可以进行反向传播。

在添加 batch norm layer 后的性能为:
train 2.0668270587921143
val 2.104844808578491

跟前面比变化不大,这是因为我们面对的是一个只有一个隐藏层的简单例子,我们可以直接计算出能让 hpreact 变成大致高斯分布的权重矩阵 (W1) 尺度,批处理归一化在这里没做什么。但可以想象的是,一旦在更深层的神经系统中,有很多不同类型的操作,调整权重矩阵的尺度会相当困难。这时候用在整个神经网络的不同层次中均匀添加 batch norm layer 就容易很多了。一般习惯是在线性层 (linear layer) 和卷积层 (convolution layer),在后面附加一个批归一化层来控制这些激活在神经网络每一个点的规模。

Batch Normalization 在实践中效果很好,并且有一些正则化 (Regularization) 的副作用,能有效地控制激活和分布。这是由于 batch 的选择是随机的,可以看作是引入了额外的 “熵 (entropy)”,降低了模型过拟合的风险。

Andrej Karpathy 提到批次归一化在数学上是 “耦合的”,即这种技术使得网络层之间的统计分布变得相互依赖。为了训练效率,我们分成了多个 batches,但在一个 batch 的数据中,每个数据点的归一化是依赖于整个批次的均值和方差的,这就导致了以下几个问题:

  1. 批次依赖性:批次归一化依赖于小批次数据的统计特性,这意味着模型的表现可能会受到批次大小的影响。对于小批次,均值和方差的估计可能会有很大的方差,导致训练不稳定。

  2. 域变化:在不同的域(如训练和推理)下,数据分布可能会有显著不同,批次归一化需要在这些不同的域下保持一致的行为。为了解决这一点,通常在训练后使用移动平均来估计整个训练集的均值和方差,用于推理。

  3. 耦合的梯度:由于批次归一化层在反向传播时计算梯度需要考虑整个批次的数据点,这意味着单个数据点的梯度更新不再是独立的。这种耦合可能限制了模型优化过程中的梯度方向和幅度。

正因为这些原因,研究者一直在寻找替代的归一化技术,比如线性归一化(Linear Normalization)层归一化(Layer Normalization)、组归一化(Group Normalization)和实例归一化(Instance Normalization)等等。

batch norm 的推理问题#

在训练阶段,每个批次的 mean 和 std 是实时计算的,用于归一化当前批次的数据。然而,在部署模型进行推理时,我们通常是一次处理一个样本,或者处理的批次大小与训练时的不同,这意味着我们不能使用单个样本的统计数据来进行批次归一化,因为这会导致过高的方差和不稳定的预测。

为了解决这个问题,这篇论文提出的方法是让 batch-norm 在训练期间计算整个数据集的均值和方差的移动平均值(moving average)。这些移动平均值随后被用来在推理时替代单个批次的统计数据。当模型被部署到生产环境中时,这些移动平均值被用来替换实时计算的批次均值和方差。这样,无论推理时的批次大小如何,模型都使用在训练数据上计算出的稳定统计数据。

# 在训练结束后校准批次归一化 

with torch.no_grad():
    # 将训练集传入
    emb = C[Xtr] 
    embcat = emb.view(emb.shape[0], -1)
    hpreact = embcat @ W1 + b1
    # 计算均值和标准差
    bnmean = hpreact.mean(0, keepdim=True)
    bnstd = hpreact.std(0, keepdim=True)

在训练集和验证集上评估性能时,将动态更新的 std 和 mean 替换为我们得到的整体平均 std、mean:

# hpreact = bngain * (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) + bnbias
hpreact = bngain * (hpreact - bnmean) / bnstd + bnbias

现在我们就也可以传入单个样例做推理了。

但实际上,没有人会愿意把 mean 和 std 的估算放在第二阶段 (也就是神经网络训练后的推理阶段)。论文中有提出一个对应这个问题的想法:我们可以以移动 (running) 的方式在神经网络训练过程中估算这两个值。

首先定义这两个值的运行时形式并做初始化:

# BatchNorm parameters
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
bnmean_running = torch.zeros((1, n_hidden))
bnstd_running = torch.ones((1, n_hidden))

之前提到过,我们初始化了 W1 和 b1 来保证每个 preact 的分布接近标准高斯,因此均值大约是 0,标准差大约为 1。

# BatchNorm layer
# -------------------------------------------------------------
bnmeani = hpreact.mean(0, keepdim=True)
bnstdi = hpreact.std(0, keepdim=True)
hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias

我们接下来要在训练过程中更新它们,在 PyTorch,这些均值和标准差并不是基于梯度下降优化方式,我们也永远不会去推导关于它们的梯度。

with torch.no_grad():
        bnmean_running = 0.999 * bnmean_running + 0.001 * bnmeani
        bnstd_running = 0.999 * bnstd_running + 0.001 * bnstdi
    # -------------------------------------------------------------

这里的 0.999 和 0.001 是移动平均中的衰减系数(decay factor)的例子。衰减系数定义了移动平均中过去值和新值的相对重要性。更具体地说:

  • 0.999momentum)是移动平均的衰减因子,决定了之前累积的运行均值和方差保持多少。
  • 0.001(等于 1 - momentum)表示新值的权重,这是每个批次的均值和方差在更新时的权重。

论文中还有这样一步:

Screenshot 2024-02-29 at 16.06.15

比如ϵ\epsilon默认为1e51e^{-5},作用基本上就是防止除以 0,对应BATCHNORM1D中的 eps 参数

其中还有一个地方是不必要的:

# Linear layer
    hpreact = embcat @ W1 # + b1 去掉这里的bias# 隐藏层预处理

# BatchNorm layer
    bnmeani = hpreact.mean(0, keepdim=True)
    bnstdi = hpreact.std(0, keepdim=True)
    hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias

Linear layer 这里 hpreact 的 bias 现在实际上无用的,因为我们后面又对每个神经元减去了 mean,所以这些 bias 不会影响后面的计算。

所以当你使用批处理归一化层时,如果你之前有权重层,如线性层或者卷积层,那就没必要有 bias。这不会有什么负面影响,只不过是训练不会得到它的梯度,有些浪费而已。

重新整理一下网络训练的整体结构:

# 批次归一化的参数和缓冲区
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
bnmean_running = torch.zeros((1, n_hidden))
bnstd_running = torch.ones((1, n_hidden))

max_steps = 200000
batch_size = 32
lossi = []

for i in range(max_steps):

    # 构建 minibatch
    ix = torch.randint(0,Xtr.shape[0],(batch_size,), generator=g)
    Xb, Yb = Xtr[ix], Ytr[ix] # batch X, Y

    # forward pass
    emb = C[Xb] # embedding 
    embcat = emb.view(emb.shape[0], -1) # 拼接所有嵌入向量
    # Linear layer
    hpreact = embcat @ W1 # + b1 # 隐藏层预处理
    # BatchNorm layer
    # -------------------------------------------------------------
    bnmeani = hpreact.mean(0, keepdim=True)
    bnstdi = hpreact.std(0, keepdim=True)
    hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias
    with torch.no_grad():
        bnmean_running = 0.999 * bnmean_running + 0.001 * bnmeani
        bnstd_running = 0.999 * bnstd_running + 0.001 * bnstdi
    # -------------------------------------------------------------
    # Non-linearity
    h = torch.tanh(hpreact) # 隐藏层
    logits = h @ W2 + b2 # 输出层 
    loss = F.cross_entropy(logits, Yb) # 损失函数

    # backward pass
    for p in parameters:
        p.grad = None  
    loss.backward()

    # update
    lr = 0.1 if i < 100000  else 0.01
    for p in parameters:
        p.data += -lr * p.grad 
    
    # tracks stats
    if i % 10000 == 0:
        print(f'{i:7d}/{max_steps:7d}: {loss.item():.4f}')
    lossi.append(loss.log10().item())

resnet中的结构为例:

Screenshot 2024-02-29 at 17.10.41

这里的 conv 卷积层基本上和我们前面用的线性层是一样的,只不过卷积层是用于图像的,所以它们有空间结构。而线性层是处理成块的图像数据,全连接层没有空间概念。

基本结构和我们现前看到的一样,都是一个 weight layer,一个 normalization layer 和 nonlinearity。

总结(省流)#

对激活,梯度以及它们在神经网络中的统计特性的理解是非常重要的,特别是当神经网络更大更深时。

回顾一下前面的内容:

  1. 初始化修正:如果我们有过度自信的错误预测,最后一层的激活会相当混乱,可能会以 hockey stick 的损失函数告终,解决这个问题会让损失变好,因为你省去了浪费的训练。

  2. 权重初始化:后面我们得知要控制激活的分布,我们不希望它们被压缩到 0 或者到无穷远,你希望神经网络中所有东西都是均匀的,接近高斯分布。等价的问题是 “如何缩放权重矩阵和初始化过程中的偏差”,暂时可以查表来精确地定义权重和偏差。

  3. batch norm:但网络更大更深后就需要引入归一化层了,其中最先出现的是batch normalization。基本思想是如果想要大致高斯分布,那就取均值和标准差并将数据居中

  4. batch norm 的推理问题:将均值和标准差的估计放到训练部分中,得到了 batch norm layer,它能够非常有效地控制神经网络中激活的统计特性。但是没人喜欢这一层,因为它会带来很多 bug,尽量避免它们选用其它替代方案如group normalizationlayer normalization

在使用像 Adam 这样的高级优化器,或是残差连接 (Residual connection),训练神经网络基本上就是这样的,你需要保证每一步都十分精确,需要从初始化考虑到激活和梯度,此时要训练非常深入的网络是不可能的。

补充 1:为什么需要 tanh 层#

为什么我们要把它们包括在内,还要考虑它们的增益呢?

答案很简单,如果你只有一堆线性层,我们当然会很容易得到很好的激活,但就其表表示而言整体就只是一个线性层,不管怎么叠加都只能得到一个线性变换。

tanh nonlinearity 就是让我们把这个 “线性叠成的三明治” 从线性函数变成一个可以近似任意函数的神经网络。

补充 2:学习率的设置#

Andrej Karpathy 提到了一种用来检查学习率是否设置得当的经验性方法,这个方法涉及到计算梯度更新与参数值本身的比例,这个比例被称为 "ratio"。

这里的 "ratio" 是指在单次参数更新中参数变化量与参数当前值的比率。通常,这个比率是通过计算参数更新步长(即梯度乘以学习率)与参数值的绝对值的比来得到的。比如说,如果有一个参数 ww 和它的梯度 gg,学习率为 η\eta,那么单次更新的 "ratio" 可以表示为:

ratio=ηgw\text{ratio} = \frac{|\eta \cdot g|}{|w|}

如果这个比率在某个合理的范围内,比如在 -3 附近,这意味着学习率设置得既不会太大,以至于使得参数更新过猛,也不会太小,以至于训练过程缓慢无效。这个范围确保了参数每次更新是温和的,既能促进学习,又不至于因为过大的更新步长导致训练不稳定。

在实际操作中,这个 "ratio" 可以用来监控和调整学习率,如果这个比率过大,可能需要减小学习率;如果这个比率过小,可能意味着可以尝试增大学习率。这是一种启发式的方法,可以帮助研究人员和实践者调试他们的神经网络模型。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。