%%HTML

感知机Perceptron 多层感知机MLP

$$ \begin{aligned} &输入\mathbf{X},标签\mathbf{y},权重\mathbf{W},偏置b, \\ &输出\mathbf{o}=\mathcal{a}(\mathbf{X}\cdot\mathbf{W}+\vec{b}) \\ &激活函数\mathcal{a}(x) = \begin{cases} +1, x \ge 0 \\ -1, x < 0 \end{cases} \end{aligned} $$

$$ \begin{aligned} 激活函数一定要是非线性的,常用的如下, \\ \mathsf{sigmod}(x) = \frac{1}{1+\exp(-x)} \\ \tanh(x) = \frac{1-\exp(-2x)}{1+\exp(-2x)} \\ \mathsf{ReLU}(x) = \max(0, x) \\ \end{aligned} $$

'''0_0'''

从现在开始,包导入放在统一放在cell0_0处。

import importlib
import LIMU
import torch
import math
import numpy as np

from torch import nn
from torch import optim
from torch.utils.data import DataLoader
from torch.utils.data import TensorDataset
'''0_1'''

Jupyter,如果LIMU模块有变化,需要重新加载。

importlib.reload(LIMU)
from LIMU import Chapter00
from LIMU import ZigzagChartAnimator

绘制图表时,使用svg格式,清晰度高。

%config InlineBackend.figure_format = 'svg'

多层感知机的从零开始实现

'''1_1'''
iterate_devset, iterate_testset = Chapter00.load_data_fashion_mnist(batch_size=256)

1个隐藏层,输入层、输出层、隐藏层节点数。

num_inputs, num_outputs, num_hiddens = 784, 10, 256

初始化参数,输入层->隐藏层1(W1,b1)、隐藏层1->输出层(W2,b2)。

权重不宜初始化过大,否则会导致梯度爆炸,激活值饱和,过拟合!

W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))
params = [W1, b1, W2, b2]

def relu_active(X):
"""ReLU激活函数。"""
return torch.max(X, torch.zeros_like(X))
def votre_modele(X):
"""定义你的模型。"""
X = X.reshape((-1, num_inputs))
H = relu_active(X @ W1 + b1)
return (H @ W2 + b2)

一定要传reduction='none',保留每个样本的损失,返回一个向量。

在下面的训练过程中,metric[0]累加的是CE.sum(),会将所有样本的损失累加起来再除以样本总数。

loss_function = nn.CrossEntropyLoss(reduction='none')
train_optimizer = optim.SGD(params, lr=0.1)

Chapter00.training_classifier_1on_fashion_mnist(votre_modele,
10, iterate_devset, iterate_testset, loss_function, train_optimizer)

训练轮次结束,在测试数据集上的准确率收敛在0.84以上,说明模型训练效果良好。

多层感知机的简洁实现

'''1_2'''
iterate_devset, iterate_testset = Chapter00.load_data_fashion_mnist(batch_size=256)

votre modèle 法语:你的模型。

votre_modele = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))
def init_weights(mo):
if isinstance(mo, nn.Linear):
nn.init.normal_(mo.weight, mean=0, std=0.01)
votre_modele.apply(init_weights)
loss_function = nn.CrossEntropyLoss(reduction='none')
train_optimizer = optim.SGD(votre_modele.parameters(), lr=0.1)
Chapter00.training_classifier_1on_fashion_mnist(votre_modele,
10, iterate_devset, iterate_testset, loss_function, train_optimizer)

炼丹

数据集划分

  1. 开发数据集(Jeu de données de Développement):用于训练模型,让模型学习数据的特征,自学习参数如权重,通常占70%。
  2. 验证数据集(Jeu de données de Validation):评估模型好坏,并调整模型架构、超参数如学习率,通常占15%。
  3. 测试数据集(Jeu de données de Test):训练完成后才使用,只能用一次!用于最终评估模型的泛化能力,通常占15%。

K块交叉验证

在没有足够多的数据时使用。过程:

将训练数据分为K个块;

