携手创作,共同生长!这是我参加「日新计划 8 月更文挑战」的第14天,点击检查活动详情

文章或许写有些匆促,细节或许不算到位,先宣布,随后会不定期更新和弥补内容

今天咱们主要来重新审视一下神经网络的参数优化和初始化。跟着神经网络层数不断增加,咱们就会遇到各式各样由于层数增加所带来的问题。最重要便是咱们需要一个安稳的梯度流,来更新参数,假如发生了梯度消失和梯度爆炸。为了防备这些问题发生,咱们就有必要来看一看参数初始化和优化。

首要咱们来尝试各种参数初始化的办法,咱们先从简略开端,然后一步一步加大难度,通过成果来剖析每种初始化方法对练习影响。随后会还会评论优化器对练习进程的影响,通过比较 SGD、带动量的 SGD 以及 Adam 来检查这些优化器对练习带来哪些影响。

引进依靠模块

规范库

import os
import json
import math
import numpy as np
import copy

制作相关库

import matplotlib.pyplot as plt
from matplotlib import cm
%matplotlib inline
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('svg', 'pdf') # For export
import seaborn as sns
sns.set()

进度条

from tqdm.notebook import tqdm

PyTorch

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as data
import torch.optim as optim
# 保存数据集的目录(e.g. MNIST)
DATASET_PATH = "/data"
# 保存预练习模型的目录
CHECKPOINT_PATH = "../saved_models/tutorial4"
# Function for setting the seed
def set_seed(seed):
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)
set_seed(42)

用于确保所有在 GPU 上运算都是一致性

torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

遍历可用的设备,假如有可用 GPU 首选在 GPU 设备上运转代码

device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
print("Using device", device)

准备数据集

和之前关于激活函数共享类似,这儿也运用一个全连接网络,数据集仍旧选用的是 FashionMNIST。

from torchvision.datasets import FashionMNIST
from torchvision import transforms

改换

对数据集应用改换,首要将数据集数据转换为 tensor,然后将数据集图画像素值进行归一化为规范正态散布,均值为 0 方差为 1。

transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Normalize((0.2861,), (0.3530,))
                               ])

下载练习数据集,然后将练习数据集拆分为练习集和验证集 2 个部分

train_dataset = FashionMNIST(root=DATASET_PATH, train=True, transform=transform, download=True)
train_set, val_set = torch.utils.data.random_split(train_dataset, [50000, 10000])

下载测试数据集

test_set = FashionMNIST(root=DATASET_PATH, train=False, transform=transform, download=True)

咱们界说一系列数据加载器,这些数据加载用于加载练习数据、验证数据或者测试数据。值得留意的是在实际练习进程会选用小批量的


train_loader = data.DataLoader(train_set, batch_size=1024, shuffle=True, drop_last=False)
val_loader = data.DataLoader(val_set, batch_size=1024, shuffle=False, drop_last=False)
test_loader = data.DataLoader(test_set, batch_size=1024, shuffle=False, drop_last=False)

与之前的共享比较,这儿数据集规范化界说有所不同,并不是简略地给出(0.5,0.5) 咱们期望通过规范化后,图画像素散布是是均值为 0 规范差为 1 的这样的散布。

规范化是将图画像素值规范化到均值为 0 规范差为 1 的范围,这个与随后咱们评论的初始有关,通过计算图画像素的均值和规范,然后用均值和规范差对数据集图画做规范化。

print("Mean", (train_dataset.data.float() / 255.0).mean().item())
print("Std", (train_dataset.data.float() / 255.0).std().item())
Mean 0.2860923707485199
Std 0.3530242443084717
imgs, _ = next(iter(train_loader))
print(f"Mean: {imgs.mean().item():5.3f}")
print(f"Standard deviation: {imgs.std().item():5.3f}")
print(f"Maximum: {imgs.max().item():5.3f}")
print(f"Minimum: {imgs.min().item():5.3f}")
Mean: 0.002
Standard deviation: 1.001
Maximum: 2.022
Minimum: -0.810

留意最大值和最小值并不是 1 和 -1 ,这个或许是由于 FashionMNIST 中包含一些 black 像素值,整个散布向整数方向有所偏移

class BaseNetwork(nn.Module):
    def __init__(self, act_fn, input_size=784, num_classes=10, hidden_sizes=[512, 256, 256, 128]):
        """
        Inputs:
            act_fn - 激活函数的对象,作为神经网络非线性部分
            input_size - 输入图画尺度(单位为像素)
            num_classes - 要猜测的类别数
            hidden_sizes - 躲藏层神经元数量列表
        """
        super().__init__()
        layers = []
        layer_sizes = [input_size] + hidden_sizes
        for layer_index in range(1, len(layer_sizes)):
            layers += [nn.Linear(layer_sizes[layer_index-1], layer_sizes[layer_index]),
                       act_fn]
        layers += [nn.Linear(layer_sizes[-1], num_classes)]
        self.layers = nn.ModuleList(layers) # A module list registers a list of modules as submodules (e.g. for parameters)
        self.config = {"act_fn": act_fn.__class__.__name__, "input_size": input_size, "num_classes": num_classes, "hidden_sizes": hidden_sizes}
    def forward(self, x):
        x = x.view(x.size(0), -1)
        for l in self.layers:
            x = l(x)
        return x

关于激活函数,这一次运用 Pytorch 供给的 torch.nn 包中供给激活函数,而不再是自己去实现激活函数。不过这儿界说了一个 Identity激活函数,尽管这个激活函数在很大程度上约束模型的能力,运用这个激活意图在于将问题简化便于研究初始化。

class Identity(nn.Module):
    def forward(self, x):
        return x
act_fn_by_name = {
    "tanh": nn.Tanh,
    "relu": nn.ReLU,
    "identity": Identity
}

终究,咱们来界说几个制作工具,用于将一些数据以图表方法展示出来

  • 检查权重/参数的散布
  • 检查各个层参数的梯度
  • 激活层输出

关于这些制作工具详细实现在下面逐个列出,大家假如感兴趣能够看一下

制作散布

def plot_dists(val_dict, color="C0", xlabel=None, stat="count", use_kde=True):
    columns = len(val_dict)
    fig, ax = plt.subplots(1, columns, figsize=(columns*3, 2.5))
    fig_index = 0
    for key in sorted(val_dict.keys()):
        key_ax = ax[fig_index%columns]
        sns.histplot(val_dict[key], ax=key_ax, color=color, bins=50, stat=stat,
                     kde=use_kde and ((val_dict[key].max()-val_dict[key].min())>1e-8)) # Only plot kde if there is variance
        key_ax.set_title(f"{key} " + (r"(%i $\to$ %i)" % (val_dict[key].shape[1], val_dict[key].shape[0]) if len(val_dict[key].shape)>1 else ""))
        if xlabel is not None:
            key_ax.set_xlabel(xlabel)
        fig_index += 1
    fig.subplots_adjust(wspace=0.4)
    return fig

制作权重散布

def visualize_weight_distribution(model, color="C0"):
    weights = {}
    for name, param in model.named_parameters():
        if name.endswith(".bias"):
            continue
        key_name = f"Layer {name.split('.')[1]}"
        weights[key_name] = param.detach().view(-1).cpu().numpy()
    ## Plotting
    fig = plot_dists(weights, color=color, xlabel="Weight vals")
    fig.suptitle("Weight distribution", fontsize=14, y=1.05)
    plt.show()
    plt.close()

制作梯度

def visualize_gradients(model, color="C0", print_variance=False):
    """
    Inputs:
        net - Object of class BaseNetwork
        color - Color in which we want to visualize the histogram (for easier separation of activation functions)
    """
    model.eval()
    small_loader = data.DataLoader(train_set, batch_size=1024, shuffle=False)
    imgs, labels = next(iter(small_loader))
    imgs, labels = imgs.to(device), labels.to(device)
    # Pass one batch through the network, and calculate the gradients for the weights
    model.zero_grad()
    preds = model(imgs)
    loss = F.cross_entropy(preds, labels) # Same as nn.CrossEntropyLoss, but as a function instead of module
    loss.backward()
    # We limit our visualization to the weight parameters and exclude the bias to reduce the number of plots
    grads = {name: params.grad.view(-1).cpu().clone().numpy() for name, params in model.named_parameters() if "weight" in name}
    model.zero_grad()
    ## Plotting
    fig = plot_dists(grads, color=color, xlabel="Grad magnitude")
    fig.suptitle("Gradient distribution", fontsize=14, y=1.05)
    plt.show()
    plt.close()
    if print_variance:
        for key in sorted(grads.keys()):
            print(f"{key} - Variance: {np.var(grads[key])}")

制作激活函数

def visualize_activations(model, color="C0", print_variance=False):
    model.eval()
    small_loader = data.DataLoader(train_set, batch_size=1024, shuffle=False)
    imgs, labels = next(iter(small_loader))
    imgs, labels = imgs.to(device), labels.to(device)
    # Pass one batch through the network, and calculate the gradients for the weights
    feats = imgs.view(imgs.shape[0], -1)
    activations = {}
    with torch.no_grad():
        for layer_index, layer in enumerate(model.layers):
            feats = layer(feats)
            if isinstance(layer, nn.Linear):
                activations[f"Layer {layer_index}"] = feats.view(-1).detach().cpu().numpy()
    ## Plotting
    fig = plot_dists(activations, color=color, stat="density", xlabel="Activation vals")
    fig.suptitle("Activation distribution", fontsize=14, y=1.05)
    plt.show()
    plt.close()
    if print_variance:
        for key in sorted(activations.keys()):
            print(f"{key} - Variance: {np.var(activations[key])}")