POUR k DANS LA PLAGE(K):

    块k作开发数据集,其余K-1个块作验证数据集;

报告K个验证集误差的平均。

欠拟合过拟合

模型容量/数据简单复杂
正常欠拟合
过拟合正常

多项式回归

用以下三次多项式来生成开发和测试数据的标签:

$ y = 5 + 1.2x - {\Large\frac{3.4}{2!}}x^2 + {\Large\frac{5.6}{3!}}x^3 + \epsilon \quad \texttt{where} \, \epsilon \sim \mathsf{Normal}(0, 0.01) $

'''2_1'''

开发、测试样本数目。

num_dev, num_test = 100, 100

权重长度为20,除了前4个其它都为0。

my_w = np.zeros(20)
my_w[0:4] = np.array([5, 1.2, -3.4, 5.6])

features即自变量x,形状为(200, 1),列向量。

features = np.random.normal(size=(num_dev+num_test, 1))
np.random.shuffle(features)

一行代码完成多项式特征生成和阶乘归一化,monomial_basis的形状为(200, 20),

对应200个样本,每个样本1个特征——的0~19次单项式基除以阶乘。

monomial_basis = np.power(features, np.arange(20).reshape(1, -1))
/ np.array([math.factorial(i) for i in range(20)])

labels即因变量y,形状为(200,),一维数组。

labels = np.dot(monomial_basis, my_w) + np.random.normal(scale=0.1, size=(num_dev+num_test,))

查看一下前2个样本。

print(features.shape, monomial_basis.shape, labels.shape)
print(features[:2], monomial_basis[:2,:], labels[:2], sep='\n')

'''2_2'''
def eval_loss_(your_model, iterate_data, loss_func):
"""评估模型在测给定数据集上的损失。"""
metric = [0.0, 0.0]
for X, y in iterate_data:
out = your_model(X)
SE = loss_func(out, y)

将torch.Tensor转为float,切断计算图,兼容matplotlib绘图。

metric[0] += SE.sum().item()
metric[1] += SE.numel()
return metric[0] / metric[1]
def update_model_variable(your_model, iterate_data, loss_func, train_optimizer):
"""用给定数据集更新模型的参数。"""
your_model.train()
for X, y in iterate_data:
out = your_model(X)
SE = loss_func(out, y)
train_optimizer.zero_grad()
SE.mean().backward()
train_optimizer.step()

nparr_to_tensor = lambda _feature, _label: TensorDataset(

_labels需要从一维数组重塑成列向量,确保与模型输出形状一致,☝️第8行、第18行用到。

torch.tensor(_feature, dtype=torch.float32), torch.tensor(_label.reshape(-1, 1), dtype=torch.float32))
def train_fitting_model(dev_features, dev_labels, test_features, test_labels, train_epochs=400):
"""训练模型来拟合数据集。"""

一定要注意传参顺序与原文不一样。

一定要传reduction='none',保留每个样本的损失,返回一个向量。

loss_func = nn.MSELoss(reduction='none')
input_shape = dev_features.shape[-1]

多项式中已经包含偏置,input_shape等于函数被调用时入参切片大小。

your_model = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
batch_size = min(10, dev_labels.shape[0])
iterate_devset = DataLoader(nparr_to_tensor(dev_features, dev_labels), batch_size=batch_size, shuffle=True)
iterate_testset = DataLoader(nparr_to_tensor(test_features, test_labels), batch_size=batch_size, shuffle=False)
train_optimizer = optim.SGD(your_model.parameters(), lr=0.01)
info_xy_ = ('训练轮数','损失',(1,train_epochs),(0.001,100),'linear','log')
animator = ZigzagChartAnimator('多项式拟合', ('deve', 'test'), info_xy_)
for epoch in range(train_epochs):

用开发数据集更新模型参数。

update_model_variable(your_model, iterate_devset, loss_func, train_optimizer)
if epoch == 0 or (epoch + 1) % 20 == 0:
animator.insert_point(epoch + 1,
[eval_loss_(your_model, iterate_devset, loss_func), eval_loss_(your_model, iterate_testset, loss_func)])
print("多项式系数:", your_model[0].weight.detach().numpy())

'''2_3'''

正常,用三次多项式拟合,传入:前100个样本的前4个特征,前100个标签,后100个样本的前4个特征,后100个标签。

train_fitting_model(monomial_basis[:num_dev, :4], labels[:num_dev], monomial_basis[num_dev:, :4], labels[num_dev:])
'''2_4'''

欠拟合,用一次多项式拟合,input_shape=2。

train_fitting_model(monomial_basis[:num_dev, :2], labels[:num_dev], monomial_basis[num_dev:, :2], labels[num_dev:])
'''2_5'''

过拟合,用高次多项式拟合,input_shape=20。

train_fitting_model(monomial_basis[:num_dev, :], labels[:num_dev], monomial_basis[num_dev:, :], labels[num_dev:])

权重衰减

也称为L2正则化"Lebesgue2-Regularization",目的是为了防止过拟合,强迫模型的权重参数保持较小的数值(趋向于0),限制了模型的复杂度,使其生成的函数曲线更加平滑,从而提高模型的泛化能力。

在原损失函数(如平均平方误差、交叉熵)上,增加一个罚项penalty:

$ \mathcal{Loss}\{\texttt{new}\}(\mathbf{w})=\mathcal{Loss}\{\texttt{old}\}(\mathbf{w})+{\large\frac{\mu}{2}}|\mathbf{w}|^2. $

此处可以不考虑偏置,$\mu$是正则化的强度,$\eta$是学习率,从而新的权重更新公式为:

$ \mathbf{w} \leftarrow (1-\eta\mu)\mathbf{w}-\eta\cdot\frac{\partial\mathcal{Loss}\{\texttt{old}\}}{\partial\mathbf{w}}, \quad \eta\mu<1.$

像之前一样,生成一些随机数据,演示权重衰减。

$ y = 0.05 + \sum x_i + \epsilon, \quad \epsilon \sim \mathsf{Normal}(0, 0.01^2). $

'''3_1'''

开发数据少一点,容易过拟合。

num_dev, num_test, num_inputs, batch_size = 20, 100, 200, 5
my_w, my_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
sample_linear_dev = Chapter00.synthetic_data_linear_noise(my_w, my_b, num_dev)

000.ipynb.cell3_1.load_data_fashion_mnist(),001.ipynb.cell2_2.train_fitting_model(),

都有下面这个步骤,将Numpy数组、Torch张量转换为Dataloader对象。

iterate_devset = DataLoader(TensorDataset(*sample_linear_dev), batch_size, shuffle=True)
sample_linear_test = Chapter00.synthetic_data_linear_noise(my_w, my_b, num_test)

测试数据集,不打乱顺序。

iterate_testset = DataLoader(TensorDataset(*sample_linear_test), batch_size, shuffle=False)

从零开始实现权重衰减

'''3_2'''

L2范数惩罚项。

l2norm_penalty = lambda w: torch.sum(w.pow(2)) / 2
def train_weight_decay_penalty(wd, lr=0.003, train_epochs=100):
"""使用L2范数惩罚项训练模型。"""

初始化模型参数。

its_w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
its_b = torch.zeros(1, requires_grad=True)
your_model = lambda X: torch.matmul(X, its_w) + its_b
loss_function = Chapter00.squared_error
info_xy_ = ('训练轮数','损失',(5,train_epochs),(0.0001,400),'linear','log')
animator = ZigzagChartAnimator('权重衰减', ('deve', 'test'), info_xy_)
for epoch in range(train_epochs):
for X, y in iterate_devset:
SE = loss_function(your_model(X), y) + wd * l2norm_penalty(its_w)
SE.sum().backward()
Chapter00.stochastic_gradient_descent([its_w, its_b], lr, batch_size)
if (epoch + 1) % 5 == 0:
animator.insert_point(epoch,
(Chapter00.eval_loss_(your_model, iterate_devset, loss_function),
Chapter00.eval_loss_(your_model, iterate_testset, loss_function)))
print(f"训练完成,its_w的L2范数是{torch.norm(its_w).item()}。")