首要咱们需要简化问题,为了更好剖析线性神经网络初始化,咱们先选用 Identity 这个激活函数,好处是让神经网络不会受到激活函数干扰,因不同激活函数关于初始化或许存在不相同反应,也便是不同激活函数和参数初始化方法不同会发生不同作用。随后能够依据激活函数挑选来选用不同初始化。

深入浅出 Pytorch 系列 — 参数初始化(1)

关于深度学习框架,不同于咱们熟知开发软件,更多时分,很少会有逻辑这样 bug,未达到预期作用背面原因有许多、网络结构、数据集、参数的初始化、优化器的挑选和方针函数界说等等。

参数初始化

早期参数初始化都是将数据和参数做均值为 0 方差为 1 规范化处理。跟着神经网络层数加深,这样方法来初始化参数往往并不能解决梯度消失和梯度爆炸的问题。

激活值的方差是逐层递减的,这导致反向传达中的梯度也逐层递减。要解决梯度消失,就要避免激活值方差的衰减,最理想的状况是,每层的输出值(激活值)保持正态散布。

整个大型前馈神经网络无非便是一个超级大映射,将原始样本安稳的映射成其的类别。也便是将样本空间映射到类别空间。试想,假如样本空间与类别空间的散布差异很大,

  • 类别空间特别稠密,样本空间特别稀少辽阔,那么在类别空间得到的用于反向传达的误差丢给样本空间后几乎变得微乎其微,也便是会导致模型的练习十分缓慢。
  • 类别空间特别稀少,样本空间特别稠密,那么在类别空间算出来的误差丢给样本空间后几乎是爆炸般的存在,即导致模型发散震荡,无法收敛。因此,咱们要让样本空间与类别空间的散布差异(密度差别)不要太大,也便是要让它们的方差尽或许持平。

对方差的性质进行简略回忆

在开端之前,咱们先简略复习关于方差和均值的公式,当 XXYY 彼此独立

Var(X+Y)=Var(X)+Var(Y)Var(X + Y) = Var(X) + Var(Y)

方差的计算公式

Var(X)=E(X2)−E(X)2Var(X) = \mathbb{E}(X^2) – \mathbb{E}(X)^2

为了说明问题,咱们这儿神经网络只要一层,且该层只要一个神经元,并且暂时不考虑偏置所以就有

y=w1x1+w2x2+w3x3y=w_1x_1 + w_2x_2 + w_3x_3

在练习神经网络之前,咱们需要对参数进行初始化,那么应该如何挑选参数,才能让神经网络顺畅练习起来。

深入浅出 Pytorch 系列 — 参数初始化(1)

关于网络参数初始化,咱们能够将 w=[0,0,0]w = [0,0,0] 也便是将 w 初始化为全 0 的向量。那么假如咱们原来基础上给这个层再去增加一个神经元,那么现在就有了两个神经元,并且假设这两个神经元权重都是 0 那么也便是

w1=[w11,w12,w13]=[0,0,0]w2=[w21,w22,w23]=[0,0,0]w_1 = [w_{11},w_{12},w_{13}] = [0,0,0]\\ w_2 = [w_{21},w_{22},w_{23}] = [0,0,0]\\

深入浅出 Pytorch 系列 — 参数初始化(1)

然后咱们去计算梯度 w11w_{11}w21w_{21}

∂L∂w11=∂L∂y1x1\frac{\partial L}{\partial w_{11}} = \frac{\partial L}{\partial y_1} x_1
∂L∂w12=∂L∂y2x1\frac{\partial L}{\partial w_{12}} = \frac{\partial L}{\partial y_2} x_1

不难看出由于参数相同以及输入值相同所以 y1y_1y2y_2 有相同值,计算梯度也是具有相同值,那么这两个神经元具有相同权重初始值,在反向传达时,梯度也是相同的,那么这两个神经元的改变在学习中也是一直保持一致,这时神经元便是失去了差异性,然后无法通过这两神经元来学习到表达更复杂的特征

一般咱们都会用一个正态散布来随机初始化参数,那么为了简化问题,咱们将输入 x1,x2,x3x_1,x_2,x_3 假设为 1 那么就得到了

y=w1+w2+w3y = w_1 + w_2 + w_3

由于 w1,w2,w3w_1,w_2,w_3 是独立同散布的采样,依据上面独立随机变量方差求和公式能够得到下面式子