'''3_3'''

wd=0禁用权重衰减,开发误差有了减少,测试误差没有减少,意味着过拟合了。

train_weight_decay_penalty(wd=0)
'''3_4'''

使用权重衰减,开发误差增大,但测试误差减少,正是我们期望从正则化中获得的效果。

train_weight_decay_penalty(wd=3)

简洁实现权重衰减

'''3_5'''
def train_weight_decay_penalty(wd, lr=0.003, train_epochs=100):
"""使用权重衰减训练模型。"""

votre modèle 法语:你的模型。

votre_modele = nn.Sequential(nn.Linear(num_inputs, 1))
for param in votre_modele.parameters():

权重、偏置都由标准正态分布来随机初始化,in-place就地操作。

param.data.normal_()
loss_function = nn.MSELoss(reduction='none')
train_optimizer = optim.SGD([
{'params': votre_modele[0].weight, 'weight_decay': wd},
{'params': votre_modele[0].bias}], lr=lr)
info_xy_ = ('训练轮数','损失',(5,train_epochs),(),'linear','log')
animator = ZigzagChartAnimator('权重衰减', ('deve', 'test'), info_xy_)
for epoch in range(train_epochs):
for X, y in iterate_devset:
train_optimizer.zero_grad()
SE = loss_function(votre_modele(X), y)
SE.mean().backward()
train_optimizer.step()
if (epoch + 1) % 5 == 0:
animator.insert_point(epoch + 1,
(Chapter00.eval_loss_(votre_modele, iterate_devset, loss_function),
Chapter00.eval_loss_(votre_modele, iterate_testset, loss_function)))
print(f"训练完成,您的模型的权重的L2范数是{votre_modele[0].weight.norm().item()}。")

'''3_6'''
train_weight_decay_penalty(wd=0)
'''3_7'''
train_weight_decay_penalty(wd=3)
'''3_8'''
train_weight_decay_penalty(wd=80)

与定义的my_w=0.01很接近了。

Dropout 暂退法

一个好的模型需要对输入数据的扰动Robustness。

使用有噪音的数据等价于Tikhonov“吉洪诺夫”正则。

暂退法:在层与层之间加入噪音,每个中间层的输出都有一定的概率被丢弃。

$h_{[t+1]} = \begin{cases}
0 & 有\rho的概率被丢弃; \
{\large\frac{h_{[t]}}{1-\rho}} & 有1-\rho的概率被放大;
\end{cases}$

其期望值保持不变:
$\mathbb{E}[h_{[t+1]}] = 0×\rho+(1-\rho)×\frac{h_{[t]}}{1-\rho}=h_{[t]}.$

$\rho$又是一个超参数,$0<=\rho<=1$。

Dropout正则项只在训练(开发)时使用,它影响模型的参数更新,在评估(测试)时不使用。

从零开始实现暂退法

'''4_1'''
def dropout_layer(X, dt):

断言dt必须在[0,1]之间。

assert 0 <= dt <= 1

概率为1时,所有元素都被丢弃;概率为0时,所有元素都被保留;再则,如上。

if dt == 1:
return torch.zeros_like(X)
elif dt == 0:
return X
else:

X.shape=(batch_size, features),mask是一个与X形状相同的随机张量,

其中每个元素从[0,1]均匀分布中采样,若大于dt,则该元素被保留,否则被丢弃。

mask = (torch.rand(X.shape) > dt).float()
return mask * X / (1.0 - dt)

看一下dropout_layer的效果。

X = torch.arange(16, dtype=torch.float32).view(2, 8)
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5))
print(dropout_layer(X, 1.))
'''4_2'''
import itertools

依旧使用Fashion-MNIST数据集,定义具有2个隐藏层的神经网络,每个隐藏层有256个单元。