Var(y)=Var(w1)+Var(w2)+Var(w3)Var(y) = Var(w_1) + Var(w_2) + Var(w_3)

然后能够计算 y 的规范差为

std(y)=3=1.732std(y) = \sqrt{3} = 1.732

这也便是表明输入通过神经元的激活函数之后,对输入离散程度被提高了,假如现在神经元是 n 输入那么 通过激活函数输出的方差就变为原有 n 倍,咱们期望输入方差和输出方差是持平,依次作为方针来来设置参数。

y=∑i=1nwiVar(y)=nVar(wi)=nStd(y)=ny = \sum_{i=1}^n w_i\\ Var(y) = n Var(w_i) = n\\ Std(y) = \sqrt{n}

Xavier 参数初始化

本着让输入方差和输出方差持平准则来设计参数 w 的方差和均值,关于参数 w 均值设置为 0 那么主要是看其方差应该如何挑选。为了让输入方差和输出方差都为 1。

Var(y)=nVar(wi)=1Var(y) = nVar(w_i) = 1

这样求出 Var(wi)=1nVar(w_i) = \frac{1}{n} 假如咱们考虑输入层,那么 wiw_i 方差就等于 1/n

Var(wi)=2nin+noutVar(w_i) = \frac{2}{n_{in} + n_{out}}

正态散布初始化

N(0,2nin+nout)\cal{N}(0,\frac{2}{n_{in} + n_{out}})

这个正态散布中随机采样即可

W1 = torch.randn(784, nh) * math.sqrt(1 / 784)
b1 = torch.zeros(nh)
W2 = torch.randn(nh, 1) * math.sqrt(1 / nh)
b2 = torch.zeros(1)
z1 = linear(x_train, W1, b1)
print(z1.mean(), z1.std())
tensor(0.1031) tensor(0.9458)
a1 = relu(z1)
a1.mean(), a1.std()
(tensor(0.4272), tensor(0.5915))

均匀散布

当 x 满意从 a 到 b 的均匀散布时,那么 x 方差等于

Var(x)=E(x2)−E(x)2=(b−a)212Var(x) = \mathbb{E}(x^2) – \mathbb{E}(x)^2 = \frac{(b – a)^2}{12}

为了确保采样均值为 0 那么能够将选用写成从 -a 到 a 的均匀散布,那么现在方差也便是

Var(x)=a23Var(x) = \frac{a^2}{3}

那么现在假如咱们将方针方差带入公式

Var(wi)=2nin+noutVar(w_i) = \frac{2}{n_{in} + n_{out}}
u(−6nin+nout,6nin+nout)\cal{u}\left(- \sqrt{\frac{6}{n_{in} + n_{out}}} ,\sqrt{\frac{6}{n_{in} + n_{out}}} \right)

Xavier 初始化(亦称Glorot 初始化) 开始由Xavier Glorot 等人在2010 的 “Understanding the difficulty of training deep feedforward neural networks” 一文中提出,其核心思想是使层的输出数据的方差与其输入数据的方差持平。而在 Xavier 初始化的论文中,作者只考虑了当时默认的 logistic sigmoid 激活函数。

kaiming 参数初始化

由于relu会抛弃掉小于0的值,关于一个均值为0的data来说,这就相当于砍掉了一半的值,这样一来,均值就会变大,前面Xavier初始化公式中E(x)=mean=0的状况就不成立了。依据新公式的推导,终究得到新的rescale系数

由于通过 ReLU 激活函数,大概有一半输出会变成 0

Var(y)=12nVar(wi)=1Var(y) = \frac{1}{2}nVar(w_i) = 1

那么咱们方针方差就变为

Var(wi)=2nVar(w_i) = \frac{2}{n}

kaiming 参数初始化

假如咱们用的是 LeakyReLU 这样激活函数那么

Var(wi)=2n(1+2)Var(w_i) = \frac{2}{n(1 + \alpha^2)}

正态初始化

N(0,2n(1+2))\cal{N}(0,\frac{2}{n(1 + \alpha^2)})

均匀散布初始化

u(−6n(1+2),6n(1+2))\cal{u}\left(- \sqrt{\frac{6}{n(1 + \alpha^2)}} ,\sqrt{\frac{6}{n(1 + \alpha^2)}} \right)
W1 = torch.randn(784, nh) * math.sqrt(2 / 784)
b1 = torch.zeros(nh)
W2 = torch.randn(nh, 1) * math.sqrt(2 / nh)
b2 = torch.zeros(1)
z1 = linear(x_train, W1, b1)
a1 = relu(z1)
a1.mean(), a1.std()
(tensor(0.4553), tensor(0.7339))