num_inputs, num_outputs, num_hid1, num_hid2 = 784, 10, 256, 256
dt1, dt2 = 0.2, 0.5

每次调用函数时都会重新初始化参数,必须在函数外定义线性层。

line_layer_0 = nn.Linear(num_inputs, num_hid1)
line_layer_1 = nn.Linear(num_hid1, num_hid2)
line_layer_2 = nn.Linear(num_hid2, num_outputs)

将参数连接起来组成一个可持续使用的列表,给下面的优化器使用。

your_variable = list(itertools.chain(
line_layer_0.parameters(),
line_layer_1.parameters(),
line_layer_2.parameters()))

不用your_model命名了,因它并非用的统一接口your_model(X)!

def neuron_forward(X, is_training:bool=True):
"""定义神经元前向传播过程,并在训练过程启用Dropout层。"""
Q1 = line_layer_0(X.reshape(-1, num_inputs))
H1 = torch.relu(Q1)
if is_training:
H1 = dropout_layer(H1, dt1)
Q2 = line_layer_1(H1)
H2 = torch.relu(Q2)
if is_training:
H2 = dropout_layer(H2, dt2)
return line_layer_2(H2)
def evaluate_accuracy(neuron_forward, iterate_data):
""""重写评估准确率函数,以在评估过程中禁用Dropout层,非类的实现就是有点麻烦。"""

2个数,分别是预测正确数、样本总数。

metric = [0.0, 0.0]
with torch.no_grad():
for X, y in iterate_data:

重要:必须禁用Dropout层,否则会导致测试准确率偏低!

metric[0] += Chapter00.count_accurate(neuron_forward(X, False), y)
metric[1] += y.numel()
return metric[0] / metric[1]

训练和评估。

train_epochs, batch_size, lr = 10, 256, 0.5
iterate_devset, iterate_testset = Chapter00.load_data_fashion_mnist(batch_size)
loss_function = nn.CrossEntropyLoss(reduction='none')
train_optimizer = optim.SGD(your_variable, lr=lr)
Chapter00.training_classifier_1on_fashion_mnist(neuron_forward, train_epochs,
iterate_devset, iterate_testset, loss_function, train_optimizer, evaluate_accuracy)

简洁实现暂退法

'''4_3'''

Dropout层放在ReLU层前面、后面都一样,随机丢弃,跟激活值没关系。

votre_modele = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, 10))
def init_weights(mo):
if isinstance(mo, nn.Linear):
nn.init.normal_(mo.weight, mean=0, std=0.01)
votre_modele.apply(init_weights)

训练和评估。

train_epochs, batch_size = 10, 256
iterate_devset, iterate_testset = Chapter00.load_data_fashion_mnist(batch_size)
loss_function = nn.CrossEntropyLoss(reduction='none')
train_optimizer = optim.SGD(votre_modele.parameters(), lr=0.5)
Chapter00.training_classifier_1on_fashion_mnist(votre_modele, train_epochs,
iterate_devset, iterate_testset, loss_function, train_optimizer)

数值稳定性

$$ \begin{aligned} &考虑有r-1个隐藏层,输入\mathbf{X}和输出\mathbf{Y}都是向量的深层神经网络, \\ &每个层l由变换f_{l}定义,神经网络可表示为: \\ &\mathbf{Y} = f_{r-1} \circ...\circ f_{l} \circ...\circ f_{1}(\mathbf{X}) \\ &隐藏变量\mathbf{H_{[l]}},\mathbf{X}=\mathbf{H_{[0]}},\mathbf{Y}=\mathbf{H_{[r]}}, \\ &其余\mathbf{H_{[l]}} = f_{l}(\mathbf{H_{[l-1]}}) = \mathcal{a}(\mathbf{W_{[l]}}\cdot\mathbf{H_{[l-1]}}) \\ &\frac{\partial\mathbf{H_{[l]}}}{\partial\mathbf{H_{[l-1]}}} = \mathtt{diag}(a')\cdot\mathbf{W_{[l]}}, \\ &对激活函数h=\mathcal{a}(g)的求导\frac{\partial{h}}{\partial{g}}是个对角矩阵。 \\ &雅可比矩阵\frac{\partial{h}}{\partial{g}}的第i行第j列的元素定义为\frac{\partial{h}}{\partial{g}}[i][j]=\frac{\partial{h_i}}{\partial{g_j}} \\ &1.\; 当i == j时,是常规的一元函数求导,\frac{\partial{h_i}}{\partial{g_i}}=a'(g_i); \\ &2.\; 当i \neq j时,h_i只与g_i有关,与g_j无关,于是\frac{\partial{h_i}}{\partial{g_j}}=0; \\ &\frac{\partial h}{\partial g} = \begin{bmatrix} a'(g_1) & 0 & \cdots & 0 \\ 0 & a'(g_2) & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & a'(g)\\ \end{bmatrix} \\ \\ &我们可以将输出对任一权重\mathbf{W_{[l]}}的梯度写为: \\ &\frac{\partial\mathbf{Y}}{\partial\mathbf{W_{[l]}}} = \frac{\partial\mathbf{H_{[r]}}}{\partial\mathbf{H_{[r-1]}}} \circ \frac{\partial\mathbf{H_{[r-1]}}}{\partial\mathbf{H_{[r-2]}}} \circ...\circ \frac{\partial\mathbf{H_{[l+1]}}}{\partial\mathbf{H_{[l]}}} \circ \frac{\partial\mathbf{H_{[l]}}}{\partial\mathbf{W_{[l]}}} \\ &换言之,该梯度是r-l个矩阵与1个向量的累乘,由指数函数的性质可知, \\&多个大于1的数累乘容易发生\textcolor{red}{梯度爆炸},多个小于1的数累乘容易发生\textcolor{blue}{梯度消失}。 \\ &不稳定梯度也威胁到我们优化算法的稳定性。 \end{aligned} $$

让训练更加稳定

目标:让梯度值在合理范围内,例如$[1×10^{-6}, 1×10^3]$。

  1. 将乘法变加法,如ResNet、LSTM的做法。
  2. 梯度归一化(归一到[0, 1]区间)、梯度裁剪(超出范围的梯度值直接取为边界值)。
  3. 合理的权重初始化和激活函数。

Xavier初始化 Glorot初始化

核心思想:保持输入变量和输出变量的方差相同、期望都为0,避免梯度爆炸或消失。

具体来说,对于网络中的每一层,$n_{l-1}$是输入神经元的个数,$n_{l}$是输出神经元的个数,$\mathsf{Var}(W)$是权重变量的方差。

我们希望:

  1. 前向传播时:激活值的方差在经过该层后保持不变,$n_{l-1}\mathsf{Var}(W) = 1$,推导略。
  2. 反向传播时:梯度的方差在经过该层后保持不变,$n_{l}\mathsf{Var}(W) = 1$,推导略。

$n_{l-1}$、$n_{l}$不能保持层层都相等,只能折中,Xavier Glorot, Yoshua Bengio. 提出方法,

使得$\mathsf{Var}(W)(n_{l-1}+n_{l})/2 = 1$,$\mathsf{Var}(W) = 2/(n_{l-1}+n_{l})$。

基于此方差,Xavier初始化有两种方式:

  1. 均匀分布,$W\sim\mathtt{Uniform}\left(-\sqrt{6/(n_{l-1}+n_{l})}, \sqrt{6/(n_{l-1}+n_{l})}\right)$。
  2. 正态分布,$W\sim\mathtt{Normal}\left(0, \sqrt{2/(n_{l-1}+n_{l})}\right)$。

适用的激活函数:

  1. $4 * \mathsf{sigmoid}(x) - 2 = 0 + x - x^3 / 12 + \omicron(x^5)$
  2. $\tanh(x) = 0 + x - x^3 / 3 + \omicron(x^5)$
最后修改于:2026年03月20日
如果觉得我的文章对你有用请狠狠地打赏我